Java Swing テキストRPGを作る ~Swingを使って画面を作る~

イントロダクション

テキストRPGを作成しようと思います。どちらかというとリメイクに近いのですが、前回作成したものが完成していないので。。。
兎にも角にも、Java Swingでの実装になりますので、クラスの扱い方の学習がしやすい、かつ、視覚的にクラス関係を理解できると思います。

IDE(開発ツール)はBlueJを使用しています。

Swingを使って画面を作る

以前、テキストRPGを作成しました。
Gitにアップしてあります。

しかし、これはコマンドプロンプト上で実行するもので「ゲーム」って感じのしないものでした。画面のリロードとかうまくいきません。。。
なので、いろいろと考えた末、Java Swingで実装したらよいと考えなおしました。

余談

実際、筆者はJavaのオブジェクト指向プログラミングという部分をSwingで学びました。つまり、クラスの扱い方を理解しました。
「オブジェクト指向」という言葉を使うと「staticおじさん」や「オブジェクト指向おじさん」よろしく。。。混沌の世界に足を踏み入れることになるので言葉を変えていきたいと思います。

クラスの扱い方を理解する

まとめると、筆者はSwingの実装を通してクラスの扱い方を理解しました。というところを言いたかった次第です。
そして、最近覚えたBlueJを使用して、テキストRPGを作成していきたいと思います。

画面を作る

Swingを使用して、画面を作成していきます。まずは、テキストRPGを実行して「テキスト」を表示する領域が必要になります。

初めのコード

この領域には次の機能が必要になります。

  1. 文字を表示する
  2. 文字をクリア(削除)する

これらを実現するためにプログラム的には、以下のものを使用します。

  • JFrame: アプリの表示する領域フレームを表現するクラス
  • JPanel: フレーム上にコンポーネントを配置するパネル
  • 各種コンポーネント: ラベル(JLabel)、テキストエリア(JTextArea)など

クラス継承について

クラスの継承関係を見てみるとわかりやすいです。

これは、JFrameクラスの親は、Frameクラス、そしてその親は。。。とそれぞれの継承関係を示しています。
つまり、クラスキャストも行うことができるということです。

JFrame frame = new JFrame();
Frame superFrame = (Frame) frame;
superFrame.XXXX;

言葉を変えると、親クラスが必要な時は、上記のようにキャストして使用することができます。
そして、親クラスのメソッドを呼び出すこともできます。

JFrame frame = new JFrame();
frame.addNotify(); // java.awt.Frameのメソッド

コードについて

作成したコードは、下のような表示を行います。どの部分がフレームなのか?も記述しました。

コード

TextRPGMainクラスは、JFrameを継承しているところに注意してください。

package jp.zenryoku.rpg;

import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.JPanel;
import javax.swing.JLabel;
import java.awt.Container;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;

/**
 * クラス TextRPGMain の注釈をここに書きます.
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TextRPGMain extends JFrame
{
    public static void main(String[] args) {
        // JFrameを継承しているのでJFrameクラスのメソッドを使える
        TextRPGMain main = new TextRPGMain();
        main.run("Text RPG");
    }

    public void run(String title) {
        // タイトルをセット
        setTitle(title);
        // 画面の表示位置と画面サイズをセット
        Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
        int xPos = (int) windowSize.getWidth() / 4;
        int yPos = (int) windowSize.getHeight() / 4;
        setBounds(xPos, yPos, xPos * 2, yPos * 2);
        // 画面を閉じたときアプリを終了する設定
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // ラベル
        JLabel titleLabel = new JLabel("Text RPG");
        JTextArea textarea = new JTextArea();
        // テキストエリア
        textarea.setColumns(40);
        textarea.setRows(10);

        // ラベル用のパネル
        JPanel titlePanel = new JPanel();
        titlePanel.add(titleLabel);

        // テキストエリア用のパネル
        JPanel textPanel = new JPanel();
        textPanel.add(textarea);

        // パネルをセットするコンテナ
        Container contentPane = getContentPane();
        // コンテナにパネルをセット
        contentPane.add(titlePanel, BorderLayout.NORTH);
        contentPane.add(textPanel, BorderLayout.CENTER);
        // 表示する設定
        setVisible(true);
    }
}

ちなみに、クラス図で見ると下のようになります。

メインメソッドを持っているクラスのみになります。

次は、クラスの拡張実装を行ってみようと思います。

クラス継承の実装

クラスの継承方法は下のように「extends クラス名」と書くだけです。

public class ChildClass extends ParentClass {
   ....
}

JLabelを拡張する

「拡張」という言葉に戸惑うかもしれません。ズバリ「JLabelを継承して新しいクラスを作成する」という意味です。

新しく「TitleLabel」クラスを追加します。このクラスは上記のTextRPGMainクラスのrun()メソッドで行っている処理を少なくするように実装しています。
別な言い方をすると「タイトルラベルの処理はTitleLabelに任せましょう。というところです。

では、どのようになるのか?というところです。

TitleLabelの実装

  1. TitleLabelクラスを作成します。
  2. JLabelを継承します。
  3. 現状はコンストラクタの実装のみで事足ります。

実際のコードです。

package jp.zenryoku.rpg;

import javax.swing.JLabel;
import java.awt.Dimension;
import java.awt.Color;

/**
 * クラス TitleLabel の注釈をここに書きます.
 * JLabelを拡張して、テキストRPGのタイトルをセットするラベルを作成する。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class TitleLabel extends JLabel
{
    public TitleLabel(String title, Dimension windowSize) {
        super(title);
        int width = (int) windowSize.getWidth() / 4;
        int height = (int) windowSize.getHeight() / 16;
        Dimension labelSize = new Dimension(width, height);
        setOpaque(true);
        setPreferredSize(labelSize);
        setBackground(Color.GREEN);
        setHorizontalAlignment(JLabel.CENTER);
    }
}
  1. JLabelを継承しているので、親クラス(JLabel)のコンストラクタを呼び出します。super(title);
  2. ラベルのサイズ(縦横の幅指定)をします。
  3. ラベルの領域がわかるように、緑色の背景を付けます。

ちなみに、ラベルのサイズは、毎回値を変更するのは、面倒なのでPCの画面サイズに合わせてサイズを変更するように実装しました。

そして、run()メソッドと今回作成したクラスの処理の関係を示します。

TextRPGMain#run()

JLabelを生成して、タイトルをセットしただけで、幅や背景などはセットしていませんでした。

// ラベル
JLabel titleLabel = new JLabel("Text RPG");

なので、この「TitleLabel」クラスを作成していなかったらTextRPGMainクラスにJLabelの処理を書くことになります。
このTextRPGMainクラスにJLabelの処理を書くことがプログラム的に美しくないのでTitleLabelを作成しタイトルラベルのことはこのクラスにコードを書きましょう。という風に考えてプログラムを作りました。

TextRPGMain#run()の修正

ズバリ下のように修正しました。1行です。
JLabel titleLabel = new JLabel("Text RPG");TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
になりました。表示の結果は以下の通り
<元々の表示>

<修正後の表示>

次は、プログラム・コードを見てみましょう。
<元々の処理>

public void run(String title) {
    // タイトルをセット
    setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    JLabel titleLabel = new JLabel("Text RPG");
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    JPanel titlePanel = new JPanel();
    titlePanel.add(titleLabel);

    // テキストエリア用のパネル
    JPanel textPanel = new JPanel();
    textPanel.add(textarea);

    // パネルをセットするコンテナ
    Container contentPane = getContentPane();
    // コンテナにパネルをセット
    contentPane.add(titlePanel, BorderLayout.NORTH);
    contentPane.add(textPanel, BorderLayout.CENTER);
    // 表示する設定
    setVisible(true);
}

<修正後>

public void run(String title) {
    // タイトルをセット
   setTitle(title);
    // 画面の表示位置と画面サイズをセット
    Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
    int xPos = (int) windowSize.getWidth() / 4;
    int yPos = (int) windowSize.getHeight() / 4;
    setBounds(xPos, yPos, xPos * 2, yPos * 2);
    // 画面を閉じたときアプリを終了する設定
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // ラベル
    TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
    // テキストエリア
    JTextArea textarea = new JTextArea();
    textarea.setColumns(40);
    textarea.setRows(10);

    // ラベル用のパネル
    JPanel titlePanel = new JPanel();
    titlePanel.add(titleLabel);

    // テキストエリア用のパネル
    JPanel textPanel = new JPanel();
    textPanel.add(textarea);

    // パネルをセットするコンテナ
    Container contentPane = getContentPane();
    // コンテナにパネルをセット
    contentPane.add(titlePanel, BorderLayout.NORTH);
    contentPane.add(textPanel, BorderLayout.CENTER);
    // 表示する設定
    setVisible(true);
}

こんな感じです。
次は、テキストエリアをタイトルラベルと同じように拡張しようと思います。

JTextAreaの拡張

まずは、現状のクラス作成状況を確認します。

次は、画面の白い部分「テキストエリア」を拡張して文字列の表示領域を作成します。

今回も、テキストエリアを担当するクラスを作成します。ネーミングセンスが問われますが、目的と役割を明確にすることを最優先にするので。。。

RpgTextクラスを作成

RpgTextAreaクラスとします。作成はまずJTextAreaを継承します。

import javax.swing.JTextArea;

/**
 * クラス RpgTextArea の注釈をここに書きます.
 * テキストRPGの表示する文字列をこの領域に出力(描画)する。
 * 背景は黒、イメージはドラ○エのような感じにしたい。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {

    }
}

そして、テキストの表示を担当するので、メインクラスに書いている次の部分が不要になります。

JTextArea textarea = new JTextArea();
textarea.setColumns(40);
textarea.setRows(10);

同様に、次のようにコードをRpgTextAreaに追加します。

public class RpgTextArea extends JTextArea
{
    public RpgTextArea() {
        setColumns(40);
        setRows(10);
    }
}

そして、TextRPGMain#run()を修正

    public void run(String title) {
        setTitle(title);
        Dimension windowSize = Toolkit.getDefaultToolkit().getScreenSize();
        int xPos = (int) windowSize.getWidth() / 4;
        int yPos = (int) windowSize.getHeight() / 4;
        setBounds(xPos, yPos, xPos * 2, yPos * 2);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        TitleLabel titleLabel = new TitleLabel("Text RPG", windowSize);
        RpgTextArea textarea = new RpgTextArea();

        JPanel titlePanel = new JPanel();
        titlePanel.add(titleLabel);

        JPanel textPanel = new JPanel();
        textPanel.add(textarea);

        Container contentPane = getContentPane();
        contentPane.add(titlePanel, BorderLayout.NORTH);
        contentPane.add(textPanel, BorderLayout.CENTER);

        setVisible(true);
    }

JPanelをRpgTextAreaに修正、不要なコードを削除しました。

この状態でプログラム実行すると下のようになります。

全く変わりません。その代わり、run()メソッドのコードの量は(少しですが)減りました。
ここから、テキストエリアのおしゃれをしていきます。
参照するのはJavaDocのJTextAreaです。
他にも次のクラスを参照します。

Fontクラスを見ると、フォントファイルを指定することでオリジナルのフォントも使えるようです。

<実行結果>

TextAreaのサイズ設定

画面のサイズ指定に関して、文字入力することも考えてPCの画面サイズから文字の数、行の数を設定するようにプログラムを組みました。
理論的なところが、はっきりと決まらなかったのですが、縦横の「~分の~」という形で実装しました。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        ...
    }
    ...
}

実行結果は、下のような形です。

とりあえずは、これで、画面が作成できたのでここでひと段落になります。

まとめ

クラスを継承すると親クラスのメソッドなどは、自分のクラス内のメソッドのように使用することができる。
なので、下のようなコードが書ける。

public class RpgTextArea extends JTextArea
{
    /** コンストラクタ */
    public RpgTextArea(Dimension size) {
        int widthCol = (int) size.getWidth() / 19;
        int heightRow = (int) size.getHeight() / 28;
        System.out.println("width: " + widthCol);
        System.out.println("height: " + heightRow);
        setColumns(widthCol);
        setRows(heightRow);
        // 背景の描画準備
        setOpaque(true);
        // フォントの設定
        setFont(createTextFont());
        // 背景を黒にする
        setBackground(Color.BLACK);
        // 白い文字の設定
        setForeground(Color.WHITE);
        // 白いボーダーの設定
        Border border = BorderFactory.createLineBorder(Color.GREEN);
        setBorder(BorderFactory.createCompoundBorder(border,
            BorderFactory.createEmptyBorder(10, 10, 10, 10)));

        setWrapStyleWord(true);
        setLineWrap(true);
    }
    ....
}

つまるところは、親クラスのpublic, (packaged, )protectedのメソッドを子クラスが使用することができるので「setXXX」のようなメソッドを直接呼び出すことができる。

今回は、コンストラクタのみを使用した形で実装しました。
次は、テキストの表示などを行っていきたいと思います。

Java 3D LWJGL GitBook: 第 19 章 – ディファード シェーディング

第 19 章 - ディファード シェーディング

