Skip to content
84 changes: 65 additions & 19 deletions h3d/impl/GlDriver.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import h3d.impl.Driver;
import h3d.mat.Pass;
import h3d.mat.Stencil;
import h3d.mat.Data;
import hxd.CompressedTextureFormat;
import hxd.res.Ktx2.EngineFormat;
import hxd.res.Ktx2.InternalFormat;

#if (js||hlsdl||usegl)

Expand Down Expand Up @@ -87,6 +90,15 @@ class GlDriver extends Driver {
static var UID = 0;
public var gl : GL;
public static var ALLOW_WEBGL2 = true;

public var textureSupport:{
astc:Bool,
astcHDR:Bool,
etc1:Bool,
etc2:Bool,
dxt:Bool,
bptc:Bool,
};
#end

#if (hlsdl||usegl)
Expand Down Expand Up @@ -992,8 +1004,9 @@ class GlDriver extends Driver {
case GL.RGB10_A2: GL.RGBA;
case GL.RED, GL.R8, GL.R16F, GL.R32F, 0x822A: GL.RED;
case GL.RG, GL.RG8, GL.RG16F, GL.RG32F, 0x822C: GL.RG;
case GL.RGB16F, GL.RGB32F, 0x8054, 0x8E8F: GL.RGB;
case 0x83F1, 0x83F2, 0x83F3, 0x805B, 0x8E8C: GL.RGBA;
case GL.RGB16F, GL.RGB32F, 0x8054, hxd.CompressedTextureFormat.BPTC_FORMAT.RGB_BPTC_UNSIGNED, hxd.CompressedTextureFormat.ETC_FORMAT.RGB_ETC1: GL.RGB;
case 0x805B, hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT1,hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT3,
hxd.CompressedTextureFormat.DXT_FORMAT.RGBA_DXT5,hxd.CompressedTextureFormat.ASTC_FORMAT.RGBA_4x4, hxd.CompressedTextureFormat.BPTC_FORMAT.RGBA_BPTC : GL.RGBA;
default: throw "Invalid format " + t.internalFmt;
}
}
Expand Down Expand Up @@ -1022,7 +1035,7 @@ class GlDriver extends Driver {
discardError();
var tt = gl.createTexture();
var bind = getBindType(t);
var tt : Texture = { t : tt, width : t.width, height : t.height, internalFmt : GL.RGBA, pixelFmt : GL.UNSIGNED_BYTE, bits : -1, bind : bind, bias : 0, startMip : t.startingMip #if multidriver, driver : this #end };
var tt : Texture = { t : tt, width : t.width, height : t.height, internalFmt : GL.RGBA, pixelFmt : GL.UNSIGNED_BYTE, bits : -1, bind : bind, bias : 0, startMip : t.startingMip #if multidriver, driver : this #end };
switch( t.format ) {
case RGBA:
// default
Expand Down Expand Up @@ -1083,16 +1096,27 @@ class GlDriver extends Driver {
tt.internalFmt = GL.R11F_G11F_B10F;
tt.pixelFmt = GL.UNSIGNED_INT_10F_11F_11F_REV;
case S3TC(n) if( n <= maxCompressedTexturesSupport ):
if( t.width&3 != 0 || t.height&3 != 0 )
throw "Compressed texture "+t+" has size "+t.width+"x"+t.height+" - must be a multiple of 4";
checkMult4(t);
switch( n ) {
case 1: tt.internalFmt = 0x83F1; // COMPRESSED_RGBA_S3TC_DXT1_EXT
case 2: tt.internalFmt = 0x83F2; // COMPRESSED_RGBA_S3TC_DXT3_EXT
case 3: tt.internalFmt = 0x83F3; // COMPRESSED_RGBA_S3TC_DXT5_EXT
case 6: tt.internalFmt = 0x8E8F; // COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT
case 7: tt.internalFmt = 0x8E8C; // COMPRESSED_RGBA_BPTC_UNORM
case 1: tt.internalFmt = DXT_FORMAT.RGBA_DXT1;
case 2: tt.internalFmt = DXT_FORMAT.RGBA_DXT3;
case 3: tt.internalFmt = DXT_FORMAT.RGBA_DXT5;
case 6: tt.internalFmt = BPTC_FORMAT.RGB_BPTC_UNSIGNED;
case 7: tt.internalFmt = BPTC_FORMAT.RGBA_BPTC;
default: throw "Unsupported texture format "+t.format;
}
case ASTC(n):
checkMult4(t);
switch (n) {
case 10: tt.internalFmt = ASTC_FORMAT.RGBA_4x4;
default: throw "Unsupported texture format " + t.format;
}
case ETC(n):
checkMult4(t);
switch (n) {
case 0: tt.internalFmt = ETC_FORMAT.RGB_ETC1;
case 1: tt.internalFmt = ETC_FORMAT.RGBA_ETC2;
}
default:
throw "Unsupported texture format "+t.format;
}
Expand Down Expand Up @@ -1125,7 +1149,7 @@ class GlDriver extends Driver {

#if js
// Modern texture allocation that supports both compressed and uncompressed texture in WebGL
// texStorate2D/3D is only defined in OpenGL 4.2 but is defined in openGL ES 3 which the js target targets
// texStorage2D/3D is only defined in OpenGL 4.2 but is defined in openGL ES 3 which the js target targets

// Patch RGBA to be RGBA8 because texStorage expect a "Sized Internal Format"
var sizedFormat = tt.internalFmt == GL.RGBA ? GL.RGBA8 : tt.internalFmt;
Expand Down Expand Up @@ -1169,6 +1193,11 @@ class GlDriver extends Driver {
return tt;
}

inline function checkMult4( t : h3d.mat.Texture ) {
if( t.width & 3 != 0 || t.height & 3 != 0 )
throw "Compressed texture " + t + " has size " + t.width + "x" + t.height + " - must be a multiple of 4";
}

function restoreBind() {
var t = boundTextures[lastActiveIndex];
if( t == null )
Expand Down Expand Up @@ -1218,7 +1247,7 @@ class GlDriver extends Driver {
}

var defaultDepth : h3d.mat.Texture;

override function getDefaultDepthBuffer() : h3d.mat.Texture {
// Unfortunately there is no way to bind the depth buffer of the default frame buffer to a frame buffer object.
if( defaultDepth != null )
Expand Down Expand Up @@ -1409,7 +1438,7 @@ class GlDriver extends Driver {
case RGB10A2, RG11B10UF: new Uint32Array(@:privateAccess pixels.bytes.b.buffer, pixels.offset, dataLen>>2);
default: new Uint8Array(@:privateAccess pixels.bytes.b.buffer, pixels.offset, dataLen);
}
if( t.format.match(S3TC(_)) ) {
if( t.format.match(S3TC(_) | ASTC(_) | ETC(_)) ) {
if( t.flags.has(IsArray) || t.flags.has(Is3D) )
gl.compressedTexSubImage3D(face, mipLevel, 0, 0, side, pixels.width, pixels.height, 1, t.t.internalFmt, buffer);
else
Expand Down Expand Up @@ -1733,7 +1762,7 @@ class GlDriver extends Driver {
throw "Invalid texture context";
#end
gl.bindFramebuffer(GL.FRAMEBUFFER, commonFB);

if( tex.flags.has(IsArray) )
gl.framebufferTextureLayer(GL.FRAMEBUFFER, GL.COLOR_ATTACHMENT0, tex.t.t, mipLevel, layer);
else
Expand Down Expand Up @@ -1893,20 +1922,37 @@ class GlDriver extends Driver {
}

#if js
public function checkTextureSupport() {
final checkExtension = ext -> {
gl.getExtension(ext) != null;
}
return {
astc: checkExtension('WEBGL_compressed_texture_astc'),
astcHDR: checkExtension('WEBGL_compressed_texture_astc')
&& gl.getExtension('WEBGL_compressed_texture_astc').getSupportedProfiles().includes('hdr'),
etc1: false, // Not supported on WebGL2 (https://registry.khronos.org/OpenGL-Refpages/es3/html/glCompressedTexSubImage2D.xhtml); checkExtension('WEBGL_compressed_texture_etc1'),
etc2: checkExtension('WEBGL_compressed_texture_etc'),
dxt: checkExtension('WEBGL_compressed_texture_s3tc'),
bptc: checkExtension('EXT_texture_compression_bptc'),
}
}

var features : Map<Feature,Bool> = new Map();
var has16Bits : Bool;
function makeFeatures() {
for( f in Type.allEnums(Feature) )
features.set(f,checkFeature(f));
if( gl.getExtension("WEBGL_compressed_texture_s3tc") != null ) {
maxCompressedTexturesSupport = 3;
if( gl.getExtension("EXT_texture_compression_bptc") != null )
maxCompressedTexturesSupport = 7;
textureSupport = checkTextureSupport();
maxCompressedTexturesSupport = if( textureSupport.dxt || textureSupport.etc1 || textureSupport.etc2 || textureSupport.astc ) {
gl.getExtension("EXT_texture_compression_bptc") != null ? 7 : 3;
} else {
3;
}
if( glES < 3 )
gl.getExtension("WEBGL_depth_texture");
has16Bits = gl.getExtension("EXT_texture_norm16") != null; // 16 bit textures
}

function checkFeature( f : Feature ) {
return switch( f ) {

Expand Down Expand Up @@ -2126,4 +2172,4 @@ class GlDriver extends Driver {

}

#end
#end
22 changes: 22 additions & 0 deletions hxd/CompressedTextureFormat.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package hxd;

enum abstract ASTC_FORMAT(Int) from Int to Int {
final RGBA_4x4 = 0x93B0;
}

enum abstract DXT_FORMAT(Int) from Int to Int {
final RGB_DXT1 = 0x83F0;
final RGBA_DXT1 = 0x83F1;
final RGBA_DXT3 = 0x83F2;
final RGBA_DXT5 = 0x83F3;
}

enum abstract ETC_FORMAT(Int) from Int to Int {
final RGB_ETC1 = 0x8D64;
final RGBA_ETC2 = 0x9278;
}

enum abstract BPTC_FORMAT(Int) from Int to Int {
final RGB_BPTC_UNSIGNED = 0x8E8F;
final RGBA_BPTC = 0x8E8C;
}
23 changes: 21 additions & 2 deletions hxd/PixelFormat.hx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,28 @@ enum PixelFormat {
RG16U;
RGB16U;
RGBA16U;
S3TC( v : Int );
/**
Adaptive Scalable Texture Compression
- `10` 4x4 block size
*/
ASTC( v:Int );
/**
Ericsson Texture Compression
- `0` ETC1 (opaque RGB)
- `1` ETC2 (with alpha)
*/
ETC( v:Int );
/**
S3 Texture Compression (DXT/BC)
- `1`: BC1/DXT1 (opaque or 1-bit alpha)
- `2`: BC2/DXT3 (explicit alpha)
- `3`: BC3/DXT5 (interpolated alpha)
- `6`: BC6H (HDR, unsigned float)
- `7`: BC7 (high quality, alpha)
*/
S3TC( v:Int );
Depth16;
Depth24;
Depth24Stencil8;
Depth32;
}
}
62 changes: 51 additions & 11 deletions hxd/Pixels.hx
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,11 @@ class Pixels {
for( i in 0 ... this.width * this.height )
nbytes.setFloat(i << 2, this.bytes.getFloat(i << 4));
this.bytes = nbytes;

case [S3TC(a),S3TC(b)] if( a == b ):
// nothing

#if js
case [S3TC(a),S3TC(b)] if( a == b ): // nothing
case [ASTC(a),ASTC(b)] if( a == b ): // Ktx2 will handle conversion
case [ETC(a),ETC(b)] if( a == b ): // Ktx2 will handle conversion
#end
#if (hl && hl_ver >= "1.10")
case [S3TC(ver),_]:
if( (width|height)&3 != 0 ) throw "Texture size should be 4x4 multiple";
Expand All @@ -405,7 +406,6 @@ class Pixels {
convert(target);
return;
#end

default:
throw "Cannot convert from " + format + " to " + target;
}
Expand Down Expand Up @@ -536,8 +536,33 @@ class Pixels {

public static function calcDataSize( width : Int, height : Int, format : PixelFormat ) {
return switch( format ) {
case S3TC(_):
(((height + 3) >> 2) << 2) * calcStride(width, format);
case S3TC(n):
var w = (width + 3) >> 2;
var h = (height + 3) >> 2;
var blocks = w * h; // Total number of blocks
if( n == 3 ) { // DXT5
blocks * 16; // 16 bytes per block
} else if( n == 1 || n == 4 ) {
blocks * 8; // DXT1 or BC4, 8 bytes per block
} else {
blocks * 16; // DXT3 or BC5, 16 bytes per block, but handling like DXT5 for simplicity
}
case ASTC(n):
var w = (width + 3) >> 2;
var h = (height + 3) >> 2;
w * h * 16;
case ETC(n):
if( n == 0 ) { // RGB_ETC1_Format or RGB_ETC2_Format
var w = (width + 3) >> 2;
var h = (height + 3) >> 2;
w * h * 8;
} else if( n == 1 || n == 2 ) { // RGBA_ETC2_EAC_Format
var w = (width + 3) >> 2;
var h = (height + 3) >> 2;
w * h * 16;
} else {
throw "Unsupported ETC format";
}
default:
height * calcStride(width, format);
}
Expand All @@ -559,11 +584,26 @@ class Pixels {
case RGB32F: 12;
case RGB10A2: 4;
case RG11B10UF: 4;
case ASTC(n):
var blocks = ((width + 3) >> 2) * 16;
blocks << 4;
case ETC(n):
if( n == 0 ) { // ETC1 and ETC2 RGB
((width + 3) >> 2) << 3;
} else if( n == 1 ) { // ETC2 EAC RGBA
((width + 3) >> 2) << 4;
} else {
throw "Unsupported ETC format";
}
case S3TC(n):
var blocks = (width + 3) >> 2;
if( n == 1 || n == 4 )
return blocks << 1;
return blocks << 2;
if( n == 3 ) { // DXT5
blocks << 4; // 16 bytes per block
} else if( n == 1 || n == 4 ) {
blocks << 1; // DXT1 or BC4, 8 bytes per block
} else {
blocks << 2; // DXT3 or BC5, 16 bytes per block, but handling like DXT5 for simplicity
}
case Depth16: 2;
case Depth24: 3;
case Depth24Stencil8, Depth32: 4;
Expand Down Expand Up @@ -605,7 +645,7 @@ class Pixels {
channel.toInt() * 4;
case RGB10A2, RG11B10UF:
throw "Bit packed format";
case S3TC(_), Depth16, Depth24, Depth24Stencil8, Depth32:
case S3TC(_), ASTC(_), ETC(_), Depth16, Depth24, Depth24Stencil8, Depth32:
throw "Not supported";
}
}
Expand Down
5 changes: 3 additions & 2 deletions hxd/fmt/pak/Build.hx
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,12 @@ class Build {
f.close();
}

public static function make( dir = "res", out = "res", ?pakDiff ) {
public static function make( dir = "res", out = "res", ?pakDiff, ?config ) {
var b = new Build();
b.resPath = dir;
b.outPrefix = out;
b.outPrefix = config != null ? '$out.$config' : out;
b.pakDiff = pakDiff;
b.configuration = config;
b.makePak();
}

Expand Down
42 changes: 42 additions & 0 deletions hxd/fs/Convert.hx
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,46 @@ class ConvertSVGToMSDF extends Convert {

static var _ = Convert.register(new ConvertSVGToMSDF("svg", "png"));
}

class ConvertPngToKtx2 extends hxd.fs.Convert {
override function convert() {
final format = params.dataFormat ?? 'R8G8B8A8_SRGB';
final oetf = params.assignOetf ?? 'srgb';
final primaries = params.assignPrimaries ?? 'bt709';
final cmd = if ( params.format == 'etc1s' ) {
final clevel = params.clevel ?? 3;
final qlevel = params.qlevel ?? 128;
'ktx create --encode basis-lz --format ${format} --assign-oetf ${oetf} --clevel ${clevel} --qlevel ${qlevel} --warn-on-color-conversions --compare-psnr ${srcPath} ${dstPath}';

} else {
final quality = params.quality ?? 2;
'ktx create --encode uastc --zstd 18 --format ${format} --assign-oetf ${oetf} --uastc-quality ${quality} --warn-on-color-conversions --compare-psnr ${srcPath} ${dstPath}';
}
final scProcess = new sys.io.Process(cmd);
final errorMsg = 'Error compressing $srcPath to ktx2: ${scProcess.stderr.readAll().toString()}';
if ( scProcess.exitCode() != 0 ) {
throw errorMsg;
}
final regexp = ~/PSNR Max: (.+)/;
final result = scProcess.stdout.readAll().toString();
final snr = regexp.match(result) ? regexp.matched(1) : null;
#if log_ktx2_snr Sys.println(' Converted ${haxe.io.Path.withoutDirectory(srcPath)} with PSNR Max: ${snr}'); #end
if ( params.snrThreshold != null && params.format == 'etc1s' ) {
if ( Std.parseFloat(snr) < params.snrThreshold ) {
params.format = 'uastc';
// If signal to noise ratio is too low, discard ETC1S and use UASTC encoding instead
sys.FileSystem.deleteFile(dstPath);
trace('⚠️⚠️⚠️ Low signal to noise ratio when encoding $srcPath as ETC1S. Will fall back to using UASTC with default settings. Update props.json to use "uastc" as format, or ajust or remove "snrThreshold" to make it pass verification. ⚠️⚠️⚠️');
final process = new sys.io.Process('ktx create --encode uastc --zstd 18 --format ${format} --assign-oetf ${oetf} --uastc-quality 2 --warn-on-color-conversions ${srcPath} ${dstPath}');
if ( process.exitCode() != 0 ) {
throw errorMsg;
}
}
}

}

// register the convert so it can be found
static var _ = hxd.fs.Convert.register(new ConvertPngToKtx2('png', 'ktx2'));
}
#end
Loading