3D 图形:threejs 初识之渲染个立方体

171 阅读8分钟

大家好,我是前端西瓜哥。

今天我们来学习三维渲染。

这里需要选定一个三维渲染引擎,这里选择最流行的 threejs。

threejs 是什么

threejs 是一个 javascript 的 3D 库,体积轻量,易于使用,非常流行,可以获取到大量的社区资源。

threejs 在底层渲染器(通常是 WebGL)的基础上封装出一些更高层的概念,像是 Geometry、Material、Camera,然后将它们组织在一起,渲染出一个三维的世界。

这样你就不必用底层的渲染器一点点绘制三角形、直线等图形然后通过三维几何运算组合起来,使用 threejs 能很快就构建出自己想要的三位场景。

先简单理解几个重要概念。

Geometry

Geometry 是 “几何图形” 的意思。

在 threejs 中,会提供很多基础几何图形,比如立方体(BoxGeometry)、圆形(CircleGeometry)等。

它们继承自 BufferGeometry 类,可以通过参数(比如立方体的长宽高),计算生成带有顶点、索引、法线、uv 等信息的对象。

从 WebGL 的角度,Geometry 主要提供了顶点信息。或者可以说 Geometry 的作用是生成了一堆三角面片

我们先创建一个立方体。

const geometry = new THREE.BoxGeometry(111);

立方体的中心点对着场景的原点位置。

Material

只有三角形面片还不够,那只是一个白模,我们还要要给这些三角面片进行着色。

这里就需要 threejs 的 Material 类了。

Material 是 “材质” 的意思,定义了几何图形的颜色。threejs 也提供了很多 Material 类,如

  1. MeshBasicMaterial,基础材质,适用于不想受光照影响的场景,设置是什么颜色或纹理就是什么,不受环境影响;

  2. MeshPhongMaterial,一种 Blinn–Phong 反射模型,适合需要高光效果的物体。

  3. MeshLambertMaterial;

  4. ...

这里我们创建一个红色的 phong 材质,这个材质。

const material = new THREE.MeshPhongMaterial({
  color0x44aa88// 红色
});

Mesh

三角面片(geometry)有了,着色方案(material)也有了。

下面我们需要用 Mesh 将它们组合起来。

Mesh 就是 “网格” 的意思,在 threejs 代表一个可渲染对象。

创建出 mesh。

const mesh = new THREE.Mesh( geometry, material );

mesh 实例就是一个 3d 模型,它可以设置位置、缩放、旋转等信息,然后添加到场景(scene)上。

Scene

Scence 是 “场景” 的意思,需要渲染出来的三位几何体需要放到场景里才会被渲染出来。

创建 Scene 实例。

const scene = new THREE.Scene();
scene.backgroundnew THREE.Color(0x87ceeb); // 天蓝色

Scene 的背景色默认是黑色,可以自己设置。

将前面创建好的 mesh 添加到场景中。

scene.add(mesh);

mesh 没有设置位置,默认放在 scene 的原点位置,即 (0, 0, 0)

Camera

场景有了,场景上也三维物体了。下面我们需要观测它,这就引入了三维渲染的经典元素——摄像机(Camera)了。

常用的摄像机有两种,一种是透视相机(PerspectiveCamera),有透视效果,表现为近大远小,是对人眼的透视效果的仿真。

另一种是 正交相机(OrthographicCamera),表现为远近一样大。比如你设置线的长度是 1px,那不管你怎么拉近拉远相机,都一直是 1px。正交相机常用于工业制图、游戏中的伪 2d 效果(如跳一跳)中。

这里我们用透视相机,它更符合人眼的认知直觉。

透视相机的视锥(Viewing frustum)是一个四棱台。

图片

视锥空间内的物体会被渲染出来,这个空间外的部份会被剔除掉。

靠近 near 的线的投影会比远离的线要大,以实现近大远小的效果。

创建一个透视相机。

const fov = 70;
const aspect = width / height;
const near = 0.01;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);

透视相机需要设置很多参数,这些参数以后再慢慢说。

透视相机的位置默认为坐标原点,其实跑到了前面 mesh 物体的内部了。这导致我们看不到这个立方体,因为 threejs 默认会做背面剔除优化。

我们把 camera 远离 mesh 一些。

camera.position.set(115);

camera 还可以设置朝向哪里,可以设置为你需要观测的物体,默认是朝向原点位置。camera 会基于自己的位置和 lookAt 位置旋转摄像机。

camera.lookAt(000);

threejs 坐标系

顺便说一下 threejs 的坐标系。

threejs 的坐标系是遵循 WebGL 的表达,使用右手坐标系,x 轴向右(红色),y 轴向上(绿色),z 轴向用户(蓝色)。

图片

从 2d 升级到 3d 的过程,可以理解为你在看一个壁画,里面的平面的图形突然有了厚度。

这时候你就会开始感受到坐标系各种各样的痛苦了,不同框架的坐标系不同。

就说 2d 的 canvas 2d 和 svg 这些用的是设备坐标系,x 向右,y 向下,属于是左手坐标系了,如果把 2d 坐标直接放到 threejs 上,会发现这个图形倒转了过来,所以这里还要给这些点做一个翻转矩阵运算才行。