これまで、3D シーンをレンダリングする方法はフォワード レンダリングと呼ばれていました。最初に 3D オブジェクトをレンダリングし、フラグメント シェーダーでテクスチャと照明効果を適用します。この方法は、多数のライトと複雑なエフェクトを含む複雑なフラグメント シェーダー パスがある場合、あまり効率的ではありません。それに加えて、これらの効果を後で深度テストのために破棄される可能性のあるフラグメントに適用することになる場合があります (ただし、初期のフラグメント テストを有効にした場合、これは正確には当てはまりません)。

上記の問題を軽減するために、ディファード シェーディングと呼ばれる技術を使用してシーンをレンダリングする方法を変更することがあります。ディファード シェーディングでは、後の段階で (フラグメント シェーダーで) 必要となるジオメトリ情報を最初にバッファーにレンダリングします。フラグメント シェーダーに必要な複雑な計算は、これらのバッファーに格納されている情報を使用する際に、後の段階に延期されます。

コンセプト

Deferred では、2 つのレンダリング パスを実行する必要があります。1 つ目はジオメトリ パスで、次の情報を含むバッファにシーンをレンダリングします。

・深さの値。
・各位置の拡散色と反射係数。
・各位置のスペキュラー コンポーネント。
・各位置の法線 (ライト ビュー座標系でも)。
そのすべての情報は、G-Buffer と呼ばれるバッファーに格納されます。

2 番目のパスは、ライティング パスと呼ばれます。このパスは、すべての画面を埋めるクワッドを取得し、G バッファーに含まれる情報を使用して各フラグメントの色情報を生成します。ライティング パスを実行するとき、深度テストでは、表示されないすべてのシーン データが既に削除されています。したがって、実行する操作の数は、画面に表示されるものに制限されます。

追加のレンダリング パスを実行するとパフォーマンスが向上するかどうかを尋ねられる場合があります。答えは、場合によるということです。ディファード シェーディングは通常、多数の異なるライト パスがある場合に使用されます。この場合、追加のレンダリング手順は、フラグメント シェーダーで実行される操作の削減によって補われます。

G-バッファ

それでは、コーディングを始めましょう。最初に行う作業は、G-Buffer の新しいクラスを作成することです。という名前のクラスは、GBuffer次のように定義されます。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryStack;
import org.lwjglb.engine.Window;

import java.nio.*;
import java.util.Arrays;

import static org.lwjgl.opengl.GL30.*;

public class GBuffer {

    private static final int TOTAL_TEXTURES = 4;

    private int gBufferId;
    private int height;
    private int[] textureIds;
    private int width;
    ...
}

このクラスは、使用されるバッファーの最大数をモデル化する定数を定義します。G バッファー自体に関連付けられた識別子と、個々のバッファーの配列。テクスチャのサイズも保存されます。

コンストラクターを確認しましょう。

public class GBuffer {
    ...
    public GBuffer(Window window) {
        gBufferId = glGenFramebuffers();
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBufferId);

        textureIds = new int[TOTAL_TEXTURES];
        glGenTextures(textureIds);

        this.width = window.getWidth();
        this.height = window.getHeight();

        for (int i = 0; i < TOTAL_TEXTURES; i++) {
            glBindTexture(GL_TEXTURE_2D, textureIds[i]);
            int attachmentType;
            if (i == TOTAL_TEXTURES - 1) {
                glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT,
                        (ByteBuffer) null);
                attachmentType = GL_DEPTH_ATTACHMENT;
            } else {
                glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, (ByteBuffer) null);
                attachmentType = GL_COLOR_ATTACHMENT0 + i;
            }
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

            glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentType, GL_TEXTURE_2D, textureIds[i], 0);
        }

        try (MemoryStack stack = MemoryStack.stackPush()) {
            IntBuffer intBuff = stack.mallocInt(TOTAL_TEXTURES);
            for (int i = 0; i < TOTAL_TEXTURES; i++) {
                intBuff.put(i, GL_COLOR_ATTACHMENT0 + i);
            }
            glDrawBuffers(intBuff);
        }

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }
    ...
}

最初に行うことは、フレーム バッファーを作成することです。フレーム バッファは、画面にレンダリングする代わりに操作をレンダリングするために使用できる単なる OpenGL オブジェクトであることに注意してください。次に、フレーム バッファに関連付けられる一連のテクスチャ (4 つのテクスチャ) を生成します。

その後、for ループを使用してテクスチャを初期化します。次のタイプがあります。

・位置、法線、拡散コンポーネントなどを格納する「通常のテクスチャ」。
・深度バッファを格納するためのテクスチャ。これが最後のテクスチャになります。
テクスチャが初期化されたら、テクスチャのサンプリングを有効にして、フレーム バッファにアタッチします。各テクスチャは、 で始まる識別子を使用してアタッチされGL_COLOR_ATTACHMENT0ます。各テクスチャはその id によって 1 ずつ増加するため、位置は を使用してアタッチされGL_COLOR_ATTACHMENT0、拡散コンポーネントはGL_COLOR_ATTACHMENT1( GL_COLOR_ATTACHMENT0 + 1) を使用するなどです。

すべてのテクスチャが作成されたら、レンダリングのためにフラグメント シェーダで使用できるようにする必要があります。これはglDrawBuffers呼び出しで行われます。使用するカラー アタッチメントの識別子を含む配列を渡すだけです (GL_COLOR_ATTACHMENT0にGL_COLOR_ATTACHMENT5)。
クラスの残りの部分は、getter メソッドとクリーンアップ メソッドだけです。

public class GBuffer {
    ...
    public void cleanUp() {
        glDeleteFramebuffers(gBufferId);
        Arrays.stream(textureIds).forEach(GL30::glDeleteTextures);
    }

    public int getGBufferId() {
        return gBufferId;
    }

    public int getHeight() {
        return height;
    }

    public int[] getTextureIds() {
        return textureIds;
    }

    public int getWidth() {
        return width;
    }
}

ジオメトリ パス

ジオメトリ パスを実行するときに適用する必要がある変更を調べてみましょう。これらの変更をSceneRenderクラスと関連するシェーダーに適用します。クラスをSceneRender見て、ライト定数とライト ユニフォームを削除する必要があります。これらはこのパスで使用されることに注意してください (単純化するためにマテリアルにアンビエント カラーも使用しません。そのユニフォームも削除する必要があり、選択したものも削除します)。実体のユニフォーム):

public class SceneRender {

    private ShaderProgram shaderProgram;
    private UniformsMap uniformsMap;
    ...
    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("projectionMatrix");
        uniformsMap.createUniform("modelMatrix");
        uniformsMap.createUniform("viewMatrix");
        uniformsMap.createUniform("bonesMatrices");
        uniformsMap.createUniform("txtSampler");
        uniformsMap.createUniform("normalSampler");
        uniformsMap.createUniform("material.diffuse");
        uniformsMap.createUniform("material.specular");
        uniformsMap.createUniform("material.reflectance");
        uniformsMap.createUniform("material.hasNormalMap");
    }
    ...
}

メソッドは次のrenderように定義されます。

public class SceneRender {
    ...
    public void render(Scene scene, GBuffer gBuffer) {
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gBuffer.getGBufferId());
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glViewport(0, 0, gBuffer.getWidth(), gBuffer.getHeight());
        glDisable(GL_BLEND);

        shaderProgram.bind();

        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        uniformsMap.setUniform("viewMatrix", scene.getCamera().getViewMatrix());

        uniformsMap.setUniform("txtSampler", 0);
        uniformsMap.setUniform("normalSampler", 1);

        Collection<Model> models = scene.getModelMap().values();
        TextureCache textureCache = scene.getTextureCache();
        for (Model model : models) {
            List<Entity> entities = model.getEntitiesList();

            for (Material material : model.getMaterialList()) {
                uniformsMap.setUniform("material.diffuse", material.getDiffuseColor());
                uniformsMap.setUniform("material.specular", material.getSpecularColor());
                uniformsMap.setUniform("material.reflectance", material.getReflectance());
                String normalMapPath = material.getNormalMapPath();
                boolean hasNormalMapPath = normalMapPath != null;
                uniformsMap.setUniform("material.hasNormalMap", hasNormalMapPath ? 1 : 0);
                Texture texture = textureCache.getTexture(material.getTexturePath());
                glActiveTexture(GL_TEXTURE0);
                texture.bind();
                if (hasNormalMapPath) {
                    Texture normalMapTexture = textureCache.getTexture(normalMapPath);
                    glActiveTexture(GL_TEXTURE1);
                    normalMapTexture.bind();
                }

                for (Mesh mesh : material.getMeshList()) {
                    glBindVertexArray(mesh.getVaoId());
                    for (Entity entity : entities) {
                        uniformsMap.setUniform("modelMatrix", entity.getModelMatrix());
                        AnimationData animationData = entity.getAnimationData();
                        if (animationData == null) {
                            uniformsMap.setUniform("bonesMatrices", AnimationData.DEFAULT_BONES_MATRICES);
                        } else {
                            uniformsMap.setUniform("bonesMatrices", animationData.getCurrentFrame().boneMatrices());
                        }
                        glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
                    }
                }
            }
        }

        glBindVertexArray(0);
        glEnable(GL_BLEND);
        shaderProgram.unbind();
    }
}

GBufferメソッドのパラメーターとしてインスタンスを受け取っていることがわかります。そのバッファはレンダリングを実行する場所であるため、最初に を呼び出してそのバッファをバインドしglBindFramebufferます。その後、そのバッファをクリアしてブレンドを無効にします。遅延レンダリングを使用する場合、透明なオブジェクトは少し注意が必要です。アプローチは、それらをライト パスでレンダリングするか、ジオメトリ パスで破棄することです。ご覧のとおり、ライトの統一設定コードをすべて削除しました。

頂点シェーダーの唯一の変更点 ( scene.vert) は、ビューの位置が 4 つのコンポーネントのベクターになったことです ( vec4)。

#version 330
...
out vec4 outViewPosition;
...
void main()
{
    ...
    outWorldPosition = modelMatrix * initPos;
    outViewPosition  = viewMatrix * outWorldPosition;
    gl_Position   = projectionMatrix * outViewPosition;
    outNormal     = normalize(modelViewMatrix * initNormal).xyz;
    outTangent    = normalize(modelViewMatrix * initTangent).xyz;
    outBitangent  = normalize(modelViewMatrix * initBitangent).xyz;
    outTextCoord  = texCoord;
}

フラグメント シェーダー ( scene.frag) は大幅に簡素化されています。

#version 330

in vec3 outNormal;
in vec3 outTangent;
in vec3 outBitangent;
in vec2 outTextCoord;
in vec4 outViewPosition;
in vec4 outWorldPosition;

layout (location = 0) out vec4 buffAlbedo;
layout (location = 1) out vec4 buffNormal;
layout (location = 2) out vec4 buffSpecular;

struct Material
{
    vec4 diffuse;
    vec4 specular;
    float reflectance;
    int hasNormalMap;
};

uniform sampler2D txtSampler;
uniform sampler2D normalSampler;
uniform Material material;

vec3 calcNormal(vec3 normal, vec3 tangent, vec3 bitangent, vec2 textCoords) {
    mat3 TBN = mat3(tangent, bitangent, normal);
    vec3 newNormal = texture(normalSampler, textCoords).rgb;
    newNormal = normalize(newNormal * 2.0 - 1.0);
    newNormal = normalize(TBN * newNormal);
    return newNormal;
}

void main() {
    vec4 text_color = texture(txtSampler, outTextCoord);
    vec4 diffuse = text_color + material.diffuse;
    if (diffuse.a < 0.5) {
        discard;
    }
    vec4 specular = text_color + material.specular;

    vec3 normal = outNormal;
    if (material.hasNormalMap > 0) {
        normal = calcNormal(outNormal, outTangent, outBitangent, outTextCoord);
    }

    buffAlbedo   = vec4(diffuse.xyz, material.reflectance);
    buffNormal   = vec4(0.5 * normal + 0.5, 1.0);
    buffSpecular = specular;
}

最も関連性の高い行は次のとおりです。

...
layout (location = 0) out vec4 buffAlbedo;
layout (location = 1) out vec4 buffNormal;
layout (location = 2) out vec4 buffSpecular;
...

これは、このフラグメント シェーダーが書き込むテクスチャを参照している場所です。ご覧のとおり、拡散反射光カラー (マテリアルのコンポーネントに関連付けられたテクスチャのカラー)、スペキュラー コンポーネント、法線、およびシャドウ マップの深度値をダンプするだけです。テクスチャに位置を保存していないことに気付くかもしれません。これは、深度値を使用してフラグメントの位置を再構築できるためです。ライティング パスでこれを行う方法について説明します。

補足:Materialアンビエント カラー コンポーネントを削除して、クラス定義を簡略化しました。

OpenGL デバッガー (RenderDoc など) を使用してサンプルの実行をデバッグすると、ジオメトリ パス中に生成されたテクスチャを表示できます。アルベド テクスチャは次のようになります。

法線の値を保持するテクスチャは次のようになります。

スペキュラ カラーの値を保持するテクスチャは次のようになります。

最後に、深度テクスチャは次のようになります。

照明パス

