diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 0aba67208..cd7d52471 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -3,6 +3,7 @@ import { config } from 'dotenv'; import { mkdir, readdir } from 'fs/promises'; import { argv } from 'process'; import sharp from 'sharp'; +import { Vector3Tuple } from 'three'; import { TankType } from '../src/components/Tanks'; import { NATION_IDS } from '../src/constants/nations'; import { extractModel } from '../src/core/blitz/extractModel'; @@ -29,6 +30,10 @@ config(); // WARNING! MOST OF THESE TYPES ARE NOT EXHAUSTIVE! +interface VehicleCustomization { + armorColor: string; +} + interface VehicleDefinitionList { [key: string]: { id: number; @@ -494,6 +499,14 @@ if (allTargets || targets?.includes('tankModels')) { const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>( `${DATA}/${DOI.vehicleDefinitions}/${nation}/list.xml.dvpl`, ); + const customization = await readXMLDVPL<{ root: VehicleCustomization }>( + `${DATA}/${DOI.vehicleDefinitions}/${nation}/customization.xml.dvpl`, + ); + const baseColor = customization.root.armorColor + .split(' ') + .slice(0, 3) + .map(Number) + .map((channel) => channel / 255) as Vector3Tuple; for (const tankIndex in tanks.root) { const tank = tanks.root[tankIndex]; @@ -511,6 +524,7 @@ if (allTargets || targets?.includes('tankModels')) { const model = await extractModel( DATA, parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''), + baseColor, ); await mkdir(`dist/assets/models/${id}`, { recursive: true }); diff --git a/src/core/blitz/extractModel.ts b/src/core/blitz/extractModel.ts index ad628b975..d3c394207 100644 --- a/src/core/blitz/extractModel.ts +++ b/src/core/blitz/extractModel.ts @@ -1,8 +1,14 @@ import { Document, Material, Node, Scene } from '@gltf-transform/core'; import { range, times } from 'lodash'; import { dirname } from 'path'; -import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three'; -import { readTexture } from '../blitzkrieg/readTexture'; +import { + Matrix4, + Quaternion, + Vector3, + Vector3Tuple, + Vector4Tuple, +} from 'three'; +import { TextureMutation, readTexture } from '../blitzkrieg/readTexture'; import { Hierarchy, Sc2Stream, Textures } from '../streams/sc2'; import { ScgStream, vertexAttributeVectorSizes } from '../streams/scg'; import { VertexAttribute } from '../streams/scpg'; @@ -23,7 +29,11 @@ const vertexAttributeGLTFName: Partial> = { [VertexAttribute.JOINTWEIGHT]: 'WEIGHT_0', }; -export async function extractModel(data: string, path: string) { +export async function extractModel( + data: string, + path: string, + baseColor: Vector3Tuple, +) { const sc2Path = `${data}/3d/${path}.sc2.dvpl`; const scgPath = `${data}/3d/${path}.scg.dvpl`; const sc2 = new Sc2Stream(await readDVPLFile(sc2Path)).sc2(); @@ -64,6 +74,7 @@ export async function extractModel(data: string, path: string) { .setImage( await readTexture( `${data}/3d/${dirname(path)}/${textures.albedo}`, + { mutation: TextureMutation.BaseColor, baseColor }, ), ), ); @@ -76,25 +87,9 @@ export async function extractModel(data: string, path: string) { .setImage( await readTexture( `${data}/3d/${dirname(path)}/${textures.baseRMMap}`, + { mutation: TextureMutation.RoughnessMetallicness }, ), ), - // .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(); - // }), - // ), ); } @@ -106,25 +101,9 @@ export async function extractModel(data: string, path: string) { .setImage( await readTexture( `${data}/3d/${dirname(path)}/${textures.baseNormalMap}`, + { mutation: TextureMutation.Normal }, ), ), - // .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(); - // }), - // ), ); } } diff --git a/src/core/blitzkrieg/readTexture.ts b/src/core/blitzkrieg/readTexture.ts index 082b96119..a014abf13 100644 --- a/src/core/blitzkrieg/readTexture.ts +++ b/src/core/blitzkrieg/readTexture.ts @@ -1,14 +1,96 @@ import sharp from 'sharp'; +import { Vector3Tuple } from 'three'; import { readDVPLFile } from '../blitz/readDVPLFile'; import { DdsStream } from '../streams/dds'; -export async function readTexture(path: string) { +export enum TextureMutation { + Normal, + RoughnessMetallicness, + BaseColor, +} + +type ReadTextureOptions = + | { + mutation: TextureMutation.Normal | TextureMutation.RoughnessMetallicness; + } + | { + mutation: TextureMutation.BaseColor; + /** + * Normalized between 0 and 1 + */ + baseColor: Vector3Tuple; + }; + +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 ddsRaw = await stream.dds(); + const dds = await stream.dds(); + const channels = 4 * dds.width * dds.height; - return await sharp(ddsRaw.data, { raw: ddsRaw }).png().toBuffer(); + /** + * 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) { + let x = dds.data[index + 3] / 255; + let y = dds.data[index + 1] / 255; + let z = Math.sqrt(1 - x ** 2 - y ** 2); + const hypot = Math.sqrt(x ** 2 + y ** 2 + z ** 2); + + if (isFinite(hypot)) { + x /= hypot; + y /= hypot; + z /= hypot; + } + + dds.data[index] = Math.round(x * 255); + dds.data[index + 1] = Math.round(y * 255); + dds.data[index + 2] = Math.round(z * 255); + dds.data[index + 3] = 255; + } + + break; + } + + case TextureMutation.RoughnessMetallicness: { + for (let index = 0; index < channels; index += 4) { + const metallicness = dds.data[index + 1]; + const roughness = dds.data[index + 3]; + + dds.data[index] = 0; + dds.data[index + 1] = roughness; + dds.data[index + 2] = metallicness; + dds.data[index + 3] = 255; + } + + break; + } + + case TextureMutation.BaseColor: { + for (let index = 0; index < channels; index += 4) { + 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 inverseA = 1 - a; + const mixedR = a * r + inverseA * options.baseColor[0]; + const mixedG = a * g + inverseA * options.baseColor[1]; + const mixedB = a * b + inverseA * options.baseColor[2]; + + dds.data[index] = mixedR * 255; + dds.data[index + 1] = mixedG * 255; + dds.data[index + 2] = mixedB * 255; + dds.data[index + 3] = 255; + } + + break; + } + } + + return await sharp(dds.data, { raw: dds }).png().toBuffer(); // const isDds = existsSync(ddsTexturePath); // const resolvedTexturePath = isDds