Better

Ethan的博客,欢迎访问交流

three.js 基础(三)

three.js 对底层 WebGL 的高级封装的同时,保留底层开发的特性,仍然可以使用点、面、法向量、UV 构造几何体,以及编写着色脚本来创造自己想要的特殊材质。

自定义几何体

自定义缓冲几何体

  • 理解 BufferGeometry.attributes 的 position、normal、uv 和 color 属性
    • 顶点的组成部分:position、normal、uv、color,一个简单的顶点是所有组成部分的集合
    • 如果顶点需要其中的任一部分变得不同,那么它必须是一个不同的顶点
    • 比如方块的 Geometry,看起来在角的地方共用顶点,但是实际不是,因为 normal 和 uv 等通常不同
  • 理解 BufferGeometry.index 属性

研究内置 Geometry 实现

  • 分析 BoxGeometry 是如何默认支持六面贴图的
    • 本质上是依次生成六个面
    • 生成一个面的步骤为
      • 计算每个面的四个顶点坐标、法向量以及 uv 坐标
      • 计算 index 数组,指定每三个顶点构成一个三角面
      • 通过 addGroup(start, count, materialIndex) 设置该面使用的索引下标,表示从 start 到 start+count 之间顶点构成的所有三角面使用该材质
  • 理解 PlaneGeometry、CircleGeometry 的贴图原理
    • PlaneGeometry 没有调用 addGroup,因此仅能运用一个 Material
    • CircleGeometry 没有调用 addGroup,那么圆形应用方形贴图会是什么效果呢?
  • ShapeGeometry 的贴图原理
    • 当传入的 shapes 为数组时,针对每个 shape 定义 addGroup,否则不指定
    • 内部调用 ShapeUtils.triangulateShape 对轮廓和 holes 进行三角化得到面
  • 分析 CylinderGeometry 如何生成 BufferGeometry 和应用贴图
    • 分别两步,生成侧面以及上底和下底。
    • 侧面:根据设置的高度分段数和圆弧分段数,生成侧面的顶点坐标、法向量以及 uv 坐标
    • 侧面:根据设置的高度分段数和圆弧分段数,生成 index 以及计算 groupCount
    • 侧面:设置 addGroup,materialIndex 设置为 0,贴图效果应该是整个侧面
    • 底面:底面逻辑差不多,计算顶点、法向量、uv 坐标,计算 index,设置 materialIndex,top 为 1,bottom 为 2
  • 分析 ExtrudeGeometry 的贴图原理,UVGenerator 工作原理
    • 相比其他规则图形而言,ExtrudeGeometry 要复杂很多
    • 遍历所有给定的 shapes 计算 position 和 uv 信息,通过调用 computeVertexNormals 计算法向量
    • 如果自定义了 UVGenerator 则使用传入的生成器,否则使用内置的 WorldUVGenerator
    • 对给定的 Shape 进行采样处理,逆时针转换,然后三角化
    • uvGen 签名
      • generateTopUV
      • generateSideWallUV

注意应用 merge geometry 后,对于一整个 mesh,我们只能应用一个材质,因此只能使用一种颜色,某些场景下可以通过顶点着色法解决

贴图原理

  • 纹理坐标系统 [0, 1],WebGL 坐标系统 [-0.5, 0.5],维护好顶点坐标对应的纹理坐标,其余色值是通过光栅化自动生成的
  • 一个几何体对象不同三角形可以对应不同材质,通过 groups 设置
  • 通过 texture.offset 设置纹理偏移
  • 通过 texture.repeat 设置重复次数

uv 表示纹理坐标,因为 xyz 已经被顶点坐标使用,所以使用 uvw 表示纹理坐标,表示贴图映射到模型表面的依据,把表面的点与平面上的像素对应起来,通常通过 0-1 表示,其中 u 表示水平,v 表示垂直,w 表示垂直于显示器表面,一般情况只是在表面贴图,就涉及不到 w,所以常称为 uv。

Matrix

任何 3D 物体 Object3D 都有三个关联的矩阵:

  • Object3D.matrix: 存储物体的本地变换矩阵。 这是对象相对于其父对象的变换矩阵。
  • Object3D.matrixWorld: 对象的全局或世界变换矩阵。如果对象没有父对象,那么这与存储在矩阵 matrix 中的本地变换矩阵相同。
  • Object3D.modelViewMatrix: 表示对象相对于摄像机坐标系的变换矩阵,一个对象的 modelViewMatrix 是物体世界变换矩阵乘以摄像机相对于世界空间变换矩阵的逆矩阵,用于计算物体在相机空间的位置。

摄像机 Cameras 有三个额外的四维矩阵:

  • Camera.matrixWorldInverse: 视矩阵 - 摄像机世界坐标变换的逆矩阵。
  • Camera.projectionMatrix: 投影变换矩阵,表示将场景中的信息投影到裁剪空间。
  • Camera.projectionMatrixInverse: 投影变换矩阵的逆矩阵。