还有就是有时候我们希望 z 朝上,而不是朝向用户(比如 blender 软件)。这种情况下 2d 就是铺在桌面上的图纸,转 3d 就是平地起高楼。

有个办法是我们加个容器,让它沿着 x 轴翻转 90 度,然后把物体都放这里面,倒是可以实现。

但是如果你用一些插件,你会发现它们还是基于 z 朝向用户的逻辑,导致效果很奇怪,你可能需要再处理下。

如果你要整合各种模块,但这些模块的坐标系都不统一,那改吧改吧,够你吃一壶的。

Light

下面是打光了。

我们使用的 phong 材质是会受到光照影响的,所以如果没有光,不管设置什么颜色和贴图,对应的都是纯黑色。

因此我们需要提供光照。

我们创建一个直射光对象。

const directionalLight = new THREE.DirectionalLight(0xffffff1);
directionalLight.position.set(123);
scene.add(directionalLight);

光的颜色设置为白色,强度为 1,然后放到物体的右上角位置,最后添加到 scene 中。

Renderer

Renderer 是渲染器,作用是将场景(Scene)和摄像机(Camera)组合在一起,将摄像机看到的场景的那部分 3D 场景,投影到 2D 的画布元素上。

每个渲染器实例里面维护了对应的 canvas DOM 元素,我们需要将这个元素添加到 html 上才能看到渲染结果。

通常我们用的渲染器是 WebGLRenderer,基于 WebGL。

const renderer = new THREE.WebGLRenderer({ antialias: true });

threejs 也有面向新一代渲染的 WebGPURenderer,但还是实验性质,问题挺多的,浏览器对 WebGPU 的支持也不稳定,目前生产环境还是不太建议上的。

此外还有 SVGRenderer、CSS3DRenderer,不过也有不少问题,支持的特性也比 WebGLRenderer 少。

设置宽高:

renderer.setSize(width, height);

添加到 html 下:

document.body.appendChild(renderer.domElement);

执行渲染:

renderer.render(scene, camera);

最终渲染效果为:

图片

添加辅助工具

虽然成功渲染了三维物体,但我们只能看这么一个方向,看不到背面,不知道原点在哪里,不确定物体的位置是否正确,以及不知道打光是怎么打的,是否放对了位置。

下面我们将添加几个辅助工具。

首先是坐标轴可视化:

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

然后是光照朝向的可视化:

const lightHelper = new THREE.DirectionalLightHelper(directionalLight, 1);
scene.add(lightHelper);

最后可以通过拖拽鼠标移动摄像机位置的控制器 OrbitControls。

左键拖拽会围绕中心点进行轨道移动,滚轮拉远拉近摄像头,右键拖拽是当前观察平面的水平垂直移动。另外它是适配移动端的,整挺好。

特别注意的是 OrbitControls 类不是在核心包里,而是在分离的其他的文件夹下,因为它属于一种扩展功能,没必要让 threejs 的核心包体积过大。

import { OrbitControlsfrom "three/examples/jsm/Addons.js";

// ...
const controls = new OrbitControls(camera, renderer.domElement);
// 开启了阻尼效果,就是释放鼠标后,摄像头还会短暂慢慢微小移动,可以去掉。
controls.enableDampingtrue;

摄像机改变位置,是需要重渲染的,所以我们还要每帧都要主动调用一下更新操作:

renderer.setAnimationLoop(animate);
function animate() {
  controls.update();
  renderer.render(scene, camera);
}


最终效果

看看最后的效果。

图片

可以看到,没打到光的面是黑色,感兴趣的读者可以试着再加个环境光。

完整代码

import * as THREE from"three";
import { OrbitControlsfrom"three/examples/jsm/Addons.js";

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

/***************** scene *****************/
const scene = new THREE.Scene();
scene.backgroundnew THREE.Color(0x87ceeb); // 天蓝色

/***************** mesh *****************/
const geometry = new THREE.BoxGeometry(111);
const material = new THREE.MeshPhongMaterial({
color0xff0000,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

/***************** camera *****************/
const fov = 70;
const aspect = width / height;
const near = 0.01;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(115);
camera.lookAt(000);

/***************** 光源 *****************/
const directionalLight = new THREE.DirectionalLight(0xffffff1); // 方向光
directionalLight.position.set(123);
scene.add(directionalLight);

const renderer = new THREE.WebGLRenderer({ antialiastrue });
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);

/***************** 辅助工具 *****************/
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);

const lightHelper = new THREE.DirectionalLightHelper(directionalLight, 1);
scene.add(lightHelper);

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

renderer.render(scene, camera);

// 每帧更新
renderer.setAnimationLoop(animate);
function animate() {
  controls.update();
  renderer.render(scene, camera);
}

结尾

最近开始学 threejs 了,搞了点简单仿真测试,验证算法是否正确。

这篇开写,算是正式开始写 3d 相关的文章了。

图片

我是前端西瓜哥,关注我,学习更多三维图形知识。