2、Three.js入门:安装与引入、创建第一个3D场景(场景、相机、渲染器)、理解动画循环
好,咱们正式开始动手了。
上一章聊了3D的基础概念,这一章直接上代码。Three.js是目前最流行的Web 3D库,没有之一。我个人习惯用它来快速搭建原型,因为API设计得很友好,社区也活跃。你想想看,如果让你从零写一个WebGL渲染器,光矩阵运算就能劝退90%的人。Three.js把这些脏活累活都封装好了,我们只需要关注「场景长什么样」就行。
2.1 安装与引入:别纠结,选一种方式就行
Three.js的引入方式有好几种,我挑最常用的三种说。
方式一:CDN直接引入(最快上手)
适合写个Demo玩玩,或者做个小工具页面。直接在HTML里加个script标签:
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// 你的代码从这里开始
</script>
这里用了importmap,说白了就是给浏览器指个路——告诉它「three」这个包名对应哪个URL。我在早期项目里经常这么干,因为不用装npm,打开浏览器就能跑。
方式二:npm安装(推荐正式项目)
如果你在用Vue、React这类框架,或者项目规模比较大,建议走npm:
npm install three@0.160.0
然后在代码里引入:
import * as THREE from 'three';
嗯,这里要注意版本号。我踩过坑——不同版本的Three.js有些API不兼容,比如Geometry在r125之后改成了BufferGeometry。所以建议锁定版本,别用^符号。
方式三:CDN传统script标签(不推荐)
有些老教程会让你这么写:
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
这种方式会把所有东西挂到window.THREE上。我个人不太喜欢,因为全局污染、模块化也不好做。除非你是在写纯静态页面,否则别用。
2.2 创建第一个3D场景:场景、相机、渲染器
Three.js里有个铁三角:场景(Scene)、相机(Camera)、渲染器(Renderer)。缺一个都玩不转。
打个比方:场景就是舞台,相机就是你的眼睛,渲染器就是把舞台上的画面拍下来显示到屏幕上的人。
2.2.1 场景(Scene)
场景就是个容器,所有物体、灯光、辅助线都放在里面。创建它很简单:
const scene = new THREE.Scene();
默认场景是黑色的。你可以给它加个背景色:
scene.background = new THREE.Color(0x87CEEB); // 天蓝色
我在项目中遇到过一个问题——场景背景色和CSS背景色搞混了。记住,Three.js的场景背景色只影响3D画布内部,不影响页面其他部分。
2.2.2 相机(Camera)
相机决定了你从哪个角度、以什么视角看场景。最常用的是透视相机(PerspectiveCamera):
const camera = new THREE.PerspectiveCamera(
75, // 视野角度(FOV),类似人眼的视角范围
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁剪面
1000 // 远裁剪面
);
camera.position.set(0, 2, 5); // 把相机往后挪5个单位,往上抬2个单位
camera.lookAt(0, 0, 0); // 让相机看向原点
这里有个坑——近裁剪面和远裁剪面。说白了就是相机能看到的最远和最近距离。如果你把物体放在相机后面,或者离得太远,它就消失了。我曾经调试一个模型半天不显示,最后发现是远裁剪面设得太小,模型被裁掉了。
2.2.3 渲染器(Renderer)
渲染器负责把场景和相机的内容画到屏幕上。WebGL渲染器是默认选择:
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // 适配高清屏
document.body.appendChild(renderer.domElement);
antialias: true是抗锯齿,能让边缘更平滑。我建议一直开着,性能影响不大。
setPixelRatio这行很多人会忘。如果不加,在Retina屏(比如MacBook)上画面会模糊。你想想看,一个高清屏上跑模糊的3D,多掉价。
2.2.4 加个物体试试
光有舞台和相机还不够,得放个东西看看效果。加个立方体:
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 加个灯光,不然MeshStandardMaterial是黑的
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
然后渲染一帧:
renderer.render(scene, camera);
打开浏览器,你应该能看到一个绿色的立方体。如果看不到,检查三点:相机位置、物体位置、灯光。我刚开始学的时候经常忘了加灯光,结果场景一片黑,还以为代码写错了。
2.3 理解动画循环:让场景动起来
上面那个例子只渲染了一帧。要让立方体旋转起来,需要持续不断地渲染。这就引出了动画循环。
2.3.1 requestAnimationFrame
浏览器提供了一个API叫requestAnimationFrame,它会在每次屏幕刷新时调用你传入的函数。一般屏幕是60Hz,也就是每秒调用60次。
function animate() {
requestAnimationFrame(animate); // 递归调用,形成循环
// 更新物体的状态
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
// 渲染
renderer.render(scene, camera);
}
animate(); // 启动循环
这段代码做了三件事:
- 请求下一帧
- 更新物体状态(旋转角度)
- 重新渲染场景
为什么不用setInterval?因为requestAnimationFrame更智能——页面切到后台时会自动暂停,不浪费性能。而且它能和浏览器的刷新率同步,画面更流畅。
2.3.2 时间增量(Delta Time)
上面那个旋转速度是固定的——每帧转0.01弧度。但有个问题:不同显示器的刷新率不一样。60Hz的屏幕每秒转0.6弧度,120Hz的屏幕每秒转1.2弧度。结果就是,高刷屏上物体转得飞快。
解决办法是用时间增量:
let clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta(); // 获取上一帧到这一帧的时间差(秒)
cube.rotation.x += 0.5 * delta; // 每秒转0.5弧度
cube.rotation.y += 0.5 * delta;
renderer.render(scene, camera);
}
这样不管屏幕是60Hz还是144Hz,物体每秒转动的角度都一样。我在做游戏项目时,所有运动逻辑都依赖delta,否则不同设备上体验差异巨大。
2.3.3 窗口自适应
用户调整浏览器窗口大小时,画布尺寸也得跟着变。不然画面会拉伸或变形:
window.addEventListener('resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height; // 更新宽高比
camera.updateProjectionMatrix(); // 通知相机重新计算投影矩阵
renderer.setSize(width, height); // 更新渲染尺寸
});
这行camera.updateProjectionMatrix()很容易忘。如果不调,画面比例会错乱。我有个同事因为这个bug调了一下午,最后发现就是少调了这行。
2.4 完整示例代码
把上面所有内容拼起来,就是一个完整的入门Demo:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 入门</title>
<style>
body { margin: 0; overflow: hidden; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// 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(0, 2, 5);
camera.lookAt(0, 0, 0);
// 3. 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// 4. 物体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 5. 灯光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
// 6. 动画循环
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
cube.rotation.x += 0.5 * delta;
cube.rotation.y += 0.5 * delta;
renderer.render(scene, camera);
}
animate();
// 7. 窗口自适应
window.addEventListener('resize', () => {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
});
</script>
</body>
</html>
2.5 本章小结
这一章我们干了三件事:
- 安装引入——CDN还是npm,看场景选
- 搭建铁三角——场景装东西,相机定视角,渲染器画出来
- 动画循环——用requestAnimationFrame驱动,用delta保证一致性
说实话,Three.js入门就这么点东西。你把这个Demo跑通了,后面学光照、材质、模型加载都会轻松很多。下一章我们聊聊「响应式布局」——怎么让3D场景在不同屏幕上都好看。