Java 3D LWJGL GitBook 〜Chapter11:ライト~

第11章 - ライト

この章では、3D ゲーム エンジンにライトを追加する方法を学習します。複雑さを除けば、膨大な量のコンピューター リソースが必要になるため、物理的に完全なライト モデルは実装しません。代わりに、適切な結果を提供する近似を実装します。フォン シェーディング (Bui Tuong Phong によって開発された) という名前のアルゴリズムを使用します。指摘すべきもう 1 つの重要な点は、ライトのみをモデル化し、それらのライトによって生成されるシャドウをモデル化しないことです (これは別の章で行います)。

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

ライトの考え方(理論編)

はじめに、ライトの考え方に関しての記述があり、その後実装に関しての記述があります。

そして、「モデル読み込みの変更」の項目部分がとても重要に思えます。大まかに次のような変更を行っています。

  1. ライトクラスの作成=ライトのモデル(データクラス)を作成する
  2. Sceneクラスにライトクラスを格納する
  3. ModelLoader(3DModelの読み込みクラス)の変更
    ・法線データの読み込み
    ・アンビエント カラー、スペキュラ カラー、光沢係数を取得
  4. 上記の変更に伴い修正する必要があるクラスの修正

ImGuiに関して

このサンプルコードでは、ImGuiの実装部分はライトコントロールクラスに実装されています。
次のように、IGuiInstanceインターフェースを実装(implements)しているクラスがImGuiをコントロールするクラスになります。

class XXX implements IGuiInstance

そして、IAppLogicインターフェースの実装クラス=Mainクラスで入力があったときに何かしらの処理をする予定だと思いますが、このサンプルでは何も実装していませんでした。

サンプルプログラムの実行結果

いくつかの概念

始める前に、いくつかのライト タイプを定義しましょう。

  • ・ポイント ライト: このタイプのライトは、空間内の 1 点から全方向に均一に放出される光源をモデル化します。
  • ・スポット ライト: このタイプのライトは、空間内の 1 点から放射される光源をモデル化しますが、すべての方向に放射するのではなく、円錐に制限されます。
  • ・指向性ライト: このタイプのライトは、太陽から受け取る光をモデル化します。3D 空間内のすべてのオブジェクトは、特定の方向から来る平行* ・光線ライトに当てられます。オブジェクトが近くにあるか遠くにあるかに関係なく、すべてのレイ ライトは同じ角度でオブジェクトに影響を与えます。
  • ・環境光: このタイプの光は、空間のあらゆる場所から来て、すべてのオブジェクトを同じように照らします。

したがって、ライトをモデル化するには、ライトのタイプ、その位置、および色などのその他のパラメータを考慮する必要があります。もちろん、レイ ライトの影響を受けたオブジェクトが光を吸収および反射する方法も考慮する必要があります。

フォン シェーディング アルゴリズムは、モデルの各ポイント、つまりすべての頂点の光の効果をモデル化します。これがローカル イルミネーション シミュレーションと呼ばれる理由であり、このアルゴリズムが影を計算しない理由です。頂点が光をブロックするオブジェクトの背後にあるかどうかを考慮せずに、すべての頂点に適用される光を計算するだけです。 . 後の章でこの欠点を克服します。しかし、そのため、非常に優れた効果を提供するシンプルで高速なアルゴリズムです。ここでは、材料を深く考慮しない単純化したバージョンを使用します。

Phong アルゴリズムは、ライティングの 3 つのコンポーネントを考慮します。

3 つのコンポーネント

  • ・環境光: どこからでも来る光をモデル化します。これは、光が当たっていない領域を (必要な強度で) 照らすのに役立ちます。これは背景光のようなものです。
  • ・拡散反射率: 光源に面している表面がより明るいことを考慮します。
  • ・鏡面反射率: 研磨面または金属面で光がどのように反射するかをモデル化します。

最後に取得したいのは、フラグメントに割り当てられた色を掛けて、受ける光に応じてその色を明るくまたは暗く設定する係数です。コンポーネントに名前を付けましょう

つけた名前

  • A: アンビエント
  • D: 拡散反射光
  • S: スペキュラ

実際、これらのコンポーネントは色であり、各光コンポーネントが寄与する色コンポーネントです。これは、光コンポーネントがある程度の強度を提供するだけでなく、モデルの色を変更できるという事実によるものです。フラグメント シェーダーでは、その明るい色を元のフラグメント カラー (テクスチャまたはベース カラーから取得) で乗算するだけです。

アンビエント、ディフューズ、スペキュラー コンポーネントで使用される、同じマテリアルに異なる色を割り当てることもできます。したがって、これらのコンポーネントは、マテリアルに関連付けられた色によって調整されます。マテリアルにテクスチャがある場合は、コンポーネントごとに 1 つのテクスチャを使用します。

したがって、非テクスチャ マテリアルの最終的な色は次のようになります。

L = A anbientColor + D diffuseColor + S specularColor

テクスチャ マテリアルの最終的な色は次のようになります。

L = A textureColor + D textureColor + S textureColor

法線

法線は、ライトを操作するときの ket 要素です。まず定義しましょう。平面の法線は、長さが 1 に等しい平面に垂直なベクトルです。

上の図からわかるように、平面には 2 つの法線があります。どちらを使用する必要がありますか? 3D グラフィックスの法線は照明に使用されるため、光源に向けられた法線を選択する必要があります。言い換えれば、モデルの外面から突き出ている法線を選択する必要があります。

3D モデルがある場合、ポリゴン、この場合は三角形で構成されます。各三角形は 3 つの頂点で構成されます。三角形の法線ベクトルは、長さが 1 に等しい三角形の表面に垂直なベクトルになります。

頂点法線は特定の頂点に関連付けられており、周囲の三角形の法線の組み合わせです (もちろん、その長さは 1 です)。ここでは、3D メッシュの頂点モデルを確認できます (ウィキペディアから取得) 。

拡散反射率

拡散反射率について話しましょう。これは、光源に対して垂直に面している面が、より間接的な角度で光を受けている面よりも明るく見えるという事実をモデル化しています。これらのオブジェクトはより多くの光を受け取り、光の密度 (このように呼びましょう) が高くなります。

しかし、これをどのように計算するのでしょうか。ここで、まず法線の使用を開始します。前の図の 3 点の法線を描きましょう。ご覧のとおり、各ポイントの法線は、各ポイントの接平面に垂直なベクトルになります。光源から来る光線を描く代わりに、各点から光の点へのベクトルを描きます (つまり、反対方向)。

ご覧のとおり、に関連付けられている法線

、名前付き

P1, N1

等しい角度を持つ

P1, 0

光源を指すベクトルを使用します。その表面は光源に対して垂直であり、一番明るいポイントになります。

P1

関連付けられている法線

P2

、名前付き

N2

、光源を指すベクトルと約 30 度の角度を持っているため、より暗い黄褐色になるはずです。

P1, P3

. 最後に、関連付けられている法線

P3

、名前付き

N3

も光源を指すベクトルに平行ですが、2 つのベクトルは反対方向です。

P3

は、光源を指すベクトルと 180 度の角度を持ち、まったく光を取得しないはずです。
したがって、点に到達する光の強度を決定するための適切なアプローチがあるようです。これは、光源を指すベクトルで法線を形成する角度に関連しています。これをどのように計算できますか?

内積という算術演算

内積という算術演算を使用できます。この操作は 2 つのベクトルを取り、それらの間の角度が鋭角の場合は正の数値 (スカラー) を生成し、それらの間の角度が広い場合は負の数値を生成します。両方のベクトルが正規化されている場合、つまり両方の長さが 1 の場合、内積は次のようになります。

-1 と 1

両方のベクトルがまったく同じ方向 (角度) を向いている場合、内積は 1 になります。
); そうなる 0 両方のベクトルが正方形の角度を形成する場合、それは -1
両方のベクトルが反対方向を指している場合。2 つのベクトルを定義しましょう。

v1 と v2

、そしてみましょう

alpha

それらの間の角度になります。内積は次の式で定義されます。

両方のベクトルが正規化されている場合、それらの長さ、モジュールは 1 に等しいため、内積はそれらの間の角度の余弦に等しくなります。この操作を使用して、拡散反射率コンポーネントを計算します。

したがって、光源を指すベクトルを計算する必要があります。これをどのように行うのですか?各点の位置 (頂点の位置) と光源の位置があります。まず、両方の座標が同じ座標空間にある必要があります。簡単にするために、それらが両方ともワールド座標空間にあると仮定しましょう。これらの位置は、頂点位置 ($$VP$$) と光源 ($$VS$$) を指すベクトルの座標です。次の図に示します。

差し引くと

V

探しているベクトルを取得します

L

これで、光源を指すベクトルと法線の間の内積を計算できます。この積は、表面の明るさをモデル化するためにその関係を最初に提案した Johann Lambert にちなんで、Lambert 項と呼ばれます。

計算方法をまとめてみます。次の変数を定義します。

計算方法をまとめ

vPos: モデル ビュー空間座標での頂点の位置。

lPos: ビュー空間座標でのライトの位置。

intensity: 光の強度 (0 から 1)。

lColor: 光の色。

normal: 頂点法線。

まず、現在の位置から光源を指すベクトルを計算する必要があります。

toLightDirection = lPos - vPos

. その操作の結果は正規化する必要があります。

次に、拡散係数 (スカラー) を計算する必要があります。

defuseFuctor = normal - toLightDirection

2 つのベクトル間の内積として計算されます。-1と1両方のベクトルを正規化する必要があります。色は間にある必要があります0と1
したがって、値がより低い場合0 に設定します。
最後に、拡散係数と光の強度によって光の色を調整する必要があります。

color = diffuseColor * lColor * diffuseColor * intensity

鏡面成分

鏡面反射光コンポーネントを検討する前に、まず光がどのように反射されるかを調べる必要があります。光が表面に当たると、その一部が吸収され、他の部分が反射されます。物理の授業で思い出したように、反射とは、光が物体から跳ね返ることです。

もちろん、表面は完全に磨かれているわけではなく、近くで見ると多くの欠陥が見られます。それに加えて、多くのレイ ライト (実際にはフォトン) があり、そのサーフェスに影響を与え、さまざまな角度で反射します。したがって、私たちが見ているのは、表面から反射された光線のようなものです。つまり、光は表面に当たると拡散します。これが、前に説明した拡散コンポーネントです。

しかし、金属などの研磨された表面に光が当たると、光の拡散が低下し、その表面に当たるとほとんどが反対方向に反射されます。

これはスペキュラ コンポーネントがモデル化するものであり、マテリアルの特性に依存します。鏡面反射率に関しては、カメラが適切な位置にある場合、つまり反射光が放出される領域にある場合にのみ、反射光が見えることに注意することが重要です。

鏡面反射の背後にあるメカニズムが説明されたので、その成分を計算する準備が整いました。まず、光源から頂点を指すベクトルが必要です。ディフューズ コンポーネントを計算していたとき、正反対の、光源を指すベクトルを計算しました。toLightDirectionですので、次のように計算してみましょう。

fromLightDirection = -(toLightDirection)

次に、衝撃による反射光を計算する必要があります。fromLightDirection
法線を考慮してサーフェスに挿入します。reflectまさにそれを行うGLSL 関数があります。そう、

reflectedLight = reflect(toLightSource, normal)

カメラを指すベクトルも必要です。名前を付けましょうcameraDirection
となり、カメラ位置と頂点位置の差として計算されます。

cameraDirection = cameraPos - vPos

. カメラ位置ベクトルと頂点位置は同じ座標系にある必要があり、結果のベクトルを正規化する必要があります。次の図は、これまでに計算した主なコンポーネントをスケッチしたものです。

