MathMLの使い方

イントロダクション

LWJGLの学習をしています。ここで数式が出てくるのですが、これがMathMLで記述されています。これを日本語に訳して理解しようとしているのですが、うまく表示されないので、これを学習しちゃんと表示できるようにしようと考えています。

参照しているページはこちらです。

MathMLとは

MDNのページを参照します。

Mathematical Markup Language (MathML) は、数学的表記を記述し、その構造と内容を表現するための XML マークアップ言語です。

このページには、MathML のパワフルな技術を用いた仕事を助けるための文書、サンプル、ツールへのリンクがあります。簡単な概要は、Mozilla Summit 2013 で行われた innovation fair 向けのスライド をご覧ください。

MathML要素リファレンス

MathML の動作についての理解を深めるためのサンプルと例。

MathMLの書き方

MathML を書くための提案とヒント、おすすめの MathML エディター、その出力をウェブコンテンツへ統合する方法。
<サンプルコード1>

<p>
    One over square root of two (inline style):
    <math>
      <mfrac>
        <mn>1</mn>
        <msqrt>
          <mn>2</mn>
        </msqrt>
      </mfrac>
    </math>
</p>

<出力結果>

One over square root of two (inline style):


1

4


<サンプルコード2>

 <p>
    One over square root of two (display style):
    <math display="block">
      <mfrac>
        <mn>1</mn>
        <msqrt>
          <mn>2</mn>
        </msqrt>
      </mfrac>
    </math>
</p>

<出力結果2>

One over square root of two (inline style):


1

4


注意

MathMLは、MarkDonw内で使うと崩れることがある。その原因がイマイチわからない。改行で間をあけてやるとうまく表示できるときもあるが。。。

Pi4J ラズパイでLチカをしてみる

イントロダクション

ラズパイでのJavaプログラミングを行うのに、BlueJが使えるというかOracleさんが言うにはラズパイ用に出ているようです。
こちらにその記事があります、そして、下のように記述がありました。

バージョン 3.14 から、BlueJ は、学校での基本的なコンピューター プログラミングの教育を促進することを目的とした、クレジット カード サイズのシングル ボード コンピューターであるRaspberry Piを完全にサポートします。BlueJ は、Pi 上での開発とプログラムの実行を可能にする Java 開発環境です。

インストール方法も記述があり、実際にはコマンド一発でインストールできました

Pi4J1でLチカを

ラズパイが世の中に出てから「時間があったら触りたい」と思い続けていたものの、なかなか触れないという状況がありました。
それというのは、「何をどの言語を使用して作ろうか?できればJavaを使いたい」と思っていたためです。

ちょうど、チュートリアルなるものがありました。
記事内のリンクからBlueJプロジェクトをダウンロードして、ラズパイで開けばよいみたいです。
Windowsで開くと、エラーが出ました。GPIO周りのクラスが参照できないエラーでした。

ラズパイで、プロジェクトをダウンロードしてBlueJを開いてみたらビルドエラーがない状態でした。
しかし、重くてキャプチャーなどを残せませんでした。オーバークロックなどいろいろとセットアップが必要です。※調査します。

チュートリアル翻訳と解釈

参照するページはこちら、オラクルで出しているBlueJのサイトにあるものです。

LED を Raspberry Pi に接続

このセクションでは、LED を Raspberry Pi に接続し、BlueJ を使用してオブジェクトを直接操作してオンとオフを切り替えます。このチュートリアルでは、プロジェクトLEDButtonを使用します。このプロジェクトをダウンロードして、Raspberry Pi で実行されている BlueJ で開く必要があります。

材料

  1. LED:
  2. 抵抗 (270Ω ~ 330Ω の任意の値
  3. ブレッドボード(あるとよい)

組み立て方

ブレッドボードなし

LEDは、電流が流れると光る部品です。長い脚と短い脚があります。短い脚 (接地端子) は黒いワイヤに直接接続し、長い脚は抵抗器に接続する必要があります。抵抗は流れる電流を制限し、LED の焼損を防ぐために使用されます。

次の図のように、抵抗器のもう一方の脚をもう一方のワイヤ (赤いワイヤ) に接続する必要があります。

次に、黒のワイヤを Pi の 20 (Ground) とマークされたピンに接続し、赤のワイヤを 22 (GPIO6) とマークされたピンに接続する必要があります。

注: Raspberry Pi モデル B+ を使用している場合は、26 個を超えるピンがあります。ただし、両方のモデル (B および B+) に存在するピンの割り当ては、ここで説明されているものと同じであり、プロジェクトは変更なしで機能するはずです。

回路は次のようになります。

ブレッドボードあり

任意のサイズのブレッドボードが利用できる場合は、それを使用して回路を構築することをお勧めします。LEDの脚を正しい方向に向けることを忘れないでください! この回路を以下の図 4 に示します。

コードについて

BlueJ で、プロジェクト LEDButton を開きます。画面は次のようになります。

上の BlueJ 画面の黄色のボックスはそれぞれ Java クラスです。LED クラスは、Raspberry Pi に接続された実際の LED を表します。

LED、Button、および ButtonListener クラスはすでに作成されており、次の演習で使用できますが、変更しないでください。Controller クラスは、独自のコードを記述する場所です。Button クラスと ButtonListener クラスについては、このチュートリアルのパート 2 で説明します。

新しい LED オブジェクトの作成
コードを書き始める前に、BlueJ を使用して直接制御することで、LED クラスが実際の LED にどのように影響するかを確認します。

開始するには、LED クラスを右クリックし、ポップアップ メニューから次の項目を選択します。

BlueJ は「インスタンスの名前」を尋ねます。提案された名前は今のところ問題ありません。BlueJ ウィンドウの左下に「lED1」という名前の赤い四角形が表示されます。

この長方形の赤いアイコンは、「lED1」オブジェクトを表します。このオブジェクトは、Raspberry Pi に接続された実際の LED の Java 表現です。

LEDの点灯

LED をオンにするには、「lED1」インスタンスを右クリックし、次を選択します。

これで LED が点灯するはずです。(すべての舞台裏の接続が行われるため、最初は少し時間がかかる場合があります)。

図 6 を見ると、gpio 番号を指定して LED も作成できることがわかります。デフォルトの gpio 番号は、LED を接続した gpio 番号 6 であるため、これを行う必要はありませんでした。

ヒント: LED クラスには、使用可能なすべてのメソッドとそれぞれの簡単な説明を示す独自のドキュメントが含まれています。それらを表示するには、LED クラス (黄色のボックス) をダブルクリックするだけで、そのドキュメント (javadoc) が表示されます。

演習

演習 1.1 : LED をオンにしました。今すぐオフにできますか?
演習 1.2 : Controller クラスにコードを記述して、今行ったことを対話的に行うことができます。Controller クラスで、メソッド「void turnLEDOn()」の本体を変更して LED をオンにするメソッドを呼び出し、同じクラスのメソッド「void turnLEDOff()」を同様に LED をオフにするように変更します。
チップ:

Controller クラスを編集するには、「Controller」の黄色のボックスをダブルクリックすると、Controller クラスのソース コードが表示されます。
Controller クラスでは、LED オブジェクトは 'led' と呼ばれます: それはすでに存在しています!
重要:コントローラー クラスに加えた変更をテストする前に、エディターの左上または BlueJ メイン画面の左パネルにある [コンパイル] をクリックしてプロジェクトをコンパイルすることを忘れないでください。

Controller クラスの変更をテストするには、LED クラスで行ったのと同じように、クラスを右クリックして Controller のインスタンスを作成し、次に赤い Controller インスタンスを右クリックして、変更したばかりのメソッドを呼び出します。 .
演習 1.3 : Controller クラスのメソッド "void flash(int time)" の本体を変更して、一定時間 (ミリ秒単位で測定) LED をオンにしてから、LED をオフにします。
チップ:

Controller クラスには、というメソッドがあります。
sleepMillisec(int 時間)
このメソッドは、プログラムを指定されたミリ秒数待機させるために使用できます。
1 ミリ秒は非常に短い時間です。
演習 1.4 : SOS のモールス信号を LED で点滅させるために、メソッド "void flashSOS()" の本体を変更します。
ヒント:

SOS のモールス符号は「. . . - - - . . .」で、ドット (.) は短いフラッシュで、ダッシュ (-) は長いフラッシュです。

前の演習で実装したフラッシュ メソッドを利用します。

演習 1.5 : モールス信号が使用するすべてのパターンを調べ、モールス符号で指定した文字列をフラッシュする Controller クラスの新しいメソッドを作成します。

ラズパイで実行するとき

  1. ラズパイのセットアップを先に行います。
  2. 必要な部品などを用意。
  3. サンプルプロジェクトをダウンロード。
  4. チュートリアルを見ながら作成。

現状は、Windowsで記事を書いているので、後に、ラズパイでの実行した結果を記述します。

Java Swing テキストRPGを作る ~Textの作成を行うRpgStoryクラスの作成~

イントロダクション

前回は、Java Swingを使用して画面作成を行いました。下のような画面です。

この状態は、表示するための領域があるだけですので、これに表示するもの(内容)を出力する部品が必要になります。
もちろん、出力する内容はテキストRPGの「RPG」の部分です。そのためにまずはタイトルを表示することを考えます。

RpgStoryクラスの作成

ストーリーを表示するための画面を作成したので、今度はストーリーを出力(文字列を読み込んだり)するクラスを作成します。
まとめると、ゲーム進行を開始、実行するクラスです。

ちなみに、現状のクラスは下のようになっています。

RpgStoryクラスの設計

クラスを作成するために、まずは設計を行います。何から考えるか?

1. 目的(役割)を考える

作成するクラスの名前は「RpgStory」という名前に決めました。なのでこのクラスの役割(目的)を考えます。
やりたいことは次の通りです。フローチャートにしてみました。

とりあえず、下のような形で処理を進める形で作りたいと考えています。

  1. ゲーム開始
  2. タイトル表示(これもシーンとして扱う)
  3. 各シーンを表示
      1. Storyの表示
      1. 入力を受ける
      1. 結果の表委
      1. 次のシーンへ移動
  4. Escが押下されたとき、終了シーンに移動したときゲームを終了

こんな感じの処理フローを考えています。これを実現するために頭をひねります。
そして、上記の処理を実現する役割のクラスが「RpgStoryクラス」になりますので、下のようにクラスを作成します。

RpgStoryクラスの実装

まずは、クラスを作成します。Storyや結果などを表示する必要があるのでこのクラスのフィールドにテキストエリアをフィールド変数にセットします。

package jp.zenryoku.rpg;

/**
 * クラス RpgStory の注釈をここに書きます.
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
public class RpgStory
{
    private RpgTextArea view;
    /** コンストラクタ */
    public RpgStory(RpgTextArea view) {
        // ここに実行するための準備を実装する
        this.view = view;
    }

    /**
     * テキストRPGのシーンを呼び出し実行する。
     * プログラムのメイン処理フローを実装。
     */
    public void run() {
         // - 1. Storyの表示
         // - 2. 入力を受ける
         // - 3. 結果の表委
         // - 4. 次のシーンへ移動
    }
}

そして、ここから考えている処理を実現するための仕組みを考えていきます。そのためにクラスとクラスの関係を作成します。
このクラス同士の関係を作るために必要になる知識が以下のものになります。

クラス関係を作るための知識

「基本」という言葉で丸め込まれることが多いですが、以下のようなものです。

  1. 通常のクラスの書き方
  2. クラスの継承の仕方
  3. インターフェース・クラスの作り方
  4. 抽象クラスの作り方

すでに、クラスの作り方は学んでいるので大丈夫だと思います。が、作り方がわかっても使うのに慣れていないと、「理解した」というところに、たどり着かないので。。。

クラスを使ってみよう

Javaプログラミングを行うときにはJDKというものを使用してプログラムを使っています。
このJDKにはJavaプログラムを作成した人々が用意しているAPIというものを使うことができます。まずはこのAPIを使い
クラスの扱い方を理解しましょう。

次は、タイトルを表示するために「Story.txt」ファイルを読み込み、それを表示するプログラムを作成します。

ファイルの読み込み

テキストRPGを起動して、初めにタイトルを表示します。このタイトルはRPGを作成するユーザーが作るものなので「固定」ではなく「動的」に作ります。

「動的」とは

上記の通り「固定」ではなく「動的」という言い方がわかりやすいと思うのですが、

固定的に実装

