Category: AI

  • roll

    <!DOCTYPE html>
    <html>
    <head>
        <title>Roll - Dark Descent</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <style>
            body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; touch-action: none; background-color: #fffafa; }
            #TutContainer { width: 100%; height: 100vh; }
            #scoreBoard { position: absolute; top: 20px; left: 20px; font-size: 20px; font-weight: bold; color: #333; z-index: 10; pointer-events: none; }
            #gameOverScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: none; flex-direction: column; justify-content: center; align-items: center; z-index: 100; color: white; }
            #gameOverScreen h1 { font-size: 32px; margin-bottom: 20px; text-align: center; }
            #restartBtn { padding: 15px 30px; font-size: 20px; cursor: pointer; background: #33ff33; border: none; border-radius: 5px; font-weight: bold; }
        </style>
    </head>
    <body>
        <div id="scoreBoard">Score: 0</div> 
        <div id="gameOverScreen">
            <h1>GAME OVER</h1>
            <button id="restartBtn">RUN AGAIN</button>
        </div>
        <div id="TutContainer"></div>  
        <script src="https://cdn.jsdelivr.net/npm/three@0.145.0/build/three.min.js"></script>   
         <script>
           let sceneWidth, sceneHeight, camera, scene, renderer, dom;
            let sun, heroRollingSpeed, clock, treesPool, treesInPath;
            let pathAngleValues;
            let globalRenderID;
    
            const worldRadius = 26;
            const heroRadius = 0.2;
            const heroBaseY = 2.1; 
            const gravity = 0.005;
            const particleCount = 20;
    
            let rollingSpeed = 0.008; 
            let treeReleaseInterval = 0.5; 
            let rightLane = 1;
            let middleLane = 0;
            let leftLane = -1;
            let lerpSpeed = 0.15; 
    
            let scoreText, score, hasCollided;
            let jumping = false;
            let bounceValue = 0;
            let currentLane = middleLane;
            let heroSphere;
            let rollingGroundSphere;
            let particles;
            let particleGeometry;
            let explosionPower = 1;
            let sphericalHelper;
    
            let touchStartX = 0, touchStartY = 0, touchEndX = 0, touchEndY = 0;
            const minSwipeDistance = 30;
    
            init();
    
            function init() {
                adjustSettings(); 
                createScene();
                update();
            }
    
            function adjustSettings() {
                if (window.innerWidth < 768) {
                    rollingSpeed = 0.005;
                    treeReleaseInterval = 1.0;
                    leftLane = -0.6;
                    rightLane = 0.6;
                    lerpSpeed = 0.25;
                } else {
                    rollingSpeed = 0.008;
                    treeReleaseInterval = 0.5;
                    leftLane = -1;
                    rightLane = 1;
                    lerpSpeed = 0.15;
                }
            }
    
            function createScene() {
                hasCollided = false;
                score = 0;
                treesPool = [];
                treesInPath = [];
                clock = new THREE.Clock();
                clock.start();
                
                heroRollingSpeed = (rollingSpeed * worldRadius / heroRadius) / 5;
                sphericalHelper = new THREE.Spherical();
                pathAngleValues = [1.52, 1.57, 1.62];
    
                sceneWidth = window.innerWidth;
                sceneHeight = window.innerHeight;
    
                scene = new THREE.Scene();
                scene.background = new THREE.Color(0xfffafa);
                scene.fog = new THREE.FogExp2(0xf0fff0, 0.14);
    
                camera = new THREE.PerspectiveCamera(60, sceneWidth / sceneHeight, 0.1, 1000);
                renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
                renderer.setClearColor(0xfffafa, 1);
                renderer.shadowMap.enabled = true;
                renderer.shadowMap.type = THREE.PCFSoftShadowMap;
                renderer.setSize(sceneWidth, sceneHeight);
    
                dom = document.getElementById('TutContainer');
                dom.appendChild(renderer.domElement);
                scoreText = document.getElementById('scoreBoard');
    
                createTreesPool();
                addWorld();
                addLight();
                addHero();
                addExplosion();
    
                camera.position.z = 6.5;
                camera.position.y = 2.5;
    
                window.addEventListener('resize', onWindowResize, false);
                document.addEventListener('keydown', handleKeyDown);
                document.addEventListener('touchstart', handleTouchStart, {passive: false});
                document.addEventListener('touchend', handleTouchEnd, {passive: false});
    
                const restartBtn = document.getElementById('restartBtn');
                if (restartBtn) {
                    restartBtn.addEventListener('click', () => { location.reload(); });
                }
            }
    
            function moveLeft() {
                if (hasCollided) return;
                if (currentLane === middleLane) currentLane = leftLane;
                else if (currentLane === rightLane) currentLane = middleLane;
                triggerJumpEffect();
            }
    
            function moveRight() {
                if (hasCollided) return;
                if (currentLane === middleLane) currentLane = rightLane;
                else if (currentLane === leftLane) currentLane = middleLane;
                triggerJumpEffect();
            }
    
            function jump() {
                if (hasCollided) return;
                if (!jumping) {
                    bounceValue = 0.12;
                    jumping = true;
                }
            }
    
            function triggerJumpEffect() {
                if (!jumping) {
                    jumping = true;
                    bounceValue = 0.06;
                }
            }
    
            function handleKeyDown(event) {
                const key = event.key.toLowerCase();
                if (key === 'a') moveLeft();
                else if (key === 'd') moveRight();
                else if (key === ' ') jump();
            }
    
            function handleTouchStart(e) {
                touchStartX = e.changedTouches[0].screenX;
                touchStartY = e.changedTouches[0].screenY;
            }
    
            function handleTouchEnd(e) {
                touchEndX = e.changedTouches[0].screenX;
                touchEndY = e.changedTouches[0].screenY;
                let dx = touchEndX - touchStartX;
                let dy = touchEndY - touchStartY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    if (Math.abs(dx) > minSwipeDistance) {
                        if (dx < 0) moveLeft(); else moveRight();
                    }
                } else {
                    if (Math.abs(dy) > minSwipeDistance) {
                        if (dy < 0) jump();
                    }
                }
            }
    
            function update() {
                if (hasCollided) {
                    doExplosionLogic();
                    render();
                    globalRenderID = requestAnimationFrame(update);
                    return; 
                }
    
                // 1. Update Lighting and Atmosphere based on score
                updateAtmosphere();
    
                rollingGroundSphere.rotation.x += rollingSpeed;
                heroSphere.rotation.x -= heroRollingSpeed;
    
                if (heroSphere.position.y <= heroBaseY) {
                    jumping = false;
                    bounceValue = (Math.random() * 0.04) + 0.005;
                }
                heroSphere.position.y += bounceValue;
                heroSphere.position.x = THREE.MathUtils.lerp(heroSphere.position.x, currentLane, lerpSpeed); 
                bounceValue -= gravity;
    
                if (clock.getElapsedTime() > treeReleaseInterval) {
                    clock.start();
                    addPathTree();
                    score += 1;
                    if (scoreText) scoreText.innerHTML = "Score: " + score;
                }
    
                doTreeLogic();
                doExplosionLogic();
                render();
                globalRenderID = requestAnimationFrame(update);
            }
    
            function updateAtmosphere() {
                // Gradually get darker. Max darkness at score 200.
                let darkness = Math.min(score / 200, 1); 
                
                // Light intensity fades from 0.9 to 0.2
                sun.intensity = THREE.MathUtils.lerp(0.9, 0.2, darkness);
                
                // Background color fades from White to Deep Midnight Blue
                scene.background.setRGB(
                    THREE.MathUtils.lerp(1, 0.05, darkness), 
                    THREE.MathUtils.lerp(1, 0.05, darkness), 
                    THREE.MathUtils.lerp(1, 0.2, darkness)
                );
                
                // Fog changes color to match
                scene.fog.color.setRGB(
                    THREE.MathUtils.lerp(0.94, 0.05, darkness), 
                    THREE.MathUtils.lerp(1, 0.05, darkness), 
                    THREE.MathUtils.lerp(0.94, 0.2, darkness)
                );
            }
    
            function render() {
                renderer.render(scene, camera);
            }
    
            function gameOver() {
                const screen = document.getElementById('gameOverScreen');
                if (screen) screen.style.display = 'flex';
            }
    
            function createTreesPool() {
                var maxTreesInPool = 15;
                for (var i = 0; i < maxTreesInPool; i++) {
                    treesPool.push(createTree());
                }
            }
    
            function addPathTree() {
                var options = [0, 1, 2];
                var lane = Math.floor(Math.random() * 3);
                addTree(true, lane);
                options.splice(lane, 1);
                if (Math.random() > 0.5) {
                    lane = Math.floor(Math.random() * 2);
                    addTree(true, options[lane]);
                }
            }
    
            function addHero() {
                var sphereGeometry = new THREE.DodecahedronGeometry(heroRadius, 1);
                var sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xe5f2f2, flatShading: true });
                heroSphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
                heroSphere.receiveShadow = true;
                heroSphere.castShadow = true;
                scene.add(heroSphere);
                heroSphere.position.set(middleLane, heroBaseY, 4.8);
                currentLane = middleLane;
            }
    
            function addWorld() {
                var sides = 40, tiers = 40;
                var sphereGeometry = new THREE.SphereGeometry(worldRadius, sides, tiers);
                var sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xfffafa, flatShading: true });
                
                const pos = sphereGeometry.attributes.position;
                for (let i = 0; i < pos.count; i++) {
                    let x = pos.getX(i);
                    let y = pos.getY(i);
                    let z = pos.getZ(i);
                    let vec = new THREE.Vector3(x, y, z).normalize();
                    let heightValue = (Math.random() * 0.05) - 0.025;
                    vec.multiplyScalar(worldRadius + heightValue);
                    pos.setXYZ(i, vec.x, vec.y, vec.z);
                }
                pos.needsUpdate = true;
    
                rollingGroundSphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
                rollingGroundSphere.receiveShadow = true;
                rollingGroundSphere.rotation.z = -Math.PI / 2;
                scene.add(rollingGroundSphere);
                rollingGroundSphere.position.set(0, -24, 2);
                addWorldTrees();
            }
    
            function addLight() {
                scene.add(new THREE.HemisphereLight(0xfffafa, 0x000000, .9));
                sun = new THREE.DirectionalLight(0xcdc1c5, 0.9);
                sun.position.set(12, 6, -7);
                sun.castShadow = true;
                scene.add(sun);
            }
    
            function createTree() {
                var sides = 8, tiers = 6;
                var scalarMultiplier = (Math.random() * 0.15) + 0.05;
                var treeGeometry = new THREE.ConeGeometry(0.5, 1, sides, tiers);
                var treeMaterial = new THREE.MeshStandardMaterial({ color: 0x33ff33, flatShading: true });
                
                const pos = treeGeometry.attributes.position;
                blowUpTree(pos, sides, 0, scalarMultiplier);
                tightenTree(pos, sides, 1);
                blowUpTree(pos, sides, 2, scalarMultiplier * 1.1, true);
                tightenTree(pos, sides, 3);
                blowUpTree(pos, sides, 4, scalarMultiplier * 1.2);
                tightenTree(pos, sides, 5);
                pos.needsUpdate = true;
    
                var treeTop = new THREE.Mesh(treeGeometry, treeMaterial);
                treeTop.castShadow = true;
                treeTop.position.y = 0.9;
                treeTop.rotation.y = Math.random() * Math.PI;
                var treeTrunk = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 0.5), new THREE.MeshStandardMaterial({ color: 0x886633, flatShading: true }));
                treeTrunk.position.y = 0.25;
                var tree = new THREE.Object3D();
                tree.add(treeTrunk);
                tree.add(treeTop);
                return tree;
            }
    
            function blowUpTree(pos, sides, currentTier, scalarMultiplier, odd) {
                for (var i = 0; i < sides; i++) {
                    var vertexIndex = (currentTier * sides) + 1;
                    var idx = i + vertexIndex;
                    let vX = pos.getX(idx);
                    let vY = pos.getY(idx);
                    let vZ = pos.getZ(idx);
                    let vertexVector = new THREE.Vector3(vX, vY, vZ);
                    let midPointVector = new THREE.Vector3(0, vY, 0);
                    let offset = vertexVector.clone().sub(midPointVector);
                    if (odd) {
                        if (i % 2 === 0) offset.normalize().multiplyScalar(scalarMultiplier / 6);
                        else {
                            offset.normalize().multiplyScalar(scalarMultiplier);
                            let nextY = pos.getY(idx + sides);
                            pos.setY(idx, nextY + 0.05);
                        }
                    } else {
                        if (i % 2 !== 0) offset.normalize().multiplyScalar(scalarMultiplier / 6);
                        else {
                            offset.normalize().multiplyScalar(scalarMultiplier);
                            let nextY = pos.getY(idx + sides);
                            pos.setY(idx, nextY + 0.05);
                        }
                    }
                    let finalVec = vertexVector.add(offset);
                    pos.setXYZ(idx, finalVec.x, finalVec.y, finalVec.z);
                }
            }
    
            function tightenTree(pos, sides, currentTier) {
                for (var i = 0; i < sides; i++) {
                    var vertexIndex = (currentTier * sides) + 1;
                    var idx = i + vertexIndex;
                    let vX = pos.getX(idx);
                    let vY = pos.getY(idx);
                    let vZ = pos.getZ(idx);
                    let vertexVector = new THREE.Vector3(vX, vY, vZ);
                    let midPointVector = new THREE.Vector3(0, vY, 0);
                    let offset = vertexVector.clone().sub(midPointVector);
                    offset.normalize().multiplyScalar(0.06);
                    let finalVec = vertexVector.sub(offset);
                    pos.setXYZ(idx, finalVec.x, finalVec.y, finalVec.z);
                }
            }
    
            function addWorldTrees() {
                var numTrees = 36, gap = 6.28 / 36;
                for (var i = 0; i < numTrees; i++) {
                    addTree(false, i * gap, true);
                    addTree(false, i * gap, false);
                }
            }
    
            function addTree(inPath, row, isLeft) {
                var newTree;
                if (inPath) {
                    if (treesPool.length == 0) return;
                    newTree = treesPool.pop();
                    newTree.visible = true;
                    
                    // Reset rotations in case it was a fallen tree
                    newTree.rotation.set(0,0,0);
    
                    // Randomly decide if this tree falls (30% chance)
                    newTree.userData.isFalling = Math.random() < 0.3;
                    newTree.userData.fallRotation = 0;
    
                    treesInPath.push(newTree);
                    sphericalHelper.set(worldRadius - 0.3, pathAngleValues[row], -rollingGroundSphere.rotation.x + 4);
                } else {
                    newTree = createTree();
                    var forestAreaAngle = isLeft ? 1.68 + Math.random() * 0.1 : 1.46 - Math.random() * 0.1;
                    sphericalHelper.set(worldRadius - 0.3, forestAreaAngle, row);
                }
                newTree.position.setFromSpherical(sphericalHelper);
                var rollingGroundVector = rollingGroundSphere.position.clone().normalize();
                var treeVector = newTree.position.clone().normalize();
                newTree.quaternion.setFromUnitVectors(treeVector, rollingGroundVector);
                newTree.rotation.x += (Math.random() * (2 * Math.PI / 10)) + -Math.PI / 10;
                rollingGroundSphere.add(newTree);
            }
    
            function addExplosion() {
                particleGeometry = new THREE.BufferGeometry();
                const positions = new Float32Array(particleCount * 3);
                particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
                var pMaterial = new THREE.PointsMaterial({ color: 0xfffafa, size: 0.2 });
                particles = new THREE.Points(particleGeometry, pMaterial);
                scene.add(particles);
                particles.visible = false;
            }
    
            function explode() {
                particles.position.set(heroSphere.position.x, 2, 4.8);
                const pos = particleGeometry.attributes.position;
                for (var i = 0; i < particleCount; i++) {
                    pos.setXYZ(i, -0.2 + Math.random() * 0.4, -0.2 + Math.random() * 0.4, -0.2 + Math.random() * 0.4);
                }
                pos.needsUpdate = true;
                explosionPower = 1.07;
                particles.visible = true;
            }
    
            function doTreeLogic() {
                var treePos = new THREE.Vector3(), treesToRemove = [];
                treesInPath.forEach(function (oneTree) {
                    treePos.setFromMatrixPosition(oneTree.matrixWorld);
                    
                    // Animation: If it's a falling tree, tip it over as it gets closer
                    if (oneTree.userData.isFalling) {
                        if (oneTree.userData.fallRotation < Math.PI / 2) {
                            oneTree.userData.fallRotation += 0.05;
                            oneTree.rotation.z = oneTree.userData.fallRotation;
                        }
                    }
    
                    if (treePos.z > 6 && oneTree.visible) {
                        treesToRemove.push(oneTree);
                    } else if (treePos.distanceTo(heroSphere.position) <= 0.6) {
                        
                        // SPECIAL COLLISION: If the tree is fallen, you can jump over it!
                        if (oneTree.userData.isFalling && heroSphere.position.y > heroBaseY + 0.2) {
                            // Safe! User jumped over the fallen tree
                        } else {
                            hasCollided = true;
                            explode();
                            gameOver();
                        }
                    }
                });
                treesToRemove.forEach(function (element) {
                    var fromWhere = treesInPath.indexOf(element);
                    treesInPath.splice(fromWhere, 1);
                    treesPool.push(element);
                    element.visible = false;
                });
            }
    
            function doExplosionLogic() {
                if (!particles.visible) return;
                const pos = particleGeometry.attributes.position;
                for (var i = 0; i < particleCount; i++) {
                    let x = pos.getX(i) * explosionPower;
                    let y = pos.getY(i) * explosionPower;
                    let z = pos.getZ(i) * explosionPower;
                    pos.setXYZ(i, x, y, z);
                }
                pos.needsUpdate = true;
                if (explosionPower > 1.005) explosionPower -= 0.001;
                else particles.visible = false;
            }
    
            function onWindowResize() {
                sceneHeight = window.innerHeight;
                sceneWidth = window.innerWidth;
                renderer.setSize(sceneWidth, sceneHeight);
                camera.aspect = sceneWidth / sceneHeight;
                camera.updateProjectionMatrix();
            } 
        </script>
    </body>
    </html>
  • World – Hot Air Balloon

    The Art of the Drift

    Engineering a Relaxing Hot Air Balloon System in Three.js

    Date: September 18, 2026
    Project: World Simulation
    Focus: Vehicle Physics, Camera Immersion, and Collision Resolution


    1. Abstract

    The objective was to implement a Hot Air Balloon vehicle that felt “weightless” and “atmospheric.” Initial attempts resulted in “snappy” movement akin to a ground vehicle. Through iterative testing, we moved from Kinematic Movement (instant) to Momentum-Based Physics (drifting), while solving critical bugs regarding spawn-teleportation and collision locking.


    2. The Evolution: What Went Wrong & What Went Right

    ❌ The “Bad” (Initial Pain Points)

    • The “Car” Feel: Initially, the balloon stopped the millisecond the key was released. It felt like a floating car, not a balloon.
    • The “Jump” Bug: Upon entering the balloon, the code forced a +3 height increase. This looked like a “glitchy jump” and ruined the smooth entry.
    • The “Stuck” Paradox: When exiting in mid-air, the player was teleported instantly to the ground. If a building was beneath them, they spawned inside the mesh, triggering the isBlocked function and freezing the player in place.
    • Speed Issues: Initial momentum was too high, making the balloon feel like a jet engine rather than a peaceful drift.

    ✅ The “Good” (The Wins)

    • Momentum Flow: Implementing a velocity vector allowed the balloon to “glide.” Now, it takes time to speed up and time to slow down.
    • The Aerial Perspective: Switching to a 45-degree diagonal camera shifted the experience from “playing a game” to “viewing a landscape.”
    • Natural Exit: By removing the forced ground-snap, players now fall naturally from the balloon, adding a sense of gravity and danger.

    3. Technical Implementation (The “Golden” Code)

    To achieve the final feel, we modified two core systems: the Vehicle Logic (index.html) and the Object Classification (build.js).

    A. The Physics Engine (index.html)

    We replaced instant position updates with a Velocity-Friction Model.

    The Constants:

    code JavaScript

    // Ultra-Slow Floating Physics
    const BALLOON_ACCEL = 0.5;      // Slow start, prevents "snapping"
    const BALLOON_FRICTION = 0.998; // Near-zero friction = infinite glide
    const BALLOON_MAX_SPEED = 4.0;  // Capped for a relaxing pace
    const BALLOON_LIFT_SPEED = 5.0; // Constant lift for the Q key

    The Logic Loop:

    code JavaScript

    // Inside animate() -> if (isBalloon)
    const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(activeControl.quaternion);
    
    // Horizontal Momentum (W/S)
    if (keys['w']) balloonVelocity.addScaledVector(forward, BALLOON_ACCEL * delta);
    if (keys['s']) balloonVelocity.addScaledVector(forward, -BALLOON_ACCEL * delta);
    
    // Direct Lift (Q) - No momentum, just constant rise
    if (keys['q']) activeControl.position.y += BALLOON_LIFT_SPEED * delta;
    
    // Downward Drift (E) - Uses momentum to "sink"
    if (keys['e']) balloonVelocityY -= BALLOON_ACCEL * delta;
    
    // Apply Glide Friction
    balloonVelocity.multiplyScalar(BALLOON_FRICTION);
    balloonVelocityY *= BALLOON_FRICTION;
    
    // Final Position Update
    activeControl.position.add(balloonVelocity);
    activeControl.position.y += balloonVelocityY;

    B. The Cinematic Camera (index.html)

    To simulate “Sunlight at 9 AM,” we positioned the camera at a high diagonal offset.

    code JavaScript

    // Specialized Balloon Camera View
    if (isDriving && currentVehicle && currentVehicle.userData && currentVehicle.userData.isBalloon) {
        const camDist = 25;
        // Offset camera 45 degrees top-down and behind
        const offset = new THREE.Vector3(camDist, camDist, camDist).applyQuaternion(activeControl.quaternion);
        _vLookTarget.copy(activeControl.position).add(offset);
        camera.position.lerp(_vLookTarget, delta * 5);
        camera.lookAt(activeControl.position);
    }

    C. The Build Classification (build.js)

    For the physics to know it’s a balloon and not a car, we added metadata during the GLB load.

    code JavaScript

    // Inside createInScene(data)
    const isBalloon = nameLower.includes('balloon') || nameLower.includes('hot-air');
    if (isBalloon) {
        obj.userData.isVehicle = true;
        obj.userData.isBalloon = true; // This flag triggers the momentum physics
    }

    4. Conclusion & Final Tuning

    The “Perfect Drift” was found by balancing Acceleration vs. Friction. By lowering acceleration to 0.5 and raising friction to 0.998, we achieved a state of “perpetual flow.”

    The removal of the nearest.position.y += 3 line in toggleVehicle finally allowed the balloon to sit naturally at ground level 0, creating a seamless transition from walking to flying.

    Current State: 🟢 Stable | Feeling: ☁️ Ethereal | Movement: 🌊 Fluid

  • gltf-transform –width 256 –height 256

    batch

    for f in *.fbx; do
      base="${f%.fbx}"
      
      # 1. Convert FBX to a temporary GLB
      fbx2gltf -i "$f" -o "${base}_raw.glb"
      
      # 2. Complete optimization pipeline: Resize textures, clear empty animations, apply Draco
      gltf-transform resize "${base}_raw.glb" "${base}.glb" --width 512 --height 512 && \
      gltf-transform draco "${base}.glb" "${base}.glb" --quantizePosition 10 --quantizeTexcoord 10
      
      # 3. Clean up the large intermediate file
      rm "${base}_raw.glb"
    done
    

    This updated command downscales all textures to 256×256 pixels and combines geometry quantization with animation thinning. This pipeline will maximize your file savings, likely bringing your 25 MB models down to under 1.5 MB each.

    Run this loop in your Debian terminal:

    bash

    for f in *.fbx; do
      base="${f%.fbx}"
      
      # 1. Convert FBX to a raw GLB
      fbx2gltf -i "$f" -o "${base}_raw.glb"
      
      # 2. Resize textures to 256x256, prune empty elements, and aggressively compress geometry
      gltf-transform resize "${base}_raw.glb" "${base}.glb" --width 256 --height 256 && \
      gltf-transform prune "${base}.glb" "${base}.glb" && \
      gltf-transform draco "${base}.glb" "${base}.glb" --quantizePosition 10 --quantizeTexcoord 10 --quantizeNormal 8
      
      # 3. Remove the heavy source file
      rm "${base}_raw.glb"
    done
    

    Use code with caution.

    What makes this code compress so aggressively?

    • resize --width 256 --height 256: Shrinks texture memory overhead by up to 98% compared to standard Mixamo 2K textures.
    • prune: This automatically detects and removes unused nodes, empty animations (like that dead Take 001 track in your log), and duplicate materials.
    • --quantizeNormal 8: Adds normal vector quantization to the Draco step, stripping extra floating-point precision data from the model’s lighting data where the human eye won’t notice it.

    Let me know what your final file sizes look like after running this loop! If they are still too large, we can add animation sampling reduction to strip out heavy keyframes, or switch the texture format to KTX2. Would you like to check the final sizes first?

  • World – map editor!

    Finally stable: the map editor demo is now better optimized for potato laptops.

  • world : fly – clone – ride – build – social – optimization

    Engineering Research Journal & Technical Dev-Log: Synchronous Multi-Agent WebGL Simulation, Mobile Aerodynamics, and Real-Time State Replication

    Abstract

    This paper documents the design, iterative failure analysis, and architectural optimization of a browser-based 3D multi-agent interactive environment built atop Three.js (WebGL), Node.js/Socket.IO, and an embedded real-time level design framework (BuildSystem).

    We examine the engineering hurdles encountered across several core subsystems:

    1. Low-latency mobile gesture recognition and aerodynamic flight modeling.
    2. Multi-agent state synchronization, asynchronous asset-loading race conditions, and spatial mesh visibility traps.
    3. Raycast-driven collision mechanics, self-intersection stuttering, and frame-rate independent camera tracking.
    4. Persistent world state management versus runtime vehicle entity lifecycles.
    5. DOM/WebGL event-loop conflicts in mobile touch-action propagation.

    System Architecture Overview

    code Code

    +------------------------------------------+
                              |              Node.js Server              |
                              | (Socket.IO Hub, Persistence, Disconnect) |
                              +--------------------+---------------------+
                                                   |
                                   WebSocket Protocol (Full Duplex)
                                                   |
                          +------------------------+------------------------+
                          |                                                 |
             +------------v------------+                       +------------v------------+
             |     Client A (Local)    |                       |    Client B (Remote)    |
             | +---------------------+ |                       | +---------------------+ |
             | | Input Engine        | |                       | | Network Receiver    | |
             | | (Touch / Keyboard)  | |                       | | (Packet Queuing)    | |
             | +----------+----------+ |                       | +----------+----------+ |
             |            |            |                       |            |            |
             | +----------v----------+ |                       | +----------v----------+ |
             | | Kinematic Engine    | |                       | | Scene Graph Sync    | |
             | | (Physics / Aero)    | |                       | | (Entity Lifecycle)  | |
             | +----------+----------+ |                       | +----------+----------+ |
             |            |            |                       |            |            |
             | +----------v----------+ |                       | +----------v----------+ |
             | | Local Render Tree   | |                       | | Remote Render Tree  | |
             | | (Scene Graph)       | |                       | | (Scene Graph)       | |
             | +---------------------+ |                       | +---------------------+ |
             +-------------------------+                       +-------------------------+

    Section 1: Mobile Touch Mapping & Kinematic Aerodynamics

    1.1 The Initial Failure: Control Disconnect & Flight Impossibility

    In desktop mode, aircraft navigation operated via discrete key codes (W, A, S, D, Space, Shift). When mobile touch handlers were initially introduced:

    • Touch movements (touchmove) applied angular displacement directly to localPlayer.rotation.y.
    • When boarded into an aircraft (isDriving = true), localPlayer was disabled and hidden. The touch inputs mutated the hidden human entity while the physical aircraft (currentVehicle) remained unresponsive.
    • Vertical swipes emitted momentary pulses ( 120 ms120 ms ) to keys[‘ ‘], causing aerodynamic pitch jitter ( 0.28 rad→0 rad0.28 rad→0 rad per frame) rather than sustained lift.

    code Code

    [Touch Drag] ---> Mutated: localPlayer.rotation.y (Hidden Entity)
                      Ignored: currentVehicle.rotation.y (Active Entity)
                      Result: Complete loss of directional steering on mobile

    1.2 The Aerodynamic Physics Model

    We implemented a velocity-coupled aerodynamic kinematic model. Horizontal displacement governs wing bank (

            ϕϕ
          

    ) and yaw (

            ψψ
          

    ). Vertical gestures control angle of attack (

            θθ
          

    ).

            v⃗forward=u⃗heading⋅vairvforward​=uheading​⋅vair​
          
            Δy=vair⋅sin⁡(θ)⋅ΔtΔy=vair​⋅sin(θ)⋅Δt
          
            ϕtarget=−clamp(Δxtouch90,−1.2,1.2)⋅0.45ϕtarget​=−clamp(90Δxtouch​​,−1.2,1.2)⋅0.45
          
            θclimb=lerp(θ,θtarget,Δt⋅3.5)θclimb​=lerp(θ,θtarget​,Δt⋅3.5)
          

    code JavaScript

    // REVISION 1: Unified Entity-Aware Steering Engine
    if (isStarted && !isHoldingMic && !build.isEditMode) {
        let moveDx = touch.clientX - touchStartX;
    
        if (isDriving) {
            // Continuous Steering: preserves forward momentum during lateral drags
            if (Math.abs(totalDx) > 12) {
                touchSteer = THREE.MathUtils.clamp(-totalDx / 90, -1.2, 1.2);
            } else {
                touchSteer = 0;
            }
    
            // Sustained Angle of Attack control
            if (isAirplane && isAirborne) {
                if (totalDy > 35) {
                    touchHoldingClimb = true;
                    touchHoldingDive = false;
                } else if (totalDy < -35) {
                    touchHoldingDive = true;
                    touchHoldingClimb = false;
                } else {
                    touchHoldingClimb = false;
                    touchHoldingDive = false;
                }
            }
        } else if (localPlayer) {
            localPlayer.rotation.y -= moveDx * 0.012;
        }
        touchStartX = touch.clientX;
    }

    Section 2: Input State Collision & The Walking Regression

    2.1 The Problem

    When implementing vehicle entry via touch, broad tap handlers intercepted gestures globally. A double-tap anywhere on the viewport evaluated vehicle bounding checks against global spatial coordinates. If a user double-tapped on open terrain to engage sprint, they were abruptly teleported into a distant vehicle. Conversely, tapping to walk on foot failed because vehicle entry raycasts consumed the tap payload.

    2.2 The Solution: Raycast Isolation & Bounding Validation

    Vehicle entry was refactored into a strict two-stage predicate:

    1. Raycast collision directly intersects an object whose root ancestor belongs to build.buildGroup with userData.isVehicle === true.
    2. The Euclidean distance between localPlayer.position and targetVehicle.position satisfies d≤8.0 metersd≤8.0 meters .

    code Code

    [Screen Tap Event]
                                  |
                         Raycast Intersect?
                        /                  \
                     (Yes)                 (No)
                      /                      \
            Is userData.isVehicle?       Toggle Standard Walk
            /                    \       (Single: Walk / Double: Run)
         (Yes)                   (No)
          /                        \
    Distance <= 8m?            Toggle Standard Walk
       /        \
    (Yes)       (No)
      |           \
    Board       Toggle Standard Walk
    Vehicle

    code JavaScript

    // REVISION 2: Spatial Raycast Verification
    let tappedVehicle = null;
    
    if (!isTouchMoving) {
        mouse.set((touch.clientX / window.innerWidth) * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
        raycaster.setFromCamera(mouse, camera);
        const hits = raycaster.intersectObjects(build.buildGroup.children, true);
        
        if (hits.length > 0) {
            let obj = hits[0].object;
            while (obj.parent && obj.parent !== build.buildGroup) obj = obj.parent;
            if (obj.userData && obj.userData.isVehicle && localPlayer.position.distanceTo(obj.position) < 8.0) {
                tappedVehicle = obj;
            }
        }
    }
    
    if (tappedVehicle) {
        toggleVehicle(tappedVehicle);
    } else {
        // Retain default walking / sprint toggle
        if (isDoubleTap) {
            keys['w'] = true;
            keys['shift'] = !keys['shift'];
        } else {
            keys['w'] = !keys['w'];
            if (!keys['w']) keys['shift'] = false;
        }
    }

    Section 3: Aerodynamic Pitch Stepping vs. Gimbal Lock

    3.1 The Gimbal Lock & Rotation Glitch

    When users steered horizontally while pitching upward, aircraft meshes exhibited unpredictable inverted flips and axis locking.

    • Cause: Default Three.js Euler evaluation order is ‘XYZ’. Computing Roll(z)Roll(z) after Pitch(x)Pitch(x) with an active Yaw(y)Yaw(y) introduces kinematic coupling where pitch rotations skew the local roll axis.
    • Solution: Switched the rotation evaluation order to ‘YXZ’. Yaw is applied first globally, followed by pitch around the local transverse axis, and finally roll around the longitudinal axis.

    code JavaScript

    activeVeh.rotation.order = 'YXZ';

    3.2 Discrete Elevation Nudging vs. Continuous Angle of Attack

    Flight controls must distinguish between a quick flick to change altitude and a sustained pull-up.

    code JavaScript

    // Quick Flick vs. Sustained Flight Stick Pull
    if (isAirplane && isAirborne) {
        if (touchDuration < 300) {
            if (totalDy > 40) {
                currentVehicle.position.y += 1.8; // Step Altitude Climb
            } else if (totalDy < -40) {
                currentVehicle.position.y = Math.max(1.5, currentVehicle.position.y - 1.8); // Step Dive
            }
        }
        touchHoldingClimb = false;
        touchHoldingDive = false;
    }

    Section 4: Scale Desynchronization in Multiplayer Replicas

    4.1 Root Cause Analysis

    Vehicles scaled up within Build Mode (e.g., scale factor

            s=2.2s=2.2
          

    ) preserved their scale locally inside map_objects.json. However, when a player boarded a vehicle:

    1. socket.emit(‘move’) emitted position and yaw coordinates, omitting vehicleScale.
    2. Remote clients received the update, instantiated the GLTF asset, and passed it through build.normalizeModel(obj, 2.5), which hard-clamped the vehicle’s bounding box to 2.5 units2.5 units .
    3. Result: The driver appeared to ride a vehicle that shrank to miniature dimensions on other screens.

    code Code

    [Driver Client]                              [Remote Client]
    Scale = 2.2 (Custom Edit Mode)               Network Packet: { pos, rot, isDriving } (Missing Scale)
          |                                            |
          v                                            v
    Broadcasting ...                             GLTF Instantiated -> Clamped to 2.5
                                                 Result: Scaled down to miniature size

    4.2 Code Implementation

    We explicitly embedded scalar matrices into the network serialization protocol:

    code JavaScript

    // Networking Payload Serialization (index.html)
    socket.emit('move', { 
        pos: activeControl.position, 
        rot: activeControl.rotation.y,
        rotX: activeControl.rotation.x || 0,
        rotZ: activeControl.rotation.z || 0,
        anim: anim,
        isDriving: isDriving,
        vehicleId: (isDriving && currentVehicle) ? currentVehicle.userData.id : null,
        vehicleScale: (isDriving && currentVehicle) ? (currentVehicle.scale.x || 1.0) : 1.0
    });

    Section 5: Kinematic Stutter & Self-Intersecting Raycasts

    5.1 The Root Cause of Forward Translation Jitter on [W]

    When driving on the ground, holding W produced severe, high-frequency stuttering.

    Three compounding issues caused this behavior:

    1. Raycast Self-Intersection: The 8-directional horizontal collision sweep (isBlocked) cast rays from (x,y+0.6,z)(x,y+0.6,z) . Because vehicle geometries possess large bounding radii, the collision rays struck the car’s own wheels and body panels, registering a false-positive obstacle distance d<1.3 md<1.3 m . The engine repeatedly reset activeControl.position to oldX and oldZ on alternating frames.
    2. Raw Ground Snapping: Vertical elevation (position.y) was set directly to raw terrain raycast hits without smoothing:
      position.y=targetGroundposition.y=targetGround
      Micro-variations across polygonal terrain triangles caused the entity to vibrate vertically.
    3. Frame-Rate Dependent Camera Latching: The third-person chase camera interpolated position using fixed frame-rate scalars:
      P⃗cam=lerp(P⃗cam,P⃗target,0.1)Pcam​=lerp(Pcam​,Ptarget​,0.1)
      Frame-time fluctuations between 14 ms14 ms and 18 ms18 ms caused visual camera hitching.

    code Code

    Frame N:   TranslateZ(+speed) -> isBlocked() hits vehicle chassis -> Reset to (oldX, oldZ)
    Frame N+1: Ray passes between internal vertices -> Movement allowed
    Frame N+2: Ray strikes internal wheel vertex -> Reset to (oldX, oldZ)
    Result:    High-frequency 30Hz position vibration (Stutter)

    5.2 The Unified Mathematical Resolution

    code JavaScript

    // 1. Rigorous Descendant Exclusion in Collision Traversal (build.js)
    const validMeshes = [];
    this.collidableMeshes.forEach(m => {
        let isPart = false;
        if (ignoreVehicle && (m.parent === ignoreVehicle || (m.userData && m.userData.isVehiclePart))) {
            isPart = true;
        }
        if (!isPart) validMeshes.push(m);
    });
    
    // 2. Continuous Exponential Ground Damping (index.html)
    activeControl.position.y = THREE.MathUtils.lerp(
        activeControl.position.y, 
        targetGround, 
        delta * 15.0
    );
    
    // 3. Delta-Compensated Camera Latching (index.html)
    const cameraDampFactor = 1.0 - Math.exp(-8.0 * delta);
    camera.position.lerp(_vLookTarget, cameraDampFactor);

    Section 6: Asynchronous Asset Loading Race Conditions & Invisibility Traps

    6.1 The One-Way Invisibility Failure

    A critical desync occurred when two players operated vehicles simultaneously:

    • Player 1 and Player 2 exited aircraft at the same destination.
    • Player 2 saw Player 1 clearly, but Player 1 could not see Player 2.

    code Code

    Timeline of the Asynchronous Loading Trap:
    
    Player 2 Client                      Player 1 Client (Observing Player 2)
    ----------------                      ------------------------------------
    1. Board Vehicle (isDriving=true) --> 2. Receives update: p.isDriving = true
                                          3. Begins GLTF loader for vehicle
                                          4. Sets Player 2 human mesh: visible = false
    5. Exits Vehicle (isDriving=false) -> 6. Receives update: p.isDriving = false
                                          7. Sets Player 2 human mesh: visible = true
                                          8. (500ms later) GLTF Loader FINISHES late!
                                          9. Loader callback executes: adds vehicle, 
                                             overrides human visibility to FALSE!
                                          Result: Player 2 permanently invisible!

    6.2 Structural Resolution: Scene-Graph Root Decoupling & Packet Queuing

    We eliminated deep hierarchy traversals (p.model.traverse(…)) by introducing an explicit Root Group Isolation Architecture:

    code Code

    rootGroup (Spatial Node: Position, Rotation)
                     /         \
        bodyMesh (Human GLTF)   vehicleMesh (Dynamic GLTF)

    code JavaScript

    // applyRemoteUpdate: Race-Condition Immune Mesh Management
    function applyRemoteUpdate(p, d) {
        p.rootGroup.position.lerp(new THREE.Vector3(d.pos.x, d.pos.y, d.pos.z), 0.3); 
        p.rootGroup.rotation.y = d.rot;
    
        p.isDriving = !!d.isDriving;
    
        if (d.isDriving && d.vehicleId) {
            if (p.bodyMesh) p.bodyMesh.visible = false;
    
            // Directly manipulate the physical world vehicle
            const worldVeh = build.buildGroup.children.find(c => c.userData && c.userData.id === d.vehicleId);
            if (worldVeh) {
                worldVeh.position.lerp(new THREE.Vector3(d.pos.x, d.pos.y, d.pos.z), 0.35);
                worldVeh.rotation.y = d.rot;
                worldVeh.rotation.x = d.rotX || 0;
                worldVeh.rotation.z = d.rotZ || 0;
            }
        } else {
            // Instant, unconditional human mesh restoration
            if (p.bodyMesh) p.bodyMesh.visible = true;
    
            let active = p[d.anim] || p.idle;
            if (active) { 
                if (d.anim === 'death' && !active.isRunning()) active.reset().play();
                else active.play();
                [p.idle, p.walk, p.run, p.jump, p.wave, p.death].forEach(a => { if (a && a !== active) a.stop(); }); 
            }
        }
    }

    code JavaScript

    // Packet Queuing for Entities Loading in Background
    function createRemote(id, data) {
        if (otherPlayers[id]) return;
    
        const rootGroup = new THREE.Group();
        rootGroup.position.set(data.pos.x, data.pos.y, data.pos.z);
        rootGroup.rotation.y = data.rot;
        rootGroup.visible = isStarted;
        scene.add(rootGroup);
    
        loader.load('player/' + data.model, (gltf) => {
            const body = gltf.scene;
            rootGroup.add(body);
    
            const playerObj = { 
                id, 
                rootGroup, 
                bodyMesh: body, 
                isDriving: !!data.isDriving,
                mixer: new THREE.AnimationMixer(body),
                // ... animations ...
            };
    
            otherPlayers[id] = playerObj;
    
            // Flush queued packets received during download phase
            if (pendingUpdates[id]) {
                applyRemoteUpdate(playerObj, pendingUpdates[id]);
                delete pendingUpdates[id];
            }
        });
    }

    Section 7: Infinite Spawner Proliferation & World-State Isolation

    7.1 The Problem: Fleet Multiplication Bug

    When a spawner automatically produced a replacement vehicle after

            5 seconds5 seconds
          

    of displacement (

            d>25 md>25 m
          

    ), an edge-case occurred:

    • Player A drove Car 1 to destination XX .
    • Car 2 spawned on the runway.
    • Player A returned on foot, boarded Car 2, and drove to destination YY .
    • A single user could generate an unbounded number of vehicles across the simulation.

    code Code

    Initial State:  [Spawn Pad: Car 1]
    Player takes it: [Car 1 moving...] -> 5s Timer -> [Spawn Pad: Car 2 created]
    Player returns:  Abandons Car 1 at Spot X -> Takes Car 2 -> [Spawn Pad: Car 3 created]
    Result:          Exponential object growth, disk bloat in map_objects.json, physics lag

    7.2 The Solution: Strict Single-Active-Vehicle-Per-Player Policy

    code Code

    Player Attempts to Board Vehicle (targetId)
                                             |
                           Does Player have myActiveVehicleId?
                                        /         \
                                     (Yes)        (No)
                                      /             \
                      myActiveVehicleId !== targetId?  Set myActiveVehicleId = targetId
                                 /          \                 |
                              (Yes)         (No)              |
                               /              \               |
                  Delete Old Vehicle           \              |
                  (BuildGroup & Server)         \             |
                             |                   \            |
                             +--------------------+-----------+
                                                  |
                                         Board New Vehicle

    code JavaScript

    // 1-Vehicle Per Player Enforcement Engine (index.html)
    if (myActiveVehicleId && myActiveVehicleId !== targetId) {
        const oldVeh = build.buildGroup.children.find(c => c.userData && c.userData.id === myActiveVehicleId);
        if (oldVeh) {
            build.buildGroup.remove(oldVeh);
            build.collidableMeshes = build.collidableMeshes.filter(m => !oldVeh.children.includes(m));
            build.disposeObject(oldVeh);
            socket.emit('delete_object', { id: myActiveVehicleId });
        }
    }
    myActiveVehicleId = targetId;

    code JavaScript

    // Spatial Clearance Verification for Spawn Pad Replacements
    let alreadyHasFreeVehicle = false;
    for (let obj of build.buildGroup.children) {
        if (obj.userData && obj.userData.isVehicle) {
            if (obj.position.distanceTo(originSpawnData.pos) < 12.0) {
                alreadyHasFreeVehicle = true;
                break;
            }
        }
    }
    
    let playerOnPad = false;
    if (!isDriving && localPlayer && localPlayer.position.distanceTo(originSpawnData.pos) < 5.0) {
        playerOnPad = true;
    }
    for (let id in otherPlayers) {
        const p = otherPlayers[id];
        if (p && p.rootGroup && p.rootGroup.position.distanceTo(originSpawnData.pos) < 5.0) {
            playerOnPad = true;
            break;
        }
    }
    
    // Postpone spawn if occupied by a physical vehicle or human avatar
    if (playerOnPad) {
        respawnCountdown = 2.0; 
    } else if (!alreadyHasFreeVehicle) {
        hasRespawnedAtOrigin = true;
        const freshSpawn = {
            id: 'obj_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
            file: originSpawnData.file,
            pos: originSpawnData.pos,
            rot: originSpawnData.rot,
            scale: originSpawnData.scale
        };
        socket.emit('place_object', freshSpawn);
        build.createInScene(freshSpawn);
    }

    Section 8: Build Mode (?edit) Scene Isolation & Welcome Screen Sandboxing

    8.1 Welcome Screen Leakage

    When unauthenticated users were on the initial avatar selection carousel (isStarted === false), remote players who joined and moved through coordinates

            (0,0,0)(0,0,0)
          

    were visible behind the modal UI.

    code JavaScript

    // Resolution: Initial Visibility Latch
    rootGroup.visible = isStarted; // Remains false until enterGame() completes

    code JavaScript

    // On 'joined' Event: Unhide remote entities simultaneously
    socket.on('joined', data => {
        isStarted = true;
        Object.values(otherPlayers).forEach(p => {
            if (p.rootGroup) p.rootGroup.visible = true;
        });
    });

    8.2 Private Editor Sandbox in Build Mode

    When entering level-editing mode (build.isEditMode === true):

    • All remote player instances and offline ghosts are dynamically hidden from the scene graph.
    • The local player avatar is suppressed.
    • OrbitControls take exclusive control of the camera view.
    • Exiting Build Mode restores visibility and state without desynchronizing network streams.

    code JavaScript

    // animate() Rendering Loop: Strict Sandbox Partitioning
    if (inBuild) {
        Object.values(otherPlayers).forEach(p => {
            if (p.rootGroup) p.rootGroup.visible = false;
        });
        if (localPlayer) localPlayer.visible = false;
    } else {
        Object.values(otherPlayers).forEach(p => {
            if (p.rootGroup) p.rootGroup.visible = true;
        });
        if (!isDriving && localPlayer) localPlayer.visible = true;
    }

    Section 9: Mobile DOM Touch-Action Interception (social.js)

    9.1 Root Cause of Horizontal Scroll Lock on Mobile

    The quick-chat social menu rendered properly on desktop, but on mobile devices, dragging the menu failed to scroll the emojis or phrases.

    • Cause: The global document listener in index.html executed e.preventDefault() on all touchmove events to prevent browser window pull-down refresh: code JavaScriptwindow.addEventListener('touchmove', (e) => { e.preventDefault(); }, { passive: false });
    • Result: Because #socialMenu was not excluded in touchmove, horizontal pan gestures inside .social-row were cancelled by the browser before native scrolling could occur.

    9.2 The Dual-Layer Fix

    1. DOM Tree Event Boundary Check (index.html): code JavaScriptwindow.addEventListener('touchmove', (e) => { if (e.target.closest('#socialMenu, #modal, #editBtn, #assetPanel, #buildToolbar')) return; // ... standard gameplay gesture processing ... e.preventDefault(); }, { passive: false });
    2. CSS Touch-Action Declaration & Event Isolation (social.js): code CSS.social-row { display: flex; gap: 6px; flex-wrap: nowrap; overflow-x: auto; touch-action: pan-x !important; /* Informs browser to allow native horizontal panning */ -webkit-overflow-scrolling: touch; } code JavaScript['touchstart', 'touchmove', 'touchend'].forEach(evt => { div.addEventListener(evt, (e) => e.stopPropagation(), { passive: true }); });

    Section 10: Ghost Shading Calibration

    To ensure disconnected players remained visible as non-intrusive historical presences across 24-hour retention cycles, offline ghost rendering was calibrated to a subtle semi-transparent appearance:

    code JavaScript

    // Calibration: 0.5 -> 0.1 Material Opacity
    if (isGhost) { 
        body.traverse(c => { 
            if (c.isMesh) {
                c.material = c.material.clone();
                c.material.transparent = true;
                c.material.opacity = 0.1; // Reduced visual footprint for offline records
            }
        }); 
    }

    Engineering Post-Mortem & Verified Architecture Matrix

    Bug / ChallengeTheoretical Root CauseEngineering Solution
    Mobile Vehicle UnresponsiveViewport drag events mutated localPlayer.rotation.y instead of the active entity currentVehicle.Dynamic control binding to activeControl (Vehicle vs. Human) in touch handlers.
    Aircraft Gimbal LockSequential Euler angle evaluation order ‘XYZ’ skewing local axes under simultaneous roll/pitch.Switched aircraft coordinate transforms to ‘YXZ’ order.
    Multiplayer Vehicle ShrinkingDefault GLTF normalization scale ( 2.5 units2.5 units ) hardcoded during remote asset load.Serialized scalar dimensions (vehicleScale) across Socket.IO payloads.
    High-Frequency Forward StutterRaycaster self-intersecting internal vehicle chassis geometry in horizontal sweep.Hierarchical mesh exclusion filters in BuildSystem.isBlocked().
    One-Way InvisibilityLate-resolving asynchronous asset loader callbacks overriding root visibility flags.Scene-graph decoupling (rootGroup vs bodyMesh) and update packet queues.
    Fleet Duplication SpawnsClient-side spawner triggers firing without global player ownership checks.Strict 1-Active-Vehicle-Per-Player enforcement and spatial clearance validation ( r=12 mr=12 m ).
    Welcome Screen Player LeaksRemote player root groups added to scene with default visible = true.Visibility state bound to isStarted flag and synchronized on joined event.
    UI Touch Scroll FailureGlobal canvas touchmove invoking e.preventDefault() indiscriminately.Added element boundary exclusions and CSS touch-action: pan-x !important.

    Conclusion

    By systematically decoupling the kinematic simulation layer, the network serialization protocol, and the Three.js scene graph, we transformed a fragile prototype into a resilient, high-performance web multiplayer engine. The system delivers 60 FPS simulations across mobile and desktop runtimes, maintains clean persistent world state, and ensures reliable spatial synchronization across all connected clients.

  • world – assets – drive and fly

    hit ‘enter’ key to in car and plane

    FLY manual

    take off – ‘W’ then ‘space bar’
    fly high – S
    fly down – W

    coming soon mobile support!

  • Dev Blog: Engineering the “Infinite Run” – Cinematic

    The Evolution: From Simple Stickmen to Cinematic Neon Giants

    https://mero.live/games/run

    What started as a basic experiment in 3D movement has evolved into a high-stakes, cinematic “Data Stream” simulation. We moved away from “balloon men” into Director Mode—a system where physics, HERO jumps, and cartoonish deaths are captured through a dynamic lens.


    1. Scaling the Threat: The 2X Giant

    We realized that to make the “Infinite Run” feel impactful, the runners needed to dominate the screen. We doubled their scale to make them feel like “Titan” data packets.

    • The Idea: Move from human-scale to giant-scale (2X taller).
    • The Code Change: We scaled the CapsuleGeometry and added a complex joint system for the arms and hands.

    code JavaScript

    // Engineering the 2X Giant
    const bodyGeo = new THREE.CapsuleGeometry(1.0, 2.0, 4, 10); // Double height
    const limbGeo = new THREE.CapsuleGeometry(0.35, 1.6, 4, 8); // Thick reinforced limbs
    const handGeo = new THREE.SphereGeometry(0.5, 8, 8);      // Large glowing contact points

    2. Hand Motion & “Wrist Flick”

    To prevent the giants from looking like stiff robots, we implemented a secondary animation layer for the wrists.

    • The Logic: As the arm swings (Pendulum physics), the hand should “flick” in the opposite direction to simulate weight and momentum.

    code JavaScript

    // Hand/Wrist flick logic
    const swingX = Math.sin(t);
    const swingY = Math.cos(t);
    
    this.lHand.rotation.x = swingX * 0.5;   // Wrist flick back/forth
    this.lHand.position.z = swingY * 0.2;   // Subtle depth "drag"
    this.lArm.rotation.z = Math.abs(swingX) * 0.2; // Natural arm flare

    3. “Hero Jump” vs. “Cartoon Flatten”

    The core gameplay mechanic involves a high-speed ball. We wanted the interaction to feel like a high-budget action movie.

    • Hero Jump: Only 1 or 2 runners per roll are “chosen” to jump. They launch into a parabolic arc, leaning forward like a superhero.
    • Flatten Physics: If a runner fails to jump or is hit, they don’t just disappear—they are instantly crushed into a 0.02-scale pancake, sliding across the mirror floor.

    code JavaScript

    // The "Pancake" Death Logic
    flatten() {
        this.state = 'flat';
        this.group.scale.set(2.0, 0.02, 2.0); // Extreme vertical squash
        this.group.position.y = 0.05;         // Stick to the mirror floor
    }

    4. Director Mode: Cinematic Camera Cuts

    A fixed camera is boring. We built a Virtual Director that watches the race and decides when to “cut” to a better angle.

    • The Logic: When a Hero Jump is detected, the camera snaps to a random cinematic preset (Side-view, Birds-eye, or Low-Front) and zooms in using Bullet Time (Slow Motion).

    code JavaScript

    // Director Camera Angle Presets
    const jumpCamAngles = [
        new THREE.Vector3(40, 15, 0),   // Dramatic Profile
        new THREE.Vector3(-40, 15, 5),  // Reverse Angle
        new THREE.Vector3(0, 8, 50),    // Low Hero Angle
    ];
    
    // Switch to Cinematic Focus
    if (anyoneJumping) {
        camTargetPos.set(hero.x + angle.x, hero.y + angle.y, hero.z + angle.z);
        bulletTime = 0.2; // Slow down the universe
    }

    5. Visible Data: KB/MB Tracking

    To tie the theme together, we treated every object as a “Download.”

    • The Concept: Spawning a runner isn’t just a 3D call; it’s a 120.4 KB data payload.
    • Tracking: Every tap to launch the ball costs data, and every death results in “Packet Loss.”

    Final Optimized Code

    This code includes the 2X Scale Giants, Hand/Wrist Motion, Director Mode Camera, Hero Jump Logic (1-2 per roll), and Flattening Physics.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Infinite Run - Director Mode</title>
        <style>
            body { margin: 0; overflow: hidden; background-color: #000; font-family: 'Courier New', Courier, monospace; }
            canvas { display: block; width: 100vw; height: 100vh; cursor: crosshair; }
            #ui-left { position: absolute; top: 15px; left: 15px; color: #00ffcc; background: rgba(0,0,0,0.8); padding: 12px; border-left: 3px solid #00ffcc; pointer-events: none; font-size: 11px; text-transform: uppercase; z-index: 10; }
            #instruction { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #ff00ff; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; pointer-events: none; animation: pulse 1.5s infinite; font-size: 12px; }
            @keyframes pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
            .highlight { color: #fff; font-weight: bold; font-size: 16px; }
            .label { opacity: 0.6; font-size: 9px; display: block; margin-top: 5px; }
        </style>
    </head>
    <body>
    
    <div id="instruction">Tap to Launch Ball | Cinematic Jumps</div>
    <div id="ui-left">
        <div>SYSTEM: <span style="color: #00ff00;">DIRECTOR_V5_STABLE</span></div>
        <span class="label">Total Streamed Data</span>
        <div><span id="total-kb" class="highlight">0.00</span> MB</div>
        <span class="label">Jumps This Roll</span>
        <div><span id="jump-count-ui" class="highlight">0</span> / <span id="jump-max-ui">0</span></div>
        <span class="label">Active Runners</span>
        <div id="unit-count" class="highlight">0</div>
    </div>
    
    <script type="importmap">
        { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } }
    </script>
    
    <script type="module">
        import * as THREE from 'three';
        import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
        import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
        import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
        import { Reflector } from 'three/addons/objects/Reflector.js';
        import Stats from 'three/addons/libs/stats.module.js';
    
        let totalKB = 0, jumpsThisRoll = 0, maxJumpsForThisRoll = 0, ballActive = false;
        const uiTotal = document.getElementById('total-kb'), uiJumpCount = document.getElementById('jump-count-ui'), uiJumpMax = document.getElementById('jump-max-ui'), uiUnits = document.getElementById('unit-count');
    
        function logData(kb) { totalKB += kb; uiTotal.innerText = (totalKB / 1024).toFixed(2); }
    
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x020205);
        scene.fog = new THREE.FogExp2(0x020205, 0.006);
        const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
        let camTargetPos = new THREE.Vector3(0, 30, 100), lookTargetSmooth = new THREE.Vector3();
    
        const renderer = new THREE.WebGLRenderer({ antialias: false });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        document.body.appendChild(renderer.domElement);
        const stats = new Stats();
        stats.dom.style.left = 'auto'; stats.dom.style.right = '0px';
        document.body.appendChild(stats.dom);
    
        const clock = new THREE.Clock(), raycaster = new THREE.Raycaster(), mouse = new THREE.Vector2();
        scene.add(new THREE.AmbientLight(0xffffff, 0.2));
        const sun = new THREE.DirectionalLight(0xffffff, 2.5);
        sun.position.set(50, 100, 50); scene.add(sun);
    
        const ground = new Reflector(new THREE.PlaneGeometry(2000, 2000), { clipBias: 0.003, textureWidth: 512, textureHeight: 512, color: 0x111111 });
        ground.rotateX(-Math.PI / 2); scene.add(ground);
    
        const headGeo = new THREE.SphereGeometry(1.2, 12, 12), bodyGeo = new THREE.CapsuleGeometry(1.0, 2.0, 4, 10), limbGeo = new THREE.CapsuleGeometry(0.35, 1.6, 4, 8); 
        limbGeo.translate(0, -0.8, 0);
        const handGeo = new THREE.SphereGeometry(0.5, 8, 8); 
    
        let runners = [];
        const colors = [0x00ffff, 0xff00ff, 0x00ff88, 0xffff00, 0xff3333];
        const jumpCamAngles = [new THREE.Vector3(40, 15, 0), new THREE.Vector3(-40, 15, 5), new THREE.Vector3(0, 8, 50)];
        let currentJumpAngle = new THREE.Vector3();
    
        class Runner {
            constructor(x, z, speed, color) {
                this.group = new THREE.Group();
                this.mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 2 });
                const body = new THREE.Mesh(bodyGeo, this.mat); body.position.y = 3.2;
                const head = new THREE.Mesh(headGeo, this.mat); head.position.y = 5.6;
                this.lLeg = new THREE.Mesh(limbGeo, this.mat); this.rLeg = new THREE.Mesh(limbGeo, this.mat);
                this.lLeg.position.set(0.7, 2, 0); this.rLeg.position.set(-0.7, 2, 0);
                this.lArm = new THREE.Mesh(limbGeo, this.mat); this.rArm = new THREE.Mesh(limbGeo, this.mat);
                this.lArm.position.set(1.5, 4.2, 0); this.rArm.position.set(-1.5, 4.2, 0);
                this.lHand = new THREE.Mesh(handGeo, this.mat); this.rHand = new THREE.Mesh(handGeo, this.mat);
                this.lHand.position.y = -1.6; this.rHand.position.y = -1.6;
                this.lArm.add(this.lHand); this.rArm.add(this.rHand);
                this.group.add(head, body, this.lLeg, this.rLeg, this.lArm, this.rArm);
                this.group.position.set(x, 0, z);
                this.speed = speed * 120; this.animOffset = Math.random() * 10;
                this.state = 'running'; this.jumpProgress = 0;
                scene.add(this.group); logData(120.4);
            }
            update(elapsed, delta, ball) {
                if (this.state === 'flat') return;
                const distZ = this.group.position.z - ball.position.z, distX = Math.abs(this.group.position.x - ball.position.x);
                if (this.state === 'running' && ballActive && jumpsThisRoll < maxJumpsForThisRoll) {
                    if (distZ < 30 && distZ > 15 && distX < 10) {
                        this.state = 'jumping'; this.jumpProgress = 0; jumpsThisRoll++;
                        uiJumpCount.innerText = jumpsThisRoll;
                        currentJumpAngle.copy(jumpCamAngles[Math.floor(Math.random() * jumpCamAngles.length)]);
                    }
                }
                if (this.state === 'jumping') {
                    this.jumpProgress += delta * 1.8;
                    this.group.position.y = Math.sin(this.jumpProgress * Math.PI) * 26;
                    this.group.rotation.x = -Math.sin(this.jumpProgress * Math.PI) * 0.6;
                    if (this.jumpProgress >= 1.0) { this.state = 'running'; this.group.position.y = 0; this.group.rotation.x = 0; }
                } else {
                    if (distZ < 60 && distZ > 0 && distX < 22) this.group.position.x += (this.group.position.x > ball.position.x ? 1 : -1) * 35 * delta;
                }
                const t = elapsed * 15 + this.animOffset, swingX = Math.sin(t), swingY = Math.cos(t);
                this.lLeg.rotation.x = swingX; this.rLeg.rotation.x = -swingX;
                this.lArm.rotation.x = -swingX * 1.2; this.rArm.rotation.x = swingX * 1.2;
                this.lHand.rotation.x = swingX * 0.5; this.rHand.rotation.x = -swingX * 0.5;
                if (this.state === 'running') this.group.position.y = Math.abs(Math.cos(t * 1.5)) * 0.6;
                this.group.position.z += this.speed * delta;
            }
            flatten() {
                if (this.state === 'flat') return;
                this.state = 'flat'; this.group.scale.set(2.0, 0.02, 2.0); this.group.position.y = 0.05;
                setTimeout(() => { scene.remove(this.group); runners = runners.filter(r => r !== this); }, 3000);
            }
        }
    
        const ballRadius = 8;
        const deathBall = new THREE.Mesh(new THREE.SphereGeometry(ballRadius, 32, 32), new THREE.MeshStandardMaterial({ color: 0x000000, emissive: 0xff0044, emissiveIntensity: 5 }));
        deathBall.position.set(0, ballRadius, -1000); scene.add(deathBall);
    
        function launchBall(event) {
            const x = (event.clientX || (event.touches && event.touches[0].clientX)), y = (event.clientY || (event.touches && event.touches[0].clientY));
            mouse.x = (x / window.innerWidth) * 2 - 1; mouse.y = -(y / window.innerHeight) * 2 + 1;
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObject(ground);
            if (intersects.length > 0) {
                deathBall.position.set(intersects[0].point.x, ballRadius, 140);
                ballActive = true; jumpsThisRoll = 0; maxJumpsForThisRoll = Math.floor(Math.random() * 2) + 1;
                uiJumpCount.innerText = "0"; uiJumpMax.innerText = maxJumpsForThisRoll; logData(480.0);
            }
        }
        window.addEventListener('mousedown', launchBall); window.addEventListener('touchstart', launchBall);
    
        let spawnTimer = 0;
        function spawnSingleRunner() {
            const x = (Math.random() - 0.5) * 120, color = colors[Math.floor(Math.random() * colors.length)];
            runners.push(new Runner(x, -550, 0.18 + Math.random() * 0.1, color));
        }
    
        const composer = new EffectComposer(renderer);
        composer.addPass(new RenderPass(scene, camera));
        composer.addPass(new UnrealBloomPass(new THREE.Vector2(256, 256), 1.3, 0.4, 0.85));
    
        function animate() {
            stats.begin();
            let rawDelta = clock.getDelta();
            const jumpingHero = runners.find(r => r.state === 'jumping');
            const anyoneJumping = !!jumpingHero;
            const bulletTime = (anyoneJumping && jumpingHero.group.position.y > 5) ? 0.2 : 1.0;
            const delta = rawDelta * bulletTime, elapsed = clock.getElapsedTime();
    
            spawnTimer += rawDelta; if (spawnTimer >= 0.4) { spawnSingleRunner(); spawnTimer = 0; }
            deathBall.position.z -= 135 * delta; deathBall.rotation.x -= 10 * delta;
            if (deathBall.position.z < -600) ballActive = false;
    
            for (let i = runners.length - 1; i >= 0; i--) {
                runners[i].update(elapsed, delta, deathBall);
                if (runners[i].group.position.distanceTo(deathBall.position) < ballRadius + 2.2 && runners[i].group.position.y < ballRadius + 1.5) runners[i].flatten();
                if (runners[i].group.position.z > 160) { scene.remove(runners[i].group); runners.splice(i, 1); }
            }
    
            if (anyoneJumping) {
                camTargetPos.set(jumpingHero.group.position.x + currentJumpAngle.x, jumpingHero.group.position.y + currentJumpAngle.y, jumpingHero.group.position.z + currentJumpAngle.z);
                lookTargetSmooth.lerp(jumpingHero.group.position, 0.1);
            } else {
                let avgX = 0; if(runners.length > 0) { runners.forEach(r => avgX += r.group.position.x); avgX /= runners.length; }
                camTargetPos.set(avgX * 0.5, 30, 110); lookTargetSmooth.lerp(new THREE.Vector3(avgX, 10, -20), 0.05);
            }
            camera.position.lerp(camTargetPos, 0.1); camera.lookAt(lookTargetSmooth);
            uiUnits.innerText = runners.length; composer.render(); stats.end();
            requestAnimationFrame(animate);
        }
        window.addEventListener('resize', () => { camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
        animate();
    </script>
    </body>
    </html>