次に、私たちが見る光の強度を計算する必要があります。specularFactor
. このコンポーネントは、cameraDirection
そしてそのreflectedLight
ベクトルは平行で同じ方向を指し、反対方向を指している場合はより低い値を取ります。これを計算するために、内積が再び役に立ちます。そう

specularFactor = cameraDirection - reflectedLight

. この値が間にあることのみが必要です0と1
それよりも低い場合0
に設定されます。
カメラが反射光円錐を指している場合、この光はより強くなければならないことも考慮する必要があります。これは、specularFactorという名前のパラメーターにspecularPower

specularFactor = specularFactor specularPower

最後に、マテリアルの反射率をモデル化する必要があります。これは、光が反射した場合の強度も変調します。これは、reflectance という名前の別のパラメーターで行われます。したがって、鏡面反射光コンポーネントの色は次のようになります。

specularColor * lColor * refrectance * specularFactor * intensity

減衰

これで、アンビエント ライトを使用してポイント ライトをモデル化するのに役立つ 3 つのコンポーネントを計算する方法がわかりました。しかし、オブジェクトが反射する光は光源からの距離に依存しないため、ライト モデルはまだ完全ではありません。つまり、光の減衰をシミュレートする必要があります。

減衰は、距離と光の関数です。光の強さは距離の二乗に反比例します。光はそのエネルギーを球の表面に沿って伝搬し、その半径は光が移動した距離と同じであり、球の表面はその半径の 2 乗に比例するため、この事実は簡単に視覚化できます。減衰係数は次の式で計算できます。

1.0 / (atConstant + atLinar * dist + atExponent * dist 2

減衰をシミュレートするには、その減衰係数を最終的な色で乗算するだけです。

指向性ライト

指向性照明は、すべて同じ方向から来る平行光線によってすべてのオブジェクトに当たります。太陽のように遠くにあるが強度の高い光源をモデル化します。

ディレクショナル ライトのもう 1 つの特徴は、減衰の影響を受けないことです。太陽光についてもう一度考えてみてください。太陽光線が当たったすべてのオブジェクトは、同じ強度で照らされます。太陽からの距離が非常に大きいため、オブジェクトの位置は関係ありません。実際、ディレクショナル ライトは無限遠に配置された光源としてモデル化されており、減衰の影響を受けた場合、どのオブジェクトにも影響しません (その色の寄与は0)。
それに加えて、ディレクショナル ライトは、ディフューズ コンポーネントとスペキュラ コンポーネントによっても構成されます。ポイント ライトとの唯一の違いは、位置ではなく方向があり、減衰の影響を受けないことです。ディレクショナル ライトの方向アトリビュートに戻り、3D ワールド全体の太陽の動きをモデリングしていると想像してください。北が増加する z 軸に向かって配置されていると仮定すると、次の図は、夜明け、日中、および夕暮れ時の光源の方向を示しています。

スポットライト

ここで、ポイント ライトに非常に似ているスポット ライトを実装しますが、放射されるライトは 3D コーンに制限されます。焦点から出る光、またはすべての方向に放射しないその他の光源をモデル化します。スポット ライトはポイント ライトと同じアトリビュートを持ちますが、円錐角度と円錐方向という 2 つの新しいパラメータが追加されています。

スポット ライトの影響は、いくつかの例外を除いて、ポイント ライトと同じ方法で計算されます。頂点位置から光源を指すベクトルがライト コーン内に含まれないポイントは、ポイント ライトの影響を受けません。

光円錐の内側にあるかどうかをどのように計算しますか? 光源からのベクトルとコーン方向ベクトル (どちらも正規化されています) の間で内積を再度行う必要があります。

間の内積:LとCベクトルは次の通りです。

L C = | L | | C | C o s ( α )

. スポット ライトの定義でカットオフ角度のコサインを格納すると、内積がその値よりも大きい場合、それがライト コーンの内側にあることがわかります (コサイン グラフを思い出してください。
角度はα、余弦は0、角度が小さいほど余弦が大きくなります)。1
2 番目の違いは、円錐ベクトルから遠く離れたポイントは、より少ない光を受け取ります。つまり、減衰が高くなります。これを計算するにはいくつかの方法があります。減衰に次の係数を掛けることにより、単純なアプローチを選択します。

1 ( 1 C o s ( α ) ) / ( 1 C o s ( c u t O f f A n g l e )

(フラグメント シェーダーでは角度ではなく、カットオフ角度のコサインを使用します。上記の式が 0 から 1 までの値を生成することを確認できます。角度がカットオフ角度と等しい場合は 0、角度が等しい場合は 1 です。 0)。

スポット ライトのサンプル

ライト クラスの実装

モデルというのはここでは、GetterとSetterを持っているデータクラスのことを指していると思ってよさそうです。

データクラスに関して

フィールド変数とGetter, Setterを持ったクラスは具体的に次のようなものです。
下のクラスは、「身長(tall)」というデータを持った、データクラスです。

public class Body {
    /** 整数で身長を表す */
    private int tall;
    /** Getter */
    public int getTall() { return tall;}
    /** Setter */
    public void setTall(int tall) {this.tall = tall}
}

PointLightクラス

まず、さまざまなタイプのライトをモデル化する一連のクラスを作成することから始めましょう。ポイント ライトをモデル化するクラスから始めます。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class PointLight {

    private Attenuation attenuation;
    private Vector3f color;
    private float intensity;
    private Vector3f position;

    public PointLight(Vector3f color, Vector3f position, float intensity) {
        attenuation = new Attenuation(0, 0, 1);
        this.color = color;
        this.position = position;
        this.intensity = intensity;
    }

    public Attenuation getAttenuation() {
        return attenuation;
    }

    public Vector3f getColor() {
        return color;
    }

    public float getIntensity() {
        return intensity;
    }

    public Vector3f getPosition() {
        return position;
    }

    public void setAttenuation(Attenuation attenuation) {
        this.attenuation = attenuation;
    }

    public void setColor(Vector3f color) {
        this.color = color;
    }

    public void setColor(float r, float g, float b) {
        color.set(r, g, b);
    }

    public void setIntensity(float intensity) {
        this.intensity = intensity;
    }

    public void setPosition(float x, float y, float z) {
        position.set(x, y, z);
    }

    public static class Attenuation {

        private float constant;
        private float exponent;
        private float linear;

        public Attenuation(float constant, float linear, float exponent) {
            this.constant = constant;
            this.linear = linear;
            this.exponent = exponent;
        }

        public float getConstant() {
            return constant;
        }

        public float getExponent() {
            return exponent;
        }

        public float getLinear() {
            return linear;
        }

        public void setConstant(float constant) {
            this.constant = constant;
        }

        public void setExponent(float exponent) {
            this.exponent = exponent;
        }

        public void setLinear(float linear) {
            this.linear = linear;
        }
    }
}

ご覧のとおり、ポイント ライトは、色、強度、位置、および減衰モデルによって定義されます。
環境光は、色と強度だけで定義されます。

AmbientLightクラス

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class AmbientLight {

    private Vector3f color;

    private float intensity;

    public AmbientLight(float intensity, Vector3f color) {
        this.intensity = intensity;
        this.color = color;
    }

    public AmbientLight() {
        this(1.0f, new Vector3f(1.0f, 1.0f, 1.0f));
    }

    public Vector3f getColor() {
        return color;
    }

    public float getIntensity() {
        return intensity;
    }

    public void setColor(Vector3f color) {
        this.color = color;
    }

    public void setColor(float r, float g, float b) {
        color.set(r, g, b);
    }

    public void setIntensity(float intensity) {
        this.intensity = intensity;
    }
}

DirLightクラス

指向性ライトは次のように定義されます。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class DirLight {

    private Vector3f color;

    private Vector3f direction;

    private float intensity;

    public DirLight(Vector3f color, Vector3f direction, float intensity) {
        this.color = color;
        this.direction = direction;
        this.intensity = intensity;
    }

    public Vector3f getColor() {
        return color;
    }

    public Vector3f getDirection() {
        return direction;
    }

    public float getIntensity() {
        return intensity;
    }

    public void setColor(Vector3f color) {
        this.color = color;
    }

    public void setColor(float r, float g, float b) {
        color.set(r, g, b);
    }

    public void setDirection(Vector3f direction) {
        this.direction = direction;
    }

    public void setIntensity(float intensity) {
        this.intensity = intensity;
    }

    public void setPosition(float x, float y, float z) {
        direction.set(x, y, z);
    }
}

SpotLightクラス

最後に、スポット ライトには、ポイント ライト リファレンスとライト コーン パラメータが含まれます。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

public class SpotLight {

    private Vector3f coneDirection;
    private float cutOff;
    private float cutOffAngle;
    private PointLight pointLight;

    public SpotLight(PointLight pointLight, Vector3f coneDirection, float cutOffAngle) {
        this.pointLight = pointLight;
        this.coneDirection = coneDirection;
        this.cutOffAngle = cutOffAngle;
        setCutOffAngle(cutOffAngle);
    }

    public Vector3f getConeDirection() {
        return coneDirection;
    }

    public float getCutOff() {
        return cutOff;
    }

    public float getCutOffAngle() {
        return cutOffAngle;
    }

    public PointLight getPointLight() {
        return pointLight;
    }

    public void setConeDirection(float x, float y, float z) {
        coneDirection.set(x, y, z);
    }

    public void setConeDirection(Vector3f coneDirection) {
        this.coneDirection = coneDirection;
    }

    public final void setCutOffAngle(float cutOffAngle) {
        this.cutOffAngle = cutOffAngle;
        cutOff = (float) Math.cos(Math.toRadians(cutOffAngle));
    }

    public void setPointLight(PointLight pointLight) {
        this.pointLight = pointLight;
    }
}

SceneLightsクラス

すべてのライトは Scene クラスに格納されます。そのために、すべてのタイプのライトへの参照を格納する という名前の新しいクラスを作成しSceneLightsます (1 つのアンビエント ライト インスタンスと 1 つのディレクショナル ライトのみが必要であることに注意してください)。

package org.lwjglb.engine.scene.lights;

import org.joml.Vector3f;

import java.util.*;

public class SceneLights {

    private AmbientLight ambientLight;
    private DirLight dirLight;
    private List<PointLight> pointLights;
    private List<SpotLight> spotLights;

    public SceneLights() {
        ambientLight = new AmbientLight();
        pointLights = new ArrayList<>();
        spotLights = new ArrayList<>();
        dirLight = new DirLight(new Vector3f(1, 1, 1), new Vector3f(0, 1, 0), 1.0f);
    }

    public AmbientLight getAmbientLight() {
        return ambientLight;
    }

    public DirLight getDirLight() {
        return dirLight;
    }

    public List<PointLight> getPointLights() {
        return pointLights;
    }

    public List<SpotLight> getSpotLights() {
        return spotLights;
    }

    public void setSpotLights(List<SpotLight> spotLights) {
        this.spotLights = spotLights;
    }
}

Sceneクラス

クラスSceneLightsには次の参照があります。Scene

public class Scene {
    ...
    private SceneLights sceneLights;
    ...
    public SceneLights getSceneLights() {
        return sceneLights;
    }
    ...
    public void setSceneLights(SceneLights sceneLights) {
        this.sceneLights = sceneLights;
    }
}

モデル読み込みの変更

ModelLoaderクラスを次のように変更する必要があります。

・マテリアルのより多くのプロパティ、特にアンビエント カラー、スペキュラ カラー、光沢係数を取得します。
・各メッシュの法線データを読み込みます。
マテリアルのより多くのプロパティを取得するには、processMaterialメソッドを変更する必要があります。

ModelLoaderクラス

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

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

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

            result = aiGetMaterialColor(aiMaterial, AI_MATKEY_COLOR_SPECULAR, aiTextureType_NONE, 0,
                    color);
            if (result == aiReturn_SUCCESS) {
                material.setSpecularColor(new Vector4f(color.r(), color.g(), color.b(), color.a()));
            }
            float reflectance = 0.0f;
            float[] shininessFactor = new float[]{0.0f};
            int[] pMax = new int[]{1};
            result = aiGetMaterialFloatArray(aiMaterial, AI_MATKEY_SHININESS_STRENGTH, aiTextureType_NONE, 0, shininessFactor, pMax);
            if (result != aiReturn_SUCCESS) {
                reflectance = shininessFactor[0];
            }
            material.setReflectance(reflectance);

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

            return material;
        }
    }
    ...

