Skip to content

4. 第一个 3D 场景

Published:

Three.js 是用来创建和渲染三维世界的。

那它是怎么描述三维世界的呢?

首先,三维世界是由一个个物体组成,比如常用的 Mesh。

每一个物体都有它的形状,也就是几何体 Geometry,还有材质 Material,比如颜色、粗糙度、金属感等等。

所有物体都有 Geometry 和 Material 这两部分。

物体可以通过 Group 分组,最终构成一棵树,添加到场景 Scene 中。

image.png

是不是和 dom 树很像?

没错,3D 的世界也是有一颗树的。

但 3D 世界我们知道,从不同角度观察,看到的内容是不一样的。

所以有相机 Camera 的概念。

相机放在不同位置,看到的画面就是不一样的。

有相机还不行,三维世界是有光和阴影的,可以展示不同的明暗效果。

所以有灯光 Light 的概念。

最后有一个渲染器 Renderer 负责渲染,把场景 Scene、相机 Camera、灯光 Light 这些综合渲染到 canvas 画布上。

分工明确,Three.js 就是这样来渲染三维世界的:

image.png

这些是贯穿 Three.js 学习始终的概念,从头用到尾。

下面我们来写下代码,创建第一个 3D 场景:

创建项目:

mkdir first-scene
cd first-scene
npm init -y

image.png

新建 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
        }
    </style>
</head>
<body>
    <script type="module">
        import * as THREE from "https://esm.sh/three@0.174.0/build/three.module.js";

        console.log(THREE);
    </script>
</body>
</html>

在 script 上加上 type=“module” 就可以用 es module 的方式引入 three.js 包了。

跑一下:

npx live-server

image.png

image.png

打开 devtools,可以看到 three.js 引入成功了。

还可以这样写:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
        }
    </style>
</head>
<body>
    <script type="importmap">
    {
        "imports": {
            "three": "https://esm.sh/three@0.174.0/build/three.module.js"
        }   
    }
    </script>
    <script type="module">
        import * as THREE from "three";

        console.log(THREE);
    </script>
</body>
</html>

用 type=“importmap” 的 script 来声明 es module 的包名和 url 之间的映射。

然后后面就可以直接 import 这个包了。

刷新页面,是一样的:

image.png

然后我们来写 Three.js 的代码:

把代码拆分到 index.js 中来写:

image.png

<script type="module" src="./index.js"></script>

然后安装下 three 的类型包:

npm install --save-dev @types/three

这样写代码就有类型提示了:

2025-03-14 17.39.35.gif

当某个 api 不会用,可以点进去看看它的类型。

而且,类型上还有文档链接:

image.png

可以直接点开查看这个 api 的文档。

image.png

默认是英文文档的链接,点击这里切换成中文就好:

image.png

所以,安装 @types/three 类型包之后,除了有类型提示,查看 api 的文档也很方便。

然后进入正题,写下 three.js 的代码:

import * as THREE from 'three';

const scene = new THREE.Scene();

{
    const geometry = new THREE.BoxGeometry(100, 100, 100);
    const material = new THREE.MeshLambertMaterial(({
        color: new THREE.Color('orange')
    }));
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(0, 0, 0);
    scene.add(mesh);
}

{
    const pointLight = new THREE.PointLight(0xffffff, 10000);
    pointLight.position.set(80, 80, 80);
    scene.add(pointLight);
}

{
    const width = window.innerWidth;
    const height = window.innerHeight;

    const camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
    camera.position.set(200, 200, 200);
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height)

    renderer.render(scene, camera);

    document.body.append(renderer.domElement);
}

先看看效果:

image.png

可以看到,页面渲染出了一个橙色的立方体,而且明显能感觉到光照的明暗变化。

回过头来看下代码:

image.png

首先,就像前面说的,我们创建了一个 Scene,往其中添加物体 Mesh 和灯光 Light。

这个 Mesh 的几何体是一个 BoxGeometry 立方体,它的材质是一个漫反射材质 MeshLambertMaterial,这个材质支持漫反射,我们设置了一个橙色。

然后传入 geometry 和 material 来创建 Mesh。

设置它的位置在 0,0,0。

我们加一个展示坐标系的工具 AxesHelper:

image.png

