Three.js 不仅仅是一个库,它是一个通往 3D 世界的大门。通过它,我们可以在 Web 浏览器中构建出令人惊叹的交互式体验。本教程将带你超越入门,深入了解 Three.js 的核心组件、工作原理以及一些高级技巧,助你构建更复杂、更酷炫的 3D 应用。

“深入 Three.js,你将发现 Web 前端的无限可能性。”


一、Three.js 核心工作流回顾与进阶

在入门教程中,我们介绍了 Three.js 的“四大件”:场景 (Scene)相机 (Camera)渲染器 (Renderer)物体 (Object = Geometry + Material)。它们是构建任何 Three.js 应用的基础。

1.1 渲染管线概览

这个流程图展示了 Three.js 应用的核心渲染循环:

  1. 初始化:设置场景、相机和渲染器。
  2. 构建场景:创建几何体、材质,组合成网格物体,并添加到场景中。添加灯光。
  3. 渲染:在 requestAnimationFrame 循环中,每次迭代都调用 renderer.render(scene, camera),将相机视角下的场景绘制到 canvas 上。
  4. 交互/动画:在每次渲染前,更新物体位置、旋转、相机位置等,实现动画和响应用户交互。

二、深入 Three.js 核心组件

2.1 场景 (Scene)

THREE.Scene 是所有 3D 对象的容器,包括几何体、灯光、相机(有时相机也添加到场景中以方便管理,但渲染时仍需独立传入 renderer.render)。

常用属性/方法

  • scene.add(object): 将对象添加到场景中。
  • scene.remove(object): 从场景中移除对象。
  • scene.children: 包含场景中所有子对象的数组。
  • scene.traverse(callback): 遍历场景中的所有对象及其子对象。
  • scene.background: 设置场景的背景,可以是颜色、纹理、立方体纹理(用于全景天空盒)。
  • scene.fog: 添加雾效。

示例:设置背景和雾效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as THREE from 'three';

const scene = new THREE.Scene();

// 设置纯色背景
scene.background = new THREE.Color(0xF0F0F0); // 浅灰色背景

// 设置纹理背景 (假设你有一个背景图片)
// const textureLoader = new THREE.TextureLoader();
// const bgTexture = textureLoader.load('path/to/your-background.jpg');
// scene.background = bgTexture;

// 添加线性雾效
// 参数: 颜色, 近距离, 远距离
scene.fog = new THREE.Fog(0xCCCCCC, 10, 50); // 从10单位开始,到50单位完全被雾覆盖
// 或者指数雾效 (更浓重)
// scene.fog = new THREE.FogExp2(0xCCCCCC, 0.05); // 颜色, 密度

2.2 相机 (Camera)

相机决定了场景如何被观察。

2.2.1 PerspectiveCamera (透视相机)

最常用的相机,模拟人眼观察效果。

  • fov (Field of View): 视野角度,垂直方向,单位度。
  • aspect (Aspect Ratio): 视口宽高比 (通常是 width / height)。
  • near (Near Clipping Plane): 近裁剪面,此距离以外的物体可见。
  • far (Far Clipping Plane): 远裁剪面,此距离以内且在近裁剪面以外的物体可见。
1
2
3
4
// 创建透视相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 5); // x, y, z
camera.lookAt(0, 0, 0); // 让相机看向场景中心

2.2.2 OrthographicCamera (正交相机)

用于 2D 场景或不需要透视效果的场景(如 CAD 工具、游戏俯视图)。

  • left, right, top, bottom: 定义了裁剪面的范围。
  • near, far: 同透视相机。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建正交相机
// 参数: left, right, top, bottom, near, far
const size = 5; // 视口大小
const aspectRatio = window.innerWidth / window.innerHeight;
const cameraOrtho = new THREE.OrthographicCamera(
-size * aspectRatio, // left
size * aspectRatio, // right
size, // top
-size, // bottom
0.1, // near
1000 // far
);
cameraOrtho.position.set(0, 0, 10);
cameraOrtho.lookAt(0, 0, 0);