固定的に実装するというのを、初めのプログラムハローワールドを例にして考えてみます。
ハローワールドのプログラムを思い出してみてください。下のように実装しました。

public class Kotei {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

これは固定的で出力する文字列が必ず「Hello World」です。

動的に実装

上記のプログラムを動的に変更します。動的になると必ずしも「Hello World」ではない文字列が出力できます。
でわ、動的なプログラムに修正しましょう。

public class Kotei {
    public static void main(String[] args) {
        if (args.length != 0) {
            System.out.println(args[0]);
        } else {
            System.out.println("Hello World");
        }
    }
}

上記のような形で実装すると動的に文字列を表示できます。

このような形で、実装すると表示する文字列を動的に出力することができます。

ファイルを読む形だと?

ファイルを読み込む形にすると、プログラムは全く同じでも、出力する文字列は、ファイルの中身に依存しますので、動的に表示するタイトルを表示することができるというわけです。別な言い方をすると「プログラムの修正をしなくても出力する文字列を変更できる」ということです。

ファイルの読み込み処理

ここで注意が一つあります。本来であれば、ファイルコントロール用のクラスを作るのが、通常のやり方ですが、今回はクラスを作成する一歩前の学習を目的としているので、メソッドを追加する形で実装します。

追加するのは下のようなメソッドです。

    private void readFile(String fileName) throws FileNotFoundException, IOException {
        File f = new File(fileName);
        BufferedReader buf = new BufferedReader(new FileReader(f));
        String line = null;
        while((line = buf.readLine()) != null) {
            System.out.println(line);
        }
        buf.close();
    }

ポイント

throws FileNotFoundException, IOExceptionの部分は「例外を呼び出し元に投げますよ」という意味です。
具体的には、例外があったときに、上記のメソッドを呼び出しているメインメソッドに例外が投げられます。

Scanner scan = new Scanner(System.in);
System.out.print("入力してください: ");
String in = scan.next();

try {
    if (in.equals("test")) {
        System.out.println("*** Testing now! ***");
    } else if ("file".equals(in)) {
        main.readFile("resources/FirstStage.txt");
    }
} catch (FileNotFoundException e) {
    System.out.println("*** No File! ***");
} catch (IOException e) {
    System.out.println("*** What's up!? ***");
}

このコードでは、以下のような処理を行っています。

  1. 標準入力を受けとる
  2. 入力した値が「test」の場合は、「* Testing now! *」を表示する
  3. 入力した値が「file」の場合は、「resources/FirstStage.txt」のファイルを読み込み表示

ここで使用しているメソッドは引数に「resources/ファイル名」を渡しています。つまり、プロジェクト直下にある「resources」フォルダ内にある「FirstStage.txt」ファイルを読み込んで表示します。

このような、「引数にファイル名(パスも含める)」を渡してやれば、ファイルを読み込むようなメソッド、そのような役割を持っているメソッドを作ってやればこのメソッドは、いろんな場面で使用することができます。

このような役割を持たせてメソッドを作ることを練習してできるようになったら、次はクラスにその役割を持たせるように実装します。
メソッドと違いクラスの場合は、もっと大きな役割を持たせることができます。詳細は後々に説明します。まずは、メソッドを作れるようになりましょう。

サンプルの説明

上記に書いた処理を実行していますが、プログラムが動いている部分を見る方が理解が早いと思います。

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」のようなメソッドを直接呼び出すことができる。

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

BlueJの使い方~インストール、クラス作成、サンプルプロジェクト~

BlueJの使い方

Oracle製のIDE「BlueJ」を使ってみようと思います。
そもそも、「なんで使ってみようと思ったのか?」に関して、以下の理由があります。
下の動画のように、作成したクラスがクラス図(簡易バージョン)で表現されるところです。
<HelloWorldの実行動画>

他にも、継承関係も表示できる用ですので、クラス間の関係性を説明するときに、自分で確認するときにも便利です。

BlueJインストール

こちらのページからダウンロードできます。MSIファイルなので、クリックするだけでインストールできました。

そして、ドキュメント、サンプルプロジェクトもそろっているようです。

そんなわけで、上記の赤丸部分をクリックしてダウンロードした。ファイルの中身を見てみます。

サンプルプロジェクト

ダウンロードしたprojects.zipを展開すると、Chapter01~16までプロジェクトが入っていました。
これらの中にあるフォルダが、BlueJで開くべきプロジェクトになります。

Chapter1のプロジェクト(figure)

開いたプロジェクトは下のような形でした。

これに、メインクラスを作成し、メインメソッドを追加して実行しました。

<実行結果>

サンプルプロジェクトを見る

ダウンロードしたZIPファイルを展開したあと、「projects」-> 「chapter01」-> プロジェクトとフォルダを下がっていく形になります。
具体的には、「projects」-> 「chapter01」-> 「home」を開きます。

プロジェクトを開いた結果

ここから、Javaプログラムの解説になります。

Java プログラム

開いたプロジェクトは、下のようなクラスがあります。

  • Picture
  • Circle
  • Square
  • Triangle
  • Person
  • Canvas

クラスの関係について

上記のクラス図から、Circle, Square, Triangleの3クラスがPictureクラスに呼び出されています。

そして、Picture以外のクラスがCanvasクラスを呼び出しています。

なので、ここに「」クラスを作成し、Picture、Personクラスを起動するメインメソッドを実装、起動します。

Pictureクラス

このクラスは、Circle, Square, Triangleの3クラスを呼び出しています。
詳細は、フィールド変数に上記の3クラスを持っています。
そして、コンストラクタで各クラスのインスタンス化を行い。
draw()メソッドで描画を行っています。

なので、メインメソッドでは、インスタンス化とdraw()を呼び出しを行っています。

/**
 * This class represents a simple picture. You can draw the picture using
 * the draw method. But wait, there's more: being an electronic picture, it
 * can be changed. You can set it to black-and-white display and back to
 * colors (only after it's been drawn, of course).
 *
 * This class was written as an early example for teaching Java with BlueJ.
 * 
 * @author  Michael Kölling and David J. Barnes
 * @version 2016.02.29
 */
public class Picture
{
    private Square wall;
    private Square window;
    private Triangle roof;
    private Circle sun;
    private boolean drawn;

    /**
     * Constructor for objects of class Picture
     */
    public Picture()
    {
        wall = new Square();
        window = new Square();
        roof = new Triangle();
        sun = new Circle();
        drawn = false;
    }

    /**
     * Draw this picture.
     */
    public void draw()
    {
        if(!drawn) {
            wall.moveHorizontal(-140);
            wall.moveVertical(20);
            wall.changeSize(120);
            wall.makeVisible();

            window.changeColor("black");
            window.moveHorizontal(-120);
            window.moveVertical(40);
            window.changeSize(40);
            window.makeVisible();

            roof.changeSize(60, 180);
            roof.moveHorizontal(20);
            roof.moveVertical(-60);
            roof.makeVisible();

            sun.changeColor("yellow");
            sun.moveHorizontal(100);
            sun.moveVertical(-40);
            sun.changeSize(80);
            sun.makeVisible();
            drawn = true;
        }
    }

    /**
     * Change this picture to black/white display
     */
    public void setBlackAndWhite()
    {
        wall.changeColor("black");
        window.changeColor("white");
        roof.changeColor("black");
        sun.changeColor("black");
    }

    /**
     * Change this picture to use color display
     */
    public void setColor()
    {
        wall.changeColor("red");
        window.changeColor("black");
        roof.changeColor("green");
        sun.changeColor("yellow");
    }
}

Mainクラス

このクラスは、筆者が作成したクラスです。単純にPictureクラスとPersonクラスをインスタンス化してそれぞれ描画するメソッドを起動しています。

public class Main
{
  public static void main(String[] args) {
      Picture pic = new Picture();
      Person per = new Person();

      pic.draw();
      per.makeVisible();
  }
}

RPi ラズパイの設定

ラズパイの設定

ラズパイ(RPo)を購入して、早速起動!などどと浮かれていると、早速設定の必要性が出てきます。
設定は一度やったらあまりいじらないので、メモしておくに限ります。

日本語入力の設定(iBus-Mozc)

日本語のキーボート、表示はOSのインストール時にできるかもしれませんが、自分の場合は入力ができませんでした。
なので、次のコマンドを実行して、必要なプログラムをインストールしました。

まずは、アップデートを行います。

sudo  apt update

次に、iBus-Mozcのインストール

sudo apt install iBus-Mozc

これが終わったら、再起動します。そして、設定後入力結果です。
この記事を書いているときのもので失礼・・・

画面の右上部分にインストールした結果追加されたアイコン?が表示さています。

インストール状況の確認

コマンドを実行して、「Java, Ruby, Pythonのインストールされているかどうか?」を確認しました。
以下のような感じでした。

コマンドの実行結果は以下の出力がありました。

 takunoji@raspberrypi:~ $ jaca -vewrsion
bash: jaca: コマンドが見つかりません
takunoji@raspberrypi:~ $ ruby -V
bash: ruby: コマンドが見つかりません
takunoji@raspberrypi:~ $ python -V
Python 3.9.2

つまり、Pythonのインストールはされているということです。

BlueJのインストール

参考サイトは「bluej.org」です。

以下のコマンドを叩きます。

sudo apt-get update && sudo apt-get install bluej

Javaは別途インストールする必要があります。

sudo apt install default-jdk

確認するには、次のコマンドで行います。

java -version

BlueJを使ってみる

インストール後に、起動してプロジェクトを作成すると以下のような形で見えます。

そしてクラスを作成すると、こんな感じです。

クラスのコードを書くために、エディタを開きます。

一連の流れを動画にしました。
<iframe width="560" height="315" src="https://www.youtube.com/embed/nQjENclzrhE"; title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

Wifiの設定

参考サイト

1, コマンドでSSIDの確認
<code>sudo iwlist wlan0 scan | grep SSID</code>で対象のアクセスポイントが生きているか確認

  1. 「/etc/wpa_supplicant/wpa_supplicant.conf」を修正
    ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
    update_config=1
    country=JP
    network={
    ssid="ここにSSID"
    psk="パスワード"
    }

これで再起動すると、Wifiの設定ができているようです。

Java 3D LWJGL GitBook: 第 21 章 – 間接描画 (アニメーション モデル) と計算シェーダー

第 21 章 - 間接描画 (アニメーション モデル) と計算シェーダー

この章では、間接描画を使用する場合のアニメーション モデルのサポートを追加します。そのために、コンピューティング シェーダーという新しいトピックを導入します。計算シェーダーを使用して、モデルの頂点をバインディング ポーズから最終的な位置に変換します (現在のアニメーションに従って)。これが完了したら、通常のシェーダーを使用してそれらをレンダリングできます。レンダリング中にアニメートされたモデルとアニメートされていないモデルを区別する必要はありません。それに加えて、レンダリング プロセスからアニメーション変換を切り離すことができます。そうすることで、アニメーション モデルをレンダリング レートとは異なるレートで更新できるようになります (アニメーション化された頂点が変更されていなければ、各フレームで変換する必要はありません)。

サンプルコードの実行結果

実行するときに、以下の修正を行いました。
<scene.vert>

out uint outMaterialIdx; -> flat out uint outMaterialIdx;

コンセプト

コードを説明する前に、アニメーション モデルの間接描画の背後にある概念を説明しましょう。私たちが従うアプローチは、前の章で使用したものと多かれ少なかれ同じです。頂点データを含むグローバル バッファを作成します。主な違いは、最初に計算シェーダーを使用して頂点をバインディング ポーズから最終ポーズに変換することです。それに加えて、モデルに複数のインスタンスを使用しません。その理由は、同じアニメーション化されたモデルを共有する複数のエンティティがある場合でも、それらは異なるアニメーション状態になる可能性があります (アニメーションが後で開始されたり、更新率が低くなったり、モデルの特定の選択されたアニメーションでさえある可能性があります)異なる場合があります)。したがって、アニメーション化された頂点を含むグローバル バッファ内に、エンティティごとに 1 つのデータ チャンクが必要になります。

データをバインドし続ける必要があります。シーンのすべてのメッシュ用に別のグローバル バッファを作成します。この場合、エンティティごとに別々のチャンクを持つ必要はなく、メッシュごとに 1 つだけです。コンピューティング シェーダーは、バインディング ポーズ データ バッファーにアクセスし、エンティティごとにそれを処理し、静的モデルに使用されるものと同様の構造を持つ別のグローバル バッファーに結果を格納します。

モデルの読み込み

