three.js精灵及精灵材质、Shader源码分析

在Three.js中,Sprite(精灵)用于创建始终面向相机的2D元素,适用于标签、图标或粒子效果。本文将分析其源码及Shader实现。


1. sprite的基本使用方法

创建精灵材质

精灵材质有个特殊的参数rotation,可以让其旋转一定的角度。

javascript 复制代码
const material = new THREE.SpriteMaterial({
  color: 0xff0000,    // 颜色
  opacity: 0.8,       // 透明度
  transparent: true,   // 开启透明通道
  rotation: Math.PI/4  // 设置旋转角度
  map: texture // 设置精灵的贴图
});
创建Sprite对象
javascript 复制代码
const sprite = new THREE.Sprite(material);
scene.add(sprite); // 添加到场景
调整位置和大小

注意通过sprite的rotation设置旋转无效,只能通过材料进行旋转。

javascript 复制代码
sprite.position.set(0, 0, 0); // 位置
sprite.scale.set(2, 2, 0);    // 缩放(宽度、高度)

sprite源码分析

打开three.js源码中的Sprite.js文件,发现Sprite继承自Object3D,具有3D对象的通用特性:

javascript 复制代码
class Sprite extends Object3D {
  // 没有传入材料会默认创建一个
	constructor( material = new SpriteMaterial() ) {

		super();

		this.isSprite = true;

		this.type = 'Sprite'; // 类型标记为精灵

        // 初始化几何
		if ( _geometry === undefined ) {

			_geometry = new BufferGeometry();

          // 两个三角形的顶点及uv坐标,放在一起可减少GPU调用次数
			const float32Array = new Float32Array( [
				- 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
			] );

			const interleavedBuffer = new InterleavedBuffer( float32Array, 5 );
           // 两个三角形的索引
			_geometry.setIndex( [ 0, 1, 2,	0, 2, 3 ] );
           // 设置坐标及uv属性
			_geometry.setAttribute( 'position', new InterleavedBufferAttribute( interleavedBuffer, 3, 0, false ) );
			_geometry.setAttribute( 'uv', new InterleavedBufferAttribute( interleavedBuffer, 2, 3, false ) );

		}

		this.geometry = _geometry;
		this.material = material;

    // 中心点,这个值会在Shader中使用
		this.center = new Vector2( 0.5, 0.5 );

	}

从其构造函数中可看出,用户使用Sprite的时候不用提供几何数据,构造函数会用两个三角形,组成一个大小为1的矩形作为其几何。也就意味着Sprite的大小默认为1。但其绘制出来是一个矩形。如下图:

为什么是矩形而不是正方形呢?因为视口有长宽比,要想得到一个正方形需按长宽比设置其scale。

SpriteMaterial源码分析

打开SpriteMaterial.js文件,这个类就是一个简单的参数容器,继承Material,有材料的基础参数,其代码本身没有太大需要说明的。其参数中sizeAttenuation可实现近大远小的效果,但只对透视相机有效。

javascript 复制代码
class SpriteMaterial extends Material {