Object3D 相关基础

  • updateMatrix 根据最新的 position/rotation/scale 计算新的矩阵
  • updateMatrixWorld:更新自身及其后代的 matrixWorld
  • updateWorldMatrix:更新自身世界矩阵,支持参数往上更新父元素世界矩阵,往下更新子元素世界矩阵

关于 object.normalMatrix

  • 法向量矩阵:模型矩阵的逆转置矩阵,不过它非常特殊,是一个 3x3 的矩阵(mat3),其主要是作用是将顶点法向量从对象空间变换到视空间,以便在片元着色中计算光照,由于其仅关心方向信息,因此 3x3 矩阵足矣。
  • 将顶点法向量从对象空间变换到视图空间,以便在片元着色器中进行光照计算
  • 思考为什么 normal 不能和 vertex 一样进行视图空间转换呢。具体说明
    // 总结下来 normalMatrix = modelViewMatrix 左上 3x3 矩阵的逆的转置。那么为什么呢?本质上是数学计算的结果。
    // this.setFromMatrix4( matrix4 ).invert().transpose();
    object.normalMatrix.getNormalMatrix( object.modelViewMatrix );
    

对于正交矩阵,转置矩阵等于逆矩阵。那么什么是正交矩阵呢,正交矩阵是一个矩阵,所有行(列)都是单位长度,并且相互垂直。这意味着当两个向量乘以这样一个矩阵时,它们之间的夹角经过正交矩阵变换后与变换前相同。简单地说,变换保留了向量之间的角度关系,因此变换后的法线仍然垂直于切线,此外,它还保留了向量的长度。什么时候我们能确定 M 是正交的,当我们把几何运算限制在旋转和平移的时候。

相关矩阵定义:转置矩阵、逆矩阵、单位矩阵

  • 转置矩阵:行列互换得到的新矩阵
  • 逆矩阵:逆变换
  • 单位矩阵

Frustum

Frustum 数学对象,是实现 frustumCulled 字段的基础,用于进行渲染优化,跳过不在视窗内的物体。

从矩阵中还原出 Frustum。

const frustum = new THREE.Frustum();
const viewProjection = new THREE.Matrix4();
viewProjection.multiplyMatrices(
    camera.projectionMatrix, camera.matrixWorldInverse);
frustum.setFromProjectionMatrix(viewProjection);
const inFrustum = frustum.contains(someMesh));

meshline vs line2

meshline vs line2

  • meshline 是社区方案,目前处于不维护状态,line2 属于官方内置示例
  • meshline 针对锐角会出现一头细一头粗的情况,line2 表现良好
  • 三维视角下,都存在正面不是维持向上问题
  • line2 设置透明度后,顶点重叠部分效果不好(之前也正是因为该原因不支持透明度)

颜色空间

我们如何确定一个颜色

  • 为什么是三原色?因为人类对色彩的感知结果位于一个三维的线性空间中。最少需要三种颜色的光才能有足够的表达能力来表现各种颜色
  • 为什么选 RGB 作为三原色?因为色彩空间不是真正数学意义上的线性空间,从工程角度考虑,以 RGB 作为三原色,能让显示器能够显示更多的颜色
  • 对于 RGB 色彩空间来说,关键点在于两个:1. 如何选择三个作为基底的颜色;2. 如何定义白色。一旦选好这两个关键参数,那么从 CIE XYZ 空间到设备的 RGB 空间的转换就完全确定了
  • 不同的 RGB 空间所能表示的颜色范围是不一样的,并且我们可以推断出,即使是同样的 RGB 分量,在不同的 RGB 空间中所代表的颜色也是不一样的。所以我们在描述一个 RGB 颜色的时候,不仅需要描述它的 RGB 三个分量,还要说明是在哪个空间,这就是 ICC 文件的作用
  • 常见的网络环境下图片的色彩空间是 sRGB,有很多浏览器不能正确地解析图片自带的色彩空间说明,默认按照 sRGB 来进行解析。

three.js 颜色空间

  • 颜色空间由基色、白点、转换函数
  • 常用空间 sRGB 和 Linear-sRGB,两者都使用相同的基色和白点,因此具有相同的颜色范围,都使用 RGB 颜色模型,它们仅在传输函数上有所不同
    • Linear-sRGB 相对于物理光强度是线性的。
    • sRGB 使用非线性 sRGB 传递函数,更接近于人眼感知光的方式和普通显示设备的灵敏度
  • 这种区别很重要。光照计算和其他渲染操作通常必须在线性色彩空间中进行。然而,线性颜色存储在图像或帧缓冲区中的效率较低,并且在由人类观察者观看时看起来不正确。因此,输入纹理和最终渲染的图像通常会使用非线性的 sRGB 色彩空间。

