跳到主要内容

场景、相机与渲染器

场景、相机和渲染器是 Three.js 的三大核心组件,它们共同构成了 3D 渲染的基础架构。理解这三个概念的工作原理和相互关系,是掌握 Three.js 的关键。

三者的关系

用一个生活中的例子来类比:场景就像是一个舞台,上面摆放着各种演员和道具;相机就像是摄像机,决定了观众能看到舞台的哪个部分;渲染器就像是电视屏幕,将摄像机拍摄的画面呈现给观众。

在代码层面,这三个组件协同工作的基本模式是:

// 1. 创建场景 - 容器
const scene = new THREE.Scene();

// 2. 创建相机 - 观察者
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);

// 3. 创建渲染器 - 输出设备
const renderer = new THREE.WebGLRenderer();

// 4. 渲染循环 - 持续输出画面
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();

场景(Scene)

场景是 Three.js 中所有 3D 对象的容器,它管理着场景图(Scene Graph),这是一个树形结构,包含了所有需要渲染的对象。

创建场景

const scene = new THREE.Scene();

创建场景非常简单,但场景提供了很多属性和方法来控制渲染效果。

场景背景

场景可以设置背景颜色或背景图片:

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

// 设置渐变背景(使用立方体纹理)
const loader = new THREE.CubeTextureLoader();
scene.background = loader.load([
'px.jpg', 'nx.jpg',
'py.jpg', 'ny.jpg',
'pz.jpg', 'nz.jpg'
]);

// 透明背景(用于叠加到网页上)
scene.background = null;

场景雾效

雾效可以增加场景的深度感,远处的物体会逐渐融入雾色:

// 线性雾:雾的浓度随距离线性增加
scene.fog = new THREE.Fog(0x1a1a2e, 1, 20);
// 参数:雾的颜色、近裁剪距离、远裁剪距离

// 指数雾:雾的浓度随距离指数增长
scene.fog = new THREE.FogExp2(0x1a1a2e, 0.1);
// 参数:雾的颜色、雾的密度

线性雾适合有明确边界范围的场景,指数雾则更自然,适合开放场景。

添加和移除对象

场景提供了添加和移除对象的方法:

// 添加对象
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 移除对象
scene.remove(mesh);

// 查找对象
const found = scene.getObjectByName('myObject');
const allMeshes = [];
scene.traverse((obj) => {
if (obj.isMesh) allMeshes.push(obj);
});

traverse 方法会遍历场景图中的所有对象,包括嵌套的子对象。

场景环境

环境贴图可以为场景中的物体提供反射和折射效果:

const pmremGenerator = new THREE.PMREMGenerator(renderer);
const envTexture = await new THREE.RGBELoader()
.loadAsync('environment.hdr');
const envMap = pmremGenerator.fromEquirectangular(envTexture).texture;

scene.environment = envMap;

相机(Camera)

相机决定了我们观察场景的视角。Three.js 提供了多种相机类型,最常用的是透视相机和正交相机。

透视相机(PerspectiveCamera)

透视相机模拟人眼的视觉体验,遵循近大远小的透视规律。

const camera = new THREE.PerspectiveCamera(
75, // fov: 视野角度
window.innerWidth / window.innerHeight, // aspect: 宽高比
0.1, // near: 近裁剪面
1000 // far: 远裁剪面
);

参数详解:

视野角度(fov):相机的垂直视野角度,单位是度。值越大,看到的范围越广,但物体会显得更远。人眼的视野大约是 60-70 度,游戏常用的值在 60-90 度之间。

宽高比(aspect):渲染结果的宽高比,通常设置为画布的宽高比。如果设置错误,画面会被拉伸或压缩。

近裁剪面(near):相机能看到的最近距离。比这个距离更近的物体不会被渲染。不要设为 0,这会导致深度测试问题。

远裁剪面(far):相机能看到的最远距离。超过这个距离的物体不会被渲染。这个值影响深度缓冲的精度,设置得越大,深度精度越低。

正交相机(OrthographicCamera)

正交相机没有透视效果,物体的大小不会随距离变化。常用于 2D 游戏、CAD 软件、等距视角游戏等场景。

const camera = new THREE.OrthographicCamera(
-5, // left: 左边界
5, // right: 右边界
5, // top: 上边界
-5, // bottom: 下边界
0.1, // near: 近裁剪面
100 // far: 远裁剪面
);

正交相机的视锥体是一个长方体,left、right、top、bottom 定义了这个长方体的四个侧面。

相机位置和朝向

相机继承自 Object3D,可以设置位置和旋转:

// 设置位置
camera.position.set(0, 5, 10);
// 或分别设置
camera.position.x = 0;
camera.position.y = 5;
camera.position.z = 10;

// 让相机看向某个点
camera.lookAt(0, 0, 0);
// 或看向某个对象
camera.lookAt(mesh.position);

相机的默认朝向是沿着负 Z 轴方向,即看向屏幕内部。

