• Runtimes
  • Libgdx FBO filtering

Hi,

I'm doing a isometric type game. For each character i have 41 different poses with corresponding animations. This means 41 different json spine files. What Im trying to achieve is to take setup pose for each of 41 poses and make a texture atlas. When exporting from spine I scale everything to 0.3f.

Im trying to use FBO to render the texture.

   

   final static int widthConst = 64;
   final static int heightConst = 128;

   final static int FBOwidthConst = 2048;
   final static int FBOheightConst = 2048;

...
YieldingFrameBuffer fbo = new YieldingFrameBuffer(Pixmap.Format.RGBA8888, FBOwidthConst, FBOheightConst, false);
      //fbo.getColorBufferTexture().setFilter(TextureFilter.Linear, TextureFilter.Linear);
      fbo.begin();
      skeletonRenderer.setPremultipliedAlpha(true);

  batch.getProjectionMatrix().setToOrtho2D(0, 0, fbo.getWidth(), fbo.getHeight());

  Gdx.gl20.glClearColor(0f, 0f, 0f, 0);
  Gdx.gl20.glClear(GL20.GL_COLOR_BUFFER_BIT);
  Gdx.gl20.glEnable(GL20.GL_TEXTURE_2D);

  batch.begin();

  int fitInRow = fbo.getWidth() / widthConst;

  int currIdx = 0;
  for (int j = 0; j < charactersNeeded.length; j++)
  {
     IntMap<TextureSkeletonHolder> map = _dictOfTextureAtlas.get(charactersNeeded[j]);

     IntArray arr = map.keys().toArray();
     arr.sort();

     for (int idx : arr.items)
     {
        TextureSkeletonHolder holder = map.get(idx);
        for (Texture tex : holder.TextureAtlas.getTextures())
           tex.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
        Skeleton skeleton = new Skeleton(holder.SkeletonData);
        AnimationStateData animationState = new AnimationStateData(holder.SkeletonData);
        AnimationState animstat = new AnimationState(animationState);
        int currentIndex = currIdx++;

        int multipyBy = currentIndex / fitInRow;

        int y = multipyBy * heightConst;

        y = (heightConst) + y;

        int x = currentIndex * widthConst - (multipyBy * fitInRow * widthConst);

        skeleton.setPosition(x, y);
        skeleton.setFlipY(true);

        animstat.apply(skeleton);
        skeleton.updateWorldTransform();

        skeleton.setToSetupPose();

        skeletonRenderer.draw(batch, skeleton);

        skeleton.setFlipY(false);

     }

  }

  batch.end();

  fbo.end();

  TextureRegion testTexture = new TextureRegion(fbo.disposeAndTakeColorTexture());
  testTexture.getTexture().setFilter(TextureFilter.Linear, TextureFilter.Linear);
  testTexture.flip(false, true);

However the rendered images don't seem reneged like they should. For example, it seems that they are little bit stretched. I played with filters, but I got blurry images if I don't set (Nearest, Nearest) in spine texture.

Here is the example, notice a slight streching.

Изображение удалено из-за отсутствия поддержки HTTPS. | Показать

Any idea what I'm doing wrong?

Related Discussions
...
  • Изменено

Hm, did you set the glViewport for the FBO properly? That'd explain why the stretching occurs. Could you draw a plain image (as Texture or TextureRegion) to the FBO? That'd be a good control of whether the FBO rendering is setup correctly.

Good idea. I took the texture atlas that i used for spine animation and it is drawn correctly. Notice after the loop I draw the atlas.

Here is the FBO rendered image that i saved on disk. On the left is the image is textureatlas drawn by batch.draw(tex.getTextures().first(), 350, 500); , right is drawn by skeletonRenderer.draw(batch, skeleton);

Still I cannot understand why is this happening. Seems there is some scaling.