Modelこのクラスにはボーン マトリックス データを保存しないため、クラスを更新する必要があります。代わりに、その情報は共通のバッファに格納されます。したがって、内部クラスAnimatedFrameはもはやレコードになることはできません (レコードは不変です)。

public class Model {
    ...
    public static class AnimatedFrame {
        private Matrix4f[] bonesMatrices;
        private int offset;

        public AnimatedFrame(Matrix4f[] bonesMatrices) {
            this.bonesMatrices = bonesMatrices;
        }

        public void clearData() {
            bonesMatrices = null;
        }

        public Matrix4f[] getBonesMatrices() {
            return bonesMatrices;
        }

        public int getOffset() {
            return offset;
        }

        public void setOffset(int offset) {
            this.offset = offset;
        }
    }
    ...
}

レコードから通常の内部クラスに渡すという事実、クラス属性へのアクセス方法を変更するには、クラスModelをわずかに変更する必要があります。ModelLoader

public class ModelLoader {
    ...
    private static void buildFrameMatrices(AIAnimation aiAnimation, List<Bone> boneList, Model.AnimatedFrame animatedFrame,
                                           int frame, Node node, Matrix4f parentTransformation, Matrix4f globalInverseTransform) {
        ...
        for (Bone bone : affectedBones) {
            ...
            animatedFrame.getBonesMatrices()[bone.boneId()] = boneTransform;
        }
        ...
    }
    ...
}

RenderBuffersクラスで管理される、必要な新しいグローバル バッファを確認しましょう。

public class RenderBuffers {

    private int animVaoId;
    private int bindingPosesBuffer;
    private int bonesIndicesWeightsBuffer;
    private int bonesMatricesBuffer;
    private int destAnimationBuffer;
    ...
    public void cleanup() {
        ...
        glDeleteVertexArrays(animVaoId);
    }
    ...
    public int getAnimVaoId() {
        return animVaoId;
    }

    public int getBindingPosesBuffer() {
        return bindingPosesBuffer;
    }

    public int getBonesIndicesWeightsBuffer() {
        return bonesIndicesWeightsBuffer;
    }

    public int getBonesMatricesBuffer() {
        return bonesMatricesBuffer;
    }

    public int getDestAnimationBuffer() {
        return destAnimationBuffer;
    }
    ...
}

はanimVaoId、変換されたアニメーション頂点を含むデータを定義する VAO を格納します。つまり、計算シェーダーによって処理された後のデータです (メッシュとエンティティごとに 1 つのチャンクを覚えておいてください)。データ自体はバッファに格納され、そのハンドルは に格納されdestAnimationBufferます。VAO を理解しない計算シェーダーでそのバッファーにアクセスする必要があります。バッファーだけです。bonesMatricesBufferまた、ボーン マトリックスとインデックスとウェイトを、それぞれ と で表される 2 つのバッファに格納する必要がありbonesIndicesWeightsBufferます。このcleanup方法では、新しい VAO をきれいにすることを忘れてはなりません。新しい属性のゲッターも追加する必要があります。

loadAnimatedModelsこれで、次のように始まる を実装できます。

public class RenderBuffers {
    ...
    public void loadAnimatedModels(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(Model::isAnimated).toList();
        loadBindingPoses(modelList);
        loadBonesMatricesBuffer(modelList);
        loadBonesIndicesWeights(modelList);

        animVaoId = glGenVertexArrays();
        glBindVertexArray(animVaoId);
        int positionsSize = 0;
        int normalsSize = 0;
        int textureCoordsSize = 0;
        int indicesSize = 0;
        int offset = 0;
        int chunkBindingPoseOffset = 0;
        int bindingPoseOffset = 0;
        int chunkWeightsOffset = 0;
        int weightsOffset = 0;
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                List<RenderBuffers.MeshDrawData> meshDrawDataList = model.getMeshDrawDataList();
                bindingPoseOffset = chunkBindingPoseOffset;
                weightsOffset = chunkWeightsOffset;
                for (MeshData meshData : model.getMeshDataList()) {
                    positionsSize += meshData.getPositions().length;
                    normalsSize += meshData.getNormals().length;
                    textureCoordsSize += meshData.getTextCoords().length;
                    indicesSize += meshData.getIndices().length;

                    int meshSizeInBytes = (meshData.getPositions().length + meshData.getNormals().length * 3 + meshData.getTextCoords().length) * 4;
                    meshDrawDataList.add(new MeshDrawData(meshSizeInBytes, meshData.getMaterialIdx(), offset,
                            meshData.getIndices().length, new AnimMeshDrawData(entity, bindingPoseOffset, weightsOffset)));
                    bindingPoseOffset += meshSizeInBytes / 4;
                    int groupSize = (int) Math.ceil((float) meshSizeInBytes / (14 * 4));
                    weightsOffset += groupSize * 2 * 4;
                    offset = positionsSize / 3;
                }
            }
            chunkBindingPoseOffset += bindingPoseOffset;
            chunkWeightsOffset += weightsOffset;
        }

        destAnimationBuffer = glGenBuffers();
        vboIdList.add(destAnimationBuffer);
        FloatBuffer meshesBuffer = MemoryUtil.memAllocFloat(positionsSize + normalsSize * 3 + textureCoordsSize);
        for (Model model : modelList) {
            model.getEntitiesList().forEach(e -> {
                for (MeshData meshData : model.getMeshDataList()) {
                    populateMeshBuffer(meshesBuffer, meshData);
                }
            });
        }
        meshesBuffer.flip();
        glBindBuffer(GL_ARRAY_BUFFER, destAnimationBuffer);
        glBufferData(GL_ARRAY_BUFFER, meshesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(meshesBuffer);

        defineVertexAttribs();

        // Index VBO
        int vboId = glGenBuffers();
        vboIdList.add(vboId);
        IntBuffer indicesBuffer = MemoryUtil.memAllocInt(indicesSize);
        for (Model model : modelList) {
            model.getEntitiesList().forEach(e -> {
                for (MeshData meshData : model.getMeshDataList()) {
                    indicesBuffer.put(meshData.getIndices());
                }
            });
        }
        indicesBuffer.flip();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(indicesBuffer);

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

以下のメソッドがどのように定義されているかについては後で説明しますが、今では:

・loadBindingPoses: アニメートされたモデルに関連付けられたすべてのメッシュのバインド ポーズ情報を格納します。
・loadBonesMatricesBuffer: アニメートされたモデルの各アニメーションのボーン マトリックスを格納します。
・loadBonesIndicesWeights: アニメートされたモデルのボーン インデックスとウェイト情報を格納します。
コードは と非常によく似ていloadStaticModelsます。アニメーション化されたモデルの VAO を作成することから始めて、モデルのメッシュを反復処理します。単一のバッファを使用してすべてのデータを保持するため、これらの要素を繰り返し処理して最終的なバッファ サイズを取得します。最初のループは、静的バージョンとは少し異なることに注意してください。モデルに関連付けられたエンティティを反復処理する必要があり、それらのそれぞれについて、関連付けられたすべてのメッシュのサイズを計算します。

loadBindingPosesメソッドを調べてみましょう。

public class RenderBuffers {
    ...
    private void loadBindingPoses(List<Model> modelList) {
        int meshSize = 0;
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                meshSize += meshData.getPositions().length + meshData.getNormals().length * 3 +
                        meshData.getTextCoords().length + meshData.getIndices().length;
            }
        }

        bindingPosesBuffer = glGenBuffers();
        vboIdList.add(bindingPosesBuffer);
        FloatBuffer meshesBuffer = MemoryUtil.memAllocFloat(meshSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                populateMeshBuffer(meshesBuffer, meshData);
            }
        }
        meshesBuffer.flip();
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, bindingPosesBuffer);
        glBufferData(GL_SHADER_STORAGE_BUFFER, meshesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(meshesBuffer);

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

モデルごとにアニメーション データの反復処理を開始し、すべての情報を保持するバッファを計算するために、アニメーション化されたフレームごとに (すべてのボーンの) 関連する変換行列を取得します。サイズを取得したら、バッファを作成し、(2 番目のループで) それらの行列を入力し始めます。前のバッファーと同様に、計算シェーダーでこのバッファーにアクセスするため、GL_SHADER_STORAGE_BUFFERフラグを使用する必要があります。

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

public class RenderBuffers {
    ...
    private void loadBonesIndicesWeights(List<Model> modelList) {
        int bufferSize = 0;
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                bufferSize += meshData.getBoneIndices().length * 4 + meshData.getWeights().length * 4;
            }
        }
        ByteBuffer dataBuffer = MemoryUtil.memAlloc(bufferSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                int[] bonesIndices = meshData.getBoneIndices();
                float[] weights = meshData.getWeights();
                int rows = bonesIndices.length / 4;
                for (int row = 0; row < rows; row++) {
                    int startPos = row * 4;
                    dataBuffer.putFloat(weights[startPos]);
                    dataBuffer.putFloat(weights[startPos + 1]);
                    dataBuffer.putFloat(weights[startPos + 2]);
                    dataBuffer.putFloat(weights[startPos + 3]);
                    dataBuffer.putFloat(bonesIndices[startPos]);
                    dataBuffer.putFloat(bonesIndices[startPos + 1]);
                    dataBuffer.putFloat(bonesIndices[startPos + 2]);
                    dataBuffer.putFloat(bonesIndices[startPos + 3]);
                }
            }
        }
        dataBuffer.flip();

        bonesIndicesWeightsBuffer = glGenBuffers();
        vboIdList.add(bonesIndicesWeightsBuffer);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, bonesIndicesWeightsBuffer);
        glBufferData(GL_SHADER_STORAGE_BUFFER, dataBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(dataBuffer);

        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    }
    ...
}

前の方法と同様に、重みとボーン インデックスの情報を 1 つのバッファーに格納するため、最初にそのサイズを計算し、後でデータを入力する必要があります。前のバッファーと同様に、計算シェーダーでこのバッファーにアクセスするため、GL_SHADER_STORAGE_BUFFERフラグを使用する必要があります。

計算シェーダー

今度は、計算シェーダーを介してアニメーション変換を実装する番です。前に述べたように、シェーダーは他のシェーダーと似ていますが、入力と出力に制限はありません。それらを使用してデータを変換し、バインディング ポーズとアニメーション変換マトリックスに関する情報を保持するグローバル バッファーにアクセスし、結果を別のバッファーにダンプします。アニメーション ( ) のシェーダー コードanim.compは次のように定義されます。

#version 460

layout (std430, binding=0) readonly buffer srcBuf {
    float data[];
} srcVector;

layout (std430, binding=1) readonly buffer weightsBuf {
    float data[];
} weightsVector;

layout (std430, binding=2) readonly buffer bonesBuf {
    mat4 data[];
} bonesMatrices;

layout (std430, binding=3) buffer dstBuf {
    float data[];
} dstVector;

struct DrawParameters
{
    int srcOffset;
    int srcSize;
    int weightsOffset;
    int bonesMatricesOffset;
    int dstOffset;
};
uniform DrawParameters drawParameters;

layout (local_size_x=1, local_size_y=1, local_size_z=1) in;

