场景与相机:搭建你的第一个3D视角

好,我们正式开始搭建3D世界。上一章我们只是让浏览器显示了一个Three.js的空白画布,说实话挺无聊的。这一章我们要往里面放东西——准确地说,是放一个「场景」和一台「相机」。

场景和相机,是Three.js里最基础的两个概念。没有场景,你没法放物体;没有相机,你啥也看不见。我刚开始学的时候,以为场景就是个容器,相机就是个镜头,后来发现没那么简单。

场景(Scene)是什么?

场景,说白了就是一个虚拟的三维空间。你所有的物体、灯光、辅助线,都得放在这个空间里。Three.js里用 THREE.Scene 来表示。

const scene = new THREE.Scene();

就这么一行。但你别小看它。场景其实是一个树状结构,你可以往里面添加各种子对象。比如添加一个立方体:

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);

嗯,这里要注意:scene.add() 可以添加任何继承自 Object3D 的对象。Mesh、Light、Group 都可以。我个人习惯把同类型的物体放在一个 Group 里,再统一加到场景,这样管理起来方便。

小技巧: 场景的背景色可以直接设置,不用额外加背景物体。比如 scene.background = new THREE.Color(0x87CEEB); 就能得到一个天蓝色的背景。

相机:透视 vs 正交

相机决定了你从哪个角度、以什么方式观察场景。Three.js 提供了两种最常用的相机:透视相机和正交相机。

透视相机(PerspectiveCamera)

透视相机模拟人眼的视觉效果——近大远小。这是最常用的相机类型,适合做游戏、室内漫游、产品展示。

const camera = new THREE.PerspectiveCamera(
  75,           // 视野角度(FOV),单位度
  window.innerWidth / window.innerHeight, // 宽高比
  0.1,          // 近裁剪面
  1000          // 远裁剪面
);

这四个参数,我一个个说:

  • FOV(视野角度):值越大,看到的范围越广,但物体显得越小。75度是个比较舒服的值。我在做建筑可视化时,经常用45-60度,更接近人眼真实感受。
  • 宽高比:一般直接用窗口的宽高比。如果不对,画面会拉伸变形。
  • 近裁剪面 / 远裁剪面:相机只能看到这两个平面之间的物体。太近或太远的物体都会被裁剪掉。我曾经遇到过一个问题:模型明明在场景里,但就是看不到,后来发现是远裁剪面设得太小了。
避坑指南: 近裁剪面不要设得太小(比如0.0001),否则会导致深度精度问题,远处的物体会出现闪烁。我一般设0.1或1。

正交相机(OrthographicCamera)

正交相机没有近大远小的效果。物体的大小不随距离改变。适合做2D游戏、UI界面、技术图纸。

const camera = new THREE.OrthographicCamera(
  -10,  // left
  10,   // right
  10,   // top
  -10,  // bottom
  0.1,  // near
  1000  // far
);

正交相机的参数是左右上下四个边界,形成一个矩形视锥体。说白了,你看到的画面就是这个矩形范围内的物体。

特性 透视相机 正交相机
近大远小
适用场景 3D游戏、漫游 2D游戏、UI
参数 FOV、宽高比、近远 左右上下、近远
视觉感受 真实、有纵深感 平面、技术感

相机位置与朝向

相机创建出来后,默认在原点 (0,0,0)。但场景里的物体也在原点,所以你会和物体重叠在一起,啥也看不见。你需要移动相机。

camera.position.set(5, 5, 5);  // 把相机放到 (5,5,5) 位置
camera.lookAt(0, 0, 0);        // 让相机看向原点

position 是相机在三维空间中的坐标。 lookAt 是相机的朝向——它看向哪个点。这两个配合使用,你就能从任意角度观察场景了。

我个人习惯先设置位置,再调用 lookAt。如果你不调用 lookAt,相机默认朝向Z轴负方向。嗯,这个细节容易忽略。

核心要点: 相机的位置和朝向是分开控制的。位置决定你在哪,朝向决定你看哪。两者缺一不可。

相机控制:OrbitControls

手动设置相机位置和朝向,写死了就没法交互了。为了让用户能自由旋转、缩放、平移视角,我们需要 OrbitControls。

OrbitControls 是 Three.js 官方提供的一个控制器,它封装了鼠标拖拽旋转、滚轮缩放、右键平移等功能。用起来非常简单:

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

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;  // 开启惯性效果
controls.dampingFactor = 0.05; // 惯性系数
controls.autoRotate = false;   // 是否自动旋转

这里要注意几点:

  • enableDamping:开启后,操作会有平滑的惯性效果,手感更好。我建议一直开着。
  • dampingFactor:惯性大小,值越小惯性越明显。0.05是个不错的起点。
  • autoRotate:可以让场景自动旋转,适合做展示效果。

然后,在动画循环里,你需要调用 controls.update()

function animate() {
  requestAnimationFrame(animate);
  controls.update();  // 更新控制器状态
  renderer.render(scene, camera);
}
animate();

为什么需要 update()?因为开启阻尼后,控制器需要每帧更新才能实现平滑效果。如果不调用,阻尼就不会生效。

小技巧: 你可以限制相机的旋转角度和缩放范围。比如 controls.minDistance = 2;controls.maxDistance = 20; 可以防止用户把相机拉得太近或太远。

完整示例:一个可交互的3D场景

把上面所有东西拼起来,就是一个完整的可交互3D场景了:

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

// 1. 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);

// 2. 创建透视相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);

// 3. 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 4. 添加一个立方体
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);

// 5. 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;

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

运行这段代码,你会看到一个绿色的立方体,可以用鼠标拖拽旋转视角,滚轮缩放。嗯,这就是一个最基础的3D交互页面了。

总结一下: 场景是容器,相机是眼睛,控制器是手。三者配合,你就能自由探索3D世界了。下一章我们会往场景里放更多有意思的东西。