Texture compression issue when size is not multiple of 4

Started by
7 comments, last by Alundra 8 years, 5 months ago

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

Advertisement

All powers of two, excluding 1 and 2, are also multiples of 4.

Niko Suni

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?

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;
  }
}

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.

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 ?

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;
};

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;

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.

This topic is closed to new replies.

Advertisement