Add ETC support to mipgen / imageio / suzanne.

This shrinks file size as follows, although it seems to darken the
rendered result.

```
  1398209 Oct  2 16:22 ao.ktx
   699172 Oct  2 16:20 ao_etc.ktx
  1398209 Oct  2 16:22 metallic.ktx
   699172 Oct  2 16:19 metallic_etc.ktx
  1398209 Oct  2 16:22 roughness.ktx
   699172 Oct  2 16:20 roughness_etc.ktx
```
This commit is contained in:
Philip Rideout
2018-10-02 11:32:09 -07:00
parent 2eeb92fc4c
commit cc61e4649b
9 changed files with 236 additions and 45 deletions

View File

@@ -189,6 +189,17 @@ public:
static constexpr uint32_t SRGB8_ALPHA8_ASTC_12x10 = 0x93DC;
static constexpr uint32_t SRGB8_ALPHA8_ASTC_12x12 = 0x93DD;
static constexpr uint32_t R11_EAC = 0x9270;
static constexpr uint32_t SIGNED_R11_EAC = 0x9271;
static constexpr uint32_t RG11_EAC = 0x9272;
static constexpr uint32_t SIGNED_RG11_EAC = 0x9273;
static constexpr uint32_t RGB8_ETC2 = 0x9274;
static constexpr uint32_t SRGB8_ETC2 = 0x9275;
static constexpr uint32_t RGB8_ALPHA1_ETC2 = 0x9276;
static constexpr uint32_t SRGB8_ALPHA1_ETC = 0x9277;
static constexpr uint32_t RGBA8_ETC2_EAC = 0x9278;
static constexpr uint32_t SRGB8_ALPHA8_ETC2_EAC = 0x9279;
private:
image::KtxInfo mInfo = {};
uint32_t mNumMipLevels;

View File

