From d60a102c0da1f6462c48e9c944e155ec83651e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:59:06 -0600 Subject: [PATCH] feat: pbr --- .vscode/settings.json | 1 + scripts/buildAssets.ts | 157 +++++++++++++++++++++-------- src/core/blitzkrieg/readTexture.ts | 21 ++++ src/core/blitzkrieg/toJpg.ts | 54 ++++++++++ src/core/blitzkrieg/toPng.ts | 38 ------- src/types/sc2.ts | 20 ++-- 6 files changed, 201 insertions(+), 90 deletions(-) create mode 100644 src/core/blitzkrieg/readTexture.ts create mode 100644 src/core/blitzkrieg/toJpg.ts delete mode 100644 src/core/blitzkrieg/toPng.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 1d387b76c..9577d1542 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,6 +36,7 @@ "krieg", "Leaderboard", "leaderboards", + "metallicness", "MURICA", "nmatname", "noout", diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 22fc97119..3f6fe0ba8 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -1,7 +1,7 @@ import { Document, Material, Node, NodeIO, Scene } from '@gltf-transform/core'; import { config } from 'dotenv'; -import { existsSync, writeFileSync } from 'fs'; -import { mkdir, readdir, rm, writeFile } from 'fs/promises'; +import { writeFileSync } from 'fs'; +import { mkdir, readdir } from 'fs/promises'; import { range, times } from 'lodash'; import { dirname } from 'path'; import { argv } from 'process'; @@ -23,6 +23,7 @@ import { toUniqueId } from '../src/core/blitz/toUniqueId'; import commitMultipleFiles, { FileChange, } from '../src/core/blitzkrieg/commitMultipleFiles'; +import { readTexture } from '../src/core/blitzkrieg/readTexture'; import { GunDefinition, ShellDefinition, @@ -32,8 +33,7 @@ import { Tier, TurretDefinition, } from '../src/core/blitzkrieg/tankDefinitions'; -import { toPng } from '../src/core/blitzkrieg/toPng'; -import { Hierarchy } from '../src/types/sc2'; +import { Hierarchy, Textures } from '../src/types/sc2'; const vertexAttributeGLTFName: Partial> = { [VertexAttribute.VERTEX]: 'POSITION', @@ -507,12 +507,11 @@ if (allTargets || targets?.includes('circleFlags')) { if (allTargets || targets?.includes('tankModels')) { console.log('Building tank models...'); - if (existsSync('dist/assets/models')) { - await rm('dist/assets/models', { recursive: true }); - } - await mkdir('dist/assets/models', { recursive: true }); + // if (existsSync('dist/assets/models')) { + // await rm('dist/assets/models', { recursive: true }); + // } - const changes: FileChange[] = []; + const nodeIO = new NodeIO(); const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( (nations) => nations.filter((nation) => nation !== 'common'), ); @@ -528,8 +527,7 @@ if (allTargets || targets?.includes('tankModels')) { const nationVehicleId = tank.id; const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; - // if (id !== 6225) continue; - if (id !== 20257) continue; + if (id !== 6753) continue; console.log(`Building model ${id} @ ${nation}/${tankIndex}`); const parameters = await readYAMLDVPL( @@ -556,7 +554,7 @@ if (allTargets || targets?.includes('tankModels')) { const materials = new Map(); await Promise.all( - sc2['#dataNodes'].map(async (node) => { + sc2['#dataNodes'].map(async (node, index) => { const id = node['#id'].readBigUInt64LE(); if (node.parentMaterialKey !== undefined) { @@ -565,38 +563,107 @@ if (allTargets || targets?.includes('tankModels')) { } const material = document.createMaterial(node.materialName); - const albedoTexture = document - .createTexture(node.materialName) - .setMimeType('image/png'); - let albedoPathRaw: string | undefined = undefined; + let textures: Textures | undefined = undefined; if (node.textures) { - albedoPathRaw = node.textures.albedo; + textures = node.textures; } else if (typeof node.configCount === 'number') { - albedoPathRaw = node.configArchive_0.textures.albedo; + textures = node.configArchive_0.textures; } - const albedoTexturePath = albedoPathRaw - ? `${DATA}/${DOI['3d']}/${dirname( + console.log(textures); + + if (textures) { + const albedoJpg = await readTexture( + `${DATA}/${DOI['3d']}/${dirname( parameters.resourcesPath.blitzModelPath, - )}/${albedoPathRaw.replace('.tex', '.dx11.dds.dvpl')}` - : undefined; - - if (albedoTexturePath) { - const isDds = existsSync(albedoTexturePath); - const resolvedAlbedoTexturePath = isDds - ? albedoTexturePath - : albedoTexturePath.replace('.dds', '.pvr'); - const decompressedAlbedo = await readDVPLFile( - resolvedAlbedoTexturePath, + )}/${textures.albedo}`, ); - const albedoPng = await toPng(decompressedAlbedo, { - format: isDds ? 'dds' : 'pvr', - alpha: false, - }); - - albedoTexture.setImage(albedoPng); + const albedoTexture = document + .createTexture(node.materialName) + .setMimeType('image/jpg') + .setImage(albedoJpg); material.setBaseColorTexture(albedoTexture); + + if (textures.baseRMMap) { + const RMJpg = await sharp( + await readTexture( + `${DATA}/${DOI['3d']}/${dirname( + parameters.resourcesPath.blitzModelPath, + )}/${textures.baseRMMap}`, + ), + ) + .raw() + .toBuffer({ resolveWithObject: true }) + .then(({ data, info }) => { + /** + * RM mods + * + * green channel --> 1 : metallicness --> blue (interpreted as metallicness) + * green channel --> 1 - x : roughness --> green (interpreted as roughness) + * red channel --> 0 : unknown --> red (unknown) + */ + + const newImage = Buffer.from(data); + + for (let index = 0; index < data.length; index += 3) { + const G = newImage[index + 1]; + + newImage[index] = 0; + newImage[index + 1] = 255 - G; + newImage[index + 2] = G; + } + + return sharp(newImage, { raw: info }).jpeg().toBuffer(); + }); + const RMTexture = document + .createTexture(node.materialName) + .setMimeType('image/jpg') + .setImage(RMJpg); + + material.setMetallicRoughnessTexture(RMTexture); + } + + if (textures.baseNormalMap) { + const normalJpg = await sharp( + await readTexture( + `${DATA}/${DOI['3d']}/${dirname( + parameters.resourcesPath.blitzModelPath, + )}/${textures.baseNormalMap}`, + ), + ) + .raw() + .toBuffer({ resolveWithObject: true }) + .then(({ data, info }) => { + /** + * normal mods + * + * red channel --> blue channel + * green channel --> green channel + * blue channel --> red channel + */ + + const newImage = Buffer.from(data); + + for (let index = 0; index < data.length; index += 3) { + const R = newImage[index]; + const G = newImage[index + 1]; + const B = newImage[index + 2]; + + newImage[index] = B; + newImage[index + 1] = G; + newImage[index + 2] = R; + } + + return sharp(newImage, { raw: info }).jpeg().toBuffer(); + }); + const normalTexture = document + .createTexture(node.materialName) + .setMimeType('image/jpg') + .setImage(normalJpg); + + material.setNormalTexture(normalTexture); + } } materials.set(node['#id'].readBigUInt64LE(), material); @@ -802,10 +869,20 @@ if (allTargets || targets?.includes('tankModels')) { }); } - parseHierarchies(sc2['#hierarchy'], scene); - writeFile('test.model.glb', await new NodeIO().writeBinary(document)); - writeFile('test.sc2.json', JSON.stringify(sc2, null, 2)); - writeFile('test.scg.json', JSON.stringify(scg, null, 2)); + // rotate 90 degrees on the x axis + const rootNode = document + .createNode() + .setRotation([Math.cos(Math.PI / 4), 0, 0, -Math.sin(Math.PI / 4)]); + + scene.addChild(rootNode); + + parseHierarchies(sc2['#hierarchy'], rootNode); + // writeFile( + // `dist/assets/models/${id}.glb`, + // JSON.stringify((await new NodeIO().writeJSON(document)).json), + // ); + await mkdir(`dist/assets/models/${id}`, { recursive: true }); + nodeIO.write(`dist/assets/models/${id}/index.gltf`, document); } }), ); diff --git a/src/core/blitzkrieg/readTexture.ts b/src/core/blitzkrieg/readTexture.ts new file mode 100644 index 000000000..7f268840f --- /dev/null +++ b/src/core/blitzkrieg/readTexture.ts @@ -0,0 +1,21 @@ +import { existsSync } from 'fs'; +import { readDVPLFile } from '../blitz/readDVPLFile'; +import { ToJpgOptions, toJpg } 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', + }); + + return jpg; +} diff --git a/src/core/blitzkrieg/toJpg.ts b/src/core/blitzkrieg/toJpg.ts new file mode 100644 index 000000000..bc9a76728 --- /dev/null +++ b/src/core/blitzkrieg/toJpg.ts @@ -0,0 +1,54 @@ +import { exec } from 'child_process'; +import { mkdir, readFile, rm, writeFile } from 'fs/promises'; +import { generateUUID } from 'three/src/math/MathUtils'; +import { promisify } from 'util'; + +const execPromise = promisify(exec); + +type Channel = 'red' | 'green' | 'blue' | 'alpha'; + +export interface ToJpgOptions { + format: string; + swap: [Channel, Channel][]; + invert: boolean; +} + +const toJpgDefaultOptions: ToJpgOptions = { + format: 'dds', + swap: [], + invert: false, +}; + +export async function toJpg( + inputBuffer: Buffer, + options?: Partial, +) { + const mergedOptions = { ...toJpgDefaultOptions, ...options }; + const id = generateUUID(); + const input = `temp/toJpg/${id}.${mergedOptions.format}`; + const output = `temp/toJpg/${id}.jpg`; + const command = `PVRTexToolCLI -noout -i ${input} ${ + mergedOptions.swap.length === 0 + ? '' + : mergedOptions.swap + .map( + (swapChannels) => `-${swapChannels[1]} ${input},${swapChannels[0]}`, + ) + .join(' ') + } ${ + mergedOptions.invert ? `-diff ${input},DIFFINVERT,255` : '' + } -d ${output}`; + + console.log(command); + + await mkdir('temp/toJpg', { recursive: true }); + await writeFile(`temp/toJpg/${id}.${mergedOptions.format}`, inputBuffer); + await execPromise(command); + + const jpgBuffer = await readFile(`temp/toJpg/${id}.jpg`); + + rm(`temp/toJpg/${id}.${mergedOptions.format}`); + rm(`temp/toJpg/${id}.jpg`); + + return jpgBuffer; +} diff --git a/src/core/blitzkrieg/toPng.ts b/src/core/blitzkrieg/toPng.ts deleted file mode 100644 index 8ba25ab12..000000000 --- a/src/core/blitzkrieg/toPng.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { exec } from 'child_process'; -import { mkdir, readFile, rm, writeFile } from 'fs/promises'; -import { generateUUID } from 'three/src/math/MathUtils'; -import { promisify } from 'util'; - -const execPromise = promisify(exec); - -interface ToPngOptions { - format: string; - alpha: boolean; -} - -const toPngDefaultOptions: ToPngOptions = { - format: 'dds', - alpha: true, -}; - -export async function toPng( - inputBuffer: Buffer, - options?: Partial, -) { - const mergedOptions = { ...toPngDefaultOptions, ...options }; - const id = generateUUID(); - await mkdir('temp/toPng', { recursive: true }); - await writeFile(`temp/toPng/${id}.${mergedOptions.format}`, inputBuffer); - await execPromise( - `PVRTexToolCLI -noout -i temp/toPng/${id}.${mergedOptions.format} -f ${ - mergedOptions.alpha ? 'r8g8b8a8' : 'r8g8b8' - },UB,sRGB -d temp/toPng/${id}.png`, - ); - - const pngBuffer = await readFile(`temp/toPng/${id}.png`); - - rm(`temp/toPng/${id}.${mergedOptions.format}`); - rm(`temp/toPng/${id}.png`); - - return pngBuffer; -} diff --git a/src/types/sc2.ts b/src/types/sc2.ts index 256b2e410..72623ea38 100644 --- a/src/types/sc2.ts +++ b/src/types/sc2.ts @@ -14,11 +14,7 @@ type DataNode = { qualityGroup?: string; fxName?: string; - textures?: { - albedo: string; - decalmask: string; - normalmap: string; - }; + textures?: Textures; } & ( | ({ configCount: number; @@ -70,15 +66,15 @@ interface TextureSampleStates { normalmap: number; } -interface Textures { +export interface Textures { albedo: string; - baseColorMap: string; - baseNormalMap: string; - baseRMMap: string; + baseColorMap?: string; + baseNormalMap?: string; + baseRMMap?: string; decalmask?: string; - maskMap: string; - miscMap: string; - normalmap: string; + maskMap?: string; + miscMap?: string; + normalmap?: string; } export interface Hierarchy {