Cocos Creator で cc.Sprite
に独自の cc.GLProgram
を適用する方法について情報がなかったのでメモ。
Cocoa Creator の紹介
(出典: cocos2d-x 公式ブログ)
Cocos Creator は cocos2d-html5 をベースとした Unity 似の開発環境で、ウェブ・モバイル向けの2Dゲームを効率良く開発することができます。対応言語は英語と中国語 (簡体字) のみで、ドキュメントも非常に少ないですが、Unity よりもずっとコンパクトで、用途を限れば (たぶん) 有用だと思います。
Cocos Creator で使用しているのは cocos2d-html5 や cocos2d-x そのものではなく、cocos2d-html5 に Unity 風エンティティ-コンポーネントアーキテクチャを追加した Cocos Creator Engine Framework と呼ばれるもので、使い方は cocos2d 系列のライブラリとは大きく異なります (ドキュメンテーション)。
Cocos Creator ではノード (cc.Node
) をシーン上に配置します。ノードにはコンポーネントを追加することで機能を持たせることができ、例えばスプライトコンポーネント (cc.Sprite
, 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
という非公開 API として残っています。cc.Sprite
は内部的には ccsg.Sprite
を利用しており、_sgNode
というフィールドでこのインスタンスを持っている為、これにアクセスすれば同様のことが Cocos Creator でも実現できるということになります。
実際に動作する例を示します。次のスクリプトをプロジェクトに追加し、スプライトノードにコンポーネントとして追加してみて下さい。ブラウザで表示すると、当該スプライトがモノクロになって表示されるはずです。
# シェーダの生成
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 レンダラーの内部動作を考慮しながらうまく実装する必要があります。
例えば、Sprite
が複数あり、それぞれ異なるパラメータを渡したい場合を考えてみましょう。シェーダに渡している頂点属性は「位置」「色」「テクスチャ座標」があり、「色」は自由に設定できることから、これをパラメータを渡すのに利用することができます。しかし、「色」は 4 個の浮動小数点値なので、渡せる情報がかなり限られます。これに対するアプローチは複数考えられますが、uniform を使用した場合について考えてみましょう。
シェーダへパラメータを渡す場合、uniform を用いるのが一般的ですが、ここで注意が必要です。Cocos Engine ではパフォーマンスを最適化するために、複数の Sprite
を一つのバッチでまとめて描画する機能があります。このため、Sprite
毎に異なる uniform を与えたい場合、まずバッチが発生しないようにする必要があります。レンダラーのコードから分かるように、複数の Sprite
がバッチされる必要条件の一つは「shader
が共通であること」です。つまり Sprite
毎に異なる GLProgram
のインスタンスを指定すればバッチは回避できます。Sprite
の個数分 GLProgram
を作りなおすのはオーバーヘッドが大き過ぎるので、例えば次のようなコードならどうでしょうか?
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 のドキュメントをご覧下さい。
MyShader = () ->
MyShader.prototype = shader
shader.use = () ->
cc.GLProgram::use.apply @, arguments
# Uniformの値を設定
@setUniformLocationWith1i "u_myparameter", 114514
return
まとめ
Cocoa Creator の cc.Sprite
でカスタムシェーダーを使用し、パラメータを設定する方法について説明しました。非公開 API を利用し、エンジン内部の動作に依存した相当ダーティーな方法でしたが、如何でしたでしょうか?