yp-rubix/src/innovation.js
2025-08-28 13:57:09 +05:30

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);
});