333 lines
11 KiB
HTML
333 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Three.js - Material Gallery</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#info {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
color: white;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
max-width: 400px;
|
|
z-index: 100;
|
|
}
|
|
#info h2 {
|
|
margin: 0 0 10px 0;
|
|
font-size: 18px;
|
|
}
|
|
#info .web-source {
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid rgba(255,255,255,0.3);
|
|
font-size: 12px;
|
|
opacity: 0.8;
|
|
}
|
|
.label {
|
|
position: absolute;
|
|
color: white;
|
|
font-size: 12px;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
pointer-events: none;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="info">
|
|
<h2>Material Gallery</h2>
|
|
<p><strong>Technique:</strong> Comparing Three.js material types</p>
|
|
<p><strong>Learning:</strong> Material properties: roughness, metalness, color, emissive</p>
|
|
<div class="web-source">
|
|
<strong>Web Source:</strong><br>
|
|
<a href="https://threejs.org/docs/#api/en/materials/Material" target="_blank" style="color: #4fc3f7;">Three.js Materials Documentation</a><br>
|
|
<em>Applied: Six different material types with varying properties to showcase how materials interact with lighting</em>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
// Scene setup
|
|
let camera, scene, renderer, controls;
|
|
let spheres = [];
|
|
let labels = [];
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
// Camera setup
|
|
camera = new THREE.PerspectiveCamera(
|
|
60,
|
|
window.innerWidth / window.innerHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
camera.position.set(0, 3, 12);
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x1a1a2e);
|
|
|
|
// Add subtle fog for depth
|
|
scene.fog = new THREE.Fog(0x1a1a2e, 15, 30);
|
|
|
|
// Renderer
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1;
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// Orbit Controls for interactive camera movement
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.minDistance = 5;
|
|
controls.maxDistance = 20;
|
|
|
|
// Create visualization
|
|
createVisualization();
|
|
|
|
// Handle resize
|
|
window.addEventListener('resize', onWindowResize);
|
|
}
|
|
|
|
function createVisualization() {
|
|
// Lighting setup - multiple lights to showcase material differences
|
|
|
|
// Main directional light from upper right
|
|
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
|
dirLight.position.set(5, 10, 5);
|
|
scene.add(dirLight);
|
|
|
|
// Fill light from the left to show material details
|
|
const fillLight = new THREE.DirectionalLight(0x6495ed, 0.6);
|
|
fillLight.position.set(-5, 3, 2);
|
|
scene.add(fillLight);
|
|
|
|
// Back light for rim lighting effect
|
|
const backLight = new THREE.DirectionalLight(0xff8c42, 0.4);
|
|
backLight.position.set(0, 3, -5);
|
|
scene.add(backLight);
|
|
|
|
// Ambient light for base illumination
|
|
const ambientLight = new THREE.AmbientLight(0x404060, 0.3);
|
|
scene.add(ambientLight);
|
|
|
|
// Add point light that will orbit
|
|
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
|
|
pointLight.position.set(0, 5, 5);
|
|
scene.add(pointLight);
|
|
|
|
// Visual helper for the point light
|
|
const pointLightHelper = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.1, 16, 16),
|
|
new THREE.MeshBasicMaterial({ color: 0xffff00 })
|
|
);
|
|
pointLightHelper.position.copy(pointLight.position);
|
|
scene.add(pointLightHelper);
|
|
|
|
// Create ground plane for context and reflections
|
|
const groundGeometry = new THREE.PlaneGeometry(30, 30);
|
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x1a1a2e,
|
|
roughness: 0.8,
|
|
metalness: 0.2
|
|
});
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
ground.rotation.x = -Math.PI / 2;
|
|
ground.position.y = -2;
|
|
ground.receiveShadow = true;
|
|
scene.add(ground);
|
|
|
|
// Material definitions - showcasing different material types
|
|
const geometry = new THREE.SphereGeometry(1, 64, 64);
|
|
|
|
const materials = [
|
|
{
|
|
name: 'Basic',
|
|
material: new THREE.MeshBasicMaterial({
|
|
color: 0xff6b6b
|
|
}),
|
|
description: 'No lighting interaction'
|
|
},
|
|
{
|
|
name: 'Lambert',
|
|
material: new THREE.MeshLambertMaterial({
|
|
color: 0x4ecdc4,
|
|
emissive: 0x001111
|
|
}),
|
|
description: 'Matte, diffuse surface'
|
|
},
|
|
{
|
|
name: 'Phong',
|
|
material: new THREE.MeshPhongMaterial({
|
|
color: 0xf7b801,
|
|
shininess: 100,
|
|
specular: 0xffffff
|
|
}),
|
|
description: 'Shiny, specular highlights'
|
|
},
|
|
{
|
|
name: 'Standard (Rough)',
|
|
material: new THREE.MeshStandardMaterial({
|
|
color: 0x95e1d3,
|
|
roughness: 0.8,
|
|
metalness: 0.2
|
|
}),
|
|
description: 'PBR - rough surface'
|
|
},
|
|
{
|
|
name: 'Standard (Metal)',
|
|
material: new THREE.MeshStandardMaterial({
|
|
color: 0xc7ceea,
|
|
roughness: 0.2,
|
|
metalness: 0.9
|
|
}),
|
|
description: 'PBR - metallic surface'
|
|
},
|
|
{
|
|
name: 'Normal',
|
|
material: new THREE.MeshNormalMaterial(),
|
|
description: 'Surface normals as colors'
|
|
}
|
|
];
|
|
|
|
// Arrange spheres in two rows of three
|
|
const spacing = 3;
|
|
const rowOffset = 2;
|
|
|
|
materials.forEach((matInfo, index) => {
|
|
const sphere = new THREE.Mesh(geometry, matInfo.material);
|
|
|
|
// Position in grid: 3 columns, 2 rows
|
|
const col = index % 3;
|
|
const row = Math.floor(index / 3);
|
|
|
|
sphere.position.x = (col - 1) * spacing;
|
|
sphere.position.y = (1 - row) * rowOffset;
|
|
sphere.position.z = 0;
|
|
|
|
sphere.castShadow = true;
|
|
sphere.receiveShadow = true;
|
|
|
|
// Store for animation
|
|
sphere.userData = {
|
|
name: matInfo.name,
|
|
initialY: sphere.position.y,
|
|
phaseOffset: index * Math.PI / 3
|
|
};
|
|
|
|
spheres.push(sphere);
|
|
scene.add(sphere);
|
|
|
|
// Create text label
|
|
createLabel(matInfo.name, sphere);
|
|
});
|
|
|
|
// Store point light for animation
|
|
scene.userData.pointLight = pointLight;
|
|
scene.userData.pointLightHelper = pointLightHelper;
|
|
}
|
|
|
|
function createLabel(text, sphere) {
|
|
const label = document.createElement('div');
|
|
label.className = 'label';
|
|
label.textContent = text;
|
|
document.body.appendChild(label);
|
|
labels.push({ element: label, sphere: sphere });
|
|
}
|
|
|
|
function updateLabels() {
|
|
labels.forEach(({ element, sphere }) => {
|
|
// Get sphere position in screen space
|
|
const vector = new THREE.Vector3();
|
|
sphere.getWorldPosition(vector);
|
|
|
|
// Offset label below sphere
|
|
vector.y -= 1.5;
|
|
|
|
vector.project(camera);
|
|
|
|
const x = (vector.x * 0.5 + 0.5) * window.innerWidth;
|
|
const y = (-(vector.y * 0.5) + 0.5) * window.innerHeight;
|
|
|
|
element.style.left = `${x}px`;
|
|
element.style.top = `${y}px`;
|
|
|
|
// Hide label if behind camera
|
|
element.style.display = vector.z > 1 ? 'none' : 'block';
|
|
});
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const time = Date.now() * 0.001;
|
|
|
|
// Rotate all spheres to show material properties from different angles
|
|
spheres.forEach((sphere, index) => {
|
|
sphere.rotation.y = time * 0.3;
|
|
sphere.rotation.x = Math.sin(time * 0.2 + sphere.userData.phaseOffset) * 0.2;
|
|
|
|
// Gentle floating animation
|
|
sphere.position.y = sphere.userData.initialY + Math.sin(time * 0.5 + sphere.userData.phaseOffset) * 0.2;
|
|
});
|
|
|
|
// Orbit the point light around the scene
|
|
if (scene.userData.pointLight) {
|
|
const radius = 8;
|
|
scene.userData.pointLight.position.x = Math.cos(time * 0.5) * radius;
|
|
scene.userData.pointLight.position.z = Math.sin(time * 0.5) * radius;
|
|
scene.userData.pointLightHelper.position.copy(scene.userData.pointLight.position);
|
|
}
|
|
|
|
// Update controls
|
|
controls.update();
|
|
|
|
// Update label positions
|
|
updateLabels();
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|