feat: dds decompression

This commit is contained in:
TrèsAbhi committed 2023-12-25 20:50:19 -06:00
1 parent 6361b09129
commit 4ffb7cac7c
6 files changed
+164 -17

No files matched your search

+2 -2
View File
@@ -519,9 +519,9 @@ if (allTargets || targets?.includes('tankModels')) {
const nationVehicleId = tank.id; const nationVehicleId = tank.id;
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
// if (id !== 15697) continue; // chieftain TODO: investigate vertices stream over read if (id !== 15697) continue; // chieftain TODO: investigate vertices stream over read
// if (id !== 4417) continue; // amx m4 mle // if (id !== 4417) continue; // amx m4 mle
if (id !== 7297) continue; // 60tp // if (id !== 7297) continue; // 60tp
console.log(`Building model ${id} @ ${nation}/${tankIndex}`); console.log(`Building model ${id} @ ${nation}/${tankIndex}`);
const parameters = await readYAMLDVPL<TankParameters>( const parameters = await readYAMLDVPL<TankParameters>(
+75
View File
@@ -0,0 +1,75 @@
import { times } from 'lodash';
import { Vector4Tuple } from 'three';
import { BinaryStream } from './binary';
const MAX_UINT5 = 2 ** 5 - 1;
const MAX_UINT6 = 2 ** 6 - 1;
type InterpolationKey = 0b00 | 0b01 | 0b10 | 0b11;
export class Bc1Stream extends BinaryStream {
color() {
const r = this.consume(5) / MAX_UINT5;
const g = this.consume(6) / MAX_UINT6;
const b = this.consume(5) / MAX_UINT5;
return [r, g, b, 1];
}
interpolation() {
const color0 = this.color();
const color1 = this.color();
const color2 = color0.map(
(channel0, index) => (channel0 + color1[index]) / 2,
);
const color3 = [...color2.slice(0, -1), 0];
return {
[0b00]: color0,
[0b01]: color1,
[0b10]: color2,
[0b11]: color3,
};
}
blockFloat() {
const interpolation = this.interpolation();
return times(16, () => interpolation[this.consume(2) as InterpolationKey]);
}
block() {
return this.blockFloat().map(
(pixel) =>
pixel.map((channel) => Math.round(channel * 255)) as Vector4Tuple,
);
}
blocks(count: number) {
return times(count, () => this.block());
}
bc1(width: number, height: number) {
if (width % 4 !== 0 || height % 4 !== 0) {
throw new RangeError('Width and height must be divisible by 4');
}
const count = (width / 4) * (height / 4);
const blocks = this.blocks(count);
const image = Buffer.alloc(width * height * 4);
let pixelIndex = 0;
for (let y = 0; y < width; y++) {
const yOffset = Math.floor(y / 4) * (width / 4);
for (let x = 0; x < height; x++) {
const xOffset = Math.floor(x / 4);
const block = blocks[yOffset + xOffset];
const pixel = block[Math.floor(y % 4) * 4 + Math.floor(x % 4)];
pixel.forEach((channel) => (image[pixelIndex++] = channel));
}
}
return image;
}
}
+35
View File
@@ -0,0 +1,35 @@
type Bit = 0 | 1;
export class BinaryStream {
public index = 0;
bits: Bit[] = [];
constructor(buffer: Buffer) {
for (let bufferIndex = 0; bufferIndex < buffer.length; bufferIndex++) {
const byte = buffer[bufferIndex];
for (let byteIndex = 7; byteIndex >= 0; byteIndex--) {
this.bits.push(((byte >> byteIndex) & 1) as Bit);
}
}
}
skip(size: number) {
this.index += size;
}
read(size: number) {
if (this.index + size > this.bits.length) {
throw new RangeError('Cannot read beyond end of stream');
}
return this.bits
.slice(this.index, this.index + size)
.reduce((accumulator, bit) => (accumulator << 1) | bit, 0 as number);
}
consume(size: number) {
const value = this.read(size);
this.skip(size);
return value;
}
}
+47 -10
View File
@@ -1,5 +1,8 @@
import { writeFileSync } from 'fs';
import { times } from 'lodash'; import { times } from 'lodash';
import sharp from 'sharp';
import { readDVPLFile } from '../blitz/readDVPLFile'; import { readDVPLFile } from '../blitz/readDVPLFile';
import { Bc1Stream } from './bc1';
import { WindowsStream } from './windows'; import { WindowsStream } from './windows';
enum DxgiFormat { enum DxgiFormat {
@@ -127,7 +130,7 @@ enum DxgiFormat {
FORCE_UINT = 0xffffffff, FORCE_UINT = 0xffffffff,
} }
enum D3d10ResourceDimension { enum DdsDimension {
UNKNOWN = 0, UNKNOWN = 0,
BUFFER = 1, BUFFER = 1,
TEXTURE1D = 2, TEXTURE1D = 2,
@@ -172,13 +175,48 @@ enum DdsCaps2 {
} }
export class DdsStream extends WindowsStream { export class DdsStream extends WindowsStream {
dds() { async dds() {
const magicNumber = this.magicNumber(); this.magicNumber();
const header = this.header(); const header = this.header();
const headerDxt10 =
header.pf.flags === DdpfFlags.FOURCC && header.pf.fourCC === 0x30315844 if (
? this.headerDxt10() header.pf.flags !== DdpfFlags.FOURCC ||
: undefined; header.pf.fourCC !== 0x30315844 // "DX10"
) {
throw new RangeError('No other formats other than DX10 are supported');
}
const headerDxt10 = this.headerDxt10();
switch (headerDxt10.dxgiFormat) {
case DxgiFormat.BC1_UNORM_SRGB: {
const image = new Bc1Stream(
this.consume((header.width / 4) * (header.height / 4) * 8),
).bc1(header.width, header.height);
writeFileSync(
'test.png',
await sharp(image, {
raw: {
channels: 4,
width: header.width,
height: header.height,
},
})
.png()
.toBuffer(),
);
break;
}
default:
throw new TypeError(
`Unsupported DXGI format: ${DxgiFormat[headerDxt10.dxgiFormat]} (${
headerDxt10.dxgiFormat
})`,
);
}
} }
magicNumber() { magicNumber() {
@@ -201,7 +239,6 @@ export class DdsStream extends WindowsStream {
caps3: this.dword(), caps3: this.dword(),
caps4: this.dword(), caps4: this.dword(),
reserved2: times(1, () => this.dword()), reserved2: times(1, () => this.dword()),
headerDxt10: undefined,
}; };
} }
@@ -221,7 +258,7 @@ export class DdsStream extends WindowsStream {
headerDxt10() { headerDxt10() {
return { return {
dxgiFormat: this.uint() as DxgiFormat, dxgiFormat: this.uint() as DxgiFormat,
resourceDimension: this.uint() as D3d10ResourceDimension, resourceDimension: this.uint() as DdsDimension,
miscFlag: this.uint(), miscFlag: this.uint(),
arraySize: this.uint(), arraySize: this.uint(),
miscFlags2: this.uint(), miscFlags2: this.uint(),
@@ -231,6 +268,6 @@ export class DdsStream extends WindowsStream {
new DdsStream( new DdsStream(
await readDVPLFile( await readDVPLFile(
'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data/3d/Tanks/German/images_pbr/E_100_BC.dx11.dds.dvpl', 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data/3d/Tanks/GB/images_pbr/Chieftain_Mk6_BC.dx11.dds.dvpl',
), ),
).dds(); ).dds();
+3 -3
View File
@@ -1,12 +1,12 @@
export class PrimitiveStream { export class PrimitiveStream {
public index = 0; public index = 0;
constructor(public buffer: Buffer) {} constructor(public stream: Buffer) {}
skip(size: number) { skip(size: number) {
this.index += size; this.index += size;
} }
read(size: number) { read(size: number) {
return this.buffer.subarray(this.index, this.index + size); return this.stream.subarray(this.index, this.index + size);
} }
consume(size: number) { consume(size: number) {
const subarray = this.read(size); const subarray = this.read(size);
@@ -19,7 +19,7 @@ export class PrimitiveStream {
return this.read(Number.POSITIVE_INFINITY); return this.read(Number.POSITIVE_INFINITY);
} }
readRemainingLength() { readRemainingLength() {
return this.buffer.length - this.index; return this.stream.length - this.index;
} }
consumeRemaining() { consumeRemaining() {
return this.consume(Number.POSITIVE_INFINITY); return this.consume(Number.POSITIVE_INFINITY);
+2 -2
View File
@@ -84,9 +84,9 @@ export class ScgStream extends ScpgStream {
ScgStream.vertexStride(polygonGroupRaw.vertexFormat) * ScgStream.vertexStride(polygonGroupRaw.vertexFormat) *
polygonGroupRaw.vertexCount; polygonGroupRaw.vertexCount;
if (verticesStream.buffer.length !== stride) { if (verticesStream.stream.length !== stride) {
console.warn( console.warn(
`Vertex stride mismatch; expected ${stride}, got ${verticesStream.buffer.length}`, `Vertex stride mismatch; expected ${stride}, got ${verticesStream.stream.length}`,
); );
return; return;