从零开始手撸WebGL3D引擎5:Shader的封装

1,311 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

上一篇之后停了好久没写,但其实mini3d.js一直在进展,目前的内容够写很多篇了,但这样欠下的债就太多了。我开始思考我写这些文章的初衷,是对这个过程的记录以及思考的总结,而不是写WebGL教程。教程的问题是如果讲得很细非常花篇章,而讲得很简略又没有用处。所以本篇开始减少代码细节的描述,只重点讨论技术原理和设计思路。并且每一组文章都要紧扣着当前讨论的里程碑目标。 本篇对应代码请参考:mini3d.js

回顾

上一篇中,我们讨论了mini3d.js对静态模型的封装,提供了自定义顶点格式的支持,以及顶点属性和shader中attribute的绑定。模型为我们当前的目标-渲染一个立方体提供了数据,但是具体怎么渲染它需要shader的支持。那么什么是shader,shader具体做了啥,mini3d.js目前对shader提供了哪些封装和接口,是本篇要讨论的问题。

材质和Shader

WebGL的shader狭义上说是指一组运行在GPU上的小程序,vertex shaderfragment shader。其中VS的作用是输出裁剪空间的顶点和其他中间变量,FS的作用是计算片段的颜色。 但是光靠VS和FS往往还不够,WebGL还可以设置各种渲染状态,如混合的参数,并且某些渲染方式可能需要执行多次不同的渲染,称为多个PASS,并且某些引擎会提供VS/FS无法执行时的替代解决方案(fallback)。所有的这些综合起来可以在一个文件里面定义(或者通过代码组织),一些引擎叫做材质(Material),而另一些引擎可能就叫shader(例如unity)。材质只是一种渲染方案的模板,可能有多个物体使用同一个材质渲染,但是有不同的参数值(如diffuse贴图),这些具有不同参数值的材质在某些引擎里面叫做材质实例(Material Instance)。而unity中的材质其实是unity shader的实例,相当于前述引擎的Material Instance。虽然叫法不同,但是思路都很类似。mini3d.js也会实现材质系统,但是目前先把shader封装一下,因为shader本身也有一些细节。

WebGL shader的输入输出

WebGL的shader是GLSL ES语言的小程序,和OpenGL ES 2.x/3.x基本是通用的。我们看一下OpenGL ES 3.0的VS和FS的输入输出: VS FS 其中VS输入Attribute和Uniform (先忽略Sampler),输出Varying和gl_Position以及gl_PointSize FS输入Varying, Uniform和Sampler,输出gl_FragColor (暂时没找到OpenGL ES 2.0的图,3.0中FS输出了一组Color先不用管它) 对于VS,输入的Attribute就来自我们提交的顶点数据,这个数据的准备工作已经在上一篇中做好了,并且已经和shader进行了绑定。这样执行VS时就可以对于模型的每个顶点执行一次VS,并且传入每个顶点的各个属性。但是为了执行这个绑定,shader必须提供Attribute的Location,所以这方面需要封装一下。 而Uniform输入需要调用相应的API,这也是需要封装的一个点,因为Uniform数据类型很多直接使用API调用不方便。至于Sampler其实也是uniform的一种。

Shader的封装

mini3d.js目前提供了Shader类,这个Shader其实对应了WebGL的一个shader program。目前提供了几个操作。

从shader源码创建shader program

create(vshader, fshader)方法输入参数为vs和fs的源码,目前的demo里面源码是直接写在js代码里面的字符串,下一个里程碑将从资源文件载入。create内部创建了一组shader对象并编译链接为一个program对象。这个是最基本的封装,避免了每次创建shader的繁琐操作。

获取Attribute的location

WebGL中使用gl.getAttribLocation(program, name)可以通过名字获取location。但是我们不是直接这么做,mini3d.js首先使用gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES)gl.getActiveAttrib(program,index)获取所有的active的attributes的信息,这个信息是一个WebGLActiveInfo对象,包含name,size,type。对于attribute我们只使用name,然后通过name获取到location并存储在this._attributes = {}; // {[name:string]:number}中。这样做的好处是防止代码中写错name并且确保attribute是激活的。但由于我们需要将Mesh中的顶点属性和shader的attribute进行绑定,还是需要在代码中输入name,但是毕竟这儿做了个检查。另外这么做相当于空间换时间,将所有的attribute location缓存起来避免重复查询。以上操作封装在findoutAttributes方法中并在create中自动调用。使用者只需要使用getAttributeLocation(semantic)接口通过semantic获取location。注意参数为semantic,这个就是在Mesh中定义的顶点属性语义,通过这个semantic将顶点属性和shader的attribute联系起来。

关联semantic和Attribute name

上面说了我们使用semantic获取attribute location,因此我们需要知道semantic对应的attribute name。Shader类提供了接口mapAttributeSemantic(semantic, attribName)进行设置。这么做还是需要在代码中写shader中的名字,但是这给以后的升级提供了基础,比如像Unity那样使用CG就可以直接将semantic标记在shader的变量后面,这样就可以自动化了。当然mini3d.js大概率不会使用CG,毕竟那需要工具链的支持,但我有可能在材质定义中进行标记,这样代码中就不用出现name了,而标记和shader都包裹在材质文件中,避免信息分散维护。

设置Uniform

WebGL关于设置uniform的API有gl.uniform[1234][fi][v]()文档.uniformMatrix[234]fv()文档 太多了吧,调用起来太麻烦,作为一个框架必须得管理这个复杂度。好在我们是javascript,没有问题,我们封装了一个方法Shader类的setUniform(name, value)。不管是什么类型的uniform,都用这个方法设置。例如:

//设置矩阵
shader.setUniform('u_mvpMatrix', mvpMatrix.elements);
//设置sampler
shader.setUniform('u_sampler', 0);
//设置vec3
shader.setUniform('u_LightColor', [1.0,1.0,1.0]);

后面两个暂时超纲了,里程碑1还没有。 这个setUniform做了所有的苦活累活,基本原理就是先获取shader中所有active的uniform的信息(在findoutUniforms中获取)。这个信息是一个如下的对象:

class UniformInfo{    
	constructor(name, location, type, size, isArray){        
		this.name = name;        
		this.location = location; //WebGLUniformLocation        				
		this.type = type;        
		this.size = size;           
		this.isArray = isArray;         
	}
}

然后在setUniform方法中会根据name所对应的info的类型调用不同的API进行设置。目前阶段还没支持所有的类型组合,先支持了一些常用的。

小结和展望

目前这个阶段对shader进行了简单的封装,降低调用的复杂度并且可以更方便的和模型进行绑定。mini3d.js没有像某些引擎那样写死所有常用的顶点属性然后定死了attribute的名字,而是通过semantic进行关联,这主要是为了扩展性,必须对于一个基于shader的渲染框架来说,让用户可以自由的写shader非常重要。虽然想达到Unity那样的成熟度很困难,但我们可以一步一步来。未来肯定会写材质系统,甚至shader graph这样方便技美创作的工具,但是到写工具的那一步就真的向一个引擎发展了,目前来说先实现基本目标吧,毕竟太多的事情想做。比如说提供一些常用的uniform,如各种矩阵,按照某个约定的名字在渲染前自动传入到shader中,再如提供一些公共方法方便写shader时调用,甚至像对于阴影的支持这种,都需要引擎提供可在shader中获取的数据。