16.Three.js 基础之自定义几何体
16 Three.js 基础之自定义几何体
重要说明:本文部分内容已经过时
.以下内容更新于 2021 年 4 月 15 日
本文写的时候还使用的是 0.124.0 版本,但是在 0.125.2 以后(目前最新的是 0.127.0 ),关于 几何体 官方做了重大调整:
-
官方已经将 Geometry 从核心库中移除,新的位置改为:
import { Geometry } from 'three/examples/jsm/deprecated/Geometry'
请注意 目录名为 deprecated,这个单词的意思就是:已弃用、不建议使用。
也就是说官方已经不再建议你使用 Geometry 这个类了,那它的替代者是谁呢?
-
在 r124 版本的时候,几何体(例如 BoxGeometry) 他们都继承的是 Geometry,但是在新版本中它们继承的是 BufferGeometry。
也就是说如果你想自定义几何体,现在应该使用的是 BufferGeometry
Three.BufferGeometry
换句话说,就是 BufferGeometry 替代了 Geometry
-
Three.Mesh() 函数中的参数也发生了变化
-
或许还有其他更多地方发生了变化…
本文在编写的时候,还采用的是 Geometry,所以本文内容过时了。
目前先暂且不做修改,等到以后有时间了再将本文中的代码 Geometry 修改为 BufferGeometry。
你可以先跳过本章,继续后面章节的学习。
以下内容更新于 2021 年 4 月 15 日
接下来开始本文(已过时)的内容。
本文说的 几何体(geometry),也就是之前 “05 Three.js 基础之图元.md” 中的 图元(primitives)。
图元和几何体只是同一个对象(事物)的不同的叫法而已。
自定义几何体也就相当于自定义图元。
自定义几何体的这个过程,在传统 3D 软件中被称为 建模
自定义几何体(custom geometry)概述
先来一个灵魂拷问。
有必要在 Three.js 中自定义几何体吗?
答:似乎没有必要,因为实际项目中,绝大多数情况下,我们都会通过专业的 3D 软件中来创建物体模型(建模),而不是通过 Three.js 自定义建模。
传统专业的 3D 转件包括:
- Blender:开源免费的 3D 软件
- Maya:侧重动画渲染的 3D 软件
- Cinema4D(C4D):轻量级的 3D 软件
- 3D Sudio Max
- …
以上软件中,都有学习成本,相对而言 C4D 更加轻量、更加简单。
当 3D 模型创建好后,我们将模型导出为 gLTF 或 .obj 的文件,然后在 Three.js 中加载并使用它们。
尽管如此,但 Three.js 依然提供了自定义几何体(自定义建模)的方法。
可以让我们在不导入 建模文件 的前提下,通过 Three.js 来自定义几何体。
所有的传统 3D 软件建模过程都是可视化的,也就是说你可以时时看到物体,并且进行细微调整。而 Three.js 则是通过代码来建模的,整个建模过程是不可见的。
尽管实际中我们可能会很少机会在 Three.js 中自定义几何体,但是学习这方面的知识还是非常有必要的。
Three.js 中如何自定义几何体?
答:在 Three.js 中,一共可以有 2 种方式自定义几何体。
第 1 种:继承于 Geometry
优点:创建和使用的难度小
缺点:渲染启动速度慢、占更多内存
第 2 种:继承于 BufferGeometry
优点:渲染启动速度快、占更少内存
缺点:创建和使用的难度大
补充说明:
上面说的 渲染启动速度 慢,是指当场景第一次被渲染、后续修改后重新渲染的速度慢。
并不是说 绘制速度慢。
无论选择 Geometry 还是 BufferGeometry,绘制过程速度是相同的,他们的 “快慢” 主要体现在 第一次渲染启动速度 这方面。
当然,你不需花太多精力去理解 慢 的细节,你只需知道 Geometry 相对而言 渲染速度更慢一些即可。
如何选择?
答:对于要创建的自定义几何体各个面的三角形总和小于 1000,则优先选择继承于 Geometry。三角形数量超过这个范围的则推荐继承于 BufferGeometry。
事实上以上的选择仅供参考,重点是你实际项目中 是否觉得渲染启动速度、修改响应速度慢,如果慢则可进行优化改进。
如果由于客户端硬件配置比较高,感知不到慢或卡顿,那么可以而继续使用 Geometry。
自定义几何体示例:HelloCustomGeometry
示例目标
- 通过自定义几何体,来实现一个 立方体
- 自定义 立方体 6 个面的颜色
- 自定义 立方体 8 个顶点的颜色
- 给 立方体 添加 光照法线,让立方体可以反光
代码思路
如何自定义一个立方体?
答:主要分为 3 步
-
第 1 步:实例化一个 Three.Geometry
const geometry = new Three.Geometry()
-
第 2 步:按照立方体相应的坐标,添加 8 个顶点
geometry.vertices.push( new Three.Vector3(-1, -1, 1), // 1 new Three.Vector3(1, -1, 1), // 2 new Three.Vector3(-1, 1, 1), // 3 new Three.Vector3(1, 1, 1), // 4 new Three.Vector3(-1, -1, -1), // 5 new Three.Vector3(1, -1, -1), // 6 new Three.Vector3(-1, 1, -1), // 7 new Three.Vector3(1, 1, -1) // 8 )
-
第 3 步:将相邻的 3 个顶点,依次按照逆时针顺序,构建成一个个三角形。
立方体一共有 6 个面,每个面由 2 个三角形构成,因此一共需要构建 12 个三角形
为什么必须是逆时针?这是 Three.js 规定的。
geometry.faces.push( //前面 new Three.Face3(0, 3, 2), new Three.Face3(0, 1, 3), //右面 new Three.Face3(1, 7, 3), new Three.Face3(1, 5, 7), //后面 new Three.Face3(5, 6, 7), new Three.Face3(5, 4, 6), //左面 new Three.Face3(4, 2, 6), new Three.Face3(4, 0, 2), //顶面 new Three.Face3(2, 7, 6), new Three.Face3(2, 3, 7), //底面 new Three.Face3(4, 1, 0), new Three.Face3(4, 5, 1) )
至此,就以成功构建出一个立方体的基本骨架。
如何自定义 6 个面的颜色额?
geometry.faces[0].color = geometry.faces[1].color = new Three.Color('red')
geometry.faces[2].color = geometry.faces[3].color = new Three.Color('yello')
geometry.faces[4].color = geometry.faces[5].color = new Three.Color('green')
geometry.faces[6].color = geometry.faces[7].color = new Three.Color('cyan')
geometry.faces[8].color = geometry.faces[9].color = new Three.Color('blue')
geometry.faces[10].color = geometry.faces[11].color = new Three.Color('magenta')
如何自定义 8 个顶点的颜色?
geometry.faces.forEach((face, index) => {
face.vertexColors = [
(new Three.Color()).setHSL(index / 12, 1, 0.5),
(new Three.Color()).setHSL(index / 12 + 0.1, 1, 0.5),
(new Three.Color()).setHSL(index / 12 + 0.2, 1, 0.5)
]
})
如何开启顶点着色?
const material = new THREE.MeshBasicMaterial({vertexColors: true})
const material = new Three.MeshPhongMaterial({ vertexColors: true })
vertexColors 默认值为 false,即默认显示材质 color 的颜色。
如果没有给材质设置 color 值,那么默认颜色值为 白色
特别说明,在 Three.js 之前的版本中,vertexColors 的值并不是 Boolean,而是进行以下设置:
//export enum Colors {}
//export const NoColors: Colors;
//export const FaceColors: Colors;
//export const VertexColors: Colors;
const material = new THREE.MeshBasicMaterial({vertexColors: THREE.FaceColors});
在目前比较新的版本中,vertexColors 的值改为 Boolean 类型。
如何添加光照法线?
答:一共有 3 种方式
-
给每一个 face 设置 normal 属性值
face.normal = new Three.Vector3(...)
-
通过 vertexNormals 属性来设置
face.vertexNormals = { new Three.Vector3(...), new Three.Vector3(...), ... new Three.Vector3(...) }
-
通过 computeFaceNormals() 和 computeVertexNormals() 这 2 个方法自动帮我们计算出光照法线。
但是对于立方体而言,只执行 computeFaceNormals() 方法即可。
geometry.computeFaceNormals() //geometry.computeVertexNormals() // 这个方法并不适用于立方体
第 3 种 方法最为简便,也比较常用。
本示例就采用第 3 种方式。
补充说明:computeVertexNormals() 为什么不适用于立方体?
答:因为 computeVertexNormals() 会从每个顶点共享的所有面的法线中计算得出法线,这样的发现会让立方体的顶点看上去更像一个球体。
如果你执行了 computeVertexNormals(),并不会报错,仅仅是立方体顶点处看似更加圆润,像球一样。
代码示例:
import { useEffect, useRef } from 'react'
import * as Three from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import './index.scss'
const HelloCustomGeometry = () => {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (canvasRef.current === null) {
return
}
const renderer = new Three.WebGLRenderer({ canvas: canvasRef.current })
const scene = new Three.Scene()
const camera = new Three.PerspectiveCamera(45, 2, 0.1, 100)
camera.position.z = 8
const light = new Three.DirectionalLight(0xFFFFFF, 1)
light.position.set(2, 2, 4)
scene.add(light)
const helper = new Three.DirectionalLightHelper(light)
scene.add(helper)
const controls = new OrbitControls(camera, canvasRef.current)
controls.update()
//自定义一个立方体几何体
const geometry = new Three.Geometry()
geometry.vertices.push(
new Three.Vector3(-1, -1, 1), // 1
new Three.Vector3(1, -1, 1), // 2
new Three.Vector3(-1, 1, 1), // 3
new Three.Vector3(1, 1, 1), // 4
new Three.Vector3(-1, -1, -1), // 5
new Three.Vector3(1, -1, -1), // 6
new Three.Vector3(-1, 1, -1), // 7
new Three.Vector3(1, 1, -1) // 8
)
geometry.faces.push(
//前面
new Three.Face3(0, 3, 2),
new Three.Face3(0, 1, 3),
//右面
new Three.Face3(1, 7, 3),
new Three.Face3(1, 5, 7),
//后面
new Three.Face3(5, 6, 7),
new Three.Face3(5, 4, 6),
//左面
new Three.Face3(4, 2, 6),
new Three.Face3(4, 0, 2),
//顶面
new Three.Face3(2, 7, 6),
new Three.Face3(2, 3, 7),
//底面
new Three.Face3(4, 1, 0),
new Three.Face3(4, 5, 1)
)
geometry.faces[0].color = geometry.faces[1].color = new Three.Color('red')
geometry.faces[2].color = geometry.faces[3].color = new Three.Color('yello')
geometry.faces[4].color = geometry.faces[5].color = new Three.Color('green')
geometry.faces[6].color = geometry.faces[7].color = new Three.Color('cyan')
geometry.faces[8].color = geometry.faces[9].color = new Three.Color('blue')
geometry.faces[10].color = geometry.faces[11].color = new Three.Color('magenta')
geometry.faces.forEach((face, index) => {
face.vertexColors = [
(new Three.Color()).setHSL(index / 12, 1, 0.5),
(new Three.Color()).setHSL(index / 12 + 0.1, 1, 0.5),
(new Three.Color()).setHSL(index / 12 + 0.2, 1, 0.5)
]
})
geometry.computeFaceNormals()
//geometry.computeVertexNormals() //对于立方体而言,无需执行此方法
//const material = new Three.MeshBasicMaterial({ color: 'red' })
const material = new Three.MeshPhongMaterial({ vertexColors: true })
//const material = new Three.MeshPhongMaterial({ color: 'red' })
const cube = new Three.Mesh(geometry, material)
scene.add(cube)
const render = (time: number) => {
cube.rotation.x = cube.rotation.y = time * 0.001
renderer.render(scene, camera)
window.requestAnimationFrame(render)
}
window.requestAnimationFrame(render)
const handleResize = () => {
if (canvasRef.current === null) {
return
}
const width = canvasRef.current.clientWidth
const height = canvasRef.current.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height, false)
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [canvasRef])
return (
<canvas ref={canvasRef} className='full-screen' />
)
}
export default HelloCustomGeometry
实际运行后,就会看到一个 炫彩的立方体。
本文小结:
通过自定义一个立方体的示例,可以看出,尽管是一个很简单的立方体,可我们都需要非常复杂的空间坐标计算配置,因此还是本文开头那段话:
- 如非必要,不要在 Three.js 中自定义几何体。
- 使用传统的 3D 软件建模,更香。
补充说明:
本系列教程,实际上是我一边学习 https://threejsfundamentals.org/threejs/lessons/ ,一边使用 React + TypeScript + 自己的语言和理解 重写一遍的。
本文对应的英文教程为:https://threejsfundamentals.org/threejs/lessons/threejs-custom-geometry.html
在原版的英文教程中,还有另外一个 通过一张图片来获得 纹理坐标(UV),进而生成一张地图的例子。
我个人感觉没有必要去这么深入学习自定义几何体,所以本文略过这个示例。
除此之外,官方还有单独一篇,使用 BufferGeometry 来自定义几何体的教程:
https://threejsfundamentals.org/threejs/lessons/threejs-custom-buffergeometry.html
我认为现阶段,没有必要如此这般的深入去学习自定义几何体,因为暂停这部分的学习。
如果你还有精力,可以去学习一下。
Three.js 基础知识总结
通过前面一系列的学习,我们终于将 Three.js 基础知识学习完成。
回顾一下我们都学习了哪些知识点:
- Three.js 简介、项目初始化、入门示例
- 图元、3D 文字、场景、材质、纹理、灯光、镜头、阴影、雾、离屏渲染、自定义几何体
- 辅助对象(灯光辅助对象 LightHelper 、镜头辅助对象 XxxxCameraHelper、坐标轴辅助对象 AxesHelper)、镜头轨道控制类(OrbitControls)
真心不容易,给自己一朵小红花!
…
我们本系列教程整体的规划是:
- 基础篇 (✓)
- 技巧篇 (x)
- 优化篇 (x)
- 解决方案 (x)
- WebVR (x)
- 实例篇 (x)
目前我们已经学习了基础篇,对 Three.js 已经有了足够的基础知识掌握,后面的学习都是建立在这些基础知识之上的。
接下来,进入技巧篇——按需渲染。
稍等,再啰嗦几句:
我们后续的讲解文章中,将加快进度,不再像 基础篇 这样如此细致,甚至是啰嗦。
因此,我希望你不看教程示例代码,而是自己独立敲出示例代码。如果做不到,那么你先不要着急进入下一篇,而是应该再回过头,反复阅读,反复敲几遍代码。
在做(动手敲代码)的过程中学习,而不是只看不动手。