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

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

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

コンセプト

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

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

コードの準備

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

エンティティの選択

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

クラス図

概要

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

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

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

モデルローダー

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

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

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

ModelLoader#loadModel()

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

package org.lwjglb.engine.scene;

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

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

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

public class ModelLoader {

    private ModelLoader() {
        // Utility class
    }

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

    }

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

    }
    ...
}

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

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

【modelPathの補足】

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

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

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

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

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

ModelLoader#コンストラクタ

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

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

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

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

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

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

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

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

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

マテリアルを処理

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

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

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

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

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

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

            return material;
        }
    }
    ...
}

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

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

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

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

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

Meshの処理

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

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

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

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

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

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

モデルの使用

Materialクラスの修正

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

public class Material {

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

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

SceneRenderクラスの修正

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

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

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

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

scene.flagの修正

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

#version 330

in vec2 outTextCoord;

out vec4 fragColor;

struct Material
{
    vec4 diffuse;
};

uniform sampler2D txtSampler;
uniform Material material;

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

UniformsMapの修正

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

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

Mainクラスの修正

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

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

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

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

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

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

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

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

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

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

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

Renderクラスの修正

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

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

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

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

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

シーケンス図の書き方

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

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

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

シーケンス図1

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

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

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

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

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

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

シーケンス図の内訳

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

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

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

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

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

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

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

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

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

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

▲PageTop

制御構造の記述

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

複合フラグメントの種類

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

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

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

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

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

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

基本的な処理を表現する

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

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

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

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

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

でわでわ。。。

FreeTts エラー ~mbrola.base

FreeTtsエラー

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

でわでわ。。。