LightsRenderライティング パスを実行するために、次のように始まる名前の新しいクラスを作成します。

package org.lwjglb.engine.graph;

import org.joml.*;
import org.lwjglb.engine.scene.*;
import org.lwjglb.engine.scene.lights.*;

import java.util.*;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL14.*;
import static org.lwjgl.opengl.GL30.*;

public class LightsRender {
    private static final int MAX_POINT_LIGHTS = 5;
    private static final int MAX_SPOT_LIGHTS = 5;

    private final ShaderProgram shaderProgram;

    private QuadMesh quadMesh;
    private UniformsMap uniformsMap;

    public LightsRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/lights.vert", GL_VERTEX_SHADER));
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/lights.frag", GL_FRAGMENT_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
        quadMesh = new QuadMesh();
        createUniforms();
    }

    public void cleanup() {
        quadMesh.cleanup();
        shaderProgram.cleanup();
    }
    ...
}

新しいシェーダー プログラムの作成に加えて、QadMeshクラスの新しい属性 (まだ定義されていません) を定義していることがわかります。render メソッドを分析する前に、ライトをどのようにレンダリングするかについて少し考えてみましょう。G-Buffer の内容を使用する必要がありますが、それらを使用するには、まず何かをレンダリングする必要があります。しかし、私たちはすでにシーンを描いているので、何をレンダリングしようとしています. 今?答えは簡単です。すべての画面を満たすクワッドをレンダリングするだけです。そのクワッドの各フラグメントに対して、G バッファーに含まれるデータを使用して、正しい出力カラーを生成します。ここでQuadMeshクラスが機能します。ライティング パスでレンダリングするために使用されるクワッドを定義するだけで、次のように定義されます。

package org.lwjglb.engine.graph;

import org.lwjgl.opengl.GL30;
import org.lwjgl.system.*;

import java.nio.*;
import java.util.*;

import static org.lwjgl.opengl.GL30.*;

public class QuadMesh {

    private int numVertices;
    private int vaoId;
    private List<Integer> vboIdList;

    public QuadMesh() {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            vboIdList = new ArrayList<>();
            float[] positions = new float[]{
                    -1.0f, 1.0f, 0.0f,
                    1.0f, 1.0f, 0.0f,
                    -1.0f, -1.0f, 0.0f,
                    1.0f, -1.0f, 0.0f,};
            float[] textCoords = new float[]{
                    0.0f, 1.0f,
                    1.0f, 1.0f,
                    0.0f, 0.0f,
                    1.0f, 0.0f,};
            int[] indices = new int[]{0, 2, 1, 1, 2, 3};
            numVertices = indices.length;

            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            // Positions VBO
            int vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
            positionsBuffer.put(0, positions);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

            // Texture coordinates VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer textCoordsBuffer = MemoryUtil.memAllocFloat(textCoords.length);
            textCoordsBuffer.put(0, textCoords);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, textCoordsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);

            // Index VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            IntBuffer indicesBuffer = stack.callocInt(indices.length);
            indicesBuffer.put(0, indices);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);

            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
        }
    }

    public void cleanup() {
        vboIdList.stream().forEach(GL30::glDeleteBuffers);
        glDeleteVertexArrays(vaoId);
    }

    public int getNumVertices() {
        return numVertices;
    }

    public int getVaoId() {
        return vaoId;
    }
}

ご覧のとおり、必要なのは位置とテクスチャの座標属性だけです (G-Buffer テクスチャに適切にアクセスするため)。クラスに戻るとLightsRender、ユニフォームを作成するメソッドが必要です。これにより、以前にクラスで使用されていたライト ユニフォームに加えて、G バッファ テクスチャ ( 、、および)SceneRenderをマップするための新しいユニフォームのセットが復元されます。それに加えて、やなどの深度値からフラグメント位置を計算するための新しいユニフォームが必要になります。シェーダー コードで、それらがどのように使用されるかを確認します。albedoSamplernormalSamplerspecularSamplerdepthSamplerinvProjectionMatrixinvViewMatrix

public class LightsRender {
    ...
    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("albedoSampler");
        uniformsMap.createUniform("normalSampler");
        uniformsMap.createUniform("specularSampler");
        uniformsMap.createUniform("depthSampler");
        uniformsMap.createUniform("invProjectionMatrix");
        uniformsMap.createUniform("invViewMatrix");
        uniformsMap.createUniform("ambientLight.factor");
        uniformsMap.createUniform("ambientLight.color");

        for (int i = 0; i < MAX_POINT_LIGHTS; i++) {
            String name = "pointLights[" + i + "]";
            uniformsMap.createUniform(name + ".position");
            uniformsMap.createUniform(name + ".color");
            uniformsMap.createUniform(name + ".intensity");
            uniformsMap.createUniform(name + ".att.constant");
            uniformsMap.createUniform(name + ".att.linear");
            uniformsMap.createUniform(name + ".att.exponent");
        }
        for (int i = 0; i < MAX_SPOT_LIGHTS; i++) {
            String name = "spotLights[" + i + "]";
            uniformsMap.createUniform(name + ".pl.position");
            uniformsMap.createUniform(name + ".pl.color");
            uniformsMap.createUniform(name + ".pl.intensity");
            uniformsMap.createUniform(name + ".pl.att.constant");
            uniformsMap.createUniform(name + ".pl.att.linear");
            uniformsMap.createUniform(name + ".pl.att.exponent");
            uniformsMap.createUniform(name + ".conedir");
            uniformsMap.createUniform(name + ".cutoff");
        }

        uniformsMap.createUniform("dirLight.color");
        uniformsMap.createUniform("dirLight.direction");
        uniformsMap.createUniform("dirLight.intensity");

        uniformsMap.createUniform("fog.activeFog");
        uniformsMap.createUniform("fog.color");
        uniformsMap.createUniform("fog.density");

        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            uniformsMap.createUniform("shadowMap_" + i);
            uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".projViewMatrix");
            uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".splitDistance");
        }
    }
    ...
}

メソッドは次のrenderように定義されます。

public class LightsRender {
    ...
    public void render(Scene scene, ShadowRender shadowRender, GBuffer gBuffer) {
        shaderProgram.bind();

        updateLights(scene);

        // Bind the G-Buffer textures
        int[] textureIds = gBuffer.getTextureIds();
        int numTextures = textureIds != null ? textureIds.length : 0;
        for (int i = 0; i < numTextures; i++) {
            glActiveTexture(GL_TEXTURE0 + i);
            glBindTexture(GL_TEXTURE_2D, textureIds[i]);
        }

        uniformsMap.setUniform("albedoSampler", 0);
        uniformsMap.setUniform("normalSampler", 1);
        uniformsMap.setUniform("specularSampler", 2);
        uniformsMap.setUniform("depthSampler", 3);

        Fog fog = scene.getFog();
        uniformsMap.setUniform("fog.activeFog", fog.isActive() ? 1 : 0);
        uniformsMap.setUniform("fog.color", fog.getColor());
        uniformsMap.setUniform("fog.density", fog.getDensity());

        int start = 4;
        List<CascadeShadow> cascadeShadows = shadowRender.getCascadeShadows();
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glActiveTexture(GL_TEXTURE0 + start + i);
            uniformsMap.setUniform("shadowMap_" + i, start + i);
            CascadeShadow cascadeShadow = cascadeShadows.get(i);
            uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".projViewMatrix", cascadeShadow.getProjViewMatrix());
            uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".splitDistance", cascadeShadow.getSplitDistance());
        }
        shadowRender.getShadowBuffer().bindTextures(GL_TEXTURE0 + start);

        uniformsMap.setUniform("invProjectionMatrix", scene.getProjection().getInvProjMatrix());
        uniformsMap.setUniform("invViewMatrix", scene.getCamera().getInvViewMatrix());

        glBindVertexArray(quadMesh.getVaoId());
        glDrawElements(GL_TRIANGLES, quadMesh.getNumVertices(), GL_UNSIGNED_INT, 0);

        shaderProgram.unbind();
    }
    ...
}

ライトを更新した後、ジオメトリ パスの結果を保持するテクスチャをアクティブにします。その後、フォグとカスケード シャドウのユニフォームを設定し、クワッドだけを描画します。

では、ライト パスの頂点シェーダーはどのように見えるのでしょうか ( lights.vert)?

#version 330

layout (location=0) in vec3 inPos;
layout (location=1) in vec2 inCoord;

out vec2 outTextCoord;

void main()
{
    outTextCoord = inCoord;
    gl_Position = vec4(inPos, 1.0f);
}

上記のコードは、頂点を直接ダンプし、テクスチャ座標をフラグメント シェーダーに渡すだけです。フラグメント シェーダー ( lights.frag) は次のように定義されます。

#version 330

const int MAX_POINT_LIGHTS = 5;
const int MAX_SPOT_LIGHTS = 5;
const float SPECULAR_POWER = 10;
const int NUM_CASCADES = 3;
const float BIAS = 0.0005;
const float SHADOW_FACTOR = 0.25;

in vec2 outTextCoord;
out vec4 fragColor;

struct Attenuation
{
    float constant;
    float linear;
    float exponent;
};
struct AmbientLight
{
    float factor;
    vec3 color;
};
struct PointLight {
    vec3 position;
    vec3 color;
    float intensity;
    Attenuation att;
};
struct SpotLight
{
    PointLight pl;
    vec3 conedir;
    float cutoff;
};
struct DirLight
{
    vec3 color;
    vec3 direction;
    float intensity;
};
struct Fog
{
    int activeFog;
    vec3 color;
    float density;
};
struct CascadeShadow {
    mat4 projViewMatrix;
    float splitDistance;
};

uniform sampler2D albedoSampler;
uniform sampler2D normalSampler;
uniform sampler2D specularSampler;
uniform sampler2D depthSampler;

uniform mat4 invProjectionMatrix;
uniform mat4 invViewMatrix;

uniform AmbientLight ambientLight;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform SpotLight spotLights[MAX_SPOT_LIGHTS];
uniform DirLight dirLight;
uniform Fog fog;
uniform CascadeShadow cascadeshadows[NUM_CASCADES];
uniform sampler2D shadowMap_0;
uniform sampler2D shadowMap_1;
uniform sampler2D shadowMap_2;

vec4 calcAmbient(AmbientLight ambientLight, vec4 ambient) {
    return vec4(ambientLight.factor * ambientLight.color, 1) * ambient;
}
vec4 calcLightColor(vec4 diffuse, vec4 specular, float reflectance, vec3 lightColor, float light_intensity, vec3 position, vec3 to_light_dir, vec3 normal) {
    vec4 diffuseColor = vec4(0, 0, 0, 1);
    vec4 specColor = vec4(0, 0, 0, 1);

    // Diffuse Light
    float diffuseFactor = max(dot(normal, to_light_dir), 0.0);
    diffuseColor = diffuse * vec4(lightColor, 1.0) * light_intensity * diffuseFactor;

    // Specular Light
    vec3 camera_direction = normalize(-position);
    vec3 from_light_dir = -to_light_dir;
    vec3 reflected_light = normalize(reflect(from_light_dir, normal));
    float specularFactor = max(dot(camera_direction, reflected_light), 0.0);
    specularFactor = pow(specularFactor, SPECULAR_POWER);
    specColor = specular * light_intensity  * specularFactor * reflectance * vec4(lightColor, 1.0);

    return (diffuseColor + specColor);
}

vec4 calcPointLight(vec4 diffuse, vec4 specular, float reflectance, PointLight light, vec3 position, vec3 normal) {
    vec3 light_direction = light.position - position;
    vec3 to_light_dir  = normalize(light_direction);
    vec4 light_color = calcLightColor(diffuse, specular, reflectance, light.color, light.intensity, position, to_light_dir, normal);

    // Apply Attenuation
    float distance = length(light_direction);
    float attenuationInv = light.att.constant + light.att.linear * distance +
    light.att.exponent * distance * distance;
    return light_color / attenuationInv;
}

vec4 calcSpotLight(vec4 diffuse, vec4 specular, float reflectance, SpotLight light, vec3 position, vec3 normal) {
    vec3 light_direction = light.pl.position - position;
    vec3 to_light_dir  = normalize(light_direction);
    vec3 from_light_dir  = -to_light_dir;
    float spot_alfa = dot(from_light_dir, normalize(light.conedir));

    vec4 color = vec4(0, 0, 0, 0);

    if (spot_alfa > light.cutoff)
    {
        color = calcPointLight(diffuse, specular, reflectance, light.pl, position, normal);
        color *= (1.0 - (1.0 - spot_alfa)/(1.0 - light.cutoff));
    }
    return color;
}

vec4 calcDirLight(vec4 diffuse, vec4 specular, float reflectance, DirLight light, vec3 position, vec3 normal) {
    return calcLightColor(diffuse, specular, reflectance, light.color, light.intensity, position, normalize(light.direction), normal);
}
vec4 calcFog(vec3 pos, vec4 color, Fog fog, vec3 ambientLight, DirLight dirLight) {
    vec3 fogColor = fog.color * (ambientLight + dirLight.color * dirLight.intensity);
    float distance = length(pos);
    float fogFactor = 1.0 / exp((distance * fog.density) * (distance * fog.density));
    fogFactor = clamp(fogFactor, 0.0, 1.0);

    vec3 resultColor = mix(fogColor, color.xyz, fogFactor);
    return vec4(resultColor.xyz, color.w);
}

