How do I render to a face of a TextureCubeMap?

In other engines, such as jME2 and for instance Ardor3d, it is possible to set the face of a cube map as the target of a texture renderer, so that the rendered scene is drawn to that texture. I tried doing this in jME3 with limited success.



I imagine that it would involve creating an empty cube map and setting it as the target of a frame buffer. The pseudocode looks like this:

[java]// Create an empty cube map

final TextureCubeMap environment =

new TextureCubeMap(/* … some 6-layered empty image? */);



final FrameBuffer environmentBuffer = new FrameBuffer(textureSize, textureSize, 1);



// Doesn’t work, only Texture2D allowed; also I can’t specify the attached target

environmentBuffer.setColorTexture(environment);[/java]

In OpenGL, it is possible to attach the faces of the cube map as individual textures to an FBO and render to it. Is this not possible using JME?

Look at TestRenderToTexture.

@normen said:
Look at TestRenderToTexture.

The example uses a Texture2D. I said in my post that I do not want to use a Texture2D. Please read my question next time ;)

Or do you mean that I can create a TextureCubeMap from 6 Texture2Ds?
@dflemstr said:
Or do you mean that I can create a TextureCubeMap from 6 Texture2Ds?

Yes
@normen said:
Yes

Is there some example code somewhere that shows how to do this? I cannot copy the image data from 6 textures manually to the cube map on each frame (because the cube map is updated every frame); I need to render directly to the cube map's data.

Just modify the map data, yeah.

I really want to avoid copying all of the texture data on each frame; it’s 6 megabytes to copy per frame if the texture size is 512x512.



Can I use the same image data (The same ByteBuffer) in the 6 Texture2Ds as in the TextureCubeMap so that when I draw to the Texture2Ds, the cube map is updated automatically?

I think just doing the same as in TestRenderToTexture and using 6 textures with the Sky factory should work if thats what you want to do with the cubemap. But I guess I supposed wrong here ^^ You cannot render directly to a part of a (cube)map currently but as a workaround you can just try and see how the performance is if you just copy the data until the engine can do it more efficiently. The awt panels basically do the same and its not that slow actually.

@normen said:
You cannot render directly to a part of a (cube)map currently

I didn't want to use a workaround any more, so I added support for cube map RTT in jME3. Could I get this patch applied to the jME code base?

[patch]Index: engine/src/lwjgl/com/jme3/renderer/lwjgl/LwjglRenderer.java
===================================================================
--- engine/src/lwjgl/com/jme3/renderer/lwjgl/LwjglRenderer.java (revision 9321)
+++ engine/src/lwjgl/com/jme3/renderer/lwjgl/LwjglRenderer.java (working copy)
@@ -1406,7 +1406,7 @@

glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
convertAttachmentSlot(rb.getSlot()),
- convertTextureType(tex.getType(), image.getMultiSamples()),
+ convertTextureType(tex.getType(), image.getMultiSamples(), rb.getFace()),
image.getId(),
0);
}
@@ -1503,7 +1503,7 @@
&& tex.getMinFilter().usesMipMapLevels()) {
setTexture(0, rb.getTexture());

- int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples());
+ int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace());
glEnable(textureType);
glGenerateMipmapEXT(textureType);
glDisable(textureType);
@@ -1655,7 +1655,7 @@
/*********************************************************************
|* Textures *|
*********************************************************************/
- private int convertTextureType(Texture.Type type, int samples) {
+ private int convertTextureType(Texture.Type type, int samples, int face) {
switch (type) {
case TwoDimensional:
if (samples > 1) {
@@ -1672,7 +1672,13 @@
case ThreeDimensional:
return GL_TEXTURE_3D;
case CubeMap:
- return GL_TEXTURE_CUBE_MAP;
+ if (face < 0) {
+ return GL_TEXTURE_CUBE_MAP;
+ } else if (face < 6) {
+ return GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
+ } else {
+ throw new UnsupportedOperationException("Invalid cube map face index: " + face);
+ }
default:
throw new UnsupportedOperationException("Unknown texture type: " + type);
}
@@ -1728,7 +1734,7 @@
@SuppressWarnings("fallthrough")
private void setupTextureParams(Texture tex) {
Image image = tex.getImage();
- int target = convertTextureType(tex.getType(), image != null ? image.getMultiSamples() : 1);
+ int target = convertTextureType(tex.getType(), image != null ? image.getMultiSamples() : 1, -1);

// filter things
int minFilter = convertMinFilter(tex.getMinFilter());
@@ -1788,7 +1794,7 @@
}

// bind texture
- int target = convertTextureType(type, img.getMultiSamples());
+ int target = convertTextureType(type, img.getMultiSamples(), -1);
if (context.boundTextureUnit != unit) {
glActiveTexture(GL_TEXTURE0 + unit);
context.boundTextureUnit = unit;
@@ -1893,7 +1899,7 @@

Image[] textures = context.boundTextures;

- int type = convertTextureType(tex.getType(), image.getMultiSamples());
+ int type = convertTextureType(tex.getType(), image.getMultiSamples(), -1);
// if (!context.textureIndexList.moveToNew(unit)) {
// if (context.boundTextureUnit != unit){
// glActiveTexture(GL_TEXTURE0 + unit);
Index: engine/src/core/com/jme3/texture/FrameBuffer.java
===================================================================
--- engine/src/core/com/jme3/texture/FrameBuffer.java (revision 9321)
+++ engine/src/core/com/jme3/texture/FrameBuffer.java (working copy)
@@ -92,6 +92,7 @@
Image.Format format;
int id = -1;
int slot = -1;
+ int face = -1;

/**
* @return The image format of the render buffer.
@@ -129,6 +130,10 @@
return slot;
}

+ public int getFace() {
+ return face;
+ }
+
public void resetObject(){
id = -1;
}
@@ -305,8 +310,8 @@
/**
* Set the color texture to use for this framebuffer.
* This automatically clears all existing textures added previously
- * with {@link FrameBuffer#addColorTexture(com.jme3.texture.Texture2D) }
- * and adds this texture as the only target.
+ * with {@link FrameBuffer#addColorTexture } and adds this texture as the
+ * only target.
*
* @param tex The color texture to set.
*/
@@ -314,6 +319,20 @@
clearColorTargets();
addColorTexture(tex);
}
+
+ /**
+ * Set the color texture to use for this framebuffer.
+ * This automatically clears all existing textures added previously
+ * with {@link FrameBuffer#addColorTexture } and adds this texture as the
+ * only target.
+ *
+ * @param tex The cube-map texture to set.
+ * @param face The face of the cube-map to render to.
+ */
+ public void setColorTexture(TextureCubeMap tex, TextureCubeMap.Face face) {
+ clearColorTargets();
+ addColorTexture(tex, face);
+ }

/**
* Clears all color targets that were set or added previously.
@@ -347,6 +366,32 @@
}

/**
+ * Add a color texture to use for this framebuffer.
+ * If MRT is enabled, then each subsequently added texture can be
+ * rendered to through a shader that writes to the array <code>gl_FragData</code>.
+ * If MRT is not enabled, then the index set with {@link FrameBuffer#setTargetIndex(int) }
+ * is rendered to by the shader.
+ *
+ * @param tex The cube-map texture to add.
+ * @param face The face of the cube-map to render to.
+ */
+ public void addColorTexture(TextureCubeMap tex, TextureCubeMap.Face face) {
+ if (id != -1)
+ throw new UnsupportedOperationException("FrameBuffer already initialized.");
+
+ Image img = tex.getImage();
+ checkSetTexture(tex, false);
+
+ RenderBuffer colorBuf = new RenderBuffer();
+ colorBuf.slot = colorBufs.size();
+ colorBuf.tex = tex;
+ colorBuf.format = img.getFormat();
+ colorBuf.face = face.ordinal();
+
+ colorBufs.add(colorBuf);
+ }
+
+ /**
* Set the depth texture to use for this framebuffer.
*
* @param tex The color texture to set.
Index: engine/src/core/com/jme3/texture/TextureCubeMap.java
===================================================================
--- engine/src/core/com/jme3/texture/TextureCubeMap.java (revision 9321)
+++ engine/src/core/com/jme3/texture/TextureCubeMap.java (working copy)
@@ -37,6 +37,8 @@
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;

/**
* Describes a cubemap texture.
@@ -63,9 +65,9 @@
* Face of the Cubemap as described by its directional offset from the
* origin.
*/
-// public enum Face {
-// PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ;
-// }
+ public enum Face {
+ PositiveX, NegativeX, PositiveY, NegativeY, PositiveZ, NegativeZ;
+ }

public TextureCubeMap(){
super();
@@ -75,6 +77,20 @@
super();
setImage(img);
}
+
+ public TextureCubeMap(int width, int height, Image.Format format){
+ this(createEmptyLayeredImage(width, height, 6, format));
+ }
+
+ private static Image createEmptyLayeredImage(int width, int height,
+ int layerCount, Image.Format format) {
+ ArrayList<ByteBuffer> layers = new ArrayList<ByteBuffer>();
+ for(int i = 0; i < layerCount; i++) {
+ layers.add(null);
+ }
+ Image image = new Image(format, width, height, 0, layers);
+ return image;
+ }

public Texture createSimpleClone() {
return createSimpleClone(new TextureCubeMap());[/patch]
5 Likes

Note: the old patch contained an error, I updated the previous post.

(moved to contribution depot)

Cool, thanks. We’ll check and integrate if possible.

@dflemstr thanks!

@Momoko_Fan check that out and say if it’s ok.

The change has been applied to core thanks @dflemstr

1 Like

Coll, thanks will be very usefull for the space skybox in my game

Cool! Will there be a test (how to use it)?

Here’s a simple example on how to use it. It is silly because all it does is that it renders a spinning cube to one side of a sky box.



(Note that there’s a bug in SkyFactory that switches the PositiveY and NegativeY faces of a sky box… This happens when you initialize skyboxes with 6 2D textures too. So, PositiveY in the demo is actually the bottom face of the sky box.)



You have to render to all 6 faces with 6 different cameras, buffers and views, and render the root node of your scene, if you want to get an useful cube map; this just shows how to render to one of them.



[java]package jme3test.post;



import com.jme3.app.SimpleApplication;

import com.jme3.input.KeyInput;

import com.jme3.input.controls.ActionListener;

import com.jme3.input.controls.KeyTrigger;

import com.jme3.material.Material;

import com.jme3.math.ColorRGBA;

import com.jme3.math.FastMath;

import com.jme3.math.Quaternion;

import com.jme3.math.Vector3f;

import com.jme3.renderer.Camera;

import com.jme3.renderer.ViewPort;

import com.jme3.scene.Geometry;

import com.jme3.scene.shape.Box;

import com.jme3.texture.FrameBuffer;

import com.jme3.texture.Image.Format;

import com.jme3.texture.Texture;

import com.jme3.texture.TextureCubeMap;

import com.jme3.util.SkyFactory;



/**

  • This test renders a scene to a cube map, and uses that cube map as a sky box

    */

    public class TestRenderToTexture extends SimpleApplication implements ActionListener {



    private static final String TOGGLE_UPDATE = “Toggle Update”;

    private Geometry offBox;

    private float angle = 0;

    private ViewPort offView;



    public static void main(String[] args){

    TestRenderToTexture app = new TestRenderToTexture();

    app.start();

    }



    public Texture setupOffscreenView(){

    Camera offCamera = new Camera(512, 512);



    offView = renderManager.createPreView(“Offscreen View”, offCamera);

    offView.setClearFlags(true, true, true);

    offView.setBackgroundColor(ColorRGBA.DarkGray);



    // create offscreen framebuffer

    FrameBuffer offBuffer = new FrameBuffer(512, 512, 1);



    //setup framebuffer’s cam

    offCamera.setFrustumPerspective(45f, 1f, 1f, 1000f);

    offCamera.setLocation(new Vector3f(0f, 0f, -5f));

    offCamera.lookAt(new Vector3f(0f, 0f, 0f), Vector3f.UNIT_Y);



    //setup framebuffer’s texture

    TextureCubeMap offTex = new TextureCubeMap(512, 512, Format.RGBA8);

    offTex.setMinFilter(Texture.MinFilter.Trilinear);

    offTex.setMagFilter(Texture.MagFilter.Bilinear);



    //setup framebuffer to use texture

    offBuffer.setDepthBuffer(Format.Depth);

    offBuffer.setColorTexture(offTex, TextureCubeMap.Face.PositiveY);



    //set viewport to render to offscreen framebuffer

    offView.setOutputFrameBuffer(offBuffer);



    // setup framebuffer’s scene

    Box boxMesh = new Box(Vector3f.ZERO, 1,1,1);

    Material material = assetManager.loadMaterial(“Interface/Logo/Logo.j3m”);

    offBox = new Geometry(“box”, boxMesh);

    offBox.setMaterial(material);



    // attach the scene to the viewport to be rendered

    offView.attachScene(offBox);



    return offTex;

    }



    @Override

    public void simpleInitApp() {

    cam.setLocation(new Vector3f(3, 3, 3));

    cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);



    Texture offTex = setupOffscreenView();

    rootNode.attachChild(SkyFactory.createSky(assetManager, offTex, false));

    inputManager.addMapping(TOGGLE_UPDATE, new KeyTrigger(KeyInput.KEY_SPACE));

    inputManager.addListener(this, TOGGLE_UPDATE);

    }



    @Override

    public void simpleUpdate(float tpf){

    Quaternion q = new Quaternion();



    if (offView.isEnabled()) {

    angle += tpf;

    angle %= FastMath.TWO_PI;

    q.fromAngles(angle, 0, angle);



    offBox.setLocalRotation(q);

    offBox.updateLogicalState(tpf);

    offBox.updateGeometricState();

    }

    }



    @Override

    public void onAction(String name, boolean isPressed, float tpf) {

    if (name.equals(TOGGLE_UPDATE) && isPressed) {

    offView.setEnabled(!offView.isEnabled());

    }

    }

    }[/java]
1 Like

Thanks, I added this test to SVN