libGDX - Textur mapping Frage

Hi,

ich starte gerade ein neues Spieleprojekt. Und es ist mein erstes Spiel, wo auch etwas 3D mit ins Spiel kommt (aber nur minimal) und natürlich habe ich gleich ein Problem.

Mein Wunsch: Eine Textur auf einer Fläche eines Würfels. Bekomme ich auch hin, aber leider ist die Textur um 90 Grad gedreht.
Jetzt gäbe es die Möglichkeit das Bild um 90 Grad zu drehen oder die Kamera rotieren und die Breite und Höhe des Würfels anzupassen, aber ich möchte verstehen was ich falsch mache bzw. einfach nicht verstanden habe bis jetzt.
Kann mir das jemand erklären bzw. einen Tipp geben, wonach ich suchen sollte?
Ich danke.

Also im Bild sieht man es sehr gut.
An den Seiten sollte das Fenster jeweils ins „nichts“ zeigen" und im Hintergrund erkennt man es schlecht, weil das Bild 1024x485 Pixel besitzt. Aber auf einem Würfel gemalt wird der auch die Skalierung hat, aber um 90 Grad gedreht und damit total verzerrt ist.

Mein aktueller Code (ich benutze libgdx):

public class Test extends InputAdapter implements ApplicationListener {

public Environment environment;
public PerspectiveCamera cam;

public ModelBatch modelBatch;

public List<ModelInstance> modelInstances;

private boolean positioning = true;

@Override
public void create() {
    AssetLoader.load();

    environment = new Environment();
    environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
    environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));

    cam = new PerspectiveCamera(55, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
    cam.position.set(0f,0,-4.5f);
    cam.lookAt(0f, 0f, 0f);
    cam.near = 1f;
    cam.far = 100f;
    cam.update();

    this.modelBatch = new ModelBatch();

    float height = 2f * AssetLoader.background.getRegionHeight() / AssetLoader.background.getRegionWidth();

    this.modelInstances = new ArrayList<>();

    this.modelInstances.add(new ModelInstance(this.getBackgroundModel(AssetLoader.background, TextureAttribute.createDiffuse(AssetLoader.background)), 0, 0, 0));
    this.modelInstances.add(new ModelInstance(this.getBackgroundModel(AssetLoader.background, TextureAttribute.createDiffuse(AssetLoader.backgroundLeft)), -2, 0, -2));
    this.modelInstances.add(new ModelInstance(this.getBackgroundModel(AssetLoader.background, TextureAttribute.createDiffuse(AssetLoader.backgroundRight)), 2, 0, -2));
    this.modelInstances.add(new ModelInstance(this.getBackgroundModel(AssetLoader.background, ColorAttribute.createDiffuse(Color.YELLOW)), 0, height, -2));
    this.modelInstances.add(new ModelInstance(this.getBackgroundModel(AssetLoader.background, ColorAttribute.createDiffuse(Color.WHITE), 1), 0, -height/2, -2.2f));

    this.modelInstances.add(new ModelInstance(this.getBackgroundModel(AssetLoader.bottom, TextureAttribute.createDiffuse(AssetLoader.bottom), 2, 0.96f, 2f), 0, -height, -2));

    Gdx.input.setInputProcessor(this);
}

private Model getBackgroundModel(TextureRegion textureRegion, Attribute diffuse) {
    float width = 2f;

    return getBackgroundModel(textureRegion, diffuse, width);
}

private Model getBackgroundModel(TextureRegion textureRegion, Attribute diffuse, float width) {
    float height = width * textureRegion.getRegionHeight() / textureRegion.getRegionWidth();
    float depth = width;

    return getBackgroundModel(textureRegion, diffuse, width, height, depth);
}

private Model getBackgroundModel(TextureRegion textureRegion, Attribute diffuse, float width, float height, float depth) {
    ModelBuilder modelBuilder = new ModelBuilder();

    int attr = VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal | VertexAttributes.Usage.TextureCoordinates;
    modelBuilder.begin();
    MeshPartBuilder meshPartBuilder = modelBuilder.part("box", GL20.GL_TRIANGLES, attr, new Material(diffuse));

    BoxShapeBuilder.build(meshPartBuilder, width, height, depth);

    return modelBuilder.end();
}