void main()
{
    int baseIdx = int(gl_GlobalInvocationID.x) * 14;
    uint baseIdxWeightsBuf  = drawParameters.weightsOffset + int(gl_GlobalInvocationID.x) * 8;
    uint baseIdxSrcBuf = drawParameters.srcOffset + baseIdx;
    uint baseIdxDstBuf = drawParameters.dstOffset + baseIdx;
    if (baseIdx >= drawParameters.srcSize) {
        return;
    }

    vec4 weights = vec4(weightsVector.data[baseIdxWeightsBuf], weightsVector.data[baseIdxWeightsBuf + 1], weightsVector.data[baseIdxWeightsBuf + 2], weightsVector.data[baseIdxWeightsBuf + 3]);
    ivec4 bonesIndices = ivec4(weightsVector.data[baseIdxWeightsBuf + 4], weightsVector.data[baseIdxWeightsBuf + 5], weightsVector.data[baseIdxWeightsBuf + 6], weightsVector.data[baseIdxWeightsBuf + 7]);

    vec4 position = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 1);
    position =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * position +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * position +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * position +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * position;
    dstVector.data[baseIdxDstBuf] = position.x / position.w;
    dstVector.data[baseIdxDstBuf + 1] = position.y / position.w;
    dstVector.data[baseIdxDstBuf + 2] = position.z / position.w;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec4 normal = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 0);
    normal =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * normal +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * normal +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * normal +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * normal;
    dstVector.data[baseIdxDstBuf] = normal.x;
    dstVector.data[baseIdxDstBuf + 1] = normal.y;
    dstVector.data[baseIdxDstBuf + 2] = normal.z;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec4 tangent = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 0);
    tangent =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * tangent +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * tangent +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * tangent +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * tangent;
    dstVector.data[baseIdxDstBuf] = tangent.x;
    dstVector.data[baseIdxDstBuf + 1] = tangent.y;
    dstVector.data[baseIdxDstBuf + 2] = tangent.z;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec4 bitangent = vec4(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1], srcVector.data[baseIdxSrcBuf + 2], 0);
    bitangent =
    weights.x * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.x] * bitangent +
    weights.y * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.y] * bitangent +
    weights.z * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.z] * bitangent +
    weights.w * bonesMatrices.data[drawParameters.bonesMatricesOffset + bonesIndices.w] * bitangent;
    dstVector.data[baseIdxDstBuf] = bitangent.x;
    dstVector.data[baseIdxDstBuf + 1] = bitangent.y;
    dstVector.data[baseIdxDstBuf + 2] = bitangent.z;

    baseIdxSrcBuf += 3;
    baseIdxDstBuf += 3;
    vec2 textCoords = vec2(srcVector.data[baseIdxSrcBuf], srcVector.data[baseIdxSrcBuf + 1]);
    dstVector.data[baseIdxDstBuf] = textCoords.x;
    dstVector.data[baseIdxDstBuf + 1] = textCoords.y;
}

ご覧のとおり、コードは前の章でアニメーション (ループの展開) に使用したものと非常によく似ています。データが共通のバッファに格納されるようになったため、メッシュごとにオフセットを適用する必要があることに気付くでしょう。計算シェーダーでプッシュ定数をサポートするため。入力/出力データは、一連のバッファーとして定義されます。

・srcVector: このバッファには、頂点情報 (位置、法線など) が含まれます。
・weightsVector: このバッファには、特定のメッシュとエンティティの現在のアニメーション状態の重みが含まれます。
・bonesMatrices: 同じですが、ボーン マトリックス情報があります。
・dstVector: このバッファは、アニメーション変換を適用した結果を保持します。
興味深いのは、そのオフセットを計算する方法です。このgl_GlobalInvocationID変数には、計算シェーダーで現在実行中の作業項目のインデックスが含まれます。この場合、グローバル バッファにある「チャンク」と同じ数の作業項目を作成します。チャンクは、その位置、法線、テクスチャ座標などの頂点データをモデル化します。したがって、ポート頂点データは、ワークアイテムが増加するたびに、バッファー内を 14 位置 (14 float: 位置の場合は 3) 移動する必要があります。法線の場合は 3、従接線の場合は 3、接線の場合は 3、テクスチャ座標の場合は 2)。同じことが、各頂点に関連付けられたウェイト (4 つの float) とボーン インデックス (4 つの float) のデータを保持するウェイト バッファーにも当てはまります。また、頂点オフセットを使用して、バインディング ポーズ バッファーと宛先バッファーを長く移動します。drawParameters各メッシュとエンティティの ebase オフセットを指すデータ。

このシェーダーは、AnimationRender次のように定義された名前の新しいクラスで使用します。

package org.lwjglb.engine.graph;

import org.lwjglb.engine.scene.*;

import java.util.*;

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

public class AnimationRender {

    private ShaderProgram shaderProgram;
    private UniformsMap uniformsMap;

    public AnimationRender() {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/anim.comp", GL_COMPUTE_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
        createUniforms();
    }

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

    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("drawParameters.srcOffset");
        uniformsMap.createUniform("drawParameters.srcSize");
        uniformsMap.createUniform("drawParameters.weightsOffset");
        uniformsMap.createUniform("drawParameters.bonesMatricesOffset");
        uniformsMap.createUniform("drawParameters.dstOffset");
    }

    public void render(Scene scene, RenderBuffers globalBuffer) {
        shaderProgram.bind();
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, globalBuffer.getBindingPosesBuffer());
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, globalBuffer.getBonesIndicesWeightsBuffer());
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, globalBuffer.getBonesMatricesBuffer());
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, globalBuffer.getDestAnimationBuffer());

        int dstOffset = 0;
        for (Model model : scene.getModelMap().values()) {
            if (model.isAnimated()) {
                for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                    RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                    Entity entity = animMeshDrawData.entity();
                    Model.AnimatedFrame frame = entity.getAnimationData().getCurrentFrame();
                    int groupSize = (int) Math.ceil((float) meshDrawData.sizeInBytes() / (14 * 4));
                    uniformsMap.setUniform("drawParameters.srcOffset", animMeshDrawData.bindingPoseOffset());
                    uniformsMap.setUniform("drawParameters.srcSize", meshDrawData.sizeInBytes() / 4);
                    uniformsMap.setUniform("drawParameters.weightsOffset", animMeshDrawData.weightsOffset());
                    uniformsMap.setUniform("drawParameters.bonesMatricesOffset", frame.getOffset());
                    uniformsMap.setUniform("drawParameters.dstOffset", dstOffset);
                    glDispatchCompute(groupSize, 1, 1);
                    dstOffset += meshDrawData.sizeInBytes() / 4;
                }
            }
        }

        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
        shaderProgram.unbind();
    }
}

ご覧のとおり、定義は非常に単純です。シェーダーを作成するときに、GL_COMPUTE_SHADERこれが計算シェーダーであることを示すように を設定する必要があります。不適切に使用されるユニフォームには、バインディング ポーズ バッファ、ウェイトおよびマトリックス バッファ、およびデスティネーション バッファにオフセットが含まれます。このrenderメソッドでは、モデルを繰り返し処理し、各エンティティのメッシュ描画データを取得して、glDispatchCompute. キーは、再びgroupSize変数をトスします。ご覧のとおり、メッシュ内にある頂点チャンクと同じ回数だけシェーダーを呼び出す必要があります。

その他の変更

SceneRenderアニメーション化されたモデルに関連付けられたエンティティをレンダリングするには、クラスを更新する必要があります。変更点を以下に示します。

public class SceneRender {
    ...
    private int animDrawCount;
    private int animRenderBufferHandle;
    ...
    public void cleanup() {
        ...
        glDeleteBuffers(animRenderBufferHandle);
    }
    ...
    public void render(Scene scene, RenderBuffers renderBuffers, GBuffer gBuffer) {
        ...
        // Animated meshes
        drawElement = 0;
        modelList = scene.getModelMap().values().stream().filter(m -> m.isAnimated()).toList();
        for (Model model : modelList) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                Entity entity = animMeshDrawData.entity();
                String name = "drawElements[" + drawElement + "]";
                uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                uniformsMap.setUniform(name + ".materialIdx", meshDrawData.materialIdx());
                drawElement++;
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, animRenderBufferHandle);
        glBindVertexArray(renderBuffers.getAnimVaoId());
        glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, animDrawCount, 0);

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

    private void setupAnimCommandBuffer(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> m.isAnimated()).toList();
        int numMeshes = 0;
        for (Model model : modelList) {
            numMeshes += model.getMeshDrawDataList().size();
        }

        int firstIndex = 0;
        int baseInstance = 0;
        ByteBuffer commandBuffer = MemoryUtil.memAlloc(numMeshes * COMMAND_SIZE);
        for (Model model : modelList) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(1);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

                firstIndex += meshDrawData.vertices();
                baseInstance++;
            }
        }
        commandBuffer.flip();
        animDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

        animRenderBufferHandle = glGenBuffers();
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, animRenderBufferHandle);
        glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);

        MemoryUtil.memFree(commandBuffer);
    }

    public void setupData(Scene scene) {
        ...
        setupAnimCommandBuffer(scene);
        ...
    }
    ...
}

アニメーション モデルをレンダリングするコードは、静的エンティティに使用されるものと非常によく似ています。違いは、同じモデルを共有するエンティティをグループ化していないことです。各エンティティと関連するメッシュの描画命令を記録する必要があります。

ShadowRenderまた、アニメーション モデルをレンダリングするようにクラスを更新する必要があります。

public class ShadowRender {
    ...
    private int animDrawCount;
    private int animRenderBufferHandle;
    ...
    public void cleanup() {
        ...
        glDeleteBuffers(animRenderBufferHandle);
    }
    ...
    public void render(Scene scene, RenderBuffers renderBuffers) {
        ...
        // Anim meshes
        drawElement = 0;
        modelList = scene.getModelMap().values().stream().filter(m -> m.isAnimated()).toList();
        for (Model model : modelList) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                Entity entity = animMeshDrawData.entity();
                String name = "drawElements[" + drawElement + "]";
                uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                drawElement++;
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, animRenderBufferHandle);
        glBindVertexArray(renderBuffers.getAnimVaoId());
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);

            CascadeShadow shadowCascade = cascadeShadows.get(i);
            uniformsMap.setUniform("projViewMatrix", shadowCascade.getProjViewMatrix());

            glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, animDrawCount, 0);
        }

        glBindVertexArray(0);
    }
    private void setupAnimCommandBuffer(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> m.isAnimated()).toList();
        int numMeshes = 0;
        for (Model model : modelList) {
            numMeshes += model.getMeshDrawDataList().size();
        }

        int firstIndex = 0;
        int baseInstance = 0;
        ByteBuffer commandBuffer = MemoryUtil.memAlloc(numMeshes * COMMAND_SIZE);
        for (Model model : modelList) {
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                RenderBuffers.AnimMeshDrawData animMeshDrawData = meshDrawData.animMeshDrawData();
                Entity entity = animMeshDrawData.entity();
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(1);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

                firstIndex += meshDrawData.vertices();
                baseInstance++;
            }
        }
        commandBuffer.flip();

        animDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

        animRenderBufferHandle = glGenBuffers();
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, animRenderBufferHandle);
        glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);

        MemoryUtil.memFree(commandBuffer);
    }
}

クラスでは、Renderクラスをインスタンス化し、それをループとメソッドAnimationRenderで使用するだけです。ループでは、最初にクラス メソッドを呼び出すため、シーンをレンダリングする前にアニメーション変換が適用されます。rendercleanuprenderAnimationRenderrender

public class Render {

    private AnimationRender animationRender;
    ...
    public Render(Window window) {
        ...
        animationRender = new AnimationRender();
        ...
    }

    public void cleanup() {
        ...
        animationRender.cleanup();
        ...
    }

    public void render(Window window, Scene scene) {
        animationRender.render(scene, renderBuffers);
        ...
    }    
    ...
}

最後に、このMainクラスでは、アニメーションの更新レートが異なる 2 つのアニメーション化されたエンティティを作成して、エンティティ情報ごとに正しく分離されていることを確認します。

public class Main implements IAppLogic {
    ...
    private AnimationData animationData1;
    private AnimationData animationData2;
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-21", opts, main);
        ...
    }
    ...
    public void init(Window window, Scene scene, Render render) {
        ...
        String bobModelId = "bobModel";
        Model bobModel = ModelLoader.loadModel(bobModelId, "resources/models/bob/boblamp.md5mesh",
                scene.getTextureCache(), scene.getMaterialCache(), true);
        scene.addModel(bobModel);
        Entity bobEntity = new Entity("bobEntity-1", bobModelId);
        bobEntity.setScale(0.05f);
        bobEntity.updateModelMatrix();
        animationData1 = new AnimationData(bobModel.getAnimationList().get(0));
        bobEntity.setAnimationData(animationData1);
        scene.addEntity(bobEntity);

        Entity bobEntity2 = new Entity("bobEntity-2", bobModelId);
        bobEntity2.setPosition(2, 0, 0);
        bobEntity2.setScale(0.025f);
        bobEntity2.updateModelMatrix();
        animationData2 = new AnimationData(bobModel.getAnimationList().get(0));
        bobEntity2.setAnimationData(animationData2);
        scene.addEntity(bobEntity2);
        ...
    }
    ...
    public void update(Window window, Scene scene, long diffTimeMillis) {
        animationData1.nextFrame();
        if (diffTimeMillis % 2 == 0) {
            animationData2.nextFrame();
        }
        ...
    }
}

すべての変更を実装すると、これに似たものが表示されるはずです。

Java 3D LWJGL GitBook: 第 20 章 – 間接描画 (静的モデル)

第 20 章 - 間接描画 (静的モデル)

