[fixed] Texture offset - bleeding texture atlas

If you are a new Irrlicht Engine user, and have a newbie-question, this is the forum for you. You may also post general programming questions here.
Post Reply
MartinVee
Posts: 139
Joined: Tue Aug 02, 2016 3:38 pm
Location: Québec, Canada

[fixed] Texture offset - bleeding texture atlas

Post by MartinVee »

I have created a class derived from a Billboard which should be textured by a sprite sheet (also known as a texture atlas). I used the techniques described here and here as basis for my implementation.

As usual, I used a sprite sheet found on the internet, that I put in a 1024x1024 image to avoid the potential NPOT problems. The texture can be found here, in case it’s relevant. Don’t judge me, my boss is a cat person. :D

My problem is that when I apply my texture transform matrix, I can see a bit of the sprite that is directly on top of the one I’m displaying. It’s like the display is offseted vertically by one pixel. As far as I can tell, I computed the matrix correctly, but I may have overlooked something.

Here’s the relevant code. Bear in mind that I snipped some validation and sanitization part to be more concise :

Code: Select all

 
void TBillboardAnimation::ParseTexture(ITexture * texture, TParseMethod parseMethod, int columnCount, int rowCount)
{
  float horizontalRatio; //! horizontal (width) value of a node relative to the texture size
  float verticalRatio;   //! vertical (height) value of a node relative to the texture size
 
  dimension2du textureSize = texture->getSize();
 
  // Since texture matrices uses vector from 0.0f to 1.0f, we need to find the ratio between the render target and the texture size.
  horizontalRatio = (f32)_Size.Width / (f32)textureSize.Width;
  verticalRatio = (f32)_Size.Height / (f32)textureSize.Height;
 
  // We loop for the number of matrices we need to parse.
  int currentColumn = 0;
  int currentRow = 0;
  core::vector2df transformVector;
  core::matrix4 transformMatrix;
  for(int i = 0; i < columnCount*rowCount; i++)
  {
    // Create a transform vector.
    transformVector = core::vector2df((f32)currentColumn * horizontalRatio, (f32)currentRow * verticalRatio);
    // Build the actual texture transform matrix from the transform vector.
    transformMatrix.buildTextureTransform(0.0f, core::vector2df(0.0f), transformVector, core::vector2df(horizontalRatio, verticalRatio));
 
    // Add the texture and the transformation matrix to the frames' list.
    // NOTE : I have a vector of TTransformationMatrix, which is simply a struct compounding an ITexture* with a matrix4. It's used when it's time to render.
    AddTransformationMatrix(texture, transformMatrix);
 
    // Handle the parse method
    // NOTE : This is so we can have texture atlas that goes horizontally or vertically.
    switch(parseMethod)
    {
      // Parse as Rows. Increment the columns ; once we're done with a row, return to the first column and switch row.
      case TParseMethod::ByRows :
      {
        currentColumn++;
        if(currentColumn >= columnCount)
        {
          currentColumn = 0;
          currentRow++;
        }
        break;
      }
      // Parse as Columns. Increment the rows ; once we're done with a column, return to the first row and switch column.
      case TParseMethod::ByColumns :
      {
        currentRow++;
        if(currentRow >= rowCount)
        {
          currentRow = 0;
          currentColumn++;
        }
        break;
      }
    }
  }
}
 
And here’s the render code, which is simply a matter of getting the values I pushed in the vector earlier, and setting the texture and the texture matrix :

Code: Select all

 
void TBillboardAnimation::render()
{
  if(_TransformationMatrixVector.size() > 0)
  {
    TTransformationMatrix ttMatrix = _TransformationMatrixVector[GetCurrentFrame()];
    setMaterialTexture(0, ttMatrix.texture);
    getMaterial(0).setTextureMatrix(0, ttMatrix.matrix);
 
    CBillboardSceneNode::render();
  }
}
 
Last edited by MartinVee on Fri Oct 21, 2016 5:41 pm, edited 2 times in total.
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: Texture offset using a sprite sheet

Post by hendu »

Bleed is a known issue with texture atlases, usually hit when using mipmaps, but also possible due to floating point inaccuracies with mipmaps disabled.

In short: if your hw supports texture arrays, use them instead. If you need texture atlases, in your 2d case you should disable mipmaps and set the interpolation to nearest.
MartinVee
Posts: 139
Joined: Tue Aug 02, 2016 3:38 pm
Location: Québec, Canada

