diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 7748f68e7..833df45a5 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -513,13 +513,14 @@ 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 !== 24609) continue; // concept 1b // if (id !== 4417) continue; // amx m4 mle // if (id !== 7297) continue; // 60tp // if (id !== 1) continue; // t-34 // if (id !== 6753) continue; // type 71 // if (id !== 5137) continue; // tiger ii + if (id !== 6225) continue; // fv215b console.log(`Building model ${id} @ ${nation}/${tankIndex}`); diff --git a/src/core/blitz/extractModel.ts b/src/core/blitz/extractModel.ts index ab97729a6..fb7fe0e51 100644 --- a/src/core/blitz/extractModel.ts +++ b/src/core/blitz/extractModel.ts @@ -93,15 +93,19 @@ export async function extractModel( ); } - if (textures.baseNormalMap) { + if (textures.baseNormalMap ?? textures.normalmap) { + const isBase = textures.baseNormalMap !== undefined; + material.setNormalTexture( document .createTexture(node.materialName) .setMimeType('image/png') .setImage( await readTexture( - `${data}/3d/${dirname(path)}/${textures.baseNormalMap}`, - { mutation: TextureMutation.Normal }, + `${data}/3d/${dirname(path)}/${ + textures.baseNormalMap ?? textures.normalmap + }`, + isBase ? { mutation: TextureMutation.Normal } : undefined, ), ), ); @@ -146,6 +150,7 @@ export async function extractModel( case 'DecorItemComponent': case 'NewSlotComponent': case 'StateSwitcherComponent': + case 'CustomPropertiesComponent': case 'SlotComponent': break; diff --git a/src/core/blitzkrieg/readTexture.ts b/src/core/blitzkrieg/readTexture.ts index ce12e48da..e9bdc6746 100644 --- a/src/core/blitzkrieg/readTexture.ts +++ b/src/core/blitzkrieg/readTexture.ts @@ -1,8 +1,10 @@ +import { existsSync } from 'fs'; import { clamp } from 'lodash'; import sharp from 'sharp'; import { Vector3Tuple } from 'three'; import { readDVPLFile } from '../blitz/readDVPLFile'; import { DdsStream } from '../streams/dds'; +import { PvrStream } from '../streams/pvr'; export enum TextureMutation { Normal, @@ -24,28 +26,32 @@ type ReadTextureOptions = export async function readTexture(path: string, options?: ReadTextureOptions) { const ddsTexturePath = path.replace('.tex', '.dx11.dds.dvpl'); - const decompressedDvpl = await readDVPLFile(ddsTexturePath); - const stream = new DdsStream(decompressedDvpl); - const dds = await stream.dds(); - const channels = 4 * dds.width * dds.height; + const isDds = existsSync(ddsTexturePath); + const resolvedTexturePath = isDds + ? ddsTexturePath + : ddsTexturePath.replace('.dds', '.pvr'); + const decompressedDvpl = await readDVPLFile(resolvedTexturePath); + const raw = isDds + ? await new DdsStream(decompressedDvpl).dds() + : new PvrStream(decompressedDvpl).pvr(); + const channels = 4 * raw.width * raw.height; - /** - * Red is always 255 and blue is always 0. Only alpha and green contain any - * sort of information. - */ switch (options?.mutation) { case TextureMutation.Normal: { for (let index = 0; index < channels; index += 4) { - // TODO: this math is pretty sus and looks very goofy when rendered - let x = dds.data[index + 3] * (2 / 255) - 1; - let y = dds.data[index + 1] * (2 / 255) - 1; + /** + * Red is always 255 and blue is always 0. Only alpha and green contain any + * sort of information. + */ + let x = raw.data[index + 3] * (2 / 255) - 1; + let y = raw.data[index + 1] * (2 / 255) - 1; const underSqrt = 1 - x ** 2 - y ** 2; let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt); - dds.data[index] = Math.round((x + 1) * (255 / 2)); - dds.data[index + 1] = Math.round((y + 1) * (255 / 2)); - dds.data[index + 2] = Math.round((z + 1) * (255 / 2)); - dds.data[index + 3] = 255; + raw.data[index] = Math.round((x + 1) * (255 / 2)); + raw.data[index + 1] = Math.round((y + 1) * (255 / 2)); + raw.data[index + 2] = Math.round((z + 1) * (255 / 2)); + raw.data[index + 3] = 255; } break; @@ -53,13 +59,16 @@ export async function readTexture(path: string, options?: ReadTextureOptions) { case TextureMutation.RoughnessMetallicness: { for (let index = 0; index < channels; index += 4) { - const metallicness = dds.data[index + 1]; - const roughness = dds.data[index + 3]; + /** + * Same channel situation as normal maps. + */ + const metallicness = raw.data[index + 1]; + const roughness = raw.data[index + 3]; - dds.data[index] = 0; - dds.data[index + 1] = roughness; - dds.data[index + 2] = metallicness; - dds.data[index + 3] = 255; + raw.data[index] = 0; + raw.data[index + 1] = roughness; + raw.data[index + 2] = metallicness; + raw.data[index + 3] = 255; } break; @@ -69,36 +78,24 @@ export async function readTexture(path: string, options?: ReadTextureOptions) { for (let index = 0; index < channels; index += 4) { // treating as pre multiplied but I can't fully confirm - const r = dds.data[index] / 255; - const g = dds.data[index + 1] / 255; - const b = dds.data[index + 2] / 255; - const a = dds.data[index + 3] / 255; + const r = raw.data[index] / 255; + const g = raw.data[index + 1] / 255; + const b = raw.data[index + 2] / 255; + const a = raw.data[index + 3] / 255; const invA = 1 - a; const rPrime = r + invA * options.baseColor[0]; const gPrime = g + invA * options.baseColor[1]; const bPrime = b + invA * options.baseColor[2]; - dds.data[index] = Math.round(clamp(rPrime * 255, 0, 255)); - dds.data[index + 1] = Math.round(clamp(gPrime * 255, 0, 255)); - dds.data[index + 2] = Math.round(clamp(bPrime * 255, 0, 255)); - dds.data[index + 3] = 255; + raw.data[index] = Math.round(clamp(rPrime * 255, 0, 255)); + raw.data[index + 1] = Math.round(clamp(gPrime * 255, 0, 255)); + raw.data[index + 2] = Math.round(clamp(bPrime * 255, 0, 255)); + raw.data[index + 3] = 255; } break; } } - return await sharp(dds.data, { raw: dds }).png().toBuffer(); - - // 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; + return await sharp(raw.data, { raw: raw }).png().toBuffer(); } diff --git a/src/core/streams/pvr.ts b/src/core/streams/pvr.ts new file mode 100644 index 000000000..8b2eba4f3 --- /dev/null +++ b/src/core/streams/pvr.ts @@ -0,0 +1,208 @@ +import { clamp, isEqual, times } from 'lodash'; +import { Vector4Tuple } from 'three'; +import { BufferStream } from './buffer'; + +export enum PvrFlags { + NoFlag = 0, + PreMultiplied = 0x02, +} + +export enum PvrPixelFormat { + PVRTC_2bpp_RGB = 0, + PVRTC_2bpp_RGBA = 1, + PVRTC_4bpp_RGB = 2, + PVRTC_4bpp_RGBA = 3, + PVRTC_II_2bpp = 4, + PVRTC_II_4bpp = 5, + ETC1 = 6, + DXT1_BC1 = 7, + DXT2 = 8, + DXT3_BC2 = 9, + DXT4 = 10, + DXT5_BC3 = 11, + BC4 = 12, + BC5 = 13, + BC6 = 14, + BC7 = 15, + UYVY = 16, + YUY2 = 17, + BW1bpp = 18, + R9G9B9E5_SharedExponent = 19, + RGBG8888 = 20, + GRGB8888 = 21, + ETC2_RGB = 22, + ETC2_RGBA = 23, + ETC2_RGB_A1 = 24, + EAC_R11 = 25, + EAC_RG11 = 26, + ASTC_4x4 = 27, + ASTC_5x4 = 28, + ASTC_5x5 = 29, + ASTC_6x5 = 30, + ASTC_6x6 = 31, + ASTC_8x5 = 32, + ASTC_8x6 = 33, + ASTC_8x8 = 34, + ASTC_10x5 = 35, + ASTC_10x6 = 36, + ASTC_10x8 = 37, + ASTC_10x10 = 38, + ASTC_12x10 = 39, + ASTC_12x12 = 40, + ASTC_3x3x3 = 41, + ASTC_4x3x3 = 42, + ASTC_4x4x3 = 43, + ASTC_4x4x4 = 44, + ASTC_5x4x4 = 45, + ASTC_5x5x4 = 46, + ASTC_5x5x5 = 47, + ASTC_6x5x5 = 48, + ASTC_6x6x5 = 49, + ASTC_6x6x6 = 50, + BASISU_ETC1S = 51, + BASISU_UASTC = 52, + RGBM = 53, + RGBD = 54, +} + +enum PvrColorSpace { + LinearRGB = 0, + sRGB = 1, +} + +enum PvrChannelType { + UnsignedByteNormalised = 0, + SignedByteNormalised = 1, + UnsignedByte = 2, + SignedByte = 3, + UnsignedShortNormalised = 4, + SignedShortNormalised = 5, + UnsignedShort = 6, + SignedShort = 7, + UnsignedIntegerNormalised = 8, + SignedIntegerNormalised = 9, + UnsignedInteger = 10, + SignedInteger = 11, + SignedFloat = 12, + UnsignedFloat = 13, +} + +enum ResolvedBitRatePixelFormat { + R4G4B4A4, +} + +export class PvrStream extends BufferStream { + pvr() { + const header = this.header(); + const metadata = this.metadata(); + + console.log(header, metadata); + + if (typeof header.pixelFormat === 'number') { + switch (header.pixelFormat) { + default: + throw new TypeError( + `Unhandled pixel format ${PvrPixelFormat[header.pixelFormat]} (${ + header.pixelFormat + })`, + ); + } + } else { + const resolvedPixelFormat = this.resolveBitRatePixelFormat( + header.pixelFormat, + ); + const pixelCount = header.width * header.height; + + switch (resolvedPixelFormat) { + case ResolvedBitRatePixelFormat.R4G4B4A4: { + const data = Buffer.alloc(pixelCount * 4); + let lastPixel: Vector4Tuple; + + times(pixelCount, (index) => { + const buffer = this.consume(2); + const bufferIndex = index * 4; + const r = ((buffer[0] & 0b11110000) >>> 4) / 15; + const g = (buffer[0] & 0b1111) / 15; + const b = ((buffer[1] & 0b11110000) >>> 4) / 15; + const a = (buffer[1] & 0b1111) / 15; + + data[bufferIndex] = Math.round(clamp(r * 255, 0, 255)); + data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255)); + data[bufferIndex + 2] = Math.round(clamp(b * 255, 0, 255)); + data[bufferIndex + 3] = Math.round(clamp(a * 255, 0, 255)); + }); + + return { + data, + width: header.width, + height: header.height, + channels: 4 as const, + }; + } + + default: + throw new TypeError( + `Unhandled resolved pixel format ${resolvedPixelFormat} (${header.pixelFormat})`, + ); + } + } + } + + header() { + const header = { + version: this.ascii(4), + flags: this.uint32() as PvrFlags, + pixelFormat: this.pixelFormat(), + colorSpace: this.uint32() as PvrColorSpace, + channelType: this.uint32() as PvrChannelType, + height: this.uint32(), + width: this.uint32(), + depth: this.uint32(), + surfaces: this.uint32(), + faces: this.uint32(), + mipMaps: this.uint32(), + metadataSize: this.uint32(), + }; + + if (header.version !== 'PVR\x03') throw new TypeError('Endian mismatch'); + + return header; + } + pixelFormat() { + this.skip(4); + const usesPresetFormat = this.uint32() === 0; + this.skip(-8); + + return usesPresetFormat + ? (Number(this.uint64()) as PvrPixelFormat) + : { + order: this.ascii(4), + bitRates: times(4, () => this.uint8()), + }; + } + resolveBitRatePixelFormat( + pixelFormat: Exclude, number>, + ) { + if (pixelFormat.order === 'rgba') { + if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) { + return ResolvedBitRatePixelFormat.R4G4B4A4; + } else + throw new TypeError( + `Unhandled rgba bit rate ${pixelFormat.bitRates.join(', ')}`, + ); + } else throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`); + } + metadata() { + const fourCC = this.ascii(4); + const key = this.uint32(); + const dataSize = this.uint32(); + const data = this.consume(dataSize); + + return { + fourCC, + key, + dataSize, + data, + }; + } +} diff --git a/src/core/streams/sc2.ts b/src/core/streams/sc2.ts index 65442722e..c5f2990c5 100644 --- a/src/core/streams/sc2.ts +++ b/src/core/streams/sc2.ts @@ -177,7 +177,13 @@ export type Component = 'comp.typename': 'StateSwitcherComponent'; 'ssc.activeState': number; 'ssc.statesCount': number; - } & Record<`ssc.state${number}`, string>); + } & Record<`ssc.state${number}`, string>) + | { + 'comp.typename': 'CustomPropertiesComponent'; + 'cpc.properties.archive': { + 'editor.donotremove': number; + }; + }; interface SceneComponents { count: number;