ご覧のとおり、プロパティを取得することでマテリアルのアンビエント カラーを取得しAI_MATKEY_COLOR_AMBIENTます。プロパティを使用してスペキュラー カラーを取得しAI_MATKEY_COLOR_SPECULARます。光沢はAI_MATKEY_SHININESS_STRENGTHフラグを使用して照会されます。

法線をロードするには、という名前の新しいメソッドを作成し、メソッドprocessNormalsで呼び出す必要がありますprocessMesh。

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

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

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

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

Materialクラス

ご覧のとおり、新しい情報を格納するためにMaterialおよびMeshクラスも変更する必要があります。クラスの変更点は次のMaterialとおりです。

public class Material {
    ...
    private Vector4f ambientColor;
    ...
    private float reflectance;
    private Vector4f specularColor;
    ...
    public Material() {
        ...
        ambientColor = DEFAULT_COLOR;
        ...
    } 
    ...
    public Vector4f getAmbientColor() {
        return ambientColor;
    }
    ...
    public float getReflectance() {
        return reflectance;
    }

    public Vector4f getSpecularColor() {
        return specularColor;
    }
    ...
    public void setAmbientColor(Vector4f ambientColor) {
        this.ambientColor = ambientColor;
    }
    ...
    public void setReflectance(float reflectance) {
        this.reflectance = reflectance;
    }

    public void setSpecularColor(Vector4f specularColor) {
        this.specularColor = specularColor;
    }
    ...
}

Meshクラス

Meshクラスは法線データの新しい float 配列を受け入れるようになり、そのために新しい VBO を作成します。

public class Mesh {
    ...
    public Mesh(float[] positions, float[] normals, float[] textCoords, int[] indices) {
        ...
            // Normals VBO
            vboId = glGenBuffers();
            vboIdList.add(vboId);
            FloatBuffer normalsBuffer = stack.callocFloat(normals.length);
            normalsBuffer.put(0, normals);
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, normalsBuffer, GL_STATIC_DRAW);
            glEnableVertexAttribArray(1);
            glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);

            // Texture coordinates VBO
            ...
            glEnableVertexAttribArray(2);
            glVertexAttribPointer(2, 2, GL_FLOAT, false, 0, 0);

            // Index VBO
            ...
        ...
    }
    ...
}

ライトでレンダリングする

レンダリング中にライトを使用する時が来ました。シェーダー、特に頂点シェーダー ( scene.vert)から始めましょう。

#version 330

layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in vec2 texCoord;

out vec3 outPosition;
out vec3 outNormal;
out vec2 outTextCoord;

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

void main()
{
    mat4 modelViewMatrix = viewMatrix * modelMatrix;
    vec4 mvPosition =  modelViewMatrix * vec4(position, 1.0);
    gl_Position = projectionMatrix * mvPosition;
    outPosition = mvPosition.xyz;
    outNormal = normalize(modelViewMatrix * vec4(normal, 0.0)).xyz;
    outTextCoord = texCoord;
}

ご覧のとおり、別の入力属性として通常のデータがあり、そのデータをフラグメント シェーダーに渡すだけです。フラグメント シェーダーの説明を続ける前に、強調しなければならない非常に重要な概念があります。mvVertexNormal上記のコードから、変数には頂点法線が含まれ、モデル ビュー空間座標に変換されることがわかります。これは、 に頂点位置を掛けることnormalによって行われます。modelViewMatrixただし、微妙な違いがあります。その頂点法線の w コンポーネントは、行列を乗算する前に 0 に設定されます。vec4(vertexNormal, 0.0). なぜこれを行うのですか?法線を回転およびスケーリングしたいが、平行移動したくないため、関心があるのはその方向だけであり、その位置には関心がありません。これは w コンポーネントを 0 に設定することで実現され、同次座標を使用する利点の 1 つです。w コンポーネントを設定することで、適用される変換を制御できます。行列の乗算を手動で行うことができ、これが発生する理由を確認できます。

フラグメント シェーダーの変更scene.fragは非常に複雑です。1 つずつステップを進めていきましょう。

<scene.frag>

#version 330

const int MAX_POINT_LIGHTS = 5;
const int MAX_SPOT_LIGHTS = 5;
const float SPECULAR_POWER = 10;

in vec3 outPosition;
in vec3 outNormal;
in vec2 outTextCoord;

out vec4 fragColor;
...

最初に、サポートするポント ライトとスポット ライトの最大数に対する定数の最大値を定義します。これらのライトのデータは、コンパイル時に適切に定義されたサイズを持つ必要があるユニフォームの配列として渡されるため、これが必要です。また、頂点シェーダーから通常のデータを受け取っていることもわかります。その後、ライト データをモデル化する構造体を定義します。

...
struct Attenuation
{
    float constant;
    float linear;
    float exponent;
};
struct Material
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float reflectance;
};
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;
};
...

その後、ライト データの新しいユニフォームを定義します。

...
uniform sampler2D txtSampler;
uniform Material material;
uniform AmbientLight ambientLight;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform SpotLight spotLights[MAX_SPOT_LIGHTS];
uniform DirLight dirLight
...

次に、周囲光から始めて、各ライト タイプの効果を計算する関数をいくつか定義します。

...
vec4 calcAmbient(AmbientLight ambientLight, vec4 ambient) {
    return vec4(ambientLight.factor * ambientLight.color, 1) * ambient;
}
...

ご覧のとおり、マテリアルのアンビエント カラーに適用される係数によってアンビエント ライトの色を変調するだけです。ここで、さまざまな種類のライトに対してカラー ライトを計算する方法を定義する関数を定義します。

...
vec4 calcLightColor(vec4 diffuse, vec4 specular, 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 * material.reflectance * vec4(lightColor, 1.0);

    return (diffuseColor + specColor);
}
...

前のコードは比較的単純で、拡散コンポーネントの色を計算し、鏡面コンポーネントの別の色を計算し、処理中の頂点への移動中に光が受ける減衰によって変調します。これで、各タイプのライトに対して呼び出される関数を定義できます。ポイント ライトから始めます。

...
vec4 calcPointLight(vec4 diffuse, vec4 specular, 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, 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, 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, light.pl, position, normal);
        color *= (1.0 - (1.0 - spot_alfa)/(1.0 - light.cutoff));
    }
    return color;
}
...

手順は、光の円錐の内側にいるかどうかを制御する必要があることを除いて、ポイント ライトに似ています。先に説明したように、円錐状の光の内側にも減衰を適用する必要があります。最後に、ディレクショナル ライトの関数を以下に定義します。

...
vec4 calcDirLight(vec4 diffuse, vec4 specular, DirLight light, vec3 position, vec3 normal) {
    return calcLightColor(diffuse, specular, light.color, light.intensity, position, normalize(light.direction), normal);
}
...

SceneRenderクラス

この場合、光の方向はすでにわかっており、減衰がないため、光の位置を考慮する必要はありません。最後に、mainメソッドでは、最終的なフラグメント カラーの拡散鏡面コンポーネントに寄与するさまざまなライト タイプを反復処理します。

public class SceneRender {

    private static final int MAX_POINT_LIGHTS = 5;
    private static final int MAX_SPOT_LIGHTS = 5;
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("material.ambient");
        uniformsMap.createUniform("material.diffuse");
        uniformsMap.createUniform("material.specular");
        uniformsMap.createUniform("material.reflectance");
        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");
    }
    ...
}

配列を使用している場合、リストの各要素に対してユニフォームを作成する必要があります。たとえば、pointLights
pointLights[0]、などの統一された名前の配列を作成する必要がありますpointLights[1]。もちろん、これは構造体の属性にも変換されるため、、などになりpointLights[0].colorますpointLights[1], color。

ダー呼び出しごとにライトのユニフォームを更新する新しいメソッドを作成します。このメソッドにupdateLightsは次のように名前が付けられ、定義されます。

public class SceneRender {
    ...
    private void updateLights(Scene scene) {
        Matrix4f viewMatrix = scene.getCamera().getViewMatrix();

        SceneLights sceneLights = scene.getSceneLights();
        AmbientLight ambientLight = sceneLights.getAmbientLight();
        uniformsMap.setUniform("ambientLight.factor", ambientLight.getIntensity());
        uniformsMap.setUniform("ambientLight.color", ambientLight.getColor());

        DirLight dirLight = sceneLights.getDirLight();
        Vector4f auxDir = new Vector4f(dirLight.getDirection(), 0);
        auxDir.mul(viewMatrix);
        Vector3f dir = new Vector3f(auxDir.x, auxDir.y, auxDir.z);
        uniformsMap.setUniform("dirLight.color", dirLight.getColor());
        uniformsMap.setUniform("dirLight.direction", dir);
        uniformsMap.setUniform("dirLight.intensity", dirLight.getIntensity());

        List<PointLight> pointLights = sceneLights.getPointLights();
        int numPointLights = pointLights.size();
        PointLight pointLight;
        for (int i = 0; i < MAX_POINT_LIGHTS; i++) {
            if (i < numPointLights) {
                pointLight = pointLights.get(i);
            } else {
                pointLight = null;
            }
            String name = "pointLights[" + i + "]";
            updatePointLight(pointLight, name, viewMatrix);
        }

        List<SpotLight> spotLights = sceneLights.getSpotLights();
        int numSpotLights = spotLights.size();
        SpotLight spotLight;
        for (int i = 0; i < MAX_SPOT_LIGHTS; i++) {
            if (i < numSpotLights) {
                spotLight = spotLights.get(i);
            } else {
                spotLight = null;
            }
            String name = "spotLights[" + i + "]";
            updateSpotLight(spotLight, name, viewMatrix);
        }
    }
    ...
}

コードは非常に単純です。環境光をディレクショナル ライト ユニフォームに設定することから始め、その後、配列の各要素のユニフォームを設定する専用のメソッドを持つポイント ライトとスポット ライトを反復処理します。

public class SceneRender {
    ...
    private void updatePointLight(PointLight pointLight, String prefix, Matrix4f viewMatrix) {
        Vector4f aux = new Vector4f();
        Vector3f lightPosition = new Vector3f();
        Vector3f color = new Vector3f();
        float intensity = 0.0f;
        float constant = 0.0f;
        float linear = 0.0f;
        float exponent = 0.0f;
        if (pointLight != null) {
            aux.set(pointLight.getPosition(), 1);
            aux.mul(viewMatrix);
            lightPosition.set(aux.x, aux.y, aux.z);
            color.set(pointLight.getColor());
            intensity = pointLight.getIntensity();
            PointLight.Attenuation attenuation = pointLight.getAttenuation();
            constant = attenuation.getConstant();
            linear = attenuation.getLinear();
            exponent = attenuation.getExponent();
        }
        uniformsMap.setUniform(prefix + ".position", lightPosition);
        uniformsMap.setUniform(prefix + ".color", color);
        uniformsMap.setUniform(prefix + ".intensity", intensity);
        uniformsMap.setUniform(prefix + ".att.constant", constant);
        uniformsMap.setUniform(prefix + ".att.linear", linear);
        uniformsMap.setUniform(prefix + ".att.exponent", exponent);
    }

