diff --git a/package.json b/package.json index 42c6fa9c3..cf50b27cf 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "markdown-escape": "^2.0.0", "next": "^14.0.3", "parse-dds": "^1.2.1", - "pngjs": "^7.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-fuzzysort": "^1.0.1", diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 2813bed8b..0aba67208 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -1,6 +1,5 @@ import { NodeIO } from '@gltf-transform/core'; import { config } from 'dotenv'; -import { writeFileSync } from 'fs'; import { mkdir, readdir } from 'fs/promises'; import { argv } from 'process'; import sharp from 'sharp'; @@ -25,24 +24,7 @@ import { Tier, TurretDefinition, } from '../src/core/blitzkrieg/tankDefinitions'; -import { VertexAttribute } from '../src/core/streams/scpg'; -const vertexAttributeGLTFName: Partial> = { - [VertexAttribute.VERTEX]: 'POSITION', - [VertexAttribute.NORMAL]: 'NORMAL', - [VertexAttribute.COLOR]: 'COLOR_0', - [VertexAttribute.TEXCOORD0]: 'TEXCOORD_0', - [VertexAttribute.TEXCOORD1]: 'TEXCOORD_0', - [VertexAttribute.TEXCOORD2]: 'TEXCOORD_0', - [VertexAttribute.TEXCOORD3]: 'TEXCOORD_0', - [VertexAttribute.TANGENT]: 'TANGENT', - [VertexAttribute.JOINTINDEX]: 'JOINT_0', - [VertexAttribute.JOINTWEIGHT]: 'WEIGHT_0', -}; - -(BigInt.prototype as any).toJSON = function () { - return this.toString(); -}; config(); // WARNING! MOST OF THESE TYPES ARE NOT EXHAUSTIVE! @@ -339,7 +321,6 @@ if (allTargets || targets?.includes('definitions')) { ); console.log('Committing tank definitions...'); - writeFileSync('test.json', JSON.stringify(definitions, null, 2)); // await commitMultipleFiles( // 'tresabhi', // 'blitzkrieg-assets', diff --git a/src/core/blitz/extractModel.ts b/src/core/blitz/extractModel.ts index 660f22d65..ad628b975 100644 --- a/src/core/blitz/extractModel.ts +++ b/src/core/blitz/extractModel.ts @@ -1,8 +1,6 @@ import { Document, Material, Node, Scene } from '@gltf-transform/core'; -import { writeFileSync } from 'fs'; import { range, times } from 'lodash'; import { dirname } from 'path'; -import sharp from 'sharp'; import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three'; import { readTexture } from '../blitzkrieg/readTexture'; import { Hierarchy, Sc2Stream, Textures } from '../streams/sc2'; @@ -35,8 +33,6 @@ export async function extractModel(data: string, path: string) { const buffer = document.createBuffer(); const materials = new Map(); - writeFileSync('test.sc2.json', JSON.stringify(sc2, null, 2)); - // create materials await Promise.all( sc2['#dataNodes'].map(async (node) => { @@ -64,7 +60,7 @@ export async function extractModel(data: string, path: string) { material.setBaseColorTexture( document .createTexture(node.materialName) - .setMimeType('image/jpg') + .setMimeType('image/png') .setImage( await readTexture( `${data}/3d/${dirname(path)}/${textures.albedo}`, @@ -76,24 +72,29 @@ export async function extractModel(data: string, path: string) { material.setMetallicRoughnessTexture( document .createTexture(node.materialName) - .setMimeType('image/jpg') + .setMimeType('image/png') .setImage( - await sharp( - await readTexture( - `${data}/3d/${dirname(path)}/${textures.baseRMMap}`, - ), - ) - .raw() - .toBuffer({ resolveWithObject: true }) - .then(({ data, info }) => { - // fake it till you make it - for (let index = 0; index < data.length; index++) { - data[index] = 255 - data[index]; - } - - return sharp(data, { raw: info }).jpeg().toBuffer(); - }), + await readTexture( + `${data}/3d/${dirname(path)}/${textures.baseRMMap}`, + ), ), + // .setImage( + // await sharp( + // await readTexture( + // `${data}/3d/${dirname(path)}/${textures.baseRMMap}`, + // ), + // ) + // .raw() + // .toBuffer({ resolveWithObject: true }) + // .then(({ data, info }) => { + // // fake it till you make it + // for (let index = 0; index < data.length; index++) { + // data[index] = 255 - data[index]; + // } + + // return sharp(data, { raw: info }).jpeg().toBuffer(); + // }), + // ), ); } @@ -101,24 +102,29 @@ export async function extractModel(data: string, path: string) { material.setNormalTexture( document .createTexture(node.materialName) - .setMimeType('image/jpg') + .setMimeType('image/png') .setImage( - await sharp( - await readTexture( - `${data}/3d/${dirname(path)}/${textures.baseNormalMap}`, - ), - ) - .raw() - .toBuffer({ resolveWithObject: true }) - .then(({ data, info }) => { - // https://www.youtube.com/watch?v=CGys6CnnYSg - for (let index = 0; index < data.length; index += 3) { - data[index + 2] = 255 - data[index + 2]; - } - - return sharp(data, { raw: info }).jpeg().toBuffer(); - }), + await readTexture( + `${data}/3d/${dirname(path)}/${textures.baseNormalMap}`, + ), ), + // .setImage( + // await sharp( + // await readTexture( + // `${data}/3d/${dirname(path)}/${textures.baseNormalMap}`, + // ), + // ) + // .raw() + // .toBuffer({ resolveWithObject: true }) + // .then(({ data, info }) => { + // // https://www.youtube.com/watch?v=CGys6CnnYSg + // for (let index = 0; index < data.length; index += 3) { + // data[index + 2] = 255 - data[index + 2]; + // } + + // return sharp(data, { raw: info }).jpeg().toBuffer(); + // }), + // ), ); } } @@ -146,8 +152,6 @@ export async function extractModel(data: string, path: string) { materials.set(id, resolvedMaterial); }); - writeFileSync('test.sc2.json', JSON.stringify(sc2, null, 2)); - function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) { hierarchies.forEach((hierarchy) => { const node = document.createNode(hierarchy.name); diff --git a/src/core/blitzkrieg/readTexture.ts b/src/core/blitzkrieg/readTexture.ts index 7f268840f..df2b9acad 100644 --- a/src/core/blitzkrieg/readTexture.ts +++ b/src/core/blitzkrieg/readTexture.ts @@ -1,21 +1,29 @@ -import { existsSync } from 'fs'; +import sharp from 'sharp'; import { readDVPLFile } from '../blitz/readDVPLFile'; -import { ToJpgOptions, toJpg } from './toJpg'; +import { DdsStream } from '../streams/dds'; +import { ToJpgOptions } from './toJpg'; export async function readTexture( path: string, options?: Partial>, ) { const ddsTexturePath = path.replace('.tex', '.dx11.dds.dvpl'); - const isDds = existsSync(ddsTexturePath); - const resolvedTexturePath = isDds - ? ddsTexturePath - : ddsTexturePath.replace('.dds', '.pvr'); - const decompressedTexture = await readDVPLFile(resolvedTexturePath); - const jpg = await toJpg(decompressedTexture, { - ...options, - format: isDds ? 'dds' : 'pvr', - }); + const decompressedDvpl = await readDVPLFile(ddsTexturePath); + const stream = new DdsStream(decompressedDvpl); + const ddsRaw = await stream.dds(); + const png = await sharp(ddsRaw.data, { raw: ddsRaw }).png().toBuffer(); - return jpg; + return png; + + // const isDds = existsSync(ddsTexturePath); + // const resolvedTexturePath = isDds + // ? ddsTexturePath + // : ddsTexturePath.replace('.dds', '.pvr'); + // const decompressedTexture = await readDVPLFile(resolvedTexturePath); + // const jpg = await toJpg(decompressedTexture, { + // ...options, + // format: isDds ? 'dds' : 'pvr', + // }); + + // return jpg; } diff --git a/src/core/streams/bit.ts b/src/core/streams/bit.ts deleted file mode 100644 index 5d7e7da83..000000000 --- a/src/core/streams/bit.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { times } from 'lodash'; - -type Bit = 0 | 1; - -export class BitStream { - 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 (size > 32) { - throw new RangeError('Cannot read more than 32 bits at a time'); - } - - 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); - } - readBytes(size: number) { - const buffer = Buffer.from(times(size, () => this.consume(8))); - this.skip(-8 * size); - - return buffer; - } - consume(size: number) { - const value = this.read(size); - this.skip(size); - - return value; - } -} diff --git a/src/core/streams/byte.ts b/src/core/streams/buffer.ts similarity index 97% rename from src/core/streams/byte.ts rename to src/core/streams/buffer.ts index 5bb18202d..c1daa20ea 100644 --- a/src/core/streams/byte.ts +++ b/src/core/streams/buffer.ts @@ -1,4 +1,4 @@ -export class ByteStream { +export class BufferStream { public index = 0; constructor(public stream: Buffer) {} diff --git a/src/core/streams/dds.ts b/src/core/streams/dds.ts index e511dd4c3..21b36ac43 100644 --- a/src/core/streams/dds.ts +++ b/src/core/streams/dds.ts @@ -1,10 +1,9 @@ -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'; +type Bits2 = 0b00 | 0b01 | 0b10 | 0b11; + enum DxgiFormat { UNKNOWN = 0, R32G32B32A32_TYPELESS = 1, @@ -180,10 +179,7 @@ export class DdsStream extends WindowsStream { const header = this.header(); - if ( - header.pf.flags !== DdpfFlags.FOURCC || - header.pf.fourCC !== 0x30315844 // "DX10" - ) { + if (header.pf.flags !== DdpfFlags.FOURCC || header.pf.fourCC !== 'DX10') { throw new RangeError('No other formats other than DX10 are supported'); } @@ -191,25 +187,57 @@ export class DdsStream extends WindowsStream { switch (headerDxt10.dxgiFormat) { case DxgiFormat.BC1_UNORM_SRGB: { - this.skip(1); + if (header.width % 4 !== 0 || header.height % 4 !== 0) { + throw new RangeError('Width and height must be divisible by 4'); + } - const image = new Bc1Stream( - // this.consume((header.width / 4) * (header.height / 4) * 8), - this.consumeRemaining(), - ).bc1(header.width, header.height); + const data = Buffer.alloc(header.width * header.height * 4); + const blocksX = header.width / 4; + const blocksY = header.height / 4; - writeFileSync( - 'test.png', - await sharp(image, { - raw: { - channels: 4, - width: header.width, - height: header.height, - }, - }) - .png() - .toBuffer(), - ); + times(blocksX * blocksY, (blockIndex) => { + const blockPosX = (blockIndex % blocksX) * 4; + const blockPosY = Math.floor(blockIndex / blocksX) * 4; + const interpolations = this.interpolations(); + const indices = this.indices(); + + indices.forEach((index, indexIndex) => { + let pixelPosX = 4 - (indexIndex % 4); + let pixelPosY = Math.floor(indexIndex / 4); + let x = blockPosX + pixelPosX; + let y = blockPosY + pixelPosY; + + const bufferIndex = 4 * (y * header.width + x); + const color = interpolations[index].map((standard) => + Math.round(255 * standard), + ); + + data[bufferIndex] = color[0]; + data[bufferIndex + 1] = color[1]; + data[bufferIndex + 2] = color[2]; + data[bufferIndex + 3] = color[3]; + }); + }); + + // writeFileSync( + // 'dist/assets/models/15697/baseColor_1.jpg', + // await sharp(image, { + // raw: { + // width: header.width, + // height: header.height, + // channels: 4, + // }, + // }) + // .jpeg() + // .toBuffer(), + // ); + + return { + width: header.width, + height: header.height, + channels: 4 as const, + data, + }; break; } @@ -223,8 +251,64 @@ export class DdsStream extends WindowsStream { } } + interpolations() { + const buffer0 = this.consume(2); + const buffer1 = this.consume(2); + const int0 = buffer0.readUInt16LE(); + const int1 = buffer1.readUInt16LE(); + const alpha = int0 < int1; + const color0 = [ + ((buffer0[1] & 0b11111000) >>> 3) / 0x1f, + (((buffer0[1] & 0b111) << 3) + ((buffer0[0] & 0b11100000) >>> 5)) / 0x3f, + (buffer0[0] & 0b11111) / 0x1f, + 1, + ]; + const color1 = [ + ((buffer1[1] & 0b11111000) >>> 3) / 0x1f, + (((buffer1[1] & 0b111) << 3) + ((buffer1[0] & 0b11100000) >>> 5)) / 0x3f, + (buffer1[0] & 0b11111) / 0x1f, + 1, + ]; + const color2 = alpha + ? color0.map((channel0, index) => (channel0 + color1[index]) / 2) + : color0.map((channel0, index) => (2 * channel0 + color1[index]) / 3); + const color3 = alpha + ? [0, 0, 0, 0] + : color0.map((channel0, index) => (channel0 + 2 * color1[index]) / 3); + + return { + [0b00]: color0, + [0b01]: color1, + [0b10]: color2, + [0b11]: color3, + }; + } + + indices() { + const indicesBuffer = this.consume(4); + const indices = times(16, (index) => { + const bufferIndex = Math.floor(index / 4); + const maskShift = 2 * (index % 4); + const valueShift = 2 * (3 - (index % 4)); + const buffer = indicesBuffer[bufferIndex]; + const mask = 0b11000000 >>> maskShift; + const masked = buffer & mask; + const value = masked >>> valueShift; + + return value as Bits2; + }); + + return indices; + } + magicNumber() { - return this.dword(); + const magic = this.dword(); + + if (magic !== 0x20534444) { + throw new Error(`Invalid DDS magic number: 0x${magic.toString(16)}`); + } + + return magic; } header() { @@ -250,7 +334,7 @@ export class DdsStream extends WindowsStream { return { size: this.dword(), flags: this.dword() as DdpfFlags, - fourCC: this.dword(), + fourCC: this.ascii(4), rgbBitCount: this.dword(), rBitMask: this.dword(), gBitMask: this.dword(), diff --git a/src/core/streams/scpg.ts b/src/core/streams/scpg.ts index 0e29b3acf..b1142b501 100644 --- a/src/core/streams/scpg.ts +++ b/src/core/streams/scpg.ts @@ -1,5 +1,5 @@ import { times } from 'lodash'; -import { ByteStream } from './byte'; +import { BufferStream } from './buffer'; enum KAType { NONE = 0, @@ -55,7 +55,7 @@ export enum VertexAttribute { CUBETEXCOORD3 = 19, } -export class ScpgStream extends ByteStream { +export class ScpgStream extends BufferStream { vectorN(size: number) { return times(size, () => this.float()); } diff --git a/src/core/streams/windows.ts b/src/core/streams/windows.ts index 6860cdf98..4cd0de709 100644 --- a/src/core/streams/windows.ts +++ b/src/core/streams/windows.ts @@ -1,6 +1,6 @@ -import { ByteStream } from './byte'; +import { BufferStream } from './buffer'; -export class WindowsStream extends ByteStream { +export class WindowsStream extends BufferStream { dword() { return this.uint32(); } diff --git a/yarn.lock b/yarn.lock index fb5b4c5a0..2ca9d57c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5049,11 +5049,6 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" -pngjs@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" - integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== - postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"