当我第一次学习摄影时,老师告诉我一句话:
"你不是在拍东西,而是在拍光。"
后来我学习 Three.js 时突然意识到:
这句话原来依旧成立。
Three.js 不只是一个 3D 引擎,更像是一台虚拟相机。要拍好这张"虚拟的照片",我们必须掌握三个核心要素:
场景(Scene)
相机(Camera)
灯光与材质(Light & Material)
于是,我把学习过程想象成一个摄影新手成长为三维光影师的故事。
空无一物的影棚 ------ Scene 场景
故事从一个空影棚开始。
当我第一次打开 Three.js 时,教程告诉我:
js
const scene = new THREE.Scene();
这就像摄影师走进了一个空旷的工作室:
没有布景、没有模特、没有灯光,甚至连相机都还没架好, 在影棚这个场景中,摄影师可以在这个场景中放任何的东西:
- 架好摄像机(Camera 📹)
- 拍照的物体(Mesh 网格物体)、物体拥有着自己的形状(Geometry几何体)和材质(Material)
- 摆设好灯光(Light)
- 也可以是任意的对象 (Object3D)
摄影师往 Scene 里布置道具,而程序员的你往 Scene 里添加各种对象,因此 场景就是一个可以放任何东西的容器
找到你要观看的角度 ------ Camera 相机
刚学摄影时,我最常做的事情,就是移动、蹲下、趴着、绕圈......
只为了找到一个"对的角度"。
Three.js 的相机就是你的眼睛。创建相机就像准备拍摄时拿起单反:
js
const camera = new THREE.PerspectiveCamera(const camera = new THREE.PerspectiveCamera(
50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);
// 2.1 设置相机的位置,放在不同的位置看到的风景当然不一样
camera.position.set(5, 10, 10); // x, y, z
camera.lookAt(0, 0, 0); // 设置相机方向(这就是你女朋友让你找最佳角度的原因)
摄影师会说:"我走两步,让模特在背景中更突出。"
程序员会说:
js
camera.position.z = 3;
camera.lookAt(0, 0, 0)
本质完全一样:
都是在调整观察世界的方式。
让世界真正亮起来 ------ Light & Material 灯光与材质
你可以有再漂亮的模特、再好的相机,如果没有光------
一切都会变成漆黑一片。
Three.js 也是如此。你搭了一个完美的 3D 模型,如果没有光,它看起来只是纯黑。
于是我制作"虚拟布光":
js
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 5);
scene.add(light);
摄影师打灯,而我在代码里放置光源:
- DirectionalLight(平行光)= 太阳光
- PointLight(点光源)= 想象灯泡发光,由点向八方发射
- SpotLight(聚光灯)= 舞台灯,从上打下来,呈现圆锥体,它离光越远,它的尺寸就越大。这种光源会产生阴影
- AmbientLight(环境光)= 影棚柔光,环境光没有特定的来源方向,不会产生阴影
同时材质(Material)也等同于现实世界的"被光击中时的反应":
- 皮肤 = standard material
- 金属 = metalness 高
- 塑料 = roughness 较高
- 玻璃 = transparent=True + envMap
想要一个皮肤质感的物体?
那么你就得给材质加入 roughness、metalness、normalMap 就像摄影师在打柔光,为人物皮肤创造质感。
光与材质的搭配,就是 Three.js 里的"布光艺术"。
最终章:按下快门 ------ Renderer 渲染器
当场景布好、相机调好、灯光到位后------
摄影师要做的就是按下快门。
在 Three.js 里:
js
renderer.render(scene, camera);
渲染器就是那个"快门",
真正把世界投射到屏幕上。
摄影师用快门把现实世界的光记录下来;
Three.js 用 GPU 把虚拟世界的光影计算出来。
本质上,两者做的是同一件事:
把真实或虚拟的三维世界,投射成一张二维图像。
js
import * as THREE from "three";
// 1. 创建场景
const scene = new THREE.Scene();
// 2. 创建相机(透视投影相机)
const camera = new THREE.PerspectiveCamera(
50, // 相机视野角度,摄像机的视野角度越大,摄像机看到的场景就越大,反之越小
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近平面(近端渲染距离),指定从距离相机多近的位置开始渲染,推荐默认值0.1
1000 // 远平面(远端渲染距离)指定摄像机从它所在的位置最远能看到多远,太小场景中的远处物体会看不见,太大会浪费资源影响性能,推荐默认值1000
);
// 2.1 设置相机的位置
camera.position.set(5, 10, 10); // x, y, z
camera.lookAt(0, 0, 0); // 设置相机方向(默认看向场景原点)
// 3. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿,使边缘更平滑
// 3.1 设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 3.2 将渲染器的canvas内容添加到body
document.body.appendChild(renderer.domElement);
// 4. 创建一个立方体几何体
const geometry = new THREE.BoxGeometry(4, 4, 4); // 宽、高、深
// 为了让光源有效果,我们使用 MeshLambertMaterial 或 MeshPhongMaterial
// 创建材质 MeshLambertMaterial (兰伯特材质) 是一种非光泽材质,会受光照影响,但没有镜面高光
const material = new THREE.MeshLambertMaterial({
color: 0x00ff00, // 颜色
// wireframe: true, // 如果需要线框效果可以加上
});
// 6. 创建一个网格模型(网格模型由几何体和材质组成)
// Mesh 构造函数通常只接受一个材质。如果需要多材质,Three.js 有专门的 MultiMaterial 或 Group 来处理
const cube = new THREE.Mesh(geometry, material); // 使用 MeshLambertMaterial
// 6.1 将几何模型添加到场景中
scene.add(cube);
// 6.2 设置相机看向物体(拍摄对象)的位置(默认状态下相机看向的是场景的原点(0,0,0))
camera.lookAt(cube.position);
// 7. 创建光源
const spotLight = new THREE.SpotLight(0xffffff); // 创建聚光灯,颜色为白色
// 7.1 设置光源的位置
spotLight.position.set(0, 20, 20); // 调整光源位置,使其能够照亮立方体
// 7.2 设置光源照射的强度,默认值为1, 越大越亮
spotLight.intensity = 2;
// 7.3 将光源添加到场景中
scene.add(spotLight);
// 8. 为了方便观察 3D 图像,添加三维坐标系对象
const axesHelper = new THREE.AxesHelper(6); // 参数表示坐标系的大小 (x轴红色, y轴绿色, z轴蓝色)
scene.add(axesHelper); // 将坐标系添加到场景中
// 9. 渲染函数
function animate() {
requestAnimationFrame(animate); // 请求再次执行渲染函数animate,形成循环
// 让立方体动起来
cube.rotation.x += 0.01; // 沿x轴旋转
cube.rotation.y += 0.01; // 沿y轴旋转
cube.rotation.z += 0.01;
renderer.render(scene, camera); // 使用渲染器,通过相机将场景渲染出来
}
animate(); // 执行渲染函数,进入无限循环,完成渲染