    private void updateSpotLight(SpotLight spotLight, String prefix, Matrix4f viewMatrix) {
        PointLight pointLight = null;
        Vector3f coneDirection = new Vector3f();
        float cutoff = 0.0f;
        if (spotLight != null) {
            coneDirection = spotLight.getConeDirection();
            cutoff = spotLight.getCutOff();
            pointLight = spotLight.getPointLight();
        }

        uniformsMap.setUniform(prefix + ".conedir", coneDirection);
        uniformsMap.setUniform(prefix + ".conedir", cutoff);
        updatePointLight(pointLight, prefix + ".pl", viewMatrix);
    }
    ...
}

すでに述べたように、これらのライトの座標はビュー スペース内にある必要があります。通常、ワールド空間座標でライト座標を設定するため、シェーダーで使用できるようにするには、それらをビュー マトリックスで乗算する必要があります。最後に、メソッドを更新してrenderメソッドを呼び出しupdateLights、モデル マテリアルの新しい要素を適切に設定する必要があります。

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

            for (Material material : model.getMaterialList()) {
                uniformsMap.setUniform("material.ambient", material.getAmbientColor());
                uniformsMap.setUniform("material.diffuse", material.getDiffuseColor());
                uniformsMap.setUniform("material.specular", material.getSpecularColor());
                uniformsMap.setUniform("material.reflectance", material.getReflectance());
                ...
            }
        }
        ...
    }
    ...
}

UniformsMapクラス

UniformsMapまた、float と 3D ベクトルの値を設定するためのユニフォームを作成するためのメソッドのペアをクラスに追加する必要があります。

public class UniformsMap {
    ...
    public void setUniform(String uniformName, float value) {
        glUniform1f(getUniformLocation(uniformName), value);
    }

    public void setUniform(String uniformName, Vector3f value) {
        glUniform3f(getUniformLocation(uniformName), value.x, value.y, value.z);
    }
    ...
}

ライトコントロール

最後のステップは、Mainクラスでライトを使用することです。ただし、その前に、Imgui を使用して GUI を作成し、ライト パラメータを制御する要素をいくつか提供します。という名前の新しいクラスでこれを行いますLightControls。コードは長すぎますが、理解するのは非常に簡単です。GUI コントロールから値を取得するための一連の属性と、必要なパネルとウィジェットを描画するためのメソッドを設定するだけで済みます。

package org.lwjglb.game;

import imgui.*;
import imgui.flag.ImGuiCond;
import org.joml.*;
import org.lwjglb.engine.*;
import org.lwjglb.engine.scene.Scene;
import org.lwjglb.engine.scene.lights.*;

public class LightControls implements IGuiInstance {

    private float[] ambientColor;
    private float[] ambientFactor;
    private float[] dirConeX;
    private float[] dirConeY;
    private float[] dirConeZ;
    private float[] dirLightColor;
    private float[] dirLightIntensity;
    private float[] dirLightX;
    private float[] dirLightY;
    private float[] dirLightZ;
    private float[] pointLightColor;
    private float[] pointLightIntensity;
    private float[] pointLightX;
    private float[] pointLightY;
    private float[] pointLightZ;
    private float[] spotLightColor;
    private float[] spotLightCuttoff;
    private float[] spotLightIntensity;
    private float[] spotLightX;
    private float[] spotLightY;
    private float[] spotLightZ;

    public LightControls(Scene scene) {
        SceneLights sceneLights = scene.getSceneLights();
        AmbientLight ambientLight = sceneLights.getAmbientLight();
        Vector3f color = ambientLight.getColor();

        ambientFactor = new float[]{ambientLight.getIntensity()};
        ambientColor = new float[]{color.x, color.y, color.z};

        PointLight pointLight = sceneLights.getPointLights().get(0);
        color = pointLight.getColor();
        Vector3f pos = pointLight.getPosition();
        pointLightColor = new float[]{color.x, color.y, color.z};
        pointLightX = new float[]{pos.x};
        pointLightY = new float[]{pos.y};
        pointLightZ = new float[]{pos.z};
        pointLightIntensity = new float[]{pointLight.getIntensity()};

        SpotLight spotLight = sceneLights.getSpotLights().get(0);
        pointLight = spotLight.getPointLight();
        color = pointLight.getColor();
        pos = pointLight.getPosition();
        spotLightColor = new float[]{color.x, color.y, color.z};
        spotLightX = new float[]{pos.x};
        spotLightY = new float[]{pos.y};
        spotLightZ = new float[]{pos.z};
        spotLightIntensity = new float[]{pointLight.getIntensity()};
        spotLightCuttoff = new float[]{spotLight.getCutOffAngle()};
        Vector3f coneDir = spotLight.getConeDirection();
        dirConeX = new float[]{coneDir.x};
        dirConeY = new float[]{coneDir.y};
        dirConeZ = new float[]{coneDir.z};

        DirLight dirLight = sceneLights.getDirLight();
        color = dirLight.getColor();
        pos = dirLight.getDirection();
        dirLightColor = new float[]{color.x, color.y, color.z};
        dirLightX = new float[]{pos.x};
        dirLightY = new float[]{pos.y};
        dirLightZ = new float[]{pos.z};
        dirLightIntensity = new float[]{dirLight.getIntensity()};
    }

    @Override
    public void drawGui() {
        ImGui.newFrame();
        ImGui.setNextWindowPos(0, 0, ImGuiCond.Always);
        ImGui.setNextWindowSize(450, 400);

        ImGui.begin("Lights controls");
        if (ImGui.collapsingHeader("Ambient Light")) {
            ImGui.sliderFloat("Ambient factor", ambientFactor, 0.0f, 1.0f, "%.2f");
            ImGui.colorEdit3("Ambient color", ambientColor);
        }

        if (ImGui.collapsingHeader("Point Light")) {
            ImGui.sliderFloat("Point Light - x", pointLightX, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Point Light - y", pointLightY, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Point Light - z", pointLightZ, -10.0f, 10.0f, "%.2f");
            ImGui.colorEdit3("Point Light color", pointLightColor);
            ImGui.sliderFloat("Point Light Intensity", pointLightIntensity, 0.0f, 1.0f, "%.2f");
        }

        if (ImGui.collapsingHeader("Spot Light")) {
            ImGui.sliderFloat("Spot Light - x", spotLightX, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Spot Light - y", spotLightY, -10.0f, 10.0f, "%.2f");
            ImGui.sliderFloat("Spot Light - z", spotLightZ, -10.0f, 10.0f, "%.2f");
            ImGui.colorEdit3("Spot Light color", spotLightColor);
            ImGui.sliderFloat("Spot Light Intensity", spotLightIntensity, 0.0f, 1.0f, "%.2f");
            ImGui.separator();
            ImGui.sliderFloat("Spot Light cutoff", spotLightCuttoff, 0.0f, 360.0f, "%2.f");
            ImGui.sliderFloat("Dir cone - x", dirConeX, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir cone - y", dirConeY, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir cone - z", dirConeZ, -1.0f, 1.0f, "%.2f");
        }

        if (ImGui.collapsingHeader("Dir Light")) {
            ImGui.sliderFloat("Dir Light - x", dirLightX, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir Light - y", dirLightY, -1.0f, 1.0f, "%.2f");
            ImGui.sliderFloat("Dir Light - z", dirLightZ, -1.0f, 1.0f, "%.2f");
            ImGui.colorEdit3("Dir Light color", dirLightColor);
            ImGui.sliderFloat("Dir Light Intensity", dirLightIntensity, 0.0f, 1.0f, "%.2f");
        }

        ImGui.end();
        ImGui.endFrame();
        ImGui.render();
    }
    ...
}

最後に、GUI 入力を処理するメソッドが必要です。ここでは、マウスの状態に基づいて Imgui を更新し、入力が GUI コントロールによって消費されたかどうかを確認します。その場合、ユーザー入力に従ってクラスの属性を入力するだけです。

public class LightControls implements IGuiInstance {
    ...
    @Override
    public boolean handleGuiInput(Scene scene, Window window) {
        ImGuiIO imGuiIO = ImGui.getIO();
        MouseInput mouseInput = window.getMouseInput();
        Vector2f mousePos = mouseInput.getCurrentPos();
        imGuiIO.setMousePos(mousePos.x, mousePos.y);
        imGuiIO.setMouseDown(0, mouseInput.isLeftButtonPressed());
        imGuiIO.setMouseDown(1, mouseInput.isRightButtonPressed());

        boolean consumed = imGuiIO.getWantCaptureMouse() || imGuiIO.getWantCaptureKeyboard();
        if (consumed) {
            SceneLights sceneLights = scene.getSceneLights();
            AmbientLight ambientLight = sceneLights.getAmbientLight();
            ambientLight.setIntensity(ambientFactor[0]);
            ambientLight.setColor(ambientColor[0], ambientColor[1], ambientColor[2]);

            PointLight pointLight = sceneLights.getPointLights().get(0);
            pointLight.setPosition(pointLightX[0], pointLightY[0], pointLightZ[0]);
            pointLight.setColor(pointLightColor[0], pointLightColor[1], pointLightColor[2]);
            pointLight.setIntensity(pointLightIntensity[0]);

            SpotLight spotLight = sceneLights.getSpotLights().get(0);
            pointLight = spotLight.getPointLight();
            pointLight.setPosition(spotLightX[0], spotLightY[0], spotLightZ[0]);
            pointLight.setColor(spotLightColor[0], spotLightColor[1], spotLightColor[2]);
            pointLight.setIntensity(spotLightIntensity[0]);
            spotLight.setCutOffAngle(spotLightColor[0]);
            spotLight.setConeDirection(dirConeX[0], dirConeY[0], dirConeZ[0]);

            DirLight dirLight = sceneLights.getDirLight();
            dirLight.setPosition(dirLightX[0], dirLightY[0], dirLightZ[0]);
            dirLight.setColor(dirLightColor[0], dirLightColor[1], dirLightColor[2]);
            dirLight.setIntensity(dirLightIntensity[0]);
        }
        return consumed;
    }
}

Mainクラス

最後のステップは、Mainクラスを更新してライトを作成し、以前のメソッドdrawGuiとhandleGuiInputメソッドを削除することです (LightControlsクラスでの処理は省略します)。

public class Main implements IAppLogic {
    ...
    private LightControls lightControls;

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

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

        SceneLights sceneLights = new SceneLights();
        sceneLights.getAmbientLight().setIntensity(0.3f);
        scene.setSceneLights(sceneLights);
        sceneLights.getPointLights().add(new PointLight(new Vector3f(1, 1, 1),
                new Vector3f(0, 0, -1.4f), 1.0f));

        Vector3f coneDir = new Vector3f(0, 0, -1);
        sceneLights.getSpotLights().add(new SpotLight(new PointLight(new Vector3f(1, 1, 1),
                new Vector3f(0, 0, -1.4f), 0.0f), coneDir, 140.0f));

        lightControls = new LightControls(scene);
        scene.setGuiInstance(lightControls);
    }
    ...
    @Override
    public void update(Window window, Scene scene, long diffTimeMillis) {
        // Nothing to be done here
    }
}

最後に、これに似たものを見ることができます。

Java 3D LWJGL 改造 〜Imgui を使用した GUI 描画を作成する〜

Imgui を使用した GUI 描画を改造

依然学習したLWJGL Gitbookをヒントにして現在実装しているテキストRPGのLWJGL化を行いたいと考えております。

現状のテキストRPG

上の動画にあるように、コマンドプロンプトなどのCUIで実行する前提で作成しています。しかし、表示する内容として余計なものが多すぎると感じています。
なので、LWJGLを使用してウィンドウから起動できるように改造しようと考えております。

しかし、学習を開始してからいろいろと上手くいかない状況になり。。。
Imguiを単体で使用することにしました。ちょうどImguiのjavaバインディングがあったのでそちらを使用します。

【参照するドキュメント一覧】

1.LWJGLのGitbook