この章まで、マテリアル ユニフォーム、テクスチャ、頂点、およびインデックス バッファをバインドし、構成されているメッシュごとに 1 つの描画コマンドを送信することによって、モデルをレンダリングしてきました。この章では、より効率的なレンダリングへの道を歩み始めます。バインドレス レンダリング (少なくともほとんどバインドレス) の実装を開始します。このタイプのレンダリングでは、一連の描画コマンドを呼び出してシーンを描画するのではなく、GPU がそれらをレンダリングできるようにする命令をバッファーに入力します。これは間接レンダリングと呼ばれ、次の理由により、より効率的な描画方法です。

・各メッシュを描画する前に、いくつかのバインド操作を実行する必要がなくなりました。
・単一の描画呼び出しを呼び出すだけで済みます。
・CPU 側の負荷を軽減するフラスタム カリングなどの GPU 内操作を実行できます。
ご覧のとおり、最終的な目標は、CPU 側で発生する可能性のある潜在的なボトルネックと、CPU から GPU への通信によるレイテンシを取り除きながら、GPU の使用率を最大化することです。この章では、レンダリングを変換して、静的モデルのみから始まる間接描画を使用します。アニメ化されたモデルは、次の章で扱います。

サンプルコードの実行

サンプルコードの実行を行ったときに、エラーがありました。

Exception in thread "main" java.lang.RuntimeException: Error linking Shader code: Type mismatch: Type of outMaterialIdx different between shaders.
Out of resource error.

これを解消するのに、以下の部分を修正しました。
<scene.vert>

out uint outMaterialIdx; -> flat out uint outMaterialIdx;

コンセプト

コードを説明する前に、間接描画の背後にある概念を説明しましょう。要するに、頂点のレンダリングに使用される描画パラメータを格納するバッファを作成する必要があります。これは、描画を実行するように指示する GPU によって読み取られる命令ブロックまたは描画コマンドと考えることができます。バッファーにデータが取り込まれたら、 を呼び出してglMultiDrawElementsIndirectそのプロセスをトリガーします。DrawElementsIndirectCommandバッファーに格納された各描画コマンドは、次のパラメーターによって定義されます (C を使用している場合、これは構造体によってモデル化されます)。
・count: 描画する頂点の数 (頂点とは、位置、法線情報、テクスチャ座標などをグループ化する構造と理解します)。glDrawElementsこれには、メッシュのレンダリング時に を呼び出したときに使用した頂点の数と同じ値が含まれている必要があります。
・instanceCount: 描画されるインスタンスの数。同じモデルを共有する複数のエンティティが存在する場合があります。エンティティごとに描画命令を保存する代わりに、単一の描画命令を送信するだけで、描画するエンティティの数を設定できます。これはインスタンス レンダリングと呼ばれ、計算時間を大幅に節約します。間接的な描画がなくても、VAO ごとに特定の属性を設定することで同じ結果を得ることができます。このテクニックだとさらに簡単だと思います。
・firstIndex: この描画命令に使用されるインデックス値を保持するバッファーへのオフセット (オフセットは、バイト オフセットではなく、インデックスの数で測定されます)。
・baseVertex: 頂点データを保持するバッファーへのオフセット (オフセットは、バイト オフセットではなく、頂点の数で測定されます)。
・baseInstance: このパラメーターを使用して、描画されるすべてのインスタンスで共有される値を設定できます。この値を描画するインスタンスの数と組み合わせると、インスタンスごとのデータにアクセスできます (これについては後で説明します)。

パラメータを説明するときにすでにコメントされていますが、間接描画には、頂点データを保持するバッファと、インデックス用に別のバッファが必要です。違いは、シーンのモデルを単一のバッファに適合させる複数のメッシュからすべてのデータを結合する必要があることです。メッシュごとの特定のデータにアクセスする方法は、描画パラメータのオフセット値を再生することです。

解決すべきもう 1 つの側面は、マテリアル情報またはエンティティごとのデータ (モデル マトリックスなど) を渡す方法です。前の章では、そのためにユニフォームを使用し、メッシュまたは描画するエンティティを変更したときに適切な値を設定しました。間接的な描画では、大量の描画命令を一度に送信するため、レンダリング プロセス中にデータを変更することはできません。これに対する解決策は、追加のバッファーを使用することです。エンティティごとのデータをバッファーに格納し、baseInstanceパラメーター (インスタンス ID と組み合わせて) を使用して、そのバッファー内の適切なデータ (エンティティごと) にアクセスできます (後で説明します。バッファの代わりにユニフォームの配列を使用しますが、より単純なバッファを使用することもできます)。そのバッファ内では、2 つの追加バッファにアクセスするためのインデックスを保持します。
・モデル行列データを保持するもの。
・マテリアル データ (アルベド カラーなど) を保持するもの。
テクスチャの場合、配列テクスチャと混同しないように、テクスチャの配列を使用します。配列テクスチャは、テクスチャ情報を持つ値の配列を含むテクスチャで、同じサイズの複数の画像があります。テクスチャの配列は、通常のテクスチャにマップされるサンプルのリストであるため、異なるサイズを持つことができます。テクスチャの配列には制限があり、その長さを任意の長さにすることはできず、例では最大 16 のテクスチャを設定するという制限があります (ただし、制限を設定する前に GPU の機能を確認することをお勧めします)。複数のモデルを使用している場合、16 テクスチャは高い値ではありません。この制限を回避するには、次の 2 つのオプションがあります。
・テクスチャ アトラス (個々のテクスチャを組み合わせた巨大なテクスチャ ファイル) を使用します。間接描画を使用していない場合でも、バインド呼び出しが制限されるため、テクスチャ アトラスをできるだけ使用するようにしてください。
・バインドレス テクスチャを使用します。このアプローチでは基本的に、ハンドル (64 ビット整数値) を渡してテクスチャを識別し、その識別子を使用してシェーダー プログラムでサンプラーを取得できます。可能であれば、これは間違いなく間接レンダリングを使用する方法です (これはコア機能ではなく、バージョン 4.4 以降の拡張機能です)。RenderDoc は現在これをサポートしていないため、このアプローチは使用しません (RenderDoc なしでデバッグする機能を失うことは、私にとって致命的です)。

次の図は、間接描画に関係するバッファーと構造を示しています (これは、静的モデルのレンダリング中にのみ有効であることに注意してください。アニメーション モデルのレンダリングに使用する必要がある新しい構造については、次の章で説明します)。

エンティティ データ、マテリアル、およびモデル マトリックスごとにユニフォームの配列を使用することに注意してください (最後に、配列はバッファーですが、ユニフォームを使用することで便利な方法でデータにアクセスできます)。

実装

間接描画を使用するには、少なくとも OpenGL バージョン 4.6 を使用する必要があります。したがって、最初のステップは、ウィンドウ作成のウィンドウ ヒントとして使用するメジャー バージョンとマイナー バージョンを更新することです。

public class Window {
    ...
    public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
        ...
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
        ...
    }
    ...
}

次のステップは、すべてのメッシュを 1 つのバッファにロードするようにコードを変更することですが、その前に、モデル、マテリアル、およびメッシュを格納するクラス階層を変更しません。これまで、モデルには一連のメッシュを持つ一連の関連付けられたマテリアルがあります。このクラス階層は、ドロー コールを最適化するように設定されており、最初にモデルを反復し、次にマテリアルを反復し、最後にメッシュを反復しました。この構造を変更し、マテリアルの下にメッシュを保存しなくなります。代わりに、メッシュはモデルの直下に保存されます。マテリアルを一種のキャッシュに保存し、そのキャッシュ内のメッシュのキーへの参照を保持します。それに加えて、以前は、Mesh各モデル メッシュのインスタンス。本質的に、メッシュ データの VAO と関連する VBO が含まれていました。すべてのメッシュに対して単一のバッファを使用するため、シーンのメッシュのセット全体に対して単一の VAO とそれに関連付けられた VBO が必要になります。Mesh したがって、クラスの下にインスタンスのリストを保存する代わりにModel、頂点バッファーのオフセット、インデックス バッファーのオフセットなど、描画パラメーターを構築するために使用されるデータを保存します。変更を調べてみましょう。一つずつ。

MaterialCache次のように定義されているクラスから始めます。

package org.lwjglb.engine.graph;

import java.util.*;

public class MaterialCache {

    public static final int DEFAULT_MATERIAL_IDX = 0;

    private List<Material> materialsList;

    public MaterialCache() {
        materialsList = new ArrayList<>();
        Material defaultMaterial = new Material();
        materialsList.add(defaultMaterial);
    }

    public void addMaterial(Material material) {
        materialsList.add(material);
        material.setMaterialIdx(materialsList.size() - 1);
    }

    public Material getMaterial(int idx) {
        return materialsList.get(idx);
    }

    public List<Material> getMaterialsList() {
        return materialsList;
    }
}

ご覧のとおり、Materialインスタンスを に格納するだけListです。したがって、 を識別するためにMaterial必要なのは、リスト内のそのインスタンスのインデックスだけです。(このアプローチでは、動的に新しいマテリアルを追加するのが難しくなる場合がありますが、このサンプルの目的には十分単純です。それを変更して、新しいモデル、マテリアルなどをコードに追加するための堅牢なサポートを提供することもできます。 )。クラスを変更してインスタンスMaterialのリストを削除しMesh、マテリアル インデックスをマテリアル キャッシュに保存する必要があります。

public class Material {
    ...
    private Vector4f ambientColor;
    private Vector4f diffuseColor;
    private int materialIdx;
    private String normalMapPath;
    private float reflectance;
    private Vector4f specularColor;
    private String texturePath;

    public Material() {
        diffuseColor = DEFAULT_COLOR;
        ambientColor = DEFAULT_COLOR;
        specularColor = DEFAULT_COLOR;
        materialIdx = 0;
    }
    ...
    public int getMaterialIdx() {
        return materialIdx;
    }
    ...
    public void setMaterialIdx(int materialIdx) {
        this.materialIdx = materialIdx;
    }
    ...
}

前に説明したように、Modelクラスを変更してマテリアルへの参照を削除する必要があります。代わりに、2 つの主要なリファレンスを保持します。

・MeshDataAssimp を使用して読み取ったメッシュ データを保持するリストインスタンス (新しいクラス)。
・RenderBuffers.MeshDrawData間接描画に必要な情報 (主に、上記で説明したデータ バッファーに関連付けられたオフセット情報) を含むインスタンス (これも新しいクラス)のリスト。
MeshDataassimp を使用してモデルをロードするときに、最初にインスタンスのリストを生成します。その後、データを保持するグローバル バッファーを作成して、RenderBuffers.MeshDrawDataインスタンスを生成します。その後、 MeshDataインスタンスへの参照を削除できます。これはあまり洗練されたソリューションではありませんが、ロード前とロード後の階層を使用して複雑さを増すことなく、概念を説明するのに十分単純です。クラスの変更点は次のModelとおりです。

public class Model {
    ...
    private final String id;
    private List<Animation> animationList;
    private List<Entity> entitiesList;
    private List<MeshData> meshDataList;
    private List<RenderBuffers.MeshDrawData> meshDrawDataList;

    public Model(String id, List<MeshData> meshDataList, List<Animation> animationList) {
        entitiesList = new ArrayList<>();
        this.id = id;
        this.meshDataList = meshDataList;
        this.animationList = animationList;
        meshDrawDataList = new ArrayList<>();
    }
    ...
    public List<MeshData> getMeshDataList() {
        return meshDataList;
    }

    public List<RenderBuffers.MeshDrawData> getMeshDrawDataList() {
        return meshDrawDataList;
    }

    public boolean isAnimated() {
        return animationList != null && !animationList.isEmpty();
    }
    ...
}

クラスの定義MeshDataは非常に単純です。頂点の位置、テクスチャ座標などを保存するだけです。

package org.lwjglb.engine.graph;

import org.joml.Vector3f;

public class MeshData {

    private Vector3f aabbMax;
    private Vector3f aabbMin;
    private float[] bitangents;
    private int[] boneIndices;
    private int[] indices;
    private int materialIdx;
    private float[] normals;
    private float[] positions;
    private float[] tangents;
    private float[] textCoords;
    private float[] weights;

    public MeshData(float[] positions, float[] normals, float[] tangents, float[] bitangents,
                    float[] textCoords, int[] indices, int[] boneIndices, float[] weights,
                    Vector3f aabbMin, Vector3f aabbMax) {
        materialIdx = 0;
        this.positions = positions;
        this.normals = normals;
        this.tangents = tangents;
        this.bitangents = bitangents;
        this.textCoords = textCoords;
        this.indices = indices;
        this.boneIndices = boneIndices;
        this.weights = weights;
        this.aabbMin = aabbMin;
        this.aabbMax = aabbMax;
    }

