import React, { useMemo } from 'react'; import { useCurrentFrame, useVideoConfig, interpolate, Easing, AbsoluteFill, } from 'remotion'; import * as THREE from 'three'; // ============================================================================= // COMPOSITION CONFIG // ============================================================================= export const compositionConfig = { id: 'TrainCountryside3DOptimized', durationInSeconds: 5, fps: 60, // 60fps is smooth enough and more performant width: 1920, height: 1080, }; // ============================================================================= // STYLE CONSTANTS // ============================================================================= const COLORS = { hillFar: 0x4a7c4a, hillMid: 0x5a8f5a, hillNear: 0x6ba36b, grass: 0x7cb87c, grassLight: 0x9ccc65, grassDark: 0x558b2f, trainRed: 0xc62828, trainOrange: 0xe65100, trainYellow: 0xffd600, trainBlue: 0x455a64, fenceWood: 0x8b6914, rail: 0x4a5568, railTie: 0x6b5344, sheep: 0xfafafa, sheepHead: 0x1a1a1a, chimney: 0x37474f, wheel: 0x546e7a, dust: 0xd4c4a8, } as const; const EASINGS = { gentle: Easing.bezier(0.25, 0.1, 0.25, 1), }; // ============================================================================= // SEEDED RANDOM // ============================================================================= const seededRandom = (seed: number): number => { const x = Math.sin(seed * 9999) * 10000; return x - Math.floor(x); }; // ============================================================================= // MAIN SCENE COMPONENT // ============================================================================= const ThreeScene: React.FC<{ frame: number; durationInFrames: number; fps: number }> = ({ frame, durationInFrames, fps, }) => { const canvasRef = React.useRef(null); const time = frame / fps; // Pre-generate static data ONCE const sceneData = useMemo(() => { // Grass blades - reduced to 4000 for performance const grassBlades: { x: number; z: number; height: number; rotation: number; colorVar: number }[] = []; for (let i = 0; i < 4000; i++) { const angle = seededRandom(i * 1.1) * Math.PI * 2; const radius = 2 + seededRandom(i * 2.2) * 40; const x = Math.cos(angle) * radius + (seededRandom(i * 3.3) - 0.5) * 15; const z = Math.sin(angle) * radius * 0.4 + seededRandom(i * 4.4) * 12 - 3; if (Math.abs(z) < 1.8 || (z > 2.5 && z < 3.5) || (z < -7.5 && z > -8.5)) continue; grassBlades.push({ x, z, height: 0.2 + seededRandom(i * 5.5) * 0.3, rotation: seededRandom(i * 6.6) * Math.PI * 2, colorVar: seededRandom(i * 7.7), }); } // Fence posts const fencePosts: { x: number; z: number }[] = []; for (let i = 0; i < 40; i++) { fencePosts.push({ x: -40 + i * 2.5, z: 3 }); fencePosts.push({ x: -40 + i * 2.5, z: -8 }); } // Track ties const trackTies: { x: number }[] = []; for (let i = 0; i < 60; i++) { trackTies.push({ x: -45 + i * 1.5 }); } // Steam particles - optimized count const steamParticles: { offsetX: number; offsetZ: number; size: number; delay: number; speed: number }[] = []; for (let i = 0; i < 25; i++) { steamParticles.push({ offsetX: seededRandom(i * 100) * 1.2 - 0.6, offsetZ: seededRandom(i * 101) * 0.6 - 0.3, size: 0.25 + seededRandom(i * 300) * 0.4, delay: seededRandom(i * 400) * 2, speed: 0.7 + seededRandom(i * 500) * 0.5, }); } // Dust particles const dustParticles: { offsetX: number; offsetZ: number; size: number; delay: number; side: number }[] = []; for (let i = 0; i < 30; i++) { dustParticles.push({ offsetX: seededRandom(i * 111) * 2 - 1, offsetZ: seededRandom(i * 222) * 0.4, size: 0.04 + seededRandom(i * 333) * 0.08, delay: seededRandom(i * 444) * 1.5, side: i % 2 === 0 ? 1 : -1, }); } // Floating particles const airParticles: { x: number; y: number; z: number; size: number; speed: number }[] = []; for (let i = 0; i < 80; i++) { airParticles.push({ x: seededRandom(i * 11) * 50 - 25, y: 1 + seededRandom(i * 22) * 6, z: seededRandom(i * 33) * 15 - 7, size: 0.015 + seededRandom(i * 44) * 0.03, speed: 0.4 + seededRandom(i * 55) * 0.8, }); } const sheep = [ { x: -8, z: 5, scale: 0.4, seed: 1 }, { x: 15, z: 6, scale: 0.35, seed: 2 }, { x: 25, z: 4.5, scale: 0.38, seed: 3 }, ]; const cows = [ { x: 5, z: 5.5, scale: 0.45, seed: 10 }, { x: 20, z: 7, scale: 0.4, seed: 11 }, ]; const trees = [ { x: -15, z: 6, scale: 1.2 }, { x: 30, z: 8, scale: 1 }, { x: -25, z: 12, scale: 0.8 }, ]; const clouds = [ { x: -15, y: 15, z: -30, scale: 3, speed: 0.6 }, { x: 10, y: 18, z: -35, scale: 2.5, speed: 0.42 }, { x: 35, y: 14, z: -28, scale: 2, speed: 0.3 }, ]; return { grassBlades, fencePosts, trackTies, steamParticles, dustParticles, airParticles, sheep, cows, trees, clouds }; }, []); React.useEffect(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; const width = 1920; const height = 1080; // Optimized renderer const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance', }); renderer.setSize(width, height); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.15; // Scene const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0xc5dbe8, 0.015); // Camera const camera = new THREE.PerspectiveCamera(48, width / height, 0.1, 500); const trainProgress = interpolate(time, [0, 5], [-25, 35], { easing: EASINGS.gentle, extrapolateRight: 'clamp', }); const cameraWobble = Math.sin(time * 1.5) * 0.08; camera.position.set(trainProgress - 7, 3.8 + cameraWobble, 11); camera.lookAt(trainProgress + 2, 1.8, 0); // ========================================================================= // LIGHTING (Simplified) // ========================================================================= const ambientLight = new THREE.AmbientLight(0x9bb8d4, 0.5); scene.add(ambientLight); const sunLight = new THREE.DirectionalLight(0xfffaea, 1.3); sunLight.position.set(35, 50, 25); sunLight.castShadow = true; sunLight.shadow.mapSize.width = 2048; sunLight.shadow.mapSize.height = 2048; sunLight.shadow.camera.near = 1; sunLight.shadow.camera.far = 150; sunLight.shadow.camera.left = -50; sunLight.shadow.camera.right = 50; sunLight.shadow.camera.top = 50; sunLight.shadow.camera.bottom = -50; sunLight.shadow.bias = -0.0002; scene.add(sunLight); const fillLight = new THREE.HemisphereLight(0x87ceeb, 0x4a7c4a, 0.35); scene.add(fillLight); // ========================================================================= // SKY (Simplified shader) // ========================================================================= const skyGeo = new THREE.SphereGeometry(120, 32, 32); const skyMat = new THREE.ShaderMaterial({ uniforms: { topColor: { value: new THREE.Color(0x7ec8e3) }, bottomColor: { value: new THREE.Color(0xd9eaf3) }, }, vertexShader: ` varying vec3 vWorldPosition; void main() { vec4 worldPosition = modelMatrix * vec4(position, 1.0); vWorldPosition = worldPosition.xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec3 topColor; uniform vec3 bottomColor; varying vec3 vWorldPosition; void main() { float h = normalize(vWorldPosition).y; vec3 skyColor = mix(bottomColor, topColor, max(h * 0.8, 0.0)); gl_FragColor = vec4(skyColor, 1.0); } `, side: THREE.BackSide, }); scene.add(new THREE.Mesh(skyGeo, skyMat)); // ========================================================================= // GROUND // ========================================================================= const groundGeo = new THREE.PlaneGeometry(200, 200); const groundMat = new THREE.MeshStandardMaterial({ color: COLORS.grass, roughness: 0.9, }); const ground = new THREE.Mesh(groundGeo, groundMat); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground); // ========================================================================= // HILLS // ========================================================================= const createHill = (x: number, z: number, w: number, h: number, d: number, color: number) => { const hillGeo = new THREE.SphereGeometry(1, 32, 32); hillGeo.scale(w, h, d); const hillMat = new THREE.MeshStandardMaterial({ color, roughness: 0.85 }); const hill = new THREE.Mesh(hillGeo, hillMat); hill.position.set(x, h * 0.3, z); hill.castShadow = true; hill.receiveShadow = true; return hill; }; scene.add(createHill(-20, -35, 25, 12, 15, COLORS.hillFar)); scene.add(createHill(15, -40, 30, 15, 18, COLORS.hillFar)); scene.add(createHill(50, -35, 22, 10, 14, COLORS.hillFar)); scene.add(createHill(-5, -25, 20, 8, 12, COLORS.hillMid)); scene.add(createHill(30, -22, 18, 7, 10, COLORS.hillMid)); scene.add(createHill(-30, -20, 15, 6, 10, COLORS.hillMid)); scene.add(createHill(10, -15, 12, 5, 8, COLORS.hillNear)); // ========================================================================= // INSTANCED GRASS - 4000 blades (optimized) // ========================================================================= const grassGeometry = new THREE.BufferGeometry(); const grassVertices = new Float32Array([ -0.015, 0, 0, 0.015, 0, 0, 0.01, 0.6, 0, -0.01, 0.6, 0, 0, 1, 0, ]); const grassIndices = [0, 1, 2, 0, 2, 3, 3, 2, 4]; grassGeometry.setAttribute('position', new THREE.BufferAttribute(grassVertices, 3)); grassGeometry.setIndex(grassIndices); grassGeometry.computeVertexNormals(); const grassMaterial = new THREE.MeshStandardMaterial({ color: COLORS.grass, roughness: 0.8, side: THREE.DoubleSide, }); const grassCount = sceneData.grassBlades.length; const grassMesh = new THREE.InstancedMesh(grassGeometry, grassMaterial, grassCount); grassMesh.castShadow = true; grassMesh.receiveShadow = true; const dummy = new THREE.Object3D(); sceneData.grassBlades.forEach((blade, i) => { const distFromTrain = blade.x - trainProgress; const windEffect = distFromTrain > -3 && distFromTrain < 6 ? Math.sin(time * 12 - distFromTrain * 0.8) * 0.5 * Math.max(0, 1 - Math.abs(distFromTrain) / 6) : 0; const naturalSway = Math.sin(time * 2.5 + blade.x * 0.3 + blade.z * 0.2) * 0.08; dummy.position.set(blade.x, 0, blade.z); dummy.rotation.set(windEffect + naturalSway, blade.rotation, 0); dummy.scale.set(1, blade.height * 4, 1); dummy.updateMatrix(); grassMesh.setMatrixAt(i, dummy.matrix); }); grassMesh.instanceMatrix.needsUpdate = true; scene.add(grassMesh); // ========================================================================= // TRACKS // ========================================================================= const trackBedGeo = new THREE.BoxGeometry(120, 0.25, 2.8); const trackBedMat = new THREE.MeshStandardMaterial({ color: 0x8b7355, roughness: 0.95 }); const trackBed = new THREE.Mesh(trackBedGeo, trackBedMat); trackBed.position.set(0, 0.12, 0); trackBed.receiveShadow = true; scene.add(trackBed); sceneData.trackTies.forEach((tie) => { const tieGeo = new THREE.BoxGeometry(0.25, 0.12, 2.2); const tieMat = new THREE.MeshStandardMaterial({ color: COLORS.railTie, roughness: 0.9 }); const tieMesh = new THREE.Mesh(tieGeo, tieMat); tieMesh.position.set(tie.x, 0.3, 0); tieMesh.receiveShadow = true; scene.add(tieMesh); }); const railGeo = new THREE.BoxGeometry(120, 0.1, 0.08); const railMat = new THREE.MeshStandardMaterial({ color: COLORS.rail, roughness: 0.3, metalness: 0.7 }); scene.add(new THREE.Mesh(railGeo, railMat).translateY(0.42).translateZ(0.55)); scene.add(new THREE.Mesh(railGeo.clone(), railMat).translateY(0.42).translateZ(-0.55)); // ========================================================================= // FENCE // ========================================================================= sceneData.fencePosts.forEach((post) => { const postGeo = new THREE.CylinderGeometry(0.07, 0.09, 1.1, 6); const postMat = new THREE.MeshStandardMaterial({ color: COLORS.fenceWood, roughness: 0.85 }); const postMesh = new THREE.Mesh(postGeo, postMat); postMesh.position.set(post.x, 0.55, post.z); postMesh.castShadow = true; scene.add(postMesh); }); for (const z of [3, -8]) { for (const h of [0.35, 0.75]) { const fenceRailGeo = new THREE.BoxGeometry(100, 0.07, 0.05); const fenceRailMat = new THREE.MeshStandardMaterial({ color: 0xa67c00, roughness: 0.8 }); const fenceRail = new THREE.Mesh(fenceRailGeo, fenceRailMat); fenceRail.position.set(0, h, z); scene.add(fenceRail); } } // ========================================================================= // ANIMALS (Simplified) // ========================================================================= const createSheep = (x: number, z: number, scale: number, seed: number) => { const group = new THREE.Group(); const bodyMat = new THREE.MeshStandardMaterial({ color: COLORS.sheep, roughness: 0.95 }); const headMat = new THREE.MeshStandardMaterial({ color: COLORS.sheepHead, roughness: 0.7 }); const body = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 12), bodyMat); body.scale.set(1.1, 0.85, 0.85); body.position.y = 0.65; body.castShadow = true; group.add(body); // Fluffy bumps for (let i = 0; i < 5; i++) { const bump = new THREE.Mesh(new THREE.SphereGeometry(0.35, 8, 8), bodyMat); const angle = (i / 5) * Math.PI * 2; bump.position.set(Math.cos(angle) * 0.5, 0.65, Math.sin(angle) * 0.4); group.add(bump); } const head = new THREE.Mesh(new THREE.SphereGeometry(0.3, 10, 10), headMat); head.position.set(1, 0.55, 0); head.castShadow = true; group.add(head); // Legs const legGeo = new THREE.CylinderGeometry(0.06, 0.06, 0.4, 6); [[0.4, 0.2, 0.25], [0.4, 0.2, -0.25], [-0.4, 0.2, 0.25], [-0.4, 0.2, -0.25]].forEach(([lx, ly, lz]) => { const leg = new THREE.Mesh(legGeo, headMat); leg.position.set(lx, ly, lz); group.add(leg); }); const bob = Math.sin((time + seed * 0.5) * 4) * 0.04; group.position.set(x, bob, z); group.scale.setScalar(scale); group.rotation.y = seededRandom(seed) * Math.PI * 2; return group; }; const createCow = (x: number, z: number, scale: number, seed: number) => { const group = new THREE.Group(); const bodyMat = new THREE.MeshStandardMaterial({ color: 0xfafafa, roughness: 0.8 }); const spotMat = new THREE.MeshStandardMaterial({ color: COLORS.sheepHead, roughness: 0.8 }); const body = new THREE.Mesh(new THREE.SphereGeometry(1, 12, 12), bodyMat); body.scale.set(1.3, 0.95, 0.95); body.position.y = 0.85; body.castShadow = true; group.add(body); // Spots [[0.25, 1, 0.45, 0.25], [-0.15, 0.75, 0.55, 0.2]].forEach(([sx, sy, sz, sr]) => { const spot = new THREE.Mesh(new THREE.SphereGeometry(sr as number, 8, 8), spotMat); spot.position.set(sx as number, sy as number, sz as number); group.add(spot); }); const head = new THREE.Mesh(new THREE.SphereGeometry(0.35, 10, 10), bodyMat); head.position.set(1.2, 0.85, 0); head.castShadow = true; group.add(head); // Legs const legGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.6, 6); [[0.5, 0.3, 0.35], [0.5, 0.3, -0.35], [-0.5, 0.3, 0.35], [-0.5, 0.3, -0.35]].forEach(([lx, ly, lz]) => { const leg = new THREE.Mesh(legGeo, bodyMat); leg.position.set(lx, ly, lz); group.add(leg); }); const bob = Math.sin((time + seed * 0.6) * 3.2) * 0.025; group.position.set(x, bob, z); group.scale.setScalar(scale); group.rotation.y = seededRandom(seed + 5) * Math.PI * 2; return group; }; sceneData.sheep.forEach((s) => scene.add(createSheep(s.x, s.z, s.scale, s.seed))); sceneData.cows.forEach((c) => scene.add(createCow(c.x, c.z, c.scale, c.seed))); // ========================================================================= // TREES // ========================================================================= const createTree = (x: number, z: number, scale: number) => { const group = new THREE.Group(); const trunkGeo = new THREE.CylinderGeometry(0.12, 0.22, 1.4, 6); const trunkMat = new THREE.MeshStandardMaterial({ color: 0x5d4037, roughness: 0.9 }); const trunk = new THREE.Mesh(trunkGeo, trunkMat); trunk.position.y = 0.7; trunk.castShadow = true; group.add(trunk); const foliageMat = new THREE.MeshStandardMaterial({ color: 0x2e7d32, roughness: 0.85 }); [{ y: 2.6, r: 0.7, h: 1.1 }, { y: 2, r: 1, h: 1.3 }, { y: 1.4, r: 1.3, h: 1.5 }].forEach((l) => { const cone = new THREE.Mesh(new THREE.ConeGeometry(l.r, l.h, 6), foliageMat); cone.position.y = l.y; cone.castShadow = true; group.add(cone); }); group.position.set(x, 0, z); group.scale.setScalar(scale); return group; }; sceneData.trees.forEach((t) => scene.add(createTree(t.x, t.z, t.scale))); // ========================================================================= // TRAIN // ========================================================================= const trainGroup = new THREE.Group(); const wheelRotation = time * 10; const createWheel = (x: number, y: number, z: number, radius: number = 0.32) => { const wheelGroup = new THREE.Group(); const wheelMat = new THREE.MeshStandardMaterial({ color: COLORS.wheel, roughness: 0.4, metalness: 0.6 }); const wheel = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, 0.12, 16), wheelMat); wheel.rotation.x = Math.PI / 2; wheel.castShadow = true; wheelGroup.add(wheel); const hub = new THREE.Mesh( new THREE.CylinderGeometry(radius * 0.35, radius * 0.35, 0.14, 12), new THREE.MeshStandardMaterial({ color: 0x37474f, metalness: 0.6 }) ); hub.rotation.x = Math.PI / 2; wheelGroup.add(hub); for (let i = 0; i < 5; i++) { const spoke = new THREE.Mesh( new THREE.BoxGeometry(0.03, radius * 1.5, 0.015), wheelMat ); spoke.rotation.z = (i / 5) * Math.PI; spoke.rotation.x = Math.PI / 2; wheelGroup.add(spoke); } wheelGroup.position.set(x, y, z); wheelGroup.rotation.z = wheelRotation; return wheelGroup; }; // Coal car const coalCar = new THREE.Group(); const carBody = new THREE.Mesh( new THREE.BoxGeometry(1.6, 0.8, 1.1), new THREE.MeshStandardMaterial({ color: COLORS.trainBlue, roughness: 0.6 }) ); carBody.position.set(-3.3, 1, 0); carBody.castShadow = true; coalCar.add(carBody); const coal = new THREE.Mesh( new THREE.SphereGeometry(0.6, 10, 8), new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.95 }) ); coal.scale.set(1.1, 0.45, 0.75); coal.position.set(-3.3, 1.45, 0); coalCar.add(coal); coalCar.add(createWheel(-3.9, 0.38, 0.5, 0.28)); coalCar.add(createWheel(-3.9, 0.38, -0.5, 0.28)); coalCar.add(createWheel(-2.7, 0.38, 0.5, 0.28)); coalCar.add(createWheel(-2.7, 0.38, -0.5, 0.28)); trainGroup.add(coalCar); // Connector const connector = new THREE.Mesh( new THREE.BoxGeometry(0.5, 0.12, 0.12), new THREE.MeshStandardMaterial({ color: 0x455a64, metalness: 0.5 }) ); connector.position.set(-2.2, 0.85, 0); trainGroup.add(connector); // Locomotive const locomotive = new THREE.Group(); const base = new THREE.Mesh( new THREE.BoxGeometry(3.2, 0.25, 1.3), new THREE.MeshStandardMaterial({ color: 0x37474f, roughness: 0.5 }) ); base.position.set(0.4, 0.6, 0); base.castShadow = true; locomotive.add(base); const boiler = new THREE.Mesh( new THREE.CylinderGeometry(0.65, 0.65, 2, 20), new THREE.MeshStandardMaterial({ color: COLORS.trainOrange, roughness: 0.4, metalness: 0.3 }) ); boiler.rotation.z = Math.PI / 2; boiler.position.set(1.1, 1.4, 0); boiler.castShadow = true; locomotive.add(boiler); const boilerFront = new THREE.Mesh( new THREE.SphereGeometry(0.65, 16, 16, 0, Math.PI), new THREE.MeshStandardMaterial({ color: COLORS.trainOrange, roughness: 0.4, metalness: 0.3 }) ); boilerFront.rotation.y = -Math.PI / 2; boilerFront.position.set(2.1, 1.4, 0); boilerFront.castShadow = true; locomotive.add(boilerFront); // Yellow bands const bandMat = new THREE.MeshStandardMaterial({ color: COLORS.trainYellow, roughness: 0.3, metalness: 0.5 }); [0.3, 0.8, 1.3, 1.8].forEach((bx) => { const band = new THREE.Mesh(new THREE.TorusGeometry(0.67, 0.05, 6, 20), bandMat); band.rotation.y = Math.PI / 2; band.position.set(bx, 1.4, 0); locomotive.add(band); }); // Cabin const cabin = new THREE.Mesh( new THREE.BoxGeometry(1.1, 1.2, 1.2), new THREE.MeshStandardMaterial({ color: COLORS.trainRed, roughness: 0.5 }) ); cabin.position.set(-0.65, 1.45, 0); cabin.castShadow = true; locomotive.add(cabin); const roof = new THREE.Mesh( new THREE.BoxGeometry(1.25, 0.12, 1.35), new THREE.MeshStandardMaterial({ color: 0xb71c1c, roughness: 0.6 }) ); roof.position.set(-0.65, 2.1, 0); locomotive.add(roof); const windowMesh = new THREE.Mesh( new THREE.BoxGeometry(0.04, 0.45, 0.55), new THREE.MeshStandardMaterial({ color: 0x4fc3f7, roughness: 0.1, transparent: true, opacity: 0.8 }) ); windowMesh.position.set(-0.08, 1.4, 0); locomotive.add(windowMesh); // Chimney const chimney = new THREE.Mesh( new THREE.CylinderGeometry(0.18, 0.22, 0.6, 10), new THREE.MeshStandardMaterial({ color: COLORS.chimney, roughness: 0.4, metalness: 0.5 }) ); chimney.position.set(1.7, 2.35, 0); chimney.castShadow = true; locomotive.add(chimney); const chimneyTop = new THREE.Mesh( new THREE.CylinderGeometry(0.25, 0.2, 0.12, 10), new THREE.MeshStandardMaterial({ color: COLORS.chimney, roughness: 0.4, metalness: 0.5 }) ); chimneyTop.position.set(1.7, 2.7, 0); locomotive.add(chimneyTop); // Dome const dome = new THREE.Mesh( new THREE.SphereGeometry(0.25, 12, 12, 0, Math.PI * 2, 0, Math.PI / 2), new THREE.MeshStandardMaterial({ color: COLORS.trainOrange, roughness: 0.4 }) ); dome.position.set(0.5, 2.05, 0); locomotive.add(dome); // Wheels locomotive.add(createWheel(-0.35, 0.38, 0.6, 0.35)); locomotive.add(createWheel(-0.35, 0.38, -0.6, 0.35)); locomotive.add(createWheel(0.55, 0.38, 0.6, 0.35)); locomotive.add(createWheel(0.55, 0.38, -0.6, 0.35)); locomotive.add(createWheel(1.45, 0.38, 0.6, 0.35)); locomotive.add(createWheel(1.45, 0.38, -0.6, 0.35)); // Connecting rods const rodMat = new THREE.MeshStandardMaterial({ color: 0x78909c, roughness: 0.3, metalness: 0.7 }); const pistonOffset = Math.sin(time * 10) * 0.08; const rod1 = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.06, 0.06), rodMat); rod1.position.set(0.55 + pistonOffset, 0.38, 0.7); locomotive.add(rod1); const rod2 = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.06, 0.06), rodMat); rod2.position.set(0.55 + pistonOffset, 0.38, -0.7); locomotive.add(rod2); // Cow catcher for (let i = 0; i < 4; i++) { const bar = new THREE.Mesh( new THREE.BoxGeometry(0.03, 0.4, 0.03), new THREE.MeshStandardMaterial({ color: 0x37474f }) ); bar.position.set(2.3 + i * 0.07, 0.35, (i - 1.5) * 0.18); bar.rotation.z = -0.35; locomotive.add(bar); } trainGroup.add(locomotive); // ========================================================================= // STEAM PARTICLES (Optimized) // ========================================================================= const steamMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, }); sceneData.steamParticles.forEach((p) => { const cycleDuration = 2.2; const cycleTime = ((time * p.speed + p.delay) % cycleDuration) / cycleDuration; const turbX = Math.sin(cycleTime * Math.PI * 3 + p.delay * 8) * 0.25 * cycleTime; const turbZ = Math.cos(cycleTime * Math.PI * 2.5 + p.delay * 6) * 0.15 * cycleTime; const steamY = cycleTime * 4; const steamScale = 0.12 + cycleTime * p.size * 1.2; const steamOpacity = Math.sin(cycleTime * Math.PI) * 0.45; if (steamOpacity > 0.02) { const steamGeo = new THREE.SphereGeometry(steamScale, 8, 8); const particleMat = steamMat.clone(); particleMat.opacity = steamOpacity; const particle = new THREE.Mesh(steamGeo, particleMat); particle.position.set( 1.7 + p.offsetX * cycleTime + turbX, 2.85 + steamY, p.offsetZ + turbZ ); trainGroup.add(particle); } }); // ========================================================================= // DUST PARTICLES (Optimized) // ========================================================================= const dustMat = new THREE.MeshBasicMaterial({ color: COLORS.dust, transparent: true, }); sceneData.dustParticles.forEach((p) => { const cycleDuration = 1; const cycleTime = ((time * 1.8 + p.delay) % cycleDuration) / cycleDuration; const dustX = -p.offsetX * cycleTime * 2.5; const dustY = Math.sin(cycleTime * Math.PI) * 0.6; const dustZ = p.side * (0.7 + cycleTime * 0.4); const dustOpacity = (1 - cycleTime) * 0.35; if (dustOpacity > 0.04) { const dustGeo = new THREE.SphereGeometry(p.size * (1 + cycleTime * 0.8), 6, 6); const pMat = dustMat.clone(); pMat.opacity = dustOpacity; const particle = new THREE.Mesh(dustGeo, pMat); particle.position.set(-0.4 + dustX, 0.25 + dustY, dustZ); trainGroup.add(particle); } }); // Position train with subtle bounce const bounce = Math.sin(time * 12) * 0.012; trainGroup.position.set(trainProgress, bounce, 0); scene.add(trainGroup); // ========================================================================= // FLOATING AIR PARTICLES (Dust motes in sunlight) // ========================================================================= sceneData.airParticles.forEach((p) => { const floatY = Math.sin(time * p.speed + p.x) * 0.2; const driftX = Math.sin(time * 0.4 + p.y) * 0.3; const particle = new THREE.Mesh( new THREE.SphereGeometry(p.size, 4, 4), new THREE.MeshBasicMaterial({ color: 0xffffee, transparent: true, opacity: 0.25 }) ); particle.position.set(p.x + driftX, p.y + floatY, p.z); scene.add(particle); }); // ========================================================================= // CLOUDS // ========================================================================= const createCloud = (x: number, y: number, z: number, scale: number) => { const group = new THREE.Group(); const cloudMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 1, transparent: true, opacity: 0.92, }); [[0, 0, 0, 1], [-0.7, 0.15, 0, 0.65], [0.7, 0.1, 0, 0.7], [0.25, 0.35, 0.15, 0.5], [-0.25, 0.25, -0.15, 0.55]].forEach(([cx, cy, cz, cr]) => { const part = new THREE.Mesh(new THREE.SphereGeometry(cr as number, 10, 10), cloudMat); part.position.set(cx as number, cy as number, cz as number); group.add(part); }); group.position.set(x, y, z); group.scale.setScalar(scale); return group; }; sceneData.clouds.forEach((c) => { scene.add(createCloud(c.x + time * c.speed, c.y, c.z, c.scale)); }); // ========================================================================= // RENDER // ========================================================================= renderer.render(scene, camera); return () => { renderer.dispose(); }; }, [frame, durationInFrames, fps, time, sceneData]); return ( ); }; // ============================================================================= // MAIN COMPONENT // ============================================================================= const TrainCountryside3DOptimized: React.FC = () => { const frame = useCurrentFrame(); const { durationInFrames, fps } = useVideoConfig(); return ( ); }; export default TrainCountryside3DOptimized;