ThreeJS自定义着色器

说起着色器的学习,强烈推荐康玉之编写的《GPU编程与CG语言之阳春白雪下里巴人》,尤其是此书的序言部分针砭时弊毫无隐晦的指出了当今学术现状的问题,更是发出了“开天辟地,日月重光”的愤慨。着色器的编程语言的根是CG(C For Graphics),语言风格类似C语言或者说就是;在ThreeJS当中,着色器的编程风格也是类似C语言的,引擎最终会通过字符串解析将着色器解析成正常的GLSL语法从而去编译连接。

1.引擎为着色器提供的内置变量和属性

所谓的内置变量和属性就是说这些东西不用开发人员传入到着色器当中,引擎生成最终的shader代码的时候自动创建并且传入,因为这些变量和属性都是渲染管线所使用的最最重要的数据,交给开发者的话肯定会有人算错或者传错,危险性太高了。内置变量和属性如下:

uniform mat4 modelMatrix;// M矩阵
uniform mat4 modelViewMatrix;// MV矩阵
uniform mat4 projectionMatrix;// P矩阵
uniform mat4 viewMatrix;// V矩阵
uniform mat3 normalMatrix;// 法线矩阵(注:渲染对象的顶点数据需要矩阵转换,对象的法线也需要矩阵转换)
uniform vec3 cameraPosition;// 相机在世界坐标系下的位置
attribute vec3 position;// 顶点数据
attribute vec3 normal;// 法线数据
attribute vec2 uv;// UV数据
// 注:这些变量属性是内置的,自动创建传入相关值,无需干预;但是还有一些变量属性是需要声明“宏”来开启,如:
#ifdef USE_COLOR
attribute vec3 color;
#endif

在编写着色器代码的时候,以上变量属性就可以直接使用,并不会因为在shader代码中没有声明而出错;经常编写shader代码的人肯定会发现,以上变量属性基本都是在“顶点着色器”中使用的,所以不要用错了位置;在“片元着色器”中也有内置变量属性如下:

uniform mat4 viewMatrix;// V矩阵
uniform vec3 cameraPosition;// 相机在世界坐标系下的位置

以下是一段自定义的顶点着色器代码,uv、projectionMatrix 、modelViewMatrix 和position都是没有声明直接使用的

"varying vec2 vUv;",

"void main() {",

"	vUv = uv;",
"	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",

"}"

2.仿照C语言的include写法

写过C语言代码的应该都知道include是干什么用的—“引用(头)文件”,在ThreeJS中自定义着色器代码的时候,也可以使用"#include “的写法,它会将引擎中内置的着色器代码(或者着色器代码片段,这个片段可能是一个光照计算函数或者其他的)引用进来,虽然是字符串层面的写法,但是我们可以把它理解为引用了一个“库”类似的。
在ThreeJS源码当中有两个有关着色器的文件:ShaderLib和ShaderChunk,其中ShaderLib中包含了引擎内置材质的着色器对象;而ShaderChunk中包含了着色器代码集合,其中从凡是”./ShaderChunk"中引入的都是可以在我们自定义着色代码时使用"#include "直接引用的,引入的库(姑且这么叫)内的变量、宏、方法或者代码片段等都可以在引入后使用。

var BokehShader = {
	// 自定义宏
	defines: {
		"DEPTH_PACKING": 1,
		"PERSPECTIVE_CAMERA": 1,
	},

	uniforms: {
		// 此处省略一些变量
	},

	vertexShader: [
		"varying vec2 vUv;",
		"void main() {",
		"	vUv = uv;",
		"	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
		"}"
	].join( "\n" ),

	fragmentShader: [
		"#include <common>",
		"#include <packing>",
		// 此处省略一些逻辑代码
		"void main() {",
		// 此处省略一些逻辑代码
		"	gl_FragColor = col / 41.0;",
		"	gl_FragColor.a = 1.0;",
		"}"
	].join( "\n" )
};

在上边这个完整的自定义着色器的代码框架中,"#include “和”#include "就引入了ShaderChunk中的common和packing代码段,这两个文件中有什么代码可以自行查看一下,代码太多在此不展示了就。需要说明一点:有些引入的代码是函数可直接调用,有些是功能比如光照等引入就可以了。

3.include引入“库”之后组织Uniform变量

我们知道了"#include "可以引入“库”来使用,但是某些库中有Uniform变量该如何组装呢?就像上面的自定义着色器代码框架中的uniforms,它的里面该怎么写变量呢?

UniformsLib.line = {
	linewidth: { value: 1 },
	resolution: { value: new Vector2( 1, 1 ) },
	dashScale: { value: 1 },
	dashSize: { value: 1 },
	gapSize: { value: 1 }
};
ShaderLib[ 'line' ] = {
	// Uniforms变量是通过UniformsLib来组装的
	uniforms: UniformsUtils.merge( [
		UniformsLib.common,// 库Uniforms
		UniformsLib.fog,// 库Uniforms
		UniformsLib.line// 自定义的Uniforms
	] ),

	vertexShader:
		`
		#include <common>
		#include <color_pars_vertex>
		#include <fog_pars_vertex>
		#include <logdepthbuf_pars_vertex>
		#include <clipping_planes_pars_vertex>

		uniform float linewidth;
		uniform vec2 resolution;

		void main() {
		// 此处省略逻辑代码
		}
		`,

	fragmentShader:
		`
		#ifdef USE_DASH
			uniform float dashSize;
			uniform float gapSize;
		#endif

		varying float vLineDistance;
		// 引入的库
		#include <common>
		#include <color_pars_fragment>
		#include <fog_pars_fragment>
		#include <logdepthbuf_pars_fragment>
		#include <clipping_planes_pars_fragment>

		void main() {
		// 此处省略逻辑代码
		}
		`
};

上面的着色器框架中,引入库的Uniform变量通过UniformsLib来引入,通过UniformsUtils.merge()来合并“库Uniforms变量”和“自定义的Uniforms变量”。那么“宏”该如何使用呢?想想C语言中宏是如何使用的,基本就能猜个差不多了吧?

注:如文章中有任何错误,请一定批评勘正。