    public Vector3f getAabbMax() {
        return aabbMax;
    }

    public Vector3f getAabbMin() {
        return aabbMin;
    }

    public float[] getBitangents() {
        return bitangents;
    }

    public int[] getBoneIndices() {
        return boneIndices;
    }

    public int[] getIndices() {
        return indices;
    }

    public int getMaterialIdx() {
        return materialIdx;
    }

    public float[] getNormals() {
        return normals;
    }

    public float[] getPositions() {
        return positions;
    }

    public float[] getTangents() {
        return tangents;
    }

    public float[] getTextCoords() {
        return textCoords;
    }

    public float[] getWeights() {
        return weights;
    }

    public void setMaterialIdx(int materialIdx) {
        this.materialIdx = materialIdx;
    }
}

クラスの変更ModelLoaderも非常に簡単です。マテリアル キャッシュを使用し、読み取ったデータをMeshData(以前のMeshクラスではなく) 新しいクラスに保存する必要があります。また、マテリアルにはメッシュ データへの参照はありませんが、メッシュ データにはキャッシュ内のマテリアルのインデックスへの参照があります。

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

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

        for (int i = 0; i < numMaterials; i++) {
            AIMaterial aiMaterial = AIMaterial.create(aiScene.mMaterials().get(i));
            Material material = processMaterial(aiMaterial, modelDir, textureCache);
            materialCache.addMaterial(material);
            materialList.add(material);
        }

        int numMeshes = aiScene.mNumMeshes();
        PointerBuffer aiMeshes = aiScene.mMeshes();
        List<MeshData> meshDataList = new ArrayList<>();
        List<Bone> boneList = new ArrayList<>();
        for (int i = 0; i < numMeshes; i++) {
            AIMesh aiMesh = AIMesh.create(aiMeshes.get(i));
            MeshData meshData = processMesh(aiMesh, boneList);
            int materialIdx = aiMesh.mMaterialIndex();
            if (materialIdx >= 0 && materialIdx < materialList.size()) {
                meshData.setMaterialIdx(materialList.get(materialIdx).getMaterialIdx());
            } else {
                meshData.setMaterialIdx(MaterialCache.DEFAULT_MATERIAL_IDX);
            }
            meshDataList.add(meshData);
        }
        ...
        return new Model(modelId, meshDataList, animations);
    }
    ...
    private static MeshData processMesh(AIMesh aiMesh, List<Bone> boneList) {
        ...
        return new MeshData(vertices, normals, tangents, bitangents, textCoords, indices, animMeshData.boneIds,
                animMeshData.weights, aabbMin, aabbMax);
    }
}

このSceneクラスは、マテリアル キャッシュを保持するクラスになります (また、cleanupVAO と VBO がモデル マップにリンクされなくなるため、mwthod は不要になります):

public class Scene {
    ...
    private MaterialCache materialCache;
    ...
    public Scene(int width, int height) {
        ...
        materialCache = new MaterialCache();
        ...
    }
    ...
    public MaterialCache getMaterialCache() {
        return materialCache;
    }
    ...    
}

クラスの変更は、Meshクラスを導入したためMeshDataです (コンストラクターの引数とメソッドを変更するだけの問題です)。

public class Mesh {
    ...
    public Mesh(MeshData meshData) {
        try (MemoryStack stack = MemoryStack.stackPush()) {
            this.aabbMin = meshData.getAabbMin();
            this.aabbMax = meshData.getAabbMax();
            numVertices = meshData.getIndices().length;
            ...
            FloatBuffer positionsBuffer = stack.callocFloat(meshData.getPositions().length);
            positionsBuffer.put(0, meshData.getPositions());
            ...
            FloatBuffer normalsBuffer = stack.callocFloat(meshData.getNormals().length);
            normalsBuffer.put(0, meshData.getNormals());
            ...
            FloatBuffer tangentsBuffer = stack.callocFloat(meshData.getTangents().length);
            tangentsBuffer.put(0, meshData.getTangents());
            ...
            FloatBuffer bitangentsBuffer = stack.callocFloat(meshData.getBitangents().length);
            bitangentsBuffer.put(0, meshData.getBitangents());
            ...
            FloatBuffer textCoordsBuffer = MemoryUtil.memAllocFloat(meshData.getTextCoords().length);
            textCoordsBuffer.put(0, meshData.getTextCoords());
            ...
            FloatBuffer weightsBuffer = MemoryUtil.memAllocFloat(meshData.getWeights().length);
            weightsBuffer.put(meshData.getWeights()).flip();
            ...
            IntBuffer boneIndicesBuffer = MemoryUtil.memAllocInt(meshData.getBoneIndices().length);
            boneIndicesBuffer.put(meshData.getBoneIndices()).flip();
            ...
            IntBuffer indicesBuffer = stack.callocInt(meshData.getIndices().length);
            indicesBuffer.put(0, meshData.getIndices());
        }
    }
    ...
}

今度は、間接描画用に作成する新しい主要なクラスの 1 つであるRenderBuffersクラスの番です。このクラスは、すべてのメッシュのデータを含む VBO を保持する単一の VAO を作成します。この場合、静的モデルのみをサポートするため、単一の VAO が必要になります。クラスは次のRenderBuffersように始まります。

public class RenderBuffers {

    private int staticVaoId;
    private List<Integer> vboIdList;

    public RenderBuffers() {
        vboIdList = new ArrayList<>();
    }

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

このクラスは、モデルをロードする 2 つのメソッドを定義します。

・loadAnimatedModelsアニメモデル用。これは、この章では実装されません。
・loadStaticModelsアニメーションのないモデルの場合。
これらのメソッドは次のように定義されています。

public class RenderBuffers {
    ...
    public final int getStaticVaoId() {
        return staticVaoId;
    }

    public void loadAnimatedModels(Scene scene) {
        // To be completed
    }

    public void loadStaticModels(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        staticVaoId = glGenVertexArrays();
        glBindVertexArray(staticVaoId);
        int positionsSize = 0;
        int normalsSize = 0;
        int textureCoordsSize = 0;
        int indicesSize = 0;
        int offset = 0;
        for (Model model : modelList) {
            List<RenderBuffers.MeshDrawData> meshDrawDataList = model.getMeshDrawDataList();
            for (MeshData meshData : model.getMeshDataList()) {
                positionsSize += meshData.getPositions().length;
                normalsSize += meshData.getNormals().length;
                textureCoordsSize += meshData.getTextCoords().length;
                indicesSize += meshData.getIndices().length;

                int meshSizeInBytes = meshData.getPositions().length * 14 * 4;
                meshDrawDataList.add(new MeshDrawData(meshSizeInBytes, meshData.getMaterialIdx(), offset,
                        meshData.getIndices().length));
                offset = positionsSize / 3;
            }
        }

        int vboId = glGenBuffers();
        vboIdList.add(vboId);
        FloatBuffer meshesBuffer = MemoryUtil.memAllocFloat(positionsSize + normalsSize * 3 + textureCoordsSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                populateMeshBuffer(meshesBuffer, meshData);
            }
        }
        meshesBuffer.flip();
        glBindBuffer(GL_ARRAY_BUFFER, vboId);
        glBufferData(GL_ARRAY_BUFFER, meshesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(meshesBuffer);

        defineVertexAttribs();
         // Index VBO
        vboId = glGenBuffers();
        vboIdList.add(vboId);
        IntBuffer indicesBuffer = MemoryUtil.memAllocInt(indicesSize);
        for (Model model : modelList) {
            for (MeshData meshData : model.getMeshDataList()) {
                indicesBuffer.put(meshData.getIndices());
            }
        }
        indicesBuffer.flip();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
        MemoryUtil.memFree(indicesBuffer);

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

まず、VAO (静的モデルに使用されます) を作成し、モデルのメッシュを反復処理します。単一のバッファを使用してすべてのデータを保持するため、これらの要素を繰り返し処理して最終的なバッファ サイズを取得します。RenderBuffers.MeshDrawData位置要素、法線などの数を計算します。最初のループを使用して、インスタンスを含むリストに保存するオフセット情報も入力します。その後、単一の VBO を作成します。MeshVAO と VBO を作成する同様のタスクを行ったクラスとの大きな違いがわかります。この場合、位置や法線などに単一の VBO を使用します。個別の VBO を使用する代わりに、すべてのデータを行ごとにロードするだけです。これは、populateMeshBuffer(これについては後で説明します)。その後、すべてのモデルのすべてのメッシュのインデックスを含むインデックス VBO を作成します。

クラスは次のMeshDrawDataように定義されます。

public class RenderBuffers {
    ...
    public record MeshDrawData(int sizeInBytes, int materialIdx, int offset, int vertices) {
    }
}

基本的に、メッシュのサイズ (バイト単位) ( sizeInBytes)、関連付けられているマテリアル インデックス、頂点情報と頂点を保持するバッファー内のオフセット、このメッシュのインデックス数を格納します。オフセットは「行」で測定されます。位置、法線、テクスチャ座標を保持するメッシュの部分を 1 つの「行」と考えることができます。この「行」は、1 つの頂点に関連付けられたすべての情報を保持し、頂点シェーダーで処理されます。これが、位置要素の数を 3 だけダイブする理由です。各「行」には 3 つの位置要素があり、位置データの「行」の数は法線データの「行」の数と一致します。 .

は次のpopulateMeshBufferように定義されます。

public class RenderBuffers {
    ...
    private void populateMeshBuffer(FloatBuffer meshesBuffer, MeshData meshData) {
        float[] positions = meshData.getPositions();
        float[] normals = meshData.getNormals();
        float[] tangents = meshData.getTangents();
        float[] bitangents = meshData.getBitangents();
        float[] textCoords = meshData.getTextCoords();

        int rows = positions.length / 3;
        for (int row = 0; row < rows; row++) {
            int startPos = row * 3;
            int startTextCoord = row * 2;
            meshesBuffer.put(positions[startPos]);
            meshesBuffer.put(positions[startPos + 1]);
            meshesBuffer.put(positions[startPos + 2]);
            meshesBuffer.put(normals[startPos]);
            meshesBuffer.put(normals[startPos + 1]);
            meshesBuffer.put(normals[startPos + 2]);
            meshesBuffer.put(tangents[startPos]);
            meshesBuffer.put(tangents[startPos + 1]);
            meshesBuffer.put(tangents[startPos + 2]);
            meshesBuffer.put(bitangents[startPos]);
            meshesBuffer.put(bitangents[startPos + 1]);
            meshesBuffer.put(bitangents[startPos + 2]);
            meshesBuffer.put(textCoords[startTextCoord]);
            meshesBuffer.put(textCoords[startTextCoord + 1]);
        }
    }
    ...
}

ご覧のとおり、データの「行」を繰り返し処理し、位置、法線、およびテクスチャ座標をバッファにパックします。は次のdefineVertexAttribsように定義されます。

public class RenderBuffers {
    ...
    private void defineVertexAttribs() {
        int stride = 3 * 4 * 4 + 2 * 4;
        int pointer = 0;
        // Positions
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Normals
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Tangents
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Bitangents
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 3, GL_FLOAT, false, stride, pointer);
        pointer += 3 * 4;
        // Texture coordinates
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 2, GL_FLOAT, false, stride, pointer);
    }
    ...
}

前の例のように、VAO の頂点属性を定義するだけです。ここでの唯一の違いは、単一の VBO を使用していることです。

クラスの変更を調べる前に、次のように始まる頂点シェーダー ( ) からSceneRender始めましょう。scene.vert

#version 460

const int MAX_DRAW_ELEMENTS = 100;
const int MAX_ENTITIES = 50;

layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in vec3 tangent;
layout (location=3) in vec3 bitangent;
layout (location=4) in vec2 texCoord;

out vec3 outNormal;
out vec3 outTangent;
out vec3 outBitangent;
out vec2 outTextCoord;
out vec4 outViewPosition;
out vec4 outWorldPosition;
out uint outMaterialIdx;

struct DrawElement
{
    int modelMatrixIdx;
    int materialIdx;
};

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform DrawElement drawElements[MAX_DRAW_ELEMENTS];
uniform mat4 modelMatrices[MAX_ENTITIES];
...

最初に気付くのは、バージョンが に増えたこと460です。また、アニメーションに関連付けられた定数 (MAX_WEIGHTSおよびMAX_BONES)、ボーン インデックスの属性、およびボーン マトリックスのユニフォームを削除しました。次の章で、アニメーションにはこの情報が必要ないことがわかります。drawElementsとmodelMatricesユニフォームのサイズを定義する 2 つの新しい定数を作成しました。drawElementsユニフォームはインスタンスを保持しますDrawElement。メッシュと関連付けられたエンティティごとに 1 つのアイテムがあります。覚えていると思いますが、メッシュに関連付けられたすべてのアイテムを描画する単一の命令を記録し、描画するインスタンスの数を設定します。ただし、モデル マトリックスなど、エンティティごとに固有のデータが必要になります。これはdrawElementsこの配列は、使用されるマテリアル インデックスも指します。modelMatrices配列は、各エンティティのモデル マトリックスのみを保持します。outMaterialIdxマテリアル情報は、出力変数を使用して渡すフラグメント シェーダーで使用されます。

mainアニメーションを扱う必要がないため、関数は大幅に単純化されています。

...
void main()
{
    vec4 initPos = vec4(position, 1.0);
    vec4 initNormal = vec4(normal, 0.0);
    vec4 initTangent = vec4(tangent, 0.0);
    vec4 initBitangent = vec4(bitangent, 0.0);

    uint idx = gl_BaseInstance + gl_InstanceID;
    DrawElement drawElement = drawElements[idx];
    outMaterialIdx = drawElement.materialIdx;
    mat4 modelMatrix =  modelMatrices[drawElement.modelMatrixIdx];
    mat4 modelViewMatrix = viewMatrix * modelMatrix;
    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;
}

ここで重要なのは、drawElementsサイズにアクセスするための適切なインデックスを取得することです。gl_BaseInstanceおよびgl_InstanceID組み込み変数を使用します。間接描画の指示を記録するときは、baseInstance属性を使用します。その属性の値は、gl_BaseInstance組み込み変数に関連付けられたものになります。は、メッシュから別のメッシュに変更するたびにgl_InstanceID開始さ0れ、モデルに関連付けられたエンティティのインスタンスの数だけ増加します。したがって、この 2 つの変数を組み合わせることで、drawElements配列内のエンティティごとの特定の情報にアクセスできるようになります。適切なインデックスを取得したら、以前のバージョンのシェーダーと同様に、位置と法線情報を変換するだけです。

シーン フラグメント シェーダー ( scene.frag) は次のように定義されます。

#version 400

const int MAX_MATERIALS  = 20;
const int MAX_TEXTURES = 16;

in vec3 outNormal;
in vec3 outTangent;
in vec3 outBitangent;
in vec2 outTextCoord;
in vec4 outViewPosition;
in vec4 outWorldPosition;
flat in uint outMaterialIdx;

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 normalMapIdx;
    int textureIdx;
};

