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