CocosCreator2.1.0版本正式支持導入3D模型
對於2.5D遊戲的開發來說有着重要意義
自己此前在寫捕魚遊戲時瞭解過自定義shader
並實現了4種不同的水波效果
但經過CocosCreator版本的不斷升級
尤其是1.10和2.0兩個版本
舊的渲染器被拋棄了
因此老的shader特效也全都不能用了
直到最近正好有時間,花了幾天把原先寫的特效升級到了最新的2.1.0版本
下面記錄一下自定義shader實現方法的改變
以及新的渲染器的理解
過往自定義shader的實現依賴
cc.gl
cc.GLProgram
cc.GLProgramState
CCSprite._sgNode
CCTexture2D.setTexParameters
這些統統都不能用了!
取而代之的是新的渲染結構
顯然又是多層封裝咯
最上層的material關聯到sprite組件
最底層的pass關聯到具體的vert和frag着色器代碼,也就是Shader層
其實Shader層才應該是最底層的
從底向上一層層來看
Shader
系統默認的shader是通過以下方法保存在lib對象中的
ProgramLib.prototype.define = function define (name, vert, frag, defines) {
而lib對象所在的位置比較奇怪,可能往後的版本會變更
cc.renderer._forward._programLib
着色器代碼也需要稍作修改
以往的CC_Texture0等固定變量都不存在了
Pass
Pass的構造函數傳入的name就是與着色器同名的name
所以Pass是直接關聯shader的
另外Pass還包含了混合參數、深度測試參數、模板測試參數等等
Base.prototype._draw = function _draw (item) { var this$1 = this; var device = this._device; var programLib = this._programLib; var node = item.node; var ia = item.ia; var effect = item.effect; var technique = item.technique; var defines = item.defines; // reset the pool // NOTE: we can use drawCounter optimize this // TODO: should be configurable _float2_pool.reset(); _float3_pool.reset(); _float4_pool.reset(); _float9_pool.reset(); _float16_pool.reset(); _float64_pool.reset(); _int2_pool.reset(); _int3_pool.reset(); _int4_pool.reset(); _int64_pool.reset(); // set common uniforms // TODO: try commit this depends on effect // { node.getWorldMatrix(_m4_tmp$2); device.setUniform('model', mat4.array(_float16_pool.add(), _m4_tmp$2)); var inverse = mat3.invert(_m3_tmp$1, mat3.fromMat4(_m3_tmp$1, _m4_tmp$2)); if (inverse) { mat3.transpose(_m3_tmp$1, inverse); device.setUniform('normalMatrix', mat3.array(_float9_pool.add(), _m3_tmp$1)); } // } // set technique uniforms for (var i = 0; i < technique._parameters.length; ++i) { // 這裏遍歷technique._parameters // 再從effect找到參數的值 // 因此參數必須在technique中聲明並同時在effect中定義 // 若不在technique中聲明,則不會遍歷不會走到device.setUniform這一步 var prop = technique._parameters[i]; var param = effect.getProperty(prop.name); // 若未在effect中賦值,則從technique中找默認 if (param === undefined) { param = prop.val; } // 默認也找不到,就給個該類型的default值 if (param === undefined) { param = this$1._type2defaultValue[prop.type]; } if (param === undefined) { console.warn(("Failed to set technique property " + (prop.name) + ", value not found.")); continue; } if ( prop.type === enums.PARAM_TEXTURE_2D || prop.type === enums.PARAM_TEXTURE_CUBE ) { if (prop.size !== undefined) { if (prop.size !== param.length) { console.error(("The length of texture array (" + (param.length) + ") is not corrent(expect " + (prop.size) + ").")); continue; } var slots = _int64_pool.add(); for (var index = 0; index < param.length; ++index) { slots[index] = this$1._allocTextuerUnit(); } device.setTextureArray(prop.name, param, slots); } else { device.setTexture(prop.name, param, this$1._allocTextuerUnit()); } } else { var convertedValue = (void 0); if (param instanceof Float32Array || param instanceof Int32Array) { convertedValue = param; } else if (prop.size !== undefined) { var convertArray = _type2uniformArrayValue[prop.type]; if (convertArray.func === undefined) { console.error('Uniform array of color3/int3/float3/mat3 can not be supportted!'); continue; } if (prop.size * convertArray.size > 64) { console.error('Uniform array is too long!'); continue; } convertedValue = convertArray.func(param); } else { var convertFn = _type2uniformValue[prop.type]; convertedValue = convertFn(param); } device.setUniform(prop.name, convertedValue); } } // for each pass for (var i$1 = 0; i$1 < technique._passes.length; ++i$1) { var pass = technique._passes[i$1]; var count = ia.getPrimitiveCount(); // set vertex buffer device.setVertexBuffer(0, ia._vertexBuffer); // set index buffer if (ia._indexBuffer) { device.setIndexBuffer(ia._indexBuffer); } // set primitive type device.setPrimitiveType(ia._primitiveType); // set program (通過pass裏保存的program名字找到着色器program!) var program = programLib.getProgram(pass._programName, defines); device.setProgram(program); // cull mode device.setCullMode(pass._cullMode); // blend if (pass._blend) { device.enableBlend(); device.setBlendFuncSep( pass._blendSrc, pass._blendDst, pass._blendSrcAlpha, pass._blendDstAlpha ); device.setBlendEqSep( pass._blendEq, pass._blendAlphaEq ); device.setBlendColor32(pass._blendColor); } // depth test & write if (pass._depthTest) { device.enableDepthTest(); device.setDepthFunc(pass._depthFunc); } if (pass._depthWrite) { device.enableDepthWrite(); } // stencil if (pass._stencilTest) { device.enableStencilTest(); // front device.setStencilFuncFront( pass._stencilFuncFront, pass._stencilRefFront, pass._stencilMaskFront ); device.setStencilOpFront( pass._stencilFailOpFront, pass._stencilZFailOpFront, pass._stencilZPassOpFront, pass._stencilWriteMaskFront ); // back device.setStencilFuncBack( pass._stencilFuncBack, pass._stencilRefBack, pass._stencilMaskBack ); device.setStencilOpBack( pass._stencilFailOpBack, pass._stencilZFailOpBack, pass._stencilZPassOpBack, pass._stencilWriteMaskBack ); } // draw pass device.draw(ia._start, count); this$1._resetTextuerUnit(); } };
Technique
Technique的構造函數如下
var Technique = function Technique(stages, parameters, passes, layer) {
stages不太瞭解
parameters聲明瞭注入shader代碼中的參數名和類型
未聲明的參數即使寫在shader裏面也是無法使用的
passes可以指定多個,是否意味着多次渲染
以下是默認的SpriteMaterial中的Technique
var mainTech = new renderer.Technique( ['transparent'], [ { name: 'texture', type: renderer.PARAM_TEXTURE_2D }, { name: 'color', type: renderer.PARAM_COLOR4 } ], [ pass ] );
可以看到只設置了兩個參數
因此在着色器中可以使用texture紋理採樣
同時使用節點顏色color
Effect
Effect的構造函數如下
var Effect = function Effect(techniques, properties, defines) {
以下是默認的SpriteMaterial中的Effect
this._effect = new renderer.Effect( [ mainTech ], { 'color': this._color }, [ { name: 'useTexture', value: true }, { name: 'useModel', value: false }, { name: 'alphaTest', value: false }, { name: 'useColor', value: true } ] );
在自定義材質中properties直接傳空對象{}即可
如果是不變的uniform參數可以在technique中賦值默認val
如果是變化的uniform參數,如time、衰減因子、點擊位置等等
通過以下方法來更新變量的值即可
Effect.prototype.setProperty = function setProperty (name, value) {
Material
自定義材質可以繼承自默認材質
也可以類比SpriteMaterial來寫
但我覺得那樣太麻煩了,直接繼承Material把幾個有用的參數填進去就行了
而材質與sprite的綁定也簡化爲兩行代碼
原本CCSprite._activateMaterial統統省去
當紋理和頂點信息不改變的情況下
我認爲以下兩句是可以省略的
this.markForUpdateRenderData(true);
this.markForRender(true);
並且在h5、微信、安卓原生平臺均驗證有效
class CustomMaterial extends cc.renderer.renderEngine.Material{ constructor(name , vert , frag , uniforms = [] , defines = []){ super(false); this.name = name let lib = cc.renderer._forward._programLib; !lib._templates[name] && lib.define(name, vert, frag, defines); this.init(name , uniforms); } use(sprite){ // cc.dynamicAtlasManager.enabled = false; // 設置基本紋理和顏色 let texture = sprite.spriteFrame.getTexture(); let color = sprite.node.color this.setTexture(texture); this.setUniform('color' , { r: color.r / 255, g: color.g / 255, b: color.b / 255, a: sprite.node.opacity / 255 }) this.updateHash(); // 指定sprite的材質 sprite._material = this; sprite._renderData._material = this; sprite._state = cc.Sprite.State.CUSTOM; } init(name , uniforms) { let renderer = cc.renderer.renderEngine.renderer; let gfx = cc.renderer.renderEngine.gfx; let 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 mainTech = new renderer.Technique( ['transparent'], [ ...uniforms, { name: 'texture', type: renderer.PARAM_TEXTURE_2D /*, val : '默認值'*/}, { name: 'color', type: renderer.PARAM_COLOR4 /*, val : '默認值'*/}, ], [pass] ); this._texture = null; this._effect = this.effect = new renderer.Effect([mainTech], {}, []); this._mainTech = mainTech; } };
Render
可渲染節點如包含CCSprite組件的node
渲染組件CCSprite繼承自RenderComponent
渲染組件onEnable時會爲node賦值渲染組件的索引
this.node._renderComponent = this;
CCDirector.mainLoop中發起渲染命令
RenderComponentWalker.visit遍歷場景節點
RenderFlow._children方法中會過濾點!active和全透明的節點
if (!c._activeInHierarchy || c._opacity === 0) continue;
RenderComponentWalker._commitComp中比較material的hash值
這也是updateHash的意義所在(若不調用updateHash很可能會報錯,比如當節點是首個渲染節點時)
若hash值相同會使用上一個材質(流水線操作)
_commitComp (comp, assembler, cullingMask) { if (this.material._hash !== comp._material._hash || this.cullingMask !== cullingMask) { this._flush(); this.node = assembler.useModel ? comp.node : this._dummyNode; this.material = comp._material; this.cullingMask = cullingMask; } assembler.fillBuffers(comp, this); },
RenderComponentWalker._flush
Scene.prototype.addModel添加至渲染模型數組
Base.prototype._render會遍歷模型數組
顯然model中是包含material等全部渲染信息的
再由Base.prototype._draw渲染每一個顯示模型
最後由Device.prototype.draw調用opengl命令完成繪製~