diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index a82d3cda8..2813bed8b 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -519,9 +519,9 @@ if (allTargets || targets?.includes('tankModels')) { const nationVehicleId = tank.id; 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 !== 7297) continue; // 60tp + // if (id !== 7297) continue; // 60tp console.log(`Building model ${id} @ ${nation}/${tankIndex}`); const parameters = await readYAMLDVPL( diff --git a/src/core/streams/bc1.ts b/src/core/streams/bc1.ts new file mode 100644 index 000000000..2583397d4 --- /dev/null +++ b/src/core/streams/bc1.ts @@ -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; + } +} diff --git a/src/core/streams/binary.ts b/src/core/streams/binary.ts new file mode 100644 index 000000000..c00951a62 --- /dev/null +++ b/src/core/streams/binary.ts @@ -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; + } +} diff --git a/src/core/streams/dds.ts b/src/core/streams/dds.ts index eb726e3e8..eaeb15d91 100644 --- a/src/core/streams/dds.ts +++ b/src/core/streams/dds.ts @@ -1,5 +1,8 @@ +import { writeFileSync } from 'fs'; import { times } from 'lodash'; +import sharp from 'sharp'; import { readDVPLFile } from '../blitz/readDVPLFile'; +import { Bc1Stream } from './bc1'; import { WindowsStream } from './windows'; enum DxgiFormat { @@ -127,7 +130,7 @@ enum DxgiFormat { FORCE_UINT = 0xffffffff, } -enum D3d10ResourceDimension { +enum DdsDimension { UNKNOWN = 0, BUFFER = 1, TEXTURE1D = 2, @@ -172,13 +175,48 @@ enum DdsCaps2 { } export class DdsStream extends WindowsStream { - dds() { - const magicNumber = this.magicNumber(); + async dds() { + this.magicNumber(); const header = this.header(); - const headerDxt10 = - header.pf.flags === DdpfFlags.FOURCC && header.pf.fourCC === 0x30315844 - ? this.headerDxt10() - : undefined; + + if ( + header.pf.flags !== DdpfFlags.FOURCC || + 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() { @@ -201,7 +239,6 @@ export class DdsStream extends WindowsStream { caps3: this.dword(), caps4: this.dword(), reserved2: times(1, () => this.dword()), - headerDxt10: undefined, }; } @@ -221,7 +258,7 @@ export class DdsStream extends WindowsStream { headerDxt10() { return { dxgiFormat: this.uint() as DxgiFormat, - resourceDimension: this.uint() as D3d10ResourceDimension, + resourceDimension: this.uint() as DdsDimension, miscFlag: this.uint(), arraySize: this.uint(), miscFlags2: this.uint(), @@ -231,6 +268,6 @@ export class DdsStream extends WindowsStream { new DdsStream( 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(); diff --git a/src/core/streams/primitive.ts b/src/core/streams/primitive.ts index b842c591f..4a84330b2 100644 --- a/src/core/streams/primitive.ts +++ b/src/core/streams/primitive.ts @@ -1,12 +1,12 @@ export class PrimitiveStream { public index = 0; - constructor(public buffer: Buffer) {} + constructor(public stream: Buffer) {} skip(size: number) { this.index += size; } read(size: number) { - return this.buffer.subarray(this.index, this.index + size); + return this.stream.subarray(this.index, this.index + size); } consume(size: number) { const subarray = this.read(size); @@ -19,7 +19,7 @@ export class PrimitiveStream { return this.read(Number.POSITIVE_INFINITY); } readRemainingLength() { - return this.buffer.length - this.index; + return this.stream.length - this.index; } consumeRemaining() { return this.consume(Number.POSITIVE_INFINITY); diff --git a/src/core/streams/scg.ts b/src/core/streams/scg.ts index c9524930f..0ddd7a381 100644 --- a/src/core/streams/scg.ts +++ b/src/core/streams/scg.ts @@ -84,9 +84,9 @@ export class ScgStream extends ScpgStream { ScgStream.vertexStride(polygonGroupRaw.vertexFormat) * polygonGroupRaw.vertexCount; - if (verticesStream.buffer.length !== stride) { + if (verticesStream.stream.length !== stride) { console.warn( - `Vertex stride mismatch; expected ${stride}, got ${verticesStream.buffer.length}`, + `Vertex stride mismatch; expected ${stride}, got ${verticesStream.stream.length}`, ); return;