	constructor( parameters ) {

		super();

		this.isSpriteMaterial = true;

		this.type = 'SpriteMaterial';

		this.color = new Color( 0xffffff );

		this.map = null;

		this.alphaMap = null;

		this.rotation = 0;

		this.sizeAttenuation = true;

		this.transparent = true;

		this.fog = true;

		this.setValues( parameters );

	}

Shader实现

Sprite的之所以可以永远朝着摄像机,其核心原理在Shader代码中。Sprite的Shader在sprite.glsl.js文件中。其Vertex Shader代码如下:

glsl 复制代码
export const vertex = /* glsl */`
uniform float rotation; // 材料中设置的旋转量
uniform vec2 center; // 永远是(0.5, 0.5)

#include <common>
#include <uv_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>

void main() {

	#include <uv_vertex>

  // 计算中心的坐标,modelViewMatrix中包含了设置的position
	vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );

  // 获取设置的scale
	vec2 scale;
	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 = isPerspectiveMatrix( projectionMatrix );

		if ( isPerspective ) scale *= - mvPosition.z;

	#endif

  // 缩放之后顶点的坐标
	vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;

  // 计算旋转之后的坐标
	vec2 rotatedPosition;
	rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
	rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;

  // 对mvPosition进行一个偏移,计算出最终的顶点位置
	mvPosition.xy += rotatedPosition;

  // 进行投影变换
	gl_Position = projectionMatrix * mvPosition;

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

}
`;

看其代码会发现有很多#include开头的代码,这种代码代表着一段通用的代码片段,three.js会在Shader编译之前替换为GLSL片段。替换之后代码量会边长很多。这些代码片段都在ShaderChunk目录下:

要想看到完整的代码可用Spector.js抓帧。言归正传,回到Sprite的Vertex Shader代码中。我们知道顶点着色器程序是每个顶点都会运行一遍,也就是输入是两个三角形的四个顶点。计算的时候先计算精灵的中心点,代码如下:

javascript 复制代码
vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );

这行代码对应于js中的位置设置:
sprite.position.set(1, 0, 0); 

然后在摄像机坐标系下计算缩放及旋转,并累加到平移量中作为最终的坐标值。旋转的计算就是二维的转动公式。旋转的角度就是材料中设置的rotation。也就意味着Sprite的几何中的三角形坐标是摄像机坐标系下XY平面上的坐标,所以其始终对着摄像机。

javascript 复制代码
// 计算缩放之后顶点的坐标
vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;
// 其代码对应于 sprite.scale.set(2, 2, 0);

// 计算旋转之后的坐标
// rotation对应材料中设置的rotation参数
vec2 rotatedPosition;
rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;

// 对mvPosition进行一个偏移,计算出最终的顶点位置
mvPosition.xy += rotatedPosition;

最后进行投影计算,输出gl_Position:

javascript 复制代码
// 进行投影变换
gl_Position = projectionMatrix * mvPosition;

Fragment Shader代码如下:

javascript 复制代码
uniform vec3 diffuse;
uniform float opacity;

#include <common>
#include <uv_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <alphatest_pars_fragment>
#include <alphahash_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>

void main() {

	vec4 diffuseColor = vec4( diffuse, opacity );
	#include <clipping_planes_fragment>

	vec3 outgoingLight = vec3( 0.0 );

	#include <logdepthbuf_fragment>
	#include <map_fragment>
	#include <alphamap_fragment>
	#include <alphatest_fragment>
	#include <alphahash_fragment>

  // outgoingLight会作为片元的颜色输出
  // diffuseColor的值来自材料的color
  // 如果有贴图,会影响diffuseColor的值
	outgoingLight = diffuseColor.rgb;

	#include <opaque_fragment>
	#include <tonemapping_fragment>
	#include <colorspace_fragment>
	#include <fog_fragment>

}

Fragment Shader是计算光栅化之后片元的颜色。对于精灵材质,其颜色来自diffuseColor,这个值来自材料参数color。如果材料设置了贴图,会在贴图进行材料,进而影响这个值。具体的计算本文不展开,这块属于通用原理。感兴趣可以Spector.js抓取这个shader代码进行研究。

相关推荐
拿我格子衫来5 天前
图形编辑器基于Paper.js教程27:对图像描摹的功能实现,以及参数调整
开发语言·前端·javascript·图像处理·编辑器·图形渲染
程序员茶馆6 天前
【unity】Vulkan模式下部分Android机型使用VideoPlayer组件播放视频异常问题
游戏·unity·游戏引擎·图形渲染·unity3d·游戏开发
头发掉光的程序员7 天前
CPU与GPU之间的交互
c++·图形渲染·direct12
与火星的孩子对话10 天前
Unity进阶课程【四】Recorder 插件的使用 - 录制游戏画面、音频、动画、图片、无水印
游戏·unity·图形渲染·开源软件
踢足球的程序员·10 天前
OpenGL学习笔记(几何着色器、实例化、抗锯齿)
笔记·学习·图形渲染
踢足球的程序员·12 天前
OpenGL学习笔记(立方体贴图、高级数据、高级GLSL)
笔记·学习·图形渲染
GOTXX12 天前
【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
开发语言·数据库·c++·qt·图形渲染·图形化界面·qt新手入门
头发掉光的程序员14 天前
Raymarching Textures In Depth
ue5·游戏引擎·图形渲染