Изображение удалено из-за отсутствия поддержки HTTPS. | Показать

      
YieldingFrameBuffer fbo = new YieldingFrameBuffer(Pixmap.Format.RGBA8888, FBOwidthConst, FBOheightConst, false); fbo.begin(); skeletonRenderer.setPremultipliedAlpha(true); batch.getProjectionMatrix().setToOrtho2D(0, 0, fbo.getWidth(), fbo.getHeight()); batch.begin(); Gdx.gl.glEnable(GL20.GL_BLEND); batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA); Gdx.gl20.glViewport(0, 0, fbo.getWidth(), fbo.getHeight()); Gdx.gl20.glClearColor(0f, 0f, 0f, 0f); Gdx.gl20.glClear(GL20.GL_COLOR_BUFFER_BIT); Gdx.gl20.glEnable(GL20.GL_TEXTURE_2D); skeletonRenderer = new SkeletonRenderer<Batch>(); skeletonRenderer.setPremultipliedAlpha(true); int fitInRow = fbo.getWidth() / widthConst; int currIdx = 0; for (int j = 0; j < charactersNeeded.length; j++) { IntMap<TextureSkeletonHolder> map = _dictOfTextureAtlas.get(charactersNeeded[j]); IntArray arr = map.keys().toArray(); arr.sort(); for (int idx : arr.items) { TextureSkeletonHolder holder = map.get(idx); //holder.TextureAtlas.getTextures().first().setFilter(TextureFilter.Nearest, (TextureFilter.Nearest)); Skeleton skeleton = new Skeleton(holder.SkeletonData); AnimationStateData animationState = new AnimationStateData(holder.SkeletonData); AnimationState animstat = new AnimationState(animationState); int currentIndex = currIdx++; int multipyBy = currentIndex / fitInRow; int y = multipyBy * heightConst; y = (heightConst) + y; int x = currentIndex * widthConst - (multipyBy * fitInRow * widthConst); skeleton.setPosition(x, y); skeleton.setFlipY(true); animstat.apply(skeleton); skeleton.updateWorldTransform(); skeleton.setToSetupPose(); skeletonRenderer.draw(batch, skeleton); skeleton.setFlipY(false); } } IntMap<TextureSkeletonHolder> map2 = _dictOfTextureAtlas.get(charactersNeeded[2]); TextureAtlas tex = map2.get(1).TextureAtlas; batch.draw(tex.getTextures().first(), 350, 500); batch.end();

Ok, from your latest screenshot i gather that the original stretching is now gone.

The discrepancy you see between FBO and non-FBO is likely a blending function issue. How do you draw the contents of the FBO to the on-screen buffer?

I don't think is gone. I suppose that this 2 renderings (plain textureatlas, spinerednerer) should be same, but in fact they are not.

If I don't put filtering, texture is blurry (my second post)
If I put nearest,nearest, the texture is stretched (first post). <- This one looks more similar to original.

What I do is:
1) Render the FBO (spine and atlas texture)
2) Save the rendered into a PNG locally

There I see that the atlas texture is not equal like the texture rendered from SpineRenderer. So I think the problem somewhere during the FBO rendering.

When I render the SpineRenderer on screen buffer, everything seems fine (like the atlas texture).

I really have difficulties understanding why is this happening.

One more thing, when exporting in texture packer, I set 0.333, and then i scale in game like this:

               SkeletonJson json = new SkeletonJson(atlas);
json.setScale(sizing);

Just to check, if the sizing is same value, this should not change the size of textures, right?

The SkeletonRenderer doesn't really care what you render to (FBO, on-screen buffer). It must have something to do with the way the FBO is handled by your code. One reason the PNG may look different to rendering the FBO to the on-screen buffer is that the PNG will only contain the FBO content. The FBO is cleared with (0, 0, 0, 0), so that's quite a bit of a difference to the on-screen buffer, which usually has alpha set to 1. Try clearing the FBO to some background color with alpha = 1 and save to PNG.

Maybe you didn't understand me.

1) I render both texture of spine and the spine animation in same FBO.
2) For debug purposes i export the FBO into png
3) PNG is same as its rendered on screen

Problem:
The rendered spine animation is blurry if compared with the texture rendered.
If i set nearest, nearest in the texture, spine animation rendered is stretched.

If i set the alpha to 1, the background is black, but the problem is still there. The image rendered with spine rendered is blurry.

Please modify one of the spine-libgdx-test to repro the behaviour. I can't seem to repro it with our assets. I also don't see how setting a nearest/nearest filter can result in the extensive stretching shown in the screenshot above.

OK, I have created a small project containing the example to show the behavior. Since I'm using private assets, is it possible to send it on email?

Yes! Please send the project to contact@esotericsoftware.com.

If you rotate or scale your skeleton's images/meshes or otherwise render in a way that doesn't map 1:1 image pixels to screen pixels (which includes rendering at non-integer coordinates with a 1:1 camera, or moving/zooming/rotating the camera so it is not 1:1) then OpenGL has to somehow map image pixels to screen pixels. Doing that is called "filtering" and can result in blurriness. There is no other way for OpenGL (or any graphics toolkit) to render an image pixel which falls between two screen pixels.

Rendering to an FBO or rendering to the screen is exactly the same. As far as OpenGL is concerned, an FBO and the screen are both just a "render buffer". When I said "screen pixels" above, you can take that to mean "render buffer pixels". Mario provided this SSCE showing there is absolutely no difference between rendering a skeleton to a (properly configured) FBO and rendering it to the screen:
https://gist.github.com/badlogic/a07a45c977c1d0b737f70c197aef5535

