乐趣区

关于前端:Threejs-Sprite源码解析

本文会解说一下 Three.js(r105)的 Sprite,次要包含以下几个方面:

  1. 简略介绍和应用
  2. Sprite 的 Geometry
  3. 始终朝向相机原理解析
  4. 大小不变原理解析

简略介绍和应用

在我的项目中,我次要会应用 Sprite 创立一些三维场景中的标签。上面举个简略的例子来理解下 Sprite 的根本用法:

const map = new THREE.TextureLoader().load("sprite.png")
const material = new THREE.SpriteMaterial({map})
const sprite = new THREE.Sprite(material)
scene.add(sprite)

成果如下(画布大小 300px * 400px,灰色背景区域是画布区域,下同):

Sprite 有几个个性:

是一个立体

Sprite 是一个立体,也就是 Sprite 的 Geometry 形容的是一个立体矩形。上面解说源码的时候会说到。

始终朝向相机

咱们晓得,3D 场景中的物体是由一个个三角形组合进去的,每个三角形都有一个法线。法线方向和相机眼帘方向能够是任意关系。Sprite 的个性就是这个立体矩形的法线方向和相机眼帘方向 始终 是平行的,方向相同。

最初的渲染成果就是绘制进去的 Sprite始终 是矩形,而不会存在 变形

比方把一个一般立体沿着 X 轴旋转 45 度:

const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({color: 0x00ff00})
const plane = new THREE.Mesh(geometry, planeMaterial)
plane.rotation.x = Math.PI / 4
scene.add(plane)

成果如下图所示:

而给 Sprite 做同样的操作(为了比照,把贴图换成了纯色):

const material = new THREE.SpriteMaterial({color: 0x00ff00})
const sprite = new THREE.Sprite(material)
sprite.rotation.x = Math.PI / 4
scene.add(sprite)

成果如下图所示:

能够设置勾销透视相机近大远小的成果

透视相机(PerspectiveCamera)模仿了人眼看世界的成果:近大远小。

Sprite 默认也是近大远小的,然而你能够通过 SpriteMaterial 的 sizeAttenuation 属性来勾销这个成果。前面会具体解说 sizeAttenuation 的实现原理。

Sprite 的 Geometry

先看下 Sprite 构造函数的源码(Sprite.js):

var geometry; // 正文 1:全局 geometry

function Sprite(material) {Object3D.call( this);

    this.type = 'Sprite';

    if (geometry === undefined) { // 正文 1:全局 geometry

        geometry = new BufferGeometry(); // 正文 1:全局 geometry

        var float32Array = new Float32Array( [ // 正文 2:顶点信息和贴图信息,一共四个顶点
            - 0.5, - 0.5, 0, 0, 0,
            0.5, - 0.5, 0, 1, 0,
            0.5, 0.5, 0, 1, 1,
            - 0.5, 0.5, 0, 0, 1
        ] );

        var interleavedBuffer = new InterleavedBuffer(float32Array, 5); // 正文 2:每个顶点信息包含 5 个数据

        geometry.setIndex([ 0, 1, 2,    0, 2, 3] ); // 正文 2:两个三角形
        geometry.addAttribute('position', new InterleavedBufferAttribute( interleavedBuffer, 3, 0, false) ); // 正文 2:顶点信息,取前三项
        geometry.addAttribute('uv', new InterleavedBufferAttribute( interleavedBuffer, 2, 3, false) ); // 正文 2:贴图信息,取后两项

    }

    this.geometry = geometry; // 正文 1:全局 geometry
    this.material = (material !== undefined) ? material : new SpriteMaterial();

    this.center = new Vector2(0.5, 0.5); // 正文 3:center 默认是(0.5, 0.5)

}

从下面的代码咱们看出两个信息:

  1. 正文 1:所有的 Sprite 共享一个 Geometry;
  2. 正文 2:

    1. 每个顶点信息的长度是 5,前三项是顶点信息的 x、y、z 值,后两项是贴图信息,这两个信息存储在了一个数组中;
    2. 一共定义了四个顶点,两个三角形。四个顶点的坐标别离是 A(-0.5, -0.5, 0)B(0.5, -0.5, 0)C(0.5, 0.5, 0)D(-0.5, 0.5, 0)。两个三角形是 T1(0, 1, 2)T2(0, 2, 3),也就是 T1(A, B, C)T2(A, C, D)。这两个三角形组成的矩形的中心点 O 的坐标是 (0, 0, 0)。这两个三角形组成了一个 1 X 1 的正方形。如下图所示(Z 轴都是 0,此处不显示):

