Cocos Creator でカスタムシェーダーを使う


Cocos Creator で `cc.Sprite` に独自の `cc.GLProgram` を適用する方法について情報がなかったのでメモ。

Cocoa Creator の紹介
-------------------

![](http://blog.cocos2d-x.org/wp-content/uploads/2016/03/darkslash.png)
(出典: [cocos2d-x 公式ブログ](http://blog.cocos2d-x.org/2016/03/cocos-creator-1-0-is-here/))

[Cocos Creator](http://cocos2d-x.org/docs/editors_and_tools/creator/) は cocos2d-html5 をベースとした ~~Unity 似の~~開発環境で、ウェブ・モバイル向けの2Dゲームを効率良く開発することができます。対応言語は英語と中国語 (簡体字) のみで、ドキュメントも非常に少ないですが、Unity よりもずっとコンパクトで、用途を限れば (たぶん) 有用だと思います。

Cocos Creator で使用しているのは cocos2d-html5 や cocos2d-x そのものではなく、cocos2d-html5 に ~~Unity 風~~エンティティ-コンポーネントアーキテクチャを追加した [Cocos Creator Engine Framework](https://github.com/cocos-creator/engine) と呼ばれるもので、使い方は cocos2d 系列のライブラリとは**大きく異なります** ([ドキュメンテーション](http://cocos2d-x.org/docs/api-ref/creator/v1.0/))。

Cocos Creator ではノード ([`cc.Node`](http://cocos2d-x.org/docs/api-ref/creator/v1.0/classes/Node.html)) をシーン上に配置します。ノードにはコンポーネントを追加することで機能を持たせることができ、例えばスプライトコンポーネント ([`cc.Sprite`](http://cocos2d-x.org/docs/api-ref/creator/v1.0/classes/Sprite.html), cocos2d-html5に同名のクラスがあるので注意！) を追加することで、スプライトを画面上に表示させることができます。さらに、ユーザ定義のコンポーネントを JavaScript/CoffeeScript で記述し、ノードに追加することで、独自の動作をさせることができます。

`cc.Sprite` でシェーダを使う
-------------------------

cocos2d-html5 では、`Sprite` の `setShaderProgram` メソッドを呼出すことで、WebGL GLSL シェーダを設定し、高度な視覚効果を実現することができました。しかし、Cocos Creator の `cc.Sprite` ではこのメソッドは提供されていないようです。

Cocos Creator Engine Framework は内部的には cocos2d-html5 をベースとして動作しています。では cocos2d-html5 の `Sprite` がどこに行ったのかと言うと、[`ccsg.Sprite`](https://github.com/cocos-creator/engine/blob/master/cocos2d/core/sprites/CCSGSprite.js) という非公開 API として残っています。`cc.Sprite` は内部的には `ccsg.Sprite` を利用しており、`_sgNode` というフィールドでこのインスタンスを持っている為、これにアクセスすれば同様のことが Cocos Creator でも実現できるということになります。

実際に動作する例を示します。次のスクリプトをプロジェクトに追加し、スプライトノードにコンポーネントとして追加してみて下さい。ブラウザで表示すると、当該スプライトがモノクロになって表示されるはずです。

```coffeescript:MakeSpriteMonochromeController.coffee
# シェーダの生成
shader = new cc.GLProgram()
shader.initWithString """
    attribute vec4 a_position;
    attribute vec2 a_texCoord;
    attribute vec4 a_color;
    
    varying lowp vec4 v_fragmentColor;
    varying mediump vec2 v_texCoord;
    								
    void main()	
    {							
        gl_Position = CC_PMatrix * a_position;
    	v_fragmentColor = a_color;
    	v_texCoord = a_texCoord;
    }""", """
    precision lowp float;
    varying vec4 v_fragmentColor;	
    varying vec2 v_texCoord;	
    		
    void main()			
    {
    	vec4 v_orColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
    	float gray = dot(v_orColor.rgb, vec3(0.299, 0.587, 0.114));
    	gl_FragColor = vec4(gray, gray, gray, v_orColor.a);
    }"""
shader.addAttribute cc.macro.ATTRIBUTE_NAME_POSITION, cc.macro.VERTEX_ATTRIB_POSITION
shader.addAttribute cc.macro.ATTRIBUTE_NAME_COLOR, cc.macro.VERTEX_ATTRIB_COLOR
shader.addAttribute cc.macro.ATTRIBUTE_NAME_TEX_COORD, cc.macro.VERTEX_ATTRIB_TEX_COORDS
shader.link()
shader.updateUniforms()

# コンポーネント定義
cc.Class {
    extends: cc.Component

    properties: {
        # foo:
        #   default: null
        #   type: cc
        #   serializable: true # [optional], default is true
        #   visible: true      # [optional], default is true
        #   displayName: 'Foo' # [optional], default is property name
        #   readonly: false    # [optional], default is false
    }
    
    onLoad: () ->
        
        sprite = @getComponent "cc.Sprite"
        sprite._sgNode.setShaderProgram shader
        
        return

    update: (dt) ->
        # do your update here
        return
}
```

パラメータの渡し方
---------------

本来想定されていない、いわゆる「ハック」的な使用法ですので、パラメータを渡すにも「公式」な方法は存在せず、Cocos Engine の [WebGL レンダラーの内部動作](https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/RendererWebGL.js)を考慮しながらうまく実装する必要があります。

例えば、`Sprite` が複数あり、それぞれ異なるパラメータを渡したい場合を考えてみましょう。シェーダに渡している頂点属性は「位置」「色」「テクスチャ座標」があり、「色」は自由に設定できることから、これをパラメータを渡すのに利用することができます。しかし、「色」は 4 個の浮動小数点値なので、渡せる情報がかなり限られます。これに対するアプローチは複数考えられますが、uniform を使用した場合について考えてみましょう。

シェーダへパラメータを渡す場合、uniform を用いるのが一般的ですが、ここで注意が必要です。Cocos Engine ではパフォーマンスを最適化するために、複数の `Sprite` を一つのバッチでまとめて描画する機能があります。このため、`Sprite` 毎に異なる uniform を与えたい場合、まずバッチが発生しないようにする必要があります。レンダラーのコードから分かるように、複数の `Sprite` がバッチされる必要条件の一つは「`shader` が共通であること」です。つまり `Sprite` 毎に異なる `GLProgram` のインスタンスを指定すればバッチは回避できます。`Sprite` の個数分 `GLProgram` を作りなおすのはオーバーヘッドが大き過ぎるので、例えば次のようなコードならどうでしょうか？

```coffee:batch-hack.coffee
shader = new cc.GLProgram()
# (中略)
shader.updateUniforms()

# JSのプロトタイプチェーンを利用して低オーバーヘッドでshaderを複製する
# 注: ここではclassは使えません!
MyShader = () ->
MyShader.prototype = shader

# (中略)

    onLoad: () ->
        
        sprite = @getComponent "cc.Sprite"
        sprite._sgNode.setShaderProgram new MyShader() # 複製
        
        return
```

これで、レンダラーから見ると、`Sprite` 毎に違う `GLProgram` が設定されているように見えるので、バッチ化は行われなくなるはずです。では、次に uniform を設定する方法について考えてみましょう。

レンダラーでは、各ドローコールの前に、その `Sprite` に設定された `shader` の `use` メソッドを呼び出しています。`Sprite` 毎に uniform の値を設定したい場合、ここで uniform を設定するのが一番確実な方法と思われます。例えばこんな感じでどうでしょうか？Uniform の設定方法については cocos2d-html5 のドキュメントを[ご覧下さい](http://www.cocos2d-x.org/docs/api-ref/js/v3x/)。

```coffeescript
MyShader = () ->
MyShader.prototype = shader
shader.use = () ->
    cc.GLProgram::use.apply @, arguments

    # Uniformの値を設定
    @setUniformLocationWith1i "u_myparameter", 114514
    return
```

まとめ
-----

Cocoa Creator の `cc.Sprite` でカスタムシェーダーを使用し、パラメータを設定する方法について説明しました。非公開 API を利用し、エンジン内部の動作に依存した相当ダーティーな方法でしたが、如何でしたでしょうか？