ride

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ride</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #fff; }
        canvas { display: block; }
    </style>
</head>
<body>

    <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 { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
        import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

        // --- CONFIGURATION ---
        const MODELS_FOLDER = './glb/'; 
        const MODEL_FILES = ['dirty.glb', 'truck.glb']; 
        const TARGET_WIDTH = 3; 
        const WORLD_SPEED = 0.6; 
        const LANE_WIDTH = 8;    

        // --- SCENE SETUP ---
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0xffffff); 

        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(15, 10, 25);

        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        document.body.appendChild(renderer.domElement);

        // STUDIO LIGHTING
        scene.add(new THREE.AmbientLight(0xffffff, 1.0));
        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.5);
        hemiLight.position.set(0, 20, 0);
        scene.add(hemiLight);

        const dirLight = new THREE.DirectionalLight(0xffffff, 2);
        dirLight.position.set(10, 20, 10);
        scene.add(dirLight);

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;

        const clock = new THREE.Clock();

        // --- THE INFINITE ROAD ---
        const roadGeo = new THREE.PlaneGeometry(40, 1000);
        const roadMat = new THREE.MeshStandardMaterial({ color: 0xdddddd }); 
        const road = new THREE.Mesh(roadGeo, roadMat);
        road.rotation.x = -Math.PI / 2;
        scene.add(road);

        // FLAT ROAD MARKERS (No Height)
        const markers = [];
        for(let i = 0; i < 40; i++) {
            // Use PlaneGeometry for zero height (like paint)
            const markerGeo = new THREE.PlaneGeometry(0.4, 2); 
            const markerMat = new THREE.MeshStandardMaterial({ 
                color: 0xbbbbbb,
                side: THREE.DoubleSide 
            });
            const marker = new THREE.Mesh(markerGeo, markerMat);
            
            // Rotate to lay flat on road
            marker.rotation.x = -Math.PI / 2;
            
            // Position slightly above 0 (0.01) to prevent flickering (Z-fighting)
            marker.position.set(0, 0.01, (i * 10) - 200);
            
            scene.add(marker);
            markers.push(marker);
        }

        // LOADER SETUP
        const loader = new GLTFLoader();
        const dracoLoader = new DRACOLoader();
        dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
        loader.setDRACOLoader(dracoLoader);

        const allActiveCars = []; 
        const mixers = [];

        function normalizeModelSize(model) {
            const box = new THREE.Box3().setFromObject(model);
            const size = new THREE.Vector3();
            box.getSize(size);
            const scaleFactor = TARGET_WIDTH / size.x;
            model.scale.set(scaleFactor, scaleFactor, scaleFactor);
            const center = new THREE.Vector3();
            box.getCenter(center);
            model.position.x -= center.x * scaleFactor;
            model.position.y -= center.y * scaleFactor;
            model.position.z -= center.z * scaleFactor;
        }

        async function loadAndStream() {
            try {
                for (let i = 0; i < MODEL_FILES.length; i++) {
                    const gltf = await loader.loadAsync(MODELS_FOLDER + MODEL_FILES[i]);
                    const model = gltf.scene;
                    normalizeModelSize(model);
                    const xPos = (i * LANE_WIDTH) - ((MODEL_FILES.length - 1) * LANE_WIDTH / 2);
                    model.position.set(xPos, 0, 0);
                    model.rotation.y = 0; 
                    scene.add(model);
                    allActiveCars.push(model);

                    if (gltf.animations?.length > 0) {
                        const mixer = new THREE.AnimationMixer(model);
                        mixer.clipAction(gltf.animations[0]).play();
                        mixers.push(mixer);
                    }
                }
            } catch (err) { console.error("Load Error:", err); }
        }

        function animate() {
            requestAnimationFrame(animate);
            const delta = clock.getDelta();
            mixers.forEach(m => m.update(delta));
            
            // MOVE ROAD MARKERS (Creates the illusion of driving)
            markers.forEach(marker => {
                marker.position.z -= WORLD_SPEED;
                if (marker.position.z < -100) {
                    marker.position.z = 100; 
                }
            });

            controls.update();
            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        loadAndStream();
        animate();
    </script>
</body>
</html>
Expand

https://mero.live/build/ride

glb – https://sketchfab.com/search?features=downloadable+animated&q=car&type=models