import React, { useMemo } from 'react'; import { useCurrentFrame, useVideoConfig, interpolate, Easing, AbsoluteFill, } from 'remotion'; import * as THREE from 'three'; // ============================================================================= // COMPOSITION CONFIG // ============================================================================= export const compositionConfig = { id: 'RocketLaunchThroughClouds', durationInSeconds: 5, fps: 60, width: 1920, height: 1080, }; // ============================================================================= // STYLE CONSTANTS // ============================================================================= const COLORS = { rocketBody: 0xf5f5f5, rocketNose: 0xe63946, rocketFins: 0xe63946, rocketStripe: 0x1d3557, flameCore: 0xffdd44, flameOuter: 0xff6600, flameTip: 0xff2200, cloudWhite: 0xffffff, cloudGray: 0xe8e8e8, skyTop: 0x0a0a20, skyMid: 0x1a3a5c, skyBottom: 0x87ceeb, stars: 0xffffff, } as const; const EASINGS = { gentle: Easing.bezier(0.25, 0.1, 0.25, 1), easeOut: Easing.bezier(0.33, 1, 0.68, 1), easeInOut: Easing.bezier(0.37, 0, 0.63, 1), accelerate: Easing.bezier(0.4, 0, 1, 1), }; // ============================================================================= // SEEDED RANDOM // ============================================================================= const seededRandom = (seed: number): number => { const x = Math.sin(seed * 9999) * 10000; return x - Math.floor(x); }; // ============================================================================= // TYPE DEFINITIONS // ============================================================================= interface CloudData { x: number; y: number; z: number; scale: number; spheres: { cx: number; cy: number; cz: number; cr: number }[]; } interface StarData { x: number; y: number; z: number; size: number; brightness: number; } interface ExhaustParticle { offsetX: number; offsetZ: number; size: number; delay: number; speed: number; colorVariant: number; } interface SmokeParticle { offsetX: number; offsetZ: number; size: number; delay: number; drift: number; } interface SparkParticle { angle: number; speed: number; size: number; delay: number; life: number; } // ============================================================================= // THREE.JS SCENE COMPONENT // ============================================================================= const ThreeScene: React.FC<{ frame: number; fps: number; durationInFrames: number }> = ({ frame, fps, durationInFrames, }) => { const canvasRef = React.useRef(null); const time = frame / fps; const sceneData = useMemo(() => { // Generate cloud layers const cloudLayers: CloudData[][] = []; // Layer 1: Low clouds (thick, close) const layer1: CloudData[] = []; for (let i = 0; i < 15; i++) { const sphereCount = 5 + Math.floor(seededRandom(i * 100) * 4); const spheres: { cx: number; cy: number; cz: number; cr: number }[] = []; for (let j = 0; j < sphereCount; j++) { spheres.push({ cx: (seededRandom(i * 100 + j * 10 + 1) - 0.5) * 3, cy: (seededRandom(i * 100 + j * 10 + 2) - 0.3) * 1.5, cz: (seededRandom(i * 100 + j * 10 + 3) - 0.5) * 2, cr: 0.8 + seededRandom(i * 100 + j * 10 + 4) * 0.8, }); } layer1.push({ x: seededRandom(i * 1.1) * 60 - 30, y: -5 + seededRandom(i * 2.2) * 8, z: seededRandom(i * 3.3) * 40 - 20, scale: 2 + seededRandom(i * 4.4) * 2, spheres, }); } cloudLayers.push(layer1); // Layer 2: Mid clouds const layer2: CloudData[] = []; for (let i = 0; i < 20; i++) { const sphereCount = 4 + Math.floor(seededRandom(i * 200) * 3); const spheres: { cx: number; cy: number; cz: number; cr: number }[] = []; for (let j = 0; j < sphereCount; j++) { spheres.push({ cx: (seededRandom(i * 200 + j * 10 + 1) - 0.5) * 2.5, cy: (seededRandom(i * 200 + j * 10 + 2) - 0.3) * 1.2, cz: (seededRandom(i * 200 + j * 10 + 3) - 0.5) * 1.8, cr: 0.6 + seededRandom(i * 200 + j * 10 + 4) * 0.6, }); } layer2.push({ x: seededRandom(i * 5.5) * 80 - 40, y: 15 + seededRandom(i * 6.6) * 15, z: seededRandom(i * 7.7) * 50 - 25, scale: 1.5 + seededRandom(i * 8.8) * 1.5, spheres, }); } cloudLayers.push(layer2); // Layer 3: High wispy clouds const layer3: CloudData[] = []; for (let i = 0; i < 12; i++) { const sphereCount = 3 + Math.floor(seededRandom(i * 300) * 2); const spheres: { cx: number; cy: number; cz: number; cr: number }[] = []; for (let j = 0; j < sphereCount; j++) { spheres.push({ cx: (seededRandom(i * 300 + j * 10 + 1) - 0.5) * 4, cy: (seededRandom(i * 300 + j * 10 + 2) - 0.5) * 0.8, cz: (seededRandom(i * 300 + j * 10 + 3) - 0.5) * 1.5, cr: 0.5 + seededRandom(i * 300 + j * 10 + 4) * 0.5, }); } layer3.push({ x: seededRandom(i * 9.9) * 100 - 50, y: 40 + seededRandom(i * 10.1) * 20, z: seededRandom(i * 11.2) * 60 - 30, scale: 1 + seededRandom(i * 12.3) * 1, spheres, }); } cloudLayers.push(layer3); // Generate stars const stars: StarData[] = []; for (let i = 0; i < 300; i++) { stars.push({ x: seededRandom(i * 1000 + 1) * 200 - 100, y: seededRandom(i * 1000 + 2) * 150 + 30, z: seededRandom(i * 1000 + 3) * 200 - 100, size: 0.1 + seededRandom(i * 1000 + 4) * 0.3, brightness: 0.5 + seededRandom(i * 1000 + 5) * 0.5, }); } // Exhaust particles const exhaustParticles: ExhaustParticle[] = []; for (let i = 0; i < 50; i++) { exhaustParticles.push({ offsetX: seededRandom(i * 500 + 1) * 0.8 - 0.4, offsetZ: seededRandom(i * 500 + 2) * 0.8 - 0.4, size: 0.15 + seededRandom(i * 500 + 3) * 0.25, delay: seededRandom(i * 500 + 4) * 2, speed: 0.8 + seededRandom(i * 500 + 5) * 0.6, colorVariant: seededRandom(i * 500 + 6), }); } // Smoke trail particles const smokeParticles: SmokeParticle[] = []; for (let i = 0; i < 40; i++) { smokeParticles.push({ offsetX: seededRandom(i * 600 + 1) * 0.6 - 0.3, offsetZ: seededRandom(i * 600 + 2) * 0.6 - 0.3, size: 0.3 + seededRandom(i * 600 + 3) * 0.4, delay: seededRandom(i * 600 + 4) * 3, drift: seededRandom(i * 600 + 5) * 2 - 1, }); } // Spark particles const sparkParticles: SparkParticle[] = []; for (let i = 0; i < 30; i++) { sparkParticles.push({ angle: seededRandom(i * 700 + 1) * Math.PI * 2, speed: 2 + seededRandom(i * 700 + 2) * 4, size: 0.03 + seededRandom(i * 700 + 3) * 0.05, delay: seededRandom(i * 700 + 4) * 1.5, life: 0.3 + seededRandom(i * 700 + 5) * 0.4, }); } return { cloudLayers, stars, exhaustParticles, smokeParticles, sparkParticles }; }, []); React.useEffect(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; const width = 1920; const height = 1080; // RENDERER SETUP 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.2; // SCENE const scene = new THREE.Scene(); // Animated fog density based on altitude const rocketY = interpolate(time, [0, 0.5, 3, 5], [-8, 0, 50, 120], { easing: EASINGS.accelerate, extrapolateRight: 'clamp', }); const fogDensity = interpolate(rocketY, [0, 30, 60], [0.025, 0.015, 0.005], { extrapolateRight: 'clamp', }); const fogColor = new THREE.Color().lerpColors( new THREE.Color(0x87ceeb), new THREE.Color(0x0a0a20), interpolate(rocketY, [0, 60], [0, 1], { extrapolateRight: 'clamp' }) ); scene.fog = new THREE.FogExp2(fogColor.getHex(), fogDensity); // CAMERA const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 500); // Camera follows rocket with dynamic framing const cameraDistance = interpolate(time, [0, 1, 4, 5], [12, 10, 8, 10], { easing: EASINGS.gentle, extrapolateRight: 'clamp', }); const cameraAngle = interpolate(time, [0, 2, 5], [0.3, 0.15, 0.1], { easing: EASINGS.gentle, extrapolateRight: 'clamp', }); const wobbleX = Math.sin(time * 3) * 0.15; const wobbleY = Math.cos(time * 2.5) * 0.1; camera.position.set( Math.sin(cameraAngle) * cameraDistance + wobbleX, rocketY + 2 + wobbleY, Math.cos(cameraAngle) * cameraDistance ); camera.lookAt(0, rocketY + 1, 0); // SKY GRADIENT (transitions from blue to space) const skyGeo = new THREE.SphereGeometry(200, 32, 32); const spaceTransition = interpolate(rocketY, [0, 40, 80], [0, 0.5, 1], { extrapolateRight: 'clamp', }); const skyMat = new THREE.ShaderMaterial({ uniforms: { topColor: { value: new THREE.Color(COLORS.skyTop) }, midColor: { value: new THREE.Color(COLORS.skyMid).lerp(new THREE.Color(COLORS.skyTop), spaceTransition * 0.7) }, bottomColor: { value: new THREE.Color(COLORS.skyBottom).lerp(new THREE.Color(COLORS.skyMid), spaceTransition) }, offset: { value: 0.4 }, }, 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 midColor; uniform vec3 bottomColor; uniform float offset; varying vec3 vWorldPosition; void main() { float h = normalize(vWorldPosition).y; vec3 color; if (h > offset) { color = mix(midColor, topColor, (h - offset) / (1.0 - offset)); } else { color = mix(bottomColor, midColor, (h + 1.0) / (offset + 1.0)); } gl_FragColor = vec4(color, 1.0); } `, side: THREE.BackSide, }); scene.add(new THREE.Mesh(skyGeo, skyMat)); // STARS (fade in as we enter space) const starOpacity = interpolate(rocketY, [20, 50], [0, 1], { extrapolateRight: 'clamp', }); if (starOpacity > 0.01) { sceneData.stars.forEach((star) => { const twinkle = 0.7 + Math.sin(time * 5 + star.x + star.y) * 0.3; const starMat = new THREE.MeshBasicMaterial({ color: COLORS.stars, transparent: true, opacity: starOpacity * star.brightness * twinkle, }); const starMesh = new THREE.Mesh( new THREE.SphereGeometry(star.size, 4, 4), starMat ); starMesh.position.set(star.x, star.y, star.z); scene.add(starMesh); }); } // LIGHTING const ambientLight = new THREE.AmbientLight(0x6688aa, 0.4); scene.add(ambientLight); // Sun light (from above-left) const sunLight = new THREE.DirectionalLight(0xfffaea, 1.4); sunLight.position.set(30, 80, 25); sunLight.castShadow = true; sunLight.shadow.mapSize.width = 2048; sunLight.shadow.mapSize.height = 2048; sunLight.shadow.camera.near = 1; sunLight.shadow.camera.far = 200; sunLight.shadow.camera.left = -30; sunLight.shadow.camera.right = 30; sunLight.shadow.camera.top = 100; sunLight.shadow.camera.bottom = -30; sunLight.shadow.bias = -0.0002; scene.add(sunLight); // Hemisphere light for sky bounce const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x444444, 0.3); scene.add(hemiLight); // Exhaust glow light const exhaustLight = new THREE.PointLight(0xff6600, 2, 15); exhaustLight.position.set(0, rocketY - 2, 0); scene.add(exhaustLight); // BUILD ROCKET const rocketGroup = new THREE.Group(); // Rocket body (main cylinder) const bodyMat = new THREE.MeshStandardMaterial({ color: COLORS.rocketBody, roughness: 0.3, metalness: 0.1, }); const bodyGeo = new THREE.CylinderGeometry(0.5, 0.6, 4, 16); const body = new THREE.Mesh(bodyGeo, bodyMat); body.position.y = 2; body.castShadow = true; rocketGroup.add(body); // Nose cone const noseMat = new THREE.MeshStandardMaterial({ color: COLORS.rocketNose, roughness: 0.4, metalness: 0.2, }); const noseGeo = new THREE.ConeGeometry(0.5, 1.5, 16); const nose = new THREE.Mesh(noseGeo, noseMat); nose.position.y = 4.75; nose.castShadow = true; rocketGroup.add(nose); // Stripe around body const stripeMat = new THREE.MeshStandardMaterial({ color: COLORS.rocketStripe, roughness: 0.5, }); const stripeGeo = new THREE.CylinderGeometry(0.52, 0.54, 0.3, 16); const stripe = new THREE.Mesh(stripeGeo, stripeMat); stripe.position.y = 3.2; rocketGroup.add(stripe); // Window const windowMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.3, emissive: 0x224466, emissiveIntensity: 0.3, }); const windowGeo = new THREE.SphereGeometry(0.2, 12, 12); const window1 = new THREE.Mesh(windowGeo, windowMat); window1.position.set(0.45, 3, 0); window1.scale.set(1, 1.3, 0.3); rocketGroup.add(window1); // Fins (4 fins) const finMat = new THREE.MeshStandardMaterial({ color: COLORS.rocketFins, roughness: 0.5, metalness: 0.1, }); for (let i = 0; i < 4; i++) { const finShape = new THREE.Shape(); finShape.moveTo(0, 0); finShape.lineTo(0.8, 0); finShape.lineTo(0.3, 1.2); finShape.lineTo(0, 1.2); finShape.lineTo(0, 0); const finGeo = new THREE.ExtrudeGeometry(finShape, { depth: 0.08, bevelEnabled: false, }); const fin = new THREE.Mesh(finGeo, finMat); fin.rotation.y = (i / 4) * Math.PI * 2; fin.position.y = 0.2; fin.position.x = Math.sin((i / 4) * Math.PI * 2) * 0.55; fin.position.z = Math.cos((i / 4) * Math.PI * 2) * 0.55; fin.castShadow = true; rocketGroup.add(fin); } // Engine nozzle const nozzleMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.3, metalness: 0.8, }); const nozzleGeo = new THREE.CylinderGeometry(0.35, 0.45, 0.5, 12); const nozzle = new THREE.Mesh(nozzleGeo, nozzleMat); nozzle.position.y = -0.1; rocketGroup.add(nozzle); // Rocket shake during launch const shakeIntensity = interpolate(time, [0, 0.5, 2, 4], [0, 0.08, 0.04, 0.01], { extrapolateRight: 'clamp', }); rocketGroup.position.set( Math.sin(time * 40) * shakeIntensity, rocketY, Math.cos(time * 35) * shakeIntensity ); // Slight tilt during ascent const tiltAngle = Math.sin(time * 0.8) * 0.03; rocketGroup.rotation.z = tiltAngle; rocketGroup.rotation.x = Math.cos(time * 0.6) * 0.02; scene.add(rocketGroup); // EXHAUST FLAME const flameGroup = new THREE.Group(); // Core flame (bright yellow/white) const coreFlameMat = new THREE.MeshBasicMaterial({ color: COLORS.flameCore, transparent: true, opacity: 0.95, }); const flameLength = interpolate(time, [0, 0.3, 0.8], [0.5, 2.5, 3], { extrapolateRight: 'clamp', }); // Inner core const coreGeo = new THREE.ConeGeometry(0.25, flameLength * 0.8, 8); const coreFlame = new THREE.Mesh(coreGeo, coreFlameMat); coreFlame.position.y = -flameLength * 0.4; coreFlame.rotation.x = Math.PI; flameGroup.add(coreFlame); // Outer flame layers const flameColors = [COLORS.flameOuter, COLORS.flameTip]; const flameSizes = [0.4, 0.55]; const flameLengths = [1, 1.2]; const flameOpacities = [0.7, 0.4]; flameColors.forEach((color, i) => { const flameMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: flameOpacities[i], }); const flameGeo = new THREE.ConeGeometry( flameSizes[i], flameLength * flameLengths[i], 8 ); const flame = new THREE.Mesh(flameGeo, flameMat); flame.position.y = -flameLength * flameLengths[i] * 0.5; flame.rotation.x = Math.PI; // Flickering effect const flicker = 0.9 + Math.sin(time * 30 + i * 5) * 0.1; flame.scale.set(flicker, 1 + Math.sin(time * 25) * 0.05, flicker); flameGroup.add(flame); }); flameGroup.position.set( rocketGroup.position.x, rocketY - 0.3, rocketGroup.position.z ); scene.add(flameGroup); // EXHAUST PARTICLES (fire particles) sceneData.exhaustParticles.forEach((p) => { const cycleDuration = 0.8; const cycleTime = ((time * p.speed + p.delay) % cycleDuration) / cycleDuration; const particleY = -cycleTime * flameLength * 2.5; const spread = cycleTime * 1.5; const turbX = Math.sin(cycleTime * Math.PI * 6 + p.delay * 10) * spread * 0.4; const turbZ = Math.cos(cycleTime * Math.PI * 5 + p.delay * 8) * spread * 0.3; const particleOpacity = (1 - cycleTime) * 0.8; const particleScale = p.size * (0.5 + cycleTime * 1.5); if (particleOpacity > 0.05 && time > 0.2) { // Color gradient from yellow to red to dark const particleColor = new THREE.Color().lerpColors( new THREE.Color(COLORS.flameCore), new THREE.Color(COLORS.flameTip), cycleTime ); const particleMat = new THREE.MeshBasicMaterial({ color: particleColor, transparent: true, opacity: particleOpacity, }); const particle = new THREE.Mesh( new THREE.SphereGeometry(particleScale, 6, 6), particleMat ); particle.position.set( rocketGroup.position.x + p.offsetX + turbX, rocketY + particleY - 0.5, rocketGroup.position.z + p.offsetZ + turbZ ); scene.add(particle); } }); // SMOKE TRAIL sceneData.smokeParticles.forEach((p) => { const cycleDuration = 3; const cycleTime = ((time * 0.8 + p.delay) % cycleDuration) / cycleDuration; const smokeY = -cycleTime * 25; const spread = cycleTime * 4; const driftX = p.drift * cycleTime * 3; const smokeOpacity = Math.sin(cycleTime * Math.PI) * 0.5 * interpolate(time, [0, 0.5], [0, 1], { extrapolateRight: 'clamp' }); const smokeScale = p.size * (1 + cycleTime * 4); if (smokeOpacity > 0.03) { const smokeMat = new THREE.MeshBasicMaterial({ color: 0xaaaaaa, transparent: true, opacity: smokeOpacity, }); const smoke = new THREE.Mesh( new THREE.SphereGeometry(smokeScale, 8, 8), smokeMat ); smoke.position.set( p.offsetX * spread + driftX, rocketY + smokeY - flameLength, p.offsetZ * spread ); scene.add(smoke); } }); // SPARK PARTICLES (small bright dots flying out) sceneData.sparkParticles.forEach((p) => { const cycleTime = ((time * 2 + p.delay) % p.life) / p.life; if (cycleTime < 1 && time > 0.3) { const sparkX = Math.cos(p.angle) * cycleTime * p.speed; const sparkY = -cycleTime * p.speed * 1.5 - cycleTime * cycleTime * 5; const sparkZ = Math.sin(p.angle) * cycleTime * p.speed; const sparkOpacity = (1 - cycleTime) * 0.9; if (sparkOpacity > 0.1) { const sparkMat = new THREE.MeshBasicMaterial({ color: 0xffff88, transparent: true, opacity: sparkOpacity, }); const spark = new THREE.Mesh( new THREE.SphereGeometry(p.size, 4, 4), sparkMat ); spark.position.set( rocketGroup.position.x + sparkX, rocketY + sparkY - 0.8, rocketGroup.position.z + sparkZ ); scene.add(spark); } } }); // CLOUDS const cloudMat = new THREE.MeshStandardMaterial({ color: COLORS.cloudWhite, roughness: 1, transparent: true, }); sceneData.cloudLayers.forEach((layer, layerIndex) => { layer.forEach((cloud) => { // Clouds move down relative to rocket (rocket going up) const relativeY = cloud.y - rocketY * 0.9; // Only render clouds that are in view if (relativeY > -30 && relativeY < 40) { const cloudGroup = new THREE.Group(); // Calculate opacity based on distance and layer const distanceFromRocket = Math.sqrt( cloud.x * cloud.x + (relativeY) * (relativeY) + cloud.z * cloud.z ); let cloudOpacity = interpolate( distanceFromRocket, [5, 15, 50], [0.4, 0.9, 0.6], { extrapolateRight: 'clamp', extrapolateLeft: 'clamp' } ); // Fade clouds as we enter space cloudOpacity *= interpolate(rocketY, [40, 70], [1, 0], { extrapolateRight: 'clamp', extrapolateLeft: 'clamp', }); if (cloudOpacity > 0.05) { cloud.spheres.forEach((sphere) => { const sphereMat = cloudMat.clone(); sphereMat.opacity = cloudOpacity * (0.7 + sphere.cr * 0.3); const sphereGeo = new THREE.SphereGeometry(sphere.cr, 10, 10); const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat); sphereMesh.position.set(sphere.cx, sphere.cy, sphere.cz); cloudGroup.add(sphereMesh); }); cloudGroup.position.set( cloud.x + Math.sin(time * 0.3 + cloud.x) * 0.5, relativeY, cloud.z ); cloudGroup.scale.setScalar(cloud.scale); scene.add(cloudGroup); } } }); }); // RENDER renderer.render(scene, camera); return () => { renderer.dispose(); }; }, [frame, fps, durationInFrames, time, sceneData]); return ( ); }; // ============================================================================= // MAIN COMPONENT // ============================================================================= const RocketLaunchThroughClouds: React.FC = () => { const frame = useCurrentFrame(); const { durationInFrames, fps } = useVideoConfig(); return ( ); }; export default RocketLaunchThroughClouds;