float textureProj(vec4 shadowCoord, vec2 offset, int idx) {
    float shadow = 1.0;

    if (shadowCoord.z > -1.0 && shadowCoord.z < 1.0) {
        float dist = 0.0;
        if (idx == 0) {
            dist = texture(shadowMap_0, vec2(shadowCoord.xy + offset)).r;
        } else if (idx == 1) {
            dist = texture(shadowMap_1, vec2(shadowCoord.xy + offset)).r;
        } else {
            dist = texture(shadowMap_2, vec2(shadowCoord.xy + offset)).r;
        }
        if (shadowCoord.w > 0 && dist < shadowCoord.z - BIAS) {
            shadow = SHADOW_FACTOR;
        }
    }
    return shadow;
}

float calcShadow(vec4 worldPosition, int idx) {
    vec4 shadowMapPosition = cascadeshadows[idx].projViewMatrix * worldPosition;
    float shadow = 1.0;
    vec4 shadowCoord = (shadowMapPosition / shadowMapPosition.w) * 0.5 + 0.5;
    shadow = textureProj(shadowCoord, vec2(0, 0), idx);
    return shadow;
}
void main()
{
    vec4 albedoSamplerValue = texture(albedoSampler, outTextCoord);
    vec3 albedo  = albedoSamplerValue.rgb;
    vec4 diffuse = vec4(albedo, 1);

    float reflectance = albedoSamplerValue.a;
    vec3 normal = normalize(2.0 * texture(normalSampler, outTextCoord).rgb  - 1.0);
    vec4 specular = texture(specularSampler, outTextCoord);

    // Retrieve position from depth
    float depth = texture(depthSampler, outTextCoord).x * 2.0 - 1.0;
    if (depth == 1) {
        discard;
    }
    vec4 clip      = vec4(outTextCoord.x * 2.0 - 1.0, outTextCoord.y * 2.0 - 1.0, depth, 1.0);
    vec4 view_w    = invProjectionMatrix * clip;
    vec3 view_pos  = view_w.xyz / view_w.w;
    vec4 world_pos = invViewMatrix * vec4(view_pos, 1);

    vec4 diffuseSpecularComp = calcDirLight(diffuse, specular, reflectance, dirLight, view_pos, normal);

    int cascadeIndex;
    for (int i=0; i<NUM_CASCADES - 1; i++) {
        if (view_pos.z < cascadeshadows[i].splitDistance) {
            cascadeIndex = i + 1;
            break;
        }
    }
    float shadowFactor = calcShadow(world_pos, cascadeIndex);

    for (int i=0; i<MAX_POINT_LIGHTS; i++) {
        if (pointLights[i].intensity > 0) {
            diffuseSpecularComp += calcPointLight(diffuse, specular, reflectance, pointLights[i], view_pos, normal);
        }
    }

    for (int i=0; i<MAX_SPOT_LIGHTS; i++) {
        if (spotLights[i].pl.intensity > 0) {
            diffuseSpecularComp += calcSpotLight(diffuse, specular, reflectance, spotLights[i], view_pos, normal);
        }
    }
    vec4 ambient = calcAmbient(ambientLight, diffuse);
    fragColor = ambient + diffuseSpecularComp;
    fragColor.rgb = fragColor.rgb * shadowFactor;

    if (fog.activeFog == 1) {
        fragColor = calcFog(view_pos, fragColor, fog, ambientLight.color, dirLight);
    }
}

ご覧のとおり、見慣れた機能が含まれています。これらは、シーン フラグメント シェーダーの前の章で使用されていました。ここで注意すべき重要な点は、次の行です。

uniform sampler2D albedoSampler;
uniform sampler2D normalSampler;
uniform sampler2D specularSampler;
uniform sampler2D depthSampler;

最初に、現在のフラグメント座標に従って、アルベド、法線マップ ([0, -1] から [-1, 1] の範囲に変換)、およびスペキュラー アタッチメントをサンプリングします。それに加えて、見慣れないコード フラグメントがあります。光の計算を実行するには、フラグメントの位置が必要です。ただし、役職のアタッチメントはありません。ここで、深度アタッチメントと逆投影行列が機能します。その情報を使用して、位置を保存する別のアタッチメントを必要とせずに、世界の位置 (ビュー空間座標) を再構築できます。他のチュートリアルでは、位置に特定のアタッチメントを設定していることがわかりますが、この方法で行う方がはるかに効率的です。遅延アタッチメントによって消費されるメモリが少ないほど良いことを常に覚えておいてください。

コードの残りの部分は、シーン レンダリングのフラグメント シェーダーのものと非常によく似ています。

最後にRender、新しいクラスを使用するようにクラスを更新する必要があります。

public class Render {
    ...
    private GBuffer gBuffer;
    ...
    private LightsRender lightsRender;
    ...
    public Render(Window window) {
        ...
        lightsRender = new LightsRender();
        gBuffer = new GBuffer(window);
    }

    public void cleanup() {
        ...
        lightsRender.cleanup();
        gBuffer.cleanUp();
    }

    private void lightRenderFinish() {
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }

    private void lightRenderStart(Window window) {
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glViewport(0, 0, window.getWidth(), window.getHeight());

        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFunc(GL_ONE, GL_ONE);

        glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer.getGBufferId());
    }

    public void render(Window window, Scene scene) {
        shadowRender.render(scene);
        sceneRender.render(scene, gBuffer);
        lightRenderStart(window);
        lightsRender.render(scene, shadowRender, gBuffer);
        skyBoxRender.render(scene);
        lightRenderFinish();
        guiRender.render(scene);
    }

    public void resize(int width, int height) {
        guiRender.resize(width, height);
    }
}

<サンプルコードの実行>

Java 3D LWJGL GitBook: 第 18 章 – 3D オブジェクトのピッキング

第 18 章 - 3D オブジェクトのピッキング

すべてのゲームの重要な側面の 1 つは、環境と対話する機能です。この機能では、3D シーンでオブジェクトを選択できる必要があります。この章では、これを実現する方法について説明します。

コンセプト

画面上でマウスをクリックしてエンティティを選択する機能を追加します。そのために、マウスでクリックしたポイントを方向として使用して、カメラの位置 (原点) からレイをキャストします (マウス座標からワールド座標に変換します)。その光線を使用して、各エンティティに関連付けられた境界ボックス (エンティティに関連付けられたモデルを囲む立方体) と交差するかどうかを確認します。
次の手順を実装する必要があります。

  • 境界ボックスを各モデルに関連付けます (実際にはモデルの各メッシュに)。
  • マウス座標をワールド空間座標に変換して、カメラ位置からレイをキャストします。
  • エンティティごとに、関連するメッシュを反復処理し、光線と交差するかどうかを確認します。
  • レイに最も近い距離で交差するエンティティを選択します。
  • 選択したエンティティがある場合は、フラグメント シェーダーで強調表示します。

コードの準備

まず、ロードするモデルの各メッシュのバウンディング ボックスを計算することから始めます。モデルをロードするときに追加のフラグを追加することで、assimpにこの作業を任せます: aiProcess_GenBoundingBoxes. このフラグは、各 mex の境界ボックスを自動的に計算します。そのボックスはすべてのメッシュを埋め込み、軸を揃えます。これに使用される頭字語「AABB」が表示される場合があります。これは、Axis Aligned Bounding Box を意味します。なぜ軸を揃えたボックスなのですか? 交差計算が大幅に簡素化されるためです。そのフラグを使用することにより、assimpは境界ボックスのコーナーとして使用できる計算を実行します (最小座標と最大座標を使用)。次の図は、立方体でどのように表示されるかを示しています。

計算を有効にしたら、メッシュを処理するときにその情報を取得する必要があります。

public class ModelLoader {
    ...
    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, boolean animation) {
        return loadModel(modelId, modelPath, textureCache, aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices |
                aiProcess_Triangulate | aiProcess_FixInfacingNormals | aiProcess_CalcTangentSpace | aiProcess_LimitBoneWeights |
                aiProcess_GenBoundingBoxes | (animation ? 0 : aiProcess_PreTransformVertices));

    }
    ...
    private static Mesh processMesh(AIMesh aiMesh, List<Bone> boneList) {
        ...
        AIAABB aabb = aiMesh.mAABB();
        Vector3f aabbMin = new Vector3f(aabb.mMin().x(), aabb.mMin().y(), aabb.mMin().z());
        Vector3f aabbMax = new Vector3f(aabb.mMax().x(), aabb.mMax().y(), aabb.mMax().z());

        return new Mesh(vertices, normals, tangents, bitangents, textCoords, indices, animMeshData.boneIds,
                animMeshData.weights, aabbMin, aabbMax);
    }
    ...
}

Meshその情報をクラスに保存する必要があります。

public class Mesh {
    ...
    private Vector3f aabbMax;
    private Vector3f aabbMin;
    ...
    public Mesh(float[] positions, float[] normals, float[] tangents, float[] bitangents, float[] textCoords, int[] indices) {
        this(positions, normals, tangents, bitangents, textCoords, indices,
                new int[Mesh.MAX_WEIGHTS * positions.length / 3], new float[Mesh.MAX_WEIGHTS * positions.length / 3],
                new Vector3f(), new Vector3f());
    }

    public Mesh(float[] positions, float[] normals, float[] tangents, float[] bitangents, float[] textCoords, int[] indices,
                int[] boneIndices, float[] weights, Vector3f aabbMin, Vector3f aabbMax) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            this.aabbMin = aabbMin;
            this.aabbMax = aabbMax;
            ...
        }        
    }
    ...
    public Vector3f getAabbMax() {
        return aabbMax;
    }

    public Vector3f getAabbMin() {
        return aabbMin;
    }
    ...
}

レイ交差計算を実行する際、スクリーン空間からワールド空間座標に変換するために、逆ビューと投影行列が必要になります。したがって、クラスが更新されるたびにそれぞれの行列の逆数を自動的に計算するようにCameraandクラスを変更します。Projection

public class Camera {
    ...
    private Matrix4f invViewMatrix;
    ...
    public Camera() {
        ...
        invViewMatrix = new Matrix4f();
        ...
    }
    ...
    public Matrix4f getInvViewMatrix() {
        return invViewMatrix;
    }
    ...
    private void recalculate() {
        viewMatrix.identity()
                .rotateX(rotation.x)
                .rotateY(rotation.y)
                .translate(-position.x, -position.y, -position.z);
        invViewMatrix.set(viewMatrix).invert();
    }
    ...
}
public class Projection {
    ...
    private Matrix4f invProjMatrix;
    ...
    public Projection(int width, int height) {
        ...
        invProjMatrix = new Matrix4f();
        ...
    }

    public Matrix4f getInvProjMatrix() {
        return invProjMatrix;
    }
    ...
    public void updateProjMatrix(int width, int height) {
        projMatrix.setPerspective(FOV, (float) width / height, Z_NEAR, Z_FAR);
        invProjMatrix.set(projMatrix).invert();
    }
}

Entity計算が完了したら、選択したものを保存する必要もあります。これをSceneクラスで行います。

public class Scene {
    ...
    private Entity selectedEntity;
    ...
    public Entity getSelectedEntity() {
        return selectedEntity;
    }
    ...
    public void setSelectedEntity(Entity selectedEntity) {
        this.selectedEntity = selectedEntity;
    }
    ...
}

最後に、シーンのレンダリング中に新しいユニフォームを作成します。これは、Entity選択されている をレンダリングしている場合にアクティブになります。

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("selected");
    }

    public void render(Scene scene, ShadowRender shadowRender) {
        ...
        Entity selectedEntity = scene.getSelectedEntity();
        for (Model model : models) {
            List<Entity> entities = model.getEntitiesList();

            for (Material material : model.getMaterialList()) {
                ...
                for (Mesh mesh : material.getMeshList()) {
                    glBindVertexArray(mesh.getVaoId());
                    for (Entity entity : entities) {
                        uniformsMap.setUniform("selected",
                                selectedEntity != null && selectedEntity.getId().equals(entity.getId()) ? 1 : 0);
                        ...
                    }
                    ...
                }
                ...
            }
        }
        ...
    }
    ...
}

フラグメント シェーダー ( scene.frag) では、選択したエンティティに属するフラグメントの青いコンポーネントのみを変更します。

#version 330
...
uniform int selected;
...
void main() {
    ...
    if (selected > 0) {
        fragColor = vec4(fragColor.x, fragColor.y, 1, 1);
    }
}

エンティティの選択

Entityを選択する必要があるかどうかを判断するためのコードに進みます。Mainクラスでは、メソッドinputで、マウスの左ボタンが押されたかどうかを確認します。selectEntityその場合、計算を行う新しいメソッド ( ) を呼び出します。

public class Main implements IAppLogic {
    ...
    public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
        ...
        if (mouseInput.isLeftButtonPressed()) {
            selectEntity(window, scene, mouseInput.getCurrentPos());
        }
        ...
    }
    ...
}