// 当窗口大小变化时,需要更新正交相机的裁剪面
window.addEventListener('resize', () => {
const aspectRatio = window.innerWidth / window.innerHeight;
cameraOrtho.left = -size * aspectRatio;
cameraOrtho.right = size * aspectRatio;
cameraOrtho.updateProjectionMatrix(); // 必须调用
});

2.2.3 相机助手 (CameraHelper)

用于可视化相机视锥体,方便调试。

1
2
const helper = new THREE.CameraHelper(camera);
scene.add(helper);

2.3 渲染器 (Renderer)

THREE.WebGLRenderer 是将场景渲染到 canvas 的核心。

常用属性/方法

  • renderer.setSize(width, height): 设置渲染器尺寸。
  • renderer.setPixelRatio(window.devicePixelRatio): 解决高清屏模糊问题,通常设置为设备的像素比。
  • renderer.setClearColor(color, alpha): 设置每次渲染前清除画布的颜色和透明度。
  • renderer.render(scene, camera): 执行渲染操作。
  • renderer.domElement: 渲染器创建的 canvas 元素。

示例:初始化渲染器

1
2
3
4
5
6
7
8
const renderer = new THREE.WebGLRenderer({
antialias: true // 启用抗锯齿,使边缘更平滑
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // 适配Retina屏
document.body.appendChild(renderer.domElement);

// 确保在 animate 循环中调用 renderer.render(scene, camera);

2.4 几何体 (Geometry)

几何体定义了 3D 对象的形状。

常用几何体

  • BoxGeometry(width, height, depth): 立方体
  • SphereGeometry(radius, widthSegments, heightSegments): 球体
  • CylinderGeometry(radiusTop, radiusBottom, height, radialSegments): 圆柱体
  • PlaneGeometry(width, height, widthSegments, heightSegments): 平面
  • TorusGeometry(radius, tube, radialSegments, tubularSegments): 圆环体
  • BufferGeometry: 更底层、更高效的几何体,可以手动定义顶点、法线等数据。大多数内置几何体最终都是 BufferGeometry 的实例。

示例:创建不同几何体

1
2
3
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const sphereGeometry = new THREE.SphereGeometry(0.75, 32, 32); // 半径, 水平分段, 垂直分段
const planeGeometry = new THREE.PlaneGeometry(5, 5);

2.5 材质 (Material)

材质定义了 3D 对象的表面外观,以及它如何与光照互动。

常用材质

  • MeshBasicMaterial: 基础材质,不受光照影响,常用于非写实或调试。
    • color: 颜色。
    • map: 纹理贴图。
    • transparent, opacity: 透明度。
    • wireframe: 线框模式。
  • MeshLambertMaterial: 兰伯特材质,模拟无光泽表面,对漫反射光照有反应。
    • color, map, transparent, opacity, wireframe
  • MeshPhongMaterial: 冯氏材质,模拟有光泽表面,对漫反射和镜面反射光照都有反应。
    • color, map, transparent, opacity, wireframe
    • specular: 镜面反射颜色。
    • shininess: 镜面反射光泽度。
  • MeshStandardMaterial: 标准材质(物理渲染材质),基于PBR(Physically Based Rendering)模型,更真实地模拟物理世界的光照。
    • color, map, transparent, opacity
    • metalness: 金属度 (0-1)。
    • roughness: 粗糙度 (0-1)。
    • 支持更多高级贴图:normalMap(法线贴图), aoMap(环境光遮蔽贴图), displacementMap(置换贴图), envMap(环境贴图) 等。
  • LineBasicMaterial, PointsMaterial: 用于渲染线段和点。

示例:使用物理渲染材质和纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
// 假设你有一个图片文件作为纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('path/to/texture.jpg');
texture.colorSpace = THREE.SRGBColorSpace; // 告诉threejs纹理的颜色空间

const material = new THREE.MeshStandardMaterial({
color: 0xffffff, // 基本颜色 (白色,由纹理覆盖)
map: texture, // 纹理贴图
metalness: 0.5, // 半金属
roughness: 0.7 // 比较粗糙
});

const cube = new THREE.Mesh(boxGeometry, material);

2.6 灯光 (Light)

灯光是让场景栩栩如生的关键。

常用灯光类型

  • AmbientLight(color, intensity): 环境光。均匀地照亮场景中的所有物体,没有方向性,使物体不会完全变黑。
  • DirectionalLight(color, intensity): 平行光。模拟太阳光。光线是平行的,有方向,没有衰减。
    • light.position.set(x, y, z): 设置光源位置。
  • PointLight(color, intensity, distance, decay): 点光源。模拟灯泡,从一个点向所有方向发光,有衰减。
    • light.position.set(x, y, z): 设置光源位置。
  • SpotLight(color, intensity, distance, angle, penumbra, decay): 聚光灯。类似手电筒,从一个点沿一个方向发光,有一个锥形区域和衰减。
    • light.position.set(x, y, z): 设置光源位置。
    • light.target: 控制灯光指向的目标对象(默认为 (0,0,0))。
  • HemisphereLight(skyColor, groundColor, intensity): 半球光。模拟户外环境光,skyColor 模拟天空光,groundColor 模拟地面反射光。

示例:组合不同灯光

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scene.add(new THREE.AmbientLight(0xffffff, 0.4)); // 柔和的环境光

const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 10, 7.5);
dirLight.castShadow = true; // 启用投射阴影 (详见下面阴影部分)
scene.add(dirLight);

const pointLight = new THREE.PointLight(0xff9900, 1, 10); // 橘黄色点光源,衰减距离10
pointLight.position.set(-3, 3, 0);
scene.add(pointLight);

// 灯光助手 (LightHelper) 调试
// scene.add(new THREE.DirectionalLightHelper(dirLight, 1));
// scene.add(new THREE.PointLightHelper(pointLight, 0.5));

2.6.1 阴影 (Shadows)

实现真实的阴影需要几个步骤:

  1. 渲染器启用阴影renderer.shadowMap.enabled = true;
  2. 灯光启用投射阴影light.castShadow = true; (仅 DirectionalLight, PointLight, SpotLight 支持)
    • 对这些灯光,还需要配置其阴影相机的参数 (light.shadow.camera.near, far, left, right, top, bottom) 和阴影贴图尺寸 (light.shadow.mapSize.width, height)。
  3. 物体启用投射/接收阴影
    • mesh.castShadow = true; (此物体投射阴影到其他物体上)
    • mesh.receiveShadow = true; (此物体接收其他物体投射的阴影)

示例:启用阴影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
renderer.shadowMap.enabled = true; // 全局开启阴影

// ... (创建立方体和平面)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true; // 立方体投射阴影
scene.add(cube);

const planeGeometry = new THREE.PlaneGeometry(10, 10);
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2; // 将平面放到底部
plane.position.y = -0.5;
plane.receiveShadow = true; // 平面接收阴影
scene.add(plane);

// ... (创建定向光源)
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 7.5);
dirLight.castShadow = true;

