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 行目は後ろ向きの面をカリング (削除) する必要があることを示しています。

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

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。

コメントを残す