@lightningjs/renderer
Version:
Lightning 3 Renderer
372 lines • 14.4 kB
JavaScript
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2023 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {} from '../textures/Texture.js';
/**
* Tests if the given location is a compressed texture container
* @param url
* @remarks
* This function is used to determine if the given image url is a compressed
* and only supports the following extensions: .ktx and .pvr
* @returns
*/
export function isCompressedTextureContainer(src) {
return /\.(ktx|pvr)$/.test(src);
}
const PVR_MAGIC = 0x03525650; // 'PVR3' in little-endian
const PVR_TO_GL_INTERNAL_FORMAT = {
0: 0x8c01,
1: 0x8c03,
2: 0x8c00,
3: 0x8c02, // PVRTC1
6: 0x8d64, // ETC1
7: 0x83f0,
8: 0x83f2,
9: 0x83f2,
10: 0x83f3,
11: 0x83f3, // DXT variants
};
const ASTC_MAGIC = 0x5ca1ab13;
const ASTC_TO_GL_INTERNAL_FORMAT = {
'4x4': 0x93b0, // COMPRESSED_RGBA_ASTC_4x4_KHR
'5x5': 0x93b1, // COMPRESSED_RGBA_ASTC_5x5_KHR
'6x6': 0x93b2, // COMPRESSED_RGBA_ASTC_6x6_KHR
'8x8': 0x93b3, // COMPRESSED_RGBA_ASTC_8x8_KHR
'10x10': 0x93b4, // COMPRESSED_RGBA_ASTC_10x10_KHR
'12x12': 0x93b5, // COMPRESSED_RGBA_ASTC_12x12_KHR
};
// KTX file identifier
const KTX_IDENTIFIER = [
0xab, 0x4b, 0x54, 0x58, 0x20, 0x31, 0x31, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a,
];
/**
* Loads a compressed texture container
* @param url
* @returns
*/
export const loadCompressedTexture = async (url) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch compressed texture: ${response.status} ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
// Ensure we have enough data to check magic numbers
if (arrayBuffer.byteLength < 16) {
throw new Error(`File too small to be a valid compressed texture (${arrayBuffer.byteLength} bytes). Expected at least 16 bytes for header inspection.`);
}
const view = new DataView(arrayBuffer);
const magic = view.getUint32(0, true);
if (magic === PVR_MAGIC) {
return loadPVR(view);
}
if (magic === ASTC_MAGIC) {
return loadASTC(view);
}
let isKTX = true;
for (let i = 0; i < KTX_IDENTIFIER.length; i++) {
if (view.getUint8(i) !== KTX_IDENTIFIER[i]) {
isKTX = false;
break;
}
}
if (isKTX === true) {
return loadKTX(view);
}
else {
throw new Error('Unrecognized compressed texture format');
}
}
catch (error) {
throw new Error(`Failed to load compressed texture from ${url}: ${error}`);
}
};
function readUint24(view, offset) {
return (view.getUint8(offset) +
(view.getUint8(offset + 1) << 8) +
(view.getUint8(offset + 2) << 16));
}
/**
* Loads an ASTC texture container and returns the texture data
* @param view
* @returns
*/
const loadASTC = async function (view) {
const blockX = view.getUint8(4);
const blockY = view.getUint8(5);
const sizeX = readUint24(view, 7);
const sizeY = readUint24(view, 10);
if (sizeX === 0 || sizeY === 0) {
throw new Error(`Invalid ASTC texture dimensions: ${sizeX}x${sizeY}`);
}
const expected = Math.ceil(sizeX / blockX) * Math.ceil(sizeY / blockY) * 16;
const dataSize = view.byteLength - 16;
if (expected !== dataSize) {
throw new Error(`Invalid ASTC texture data size: expected ${expected}, got ${dataSize}`);
}
const internalFormat = ASTC_TO_GL_INTERNAL_FORMAT[`${blockX}x${blockY}`];
if (internalFormat === undefined) {
throw new Error(`Unsupported ASTC block size: ${blockX}x${blockY}`);
}
const buffer = view.buffer;
const mipmaps = [];
mipmaps.push(buffer.slice(16));
return {
data: {
blockInfo: blockInfoMap[internalFormat],
glInternalFormat: internalFormat,
mipmaps,
width: sizeX,
height: sizeY,
type: 'astc',
},
premultiplyAlpha: false,
};
};
const uploadASTC = function (glw, texture, data) {
if (glw.getExtension('WEBGL_compressed_texture_astc') === null) {
throw new Error('ASTC compressed textures not supported by this device');
}
glw.bindTexture(texture);
const { glInternalFormat, mipmaps, width, height } = data;
const view = new Uint8Array(mipmaps[0]);
glw.compressedTexImage2D(0, glInternalFormat, width, height, 0, view);
// ASTC textures MUST use no mipmaps unless stored
glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR);
};
/**
* Loads a KTX texture container and returns the texture data
* @param view
* @returns
*/
const loadKTX = async function (view) {
const endianness = view.getUint32(12, true);
const littleEndian = endianness === 0x04030201;
if (littleEndian === false && endianness !== 0x01020304) {
throw new Error('Invalid KTX endianness value');
}
const glType = view.getUint32(16, littleEndian);
const glFormat = view.getUint32(24, littleEndian);
if (glType !== 0 || glFormat !== 0) {
throw new Error(`KTX texture is not compressed (glType: ${glType}, glFormat: ${glFormat})`);
}
const glInternalFormat = view.getUint32(28, littleEndian);
if (blockInfoMap[glInternalFormat] === undefined) {
throw new Error(`Unsupported KTX compressed texture format: 0x${glInternalFormat.toString(16)}`);
}
const width = view.getUint32(36, littleEndian);
const height = view.getUint32(40, littleEndian);
if (width === 0 || height === 0) {
throw new Error(`Invalid KTX texture dimensions: ${width}x${height}`);
}
const mipmapLevels = view.getUint32(56, littleEndian);
if (mipmapLevels === 0) {
throw new Error('KTX texture has no mipmap levels');
}
const bytesOfKeyValueData = view.getUint32(60, littleEndian);
const mipmaps = [];
const buffer = view.buffer;
let offset = 64 + bytesOfKeyValueData;
if (offset > view.byteLength) {
throw new Error('Invalid KTX file: key/value data exceeds file size');
}
for (let i = 0; i < mipmapLevels; i++) {
const imageSize = view.getUint32(offset, littleEndian);
offset += 4;
const end = offset + imageSize;
mipmaps.push(buffer.slice(offset, end));
offset = end;
if (offset % 4 !== 0) {
offset += 4 - (offset % 4);
}
}
return {
data: {
blockInfo: blockInfoMap[glInternalFormat],
glInternalFormat: glInternalFormat,
mipmaps,
width: width,
height: height,
type: 'ktx',
},
premultiplyAlpha: false,
};
};
const uploadKTX = function (glw, texture, data) {
const { glInternalFormat, mipmaps, width, height, blockInfo } = data;
glw.bindTexture(texture);
const blockWidth = blockInfo.width;
const blockHeight = blockInfo.height;
let w = width;
let h = height;
for (let i = 0; i < mipmaps.length; i++) {
let view = new Uint8Array(mipmaps[i]);
const uploadW = Math.ceil(w / blockWidth) * blockWidth;
const uploadH = Math.ceil(h / blockHeight) * blockHeight;
const expectedBytes = Math.ceil(w / blockWidth) * Math.ceil(h / blockHeight) * blockInfo.bytes;
if (view.byteLength < expectedBytes) {
const padded = new Uint8Array(expectedBytes);
padded.set(view);
view = padded;
}
glw.compressedTexImage2D(i, glInternalFormat, uploadW, uploadH, 0, view);
w = Math.max(1, w >> 1);
h = Math.max(1, h >> 1);
}
glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
glw.texParameteri(glw.TEXTURE_MIN_FILTER, mipmaps.length > 1 ? glw.LINEAR_MIPMAP_LINEAR : glw.LINEAR);
};
function pvrtcMipSize(width, height, bpp) {
const minW = bpp === 2 ? 16 : 8;
const minH = 8;
const w = Math.max(width, minW);
const h = Math.max(height, minH);
return (w * h * bpp) / 8;
}
const loadPVR = async function (view) {
const pixelFormatLow = view.getUint32(8, true);
const internalFormat = PVR_TO_GL_INTERNAL_FORMAT[pixelFormatLow];
if (internalFormat === undefined) {
throw new Error(`Unsupported PVR pixel format: 0x${pixelFormatLow.toString(16)}`);
}
const height = view.getInt32(24, true);
const width = view.getInt32(28, true);
// validate dimensions
if (width === 0 || height === 0) {
throw new Error(`Invalid PVR texture dimensions: ${width}x${height}`);
}
const mipmapLevels = view.getInt32(44, true);
const metadataSize = view.getUint32(48, true);
const buffer = view.buffer;
let offset = 52 + metadataSize;
if (offset > buffer.byteLength) {
throw new Error('Invalid PVR file: metadata exceeds file size');
}
const mipmaps = [];
const block = blockInfoMap[internalFormat];
for (let i = 0; i < mipmapLevels; i++) {
const declaredSize = view.getUint32(offset, true);
const max = buffer.byteLength - (offset + 4);
if (declaredSize > 0 && declaredSize <= max) {
offset += 4;
const start = offset;
const end = offset + declaredSize;
mipmaps.push(buffer.slice(start, end));
offset = end;
offset = (offset + 3) & ~3; // align to 4 bytes
continue;
}
if (pixelFormatLow === 0 ||
pixelFormatLow === 1 ||
pixelFormatLow === 2 ||
pixelFormatLow === 3) {
const bpp = pixelFormatLow === 0 || pixelFormatLow === 1 ? 2 : 4;
const computed = pvrtcMipSize(width >> i, height >> i, bpp);
mipmaps.push(buffer.slice(offset, offset + computed));
offset += computed;
offset = (offset + 3) & ~3; // align to 4 bytes
continue;
}
if (block !== undefined) {
const blockW = Math.ceil((width >> i) / block.width);
const blockH = Math.ceil((height >> i) / block.height);
const computed = blockW * blockH * block.bytes;
mipmaps.push(buffer.slice(offset, offset + computed));
offset += computed;
offset = (offset + 3) & ~3;
}
}
return {
data: {
blockInfo: blockInfoMap[internalFormat],
glInternalFormat: internalFormat,
mipmaps,
width: width,
height: height,
type: 'pvr',
},
premultiplyAlpha: false,
};
};
const uploadPVR = function (glw, texture, data) {
const { glInternalFormat, mipmaps, width, height } = data;
glw.bindTexture(texture);
let w = width;
let h = height;
for (let i = 0; i < mipmaps.length; i++) {
glw.compressedTexImage2D(i, glInternalFormat, w, h, 0, new Uint8Array(mipmaps[i]));
w = Math.max(1, w >> 1);
h = Math.max(1, h >> 1);
}
glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE);
glw.texParameteri(glw.TEXTURE_MAG_FILTER, glw.LINEAR);
glw.texParameteri(glw.TEXTURE_MIN_FILTER, mipmaps.length > 1 ? glw.LINEAR_MIPMAP_LINEAR : glw.LINEAR);
};
// Predefined block info for common compressed texture formats
const BLOCK_4x4x8 = { width: 4, height: 4, bytes: 8 };
const BLOCK_4x4x16 = { width: 4, height: 4, bytes: 16 };
const BLOCK_5x5x16 = { width: 5, height: 5, bytes: 16 };
const BLOCK_6x6x16 = { width: 6, height: 6, bytes: 16 };
const BLOCK_8x4x8 = { width: 8, height: 4, bytes: 8 };
const BLOCK_8x8x16 = { width: 8, height: 8, bytes: 16 };
const BLOCK_10x10x16 = { width: 10, height: 10, bytes: 16 };
const BLOCK_12x12x16 = { width: 12, height: 12, bytes: 16 };
// Map of GL internal formats to their corresponding block info
export const blockInfoMap = {
// S3TC / DXTn (WEBGL_compressed_texture_s3tc, sRGB variants)
0x83f0: BLOCK_4x4x8, // COMPRESSED_RGB_S3TC_DXT1_EXT
0x83f1: BLOCK_4x4x8, // COMPRESSED_RGBA_S3TC_DXT1_EXT
0x83f2: BLOCK_4x4x16, // COMPRESSED_RGBA_S3TC_DXT3_EXT
0x83f3: BLOCK_4x4x16, // COMPRESSED_RGBA_S3TC_DXT5_EXT
// ETC1 / ETC2 / EAC
0x8d64: BLOCK_4x4x8, // COMPRESSED_RGB_ETC1_WEBGL
0x9274: BLOCK_4x4x8, // COMPRESSED_RGB8_ETC2
0x9275: BLOCK_4x4x8, // COMPRESSED_SRGB8_ETC2
0x9278: BLOCK_4x4x16, // COMPRESSED_RGBA8_ETC2_EAC
0x9279: BLOCK_4x4x16, // COMPRESSED_SRGB8_ALPHA8_ETC2_EAC
// PVRTC (WEBGL_compressed_texture_pvrtc)
0x8c00: BLOCK_4x4x8, // COMPRESSED_RGB_PVRTC_4BPPV1_IMG
0x8c02: BLOCK_4x4x8, // COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
0x8c01: BLOCK_8x4x8, // COMPRESSED_RGB_PVRTC_2BPPV1_IMG
0x8c03: BLOCK_8x4x8,
// ASTC (WEBGL_compressed_texture_astc)
0x93b0: BLOCK_4x4x16, // COMPRESSED_RGBA_ASTC_4x4_KHR
0x93d0: BLOCK_4x4x16, // COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR
0x93b1: BLOCK_5x5x16, // 5x5
0x93d1: BLOCK_5x5x16,
0x93b2: BLOCK_6x6x16, // 6x6
0x93d2: BLOCK_6x6x16,
0x93b3: BLOCK_8x8x16, // 8x8
0x93d3: BLOCK_8x8x16,
0x93b4: BLOCK_10x10x16, // 10x10
0x93d4: BLOCK_10x10x16,
0x93b5: BLOCK_12x12x16, // 12x12
0x93d5: BLOCK_12x12x16,
};
export const uploadCompressedTexture = {
ktx: uploadKTX,
pvr: uploadPVR,
astc: uploadASTC,
};
//# sourceMappingURL=textureCompression.js.map