uniform sampler2D txtSampler[MAX_TEXTURES];
uniform Material materials[MAX_MATERIALS];

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

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

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

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

主な変更点は、マテリアル情報とテクスチャへのアクセス方法に関連しています。これで、マテリアル情報の配列が得られます。これは、現在outMaterialIdx入力変数にある頂点シェーダーで計算したインデックスによってアクセスされます (これには、flatこの値を頂点からフラグメント ステージに補間してはならないことを示す修飾子があります)。 . テクスチャの配列を使用して、通常のテクスチャまたは法線マップにアクセスします。これらのテクスチャへのインデックスは、Material構造体に格納されるようになりました。非定数式を使用してサンプラーの配列にアクセスするため、GLSL バージョンを 400 にアップグレードする必要があります (この機能は OpenGL 4.0 以降でのみ使用可能です)。

今度は、SceneRenderクラスの変化を調べる番です。コードで使用される一連の定数、間接描画命令 ( staticRenderBufferHandle) および描画コマンドの数( ) を持つバッファーの 1 つのハンドルを定義することから始めますstaticDrawCount。createUniforms前に示したシェーダーの変更に従って、メソッドを変更する必要もあります。

public class SceneRender {
    ...
    public static final int MAX_DRAW_ELEMENTS = 100;
    public static final int MAX_ENTITIES = 50;
    private static final int COMMAND_SIZE = 5 * 4;
    private static final int MAX_MATERIALS = 20;
    private static final int MAX_TEXTURES = 16;
    ...
    private Map<String, Integer> entitiesIdxMap;
    ...
    private int staticDrawCount;
    private int staticRenderBufferHandle;
    ...
    public SceneRender() {
        ...
        entitiesIdxMap = new HashMap<>();
    }

    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("projectionMatrix");
        uniformsMap.createUniform("viewMatrix");

        for (int i = 0; i < MAX_TEXTURES; i++) {
            uniformsMap.createUniform("txtSampler[" + i + "]");
        }

        for (int i = 0; i < MAX_MATERIALS; i++) {
            String name = "materials[" + i + "]";
            uniformsMap.createUniform(name + ".diffuse");
            uniformsMap.createUniform(name + ".specular");
            uniformsMap.createUniform(name + ".reflectance");
            uniformsMap.createUniform(name + ".normalMapIdx");
            uniformsMap.createUniform(name + ".textureIdx");
        }

        for (int i = 0; i < MAX_DRAW_ELEMENTS; i++) {
            String name = "drawElements[" + i + "]";
            uniformsMap.createUniform(name + ".modelMatrixIdx");
            uniformsMap.createUniform(name + ".materialIdx");
        }

        for (int i = 0; i < MAX_ENTITIES; i++) {
            uniformsMap.createUniform("modelMatrices[" + i + "]");
        }
    }
    ...
}

は、各エンティティが配置されているモデルに関連付けられたエンティティのリスト内のentitiesIdxMap位置を保存します。Mapその情報をエンティティ識別子をキーとして使用して保存します。間接描画コマンドは、各モデルに関連付けられたメッシュを反復して記録されるため、後でこの情報が必要になります。主な変更点はrenderメソッドにあり、次のように定義されています。

public class SceneRender {
    ...
    public void render(Scene scene, RenderBuffers renderBuffers, 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());

        TextureCache textureCache = scene.getTextureCache();
        List<Texture> textures = textureCache.getAll().stream().toList();
        int numTextures = textures.size();
        if (numTextures > MAX_TEXTURES) {
            Logger.warn("Only " + MAX_TEXTURES + " textures can be used");
        }
        for (int i = 0; i < Math.min(MAX_TEXTURES, numTextures); i++) {
            uniformsMap.setUniform("txtSampler[" + i + "]", i);
            Texture texture = textures.get(i);
            glActiveTexture(GL_TEXTURE0 + i);
            texture.bind();
        }

        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                uniformsMap.setUniform("modelMatrices[" + entityIdx + "]", entity.getModelMatrix());
                entityIdx++;
            }
        }

        // Static meshes
        int drawElement = 0;
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                for (Entity entity : entities) {
                    String name = "drawElements[" + drawElement + "]";
                    uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                    uniformsMap.setUniform(name + ".materialIdx", meshDrawData.materialIdx());
                    drawElement++;
                }
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBindVertexArray(renderBuffers.getStaticVaoId());
        glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, staticDrawCount, 0);
        glBindVertexArray(0);

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

テクスチャ サンプラーの配列をバインドし、すべてのテクスチャ ユニットをアクティブにする必要があることがわかります。それに加えて、エンティティを繰り返し処理し、モデル マトリックスに均一な値を設定します。drawElements次のステップは、モデル マトリックスのインデックスとマテリアル インデックスを指す各エンティティの適切な値を使用して、アレイ ユニフォームをセットアップすることです。その後、glMultiDrawElementsIndirect間接描画を行う機能。その前に、描画命令 (描画コマンド) を保持するバッファーと、メッシュとインデックス データを保持する VAO をバインドする必要があります。しかし、いつ間接描画用のバッファを設定するのでしょうか? 答えは、レンダリング コールごとにこれを実行する必要はないということです。エンティティの数に変化がない場合は、そのバッファを 1 回記録して、各レンダリング コールで使用できます。この特定の例では、起動時にそのバッファにデータを入力するだけです。つまり、エンティティの数を変更したい場合は、そのバッファを再度作成する必要があります (独自のエンジンに対して行う必要があります)。

間接描画バッファーを実際に構築するメソッドが呼び出さsetupStaticCommandBufferれ、次のように定義されます。

public class SceneRender {
    ...
    private void setupStaticCommandBuffer(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        int numMeshes = 0;
        for (Model model : modelList) {
            numMeshes += model.getMeshDrawDataList().size();
        }

        int firstIndex = 0;
        int baseInstance = 0;
        ByteBuffer commandBuffer = MemoryUtil.memAlloc(numMeshes * COMMAND_SIZE);
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            int numEntities = entities.size();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(numEntities);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

                firstIndex += meshDrawData.vertices();
                baseInstance += entities.size();
            }
        }
        commandBuffer.flip();

        staticDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

        staticRenderBufferHandle = glGenBuffers();
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);

        MemoryUtil.memFree(commandBuffer);
    }
    ...
}

最初にメッシュの総数を計算します。その後、間接描画命令を保持するバッファを作成して入力します。ご覧のとおり、最初に を割り当てますByteBuffer。このバッファは、メッシュと同じ数の命令セットを保持します。描画命令の各セットは 5 つの属性で構成され、それぞれの長さは 4 バイトです (パラメーターの各セットの合計の長さがCOMMAND_SIZE定数を定義します)。すぐにスペースが不足するため、このバッファーを使用して割り当てることはできませんMemoryStack(LWJGL がこれに使用するスタックのサイズは制限されています)。したがって、使用して割り当てる必要がありますMemoryUtil完了したら、手動で割り当てを解除することを忘れないでください。バッファを取得したら、モデルに関連付けられたメッシュの反復処理を開始します。この章の冒頭を見て、間接描画に必要な構造体を確認してください。それに加えて、各エンティティのモデル マトリックス インデックスを適切に取得するために、以前に計算した をdrawElements使用してユニフォームを設定します。Map最後に、GPU バッファーを作成し、データをそこにダンプします。

メソッドを更新しcleanupて間接描画バッファを解放する必要があります。

public class SceneRender {
    ...
    public void cleanup() {
        shaderProgram.cleanup();
        glDeleteBuffers(staticRenderBufferHandle);
    }
    ...
}

マテリアル ユニフォームの値を設定するには、新しいメソッドが必要になります。

public class SceneRender {
   private void setupMaterialsUniform(TextureCache textureCache, MaterialCache materialCache) {
        List<Texture> textures = textureCache.getAll().stream().toList();
        int numTextures = textures.size();
        if (numTextures > MAX_TEXTURES) {
            Logger.warn("Only " + MAX_TEXTURES + " textures can be used");
        }
        Map<String, Integer> texturePosMap = new HashMap<>();
        for (int i = 0; i < Math.min(MAX_TEXTURES, numTextures); i++) {
            texturePosMap.put(textures.get(i).getTexturePath(), i);
        }

        shaderProgram.bind();
        List<Material> materialList = materialCache.getMaterialsList();
        int numMaterials = materialList.size();
        for (int i = 0; i < numMaterials; i++) {
            Material material = materialCache.getMaterial(i);
            String name = "materials[" + i + "]";
            uniformsMap.setUniform(name + ".diffuse", material.getDiffuseColor());
            uniformsMap.setUniform(name + ".specular", material.getSpecularColor());
            uniformsMap.setUniform(name + ".reflectance", material.getReflectance());
            String normalMapPath = material.getNormalMapPath();
            int idx = 0;
            if (normalMapPath != null) {
                idx = texturePosMap.computeIfAbsent(normalMapPath, k -> 0);
            }
            uniformsMap.setUniform(name + ".normalMapIdx", idx);
            Texture texture = textureCache.getTexture(material.getTexturePath());
            idx = texturePosMap.computeIfAbsent(texture.getTexturePath(), k -> 0);
            uniformsMap.setUniform(name + ".textureIdx", idx);
        }
        shaderProgram.unbind();
    }
}

サポートされているテクスチャの最大数 ( MAX_TEXTURES) を超えていないことを確認し、前の章で使用した情報を使用してマテリアル情報の配列を作成するだけです。唯一の変更点は、関連するテクスチャと法線マップのインデックスをマテリアル情報に保存する必要があることです。

エンティティ インデックス マップを更新するには、別のメソッドが必要です。

public class SceneRender {
    ...
    private void setupEntitiesData(Scene scene) {
        entitiesIdxMap.clear();
        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                entitiesIdxMap.put(entity.getId(), entityIdx);
                entityIdx++;
            }
        }
    }
    ...
}

