最近比较空闲,准备趁着这个时候把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特效将在后续的博客中进行实现。