始终朝向相机原理解析

对于 3D 场景中的一个点,最初地位的计算形式个别如下:

gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);

其中,position是 3D 场景中的坐标,这个坐标要通过

  1. 物体本身坐标系的矩阵变换(位移、旋转、缩放等)(modelMatrix)
  2. 相机坐标系的矩阵变换(viewMatrix)
  3. 投影矩阵变换(projectionMatrix)

也就是,最初应用的坐标是 3D 场景中的坐标通过一系列固有的变换失去的。其中,上述相机坐标系的矩阵变换和相机是有关系的,也就是相机的信息会影响最初的坐标。

然而,Sprite 是始终朝向相机的。咱们能够揣测,Sprite 地位的计算必定不是走的下面这个固有变换。上面让咱们看下 Sprite 的实现形式。

这块的逻辑是在 shader 外面实现的(sprite_vert.glsl.js):

void main() {
    // ...
    vec4 mvPosition = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0); // 正文 1:利用模型和相机矩阵在点 O 上

    // 正文 6:缩放相干

    vec2 alignedPosition = (position.xy - ( center - vec2( 0.5) ) ) * scale;
    // 正文 2:center 默认值是 vec2(0.5),scale 是模型的缩放,简略状况下是 1,所以,此处能够简化为:
    // vec2 alignedPosition = position.xy;

    vec2 rotatedPosition;
    rotatedPosition.x = cos(rotation) * alignedPosition.x - sin(rotation) * alignedPosition.y;
    rotatedPosition.y = sin(rotation) * alignedPosition.x + cos(rotation) * alignedPosition.y;
    // 正文 3:利用旋转,没有旋转的状况下,rotatedPosition = alignedPosition
    // 其实就是把 rotation 等于 0 带入上述计算过程

    mvPosition.xy += rotatedPosition; // 正文 4:在点 O 的根底上,从新计算每个顶点的坐标,Z 重量不变,保障相机眼帘和 Sprite 是垂直的

    gl_Position = projectionMatrix * mvPosition; // 正文 5:利用投影矩阵
    // ...
}

顶点坐标的计算过程如下:

  1. 正文 1:计算点 O 在相机坐标系中的坐标;
  2. 正文 2 -4:以 O 为核心,在垂直相机眼帘的立体 Plane1 上,间接求取各个顶点在相机坐标系的坐标;
  3. 正文 5:上述求取的坐标间接就在相机坐标系了,所以不必再利用 modelMatrix 和 viewMatrix,间接利用 projectionMatrix 就行了;

ABCD 在空间的理论地位和理论绘制的 ABCD 的地位 A ’B’C’D’ 如下图所示:

大小不变原理解析

后面咱们提到,能够通过设置 SpriteMaterialsizeAttenuation 属性来勾销透视相机近大远小的成果。这块的实现逻辑还是在 shader 外面实现的(sprite_vert.glsl.js):

void main() {

    // ...

    vec4 mvPosition = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);

    vec2 scale; // 正文 1:依据模型矩阵计算缩放
    scale.x = length(vec3( modelMatrix[ 0].x, modelMatrix[0].y, modelMatrix[0].z ) );
    scale.y = length(vec3( modelMatrix[ 1].x, modelMatrix[1].y, modelMatrix[1].z ) );

    #ifndef USE_SIZEATTENUATION

        bool isPerspective = (projectionMatrix[ 2][3] == - 1.0 ); // 正文 2:判断是否是透视相机

        if (isPerspective) scale *= - mvPosition.z; // 正文 2:依据相机间隔利用不同的缩放值

    #endif

    vec2 alignedPosition = (position.xy - ( center - vec2( 0.5) ) ) * scale;
    // 正文 2:顶点信息计算思考缩放因子,此处,同样不思考 center 的影响,简化后如下:// vec2 alignedPosition = position.xy * scale;

    // ... 正文 3:同上,计算顶点地位过程

    #include <logdepthbuf_vertex>
    #include <clipping_planes_vertex>
    #include <fog_vertex>

}