// 配置阴影相机参数 (根据场景大小调整)
dirLight.shadow.mapSize.width = 1024; // 阴影贴图分辨率
dirLight.shadow.mapSize.height = 1024;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;

scene.add(dirLight);

// 可以添加一个 DirectionalLightHelper 来查看阴影相机范围
// scene.add(new THREE.DirectionalLightHelper(dirLight, 1));

三、高级主题

3.1 动画 (Animation)

除了简单地在 animate 循环中改变 positionrotation,Three.js 还支持更复杂的动画。

3.1.1 requestAnimationFrame 循环

这是最基本的动画方式。

1
2
3
4
5
6
7
8
9
10
function animate() {
requestAnimationFrame(animate);

// 每帧递增旋转
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;

renderer.render(scene, camera);
}
animate();

3.1.2 外部动画库 (GSAP)

对于复杂的缓动动画,通常会结合像 GSAP 这样的专业动画库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 假设你已安装 GSAP 并引入
// npm install gsap
// import { gsap } from 'gsap';

// 让立方体在 2 秒内移动到 (2, 2, 0) 并旋转
gsap.to(cube.position, {
duration: 2,
x: 2,
y: 2,
ease: "power2.out"
});

gsap.to(cube.rotation, {
duration: 2,
x: Math.PI * 2, // 旋转360度
ease: "power2.out",
onComplete: () => console.log('动画完成')
});

