three.js 核心基础以及点线面的二维绘制基础
WebGL & Three.js 基础
对于图形绘制而言,只有点、线和面片,对应 WebGL 中就是
- gl.POINTS 画点
- gl.LINES 画线
- gl.TRIANGLES 画三角形
WebGL 绘制流程
- 获取顶点坐标
- 定义的几何体或者三维软件导出,生成顶点坐标
- 顶点坐标写入缓存区,写入显存,方便 GPU 更快读取
- 图元装配(三角化)
- 由顶点坐标生成一个个图元,将顶点坐标传入顶点着色器 gl_Position
- 顶点着色器先通过 apply 一个投影矩阵,将世界坐标转换成屏幕坐标,有多少个顶点,顶点着色器程序就会运行多少次
- 图元装配
- 光栅化(生成片元,即一个个像素点)
- 完成模型上色工作,通过 GPU 的片元着色器完成 gl_FragColor
- 公式为 gl_FragColor = color * light,生成多少片元(像素),运行多少次
WebGL 完整工作流程
- 准备数据阶段
- 顶点坐标:顶点数据存储在缓存区 Vertex Buffer Objects(因为数量巨大),以修饰符 attribute 传递给顶点着色器
- 索引:三角形绘制顺序,存储在索引缓存区 Index Buffer Objects
- uv:贴图坐标
- 法向量:决定光照效果,法线是垂直于我们想要照亮的物体表面的向量。法线代表表面的的方向,因此它们为光源和物体的交互建模中具有决定性作用
- 各种矩阵:以修饰符 uniform 传递给顶点着色器
- 生成顶点着色器
- 生成片元着色器:质地、光照、阴影
- 光栅化:能过片元着色器,确定好了每个片元的颜色,以及根据深度缓存区判断哪些片元被挡住了,不需要渲染,最终将片元信息存储到颜色缓存区,最终完成整个渲染
Three.js 是基于 WebGL 的封装,帮我们做了很多事
- 各种内置矩阵
- 生成顶点着色器,不需要自己写 opengl es 代码,毕竟比较难懂
- 提供材质、灯光对象,自动生成片元着色器
聊聊各种矩阵在顶点处理过程中发挥的作用
- 准备好顶点数据
- 应用模型矩阵(modelMatrix):物体自身的变换以及父元素的变换,在 Three.js 中的定义
- object.matrix:局部变换矩阵
- object.matrixWorld:物体的世界变换,如果没有父级,则和 matrix 等同
- 视图矩阵(viewMatrix):相机的位置及方向,在 Three.js 中的定义
- 相机的位置矩阵是:camera.matrixWorld,通过 camera 的 position、up 以及 lookAt 会改变位置视图矩阵
- 视图矩阵是:相机矩阵的逆矩阵,camera.matrixWorldInverse
- 投影矩阵(projectionMatrix):根据相机的投影类型,会有不同的效果。Three.js 中通过 camera.projectMatrix 直接拿到
- 传入顶点着色器:
gl_Position = position * modelMatrix * viewMatrix * projectionMatrix
- 图元装配
注意点1:Three.js 使用的列向量,同时矩阵存储列式存储
注意点2:最终的投影是在 GLSL 顶点着色器中计算的。在一次绘制中,ProjectionMatrix 和 CameraMatrixWorldInverse 一般不会发生变化,而ObjectMatrixWorld 每个物体都可能不同, 所以为了减少顶点着色器中的计算量,有些三维引擎会在 javascript 程序中提前计算出 ProjectionMatrix * CameraMatrixWorldInverse 的值传递给顶点着色器,这个矩阵一般称为 ViewProjectionMatrix
我们需要特别注意的是,顶点着色器和片元着色器是通过 GPU 进行计算的,比如旋转一个模型,我们可以手动计算每个顶点坐标进行变换后的新坐标,然后重新进行渲染,但针对复杂图形,通常顶点是非常多的,这样通过 CPU 计算,难免性能会下降,更合适的方式是通过记录一个 modelMatrix,然后传入顶点着色器中进行计算。
片元着色器处理流程
- 材质颜色
- 灯光颜色
- 雾化颜色
- 传入片元着色器:gl_FragColor = vec4(color.rgb * vLighting, color.a)
BufferGeometry
BufferGeometry 是所有 Geometry 的核心基类。你可以使用它创建任何形状。内置的其他 Geometry 仅仅是特定场景的 High Level Api 封装而已。
BufferGeometry 是点、线或面片的有效表述,包括
- 顶点位置 position
- 面片索引 index
- 法向量 normal
- 颜色值 color
- UV 坐标 uv
- 自定义缓存属性值
常用 Api
- 变换相关:translate、rotate、scale、center
- 工具相关:merge、dispose、computeBoundingBox、computeBoundingSphere
- 初始化相关:setFromPoints
- 属性值
- index:允许顶点在多个三角面间可以重用,这样的顶点被称为已索引的三角面片,每个三角面片和三个顶点的索引相关。如果该 attribute 没有设置过,则认定没三个连续的位置代表一个三角面片,默认值为 null
- setIndex/getIndex
- setAttribute/getAttribute/deleteAttribute
Material
材质的抽象类,不能被直接实例化。
包含太多高级参数了,列举几个常见的
- fog 是否受雾影响
- opacity 透明度,如果材质的 transparent 属性未设置为 true,则材质将保持完全不透明,此值仅影响其颜色
- polygonOffset、polygonOffsetFactor、polygonOffsetUnits
- side:FrontSide、BackSide、DoubleSide
- transparent
- vertexColors:是否使用顶点着色,如果该选项设置为 true,则 color 属性失效
- visible:材质是否可见
Texture
创建一个纹理贴图,将其应用到一个表面。
常见属性
- image:通常由 TextureLoader.load 方法创建
- mapping:图像如何应用到物体上,默认方式 uvmapping 表示应用纹理坐标
- wrapS:纹理贴图在水平方向上将如何包裹,在 UV 映射中对应于 U
- wrapT:纹理贴图在垂直方向上将如何包裹,在 UV 映射中对应于 V
- repeat:设置重复次数
- rotation
- center
关于点
相关类: Points、BufferGeometry、PointsMaterial
其中 PointsMaterial 常用属性有
- color
- size 点像素尺寸
- map 贴图
关于线
相关类
- Geometry:BufferGeometry
- Material:LineBasicMaterial、LineDashedMaterial
- Object:Line、LineSegments
linecap 和 linejoin 会被 webgl 忽略
以上的实现方案,会存在一些限制,最典型的就是 linewidth 不会生效。因此 examples 中有提供了其他封装
- LineMaterial
- worldUnits 用于控制否是响应缩放
- resolution 用于使得 pixel 生效所必须
- LineGeometry
- Line2
进一步使用可以参考 EdgesGeometry 源码
关于平面
相关类
- Geometry:ShapeGeometry、PlaneGeometry、RingGeometry
- Material:MeshBasicMaterial、MeshLambertMaterial、MeshPhongMaterial、……
- Object:Mesh
关于 Geometry 这一块,最灵活的当属于 ShapeGeometry 了。它依赖 Shape 类,你可以创建任意形状,以及 holes 的支持。更重要的 Shape 继承 Path 类,不仅可以创建多段线,甚至各种曲线也是 OK 的。具体见最后 Path 的介绍
实用工具
BufferGeometryUtils
- mergeBufferGeometries
- mergeBufferAttributes
- mergeVertices
ShapeUtils
- area 计算形状面积
- isClockWise 判断是不是顺时针
- triangulateShape 三角化
SceneUtils
- createMultiMaterialObject
Path 提供很多实用 api 用于绘制多段线,甚至曲线
- moveTo 移动起点
- absarc/arc 绘制圆弧
- absellipse/ellipse 绘制椭圆弧
- bezierCurveTo 三次贝塞尔曲线
- lineTo 连接直线
- quadraticCurveTo 二次贝塞尔曲线
- splineThru 样条曲线
规则的曲线比如圆、椭圆、抛物线都可以用一个函数去描述,对于不规则的曲线无法使用一个特定的函数去描述,这也就是样条曲线和贝塞尔曲线出现的原因。
组合曲线:CurvePath,把多个圆弧线、样条曲线、直线等多个曲线合并成一个曲线
// 创建组合曲线对象 CurvePath
const curvePath = new THREE.CurvePath();
// 把多个线条插入到 curvePath中
curvePath.curves.push(line1, arc, line2);
得到 path 后,通过 getPoints(),传入采样点数量得到所有的点坐标,通过 geometry. setFromPoints 即可完成 geometry 的创建
你可能观察到 BufferGeometry 的 boundingBox 和 boundingSphere 的属性,Three.js 中也提供了很多数学工具供我们使用,不在需要我们手动去计算。
首先我们先了解一下包围盒,主要有轴对齐包围盒(AABB)和有向包围盒(OBB)
- 包围盒广泛地应用于碰撞检测,比如射击、点击、相撞等,每一个物体都有自己的包围盒。因为包围盒一般为规则物体,因此用它来代替物体本身进行计算,会比直接用物体本身更加高效和简单。
- AABB 包围盒更常见,因为生成方法简单,它是与坐标轴对齐的。但也有不足,就是它不随物体旋转,比如一只笔水平或垂直放,则包围盒会比较小,斜着放则包围盒就会比较大
- OBB 有向包围盒,它始终沿着物体的主成分方向生成最小的一个矩形包围盒,可用于较精确的碰撞检测。three.js 有对于 OBB 的封装,具体见OBB
Box2 包围矩形
- containsBox
- containsPoint
- distanceToPoint
- expandByPoint
- expandByScalar
- expandByVector
- getCenter
- getSize
- intersect
- intersectsBox
- isEmpty
- makeEmpty
- setFromCenterAndSize
- setFromPoints
- translate
- union
Box3 包围盒,三维长方体包围区域
- 调用模型的 geometry.boundingBox,然后乘以模型的世界变换矩阵,就可以实现包围盒的获取(AABB 盒)。注意:几何体包围盒属性 boundingBox 默认值为空 null,执行 computeBoundingBox() 方法才会计算该几何体的包围盒 Box3,然后赋值给 boundingBox 属性
- 调用模型的 geometry.center 方法,也就是把几何体对应的包围盒中心平移到坐标原点
- applyMatrix4
- containsBox
- containsPoint
- distanceToPoint
- expandByObject:获取到此模型的包围盒(AABB 盒),而且还能包含它的子对象
- expandByPoint
- expandByScalar
- getBoundingSphere
- getSize 返回长宽高尺寸
- getCenter 返回几何体中心
- intersect
- intersectsBox
- intersectsPlane
- intersectsTriangle
- isEmpty
- makeEmpty
- setFromArray
- setFromBufferAttribute
- setFromCenterAndSize
- setFromPoints
- setFromObject
- translate
- union
Sphere 包围球
- 通过 center 和 radius 描述
- 几何体包围球属性 boundingSphere 使用方式和包围盒属性 boundingBox 一样
- containsPoint
- distanceToPoint
- expandByPoint
- isEmpty
- makeEmpty
- getBoundingBox
- intersectsBox
- intersectsPlane
- set
- setFromPoints
- translate
- union
hhhh,看到这些 api 命名,真的是体会到命名的快乐。同时感觉 Three.js 内置提供的数学库,已经能应对很多常用的场景了,不需要在自行编写或使用第三方库,如 jsts、turfjs 等
fill/stroke
你如果想图形支持类似 Canvas2D 对于 fill 和 stoke 样式的配置,这在 three.js 中会比较复杂一点,因为 three.js 中线是线、面是面,不存在 fill 这种概念。因此我们需要分两步走,第一拆成线框的方式,使用线的方式绘制,第二创建 Shape,使用 mesh 的方式进行绘制。
在 Three 中实现类似 canvas2D 的 stroke 和 fill 效果,需要通过平面和线来实现
- createPolygon:Shape、ShapeGeometry、MeshBasicMaterial
- createLine:BufferGeometry、LineBasicMaterial
three-dxf
意外发现一个three-dxf库,还是有不少借鉴点的
- 如何实现 fitExtents
- 如何利用 THREE 内置的 Curve 对象进行曲线绘制的
- 如何解析和渲染文本的
- 如何解析元素颜色的,entity 有设置颜色,则使用 entity.color,否则获取 layer 颜色,再没有则使用默认颜色
ShapeBufferGeometry 源码分析
分析 ShapeGeometry 需要先了解 Path 和 Shape 类
其实源码比较简单,流程图如下