From 3f4b101442d4927957993b6519d72fec479c51bf Mon Sep 17 00:00:00 2001 From: AnriTool <59344539+AnriTool@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:22:31 +1000 Subject: [PATCH 1/7] Remove dxt.js, added custom decoding, support VC/III formats --- package-lock.json | 11 - package.json | 3 - src/renderware/txd/TxdParser.ts | 119 +++-- src/renderware/utils/ImageDecoder.ts | 525 +++++++++++++++++++++++ src/renderware/utils/ImageFormatEnums.ts | 28 ++ 5 files changed, 647 insertions(+), 39 deletions(-) create mode 100644 src/renderware/utils/ImageDecoder.ts create mode 100644 src/renderware/utils/ImageFormatEnums.ts diff --git a/package-lock.json b/package-lock.json index dd18f95..57e9695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "rw-parser", "version": "1.4.0", "license": "GPL-3.0", - "dependencies": { - "dxt-js": "0.0.3" - }, "devDependencies": { "@types/jest": "^29.5.12", "@types/node": "^20.11.24", @@ -1635,14 +1632,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dxt-js": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/dxt-js/-/dxt-js-0.0.3.tgz", - "integrity": "sha512-qNBx0i5/ICyNFO7rs5ZothChgfFD408dg/ZBBfWFXDy0xeDQ86t91QUZxj5WqeUzZQ/+PPtjTUC0w3mS+198jg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.690", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz", diff --git a/package.json b/package.json index b858469..5aee4ef 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,6 @@ ], "main": "lib/index.js", "types": "lib/index.d.ts", - "dependencies": { - "dxt-js": "0.0.3" - }, "devDependencies": { "@types/jest": "^29.5.12", "@types/node": "^20.11.24", diff --git a/src/renderware/txd/TxdParser.ts b/src/renderware/txd/TxdParser.ts index cd9ffd0..5bd3e35 100644 --- a/src/renderware/txd/TxdParser.ts +++ b/src/renderware/txd/TxdParser.ts @@ -1,6 +1,11 @@ import { RwFile } from '../RwFile'; - -const dxt = require('dxt-js'); +import { ImageDecoder } from '../utils/ImageDecoder' +import { + D3DFormat, + PaletteType, + PlatformType, + RasterFormat +} from '../utils/ImageFormatEnums' export interface RwTxd { textureDictionary: RwTextureDictionary, @@ -90,21 +95,23 @@ export class TxdParser extends RwFile { const mipmapCount = this.readUint8(); const rasterType = this.readUint8(); - const _isPAL4 = rasterType & 0x4000; - const _isPAL8 = rasterType & 0x2000; - - const compressionFlags = this.readUint8(); + const compressionFlags = this.readUint8(); //dxtType for III/VC + // SA const alpha = (compressionFlags & (1 << 0)) !== 0; const cubeTexture = (compressionFlags & (1 << 1)) !== 0; const autoMipMaps = (compressionFlags & (1 << 2)) !== 0; const compressed = (compressionFlags & (1 << 3)) !== 0; + const paletteType = (rasterFormat >> 13) & 0b11; + let mipWidth = width; let mipHeight = height; let mipmaps: number[][] = []; + const palette: Uint8Array = (paletteType !== PaletteType.PALETTE_NONE ? this.readPalette(paletteType, depth) : new Uint8Array(0)); + for (let i = 0; i < mipmapCount; i++) { const rasterSize = this.readUint32(); @@ -114,25 +121,26 @@ export class TxdParser extends RwFile { // Raw RGBA presentation let bitmap: number[]; - if (compressed || d3dFormat.includes('DXT')) { - bitmap = Array.from(dxt.decompress(raster, mipWidth, mipHeight, dxt.flags[d3dFormat])); - } else { - // TODO: Make raster format an enum and add more formats - // All formats are in D3D9 color order (BGRA), so we swap them - - switch (rasterFormat) { - // FORMAT_8888, depth 32 (D3DFMT_A8R8G8B8) - case 0x0500: - // FORMAT_888 (RGB 8 bits each, D3DFMT_X8R8G8B8) - case 0x0600: - for (let i = 0; i < raster.length; i += 4) { - // Fancy array destructuring, just swaps R and B values - [raster[i], raster[i + 2]] = [raster[i + 2], raster[i]]; - } - break; - } - - bitmap = Array.from(raster); + if (0 !== palette.length) { + const rasterFormatsWithoutAlpha = [ + RasterFormat.RASTER_565, + RasterFormat.RASTER_LUM, + RasterFormat.RASTER_888, + RasterFormat.RASTER_555 + ]; + + const hasAlpha = ((platformId === PlatformType.D3D9 && alpha) || (platformId == PlatformType.D3D8 && !rasterFormatsWithoutAlpha.includes(rasterFormat))); + + bitmap = Array.from(this.getBitmapWithPalette(paletteType, depth, hasAlpha, raster, palette, width, height)); + } + else if (platformId === PlatformType.D3D8 && compressionFlags !== 0) { + bitmap = Array.from(this.getBitmapWithDXT('DXT' + compressionFlags, raster, width, height)); + } + else if (platformId === PlatformType.D3D9 && compressed) { + bitmap = Array.from(this.getBitmapWithDXT(d3dFormat, raster, width, height)); + } + else { + bitmap = Array.from(this.getBitmapWithRasterFormat(rasterFormat, raster, width, height)) } mipmaps.push(bitmap); @@ -162,4 +170,65 @@ export class TxdParser extends RwFile { mipmaps, }; } + + public readPalette(paletteType: number, depth: number): Uint8Array { + const size = (paletteType === PaletteType.PALETTE_8 ? 1024 : (depth === 4 ? 64 : 128)) + + return this.read(size); + } + + public getBitmapWithPalette(paletteType: number, depth: number, hasAlpha: boolean, raster: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + if (paletteType !== PaletteType.PALETTE_8 && depth == 4) { + return (hasAlpha + ? ImageDecoder.pal4(raster, palette, width, height) + : ImageDecoder.pal4NoAlpha(raster, palette, width, height) + ); + } + + return (hasAlpha + ? ImageDecoder.pal8(raster, palette, width, height) + : ImageDecoder.pal8NoAlpha(raster, palette, width, height) + ) + } + + public getBitmapWithDXT(dxtType:string, raster: Uint8Array, width: number, height: number): Uint8Array { + switch (dxtType) { + case D3DFormat.D3D_DXT1: + return ImageDecoder.bc1(raster, width, height); + case D3DFormat.D3D_DXT2: + return ImageDecoder.bc2(raster, width, height, true); + case D3DFormat.D3D_DXT3: + return ImageDecoder.bc2(raster, width, height, false); + case D3DFormat.D3D_DXT4: + return ImageDecoder.bc3(raster, width, height, true); + case D3DFormat.D3D_DXT5: + return ImageDecoder.bc3(raster, width, height, false); + // LUM8_A8 has compressed flag + case D3DFormat.D3DFMT_A8L8: + return ImageDecoder.lum8a8(raster, width, height); + default: + return new Uint8Array(0); + } + } + + public getBitmapWithRasterFormat (rasterFormat: number, raster: Uint8Array, width: number, height: number): Uint8Array { + switch (rasterFormat) { + case RasterFormat.RASTER_1555: + return ImageDecoder.bgra1555(raster, width, height); + case RasterFormat.RASTER_565: + return ImageDecoder.bgra565(raster, width, height); + case RasterFormat.RASTER_4444: + return ImageDecoder.bgra4444(raster, width, height); + case RasterFormat.RASTER_LUM: + return ImageDecoder.lum8(raster, width, height); + case RasterFormat.RASTER_8888: + return ImageDecoder.bgra8888(raster, width, height); + case RasterFormat.RASTER_888: + return ImageDecoder.bgra888(raster, width, height); + case RasterFormat.RASTER_555: + return ImageDecoder.bgra555(raster, width, height); + default: + return new Uint8Array(0); + } + } } diff --git a/src/renderware/utils/ImageDecoder.ts b/src/renderware/utils/ImageDecoder.ts new file mode 100644 index 0000000..a09fad5 --- /dev/null +++ b/src/renderware/utils/ImageDecoder.ts @@ -0,0 +1,525 @@ +// Source: https://github.com/Parik27/DragonFF/blob/master/gtaLib/txd.py +export class ImageDecoder { + + static readUInt16LE(buf: Uint8Array, offset: number): number { + return buf[offset] | (buf[offset + 1] << 8); + } + + static readUInt32LE(buf: Uint8Array, offset: number): number { + return ( + buf[offset] | + (buf[offset + 1] << 8) | + (buf[offset + 2] << 16) | + (buf[offset + 3] << 24) + ); + } + + static decode565(bits: number): [number, number, number] { + const r = Math.round(((bits >> 11) & 0b11111) * 255 / 31); + const g = Math.round(((bits >> 5) & 0b111111) * 255 / 63); + const b = Math.round((bits & 0b11111) * 255 / 31); + return [r, g, b]; + } + + static decode555(bits:number): [number, number, number] { + const r = Math.round(((bits >> 10) & 0b11111) * 255 / 31); + const g = Math.round(((bits >> 5) & 0b11111) * 255 / 31); + const b = Math.round((bits & 0b11111) * 255 / 31); + return [r, g, b]; + } + + static decode1555(bits: number): [number, number, number, number] { + const a = Math.round(((bits >> 15) & 0b1) * 255); + const r = Math.round(((bits >> 10) & 0b11111) * 255 / 31); + const g = Math.round(((bits >> 5) & 0b11111) * 255 / 31); + const b = Math.round((bits & 0b11111) * 255 / 31); + return [a, r, g, b]; + } + + static decode4444(bits: number): [number, number, number, number] { + const a = Math.round(((bits >> 12) & 0b1111) * 255 / 15); + const r = Math.round(((bits >> 8) & 0b1111) * 255 / 15); + const g = Math.round(((bits >> 4) & 0b1111) * 255 / 15); + const b = Math.round((bits & 0b1111) * 255 / 15); + return [a, r, g, b]; + } + + // Using if color0 > color1 on bcN + static color2Interpolation(color0:number, color1:number): number { + return (2 * color0 + color1) / 3; + } + + // Using if color0 <= color1 on bcN + static color2Average(color0:number, color1:number): number { + return (color0 + color1) / 2; + } + + // Using if color0 > color1 on bcN + static color3Interpolation(color0:number, color1:number): number { + return (2 * color1 + color0) / 3; + } + + static bc1(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const color0 = ImageDecoder.readUInt16LE(data, offset); + const color1 = ImageDecoder.readUInt16LE(data, offset + 2); + let bits = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + const [r0, g0, b0] = ImageDecoder.decode565(color0); + const [r1, g1, b1] = ImageDecoder.decode565(color1); + + for (let j = 0; j < 4; j++) { + for (let i = 0; i < 4; i++) { + const control = bits & 3; + bits >>= 2; + + let [r, g, b, a] = [0,0,0,0]; + + switch (control) { + case 0: + [r, g, b, a] = [r0, g0, b0, 0xff]; + break; + case 1: + [r, g, b, a] = [r1, g1, b1, 0xff]; + break; + case 2: + if (color0 > color1) { + r = ImageDecoder.color2Interpolation(r0, r1); + g = ImageDecoder.color2Interpolation(g0, g1); + b = ImageDecoder.color2Interpolation(b0, b1); + a = 0xff; + } else { + r = ImageDecoder.color2Average(r0, r1); + g = ImageDecoder.color2Average(g0, g1); + b = ImageDecoder.color2Average(b0, b1); + a = 0xff; + } + break; + case 3: + if (color0 > color1) { + r = ImageDecoder.color3Interpolation(r0, r1); + g = ImageDecoder.color3Interpolation(g0, g1); + b = ImageDecoder.color3Interpolation(b0, b1); + a = 0xff; + } else { + [r, g, b, a] = [0, 0, 0, 0]; + } + break; + } + + const idx = 4 * ((y + j) * width + (x + i)); + rgba[idx + 0] = r; + rgba[idx + 1] = g; + rgba[idx + 2] = b; + rgba[idx + 3] = 0xff; + } + } + } + } + + return rgba; + } + + static bc2(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const alpha0 = ImageDecoder.readUInt16LE(data, offset); + const alpha1 = ImageDecoder.readUInt16LE(data, offset + 2); + const alpha2 = ImageDecoder.readUInt16LE(data, offset + 4); + const alpha3 = ImageDecoder.readUInt16LE(data, offset + 6); + const color0 = ImageDecoder.readUInt16LE(data, offset + 8); + const color1 = ImageDecoder.readUInt16LE(data, offset + 10); + let bits = ImageDecoder.readUInt32LE(data, offset + 12); + offset += 16; + + const [r0, g0, b0] = ImageDecoder.decode565(color0); + const [r1, g1, b1] = ImageDecoder.decode565(color1); + const alphas = [alpha0, alpha1, alpha2, alpha3]; + + for (let j = 0; j < 4; j++) { + for (let i = 0; i < 4; i++) { + const control = bits & 3; + bits >>= 2; + + let [r, g, b] = [0,0,0]; + + switch (control) { + case 0: + [r, g, b] = [r0, g0, b0]; + break; + case 1: + [r, g, b] = [r1, g1, b1]; + break; + case 2: + if (color0 > color1) { + r = ImageDecoder.color2Interpolation(r0, r1); + g = ImageDecoder.color2Interpolation(g0, g1); + b = ImageDecoder.color2Interpolation(b0, b1); + } else { + r = ImageDecoder.color2Average(r0, r1); + g = ImageDecoder.color2Average(g0, g1); + b = ImageDecoder.color2Average(b0, b1); + } + break; + case 3: + if (color0 > color1) { + r = ImageDecoder.color3Interpolation(r0, r1); + g = ImageDecoder.color3Interpolation(g0, g1); + b = ImageDecoder.color3Interpolation(b0, b1); + } else { + [r, g, b] = [0, 0, 0]; + } + break; + } + + const a = ((alphas[j] >> (i * 4)) & 0xf) * 0x11; + + const idx = 4 * ((y + j) * width + (x + i)); + + if (premultiplied && a > 0) { + r = Math.min(Math.round((r * 255) / a), 255); + g = Math.min(Math.round((g * 255) / a), 255); + b = Math.min(Math.round((b * 255) / a), 255); + } + + rgba[idx + 0] = r; + rgba[idx + 1] = g; + rgba[idx + 2] = b; + rgba[idx + 3] = a; + } + } + } + } + + return rgba; + } + + static bc3(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const alpha0 = data[offset]; + const alpha1 = data[offset + 1]; + const alpha2 = ImageDecoder.readUInt16LE(data, offset + 2); + const alpha3 = ImageDecoder.readUInt16LE(data, offset + 4); + const alpha4 = ImageDecoder.readUInt16LE(data, offset + 6); + const color0 = ImageDecoder.readUInt16LE(data, offset + 8); + const color1 = ImageDecoder.readUInt16LE(data, offset + 10); + let bits = ImageDecoder.readUInt32LE(data, offset + 12); + offset += 16; + + const [r0, g0, b0] = ImageDecoder.decode565(color0); + const [r1, g1, b1] = ImageDecoder.decode565(color1); + + let alphas: number[]; + if (alpha0 > alpha1) { + alphas = [ + alpha0, + alpha1, + Math.round(alpha0 * (6 / 7) + alpha1 * (1 / 7)), + Math.round(alpha0 * (5 / 7) + alpha1 * (2 / 7)), + Math.round(alpha0 * (4 / 7) + alpha1 * (3 / 7)), + Math.round(alpha0 * (3 / 7) + alpha1 * (4 / 7)), + Math.round(alpha0 * (2 / 7) + alpha1 * (5 / 7)), + Math.round(alpha0 * (1 / 7) + alpha1 * (6 / 7)) + ]; + } else { + alphas = [ + alpha0, + alpha1, + Math.round(alpha0 * (4 / 5) + alpha1 * (1 / 5)), + Math.round(alpha0 * (3 / 5) + alpha1 * (2 / 5)), + Math.round(alpha0 * (2 / 5) + alpha1 * (3 / 5)), + Math.round(alpha0 * (1 / 5) + alpha1 * (4 / 5)), + 0, + 255 + ]; + } + + const alphaIndices = [alpha4, alpha3, alpha2]; + + for (let j = 0; j < 4; j++) { + for (let i = 0; i < 4; i++) { + const control = bits & 3; + bits >>= 2; + + let [r, g, b] = [0,0,0]; + + switch (control) { + case 0: + [r, g, b] = [r0, g0, b0]; + break; + case 1: + [r, g, b] = [r1, g1, b1]; + break; + case 2: + if (color0 > color1) { + r = ImageDecoder.color2Interpolation(r0, r1); + g = ImageDecoder.color2Interpolation(g0, g1); + b = ImageDecoder.color2Interpolation(b0, b1); + } else { + r = ImageDecoder.color2Average(r0, r1); + g = ImageDecoder.color2Average(g0, g1); + b = ImageDecoder.color2Average(b0, b1); + } + break; + case 3: + if (color0 > color1) { + r = ImageDecoder.color3Interpolation(r0, r1); + g = ImageDecoder.color3Interpolation(g0, g1); + b = ImageDecoder.color3Interpolation(b0, b1); + } else { + [r, g, b] = [0, 0, 0]; + } + break; + } + + const shift = 3 * (15 - ((3 - i) + j * 4)); + const shiftS = shift % 16; + const rowS = Math.floor(shift / 16); + const rowE = Math.floor((shift + 2) / 16); + + let alphaIndex = (alphaIndices[2 - rowS] >> shiftS) & 0x7; + + if (rowS !== rowE) { + const shift_e = 16 - shiftS; + alphaIndex += (alphaIndices[2 - rowE] & ((1 << (3 - shift_e)) - 1)) << shift_e; + } + + const a = alphas[alphaIndex]; + + const idx = 4 * ((y + j) * width + (x + i)); + + if (premultiplied && a > 0) { + r = Math.min(Math.round((r * 255) / a), 255); + g = Math.min(Math.round((g * 255) / a), 255); + b = Math.min(Math.round((b * 255) / a), 255); + } + + rgba[idx + 0] = r; + rgba[idx + 1] = g; + rgba[idx + 2] = b; + rgba[idx + 3] = a; + } + } + } + } + + return rgba; + } + + static bgra1555(data: Uint8Array, width: number, height: number): Uint8Array { + const rbga = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [a, r, g, b] = ImageDecoder.decode1555(color); + + rbga[offset++] = r; + rbga[offset++] = g; + rbga[offset++] = b; + rbga[offset++] = a; + } + + return rbga; + } + + static bgra4444(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [a, r, g, b] = ImageDecoder.decode4444(color); + + rgba[offset++] = r; + rgba[offset++] = g; + rgba[offset++] = b; + rgba[offset++] = a; + } + + return rgba; + } + + static bgra555(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [r, g, b] = ImageDecoder.decode555(color); + + rgba[offset++] = r; + rgba[offset++] = g; + rgba[offset++] = b; + rgba[offset++] = 0xff; + } + + return rgba; + } + + static bgra565(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [r, g, b] = ImageDecoder.decode565(color); + + rgba[offset++] = r; + rgba[offset++] = g; + rgba[offset++] = b; + rgba[offset++] = 0xff; + } + + return rgba; + } + + static bgra888(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + for (let i = 0; i < data.length; i += 4) { + rgba[i + 0] = data[i + 2]; + rgba[i + 1] = data[i + 1]; + rgba[i + 2] = data[i + 0]; + rgba[i + 3] = 0xff; + } + + return rgba; + } + + static bgra8888(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + for (let i = 0; i < data.length; i += 4) { + rgba[i + 0] = data[i + 2]; + rgba[i + 1] = data[i + 1]; + rgba[i + 2] = data[i + 0]; + rgba[i + 3] = data[i + 3]; + } + + return rgba; + } + + static lum8(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + + for (let i = 0; i < data.length; i++) { + const offset = i * 4; + const luminance = data[i]; + rgba[offset + 0] = luminance; // R + rgba[offset + 1] = luminance; // G + rgba[offset + 2] = luminance; // B + rgba[offset + 3] = 0xff; + } + + return rgba; + } + + static lum8a8(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const luminance = data[i]; + const alpha = data[i + 1]; + + rgba[offset++] = luminance; + rgba[offset++] = luminance; + rgba[offset++] = luminance; + rgba[offset++] = alpha; + } + + return rgba; + } + + static pal4(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i++) { + const b = data[i]; + const idx1 = (b >> 4) & 0xf; + const idx2 = b & 0xf; + + // Copying RGBA from the palette for two pixels + rgba[offset++] = palette[idx1 * 4 + 0]; // R + rgba[offset++] = palette[idx1 * 4 + 1]; // G + rgba[offset++] = palette[idx1 * 4 + 2]; // B + rgba[offset++] = palette[idx1 * 4 + 3]; // A + + rgba[offset++] = palette[idx2 * 4 + 0]; // R + rgba[offset++] = palette[idx2 * 4 + 1]; // G + rgba[offset++] = palette[idx2 * 4 + 2]; // B + rgba[offset++] = palette[idx2 * 4 + 3]; // A + } + + return rgba; + } + + static pal4NoAlpha(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i++) { + const b = data[i]; + const colorIndex1 = (b >> 4) & 0xf; + const colorIndex2 = b & 0xf; + + // First pixel + rgba[offset++] = palette[colorIndex1 * 4 + 0]; // R + rgba[offset++] = palette[colorIndex1 * 4 + 1]; // G + rgba[offset++] = palette[colorIndex1 * 4 + 2]; // B + rgba[offset++] = 0xff; + + // Second pixel + rgba[offset++] = palette[colorIndex2 * 4 + 0]; // R + rgba[offset++] = palette[colorIndex2 * 4 + 1]; // G + rgba[offset++] = palette[colorIndex2 * 4 + 2]; // B + rgba[offset++] = 0xff; + } + + return rgba; + } + + static pal8(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + + for (let i = 0; i < data.length; i++) { + const colorIndex = data[i]; + + // Copy RGBA from palette + rgba[i * 4 + 0] = palette[colorIndex * 4 + 0]; // R + rgba[i * 4 + 1] = palette[colorIndex * 4 + 1]; // G + rgba[i * 4 + 2] = palette[colorIndex * 4 + 2]; // B + rgba[i * 4 + 3] = palette[colorIndex * 4 + 3]; // A + } + + return rgba; + } + + static pal8NoAlpha(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + + for (let i = 0; i < data.length; i++) { + const colorIndex = data[i]; + + // Copy RGB from palette + rgba[i * 4 + 0] = palette[colorIndex * 4 + 0]; // R + rgba[i * 4 + 1] = palette[colorIndex * 4 + 1]; // G + rgba[i * 4 + 2] = palette[colorIndex * 4 + 2]; // B + rgba[i * 4 + 3] = 0xff; + } + + return rgba; + } +} \ No newline at end of file diff --git a/src/renderware/utils/ImageFormatEnums.ts b/src/renderware/utils/ImageFormatEnums.ts new file mode 100644 index 0000000..dbb362a --- /dev/null +++ b/src/renderware/utils/ImageFormatEnums.ts @@ -0,0 +1,28 @@ +export enum RasterFormat { + RASTER_1555 = 0x0100, + RASTER_565 = 0x0200, + RASTER_4444 = 0x0300, + RASTER_LUM = 0x0400, + RASTER_8888 = 0x0500, + RASTER_888 = 0x0600, + RASTER_555 = 0x0a00, +} + +export enum D3DFormat { + D3DFMT_A8L8 = "3", + D3D_DXT1 = "DXT1", + D3D_DXT2 = "DXT2", + D3D_DXT3 = "DXT3", + D3D_DXT4 = "DXT4", + D3D_DXT5 = "DXT5", +} + +export enum PaletteType { + PALETTE_NONE = 0, + PALETTE_8 = 1, +} + +export enum PlatformType { + D3D8 = 0x8, + D3D9 = 0x9, +} \ No newline at end of file From 2376c4bab5c67ae626f349a290f5fc0ee0d77d82 Mon Sep 17 00:00:00 2001 From: AnriTool <59344539+AnriTool@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:40:00 +1000 Subject: [PATCH 2/7] Remove dxt.js, added custom decoding, support VC/III formats --- package-lock.json | 11 +++++++++++ package.json | 3 +++ src/renderware/txd/TxdParser.ts | 2 +- src/renderware/utils/ImageDecoder.ts | 4 ++-- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 57e9695..dd18f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "rw-parser", "version": "1.4.0", "license": "GPL-3.0", + "dependencies": { + "dxt-js": "0.0.3" + }, "devDependencies": { "@types/jest": "^29.5.12", "@types/node": "^20.11.24", @@ -1632,6 +1635,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dxt-js": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/dxt-js/-/dxt-js-0.0.3.tgz", + "integrity": "sha512-qNBx0i5/ICyNFO7rs5ZothChgfFD408dg/ZBBfWFXDy0xeDQ86t91QUZxj5WqeUzZQ/+PPtjTUC0w3mS+198jg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.690", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz", diff --git a/package.json b/package.json index 5aee4ef..b858469 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ ], "main": "lib/index.js", "types": "lib/index.d.ts", + "dependencies": { + "dxt-js": "0.0.3" + }, "devDependencies": { "@types/jest": "^29.5.12", "@types/node": "^20.11.24", diff --git a/src/renderware/txd/TxdParser.ts b/src/renderware/txd/TxdParser.ts index 5bd3e35..a171682 100644 --- a/src/renderware/txd/TxdParser.ts +++ b/src/renderware/txd/TxdParser.ts @@ -95,7 +95,7 @@ export class TxdParser extends RwFile { const mipmapCount = this.readUint8(); const rasterType = this.readUint8(); - const compressionFlags = this.readUint8(); //dxtType for III/VC + const compressionFlags = this.readUint8(); // Is "dxtType" for III/VC // SA const alpha = (compressionFlags & (1 << 0)) !== 0; diff --git a/src/renderware/utils/ImageDecoder.ts b/src/renderware/utils/ImageDecoder.ts index a09fad5..2197812 100644 --- a/src/renderware/utils/ImageDecoder.ts +++ b/src/renderware/utils/ImageDecoder.ts @@ -149,7 +149,7 @@ export class ImageDecoder { const control = bits & 3; bits >>= 2; - let [r, g, b] = [0,0,0]; + let [r, g, b] = [0, 0, 0]; switch (control) { case 0: @@ -253,7 +253,7 @@ export class ImageDecoder { const control = bits & 3; bits >>= 2; - let [r, g, b] = [0,0,0]; + let [r, g, b] = [0, 0, 0]; switch (control) { case 0: From c9f71514c5635373db6096dbd10b7d4308ae40aa Mon Sep 17 00:00:00 2001 From: AnriTool <59344539+AnriTool@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:39:20 +1000 Subject: [PATCH 3/7] DXT1 Alpha --- src/renderware/txd/TxdParser.ts | 4 ++-- src/renderware/utils/ImageDecoder.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderware/txd/TxdParser.ts b/src/renderware/txd/TxdParser.ts index a171682..d0378ad 100644 --- a/src/renderware/txd/TxdParser.ts +++ b/src/renderware/txd/TxdParser.ts @@ -110,7 +110,7 @@ export class TxdParser extends RwFile { let mipmaps: number[][] = []; - const palette: Uint8Array = (paletteType !== PaletteType.PALETTE_NONE ? this.readPalette(paletteType, depth) : new Uint8Array(0)); + const palette = (paletteType !== PaletteType.PALETTE_NONE ? this.readPalette(paletteType, depth) : new Uint8Array(0)); for (let i = 0; i < mipmapCount; i++) { @@ -121,7 +121,7 @@ export class TxdParser extends RwFile { // Raw RGBA presentation let bitmap: number[]; - if (0 !== palette.length) { + if (palette.length !== 0) { const rasterFormatsWithoutAlpha = [ RasterFormat.RASTER_565, RasterFormat.RASTER_LUM, diff --git a/src/renderware/utils/ImageDecoder.ts b/src/renderware/utils/ImageDecoder.ts index 2197812..5b3b0ac 100644 --- a/src/renderware/utils/ImageDecoder.ts +++ b/src/renderware/utils/ImageDecoder.ts @@ -78,7 +78,7 @@ export class ImageDecoder { const control = bits & 3; bits >>= 2; - let [r, g, b, a] = [0,0,0,0]; + let [r, g, b, a] = [0, 0, 0, 0]; switch (control) { case 0: @@ -116,7 +116,7 @@ export class ImageDecoder { rgba[idx + 0] = r; rgba[idx + 1] = g; rgba[idx + 2] = b; - rgba[idx + 3] = 0xff; + rgba[idx + 3] = a; } } } From 029d01cb8e90d1b37832862f89639a9ca9121107 Mon Sep 17 00:00:00 2001 From: AnriTool <59344539+AnriTool@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:47:22 +1000 Subject: [PATCH 4/7] dxt-decompress optimization and fixes --- src/renderware/utils/ImageDecoder.ts | 502 +++++++++++++++------------ 1 file changed, 273 insertions(+), 229 deletions(-) diff --git a/src/renderware/utils/ImageDecoder.ts b/src/renderware/utils/ImageDecoder.ts index 5b3b0ac..cf81fef 100644 --- a/src/renderware/utils/ImageDecoder.ts +++ b/src/renderware/utils/ImageDecoder.ts @@ -15,10 +15,15 @@ export class ImageDecoder { } static decode565(bits: number): [number, number, number] { - const r = Math.round(((bits >> 11) & 0b11111) * 255 / 31); - const g = Math.round(((bits >> 5) & 0b111111) * 255 / 63); - const b = Math.round((bits & 0b11111) * 255 / 31); - return [r, g, b]; + const r = (bits >> 11) & 0b11111; + const g = (bits >> 5) & 0b111111; + const b = bits & 0b11111; + + return [ + (r << 3) | (r >> 2), + (g << 2) | (g >> 4), + (b << 3) | (b >> 2) + ]; } static decode555(bits:number): [number, number, number] { @@ -44,79 +49,78 @@ export class ImageDecoder { return [a, r, g, b]; } - // Using if color0 > color1 on bcN - static color2Interpolation(color0:number, color1:number): number { - return (2 * color0 + color1) / 3; - } - - // Using if color0 <= color1 on bcN - static color2Average(color0:number, color1:number): number { - return (color0 + color1) / 2; - } - - // Using if color0 > color1 on bcN - static color3Interpolation(color0:number, color1:number): number { - return (2 * color1 + color0) / 3; - } - + /* + bc1 - block compression format, using for DXT1 + compress 4x4 block of pixels + format: + +---------------+ + | color0 | color0 in palette. 16bit (RGB 565 format) + +---------------+ + | color1 | color1 in palette. 16bit (RGB 565 format) + +---+---+---+---+ + | a | b | c | d | a-p color palette index 2bit * 16 + +---+---+---+---+ + | e | f | g | h | + +---+---+---+---+ + | i | j | k | l | + +---+---+---+---+ + | m | n | o | p | total: 8byte in 4x4 colors + +---+---+---+---+ + + color2 and color3 in the palette are calculated by interpolating other colors or choosing the average between them. + color0 > color1 => interpolation, else => average + */ static bc1(data: Uint8Array, width: number, height: number): Uint8Array { const rgba = new Uint8Array(4 * width * height); + const colorPalette = new Uint8Array(16); let offset = 0; for (let y = 0; y < height; y += 4) { for (let x = 0; x < width; x += 4) { const color0 = ImageDecoder.readUInt16LE(data, offset); const color1 = ImageDecoder.readUInt16LE(data, offset + 2); - let bits = ImageDecoder.readUInt32LE(data, offset + 4); + let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); offset += 8; - const [r0, g0, b0] = ImageDecoder.decode565(color0); - const [r1, g1, b1] = ImageDecoder.decode565(color1); - - for (let j = 0; j < 4; j++) { - for (let i = 0; i < 4; i++) { - const control = bits & 3; - bits >>= 2; - - let [r, g, b, a] = [0, 0, 0, 0]; - - switch (control) { - case 0: - [r, g, b, a] = [r0, g0, b0, 0xff]; - break; - case 1: - [r, g, b, a] = [r1, g1, b1, 0xff]; - break; - case 2: - if (color0 > color1) { - r = ImageDecoder.color2Interpolation(r0, r1); - g = ImageDecoder.color2Interpolation(g0, g1); - b = ImageDecoder.color2Interpolation(b0, b1); - a = 0xff; - } else { - r = ImageDecoder.color2Average(r0, r1); - g = ImageDecoder.color2Average(g0, g1); - b = ImageDecoder.color2Average(b0, b1); - a = 0xff; - } - break; - case 3: - if (color0 > color1) { - r = ImageDecoder.color3Interpolation(r0, r1); - g = ImageDecoder.color3Interpolation(g0, g1); - b = ImageDecoder.color3Interpolation(b0, b1); - a = 0xff; - } else { - [r, g, b, a] = [0, 0, 0, 0]; - } - break; - } - - const idx = 4 * ((y + j) * width + (x + i)); - rgba[idx + 0] = r; - rgba[idx + 1] = g; - rgba[idx + 2] = b; - rgba[idx + 3] = a; + let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); + let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); + + if (color0 > color1) { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (2 * c0r + c1r + 1) / 3; + colorPalette[9] = (2 * c0g + c1g + 1) / 3; + colorPalette[10] = (2 * c0b + c1b + 1) / 3; + colorPalette[12] = (c0r + 2 * c1r + 1) / 3; + colorPalette[13] = (c0g + 2 * c1g + 1) / 3; + colorPalette[14] = (c0b + 2 * c1b + 1) / 3; + } + else { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (c0r + c1r + 1) >> 1; + colorPalette[9] = (c0g + c1g + 1) >> 1; + colorPalette[10] = (c0b + c1b + 1) >> 1; + colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; + } + + const baseIndex = (y * width + x) * 4; + for (let k = 0; k < 16; k++) { + const colorIdx = colorBits & 0x3; + colorBits >>>= 2; + + const j = k >> 2; + const i = k & 3; + const idx = baseIndex + ((j * width + i) << 2); + + rgba[idx + 0] = colorPalette[colorIdx * 4]; + rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; + rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; + + if (color0 <= color1 && colorIdx === 3) { + rgba[idx + 3] = 0; + } else { + rgba[idx + 3] = 255; } } } @@ -125,76 +129,100 @@ export class ImageDecoder { return rgba; } + /* + bc2 - block compression format, using for DXT2 and DXT3 + compress 4x4 block of pixels with 4x4 4bit alpha + format: + +---+---+---+---+ + | a | b | c | d | a-p pixel alpha. 4bit * 16 + +---+---+---+---+ + | e | f | g | h | + +---+---+---+---+ + | i | j | k | l | + +---+---+---+---+ + | m | n | o | p | + +---+---+---+---+ + | | bc1 collor compression. 8byte + | bc1 block | + | | total: 16byte in 4x4 colors + +---------------+ + + in DXT2, the color data is interpreted as being premultiplied by alpha + */ static bc2(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { const rgba = new Uint8Array(4 * width * height); + const colorPalette = new Uint8Array(16); + + const alphaTable = new Uint8Array(16); + for (let i = 0; i < 16; i++) { + alphaTable[i] = (i * 255 + 7.5) / 15 | 0; + } + let offset = 0; for (let y = 0; y < height; y += 4) { for (let x = 0; x < width; x += 4) { - const alpha0 = ImageDecoder.readUInt16LE(data, offset); - const alpha1 = ImageDecoder.readUInt16LE(data, offset + 2); - const alpha2 = ImageDecoder.readUInt16LE(data, offset + 4); - const alpha3 = ImageDecoder.readUInt16LE(data, offset + 6); - const color0 = ImageDecoder.readUInt16LE(data, offset + 8); - const color1 = ImageDecoder.readUInt16LE(data, offset + 10); - let bits = ImageDecoder.readUInt32LE(data, offset + 12); - offset += 16; - - const [r0, g0, b0] = ImageDecoder.decode565(color0); - const [r1, g1, b1] = ImageDecoder.decode565(color1); - const alphas = [alpha0, alpha1, alpha2, alpha3]; - - for (let j = 0; j < 4; j++) { - for (let i = 0; i < 4; i++) { - const control = bits & 3; - bits >>= 2; - - let [r, g, b] = [0, 0, 0]; - - switch (control) { - case 0: - [r, g, b] = [r0, g0, b0]; - break; - case 1: - [r, g, b] = [r1, g1, b1]; - break; - case 2: - if (color0 > color1) { - r = ImageDecoder.color2Interpolation(r0, r1); - g = ImageDecoder.color2Interpolation(g0, g1); - b = ImageDecoder.color2Interpolation(b0, b1); - } else { - r = ImageDecoder.color2Average(r0, r1); - g = ImageDecoder.color2Average(g0, g1); - b = ImageDecoder.color2Average(b0, b1); - } - break; - case 3: - if (color0 > color1) { - r = ImageDecoder.color3Interpolation(r0, r1); - g = ImageDecoder.color3Interpolation(g0, g1); - b = ImageDecoder.color3Interpolation(b0, b1); - } else { - [r, g, b] = [0, 0, 0]; - } - break; - } - - const a = ((alphas[j] >> (i * 4)) & 0xf) * 0x11; - - const idx = 4 * ((y + j) * width + (x + i)); - - if (premultiplied && a > 0) { - r = Math.min(Math.round((r * 255) / a), 255); - g = Math.min(Math.round((g * 255) / a), 255); - b = Math.min(Math.round((b * 255) / a), 255); - } - - rgba[idx + 0] = r; - rgba[idx + 1] = g; - rgba[idx + 2] = b; - rgba[idx + 3] = a; + const alpha0 = ImageDecoder.readUInt32LE(data, offset); + const alpha1 = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + const color0 = ImageDecoder.readUInt16LE(data, offset); + const color1 = ImageDecoder.readUInt16LE(data, offset + 2); + let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); + let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); + + if (color0 > color1) { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (2 * c0r + c1r + 1) / 3; + colorPalette[9] = (2 * c0g + c1g + 1) / 3; + colorPalette[10] = (2 * c0b + c1b + 1) / 3; + colorPalette[12] = (c0r + 2 * c1r + 1) / 3; + colorPalette[13] = (c0g + 2 * c1g + 1) / 3; + colorPalette[14] = (c0b + 2 * c1b + 1) / 3; + } + else { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (c0r + c1r + 1) >> 1; + colorPalette[9] = (c0g + c1g + 1) >> 1; + colorPalette[10] = (c0b + c1b + 1) >> 1; + colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; + } + + const baseIndex = ((y * width + x) << 2); + + for (let k = 0; k < 16; k++) { + const j = k >> 2; + const i = k & 3; + + const idx = baseIndex + ((((j * width + i) << 2))); + + const colorIdx = colorBits & 0x3; + colorBits >>>= 2; + + rgba[idx + 0] = colorPalette[colorIdx * 4]; + rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; + rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; + + const bitPos = (j << 2) + i; + const byteIndex = bitPos >> 3; + const shift = (bitPos & 7) << 2; + + const alpha4 = ((byteIndex === 0 ? alpha0 : alpha1) >>> shift) & 0xF; + const alpha = alphaTable[alpha4]; + + if (premultiplied && alpha > 0 && alpha < 255) { + const factor = 255 / alpha; + rgba[idx + 0] = Math.min(255, Math.round(rgba[idx + 0] * factor)); + rgba[idx + 1] = Math.min(255, Math.round(rgba[idx + 1] * factor)); + rgba[idx + 2] = Math.min(255, Math.round(rgba[idx + 2] * factor)); } + + rgba[idx + 3] = alpha; } } } @@ -202,114 +230,130 @@ export class ImageDecoder { return rgba; } + /* + bc3 - block compression format, using for DXT4 and DXT5 + compress 4x4 block of pixels with alpha + format: + +---------------+ + | alpha0 | min alpha value. 8bit + +---------------+ + | alpha1 | max alpha value. 8bit + +---+---+---+---+ + | a | b | c | d | bc1-like alpha block but 3bit * 16 (index in alpha palette) + +---+---+---+---+ + | e | f | g | h | + +---+---+---+---+ + | i | j | k | l | + +---+---+---+---+ + | m | n | o | p | + +---+---+---+---+ + | | bc1 color compression. 8byte + | bc1 block | + | | total: 16byte in 4x4 colors + +---------------+ + + in DXT4, the color data is interpreted as being premultiplied by alpha + */ static bc3(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { const rgba = new Uint8Array(4 * width * height); + const alphaPalette = new Uint8Array(8); + const colorPalette = new Uint8Array(16); + const alphaIndices = new Uint8Array(16); let offset = 0; for (let y = 0; y < height; y += 4) { for (let x = 0; x < width; x += 4) { - const alpha0 = data[offset]; - const alpha1 = data[offset + 1]; - const alpha2 = ImageDecoder.readUInt16LE(data, offset + 2); - const alpha3 = ImageDecoder.readUInt16LE(data, offset + 4); - const alpha4 = ImageDecoder.readUInt16LE(data, offset + 6); - const color0 = ImageDecoder.readUInt16LE(data, offset + 8); - const color1 = ImageDecoder.readUInt16LE(data, offset + 10); - let bits = ImageDecoder.readUInt32LE(data, offset + 12); - offset += 16; - - const [r0, g0, b0] = ImageDecoder.decode565(color0); - const [r1, g1, b1] = ImageDecoder.decode565(color1); - - let alphas: number[]; + const alpha0 = data[offset++]; + const alpha1 = data[offset++]; + + const alphaBits = data.subarray(offset, offset + 6); + offset += 6; + + const color0 = ImageDecoder.readUInt16LE(data, offset); + const color1 = ImageDecoder.readUInt16LE(data, offset + 2); + let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); + let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); + + if (color0 > color1) { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (2 * c0r + c1r + 1) / 3; + colorPalette[9] = (2 * c0g + c1g + 1) / 3; + colorPalette[10] = (2 * c0b + c1b + 1) / 3; + colorPalette[12] = (c0r + 2 * c1r + 1) / 3; + colorPalette[13] = (c0g + 2 * c1g + 1) / 3; + colorPalette[14] = (c0b + 2 * c1b + 1) / 3; + } + else { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (c0r + c1r + 1) >> 1; + colorPalette[9] = (c0g + c1g + 1) >> 1; + colorPalette[10] = (c0b + c1b + 1) >> 1; + colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; + } + if (alpha0 > alpha1) { - alphas = [ - alpha0, - alpha1, - Math.round(alpha0 * (6 / 7) + alpha1 * (1 / 7)), - Math.round(alpha0 * (5 / 7) + alpha1 * (2 / 7)), - Math.round(alpha0 * (4 / 7) + alpha1 * (3 / 7)), - Math.round(alpha0 * (3 / 7) + alpha1 * (4 / 7)), - Math.round(alpha0 * (2 / 7) + alpha1 * (5 / 7)), - Math.round(alpha0 * (1 / 7) + alpha1 * (6 / 7)) - ]; - } else { - alphas = [ - alpha0, - alpha1, - Math.round(alpha0 * (4 / 5) + alpha1 * (1 / 5)), - Math.round(alpha0 * (3 / 5) + alpha1 * (2 / 5)), - Math.round(alpha0 * (2 / 5) + alpha1 * (3 / 5)), - Math.round(alpha0 * (1 / 5) + alpha1 * (4 / 5)), - 0, - 255 - ]; + alphaPalette[0] = alpha0; + alphaPalette[1] = alpha1; + alphaPalette[2] = (alpha0 * 6 + alpha1 * 1 + 3) / 7; + alphaPalette[3] = (alpha0 * 5 + alpha1 * 2 + 3) / 7; + alphaPalette[4] = (alpha0 * 4 + alpha1 * 3 + 3) / 7; + alphaPalette[5] = (alpha0 * 3 + alpha1 * 4 + 3) / 7; + alphaPalette[6] = (alpha0 * 2 + alpha1 * 5 + 3) / 7; + alphaPalette[7] = (alpha0 * 1 + alpha1 * 6 + 3) / 7; + } + else { + alphaPalette[0] = alpha0; + alphaPalette[1] = alpha1; + alphaPalette[2] = (alpha0 * 4 + alpha1 * 1 + 2) / 5; + alphaPalette[3] = (alpha0 * 3 + alpha1 * 2 + 2) / 5; + alphaPalette[4] = (alpha0 * 2 + alpha1 * 3 + 2) / 5; + alphaPalette[5] = (alpha0 * 1 + alpha1 * 4 + 2) / 5; + alphaPalette[6] = 0; + alphaPalette[7] = 255; + } + + for (let k = 0; k < 16; k++) { + const bitOffset = k * 3; + const byteOffset = bitOffset >> 3; + const shift = bitOffset & 7; + + if (shift <= 5) { + alphaIndices[k] = (alphaBits[byteOffset] >> shift) & 0x7; + } else { + const part1 = (alphaBits[byteOffset] >> shift) & 0x7; + const part2 = (alphaBits[byteOffset + 1] << (8 - shift)) & 0x7; + alphaIndices[k] = part1 | part2; + } } - const alphaIndices = [alpha4, alpha3, alpha2]; - - for (let j = 0; j < 4; j++) { - for (let i = 0; i < 4; i++) { - const control = bits & 3; - bits >>= 2; - - let [r, g, b] = [0, 0, 0]; - - switch (control) { - case 0: - [r, g, b] = [r0, g0, b0]; - break; - case 1: - [r, g, b] = [r1, g1, b1]; - break; - case 2: - if (color0 > color1) { - r = ImageDecoder.color2Interpolation(r0, r1); - g = ImageDecoder.color2Interpolation(g0, g1); - b = ImageDecoder.color2Interpolation(b0, b1); - } else { - r = ImageDecoder.color2Average(r0, r1); - g = ImageDecoder.color2Average(g0, g1); - b = ImageDecoder.color2Average(b0, b1); - } - break; - case 3: - if (color0 > color1) { - r = ImageDecoder.color3Interpolation(r0, r1); - g = ImageDecoder.color3Interpolation(g0, g1); - b = ImageDecoder.color3Interpolation(b0, b1); - } else { - [r, g, b] = [0, 0, 0]; - } - break; - } - - const shift = 3 * (15 - ((3 - i) + j * 4)); - const shiftS = shift % 16; - const rowS = Math.floor(shift / 16); - const rowE = Math.floor((shift + 2) / 16); - - let alphaIndex = (alphaIndices[2 - rowS] >> shiftS) & 0x7; - - if (rowS !== rowE) { - const shift_e = 16 - shiftS; - alphaIndex += (alphaIndices[2 - rowE] & ((1 << (3 - shift_e)) - 1)) << shift_e; - } - - const a = alphas[alphaIndex]; - - const idx = 4 * ((y + j) * width + (x + i)); - - if (premultiplied && a > 0) { - r = Math.min(Math.round((r * 255) / a), 255); - g = Math.min(Math.round((g * 255) / a), 255); - b = Math.min(Math.round((b * 255) / a), 255); - } - - rgba[idx + 0] = r; - rgba[idx + 1] = g; - rgba[idx + 2] = b; - rgba[idx + 3] = a; + const baseIndex = (y * width + x) << 2; + let bits = colorBits; + + for (let k = 0; k < 16; k++) { + const j = k >> 2; + const i = k & 3; + + const idx = baseIndex + ((((j * width + i) << 2))); + const colorIdx = bits & 0x3; + bits >>>= 2; + + const alpha = alphaPalette[alphaIndices[k] & 0x7]; + + rgba[idx + 0] = colorPalette[colorIdx * 4]; + rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; + rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; + rgba[idx + 3] = alpha; + + if (premultiplied && alpha > 0 && alpha < 255) { + const factor = 255 / alpha; + rgba[idx] = Math.min(255, Math.round(rgba[idx] * factor)); + rgba[idx + 1] = Math.min(255, Math.round(rgba[idx + 1] * factor)); + rgba[idx + 2] = Math.min(255, Math.round(rgba[idx + 2] * factor)); } } } From 896ec2c8baefce9fcec4c4746047831e69bafdfc Mon Sep 17 00:00:00 2001 From: AnriTool <59344539+AnriTool@users.noreply.github.com> Date: Sun, 15 Jun 2025 22:00:05 +1000 Subject: [PATCH 5/7] dxt-decompress optimization and fixes. Some refactor --- src/renderware/utils/ImageDecoder.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/renderware/utils/ImageDecoder.ts b/src/renderware/utils/ImageDecoder.ts index cf81fef..3954c06 100644 --- a/src/renderware/utils/ImageDecoder.ts +++ b/src/renderware/utils/ImageDecoder.ts @@ -119,7 +119,8 @@ export class ImageDecoder { if (color0 <= color1 && colorIdx === 3) { rgba[idx + 3] = 0; - } else { + } + else { rgba[idx + 3] = 255; } } @@ -142,7 +143,7 @@ export class ImageDecoder { +---+---+---+---+ | m | n | o | p | +---+---+---+---+ - | | bc1 collor compression. 8byte + | | bc1 color compression. 8byte | bc1 block | | | total: 16byte in 4x4 colors +---------------+ @@ -204,10 +205,6 @@ export class ImageDecoder { const colorIdx = colorBits & 0x3; colorBits >>>= 2; - rgba[idx + 0] = colorPalette[colorIdx * 4]; - rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; - rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; - const bitPos = (j << 2) + i; const byteIndex = bitPos >> 3; const shift = (bitPos & 7) << 2; @@ -215,14 +212,18 @@ export class ImageDecoder { const alpha4 = ((byteIndex === 0 ? alpha0 : alpha1) >>> shift) & 0xF; const alpha = alphaTable[alpha4]; + rgba[idx + 0] = colorPalette[colorIdx * 4]; + rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; + rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; + rgba[idx + 3] = alpha; + + if (premultiplied && alpha > 0 && alpha < 255) { const factor = 255 / alpha; rgba[idx + 0] = Math.min(255, Math.round(rgba[idx + 0] * factor)); rgba[idx + 1] = Math.min(255, Math.round(rgba[idx + 1] * factor)); rgba[idx + 2] = Math.min(255, Math.round(rgba[idx + 2] * factor)); } - - rgba[idx + 3] = alpha; } } } @@ -324,7 +325,8 @@ export class ImageDecoder { if (shift <= 5) { alphaIndices[k] = (alphaBits[byteOffset] >> shift) & 0x7; - } else { + } + else { const part1 = (alphaBits[byteOffset] >> shift) & 0x7; const part2 = (alphaBits[byteOffset + 1] << (8 - shift)) & 0x7; alphaIndices[k] = part1 | part2; From d834e66a86bc400d3597aa0fb4e676edb5a6cffe Mon Sep 17 00:00:00 2001 From: AnriTool <59344539+AnriTool@users.noreply.github.com> Date: Sun, 15 Jun 2025 22:22:47 +1000 Subject: [PATCH 6/7] dxt-decompress optimization and fixes. Pixel mismatch --- src/renderware/utils/ImageDecoder.ts | 54 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/renderware/utils/ImageDecoder.ts b/src/renderware/utils/ImageDecoder.ts index 3954c06..9c03e4e 100644 --- a/src/renderware/utils/ImageDecoder.ts +++ b/src/renderware/utils/ImageDecoder.ts @@ -88,19 +88,19 @@ export class ImageDecoder { if (color0 > color1) { colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (2 * c0r + c1r + 1) / 3; - colorPalette[9] = (2 * c0g + c1g + 1) / 3; - colorPalette[10] = (2 * c0b + c1b + 1) / 3; - colorPalette[12] = (c0r + 2 * c1r + 1) / 3; - colorPalette[13] = (c0g + 2 * c1g + 1) / 3; - colorPalette[14] = (c0b + 2 * c1b + 1) / 3; + colorPalette[8] = (2 * c0r + c1r) / 3; + colorPalette[9] = (2 * c0g + c1g) / 3; + colorPalette[10] = (2 * c0b + c1b) / 3; + colorPalette[12] = (c0r + 2 * c1r) / 3; + colorPalette[13] = (c0g + 2 * c1g) / 3; + colorPalette[14] = (c0b + 2 * c1b) / 3; } else { colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (c0r + c1r + 1) >> 1; - colorPalette[9] = (c0g + c1g + 1) >> 1; - colorPalette[10] = (c0b + c1b + 1) >> 1; + colorPalette[8] = (c0r + c1r) >> 1; + colorPalette[9] = (c0g + c1g) >> 1; + colorPalette[10] = (c0b + c1b) >> 1; colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; } @@ -178,19 +178,19 @@ export class ImageDecoder { if (color0 > color1) { colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (2 * c0r + c1r + 1) / 3; - colorPalette[9] = (2 * c0g + c1g + 1) / 3; - colorPalette[10] = (2 * c0b + c1b + 1) / 3; - colorPalette[12] = (c0r + 2 * c1r + 1) / 3; - colorPalette[13] = (c0g + 2 * c1g + 1) / 3; - colorPalette[14] = (c0b + 2 * c1b + 1) / 3; + colorPalette[8] = (2 * c0r + c1r) / 3; + colorPalette[9] = (2 * c0g + c1g) / 3; + colorPalette[10] = (2 * c0b + c1b) / 3; + colorPalette[12] = (c0r + 2 * c1r) / 3; + colorPalette[13] = (c0g + 2 * c1g) / 3; + colorPalette[14] = (c0b + 2 * c1b) / 3; } else { colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (c0r + c1r + 1) >> 1; - colorPalette[9] = (c0g + c1g + 1) >> 1; - colorPalette[10] = (c0b + c1b + 1) >> 1; + colorPalette[8] = (c0r + c1r) >> 1; + colorPalette[9] = (c0g + c1g) >> 1; + colorPalette[10] = (c0b + c1b) >> 1; colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; } @@ -281,19 +281,19 @@ export class ImageDecoder { if (color0 > color1) { colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (2 * c0r + c1r + 1) / 3; - colorPalette[9] = (2 * c0g + c1g + 1) / 3; - colorPalette[10] = (2 * c0b + c1b + 1) / 3; - colorPalette[12] = (c0r + 2 * c1r + 1) / 3; - colorPalette[13] = (c0g + 2 * c1g + 1) / 3; - colorPalette[14] = (c0b + 2 * c1b + 1) / 3; + colorPalette[8] = (2 * c0r + c1r) / 3; + colorPalette[9] = (2 * c0g + c1g) / 3; + colorPalette[10] = (2 * c0b + c1b) / 3; + colorPalette[12] = (c0r + 2 * c1r) / 3; + colorPalette[13] = (c0g + 2 * c1g) / 3; + colorPalette[14] = (c0b + 2 * c1b) / 3; } else { colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (c0r + c1r + 1) >> 1; - colorPalette[9] = (c0g + c1g + 1) >> 1; - colorPalette[10] = (c0b + c1b + 1) >> 1; + colorPalette[8] = (c0r + c1r) >> 1; + colorPalette[9] = (c0g + c1g) >> 1; + colorPalette[10] = (c0b + c1b) >> 1; colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; } From 23cf676f1d5d54a30140d2a03b29ffbe88770e38 Mon Sep 17 00:00:00 2001 From: AnriTool <59344539+AnriTool@users.noreply.github.com> Date: Sun, 15 Jun 2025 22:25:33 +1000 Subject: [PATCH 7/7] dxt-decompress optimization and fixes. Convert tabs to spaces --- src/renderware/utils/ImageDecoder.ts | 1134 +++++++++++----------- src/renderware/utils/ImageFormatEnums.ts | 34 +- 2 files changed, 584 insertions(+), 584 deletions(-) diff --git a/src/renderware/utils/ImageDecoder.ts b/src/renderware/utils/ImageDecoder.ts index 9c03e4e..ab9bd77 100644 --- a/src/renderware/utils/ImageDecoder.ts +++ b/src/renderware/utils/ImageDecoder.ts @@ -1,571 +1,571 @@ // Source: https://github.com/Parik27/DragonFF/blob/master/gtaLib/txd.py export class ImageDecoder { - static readUInt16LE(buf: Uint8Array, offset: number): number { - return buf[offset] | (buf[offset + 1] << 8); - } - - static readUInt32LE(buf: Uint8Array, offset: number): number { - return ( - buf[offset] | - (buf[offset + 1] << 8) | - (buf[offset + 2] << 16) | - (buf[offset + 3] << 24) - ); - } - - static decode565(bits: number): [number, number, number] { - const r = (bits >> 11) & 0b11111; - const g = (bits >> 5) & 0b111111; - const b = bits & 0b11111; - - return [ - (r << 3) | (r >> 2), - (g << 2) | (g >> 4), - (b << 3) | (b >> 2) - ]; - } - - static decode555(bits:number): [number, number, number] { - const r = Math.round(((bits >> 10) & 0b11111) * 255 / 31); - const g = Math.round(((bits >> 5) & 0b11111) * 255 / 31); - const b = Math.round((bits & 0b11111) * 255 / 31); - return [r, g, b]; - } - - static decode1555(bits: number): [number, number, number, number] { - const a = Math.round(((bits >> 15) & 0b1) * 255); - const r = Math.round(((bits >> 10) & 0b11111) * 255 / 31); - const g = Math.round(((bits >> 5) & 0b11111) * 255 / 31); - const b = Math.round((bits & 0b11111) * 255 / 31); - return [a, r, g, b]; - } - - static decode4444(bits: number): [number, number, number, number] { - const a = Math.round(((bits >> 12) & 0b1111) * 255 / 15); - const r = Math.round(((bits >> 8) & 0b1111) * 255 / 15); - const g = Math.round(((bits >> 4) & 0b1111) * 255 / 15); - const b = Math.round((bits & 0b1111) * 255 / 15); - return [a, r, g, b]; - } - - /* - bc1 - block compression format, using for DXT1 - compress 4x4 block of pixels - format: - +---------------+ - | color0 | color0 in palette. 16bit (RGB 565 format) - +---------------+ - | color1 | color1 in palette. 16bit (RGB 565 format) - +---+---+---+---+ - | a | b | c | d | a-p color palette index 2bit * 16 - +---+---+---+---+ - | e | f | g | h | - +---+---+---+---+ - | i | j | k | l | - +---+---+---+---+ - | m | n | o | p | total: 8byte in 4x4 colors - +---+---+---+---+ - - color2 and color3 in the palette are calculated by interpolating other colors or choosing the average between them. - color0 > color1 => interpolation, else => average - */ - static bc1(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - const colorPalette = new Uint8Array(16); - let offset = 0; - - for (let y = 0; y < height; y += 4) { - for (let x = 0; x < width; x += 4) { - const color0 = ImageDecoder.readUInt16LE(data, offset); - const color1 = ImageDecoder.readUInt16LE(data, offset + 2); - let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); - offset += 8; - - let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); - let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); - - if (color0 > color1) { - colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; - colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (2 * c0r + c1r) / 3; - colorPalette[9] = (2 * c0g + c1g) / 3; - colorPalette[10] = (2 * c0b + c1b) / 3; - colorPalette[12] = (c0r + 2 * c1r) / 3; - colorPalette[13] = (c0g + 2 * c1g) / 3; - colorPalette[14] = (c0b + 2 * c1b) / 3; - } - else { - colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; - colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (c0r + c1r) >> 1; - colorPalette[9] = (c0g + c1g) >> 1; - colorPalette[10] = (c0b + c1b) >> 1; - colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; - } - - const baseIndex = (y * width + x) * 4; - for (let k = 0; k < 16; k++) { - const colorIdx = colorBits & 0x3; - colorBits >>>= 2; - - const j = k >> 2; - const i = k & 3; - const idx = baseIndex + ((j * width + i) << 2); - - rgba[idx + 0] = colorPalette[colorIdx * 4]; - rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; - rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; - - if (color0 <= color1 && colorIdx === 3) { - rgba[idx + 3] = 0; - } - else { - rgba[idx + 3] = 255; - } - } - } - } - - return rgba; - } - - /* - bc2 - block compression format, using for DXT2 and DXT3 - compress 4x4 block of pixels with 4x4 4bit alpha - format: - +---+---+---+---+ - | a | b | c | d | a-p pixel alpha. 4bit * 16 - +---+---+---+---+ - | e | f | g | h | - +---+---+---+---+ - | i | j | k | l | - +---+---+---+---+ - | m | n | o | p | - +---+---+---+---+ - | | bc1 color compression. 8byte - | bc1 block | - | | total: 16byte in 4x4 colors - +---------------+ - - in DXT2, the color data is interpreted as being premultiplied by alpha - */ - static bc2(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - const colorPalette = new Uint8Array(16); - - const alphaTable = new Uint8Array(16); - for (let i = 0; i < 16; i++) { - alphaTable[i] = (i * 255 + 7.5) / 15 | 0; - } - - let offset = 0; - - for (let y = 0; y < height; y += 4) { - for (let x = 0; x < width; x += 4) { - const alpha0 = ImageDecoder.readUInt32LE(data, offset); - const alpha1 = ImageDecoder.readUInt32LE(data, offset + 4); - offset += 8; - - const color0 = ImageDecoder.readUInt16LE(data, offset); - const color1 = ImageDecoder.readUInt16LE(data, offset + 2); - let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); - offset += 8; - - let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); - let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); - - if (color0 > color1) { - colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; - colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (2 * c0r + c1r) / 3; - colorPalette[9] = (2 * c0g + c1g) / 3; - colorPalette[10] = (2 * c0b + c1b) / 3; - colorPalette[12] = (c0r + 2 * c1r) / 3; - colorPalette[13] = (c0g + 2 * c1g) / 3; - colorPalette[14] = (c0b + 2 * c1b) / 3; - } - else { - colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; - colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (c0r + c1r) >> 1; - colorPalette[9] = (c0g + c1g) >> 1; - colorPalette[10] = (c0b + c1b) >> 1; - colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; - } - - const baseIndex = ((y * width + x) << 2); - - for (let k = 0; k < 16; k++) { - const j = k >> 2; - const i = k & 3; - - const idx = baseIndex + ((((j * width + i) << 2))); - - const colorIdx = colorBits & 0x3; - colorBits >>>= 2; - - const bitPos = (j << 2) + i; - const byteIndex = bitPos >> 3; - const shift = (bitPos & 7) << 2; - - const alpha4 = ((byteIndex === 0 ? alpha0 : alpha1) >>> shift) & 0xF; - const alpha = alphaTable[alpha4]; - - rgba[idx + 0] = colorPalette[colorIdx * 4]; - rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; - rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; - rgba[idx + 3] = alpha; - - - if (premultiplied && alpha > 0 && alpha < 255) { - const factor = 255 / alpha; - rgba[idx + 0] = Math.min(255, Math.round(rgba[idx + 0] * factor)); - rgba[idx + 1] = Math.min(255, Math.round(rgba[idx + 1] * factor)); - rgba[idx + 2] = Math.min(255, Math.round(rgba[idx + 2] * factor)); - } - } - } - } - - return rgba; - } - - /* - bc3 - block compression format, using for DXT4 and DXT5 - compress 4x4 block of pixels with alpha - format: - +---------------+ - | alpha0 | min alpha value. 8bit - +---------------+ - | alpha1 | max alpha value. 8bit - +---+---+---+---+ - | a | b | c | d | bc1-like alpha block but 3bit * 16 (index in alpha palette) - +---+---+---+---+ - | e | f | g | h | - +---+---+---+---+ - | i | j | k | l | - +---+---+---+---+ - | m | n | o | p | - +---+---+---+---+ - | | bc1 color compression. 8byte - | bc1 block | - | | total: 16byte in 4x4 colors - +---------------+ - - in DXT4, the color data is interpreted as being premultiplied by alpha - */ - static bc3(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - const alphaPalette = new Uint8Array(8); - const colorPalette = new Uint8Array(16); - const alphaIndices = new Uint8Array(16); - let offset = 0; - - for (let y = 0; y < height; y += 4) { - for (let x = 0; x < width; x += 4) { - const alpha0 = data[offset++]; - const alpha1 = data[offset++]; - - const alphaBits = data.subarray(offset, offset + 6); - offset += 6; - - const color0 = ImageDecoder.readUInt16LE(data, offset); - const color1 = ImageDecoder.readUInt16LE(data, offset + 2); - let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); - offset += 8; - - let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); - let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); - - if (color0 > color1) { - colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; - colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (2 * c0r + c1r) / 3; - colorPalette[9] = (2 * c0g + c1g) / 3; - colorPalette[10] = (2 * c0b + c1b) / 3; - colorPalette[12] = (c0r + 2 * c1r) / 3; - colorPalette[13] = (c0g + 2 * c1g) / 3; - colorPalette[14] = (c0b + 2 * c1b) / 3; - } - else { - colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; - colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; - colorPalette[8] = (c0r + c1r) >> 1; - colorPalette[9] = (c0g + c1g) >> 1; - colorPalette[10] = (c0b + c1b) >> 1; - colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; - } - - if (alpha0 > alpha1) { - alphaPalette[0] = alpha0; - alphaPalette[1] = alpha1; - alphaPalette[2] = (alpha0 * 6 + alpha1 * 1 + 3) / 7; - alphaPalette[3] = (alpha0 * 5 + alpha1 * 2 + 3) / 7; - alphaPalette[4] = (alpha0 * 4 + alpha1 * 3 + 3) / 7; - alphaPalette[5] = (alpha0 * 3 + alpha1 * 4 + 3) / 7; - alphaPalette[6] = (alpha0 * 2 + alpha1 * 5 + 3) / 7; - alphaPalette[7] = (alpha0 * 1 + alpha1 * 6 + 3) / 7; - } - else { - alphaPalette[0] = alpha0; - alphaPalette[1] = alpha1; - alphaPalette[2] = (alpha0 * 4 + alpha1 * 1 + 2) / 5; - alphaPalette[3] = (alpha0 * 3 + alpha1 * 2 + 2) / 5; - alphaPalette[4] = (alpha0 * 2 + alpha1 * 3 + 2) / 5; - alphaPalette[5] = (alpha0 * 1 + alpha1 * 4 + 2) / 5; - alphaPalette[6] = 0; - alphaPalette[7] = 255; - } - - for (let k = 0; k < 16; k++) { - const bitOffset = k * 3; - const byteOffset = bitOffset >> 3; - const shift = bitOffset & 7; - - if (shift <= 5) { - alphaIndices[k] = (alphaBits[byteOffset] >> shift) & 0x7; - } - else { - const part1 = (alphaBits[byteOffset] >> shift) & 0x7; - const part2 = (alphaBits[byteOffset + 1] << (8 - shift)) & 0x7; - alphaIndices[k] = part1 | part2; - } - } - - const baseIndex = (y * width + x) << 2; - let bits = colorBits; - - for (let k = 0; k < 16; k++) { - const j = k >> 2; - const i = k & 3; - - const idx = baseIndex + ((((j * width + i) << 2))); - const colorIdx = bits & 0x3; - bits >>>= 2; - - const alpha = alphaPalette[alphaIndices[k] & 0x7]; - - rgba[idx + 0] = colorPalette[colorIdx * 4]; - rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; - rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; - rgba[idx + 3] = alpha; - - if (premultiplied && alpha > 0 && alpha < 255) { - const factor = 255 / alpha; - rgba[idx] = Math.min(255, Math.round(rgba[idx] * factor)); - rgba[idx + 1] = Math.min(255, Math.round(rgba[idx + 1] * factor)); - rgba[idx + 2] = Math.min(255, Math.round(rgba[idx + 2] * factor)); - } - } - } - } - - return rgba; - } - - static bgra1555(data: Uint8Array, width: number, height: number): Uint8Array { - const rbga = new Uint8Array(4 * width * height); - let offset = 0; - - for (let i = 0; i < data.length; i += 2) { - const color = ImageDecoder.readUInt16LE(data, i); - const [a, r, g, b] = ImageDecoder.decode1555(color); - - rbga[offset++] = r; - rbga[offset++] = g; - rbga[offset++] = b; - rbga[offset++] = a; - } - - return rbga; - } - - static bgra4444(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - let offset = 0; - - for (let i = 0; i < data.length; i += 2) { - const color = ImageDecoder.readUInt16LE(data, i); - const [a, r, g, b] = ImageDecoder.decode4444(color); - - rgba[offset++] = r; - rgba[offset++] = g; - rgba[offset++] = b; - rgba[offset++] = a; - } - - return rgba; - } - - static bgra555(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - let offset = 0; - - for (let i = 0; i < data.length; i += 2) { - const color = ImageDecoder.readUInt16LE(data, i); - const [r, g, b] = ImageDecoder.decode555(color); - - rgba[offset++] = r; - rgba[offset++] = g; - rgba[offset++] = b; - rgba[offset++] = 0xff; - } - - return rgba; - } - - static bgra565(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - let offset = 0; - - for (let i = 0; i < data.length; i += 2) { - const color = ImageDecoder.readUInt16LE(data, i); - const [r, g, b] = ImageDecoder.decode565(color); - - rgba[offset++] = r; - rgba[offset++] = g; - rgba[offset++] = b; - rgba[offset++] = 0xff; - } - - return rgba; - } - - static bgra888(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - for (let i = 0; i < data.length; i += 4) { - rgba[i + 0] = data[i + 2]; - rgba[i + 1] = data[i + 1]; - rgba[i + 2] = data[i + 0]; - rgba[i + 3] = 0xff; - } - - return rgba; - } - - static bgra8888(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - for (let i = 0; i < data.length; i += 4) { - rgba[i + 0] = data[i + 2]; - rgba[i + 1] = data[i + 1]; - rgba[i + 2] = data[i + 0]; - rgba[i + 3] = data[i + 3]; - } - - return rgba; - } - - static lum8(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - - for (let i = 0; i < data.length; i++) { - const offset = i * 4; - const luminance = data[i]; - rgba[offset + 0] = luminance; // R - rgba[offset + 1] = luminance; // G - rgba[offset + 2] = luminance; // B - rgba[offset + 3] = 0xff; - } - - return rgba; - } - - static lum8a8(data: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - let offset = 0; - - for (let i = 0; i < data.length; i += 2) { - const luminance = data[i]; - const alpha = data[i + 1]; - - rgba[offset++] = luminance; - rgba[offset++] = luminance; - rgba[offset++] = luminance; - rgba[offset++] = alpha; - } - - return rgba; - } - - static pal4(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - let offset = 0; - - for (let i = 0; i < data.length; i++) { - const b = data[i]; - const idx1 = (b >> 4) & 0xf; - const idx2 = b & 0xf; - - // Copying RGBA from the palette for two pixels - rgba[offset++] = palette[idx1 * 4 + 0]; // R - rgba[offset++] = palette[idx1 * 4 + 1]; // G - rgba[offset++] = palette[idx1 * 4 + 2]; // B - rgba[offset++] = palette[idx1 * 4 + 3]; // A - - rgba[offset++] = palette[idx2 * 4 + 0]; // R - rgba[offset++] = palette[idx2 * 4 + 1]; // G - rgba[offset++] = palette[idx2 * 4 + 2]; // B - rgba[offset++] = palette[idx2 * 4 + 3]; // A - } - - return rgba; - } - - static pal4NoAlpha(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - let offset = 0; - - for (let i = 0; i < data.length; i++) { - const b = data[i]; - const colorIndex1 = (b >> 4) & 0xf; - const colorIndex2 = b & 0xf; - - // First pixel - rgba[offset++] = palette[colorIndex1 * 4 + 0]; // R - rgba[offset++] = palette[colorIndex1 * 4 + 1]; // G - rgba[offset++] = palette[colorIndex1 * 4 + 2]; // B - rgba[offset++] = 0xff; - - // Second pixel - rgba[offset++] = palette[colorIndex2 * 4 + 0]; // R - rgba[offset++] = palette[colorIndex2 * 4 + 1]; // G - rgba[offset++] = palette[colorIndex2 * 4 + 2]; // B - rgba[offset++] = 0xff; - } - - return rgba; - } - - static pal8(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - - for (let i = 0; i < data.length; i++) { - const colorIndex = data[i]; - - // Copy RGBA from palette - rgba[i * 4 + 0] = palette[colorIndex * 4 + 0]; // R - rgba[i * 4 + 1] = palette[colorIndex * 4 + 1]; // G - rgba[i * 4 + 2] = palette[colorIndex * 4 + 2]; // B - rgba[i * 4 + 3] = palette[colorIndex * 4 + 3]; // A - } - - return rgba; - } - - static pal8NoAlpha(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { - const rgba = new Uint8Array(4 * width * height); - - for (let i = 0; i < data.length; i++) { - const colorIndex = data[i]; - - // Copy RGB from palette - rgba[i * 4 + 0] = palette[colorIndex * 4 + 0]; // R - rgba[i * 4 + 1] = palette[colorIndex * 4 + 1]; // G - rgba[i * 4 + 2] = palette[colorIndex * 4 + 2]; // B - rgba[i * 4 + 3] = 0xff; - } - - return rgba; - } + static readUInt16LE(buf: Uint8Array, offset: number): number { + return buf[offset] | (buf[offset + 1] << 8); + } + + static readUInt32LE(buf: Uint8Array, offset: number): number { + return ( + buf[offset] | + (buf[offset + 1] << 8) | + (buf[offset + 2] << 16) | + (buf[offset + 3] << 24) + ); + } + + static decode565(bits: number): [number, number, number] { + const r = (bits >> 11) & 0b11111; + const g = (bits >> 5) & 0b111111; + const b = bits & 0b11111; + + return [ + (r << 3) | (r >> 2), + (g << 2) | (g >> 4), + (b << 3) | (b >> 2) + ]; + } + + static decode555(bits:number): [number, number, number] { + const r = Math.round(((bits >> 10) & 0b11111) * 255 / 31); + const g = Math.round(((bits >> 5) & 0b11111) * 255 / 31); + const b = Math.round((bits & 0b11111) * 255 / 31); + return [r, g, b]; + } + + static decode1555(bits: number): [number, number, number, number] { + const a = Math.round(((bits >> 15) & 0b1) * 255); + const r = Math.round(((bits >> 10) & 0b11111) * 255 / 31); + const g = Math.round(((bits >> 5) & 0b11111) * 255 / 31); + const b = Math.round((bits & 0b11111) * 255 / 31); + return [a, r, g, b]; + } + + static decode4444(bits: number): [number, number, number, number] { + const a = Math.round(((bits >> 12) & 0b1111) * 255 / 15); + const r = Math.round(((bits >> 8) & 0b1111) * 255 / 15); + const g = Math.round(((bits >> 4) & 0b1111) * 255 / 15); + const b = Math.round((bits & 0b1111) * 255 / 15); + return [a, r, g, b]; + } + + /* + bc1 - block compression format, using for DXT1 + compress 4x4 block of pixels + format: + +---------------+ + | color0 | color0 in palette. 16bit (RGB 565 format) + +---------------+ + | color1 | color1 in palette. 16bit (RGB 565 format) + +---+---+---+---+ + | a | b | c | d | a-p color palette index 2bit * 16 + +---+---+---+---+ + | e | f | g | h | + +---+---+---+---+ + | i | j | k | l | + +---+---+---+---+ + | m | n | o | p | total: 8byte in 4x4 colors + +---+---+---+---+ + + color2 and color3 in the palette are calculated by interpolating other colors or choosing the average between them. + color0 > color1 => interpolation, else => average + */ + static bc1(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + const colorPalette = new Uint8Array(16); + let offset = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const color0 = ImageDecoder.readUInt16LE(data, offset); + const color1 = ImageDecoder.readUInt16LE(data, offset + 2); + let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); + let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); + + if (color0 > color1) { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (2 * c0r + c1r) / 3; + colorPalette[9] = (2 * c0g + c1g) / 3; + colorPalette[10] = (2 * c0b + c1b) / 3; + colorPalette[12] = (c0r + 2 * c1r) / 3; + colorPalette[13] = (c0g + 2 * c1g) / 3; + colorPalette[14] = (c0b + 2 * c1b) / 3; + } + else { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (c0r + c1r) >> 1; + colorPalette[9] = (c0g + c1g) >> 1; + colorPalette[10] = (c0b + c1b) >> 1; + colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; + } + + const baseIndex = (y * width + x) * 4; + for (let k = 0; k < 16; k++) { + const colorIdx = colorBits & 0x3; + colorBits >>>= 2; + + const j = k >> 2; + const i = k & 3; + const idx = baseIndex + ((j * width + i) << 2); + + rgba[idx + 0] = colorPalette[colorIdx * 4]; + rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; + rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; + + if (color0 <= color1 && colorIdx === 3) { + rgba[idx + 3] = 0; + } + else { + rgba[idx + 3] = 255; + } + } + } + } + + return rgba; + } + + /* + bc2 - block compression format, using for DXT2 and DXT3 + compress 4x4 block of pixels with 4x4 4bit alpha + format: + +---+---+---+---+ + | a | b | c | d | a-p pixel alpha. 4bit * 16 + +---+---+---+---+ + | e | f | g | h | + +---+---+---+---+ + | i | j | k | l | + +---+---+---+---+ + | m | n | o | p | + +---+---+---+---+ + | | bc1 color compression. 8byte + | bc1 block | + | | total: 16byte in 4x4 colors + +---------------+ + + in DXT2, the color data is interpreted as being premultiplied by alpha + */ + static bc2(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + const colorPalette = new Uint8Array(16); + + const alphaTable = new Uint8Array(16); + for (let i = 0; i < 16; i++) { + alphaTable[i] = (i * 255 + 7.5) / 15 | 0; + } + + let offset = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const alpha0 = ImageDecoder.readUInt32LE(data, offset); + const alpha1 = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + const color0 = ImageDecoder.readUInt16LE(data, offset); + const color1 = ImageDecoder.readUInt16LE(data, offset + 2); + let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); + let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); + + if (color0 > color1) { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (2 * c0r + c1r) / 3; + colorPalette[9] = (2 * c0g + c1g) / 3; + colorPalette[10] = (2 * c0b + c1b) / 3; + colorPalette[12] = (c0r + 2 * c1r) / 3; + colorPalette[13] = (c0g + 2 * c1g) / 3; + colorPalette[14] = (c0b + 2 * c1b) / 3; + } + else { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (c0r + c1r) >> 1; + colorPalette[9] = (c0g + c1g) >> 1; + colorPalette[10] = (c0b + c1b) >> 1; + colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; + } + + const baseIndex = ((y * width + x) << 2); + + for (let k = 0; k < 16; k++) { + const j = k >> 2; + const i = k & 3; + + const idx = baseIndex + ((((j * width + i) << 2))); + + const colorIdx = colorBits & 0x3; + colorBits >>>= 2; + + const bitPos = (j << 2) + i; + const byteIndex = bitPos >> 3; + const shift = (bitPos & 7) << 2; + + const alpha4 = ((byteIndex === 0 ? alpha0 : alpha1) >>> shift) & 0xF; + const alpha = alphaTable[alpha4]; + + rgba[idx + 0] = colorPalette[colorIdx * 4]; + rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; + rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; + rgba[idx + 3] = alpha; + + + if (premultiplied && alpha > 0 && alpha < 255) { + const factor = 255 / alpha; + rgba[idx + 0] = Math.min(255, Math.round(rgba[idx + 0] * factor)); + rgba[idx + 1] = Math.min(255, Math.round(rgba[idx + 1] * factor)); + rgba[idx + 2] = Math.min(255, Math.round(rgba[idx + 2] * factor)); + } + } + } + } + + return rgba; + } + + /* + bc3 - block compression format, using for DXT4 and DXT5 + compress 4x4 block of pixels with alpha + format: + +---------------+ + | alpha0 | min alpha value. 8bit + +---------------+ + | alpha1 | max alpha value. 8bit + +---+---+---+---+ + | a | b | c | d | bc1-like alpha block but 3bit * 16 (index in alpha palette) + +---+---+---+---+ + | e | f | g | h | + +---+---+---+---+ + | i | j | k | l | + +---+---+---+---+ + | m | n | o | p | + +---+---+---+---+ + | | bc1 color compression. 8byte + | bc1 block | + | | total: 16byte in 4x4 colors + +---------------+ + + in DXT4, the color data is interpreted as being premultiplied by alpha + */ + static bc3(data: Uint8Array, width: number, height: number, premultiplied: boolean): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + const alphaPalette = new Uint8Array(8); + const colorPalette = new Uint8Array(16); + const alphaIndices = new Uint8Array(16); + let offset = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const alpha0 = data[offset++]; + const alpha1 = data[offset++]; + + const alphaBits = data.subarray(offset, offset + 6); + offset += 6; + + const color0 = ImageDecoder.readUInt16LE(data, offset); + const color1 = ImageDecoder.readUInt16LE(data, offset + 2); + let colorBits = ImageDecoder.readUInt32LE(data, offset + 4); + offset += 8; + + let [c0r, c0g, c0b] = ImageDecoder.decode565(color0); + let [c1r, c1g, c1b] = ImageDecoder.decode565(color1); + + if (color0 > color1) { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (2 * c0r + c1r) / 3; + colorPalette[9] = (2 * c0g + c1g) / 3; + colorPalette[10] = (2 * c0b + c1b) / 3; + colorPalette[12] = (c0r + 2 * c1r) / 3; + colorPalette[13] = (c0g + 2 * c1g) / 3; + colorPalette[14] = (c0b + 2 * c1b) / 3; + } + else { + colorPalette[0] = c0r; colorPalette[1] = c0g; colorPalette[2] = c0b; + colorPalette[4] = c1r; colorPalette[5] = c1g; colorPalette[6] = c1b; + colorPalette[8] = (c0r + c1r) >> 1; + colorPalette[9] = (c0g + c1g) >> 1; + colorPalette[10] = (c0b + c1b) >> 1; + colorPalette[12] = 0; colorPalette[13] = 0; colorPalette[14] = 0; + } + + if (alpha0 > alpha1) { + alphaPalette[0] = alpha0; + alphaPalette[1] = alpha1; + alphaPalette[2] = (alpha0 * 6 + alpha1 * 1 + 3) / 7; + alphaPalette[3] = (alpha0 * 5 + alpha1 * 2 + 3) / 7; + alphaPalette[4] = (alpha0 * 4 + alpha1 * 3 + 3) / 7; + alphaPalette[5] = (alpha0 * 3 + alpha1 * 4 + 3) / 7; + alphaPalette[6] = (alpha0 * 2 + alpha1 * 5 + 3) / 7; + alphaPalette[7] = (alpha0 * 1 + alpha1 * 6 + 3) / 7; + } + else { + alphaPalette[0] = alpha0; + alphaPalette[1] = alpha1; + alphaPalette[2] = (alpha0 * 4 + alpha1 * 1 + 2) / 5; + alphaPalette[3] = (alpha0 * 3 + alpha1 * 2 + 2) / 5; + alphaPalette[4] = (alpha0 * 2 + alpha1 * 3 + 2) / 5; + alphaPalette[5] = (alpha0 * 1 + alpha1 * 4 + 2) / 5; + alphaPalette[6] = 0; + alphaPalette[7] = 255; + } + + for (let k = 0; k < 16; k++) { + const bitOffset = k * 3; + const byteOffset = bitOffset >> 3; + const shift = bitOffset & 7; + + if (shift <= 5) { + alphaIndices[k] = (alphaBits[byteOffset] >> shift) & 0x7; + } + else { + const part1 = (alphaBits[byteOffset] >> shift) & 0x7; + const part2 = (alphaBits[byteOffset + 1] << (8 - shift)) & 0x7; + alphaIndices[k] = part1 | part2; + } + } + + const baseIndex = (y * width + x) << 2; + let bits = colorBits; + + for (let k = 0; k < 16; k++) { + const j = k >> 2; + const i = k & 3; + + const idx = baseIndex + ((((j * width + i) << 2))); + const colorIdx = bits & 0x3; + bits >>>= 2; + + const alpha = alphaPalette[alphaIndices[k] & 0x7]; + + rgba[idx + 0] = colorPalette[colorIdx * 4]; + rgba[idx + 1] = colorPalette[colorIdx * 4 + 1]; + rgba[idx + 2] = colorPalette[colorIdx * 4 + 2]; + rgba[idx + 3] = alpha; + + if (premultiplied && alpha > 0 && alpha < 255) { + const factor = 255 / alpha; + rgba[idx] = Math.min(255, Math.round(rgba[idx] * factor)); + rgba[idx + 1] = Math.min(255, Math.round(rgba[idx + 1] * factor)); + rgba[idx + 2] = Math.min(255, Math.round(rgba[idx + 2] * factor)); + } + } + } + } + + return rgba; + } + + static bgra1555(data: Uint8Array, width: number, height: number): Uint8Array { + const rbga = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [a, r, g, b] = ImageDecoder.decode1555(color); + + rbga[offset++] = r; + rbga[offset++] = g; + rbga[offset++] = b; + rbga[offset++] = a; + } + + return rbga; + } + + static bgra4444(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [a, r, g, b] = ImageDecoder.decode4444(color); + + rgba[offset++] = r; + rgba[offset++] = g; + rgba[offset++] = b; + rgba[offset++] = a; + } + + return rgba; + } + + static bgra555(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [r, g, b] = ImageDecoder.decode555(color); + + rgba[offset++] = r; + rgba[offset++] = g; + rgba[offset++] = b; + rgba[offset++] = 0xff; + } + + return rgba; + } + + static bgra565(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const color = ImageDecoder.readUInt16LE(data, i); + const [r, g, b] = ImageDecoder.decode565(color); + + rgba[offset++] = r; + rgba[offset++] = g; + rgba[offset++] = b; + rgba[offset++] = 0xff; + } + + return rgba; + } + + static bgra888(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + for (let i = 0; i < data.length; i += 4) { + rgba[i + 0] = data[i + 2]; + rgba[i + 1] = data[i + 1]; + rgba[i + 2] = data[i + 0]; + rgba[i + 3] = 0xff; + } + + return rgba; + } + + static bgra8888(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + for (let i = 0; i < data.length; i += 4) { + rgba[i + 0] = data[i + 2]; + rgba[i + 1] = data[i + 1]; + rgba[i + 2] = data[i + 0]; + rgba[i + 3] = data[i + 3]; + } + + return rgba; + } + + static lum8(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + + for (let i = 0; i < data.length; i++) { + const offset = i * 4; + const luminance = data[i]; + rgba[offset + 0] = luminance; // R + rgba[offset + 1] = luminance; // G + rgba[offset + 2] = luminance; // B + rgba[offset + 3] = 0xff; + } + + return rgba; + } + + static lum8a8(data: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i += 2) { + const luminance = data[i]; + const alpha = data[i + 1]; + + rgba[offset++] = luminance; + rgba[offset++] = luminance; + rgba[offset++] = luminance; + rgba[offset++] = alpha; + } + + return rgba; + } + + static pal4(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i++) { + const b = data[i]; + const idx1 = (b >> 4) & 0xf; + const idx2 = b & 0xf; + + // Copying RGBA from the palette for two pixels + rgba[offset++] = palette[idx1 * 4 + 0]; // R + rgba[offset++] = palette[idx1 * 4 + 1]; // G + rgba[offset++] = palette[idx1 * 4 + 2]; // B + rgba[offset++] = palette[idx1 * 4 + 3]; // A + + rgba[offset++] = palette[idx2 * 4 + 0]; // R + rgba[offset++] = palette[idx2 * 4 + 1]; // G + rgba[offset++] = palette[idx2 * 4 + 2]; // B + rgba[offset++] = palette[idx2 * 4 + 3]; // A + } + + return rgba; + } + + static pal4NoAlpha(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + let offset = 0; + + for (let i = 0; i < data.length; i++) { + const b = data[i]; + const colorIndex1 = (b >> 4) & 0xf; + const colorIndex2 = b & 0xf; + + // First pixel + rgba[offset++] = palette[colorIndex1 * 4 + 0]; // R + rgba[offset++] = palette[colorIndex1 * 4 + 1]; // G + rgba[offset++] = palette[colorIndex1 * 4 + 2]; // B + rgba[offset++] = 0xff; + + // Second pixel + rgba[offset++] = palette[colorIndex2 * 4 + 0]; // R + rgba[offset++] = palette[colorIndex2 * 4 + 1]; // G + rgba[offset++] = palette[colorIndex2 * 4 + 2]; // B + rgba[offset++] = 0xff; + } + + return rgba; + } + + static pal8(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + + for (let i = 0; i < data.length; i++) { + const colorIndex = data[i]; + + // Copy RGBA from palette + rgba[i * 4 + 0] = palette[colorIndex * 4 + 0]; // R + rgba[i * 4 + 1] = palette[colorIndex * 4 + 1]; // G + rgba[i * 4 + 2] = palette[colorIndex * 4 + 2]; // B + rgba[i * 4 + 3] = palette[colorIndex * 4 + 3]; // A + } + + return rgba; + } + + static pal8NoAlpha(data: Uint8Array, palette: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(4 * width * height); + + for (let i = 0; i < data.length; i++) { + const colorIndex = data[i]; + + // Copy RGB from palette + rgba[i * 4 + 0] = palette[colorIndex * 4 + 0]; // R + rgba[i * 4 + 1] = palette[colorIndex * 4 + 1]; // G + rgba[i * 4 + 2] = palette[colorIndex * 4 + 2]; // B + rgba[i * 4 + 3] = 0xff; + } + + return rgba; + } } \ No newline at end of file diff --git a/src/renderware/utils/ImageFormatEnums.ts b/src/renderware/utils/ImageFormatEnums.ts index dbb362a..c2c983e 100644 --- a/src/renderware/utils/ImageFormatEnums.ts +++ b/src/renderware/utils/ImageFormatEnums.ts @@ -1,28 +1,28 @@ export enum RasterFormat { - RASTER_1555 = 0x0100, - RASTER_565 = 0x0200, - RASTER_4444 = 0x0300, - RASTER_LUM = 0x0400, - RASTER_8888 = 0x0500, - RASTER_888 = 0x0600, - RASTER_555 = 0x0a00, + RASTER_1555 = 0x0100, + RASTER_565 = 0x0200, + RASTER_4444 = 0x0300, + RASTER_LUM = 0x0400, + RASTER_8888 = 0x0500, + RASTER_888 = 0x0600, + RASTER_555 = 0x0a00, } export enum D3DFormat { - D3DFMT_A8L8 = "3", - D3D_DXT1 = "DXT1", - D3D_DXT2 = "DXT2", - D3D_DXT3 = "DXT3", - D3D_DXT4 = "DXT4", - D3D_DXT5 = "DXT5", + D3DFMT_A8L8 = "3", + D3D_DXT1 = "DXT1", + D3D_DXT2 = "DXT2", + D3D_DXT3 = "DXT3", + D3D_DXT4 = "DXT4", + D3D_DXT5 = "DXT5", } export enum PaletteType { - PALETTE_NONE = 0, - PALETTE_8 = 1, + PALETTE_NONE = 0, + PALETTE_8 = 1, } export enum PlatformType { - D3D8 = 0x8, - D3D9 = 0x9, + D3D8 = 0x8, + D3D9 = 0x9, } \ No newline at end of file