3.1.3 骨骼动画 (SkinnedMesh)

对于加载的人体或角色模型,Three.js 支持骨骼动画,通过 AnimationMixerAnimationClip 来控制。这通常涉及到从外部模型文件(如 .gltf)中导入动画数据。

3.2 几何变换 (Transformations)

每个 Object3D (包括 Mesh, Light, Camera 等) 都有 position, rotation, scale 属性以及 matrix 等。

  • object.position.set(x, y, z);
  • object.rotation.set(x, y, z, order); (欧拉角,order 为旋转顺序,如 'XYZ')
  • object.rotation.x += 0.01;
  • object.scale.set(x, y, z);
  • object.translateOnAxis(axis, distance); (沿指定轴移动)
  • object.lookAt(targetVector); (使对象看向目标点)

3.3 纹理与贴图 (Textures)

纹理是 3D 对象表面最常用的视觉增强方式。

  • THREE.TextureLoader().load(url): 加载图片纹理。
  • texture.wrapS / texture.wrapT: 设置纹理在 S/T 轴上的包裹方式 (THREE.RepeatWrapping, THREE.ClampToEdgeWrapping)。
  • texture.repeat.set(u, v): 设置纹理重复次数。
  • texture.offset.set(u, v): 设置纹理偏移。
  • texture.rotation: 旋转纹理。

高级贴图

  • normalMap (法线贴图): 模拟表面细节,让物体看起来有凹凸感而无需增加几何体顶点。
  • aoMap (环境光遮蔽贴图): 模拟 crevices/corners 处的阴影。
  • displacementMap (置换贴图): 实际改变几何体的顶点位置以创建物理上的凹凸,需要更多几何细分。
  • roughnessMap / metalnessMap: 控制物理材质的粗糙度和金属度。
  • envMap (环境贴图 / 反射贴图): 模拟环境反射,常用于创建镜面反射或玻璃效果。通常使用 CubeTextureLoader 加载六张图片组成的环境贴图。

示例:加载法线贴图

1
2
3
4
5
6
7
8
9
10
const textureLoader = new THREE.TextureLoader();
const colorTexture = textureLoader.load('path/to/texture_color.jpg');
const normalTexture = textureLoader.load('path/to/texture_normal.jpg');

const material = new THREE.MeshStandardMaterial({
map: colorTexture,
normalMap: normalTexture, // 应用法线贴图
metalness: 0,
roughness: 1
});

3.4 交互 (Interactions)

Three.js 交互通常通过以下方式实现:

  1. 控制器 (Controls): 如 OrbitControls (轨道控制器),PointerLockControls (第一人称射击游戏控制器) 等。
    • 安装: npm install three 后,控制器在 node_modules/three/examples/jsm/controls/ 目录下。
    • CDN 引入: import { OrbitControls } from 'https://unpkg.com/three@0.163.0/examples/jsm/controls/OrbitControls.js';
  2. 射线投射 (Raycaster): 用于检测鼠标点击或触摸事件与 3D 场景中对象的交集,实现拾取、悬停等效果。

