diff --git a/.vscode/settings.json b/.vscode/settings.json index 424cf4eb2..1d387b76c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "leaderboards", "MURICA", "nmatname", + "noout", "normalmap", "Oopsy", "outdir", diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 94361ab70..22fc97119 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -3,7 +3,7 @@ import { config } from 'dotenv'; import { existsSync, writeFileSync } from 'fs'; import { mkdir, readdir, rm, writeFile } from 'fs/promises'; import { range, times } from 'lodash'; -import { dirname, parse } from 'path'; +import { dirname } from 'path'; import { argv } from 'process'; import sharp from 'sharp'; import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three'; @@ -23,7 +23,6 @@ import { toUniqueId } from '../src/core/blitz/toUniqueId'; import commitMultipleFiles, { FileChange, } from '../src/core/blitzkrieg/commitMultipleFiles'; -import { clearDdsToPng, ddsToPng } from '../src/core/blitzkrieg/ddsToPng'; import { GunDefinition, ShellDefinition, @@ -33,6 +32,7 @@ import { Tier, TurretDefinition, } from '../src/core/blitzkrieg/tankDefinitions'; +import { toPng } from '../src/core/blitzkrieg/toPng'; import { Hierarchy } from '../src/types/sc2'; const vertexAttributeGLTFName: Partial> = { @@ -505,8 +505,6 @@ if (allTargets || targets?.includes('circleFlags')) { } if (allTargets || targets?.includes('tankModels')) { - await clearDdsToPng(); - console.log('Building tank models...'); if (existsSync('dist/assets/models')) { @@ -530,7 +528,8 @@ if (allTargets || targets?.includes('tankModels')) { const nationVehicleId = tank.id; const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; - if (id !== 7297) continue; + // if (id !== 6225) continue; + if (id !== 20257) continue; console.log(`Building model ${id} @ ${nation}/${tankIndex}`); const parameters = await readYAMLDVPL( @@ -554,54 +553,73 @@ if (allTargets || targets?.includes('tankModels')) { const document = new Document(); const scene = document.createScene(); const buffer = document.createBuffer(); - const materials = new Map(); + const materials = new Map(); await Promise.all( sc2['#dataNodes'].map(async (node) => { - const material = document.createMaterial(node.materialName); + const id = node['#id'].readBigUInt64LE(); - let albedoTexturePath: string | undefined = undefined; - let albedoTextureName: string | undefined = undefined; - - if (node.textures) { - albedoTexturePath = `${DATA}/${DOI['3d']}/${dirname( - parameters.resourcesPath.blitzModelPath, - )}/${node.textures.albedo.replace('.tex', '.dx11.dds.dvpl')}`; - albedoTextureName = parse(albedoTexturePath).name; - } else if (typeof node.configCount === 'number') { - const config = node.configArchive_0; - albedoTexturePath = `${DATA}/${DOI['3d']}/${dirname( - parameters.resourcesPath.blitzModelPath, - )}/${config.textures.albedo.replace('.tex', '.dx11.dds.dvpl')}`; - albedoTextureName = config.configName; + if (node.parentMaterialKey !== undefined) { + materials.set(id, node.parentMaterialKey); + return; } - if (albedoTexturePath) { - const albedoBuffer = await ddsToPng( - await readDVPLFile(albedoTexturePath), - ); - const albedoTexture = document - .createTexture(albedoTextureName) - .setMimeType('image/png') - .setImage(albedoBuffer); + const material = document.createMaterial(node.materialName); + const albedoTexture = document + .createTexture(node.materialName) + .setMimeType('image/png'); + let albedoPathRaw: string | undefined = undefined; + if (node.textures) { + albedoPathRaw = node.textures.albedo; + } else if (typeof node.configCount === 'number') { + albedoPathRaw = node.configArchive_0.textures.albedo; + } + + const albedoTexturePath = albedoPathRaw + ? `${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, + ); + const albedoPng = await toPng(decompressedAlbedo, { + format: isDds ? 'dds' : 'pvr', + alpha: false, + }); + + albedoTexture.setImage(albedoPng); material.setBaseColorTexture(albedoTexture); } - materials.set(node['#id'].readBigUInt64LE(), { material }); + materials.set(node['#id'].readBigUInt64LE(), material); }), ); - sc2['#dataNodes'].forEach((node) => { - if (node.parentMaterialKey === undefined) return; + // replace children with parents + materials.forEach((material, id) => { + if (typeof material !== 'bigint') return; - const parentMaterial = materials.get(node.parentMaterialKey); + let resolvedMaterial: Material | bigint = material; - if (parentMaterial === undefined) return; + while (typeof resolvedMaterial === 'bigint') { + const linkedParentMaterial = materials.get(resolvedMaterial); - materials.set(node['#id'].readBigUInt64LE(), { - material: parentMaterial.material, - }); + if (linkedParentMaterial === undefined) { + throw new Error('Could not resolve material'); + } + + resolvedMaterial = linkedParentMaterial; + } + + materials.set(id, resolvedMaterial); }); function parseHierarchies( @@ -723,14 +741,16 @@ if (allTargets || targets?.includes('tankModels')) { .setBuffer(buffer); const material = materials.get(batch['rb.nmatname']); - if (!material) { - throw new Error(`Missing material ${batch['rb.nmatname']}`); + if (!(material instanceof Material)) { + throw new Error( + `Material ${batch['rb.nmatname']} is unresolved`, + ); } const primitive = document .createPrimitive() .setIndices(indexAccessor) - .setMaterial(material.material); + .setMaterial(material); indexedVertexTypes.forEach((type) => { if (!(type in vertexAttributeGLTFName)) { diff --git a/src/core/blitzkrieg/ddsToPng.ts b/src/core/blitzkrieg/ddsToPng.ts deleted file mode 100644 index 8be35f675..000000000 --- a/src/core/blitzkrieg/ddsToPng.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { execFile } from 'child_process'; -import { existsSync } from 'fs'; -import { mkdir, readFile, rm, writeFile } from 'fs/promises'; -import { uniqueId } from 'lodash'; -import { promisify } from 'util'; - -export async function clearDdsToPng() { - await rm('temp/ddsToPng/images', { force: true, recursive: true }); -} - -const execFilePromise = promisify(execFile); -let downloadStatus: Promise | undefined = undefined; - -export async function ddsToPng(ddsBuffer: Buffer) { - const id = uniqueId(); - await mkdir('temp/ddsToPng/images', { recursive: true }); - - if ( - !existsSync('temp/ddsToPng/texconv.exe') && - downloadStatus === undefined - ) { - console.log('texconv.exe not found, downloading...'); - - downloadStatus = new Promise(async (resolve, reject) => { - const releaseResponse = await fetch( - 'https://api.github.com/repos/microsoft/DirectXTex/releases/latest', - ); - const releaseData = (await releaseResponse.json()) as { - assets: { browser_download_url: string; name: string }[]; - }; - const asset = releaseData.assets.find( - (asset) => asset.name === 'texconv.exe', - ); - - if (!asset) throw new Error('Could not find texconv.exe'); - - const downloadResponse = await fetch(asset.browser_download_url); - const downloadData = await downloadResponse.arrayBuffer(); - - await writeFile( - 'temp/ddsToPng/texconv.exe', - new Uint8Array(downloadData), - ); - - resolve(); - }); - } - - await downloadStatus; - - return new Promise(async (resolve, reject) => { - await writeFile(`temp/ddsToPng/images/${id}.dds`, ddsBuffer); - await execFilePromise('temp/ddsToPng/texconv.exe', [ - '-ft', - 'png', - '-o', - 'temp/ddsToPng/images', - `temp/ddsToPng/images/${id}.dds`, - ]); - - const pngBuffer = await readFile(`temp/ddsToPng/images/${id}.png`); - - resolve(pngBuffer); - rm(`temp/ddsToPng/images/${id}.dds`); - // rm(`temp/ddsToPng/images/${id}.png`); - }); -} diff --git a/src/core/blitzkrieg/toPng.ts b/src/core/blitzkrieg/toPng.ts new file mode 100644 index 000000000..8ba25ab12 --- /dev/null +++ b/src/core/blitzkrieg/toPng.ts @@ -0,0 +1,38 @@ +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; +}