当前位置: 首页 > Web前端 > HTML

Three.jsSprite源码分析

时间:2023-03-27 23:46:07 HTML

本文将对Three.js(r105)的Sprite进行讲解,主要包括以下几个方面:介绍与使用在项目中,我主要是使用Sprite在一些3D场景中创建标签。下面是一个简单的例子来理解Sprite的基本用法:constmap=newTHREE.TextureLoader().load("sprite.png")constmaterial=newTHREE.SpriteMaterial({map})constsprite=newTHREE.Sprite(material)scene.add(sprite)效果如下(画布大小为300px*400px,灰色背景区域为画布区域,下同):Sprite有几个特点:它是一个平面Sprite是一个平面,也就是Sprite的Geometry描述是一个扁平的矩形。下面我在讲解源码的时候会讲到。永远面对镜头我们知道3D场景中的物体是由三角形组成的,每个三角形都有一条法线。法线方向和相机视角方向可以是任何关系。Sprite的特点是这个平面矩形的法线方向总是平行于相机的视线方向,方向相反。最终的渲染效果是绘制出来的Sprite始终是一个没有变形的矩形。例如,将普通平面沿X轴旋转45度:constgeometry=newTHREE.PlaneGeometry(1,1)constplaneMaterial=newTHREE.MeshBasicMaterial({color:0x00ff00})constplane=newTHREE.Mesh(geometry,planeMaterial)plane.rotation.x=Math.PI/4scene.add(plane)效果如下图:对Sprite做同样的操作(为了对比,将贴图换成纯色):constmaterial=newTHREE.SpriteMaterial({color:0x00ff00})constsprite=newTHREE.Sprite(material)sprite.rotation.x=Math.PI/4scene.add(sprite)效果如下图所示:可以设置取消相机近大远小的效果相机(PerspectiveCamera)模拟人眼看世界的效果:近的大,远的小。默认情况下,精灵也是远近小,但是你可以通过SpriteMaterial的sizeAttenuation属性来取消这个效果。后面会详细讲解sizeAttenuation的实现原理。Sprite的Geometry首先看一下Sprite的构造函数(Sprite.js)的源码:vargeometry;//注意1:全局几何函数Sprite(material){Object3D.call(this);this.type='雪碧';if(geometry===undefined){//注1:全局几何geometry=newBufferGeometry();//注1:全局几何varfloat32Array=newFloat32Array([//注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]);varinterleavedBuffer=newInterleavedBuffer(float32Array,5);//注2:每个顶点信息包括5条数据geometry.setIndex([0,1,2,0,2,3]);//注意2:两个三角形geometry.addAttribute('position',newInterleavedBufferAttribute(interleavedBuffer,3,0,false));//注2:顶点信息,取前三项geometry.addAttribute('uv',newInterleavedBufferAttribute(interleavedBuffer,2,3,false));//注2:纹理信息,取最后两项}this.geometry=geometry;//注1:全局几何this.material=(material!==undefined)?材质:新的SpriteMaterial();this.center=newVector2(0.5,0.5);//Note3:centerdefaultsto(0.5,0.5)}从上面的代码中,我们可以看到两条信息:Note1:所有的Sprites共享一个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)。这两个三角形形成一个1X1的正方形。如下图(Z轴全0,此处不显示):始终面向相机原理分析对于3D场景中的一个点,其最终位置的计算方式一般如下:gl_Position=projectionMatrix*viewMatrix*模型矩阵*vec4(位置,1.0);其中,position是3D场景中的坐标,这个坐标需要经过物体自身坐标系(modelMatrix)的矩阵变换(位移,旋转,缩放等)相机坐标系(viewMatrix)的矩阵变换)和投影矩阵变换(projectionMatrix)即最后使用的坐标是通过3D场景中坐标的一系列固有变换得到的。其中,上述相机坐标系的矩阵变换与相机有关,即相机的信息会影响最终的坐标。但是,精灵始终面向相机。我们可以推测,Sprite位置的计算肯定不是上面的固有变换。我们来看看Sprite是如何实现的。这块的逻辑是在着色器(sprite_vert.glsl.js)中实现的:voidmain(){//...vec4mvPosition=modelViewMatrix*vec4(0.0,0.0,0.0,1.0);//Note1:ApplicationModelandcameramatrixatpointO//Note6:Scalingrelatedvec2alignedPosition=(position.xy-(center-vec2(0.5)))*scale;//注2:center的默认值为vec2(0.5),scale为模型的缩放比例,简单情况下为1,所以这里可以简化为://vec2alignedPosition=position.xy;vec2旋转位置;rotatedPosition.x=cos(旋转)*alignedPosition.x-sin(旋转)*alignedPosition.y;rotatedPosition.y=sin(旋转)*alignedPosition.x+cos(旋转)*alignedPosition.y;//注3:应用旋转时,rotatedPosition=alignedPosition//其实旋转等于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属性相机近大远小的效果这个block的实现逻辑还是在shader(sprite_vert.glsl.js)中实现:vec2规模;//注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));#ifndefUSE_SIZEATTENUATIONboolisPerspective=(projectionMatrix[2][3]==-1.0);//注2:判断是否为透视相机if(isPerspective)scale*=-mvPosition.z;//注意2:根据相机距离应用不同的缩放值#endifvec2alignedPosition=(position.xy-(center-vec2(0.5)))*scale;//注2:顶点信息的计算考虑了比例因子。这里也没有考虑中心的影响。简化如下://vec2alignedPosition=position.xy*scale;//...注3:同上计算顶点位置的过程#include#include#include}透视相机有近的大远的效果更小。如果要消除这种影响,可以为不同相机深度的物体设置不同的缩放比例。显然,这个变焦比与相机的景深有关。Sprite也是这样实现的:注1:计算模型自身应用的缩放比例,包括水平方向和垂直方向。在没有设置的情况下,两个方向的缩放比例都是1;注2:将变焦倍率与相机距离相关联;注3:在计算A'B'C'D'的位置时,加上zoomImpact。接下来我们看关键代码scale*=-mvPosition.z;为什么是合理的?首先介绍物体实际渲染尺寸与相机的关系。这里,我们只考虑最简单的情况:垂直于相机视线的平面上垂直线段L的实际渲染尺寸是多少?计算过程如下图所示:在垂直方向,实际渲染尺寸为:PX=L/(2*Z*tan(fov/2))*canvasHeight其中,L为物体实际尺寸,Z是物体到相机的距离Far和near,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当物体尺寸固定时,相机视角固定,并且canvas是固定的,实际显示的尺寸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是一个1X1的矩形。那么L是什么,我们可以给物体加上L倍缩放。例如当摄像机视角为90度,画布尺寸为300px*400px,Sprite显示高度为100px时,设置比例为100*2/400=0.5:constmaterial=newTHREE.SpriteMaterial({color:0xff0000,//以纯色材质为例,纯色材质容易判断边界,可以通过截图验证实际渲染的像素大小是否正确sizeAttenuation:false//关闭大小随相机距离变化的特性})constsprite=newTHREE。(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以及一些特性的实现原理,希望大家有所收获!如有错误,欢迎留言讨论。