サイトのトップへ戻る

libGDX ドキュメント 日本語訳

サイト内検索

libGDXでシェーダーを作成する

このチュートリアルでは、LibGDX 3D apiを使ってシェーダーを作成して使用する方法について説明します。 ここでは非常に基本的な内容のみを扱います。 DefaultShader を改良して使用する方法について見ていきます。 その次に、シェーダーを1から作成する方法について見ていきます。

このチュートリアルの全ソース素材、実行可能なテストは このgithub リポジトリにあります。

前回見てきたように、 シェーダーはRenderableの実際の描写を司っています。 LibGDXに付属している既定のシェーダーは、ゲーム制作に必要な基本的な描写のほとんどをサポートしています。 しかし、エフェクトのようなより高度な描写の場合は、カスタムシェーダーを使用したくなるでしょう。

シェーダーについて深く学ぶ前に、まずは簡単な例から始めてみましょう。 これについては、前回のチュートリアルでやったところから始めます。:


public class ShaderTest implements ApplicationListener {
   public PerspectiveCamera cam;
   public CameraInputController camController;
   public Shader shader;
   public RenderContext renderContext;
   public Model model;
   public Environment environment;
   public Renderable renderable;

   @Override
   public void create () {
       environment = new Environment();
       environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
       environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));

       cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
       cam.position.set(2f, 2f, 2f);
       cam.lookAt(0,0,0);
       cam.near = 1f;
       cam.far = 300f;
       cam.update();

       camController = new CameraInputController(cam);
       Gdx.input.setInputProcessor(camController);

       ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
       model = modelLoader.loadModel(Gdx.files.internal("data/invaders.g3dj"));

       NodePart blockPart = model.getNode("ship").parts.get(0);

       renderable = new Renderable();
       blockPart.setRenderable(renderable);
       renderable.environment = environment;
       renderable.worldTransform.idt();

       renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
       shader = new DefaultShader(renderable);
       shader.init();
   }

   @Override
   public void render () {
       camController.update();

       Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
       Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

       renderContext.begin();
       shader.begin(cam, renderContext);
       shader.render(renderable);
       shader.end();
       renderContext.end();
   }

   @Override
   public void dispose () {
       shader.dispose();
       model.dispose();
   }

    @Override public void resume () {}
    @Override public void resize (int width, int height) {}
    @Override public void pause () {}
}

クラス名を変更し、Renderable 値を設定する便利なメソッドを使用したので注意してください。 最初にシェーダーを作成する時、renderable はできるだけシンプルにすると良いでしょう。 では、コードを少し変更してみましょう:


   public void create () {
       cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
       cam.position.set(2f, 2f, 2f);
       cam.lookAt(0,0,0);
       cam.near = 1f;
       cam.far = 300f;
       cam.update();

       camController = new CameraInputController(cam);
       Gdx.input.setInputProcessor(camController);

       ModelBuilder modelBuilder = new ModelBuilder();
       model = modelBuilder.createSphere(2f, 2f, 2f, 20, 20, 
         new Material(),
         Usage.Position | Usage.Normal | Usage.TextureCoordinates);

       NodePart blockPart = model.nodes.get(0).parts.get(0);

       renderable = new Renderable();
       blockPart.setRenderable(renderable);
       renderable.environment = null;
       renderable.worldTransform.idt();

       renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
       shader = new DefaultShader(renderable);
       shader.init();
   }

githubにある全ソースコードを見てください

上記コードでは、environment オブジェクトを削除してrenderable.environment にnullを設定しているだけです。 これは、environment (例えば lightsなど)を適用しないことを示しています。 次に、ModelLoader を削除して代わりにModelBuilderを使用しています。 ModelBuilderは 以前に使用したもので、簡単な球体を作成します。 球体の境界は [2, 2, 2]で、空っぽのマテリアルを持ち、球体の各頂点は位置属性と法線属性とテクスチャ座標属性を持っています。

OpenGL ES 2.0 を有効にしてテストを実行すると、作成した非常に地味な球体が表示されます。:

shadertest2

実際のところ、これだと球体ではなくただの円に見えます。 描写しているものが何なのかを分かるようにするため、 create メソッドに以下の行を追加してください:


renderable.meshPart.primitiveType = GL20.GL_POINTS;