const axesHelper = new THREE.AxesHelper(200);
scene.add(axesHelper);

参数是坐标轴的长度,设置 200

image.png

现在就可以看到坐标轴了,红绿蓝分别对应 x、y、z 轴,非常好记,所有的 3D 软件基本都是这样。

可以看到,立方体确实是在 0,0,0 的位置。

灯光我们用了点光源,就和灯泡一样,从一个点发射光线:

image.png

设置颜色为白色,光照强度 10000

位置在 80,80,80 的位置,默认照向 0,0,0 的方向。

很显然,立方体长宽高为 100,有一个角是在 50,50,50 的位置,离灯光近一点,所以有明显的高亮。

image.png

然后我们用了一个透视相机,在 200,200,200 的位置看向 0,0,0

image.png

它有 4 个参数。

我们从一个位置往另一个位置看,其实看到的是一个椎体的范围:

image.png

这叫做视椎体。

相机的 4 个参数就是描述这个视椎体的。

第一个参数是角度(fov),也就是看的范围有多大。

第二个参数是宽高比,也就是这个视椎体的宽和高的比例。

第三个和第四个参数是展示视椎体的哪一部分,最近是哪,最远是哪。

我们设置了角度为 60,宽高比是窗口的宽高比(window.innerWidth/window.innerHeight )。

然后最近和最远的截面距离也设置了一个比较大的范围。

最终就是我们看到的这个角度和可视范围:

image.png

最后,就像前面说的,用 Renderer 把 Scene 渲染到 canvas 上:

image.png

参数是 scene、camera,就是把 camera 看到的场景 scene 的样子渲染出来。

image.png

返回的这个 domElement 就是 canvas 元素,把它挂到 dom 树就行了。

打开 devtools 看下:

image.png

确实是在 body 下挂了一个 canvas 元素。

这样,我们就把 Three.js 渲染流程过了一遍。

在 Scene 中添加各种 Mesh,每个 Mesh 都是由几何体 Geometry 和材质 Material 构成,设置相机 Camera 的角度和可视范围,设置灯光 Light 的位置,然后通过渲染器 Renderer 渲染到 canvas 元素上,把这个 canvas 挂载到 dom。

最后,很多三维软件都支持通过鼠标拖动来 360 度观察 3D 场景。

这个如何实现呢?

用 Three.js 提供的轨道控制器 OrbitControls 即可。

image.png

这个类在 /examples/jsm/ 目录下。

但后面我们用 three 的 npm 包的时候,就是 addons/ 的路径。

所以这里也统一映射成 three/addons/

<script type="importmap">
{
    "imports": {
        "three": "https://esm.sh/three@0.174.0/build/three.module.js",
        "three/addons/": "https://esm.sh/three@0.174.0/examples/jsm/"
    }
}
</script>

在代码引入 OrbitControls:

image.png

image.png

import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);

创建 OrbitControls 的实例,传入 camera 和 canvas 元素。

这里需要把 render 改成渲染循环,用 requestAnimationFrame 来一帧帧的循环渲染。

image.png

requestAnimationFrame的调用频率和显示器刷新率一致。

看下效果:

2025-03-14 21.40.04.gif

现在就可以通过鼠标拖动来 360 度观察 3D 场景了。

大家想一下是啥原理?

其实很简单,就是 canvas 监听鼠标事件,然后根据鼠标的移动来修改相机 camera 的位置就可以了。

它的参数也是这俩:

image.png

注释掉:

image.png

打开 devtools 看下 canvas 元素的事件监听器:

image.png

放开注释,再看一下:

image.png

可以看到,监听了 canvas 元素的 pointer、contextmenu、wheel 等鼠标事件,内部修改 camara 参数就可以了。

案例代码上传了小册仓库

总结

这节我们了解了下 Three.js 是如何描述三维世界的。

Three.js 通过 Scene 来管理各种物体,这些所有的物体组成一棵树。

每个物体(常用的是 Mesh)都有几何体 Geometry、材质 Material 来描述形状、颜色等。

通过相机 Camera 在不同角度来观察,通过灯光 Light 来照亮这个三维世界。

最后通过 Renderer 把这个 Scene 渲染到 canvas 画布上,把返回的 canvas 元素挂载到 dom 就可以了。

这就是 Three.js 里的几个核心概念。

评论