In libgdx/OpenGL you can choose from linear or nearest neighbor filtering (which is a setting on the atlas texture). Nearest is less blurry/more sharp, but still not the same as 1:1 rendering.

Keep in mind that drawing a skeleton consists of drawing many images, which almost always have some rotation, especially when animated. Most apps don't mind the filtering because skeletons are always animated (eg an idle animation). I understand you are rendering static images of your skeletons. If all the images in your setup pose do not have rotation, if you are rendering your setup pose then you can have 1:1 image to screen pixels without blurriness from filtering. However, if you do anything that invokes filtering (as described above) then you will have blurriness. This isn't a Spine runtime deficiency, it is just how graphics toolkits function.

To replicate the problem I'm having:
With the code you provided me, I put the FBO in constructor, and during rendering (after rendering the FBO) zoomed the camera.

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Pixmap; 
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.esotericsoftware.spine.*;

public class SpineFBOTest extends ApplicationAdapter
{
   OrthographicCamera camera;
   SpriteBatch batch;
   SkeletonRenderer renderer;

   TextureAtlas atlas;
   Skeleton skeleton;

   FrameBuffer fbo;
   TextureRegion fboRegion;
   boolean useFbo = false;

   @Override
   public void create()
   {
      camera = new OrthographicCamera();
      batch = new SpriteBatch();
      renderer = new SkeletonRenderer();
      renderer.setPremultipliedAlpha(true);

  atlas = new TextureAtlas(Gdx.files.internal("annimation/Farmer1/Farmer1.atlas"));
         
  SkeletonJson json = new SkeletonJson(atlas);
  json.setScale(0.33f);
  SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("annimation/Farmer1/Farmer12.json"));

  skeleton = new Skeleton(skeletonData);
  skeleton.setPosition(250, 20);

  fbo = new FrameBuffer(Pixmap.Format.RGBA8888, 512, 512, false);
  fboRegion = new TextureRegion(fbo.getColorBufferTexture());  
  fboRegion.flip(false, true);

  skeleton.updateWorldTransform();
  fbo.begin();
  Gdx.gl.glClearColor(0, 0, 0, 0);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  camera.setToOrtho(false, fbo.getWidth(), fbo.getHeight());
  camera.update();
  batch.getProjectionMatrix().set(camera.combined);
  batch.begin();
  renderer.draw(batch, skeleton);
  batch.end();
  fbo.end();

   }

   @Override
   public void render()
   {
      Gdx.gl.glClearColor(1, 1, 1, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

  camera.zoom = 0.5f; 

  skeleton.updateWorldTransform();

  if (!useFbo)
  {
     camera.setToOrtho(false);
     camera.update();
     batch.getProjectionMatrix().set(camera.combined);
     batch.begin();
     renderer.draw(batch, skeleton);
     batch.end();
  } else
  {
     // Render skeleton to FBO
     // Render FBO color buffer texture to screen      
     camera.setToOrtho(false);
     camera.update();
     batch.getProjectionMatrix().set(camera.combined);
     batch.begin();
     batch.draw(fboRegion, 0, 0);
     batch.end();
  }

  if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE))
  {
     useFbo = !useFbo;
     Gdx.app.log("SpineFBOTest", "Using FBO: " + useFbo);
  }

   }

   @Override
   public void resize(int width, int height)
   {
      camera.setToOrtho(false);
   }
}

If you don't use a pixel perfect projection matrix (read: without zoom), it's not possible to get pixel perfect rendering. Texture filtering will kick in and result in the blur you see.

Your code is not an SSCCE. It has no main method and requires assets. It should use one of the examples in spine-libgdx-tests so anyone who wants to help can just copy/paste and run it. This also ensures that we are seeing the same results. Here is an SSCCE:
http://n4te.com/x/125-H3Is.txt
The code renders the FBO initially at full size, then draws that FBO texture to the screen at twice the size (camera zoom 0.5). Pressing spacebar allows comparison between rendering the FBO at twice the size and rendering the skeleton at twice the size.

When you render to the FBO, the skeleton images are sampled and the resulting pixels are written to the FBO buffer. When you draw this twice as large, the result is poor/blurry. This is not surprising, because you are essentially drawing an image (the FBO) at twice the size, so it of course lacks details.

When you render the skeleton at twice the size, it samples the skeleton images to draw them at that larger size. If the images themselves are higher resolution than what was rendered to the FBO (as they are in this case), then this results in more detail than was rendered to the FBO.