Rendering a 3D model to texture in LibGDX

 

Back in this post I discussed ways of making dynamically equipped 2D sprites.  One way was to render out a 3D object to 2D textures dynamically.  So far we have looked at working in 3D in LibGDX, then exporting and rendering an animated model from Blender, the next step would be a dynamic 3D object to texture.  At first glance I thought this would be difficult, but reality is, it was stupidly simple.  In fact, the very first bit of code I wrote simply worked!  Well, except the results being upside down I suppose… details details…

 

Anyways, that is what this post discusses.  Taking a 3D scene in LibGDX and rendering in a 2D texture.  The code is just a modification of the code from the Blender to GDX post from a couple days back.

 

package com.gamefromscratch;    import com.badlogic.gdx.ApplicationListener;  import com.badlogic.gdx.Files.FileType;  import com.badlogic.gdx.Gdx;  import com.badlogic.gdx.InputProcessor;  import com.badlogic.gdx.graphics.GL10;  import com.badlogic.gdx.graphics.PerspectiveCamera;  import com.badlogic.gdx.graphics.Pixmap.Format;  import com.badlogic.gdx.graphics.Texture;  import com.badlogic.gdx.graphics.g2d.SpriteBatch;  import com.badlogic.gdx.graphics.g2d.TextureRegion;  import com.badlogic.gdx.graphics.g3d.Environment;  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.loader.G3dModelLoader;  import com.badlogic.gdx.utils.UBJsonReader;  import com.badlogic.gdx.graphics.g3d.utils.AnimationController;  import com.badlogic.gdx.graphics.g3d.utils.AnimationController.AnimationDesc;  import com.badlogic.gdx.graphics.g3d.utils.AnimationController.AnimationListener;  import com.badlogic.gdx.graphics.glutils.FrameBuffer;        public class ModelTest implements ApplicationListener, InputProcessor {      private PerspectiveCamera camera;      private ModelBatch modelBatch;      private Model model;      private ModelInstance modelInstance;      private Environment environment;      private AnimationController controller;      private boolean screenShot = false;      private FrameBuffer frameBuffer;      private Texture texture = null;      private TextureRegion textureRegion;      private SpriteBatch spriteBatch;            @Override      public void create() {                  // Create camera sized to screens width/height with Field of View of 75 degrees          camera = new PerspectiveCamera(                  75,                  Gdx.graphics.getWidth(),                  Gdx.graphics.getHeight());                    // Move the camera 5 units back along the z-axis and look at the origin          camera.position.set(0f,0f,7f);          camera.lookAt(0f,0f,0f);                    // Near and Far (plane) represent the minimum and maximum ranges of the camera in, um, units          camera.near = 0.1f;           camera.far = 300.0f;            // A ModelBatch is like a SpriteBatch, just for models.  Use it to batch up geometry for OpenGL          modelBatch = new ModelBatch();                    // Model loader needs a binary json reader to decode          UBJsonReader jsonReader = new UBJsonReader();          // Create a model loader passing in our json reader          G3dModelLoader modelLoader = new G3dModelLoader(jsonReader);          // Now load the model by name          // Note, the model (g3db file ) and textures need to be added to the assets folder of the Android proj          model = modelLoader.loadModel(Gdx.files.getFileHandle("data/benddemo.g3db", FileType.Internal));          // Now create an instance.  Instance holds the positioning data, etc of an instance of your model          modelInstance = new ModelInstance(model);            //move the model down a bit on the screen ( in a z-up world, down is -z ).          modelInstance.transform.translate(0, 0, -2);            // Finally we want some light, or we wont see our color.  The environment gets passed in during          // the rendering process.  Create one, then create an Ambient ( non-positioned, non-directional ) light.          environment = new Environment();          environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.8f, 0.8f, 0.8f, 1.0f));                    // You use an AnimationController to um, control animations.  Each control is tied to the model instance          controller = new AnimationController(modelInstance);            // Pick the current animation by name          controller.setAnimation("Bend",1, new AnimationListener(){                @Override              public void onEnd(AnimationDesc animation) {                  // this will be called when the current animation is done.                   // queue up another animation called "balloon".                   // Passing a negative to loop count loops forever.  1f for speed is normal speed.                  controller.queue("Balloon",-1,1f,null,0f);              }                @Override              public void onLoop(AnimationDesc animation) {                  // TODO Auto-generated method stub                                }                        });                    frameBuffer = new FrameBuffer(Format.RGB888,Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),false);          Gdx.input.setInputProcessor(this);                    spriteBatch = new SpriteBatch();      }        @Override      public void dispose() {          modelBatch.dispose();          model.dispose();      }        @Override      public void render() {          // You've seen all this before, just be sure to clear the GL_DEPTH_BUFFER_BIT when working in 3D          Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());          Gdx.gl.glClearColor(1, 1, 1, 1);          Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);            // When you change the camera details, you need to call update();          // Also note, you need to call update() at least once.          camera.update();                    // You need to call update on the animation controller so it will advance the animation.  Pass in frame delta          controller.update(Gdx.graphics.getDeltaTime());                              // If the user requested a screenshot, we need to call begin on our framebuffer          // This redirects output to the framebuffer instead of the screen.          if(screenShot)              frameBuffer.begin();            // Like spriteBatch, just with models!  pass in the box Instance and the environment          modelBatch.begin(camera);          modelBatch.render(modelInstance, environment);          modelBatch.end();                    // Now tell OpenGL that we are done sending graphics to the framebuffer          if(screenShot)          {              frameBuffer.end();              // get the graphics rendered to the framebuffer as a texture              texture = frameBuffer.getColorBufferTexture();              // welcome to the wonderful world of different coordinate systems!              // simply put, the framebuffer is upside down to normal textures, so we have to flip it              // Use a TextureRegion to do so              textureRegion = new TextureRegion(texture);              // and.... FLIP!  V (vertical) only              textureRegion.flip(false, true);          }                    // In the case that we have a texture object to actually draw, we do so          // using the old familiar SpriteBatch to do so.          if(texture != null)          {              spriteBatch.begin();              spriteBatch.draw(textureRegion,0,0);              spriteBatch.end();              screenShot = false;          }      }        @Override      public void resize(int width, int height) {      }        @Override      public void pause() {      }        @Override      public void resume() {      }        @Override      public boolean keyDown(int keycode) {          // TODO Auto-generated method stub          return false;      }        @Override      public boolean keyUp(int keycode) {          // TODO Auto-generated method stub          return false;      }        @Override      public boolean keyTyped(char character) {          // If the user hits a key, take a screen shot.          this.screenShot = true;          return false;      }        @Override      public boolean touchDown(int screenX, int screenY, int pointer, int button) {          // TODO Auto-generated method stub          return false;      }        @Override      public boolean touchUp(int screenX, int screenY, int pointer, int button) {          // TODO Auto-generated method stub          return false;      }        @Override      public boolean touchDragged(int screenX, int screenY, int pointer) {          // TODO Auto-generated method stub          return false;      }        @Override      public boolean mouseMoved(int screenX, int screenY) {          // TODO Auto-generated method stub          return false;      }        @Override      public boolean scrolled(int amount) {          // TODO Auto-generated method stub          return false;      }  }

And… that’s it.

Run the code and you get the exact same results as the last example:

blenderAnimation

 

However, if you press any key, the current frame is saved to a texture, and that is instead displayed on screen.  Press a key again and it will update to the current frame:

image

Of course, the background colour is different, because we didn’t implicitly set one.  The above a is LibGDX Texture object, which can now be treated as a 2D sprite, used as a texture map, whatever.

 

The code is ultra simple.  We have a toggle variable screenShot, that gets set if a user hits a key.  The actual process of rendering to texture is done with the magic of FrameBuffersl  Think of a framebuffer as a place for OpenGL to render other than your video card.  So instead of drawing the graphics to the screen, it instead draws the graphics to a memory buffer.  We then get this memory buffer as a texture using getColorBufferTexture().  The only complication is the frame buffer is rendered upside down.  This is easily fixed by wrapping the Texture in a TextureRegion and flipping the V coordinate.  Finally we display our newly generated texture using our old friend, the SpriteBatch.

 

 

Gotta love it when something you expect to be difficult ends up being ultra easy.  Next I have to measure the performance, so see if this is something that can be done in a realtime situation, or do we need to do this onload/change?

Programming


Scroll to Top