githubにある全ソースコードを見てください

そしてもう一度実行してください。

shadertest3

スクリーンをドラッグしてカメラを回転させることができるので、覚えておいてください。 上記画像では、球体に含まれている全ての頂点が表示されています。 これをよく見てみると、(上から下へかけて)徐々にサイズと間隔が大きくなっていく20個の円によって構成されており、各円には(Y 軸を中心として)20個の点が含まれています。 この値は、球体を作成する際に設定した divisionsU 引数と divisionsV 引数の値と一致します。 あなたが頂点とメッシュについて既に理解しているものとして説明しているので、これについて深くは説明しません。 しかし、頂点(上記画像における点) とフラグメント (メッシュの各可視ピクセル)は何が違うのかを覚えておくようにしてください。

では、先ほど追加した行 (renderable.meshPart.primitiveType = GL20.GL_POINTS;)を削除してください。元の不透明で地味な球体に戻ります。

今度は、シェーダーをカスタマイズして球体をもう少し面白い見た目にしてみましょう。 これは、シェーダーコードを記述した二つのglsl ファイルによって行います。 一つ目のコードは球体内の全ての頂点(上記画像で表示されているドット) に対して実行され、 二つ目のコードは球体内の全てのピクセル(フラグメント)に対して実行されます。 そのため、assets フォルダ内のdataフォルダに二つの空っぽのファイルを作成し、ファイル名はtest.vertex.glsltest.fragment.glslにしてください。

まず最初に test.vertex.glsl ファイルを記述します:


attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord0;

uniform mat4 u_worldTrans;
uniform mat4 u_projViewTrans;

varying vec2 v_texCoord0;

void main() {
    v_texCoord0 = a_texCoord0;
    gl_Position = u_projViewTrans * u_worldTrans * vec4(a_position, 1.0);
}

githubにある全ソースコードを見てください

上記コードでは、まず最初に三つのattribute: a_positiona_normala_texCoord0を定義しています。 これらには、各頂点の位置、法線、テクスチャ座標といった値が設定されます。 次に二つのuniform を定義しています。 u_worldTransrenderable.transform 値を受け取り、 u_projViewTrans には cam.combined 値が設定されます。 これらの変数の名前はDefaultShader クラス内で定義されているので注意してください。詳細は後で説明します。 最後に varying: v_texCoord0を定義します。これを使用して a_texCoord0 値をフラグメントシェーダーに転送します。

main メソッドは、全ての頂点に対して呼び出されるメソッドです。 main メソッド内では a_texCoord0の値をv_texCoord0に代入して、次に頂点の画面上の位置を計算しています。

今度は test.fragment.glsl ファイルを記述します:


#ifdef GL_ES 
precision mediump float;
#endif

varying vec2 v_texCoord0;

void main() {
    gl_FragColor = vec4(v_texCoord0, 0.0, 1.0);
}

githubにある全ソースコードを見てください

まず最初にマクロを使って、OpenGL ES (例えば Androidや iOS や webGL)を使用している場合の精度を設定します。 次に、vertex シェーダーの時と同じ様に、varying v_texCoord0を定義します。

main メソッド内では、フラグメントの色の赤成分をテクスチャ X 座標 (U)へ設定し、緑成分をテクスチャ Y 座標 (V)へ設定します。

既にglsl ファイルの用意ができたので、既定のシェーダーをカスタマイズしてそのglsl ファイルを使ってみましょう:


   public void create () {
       ...
       String vert = Gdx.files.internal("data/test.vertex.glsl").readString();
       String frag = Gdx.files.internal("data/test.fragment.glsl").readString();
       shader = new DefaultShader(renderable, new DefaultShader.Config(vert, frag));
       shader.init();
   }

githubにある全ソースコードを見てください

上記コードでは、作成した二つのファイルをString変数へと読み込み、それらを引数として渡してDefaultShaderを作成します。それでは実行してみましょう:

shadertest4

ちゃんと表示されましたね。球体の赤色コンポーネントと緑色コンポートネントには、 X (U) テクスチャ座標とY (V) テクスチャ座標の値が設定されます。 こうしてたった数行のコードだけで、独自のGLSL コードでDefaultShader をカスタマイズできました。