示例:使用 Raycaster 进行点击检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import * as THREE from 'three';
// ... 初始化场景、相机、渲染器等

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// 存储可被射线检测的物体
const intersectableObjects = [];
// 假设你有一个立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
intersectableObjects.push(cube); // 将它添加到可检测列表中

// 记录上一次检测到的物体
let intersectedObject = null;
const originalMaterial = cube.material.clone(); // 保存原始材质

function onMouseMove(event) {
// 将鼠标坐标转换为标准化设备坐标 (NDC)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

// 更新射线投射器
raycaster.setFromCamera(mouse, camera);

// 计算物体与射线的交点
const intersects = raycaster.intersectObjects(intersectableObjects, false); // false表示不递归检测子对象

if (intersects.length > 0) {
// 有物体被射线击中
if (intersectedObject != intersects[0].object) {
// 新物体被击中,恢复旧物体的材质(如果有)
if (intersectedObject) {
intersectedObject.material = originalMaterial;
}
intersectedObject = intersects[0].object;
// 改变新击中物体的材质
intersectedObject.material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); // 红色
}
} else {
// 没有物体被击中,恢复旧物体的材质(如果有)
if (intersectedObject) {
intersectedObject.material = originalMaterial;
intersectedObject = null;
}
}
}

window.addEventListener('mousemove', onMouseMove);

3.5 模型加载 (Model Loading)

将外部 3D 模型导入 Three.js 场景是高复杂度应用中不可或缺的一部分。

最常用格式GLTF/GLB (Graphics Library Transmission Format)。它是 3D 资产的开放标准,支持几何体、材质、动画、骨骼等所有数据,且文件体积小。

常用加载器

  • GLTFLoader: 加载 .gltf.glb 模型。
  • OBJLoader: 加载 .obj 模型。
  • FBXLoader: 加载 .fbx 模型。

示例:加载 GLTF 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const loader = new GLTFLoader();

loader.load(
'path/to/your/model.glb', // 模型的路径
function (gltf) {
// 模型加载成功后的回调
scene.add(gltf.scene); // 将加载的场景添加到主场景中

// 遍历模型中的所有网格,并启用阴影
gltf.scene.traverse(function (child) {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
// 确保材质能接收阴影
if (child.material.isMeshStandardMaterial || child.material.isMeshPhongMaterial) {
child.material.needsUpdate = true;
}
}
});

// 如果模型包含动画,可以这样播放:
// const mixer = new THREE.AnimationMixer(gltf.scene);
// gltf.animations.forEach(clip => {
// mixer.clipAction(clip).play();
// });
// // 记得在 animate 循环中更新 mixer: mixer.update(deltaTime);
},
// 加载进度回调
function (xhr) {
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
// 加载失败回调
function (error) {
console.error('An error happened', error);
}
);

3.6 性能优化

大规模 3D 场景的性能优化至关重要。

  • 减少绘制调用 (Draw Calls)
    • 合并几何体 (BufferGeometryUtils.mergeBufferGeometries)。
    • 使用多材质 (Mesh.material 属性可以是一个数组)。
    • 使用实例化 (InstancedMesh) 绘制大量相同几何体。
  • 优化几何体
    • 使用低多边形模型。
    • 移除不必要的面和顶点。
    • 禁用背面剔除 (material.side = THREE.FrontSide;) 如果不必要。
  • 优化纹理
    • 使用合适尺寸的纹理。
    • 开启 texture.mipmaps(默认开启,但需要了解)。
    • 使用压缩纹理格式(如 KTX2)。
  • 着色器优化
    • 避免在着色器中进行复杂计算。
    • 使用 gl_Position 代替 position * matrix(Three.js 会自动优化)。
  • 阴影优化
    • 调整 shadow.mapSizeshadow.camera 范围。
    • 减少投射阴影的灯光数量。
  • Dispose 资源:在 scene.remove() 对象后,还需要手动释放其几何体、材质和纹理在 GPU 上的内存:
    1
    2
    3
    4
    myMesh.geometry.dispose();
    myMesh.material.dispose();
    if (myMesh.material.map) myMesh.material.map.dispose();
    scene.remove(myMesh); // 移除实际对象