透视相机有近大远小的成果。如果要打消这个成果,能够给物体在不同的相机深度的时候,设置不同的缩放比例。显然,这个缩放比例和相机的深度相干。Sprite 也是这样实现的:

  1. 正文 1:计算模型自身利用的缩放,包含程度方向和垂直方向。在没有设置的状况下,这两个方向上的缩放比例都是 1;
  2. 正文 2:把缩放比例和相机间隔关联上;
  3. 正文 3:计算 A ’B’C’D’ 的地位时,加上缩放的影响。

接下来,咱们看下要害代码 scale *= - mvPosition.z; 为什么是正当的?

首先,介绍下物体理论渲染大小和相机的关系。这里,咱们只思考最简略的状况:在和相机眼帘垂直的立体上的一条竖直线段 L 理论渲染的大小是多少?

计算过程如下图所示:

在垂直方向上,理论渲染的大小为:

PX = L / (2 * Z * tan(fov / 2)) * canvasHeight

其中,L 是物体的理论大小,Z 是物体间隔相机的远近,fov 是弧度值,canvasHeight是画布的高度。

显然,理论显示的大小是和 Z 相干的。Z 越大,PX 的值越小,Z 越小,PX 的值越大。那么,要想打消 Z 的影响,咱们能够给 L 乘上一个 Z,也就是L' = L * Z

PX = L' / (2 * Z * tan(fov / 2)) * canvasHeight
PX = (L * Z) / (2 * Z * tan(fov / 2)) * canvasHeight
PX = L / (2 * tan(fov / 2)) * canvasHeight

在物体大小固定,相机视角固定,画布固定的状况下,理论显示的大小 PX 就是一个固定的值,也就实现了 Sprite 大小不变的成果。

这也是下面 scale *= - mvPosition.z; 的作用。mvPosition.z 就是咱们上述公式中的 Z。之所以还有一个负号,是因为在相机坐标系下,相机看向的方向是 Z 轴负方向,所以呈现在相机眼帘内的物体的 Z 值是负的,所以加了一个负号变成负数。

那么,如何设置 Sprite 的显示大小呢,比方让 Sprite 的显示高度为100px

其实,从下面的公式咱们就能够得出:

PX = L / (2 * tan(fov / 2)) * canvasHeight
L = PX * (2 * tan(fov / 2)) / canvasHeight

咱们以 fov90 度 为例,因为这个时候的 tan(PI / 2 / 2) 正好是 1,所以计算起来和看起来都比拟直观,此时:

L = PX * (2 * tan(fov / 2)) / canvasHeight
L = PX * 2 / canvasHeight

在 Sprite 的 Geometry 局部,咱们晓得 Geometry 是一个 1 X 1 的矩形。所以 L 是多少,咱们给物体增加 L 倍的缩放即可。

比方当相机视角是 90 度,画布大小是 300px * 400px,想要 Sprite 显示的高度是 100px 的话,设置 scale 为 100 * 2 / 400 = 0.5 即可:

const material = new THREE.SpriteMaterial({
  color: 0xff0000, // 应用纯色的材质举例,纯色的容易判断边界,能够通过截图的形式验证理论渲染的像素大小是否正确
  sizeAttenuation: false // 敞开大小追随相机间隔变动的个性
})
const sprite = new THREE.Sprite(material)
sprite.scale.x = 0.5 // 正文 1:X 轴方向也设置为 0.5
sprite.scale.y = 0.5
scene.add(sprite)

成果截图如下:

下面的代码 正文 1 局部,咱们也应用了和 Y 轴缩放一样的缩放比例,最初理论显示的 X 轴的像素大小也是 100px。如果咱们想要 X 轴方向显示不同的像素大小怎么办呢?其实和计算垂直方向是一样的情理。

通过上图,能够发现 X 轴像素大小的计算方法和 Y 轴统一。次要起因在于上图中的正文①②都利用了一个相机的宽高比,所以对消了。这也就是为什么 sprite.scale.x = 0.5 渲染的 X 轴的像素大小也是 100px 的起因。

比方,还是下面那个例子,如果想让 X 轴显示 75px,能够设置 scale.x = 75 * 2 / 400 = 0.375,成果如下图所示:

总结

本文介绍了 Sprite 的简略应用和一些个性的实现原理,心愿大家有所播种!

如有谬误,欢送留言探讨。

退出移动版