mirror of
https://github.com/galacean/engine.git
synced 2026-06-03 09:14:01 +08:00
feat: support ktx2 hdr(BC6H) (#2784)
This commit is contained in:
BIN
e2e/.dev/public/autumn_field_puresky_1k.ktx2
Normal file
BIN
e2e/.dev/public/autumn_field_puresky_1k.ktx2
Normal file
Binary file not shown.
55
e2e/case/texture-hdr-ktx2.ts
Normal file
55
e2e/case/texture-hdr-ktx2.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @title HDR KTX2
|
||||
* @category Texture
|
||||
*/
|
||||
import {
|
||||
BloomEffect,
|
||||
Camera,
|
||||
DirectLight,
|
||||
Logger,
|
||||
MeshRenderer,
|
||||
PostProcess,
|
||||
PrimitiveMesh,
|
||||
Texture2D,
|
||||
TonemappingEffect,
|
||||
UnlitMaterial,
|
||||
Vector3,
|
||||
WebGLEngine
|
||||
} from "@galacean/engine";
|
||||
import { OrbitControl } from "@galacean/engine-toolkit";
|
||||
import { initScreenshot, updateForE2E } from "./.mockForE2E";
|
||||
|
||||
Logger.enable();
|
||||
|
||||
WebGLEngine.create({ canvas: "canvas", ktx2Loader: { workerCount: 0 } }).then((engine) => {
|
||||
engine.canvas.resizeByClientSize(2);
|
||||
const scene = engine.sceneManager.activeScene;
|
||||
const rootEntity = scene.createRootEntity();
|
||||
|
||||
// camera
|
||||
const cameraEntity = rootEntity.createChild("camera_node");
|
||||
cameraEntity.transform.position = new Vector3(0, 1, 5);
|
||||
const camera = cameraEntity.addComponent(Camera);
|
||||
cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 0, 0);
|
||||
|
||||
const lightNode = rootEntity.createChild("light_node");
|
||||
lightNode.addComponent(DirectLight).intensity = 0.6;
|
||||
lightNode.transform.lookAt(new Vector3(0, 0, 1));
|
||||
lightNode.transform.rotate(new Vector3(0, 90, 0));
|
||||
|
||||
const planeEntity = rootEntity.createChild("plane");
|
||||
const meshRenderer = planeEntity.addComponent(MeshRenderer);
|
||||
meshRenderer.mesh = PrimitiveMesh.createCuboid(engine);
|
||||
const mtl = new UnlitMaterial(engine);
|
||||
meshRenderer.setMaterial(mtl);
|
||||
const postProcess = rootEntity.addComponent(PostProcess);
|
||||
postProcess.addEffect(BloomEffect);
|
||||
postProcess.addEffect(TonemappingEffect);
|
||||
|
||||
engine.resourceManager.load<Texture2D>("/autumn_field_puresky_1k.ktx2").then((tex) => {
|
||||
mtl.baseTexture = tex;
|
||||
updateForE2E(engine);
|
||||
|
||||
initScreenshot(engine, camera);
|
||||
});
|
||||
});
|
||||
@@ -151,6 +151,11 @@ export const E2E_CONFIG = {
|
||||
category: "Texture",
|
||||
caseFileName: "texture-R8G8",
|
||||
threshold: 0.1
|
||||
},
|
||||
KTX2HDR: {
|
||||
category: "Texture",
|
||||
caseFileName: "texture-hdr-ktx2",
|
||||
threshold: 0.1
|
||||
}
|
||||
},
|
||||
Shadow: {
|
||||
|
||||
@@ -24,13 +24,14 @@ export enum TextureFormat {
|
||||
R8 = 36,
|
||||
/** RG float format, 8 bits per channel. */
|
||||
R8G8 = 37,
|
||||
|
||||
/** RGB compressed format, 4 bits per pixel. */
|
||||
BC1 = 10,
|
||||
/** RGBA compressed format, 8 bits per pixel. */
|
||||
BC3 = 11,
|
||||
/** RGB(A) compressed format, 128 bits per 4x4 pixel block. */
|
||||
BC7 = 12,
|
||||
/** RGB HDR compressed format, 8 bits per pixel.. */
|
||||
BC6H = 100,
|
||||
/** RGB compressed format, 4 bits per pixel. */
|
||||
ETC1_RGB = 13,
|
||||
/** RGB compressed format, 4 bits per pixel. */
|
||||
|
||||
@@ -30,7 +30,7 @@ export * from "./SceneLoader";
|
||||
export type { Texture2DParams } from "./Texture2DLoader";
|
||||
export { parseSingleKTX } from "./compressed-texture";
|
||||
export * from "./gltf";
|
||||
export { KTX2Loader, KTX2Transcoder } from "./ktx2/KTX2Loader";
|
||||
export { KTX2Loader } from "./ktx2/KTX2Loader";
|
||||
export { KTX2TargetFormat } from "./ktx2/KTX2TargetFormat";
|
||||
export * from "./resource-deserialize";
|
||||
export * from "./prefab/PrefabResource";
|
||||
|
||||
@@ -6,9 +6,10 @@ export enum DFDTransferFunction {
|
||||
sRGB = 2
|
||||
}
|
||||
|
||||
enum ColorModel {
|
||||
export enum ColorModel {
|
||||
ETC1S = 163,
|
||||
UASTC = 166
|
||||
UASTC_LDR_4X4 = 166,
|
||||
UASTC_HDR_4X4 = 167
|
||||
}
|
||||
|
||||
export enum SupercompressionScheme {
|
||||
@@ -52,8 +53,8 @@ export class KTX2Container {
|
||||
return this.dataFormatDescriptor.transferFunction === DFDTransferFunction.sRGB;
|
||||
}
|
||||
|
||||
get isUASTC() {
|
||||
return this.dataFormatDescriptor.colorModel === ColorModel.UASTC;
|
||||
get colorModel(): ColorModel {
|
||||
return this.dataFormatDescriptor.colorModel;
|
||||
}
|
||||
|
||||
private parse(data: Uint8Array) {
|
||||
|
||||
@@ -17,32 +17,30 @@ import {
|
||||
resourceLoader
|
||||
} from "@galacean/engine-core";
|
||||
import { MathUtil } from "@galacean/engine-math";
|
||||
import { DFDTransferFunction, KTX2Container } from "./KTX2Container";
|
||||
import { DFDTransferFunction, KTX2Container, ColorModel } from "./KTX2Container";
|
||||
import { KTX2TargetFormat } from "./KTX2TargetFormat";
|
||||
import { TranscodeResult } from "./transcoder/AbstractTranscoder";
|
||||
import { BinomialLLCTranscoder } from "./transcoder/BinomialLLCTranscoder";
|
||||
import { KhronosTranscoder } from "./transcoder/KhronosTranscoder";
|
||||
|
||||
@resourceLoader(AssetType.KTX2, ["ktx2"])
|
||||
export class KTX2Loader extends Loader<Texture2D | TextureCube> {
|
||||
private static _isBinomialInit: boolean = false;
|
||||
private static _binomialLLCTranscoder: BinomialLLCTranscoder;
|
||||
private static _khronosTranscoder: KhronosTranscoder;
|
||||
private static _priorityFormats = {
|
||||
etc1s: [
|
||||
[ColorModel.ETC1S]: [
|
||||
KTX2TargetFormat.ETC,
|
||||
KTX2TargetFormat.BC7,
|
||||
KTX2TargetFormat.ASTC,
|
||||
KTX2TargetFormat.BC1_BC3,
|
||||
KTX2TargetFormat.PVRTC
|
||||
],
|
||||
uastc: [
|
||||
[ColorModel.UASTC_LDR_4X4]: [
|
||||
KTX2TargetFormat.ASTC,
|
||||
KTX2TargetFormat.BC7,
|
||||
KTX2TargetFormat.ETC,
|
||||
KTX2TargetFormat.BC1_BC3,
|
||||
KTX2TargetFormat.PVRTC
|
||||
]
|
||||
],
|
||||
[ColorModel.UASTC_HDR_4X4]: [KTX2TargetFormat.BC6H, KTX2TargetFormat.R16G16B16A16]
|
||||
};
|
||||
private static _capabilityMap = {
|
||||
[KTX2TargetFormat.ASTC]: {
|
||||
@@ -61,6 +59,14 @@ export class KTX2Loader extends Loader<Texture2D | TextureCube> {
|
||||
[DFDTransferFunction.linear]: [GLCapabilityType.s3tc],
|
||||
[DFDTransferFunction.sRGB]: [GLCapabilityType.s3tc_srgb]
|
||||
},
|
||||
[KTX2TargetFormat.BC6H]: {
|
||||
[DFDTransferFunction.linear]: [GLCapabilityType.bptc],
|
||||
[DFDTransferFunction.sRGB]: [GLCapabilityType.bptc]
|
||||
},
|
||||
[KTX2TargetFormat.R16G16B16A16]: {
|
||||
[DFDTransferFunction.linear]: [GLCapabilityType.textureHalfFloatLinear],
|
||||
[DFDTransferFunction.sRGB]: [GLCapabilityType.textureHalfFloat]
|
||||
},
|
||||
[KTX2TargetFormat.PVRTC]: { [DFDTransferFunction.linear]: [GLCapabilityType.pvrtc, GLCapabilityType.pvrtc_webkit] }
|
||||
};
|
||||
|
||||
@@ -70,35 +76,27 @@ export class KTX2Loader extends Loader<Texture2D | TextureCube> {
|
||||
*/
|
||||
static release(): void {
|
||||
if (this._binomialLLCTranscoder) this._binomialLLCTranscoder.destroy();
|
||||
if (this._khronosTranscoder) this._khronosTranscoder.destroy();
|
||||
this._binomialLLCTranscoder = null;
|
||||
this._khronosTranscoder = null;
|
||||
this._isBinomialInit = false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
static _parseBuffer(buffer: Uint8Array, engine: Engine, params?: KTX2Params) {
|
||||
const ktx2Container = new KTX2Container(buffer);
|
||||
const formatPriorities =
|
||||
params?.priorityFormats ?? KTX2Loader._priorityFormats[ktx2Container.isUASTC ? "uastc" : "etc1s"];
|
||||
const formatPriorities = params?.priorityFormats ?? KTX2Loader._priorityFormats[ktx2Container.colorModel];
|
||||
const targetFormat = KTX2Loader._decideTargetFormat(engine, ktx2Container, formatPriorities);
|
||||
let transcodeResultPromise: Promise<TranscodeResult>;
|
||||
if (KTX2Loader._isBinomialInit || !KhronosTranscoder.transcoderMap[targetFormat] || !ktx2Container.isUASTC) {
|
||||
const binomialLLCWorker = KTX2Loader._getBinomialLLCTranscoder();
|
||||
transcodeResultPromise = binomialLLCWorker.init().then(() => binomialLLCWorker.transcode(buffer, targetFormat));
|
||||
} else {
|
||||
const khronosWorker = KTX2Loader._getKhronosTranscoder();
|
||||
transcodeResultPromise = khronosWorker.init().then(() => khronosWorker.transcode(ktx2Container));
|
||||
}
|
||||
return transcodeResultPromise.then((result) => {
|
||||
return {
|
||||
ktx2Container,
|
||||
engine,
|
||||
result,
|
||||
targetFormat,
|
||||
params: ktx2Container.keyValue["GalaceanTextureParams"] as Uint8Array
|
||||
};
|
||||
});
|
||||
const binomialLLCWorker = KTX2Loader._getBinomialLLCTranscoder();
|
||||
return binomialLLCWorker
|
||||
.init()
|
||||
.then(() => binomialLLCWorker.transcode(buffer, targetFormat))
|
||||
.then((result) => {
|
||||
return {
|
||||
ktx2Container,
|
||||
engine,
|
||||
result,
|
||||
targetFormat,
|
||||
params: ktx2Container.keyValue["GalaceanTextureParams"] as Uint8Array
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -193,14 +191,9 @@ export class KTX2Loader extends Loader<Texture2D | TextureCube> {
|
||||
}
|
||||
|
||||
private static _getBinomialLLCTranscoder(workerCount: number = 4) {
|
||||
KTX2Loader._isBinomialInit = true;
|
||||
return (this._binomialLLCTranscoder ??= new BinomialLLCTranscoder(workerCount));
|
||||
}
|
||||
|
||||
private static _getKhronosTranscoder(workerCount: number = 4) {
|
||||
return (this._khronosTranscoder ??= new KhronosTranscoder(workerCount, KTX2TargetFormat.ASTC));
|
||||
}
|
||||
|
||||
private static _getEngineTextureFormat(
|
||||
basisFormat: KTX2TargetFormat,
|
||||
transcodeResult: TranscodeResult
|
||||
@@ -219,6 +212,10 @@ export class KTX2Loader extends Loader<Texture2D | TextureCube> {
|
||||
return hasAlpha ? TextureFormat.PVRTC_RGBA4 : TextureFormat.PVRTC_RGB4;
|
||||
case KTX2TargetFormat.R8G8B8A8:
|
||||
return TextureFormat.R8G8B8A8;
|
||||
case KTX2TargetFormat.BC6H:
|
||||
return TextureFormat.BC6H;
|
||||
case KTX2TargetFormat.R16G16B16A16:
|
||||
return TextureFormat.R16G16B16A16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,11 +227,7 @@ export class KTX2Loader extends Loader<Texture2D | TextureCube> {
|
||||
KTX2Loader._priorityFormats["uastc"] = options.priorityFormats;
|
||||
}
|
||||
|
||||
if (options.transcoder === KTX2Transcoder.Khronos) {
|
||||
return KTX2Loader._getKhronosTranscoder(options.workerCount).init();
|
||||
} else {
|
||||
return KTX2Loader._getBinomialLLCTranscoder(options.workerCount).init();
|
||||
}
|
||||
return KTX2Loader._getBinomialLLCTranscoder(options.workerCount).init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,14 +300,6 @@ export interface KTX2Params {
|
||||
priorityFormats: KTX2TargetFormat[];
|
||||
}
|
||||
|
||||
/** Used for initialize KTX2 transcoder. */
|
||||
export enum KTX2Transcoder {
|
||||
/** BinomialLLC transcoder. */
|
||||
BinomialLLC,
|
||||
/** Khronos transcoder. */
|
||||
Khronos
|
||||
}
|
||||
|
||||
declare module "@galacean/engine-core" {
|
||||
interface EngineConfiguration {
|
||||
/** KTX2 loader options. If set this option and workCount is great than 0, workers will be created. */
|
||||
@@ -324,8 +309,6 @@ declare module "@galacean/engine-core" {
|
||||
/** Global transcoding format queue which will be used if not specified in per-instance param, default is BC7/ASTC/BC3_BC1/ETC/PVRTC/R8G8B8A8. */
|
||||
/** @deprecated */
|
||||
priorityFormats?: KTX2TargetFormat[];
|
||||
/** Used for initialize KTX2 transcoder, default is BinomialLLC. */
|
||||
transcoder?: KTX2Transcoder;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,5 +17,9 @@ export enum KTX2TargetFormat {
|
||||
/** RG format, 16 bits per pixel. */
|
||||
R8G8,
|
||||
/** RGBA format, 32 bits per pixel. */
|
||||
R8G8B8A8
|
||||
R8G8B8A8,
|
||||
/** RGB HDR compressed format, 8 bits per pixel. */
|
||||
BC6H,
|
||||
/** RGBA format, 16 bits per channel. */
|
||||
R16G16B16A16
|
||||
}
|
||||
|
||||
@@ -80,9 +80,15 @@ export function transcode(buffer: Uint8Array, targetFormat: any, KTX2File: any):
|
||||
PVRTC1_4_RGB = 8,
|
||||
PVRTC1_4_RGBA = 9,
|
||||
ASTC_4x4 = 10,
|
||||
RGBA8 = 13
|
||||
RGBA8 = 13,
|
||||
BC6H = 22,
|
||||
RGB_HALF = 24,
|
||||
RGBA_HALF = 25
|
||||
}
|
||||
|
||||
/**
|
||||
* A copy of `KTX2TargetFormat`, which need be to specify in the worker.
|
||||
*/
|
||||
enum TargetFormat {
|
||||
ASTC,
|
||||
BC7,
|
||||
@@ -91,7 +97,9 @@ export function transcode(buffer: Uint8Array, targetFormat: any, KTX2File: any):
|
||||
ETC,
|
||||
R8,
|
||||
RG8,
|
||||
RGBA8
|
||||
RGBA8,
|
||||
BC6H,
|
||||
RGBA16
|
||||
}
|
||||
|
||||
function getTranscodeFormatFromTarget(target: TargetFormat, hasAlpha: boolean) {
|
||||
@@ -102,16 +110,20 @@ export function transcode(buffer: Uint8Array, targetFormat: any, KTX2File: any):
|
||||
return hasAlpha ? BasisFormat.ETC2 : BasisFormat.ETC1;
|
||||
case TargetFormat.PVRTC:
|
||||
return hasAlpha ? BasisFormat.PVRTC1_4_RGBA : BasisFormat.PVRTC1_4_RGB;
|
||||
case TargetFormat.BC6H:
|
||||
return BasisFormat.BC6H;
|
||||
case TargetFormat.RGBA8:
|
||||
return BasisFormat.RGBA8;
|
||||
case TargetFormat.ASTC:
|
||||
return BasisFormat.ASTC_4x4;
|
||||
case TargetFormat.BC7:
|
||||
return BasisFormat.BC7;
|
||||
case TargetFormat.RGBA16:
|
||||
return BasisFormat.RGBA_HALF;
|
||||
}
|
||||
}
|
||||
|
||||
function concat(arrays: Uint8Array[]) {
|
||||
function concat(arrays: Uint8Array[] | Uint16Array[]) {
|
||||
if (arrays.length === 1) return arrays[0];
|
||||
let totalByteLength = 0;
|
||||
|
||||
@@ -160,7 +172,7 @@ export function transcode(buffer: Uint8Array, targetFormat: any, KTX2File: any):
|
||||
for (let face = 0; face < faceCount; face++) {
|
||||
const mipmaps = new Array(levelCount);
|
||||
for (let mip = 0; mip < levelCount; mip++) {
|
||||
const layerMips: Uint8Array[] = new Array(layerCount);
|
||||
const layerMips: Uint8Array[] | Uint16Array[] = new Array(layerCount);
|
||||
let mipWidth: number, mipHeight: number;
|
||||
|
||||
for (let layer = 0; layer < layerCount; layer++) {
|
||||
@@ -177,10 +189,16 @@ export function transcode(buffer: Uint8Array, targetFormat: any, KTX2File: any):
|
||||
mipHeight = levelInfo.origHeight;
|
||||
}
|
||||
|
||||
const dst = new Uint8Array(ktx2File.getImageTranscodedSizeInBytes(mip, layer, 0, format));
|
||||
let dst: Uint8Array | Uint16Array = new Uint8Array(
|
||||
ktx2File.getImageTranscodedSizeInBytes(mip, layer, 0, format)
|
||||
);
|
||||
|
||||
const status = ktx2File.transcodeImage(dst, mip, layer, face, format, 0, -1, -1);
|
||||
|
||||
if (targetFormat === TargetFormat.RGBA16) {
|
||||
dst = new Uint16Array(dst.buffer, dst.byteOffset, dst.byteLength / Uint16Array.BYTES_PER_ELEMENT);
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
cleanup();
|
||||
throw new Error("transcodeImage failed.");
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { KTX2Container, SupercompressionScheme } from "../KTX2Container";
|
||||
import { KTX2TargetFormat } from "../KTX2TargetFormat";
|
||||
import { AbstractTranscoder, EncodedData, KhronosTranscoderMessage, TranscodeResult } from "./AbstractTranscoder";
|
||||
import { TranscodeWorkerCode } from "./KhronosWorkerCode";
|
||||
|
||||
/** @internal */
|
||||
export class KhronosTranscoder extends AbstractTranscoder {
|
||||
public static transcoderMap = {
|
||||
// TODO: support bc7
|
||||
[KTX2TargetFormat.ASTC]:
|
||||
"https://mdn.alipayobjects.com/rms/afts/file/A*0jiKRK6D1-kAAAAAAAAAAAAAARQnAQ/uastc_astc.wasm"
|
||||
};
|
||||
|
||||
constructor(
|
||||
workerLimitCount: number,
|
||||
public readonly type: KTX2TargetFormat
|
||||
) {
|
||||
super(workerLimitCount);
|
||||
}
|
||||
|
||||
_initTranscodeWorkerPool() {
|
||||
return fetch(KhronosTranscoder.transcoderMap[this.type])
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then((wasmBuffer) => {
|
||||
const funcCode = TranscodeWorkerCode.toString();
|
||||
const workerURL = URL.createObjectURL(
|
||||
new Blob([funcCode.substring(funcCode.indexOf("{") + 1, funcCode.lastIndexOf("}"))], {
|
||||
type: "application/javascript"
|
||||
})
|
||||
);
|
||||
|
||||
return this._createTranscodePool(workerURL, wasmBuffer);
|
||||
});
|
||||
}
|
||||
|
||||
transcode(ktx2Container: KTX2Container): Promise<TranscodeResult> {
|
||||
const needZstd = ktx2Container.supercompressionScheme === SupercompressionScheme.Zstd;
|
||||
|
||||
const levelCount = ktx2Container.levels.length;
|
||||
const faceCount = ktx2Container.faceCount;
|
||||
|
||||
const decodedData: any = {
|
||||
width: ktx2Container.pixelWidth,
|
||||
height: ktx2Container.pixelHeight,
|
||||
mipmaps: null
|
||||
};
|
||||
|
||||
const postMessageData: KhronosTranscoderMessage = {
|
||||
type: "transcode",
|
||||
format: 0,
|
||||
needZstd,
|
||||
data: new Array<EncodedData[]>(faceCount)
|
||||
};
|
||||
|
||||
const messageData = postMessageData.data;
|
||||
|
||||
for (let faceIndex = 0; faceIndex < faceCount; faceIndex++) {
|
||||
const mipmapData = new Array(levelCount);
|
||||
for (let mipmapIndex = 0; mipmapIndex < levelCount; mipmapIndex++) {
|
||||
const level = ktx2Container.levels[mipmapIndex];
|
||||
const levelWidth = Math.floor(ktx2Container.pixelWidth / (1 << mipmapIndex)) || 1;
|
||||
const levelHeight = Math.floor(ktx2Container.pixelHeight / (1 << mipmapIndex)) || 1;
|
||||
const originBuffer = level.levelData.buffer;
|
||||
const originOffset = level.levelData.byteOffset;
|
||||
const originByteLength = level.levelData.byteLength;
|
||||
|
||||
mipmapData[mipmapIndex] = {
|
||||
buffer: new Uint8Array(originBuffer, originOffset, originByteLength),
|
||||
levelWidth,
|
||||
levelHeight,
|
||||
uncompressedByteLength: level.uncompressedByteLength
|
||||
};
|
||||
}
|
||||
messageData[faceIndex] = mipmapData;
|
||||
}
|
||||
|
||||
return this._transcodeWorkerPool.postMessage(postMessageData).then((data) => {
|
||||
decodedData.faces = data;
|
||||
decodedData.hasAlpha = true;
|
||||
return decodedData;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import { EncodedData, IKhronosMessageMessage } from "./AbstractTranscoder";
|
||||
|
||||
interface WasmModule extends WebAssembly.Exports {
|
||||
memory: WebAssembly.Memory;
|
||||
transcode: (nBlocks: number) => number;
|
||||
}
|
||||
|
||||
interface DecoderExports {
|
||||
memory: Uint8Array;
|
||||
|
||||
ZSTD_findDecompressedSize: (compressedPtr: number, compressedSize: number) => number;
|
||||
ZSTD_decompress: (
|
||||
uncompressedPtr: number,
|
||||
uncompressedSize: number,
|
||||
compressedPtr: number,
|
||||
compressedSize: number
|
||||
) => number;
|
||||
malloc: (ptr: number) => number;
|
||||
free: (ptr: number) => void;
|
||||
}
|
||||
|
||||
export function TranscodeWorkerCode() {
|
||||
let wasmPromise: Promise<WasmModule>;
|
||||
/**
|
||||
* ZSTD (Zstandard) decoder.
|
||||
*/
|
||||
class ZSTDDecoder {
|
||||
public static heap: Uint8Array;
|
||||
public static IMPORT_OBJECT = {
|
||||
env: {
|
||||
emscripten_notify_memory_growth: function (): void {
|
||||
ZSTDDecoder.heap = new Uint8Array(ZSTDDecoder.instance.exports.memory.buffer);
|
||||
}
|
||||
}
|
||||
};
|
||||
public static instance: { exports: DecoderExports };
|
||||
public static WasmModuleURL =
|
||||
"https://mdn.alipayobjects.com/rms/afts/file/A*awNJR7KqIAEAAAAAAAAAAAAAARQnAQ/zstddec.wasm";
|
||||
|
||||
public _initPromise: Promise<any>;
|
||||
|
||||
init(): Promise<void> {
|
||||
if (!this._initPromise) {
|
||||
this._initPromise = fetch(ZSTDDecoder.WasmModuleURL)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
throw new Error(
|
||||
`Could not fetch the wasm component for the Zstandard decompression lib: ${response.status} - ${response.statusText}`
|
||||
);
|
||||
})
|
||||
.then((arrayBuffer) => WebAssembly.instantiate(arrayBuffer, ZSTDDecoder.IMPORT_OBJECT))
|
||||
.then(this._init);
|
||||
}
|
||||
return this._initPromise;
|
||||
}
|
||||
|
||||
_init(result: WebAssembly.WebAssemblyInstantiatedSource): void {
|
||||
ZSTDDecoder.instance = result.instance as unknown as {
|
||||
exports: DecoderExports;
|
||||
};
|
||||
|
||||
ZSTDDecoder.IMPORT_OBJECT.env.emscripten_notify_memory_growth(); // initialize heap.
|
||||
}
|
||||
|
||||
decode(array: Uint8Array, uncompressedSize = 0): Uint8Array {
|
||||
if (!ZSTDDecoder.instance) {
|
||||
throw new Error(`ZSTDDecoder: Await .init() before decoding.`);
|
||||
}
|
||||
|
||||
const exports = ZSTDDecoder.instance.exports;
|
||||
|
||||
// Write compressed data into WASM memory
|
||||
const compressedSize = array.byteLength;
|
||||
const compressedPtr = exports.malloc(compressedSize);
|
||||
ZSTDDecoder.heap.set(array, compressedPtr);
|
||||
|
||||
// Decompress into WASM memory
|
||||
uncompressedSize = uncompressedSize || Number(exports.ZSTD_findDecompressedSize(compressedPtr, compressedSize));
|
||||
const uncompressedPtr = exports.malloc(uncompressedSize);
|
||||
const actualSize = exports.ZSTD_decompress(uncompressedPtr, uncompressedSize, compressedPtr, compressedSize);
|
||||
|
||||
// Read decompressed data and free WASM memory
|
||||
const dec = ZSTDDecoder.heap.slice(uncompressedPtr, uncompressedPtr + actualSize);
|
||||
exports.free(compressedPtr);
|
||||
exports.free(uncompressedPtr);
|
||||
|
||||
return dec;
|
||||
}
|
||||
}
|
||||
function transcodeASTCAndBC7(wasmTranscoder: WasmModule, compressedData: Uint8Array, width: number, height: number) {
|
||||
const nBlocks = ((width + 3) >> 2) * ((height + 3) >> 2);
|
||||
|
||||
const texMemoryPages = (nBlocks * 16 + 65535) >> 16;
|
||||
const memory = wasmTranscoder.memory;
|
||||
const delta = texMemoryPages + 1 - (memory.buffer.byteLength >> 16);
|
||||
if (delta > 0) memory.grow(delta);
|
||||
|
||||
const textureView = new Uint8Array(memory.buffer, 65536, nBlocks * 16);
|
||||
textureView.set(compressedData);
|
||||
return wasmTranscoder.transcode(nBlocks) === 0 ? textureView : null;
|
||||
}
|
||||
|
||||
function initWasm(buffer: ArrayBuffer): Promise<WasmModule> {
|
||||
wasmPromise = WebAssembly.instantiate(buffer, {
|
||||
env: { memory: new WebAssembly.Memory({ initial: 16 }) }
|
||||
}).then((moduleWrapper) => <WasmModule>moduleWrapper.instance.exports);
|
||||
return wasmPromise;
|
||||
}
|
||||
|
||||
const zstdDecoder = new ZSTDDecoder();
|
||||
|
||||
function transcode(data: EncodedData[][], needZstd: boolean, wasmModule: WasmModule) {
|
||||
const faceCount = data.length;
|
||||
const result = new Array<
|
||||
{
|
||||
width: number;
|
||||
height: number;
|
||||
data: Uint8Array;
|
||||
}[]
|
||||
>(faceCount);
|
||||
|
||||
let promise = Promise.resolve();
|
||||
if (needZstd) {
|
||||
zstdDecoder.init();
|
||||
promise = zstdDecoder._initPromise;
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
for (let faceIndex = 0; faceIndex < faceCount; faceIndex++) {
|
||||
const mipmapCount = data[faceIndex].length;
|
||||
const decodedData = new Array<{
|
||||
width: number;
|
||||
height: number;
|
||||
data: Uint8Array;
|
||||
}>(mipmapCount);
|
||||
|
||||
for (let i = 0; i < mipmapCount; i++) {
|
||||
let { buffer, levelHeight, levelWidth, uncompressedByteLength } = data[faceIndex][i];
|
||||
if (needZstd) buffer = zstdDecoder.decode(buffer.slice(), uncompressedByteLength);
|
||||
|
||||
const faceByteLength = buffer.byteLength / faceCount;
|
||||
const originByteOffset = buffer.byteOffset;
|
||||
const decodedBuffer = transcodeASTCAndBC7(
|
||||
wasmModule,
|
||||
new Uint8Array(buffer.buffer, originByteOffset + faceIndex * faceByteLength, faceByteLength),
|
||||
levelWidth,
|
||||
levelHeight
|
||||
);
|
||||
if (decodedBuffer) {
|
||||
decodedData[i] = {
|
||||
// use wasm memory as buffer, should slice to avoid duplicate
|
||||
data: decodedBuffer.slice(),
|
||||
width: levelWidth,
|
||||
height: levelHeight
|
||||
};
|
||||
} else {
|
||||
throw "buffer decoded error";
|
||||
}
|
||||
}
|
||||
result[faceIndex] = decodedData;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
self.onmessage = function onmessage(event: MessageEvent<IKhronosMessageMessage>) {
|
||||
const message = event.data;
|
||||
switch (message.type) {
|
||||
case "init":
|
||||
initWasm(message.transcoderWasm)
|
||||
.then(() => {
|
||||
self.postMessage("init-completed");
|
||||
})
|
||||
.catch((e) => {
|
||||
self.postMessage({ error: e });
|
||||
});
|
||||
break;
|
||||
case "transcode":
|
||||
wasmPromise.then((module) => {
|
||||
transcode(message.data, message.needZstd, module)
|
||||
.then((decodedData) => {
|
||||
self.postMessage(decodedData);
|
||||
})
|
||||
.catch((e) => self.postMessage({ error: e }));
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -161,6 +161,11 @@ export class GLTexture implements IPlatformTexture {
|
||||
: GLCompressedTextureInternalFormat.RGBA_BPTC_UNORM_EXT,
|
||||
isCompressed: true
|
||||
};
|
||||
case TextureFormat.BC6H:
|
||||
return {
|
||||
internalFormat: GLCompressedTextureInternalFormat.RGB_BPTC_UNSIGNED_FLOAT_EXT,
|
||||
isCompressed: true
|
||||
};
|
||||
case TextureFormat.ETC1_RGB:
|
||||
return {
|
||||
internalFormat: GLCompressedTextureInternalFormat.RGB_ETC1_WEBGL,
|
||||
|
||||
@@ -45,8 +45,6 @@ describe("ktx2 Loader test", function () {
|
||||
expect(transcoder).not.to.be.null;
|
||||
KTX2Loader.release();
|
||||
// @ts-ignore
|
||||
expect(KTX2Loader._khronosTranscoder).to.be.null;
|
||||
// @ts-ignore
|
||||
expect(KTX2Loader._binomialLLCTranscoder).to.be.null;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user