@Override
public void resize(int width, int height) {

}

@Override
public void render() {
    Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
    Gdx.gl.glClearColor(0, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

    cam.update();

    modelBatch.begin(cam);
    for (ModelInstance instance : this.modelInstances) {
        modelBatch.render(instance, environment);
    }
    modelBatch.end();
}

@Override
public void pause() {

}

@Override
public void resume() {

}

@Override
public void dispose() {
    modelBatch.dispose();
    AssetLoader.dispose();
}

@Override
public boolean keyUp(int keycode) {
    if (keycode == Input.Keys.SPACE) {
        positioning = !positioning;
    }
    if (positioning) {
        switch (keycode) {
            case Input.Keys.LEFT: cam.position.set(cam.position.x + 0.05f, cam.position.y, cam.position.z); break; //
            case Input.Keys.RIGHT: cam.position.set(cam.position.x - 0.05f, cam.position.y, cam.position.z); break; //
            case Input.Keys.UP: cam.position.set(cam.position.x, cam.position.y + 0.05f, cam.position.z); break; //
            case Input.Keys.DOWN: cam.position.set(cam.position.x, cam.position.y - 0.05f, cam.position.z); break; //
        }
    } else {
        switch (keycode) {
            case Input.Keys.LEFT: cam.direction.add(0.05f, 0, 0); break; //
            case Input.Keys.RIGHT: cam.direction.add(-0.05f, 0, 0); break; //
            case Input.Keys.UP: cam.direction.add(0, 0.05f, 0); break; //
            case Input.Keys.DOWN: cam.direction.add(0, -0.05f, 0); break; //
        }
    }
    return false;
}

}

Mit libGDX speziell kenne ich mich zwar nicht aus, aber … so ein bißchen 3D-Zeug hab’ ich ab und zu mal gemacht.

Jetzt gäbe es die Möglichkeit das Bild um 90 Grad zu drehen oder die Kamera rotieren und die Breite und Höhe des Würfels anzupassen

Wenn man das weiter zerpflückt, zerfällt es in mehrere Möglichkeiten. Du könntest auch die Box drehen, oder die Texturkoordinaten anpassen…

Wenn du dir einen Würfel vorstellst, mit 6 Seiten, und auf jeder Seite ein Bild, stellt sich ja sofort die Frage, wie rum rotiert das Bild auf jeder Seite ist. Man könnte mit den Händen wedeln und sagen „Ja, aufrecht halt!!!111 … auf den Seitenflächen … und oben und unten dann halt… hm… naja, irgendwie halt…“. Irgendwann hat irgendjemand bei libGDX eine Entscheidung getroffen. Und es ist fast egal, ob es egal ist, wie man es macht, oder ob es eine begründbare „erste Wahl“ gibt, und derjenige diese als „sensible default“ getroffen hat: Für den einen oder anderen Zweck passt’s halt vielleicht mal nicht.

Langer Rede gar kein Sinn: Du rufst da ja BoxShapeBuilder#build, was über einige Umwege dann die Flächen mit MeshBuilder#rect baut - und dort werden offenbar default-Texturkoordinaten vergeben.

Hab’ mal testweise den „Pfad“ dorthin rausgeschält, und an der entscheidenden Stelle zum Testen so ein boolean ROTATE eingefügt, das einfach irgendwelche (!) anderen Texturkoordinaten verwendet - hier als KSKB:

package de.javagl.libgdxtest;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.VertexAttributes;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder.VertexInfo;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.math.Vector3;

public class BasicCubeTextureTest extends ApplicationAdapter {
	
    public static void main (String[] arg) {
        LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
        new LwjglApplication(new BasicCubeTextureTest(), config);
    }
	
	private Environment environment;
	private PerspectiveCamera camera;
	private CameraInputController cameraController;
	private ModelBatch modelBatch;
	private Model model;
	private ModelInstance instance;
	private Texture texture;

