Java 3D LWJGL GitBook: ModelLoaderの分析

ModelLoaderの分析

個人的な話ですが、LWJGLの学習する元々の理由は、Objファイルから3Dモデルをロード(読み込み)する事でした。
学習している間に、Gitbookの内容を理解することが目的になってきましたが。。。

ModelLoaderクラス

ModelLoaderクラスは、次のような処理を行っています。というかクラス名が「モデル読込」なので、まぁ3Dモデルを生成するような処理をするクラスといいうことになります。

処理概要

呼び出しているメソッドの定義は下のようになっています。

loadModel(String modelId, String modelPath, TextureCache textureCache) { ... }

引数には、「モデルID」「モデルのパス」「テクスチャキャッシュ」の3つがあります。

大まかに、下のような処理を行っています。

  1. 「モデルのパス」のファイルを開く
  2. 「モデルのパス」のディレクトリを取得
  3. aiImportFile()を使用してAISceneを取得
  4. AISceneからマテリアルを取得
  5. メッシュの数を取得
  6. メッシュの集まり(PointerBuffer)を取得
  7. 各メッシュにマテリアルの割り当て
  8. モデル(クラス)の生成

ここまでやった後に、エンティティにモデルを登録し、エンティティをシーンに登録。
ライトも同様にシーンに登録しライトコントロールもシーンに登録。

<そして実行>

参照したファイル

上記の動画にあるように、読み込んだのは「CUBE(立方体)」です。以下のファイルになります。

  1. cube.mtl
  2. cube.obj
  3. cube.png

処理の中身を調査する

シンプルに下のコードを見ていきます。解析したときの残骸が入っていますが。。。

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);

    }

    private static void printMesh(AIMesh mesh) {
        System.out.println("このメッシュのプリミティブ (三角形、ポリゴン、線) の数。: " + mesh.mNumFaces());
        AIFace.Buffer buf = mesh.mFaces();
        System.out.println("この面を定義するインデックスの数。: " + buf.mNumIndices());
        System.out.println("buf.get(): " + buf.get());
        buf.forEach(face -> {
            System.out.println("");
        });

    }

    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 + "]");
        }

        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));
            printMesh(aiMesh);
            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);
    }

    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 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;
        }
    }

    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
        //System.out.println("vertices: " + vertices.length);
        if (textCoords.length == 0) {
            int numElements = (vertices.length / 3) * 2;
            textCoords = new float[numElements];
        }

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

    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;
    }
}

コンストラクタ

コンストラクターはデフォルトコンストラクタのみです。

    private ModelLoader() {
        // Utility class
    }

メソッド:loadModel()

    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);

    }

引数に以下のものを持っています。

  1. モデルID:これは、モデルを一意に識別するための情報です。
  2. モデルパス:これは、3Dモデルを生成するのに必要なファイルを格納したフォルダを示します。
  3. TextureCacheクラス:テクスチャ生成に必要な情報イメージファイルなどを保持するクラス

このメソッドで呼び出しているloadModel()メソッドはオーバーロードしているメソッドです。

これを見てみると引数にビット演算処理が渡されています。

aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_FixInfacingNormals | aiProcess_CalcTangentSpace | aiProcess_LimitBoneWeights | aiProcess_PreTransformVertices

これは定数をビット演算(OR演算)しています。そして、結局は整数値が第四引数に渡されています。

そして、Asimpライブラリのメソッド「aiImportFile」を使用して「AISceneクラス」を取得します。
※ドキュメントはC言語のものなので、ストラクチャーと書いていますが気にしなくてよいです。ちなにみ、Javaのものはこちらです。

このクラスにシーンで定義しているオブジェクトを色々と取得できます。

その中のマテリアル(Materialクラス)を取得して、リストに追加、Modelクラスのコンストラクタに渡すという処理を行い。

Modelクラスのインスタンスを返却するという処理でした。

LWJGLの記事は「LWJGL Git Chapter」で一覧できます。

投稿者:

takunoji

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

コメントを残す