342 lines
11 KiB
JavaScript
342 lines
11 KiB
JavaScript
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);
|
|
});
|