  1. テキストRPGのドキュメント
  2. Imguiのドキュメント
  3. ImguiのJavaDoc
  4. Imguiのチュートリアル

Imguiの実装に関して(Java Binding)

この動画を参照しました。

動画の参照ページ

Imguiのセットアップ

  1. こちらのサイトより「java-libraries.zip」をダウンロードします。
  2. imgui-app-X.XX.X-all.jarをプロジェクトから参照できるように設定します。
  3. 下のコードをコピペします。参照先はこちらです。
    
    import imgui.ImGui;
    import imgui.app.Application;

public class Main extends Application {
@Override
protected void configure(Configuration config) {
config.setTitle("Dear ImGui is Awesome!");
}

@Override
public void process() {
    ImGui.text("Hello, World!");
}

public static void main(String[] args) {
    launch(new Main());
}

}


自分が作成したクラスは次のようになっています。(ImguiMain.java)
```java
package jp.zenryoku.imgui;

import imgui.ImGui;
import imgui.app.Application;
import imgui.app.Configuration;

/** ImGuiの学習用メインクラス */
public class ImguiMain extends Application {
    @Override
    protected void configure(Configuration config) {
        config.setTitle("Dear ImGui is Awesome!");
    }

    @Override
    public void process() {
        ImGui.text("Hello, World!");
    }

    public static void main(String[] args) {
        launch(new ImguiMain());
    }
}

書いて動かしてみた動画です。

どこから学習するか?

チュートリアルの動画など見ましたが、結局のところImguiを基本文法の学習時と同じように学んだほうが早いと判断しました。
具体的には次のようなところを学びます。

  1. Imguiでハローワールド(これは、上記で実施済み)
  2. 同様に、ハローワールドを改造する。参考ページはこちら
  3. こちらのFAQを見ながらやりたいことを実行する

まずは、実行したのが2の参考ページを見ながら作成したコードを見てください。

public class ImguiMain extends Application {
    @Override
    protected void configure(Configuration config) {
        config.setTitle("Dear ImGui is Awesome!");
    }

    @Override
    public void process() {
        // Windowその1
        ImGui.begin("Title A");
        ImGui.text("Hello, World!");
        ImGui.end();

        // Windowその2
        ImGui.begin("Title B");
        ImGui.text("A nice World!");
        ImGui.end();
    }

    public static void main(String[] args) {
        launch(new ImguiMain());
    }
}

ここから、基本を学びます。参照先はこちらのページにあるものを見ます。

ImGui::ShowDemoWindow()はどこに?

上の参照先にある記事を読むと、「デモコードをみて理解してね」とあるのでそれを見るようにします。

  1. デモ・コードに移動します。
  2. 下のような画面で247行目からのプログラムがそうです。

ImGui::ShowDemoWindow()を読む

289-302行目では、デモウィンドウを表示するメソッドが呼ばれているようです。しかし、どのようにウィンドウを作成するのかわからないので細かく見ません。

330-334行目にあるコードが参考になりそうです。

    // We specify a default position/size in case there's no data in the .ini file.
    // We only do it to make the demo applications a little more welcoming, but typically this isn't required.
    const ImGuiViewport* main_viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 650, main_viewport->WorkPos.y + 20), ImGuiCond_FirstUseEver);
    ImGui::SetNextWindowSize(ImVec2(550, 680), ImGuiCond_FirstUseEver);

上記のコードは、メソッド呼び出しを行っているだけなので、プログラムを動かしてみるのが早そうです。というわけでコードを書いて実行します。
ここでもポイントは、呼び出しているメソッド(関数 or ファンクション)の名前がC言語と変わっているかどうか?です。

<動かしてみる>

Javaのデモ・コードをみる

上記の「参照先はこちらのページ」にもあるように、デモ・コードから基本を学びます。
実際のプログラムは、ImGui::ShowDemoWindow();に実装しているようです。

しかし、上記のでもコードはC/C++なので、Javaでのサンプルコードが欲しいところ。。。
そんなわけで、見つけました。サンプルはGithubにありました。

ImGui Java-bindingがありました。

まずは、Mainクラスを参照します。

中身を読んでみたところ、同じディレクトリにあるクラスがすべて必要なようなのでこのクラスたちをダウンロードしてきます。

このページにある下のような部分をクリック

ダウンロードしてきたファイルを展開して、次のディレクトリに各ファイルがあります。

  • imgui-java/example/src/main/java/: 実行するJavaファイル
  • imgui-java/example/src/main/resources/: 使用するファイル(Tahoma.ttfなど)

実行してみた!

コードを書いてみる

ここで、サンプルコードをいじって書いたコードを紹介します。
ほぼ、Mainクラスをコピーしたものですが、process()メソッドの中身を書き換えて下のように修正、実行しました。

    @Override
    public void process() {
        if (ImGui.begin("Stories", ImGuiWindowFlags.AlwaysAutoResize)) {
            ImGui.text("Hello, World! " + FontAwesomeIcons.Smile);
            ImGui.sameLine();
            ImGui.text(String.valueOf(count));
            ExampleImGuiColorTextEdit.show(new ImBoolean(true));
        }
        ImGui.end();
    }

キーポイントになるのは「ExampleImGuiColorTextEdit.show(new ImBoolean(true));」の部分です。
ExampleImGuiColorTextEditクラスのstaticメソッド「show()」を呼び出しています。
早速、クラスの中身を見ます。

import imgui.ImGui;
import imgui.extension.texteditor.TextEditor;
import imgui.extension.texteditor.TextEditorLanguageDefinition;
import imgui.flag.ImGuiWindowFlags;
import imgui.type.ImBoolean;

import java.util.HashMap;
import java.util.Map;

public class ExampleImGuiColorTextEdit {
    private static final TextEditor EDITOR = new TextEditor();

    static {
        TextEditorLanguageDefinition lang = TextEditorLanguageDefinition.c();

        String[] ppnames = {
            "NULL", "PM_REMOVE",
            "ZeroMemory", "DXGI_SWAP_EFFECT_DISCARD", "D3D_FEATURE_LEVEL", "D3D_DRIVER_TYPE_HARDWARE", "WINAPI", "D3D11_SDK_VERSION", "assert"};
        String[] ppvalues = {
            "#define NULL ((void*)0)",
            "#define PM_REMOVE (0x0001)",
            "Microsoft's own memory zapper function\n(which is a macro actually)\nvoid ZeroMemory(\n\t[in] PVOID  Destination,\n\t[in] SIZE_T Length\n); ",
            "enum DXGI_SWAP_EFFECT::DXGI_SWAP_EFFECT_DISCARD = 0",
            "enum D3D_FEATURE_LEVEL",
            "enum D3D_DRIVER_TYPE::D3D_DRIVER_TYPE_HARDWARE  = ( D3D_DRIVER_TYPE_UNKNOWN + 1 )",
            "#define WINAPI __stdcall",
            "#define D3D11_SDK_VERSION (7)",
            " #define assert(expression) (void)(                                                  \n" +
                "    (!!(expression)) ||                                                              \n" +
                "    (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \n" +
                " )"
        };

        // Adding custom preproc identifiers
        Map<String, String> preprocIdentifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            preprocIdentifierMap.put(ppnames[i], ppvalues[i]);
        }
        lang.setPreprocIdentifiers(preprocIdentifierMap);

        String[] identifiers = {
            "HWND", "HRESULT", "LPRESULT","D3D11_RENDER_TARGET_VIEW_DESC", "DXGI_SWAP_CHAIN_DESC","MSG","LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "TextEditor" };
        String[] idecls = {
            "typedef HWND_* HWND", "typedef long HRESULT", "typedef long* LPRESULT", "struct D3D11_RENDER_TARGET_VIEW_DESC", "struct DXGI_SWAP_CHAIN_DESC",
                "typedef tagMSG MSG\n * Message structure","typedef LONG_PTR LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "class TextEditor" };

        // Adding custom identifiers
        Map<String, String> identifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            identifierMap.put(identifiers[i], idecls[i]);
        }
        lang.setIdentifiers(identifierMap);

        EDITOR.setLanguageDefinition(lang);

        // Adding error markers
        Map<Integer, String> errorMarkers = new HashMap<>();
        errorMarkers.put(1, "Expected '>'");
        EDITOR.setErrorMarkers(errorMarkers);

        EDITOR.setTextLines(new String[]{
            "#include <iostream",
            "",
            "int main() {",
            "   std::cout << \"Hello, World!\" << std::endl;",
            "}"
        });
    }
    public static void show(final ImBoolean showImColorTextEditWindow) {
        ImGui.setNextWindowSize(500, 400);
        if (ImGui.begin("Text Editor", showImColorTextEditWindow,
                ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.MenuBar)) {
            if (ImGui.beginMenuBar()) {
                if (ImGui.beginMenu("File")) {
                    if (ImGui.menuItem("Save")) {
                        String textToSave = EDITOR.getText();
                        /// save text....
                    }

                    ImGui.endMenu();
                }
                if (ImGui.beginMenu("Edit")) {
                    final boolean ro = EDITOR.isReadOnly();
                    if (ImGui.menuItem("Read-only mode", "", ro)) {
                        EDITOR.setReadOnly(!ro);
                    }

                    ImGui.separator();

                    if (ImGui.menuItem("Undo", "ALT-Backspace", !ro && EDITOR.canUndo())) {
                        EDITOR.undo(1);
                    }
                    if (ImGui.menuItem("Redo", "Ctrl-Y", !ro && EDITOR.canRedo())) {
                        EDITOR.redo(1);
                    }

                    ImGui.separator();

                    if (ImGui.menuItem("Copy", "Ctrl-C", EDITOR.hasSelection())) {
                        EDITOR.copy();
                    }
                    if (ImGui.menuItem("Cut", "Ctrl-X", !ro && EDITOR.hasSelection())) {
                        EDITOR.cut();
                    }
                    if (ImGui.menuItem("Delete", "Del", !ro && EDITOR.hasSelection())) {
                        EDITOR.delete();
                    }
                    if (ImGui.menuItem("Paste", "Ctrl-V", !ro && ImGui.getClipboardText() != null)) {
                        EDITOR.paste();
                    }

                    ImGui.endMenu();
                }

                ImGui.endMenuBar();
            }

            int cposX = EDITOR.getCursorPositionLine();
            int cposY = EDITOR.getCursorPositionColumn();

            String overwrite = EDITOR.isOverwrite() ? "Ovr" : "Ins";
            String canUndo = EDITOR.canUndo() ? "*" : " ";

            ImGui.text(cposX + "/" + cposY + " " + EDITOR.getTotalLines() + " lines | " + overwrite + " | " + canUndo);

            EDITOR.render("TextEditor");

            ImGui.end();
        }
    }
}

処理の中身を見ていると「ImGui.XXX()」のメソッドを呼び出している処理がほとんどです。
つまり、ほぼ、ImGuiクラスで画面の作成を行っているということです。ということは、JavaDocを見れば何とかなりそうです。

ImGuiでテキストエリア

JavaDocを見ながら、なんとか作成しました。実行結果は下のようになりました。

プログラムは、元のコードから変更しました。
<Main.java>

    @Override
    public void process() {
        ExampleImGuiColorTextEdit.show(new ImBoolean(false));
    }

元々は、ImGui.begin()を使用して、メニューバーを追加する処理を行っていましたが、これを削除して「ExampleImGuiColorTextEdit」クラスのshow()メソッドで定義している処理を呼び出すだけに修正しました。

<ExampleImGuiColorTextEdit.java>

public class ExampleImGuiColorTextEdit {
    private static final TextEditor EDITOR = new TextEditor();
    private static final String SEP = System.lineSeparator();

    static {
        TextEditorLanguageDefinition lang = TextEditorLanguageDefinition.c();

        String[] ppnames = {
            "NULL", "PM_REMOVE",
            "ZeroMemory", "DXGI_SWAP_EFFECT_DISCARD", "D3D_FEATURE_LEVEL", "D3D_DRIVER_TYPE_HARDWARE", "WINAPI", "D3D11_SDK_VERSION", "assert"};
        String[] ppvalues = {
            "#define NULL ((void*)0)",
            "#define PM_REMOVE (0x0001)",
            "Microsoft's own memory zapper function\n(which is a macro actually)\nvoid ZeroMemory(\n\t[in] PVOID  Destination,\n\t[in] SIZE_T Length\n); ",
            "enum DXGI_SWAP_EFFECT::DXGI_SWAP_EFFECT_DISCARD = 0",
            "enum D3D_FEATURE_LEVEL",
            "enum D3D_DRIVER_TYPE::D3D_DRIVER_TYPE_HARDWARE  = ( D3D_DRIVER_TYPE_UNKNOWN + 1 )",
            "#define WINAPI __stdcall",
            "#define D3D11_SDK_VERSION (7)",
            " #define assert(expression) (void)(                                                  \n" +
                "    (!!(expression)) ||                                                              \n" +
                "    (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \n" +
                " )"
        };

        // Adding custom preproc identifiers
        Map<String, String> preprocIdentifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            preprocIdentifierMap.put(ppnames[i], ppvalues[i]);
        }
        lang.setPreprocIdentifiers(preprocIdentifierMap);

        String[] identifiers = {
            "HWND", "HRESULT", "LPRESULT","D3D11_RENDER_TARGET_VIEW_DESC", "DXGI_SWAP_CHAIN_DESC","MSG","LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "TextEditor" };
        String[] idecls = {
            "typedef HWND_* HWND", "typedef long HRESULT", "typedef long* LPRESULT", "struct D3D11_RENDER_TARGET_VIEW_DESC", "struct DXGI_SWAP_CHAIN_DESC",
                "typedef tagMSG MSG\n * Message structure","typedef LONG_PTR LRESULT","WPARAM", "LPARAM","UINT","LPVOID",
                "ID3D11Device", "ID3D11DeviceContext", "ID3D11Buffer", "ID3D11Buffer", "ID3D10Blob", "ID3D11VertexShader", "ID3D11InputLayout", "ID3D11Buffer",
                "ID3D10Blob", "ID3D11PixelShader", "ID3D11SamplerState", "ID3D11ShaderResourceView", "ID3D11RasterizerState", "ID3D11BlendState", "ID3D11DepthStencilState",
                "IDXGISwapChain", "ID3D11RenderTargetView", "ID3D11Texture2D", "class TextEditor" };

        // Adding custom identifiers
        Map<String, String> identifierMap = new HashMap<>();
        for (int i = 0; i < ppnames.length; ++i) {
            identifierMap.put(identifiers[i], idecls[i]);
        }
        lang.setIdentifiers(identifierMap);

        EDITOR.setLanguageDefinition(lang);
    }

    public static void show(final ImBoolean showImColorTextEditWindow) {
        ImGui.setNextWindowSize(500, 400);
        if (ImGui.begin("Story", showImColorTextEditWindow)) {
            int cposX = EDITOR.getCursorPositionLine();
            int cposY = EDITOR.getCursorPositionColumn();

            String overwrite = EDITOR.isOverwrite() ? "Ovr" : "Ins";
            String canUndo = EDITOR.canUndo() ? "*" : " ";
            EDITOR.setReadOnly(true);

            //ImGui.text(cposX + "/" + cposY + " " + EDITOR.getTotalLines() + " lines | " + overwrite + " | " + canUndo);

            // 実装部分
            EDITOR.setText("GoodMorning" + SEP + "Hello World");
            EDITOR.render("TextEditor");

            ImGui.end();
        }
    }
}

同様に、メニューバー内に設定する文字列、Saveをクリックしたときの処理(処理内容はなし)が定義してありましたが、それを削除。
Mainクラスでshow(true)としていた部分をshow(false)に変更して、赤いラインを表示しないようにしました。

これで、目的の文字表示領域を作成することができました。

ImGui + TextRPG

テキストRPGを作成中でしたので、これを単体の画面で実行できるようにしたいと思っていたので、ImGuiは格好のライブラリです。
LWJGLを実行しないと実現できないと思っていましたが、ImGuiで事足りそうです。

TextRPGに関して、現状の考えとしてはImGuiをメインにして追加でLWJGLを実行したいと考えています。

TextRPGとは、下の動画のようなものです。これは、コマンドプロンプトで実行しているので「ゲーム」って感じがしない。。。と思っていたところです。

Java 3D LWJGL GitBook 〜Chapter10:ImGui を使用した GUI 描画〜

Imgui を使用した GUI 描画

今まで、3Dモデルの描画を中心に学習してきましたが、GUI作成の処理を学習します。

Dear ImGui は、OpenGL や Vulkan などの複数のバックエンドを使用できるユーザー インターフェイス ライブラリです。これを使用して、GUI コントロールを表示したり、HUD を開発したりします。複数のウィジェットを提供し、外観は簡単にカスタマイズできます。

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

やはり、ライブラリを追加しますのでPOMファイルに追記します。

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

<dependency>
   <groupId>io.github.spair</groupId>
   <artifactId>imgui-java-binding</artifactId>
   <version>${imgui-java.version}</version>
</dependency>
<dependency>
    <groupId>io.github.spair</groupId>
    <artifactId>imgui-java-${native.target}</artifactId>
    <version>${imgui-java.version}</version>
    <scope>runtime</scope>
</dependency>

Imgui を使用すると、2D 形状のみを使用して、他の 3D モデルをレンダリングするように、ウィンドウ、パネルなどをレンダリングできます。使用するコントロールを設定すると、Imgui がそれを、シェーダーを使用してレンダリングできる一連の頂点バッファーに変換します。これが、あらゆるバックエンドで使用できる理由です。

各頂点に対して、Imgui はその座標 (2D 座標)、テクスチャ座標、および関連する色を定義します。したがって、Gui メッシュをモデル化し、関連する VAO と VBO を作成する新しいクラスを作成する必要があります。という名前のクラスは、GuiMesh次のように定義されます。

こんな感じの表示が行われます。

GuiMeshの作成

package org.lwjglb.engine.graph;

import imgui.ImDrawData;

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

public class GuiMesh {

    private int indicesVBO;
    private int vaoId;
    private int verticesVBO;

    public GuiMesh() {
        vaoId = glGenVertexArrays();
        glBindVertexArray(vaoId);

        // Single VBO
        verticesVBO = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, verticesVBO);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 2, GL_FLOAT, false, ImDrawData.SIZEOF_IM_DRAW_VERT, 0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, false, ImDrawData.SIZEOF_IM_DRAW_VERT, 8);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, true, ImDrawData.SIZEOF_IM_DRAW_VERT, 16);

        indicesVBO = glGenBuffers();

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

    public void cleanup() {
        glDeleteBuffers(indicesVBO);
        glDeleteBuffers(verticesVBO);
        glDeleteVertexArrays(vaoId);
    }

    public int getIndicesVBO() {
        return indicesVBO;
    }

    public int getVaoId() {
        return vaoId;
    }

    public int getVerticesVBO() {
        return verticesVBO;
    }
}

ご覧のとおり、単一の VBO を使用していますが、位置、テクスチャ座標、および色に対していくつかの属性を定義しています。この場合、バッファにデータを入力しません。使用方法については後で説明します。

IGuiInstanceの作成

また、アプリケーションが GUI コントロールを作成し、ユーザー入力に反応できるようにする必要もあります。これをサポートするために、IGuiInstance次のように定義される名前の新しいインターフェースを定義します。

package org.lwjglb.engine;

import org.lwjglb.engine.scene.Scene;

public interface IGuiInstance {
    void drawGui();

    boolean handleGuiInput(Scene scene, Window window);
}

Sceneの更新

このメソッドは、drawGuiGUI の構築に使用されます。ここで、GUI メッシュの構築に使用されるウィンドウとウィジェットを定義します。このメソッドを使用して、handleGuiInputGUI で入力イベントを処理します。入力が GUI によって処理されたかどうかを示すブール値を返します。たとえば、オーバーラップ ウィンドウを表示する場合、ゲーム ロジックでキーストロークを処理し続けることに関心がない場合があります。戻り値を使用してそれを制御できます。IGuiInstanceインターフェイスの特定の実装をクラスに格納しますScene。

public class Scene {
    ...
    private IGuiInstance guiInstance;
    ...
    public IGuiInstance getGuiInstance() {
        return guiInstance;
    }
    ...
    public void setGuiInstance(IGuiInstance guiInstance) {
        this.guiInstance = guiInstance;
    }
}

GuiRenderの作成

次のステップは、GUI をレンダリングするための新しいクラスを作成することですGuiRender。

package org.lwjglb.engine.graph;

import imgui.*;
import imgui.type.ImInt;
import org.joml.Vector2f;
import org.lwjglb.engine.*;
import org.lwjglb.engine.scene.Scene;

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

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

public class GuiRender {

    private GuiMesh guiMesh;
    private Vector2f scale;
    private ShaderProgram shaderProgram;
    private Texture texture;
    private UniformsMap uniformsMap;

    public GuiRender(Window window) {
        List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/gui.vert", GL_VERTEX_SHADER));
        shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/gui.frag", GL_FRAGMENT_SHADER));
        shaderProgram = new ShaderProgram(shaderModuleDataList);
        createUniforms();
        createUIResources(window);
    }

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

ご覧のとおり、ここにあるもののほとんどは非常になじみ深いもので、シェーダーとユニフォームをセットアップしただけです。createUIResourcesただし、次のように定義された新しいメソッドが呼び出されます。

public class GuiRender {
    ...
    private void createUIResources(Window window) {
        ImGui.createContext();

        ImGuiIO imGuiIO = ImGui.getIO();
        imGuiIO.setIniFilename(null);
        imGuiIO.setDisplaySize(window.getWidth(), window.getHeight());

        ImFontAtlas fontAtlas = ImGui.getIO().getFonts();
        ImInt width = new ImInt();
        ImInt height = new ImInt();
        ByteBuffer buf = fontAtlas.getTexDataAsRGBA32(width, height);
        texture = new Texture(width.get(), height.get(), buf);

        guiMesh = new GuiMesh();
    }
    ...
}

上記の方法では、Imgui をセットアップします。最初にコンテキスト (操作を実行するために必要) を作成し、表示サイズをウィンドウ サイズに設定します。Imgui はステータスを ini ファイルに保存します。実行間でステータスを保持したくないため、null に設定する必要があります。次のステップは、フォント アトラスを初期化し、シェーダーで使用されるテクスチャを設定して、テキストなどを適切にレンダリングできるようにすることです。最後のステップは、インスタンスを作成することですGuiMesh。

は、createUniformsスケール用に単一の 2 つのフロートを作成するだけです (使用方法については後で説明します)。

public class GuiRender {
    ...
    private void createUniforms() {
        uniformsMap = new UniformsMap(shaderProgram.getProgramId());
        uniformsMap.createUniform("scale");
        scale = new Vector2f();
    }
    ...
}

メソッドを見てみましょうrender。

public class GuiRender {
    ...
    public void render(Scene scene) {
        IGuiInstance guiInstance = scene.getGuiInstance();
        if (guiInstance == null) {
            return;
        }
        guiInstance.drawGui();

        shaderProgram.bind();

        glEnable(GL_BLEND);
        glBlendEquation(GL_FUNC_ADD);
        glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
        glDisable(GL_DEPTH_TEST);
        glDisable(GL_CULL_FACE);

        glBindVertexArray(guiMesh.getVaoId());

        glBindBuffer(GL_ARRAY_BUFFER, guiMesh.getVerticesVBO());
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, guiMesh.getIndicesVBO());

        ImGuiIO io = ImGui.getIO();
        scale.x = 2.0f / io.getDisplaySizeX();
        scale.y = -2.0f / io.getDisplaySizeY();
        uniformsMap.setUniform("scale", scale);

        ImDrawData drawData = ImGui.getDrawData();
        int numLists = drawData.getCmdListsCount();
        for (int i = 0; i < numLists; i++) {
            glBufferData(GL_ARRAY_BUFFER, drawData.getCmdListVtxBufferData(i), GL_STREAM_DRAW);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, drawData.getCmdListIdxBufferData(i), GL_STREAM_DRAW);

            int numCmds = drawData.getCmdListCmdBufferSize(i);
            for (int j = 0; j < numCmds; j++) {
                final int elemCount = drawData.getCmdListCmdBufferElemCount(i, j);
                final int idxBufferOffset = drawData.getCmdListCmdBufferIdxOffset(i, j);
                final int indices = idxBufferOffset * ImDrawData.SIZEOF_IM_DRAW_IDX;

                texture.bind();
                glDrawElements(GL_TRIANGLES, elemCount, GL_UNSIGNED_SHORT, indices);
            }
        }

        glEnable(GL_DEPTH_TEST);
        glEnable(GL_CULL_FACE);
        glDisable(GL_BLEND);
    }
    ...
}

最初に行うことは、インターフェースの実装をセットアップしたかどうかを確認することですIGuiInstance。インスタンスがない場合は、何もレンダリングする必要はなく、単に戻ります。その後、drawGuiメソッドを呼び出します。つまり、各レンダー呼び出しでそのメソッドを呼び出して、Imgui がそのステータスを更新し、適切な頂点データを生成できるようにします。シェーダーをバインドした後、まず透明度を使用できるブレンドを有効にします。ブレンディングを有効にするだけでは、透明度は表示されません。ブレンディングがどのように適用されるかについて、OpenGL に指示する必要もあります。これは、関数を介して行われますglBlendFunc。ここで適用できるさまざまな機能の詳細についての優れた説明を確認できます。

その後、Imgui が適切に機能するように深度テストと顔カリングを無効にする必要があります。次に、データの構造を定義する gui メッシュをバインドし、データとインデックス バッファーをバインドします。Imgui は画面座標を使用して頂点データを生成します。つまり、x値は[0, screen width]範囲をカバーし、y値は をカバーします[0, screen height]。ユニフォームを使用してscale、その座標系から[-1, 1]OpenGL のクリップ スペースの範囲にマップします。

その後、Imgui によって生成されたデータを取得して、GUI をレンダリングします。IMgui はまず、コマンド リストと呼ばれるものにデータを整理します。各コマンド リストには、頂点とインデックスのデータを格納するバッファーがあるため、最初に を呼び出してデータを GPU にダンプしますglBufferData。各コマンド リストは、ドロー コールを生成するために使用するコマンドのセットとしても定義します。各コマンドは、描画される要素の数とバッファに適用されるオフセットをコマンド リストに保存します。すべての要素を描画したら、深度テストを再度有効にできます。

resize最後に、 Imgui の表示サイズを調整するためにウィンドウのサイズが変更されるたびに呼び出されるメソッドを追加する必要があります。

public class GuiRender {
    ...
    public void resize(int width, int height) {
        ImGuiIO imGuiIO = ImGui.getIO();
        imGuiIO.setDisplaySize(width, height);
    }
}

UniformsMapの更新

UniformsMap2D ベクトルのサポートを追加するには、クラスを更新する必要があります。

public class UniformsMap {
    ...
    public void setUniform(String uniformName, Vector2f value) {
        glUniform2f(getUniformLocation(uniformName), value.x, value.y);
    }
}

scene.flagの更新

GUI のレンダリングに使用される頂点シェーダーは非常に単純です ( gui.vert)。座標が範囲内に収まるように座標を変換し[-1, 1]、フラグメント シェーダーで使用できるようにテクスチャ座標と色を出力するだけです。

#version 330

layout (location=0) in vec2 inPos;
layout (location=1) in vec2 inTextCoords;
layout (location=2) in vec4 inColor;

out vec2 frgTextCoords;
out vec4 frgColor;

uniform vec2 scale;

void main()
{
    frgTextCoords = inTextCoords;
    frgColor = inColor;
    gl_Position = vec4(inPos * scale + vec2(-1.0, 1.0), 0.0, 1.0);
}

フラグメント シェーダー ( gui.frag) では、テクスチャ座標に関連付けられた頂点カラーとテクスチャ カラーの組み合わせを出力するだけです。

#version 330

in vec2 frgTextCoords;
in vec4 frgColor;

uniform sampler2D txtSampler;

out vec4 outColor;

void main()
{
    outColor = frgColor  * texture(txtSampler, frgTextCoords);
}

すべてをまとめる

全体的には下のような感じです。

ここで、GUI をレンダリングするために、以前のすべての写真を接着する必要があります。最初に、新しいGuiRenderクラスをそのクラスに使用することから始めますRender。

public class Render {
    ...
    private GuiRender guiRender;
    ...
    public Render(Window window) {
        ...
        guiRender = new GuiRender(window);
    }

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

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

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

また、更新ループにEngine含めるようにクラスを変更し、その戻り値を使用して入力が消費されたかどうかを示す必要があります。IGuiInstance

public class Engine {
    ...
    public Engine(String windowTitle, Window.WindowOptions opts, IAppLogic appLogic) {
        ...
        render = new Render(window);
        ...
    }
    ...
    private void resize() {
        int width = window.getWidth();
        int height = window.getHeight();
        scene.resize(width, height);
        render.resize(width, height);
    }

    private void run() {
        ...
        IGuiInstance iGuiInstance = scene.getGuiInstance();
        while (running && !window.windowShouldClose()) {
            ...
            if (targetFps <= 0 || deltaFps >= 1) {
                boolean inputConsumed = iGuiInstance != null ? iGuiInstance.handleGuiInput(scene, window) : false;
                appLogic.input(window, scene, now - initialTime, inputConsumed);
            }
            ...
        }
        ...
    }
    ...
}

入力消費された戻り値を使用するようにインターフェイスを更新する必要もありますIAppLogic。

public interface IAppLogic {
    ...
    void update(Window window, Scene scene, long diffTimeMillis);
    ...
}

ImGuiの描画部分

最後に、IGuiInstance in the Main` クラスを実装します。

ImGuiはでも画面の作成メソッドが実装されています。それが ImGui.showDemoWindow();です。
画面の作成方法は、「ImGui」クラスからstaticメソッドを呼び出して作成します。
詳細はこちらの記事に記載しています。

ImGuiのハローワールド(デモ表示)

ImGUiをいじってみる

ハローワールドの次

public class Main implements IAppLogic, IGuiInstance {
    ...
    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-10", new Window.WindowOptions(), main);
        ...
    }

