【CocosCreator】Shader学习之准备篇

目录

  • 写在前面
  • 准备开始
    • 自定义材质
    • 添加材质
    • 创建一个shader基类
  • 自定义shader

写在前面

最近比较空闲,准备趁着这个时候把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程序了,但是为了简化编写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

至此,我们已经完成了所有的准备工作,接下来让我们创建一个最基本的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特效将在后续的博客中进行实现。

你可能感兴趣的:(CocosCreator)