メソッドは次のselectEntityように始まります。

public class Main implements IAppLogic {
    ...
    private void selectEntity(Window window, Scene scene, Vector2f mousePos) {
        int wdwWidth = window.getWidth();
        int wdwHeight = window.getHeight();

        float x = (2 * mousePos.x) / wdwWidth - 1.0f;
        float y = 1.0f - (2 * mousePos.y) / wdwHeight;
        float z = -1.0f;

        Matrix4f invProjMatrix = scene.getProjection().getInvProjMatrix();
        Vector4f mouseDir = new Vector4f(x, y, z, 1.0f);
        mouseDir.mul(invProjMatrix);
        mouseDir.z = -1.0f;
        mouseDir.w = 0.0f;

        Matrix4f invViewMatrix = scene.getCamera().getInvViewMatrix();
        mouseDir.mul(invViewMatrix);
        ...
    }
    ...
}

クリック座標を使用してその方向ベクトルを計算する必要があります。しかし、どのように)(x, y)
ビューポート空間の座標をワールド空間に合わせますか? モデル空間座標からビュー空間に渡す方法を確認しましょう。それを達成するために適用されるさまざまな座標変換は次のとおりです。

  • モデル行列を使用して、モデル座標からワールド座標に渡します。
  • ビューマトリックス(カメラ効果を提供する)を使用して、ワールド座標からビュー空間座標に渡します-
  • 透視投影行列を適用することにより、ビュー座標から均一なクリップ空間に渡します。
  • 最終的な画面座標は、OpenGL によって自動的に計算されます。それを行う前に、正規化されたデバイス空間に渡されます (x,y,z)
    によるコーディネート w
    コンポーネント)、そして z,y
    画面座標。
    したがって、画面座標から取得するには、逆パスをトラバースするだけで済みます (x, y)
    、ワールド座標へ。

最初のステップは、画面座標から正規化されたデバイス空間に変換することです。の (z,y)
ビューポート空間の座標が範囲内 [0,screenWith],[0, screenHeight]
. 画面の左上隅の座標は (0,0)
それを範囲内の座標に変換する必要があります (-1,1)

"x = 2 cdot screen_x / screenwidth - 1"
"y = 1 - 2 * screen_y / screenheight"

しかし、どうやって計算するのですか? z
成分?答えは簡単です。 -1
光線が最も遠い可視距離を指すように値を設定します (OpenGL では、 -1
画面を指します)。これで、正規化されたデバイス空間の座標が得られました。

変換を続行するには、それらを均一なクリップ スペースに変換する必要があります。私たちは持っている必要があります w
コンポーネント、つまり同次座標を使用します。この概念は前の章で説明しましたが、話を戻しましょう。3D ポイントを表すために必要なのは、 x, y, zとy, zコンポーネントですが、追加のコンポーネントである w
成分。マトリックスを使用してさまざまな変換を実行するには、この追加のコンポーネントが必要です。追加のコンポーネントを必要としない変換もあれば、必要とする変換もあります。たとえば、次の式しかない場合、変換行列は機能しません x, yとzコンポーネント。したがって、w コンポーネントを追加し、値を割り当てました。 1
そのため、4 x 4 の行列を扱うことができます。
それに加えて、ほとんどの変換、より正確には、ほとんどの変換行列は、w
成分。これに対する例外は射影行列です。このマトリックスは、 wに比例する値z成分。
同種のクリップ空間から正規化されたデバイス座標への変換は、x,yとz コンポーネントw
. この成分は z 成分に比例するため、遠くにあるオブジェクトは小さく描画されることを意味します。私たちの場合、逆を行う必要があり、投影を解除する必要がありますが、計算しているのは光線であるため、そのステップを単に無視して、w
コンポーネントへ1
残りのコンポーネントは元の値のままにします。

ここで、ビュー スペースに戻る必要があります。これは簡単です。射影行列の逆行列を計算し、それを 4 成分ベクトルで乗算するだけです。それが完了したら、それらをワールド空間に変換する必要があります。繰り返しますが、ビュー マトリックスを使用し、その逆数を計算し、それをベクトルで乗算するだけです。

方向のみに関心があることを思い出してください。したがって、この場合、wコンポーネントへ0
. また、設定することもできますz
コンポーネントに再び-1
、画面の方を指すようにするためです。それを行って逆ビュー行列を適用すると、ワールド空間にベクトルができます。

次のステップは、関連付けられたメッシュを使用してエンティティを繰り返し処理し、それらのバウンディング ボックスがカメラ位置から始まる光線と交差するかどうかを確認することです。

public class Main implements IAppLogic {
    ...
    private void selectEntity(Window window, Scene scene, Vector2f mousePos) {
        ...
        Vector4f min = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
        Vector4f max = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
        Vector2f nearFar = new Vector2f();

        Entity selectedEntity = null;
        float closestDistance = Float.POSITIVE_INFINITY;
        Vector3f center = scene.getCamera().getPosition();

        Collection<Model> models = scene.getModelMap().values();
        Matrix4f modelMatrix = new Matrix4f();
        for (Model model : models) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                modelMatrix.translate(entity.getPosition()).scale(entity.getScale());
                for (Material material : model.getMaterialList()) {
                    for (Mesh mesh : material.getMeshList()) {
                        Vector3f aabbMin = mesh.getAabbMin();
                        min.set(aabbMin.x, aabbMin.y, aabbMin.z, 1.0f);
                        min.mul(modelMatrix);
                        Vector3f aabMax = mesh.getAabbMax();
                        max.set(aabMax.x, aabMax.y, aabMax.z, 1.0f);
                        max.mul(modelMatrix);
                        if (Intersectionf.intersectRayAab(center.x, center.y, center.z, mouseDir.x, mouseDir.y, mouseDir.z,
                                min.x, min.y, min.z, max.x, max.y, max.z, nearFar) && nearFar.x < closestDistance) {
                            closestDistance = nearFar.x;
                            selectedEntity = entity;
                        }
                    }
                }
                modelMatrix.identity();
            }
        }
    }
    ...
}

という名前の変数を定義しますclosestDistance。この変数は、最も近い距離を保持します。交差するゲーム アイテムの場合、カメラから交点までの距離が計算され、 に格納されている値よりも小さい場合、closestDistanceこのアイテムが新しい候補になります。各メッシュのバウンディング ボックスを移動およびスケーリングする必要があります。回転も考慮されるため、座っているモデルマトリックスを使用することはできません(ボックスを軸に揃えたいので、これは望ましくありません)。これが、エンティティのデータを使用して変換とスケーリングを適用してモデル マトリックスを構築する理由です。しかし、交点をどのように計算するのでしょうか? ここで、見事なJOMLライブラリが助けになります。JOMLを使用していますIntersectionfこのクラスは、2D および 3D で交点を計算するいくつかのメソッドを提供します。具体的には、intersectRayAabメソッドを使用しています。

このメソッドは、Axis Aligned Boxes の交差をテストするアルゴリズムを実装します。JOML ドキュメントで指摘されているように、ここで詳細を確認できます。

このメソッドは、原点と方向で定義された光線が、最小コーナーと最大コーナーで定義されたボックスと交差するかどうかをテストします。前に述べたように、このアルゴリズムは有効です。立方体は軸に沿って配置されているため、回転した場合、この方法は機能しません。それに加えて、アニメーションを使用する場合、アニメーション フレームごとに異なるバウンディング ボックスが必要になる場合があります (assimp はバインディング ポーズのバウンディング ボックスを計算します)。このintersectRayAabメソッドは、次のパラメーターを受け取ります。

  • 原点: この場合、これはカメラの位置になります。
  • 方向: これは、マウス座標 (ワールド空間) を指す光線です。
  • ボックスの最小コーナー。
  • 最大角。自明。
  • 結果ベクトル。これには、交点の近距離と遠距離が含まれます。
    交差点がある場合、メソッドは true を返します。true の場合、終了距離を確認し、必要に応じて更新し、選択した候補の参照を保存します。

明らかに、ここで紹介する方法は最適とは言えませんが、より洗練された方法を独自に開発するための基礎を学ぶことができます。カメラの背後にあるオブジェクトなど、シーンの一部は交差しないため、簡単に破棄できます。それに加えて、計算を高速化するために、カメラまでの距離に従ってアイテムを並べ替えることができます。

Mainこのテクニックを説明するために、回転する 2 つの立方体を表示するようにクラスを変更します。

public class Main implements IAppLogic {
    ...
    private Entity cubeEntity1;
    private Entity cubeEntity2;
    ...
    private float rotation;

    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-18", opts, main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        ...
        Model cubeModel = ModelLoader.loadModel("cube-model", "resources/models/cube/cube.obj",
                scene.getTextureCache(), false);
        scene.addModel(cubeModel);
        cubeEntity1 = new Entity("cube-entity-1", cubeModel.getId());
        cubeEntity1.setPosition(0, 2, -1);
        scene.addEntity(cubeEntity1);

        cubeEntity2 = new Entity("cube-entity-2", cubeModel.getId());
        cubeEntity2.setPosition(-2, 2, -1);
        scene.addEntity(cubeEntity2);
        ...
    }
    ...
    public void update(Window window, Scene scene, long diffTimeMillis) {
        rotation += 1.5;
        if (rotation > 360) {
            rotation = 0;
        }
        cubeEntity1.setRotation(1, 1, 1, (float) Math.toRadians(rotation));
        cubeEntity1.updateModelMatrix();

        cubeEntity2.setRotation(1, 1, 1, (float) Math.toRadians(360 - rotation));
        cubeEntity2.updateModelMatrix();
    }
}

マウスでなめたときに立方体がどのように青くレンダリングされるかを確認できます。

Java 3D LWJGL GitBook 〜Chapter09:より複雑なモデルのロード〜

より複雑なモデルのロード

無料で配布している3Dモデルなど6章(Chapter06)で学習した3Dモデルのロード方法とは別の方法でロードする必要があります。

参照するドキュメントとプログラムソースへのリンクは以下になります。

ゲームを作成するには、複雑な 3D モデルをさまざまな形式で読み込む機能が不可欠です。それらの一部のパーサーを作成するには、多くの作業が必要になります。1 つのフォーマットをサポートするだけでも、時間がかかる場合があります。幸いなことに、Assimpライブラリは、多くの一般的な 3D 形式を解析するために既に使用できます。これは、静的およびアニメーション モデルをさまざまな形式でロードできる C/C++ ライブラリです。LWJGL は、Java コードからそれらを使用するためのバインディングを提供します。この章では、その使用方法について説明します。

プログラムを実行したときに出たエラーメッセージ

[LWJGL] [ThreadLocalUtil] Unsupported JNI version detected, this may result in a crash.

これは、最新のJDKを使用しないようにすることで解消しました。※詳細はこちら

クラス図

概要

Assimpライブラリを使用して、今までよりも複雑な、具体的には、3dモデルファイルを読み込む形で3Dモデルを表示できるようにプログラムを改造するというところです。
この章では、以下のような手順で説明しています。

  1. モデルローダークラスの作成
  2. Materialクラス、SceneRenderクラスの修正
  3. Renderクラスを周世押して、顔カリングを有効にする

LWJGLのJavaDocを見て、モデルローダーの処理内容を理解する

モデルローダー

3Dモデルを読み込むためのクラスを作成します。

最初に、プロジェクト pom.xml に Assimp maven の依存関係を追加します。コンパイル時と実行時の依存関係を追加する必要があります。

<dependency>
    <groupId>org.lwjgl</groupId>
    <artifactId>lwjgl-assimp</artifactId>
    <version>${lwjgl.version}</version>
</dependency>
<dependency>
    <groupId>org.lwjgl</groupId>
    <artifactId>lwjgl-assimp</artifactId>
    <version>${lwjgl.version}</version>
    <classifier>${native.target}</classifier>
    <scope>runtime</scope>
</dependency>

ModelLoader#loadModel()

ModelLoader依存関係が設定されたら、 Assimp でモデルをロードするために使用されるという名前の新しいクラスを作成します。このクラスは、次の 2 つの静的パブリック メソッドを定義します。

package org.lwjglb.engine.scene;

import org.joml.Vector4f;
import org.lwjgl.PointerBuffer;
import org.lwjgl.assimp.*;
import org.lwjgl.system.MemoryStack;
import org.lwjglb.engine.graph.*;

import java.io.File;
import java.nio.IntBuffer;
import java.util.*;

import static org.lwjgl.assimp.Assimp.*;

public class ModelLoader {

    private ModelLoader() {
        // Utility class
    }

    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache) {
        return loadModel(modelId, modelPath, textureCache, aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices |
                aiProcess_Triangulate | aiProcess_FixInfacingNormals | aiProcess_CalcTangentSpace | aiProcess_LimitBoneWeights |
                aiProcess_PreTransformVertices);

    }

    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, int flags) {
        ...

    }
    ...
}

どちらのメソッドにも次の引数があります。

変数名 説明
modelId ロードするモデルの一意の識別子。
modelPath モデル ファイルが配置されているファイルへのパス。
textureCache 同じテクスチャを複数回ロードすることを避けるためのテクスチャ キャッシュへの参照。