    ...
    @Override
    public void drawGui() {
        ImGui.newFrame();
        ImGui.setNextWindowPos(0, 0, ImGuiCond.Always);
        ImGui.showDemoWindow();
        ImGui.endFrame();
        ImGui.render();
    }

    @Override
    public boolean handleGuiInput(Scene scene, Window window) {
        ImGuiIO imGuiIO = ImGui.getIO();
        MouseInput mouseInput = window.getMouseInput();
        Vector2f mousePos = mouseInput.getCurrentPos();
        imGuiIO.setMousePos(mousePos.x, mousePos.y);
        imGuiIO.setMouseDown(0, mouseInput.isLeftButtonPressed());
        imGuiIO.setMouseDown(1, mouseInput.isRightButtonPressed());

        return imGuiIO.getWantCaptureMouse() || imGuiIO.getWantCaptureKeyboard();
    }
    ...
    public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
        ...
    }
}

このdrawGuiメソッドでは、新しいフレームとウィンドウの位置を設定し、 を呼び出してshowDemoWindowImgui のデモ ウィンドウを生成するだけです。フレームを終了した後、これを呼び出すことが非常に重要ですrender。これは、以前に定義された GUI 構造で一連のコマンドを生成するものです。1handleGuiInputつ目は、マウスの位置を取得し、その情報とマウス ボタンの状態で IMgui の IO クラスを更新します。入力が Imgui によってキャプチャされたことを示すブール値も返します。最後に、そのフラグを受け取るようにメソッドを更新する必要がありますinput(まだ何もしていませんが、実装しているインターフェイスにあります)。