クラスの変更を完了するには、クラスから呼び出せるようにSceneRenderをラップするメソッドを作成します。setupXXRender

public class SceneRender {
    ...
    public void setupData(Scene scene) {
        setupEntitiesData(scene);
        setupStaticCommandBuffer(scene);
        setupMaterialsUniform(scene.getTextureCache(), scene.getMaterialCache());
    }
    ...
}

影のレンダリング プロセスも間接描画を使用するように変更します。頂点シェーダー ( ) の変更は非常に似ています。アニメーション情報は使用せず、組み込み変数とshadow.vertの組み合わせを使用して適切なモデル マトリックスにアクセスする必要があります。この場合、マテリアル情報は必要ないため、フラグメント シェーダー ( ) は変更されません。gl_BaseInstancegl_InstanceIDshadow.frag

#version 460

const int MAX_DRAW_ELEMENTS = 100;
const int MAX_ENTITIES = 50;

layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in vec3 tangent;
layout (location=3) in vec3 bitangent;
layout (location=4) in vec2 texCoord;

struct DrawElement
{
    int modelMatrixIdx;
};

uniform mat4 modelMatrix;
uniform mat4 projViewMatrix;
uniform DrawElement drawElements[MAX_DRAW_ELEMENTS];
uniform mat4 modelMatrices[MAX_ENTITIES];

void main()
{
    vec4 initPos = vec4(position, 1.0);
    uint idx = gl_BaseInstance + gl_InstanceID;
    int modelMatrixIdx = drawElements[idx].modelMatrixIdx;
    mat4 modelMatrix = modelMatrices[modelMatrixIdx];
    gl_Position = projViewMatrix * modelMatrix * initPos;
}

の変更ShadowRenderも、SceneRenderクラスの変更と非常によく似ています。

public class ShadowRender {

    private static final int COMMAND_SIZE = 5 * 4;
    ...
    private Map<String, Integer> entitiesIdxMap;
    ...
    private int staticRenderBufferHandle;
    ...
    public ShadowRender() {
        ...
        entitiesIdxMap = new HashMap<>();
    }

    public void cleanup() {
        shaderProgram.cleanup();
        shadowBuffer.cleanup();
        glDeleteBuffers(staticRenderBufferHandle);
    }

    private void createUniforms() {
        ...
        for (int i = 0; i < SceneRender.MAX_DRAW_ELEMENTS; i++) {
            String name = "drawElements[" + i + "]";
            uniformsMap.createUniform(name + ".modelMatrixIdx");
        }

        for (int i = 0; i < SceneRender.MAX_ENTITIES; i++) {
            uniformsMap.createUniform("modelMatrices[" + i + "]");
        }
    }
    ...
}

新しいユニフォームを使用するには、createUniformsメソッドを更新する必要がありcleanup、間接描画バッファーを解放する必要があります。renderメソッドは、メッシュとエンティティに対して個別の描画コマンドを送信する代わりに、 を使用するようになりましたglMultiDrawElementsIndirect。

public class ShadowRender {
    ...
    public void render(Scene scene, RenderBuffers renderBuffers) {
        CascadeShadow.updateCascadeShadows(cascadeShadows, scene);

        glBindFramebuffer(GL_FRAMEBUFFER, shadowBuffer.getDepthMapFBO());
        glViewport(0, 0, ShadowBuffer.SHADOW_MAP_WIDTH, ShadowBuffer.SHADOW_MAP_HEIGHT);

        shaderProgram.bind();

        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                uniformsMap.setUniform("modelMatrices[" + entityIdx + "]", entity.getModelMatrix());
                entityIdx++;
            }
        }

        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);
            glClear(GL_DEPTH_BUFFER_BIT);
        }

        // Static meshes
        int drawElement = 0;
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                for (Entity entity : entities) {
                    String name = "drawElements[" + drawElement + "]";
                    uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                    drawElement++;
                }
            }
        }
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBindVertexArray(renderBuffers.getStaticVaoId());
        for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);

            CascadeShadow shadowCascade = cascadeShadows.get(i);
            uniformsMap.setUniform("projViewMatrix", shadowCascade.getProjViewMatrix());

            glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, 0, staticDrawCount, 0);
        }
        glBindVertexArray(0);

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

最後に、間接描画バッファとエンティティ マップをセットアップする同様のメソッドが必要です。

public class ShadowRender {
    ...
    public void setupData(Scene scene) {
        setupEntitiesData(scene);
        setupStaticCommandBuffer(scene);
    }

    private void setupEntitiesData(Scene scene) {
        entitiesIdxMap.clear();
        int entityIdx = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            for (Entity entity : entities) {
                entitiesIdxMap.put(entity.getId(), entityIdx);
                entityIdx++;
            }
        }
    }

    private void setupStaticCommandBuffer(Scene scene) {
        List<Model> modelList = scene.getModelMap().values().stream().filter(m -> !m.isAnimated()).toList();
        Map<String, Integer> entitiesIdxMap = new HashMap<>();
        int entityIdx = 0;
        int numMeshes = 0;
        for (Model model : scene.getModelMap().values()) {
            List<Entity> entities = model.getEntitiesList();
            numMeshes += model.getMeshDrawDataList().size();
            for (Entity entity : entities) {
                entitiesIdxMap.put(entity.getId(), entityIdx);
                entityIdx++;
            }
        }

        int firstIndex = 0;
        int baseInstance = 0;
        int drawElement = 0;
        shaderProgram.bind();
        ByteBuffer commandBuffer = MemoryUtil.memAlloc(numMeshes * COMMAND_SIZE);
        for (Model model : modelList) {
            List<Entity> entities = model.getEntitiesList();
            int numEntities = entities.size();
            for (RenderBuffers.MeshDrawData meshDrawData : model.getMeshDrawDataList()) {
                // count
                commandBuffer.putInt(meshDrawData.vertices());
                // instanceCount
                commandBuffer.putInt(numEntities);
                commandBuffer.putInt(firstIndex);
                // baseVertex
                commandBuffer.putInt(meshDrawData.offset());
                commandBuffer.putInt(baseInstance);

                firstIndex += meshDrawData.vertices();
                baseInstance += entities.size();
                for (Entity entity : entities) {
                    String name = "drawElements[" + drawElement + "]";
                    uniformsMap.setUniform(name + ".modelMatrixIdx", entitiesIdxMap.get(entity.getId()));
                    drawElement++;
                }
            }
        }
        commandBuffer.flip();
        shaderProgram.unbind();

        staticDrawCount = commandBuffer.remaining() / COMMAND_SIZE;

        staticRenderBufferHandle = glGenBuffers();
        glBindBuffer(GL_DRAW_INDIRECT_BUFFER, staticRenderBufferHandle);
        glBufferData(GL_DRAW_INDIRECT_BUFFER, commandBuffer, GL_DYNAMIC_DRAW);

        MemoryUtil.memFree(commandBuffer);
    }
}

クラスでは、Renderクラスをインスタンス化し、間接描画バッファと関連データを作成するためにすべてのモデルとエンティティが作成されたときに呼び出すことができるRenderBuffers新しいメソッドを提供するだけです。setupData

public class Render {
    ...
    private RenderBuffers renderBuffers;
    ...
    public Render(Window window) {
        ...
        renderBuffers = new RenderBuffers();
    }

    public void cleanup() {
        ...
        renderBuffers.cleanup();
    }
    ...
    public void render(Window window, Scene scene) {
        shadowRender.render(scene, renderBuffers);
        sceneRender.render(scene, renderBuffers, gBuffer);
        ...
    }
    ...
    public void setupData(Scene scene) {
        renderBuffers.loadStaticModels(scene);
        renderBuffers.loadAnimatedModels(scene);
        sceneRender.setupData(scene);
        shadowRender.setupData(scene);
        List<Model> modelList = new ArrayList<>(scene.getModelMap().values());
        modelList.forEach(m -> m.getMeshDataList().clear());
    }
}

クラスを更新して、TextureCacheすべてのテクスチャを返すメソッドを提供する必要があります。

public class TextureCache {
    ...
    public Collection<Texture> getAll() {
        return textureMap.values();
    }
    ...
}

モデルとマテリアルを扱うクラス階層を変更したため、クラスを更新する必要がありますSkyBox(個々のモデルをロードするには、追加の手順が必要になります)。

public class SkyBox {

    private Material material;
    private Mesh mesh;
    ...
    public SkyBox(String skyBoxModelPath, TextureCache textureCache, MaterialCache materialCache) {
        skyBoxModel = ModelLoader.loadModel("skybox-model", skyBoxModelPath, textureCache, materialCache, false);
        MeshData meshData = skyBoxModel.getMeshDataList().get(0);
        material = materialCache.getMaterial(meshData.getMaterialIdx());
        mesh = new Mesh(meshData);
        skyBoxModel.getMeshDataList().clear();
        skyBoxEntity = new Entity("skyBoxEntity-entity", skyBoxModel.getId());
    }

    public void cleanuo() {
        mesh.cleanup();
    }

    public Material getMaterial() {
        return material;
    }

    public Mesh getMesh() {
        return mesh;
    }
    ...
}

これらの変更はクラスにも影響しSkyBoxRenderます。sky bos render では、間接描画は使用しません (メッシュを 1 つだけレンダリングするため、使用する価値はありません)。

public class SkyBoxRender {
    ...
    public void render(Scene scene) {
        SkyBox skyBox = scene.getSkyBox();
        if (skyBox == null) {
            return;
        }
        shaderProgram.bind();

        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        viewMatrix.set(scene.getCamera().getViewMatrix());
        viewMatrix.m30(0);
        viewMatrix.m31(0);
        viewMatrix.m32(0);
        uniformsMap.setUniform("viewMatrix", viewMatrix);
        uniformsMap.setUniform("txtSampler", 0);

        Entity skyBoxEntity = skyBox.getSkyBoxEntity();
        TextureCache textureCache = scene.getTextureCache();
        Material material = skyBox.getMaterial();
        Mesh mesh = skyBox.getMesh();
        Texture texture = textureCache.getTexture(material.getTexturePath());
        glActiveTexture(GL_TEXTURE0);
        texture.bind();

        uniformsMap.setUniform("diffuse", material.getDiffuseColor());
        uniformsMap.setUniform("hasTexture", texture.getTexturePath().equals(TextureCache.DEFAULT_TEXTURE) ? 0 : 1);

        glBindVertexArray(mesh.getVaoId());

        uniformsMap.setUniform("modelMatrix", skyBoxEntity.getModelMatrix());
        glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);

        glBindVertexArray(0);

        shaderProgram.unbind();
    }
    ...
}

クラスではScene、メソッドを呼び出す必要はありませんScene cleanup(バッファに関連付けられたデータがRenderBuffersクラスにあるため)。

public class Engine {
    ...
    private void cleanup() {
        appLogic.cleanup();
        render.cleanup();
        window.cleanup();
    }
    ...
}

最後に、Mainクラスで、キューブ モデルに関連付けられた 2 つのエンティティを読み込みます。それらを個別にローテーションして、コードが正常に機能することを確認します。最も重要な部分は、すべてがロードされたときにRenderクラスメソッドを呼び出すことです。setupData

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-20", opts, main);
        ...
    }

    public void init(Window window, Scene scene, Render render) {
        ...
        Model terrainModel = ModelLoader.loadModel(terrainModelId, "resources/models/terrain/terrain.obj",
                scene.getTextureCache(), scene.getMaterialCache(), false);
        ...
        Model cubeModel = ModelLoader.loadModel("cube-model", "resources/models/cube/cube.obj",
                scene.getTextureCache(), scene.getMaterialCache(), false);
        scene.addModel(cubeModel);
        cubeEntity1 = new Entity("cube-entity-1", cubeModel.getId());
        cubeEntity1.setPosition(0, 2, -1);
        cubeEntity1.updateModelMatrix();
        scene.addEntity(cubeEntity1);

        cubeEntity2 = new Entity("cube-entity-2", cubeModel.getId());
        cubeEntity2.setPosition(-2, 2, -1);
        cubeEntity2.updateModelMatrix();
        scene.addEntity(cubeEntity2);

        render.setupData(scene);
        ...
        SkyBox skyBox = new SkyBox("resources/models/skybox/skybox.obj", scene.getTextureCache(),
                scene.getMaterialCache());
        ...
    }
    ...
    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: 第 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();
    }
}

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