【modelPathの補足】

これは通常のファイル パスであり、CLASSPATH の相対パスではありません。Assimp は追加のファイルをロードする必要があり、同じベース パスを使用する可能性があるためですmodelPath(たとえば、wavefront のマテリアル ファイル、OBJ、ファイル)。リソースを JAR ファイル内に埋め込むと、Assimp はそれをインポートできないため、ファイル システム パスである必要があります。テクスチャをロードするとき、テクスチャをロードするためにモデルが配置されているベース ディレクトリを取得するために使用modelPathします (モデルで定義されているパスをオーバーライドします)。一部のモデルには、明らかにアクセスできない、モデルが開発された場所のローカル フォルダーへの絶対パスが含まれているため、これを行います。

【loadModel()オーバーロードしている方】
public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, int flags) {

2 番目のメソッドには、 という名前の追加の引数がありflagsます。このパラメータを使用すると、ロード プロセスを調整できます。最初のメソッドは 2 番目のメソッドを呼び出し、ほとんどの状況で役立ついくつかの値を渡します。

変数名 説明
aiProcess_JoinIdenticalVertices このフラグは、使用される頂点の数を減らし、面間で再利用できる頂点を識別します。
aiProcess_Triangulate モデルは、クワッドまたはその他のジオメトリを使用して要素を定義する場合があります。三角形のみを扱っているため、このフラグを使用して、彼の面をすべて三角形に分割する必要があります (必要な場合)。
aiProcess_FixInfacingNormals このフラグは、内側を向いている法線を反転させようとします。
aiProcess_CalcTangentSpace ライトを実装するときにこのパラメーターを使用しますが、基本的には法線情報を使用してタンジェントとバイタンジェントを計算します。
aiProcess_LimitBoneWeights アニメーションを実装するときにこのパラメーターを使用しますが、基本的には 1 つの頂点に影響を与えるウェイトの数を制限します。
aiProcess_PreTransformVertices このフラグは、ロードされたデータに対して何らかの変換を実行するため、モデルは原点に配置され、座標は数学 OpenGL 座標系に修正されます。回転したモデルに問題がある場合は、必ずこのフラグを使用してください。重要: モデルがアニメーションを使用している場合は、このフラグを使用しないでください。このフラグはその情報を削除します。

使用できるフラグは他にもたくさんあります。LWJGL または Assimp のドキュメントで確認できます。

ModelLoader#コンストラクタ

2 番目のコンストラクターに戻りましょう。最初に行うことは、メソッドを呼び出してaiImportFile、選択したフラグでモデルをロードすることです。

public class ModelLoader {
    ...
    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, int flags) {
        File file = new File(modelPath);
        if (!file.exists()) {
            throw new RuntimeException("Model path does not exist [" + modelPath + "]");
        }
        String modelDir = file.getParent();

        AIScene aiScene = aiImportFile(modelPath, flags);
        if (aiScene == null) {
            throw new RuntimeException("Error loading model [modelPath: " + modelPath + "]");
        }
        ...
    }
    ...
}

コンストラクターの残りのコードは次のとおりです。

public class ModelLoader {
    ...
    public static Model loadModel(String modelId, String modelPath, TextureCache textureCache, int flags) {
        ...
        int numMaterials = aiScene.mNumMaterials();
        List<Material> materialList = new ArrayList<>();
        for (int i = 0; i < numMaterials; i++) {
            AIMaterial aiMaterial = AIMaterial.create(aiScene.mMaterials().get(i));
            materialList.add(processMaterial(aiMaterial, modelDir, textureCache));
        }

        int numMeshes = aiScene.mNumMeshes();
        PointerBuffer aiMeshes = aiScene.mMeshes();
        Material defaultMaterial = new Material();
        for (int i = 0; i < numMeshes; i++) {
            AIMesh aiMesh = AIMesh.create(aiMeshes.get(i));
            Mesh mesh = processMesh(aiMesh);
            int materialIdx = aiMesh.mMaterialIndex();
            Material material;
            if (materialIdx >= 0 && materialIdx < materialList.size()) {
                material = materialList.get(materialIdx);
            } else {
                material = defaultMaterial;
            }
            material.getMeshList().add(mesh);
        }

        if (!defaultMaterial.getMeshList().isEmpty()) {
            materialList.add(defaultMaterial);
        }

        return new Model(modelId, materialList);
    }
    ...
}

<補足>
オブジェクト:早い話が、インスタンスのこと
モデル:メッシュとマテリアルを統合したオブジェクト
マテリアル:メッシュで使用される色とテクスチャを定義(表す)したオブジェクト
メッシュ:形を作る頂点、面を表すオブジェクト

マテリアルを処理

モデルに含まれるマテリアルを処理します。マテリアルは、モデルを構成するメッシュで使用される色とテクスチャを定義します。次に、さまざまなメッシュを処理します。モデルは複数のメッシュを定義でき、各メッシュはモデルに定義されたマテリアルの 1 つを使用できます。これが、レンダリング時にバインディング呼び出しを繰り返さないように、マテリアルとリンクの後にメッシュを処理する理由です。

上記のコードを調べると、Assimp ライブラリへの呼び出しの多くがPointerBufferインスタンスを返すことがわかります。それらは C ポインターのように考えることができます。データを含むメモリー領域を指すだけです。それらを処理するには、それらが保持するデータのタイプを事前に知る必要があります。マテリアルの場合、そのバッファを繰り返し処理して、AIMaterialクラスのインスタンスを作成します。AIMesh2 番目のケースでは、クラスのインスタンスを作成するメッシュ データを保持するバッファーを反復処理します。

processMaterialその方法を調べてみましょう。

public class ModelLoader {
    ...
    private static Material processMaterial(AIMaterial aiMaterial, String modelDir, TextureCache textureCache) {
        Material material = new Material();
        try (MemoryStack stack = MemoryStack.stackPush()) {
            AIColor4D color = AIColor4D.create();

            int result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_DIFFUSE, aiTextureType_NONE, 0,
                    color);
            if (result == aiReturn_SUCCESS) {
                material.setDiffuseColor(new Vector4f(color.r(), color.g(), color.b(), color.a()));
            }

            AIString aiTexturePath = AIString.calloc(stack);
            aiGetMaterialTexture(aiMaterial, aiTextureType_DIFFUSE, 0, aiTexturePath, (IntBuffer) null,
                    null, null, null, null, null);
            String texturePath = aiTexturePath.dataString();
            if (texturePath != null && texturePath.length() > 0) {
                material.setTexturePath(modelDir + File.separator + new File(texturePath).getName());
                textureCache.createTexture(material.getTexturePath());
                material.setDiffuseColor(Material.DEFAULT_COLOR);
            }

            return material;
        }
    }
    ...
}

最初にマテリアル カラーを取得します。この場合は、(AI_MATKEY_COLOR_DIFFUSEフラグを設定して) ディフューズ カラーを取得します。ライトを適用するときに使用する色にはさまざまな種類があります。たとえば、ディフューズ、アンビエント (アンビエント ライト用)、スペキュラー (ライトのスペキュラー ファクター用) などがあります。その後、マテリアルがテクスチャまたはテクスチャを定義しているかどうかを確認します。そうでない場合、つまりテクスチャ パスが存在する場合、テクスチャ パスを保存し、テクスチャの作成をTexturCache前の例のようにクラス。この場合、マテリアルがテクスチャを定義する場合、拡散色をデフォルト値の黒に設定します。これにより、テクスチャの有無をチェックせずに、ディフューズ カラーとテクスチャの両方の値を使用できるようになります。モデルでテクスチャが定義されていない場合は、マテリアル カラーと組み合わせることができるデフォルトの黒のテクスチャを使用します。

processMeshメソッドはこのように定義されています。

public class ModelLoader {
    ...
    private static Mesh processMesh(AIMesh aiMesh) {
        float[] vertices = processVertices(aiMesh);
        float[] textCoords = processTextCoords(aiMesh);
        int[] indices = processIndices(aiMesh);

        // Texture coordinates may not have been populated. We need at least the empty slots
        if (textCoords.length == 0) {
            int numElements = (vertices.length / 3) * 2;
            textCoords = new float[numElements];
        }

        return new Mesh(vertices, textCoords, indices);
    }
    ...
}

Meshの処理

AMeshは、頂点の位置、テクスチャ座標、およびインデックスのセットによって定義されます。これらの各要素はprocessVertices、 processTextCoordsおよびprocessIndicesメソッドで処理されます。すべてのデータを処理した後、テクスチャ座標が定義されているかどうかを確認します。そうでない場合は、テクスチャ座標のセットを 0.0f に割り当てて、VAO の一貫性を確保します。

processXXXメソッドは非常に単純です。目的のデータを返すインスタンスに対して対応するメソッドを呼び出し、それAIMeshを配列に格納するだけです。

public class ModelLoader {
    ...
    private static int[] processIndices(AIMesh aiMesh) {
        List<Integer> indices = new ArrayList<>();
        int numFaces = aiMesh.mNumFaces();
        AIFace.Buffer aiFaces = aiMesh.mFaces();
        for (int i = 0; i < numFaces; i++) {
            AIFace aiFace = aiFaces.get(i);
            IntBuffer buffer = aiFace.mIndices();
            while (buffer.remaining() > 0) {
                indices.add(buffer.get());
            }
        }
        return indices.stream().mapToInt(Integer::intValue).toArray();
    }
    ...
    private static float[] processTextCoords(AIMesh aiMesh) {
        AIVector3D.Buffer buffer = aiMesh.mTextureCoords(0);
        if (buffer == null) {
            return new float[]{};
        }
        float[] data = new float[buffer.remaining() * 2];
        int pos = 0;
        while (buffer.remaining() > 0) {
            AIVector3D textCoord = buffer.get();
            data[pos++] = textCoord.x();
            data[pos++] = 1 - textCoord.y();
        }
        return data;
    }

    private static float[] processVertices(AIMesh aiMesh) {
        AIVector3D.Buffer buffer = aiMesh.mVertices();
        float[] data = new float[buffer.remaining() * 3];
        int pos = 0;
        while (buffer.remaining() > 0) {
            AIVector3D textCoord = buffer.get();
            data[pos++] = textCoord.x();
            data[pos++] = textCoord.y();
            data[pos++] = textCoord.z();
        }
        return data;
    }
}

メソッドを呼び出すと、頂点へのバッファーが取得されることがわかりますmVertices。それらを単純に処理してList、頂点の位置を含む浮動小数点数を作成します。このメソッドはバッファを返すだけなので、その情報を頂点を作成する OpenGL メソッドに直接渡すことができます。2 つの理由から、そのようにはしません。1 つ目は、コード ベースの変更を可能な限り減らすことです。2 つ目は、中間構造にロードすることで、プロ処理タスクを実行したり、ロード プロセスをデバッグしたりできる可能性があることです。

はるかに効率的な方法、つまりバッファーを OpenGL に直接渡す方法のサンプルが必要な場合は、このサンプルを確認してください。

モデルの使用

Materialクラスの修正

クラスを変更してMaterial、拡散色のサポートを追加する必要があります。

public class Material {

    public static final Vector4f DEFAULT_COLOR = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);

    private Vector4f diffuseColor;
    ...
    public Material() {
        diffuseColor = DEFAULT_COLOR;
        ...
    }
    ...
    public Vector4f getDiffuseColor() {
        return diffuseColor;
    }
    ...
    public void setDiffuseColor(Vector4f diffuseColor) {
        this.diffuseColor = diffuseColor;
    }
    ...
}

SceneRenderクラスの修正

このSceneRenderクラスでは、マテリアルの拡散色を作成し、レンダリング中に適切に設定する必要があります。

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("material.diffuse");
    }

    public void render(Scene scene) {
        ...
        for (Model model : models) {
            List<Entity> entities = model.getEntitiesList();

            for (Material material : model.getMaterialList()) {
                uniformsMap.setUniform("material.diffuse", material.getDiffuseColor());
                ...
            }
        }
        ...
    }
    ...
}

scene.flagの修正

ご覧のとおり、ユニフォームの名前に が含まれる変な名前を使用してい.ます。これは、シェーダーで構造を使用するためです。構造を使用すると、複数の型を 1 つの結合型にグループ化できます。これはフラグメント シェーダーで確認できます。

#version 330

in vec2 outTextCoord;

out vec4 fragColor;

struct Material
{
    vec4 diffuse;
};

uniform sampler2D txtSampler;
uniform Material material;

void main()
{
    fragColor = texture(txtSampler, outTextCoord) + material.diffuse;
}

UniformsMapの修正

また、値UniformsMapを渡すためのサポートを追加するために、クラスに新しいメソッドを追加する必要がありますVector4f

public class UniformsMap {
    ...
    public void setUniform(String uniformName, Vector4f value) {
        glUniform4f(getUniformLocation(uniformName), value.x, value.y, value.z, value.w);
    }
}

Mainクラスの修正

最後に、Mainクラスを使用してModelLoaderモデルをロードするようにクラスを変更する必要があります。

