diff --git a/public/test/13569.glb b/public/test/13569.glb index d506846b4..493f092f3 100644 Binary files a/public/test/13569.glb and b/public/test/13569.glb differ diff --git a/public/test/6225.glb b/public/test/6225.glb index dd6b9917d..30e23e545 100644 Binary files a/public/test/6225.glb and b/public/test/6225.glb differ diff --git a/scripts/buildAssets/circleFlags.ts b/scripts/buildAssets/circleFlags.ts index 27c70fdf8..94f63dbfc 100644 --- a/scripts/buildAssets/circleFlags.ts +++ b/scripts/buildAssets/circleFlags.ts @@ -1,9 +1,9 @@ import { readdir } from 'fs/promises'; -import { DATA, DOI } from '.'; import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import commitMultipleFiles, { FileChange, } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, DOI } from './constants'; export async function circleFlags() { const flags = await readdir(`${DATA}/${DOI.flags}`); diff --git a/scripts/buildAssets/constants.ts b/scripts/buildAssets/constants.ts new file mode 100644 index 000000000..31fc9b7b6 --- /dev/null +++ b/scripts/buildAssets/constants.ts @@ -0,0 +1,14 @@ +export const DATA = + 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data'; +export const DOI = { + vehicleDefinitions: 'XML/item_defs/vehicles', + strings: 'Strings', + tankParameters: '3d/Tanks/Parameters', + smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons', + bigIcons: 'Gfx/UI/BigTankIcons', + flags: 'Gfx/Lobby/flags', + '3d': '3d', + bigShellIcons: 'Gfx/Shared/tank-supply/ammunition/big', + moduleIcons: 'Gfx/UI/ModulesTechTree', + collisionMeshes: '3d/Tanks/CollisionMeshes', +} as const; diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index e2943e70c..928a1fc09 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -1,7 +1,6 @@ import { readdir } from 'fs/promises'; import { parse } from 'path'; import { Vector3Tuple } from 'three'; -import { DATA, DOI } from '.'; import { TankType } from '../../src/components/Tanks'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; @@ -15,6 +14,7 @@ import { TankDefinitions, Tier, } from '../../src/core/blitzkrieg/tankDefinitions'; +import { DATA, DOI } from './constants'; type BlitzTankType = 'AT-SPG' | 'lightTank' | 'mediumTank' | 'heavyTank'; interface Strings { diff --git a/scripts/buildAssets/index.ts b/scripts/buildAssets/index.ts index f76454b69..a3e7010eb 100644 --- a/scripts/buildAssets/index.ts +++ b/scripts/buildAssets/index.ts @@ -11,21 +11,6 @@ import { buildTankModels } from './tankModels'; config(); -export const DATA = - 'C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz/Data'; -export const DOI = { - vehicleDefinitions: 'XML/item_defs/vehicles', - strings: 'Strings', - tankParameters: '3d/Tanks/Parameters', - smallIcons: 'Gfx/UI/BattleScreenHUD/SmallTankIcons', - bigIcons: 'Gfx/UI/BigTankIcons', - flags: 'Gfx/Lobby/flags', - '3d': '3d', - bigShellIcons: 'Gfx/Shared/tank-supply/ammunition/big', - moduleIcons: 'Gfx/UI/ModulesTechTree', - collisionMeshes: '3d/Tanks/CollisionMeshes', -} as const; - const allTargets = argv.includes('--all-targets'); const targets = argv .find((argument) => argument.startsWith('--target')) diff --git a/scripts/buildAssets/moduleIcons.ts b/scripts/buildAssets/moduleIcons.ts index 8900f8a0f..533e9269b 100644 --- a/scripts/buildAssets/moduleIcons.ts +++ b/scripts/buildAssets/moduleIcons.ts @@ -1,9 +1,9 @@ import { readdir } from 'fs/promises'; -import { DATA, DOI } from '.'; import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import commitMultipleFiles, { FileChange, } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, DOI } from './constants'; export async function buildModuleIcons() { console.log('Building module icons...'); diff --git a/scripts/buildAssets/scratchedFlags.ts b/scripts/buildAssets/scratchedFlags.ts index 1df8d2671..c6f056933 100644 --- a/scripts/buildAssets/scratchedFlags.ts +++ b/scripts/buildAssets/scratchedFlags.ts @@ -1,9 +1,9 @@ import { readdir } from 'fs/promises'; -import { DATA, DOI } from '.'; import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import commitMultipleFiles, { FileChange, } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, DOI } from './constants'; export async function buildScratchedFlags() { console.log('Building scratched flags...'); diff --git a/scripts/buildAssets/shellIcons.ts b/scripts/buildAssets/shellIcons.ts index 3ff88207e..849a99c19 100644 --- a/scripts/buildAssets/shellIcons.ts +++ b/scripts/buildAssets/shellIcons.ts @@ -1,11 +1,11 @@ import { readdir } from 'fs/promises'; import sharp from 'sharp'; -import { DATA, DOI } from '.'; import { readDVPLFile } from '../../src/core/blitz/readDVPLFile'; import { readStringDVPL } from '../../src/core/blitz/readStringDVPL'; import commitMultipleFiles, { FileChange, } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, DOI } from './constants'; export async function buildShellIcons() { console.log('Building shell icons...'); diff --git a/scripts/buildAssets/tankArmors.ts b/scripts/buildAssets/tankArmors.ts index f60d1ae9a..d8a1ab748 100644 --- a/scripts/buildAssets/tankArmors.ts +++ b/scripts/buildAssets/tankArmors.ts @@ -1,13 +1,19 @@ +import { NodeIO } from '@gltf-transform/core'; import { readdir } from 'fs/promises'; -import { DATA, DOI } from '.'; -import { NATION_IDS } from '../../src/constants/nations'; -import { extractModel } from '../../src/core/blitz/extractModel'; +import { extractArmor } from '../../src/core/blitz/extractArmor'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; +import { toUniqueId } from '../../src/core/blitz/toUniqueId'; +import { DATA, DOI } from './constants'; import { VehicleDefinitionList } from './definitions'; +BigInt.prototype.toJSON = function () { + return this.toString(); +}; + export async function buildTankArmors() { console.log('Building tank armors...'); + const nodeIO = new NodeIO(); const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( (nations) => nations.filter((nation) => nation !== 'common'), ); @@ -20,17 +26,19 @@ export async function buildTankArmors() { for (const tankKey in tanks.root) { const tank = tanks.root[tankKey]; - const nationVehicleId = tank.id; - const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; + const id = toUniqueId(nation, tank.id); - if (id !== 24609) continue; // concept 1b - - const model = extractModel( - DATA, - `${DOI.collisionMeshes}/${nation}-${tankKey}`, - ); + // if (id !== 7297) continue; // 60tp + if (id !== 5137) continue; // tiger ii + // if (id !== 24609) continue; // concept 1b console.log(`Building armor ${id} @ ${nation}/${tankKey}`); + + const armor = await extractArmor(DATA, `${nation}-${tankKey}`); + + // await nodeIO.write('temp/armor/index.gltf', armor); + + // writeFile('test.sc2.json', JSON.stringify(sc2, null, 2)); } }), ); diff --git a/scripts/buildAssets/tankIcons.ts b/scripts/buildAssets/tankIcons.ts index a8ef3847e..a6ea24d95 100644 --- a/scripts/buildAssets/tankIcons.ts +++ b/scripts/buildAssets/tankIcons.ts @@ -1,5 +1,4 @@ import { readdir } from 'fs/promises'; -import { DATA, DOI } from '.'; import { NATION_IDS } from '../../src/constants/nations'; import { readBase64DVPL } from '../../src/core/blitz/readBase64DVPL'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; @@ -7,6 +6,7 @@ import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import commitMultipleFiles, { FileChange, } from '../../src/core/blitzkrieg/commitMultipleFiles'; +import { DATA, DOI } from './constants'; import { VehicleDefinitionList } from './definitions'; export interface TankParameters { diff --git a/scripts/buildAssets/tankModels.ts b/scripts/buildAssets/tankModels.ts index 9cb4b3fa5..3a3a0a6f2 100644 --- a/scripts/buildAssets/tankModels.ts +++ b/scripts/buildAssets/tankModels.ts @@ -1,11 +1,11 @@ import { NodeIO } from '@gltf-transform/core'; import { readdir, writeFile } from 'fs/promises'; import { Vector3Tuple } from 'three'; -import { DATA, DOI } from '.'; -import { NATION_IDS } from '../../src/constants/nations'; import { extractModel } from '../../src/core/blitz/extractModel'; import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; +import { toUniqueId } from '../../src/core/blitz/toUniqueId'; +import { DATA, DOI } from './constants'; import { VehicleDefinitionList } from './definitions'; import { TankParameters } from './tankIcons'; @@ -37,8 +37,7 @@ export async function buildTankModels() { for (const tankKey in tanks.root) { const tank = tanks.root[tankKey]; - const nationVehicleId = tank.id; - const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; + const id = toUniqueId(nation, tank.id); // if (id !== 15697) continue; // chieftain // if (id !== 24609) continue; // concept 1b @@ -49,13 +48,13 @@ export async function buildTankModels() { // 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 !== 5137) continue; // tiger ii // if (id !== 11633) continue; // forest witch // if (id !== 6225) continue; // fv215b // if (id !== 4481) continue; // kran // if (id !== 9489) continue; // e100 // if (id !== 12049) continue; // jg e100 - if (id !== 13569) continue; // jg e100 + // if (id !== 13569) continue; // obj 268 console.log(`Building model ${id} @ ${nation}/${tankKey}`); @@ -69,8 +68,6 @@ export async function buildTankModels() { ); writeFile(`public/test/${id}.glb`, await nodeIO.writeBinary(model)); - // await mkdir(`dist/assets/models/${id}`, { recursive: true }); - // nodeIO.write(`dist/assets/models/${id}/index.gltf`, model); } }), ); diff --git a/src/core/blitz/extractArmor.ts b/src/core/blitz/extractArmor.ts new file mode 100644 index 000000000..785701ae6 --- /dev/null +++ b/src/core/blitz/extractArmor.ts @@ -0,0 +1,226 @@ +import { Document, Node, Scene } from '@gltf-transform/core'; +import { writeFile } from 'fs/promises'; +import { range, times } from 'lodash'; +import { + Matrix4, + Quaternion, + Vector3, + Vector3Tuple, + Vector4Tuple, +} from 'three'; +import { Hierarchy, Sc2Stream } from '../streams/sc2'; +import { ScgStream, vertexAttributeVectorSizes } from '../streams/scg'; +import { VertexAttribute } from '../streams/scpg'; +import { readDVPLFile } from './readDVPLFile'; + +Buffer.prototype.toJSON = function () { + return [...(this as Buffer)].map((int) => int.toString(16)).join(''); +}; + +const MAX_FLOAT32 = 2 ** 127 * (2 - 2 ** -23); + +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', +}; + +const vertexAttributeGltfVectorSizes = { + ...vertexAttributeVectorSizes, + + [VertexAttribute.TANGENT]: 4, +} as const; + +export async function extractArmor( + data: string, + fileName: string, + baseColor?: Vector3Tuple, +) { + const sc2Path = `${data}/3d/Tanks/CollisionMeshes/${fileName}.sc2.dvpl`; + const scgPath = `${data}/3d/Tanks/CollisionMeshes/${fileName}.scg.dvpl`; + const sc2 = new Sc2Stream(await readDVPLFile(sc2Path)).sc2(); + const scg = new ScgStream(await readDVPLFile(scgPath)).scg(); + const document = new Document(); + const scene = document.createScene(); + const buffer = document.createBuffer(); + + writeFile('test.sc2.json', JSON.stringify(sc2, null, 2)); + + function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) { + hierarchies.forEach((hierarchy) => { + console.log(`hierarchy: ${hierarchy.name}`); + + const node = document.createNode(hierarchy.name); + const components = times( + hierarchy.components.count, + (index) => hierarchy.components[index.toString().padStart(4, '0')], + ); + + components.forEach((component) => { + switch (component['comp.typename']) { + case 'TransformComponent': { + const translation = new Vector3(); + const rotation = new Quaternion(); + const scale = new Vector3(); + + new Matrix4() + .multiplyMatrices( + new Matrix4().compose( + new Vector3().fromArray(component['tc.worldTranslation']), + new Quaternion().fromArray(component['tc.worldRotation']), + new Vector3().fromArray(component['tc.worldScale']), + ), + new Matrix4().compose( + new Vector3().fromArray(component['tc.localTranslation']), + new Quaternion().fromArray(component['tc.localRotation']), + new Vector3().fromArray(component['tc.localScale']), + ), + ) + .decompose(translation, rotation, scale); + + node.setTranslation(translation.toArray()); + node.setRotation(rotation.toArray() as Vector4Tuple); + node.setScale(scale.toArray()); + + break; + } + + case 'RenderComponent': { + let minLODDistanceBatchId: undefined | number; + + if (component['rc.renderObj']['rb0.lodIndex'] === -1) { + minLODDistanceBatchId = 0; + } else { + const lodList = components.find( + (component) => component['comp.typename'] === 'LodComponent', + ); + + if ( + lodList === undefined || + lodList['comp.typename'] !== 'LodComponent' // type annotation hack + ) { + throw new SyntaxError('Missing LodComponent'); + } + + let minLODDistance = Infinity; + let isFirstMaxFloat = true; + let lodIndexStreak = 0; + let lodIndexStreakIndex: undefined | number = undefined; + + range(component['rc.renderObj']['ro.batchCount']).forEach( + (id) => { + const lodIndex = + component['rc.renderObj'][`rb${id}.lodIndex`]; + const lodDistance = + lodList['lc.loddist'][`distance${lodIndex}`]; + + if (lodIndexStreakIndex === lodIndex) { + lodIndexStreak++; + } else { + lodIndexStreak = 0; + lodIndexStreakIndex = lodIndex; + } + + if ( + lodDistance < minLODDistance || + (lodDistance === minLODDistance && lodIndexStreak > 0) || + (lodDistance === MAX_FLOAT32 && isFirstMaxFloat) + ) { + if (lodDistance === MAX_FLOAT32) isFirstMaxFloat = false; + minLODDistance = lodDistance; + minLODDistanceBatchId = id; + } + }, + ); + } + + const batch = + component['rc.renderObj']['ro.batches'][ + minLODDistanceBatchId!.toString().padStart(4, '0') + ]; + const polygonGroup = scg.get(batch['rb.datasource']); + + if (!polygonGroup) { + console.warn( + `Missing polygon group ${batch['rb.datasource']} (${hierarchy.name}); skipping...`, + ); + + break; + } + + const indicesAccessor = document + .createAccessor() + .setType('SCALAR') + .setArray(new Uint16Array(polygonGroup.indices)) + .setBuffer(buffer); + const primitive = document + .createPrimitive() + .setIndices(indicesAccessor); + const attributes: Partial> = {}; + const indexedAttributes: VertexAttribute[] = []; + + polygonGroup.vertices.forEach((vertex) => { + vertex.forEach(({ attribute, value }) => { + if (attributes[attribute] === undefined) { + attributes[attribute] = []; + indexedAttributes.push(attribute); + } + + attributes[attribute]!.push(value); + }); + }); + + if (indexedAttributes.includes(VertexAttribute.TANGENT)) { + attributes[VertexAttribute.TANGENT] = attributes[ + VertexAttribute.TANGENT + ]!.map((tangent) => [...tangent, 1]); + } + + indexedAttributes.forEach((attribute) => { + const name = vertexAttributeGLTFName[attribute]; + if (!name || primitive.getAttribute(name)) return; + const vertexSize = vertexAttributeGltfVectorSizes[attribute]; + + const attributeAccessor = document + .createAccessor(name) + .setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`) + .setArray(new Float32Array(attributes[attribute]!.flat())) + .setBuffer(buffer); + primitive.setAttribute(name, attributeAccessor); + }); + + const mesh = document + .createMesh(batch['##name']) + .addPrimitive(primitive); + + node.setMesh(mesh); + + break; + } + + default: + throw new TypeError( + `Unhandled component type: ${component['comp.typename']}`, + ); + } + }); + + if (hierarchy['#hierarchy']) { + parseHierarchies(hierarchy['#hierarchy'], node); + } + + parent.addChild(node); + }); + } + + parseHierarchies(sc2['#hierarchy'], scene); + + return document; +} diff --git a/src/core/blitz/extractModel.ts b/src/core/blitz/extractModel.ts index 991260554..403af4d92 100644 --- a/src/core/blitz/extractModel.ts +++ b/src/core/blitz/extractModel.ts @@ -36,8 +36,8 @@ const vertexAttributeGltfVectorSizes = { } as const; const omitMeshNames = { - start: ['chassis_chassis_', 'chassis_track_crash_', 'HP_'], - end: ['_POINT'], + start: [], //['chassis_chassis_', 'chassis_track_crash_', 'HP_'], + end: [], //['_POINT'], }; export async function extractModel( @@ -262,7 +262,7 @@ export async function extractModel( if (!polygonGroup) { console.warn( - `Missing polygon group ${batch['rb.datasource']}; skipping...`, + `Missing polygon group ${batch['rb.datasource']} (${hierarchy.name}); skipping...`, ); break; diff --git a/src/core/streams/sc2.ts b/src/core/streams/sc2.ts index 8567f6f21..46c2fa585 100644 --- a/src/core/streams/sc2.ts +++ b/src/core/streams/sc2.ts @@ -42,6 +42,7 @@ interface EnabledPresets { interface Flags { AMBIENT_ATTENUATION_BOX: number; + BLENDING: number; } interface Properties { diff --git a/src/core/streams/scg.ts b/src/core/streams/scg.ts index 5dade80cd..5162a1570 100644 --- a/src/core/streams/scg.ts +++ b/src/core/streams/scg.ts @@ -1,3 +1,4 @@ +import { writeFile } from 'fs/promises'; import { ScpgStream, VertexAttribute } from './scpg'; interface PolygonGroupRaw { @@ -66,6 +67,8 @@ export class ScgStream extends ScpgStream { polygonGroupsRaw.push(polygonGroupRaw); } + writeFile('test.scg.json', JSON.stringify({ polygonGroupsRaw }, null, 2)); + polygonGroupsRaw.forEach((polygonGroupRaw) => { const indicesStream = new ScpgStream(polygonGroupRaw.indices); const indices: number[] = [];