しかし、これはあなたのシェーダーがDefaultShader と同じ属性とユニフォーム変数を使用している場合のみ動作します。 言い換えれば、DefaultShader はカスタムGLSL コードを実行することができるGLSL コンテキストを提供しています。

それでは、ここで実際に何が起こっているかを見ていきましょう。

さっき記述したGLSL コードは GPU上で動作します。 頂点の属性(a_positionなど) と ユニフォーム変数 (u_worldTransなど)を設定する、メッシュをGPU へ渡す、オプションでテクスチャなどをGPUへ渡す、と行った処理はCPU上で行われます。 renderableを描写するためには、GPU部分 とCPU部分は連携して動作する必要があります。 例えば、CPUでテクスチャの紐付けをしたのにそれをGPUで使用しないと無意味です。 そしてCPU で設定されていないユニフォーム変数をGPU 上で使用するても無意味です。 LibGDX では、CPU部分とGPU部分が一緒にShaderを構成しているのです。 Shaderは渡されたrenderableの描写に必要なことは全て行います(しかしそれ以外のことは一切行いません)。

ほとんどの文献ではシェーダーはCPU部分 (GLSL コード)のみを参照しているので、この話は少し混乱するかもしれません。 LibGDX では、 GPU 部分(頂点シェーダーとフラグメントシェーダーを合わせたもの) は ShaderProgramと呼ばれます。 そして Shader は CPU部分と GPU 部分の両方を合わせたものになります。 一般的に Shader は ShaderProgramを使用しますが、それは必須ではありません (例えば OpenGL ES 1.xの場合など)。

今度はDefaultShaderに頼るのではなく、一からShader を記述します。それでは、TestShaderという名前のクラスを新規に作成してください:


public class TestShader implements Shader {
    @Override
    public void init () {}
    @Override
    public void dispose () {}
    @Override
    public void begin (Camera camera, RenderContext context) {  }
    @Override
    public void render (Renderable renderable) {    }
    @Override
    public void end () {    }
    @Override
    public int compareTo (Shader other) {
        return 0;
    }
    @Override
    public boolean canRender (Renderable instance) {
        return true;
    }
}

このシェーダーを実装する前に、まずは最後の二つのメソッドを見ていきます。 compareToメソッドは、最初にどのシェーダー使用するが決める際にModelBatchが使用します。 今のところ我々は使用しません。 canRenderメソッドは、指定されたrenderableを描写するのにこのシェーダーを使用するかを決める際に使用されます。これは後で詳しく見ていきます。 しかし今のところは常にtrueを戻り値として返します。

init() メソッドは、このシェーダーが作成された直後に1回呼び出されます。 ここでは使用するShaderProgramを作成できます。:


public class TestShader implements Shader {
    ShaderProgram program;

    @Override
    public void init () {
        String vert = Gdx.files.internal("data/test.vertex.glsl").readString();
        String frag = Gdx.files.internal("data/test.fragment.glsl").readString();
        program = new ShaderProgram(vert, frag);
        if (!program.isCompiled())
            throw new GdxRuntimeException(program.getLog());
    }

    @Override
    public void dispose () {
        program.dispose();
    }
    ...
}

init メソッド内では、vertexの GLSLファイルとfragmentの GLSLファイルを読み込み、それを使ってShaderProgramのインスタンスを作成します。 ShaderProgramが正常にコンパイルできながかった場合は、重要な例外を投げるのでGLSLコードを簡単にデバッグできます。 ShaderProgram は破棄をする必要があるので、disposeメソッドにも1行追加しています。

beginメソッドは、シェーダーが1つ以上のrenderableの描写で使用される直前のフレーム毎で呼び出されます。 end()メソッドは、シェーダーを使った描写の準備ができたタイミングのフレーム毎で呼び出されます。 render メソッドは、 begin() メソッドと end()メソッドの間でのみ呼び出されます。 こうして、begin()メソッドと end() メソッドを使って ShaderProgramの紐付けと紐付け解除を行うことができます:


public class TestShader implements Shader {
    ...
    @Override
    public void begin (Camera camera, RenderContext context) {
        program.begin();
    }
    ...
    @Override
    public void end () {
        program.end();
    }
    ...
}

begin メソッドは二つの引数 camera と contextを持っています。この引数は、 endメソッドが呼び出されるまでシェーダーに対して排他的となります(そして変更できません)。 それでは、この引数をメンバに保存しましょう:


