组织架构
shader就是一段着色器代码,跑在GPU上,一个显示节点需要一段shader代码才可以被绘制出来,那么显示节点和shader之间有怎么样的管理关系呢
一个显示节点会配一一个材质球(material),一个材质球会管理若干个techniques,每一个technique都有若干个pass,每一个pass里有两个着色器代码,分别是vs和ts,每一个pass才是真正要使用的着色器代码,所以至于要渲染那个pass,我们可以自行决定
在对于一个pass而言,它是通过宏来磨平各种功能的,比如说是否使用光照,是否使用纹理,是否使用透明度测试等
中间过程
看一张图片,如下:
上面这张图片介绍了一个普通精灵sprite对于shader的引用
4: 材质文件,它就是一个材质球,
主要就是记录了使用那个shader,以及使用这个shader需要用到的宏
比如这个shader是否使用纹理(USE_TEXTURE)
5:是具体的shader文件,这里用的是精灵的shader,内容如下
// Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
CCEffect %{
techniques:
- passes:
- vert: vs
frag: fs
blendState:
targets:
- blend: true
rasterizerState:
cullMode: none
properties:
texture: { value: white }
alphaThreshold: { value: 0.5 }
}%
CCProgram vs %{
precision highp float;
#include <cc-global>
#include <cc-local>
in vec3 a_position;
in vec4 a_color;
out vec4 v_color;
#if USE_TEXTURE
in vec2 a_uv0;
out vec2 v_uv0;
#endif
void main () {
vec4 pos = vec4(a_position, 1);
#if CC_USE_MODEL
pos = cc_matViewProj * cc_matWorld * pos;
#else
pos = cc_matViewProj * pos;
#endif
#if USE_TEXTURE
v_uv0 = a_uv0;
#endif
v_color = a_color;
gl_Position = pos;
}
}%
CCProgram fs %{
precision highp float;
#include <alpha-test>
#include <texture>
in vec4 v_color;
#if USE_TEXTURE
in vec2 v_uv0;
uniform sampler2D texture;
#endif
void main () {
vec4 o = vec4(1, 1, 1, 1);
#if USE_TEXTURE
CCTexture(texture, v_uv0, o);
#endif
o *= v_color;
ALPHA_TEST(o);
gl_FragColor = o;
}
}%
上面有一个大致的了解,下面我们将详细的来讲解这一过程:
Builtin:内置的意思
ccSprite 继承RenderComponent
当我们在界面上给精灵拖拽一个材质球的时候,就会触发下面的_activateMaterial()函数
properties: {
_materials: {
default: [],
type: Material,
},
/**
* !#en The materials used by this render component.
* !#zh 渲染组件使用的材质。
* @property {[Material]} sharedMaterials
*/
materials: {
get () {
return this._materials;
},
set (val) {
this._materials = val;
this._activateMaterial();
},
type: [Material],
displayName: 'Materials',
animatable: false
}
下面是activateMaterial的函数实现
1:如果指定了材质球资源,就直接拿着这个材质球资源来创建一个材质球实例
2:如果没有指定材质球资源,就会找一个默认的材质球资源,进而完成材质球实例的创建
/**
* Init material.
*/
_activateMaterial () {
let materials = this._materials;
if (!materials[0]) {
let material = this._getDefaultMaterial();
materials[0] = material;
}
for (let i = 0; i < materials.length; i++) {
materials[i] = MaterialVariant.create(materials[i], this);
}
this._updateMaterial();
},
sprite 默认的材质球资源,看完下面的代码其实就是去找builtin-2d-sprite这个内置的材质球文件
注意,我们看到这个资源是通过资源管理员去找的,所以我们可以确定这些内置资源,在游戏启动的时候就应该已经被加载到内存中了
//CCRenderComponent.js
_getDefaultMaterial () {
return Material.getBuiltinMaterial('2d-sprite');
}
//CCMaterial.js 下面这是一个材质球的一个静态函数
/**
* !#en Get built-in materials
* !#zh 获取内置材质
* @static
* @method getBuiltinMaterial
* @param {string} name
* @return {Material}
*/
getBuiltinMaterial (name) {
if (cc.game.renderType === cc.game.RENDER_TYPE_CANVAS) {
return new cc.Material();
}
return cc.assetManager.builtins.getBuiltin('material', 'builtin-' + name);
}
使用材质球资源来创建一个材质球实例
//material-variant.js
/**
* @method create
* @param {Material} material
* @param {RenderComponent} [owner]
* @typescript
* static create (material: Material, owner: cc.RenderComponent): MaterialVariant | null
*/
static create (material: Material, owner: cc.RenderComponent): MaterialVariant | null {
if (!material) return null;
return MaterialPool.get(material, owner);
}
//material-pool.js
get (exampleMat, renderComponent) {
let pool = this._pool;
if (exampleMat instanceof cc.MaterialVariant) {
if (exampleMat._owner) {
if (exampleMat._owner === renderComponent) {
return exampleMat;
}
else {
exampleMat = exampleMat.material;
}
}
else {
exampleMat._owner = renderComponent;
return exampleMat;
}
}
let instance;
if (this.enabled) {
let uuid = exampleMat.effectAsset._uuid;
if (pool[uuid]) {
let key =
utils.serializeDefines(exampleMat._effect._defines) +
utils.serializeTechniques(exampleMat._effect._techniques);
instance = pool[uuid][key] && pool[uuid][key].pop();
}
}
if (!instance) {
instance = new cc.MaterialVariant(exampleMat);
instance._name = exampleMat._name + ' (Instance)';
instance._uuid = exampleMat._uuid;
}
else {
this.count--;
}
instance._owner = renderComponent;
return instance;
}
effect
下图显示的是一份creator内置的材质球和shader资源,在游戏开始的时候,会自动加载
看下面这两段代码,分别是获取shader资源和材质球资源,
let effectAsset = cc.assetManager.builtins.getBuiltin('effect', val);
cc.assetManager.builtins.getBuiltin('material', 'builtin-' + name)
那creator加载器又是如何加载内置的shader资源和材质球的资源呢,这里我们举一个sprite默认的资源作为一个例子,builtin-2d-sprite.mtl
{
"__type__": "cc.Material",
"_name": "builtin-2d-sprite",
"_objFlags": 0,
"_native": "",
"_effectAsset": {
"__uuid__": "2874f8dd-416c-4440-81b7-555975426e93"
},
"_techniqueData": {
"0": {
"defines": {
"USE_TEXTURE": true
}
}
}
}
creator加载器将上面这个文件加载到内存以后,会根据__type__来选择资源实例化类来进行解析,注意这里说的是资源实例化类,其实说白了就是找对应的解析类来解析数据,这里的type的类型是cc.Material,OK,那么就找CCMaterial来解析,_effectAsset这个指明了使用个effect文件,_techniqueData这个就是一个passData数组,里面管理这若干份pass,有个defines的下表,这个就是传说中的宏,我们的shader代码千变万化,最终都是靠宏来进行统一的,因为宏会在在最终编译的时候确定shader,所以我们不需要为每一个节点去写不同的shader文件,比如是否使用纹理,是否使用透明度测试,是否使用光照等,下面就是解析
onLoad () {
this.effectAsset = this._effectAsset;
if (!this._effect) return;
if (this._techniqueIndex) {
this._effect.switchTechnique(this._techniqueIndex);
}
this._techniqueData = this._techniqueData || {};
let passDatas = this._techniqueData;
for (let index in passDatas) {
index = parseInt(index);
let passData = passDatas[index];
if (!passData) continue;
for (let def in passData.defines) {
this.define(def, passData.defines[def], index);
}
for (let prop in passData.props) {
this.setProperty(prop, passData.props[prop], index);
}
}
}
pass.js:这个类代表一次drawcall信息,这里面存放了当前要绘制的shader,以及一些GPU状态
this._name = name; //当前pass的名字
//${effectAsset.name}-${techName}-${passName}
this._detailName = detailName;//当前pass的详细名字
this._programName = programName;//shader的名字
this._stage = stage;//绘制的阶段 不透明 透明
this._properties = properties;
this._defines = defines;//使用了那些宏
technique.js:这个主要用来管理pass的,你可以这么理解,pass呢主要用来具体的绘制某一个效果,但是一个显示节点可以被用来绘制很多个不同情况下的效果,所以就使用了这个technique类来管理一下pass,在众多效果中,可以抽象的分为几大类,所以显示节点对于这个类的管理又是一个数组,也就是说一个显示节点可以有若干个technique,每个technique又可以有若干个pass
CCMaterial.js:这个类主要用来管理材质球
CCEffectAsset.js:这个类主要用来生成着色器的资源
–>onLoad ():注意看这个函数的实现,在资源加载的时候,立马就去shader管理中心(programLib)交代了一下,接着就初始化shader
onLoad () {
if (cc.game.renderType === cc.game.RENDER_TYPE_CANVAS) {
return;
}
let lib = cc.renderer._forward._programLib;
for (let i = 0; i < this.shaders.length; i++) {
lib.define(this.shaders[i]);
}
this._initEffect();
}
Program.js:这个类就是用来管理一份shader着色器的,一个着色器包含两个着色器代码,vs和fs,在这两份代码中,包含attribute变量,uniform变量,当前着色器是否编译,是否连接,编译成功以后会生成一个显存的地址_glID来存放当前的shader程序,以及必要的时候destroy这个着色器
program-lib.js:这个类主要就是负责生成和管理若干份program,
–>_templates:这是一个模板数组,里面存放了若干份着色器生成数据,我们的着色器的生成需要依靠一些数据,而下面就罗列了这些数据,也是这个模板数组的item
{
id, //着色器的唯一id 每生成一个着色器 id自动+1
name,//着色器的名字
vert,//着色器的顶点代码
frag,//着色器的片元代码
defines,//着色器的定义 这个非常重要 比如使用光照信息等
attributes: prog.attributes,//顶点属性
uniforms, //uniform属性
extensions: prog.extensions //
};
–>define(prog):理论上所有的shader代码都要调用这个函数,因为要统一管理所有的shader代码,这个函数会将shader代码存放到模板数组中
creator是如何给shader中传宏定义的值?
这个过程就是先将shader代码加载到内存,他其实就是一个字符串,然后根据宏定义的值,给字符串对应的宏赋值或者添加宏的定义,最后再将这个shader代码发送给GPU渲染
关于宏定义的值,这个是我们控制的,比如要不要alpha测试等
宏 USE_ALPHA_TEST:在下面的shader代码中使用了这个宏
#if USE_ALPHA_TEST
uniform float alphaThreshold;
#endif
void ALPHA_TEST (in vec4 color) {
#if USE_ALPHA_TEST
if (color.a < alphaThreshold) discard;
#endif
}
void ALPHA_TEST (in float alpha) {
#if USE_ALPHA_TEST
if (alpha < alphaThreshold) discard;
#endif
}
ccMask.js
material.define('USE_ALPHA_TEST', true);
CCMaterial.js
/**
* !#en Sets the Material define.
* !#zh 设置材质的宏定义。
* @method define
* @param {string} name 宏的名字
* @param {boolean|number} val 宏的值
* @param {number} [passIdx] pass的索引
* @param {boolean} [force]
*/
define (name, val, passIdx, force) {
if (cc.game.renderType === cc.game.RENDER_TYPE_CANVAS) return;
if (typeof passIdx === 'string') {
passIdx = parseInt(passIdx);
}
this._effect.define(name, val, passIdx, force);
},
effect-base.ts
define (name, value, passIdx, force) {
let success = false;
let passes = this.passes;
let start = 0, end = passes.length;
if (passIdx !== undefined) {
start = passIdx, end = passIdx + 1;
}
for (let i = start; i < end; i++) {
if (passes[i].define(name, value, force)) {
success = true;
}
}
if (!success) {
cc.warnID(9104, this.name, name);
}
}
pass.js
define (name, value, force) {
if (!force) {
let def = this._defines[name];
if (def === undefined) {
return false;
}
}
this._defines[name] = value;
return true;
}
program-lib.js
/*
tmpDefines:shader代码中关于宏的定义
defines:外界关于这些宏的使用所定义的值
*/
function _generateDefines(tmpDefines, defines) {
let results = [];
for (let i = 0; i < tmpDefines.length; i++) {
let name = tmpDefines[i].name;
let value = defines[name];
if (typeof value !== 'number') {
value = value ? 1 : 0;
}
results.push(`#define ${name} ${value}`);
}
return results.join('\n') + '\n';
}
/*
这个函数的作用就是如果发现宏定义的值是一个整数那么就直接替换为整数
例如 #define CZJ 1
#if CZJ
xxxx
#endif
替换为
#if 1
xxxx
#endif
string:顶点着色器或者片元着色器的代码
tmpDefines:着色器代码中的一些宏的定义
defines:外界关于宏的使用情况的一些值定义
*/
function _replaceMacroNums(string, tmpDefines, defines) {
let tmp = string;
for (let i = 0; i < tmpDefines.length; i++) {
let name = tmpDefines[i].name;
let value = defines[name];
if (Number.isInteger(value)) {
let reg = new RegExp(name, 'g');
tmp = tmp.replace(reg, value);
}
}
return tmp;
}
getProgram(name, defines, errPrefix) {
let key = this.getKey(name, defines);
let program = this._cache[key];
if (program) {
return program;
}
// get template
let tmpl = this._templates[name];
let customDef = _generateDefines(tmpl.defines, defines);
let vert = _replaceMacroNums(tmpl.vert, tmpl.defines, defines);
vert = customDef + _unrollLoops(vert);
if (!this._highpSupported) {
vert = _replaceHighp(vert);
}
let frag = _replaceMacroNums(tmpl.frag, tmpl.defines, defines);
frag = customDef + _unrollLoops(frag);
if (!this._highpSupported) {
frag = _replaceHighp(frag);
}
program = new gfx.Program(this._device, {
vert,
frag
});
let errors = program.link();
if (errors) {
let vertLines = vert.split('\n');
let fragLines = frag.split('\n');
let defineLength = tmpl.defines.length;
errors.forEach(err => {
let line = err.line - 1;
let originLine = err.line - defineLength;
let lines = err.type === 'vs' ? vertLines : fragLines;
// let source = ` ${lines[line-1]}\n>${lines[line]}\n ${lines[line+1]}`;
let source = lines[line];
let info = err.info || `Failed to compile ${err.type} ${err.fileID} (ln ${originLine}): \n ${err.message}: \n ${source}`;
cc.error(`${errPrefix} : ${info}`);
})
}
this._cache[key] = program;
return program;
}