Re: Texture offset using a sprite sheet - bleeding texture a

Post by MartinVee »

Thanks hendu for your help!

I checked, and I'd already disabled the mipmaps. I tried to set the interpolation to nearest (using OpenGL code, as I haven't found a way of doing it directly in Irrlicht), but it didn't quite help ; some bleeding frame didn't bleed anymore, and some other started to bleed.

But once you correctly named the problem I was having (that is, texture bleeding), I could google that and look for a solution.

Here's what I found for future reference.

First of all, I found that rule of thumb :
If you 2D, don't mipmaps.
If you 3D, don't texture atlas.
And then I found the following answer on Gamedev which pointed me to that MSDN article about directly mapping texels to pixels.

So I changed the way I parsed the texture, and updated the function like this :

Code: Select all

 
irr::core::vector2df TBillboardAnimation::GetTexturePixelPosition(irr::video::ITexture * texture, irr::core::position2di position)
{
  dimension2du textureSize = texture->getSize();
  position2df textureMatrixPosition = position2df(position.X, position.Y);
 
  // Fix the position by a half-pixel (a technique called incidentally half-pixel correction).
  textureMatrixPosition.X += 0.5;
  textureMatrixPosition.Y += 0.5;
 
  textureMatrixPosition.X /= (f32)textureSize.Width;
  textureMatrixPosition.Y /= (f32)textureSize.Height;
 
  return vector2df(textureMatrixPosition);
}
 
 
void TBillboardAnimation::ParseTexture(ITexture * texture, TParseMethod parseMethod, int columnCount, int rowCount, s32 xOffset/* = 0*/, s32 yOffset/* = 0*/)
{
  float horizontalRatio; //! horizontal (width) value of a node relative to the texture size
  float verticalRatio;   //! vertical (height) value of a node relative to the texture size
 
  dimension2du textureSize = texture->getSize();
 
  // Since texture matrices uses vector from 0.0f to 1.0f, we need to find the ratio between the render target and the texture size.
  horizontalRatio = (f32)_Size.Width / (f32)textureSize.Width;
  verticalRatio = (f32)_Size.Height / (f32)textureSize.Height;
 
  // We loop for the number of matrices we need to parse.
  int currentColumn = 0;
  int currentRow = 0;
  core::vector2df transformVector;
  core::matrix4 transformMatrix;
  for(int i = 0; i < columnCount*rowCount; i++)
  {
    // Get the extrapolated texture position, and create a transformation vector with it
    transformVector = GetTexturePixelPosition(texture, position2di((currentColumn * _Size.Width) + xOffset, (currentRow * _Size.Height) + yOffset));
 
    // Build the actual texture transform matrix from the transform vector.
    transformMatrix.buildTextureTransform(0.0f, core::vector2df(0.0f), transformVector, core::vector2df(horizontalRatio, verticalRatio));
 
    // Add the texture and the transformation matrix to the frames' list.
    // NOTE : I have a vector of TTransformationMatrix, which is simply a struct compounding an ITexture* with a matrix4. It's used when it's time to render.
    AddTransformationMatrix(texture, transformMatrix);
 
    // Handle the parse method
    // NOTE : This is so we can have texture atlas that goes horizontally or vertically.
    switch(parseMethod)
    {
      // Parse as Rows. Increment the columns ; once we're done with a row, return to the first column and switch row.
      case TParseMethod::ByRows :
      {
        currentColumn++;
        if(currentColumn >= columnCount)
        {
          currentColumn = 0;
          currentRow++;
        }
        break;
      }
      // Parse as Columns. Increment the rows ; once we're done with a column, return to the first row and switch column.
      case TParseMethod::ByColumns :
      {
        currentRow++;
        if(currentRow >= rowCount)
        {
          currentRow = 0;
          currentColumn++;
        }
        break;
      }
    }
  }
}
 
I than proceeded to test the results, and so far, I created a 800x600 texture atlas with twelve 128x128 sprites, twenty five 32x32 sprites, ten 17x17 sprites, and ten 17x17 sprites offseted by 1 pixel on the X-axis, and everything is displaying properly.

I also realized, browsing the code, and knowing what to look for, that Irrlicht is also using the half-pixel correction technique profusely, for example, in CMD2MeshFileLoader.cpp
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: Texture offset using a sprite sheet - bleeding texture a

Post by hendu »

To use nearest interpolation in irr, disable anisotropic, trilinear, and bilinear in the material.
Post Reply