これらすべての変更により、Imgui デモ ウィンドウが回転する立方体に重なっているのを確認できます。パネルのさまざまな方法を操作して、Imgui の機能を垣間見ることができます。

動かしてみた(IntelliJ IDEA)

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

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

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

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

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

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

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

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

クラス図

概要

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

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

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

モデルローダー

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

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

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

ModelLoader#loadModel()

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

package org.lwjglb.engine.scene;

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

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

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

public class ModelLoader {

    private ModelLoader() {
        // Utility class
    }

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

    }

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

    }
    ...
}

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

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

【modelPathの補足】

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

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

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

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

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

ModelLoader#コンストラクタ

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

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

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

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

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

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

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

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

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

マテリアルを処理

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

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

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

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

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

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

            return material;
        }
    }
    ...
}

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

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

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

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

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

Meshの処理

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

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

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

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

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

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

モデルの使用

Materialクラスの修正

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

public class Material {

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

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

SceneRenderクラスの修正

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

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

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

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

scene.flagの修正

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

#version 330

in vec2 outTextCoord;

out vec4 fragColor;

struct Material
{
    vec4 diffuse;
};

uniform sampler2D txtSampler;
uniform Material material;

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

UniformsMapの修正

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

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

Mainクラスの修正

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

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

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

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

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

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

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

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

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

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

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

Renderクラスの修正

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

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

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

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

Java 3D LWJGL GitBook 〜カメラChapter08:カメラを動かすとは〜

カメラ

この章では、レンダリングされた 3D シーン内を移動する方法を学習します。この機能は、3D ワールド内を移動できるカメラを持つようなものであり、実際、3D ワールドを参照するために使用される用語です。

参照するドキュメントとソースは以下になります。

カメラ紹介

OpenGL で特定のカメラ機能を検索しようとすると、カメラの概念がないことがわかります。つまり、カメラは常に固定されており、画面の中心にある (0, 0, 0) の位置に中心があります。そこで、3D シーン内を移動できるカメラがあるかのような印象を与えるシミュレーションを行います。どうすればこれを達成できますか? カメラを移動できない場合は、3D 空間に含まれるすべてのオブジェクトを一度に移動する必要があります。つまり、カメラを動かすことができなければ、世界全体を動かすことになります。

したがって、カメラの位置を z 軸に沿って開始位置 (Cx、Cy、Cz) から位置 (Cx、Cy、Cz+dz) に移動して、座標 (Ox、Oy、Oz)。

<カメラの概念がある場合> => カメラを動かす

実際に行うことは、オブジェクト (実際には 3D 空間内のすべてのオブジェクト) を、カメラが移動する方向とは反対の方向に移動することです。トレッドミルに置かれているオブジェクトのように考えてください。

<カメラの概念がない場合> => 世界を動かす

カメラは 3 つの軸 (x、y、z) に沿って変位することができ、それらに沿って回転することもできます (ロール、ピッチ、ヨー)。

ロール(X軸)、ピッチ(Y軸)、ヨー(Z軸)

※参考サイト:3次元ベクトルのロール、ピッチ、ヨー=(roll, pitch and yaw).
 上記リンクのサイトに以下のような図がありました。
ロール(X軸)、ピッチ(Y軸)、ヨー(Z軸)

そして、右手と、左手の2種類あるようです。親指(X軸), 人差し指(Y軸), 中指(Z軸)の形です。
なので、上記の画像は「右手」タイプのものになりマス。

右手 左手

したがって、基本的に私たちがしなければならないことは、3D ワールドのすべてのオブジェクトを移動および回転できるようにすることです。これをどのように行うのですか?答えは、すべてのオブジェクトのすべての頂点をカメラの動きの反対方向に移動し、カメラの回転に従ってそれらを回転させる別の変換を適用することです。もちろん、これは別のマトリックス、いわゆるビューマトリックスで行われます。このマトリックスは、最初に平行移動を実行し、次に軸に沿って回転を実行します。

その行列を構築する方法を見てみましょう。変換の章を覚えているなら、私たちの変換方程式は次のようでした:

射影行列を乗算する前にビュー行列を適用する必要があるため、式は次のようになります。

成すべきこと

3D ワールドのすべてのオブジェクトを移動および回転できるようにすること
つまり、「すべてのオブジェクトのすべての頂点をカメラの動きの反対方向に移動し、カメラの回転に従ってそれらを回転させる別の変換を適用することです。」
具体的な方法としては、頂点座標を持っているオブジェクトにマトリックス(行列)を掛け算などして、移動・回転するということです。
その計算方法として、上記の計算式を使用できます。

実装部分

今回は、カメラとマウスの入力を追加します。

カメラの実装

それでは、カメラをサポートするようにコードを変更してみましょう。Camera最初に、カメラの位置と回転状態、およびそのビュー マトリックスを保持する、という名前の新しいクラスを作成します。クラスは次のように定義されます。

この章で、Cameraクラスが追加されます。カメラを動かすときの処理(マトリックスの計算処理)が実装されています。
※ ビュー行列は「ワールドに置いたあらゆる物をカメラの世界に移動させる行列」(参考サイト)

package org.lwjglb.engine.scene;

import org.joml.*;

public class Camera {