@@ -30,35 +30,20 @@
namespace image {
// Controls how fast compression occurs at the cost of quality in the resulting image.
enum class AstcPreset {
VERYFAST,
FAST,
MEDIUM,
THOROUGH,
EXHAUSTIVE,
};
// Informs the encoder what texels represent; this is especially crucial for normal maps.
enum class AstcSemantic {
COLORS_LDR,
COLORS_HDR,
NORMALS,
};
// The encoder configuration controls the quality and speed of compression, as well as the resulting
// format. The specified block size must be one of the 14 block sizes that can be consumed by ES 3.2
// as per https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glCompressedTexImage2D.xhtml
struct AstcConfig {
AstcPreset quality;
AstcSemantic semantic;
math::ushort2 blocksize;
bool srgb;
};
enum class CompressedFormat {
INVALID = 0,
R11_EAC = 0x9270,
SIGNED_R11_EAC = 0x9271,
RG11_EAC = 0x9272,
SIGNED_RG11_EAC = 0x9273,
RGB8_ETC2 = 0x9274,
SRGB8_ETC2 = 0x9275,
RGB8_ALPHA1_ETC2 = 0x9276,
SRGB8_ALPHA1_ETC = 0x9277,
RGBA8_ETC2_EAC = 0x9278,
SRGB8_ALPHA8_ETC2_EAC = 0x9279,
RGB_S3TC_DXT1 = 0x83F0,
RGBA_S3TC_DXT1 = 0x83F1,
RGBA_S3TC_DXT3 = 0x83F2,
@@ -105,6 +90,34 @@ struct CompressedTexture {
std::unique_ptr<uint8_t[]> data;
};
// ASTC ////////////////////////////////////////////////////////////////////////////////////////////
// Controls how fast compression occurs at the cost of quality in the resulting image.
enum class AstcPreset {
VERYFAST,
FAST,
MEDIUM,
THOROUGH,
EXHAUSTIVE,
};
// Informs the encoder what texels represent; this is especially crucial for normal maps.
enum class AstcSemantic {
COLORS_LDR,
COLORS_HDR,
NORMALS,
};
// The encoder configuration controls the quality and speed of compression, as well as the resulting
// format. The specified block size must be one of the 14 block sizes that can be consumed by ES 3.2
// as per https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glCompressedTexImage2D.xhtml
struct AstcConfig {
AstcPreset quality;
AstcSemantic semantic;
math::ushort2 blocksize;
bool srgb;
};
// Uses the CPU to compress a linear image (1 to 4 channels) into an ASTC texture. The 16-byte
// header block that ARM uses in their file format is not included.
CompressedTexture astcCompress(const LinearImage& source, AstcConfig config);
@@ -115,6 +128,37 @@ CompressedTexture astcCompress(const LinearImage& source, AstcConfig config);
// thorough_normals_6x6, veryfast_hdr_12x10
AstcConfig astcParseOptionString(const std::string& options);
// ETC /////////////////////////////////////////////////////////////////////////////////////////////
enum class EtcErrorMetric {
RGBA,
RGBX,
REC709,
NUMERIC,
NORMALXYZ,
};
// Informs the ETC encoder of the desired output. Effort sets the quality / speed tradeoff with
// a number between 0 and 100.
struct EtcConfig {
CompressedFormat format;
EtcErrorMetric metric;
int effort;
};
// Uses the CPU to compress a linear image (1 to 4 channels) into an ETC texture.
CompressedTexture etcCompress(const LinearImage& source, EtcConfig config);
// Converts a string into an ETC compression configuration where the string has the form
// FORMAT_METRIC_EFFORT where:
// - FORMAT is one of: r11, signed_r11, rg11, signed_rg11, rgb8, srgb8, rgb8_alpha,
// srgb8_alpha, rgba8, and srgb8_alpha8
// - METRIC is one of: rgba, rgbx, rec709, numeric, and normalxyz
// - EFFORT is an integer between 0 and 100
EtcConfig etcParseOptionString(const std::string& options);
// S3TC ////////////////////////////////////////////////////////////////////////////////////////////
// Informs the S3TC encoder of the desired output.
struct S3tcConfig {
CompressedFormat format;

View File

@@ -21,6 +21,7 @@
#include <cmath>
#include <astcenc.h>
#include <Etc.h>
#define STB_DXT_IMPLEMENTATION
#include <stb_dxt.h>
@@ -314,6 +315,103 @@ S3tcConfig s3tcParseOptionString(const std::string& options) {
return {};
}
CompressedTexture etcCompress(const LinearImage& original, EtcConfig config) {
LinearImage source = extendToFourChannels(original);
constexpr int threadcount = 1; // TODO: set this thread count
Etc::Image::Format etcformat;
switch (config.format) {
case CompressedFormat::R11_EAC: etcformat = Etc::Image::Format::R11; break;
case CompressedFormat::SIGNED_R11_EAC: etcformat = Etc::Image::Format::SIGNED_R11; break;
case CompressedFormat::RG11_EAC: etcformat = Etc::Image::Format::RG11; break;
case CompressedFormat::SIGNED_RG11_EAC: etcformat = Etc::Image::Format::SIGNED_RG11; break;
case CompressedFormat::RGB8_ETC2: etcformat = Etc::Image::Format::RGB8; break;
case CompressedFormat::SRGB8_ETC2: etcformat = Etc::Image::Format::SRGB8; break;
case CompressedFormat::RGB8_ALPHA1_ETC2: etcformat = Etc::Image::Format::RGB8A1; break;
case CompressedFormat::SRGB8_ALPHA1_ETC: etcformat = Etc::Image::Format::SRGB8A1; break;
case CompressedFormat::RGBA8_ETC2_EAC: etcformat = Etc::Image::Format::RGBA8; break;
case CompressedFormat::SRGB8_ALPHA8_ETC2_EAC: etcformat = Etc::Image::Format::SRGBA8; break;
default: return {};
}
Etc::ErrorMetric etcmetric;
switch (config.metric) {
case EtcErrorMetric::RGBA: etcmetric = Etc::RGBA; break;
case EtcErrorMetric::RGBX: etcmetric = Etc::RGBX; break;
case EtcErrorMetric::REC709: etcmetric = Etc::REC709; break;
case EtcErrorMetric::NUMERIC: etcmetric = Etc::NUMERIC; break;
case EtcErrorMetric::NORMALXYZ: etcmetric = Etc::NORMALXYZ; break;
default: return {};
}
unsigned char *paucEncodingBits;
unsigned int uiEncodingBitsBytes;
unsigned int uiExtendedWidth;
unsigned int uiExtendedHeight;
int iEncodingTime_ms;
// The etc2comp API doesn't tell you that you need to free paucEncodingBits, but they have a
// commented-out "delete[] m_paucEncodingBits" in their Image destructor, which is essentially
// what our unique_ptr wrapper does (CompressedTexture::data).
Etc::Encode(source.getPixelRef(0, 0),
source.getWidth(), source.getHeight(),
etcformat,
etcmetric,
config.effort,
threadcount,
1024,
&paucEncodingBits, &uiEncodingBitsBytes,
&uiExtendedWidth, &uiExtendedHeight,
&iEncodingTime_ms);
return {
.format = config.format,
.size = uiEncodingBitsBytes,
.data = decltype(CompressedTexture::data)(paucEncodingBits)
};
}
EtcConfig etcParseOptionString(const std::string& options) {
EtcConfig result {};
const size_t _2 = options.rfind('_');
const size_t _1 = options.rfind('_', _2 - 1);
std::string sformat = options.substr(0, _1);
std::string smetric = options.substr(_1 + 1, _2 - _1 - 1);
std::string seffort = options.substr(_2 + 1);
if (sformat == "r11") {
result.format = CompressedFormat::R11_EAC;
} else if (sformat == "signed_r11") {
result.format = CompressedFormat::SIGNED_R11_EAC;
} else if (sformat == "rg11") {
result.format = CompressedFormat::RG11_EAC;
} else if (sformat == "signed_rg11") {
result.format = CompressedFormat::SIGNED_RG11_EAC;
} else if (sformat == "rgb8") {
result.format = CompressedFormat::RGB8_ETC2;
} else if (sformat == "srgb8") {
result.format = CompressedFormat::SRGB8_ETC2;
} else if (sformat == "rgb8_alpha") {
result.format = CompressedFormat::RGB8_ALPHA1_ETC2;
} else if (sformat == "srgb8_alpha") {
result.format = CompressedFormat::SRGB8_ALPHA1_ETC;
} else if (sformat == "rgba8_etc2") {
result.format = CompressedFormat::RGBA8_ETC2_EAC;
} else if (sformat == "srgb8_alpha8_etc2") {
result.format = CompressedFormat::SRGB8_ALPHA8_ETC2_EAC;
}
if (smetric == "rgba") {
result.metric = EtcErrorMetric::RGBA;
} else if (smetric == "rgbx") {
result.metric = EtcErrorMetric::RGBX;
} else if (smetric == "rec709") {
result.metric = EtcErrorMetric::REC709;
} else if (smetric == "numeric") {
result.metric = EtcErrorMetric::NUMERIC;
} else if (smetric == "normalxyz") {
result.metric = EtcErrorMetric::NORMALXYZ;
}
result.effort = std::stoi(seffort);
return result;
}
static LinearImage extendToFourChannels(LinearImage original) {
LinearImage source = original;
const uint32_t width = source.getWidth();

View File

@@ -83,13 +83,18 @@ function(add_ktxfiles SOURCE TARGET EXTRA_ARGS)
DEPENDS mipgen)
endfunction()
set(ETC_R11_ARGS "--grayscale;--compression=etc_r11_numeric_40")
add_ktxfiles("assets/models/monkey/albedo.png" "monkey/albedo.ktx" "")
add_ktxfiles("assets/models/monkey/albedo.png" "monkey/albedo_astc.ktx" "--compression=astc_fast_ldr_4x4")
add_ktxfiles("assets/models/monkey/albedo.png" "monkey/albedo_s3tc.ktx" "--compression=s3tc_rgb_dxt1")
add_ktxfiles("assets/models/monkey/normal.png" "monkey/normal.ktx" "--kernel=NORMALS;--linear")
add_ktxfiles("assets/models/monkey/roughness.png" "monkey/roughness.ktx" "--grayscale")
add_ktxfiles("assets/models/monkey/roughness.png" "monkey/roughness_etc.ktx" "${ETC_R11_ARGS}")
add_ktxfiles("assets/models/monkey/metallic.png" "monkey/metallic.ktx" "--grayscale")
add_ktxfiles("assets/models/monkey/metallic.png" "monkey/metallic_etc.ktx" "${ETC_R11_ARGS}")
add_ktxfiles("assets/models/monkey/ao.png" "monkey/ao.ktx" "--grayscale")
add_ktxfiles("assets/models/monkey/ao.png" "monkey/ao_etc.ktx" "${ETC_R11_ARGS}")
# Convert OBJ files into filamesh files.
function(add_mesh SOURCE TARGET)

View File

@@ -348,6 +348,16 @@ T toFilamentEnum(uint32_t format) {
case KtxBundle::SRGB8_ALPHA8_ASTC_10x10: return T::SRGB8_ALPHA8_ASTC_10x10;
case KtxBundle::SRGB8_ALPHA8_ASTC_12x10: return T::SRGB8_ALPHA8_ASTC_12x10;
case KtxBundle::SRGB8_ALPHA8_ASTC_12x12: return T::SRGB8_ALPHA8_ASTC_12x12;
case KtxBundle::R11_EAC: return T::EAC_R11;
case KtxBundle::SIGNED_R11_EAC: return T::EAC_R11_SIGNED;
case KtxBundle::RG11_EAC: return T::EAC_RG11;
case KtxBundle::SIGNED_RG11_EAC: return T::EAC_RG11_SIGNED;
case KtxBundle::RGB8_ETC2: return T::ETC2_RGB8;
case KtxBundle::SRGB8_ETC2: return T::ETC2_SRGB8;
case KtxBundle::RGB8_ALPHA1_ETC2: return T::ETC2_RGB8_A1;
case KtxBundle::SRGB8_ALPHA1_ETC: return T::ETC2_SRGB8_A1;
case KtxBundle::RGBA8_ETC2_EAC: return T::ETC2_EAC_RGBA8;
case KtxBundle::SRGB8_ALPHA8_ETC2_EAC: return T::ETC2_EAC_SRGBA8;
}
return (T) 0xffff;
}

View File

@@ -6,7 +6,7 @@ let queued_mouse_events = [];
let use_astc = false;
let use_s3tc = false;
let use_etc1 = false;
let use_etc = false;
let canvas = document.getElementById('filament-canvas');
let ctx = GL.createContext(canvas, {
@@ -19,12 +19,14 @@ let ctx = GL.createContext(canvas, {
GL.makeContextCurrent(ctx);
for (let ext of Module.ctx.getSupportedExtensions()) {
if (ext == "WEBGL_compressed_texture_s3tc") {
Module.ctx.getExtension('WEBGL_compressed_texture_s3tc');
use_s3tc = true;
} else if (ext == "WEBGL_compressed_texture_astc") {
Module.ctx.getExtension('WEBGL_compressed_texture_astc');
use_astc = true;
} else if (ext == "WEBGL_compressed_texture_etc1") {
use_etc1 = true;
} else if (ext == "WEBGL_compressed_texture_etc") {
Module.ctx.getExtension('WEBGL_compressed_texture_etc');
use_etc = true;
}
}

View File

@@ -66,14 +66,6 @@ static Texture* setTextureParameter(Engine& engine, filaweb::Asset& asset, strin
asset->texture.reset();
};
Texture::Format format;
switch (info.glTypeSize) {
case 1: format = Texture::Format::R; break;
case 2: format = Texture::Format::RG; break;
case 3: format = Texture::Format::RGB; break;
case 4: format = Texture::Format::RGBA; break;
}
uint8_t* data;
uint32_t nbytes;
asset.texture->getBlob({}, &data, &nbytes);
@@ -97,6 +89,14 @@ static Texture* setTextureParameter(Engine& engine, filaweb::Asset& asset, strin
return texture;
}
Texture::Format format;
switch (info.glTypeSize) {
case 1: format = Texture::Format::R; break;
case 2: format = Texture::Format::RG; break;
case 3: format = Texture::Format::RGB; break;
case 4: format = Texture::Format::RGBA; break;
}
Texture::PixelBufferDescriptor pb(data, nbytes, format, Texture::Type::UBYTE, destructor,
&asset);

View File

@@ -22,14 +22,19 @@
<script src="suzanne.js"></script>
<script src="filaweb.js"></script>
<script>
function load_etcfile(basename) {
return use_etc ? load_rawfile(basename + "_etc.ktx") : load_rawfile(basename + ".ktx");
}
load({
'albedo': (use_s3tc ? load_rawfile('monkey/albedo_s3tc.ktx') :
(use_astc ? load_rawfile('monkey/albedo_astc.ktx') :
load_rawfile('monkey/albedo.ktx'))),
'metallic': load_rawfile('monkey/metallic.ktx'),
'roughness': load_rawfile('monkey/roughness.ktx'),
'metallic': load_etcfile('monkey/metallic'),
'roughness': load_etcfile('monkey/roughness'),
'normal': load_rawfile('monkey/normal.ktx'),
'ao': load_rawfile('monkey/ao.ktx'),
'ao': load_etcfile('monkey/ao'),
'mesh': load_rawfile('monkey/mesh.filamesh'),
'syferfontein_18d_clear_2k': load_cubemap('syferfontein_18d_clear_2k', '.ktx')
});

View File

@@ -81,6 +81,11 @@ Options:
KTX:
astc_[fast|thorough]_[ldr|hdr]_WxH, where WxH is a valid block size
st3c_rgb_dxt1, st3c_rgba_dxt5
etc_FORMAT_METRIC_EFFORT
FORMAT is r11, signed_r11, rg11, signed_rg11, rgb8, srgb8, rgb8_alpha
srgb8_alpha, rgba8, or srgb8_alpha8
METRIC is rgba, rgbx, rec709, numeric, or normalxyz
EFFORT is an integer between 0 and 100
PNG: Ignored
Radiance: Ignored
Photoshop: 16 (default), 32
@@ -90,6 +95,7 @@ Options:
Examples:
MIPGEN -g --kernel=hermite grassland.png mip_%03d.png
MIPGEN -f ktx --compression=astc_fast_ldr_4x4 grassland.png mips.ktx
MIPGEN -f ktx --compression=etc_rgb_rgba_40 grassland.png mips.ktx
)TXT";
static const char* HTML_PREFIX = R"HTML(<!DOCTYPE html>
@@ -278,15 +284,17 @@ int main(int argc, char* argv[]) {
}
AstcConfig astcConfig {};
S3tcConfig s3tcConfig {};
EtcConfig etcConfig {};
if (!g_compression.empty()) {
if (g_compression.substr(0, 5) == "astc_") {
string suffix = g_compression.substr(5);
astcConfig = astcParseOptionString(suffix);
astcConfig = astcParseOptionString(g_compression.substr(5));
} else if (g_compression.substr(0, 5) == "s3tc_") {
string suffix = g_compression.substr(5);
s3tcConfig = s3tcParseOptionString(suffix);
s3tcConfig = s3tcParseOptionString(g_compression.substr(5));
} else if (g_compression.substr(0, 4) == "etc_") {
etcConfig = etcParseOptionString(g_compression.substr(4));
}
if (astcConfig.blocksize[0] == 0 && s3tcConfig.format == CompressedFormat::INVALID) {
if (astcConfig.blocksize[0] == 0 && s3tcConfig.format == CompressedFormat::INVALID
&& etcConfig.format == CompressedFormat::INVALID) {
cerr << "Unrecognized compression: " << g_compression << endl;
return 1;
}
@@ -322,6 +330,14 @@ int main(int argc, char* argv[]) {
info.glInternalFormat = (uint32_t) tex.format;
return;
}
if (etcConfig.format != CompressedFormat::INVALID) {
printf("Starting ETC compression for %s (%dx%d)\n", inputPath.getName().c_str(),
image.getWidth(), image.getHeight());
CompressedTexture tex = etcCompress(image, etcConfig);
container.setBlob({mip++}, tex.data.get(), tex.size);
info.glInternalFormat = (uint32_t) tex.format;
return;
}
if (g_grayscale && g_linearized) {
data = fromLinearToGrayscale<uint8_t>(image);
} else if (g_grayscale) {