first
This commit is contained in:
commit
b18d4c090b
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
13
index.html
Normal file
13
index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Young Pandas</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1038
package-lock.json
generated
Normal file
1038
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
17
package.json
Normal file
17
package.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "young-pandas",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^7.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"three": "^0.179.1"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/agility.glb
Normal file
BIN
public/agility.glb
Normal file
Binary file not shown.
245
public/agility.html
Normal file
245
public/agility.html
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>3D Model Viewer</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
||||||
|
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
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);
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setClearColor(0x000000); // Black background
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
// Lighting is authored below.
|
||||||
|
|
||||||
|
// Lighting
|
||||||
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.2);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.9);
|
||||||
|
hemiLight.position.set(0, 20, 0);
|
||||||
|
scene.add(hemiLight);
|
||||||
|
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xffffff, 2.0);
|
||||||
|
keyLight.position.set(8, 12, 10);
|
||||||
|
keyLight.castShadow = true;
|
||||||
|
keyLight.shadow.mapSize.width = 2048;
|
||||||
|
keyLight.shadow.mapSize.height = 2048;
|
||||||
|
scene.add(keyLight);
|
||||||
|
|
||||||
|
const fillLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
fillLight.position.set(-8, 6, -10);
|
||||||
|
scene.add(fillLight);
|
||||||
|
|
||||||
|
const cameraLight = new THREE.PointLight(0xffffff, 1.2, 0, 2);
|
||||||
|
camera.add(cameraLight);
|
||||||
|
scene.add(camera);
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.25;
|
||||||
|
|
||||||
|
|
||||||
|
// Load GLTF model
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const dracoLoader = new DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('https://unpkg.com/three@0.160.0/examples/jsm/libs/draco/');
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
let mixer = null;
|
||||||
|
let model = null;
|
||||||
|
|
||||||
|
loader.load('agility.glb', (gltf) => {
|
||||||
|
model = gltf.scene;
|
||||||
|
scene.add(model);
|
||||||
|
|
||||||
|
// --- Define and Apply Materials ---
|
||||||
|
|
||||||
|
const glassMaterial = 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create materials with normal maps for dpd, dblsc, and gemini
|
||||||
|
|
||||||
|
|
||||||
|
// Apply different materials based on mesh names
|
||||||
|
console.log('=== Material Assignment Debug ===');
|
||||||
|
let meshCount = 0;
|
||||||
|
|
||||||
|
model.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
meshCount++;
|
||||||
|
console.log(`Found mesh: "${object.name}"`);
|
||||||
|
|
||||||
|
const previousMaterial = object.material;
|
||||||
|
|
||||||
|
// Apply materials by mesh name pattern
|
||||||
|
if (object.name.startsWith('base')) {
|
||||||
|
console.log(` → Applying glass material to "${object.name}"`);
|
||||||
|
object.material = glassMaterial.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other meshes, use glass material as fallback
|
||||||
|
else {
|
||||||
|
console.log(` → Applying glass material (fallback) to "${object.name}"`);
|
||||||
|
object.material = glassMaterial.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Material properties:`, {
|
||||||
|
name: object.material.name,
|
||||||
|
normalMap: object.material.normalMap ? 'Loaded' : 'None',
|
||||||
|
normalScale: object.material.normalScale,
|
||||||
|
transmission: object.material.transmission,
|
||||||
|
transparent: object.material.transparent
|
||||||
|
});
|
||||||
|
|
||||||
|
object.material.needsUpdate = true;
|
||||||
|
object.castShadow = true;
|
||||||
|
object.receiveShadow = true;
|
||||||
|
|
||||||
|
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 ===');
|
||||||
|
|
||||||
|
// Set up animations
|
||||||
|
if (gltf.animations && gltf.animations.length > 0) {
|
||||||
|
mixer = new THREE.AnimationMixer(model);
|
||||||
|
|
||||||
|
gltf.animations.forEach((clip) => {
|
||||||
|
mixer.clipAction(clip).play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center model at origin and frame it with the camera
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
model.position.sub(center);
|
||||||
|
model.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
camera.position.set(0, 0, maxDim * 2);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
controls.update();
|
||||||
|
|
||||||
|
// Ground plane removed per request
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error('Error loading model:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
const clock = new THREE.Clock();
|
||||||
|
let autoRotationAngle = 0;
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
const delta = clock.getDelta();
|
||||||
|
if (mixer) mixer.update(delta);
|
||||||
|
|
||||||
|
// Auto-rotation (turntable effect)
|
||||||
|
if (model) {
|
||||||
|
autoRotationAngle += delta * 0.5; // Adjust speed here (0.5 = slow rotation)
|
||||||
|
model.rotation.y = autoRotationAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.update();
|
||||||
|
composer.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
public/innovation.glb
Normal file
BIN
public/innovation.glb
Normal file
Binary file not shown.
BIN
public/shader-flash.webm
Normal file
BIN
public/shader-flash.webm
Normal file
Binary file not shown.
BIN
public/storytelling.glb
Normal file
BIN
public/storytelling.glb
Normal file
Binary file not shown.
249
public/storytelling.html
Normal file
249
public/storytelling.html
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>3D Model Viewer</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
||||||
|
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
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);
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setClearColor(0x000000); // Black background
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
// Lighting is authored below.
|
||||||
|
|
||||||
|
// Lighting
|
||||||
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.2);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.9);
|
||||||
|
hemiLight.position.set(0, 20, 0);
|
||||||
|
scene.add(hemiLight);
|
||||||
|
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xffffff, 2.0);
|
||||||
|
keyLight.position.set(8, 12, 10);
|
||||||
|
keyLight.castShadow = true;
|
||||||
|
keyLight.shadow.mapSize.width = 2048;
|
||||||
|
keyLight.shadow.mapSize.height = 2048;
|
||||||
|
scene.add(keyLight);
|
||||||
|
|
||||||
|
const fillLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
fillLight.position.set(-8, 6, -10);
|
||||||
|
scene.add(fillLight);
|
||||||
|
|
||||||
|
const cameraLight = new THREE.PointLight(0xffffff, 1.2, 0, 2);
|
||||||
|
camera.add(cameraLight);
|
||||||
|
scene.add(camera);
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.25;
|
||||||
|
|
||||||
|
|
||||||
|
// Load GLTF model
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const dracoLoader = new DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('https://unpkg.com/three@0.160.0/examples/jsm/libs/draco/');
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
let mixer = null;
|
||||||
|
let model = null;
|
||||||
|
|
||||||
|
loader.load('storytelling.glb', (gltf) => {
|
||||||
|
model = gltf.scene;
|
||||||
|
scene.add(model);
|
||||||
|
|
||||||
|
// --- Define and Apply Materials ---
|
||||||
|
|
||||||
|
const glassMaterial = 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create materials with normal maps for dpd, dblsc, and gemini
|
||||||
|
|
||||||
|
|
||||||
|
// Apply different materials based on mesh names
|
||||||
|
console.log('=== Material Assignment Debug ===');
|
||||||
|
let meshCount = 0;
|
||||||
|
|
||||||
|
model.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
meshCount++;
|
||||||
|
console.log(`Found mesh: "${object.name}"`);
|
||||||
|
|
||||||
|
const previousMaterial = object.material;
|
||||||
|
|
||||||
|
// Apply materials by mesh name pattern
|
||||||
|
if (object.name.startsWith('base')) {
|
||||||
|
console.log(` → Applying glass material to "${object.name}"`);
|
||||||
|
object.material = glassMaterial.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other meshes, use glass material as fallback
|
||||||
|
else {
|
||||||
|
console.log(` → Applying glass material (fallback) to "${object.name}"`);
|
||||||
|
object.material = glassMaterial.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Material properties:`, {
|
||||||
|
name: object.material.name,
|
||||||
|
normalMap: object.material.normalMap ? 'Loaded' : 'None',
|
||||||
|
normalScale: object.material.normalScale,
|
||||||
|
transmission: object.material.transmission,
|
||||||
|
transparent: object.material.transparent
|
||||||
|
});
|
||||||
|
|
||||||
|
object.material.needsUpdate = true;
|
||||||
|
object.castShadow = true;
|
||||||
|
object.receiveShadow = true;
|
||||||
|
|
||||||
|
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 ===');
|
||||||
|
|
||||||
|
// Set up animations
|
||||||
|
if (gltf.animations && gltf.animations.length > 0) {
|
||||||
|
mixer = new THREE.AnimationMixer(model);
|
||||||
|
|
||||||
|
gltf.animations.forEach((clip) => {
|
||||||
|
const action = mixer.clipAction(clip);
|
||||||
|
// Set the animation to play once and stop at the last frame
|
||||||
|
action.loop = THREE.LoopOnce;
|
||||||
|
action.clampWhenFinished = true;
|
||||||
|
action.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center model at origin and frame it with the camera
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
model.position.sub(center);
|
||||||
|
model.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
camera.position.set(0, 0, maxDim * 2);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
controls.update();
|
||||||
|
|
||||||
|
// Ground plane removed per request
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error('Error loading model:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
const clock = new THREE.Clock();
|
||||||
|
let autoRotationAngle = 0;
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
const delta = clock.getDelta();
|
||||||
|
if (mixer) mixer.update(delta);
|
||||||
|
|
||||||
|
// Auto-rotation (turntable effect)
|
||||||
|
if (model) {
|
||||||
|
autoRotationAngle += delta * 0.5; // Adjust speed here (0.5 = slow rotation)
|
||||||
|
model.rotation.y = autoRotationAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.update();
|
||||||
|
composer.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
341
src/innovation.js
Normal file
341
src/innovation.js
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
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);
|
||||||
|
});
|
643
src/main copy.js
Normal file
643
src/main copy.js
Normal file
|
@ -0,0 +1,643 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Transition state management
|
||||||
|
let currentScene = 0; // 0: innovation, 1: agility, 2: 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 scrollCount = 0;
|
||||||
|
const scrollThreshold = 10; // Changed to 10 as requested
|
||||||
|
let transitionStartTime = 0;
|
||||||
|
|
||||||
|
// Scene objects
|
||||||
|
let currentModel = null;
|
||||||
|
let nextModel = null;
|
||||||
|
let mixer = null;
|
||||||
|
let nextMixer = null;
|
||||||
|
let autoRotationAngle = 0;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.25;
|
||||||
|
|
||||||
|
// Material definitions
|
||||||
|
// Clear thick glass for innovation
|
||||||
|
const innovationGlassMaterial = 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const dracoLoader = new DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
|
||||||
|
// 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 === '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;
|
||||||
|
|
||||||
|
// Create inner glass shell
|
||||||
|
const innerShell = object.clone();
|
||||||
|
innerShell.material = innovationGlassMaterial.clone();
|
||||||
|
innerShell.material.side = THREE.DoubleSide;
|
||||||
|
innerShell.material.depthWrite = false;
|
||||||
|
innerShell.material.thickness = 4;
|
||||||
|
innerShell.material.transmission = 0.8;
|
||||||
|
innerShell.renderOrder = 1;
|
||||||
|
innerShell.scale.multiplyScalar(0.95);
|
||||||
|
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);
|
||||||
|
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
// Only set camera position if it's not already positioned (avoid reset during transitions)
|
||||||
|
if (!isTransitioning) {
|
||||||
|
targetCamera.position.set(0, 0, maxDim * 2);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
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 === '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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load model function
|
||||||
|
function loadModel(filename, modelType, onLoadCallback) {
|
||||||
|
loader.load(`/${filename}`, (gltf) => {
|
||||||
|
const model = gltf.scene;
|
||||||
|
|
||||||
|
// Apply materials
|
||||||
|
applyMaterials(model, modelType);
|
||||||
|
|
||||||
|
// Setup animations
|
||||||
|
const animMixer = setupAnimations(model, gltf, modelType);
|
||||||
|
|
||||||
|
// Center and frame model
|
||||||
|
centerAndFrameModel(model);
|
||||||
|
|
||||||
|
if (onLoadCallback) {
|
||||||
|
onLoadCallback(model, animMixer);
|
||||||
|
}
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error(`Error loading ${filename}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial innovation model
|
||||||
|
loadModel('innovation.glb', 'innovation', (model, animMixer) => {
|
||||||
|
currentModel = model;
|
||||||
|
mixer = animMixer;
|
||||||
|
scene.add(currentModel);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Twist animation function
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transition to next scene
|
||||||
|
function startTransition() {
|
||||||
|
if (isTransitioning || currentScene >= 2) return;
|
||||||
|
|
||||||
|
isTransitioning = true;
|
||||||
|
isTwisting = true;
|
||||||
|
twistProgress = 0;
|
||||||
|
transitionStartTime = performance.now();
|
||||||
|
|
||||||
|
// Load next model
|
||||||
|
let nextModelFile = '';
|
||||||
|
let nextModelType = '';
|
||||||
|
|
||||||
|
if (currentScene === 0) {
|
||||||
|
nextModelFile = 'agility.glb';
|
||||||
|
nextModelType = 'agility';
|
||||||
|
} else if (currentScene === 1) {
|
||||||
|
nextModelFile = 'storytelling.glb';
|
||||||
|
nextModelType = 'storytelling';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextModelFile) {
|
||||||
|
loadModel(nextModelFile, nextModelType, (model, animMixer) => {
|
||||||
|
nextModel = model;
|
||||||
|
nextMixer = animMixer;
|
||||||
|
|
||||||
|
// Start next model as invisible and positioned below
|
||||||
|
nextModel.position.y = -10;
|
||||||
|
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) {
|
||||||
|
// Move next model to center and fade in
|
||||||
|
nextModel.position.y = -10 + (easedProgress * 10);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Remove current model
|
||||||
|
if (currentModel) {
|
||||||
|
scene.remove(currentModel);
|
||||||
|
|
||||||
|
// Clean up geometry user data
|
||||||
|
currentModel.traverse((obj) => {
|
||||||
|
if (obj.geometry && obj.geometry.userData.originalPositions) {
|
||||||
|
delete obj.geometry.userData.originalPositions;
|
||||||
|
delete obj.geometry.userData.bounds;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 === 2) { // Keep transparency for storytelling glass
|
||||||
|
mat.transparent = mat.transmission > 0;
|
||||||
|
} else {
|
||||||
|
mat.transparent = mat.transmission > 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj.material.opacity = 1;
|
||||||
|
if (currentScene === 2) { // 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++;
|
||||||
|
scrollCount = 0;
|
||||||
|
|
||||||
|
console.log(`Transition complete. Current scene: ${currentScene}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll event handler
|
||||||
|
function onMouseScroll(event) {
|
||||||
|
// Only count downward scrolls and if not currently transitioning
|
||||||
|
if (!isTransitioning && event.deltaY > 0) {
|
||||||
|
scrollCount++;
|
||||||
|
console.log(`Scroll count: ${scrollCount}`);
|
||||||
|
|
||||||
|
if (scrollCount >= scrollThreshold) {
|
||||||
|
startTransition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach scroll event listener
|
||||||
|
window.addEventListener('wheel', onMouseScroll, {passive: true});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turntable rotation for current model
|
||||||
|
if (currentModel && !isTransitioning) {
|
||||||
|
autoRotationAngle += delta * 0.5;
|
||||||
|
currentModel.rotation.y = autoRotationAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.update();
|
||||||
|
composer.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
701
src/main.js
Normal file
701
src/main.js
Normal file
|
@ -0,0 +1,701 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Transition state management
|
||||||
|
let currentScene = 0; // 0: innovation, 1: agility, 2: 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;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// Clear thick glass for innovation
|
||||||
|
const innovationGlassMaterial = 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
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const dracoLoader = new DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
|
||||||
|
// 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 === '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;
|
||||||
|
|
||||||
|
// Create inner glass shell
|
||||||
|
const innerShell = object.clone();
|
||||||
|
innerShell.material = innovationGlassMaterial.clone();
|
||||||
|
innerShell.material.side = THREE.DoubleSide;
|
||||||
|
innerShell.material.depthWrite = false;
|
||||||
|
innerShell.material.thickness = 4;
|
||||||
|
innerShell.material.transmission = 0.8;
|
||||||
|
innerShell.renderOrder = 1;
|
||||||
|
innerShell.scale.multiplyScalar(0.95);
|
||||||
|
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);
|
||||||
|
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
// Only set camera position if it's not already positioned (avoid reset during transitions)
|
||||||
|
// Increased distance multiplier from 2 to 2.5 for further camera position
|
||||||
|
if (!isTransitioning) {
|
||||||
|
const cameraDistance = maxDim * 2.5;
|
||||||
|
targetCamera.position.set(0, 0, cameraDistance);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
|
||||||
|
// Set distance limits to lock the camera at this distance
|
||||||
|
controls.minDistance = cameraDistance;
|
||||||
|
controls.maxDistance = cameraDistance;
|
||||||
|
|
||||||
|
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 === '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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load model function
|
||||||
|
function loadModel(filename, modelType, onLoadCallback) {
|
||||||
|
loader.load(`/${filename}`, (gltf) => {
|
||||||
|
const model = gltf.scene;
|
||||||
|
|
||||||
|
// Apply materials
|
||||||
|
applyMaterials(model, modelType);
|
||||||
|
|
||||||
|
// Setup animations
|
||||||
|
const animMixer = setupAnimations(model, gltf, modelType);
|
||||||
|
|
||||||
|
// Center and frame model
|
||||||
|
centerAndFrameModel(model);
|
||||||
|
|
||||||
|
if (onLoadCallback) {
|
||||||
|
onLoadCallback(model, animMixer);
|
||||||
|
}
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error(`Error loading ${filename}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial innovation model
|
||||||
|
loadModel('innovation.glb', 'innovation', (model, animMixer) => {
|
||||||
|
currentModel = model;
|
||||||
|
mixer = animMixer;
|
||||||
|
scene.add(currentModel);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transition to next or previous scene
|
||||||
|
function startTransition(direction = 1) {
|
||||||
|
if (isTransitioning) return;
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if (direction > 0 && currentScene >= 2) return; // Can't go forward from storytelling
|
||||||
|
if (direction < 0 && currentScene <= 0) return; // Can't go backward from innovation
|
||||||
|
|
||||||
|
isTransitioning = true;
|
||||||
|
isTwisting = true;
|
||||||
|
twistProgress = 0;
|
||||||
|
transitionStartTime = performance.now();
|
||||||
|
transitionDirection = direction;
|
||||||
|
|
||||||
|
// Determine next model based on direction
|
||||||
|
let nextModelFile = '';
|
||||||
|
let nextModelType = '';
|
||||||
|
|
||||||
|
if (direction > 0) {
|
||||||
|
// Moving forward
|
||||||
|
if (currentScene === 0) {
|
||||||
|
nextModelFile = 'agility.glb';
|
||||||
|
nextModelType = 'agility';
|
||||||
|
} else if (currentScene === 1) {
|
||||||
|
nextModelFile = 'storytelling.glb';
|
||||||
|
nextModelType = 'storytelling';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Moving backward
|
||||||
|
if (currentScene === 1) {
|
||||||
|
nextModelFile = 'innovation.glb';
|
||||||
|
nextModelType = 'innovation';
|
||||||
|
} else if (currentScene === 2) {
|
||||||
|
nextModelFile = 'agility.glb';
|
||||||
|
nextModelType = 'agility';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextModelFile) {
|
||||||
|
loadModel(nextModelFile, nextModelType, (model, animMixer) => {
|
||||||
|
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) {
|
||||||
|
// Remove current model
|
||||||
|
if (currentModel) {
|
||||||
|
scene.remove(currentModel);
|
||||||
|
|
||||||
|
// Clean up geometry user data
|
||||||
|
currentModel.traverse((obj) => {
|
||||||
|
if (obj.geometry && obj.geometry.userData.originalPositions) {
|
||||||
|
delete obj.geometry.userData.originalPositions;
|
||||||
|
delete obj.geometry.userData.bounds;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 === 2) { // Keep transparency for storytelling glass
|
||||||
|
mat.transparent = mat.transmission > 0;
|
||||||
|
} else {
|
||||||
|
mat.transparent = mat.transmission > 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj.material.opacity = 1;
|
||||||
|
if (currentScene === 2) { // 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;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach scroll event listener
|
||||||
|
window.addEventListener('wheel', onMouseScroll, {passive: true});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turntable rotation for current model
|
||||||
|
if (currentModel && !isTransitioning) {
|
||||||
|
autoRotationAngle += delta * 0.5;
|
||||||
|
currentModel.rotation.y = autoRotationAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.update();
|
||||||
|
composer.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
8
src/style.css
Normal file
8
src/style.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
10
vite.config.js
Normal file
10
vite.config.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
allowedHosts: [
|
||||||
|
'dlozi.aiquiral.me'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in a new issue