#

Monkeys I

Mein erstes Experiment mit der jMonkeyEngine hat als Ziel, den Fußboden eines Dungeons zu erzeugen. Selbst diese triviale Aufgabe ist für einen 3D Noob durchaus nicht leicht.

Als Grundlage dient die Klasse HelloTriMesh aus dem Tutorial. Hierbei wird ein flaches Quadrat aus Dreiecken erzeugt und angezeigt:

public class HelloTriMesh extends SimpleGame {
    public static void main(String[] args) {
        HelloTriMesh app = new HelloTriMesh();
        // app.setDialogBehaviour(AbstractGame.ALWAYS_SHOW_PROPS_DIALOG);
        app.start();
    }
 
    protected void simpleInitGame() {
        // TriMesh is what most of what is drawn in jME actually is
        TriMesh m = new TriMesh("My Mesh");
 
        // Vertex positions for the mesh
        Vector3f[] vertexes={
            new Vector3f(0,0,0),
            new Vector3f(1,0,0),
            new Vector3f(0,1,0),
            new Vector3f(1,1,0)
        };
 
        // Normal directions for each vertex position
        Vector3f[] normals={
            new Vector3f(0,0,1),
            new Vector3f(0,0,1),
            new Vector3f(0,0,1),
            new Vector3f(0,0,1)
        };
 
        // Color for each vertex position
        ColorRGBA[] colors={
            new ColorRGBA(1,0,0,1),
            new ColorRGBA(1,0,0,1),
            new ColorRGBA(0,1,0,1),
            new ColorRGBA(0,1,0,1)
        };
 
        // Texture Coordinates for each position
        Vector2f[] texCoords={
            new Vector2f(0,0),
            new Vector2f(1,0),
            new Vector2f(0,1),
            new Vector2f(1,1)
        };
 
        // The indexes of Vertex/Normal/Color/TexCoord sets.  Every 3 makes a triangle.
        int[] indexes={
            0,1,2,1,2,3
        };
 
        // Feed the information to the TriMesh
        m.reconstruct(BufferUtils.createFloatBuffer(vertexes), 
                BufferUtils.createFloatBuffer(normals),
                BufferUtils.createFloatBuffer(colors), 
                BufferUtils.createFloatBuffer(texCoords), 
                BufferUtils.createIntBuffer(indexes));
 
        // Create a bounds
        m.setModelBound(new BoundingBox());
        m.updateModelBound();
 
        // Attach the mesh to my scene graph
        rootNode.attachChild(m);
 
        // Let us see the per vertex colors
        lightState.setEnabled(false);
    }
}

Selbst das Erzeugen eines Quadrates erscheint recht komplex:
Zuerst müssen die dreidimensionalen Eckpunkte des Quadrates als Vertices definiert werden:

Vector3f[] vertexes={
  new Vector3f(0,0,0),
  new Vector3f(1,0,0),
  new Vector3f(0,1,0),
  new Vector3f(1,1,0)
};

Anschließend wird über ein Array definiert, in welcher Reihenfolge die Eckpunkte zu Dreiecken verbunden werden sollen.

// The indexes of Vertex/Normal/Color/TexCoord sets.  Every 3 makes a triangle.
int[] indexes={
  0,1,2,
  1,2,3
};

Entsprechend werden Dreiecke mit jeweils 3 der Punkte gebildet: 0->1->2 und 1->2->3.

Zusätzlich muss für jeden Vertex eine Normale definiert werden. Die Normale, so wird in dem Tutorial beschrieben, gibt in etwa an, in welcher Richtung die Lichtintensität des Vertex am höchsten ist. Tatsächlich handelt es sich bei der Normalen um einen Vektor, der orthogonal (senkrecht) zu dem gegebenen Punkt, bzw. der jeweiligen Ecke des Dreiecks ist. Mit Hilfe der Normalen kann bestimmt werden, wie einfallendes Licht reflektiert wird, aber auch, ob das Dreieck zum Benutzer hin oder vom Benutzer weg zeigt. (Flächen, die vom Benutzer wegzeigen sind meist nicht sichtbar.)
Und eine Farbe, sowie eine Position für die Textur:

