import './style.css' 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'; import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; // Loading Manager class SceneLoader { constructor() { this.loadingScreen = document.getElementById('loading-screen'); this.loadingText = document.getElementById('loading-text'); this.loadingProgressBar = document.getElementById('loading-progress-bar'); this.loadingPercentage = document.getElementById('loading-percentage'); this.modelsToLoad = [ { file: 'bold.glb', type: 'bold' }, { file: 'innovation.glb', type: 'innovation' }, { file: 'agility.glb', type: 'agility' }, { file: 'storytelling.glb', type: 'storytelling' } ]; this.loadedModels = {}; this.loadedCount = 0; this.totalModels = this.modelsToLoad.length; } setLoadingMessage(message) { this.loadingText.textContent = message; } updateProgress(progress) { const percentage = Math.round(progress * 100); this.loadingProgressBar.style.width = `${percentage}%`; this.loadingPercentage.textContent = `${percentage}%`; } hideLoadingScreen() { this.loadingScreen.classList.add('hidden'); setTimeout(() => { this.loadingScreen.style.display = 'none'; }, 800); } async loadAllModels() { return new Promise((resolve) => { const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/'); loader.setDRACOLoader(dracoLoader); this.modelsToLoad.forEach((modelInfo, index) => { // this.setLoadingMessage(`Loading ${modelInfo.type}...`); this.setLoadingMessage(`Loading experience...`); loader.load(`/${modelInfo.file}`, (gltf) => { this.loadedModels[modelInfo.type] = { scene: gltf.scene, animations: gltf.animations, gltf: gltf }; this.loadedCount++; const progress = this.loadedCount / this.totalModels; this.updateProgress(progress); if (this.loadedCount === this.totalModels) { this.setLoadingMessage('Initializing Experience...'); setTimeout(() => { this.hideLoadingScreen(); resolve(this.loadedModels); }, 500); } }, (progress) => { // Individual file progress const fileProgress = progress.loaded / progress.total; const totalProgress = (this.loadedCount + fileProgress) / this.totalModels; this.updateProgress(totalProgress); }, (error) => { console.error(`Error loading ${modelInfo.file}:`, error); } ); }); }); } } // Initialize loader const sceneLoader = new SceneLoader(); // You can customize the loading message here: sceneLoader.setLoadingMessage('Preparing Your Experience...'); // Scene setup const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.setFocalLength(50); const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); // Transition state management let currentScene = 0; // 0: bold, 1: innovation, 2: agility, 3: storytelling let isTransitioning = false; let isTwisting = false; let twistProgress = 0; const twistSpeed = 0.02; // Easily adjustable twist speed const twistStrength = 0.3; const fadeSpeed = 1; // Easily adjustable fade speed const transitionDuration = 1; // Easily adjustable transition duration (seconds) let scrollDownCount = 0; let scrollUpCount = 0; const scrollThreshold = 10; // Changed to 10 as requested let transitionStartTime = 0; let transitionDirection = 1; // 1 for forward, -1 for backward // Scene objects let currentModel = null; let nextModel = null; let mixer = null; let nextMixer = null; let autoRotationAngle = 0; // Store preloaded models let preloadedModels = {}; // Bold scene roughness animation state let boldRoughnessAnimation = { isActive: false, startTime: 0, delayDuration: 1.0, // 1 second delay transitionDuration: 1.0, // 1 second transition startRoughness: 0.25, endRoughness: 0.05, materials: [] // Store references to bold materials }; // Innovation glass animation state let innovationGlassAnimation = { isActive: false, startTime: 0, transitionDuration: 0.2, startIor: 1.0, endIor: 2.0, startThickness: 1.0, endThickness: 2.0, materials: [] // Store references to innovation glass materials }; // Renderer setup const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setClearColor(0x000000); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.2; renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.physicallyCorrectLights = true; document.body.appendChild(renderer.domElement); // Post-processing: Bloom const composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.0, // strength 0.45, // radius 0.85 // threshold ); composer.addPass(bloomPass); // Video texture for emissive "screen"-like effect on orange material const video = document.createElement('video'); video.src = '/shader-flash.webm'; video.muted = true; video.loop = true; video.playsInline = true; video.autoplay = true; video.preload = 'auto'; const videoTexture = new THREE.VideoTexture(video); videoTexture.colorSpace = THREE.SRGBColorSpace; videoTexture.generateMipmaps = false; videoTexture.minFilter = THREE.LinearFilter; videoTexture.magFilter = THREE.LinearFilter; // Ensure autoplay starts (muted autoplay is commonly allowed) video.play().catch(() => {}); // Local procedural environment for better PBR response (no network) const pmrem = new THREE.PMREMGenerator(renderer); const roomEnv = new RoomEnvironment(); scene.environment = pmrem.fromScene(roomEnv).texture; pmrem.dispose(); roomEnv.dispose(); scene.environment = null; // This will make the renderer's clear color visible again // Consistent Lighting Setup const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const hemiLight = new THREE.HemisphereLight(0xffffff, 0x666666, 1.5); hemiLight.position.set(0, 20, 0); scene.add(hemiLight); const fillLight = new THREE.DirectionalLight(0xffffff, 1.2); fillLight.position.set(-12, 6, -8); scene.add(fillLight); const topLight = new THREE.DirectionalLight(0xffffff, 1.5); topLight.position.set(5, 15, 5); scene.add(topLight); const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8); bottomLight.position.set(-3, -8, 3); scene.add(bottomLight); const leftLight = new THREE.DirectionalLight(0xffffff, 1.0); leftLight.position.set(-12, 2, 5); scene.add(leftLight); const rightLight = new THREE.DirectionalLight(0xffffff, 1.0); rightLight.position.set(12, 2, -5); scene.add(rightLight); const frontLight = new THREE.DirectionalLight(0xffffff, 0.8); frontLight.position.set(8, 4, 12); scene.add(frontLight); const backLight = new THREE.DirectionalLight(0xffffff, 0.8); backLight.position.set(-8, 4, -12); scene.add(backLight); const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2); camera.add(cameraLight); scene.add(camera); // Controls with zoom disabled const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.25; controls.enableZoom = false; // Disable zoom // Material definitions // Bold glass material (starts rough, will transition to clear) const boldGlassMaterial = new THREE.MeshPhysicalMaterial({ color: 0xFFA500, metalness: 0.2, roughness: 0.25, // Start with rough glass transmission: 1, ior: 2, thickness: 2, clearcoat: 1.0, clearcoatRoughness: 0.1, attenuationColor: new THREE.Color(0xffffff), attenuationDistance: 0.8, envMapIntensity: 0, specularIntensity: 1.0, specularColor: new THREE.Color(0xffffff), transparent: true, depthWrite: false, alphaTest: 0 }); // Clear thick glass for innovation (starts with animated values) const innovationGlassMaterial = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0.2, roughness: 0.05, transmission: 1, ior: 1.0, // Will animate from 1 to 2 thickness: 1.0, // Will animate from 1 to 2 clearcoat: 1.0, clearcoatRoughness: 0.1, attenuationColor: new THREE.Color(0xffffff), attenuationDistance: 0.8, envMapIntensity: 0, specularIntensity: 1.0, specularColor: new THREE.Color(0x000000), transparent: true, depthWrite: false, alphaTest: 0 }); // Slightly frosted glass for agility and storytelling const frostedGlassMaterial = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0.0, roughness: 0.25, transmission: 1.0, ior: 1.5, thickness: 2.0, clearcoat: 0.75, clearcoatRoughness: 0.25, attenuationColor: new THREE.Color(0xffffff), attenuationDistance: 1.5, envMapIntensity: 1.25, specularIntensity: 1.0, specularColor: new THREE.Color(0xffffff), transparent: true, depthWrite: false, side: THREE.DoubleSide }); // Orange material with video shader for innovation const lightOrangeMaterial = new THREE.MeshStandardMaterial({ color: 0xff8600, metalness: 0.05, roughness: 0.4, envMapIntensity: 0, emissive: new THREE.Color(0xffad47), emissiveMap: videoTexture, emissiveIntensity: 2.25 }); // Apply materials based on model type function applyMaterials(model, modelType) { console.log(`=== Material Assignment Debug for ${modelType} ===`); let meshCount = 0; model.traverse((object) => { if (object.isMesh) { meshCount++; console.log(`Found mesh: "${object.name}"`); const previousMaterial = object.material; object.castShadow = true; object.receiveShadow = true; if (modelType === 'bold') { // Bold-specific material logic - apply bold glass material to Cube mesh if (object.name === 'Cube') { console.log(` → Applying bold glass material to "${object.name}"`); object.material = boldGlassMaterial.clone(); object.material.side = THREE.DoubleSide; object.material.depthWrite = false; object.renderOrder = 2; // Store material reference for roughness animation boldRoughnessAnimation.materials.push(object.material); } else { console.log(` → Applying bold glass material (fallback) to "${object.name}"`); object.material = boldGlassMaterial.clone(); // Store material reference for roughness animation boldRoughnessAnimation.materials.push(object.material); } } else if (modelType === 'innovation') { // Innovation-specific material logic const orangeMeshes = ['dblsc', 'ec', 'gemini', 'infinity', 'star', 'dpd']; const targetGlassNames = ['Cube.alt90.df']; const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); const nameMatches = (name, targets) => { const clean = sanitize(name); return targets.some((t) => { const ct = sanitize(t); return clean === ct || clean.includes(ct) || ct.includes(clean); }); }; if (nameMatches(object.name, targetGlassNames)) { // Create outer glass shell with innovation-specific material object.material = innovationGlassMaterial.clone(); object.material.side = THREE.DoubleSide; object.material.depthWrite = false; object.renderOrder = 2; // Store material reference for animation innovationGlassAnimation.materials.push(object.material); // Create inner glass shell const innerShell = object.clone(); innerShell.material = innovationGlassMaterial.clone(); innerShell.material.side = THREE.DoubleSide; innerShell.material.depthWrite = false; innerShell.material.transmission = 0.8; innerShell.renderOrder = 1; innerShell.scale.multiplyScalar(0.95); // Store inner shell material reference for animation too innovationGlassAnimation.materials.push(innerShell.material); object.parent.add(innerShell); } else if (nameMatches(object.name, orangeMeshes)) { object.material = lightOrangeMaterial.clone(); object.renderOrder = 0; } } else { // Agility and Storytelling use frosted glass material for all meshes if (object.name.startsWith('base')) { console.log(` → Applying frosted glass material to "${object.name}"`); object.material = frostedGlassMaterial.clone(); } else { console.log(` → Applying frosted glass material (fallback) to "${object.name}"`); object.material = frostedGlassMaterial.clone(); } } object.material.needsUpdate = true; // Cleanup previous materials if (Array.isArray(previousMaterial)) { previousMaterial.forEach((mat) => mat && mat.dispose && mat.dispose()); } else if (previousMaterial && previousMaterial.dispose) { previousMaterial.dispose(); } } }); console.log(`Total meshes processed: ${meshCount}`); console.log(`=== End Material Assignment Debug for ${modelType} ===`); } // Center and frame model with camera function centerAndFrameModel(model, targetCamera = camera) { const box = new THREE.Box3().setFromObject(model); const center = box.getCenter(new THREE.Vector3()); model.position.sub(center); model.updateMatrixWorld(true); // Only set camera position if it's not already positioned (avoid reset during transitions) // Use fixed camera distance that's further away from the origin if (!isTransitioning) { const fixedCameraDistance = 50; // Fixed distance, much further than before // Calculate isometric-like position with 35-degree angles const angle = 35 * Math.PI / 180; // Convert 35 degrees to radians const cosAngle = Math.cos(angle); const x = fixedCameraDistance * cosAngle; const y = fixedCameraDistance * cosAngle; const z = fixedCameraDistance * cosAngle; targetCamera.position.set(x, y, z); controls.target.set(0, 0, 0); // Set distance limits to lock the camera at this distance controls.minDistance = fixedCameraDistance; controls.maxDistance = fixedCameraDistance; controls.update(); } } // Setup animations based on model type function setupAnimations(model, gltf, modelType) { if (gltf.animations && gltf.animations.length > 0) { const animMixer = new THREE.AnimationMixer(model); gltf.animations.forEach((clip) => { const action = animMixer.clipAction(clip); if (modelType === 'bold') { // Play once for bold action.loop = THREE.LoopOnce; action.clampWhenFinished = true; action.play(); } else if (modelType === 'innovation') { // PingPong loop for innovation action.loop = THREE.LoopPingPong; action.play(); } else if (modelType === 'agility') { // Regular loop for agility action.loop = THREE.LoopRepeat; action.play(); } else if (modelType === 'storytelling') { // Play once for storytelling action.loop = THREE.LoopOnce; action.clampWhenFinished = true; action.play(); } }); if (modelType === 'innovation') { animMixer.timeScale = 3.0; // Keep existing timeScale for innovation } return animMixer; } return null; } // Create model from preloaded data - FIXED: Always create fresh geometry function createModelFromPreloaded(modelType) { const preloadedData = preloadedModels[modelType]; if (!preloadedData) { console.error(`Preloaded model not found: ${modelType}`); return { model: null, animMixer: null }; } // Clear animation materials arrays when creating new models if (modelType === 'bold') { boldRoughnessAnimation.materials = []; } else if (modelType === 'innovation') { innovationGlassAnimation.materials = []; } // Clone the scene deeply to ensure fresh geometry const model = preloadedData.scene.clone(true); // IMPORTANT: Clone all geometries to ensure they're independent model.traverse((object) => { if (object.isMesh && object.geometry) { object.geometry = object.geometry.clone(); // Clear any previous twist data delete object.geometry.userData.originalPositions; delete object.geometry.userData.originalWorldPositions; delete object.geometry.userData.inverseWorldMatrix; } }); // Apply materials applyMaterials(model, modelType); // Setup animations const animMixer = setupAnimations(model, preloadedData.gltf, modelType); // Center and frame model centerAndFrameModel(model); return { model, animMixer }; } // Initialize first scene after all models are loaded function initializeScene() { const { model, animMixer } = createModelFromPreloaded('bold'); currentModel = model; mixer = animMixer; scene.add(currentModel); // Start the roughness animation for bold scene boldRoughnessAnimation.isActive = true; boldRoughnessAnimation.startTime = performance.now(); } // Start innovation glass animation function startInnovationGlassAnimation() { // Reset all innovation glass materials to starting values innovationGlassAnimation.materials.forEach(material => { material.ior = innovationGlassAnimation.startIor; material.thickness = innovationGlassAnimation.startThickness; material.needsUpdate = true; }); innovationGlassAnimation.isActive = true; innovationGlassAnimation.startTime = performance.now(); console.log('Innovation glass animation started'); } // Twist animation function - Updated to twist around world center (0,0,0) function twistMesh(mesh, progress) { if (!mesh || !mesh.geometry || !mesh.geometry.attributes.position) { return; } const positions = mesh.geometry.attributes.position; // Store original positions on the first run if (!mesh.geometry.userData.originalPositions) { mesh.geometry.userData.originalPositions = new Float32Array(positions.array); // Store original world positions for each vertex mesh.geometry.userData.originalWorldPositions = []; // Update world matrix to get accurate world positions mesh.updateMatrixWorld(true); const tempVector = new THREE.Vector3(); for (let i = 0; i < positions.count; i++) { tempVector.fromBufferAttribute(positions, i); tempVector.applyMatrix4(mesh.matrixWorld); mesh.geometry.userData.originalWorldPositions.push({ x: tempVector.x, y: tempVector.y, z: tempVector.z }); } // Store the inverse of the current world matrix for transforming back to local space mesh.geometry.userData.inverseWorldMatrix = mesh.matrixWorld.clone().invert(); } const originalWorldPositions = mesh.geometry.userData.originalWorldPositions; const inverseWorldMatrix = mesh.geometry.userData.inverseWorldMatrix; for (let i = 0; i < positions.count; i++) { const worldPos = originalWorldPositions[i]; // Use world Y position for consistent twisting around world Y-axis const worldY = worldPos.y; // Calculate twist angle based on world Y position // Normalize Y based on a reasonable range (adjust as needed) const normalizedY = (worldY + 5) / 10; // Assuming meshes are roughly within -5 to +5 world units in Y const twistAngle = normalizedY * progress * twistStrength * 2 * Math.PI; // Apply twist in world coordinates around world Y-axis const twistedWorldX = worldPos.x * Math.cos(twistAngle) - worldPos.z * Math.sin(twistAngle); const twistedWorldY = worldPos.y; // Y remains unchanged const twistedWorldZ = worldPos.x * Math.sin(twistAngle) + worldPos.z * Math.cos(twistAngle); // Convert twisted world position back to local coordinates const twistedWorldVector = new THREE.Vector3(twistedWorldX, twistedWorldY, twistedWorldZ); twistedWorldVector.applyMatrix4(inverseWorldMatrix); positions.setXYZ(i, twistedWorldVector.x, twistedWorldVector.y, twistedWorldVector.z); } positions.needsUpdate = true; mesh.geometry.computeVertexNormals(); } // Reset mesh geometry to original state function resetMeshGeometry(mesh) { if (!mesh || !mesh.geometry || !mesh.geometry.userData.originalPositions) { return; } const positions = mesh.geometry.attributes.position; const original = mesh.geometry.userData.originalPositions; for (let i = 0; i < positions.count; i++) { positions.setXYZ(i, original[i * 3], original[i * 3 + 1], original[i * 3 + 2]); } positions.needsUpdate = true; mesh.geometry.computeVertexNormals(); } // FIXED: Clean up geometry data completely function cleanupGeometryData(model) { if (!model) return; model.traverse((object) => { if (object.isMesh && object.geometry && object.geometry.userData) { delete object.geometry.userData.originalPositions; delete object.geometry.userData.originalWorldPositions; delete object.geometry.userData.inverseWorldMatrix; } }); } // Start transition to next or previous scene function startTransition(direction = 1) { if (isTransitioning) return; // Check bounds - now 4 scenes (0-3) if (direction > 0 && currentScene >= 3) return; // Can't go forward from storytelling if (direction < 0 && currentScene <= 0) return; // Can't go backward from bold isTransitioning = true; isTwisting = true; twistProgress = 0; transitionStartTime = performance.now(); transitionDirection = direction; // Determine next model based on direction and current scene let nextModelType = ''; if (direction > 0) { // Moving forward if (currentScene === 0) { nextModelType = 'innovation'; } else if (currentScene === 1) { nextModelType = 'agility'; } else if (currentScene === 2) { nextModelType = 'storytelling'; } } else { // Moving backward if (currentScene === 1) { nextModelType = 'bold'; } else if (currentScene === 2) { nextModelType = 'innovation'; } else if (currentScene === 3) { nextModelType = 'agility'; } } if (nextModelType) { const { model, animMixer } = createModelFromPreloaded(nextModelType); nextModel = model; nextMixer = animMixer; // Start next model as invisible but in normal position (no vertical offset) nextModel.position.y = 0; nextModel.traverse((obj) => { if (obj.material) { if (Array.isArray(obj.material)) { obj.material.forEach(mat => { mat.transparent = true; mat.opacity = 0; }); } else { obj.material.transparent = true; obj.material.opacity = 0; } } }); scene.add(nextModel); } } // Update transition animation function updateTransition(deltaTime) { if (!isTransitioning) return; const elapsed = (performance.now() - transitionStartTime) / 1000; const transitionProgress = Math.min(elapsed / transitionDuration, 1); // Smooth easing function (ease-in-out) const easeInOut = (t) => t * t * (3 - 2 * t); const easedProgress = easeInOut(transitionProgress); if (currentModel) { // Move current model up and fade out // currentModel.position.y = easedProgress * 10; currentModel.traverse((obj) => { if (obj.material) { const targetOpacity = 1 - easedProgress; if (Array.isArray(obj.material)) { obj.material.forEach(mat => { mat.transparent = true; mat.opacity = targetOpacity; }); } else { obj.material.transparent = true; obj.material.opacity = targetOpacity; } } }); } if (nextModel) { // Keep next model in place and just fade in (no vertical movement) nextModel.position.y = 0; nextModel.traverse((obj) => { if (obj.material) { const targetOpacity = easedProgress; if (Array.isArray(obj.material)) { obj.material.forEach(mat => { mat.transparent = true; mat.opacity = targetOpacity; }); } else { obj.material.transparent = true; obj.material.opacity = targetOpacity; } } }); } // Complete transition if (transitionProgress >= 1) { // FIXED: Reset geometry before removing the model if (currentModel) { // Reset all geometry to original state before removal currentModel.traverse((object) => { if (object.isMesh) { resetMeshGeometry(object); } }); // Clean up geometry user data completely cleanupGeometryData(currentModel); scene.remove(currentModel); } // Switch to next model if (nextModel) { currentModel = nextModel; mixer = nextMixer; // Reset position and opacity currentModel.position.y = 0; currentModel.traverse((obj) => { if (obj.material) { if (Array.isArray(obj.material)) { obj.material.forEach(mat => { mat.opacity = 1; if (currentScene + transitionDirection === 3) { // Keep transparency for storytelling glass mat.transparent = mat.transmission > 0; } else { mat.transparent = mat.transmission > 0; } }); } else { obj.material.opacity = 1; if (currentScene + transitionDirection === 3) { // Keep transparency for storytelling glass obj.material.transparent = obj.material.transmission > 0; } else { obj.material.transparent = obj.material.transmission > 0; } } } }); } nextModel = null; nextMixer = null; isTransitioning = false; isTwisting = false; twistProgress = 0; currentScene += transitionDirection; // Update scene based on direction scrollDownCount = 0; scrollUpCount = 0; // Start innovation glass animation if we're now in the innovation scene if (currentScene === 1) { startInnovationGlassAnimation(); } console.log(`Transition complete. Current scene: ${currentScene}`); } } // Scroll event handler function onMouseScroll(event) { if (isTransitioning) return; if (event.deltaY > 0) { // Scrolling down - move forward scrollDownCount++; scrollUpCount = 0; // Reset up count console.log(`Scroll down count: ${scrollDownCount}`); if (scrollDownCount >= scrollThreshold) { startTransition(1); // Forward direction } } else if (event.deltaY < 0) { // Scrolling up - move backward scrollUpCount++; scrollDownCount = 0; // Reset down count console.log(`Scroll up count: ${scrollUpCount}`); if (scrollUpCount >= scrollThreshold) { startTransition(-1); // Backward direction } } } // Animation loop const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); // Update mixers if (mixer) mixer.update(delta); if (nextMixer) nextMixer.update(delta); // Update transition if (isTransitioning) { updateTransition(delta); // Apply twist during transition if (isTwisting && currentModel) { twistProgress += twistSpeed; if (twistProgress > 1.0) { twistProgress = 1.0; // FIXED: Reset geometry after twist completes currentModel.traverse((object) => { if (object.isMesh) { resetMeshGeometry(object); } }); isTwisting = false; } else { // Apply twist to current model currentModel.traverse((object) => { if (object.isMesh) { twistMesh(object, twistProgress); } }); } } } // Update bold roughness animation if (boldRoughnessAnimation.isActive) { const elapsed = (performance.now() - boldRoughnessAnimation.startTime) / 1000; if (elapsed >= boldRoughnessAnimation.delayDuration) { // Delay period is over, start roughness transition const transitionElapsed = elapsed - boldRoughnessAnimation.delayDuration; const transitionProgress = Math.min(transitionElapsed / boldRoughnessAnimation.transitionDuration, 1); // Smooth easing function (ease-in-out) const easeInOut = (t) => t * t * (3 - 2 * t); const easedProgress = easeInOut(transitionProgress); // Interpolate roughness from 0.25 to 0.05 const currentRoughness = boldRoughnessAnimation.startRoughness + (boldRoughnessAnimation.endRoughness - boldRoughnessAnimation.startRoughness) * easedProgress; // Apply to all bold materials boldRoughnessAnimation.materials.forEach(material => { material.roughness = currentRoughness; material.needsUpdate = true; }); // End animation when complete if (transitionProgress >= 1) { boldRoughnessAnimation.isActive = false; } } } // Update innovation glass animation if (innovationGlassAnimation.isActive) { const elapsed = (performance.now() - innovationGlassAnimation.startTime) / 1000; const transitionProgress = Math.min(elapsed / innovationGlassAnimation.transitionDuration, 1); // Smooth easing function (ease-in-out) const easeInOut = (t) => t * t * (3 - 2 * t); const easedProgress = easeInOut(transitionProgress); // Interpolate IOR from 1.0 to 2.0 const currentIor = innovationGlassAnimation.startIor + (innovationGlassAnimation.endIor - innovationGlassAnimation.startIor) * easedProgress; // Interpolate thickness from 1.0 to 2.0 const currentThickness = innovationGlassAnimation.startThickness + (innovationGlassAnimation.endThickness - innovationGlassAnimation.startThickness) * easedProgress; // Apply to all innovation glass materials innovationGlassAnimation.materials.forEach(material => { material.ior = currentIor; material.thickness = currentThickness; material.needsUpdate = true; }); // End animation when complete if (transitionProgress >= 1) { innovationGlassAnimation.isActive = false; console.log('Innovation glass animation completed'); } } // Turntable rotation for current model // if (currentModel && !isTransitioning) { // autoRotationAngle += delta * 0.5; // currentModel.rotation.y = autoRotationAngle; // } controls.update(); composer.render(); } // Initialize the scene async function init() { try { // Load all models first preloadedModels = await sceneLoader.loadAllModels(); // Initialize the first scene initializeScene(); // Start the animation loop animate(); // Attach scroll event listener window.addEventListener('wheel', onMouseScroll, {passive: true}); } catch (error) { console.error('Failed to initialize scene:', error); sceneLoader.setLoadingMessage('Error loading experience. Please refresh.'); } } // Handle window resize window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); }); // Start the application init();