相机矩阵

相机内部维护了两个重要的矩阵:

// 视图矩阵:将世界坐标转换到相机坐标
camera.updateMatrixWorld();
const viewMatrix = camera.matrixWorldInverse;

// 投影矩阵:将相机坐标转换到裁剪空间
const projectionMatrix = camera.projectionMatrix;

通常不需要手动操作这些矩阵,Three.js 会自动处理。

相机控制器

手动控制相机位置和旋转很繁琐,Three.js 提供了多种控制器简化操作:

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);

// 启用阻尼(惯性)
controls.enableDamping = true;
controls.dampingFactor = 0.05;

// 设置目标点
controls.target.set(0, 0, 0);

// 限制缩放范围
controls.minDistance = 2;
controls.maxDistance = 20;

// 限制垂直旋转角度
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2;

// 在动画循环中更新
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}

OrbitControls 让用户可以用鼠标旋转、缩放、平移视角。启用阻尼后,操作会有惯性,体验更流畅。

渲染器(Renderer)

渲染器负责将场景和相机结合,计算出最终的 2D 图像。Three.js 主要使用 WebGLRenderer。

创建渲染器

const renderer = new THREE.WebGLRenderer({
antialias: true, // 抗锯齿
alpha: true, // 透明背景
powerPreference: 'high-performance', // GPU 优先级
stencil: false // 是否使用模板缓冲
});

设置渲染尺寸

// 设置输出尺寸
renderer.setSize(window.innerWidth, window.innerHeight);

// 设置像素比(高分辨率屏幕)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

setPixelRatio 影响渲染分辨率。设备像素比大于 1 时(如 Retina 屏幕),渲染的实际像素数会成倍增加。为了性能,通常限制最大值为 2。

渲染配置

// 输出编码
renderer.outputColorSpace = THREE.SRGBColorSpace;

// 色调映射
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

// 阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

色调映射可以改善高动态范围(HDR)场景的显示效果。ACESFilmicToneMapping 是一种常用的电影级色调映射算法。

渲染方法

// 渲染一帧
renderer.render(scene, camera);

// 清除缓冲区
renderer.clear();
renderer.clearColor();
renderer.clearDepth();
renderer.clearStencil();

// 设置背景色
renderer.setClearColor(0x000000, 1);

响应窗口变化

当窗口大小变化时,需要更新相机和渲染器:

window.addEventListener('resize', () => {
// 更新相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

// 更新渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
});

获取画布

渲染器创建的 canvas 元素可以通过 domElement 属性访问:

document.body.appendChild(renderer.domElement);

// 获取画布数据(用于截图)
const dataURL = renderer.domElement.toDataURL('image/png');

渲染循环

Three.js 使用 requestAnimationFrame 创建渲染循环:

function animate() {
requestAnimationFrame(animate);

// 更新动画
mesh.rotation.y += 0.01;

// 更新控制器
controls.update();

// 渲染
renderer.render(scene, camera);
}

animate();

requestAnimationFrame 会在浏览器准备好绘制下一帧时调用回调函数,通常是每秒 60 次。它比 setInterval 更高效,因为浏览器会自动优化调用时机。

使用 Clock 计时

对于需要精确计时的动画,可以使用 Clock:

const clock = new THREE.Clock();

function animate() {
requestAnimationFrame(animate);

const delta = clock.getDelta(); // 距上一帧的时间(秒)
const elapsed = clock.getElapsedTime(); // 总时间(秒)

// 基于时间的动画
mesh.rotation.y = elapsed;

renderer.render(scene, camera);
}

使用 delta 时间可以确保动画在不同帧率下保持一致的速度。

完整示例

下面是一个完整的示例,展示了场景、相机、渲染器的基本用法:

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 5, 25);

// 创建相机
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.set(5, 5, 5);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 0);

// 创建物体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
color: 0x00ff88,
metalness: 0.3,
roughness: 0.4
});
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);

// 创建地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x333344
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.5;
ground.receiveShadow = true;
scene.add(ground);

// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
scene.add(directionalLight);

// 时钟
const clock = new THREE.Clock();

// 渲染循环
function animate() {
requestAnimationFrame(animate);

const elapsed = clock.getElapsedTime();

// 旋转立方体
cube.rotation.x = elapsed;
cube.rotation.y = elapsed * 0.5;

// 更新控制器
controls.update();

// 渲染
renderer.render(scene, camera);
}

animate();

// 响应窗口变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});

小结

场景、相机和渲染器是 Three.js 的基础架构:

  • 场景是容器,管理所有 3D 对象,可以设置背景、雾效、环境贴图
  • 相机决定视角,透视相机模拟人眼,正交相机适合 2D 效果
  • 渲染器输出画面,处理 WebGL 渲染、阴影、色调映射等

理解这三个组件的工作原理和配置选项,是开发 Three.js 应用的基础。下一章我们将学习如何创建各种几何体和材质。