Three.js 实战案例 #
通过实战案例,将前面学习的知识应用到实际项目中,巩固所学技能。
案例一:3D 产品展示 #
功能需求 #
- 360° 产品旋转展示
- 鼠标拖拽旋转
- 自动旋转
- 材质切换
- 缩放查看细节
完整代码 #
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
class ProductViewer {
constructor(container) {
this.container = container;
this.autoRotate = true;
this.currentMaterial = 0;
this.init();
this.loadModel();
this.setupEventListeners();
this.animate();
}
init() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xf5f5f5);
const aspect = this.container.clientWidth / this.container.clientHeight;
this.camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
this.camera.position.set(0, 1, 5);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.0;
this.container.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.minDistance = 2;
this.controls.maxDistance = 10;
this.controls.maxPolarAngle = Math.PI / 2;
this.setupLights();
this.setupGround();
}
setupLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const mainLight = new THREE.DirectionalLight(0xffffff, 1);
mainLight.position.set(5, 10, 5);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
mainLight.shadow.camera.near = 0.5;
mainLight.shadow.camera.far = 50;
mainLight.shadow.camera.left = -10;
mainLight.shadow.camera.right = 10;
mainLight.shadow.camera.top = 10;
mainLight.shadow.camera.bottom = -10;
this.scene.add(mainLight);
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
fillLight.position.set(-5, 5, -5);
this.scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0xffffff, 0.5);
rimLight.position.set(0, 5, -10);
this.scene.add(rimLight);
}
setupGround() {
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.ShadowMaterial({ opacity: 0.3 });
this.ground = new THREE.Mesh(groundGeometry, groundMaterial);
this.ground.rotation.x = -Math.PI / 2;
this.ground.position.y = -0.5;
this.ground.receiveShadow = true;
this.scene.add(this.ground);
}
loadModel() {
const loader = new GLTFLoader();
loader.load(
'product.glb',
(gltf) => {
this.model = gltf.scene;
this.model.scale.set(1, 1, 1);
this.model.position.set(0, 0, 0);
this.model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
this.scene.add(this.model);
this.originalMaterials = this.getMaterials();
},
(progress) => {
const percent = (progress.loaded / progress.total * 100).toFixed(2);
console.log(`Loading: ${percent}%`);
},
(error) => {
console.error('Error loading model:', error);
}
);
}
getMaterials() {
const materials = {};
this.model.traverse((child) => {
if (child.isMesh && child.material) {
materials[child.name] = child.material.clone();
}
});
return materials;
}
changeMaterial(type) {
if (!this.model) return;
const materials = {
gold: new THREE.MeshStandardMaterial({
color: 0xffd700,
metalness: 1.0,
roughness: 0.2
}),
silver: new THREE.MeshStandardMaterial({
color: 0xc0c0c0,
metalness: 1.0,
roughness: 0.3
}),
plastic: new THREE.MeshStandardMaterial({
color: 0xff6b6b,
metalness: 0.0,
roughness: 0.5
})
};
const newMaterial = materials[type];
this.model.traverse((child) => {
if (child.isMesh) {
child.material = newMaterial;
}
});
}
resetMaterial() {
if (!this.model || !this.originalMaterials) return;
this.model.traverse((child) => {
if (child.isMesh && this.originalMaterials[child.name]) {
child.material = this.originalMaterials[child.name].clone();
}
});
}
setupEventListeners() {
window.addEventListener('resize', () => this.onResize());
this.controls.addEventListener('start', () => {
this.autoRotate = false;
});
}
onResize() {
const width = this.container.clientWidth;
const height = this.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
animate() {
requestAnimationFrame(() => this.animate());
if (this.autoRotate && this.model) {
this.model.rotation.y += 0.005;
}
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
const viewer = new ProductViewer(document.getElementById('container'));
案例二:3D 数据可视化 #
功能需求 #
- 3D 柱状图展示
- 数据动态更新
- 鼠标悬停提示
- 动画过渡效果
完整代码 #
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
class DataVisualization {
constructor(container) {
this.container = container;
this.data = [];
this.bars = [];
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.hoveredBar = null;
this.init();
this.setupEventListeners();
this.animate();
}
init() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x1a1a2e);
const aspect = this.container.clientWidth / this.container.clientHeight;
this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000);
this.camera.position.set(15, 15, 15);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.container.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.setupLights();
this.setupGrid();
}
setupLights() {
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(10, 20, 10);
this.scene.add(directionalLight);
}
setupGrid() {
const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
this.scene.add(gridHelper);
const axesHelper = new THREE.AxesHelper(10);
this.scene.add(axesHelper);
}
setData(data) {
this.data = data;
this.createBars();
}
createBars() {
this.bars.forEach((bar) => this.scene.remove(bar));
this.bars = [];
const maxValue = Math.max(...this.data.map((d) => d.value));
const barWidth = 0.8;
const spacing = 1.5;
const startX = -(this.data.length - 1) * spacing / 2;
this.data.forEach((item, index) => {
const height = (item.value / maxValue) * 10;
const geometry = new THREE.BoxGeometry(barWidth, height, barWidth);
geometry.translate(0, height / 2, 0);
const color = new THREE.Color().setHSL(index / this.data.length, 0.7, 0.5);
const material = new THREE.MeshStandardMaterial({
color: color,
metalness: 0.3,
roughness: 0.7
});
const bar = new THREE.Mesh(geometry, material);
bar.position.x = startX + index * spacing;
bar.position.z = 0;
bar.userData = { ...item, index };
this.scene.add(bar);
this.bars.push(bar);
});
}
updateData(newData) {
const maxValue = Math.max(...newData.map((d) => d.value));
newData.forEach((item, index) => {
if (this.bars[index]) {
const targetHeight = (item.value / maxValue) * 10;
const currentHeight = this.bars[index].geometry.parameters.height;
this.animateBar(this.bars[index], currentHeight, targetHeight);
this.bars[index].userData.value = item.value;
}
});
}
animateBar(bar, fromHeight, toHeight) {
const duration = 500;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const currentHeight = fromHeight + (toHeight - fromHeight) * eased;
bar.geometry.dispose();
bar.geometry = new THREE.BoxGeometry(0.8, currentHeight, 0.8);
bar.geometry.translate(0, currentHeight / 2, 0);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
animate();
}
setupEventListeners() {
window.addEventListener('resize', () => this.onResize());
this.container.addEventListener('mousemove', (e) => this.onMouseMove(e));
}
onResize() {
const width = this.container.clientWidth;
const height = this.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
onMouseMove(event) {
const rect = this.container.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.bars);
if (this.hoveredBar) {
this.hoveredBar.material.emissive.setHex(0x000000);
}
if (intersects.length > 0) {
this.hoveredBar = intersects[0].object;
this.hoveredBar.material.emissive.setHex(0x333333);
const data = this.hoveredBar.userData;
this.showTooltip(event, `${data.label}: ${data.value}`);
} else {
this.hoveredBar = null;
this.hideTooltip();
}
}
showTooltip(event, text) {
let tooltip = document.getElementById('tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.id = 'tooltip';
tooltip.style.cssText = `
position: absolute;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 4px;
pointer-events: none;
font-size: 14px;
`;
document.body.appendChild(tooltip);
}
tooltip.textContent = text;
tooltip.style.left = event.clientX + 10 + 'px';
tooltip.style.top = event.clientY + 10 + 'px';
tooltip.style.display = 'block';
}
hideTooltip() {
const tooltip = document.getElementById('tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
}
animate() {
requestAnimationFrame(() => this.animate());
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
const viz = new DataVisualization(document.getElementById('container'));
viz.setData([
{ label: '一月', value: 120 },
{ label: '二月', value: 190 },
{ label: '三月', value: 300 },
{ label: '四月', value: 250 },
{ label: '五月', value: 280 },
{ label: '六月', value: 320 }
]);
案例三:粒子场景 #
功能需求 #
- 大量粒子效果
- 粒子动画
- 鼠标交互
- 性能优化
完整代码 #
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
class ParticleScene {
constructor(container) {
this.container = container;
this.particleCount = 5000;
this.mouse = new THREE.Vector2();
this.mouseWorld = new THREE.Vector3();
this.init();
this.createParticles();
this.setupEventListeners();
this.animate();
}
init() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x000000);
const aspect = this.container.clientWidth / this.container.clientHeight;
this.camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
this.camera.position.set(0, 0, 30);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.container.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.clock = new THREE.Clock();
}
createParticles() {
const positions = new Float32Array(this.particleCount * 3);
const colors = new Float32Array(this.particleCount * 3);
const sizes = new Float32Array(this.particleCount);
const randoms = new Float32Array(this.particleCount);
for (let i = 0; i < this.particleCount; i++) {
const i3 = i * 3;
const radius = Math.random() * 20;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi);
const color = new THREE.Color();
color.setHSL(Math.random(), 0.7, 0.5);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
sizes[i] = Math.random() * 0.5 + 0.1;
randoms[i] = Math.random();
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aColor', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1));
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uMouse: { value: new THREE.Vector3() },
uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }
},
vertexShader: `
uniform float uTime;
uniform vec3 uMouse;
uniform float uPixelRatio;
attribute vec3 aColor;
attribute float aSize;
attribute float aRandom;
varying vec3 vColor;
varying float vAlpha;
void main() {
vColor = aColor;
vec3 pos = position;
float angle = uTime * 0.2 * (1.0 + aRandom * 0.5);
float cosA = cos(angle);
float sinA = sin(angle);
vec3 rotatedPos;
rotatedPos.x = pos.x * cosA - pos.z * sinA;
rotatedPos.y = pos.y + sin(uTime + aRandom * 6.28) * 0.5;
rotatedPos.z = pos.x * sinA + pos.z * cosA;
float dist = distance(rotatedPos, uMouse);
float influence = smoothstep(5.0, 0.0, dist);
rotatedPos += normalize(rotatedPos - uMouse) * influence * 2.0;
vec4 mvPosition = modelViewMatrix * vec4(rotatedPos, 1.0);
gl_Position = projectionMatrix * mvPosition;
float sizeAttenuation = 300.0 / -mvPosition.z;
gl_PointSize = aSize * sizeAttenuation * uPixelRatio;
vAlpha = smoothstep(30.0, 5.0, -mvPosition.z);
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vAlpha;
void main() {
float dist = length(gl_PointCoord - vec2(0.5));
if (dist > 0.5) discard;
float alpha = 1.0 - smoothstep(0.2, 0.5, dist);
alpha *= vAlpha;
gl_FragColor = vec4(vColor, alpha);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
this.particles = new THREE.Points(geometry, material);
this.scene.add(this.particles);
}
setupEventListeners() {
window.addEventListener('resize', () => this.onResize());
this.container.addEventListener('mousemove', (e) => this.onMouseMove(e));
}
onResize() {
const width = this.container.clientWidth;
const height = this.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
this.particles.material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2);
}
onMouseMove(event) {
const rect = this.container.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(this.mouse, this.camera);
const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
raycaster.ray.intersectPlane(plane, this.mouseWorld);
}
animate() {
requestAnimationFrame(() => this.animate());
const elapsedTime = this.clock.getElapsedTime();
this.particles.material.uniforms.uTime.value = elapsedTime;
this.particles.material.uniforms.uMouse.value.copy(this.mouseWorld);
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
const particleScene = new ParticleScene(document.getElementById('container'));
案例四:交互式地球 #
功能需求 #
- 地球模型
- 地点标记
- 点击交互
- 动画效果
完整代码 #
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
class InteractiveGlobe {
constructor(container) {
this.container = container;
this.locations = [];
this.markers = [];
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.init();
this.createGlobe();
this.createStars();
this.setupEventListeners();
this.animate();
}
init() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x000011);
const aspect = this.container.clientWidth / this.container.clientHeight;
this.camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
this.camera.position.set(0, 0, 5);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.container.appendChild(this.renderer.domElement);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.minDistance = 3;
this.controls.maxDistance = 10;
this.controls.enablePan = false;
}
createGlobe() {
const geometry = new THREE.SphereGeometry(1, 64, 64);
const material = new THREE.MeshPhongMaterial({
color: 0x1565c0,
emissive: 0x072536,
shininess: 10
});
this.globe = new THREE.Mesh(geometry, material);
this.scene.add(this.globe);
const atmosphereGeometry = new THREE.SphereGeometry(1.1, 64, 64);
const atmosphereMaterial = new THREE.ShaderMaterial({
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec3 vNormal;
void main() {
float intensity = pow(0.7 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
}
`,
blending: THREE.AdditiveBlending,
side: THREE.BackSide,
transparent: true
});
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
this.scene.add(atmosphere);
const ambientLight = new THREE.AmbientLight(0x333333);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 3, 5);
this.scene.add(directionalLight);
}
createStars() {
const starsGeometry = new THREE.BufferGeometry();
const starsCount = 2000;
const positions = new Float32Array(starsCount * 3);
for (let i = 0; i < starsCount; i++) {
const radius = 50 + Math.random() * 50;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(Math.random() * 2 - 1);
positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i * 3 + 2] = radius * Math.cos(phi);
}
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const starsMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.1
});
const stars = new THREE.Points(starsGeometry, starsMaterial);
this.scene.add(stars);
}
addLocation(lat, lng, name) {
const phi = (90 - lat) * (Math.PI / 180);
const theta = (lng + 180) * (Math.PI / 180);
const x = -Math.sin(phi) * Math.cos(theta);
const y = Math.cos(phi);
const z = Math.sin(phi) * Math.sin(theta);
const markerGeometry = new THREE.SphereGeometry(0.02, 16, 16);
const markerMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const marker = new THREE.Mesh(markerGeometry, markerMaterial);
marker.position.set(x, y, z);
marker.userData = { name, lat, lng };
this.globe.add(marker);
this.markers.push(marker);
}
setupEventListeners() {
window.addEventListener('resize', () => this.onResize());
this.container.addEventListener('click', (e) => this.onClick(e));
}
onResize() {
const width = this.container.clientWidth;
const height = this.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
onClick(event) {
const rect = this.container.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.markers);
if (intersects.length > 0) {
const marker = intersects[0].object;
console.log('Clicked:', marker.userData.name);
marker.material.color.setHex(0x00ff00);
setTimeout(() => {
marker.material.color.setHex(0xff0000);
}, 500);
}
}
animate() {
requestAnimationFrame(() => this.animate());
this.globe.rotation.y += 0.001;
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
const globe = new InteractiveGlobe(document.getElementById('container'));
globe.addLocation(39.9042, 116.4074, '北京');
globe.addLocation(40.7128, -74.0060, '纽约');
globe.addLocation(51.5074, -0.1278, '伦敦');
globe.addLocation(35.6762, 139.6503, '东京');
globe.addLocation(-33.8688, 151.2093, '悉尼');
总结 #
通过这些实战案例,你已经学会了:
- 产品展示:模型加载、材质切换、交互控制
- 数据可视化:动态图表、数据更新、动画过渡
- 粒子场景:GPU 粒子、着色器动画、鼠标交互
- 交互式地球:地理坐标转换、标记点、点击交互
继续实践和探索,创建更多精彩的 3D 应用!
资源推荐 #
最后更新:2026-03-28