// Color for each vertex position
ColorRGBA[] colors={
  new ColorRGBA(1,0,0,1),
  new ColorRGBA(1,0,0,1),
  new ColorRGBA(0,1,0,1),
  new ColorRGBA(0,1,0,1)
};

Im Beispiel werden den Vertices der unteren Ecken jeweils die Farbe Rot und den oberen die Farbe Grün zugeordnet. Entsprechend hat das Rechteck dann einen Farbverlauf.
Obwohl in diesem Beispiel keine echte Textur benutz wird, sondern das Rechteck solid ist, werden die Koordinaten der Textur entsprechend dem Quadrat an den Eckpunkten, in diesem Fall jedoch zweidimensional, definiert:

// Texture Coordinates for each position
Vector2f[] texCoords={
  new Vector2f(0,0),
  new Vector2f(1,0),
  new Vector2f(0,1),
  new Vector2f(1,1)
};

Das Resultat kann hier angeschaut werden.

Weiterhin muss das generierte Objekt eine Bounding Box erhalten.

// Create a bounds
m.setModelBound(new BoundingBox());
m.updateModelBound();

Bounding Boxes (im folgenden BB, gelegentlich auch als „Bounding Volume“ bezeichnet, dt. Umschließender Würfel) repräsentieren einen Würfel, welcher das Objekt minimal komplett umschließt (minimal-> gerade so groß, dass das Objekt drin liegt). In jME werden die Maße dieser Box automatisch durch updateModelBound() bestimmt.
BB dienen in 3D Renderingengines dazu, komplexere (aber auch einfache) 3D Objekte vereinfacht darzustellen und damit die Geschwindigkeit der Anwendung zu erhöhen. Soll beispielsweise bestimmt werden, ob ein Objekt in der Szene gerade sichtbar ist und daher gerendert werden soll, so wird zuerst geprüft, ob der Viewport (das sichtbare Rechteck) diese Box schneidet. Ist dies nicht der Fall, so wird das Objekt verworfen. Würde man alle Dreiecke eines komplexen Objektes (z. B. mit 100 Rechtecken) gegen den Viewport prüfen, so würde die Anwendung bald zu langsam werden. Gleichermaßen werden die BB auch für Kollisionsdetektionen und andere Zwecke benutzt.

Zu guter letzt muss der Szenengraph erstellt werden. Der Szenengraph verwaltet alle Objekte, ihre Texturen und weiteren Eigenschaften und auch Interaktionen.
Die Klasse SimpleGame enthält bereits einen Szenengraphen, dem unser Quadrat lediglich hinzugefügt werden muss.

// Attach the mesh to my scene graph
rootNode.attachChild(m);

Und die durch SimpleGame hinzugefügten default Lichtquellen werden vorerst ebenfalls ausgeschaltet:

// Let us see the per vertex colors
lightState.setEnabled(false);

Nun gilt es, diesem Rechteck eine Textur zu verpassen. Hierzu wird das Beispiel HelloStates aus dem Tutorial zu rate gezogen.
States sind Metadaten zu einem Objekt, welche u. a. die Textur- oder Lichtreflektionseigenschaften repräsentieren. Sie stellen Knoten im Szenengraphen dar, welche an das entsprechende Objekt angehängt werden.

...
// Create a bounds
m.setModelBound(new BoundingBox());
m.updateModelBound();
 
// Get a TextureState
TextureState ts=display.getRenderer().createTextureState();
// Use the TextureManager to load a texture
Texture t=TextureManager.loadTexture(
        "resources/Marmor13.JPG",
        Texture.MM_LINEAR,
        Texture.FM_LINEAR);
// Assign the texture to the TextureState
ts.setTexture(t);
 
// Get a MaterialState
MaterialState ms=display.getRenderer().createMaterialState();
// Give the MaterialState an emissive tint
ms.setEmissive(new ColorRGBA(0f,0f,0f,1));
 
// Signal that b should use renderstate ts
m.setRenderState(ts);
// Signal that n should use renderstate ms
m.setRenderState(ms);
...
<tt width=“200″/>   Auf diese Weise erhält das Quadrat eine Marmor-Textur. Damit ist ein Tile (Fliesse) des Bodens definiert. Nun gilt es, den Boden etwas größer zu gestalten.