	@Override
	public void create() {
		environment = new Environment();
		environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
		environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -1f, -1f));
		
		camera = new PerspectiveCamera(60, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		camera.position.set(1.5f, 1.5f, 1.5f);
		camera.lookAt(0,0,0);
		camera.near = 1f;
		camera.far = 10f;
		camera.update();
		
		FileHandle imageFileHandle = Gdx.files.internal("image.png"); 
	    texture = new Texture(imageFileHandle);
		
		modelBatch = new ModelBatch();
        model = createModel();
        instance = new ModelInstance(model);
        
        cameraController = new CameraInputController(camera);
        Gdx.input.setInputProcessor(cameraController);
	}
	
	private Model createModel()
	{
		ModelBuilder modelBuilder = new ModelBuilder();

		int attr = VertexAttributes.Usage.Position 
				| VertexAttributes.Usage.Normal
				| VertexAttributes.Usage.TextureCoordinates;
		modelBuilder.begin();
		
		TextureAttribute diffuse = TextureAttribute.createDiffuse(texture);
		MeshPartBuilder meshPartBuilder = modelBuilder.part(
				"box", GL20.GL_TRIANGLES, attr, new Material(diffuse));

		//BoxShapeBuilder.build(meshPartBuilder, 1, 1, 1);
		build(meshPartBuilder, 1, 1, 1);

		return modelBuilder.end();
		
	}
	
	private static void build(MeshPartBuilder builder, float width, float height, float depth)
	{
		build(builder, 0, 0, 0, width, height, depth);
	}
	
	private static void build (MeshPartBuilder builder, 
			float x, float y, float z, 
			float width, float height, float depth) {
		final float hw = width * 0.5f;
		final float hh = height * 0.5f;
		final float hd = depth * 0.5f;
		final float x0 = x - hw, y0 = y - hh, z0 = z - hd, x1 = x + hw, y1 = y + hh, z1 = z + hd;
		
		builder.ensureVertices(24);
		builder.ensureRectangleIndices(6);
		
		Vector3 corner000 = new Vector3();
		Vector3 corner010 = new Vector3();
		Vector3 corner100 = new Vector3();
		Vector3 corner110 = new Vector3();
		Vector3 corner001 = new Vector3();
		Vector3 corner011 = new Vector3(); 
		Vector3 corner101 = new Vector3();
		Vector3 corner111 = new Vector3();
		
		corner000.set(x0, y0, z0);
		corner010.set(x0, y1, z0); 
		corner100.set(x1, y0, z0); 
		corner110.set(x1, y1, z0);
		corner001.set(x0, y0, z1); 
		corner011.set(x0, y1, z1); 
		corner101.set(x1, y0, z1); 
		corner111.set(x1, y1, z1);
	
		Vector3 tmpV1 = new Vector3();
		Vector3 tmpV2 = new Vector3();
		
		Vector3 nor = tmpV1.set(corner000).lerp(corner110, 0.5f).sub(tmpV2.set(corner001).lerp(corner111, 0.5f)).nor();
		
		rect(builder, corner000, corner010, corner110, corner100, nor);
		rect(builder, corner011, corner001, corner101, corner111, nor.scl(-1));
		
		nor = tmpV1.set(corner000).lerp(corner101, 0.5f).sub(tmpV2.set(corner010).lerp(corner111, 0.5f)).nor();
		
		rect(builder, corner001, corner000, corner100, corner101, nor);
		rect(builder, corner010, corner011, corner111, corner110, nor.scl(-1));
		
		nor = tmpV1.set(corner000).lerp(corner011, 0.5f).sub(tmpV2.set(corner100).lerp(corner111, 0.5f)).nor();
		
		rect(builder, corner001, corner011, corner010, corner000, nor);
		rect(builder, corner100, corner110, corner111, corner101, nor.scl(-1));
	}
	
	private static void rect(MeshPartBuilder builder, 
			Vector3 corner00, Vector3 corner10,
			Vector3 corner11, Vector3 corner01, 
			Vector3 normal) {
		VertexInfo vertTmp1 = new VertexInfo();
		VertexInfo vertTmp2 = new VertexInfo();
		VertexInfo vertTmp3 = new VertexInfo();
		VertexInfo vertTmp4 = new VertexInfo();
		
		boolean ROTATE = false;
		//ROTATE = true;
		if (ROTATE)
		{
			builder.rect(
				vertTmp1.set(corner00, normal, null, null).setUV(0f, 0f), 
				vertTmp2.set(corner10, normal, null, null).setUV(0f, 1f),
				vertTmp3.set(corner11, normal, null, null).setUV(1f, 1f), 
				vertTmp4.set(corner01, normal, null, null).setUV(1f, 0f));
		}
		else
		{
			builder.rect(
				vertTmp1.set(corner00, normal, null, null).setUV(0f, 1f), 
				vertTmp2.set(corner10, normal, null, null).setUV(1f, 1f),
				vertTmp3.set(corner11, normal, null, null).setUV(1f, 0f), 
				vertTmp4.set(corner01, normal, null, null).setUV(0f, 0f));
		}
		
		
	}	

	@Override
	public void render() {
		cameraController.update();
		
        Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
 
        texture.bind();
        modelBatch.begin(camera);
        modelBatch.render(instance, environment);
        modelBatch.end();
	}
	
	@Override
	public void dispose() {
		modelBatch.dispose();
		model.dispose();
	}
}

