最近比较空闲,准备趁着这个时候把shader研究一下。前几年写过Unity的Shader,但两三年不用发现忘得都差不多了,这次从头开始查资料发现都比较零散,所以就在这里记录一下,方便日后回顾。 本文使用的环境是:CocosCreator 2.0.8 注意:CocosCreator 1.x和2.x的差别很大,本文仅适用于2.x的版本。
CocosCreator并没有提供Shader编辑的接口,据说2.1.x会完善材质系统(Shader就是依托于材质的),因为短时间内不会升级到2.1,所以也没有关注。 为了能够使用自定义的Shader,我们需要对引擎的功能做一些扩展,在cocos论坛中找到了大佬提供的源码,点击这里。 其中最重要的就是三个文件:CustomMaterial.js,ShaderHook.js,ShaderHelper.js 看懂这三个文件,就能随心所欲的添加自定义Shader啦~接下来的内容是对这三个文件的分析,并做了一定的修改以适应自己的习惯。
CocosCreator中所有渲染组件都有材质material,这也是渲染的基础,引擎提供了两个默认材质SpriteMaterial(正常模式)和GraySpriteMaterial(灰度模式),具体的可以看引擎源码。 为了支持自定义Shader,我们新增了一个材质ShaderMaterial,即上文的CustomMaterial,改名只是个人习惯。
/** * Shader材质 */ const renderEngine = cc.renderer.renderEngine; const renderer = renderEngine.renderer; const gfx = renderEngine.gfx; const Material = renderEngine.Material; let ShaderMaterial = (function (Material$$1) { function ShaderMaterial(name, params, defines) { Material$$1.call(this, false); var pass = new renderer.Pass(name); pass.setDepth(false, false); pass.setCullMode(gfx.CULL_NONE); // 设置混合模式 pass.setBlend( gfx.BLEND_FUNC_ADD, gfx.BLEND_SRC_ALPHA, gfx.BLEND_ONE_MINUS_SRC_ALPHA, gfx.BLEND_FUNC_ADD, gfx.BLEND_SRC_ALPHA, gfx.BLEND_ONE_MINUS_SRC_ALPHA ); // 默认参数 let techParams = [ { name: 'texture', type: renderer.PARAM_TEXTURE_2D }, { name: 'color', type: renderer.PARAM_COLOR4 } ]; // 额外参数(每个Shader自定义的参数) if (params) { techParams = techParams.concat(params); } var mainTech = new renderer.Technique( ['transparent'], // 固定transparent,貌似只有这种模式 techParams, // 配置的参数 [pass] ); this.name = name; this._color = { r: 1, g: 1, b: 1, a: 1 }; this._effect = new renderer.Effect( [mainTech], {}, // proteries,传入的uniform参数,即params,此处留空,将在后面设置 [defines] // Shader配置的defines ); this._mainTech = mainTech; this._texture = null; } // 继承Material cc.js.extend(ShaderMaterial, Material$$1); var prototypeAccessors = { effect: { configurable: true }, texture: { configurable: true }, color: { configurable: true } }; // 以下是对一些参数的get/set方法 prototypeAccessors.effect.get = function () { return this._effect; }; prototypeAccessors.texture.get = function () { return this._texture; }; prototypeAccessors.texture.set = function (val) { if (this._texture !== val) { this._texture = val; this._effect.setProperty('texture', val.getImpl()); this._texIds['texture'] = val.getId(); } }; prototypeAccessors.color.get = function () { return this._color; }; prototypeAccessors.color.set = function (val) { var color = this._color; color.r = val.r / 255; color.g = val.g / 255; color.b = val.b / 255; color.a = val.a / 255; this._effect.setProperty('color', color); }; // 拷贝函数 ShaderMaterial.prototype.clone = function clone() { var copy = new ShaderMaterial(); copy._mainTech.copy(this._mainTech); copy.texture = this.texture; copy.color = this.color; copy.updateHash(); return copy; }; // 获取自定义参数 ShaderMaterial.prototype.getParamValue = function (name) { return this._effect.getProperty(name); } // 设置自定义参数 ShaderMaterial.prototype.setParamValue = function (name, value) { return this._effect.setProperty(name, value); } // 设置定义值 ShaderMaterial.prototype.setDefine = function (name, value) { return this._effect.define(name); } Object.defineProperties(ShaderMaterial.prototype, prototypeAccessors); return ShaderMaterial; }(Material)); // 全局保存的Shader实例,避免重复创建 let g_shaders = {}; // 添加自定义shader ShaderMaterial.addShader = function (shader) { if (!shader) return; if (g_shaders[shader.name]) return; if (cc.renderer._forward) { let lib = cc.renderer._forward._programLib; if (!g_shaders[shader.name]) { // 首次创建 lib.define(shader.name, shader.vert, shader.frag, shader.defines || []); g_shaders[shader.name] = shader; } } else { // 避免还没有初始化导致报错的问题 cc.game.once(cc.game.EVENT_ENGINE_INITED, function () { let lib = cc.renderer._forward._programLib; if (!g_shaders[shader.name]) { // 首次创建 lib.define(shader.name, shader.vert, shader.frag, shader.defines || []); g_shaders[shader.name] = shader; } }) } } // 获取shader ShaderMaterial.getShader = function (name) { return g_shaders[name]; } module.exports = ShaderMaterial;上面创建了一个自定义的材质,那么如何使用这个材质呢?我们要渲染组件能支持自定义材质,所以通过下面的代码新增和覆盖了一些函数,此处以CCSprite为例进行修改,其他的渲染组件也是类似的。
/** * 为cc.Sprite增加材质接口 */ const renderEngine = cc.renderer.renderEngine; const SpriteMaterial = renderEngine.SpriteMaterial; const GraySpriteMaterial = renderEngine.GraySpriteMaterial; const STATE_CUSTOM = 101; // 自定义材质的state // 获取自定义材质 cc.Sprite.prototype.getCustomMaterial = function(name) { return this._materials ? this._materials[name] : undefined; } // 设置自定义材质 cc.Sprite.prototype.setCustomMaterial = function(name, mat) { if (!this._materials) { this._materials = {} } this._materials[name] = mat; } // 激活某个自定义材质 cc.Sprite.prototype.activateMaterial = function(name) { var mat = this.getCustomMaterial(name); if (mat && mat !== this._currMaterial) { if (mat) { if (this.node) { mat.color = this.node.color; } if (this.spriteFrame) { mat.texture = this.spriteFrame.getTexture(); } this._currMaterial = mat; // 切换当前材质 this._currMaterial.name = name; this._state = STATE_CUSTOM; // 切换模式为自定义材质 this._activateMaterial(); // 刷新渲染对象 } else { console.error("activateMaterial - unknwon material: ", name); } } } // 重置材质,切换为普通材质 cc.Sprite.prototype.resetCustomMaterial = function(){ this._state = 0; this._activateMaterial(); } // 获取当前的自定义材质 cc.Sprite.prototype.getCurrMaterial = function() { if (this._state === STATE_CUSTOM) { return this._currMaterial; } } // override // 刷新对象 cc.Sprite.prototype._activateMaterial = function() { let spriteFrame = this._spriteFrame; // WebGL if (cc.game.renderType !== cc.game.RENDER_TYPE_CANVAS) { // Get material let material; if (this._state === cc.Sprite.State.GRAY) { // 默认的灰度模式下 if (!this._graySpriteMaterial) { this._graySpriteMaterial = new GraySpriteMaterial(); } material = this._graySpriteMaterial; // For batch rendering, do not use uniform color. material.useColor = false; // 清除自定义材质 this._currMaterial = null; } else if (this._state === STATE_CUSTOM && this._currMaterial) { // 自定义模式下,只有加载了自定义材质才实际起效,否则使用普通模式渲染 material = this._currMaterial; } else { // 普通模式 if (!this._spriteMaterial) { this._spriteMaterial = new SpriteMaterial(); } material = this._spriteMaterial; // For batch rendering, do not use uniform color. material.useColor = false; // 清除自定义材质 this._currMaterial = null; } // Set texture if (spriteFrame && spriteFrame.textureLoaded()) { let texture = spriteFrame.getTexture(); if (material.texture !== texture) { material.texture = texture; this._updateMaterial(material); } else if (material !== this._material) { this._updateMaterial(material); } if (this._renderData) { this._renderData.material = material; } this.node._renderFlag |= cc.RenderFlow.FLAG_COLOR; this.markForUpdateRenderData(true); this.markForRender(true); } else { this.disableRender(); } } else { this.markForUpdateRenderData(true); this.markForRender(true); } }主要需要注意的就是重写的引擎函数_activateMaterial(),不同版本的CocosCreator引擎的这个函数可能不同,需要相应的修改以适应当前引擎,此处是根据2.0.8。 为什么我们能通过这种方式修改引擎的代码呢?这就是应用了javascript的prototype机制了,具体原理在这里就不细说了。
经过上面两步,我们已经完成了大部分的准备工作,接下来其实已经可以直接编写自己的shader程序了,但是为了简化编写shader中的重复性工作,我们提炼了一个ShaderBase基类(参考了大佬提供的ShaderHelper脚本),代码如下:
/** * 自定义shader组件的基类 */ const { ccclass, executeInEditMode, requireComponent, disallowMultiple } = cc._decorator; import ShaderMaterial = require('./ShaderMaterial'); @ccclass // @executeInEditMode // 在编辑器中运行 @requireComponent(cc.Sprite) // 依赖sprite组件 @disallowMultiple // 不允许重复添加 export default class ShaderBase extends cc.Component { protected shaderName = "BaseShader"; // shader名,不可重复 protected defines = []; // 一些定义 protected params = null; // 自定义参数 private sprite: cc.Sprite = null; private _shaderObj = null; // shader实例对象 private _material = null; // 当前材质 // 顶点着色器程序(通常不变) protected vert = ` uniform mat4 viewProj; attribute vec3 a_position; attribute vec2 a_uv0; varying vec2 uv0; void main(){ vec4 pos = viewProj * vec4(a_position, 1); gl_Position = pos; uv0 = a_uv0; } `; // 片元着色器程序 protected frag = null; onLoad() { // 获取sprite对象 this.sprite = this.node.getComponent(cc.Sprite); // 设置shader程序 this.setShaderProgram(); } onEnable(){ // 加载shader程序 this.applyShader(); } onDisable(){ // 重置为默认材质 this.sprite.resetCustomMaterial(); } update(dt){ // 每帧刷新shader this.updateShader(this._material, dt); } // 加载shader applyShader() { if (!this.vert || !this.frag) { console.warn(`${this.shaderName} in file(${this.name}) shader not defined`) return; } let shader = { name: this.shaderName, vert: this.vert, frag: this.frag, defines: this.defines, } cc.dynamicAtlasManager.enabled = false; // 获取内存中的shader对象,如果没有就进行创建 let shaderObj = ShaderMaterial.getShader(this.shaderName); if(!shaderObj){ ShaderMaterial.addShader(shader); shaderObj = ShaderMaterial.getShader(this.shaderName); } this._shaderObj = shaderObj; // 获取当前shader相应的材质,如果没有就进行创建 let material = this.sprite.getCustomMaterial(this.shaderName); if(!material){ material = new ShaderMaterial(this.shaderName,this.params, this.defines); this.sprite.setCustomMaterial(this.shaderName, material); } this._material = material; // 切换材质 this.sprite.activateMaterial(this._shaderObj.name); // 设置shader的自定义参数 if(this.params){ this.params.forEach(item => { if (item.defaultValue !== undefined) { material.setParamValue(item.name, item.defaultValue); } }); } // 初始化shader this.initShader(material) } // 在此处填写vert和frag程序 setShaderProgram(){ } // 设置初始变量 initShader(material){ } // 每帧刷新 updateShader(material, dt){ } }至此,我们已经完成了所有的准备工作,接下来让我们创建一个最基本的shader,做为样例。
import ShaderBase from "../ShaderBase"; const {ccclass, property} = cc._decorator; @ccclass export default class DefaultShader extends ShaderBase { @property color:cc.Color = cc.Color.WHITE; @property thresholdAlpha = 0; shaderName = "DefaultShader"; setShaderProgram() { this.frag = ` uniform sampler2D texture; uniform vec4 color; varying vec2 uv0; void main(){ vec4 c = color * texture2D(texture, uv0); gl_FragColor = c; } `; } }上面这个shader非常简单,只是显示一张纹理,不做其他任何操作。Shader特效将在后续的博客中进行实现。