Mein erster Ansatz war es, mehrere Quadrate zu erzeugen, ihnen die jeweilige Textur zu zuweisen und sie nebeneinander zu stellen:

    private TriMesh createTile(int x, int y, TextureState ts, MaterialState ms){
    TriMesh tileMesh = new TriMesh("tile "+x+"/"+y);
        // Vertex positions for the mesh
        Vector3f[] vertexes={
            new Vector3f(x,y,0),
            new Vector3f(x+1,y,0),
            new Vector3f(x,y+1,0),
            new Vector3f(x+1,y+1,0)
        };
 
        // Normal directions for each vertex position
        Vector3f[] normals={
            new Vector3f(0,0,1),
            new Vector3f(0,0,1),
            new Vector3f(0,0,1),
            new Vector3f(0,0,1)
        };
 
        // Color for each vertex position
        ColorRGBA[] colors={
            new ColorRGBA(1,0,0,1),
            new ColorRGBA(1,0,0,1),
            new ColorRGBA(0,1,0,1),
            new ColorRGBA(0,1,0,1)
        };
 
        // Texture Coordinates for each position
        Vector2f[] texCoords={
            new Vector2f(0,0),
            new Vector2f(1,0),
            new Vector2f(0,1),
            new Vector2f(1,1)
        };
 
        // The indexes of Vertex/Normal/Color/TexCoord sets.  Every 3 makes a triangle.
        int[] indexes={
            0,1,2,1,2,3
        };
 
        // Feed the information to the TriMesh
        tileMesh.reconstruct(
                BufferUtils.createFloatBuffer(vertexes), 
                BufferUtils.createFloatBuffer(normals),
                BufferUtils.createFloatBuffer(colors), 
                BufferUtils.createFloatBuffer(texCoords), BufferUtils.createIntBuffer(indexes));
 
        // Signal that b should use renderstate ts
        tileMesh.setRenderState(ts);
        // Signal that n should use renderstate ms
        tileMesh.setRenderState(ms);
 
       // Create a bounds
        tileMesh.setModelBound(new BoundingBox());
        tileMesh.updateModelBound();
 
    return tileMesh;
    }
 
    private TriMesh[] createFloor(int width, int height, TextureState ts, MaterialState ms){
    TriMesh [] tiles = new TriMesh[width*height];
    for (int x=0; x<width; x++){
    for (int y=0; y<height; y++){
    tiles[y*height+x] = createTile(x, y, ts, ms);
    }
    }
    return tiles;
    }
 
    protected void simpleInitGame() {
        // Get a TextureState
        TextureState ts=display.getRenderer().createTextureState();
        // Use the TextureManager to load a texture
        Texture t=TextureManager.loadTexture(
                "resources/Marmor13.JPG",
                Texture.MM_LINEAR,
                Texture.FM_LINEAR);
 
        // Assign the texture to the TextureState
        ts.setTexture(t);
 
        // Get a MaterialState
        MaterialState ms=display.getRenderer().createMaterialState();
        // Give the MaterialState an emissive tint
        ms.setEmissive(new ColorRGBA(0f,0f,0f,1));
 
        // TriMesh is what most of what is drawn in jME actually is
        TriMesh [] m = createFloor(10, 10,ts, ms);
 
        // Create a point light
        PointLight l=new PointLight();
        // Give it a location
        l.setLocation(new Vector3f(2,10,3));
        // Make it a red light
        l.setDiffuse(ColorRGBA.white);
        // Enable it
        l.setEnabled(true);
 
        // Create a LightState to put my light in
        LightState ls=display.getRenderer().createLightState();
        // Attach the light
        ls.attach(l);
 
        // Detach all the default lights made by SimpleGame
        lightState.detachAll();
 
 
        // Make my light effect everything below node n
        rootNode.setRenderState(ls);
 
        // Attach the mesh to my scene graph
        for (int i=0; i<m.length; i++)
        rootNode.attachChild(m[i]);
 
        // Let us see the per vertex colors
        lightState.setEnabled(false);
    }

Außer den Methoden, um die Fliessen zu Erzeugen habe ich noch eine Lichtquelle hinzugefügt, welche am rootNode des Scenegraphen eingefügt wird, um für alle Fliessen zu gelten.
Es werden 10×10 Fliessen erzeugt.

</tt width=“200″/>   <tt width=“200″/>

