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

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

投稿者:

takunoji

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

コメントを残す