颜色空间中三个角色

  • 输入颜色空间:纹理、三维模型等资源都会有关联的颜色空间,不是 Linear-sRGB 颜色空间的必须被转换,在开启了 ColorManagement 后,某些转换(对于 sRGB 中的十六进制和 CSS 颜色)可以自动进行
  • 工作颜色空间:渲染、插值和许多其他操作必须在开放域线性工作色彩空间中执行,其中 RGB 分量与物理照明成比例。在 three.js 中,工作颜色空间是 Linear-sRGB。
  • 输入颜色空间:输出到显示设备、图像或视频可能涉及从开放域 Linear-sRGB 工作色彩空间到另一个色彩空间的转换。这种转换可以在主渲染通道(WebGLRenderer.outputColorSpace)中执行,也可以在后期处理期间执行。自定义着色器必须实现自己的输出颜色空间转换。

关于色彩系统的重要破坏性更新

其他场景

大量物体的动画

  • 使用 Morphtargets 给每个顶点提供多个值,以及使用他们变形或 lerp 的方法
  • 通过设置 BufferGeometry 的 morphAttributes 来设置多个值,不同的是 position、color 等属性是一个 attribute 数组
  • 通过改变 mesh 的 morphTargetInfluences 属性来应用突变

天空盒

  • 使用 CubeTextureLoader 加载天空盒的六张图
  • 使用等距矩形贴图(Equirectangular map), 这是被 360 全景相机拍摄的一种特殊类型的图片。使用 WebGLCubeRenderTarget.fromEquirectangularTexture 从等距矩形纹理中生成一个立方体贴图,我们传入预期的立方体贴图的大小给 WebGLCubeRenderTarget

如何绘制透明的物体

  • 简单而言是使用 transparent 和 opacity 属性
  • 但有时候效果可能不太符合预期,因为对于 3d 物体的一般绘制方式,对于每个几何体,一次绘制一个三角形。当三角形的一个像素在被绘制的时候,会记录两件事情,一是像素的颜色,一是像素的深度。当下一个三角形绘制时,对于深度大于先前被记录的深度的像素,将不会被绘制,这就对于透明物体而言,有东西消失了一般。这个问题的解决方案是将透明的物体进行排序,排在后面的物体比排在前面的物体先绘制。
  • 三角形绘制的顺序和在几何体中构建的顺序是一致的,取决于我们从哪个方向看向这些三角形,距离摄像机近一些的先被绘制。因此,在后面的那些三角形不会被绘制。这就是我们看不到后面的面的原因。
  • alpha 测试是指像素的 alpha 值低于某个水平的时候,three.js 就不会绘制它。
  • 完美的透明是困难的,有着各种问题、取舍和变通方法。

浏览器限制了 WebGL 上下文(WebGL contexts)的数量。通常浏览器将其限制为 8 个,一旦超出这个数量,最先创建的 WebGL 上下文就会被自动弃用。

有趣的做法

  • 把光源也绑定到摄像机上,这样光源就会随着摄像机移动
  • 三维建模软件组成:本质上是一个 Group,组合多个 3D 对象层级,对象定义自己的 BufferGeometry 和 Materials 材质
  • 通过 CanvasTexture 运行时生成纹理
  • 通过 RenderTarget 根据 three api 动态生成纹理。渲染目标大体上指的是可以被渲染的纹理,当它被渲染之后,你可以像使用其他纹理一样使用它
  • 判断用户是点击还是拖动,除了判断按下和松开的距离外,我们可以也判断时间间隔,如果大于 200ms,则认为是拖动,可能效果更好

WebGLRenderTarget(离屏渲染)

  • renderer.render 方法如果指定了 WebGLRenderTarget,则渲染图像结果保存到该对象,不会显示在 canvas 上
  • renderer.render 方法如果没有指定渲染目标,渲染结果会直接显示到 canvas 画布上
  • 自身的 texture 属性可以获得 WebGL 渲染器的渲染结果,可以作为材质对象属性 map 的值

后期处理

  • 在结果被输出到 canvas 之前,我们也可以通过另外的一个 RenderTarget 并应用一些后置效果。这被称为 PostProcessing,因为它发生在主场景渲染过程之后。
  • 使用方式:EffectComposer 搭配各种 Pass 对象,每个 Pass 对象就是一个效果
  • EffectComposer 工作原理:创建两个 RenderTarget,交替应用不同的 pass 对象,实现叠加的效果
  • 对于几乎所有的后期处理 EffectComposer,RenderPass 都是必需的。因为它是初始输入。
  • 自定义后期处理:ShaderPass

关于 GPU 拾取

  • 概念简单,使用复杂,为了完成 GPU 拾取,对每个对象使用唯一的颜色进行离屏渲染,然后检查鼠标位置关联的像素颜色,通过颜色告诉我们哪个对象被选中。这里演示了 GPU 拾取的简单例子
  • 注意:raycaster 只会与有 geometry 对象做相交判断,因此 group 做相交会得到它的 children

有趣的例子:模型加载

  • 如何一步一步矫正模型
  • 如何让物体根据轨迹动起来

这两个示例好强



留言