Damit hat man aber bereits 200 Dreiecke und 400 Vertices.

</tt width=“200″/>   Bei einer realistischeren Szenengröße mit 100×100 Fliessen geht die Performanz mit 20.000 Dreiecken bereits in die Knie (FPS bei 2-8), obwohl der Szenengraph die nicht dargestellten Dreiecke auch gar nicht mitberechnet.

Hier hilft nun das Bespiel HelloKeyInput weiter. Es ist nämlich möglich, für ein Objekt die Textur wrappen (wiederholt anzeigen) zu lassen.

Die entscheidene Änderung wird in der Methode createFloor durchgeführt, die Methode createTile wird verworfen:

private TriMesh createFloor(int width, int height, 
 TextureState ts, MaterialState ms){
 TriMesh tileMesh = new TriMesh("floor");
// Vertex positions for the mesh
Vector3f[] vertexes={
new Vector3f(0,0,0),
new Vector3f(width,0,0),
new Vector3f(0,height,0),
new Vector3f(width,height,0)
};
 
    // Normal directions for each vertex position
    Vector3f[] normals={
        new Vector3f(0,0,1),
        new Vector3f(0,0,1),
        new Vector3f(0,0,1),
        new Vector3f(0,0,1)
    };
 
    // Color for each vertex position
    ColorRGBA[] colors={
        new ColorRGBA(1,0,0,1),
        new ColorRGBA(1,0,0,1),
        new ColorRGBA(0,1,0,1),
        new ColorRGBA(0,1,0,1)
    };
 
    // Texture Coordinates for each position
    Vector2f[] texCoords={
        new Vector2f(0,0),
        new Vector2f(width,0),
        new Vector2f(0,height),
        new Vector2f(width,height)
    };
 
    // The indexes of Vertex/Normal/Color/TexCoord sets.  Every 3 makes a triangle.
    int[] indexes={
        0,1,2,1,2,3
    };
 
    // Feed the information to the TriMesh
    tileMesh.reconstruct(BufferUtils.createFloatBuffer(vertexes), 
BufferUtils.createFloatBuffer(normals),
BufferUtils.createFloatBuffer(colors), 
BufferUtils.createFloatBuffer(texCoords), 
BufferUtils.createIntBuffer(indexes));
 
    // Signal that b should use renderstate ts
    tileMesh.setRenderState(ts);
    // Signal that n should use renderstate ms
    tileMesh.setRenderState(ms);
 
   // Create a bounds
    tileMesh.setModelBound(new BoundingBox());
    tileMesh.updateModelBound();
 
    return tileMesh;
    }
 
    protected void simpleInitGame() {
        // Get a TextureState
        TextureState ts=display.getRenderer().createTextureState();
        // Use the TextureManager to load a texture
        Texture t=TextureManager.loadTexture(
                "resources/Marmor13.JPG",
                Texture.MM_LINEAR,
                Texture.FM_LINEAR);
        t.setWrap(Texture.WM_WRAP_S_WRAP_T);
 
        // Assign the texture to the TextureState
        ts.setTexture(t);
 
        // Get a MaterialState
        MaterialState ms=display.getRenderer().createMaterialState();
        // Give the MaterialState an emissive tint
        ms.setEmissive(new ColorRGBA(0f,0f,0f,1));
 
        // TriMesh is what most of what is drawn in jME actually is
        TriMesh m = createFloor(100,100,ts, ms);
        ...

Enscheidend ist hierbei die Option t.setWrap(Texture.WM_WRAP_S_WRAP_T) für die Textur.

  Bei gleicher Wirkung nur 2 Dreiecke und mehr als 120 FPS.

Damit wird aber ein weiteres Problem aufgeworfen: Was passiert, wenn nicht alle Fliessen gleich aussehen sollen? Zum einen könnte man den ganzen Boden in Blöcke mit gleicher Textur unterteilen. Vielleicht gibt es aber auch die Möglichkeit, mehrere alternative Texturen für ein Objekt zu definieren. Aber dazu zu einem anderen Zeitpunkt mehr.

Tags:, ,

Leave a Reply »»

Note: All comments are manually approved to avoid spam. So if your comment doesn't appear immediately, that's ok. Have patience, it can take some days until I have the time to approve my comments.