    private Vector3f direction;
    private Vector3f position;
    private Vector3f right;
    private Vector2f rotation;
    private Vector3f up;
    private Matrix4f viewMatrix;

    public Camera() {
        direction = new Vector3f();
        right = new Vector3f();
        up = new Vector3f();
        position = new Vector3f();
        viewMatrix = new Matrix4f();
        rotation = new Vector2f();
    }

    public void addRotation(float x, float y) {
        rotation.add(x, y);
        recalculate();
    }

    public Vector3f getPosition() {
        return position;
    }

    public Matrix4f getViewMatrix() {
        return viewMatrix;
    }

    public void moveBackwards(float inc) {
        viewMatrix.positiveZ(direction).negate().mul(inc);
        position.sub(direction);
        recalculate();
    }

    public void moveDown(float inc) {
        viewMatrix.positiveY(up).mul(inc);
        position.sub(up);
        recalculate();
    }

    public void moveForward(float inc) {
        viewMatrix.positiveZ(direction).negate().mul(inc);
        position.add(direction);
        recalculate();
    }

    public void moveLeft(float inc) {
        viewMatrix.positiveX(right).mul(inc);
        position.sub(right);
        recalculate();
    }

    public void moveRight(float inc) {
        viewMatrix.positiveX(right).mul(inc);
        position.add(right);
        recalculate();
    }

    public void moveUp(float inc) {
        viewMatrix.positiveY(up).mul(inc);
        position.add(up);
        recalculate();
    }

    private void recalculate() {
        viewMatrix.identity()
                .rotateX(rotation.x)
                .rotateY(rotation.y)
                .translate(-position.x, -position.y, -position.z);
    }

    public void setPosition(float x, float y, float z) {
        position.set(x, y, z);
        recalculate();
    }

    public void setRotation(float x, float y) {
        rotation.set(x, y);
        recalculate();
    }
}

ご覧のとおり、回転と位置に加えて、前方上方向と右方向を定義するいくつかのベクトルを定義します。これは、自由空間移動カメラを実装しているためです。前方に移動したい場合にカメラを回転させると、定義済みの軸ではなく、カメラが指している場所に移動したいだけです。次の位置がどこに配置されるかを計算するには、これらのベクトルを取得する必要があります。そして最後に、カメラの状態が 4x4 マトリックス (ビュー マトリックス) に格納されるため、位置や回転を変更するたびに更新する必要があります。ご覧のとおり、ビュー マトリックスを更新するときは、最初に回転を行い、次に平行移動を行う必要があります。逆にすると、カメラの位置に沿って回転するのではなく、座標の原点に沿って回転します。

このCameraクラスは、前方、上、または右に移動するときに位置を更新するメソッドも提供します。これらのメソッドでは、ビュー マトリックスを使用して、現在の状態に応じて前方、上、または右のメソッドがどこにあるべきかを計算し、それに応じて位置を増やします。コードを非常にシンプルに保ちながら、これらの計算に素晴らしい JOML ライブラリを使用します。

JOMOLライブラリは、以下のクラスのこと

  • org.joml.Vector2f;

他にもありますが、つまりは「org.joml.*;」のクラスということです。これらのJOMOLライブラリクラス群を使用することが、ライブラリを使用するということです。まぁ、そのままですね。。。

カメラの使用(呼び出し元)

Cameraクラスにインスタンスを保存するSceneので、変更に進みましょう。

public class Scene {
    ...
    private Camera camera;
    ...
    public Scene(int width, int height) {
        ...
        camera = new Camera();
    }
    ...
    public Camera getCamera() {
        return camera;
    }
    ...
}

MouseInput

マウスでカメラを操作できるといいですね。そのために、マウス イベントを処理する新しいクラスを作成し、それらを使用してカメラの回転を更新できるようにします。これがそのクラスのコードです。

package org.lwjglb.engine;

import org.joml.Vector2f;

import static org.lwjgl.glfw.GLFW.*;

public class MouseInput {

    private Vector2f currentPos;
    private Vector2f displVec;
    private boolean inWindow;
    private boolean leftButtonPressed;
    private Vector2f previousPos;
    private boolean rightButtonPressed;

    public MouseInput(long windowHandle) {
        previousPos = new Vector2f(-1, -1);
        currentPos = new Vector2f();
        displVec = new Vector2f();
        leftButtonPressed = false;
        rightButtonPressed = false;
        inWindow = false;

        glfwSetCursorPosCallback(windowHandle, (handle, xpos, ypos) -> {
            currentPos.x = (float) xpos;
            currentPos.y = (float) ypos;
        });
        glfwSetCursorEnterCallback(windowHandle, (handle, entered) -> inWindow = entered);
        glfwSetMouseButtonCallback(windowHandle, (handle, button, action, mode) -> {
            leftButtonPressed = button == GLFW_MOUSE_BUTTON_1 && action == GLFW_PRESS;
            rightButtonPressed = button == GLFW_MOUSE_BUTTON_2 && action == GLFW_PRESS;
        });
    }

    public Vector2f getCurrentPos() {
        return currentPos;
    }

    public Vector2f getDisplVec() {
        return displVec;
    }

    public void input() {
        displVec.x = 0;
        displVec.y = 0;
        if (previousPos.x > 0 && previousPos.y > 0 && inWindow) {
            double deltax = currentPos.x - previousPos.x;
            double deltay = currentPos.y - previousPos.y;
            boolean rotateX = deltax != 0;
            boolean rotateY = deltay != 0;
            if (rotateX) {
                displVec.y = (float) deltax;
            }
            if (rotateY) {
                displVec.x = (float) deltay;
            }
        }
        previousPos.x = currentPos.x;
        previousPos.y = currentPos.y;
    }

    public boolean isLeftButtonPressed() {
        return leftButtonPressed;
    }

    public boolean isRightButtonPressed() {
        return rightButtonPressed;
    }
}

このMouseInputクラスは、そのコンストラクターで、マウス イベントを処理するための一連のコールバックを登録します。

・glfwSetCursorPosCallback: マウスが移動したときに呼び出されるコールバックを登録します。
・glfwSetCursorEnterCallback: マウスがウィンドウに入ったときに呼び出されるコールバックを登録します。マウスがウィンドウ内にない場合でも、マウス イベントを受け取ります。このコールバックを使用して、マウスがウィンドウ内にあるときを追跡します。
・glfwSetMouseButtonCallback: マウス ボタンが押されたときに呼び出されるコールバックを登録します。
このMouseInputクラスは、ゲーム入力が処理されるときに呼び出される入力メソッドを提供します。このメソッドは、前の位置からのマウスの変位を計算し、それをdisplVec変数に格納して、ゲームで使用できるようにします。

クラスはMouseInputクラスでインスタンス化され、Windowそのインスタンスを返すゲッターも提供されます。また、イベントがポーリングされるたびに入力メソッドを呼び出します。

Windowクラスの追加

public class Window {
    ...
    private MouseInput mouseInput;
    ...
    public Window(String title, WindowOptions opts, Callable<Void> resizeFunc) {
        ...
        mouseInput = new MouseInput(windowHandle);
    }
    ...
    public MouseInput getMouseInput() {
        return mouseInput;
    }
    ...    
    public void pollEvents() {
        ...
        mouseInput.input();
    }
    ...
}

これで、 のビュー マトリックスを使用するように頂点シェーダーを変更できます。これは、ご想像のとおりCamera、uniform として渡されます。

<scene.vert>

#version 330

layout (location=0) in vec3 position;
layout (location=1) in vec2 texCoord;

out vec2 outTextCoord;

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

void main()
{
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    outTextCoord = texCoord;
}

SceneRender

したがって、次のステップは、クラスでユニフォームを適切に作成し、各呼び出しSceneRenderでその値を更新することです。render

public class SceneRender {
    ...
    private void createUniforms() {
        ...
        uniformsMap.createUniform("viewMatrix");
        ...
    }    
    ...
    public void render(Scene scene) {
        ...
        uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
        uniformsMap.setUniform("viewMatrix", scene.getCamera().getViewMatrix());
        ...
    }
}

Main

以上で、基本コードはカメラの概念をサポートします。今、それを使用する必要があります。入力の処理方法を変更して、カメラを更新できます。次のコントロールを設定します。

・キー「A」と「D」は、カメラをそれぞれ左と右 (x 軸) に移動します。
・「W」キーと「S」キーは、それぞれカメラを前後 (z 軸) に移動します。
・「Z」キーと「X」キーは、それぞれカメラを上下 (y 軸) に移動します。
マウスの右ボタンが押されたときに、マウスの位置を使用して x 軸と y 軸に沿ってカメラを回転させます。

Mainこれで、クラスを更新してキーボードとマウスの入力を処理する準備が整いました。

public class Main implements IAppLogic {

    private static final float MOUSE_SENSITIVITY = 0.1f;
    private static final float MOVEMENT_SPEED = 0.005f;
    ...

    public static void main(String[] args) {
        ...
        Engine gameEng = new Engine("chapter-08", new Window.WindowOptions(), main);
        ...
    }
    ...
    public void input(Window window, Scene scene, long diffTimeMillis) {
        float move = diffTimeMillis * MOVEMENT_SPEED;
        Camera camera = scene.getCamera();
        if (window.isKeyPressed(GLFW_KEY_W)) {
            camera.moveForward(move);
        } else if (window.isKeyPressed(GLFW_KEY_S)) {
            camera.moveBackwards(move);
        }
        if (window.isKeyPressed(GLFW_KEY_A)) {
            camera.moveLeft(move);
        } else if (window.isKeyPressed(GLFW_KEY_D)) {
            camera.moveRight(move);
        }
        if (window.isKeyPressed(GLFW_KEY_UP)) {
            camera.moveUp(move);
        } else if (window.isKeyPressed(GLFW_KEY_DOWN)) {
            camera.moveDown(move);
        }

        MouseInput mouseInput = window.getMouseInput();
        if (mouseInput.isRightButtonPressed()) {
            Vector2f displVec = mouseInput.getDisplVec();
            camera.addRotation((float) Math.toRadians(-displVec.x * MOUSE_SENSITIVITY),
                    (float) Math.toRadians(-displVec.y * MOUSE_SENSITIVITY));
        }
    }

UML(クラス図)

プログラムの実行

プログラムを実行してみました。

<<<前回 次回 >>>