250 lines
9.1 KiB
HTML
250 lines
9.1 KiB
HTML
<!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>
|