public class TestShader implements Shader {
    ShaderProgram program;
    Camera camera;
    RenderContext context;
    ...
    @Override
    public void begin (Camera camera, RenderContext context) {
        this.camera = camera;
        this.context = context;
        program.begin();
    }
    ...
}

シェーダーのプログラミングでは、二つのユニフォーム変数u_worldTransu_projViewTransが必要です。 後者はcameraにのみ依存しています。つまり、 beginメソッド内で設定することができます。:


    @Override
    public void begin (Camera camera, RenderContext context) {
        this.camera = camera;
        this.context = context;
        program.begin();
        program.setUniformMatrix("u_projViewTrans", camera.combined);
    }

u_worldTrans は renderableに依存するので、render() メソッド内で値を設定する必要があります:


    @Override
    public void render (Renderable renderable) {
        program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
    }

これで全ての ユニフォーム変数が設定できたので、あとは属性を設定してメッシュを紐付けて描写を行う必要があります。 これは mesh.render()を1回呼び出すことで行えます。:


public class TestShader implements Shader {
    ...
    @Override
    public void render (Renderable renderable) {
        program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
        renderable.meshPart.render(program);
    }
    ...
}

githubにある全ソースコードを見てください

だいたい出来たので、ShaderTestでこのシェーダーを使ってみましょう:


public class ShaderTest implements ApplicationListener {
   ...
   @Override
   public void create () {
       ...
       renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
       shader = new TestShader();
       shader.init();
   }
   ...
}

githubにある全ソースコードを見てください

これでテストを実行すると、以下のようになるでしょう:

shadertest5

うーん、正しくできてない気がしますね。 なぜなら、深度テストを使用するようにRenderContextを設定していないからです。 ですからそれを変更してみましょう。 また、背面カリングの設定も合わせて有効にしましょう。 背面カリング処理を有効にすると、カメラに向いていない面は描写されなくなります。 そのためカメラが球体の中にある場合は球体は表示されません(球体にズームすることでこれをテストできます):


public class TestShader implements Shader {
    ...
    @Override
    public void begin (Camera camera, RenderContext context) {
        this.camera = camera;
        this.context = context;
        program.begin();
        program.setUniformMatrix("u_projViewTrans", camera.combined);
        context.setDepthTest(true, GL20.GL_LEQUAL);
        context.setCullFace(GL20.GL_BACK);
    }

githubにある全ソースコードを見てください

では再度テストを実行して、見た目がかなり良くなっていることを確認してください。

shadertest4

これで完了です。シェーダーのCPU 部分とCPU 部分の両方が作成できました。 しかし、今日の作業をこれでお終りにする前に以下の行を見てください。:


program.setUniformMatrix("u_worldTrans", renderable.worldTransform);

上記のコードでは、 u_worldTrans という名前のユニフォーム変数 をrenderable.worldTransformの値に設定しています。 しかしこれは、render()メソッドが呼び出される度に、ShaderProgram はプログラム内にあるユニフォーム変数u_worldTrans の位置を見つける必要があるということになります(この文字列検索は負荷が高いです):


public class TestShader implements Shader {
    ShaderProgram program;
    Camera camera;
    RenderContext context;
    int u_projViewTrans;
    int u_worldTrans;

    @Override
    public void init () {
        ...
        u_projViewTrans = program.getUniformLocation("u_projViewTrans");
        u_worldTrans = program.getUniformLocation("u_worldTrans");
    }
    ...
    @Override
    public void begin (Camera camera, RenderContext context) {
        this.camera = camera;
        this.context = context;
        program.begin();
        program.setUniformMatrix(u_projViewTrans, camera.combined);
        context.setDepthTest(true, GL20.GL_LEQUAL);
        context.setCullFace(GL20.GL_BACK);
    }

    @Override
    public void render (Renderable renderable) {
        program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
        renderable.meshPart.render(program);
    }
    ...
}

githubにある全ソースコードを見てください

これで、LibGDX 3D apiを使った非常に基本的な最初のシェーダーが作成できました。 次のチュートリアルでは、 シェーダー内のマテリアル属性を使用する(カスタマイズする)方法について見ていきます。




エンジェル戦記