public class Main implements IAppLogic {
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-09", new Window.WindowOptions(), main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        Model cubeModel = ModelLoader.loadModel("cube-model", "resources/models/cube/cube.obj",
                scene.getTextureCache());
        scene.addModel(cubeModel);

        cubeEntity = new Entity("cube-entity", cubeModel.getId());
        cubeEntity.setPosition(0, 0, -2);
        scene.addEntity(cubeEntity);
    }
    ...
}

ご覧のとおり、initメソッドは大幅に簡素化され、コードにモデル データが埋め込まれなくなりました。現在、波面フォーマットを使用する立方体モデルを使用しています。フォルダー内にモデル ファイルがありresources\models\cubeます。そこには、次のファイルがあります。

? cube.obj: メイン モデル ファイル。実際にはテキストベースのフォーマットであるため、それを開いて、頂点、インデックス、テクスチャ座標がどのように定義され、面を定義することで結合されているかを確認できます。また、マテリアル ファイルへの参照も含まれています。

cube.mtl: マテリアル ファイル。色とテクスチャを定義します。

cube.png: モデルのテクスチャ ファイル。

最後に、レンダリングを最適化する別の機能を追加します。顔カリングを適用することで、レンダリングされるデータの量を減らします。よく知られているように、立方体は 6 つの面で構成されており、6 つの面が表示されていません。立方体の内部をズームすると、これを確認できます。内部が表示されます。

見えない顔はすぐに破棄する必要があります。これが顔カリングの機能です。実際、立方体の場合、同時に 3 つの面しか見ることができないため、面のカリングを適用するだけで面の半分を破棄できます (これは、ゲームで内側に飛び込む必要がない場合にのみ有効です)。モデルの)。

すべての三角形について、フェース カリングは、それが私たちの方を向いているかどうかをチェックし、その方向を向いていないものを破棄します。しかし、三角形が自分の方を向いているかどうかはどうすればわかりますか? OpenGL がこれを行う方法は、三角形を構成する頂点の巻き順によるものです。

最初の章で、三角形の頂点を時計回りまたは反時計回りの順序で定義できることを思い出してください。OpenGL では、デフォルトで、反時計回りの三角形はビューアーの方を向き、時計回りの三角形は後ろ向きです。ここで重要なことは、視点を考慮してレンダリング中にこの順序がチェックされることです。したがって、反時計回りの順序で定義された三角形は、レンダリング時に、視点のために時計回りに定義されていると解釈できます。

Renderクラスの修正

Renderクラスで顔カリングを有効にします。

public class Render {
    ...
    public Render() {
        ...
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        ...
    }
    ...
}

1 行目は面のカリングを有効にし、2 行目は後ろ向きの面をカリング (削除) する必要があることを示しています。

サンプルを実行すると、前の章と同じ結果が表示されますが、立方体を拡大すると、内側の面はレンダリングされません。このサンプルを変更して、より複雑なモデルを読み込むことができます。

IntelliJ 使い方 ~JARファイルを出力する~

JARファイルを出力する

Javaでプログラムを作成していると、最終的に配布できるようにしたくなるものです。
まぁ、どのプログラムでも同じでしょうが。。。
C/C++などでは「*.exe」ファイルを出力します。このようなダブルクリックで起動できるファイルのことを
実行可能ファイル(Wiki参照)と呼びます。

でわ、Javaの場合はどうでしょうか?主にJARファイル、WARファイル、EARファイルなどがあります。
WARとかEARファイルは、ウェブアプリケーションとして、デプロイ(Tomcatなどのサーバー上に配置)して起動します。

じゃ、JARファイルは?

JARファイルは、どちらかというとローカル(自分のPC)上で動かすための起動ファイルです。
Windowsであれば、「java -jar ファイル名(パス付き)」コマンドで実行することができます。

外国のものばかりのサイトですが、このようなアプリがあるようです。

JAR出力

IntelliJでJARの出力をするときは以下のような手順でやるようです。

  1. File -> ProjectStructureを選択
  2. Airtifactsを選択
  3. 「+」ボタンを押下、JAR -> From module with dependenciesを選択
  4. 起動するメインメソッドのあるクラスを指定
  5. 下のように、出力するファイルを一覧できるので、確認、OKボタンを押下
  6. 上部にある「Build」 -> 「Build Artifact」を選択

指定のフォルダにJARファイルが出力されます。

出力したJARを動かす

下のように動画にしました。

コマンド実行

JARファイルをコマンドで動かすのにJavaプログラミングの記事ではあまり見かけませんが、プログラム引数を使用したいと考えています。

よくある使用方法。

  1. 起動するときにオプションを付けて実行する
  2. 実行するときの設定ファイルを変更する

考えればいくらでも出てくるのでこのくらいにしますが、今回は2の「実行するときの設定ファイルを変更する」を実装することを考えたいと思います。

ビルドパス
ファイルやクラスを参照するために設定するのが、ビルドパスです。大体はプロジェクトのルートにせってしてあります。
なので、ファイルを参照するときはプロジェクト直下から記述します。
下の図は、作成しているプロジェクトのフォルダです。

ちなみに、IntelliJ IDEAで開くと下のような画面です。

ここからJARファイルを参照したければ「ObjectOrientedPrograming.jar」と書いてやれば参照できます。
具体的には、下のようなコードです。

File jarFile = Files.newBufferedReader(Paths.get("ObjectOrientedPrograming.jar"));

では、上のプロジェクトのようなフォルダ構成の場合、resourcesフォルダ内の「SampleRpg_story.txt」を参照したいとしましょう。この場合は、「resources/SampleRpg_story.txt」を参照すればよいです。
プログラムで書くと下のようになります。

File jarFile = Files.newBufferedReader(Paths.get("resources", "SampleRpg_story.txt"));

この場合は、フォルダを第一引数("resources")フォルダを指定します。同様にSample_story.txtを参照します。

ちょっとわかりずらいので、下の図に示します。

これを参照するときは、「src/main/resources/Sample_story.txt」とパスを指定してやればOKです。
プログラムで書くと下のようになります。

File jarFile = Files.newBufferedReader(Paths.get("src/main/resources", "SampleRpg_story.txt"));

初めの問題

JARファイルで起動したときに、パスの指定がどのようになるのか?というところを
明確にしたいというところです。

ためにしにJARファイルを作成して実行してみたところ。。。
JARファイルのある場所から普通にファイルの参照ができました。つまりJARファイルのある場所からパスを指定してやれば、対象のファイルを参照できるということがわかりました。

具体的には、下のようなコードです。

    public static void main(String[] args) {
        System.out.println("Param: " + args[0]);
        BufferedReader buf = null;
        try {
            if (args.length != 0) {
                buf = Files.newBufferedReader(Paths.get(args[0]));
            }
            RpgLogic gameLogic = new TextRpgLogic();
            TextRpgGameEngine engine  = new TextRpgGameEngine(gameLogic);

            engine.start();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("想定外のエラーで終了します。:" + e.getMessage());
            System.exit(-1);
        }
    }

実行結果はこちらです。

UMLの書き方読み方~シーケンス図~

シーケンス図の書き方

IT専科というサイトを参考にするとシーケンス図とは。。。

シーケンス図とは、クラスやオブジェクト間のやりとりを時間軸に沿って表現する図です。

アジャイルモデリング(AM)というサイトを参考にして学習します。そして、下のような図があります。

シーケンス図1

これは、手書きなのでちょっとわかりずらいですが、ポイントとしては、次のような部分です。

  1. 登場人物が3人(アプリケーションを含む)

  2. それぞれの時間軸が縦に伸びている

  3. 縦軸に対して、横軸は「何かしらの動きを示す」

大まかに、このようなところがポイントになります。
アジャイルモデリング(AM)というサイトには、下のような説明がありました。

これがシーケンス図と呼ばれている理由は明らかでしょう。ロジックの実行順序(シーケンス)がメッセージ(横向きの矢印)の順序で示されています。最初のメッセージは左上から始まり、次のメッセージはそのすぐ下に書く、というように表していきます。

シーケンス図の内訳

サービスレベルのシーケンス図

参考サイトには、下のような説明があります。

図の上部に横に並んでいる箱は、分類子またはそのインスタンスを表します。この分類子は通常、ユースケース、オブジェクト、クラス、またはアクターです。オブジェクトとクラスにはメッセージを送ることができるため(オブジェクトは操作の呼び出しを通じて、クラスは静的操作の呼び出しを通じてメッセージに応答します)、これらをシーケンス図に含めるのは筋が通っています。アクターも、利用シナリオを開始したり、利用シナリオで能動的な役割を果たすので、シーケンス図に含めることができます。オブジェクトにはUML標準の「名前: クラス名」という書式でラベルをつけます。この名前は必須ではありません(図で名前の付いていないオブジェクトのことを無名オブジェクトと呼びます)。クラスには「クラス名」という書式でラベルを付け、アクターには「アクター名」の書式で名前を付けます。オブジェクトのラベルには下線が引かれていますが、クラスとアクターには引かれていないことに注意してください。たとえば、図3の学生オブジェクトにはある学生という名前が付けられています。これが名前付きオブジェクトです。それに対してゼミのインスタンスは無名オブジェクトです。Studentのインスタンスに名前が付けられているのは、複数の場所でメッセージのパラメータとして使われているためです。ゼミのインスタンスの方は、図の他の場所で参照する必要がないので、無名にしておくことができます。図2では、学生クラスが永続性フレームワーククラスにメッセージを送っています(永続性フレームワーククラスには\<\>というステレオタイプを付けてもよかったのですが、図を簡潔にしておくために付けませんでした)。クラスに対して送られたメッセージは、すべて静的メソッドとして実装します。これについては後で説明します。

まとめると次のようになります。
上の図で示した「登場人物」は、「ユースケース、オブジェクト、クラス、またはアクター」

  • ユースケース:「申込用紙を書く」とか、「送信ボタンを押下」などのように人の動き、作業を示す。
  • オブジェクト:クラスとかインスタンスのこと(厳密にはインスタンスの事)
  • アクター  ;人のモデル(人を示す絵)

そして、オブジェクトはメッセージを送信することができるので、ほかのオブジェクトを呼び出し、何かしらの処理を行わせることができます。その処理が終わったら、また元のオブジェクトの線に戻ってきます。下のような矢印のことです。

※引用した文言の中に「すべて静的メソッドとして実装します」とありますが、これはこちらのサイトでそのように実装しているということです。別に静的メソッドである必要はありません

そして、上記の手書きの画像をきれいに書くと下のようになるようです。

ここで、シーケンス図に使用される図をまとめると、IT専科というサイトの表を借りると下のようになります。

構成要素一覧
要素 表示形式 意味
ライフライン(Lifeline) ライフライン 記号 使用するオブジェクトやクラスを表現します。どちらか一方なら省略可能です。
実行仕様(ExecutionSpecification) 実行仕様 記号 生成されているライフラインが実行状態であることを意味します。
停止(Stop) 停止 記号 生成されたライフライン自体の消滅を意味します。
メッセージ(Message) 同期(Synchronous)メッセージ 同期メッセージ 記号 送り先のライフラインの実行に同期されるメッセージを意味します。メッセージ名には具体的な関数やINCLUDEディレクティブ等を記入します。
非同期(Asynchronous)メッセージ 非同期メッセージ 記号 送り先のライフラインの実行に同期されないメッセージを意味します。メッセージ名には具体的な関数やINCLUDEディレクティブ等を記入します。
応答(Reply)メッセージ 応答メッセージ 記号 送り先のライフラインから送り手への戻り値を意味します。メッセージ名には戻り値を格納する具体的な変数名等を記入します。
ファウンド(Found)メッセージ ファウンドメッセージ 記号 図解上にない送り手から送られた、もしくは送り手がダイアグラム上にないことを意味します。
ロスト(Lost)メッセージ ロストメッセージ 記号 意図された受け手に送られていない、もしくは受け手がダイアグラム上にないことを意味します。

▲PageTop

制御構造の記述

シーケンス図では、制御構造を表現するために「複合フラグメント」を使用します。種類および、記述例は次の通りです。

複合フラグメントの種類

複合フラグメントには、次の種類があります。

複合フラグメント一覧
InteractionOperator 読み 意味
ref 相互作用使用(InteractionUse) 別のシーケンス図を参照することを表します。
alt オルタナティブ(Alternative) 分岐処理を表します。
opt オプション(Option)

条件を満たした場合のみ実行される処理を表します。

par パラレル(Parallel) 並列処理を表します。
loop ループ(Loop) ループ(繰り返し)処理を表します。
break ブレイク(Break) 処理の中断を表します。
critical クリティカル(Critical) マルチスレッド環境での同期処理など、排他制御を表します。
assert アサーション(Assert) 処理が妥当であるための定義を表します。
neg 否定(negation)

本来、実行されるはずがない処理(メッセージ)であることを表します。

ignore 無効(ignore) あまり重要な処理(メッセージ)ではないことを表します。
consider 有効(Consider) 重要な処理(メッセージ)であることを表します。

基本的な処理を表現する

