497 lines
18 KiB
HTML
497 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Three.js 技术分享 Demo</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 {
|
|
text-align: center;
|
|
color: white;
|
|
margin-bottom: 30px;
|
|
font-size: 2.5em;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
.demo-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.demo-card {
|
|
background: white;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
transition: transform 0.3s;
|
|
}
|
|
.demo-card:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
.demo-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 15px;
|
|
color: white;
|
|
}
|
|
.demo-header h3 {
|
|
margin: 0;
|
|
font-size: 1.3em;
|
|
}
|
|
.demo-header p {
|
|
margin: 5px 0 0 0;
|
|
opacity: 0.9;
|
|
font-size: 0.9em;
|
|
}
|
|
.canvas-container {
|
|
width: 100%;
|
|
height: 350px;
|
|
background: #f0f0f0;
|
|
position: relative;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.controls {
|
|
padding: 15px;
|
|
background: #f9f9f9;
|
|
}
|
|
.control-group {
|
|
margin-bottom: 10px;
|
|
}
|
|
.control-group label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
button {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
margin-right: 10px;
|
|
margin-bottom: 5px;
|
|
transition: transform 0.2s;
|
|
}
|
|
button:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
input[type="range"] {
|
|
width: 100%;
|
|
}
|
|
.info-panel {
|
|
background: white;
|
|
border-radius: 15px;
|
|
padding: 30px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
}
|
|
.info-panel h2 {
|
|
color: #667eea;
|
|
margin-bottom: 15px;
|
|
}
|
|
.info-panel ul {
|
|
margin-left: 20px;
|
|
line-height: 1.8;
|
|
}
|
|
.concept-box {
|
|
background: #f0f4ff;
|
|
padding: 15px;
|
|
border-left: 4px solid #667eea;
|
|
margin: 10px 0;
|
|
border-radius: 5px;
|
|
}
|
|
.concept-box strong {
|
|
color: #667eea;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🎨 Three.js 技术分享 - 互动 Demo</h1>
|
|
|
|
<div class="demo-grid">
|
|
<!-- Demo 1: 旋转立方体 -->
|
|
<div class="demo-card">
|
|
<div class="demo-header">
|
|
<h3>Demo 1: Hello Cube</h3>
|
|
<p>最基础的 Three.js 场景:旋转的彩色立方体</p>
|
|
</div>
|
|
<div class="canvas-container" id="demo1"></div>
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<label>旋转速度:</label>
|
|
<input type="range" id="speed1" min="0" max="0.05" step="0.001" value="0.01">
|
|
</div>
|
|
<button onclick="changeColor1()">随机颜色</button>
|
|
<button onclick="toggleRotation1()">暂停/继续</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Demo 2: 交互式场景 -->
|
|
<div class="demo-card">
|
|
<div class="demo-header">
|
|
<h3>Demo 2: 交互式多物体</h3>
|
|
<p>鼠标控制相机,点击物体改变颜色</p>
|
|
</div>
|
|
<div class="canvas-container" id="demo2"></div>
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<label>光照强度:</label>
|
|
<input type="range" id="lightIntensity" min="0" max="3" step="0.1" value="1">
|
|
</div>
|
|
<button onclick="addSphere()">添加球体</button>
|
|
<button onclick="resetScene2()">重置场景</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Demo 3: 动画场景 -->
|
|
<div class="demo-card">
|
|
<div class="demo-header">
|
|
<h3>Demo 3: 粒子系统</h3>
|
|
<p>动态粒子效果与后处理</p>
|
|
</div>
|
|
<div class="canvas-container" id="demo3"></div>
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<label>粒子数量: <span id="particleCount">1000</span></label>
|
|
<input type="range" id="particleSlider" min="500" max="5000" step="100" value="1000">
|
|
</div>
|
|
<button onclick="changeParticleColor()">改变颜色</button>
|
|
<button onclick="resetParticles()">重置粒子</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-panel">
|
|
<h2>💡 Three.js 核心概念速记</h2>
|
|
<div class="concept-box">
|
|
<strong>Scene(场景)</strong> - 电影片场,所有3D对象的容器
|
|
</div>
|
|
<div class="concept-box">
|
|
<strong>Camera(相机)</strong> - 观众的眼睛,决定从哪个角度看场景
|
|
</div>
|
|
<div class="concept-box">
|
|
<strong>Renderer(渲染器)</strong> - 摄影机,把3D场景渲染成2D图像
|
|
</div>
|
|
<div class="concept-box">
|
|
<strong>Mesh = Geometry + Material</strong> - 网格 = 形状骨架 + 表面材质
|
|
</div>
|
|
<div class="concept-box">
|
|
<strong>Light(光源)</strong> - 片场的灯光,让物体可见
|
|
</div>
|
|
|
|
<h2 style="margin-top: 30px;">🎯 与建模师沟通清单</h2>
|
|
<ul>
|
|
<li>✅ 文件格式:优先使用 <strong>GLTF/GLB</strong> 格式</li>
|
|
<li>✅ 面数控制:移动端 <5万面,PC端 <20万面</li>
|
|
<li>✅ 贴图尺寸:建议 2K 以内,使用 2 的幂次方(512, 1024, 2048)</li>
|
|
<li>✅ 材质类型:推荐 <strong>PBR 材质</strong>(基于物理的渲染)</li>
|
|
<li>✅ 命名规范:使用英文,有意义的名称</li>
|
|
<li>✅ 坐标系统:统一使用米制单位</li>
|
|
<li>✅ 轴心点:确认物体中心点位置是否正确</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
// ========== Demo 1: 基础旋转立方体 ==========
|
|
let scene1, camera1, renderer1, cube1, isRotating1 = true;
|
|
|
|
function initDemo1() {
|
|
const container = document.getElementById('demo1');
|
|
scene1 = new THREE.Scene();
|
|
scene1.background = new THREE.Color(0x1a1a2e);
|
|
|
|
camera1 = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
|
camera1.position.z = 5;
|
|
|
|
renderer1 = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer1.setSize(container.clientWidth, container.clientHeight);
|
|
container.appendChild(renderer1.domElement);
|
|
|
|
const geometry = new THREE.BoxGeometry(2, 2, 2);
|
|
const material = new THREE.MeshPhongMaterial({
|
|
color: 0x00ff88,
|
|
shininess: 100
|
|
});
|
|
cube1 = new THREE.Mesh(geometry, material);
|
|
scene1.add(cube1);
|
|
|
|
const light = new THREE.PointLight(0xffffff, 1, 100);
|
|
light.position.set(5, 5, 5);
|
|
scene1.add(light);
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040);
|
|
scene1.add(ambientLight);
|
|
|
|
animate1();
|
|
}
|
|
|
|
function animate1() {
|
|
requestAnimationFrame(animate1);
|
|
if (isRotating1) {
|
|
const speed = parseFloat(document.getElementById('speed1').value);
|
|
cube1.rotation.x += speed;
|
|
cube1.rotation.y += speed;
|
|
}
|
|
renderer1.render(scene1, camera1);
|
|
}
|
|
|
|
function changeColor1() {
|
|
cube1.material.color.setHex(Math.random() * 0xffffff);
|
|
}
|
|
|
|
function toggleRotation1() {
|
|
isRotating1 = !isRotating1;
|
|
}
|
|
|
|
// ========== Demo 2: 交互式场景 ==========
|
|
let scene2, camera2, renderer2, objects2 = [], pointLight2;
|
|
|
|
function initDemo2() {
|
|
const container = document.getElementById('demo2');
|
|
scene2 = new THREE.Scene();
|
|
scene2.background = new THREE.Color(0x16213e);
|
|
|
|
camera2 = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
|
camera2.position.set(0, 3, 8);
|
|
camera2.lookAt(0, 0, 0);
|
|
|
|
renderer2 = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer2.setSize(container.clientWidth, container.clientHeight);
|
|
container.appendChild(renderer2.domElement);
|
|
|
|
// 添加地面
|
|
const planeGeometry = new THREE.PlaneGeometry(20, 20);
|
|
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x0f3460 });
|
|
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
|
|
plane.rotation.x = -Math.PI / 2;
|
|
scene2.add(plane);
|
|
|
|
// 添加初始物体
|
|
createInitialObjects();
|
|
|
|
// 光源
|
|
pointLight2 = new THREE.PointLight(0xffffff, 1);
|
|
pointLight2.position.set(0, 5, 5);
|
|
scene2.add(pointLight2);
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040);
|
|
scene2.add(ambientLight);
|
|
|
|
// 鼠标控制
|
|
let isDragging = false;
|
|
let previousMousePosition = { x: 0, y: 0 };
|
|
|
|
renderer2.domElement.addEventListener('mousedown', () => isDragging = true);
|
|
renderer2.domElement.addEventListener('mouseup', () => isDragging = false);
|
|
renderer2.domElement.addEventListener('mousemove', (e) => {
|
|
if (isDragging) {
|
|
const deltaX = e.offsetX - previousMousePosition.x;
|
|
const deltaY = e.offsetY - previousMousePosition.y;
|
|
camera2.position.x += deltaX * 0.01;
|
|
camera2.position.y -= deltaY * 0.01;
|
|
camera2.lookAt(0, 0, 0);
|
|
}
|
|
previousMousePosition = { x: e.offsetX, y: e.offsetY };
|
|
});
|
|
|
|
// 点击改变颜色
|
|
renderer2.domElement.addEventListener('click', (e) => {
|
|
const mouse = new THREE.Vector2(
|
|
(e.offsetX / container.clientWidth) * 2 - 1,
|
|
-(e.offsetY / container.clientHeight) * 2 + 1
|
|
);
|
|
const raycaster = new THREE.Raycaster();
|
|
raycaster.setFromCamera(mouse, camera2);
|
|
const intersects = raycaster.intersectObjects(objects2);
|
|
if (intersects.length > 0) {
|
|
intersects[0].object.material.color.setHex(Math.random() * 0xffffff);
|
|
}
|
|
});
|
|
|
|
document.getElementById('lightIntensity').addEventListener('input', (e) => {
|
|
pointLight2.intensity = parseFloat(e.target.value);
|
|
});
|
|
|
|
animate2();
|
|
}
|
|
|
|
function createInitialObjects() {
|
|
const geometries = [
|
|
new THREE.BoxGeometry(1, 1, 1),
|
|
new THREE.SphereGeometry(0.6, 32, 32),
|
|
new THREE.ConeGeometry(0.5, 1, 32),
|
|
new THREE.TorusGeometry(0.5, 0.2, 16, 100)
|
|
];
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: Math.random() * 0xffffff,
|
|
metalness: 0.5,
|
|
roughness: 0.5
|
|
});
|
|
const mesh = new THREE.Mesh(geometries[i], material);
|
|
mesh.position.set((i - 1.5) * 2, 1, 0);
|
|
scene2.add(mesh);
|
|
objects2.push(mesh);
|
|
}
|
|
}
|
|
|
|
function animate2() {
|
|
requestAnimationFrame(animate2);
|
|
objects2.forEach((obj, i) => {
|
|
obj.rotation.y += 0.01;
|
|
obj.position.y = 1 + Math.sin(Date.now() * 0.001 + i) * 0.3;
|
|
});
|
|
renderer2.render(scene2, camera2);
|
|
}
|
|
|
|
function addSphere() {
|
|
const geometry = new THREE.SphereGeometry(0.5, 32, 32);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: Math.random() * 0xffffff,
|
|
metalness: 0.5,
|
|
roughness: 0.5
|
|
});
|
|
const sphere = new THREE.Mesh(geometry, material);
|
|
sphere.position.set(Math.random() * 6 - 3, 1, Math.random() * 6 - 3);
|
|
scene2.add(sphere);
|
|
objects2.push(sphere);
|
|
}
|
|
|
|
function resetScene2() {
|
|
objects2.forEach(obj => scene2.remove(obj));
|
|
objects2 = [];
|
|
createInitialObjects();
|
|
}
|
|
|
|
// ========== Demo 3: 粒子系统 ==========
|
|
let scene3, camera3, renderer3, particles3;
|
|
|
|
function initDemo3() {
|
|
const container = document.getElementById('demo3');
|
|
scene3 = new THREE.Scene();
|
|
scene3.background = new THREE.Color(0x000000);
|
|
|
|
camera3 = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
|
|
camera3.position.z = 50;
|
|
|
|
renderer3 = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer3.setSize(container.clientWidth, container.clientHeight);
|
|
container.appendChild(renderer3.domElement);
|
|
|
|
createParticles(1000);
|
|
|
|
document.getElementById('particleSlider').addEventListener('input', (e) => {
|
|
const count = parseInt(e.target.value);
|
|
document.getElementById('particleCount').textContent = count;
|
|
scene3.remove(particles3);
|
|
createParticles(count);
|
|
});
|
|
|
|
animate3();
|
|
}
|
|
|
|
function createParticles(count) {
|
|
const geometry = new THREE.BufferGeometry();
|
|
const positions = [];
|
|
const colors = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
positions.push(
|
|
Math.random() * 100 - 50,
|
|
Math.random() * 100 - 50,
|
|
Math.random() * 100 - 50
|
|
);
|
|
colors.push(Math.random(), Math.random(), Math.random());
|
|
}
|
|
|
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
|
|
const material = new THREE.PointsMaterial({
|
|
size: 0.5,
|
|
vertexColors: true,
|
|
transparent: true,
|
|
opacity: 0.8
|
|
});
|
|
|
|
particles3 = new THREE.Points(geometry, material);
|
|
scene3.add(particles3);
|
|
}
|
|
|
|
function animate3() {
|
|
requestAnimationFrame(animate3);
|
|
particles3.rotation.y += 0.002;
|
|
particles3.rotation.x += 0.001;
|
|
|
|
const positions = particles3.geometry.attributes.position.array;
|
|
for (let i = 0; i < positions.length; i += 3) {
|
|
positions[i + 1] += Math.sin(Date.now() * 0.001 + i) * 0.01;
|
|
}
|
|
particles3.geometry.attributes.position.needsUpdate = true;
|
|
|
|
renderer3.render(scene3, camera3);
|
|
}
|
|
|
|
function changeParticleColor() {
|
|
const colors = particles3.geometry.attributes.color.array;
|
|
for (let i = 0; i < colors.length; i += 3) {
|
|
colors[i] = Math.random();
|
|
colors[i + 1] = Math.random();
|
|
colors[i + 2] = Math.random();
|
|
}
|
|
particles3.geometry.attributes.color.needsUpdate = true;
|
|
}
|
|
|
|
function resetParticles() {
|
|
const count = parseInt(document.getElementById('particleSlider').value);
|
|
scene3.remove(particles3);
|
|
createParticles(count);
|
|
}
|
|
|
|
// 初始化所有 Demo
|
|
window.onload = () => {
|
|
initDemo1();
|
|
initDemo2();
|
|
initDemo3();
|
|
};
|
|
|
|
// 响应式处理
|
|
window.addEventListener('resize', () => {
|
|
const containers = ['demo1', 'demo2', 'demo3'];
|
|
const cameras = [camera1, camera2, camera3];
|
|
const renderers = [renderer1, renderer2, renderer3];
|
|
|
|
containers.forEach((id, i) => {
|
|
const container = document.getElementById(id);
|
|
cameras[i].aspect = container.clientWidth / container.clientHeight;
|
|
cameras[i].updateProjectionMatrix();
|
|
renderers[i].setSize(container.clientWidth, container.clientHeight);
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |