Files
engine/packages/loader/src/HDRDecoder.ts
zhuxudong c751f38bb8 Move pixelStorei calls to non-compressed branch in GLTextureCube (#2918)
* fix: move pixelStorei calls to non-compressed branch in GLTextureCube
2026-03-12 20:44:18 +08:00

246 lines
8.1 KiB
TypeScript

/**
* HDR panorama to cubemap decoder.
*/
export class HDRDecoder {
// Float32 to Float16 lookup tables (http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf)
private static _float2HalfTables = HDRDecoder._generateFloat2HalfTables();
private static _floatView = new Float32Array(1);
private static _uint32View = new Uint32Array(HDRDecoder._floatView.buffer);
private static _one = 0x3c00; // Half float for 1.0
// prettier-ignore
private static _faces = [ // Cubemap face corners [bottomLeft, bottomRight, topLeft, topRight] as flat xyz
/* +X */ [ 1,-1,-1, 1,-1, 1, 1, 1,-1, 1, 1, 1],
/* -X */ [-1,-1, 1, -1,-1,-1, -1, 1, 1, -1, 1,-1],
/* +Y */ [-1,-1, 1, 1,-1, 1, -1,-1,-1, 1,-1,-1],
/* -Y */ [-1, 1,-1, 1, 1,-1, -1, 1, 1, 1, 1, 1],
/* +Z */ [-1,-1,-1, 1,-1,-1, -1, 1,-1, 1, 1,-1],
/* -Z */ [ 1,-1, 1, -1,-1, 1, 1, 1, 1, -1, 1, 1]
];
static parseHeader(uint8array: Uint8Array): IHDRHeader {
let line = this._readStringLine(uint8array, 0);
if (line[0] !== "#" || line[1] !== "?") {
throw "HDRDecoder: invalid file header";
}
let endOfHeader = false;
let findFormat = false;
let lineIndex = 0;
do {
lineIndex += line.length + 1;
line = this._readStringLine(uint8array, lineIndex);
if (line === "FORMAT=32-bit_rle_rgbe") findFormat = true;
else if (line.length === 0) endOfHeader = true;
} while (!endOfHeader);
if (!findFormat) {
throw "HDRDecoder: unsupported format, expected 32-bit_rle_rgbe";
}
lineIndex += line.length + 1;
line = this._readStringLine(uint8array, lineIndex);
const match = /^\-Y (.*) \+X (.*)$/g.exec(line);
if (!match || match.length < 3) {
throw "HDRDecoder: missing image size, only -Y +X layout is supported";
}
const width = parseInt(match[2]);
const height = parseInt(match[1]);
if (width < 8 || width > 0x7fff) {
throw "HDRDecoder: unsupported image width, must be between 8 and 32767";
}
return { height, width, dataPosition: lineIndex + line.length + 1 };
}
static decodeFaces(
bufferArray: Uint8Array,
header: IHDRHeader,
onFace: (faceIndex: number, data: Uint16Array) => void
): void {
const { width, height, dataPosition } = header;
const cubeSize = height >> 1;
const pixels = HDRDecoder._readPixels(bufferArray.subarray(dataPosition), width, height);
const faces = HDRDecoder._faces;
const faceBuffer = new Uint16Array(cubeSize * cubeSize * 4);
for (let faceIndex = 0; faceIndex < 6; faceIndex++) {
HDRDecoder._createCubemapData(cubeSize, faces[faceIndex], pixels, width, height, faceBuffer);
onFace(faceIndex, faceBuffer);
}
}
private static _generateFloat2HalfTables(): { baseTable: Uint32Array; shiftTable: Uint32Array } {
const baseTable = new Uint32Array(512);
const shiftTable = new Uint32Array(512);
for (let i = 0; i < 256; ++i) {
const e = i - 127;
if (e < -27) {
baseTable[i] = 0x0000;
baseTable[i | 0x100] = 0x8000;
shiftTable[i] = 24;
shiftTable[i | 0x100] = 24;
} else if (e < -14) {
baseTable[i] = 0x0400 >> (-e - 14);
baseTable[i | 0x100] = (0x0400 >> (-e - 14)) | 0x8000;
shiftTable[i] = -e - 1;
shiftTable[i | 0x100] = -e - 1;
} else if (e <= 15) {
baseTable[i] = (e + 15) << 10;
baseTable[i | 0x100] = ((e + 15) << 10) | 0x8000;
shiftTable[i] = 13;
shiftTable[i | 0x100] = 13;
} else if (e < 128) {
baseTable[i] = 0x7c00;
baseTable[i | 0x100] = 0xfc00;
shiftTable[i] = 24;
shiftTable[i | 0x100] = 24;
} else {
baseTable[i] = 0x7c00;
baseTable[i | 0x100] = 0xfc00;
shiftTable[i] = 13;
shiftTable[i | 0x100] = 13;
}
}
return { baseTable, shiftTable };
}
private static _createCubemapData(
texSize: number,
face: number[],
pixels: Uint8Array,
inputWidth: number,
inputHeight: number,
facePixels: Uint16Array
): void {
const invSize = 1 / texSize;
const rotDX1X = (face[3] - face[0]) * invSize;
const rotDX1Y = (face[4] - face[1]) * invSize;
const rotDX1Z = (face[5] - face[2]) * invSize;
const rotDX2X = (face[9] - face[6]) * invSize;
const rotDX2Y = (face[10] - face[7]) * invSize;
const rotDX2Z = (face[11] - face[8]) * invSize;
const floatView = HDRDecoder._floatView;
const uint32View = HDRDecoder._uint32View;
const { baseTable, shiftTable } = HDRDecoder._float2HalfTables;
const one = HDRDecoder._one;
let fy = 0;
for (let y = 0; y < texSize; y++) {
let xv1X = face[0],
xv1Y = face[1],
xv1Z = face[2];
let xv2X = face[6],
xv2Y = face[7],
xv2Z = face[8];
for (let x = 0; x < texSize; x++) {
let dirX = xv1X + (xv2X - xv1X) * fy;
let dirY = xv1Y + (xv2Y - xv1Y) * fy;
let dirZ = xv1Z + (xv2Z - xv1Z) * fy;
const invLen = 1 / Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ);
dirX *= invLen;
dirY *= invLen;
dirZ *= invLen;
let px = Math.round(((Math.atan2(dirZ, dirX) / Math.PI) * 0.5 + 0.5) * inputWidth);
if (px < 0) px = 0;
else if (px >= inputWidth) px = inputWidth - 1;
let py = Math.round((Math.acos(dirY) / Math.PI) * inputHeight);
if (py < 0) py = 0;
else if (py >= inputHeight) py = inputHeight - 1;
const srcIndex = (inputHeight - py - 1) * inputWidth * 4 + px * 4;
const scaleFactor = Math.pow(2, pixels[srcIndex + 3] - 128) / 255;
const dstIndex = y * texSize * 4 + x * 4;
for (let c = 0; c < 3; c++) {
// Clamp to half-float max (65504) to prevent Infinity in R16G16B16A16
floatView[0] = Math.min(pixels[srcIndex + c] * scaleFactor, 65504);
const f = uint32View[0];
const e = (f >> 23) & 0x1ff;
facePixels[dstIndex + c] = baseTable[e] + ((f & 0x007fffff) >> shiftTable[e]);
}
facePixels[dstIndex + 3] = one;
xv1X += rotDX1X;
xv1Y += rotDX1Y;
xv1Z += rotDX1Z;
xv2X += rotDX2X;
xv2Y += rotDX2Y;
xv2Z += rotDX2Z;
}
fy += invSize;
}
}
private static _readStringLine(uint8array: Uint8Array, startIndex: number): string {
let line = "";
for (let i = startIndex, n = uint8array.length; i < n; i++) {
const character = String.fromCharCode(uint8array[i]);
if (character === "\n") break;
line += character;
}
return line;
}
private static _readPixels(buffer: Uint8Array, width: number, height: number): Uint8Array {
const byteLength = buffer.byteLength;
const dataRGBA = new Uint8Array(4 * width * height);
let offset = 0;
let pos = 0;
const ptrEnd = 4 * width;
const scanLineBuffer = new Uint8Array(ptrEnd);
let numScanLines = height;
while (numScanLines > 0 && pos < byteLength) {
const a = buffer[pos++];
const b = buffer[pos++];
const c = buffer[pos++];
const d = buffer[pos++];
if (a !== 2 || b !== 2 || c & 0x80 || width < 8 || width > 32767) return buffer;
if (((c << 8) | d) !== width) throw "HDRDecoder: wrong scanline width";
let ptr = 0;
while (ptr < ptrEnd && pos < byteLength) {
let count = buffer[pos++];
const isEncodedRun = count > 128;
if (isEncodedRun) count -= 128;
if (count === 0 || ptr + count > ptrEnd) throw "HDRDecoder: bad scanline data";
if (isEncodedRun) {
const byteValue = buffer[pos++];
for (let i = 0; i < count; i++) scanLineBuffer[ptr++] = byteValue;
} else {
scanLineBuffer.set(buffer.subarray(pos, pos + count), ptr);
ptr += count;
pos += count;
}
}
for (let i = 0; i < width; i++, offset += 4) {
dataRGBA[offset] = scanLineBuffer[i];
dataRGBA[offset + 1] = scanLineBuffer[i + width];
dataRGBA[offset + 2] = scanLineBuffer[i + width * 2];
dataRGBA[offset + 3] = scanLineBuffer[i + width * 3];
}
numScanLines--;
}
return dataRGBA;
}
}
interface IHDRHeader {
width: number;
height: number;
dataPosition: number;
}