本文会解说一下Three.js(r105)的Sprite,次要包含以下几个方面:
- 简略介绍和应用
- Sprite的Geometry
- 始终朝向相机原理解析
- 大小不变原理解析
简略介绍和应用
在我的项目中,我次要会应用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 / 4scene.add(plane)
成果如下图所示:
而给Sprite做同样的操作(为了比照,把贴图换成了纯色):
const material = new THREE.SpriteMaterial({ color: 0x00ff00 })const sprite = new THREE.Sprite(material)sprite.rotation.x = Math.PI / 4scene.add(sprite)
成果如下图所示:
能够设置勾销透视相机近大远小的成果
透视相机(PerspectiveCamera)模仿了人眼看世界的成果:近大远小。
Sprite默认也是近大远小的,然而你能够通过SpriteMaterial的sizeAttenuation属性来勾销这个成果。前面会具体解说sizeAttenuation的实现原理。
Sprite的Geometry
先看下Sprite构造函数的源码(Sprite.js):
var geometry; // 正文1:全局geometryfunction 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:所有的Sprite共享一个Geometry;
正文2:
- 每个顶点信息的长度是5,前三项是顶点信息的x、y、z值,后两项是贴图信息,这两个信息存储在了一个数组中;
- 一共定义了四个顶点,两个三角形。四个顶点的坐标别离是
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场景中的坐标,这个坐标要通过
- 物体本身坐标系的矩阵变换(位移、旋转、缩放等)(modelMatrix)
- 相机坐标系的矩阵变换(viewMatrix)
- 投影矩阵变换(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:计算点
O
在相机坐标系中的坐标; - 正文2-4:以
O
为核心,在垂直相机眼帘的立体Plane1上,间接求取各个顶点在相机坐标系的坐标; - 正文5:上述求取的坐标间接就在相机坐标系了,所以不必再利用modelMatrix和viewMatrix,间接利用projectionMatrix就行了;
ABCD在空间的理论地位和理论绘制的ABCD的地位A'B'C'D'如下图所示:
大小不变原理解析
后面咱们提到,能够通过设置 SpriteMaterial
的 sizeAttenuation
属性来勾销透视相机近大远小的成果。这块的实现逻辑还是在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;
- 正文2:把缩放比例和相机间隔关联上;
- 正文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)) * canvasHeightPX = (L * Z) / (2 * Z * tan(fov / 2)) * canvasHeightPX = 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)) * canvasHeightL = PX * (2 * tan(fov / 2)) / canvasHeight
咱们以 fov
是 90度
为例,因为这个时候的 tan(PI / 2 / 2)
正好是 1
,所以计算起来和看起来都比拟直观,此时:
L = PX * (2 * tan(fov / 2)) / canvasHeightL = 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.5sprite.scale.y = 0.5scene.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的简略应用和一些个性的实现原理,心愿大家有所播种!
如有谬误,欢送留言探讨。