Jump to content
  • Advertisement
Sign in to follow this  
Alundra

Texture compression issue when size is not multiple of 4

This topic is 1084 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hi,

I have an issue of compression when the texture size is not a multiple of 4, I got a black texture.

I use NVTT to compress the texture after mipmaps generated if the texture is a power of two.

Is it needed to check if the texture size is a multiple of 4 when compressing to stay RGBA on this case ?

Thanks

Edited by Alundra

Share this post


Link to post
Share on other sites
Advertisement

If the size isn't a multiple of 4, then the blocks along the bottom/right edges will just contain some undefined pixels in the "unused" area. It's perfectly valid.

NVTT correctly rounds up the width/height in pixels to the next multiple of 4 when calculating the width/height in blocks.

At what point do you discover that the texture is black? Do all the NVTT output callbacks get called / do the supply the right amount of data / is that data all zeros?

Share this post


Link to post
Share on other sites

Here what happens :

1) I import the image 186x138 (not power of two and not multiple of 4).

2) GenerateMipmaps called but not effect because it's not a power of two.

3) ChangeImageFormat called to BC3 by default.

4) Save the texture data to the file.

Once imported I see the result in the texture editor, a fully black texture.

I simply set the handler of the NVTT :

struct TOutputHandler : nvtt::OutputHandler
{
  TOutputHandler() :
  MipmapDataSize( 0 )
  {
  }

  virtual void beginImage( int size, int width, int height, int depth, int face, int miplevel ) override
  {
    CArray< DE::UInt8 > NewMipmap;
    NewMipmap.SetSize( size );
    MipmapArray.Add( NewMipmap );
    MipmapDataSize += size;
  }

  virtual bool writeData( const void* data, int size ) override
  {
    std::memcpy( &MipmapArray.GetLast()[ 0 ], data, size );
    return true;
  }

  virtual void endImage() override{}

  CArray< DE::CArray< DE::UInt8 > > MipmapArray;
  UInt32 MipmapDataSize;
};

Here the ChangeImageFormat function :

void CImage::ChangeImageFormat( const TImageFormat& ImageFormat )
{
  // Check for invalid data.
  if( m_Data == nullptr || m_ImageFormat != IF_RGBA8 || m_ImageFormat == ImageFormat )
    return;

  // Set the input options.
  nvtt::InputOptions InputOptions;
  InputOptions.setTextureLayout( nvtt::TextureType_2D, m_Width, m_Height );

  // Offset variable used for mipmaps.
  UInt32 Offset = 0;

  // Set each mipmap data.
  UInt32 MipmapWidth = m_Width;
  UInt32 MipmapHeight = m_Height;
  for( UInt32 i = 0; i < m_NumMipMaps; ++i )
  {
    // Convert to BGRA.
    UInt8* MipmapData = new UInt8[ MipmapWidth * MipmapHeight * 4 ];
    UInt8* DataRGBA = m_Data + Offset;
    UInt8* DataBGRA = MipmapData;
    for( UInt32 y = 0; y < MipmapHeight; ++y )
    {
      for( UInt32 x = 0; x < MipmapWidth; ++x )
      {
        DataBGRA[ 0 ] = DataRGBA[ 2 ];
        DataBGRA[ 1 ] = DataRGBA[ 1 ];
        DataBGRA[ 2 ] = DataRGBA[ 0 ];
        DataBGRA[ 3 ] = DataRGBA[ 3 ];
        DataBGRA += 4;
        DataRGBA += 4;
      }
    }

    // Set the mipmap data and update the offset.
    InputOptions.setMipmapData( MipmapData, MipmapWidth, MipmapHeight, 1, 0, i );
    Offset += MipmapWidth * MipmapHeight * 4;

    // Next mipmap.
    MipmapWidth /= 2;
    MipmapHeight /= 2;
    delete[] MipmapData;
  }

  // Handlers.
  TErrorHandler ErrorHandler;
  TOutputHandler OutputHandler;

  // Set the output options.
  nvtt::OutputOptions OutputOptions;
  OutputOptions.setOutputHeader( false );
  OutputOptions.setErrorHandler( &ErrorHandler );
  OutputOptions.setOutputHandler( &OutputHandler );

  // Set the format.
  nvtt::Format Format;
  switch( ImageFormat )
  {
    case IF_BC1 : Format = nvtt::Format_BC1; break;
    case IF_BC2 : Format = nvtt::Format_BC2; break;
    case IF_BC3 : Format = nvtt::Format_BC3; break;
    case IF_BC4 : Format = nvtt::Format_BC4; break;
    case IF_BC5 : Format = nvtt::Format_BC5; break;
    case IF_BC6 : Format = nvtt::Format_BC6; break;
    case IF_BC7 : Format = nvtt::Format_BC7; break;
    default:
    {
      CEngine::GetLogger().LogError( "CImage, change image format failed" );
      return;
    }
  }

  // Set the compression options.
  nvtt::CompressionOptions CompressionOptions;
  CompressionOptions.setQuality( nvtt::Quality_Highest );
  CompressionOptions.setFormat( Format );

  // Compress.
  nvtt::Compressor Compressor;
  if( Compressor.process( InputOptions, CompressionOptions, OutputOptions ) )
  {
    // Create the new image data.
    delete[] m_Data;
    m_Data = new UInt8[ OutputHandler.MipmapDataSize ];
    m_DataSize = OutputHandler.MipmapDataSize;

    // Set the new image data.
    Offset = 0;
    for( UInt32 i = 0; i < OutputHandler.MipmapArray.GetSize(); ++i )
    {
      const CArray< UInt8 >& Mipmap = OutputHandler.MipmapArray[ i ];
      std::memcpy( m_Data + Offset, &Mipmap[ 0 ], Mipmap.GetSize() );
      Offset += Mipmap.GetSize();
    }

    // Change the image format.
    m_ImageFormat = ImageFormat;
  }
}
Edited by Alundra