Natürlich könnte man das für jede Seite einzeln entscheiden…

Disclaimer:

Das geht bestimmt auch irgendwie einfacher. Zumindest „kompakter“ oder „auf niedrigerem Level“ geht/ginge es auf jeden Fall. (Hab’ mir mal irgendwann ein „Template“ für einen Würfel gebastelt:

static BufferHandle createCubeGeometry()
{
    // "Boolean" numbering scheme
    float points[][] =
    {
        { 0, 0, 0 },
        { 1, 0, 0 },
        { 0, 1, 0 },
        { 1, 1, 0 },
        { 0, 0, 1 },
        { 1, 0, 1 },
        { 0, 1, 1 },
        { 1, 1, 1 },
    };

    int indices[] = 
    { 
        0,   2,  1,  
        0,   3,  2, 
        4,   6,  5, 
        4,   7,  6, 
        8,  10,  9, 
        8,  11, 10, 
        12, 14, 13, 
        12, 15, 14, 
        16, 18, 17, 
        16, 19, 18, 
        20, 22, 21, 
        20, 23, 22, 
    };

    byte quadIndices[] = 
    {
        0, 1, 3, 2, // front
        5, 4, 6, 7, // back
        1, 5, 7, 3, // right
        4, 0, 2, 6, // left
        2, 3, 7, 6, // top 
        4, 5, 1, 0, // bottom
    };
    float positions[] = new float[24*3];
    for (int i=0; i<24; i++)
    {
        positions[i*3+0] = points[quadIndices[i]][0];
        positions[i*3+1] = points[quadIndices[i]][1];
        positions[i*3+2] = points[quadIndices[i]][2];
    }

    
    float normals[] = 
    { 
         0.0f,  0.0f, -1.0f, 
         0.0f,  0.0f, -1.0f, 
         0.0f,  0.0f, -1.0f, 
         0.0f,  0.0f, -1.0f, 
         0.0f,  0.0f, +1.0f, 
         0.0f,  0.0f, +1.0f, 
         0.0f,  0.0f, +1.0f, 
         0.0f,  0.0f, +1.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
         0.0f, +1.0f,  0.0f, 
         0.0f, +1.0f,  0.0f, 
         0.0f, +1.0f,  0.0f, 
         0.0f, +1.0f,  0.0f, 
         0.0f, -1.0f,  0.0f, 
         0.0f, -1.0f,  0.0f, 
         0.0f, -1.0f,  0.0f, 
         0.0f, -1.0f,  0.0f, 
    };
                                
    float texCoords[] = 
    { 
        0.0f, 0.0f, 
        1.0f, 0.0f, 
        1.0f, 1.0f, 
        0.0f, 1.0f, 
        0.0f, 0.0f, 
        1.0f, 0.0f, 
        1.0f, 1.0f, 
        0.0f, 1.0f, 
        0.0f, 0.0f, 
        1.0f, 0.0f, 
        1.0f, 1.0f, 
        0.0f, 1.0f, 
        0.0f, 0.0f, 
        1.0f, 0.0f, 
        1.0f, 1.0f, 
        0.0f, 1.0f, 
        0.0f, 0.0f, 
        1.0f, 0.0f, 
        1.0f, 1.0f, 
        0.0f, 1.0f, 
        0.0f, 0.0f, 
        1.0f, 0.0f, 
        1.0f, 1.0f, 
        0.0f, 1.0f, 
    };

    float colors[] = new float[24*3];
    for (int i=0; i<24*3; i++)
    {
        if (positions[i]==1)
        {
            colors[i] = 1;
        }
    }

    float tangents[] = 
    { 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
        -1.0f,  0.0f,  0.0f, 
         0.0f,  0.0f, +1.0f, 
         0.0f,  0.0f, +1.0f, 
         0.0f,  0.0f, +1.0f, 
         0.0f,  0.0f, +1.0f, 
         0.0f,  0.0f, -1.0f, 
         0.0f,  0.0f, -1.0f, 
         0.0f,  0.0f, -1.0f, 
         0.0f,  0.0f, -1.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
        +1.0f,  0.0f,  0.0f, 
    };
    
    colors = null;
    if (dimensions == 4)
    {
        return createDefault(toShorts(indices), 4, convert3Dto4D(positions, 1.0f), convert3Dto4D(normals, 0.0f), colors, texCoords, convert3Dto4D(tangents, 0.0f));
    }
    return createDefault(toShorts(indices), 3, positions, normals, colors, texCoords, tangents);
}

und wenn ich mal irgendwo einen Würfel brauche, nehme ich das … dort könnte man halt die textCoords ändern…


Je nachdem, was du da genau vorhast, wäre es vielleicht sinnvoll, ein Modell (notfalls eine dumme OBJ-Datei…) zu laden, das schon die gewünschten Texturkoordinaten hat…

Um das ganze technisch zu untermauern:

Ein Dreieck besteht z.B. aus 3 Eckpunkten. Jeder Eckpunkt wird beschrieben durch seine Koordinaten im Raum „x y und z“. Soweit so offensichtlich. Der Grafikkarte werden zu jedem Punkt aber noch 2D Texturkoordinaten, meist „uv“ genannt, geschickt. Die UV Koordinaten beschreiben, wo die Eckpunkte der Fläche auf der Textur liegen.

Meist will man die Texturkoordinaten im 1:1 Verhältnis zur Geometrie haben, aber wie du selbst festgestellt hast kann man damit auch das „Mapping“ der Textur beliebig strecken und natürlich drehen.


Jetzt zu LibGDX. Du baust dir mit BoxShapeBuilder eine Box aus einer(!) Fläche zusammen. Wie der da genau die Textur drüber legt, liegt im Ermessen der Implementierung. Wie Marco13 bereits sagte, am Besten verwendest du ein 3D Format.

Ansonsten musst du deine Box so hindrehen, dass die Textur halt richtig liegt. Oder du passt die UV Koordinaten selbst an

mpb.setUVRange(AssetLoader.tr[0-5]); // Fläche
mpb.rect(...);

Vielen Dank an euch beide.

Ich bin mit Marcos Code erst einmal weitergekommen.
Aber morgen schaue ich mir auch einmal an, ob ich mit dem Tipp von TMII weiterkomme (also codetechnisch mit den UV Koordinaten, ich muss das Prinzip erst einmal besser verstehen und mich etwas einlesen).
Habe (aber erst einmal nur als Notlösung, falls ich nicht weiterkomme) auch Blender nun installiert, um ein Model zu basteln.

Es sieht nun so aus. Es wird. =)
image

Setz’ mal in deiner ApplicationConfig samples=2 (oder numSamples=2, je nachdem), und ersetze die Gdx.gl.glClear-Zeile mit

Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT |
  (Gdx.graphics.getBufferFormat().coverageSampling?GL20.GL_COVERAGE_BUFFER_BIT_NV:0));

Das sollte das Aliasing etwas weniger werden lassen…