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'; // 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(); let isTwisting = false; let twistProgress = 0; const twistSpeed = 0.05; // Adjust speed const twistStrength = 0.3; // Adjust strength let scrollCount = 0; const scrollThreshold = 20; // Number of scroll events to trigger the animation // 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 // Lighting is authored below. // Lighting 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); // // Key light (main directional) - angled to avoid direct reflection // const keyLight = new THREE.DirectionalLight(0xffffff, 2.0); // keyLight.position.set(12, 8, 8); // keyLight.castShadow = true; // keyLight.shadow.mapSize.width = 2048; // keyLight.shadow.mapSize.height = 2048; // scene.add(keyLight); // Fill light (opposite side) - angled const fillLight = new THREE.DirectionalLight(0xffffff, 1.2); fillLight.position.set(-12, 6, -8); scene.add(fillLight); // Top light - angled to avoid direct downward reflection const topLight = new THREE.DirectionalLight(0xffffff, 1.5); topLight.position.set(5, 15, 5); scene.add(topLight); // Bottom light - angled upward const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8); bottomLight.position.set(-3, -8, 3); scene.add(bottomLight); // Side lights for even illumination - angled 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); // Front and back lights - angled to avoid direct camera reflection 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); // Reduced camera light const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2); camera.add(cameraLight); scene.add(camera); // Controls const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.25; const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/'); loader.setDRACOLoader(dracoLoader); let mixer = null; loader.load('/innovation.glb', (gltf) => { const model = gltf.scene; scene.add(model); // --- Define and Apply Materials --- const glassMaterial = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0.2, roughness: 0.05, 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(0x000000), transparent: true, depthWrite: false, alphaTest: 0 }); 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 }); 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); }); }; model.traverse((object) => { if (object.isMesh) { object.castShadow = true; object.receiveShadow = true; if (nameMatches(object.name, targetGlassNames)) { // Create outer glass shell object.material = glassMaterial.clone(); object.material.side = THREE.DoubleSide; object.material.depthWrite = false; object.renderOrder = 2; // Render outer glass last // Create inner glass shell for better depth perception const innerShell = object.clone(); innerShell.material = glassMaterial.clone(); innerShell.material.side = THREE.DoubleSide; innerShell.material.depthWrite = false; innerShell.material.thickness = 4; // Thinner inner layer innerShell.material.transmission = 0.8; // More transparent inner layer innerShell.renderOrder = 1; // Render inner glass before outer // Scale inner shell slightly smaller innerShell.scale.multiplyScalar(0.95); object.parent.add(innerShell); } else if (nameMatches(object.name, orangeMeshes)) { object.material = lightOrangeMaterial.clone(); object.renderOrder = 0; // Render orange objects first } } }); // Compute bounds for camera framing const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); // Set up animations if (gltf.animations && gltf.animations.length > 0) { mixer = new THREE.AnimationMixer(model); gltf.animations.forEach((clip) => { mixer.clipAction(clip).play(); }); mixer.timeScale = 3.0; } // Position camera const maxDim = Math.max(size.x, size.y, size.z); camera.position.set(center.x, center.y, center.z + maxDim * 2); controls.target.copy(center); controls.update(); }, undefined, (error) => { console.error('Error loading model:', error); }); const clock = new THREE.Clock(); function onMouseScroll(event) { // Only count scrolls if the animation is not already running if (!isTwisting) { // You can check event.deltaY to determine scroll direction if (event.deltaY !== 0) { scrollCount++; console.log(`Scroll count: ${scrollCount}`); // For debugging } if (scrollCount >= scrollThreshold) { isTwisting = true; twistProgress = 0; scrollCount = 0; // Reset the counter } } } 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); // Also store bounding box data const box = new THREE.Box3().setFromObject(mesh); mesh.geometry.userData.bounds = { size: box.getSize(new THREE.Vector3()), center: box.getCenter(new THREE.Vector3()) }; } const original = mesh.geometry.userData.originalPositions; const { size, center } = mesh.geometry.userData.bounds; const totalHeight = size.y; // Use Y-size for the twist axis for (let i = 0; i < positions.count; i++) { const x = original[i * 3]; const y = original[i * 3 + 1]; const z = original[i * 3 + 2]; // Normalize the y-position from 0 to 1 based on the mesh's height const normalizedY = (y - center.y + totalHeight / 2) / totalHeight; // Calculate the twist angle based on normalized y and progress const twistAngle = normalizedY * progress * twistStrength * 2 * Math.PI; // Apply rotation to the X and Z coordinates positions.setX(i, x * Math.cos(twistAngle) - z * Math.sin(twistAngle)); positions.setY(i, y); // Y remains unchanged as it's the axis of rotation positions.setZ(i, x * Math.sin(twistAngle) + z * Math.cos(twistAngle)); } positions.needsUpdate = true; mesh.geometry.computeVertexNormals(); } // Attach the click event listener window.addEventListener('wheel', onMouseScroll, {passive: true}); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); if (mixer) mixer.update(delta); controls.update(); // The main loop for the twisting animation if (isTwisting) { twistProgress += twistSpeed; if (twistProgress > 1.0) { twistProgress = 1.0; isTwisting = false; } // Traverse the entire scene to find all meshes to twist scene.traverse((object) => { if (object.isMesh) { twistMesh(object, twistProgress); } }); } composer.render(); } animate(); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); });