Share this post


Link to post
Share on other sites

Does writeData get called more than once per image? Your handler won't cope, if so.

 

p.s. you can have mipmaps for non-power-of-two textures if you like. Each mip is just half the size of the previous one, rounding down, but clamping to 1px.

Share this post


Link to post
Share on other sites

I logged at the start of writeData, I got multiple log message :

[2015/10/25-12:53:30] writeData called
[2015/10/25-12:53:31] writeData called
[2015/10/25-12:53:31] writeData called
[2015/10/25-12:53:31] writeData called
[2015/10/25-12:53:31] writeData called
[2015/10/25-12:53:31] writeData called
[2015/10/25-12:53:31] writeData called
[2015/10/25-12:53:31] writeData called

I tested to import the image into unreal engine 4, no mipmaps possible and the format stays B8G8R8A8.

What is the good behavior to handle this case, check if the mipmap array equals the number of mipmaps or check if it's not multiple of 4 ?

Since not multiple of 4 works in some case from what it was said, check the mipmap array is more correct ?

Edited by Alundra

Share this post


Link to post
Share on other sites

I logged at the start of writeData, I got multiple log message

So your handler is receiving 8 blocks of data, but you're writing them all to the exact same location (where the first block should be located).
Change your handler to increment a destination offset, e.g.
struct TOutputHandler : nvtt::OutputHandler
{
...
  virtual void beginImage( int size, int width, int height, int depth, int face, int miplevel ) override
  {
...
    CurrentOffset = 0;
  }
  virtual bool writeData( const void* data, int size ) override
  {
    std::memcpy( &MipmapArray.GetLast()[ CurrentOffset ], data, size );
    CurrentOffset += size;
    return true;
  }
...
  UInt32 CurrentOffset;
};

Share this post


Link to post
Share on other sites

I tried, the texture still fully black.

I logger the mipmap array size, it has size of 8, here the log beginImage + writeData :

[2015/10/25-16:35:41] beginImage called
[2015/10/25-16:35:42] writeData called
[2015/10/25-16:35:42] beginImage called
[2015/10/25-16:35:42] writeData called
[2015/10/25-16:35:42] beginImage called
[2015/10/25-16:35:42] writeData called
[2015/10/25-16:35:42] beginImage called
[2015/10/25-16:35:42] writeData called
[2015/10/25-16:35:42] beginImage called
[2015/10/25-16:35:42] writeData called
[2015/10/25-16:35:42] beginImage called
[2015/10/25-16:35:42] writeData called
[2015/10/25-16:35:42] beginImage called
[2015/10/25-16:35:42] writeData called
[2015/10/25-16:35:42] beginImage called
[2015/10/25-16:35:42] writeData called

I tried to import an image with the size 33x61, same case, not power of two and not multiple of 4, got fully black too.

Imported in unreal as test like the previous test, the format still B8G8R8A8, not compressed.

 

EDIT:

Apparently by default mipmaps are generated if not specified it's why 8 writeData was there, I added :

InputOptions.setMipmapGeneration( true, m_NumMipMaps );

Now only one beginImage and writeData is called, I logged the size, width and height :

[2015/10/25-22:44:23] ChangeImageFormat called:186,138
[2015/10/25-22:44:23] beginImage called:26320,186,138
[2015/10/25-22:44:23] writeData called:26320

The size is the same, it's not padded to a multiple of 4 apparently, it's why the texture is fully black surely.

From this link : https://www.opengl.org/registry/specs/EXT/texture_compression_s3tc.txt

TexSubImage2D and CopyTexSubImage2D will result in an INVALID_OPERATION
error only if one of the following conditions occurs:

* <width> is not a multiple of four, <width> plus <xoffset> is not
equal to TEXTURE_WIDTH, and either <xoffset> or <yoffset> is
non-zero;

* <height> is not a multiple of four, <height> plus <yoffset> is not
equal to TEXTURE_HEIGHT, and either <xoffset> or <yoffset> is
non-zero; or

* <xoffset> or <yoffset> is not a multiple of four. 

I think it's the same for Direct3D.

I changed the starting of the ChangeImageFormat like that since no other working solution found :

// Check for invalid data.
if( m_Data == nullptr || m_ImageFormat != IF_RGBA8 || ImageFormat == IF_RGBA8 || ( m_Width % 4 ) != 0 || ( m_Height % 4 ) != 0 )
    return;
Edited by Alundra

Share this post


Link to post
Share on other sites

I contacted one dev of NVTT and now it contains :

RoundMode_ToNextMultipleOfFour
RoundMode_ToNearestMultipleOfFour
RoundMode_ToPreviousMultipleOfFour

This line solve the issue of multiple of 4 :

InputOptions.setRoundMode(RoundMode_ToPreviousMultipleOfFour);

But it's important to note if the texture is not a multiple of 4 the mipmaps are genererated by NVTT from the top level and do not use the provided.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!