ここでいう「基本的な処理」とは、次のものを指します。

  1. 参照(REF)
  2. 条件分岐(ALT)
  3. 条件判断(OPT)
  4. 並列処理(PAR)
  5. 反復処理(LOOP
  6. 中断(BREAK
  7. クリティカルセッション(CRITICAL
  8. アサート(ASSERT
  9. 不正なシーケンス(NEG
  10. 無効(IGNORE
  11. 有効(CONSIDER

このような形で記述します。あとは、どのような動きを表現したいのか?を考えるだけです。
しかし、これらの「動き」を考えるためには、プログラミングの基礎を理解する必要があります。
※よかったら参考にどうぞ、Java Basic学習フロー

具体的には、「じゃんけんゲーム」を作成しようと考えたときには、どのような画面で、ユーザーの入力はどのように行うのか?などの「人間レベル」の動きから、「入力値からどのような処理をして勝敗の判定を行うか?」という「プログラムレベル」の動きを考える必要があるためです。

こんなところで失礼します。

でわでわ。。。

FreeTts エラー ~mbrola.base

FreeTtsエラー

下のような警告があり、修正してみました。

System property "mbrola.base" is undefined. Will not use MBROLA voices.

インストールしたFreeTtsのディレクトリに「mbrola」というフォルダがあったのでシステムプロパティにそのパスを渡して実行すると下のようにエラーが出ました。

System.setProperty("mbrola.base", "D:\\Apps\\freetts-1.2\\mbrola");

Make sure you FULLY specify the path to
the MBROLA directory using the mbrola.base
system property.

調べてみると、JDKのあるフォルダlibの下にmbrola.jarをコピーしてやればOKということでした。

しかし、MBROLAの音声ファイル(Voice)がダウンロードできなかったので、(502 Bad Gateway)打つ手なしと判断しました。。。

IntelliJ IDEA Maven リポジトリからロードできない

Mavenでソースをロードできない

IntelliJ IDEAを使用してpom.xmlにMavenリポジトリからソースをロードしようとすると下のような文言が出てロードできない事象にあいました。

依存関係 'com.ibm.icu:icu4j:2.9.1' が見つかりません

これは、間違っているので、エラーになっているのですが。正しくは、下のような形でpom.xmlを書きます。

<dependencies>
    <dependency>
        <groupId>com.ibm.icu</groupId>
        <artifactId>icu4j</artifactId>
        <version>2.6.1</version>
    </dependency>
</dependencies>

これでロードできるはずなのですが、出来ない。。。
こちらのページを参考にすると、「Mavenの更新ができていないから」ということでした。
下のような操作を行います。

  1. プロジェクトを右クリック
  2. Mavenを選択
  3. プロジェクトの再ロード

これで、ソース(JARなど)をロードすることができます。

でわでわ。。。

Java Speach APIを学ぶ(遊ぶ)

Java Speach API(JSAPI)

セットアップ

こちらのサイトを参考にインストールしました。
結局はライブラリをインストールする形になりました。

  1. ライブラリをSourceForgeからダウンロードします。

  2. これを展開して中にあるjsapi.exeを実行する

    この例では、D:\ apps \フォルダーが使用されます
    D:\apps\freetts-1.2.1\lib jsapi.exeに移動し て実行します。

  3. これによりjsapi.exeが作成されるようですが、初めからありました。

  4. jsapi.exeのあるディレクトリ(フォルダ)をライブラリとして指定します。

    1. 上部メニューのファイルを選択
    2. プロジェクトの構造を選択
    3. ライブラリの作成
    4. プロジェクトに追加されていることを確認
    5. プロパティファイルをJDK/jre/libに配置します。※ D:\Apps\jdk1.8.0_265\jre\lib

      D:\apps\freetts-1.2.1\speech.properties ファイルを %user.home% または %java.home%/lib フォルダにコピーし ます。このファイルは、JSAPIが使用する音声エンジンを決定するために使用されます。
      具体的には、

実行

参考サイトに載っているコード(java)を三つコピーして作成しました。

  • BriefVoiceDemo.java
  • BriefSpeakable.java
  • BriefListener.java

日本語をしゃべらせる

調べてみると「mbrola」が日本語に対応する声を持っているようです。具体的にはfreettsのフォルダ内にある「mdrola」のことです。
とりあえずは、実行するためのコードを見てみるとMBROLAを使用しているようなので、ロケールをJAPANESEに変更し、動かしてみると...

「Locale.US」を「Locale.JAPANESE」に変更してあります。

//default synthesizer values
SynthesizerModeDesc modeDesc = new SynthesizerModeDesc(
        null,       // engine name
        "general",  // mode name use 'general' or 'time'
        Locale.JAPANESE,  // locale, see MBROLA Project for i18n examples
        null,       // prefer a running synthesizer (Boolean)
        null);      // preload these voices (Voice[])

下のようなエラーが出ました。

System property "mbrola.base" is undefined. Will not use MBROLA voices.
Unable to create synthesizer with the required properties

Be sure to check that the "speech.properties" file is in one of these locations:

mbrolaの設定が良くないようです。なのでMBROLAを調べることにします。

調べていくと次のページを見つけました。ここに細かいところの記載があるので、これを参考にしてみます。

しかし、必要なファイルなどがダウンロードできません。。。。
結局はGithubにありました。

これをダウンロード(ZIP)して見ましたが、これもリンク切れが多く、調査が進みませんでした。。。。

しかし、英語を日本語調で発音させることはできるようです。

やはり、人工知能処理を入れないとできないようです。。。

ここであきらめない!

しかし、既製品のものがあります。FreeTtsも日本語での発音は実現しています。
詳細に関しては、商品化しているであろうため公開されていないと思われます。

やはり、下のような手順で行うのが無難なのかもしれません。

  1. 入力した文字をすべてひらがなに変換
  2. ひらがなをそれっぽい発音をする単語に関連図ける(Mapを使用する)
  3. 各単語をすべてアルファベットに変換
  4. 再生する

こんな方法しか見つかりませんでした。もちろん、TTSサービスを使用できるサイトなどたくさんあります。
しかし、Javaで自力で音声を再生したかったのです。。。

粘ってみた

とりあえずのところ、FreeTtsでは日本語をスピーチさせることが実現できない状態ですが、他にMaryTtsというライブラリ?がありました。
ソースをコンパイルして、起動すればサーバーとして起動できるようです。ここら辺に解決の糸口を見つけたいと思います。

MaryTts

MaryTtsをpom.xmlに追加して、MavenでJARを追加しました。
そして、ここにMaryTtsの新言語の追加方法が書いてありました。
GithubのWikiページですね。
ここを読み進めてみます。

しかし、色々と躓き断念することにしました。。。

FreeTtsで頑張る

結局のところ上に記載した方法で実装することにしました。

  1. 入力した文字をすべてひらがなに変換
  2. ひらがなをそれっぽい発音をする単語に関連図ける(Mapを使用する)
  3. 各単語をすべてアルファベットに変換
  4. 再生する

そして、必要になる(あったら無難な)ライブラリを使用することにします。

  • ICU4J:漢字ひらがな変換ライブラリ:

漢字ひらがな変換ライブラリ

pom.xmlの設定では、リポジトリの指定と、依存関係の指定で追加できました。

<repositories>
    <repository>
        <id>icu4j</id>
        <url>https://repo1.maven.org/maven2/</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.ibm.icu</groupId>
        <artifactId>icu4j</artifactId>
        <version>2.6.1</version>
    </dependency>
</dependencies>

ICU4Jに関しては、こちらの記事を参照ください。

なんだかんだと実装した結果「本日は晴天なり」としゃべることに成功しました。

実装する(テストクラス)

まずは、音声に変換するための文字列をマッピングします。なので、初めのBriefVoiceDemoクラスで書く発音の文字列を決めます。
次のような感じで発音できました。

これらの文字列と各カタカナを関連付けていきます。要領としては以下の通りです。

private static final String[] KANA_LIST = {"ア", "イ", "ウ", "エ", "オ", // 1
                                            "カ", "キ", "ク", "ケ", "コ", // 2
                                            "ガ", "ギ", "グ", "ゲ", "ゴ", // 3
                                            "サ", "シ", "ス", "セ", "ソ", // 4
                                            "ザ", "ジ", "ズ", "ゼ", "ゾ", // 5
                                            "タ", "チ", "ツ", "テ", "ト", // 6
                                            "ダ", "ヂ", "ヅ", "デ", "ド", // 7
                                            "ナ", "ニ", "ヌ", "ネ", "ノ", // 8
                                            "ハ", "ヒ", "フ", "ヘ", "ホ", // 9
                                            "バ", "ビ", "ブ", "ベ", "ボ", // 10
                                            "マ", "ミ", "ム", "メ", "モ", // 11
                                            "ヤ", "ユ", "ヨ", // 12
                                            "ラ", "リ", "ル", "レ", "ロ", // 13
                                            "ワ", "ヲ", "ン", // 14
                                        };

String[] moji = {"ah", "yee" , "hu", "a", "oh" // 1
    , "kah", "kee", "ku", "ckea", "koh" // 2
    , "gaah", "gy", "goo", "gue", "goh" // 3
    , "saeh", "see", "su", "thea", "soh" // 4
    , "zaeh", "zee", "zoo", "zea", "zoh" // 5
    , "taeh", "tiee", "tsu", "te", "toh" // 6
    , "daeh", "dgee", "do", "de", "doh" // 7
    , "naeh", "niee", "nuh", "nea", "noh" // 8
    , "haeh", "hiee", "hu", "hea", "hoh" // 9
    , "baeh", "bee", "boo", "be", "boh" // 10
    , "maeh", "miee", "muh", "me", "moh" // 11
    , "yaeh", "yu", "yoh" // 12
    , "ra", "ri", "ru", "re", "roh" // 13
    , "wa", "oh", "um"}; // 14

// サイズ(長さ)はおなじなので
for (int i = 0; i < KANA_LIST.length; i++) {
    String key = KANA_LIST[i];
    String value = moji[i];
    talkMap.put(key, value);
}

これで入力文字を発音用の文字列に変換し再生します。作成したクラスは次の通りです。

public class BriefVoiceClsTest {
    private static BriefVoiceCls target;
    @BeforeClass
    public static void init() {
        target = new BriefVoiceCls();
    }

    @Test
    public void testTalkVoice() {
        target.execute("本日は晴天なり");
    }
}

<実行結果>

でわでわ。。。

でわでわ。。。

<IntelliJ IDEAを操作して時の動画リスト>

JavaCV エラー (-215:Assertion failed) !image.empty() in function ‘cv::imencode’ ]

OpenCVでエラー発生

下のようなエラーメッセージです。

Caused by: CvException [org.opencv.core.CvException: cv::Exception: OpenCV(4.4.0) C:\build\master_winpack-bindings-win64-vc14-static\opencv\modules\imgcodecs\src\loadsave.cpp:919: error: (-215:Assertion failed) !image.empty() in function 'cv::imencode'
]
at org.opencv.imgcodecs.Imgcodecs.imencode_1(Native Method)
at org.opencv.imgcodecs.Imgcodecs.imencode(Imgcodecs.java:378)
at zenryokuservice.opencv.fx.learn.LearnOpenCv.createBufferedImage(LearnOpenCv.java:137)
at zenryokuservice.opencv.fx.learn.LearnOpenCv.execute(LearnOpenCv.java:72)
at zenryokuservice.opencv.fx.controller.TestingCvController.clickExecute(TestingCvController.java:89)
... 59 more

実行したときのコード

        // 表示するイメージを取得
        URL url = getClass().getResource("/charactors/myFace.png");
        URL url_kanaB = getClass().getResource("/charactors/kanabo.png");
System.out.println(url.getPath());
System.out.println(url_kanaB.getPath());
        // 表示イメージを読み取る
        Mat charactor = Imgcodecs.imread(url.getPath());
        Mat gray = Imgcodecs.imread(url.getPath(), Imgcodecs.IMREAD_GRAYSCALE);
        Mat kanaImg = Imgcodecs.imread(url_kanaB.getPath());
        System.out.println(kanaImg);
        optImg = createBufferedImage(kanaImg, ".png");

<createBufferedImage()>

    private BufferedImage createBufferedImage(Mat img, String ext) throws IOException {
        MatOfByte matOfByte = new MatOfByte();
        Imgcodecs.imencode(ext, img, matOfByte); -> ここでエラー
         return ImageIO.read(new ByteArrayInputStream(matOfByte.toArray()));
    }

エラーになるのは「Imgcodecs.imencode(ext, img, matOfByte);」の部分です。

原因として考えられること

  1. imgを渡すのに、データが不正(画像が読み込めていないなど)
  2. ファイル拡張子が違う
  3. Matの出力結果から怪しいところをみる

    Mat [ -1-1CV_8UC1, isCont=false, isSubmat=false, nativeObj=0x1c407d00, dataAddr=0x0 ]

このうちの「dataAddr=0x0」というのがnull参照になっている?と疑問に思いました。

解決

結局のところは、BufferedImageを作るのに、Matクラスを使う必要がないので、したのようにImageIOを使用することにしました。

BufferedImage buf = ImageIO.read(url);

これで一応の解決をしました。