四、项目结构与开发实践

对于更复杂的 Three.js 项目,良好的结构至关重要。

  1. 模块化:将场景初始化、几何体创建、动画逻辑等分别放在不同的模块文件中。
  2. 使用构建工具:Vite 或 Webpack 是处理 Three.js (包括其 examples/jsm 中的模块) 的理想选择。
  3. 状态管理:对于复杂的交互,可以考虑使用简单的状态管理模式来协调各种组件。
  4. 调试工具
    • 浏览器开发者工具。
    • dat.guilil-gui 用于创建可交互的调试 UI。
    • Three.js 提供的各类 Helper (如 GridHelper, AxesHelper, CameraHelper, LightHelper)。

示例项目结构 (使用 Vite)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
my-threejs-app/
├── public/ # 静态资源,如模型、纹理
│ ├── models/
│ │ └── chair.glb
│ └── textures/
│ └── wood.jpg
├── src/
│ ├── main.js # 应用程序入口
│ ├── scene.js # 场景初始化和对象添加
│ ├── camera.js # 相机配置
│ ├── renderer.js # 渲染器配置
│ ├── lights.js # 灯光配置
│ ├── assets/ # 其他组件或工具类
│ │ └── CustomObject.js
│ ├── styles/ # CSS样式
│ └── utils/
│ └── helpers.js
├── index.html # HTML 模板
├── package.json
└── vite.config.js # Vite 配置

main.js 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

// 1. Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xefefef); // Light grey background
scene.add(new THREE.AxesHelper(5)); // 添加坐标轴助手

// 2. Camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(2, 2, 5);

// 3. Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true; // Enable shadows
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
document.body.appendChild(renderer.domElement);

// 4. Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight.position.set(5, 10, 7);
dirLight.castShadow = true;
// Configure shadow properties
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 20;
dirLight.shadow.camera.left = -5;
dirLight.shadow.camera.right = 5;
dirLight.shadow.camera.top = 5;
dirLight.shadow.camera.bottom = -5;
scene.add(dirLight);

// Optional: Light helper for debugging shadow camera
// scene.add(new THREE.DirectionalLightHelper(dirLight, 1));
// scene.add(new THREE.CameraHelper(dirLight.shadow.camera));

// 5. Objects
// Create a ground plane
const planeGeometry = new THREE.PlaneGeometry(10, 10);
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -0.5;
plane.receiveShadow = true;
scene.add(plane);

// Load a GLTF model
const gltfLoader = new GLTFLoader();
gltfLoader.load(
'/models/chair.glb', // Path relative to public folder
(gltf) => {
gltf.scene.scale.set(0.5, 0.5, 0.5); // Scale the model
gltf.scene.position.y = -0.5; // Place on the ground
gltf.scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(gltf.scene);
console.log('Model loaded:', gltf.scene);
},
undefined,
(error) => console.error('Error loading model:', error)
);

// 6. Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

// 7. Animation Loop
function animate() {
requestAnimationFrame(animate);

// Update controls (if damping is enabled)
controls.update();

renderer.render(scene, camera);
}
animate();

// 8. Handle Window Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // Re-apply pixel ratio
});

五、总结

Three.js 是一个令人兴奋的库,它为 Web 带来了强大的 3D 能力。通过本教程,你应该对 Three.js 的核心组件、渲染管线、常用功能以及高级实践有了更深入的理解。

从简单的立方体到复杂的模型加载和交互,Three.js 的世界值得你去探索。不断实践,勇敢尝试新的功能和效果,你将能够构建出令人印象深刻的 3D Web 应用。

记住,实践是最好的老师! 开始你的 Three.js 项目,利用这些知识,将你的创意变为现实吧!