diff --git a/.vscode/settings.json b/.vscode/settings.json index 2d6a976a9..1533c2538 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "atgm", "bepaint", "BINORMAL", + "Binormals", "blitzstars", "classname", "cleandist", @@ -27,6 +28,7 @@ "fullstats", "fuzzysort", "gamemode", + "GLTF", "immer", "JOINTINDEX", "JOINTWEIGHT", @@ -43,6 +45,7 @@ "ownedtanks", "playerachievements", "playerinfo", + "pngjs", "ratingsleaderboard", "ratingsplot", "researchable", diff --git a/package.json b/package.json index 348fd9998..42c6fa9c3 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "lodash": "^4.17.21", "markdown-escape": "^2.0.0", "next": "^14.0.3", + "parse-dds": "^1.2.1", + "pngjs": "^7.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-fuzzysort": "^1.0.1", @@ -75,6 +77,8 @@ "@types/lz4": "^0.6.4", "@types/markdown-escape": "^1.1.0", "@types/node": "^20.6.5", + "@types/parse-dds": "^1.0.3", + "@types/pngjs": "^6.0.4", "@types/react": "^18.2.22", "@types/three": "^0.159.0", "@vanilla-extract/next-plugin": "^2.3.1", diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 20d84144c..a76d9de30 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -1,14 +1,18 @@ -import { Document, Node, NodeIO, Scene } from '@gltf-transform/core'; +import { Document, Material, Node, NodeIO, Scene } from '@gltf-transform/core'; import { config } from 'dotenv'; import { existsSync, writeFileSync } from 'fs'; -import { mkdir, readdir, rm } from 'fs/promises'; -import { times } from 'lodash'; +import { mkdir, readFile, readdir, rm } from 'fs/promises'; +import { range, times } from 'lodash'; import { argv } from 'process'; import sharp from 'sharp'; import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three'; import { TankType } from '../src/components/Tanks'; import { NATION_IDS } from '../src/constants/nations'; -import { SCPGStream, VertexType } from '../src/core/blitz/SCPGStream'; +import { + SCPGStream, + VertexAttribute, + vertexAttributeVectorSizes, +} from '../src/core/blitz/SCPGStream'; import { readBase64DVPL } from '../src/core/blitz/readBase64DVPL'; import { readDVPLFile } from '../src/core/blitz/readDVPLFile'; import { readStringDVPL } from '../src/core/blitz/readStringDVPL'; @@ -29,6 +33,19 @@ import { } from '../src/core/blitzkrieg/tankDefinitions'; import { Hierarchy } from '../src/types/sc2'; +const vertexTypeGLTFName: 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', +}; + (BigInt.prototype as any).toJSON = function () { return this.toString(); }; @@ -533,6 +550,30 @@ if (allTargets || targets?.includes('tankModels')) { const document = new Document(); const scene = document.createScene(); const buffer = document.createBuffer(); + const materials: Record = {}; + + // writeFileSync( + // 'test.txt', + // scg + // .map((group) => + // group.vertices + // .map((vertex) => + // vertex + // .map( + // (vertexProperty) => + // `${VertexAttribute[vertexProperty.type].padEnd( + // 14, + // ' ', + // )} ${vertexProperty.value + // .map((value) => value.toString().padEnd(25, ' ')) + // .join(' ')}`, + // ) + // .join('\n'), + // ) + // .join('\n\n'), + // ) + // .join('\n\n#######\n\n'), + // ); function parseHierarchies( hierarchies: Hierarchy[], @@ -598,68 +639,95 @@ if (allTargets || targets?.includes('tankModels')) { } case 'RenderComponent': { - times( + const batchIds = range( component['rc.renderObj']['ro.batchCount'], - (index) => - component['rc.renderObj']['ro.batches'][ - index.toString().padStart(4, '0') - ], - ).map((batch) => { - const polygonGroup = scg.find( - (group) => group.id === batch['rb.datasource'], - ); - const unpackedVertices: number[] = []; + ); + let minLODIndex = Infinity; + let minLODBatchId = Infinity; - if (!polygonGroup) - throw new Error( - `Missing polygon group ${batch['rb.datasource']}`, - ); + batchIds.forEach((batchId) => { + const thisLODIndex = + component['rc.renderObj'][`rb${batchId}.lodIndex`]; - polygonGroup.vertices.forEach((vertex) => { - vertex.map((vertexItem) => { - switch (vertexItem.type) { - case VertexType.VERTEX: { - unpackedVertices.push(...vertexItem.value); - break; - } - - // default: - // throw new TypeError( - // `Unhandled vertex type: ${ - // VertexType[vertexItem.type] - // } (${vertexItem.type})`, - // ); - } - }); - }); - - const vertexAccessor = document - .createAccessor() - .setType('VEC3') - .setArray(new Float32Array(unpackedVertices)) - .setBuffer(buffer); - const indexAccessor = document - .createAccessor() - .setType('SCALAR') - .setArray(new Uint16Array(polygonGroup.indices)) - .setBuffer(buffer); - - const primitive = document - .createPrimitive() - .setIndices(indexAccessor) - .setAttribute('POSITION', vertexAccessor); - - const mesh = document - .createMesh(batch['##name']) - .addPrimitive(primitive); - - const polygons = document - .createNode(batch['##name']) - .setMesh(mesh); - - node.addChild(polygons); + if (thisLODIndex <= minLODIndex) { + minLODIndex = thisLODIndex; + minLODBatchId = batchId; + } }); + const batch = + component['rc.renderObj']['ro.batches'][ + minLODBatchId.toString().padStart(4, '0') + ]; + const polygonGroup = scg.find( + (group) => group.id === batch['rb.datasource'], + ); + const material = materials[batch['rb.nmatname']]; + + if (!polygonGroup) { + throw new Error( + `Missing polygon group ${batch['rb.datasource']}`, + ); + } + + const indexedVertexTypes: VertexAttribute[] = []; + const unpackedVertices: Partial< + Record + > = {}; + + polygonGroup.vertices.forEach((vertex) => { + vertex.map((vertexItem) => { + if (!(vertexItem.type in unpackedVertices)) { + unpackedVertices[vertexItem.type] = []; + indexedVertexTypes.push(vertexItem.type); + } + + unpackedVertices[vertexItem.type]!.push( + ...vertexItem.value, + ); + }); + }); + + const indexAccessor = document + .createAccessor() + .setType('SCALAR') + .setArray(new Uint16Array(polygonGroup.indices)) + .setBuffer(buffer); + const primitive = document + .createPrimitive() + .setIndices(indexAccessor) + .setMaterial(material); + + indexedVertexTypes.forEach((type) => { + if (!(type in vertexTypeGLTFName)) { + return; + // throw new TypeError( + // `Unhandled vertex type GLTF name: ${VertexType[type]} (${type})`, + // ); + } + + const vertexSize = vertexAttributeVectorSizes[type]; + + if (!primitive.getAttribute(vertexTypeGLTFName[type]!)) { + primitive.setAttribute( + vertexTypeGLTFName[type]!, + document + .createAccessor() + .setType( + vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`, + ) + .setArray(new Float32Array(unpackedVertices[type]!)) + .setBuffer(buffer), + ); + } + }); + + const mesh = document + .createMesh(batch['##name']) + .addPrimitive(primitive); + + node.setMesh(mesh); + break; } @@ -678,35 +746,66 @@ if (allTargets || targets?.includes('tankModels')) { }); } + await Promise.all( + sc2['#dataNodes'].map(async (node) => { + const material = document.createMaterial(node.materialName); + + // if (typeof node.configCount === 'number') { + // try { + // const config = node.configArchive_0; + // const texturePath = `${DATA}/${DOI['3d']}/${dirname( + // parameters.resourcesPath.blitzModelPath, + // )}/${config.textures.albedo.replace( + // /\.tex$/, + // '.dx11.dds.dvpl', + // )}`; + // console.log(texturePath); + // const dds = await readDVPLFile(texturePath); + // const image = parseDDS(dds).images[0]; + // const textureArray = new Uint8Array( + // dds, + // image.offset, + // image.length, + // ); + // const png = new PNG({ + // width: image.shape[0], + // height: image.shape[1], + // }); + + // for (var index = 0; index < textureArray.length; index += 4) { + // png.data[index] = textureArray[index]; + // png.data[index + 1] = textureArray[index + 1]; + // png.data[index + 2] = textureArray[index + 2]; + // png.data[index + 3] = textureArray[index + 3]; + // } + + // const pngBuffer = PNG.sync.write(png); + // const texture = document + // .createTexture(config.configName) + // .setImage(await readFile('C:/Users/coola/Downloads/33e.jpg')); + + // material.setBaseColorTexture(texture); + // } catch (error) { + // // console.log(error); + // } + // } + + if (node.configCount) { + console.log('uhhh'); + const config = node.configArchive_0; + const texturePath = 'C:/Users/coola/Downloads/33e.jpg'; + const textureBuffer = await readFile(texturePath); + const texture = document.createTexture(config?.configName); + + texture.setImage(textureBuffer); + material.setBaseColorTexture(texture); + } + + materials[node['#id'].readBigUInt64LE().toString()] = material; + }), + ); parseHierarchies(sc2['#hierarchy'], scene); writeFileSync('test.glb', await new NodeIO().writeBinary(document)); - - // sc2['#hierarchy'][0]['#hierarchy']!.forEach((hierarchy) => { - // times( - // hierarchy.components.count, - // (index) => hierarchy.components[index.toString().padStart(4, '0')], - // ).forEach((component) => { - // if (component['comp.typename'] !== 'RenderComponent') return; - - // hierarchy.name, - // times( - // component['rc.renderObj']['ro.batchCount'], - // (index) => - // component['rc.renderObj']['ro.batches'][ - // index.toString().padStart(4, '0') - // ], - // ).map((batch) => { - // const polygonGroup = scg.find( - // (group) => group.id === batch['rb.datasource'], - // ); - - // console.log(batch['rb.datasource'], polygonGroup !== undefined); - // }); - // }); - // }); - - // writeFile('test.sc2.json', JSON.stringify(sc2, null, 2)); - // writeFile('test.scg.json', JSON.stringify(scg, null, 2)); } }), ); diff --git a/src/core/blitz/SCPGStream.ts b/src/core/blitz/SCPGStream.ts index 4934f71cc..b850e905c 100644 --- a/src/core/blitz/SCPGStream.ts +++ b/src/core/blitz/SCPGStream.ts @@ -34,7 +34,7 @@ enum KAType { UNCONFIRMED_TRANSFORM = 29, } -export enum VertexType { +export enum VertexAttribute { VERTEX, NORMAL, COLOR, @@ -46,6 +46,7 @@ export enum VertexType { BINORMAL, HARD_JOINTINDEX, PIVOT4, + PIVOT_DEPRECATED, FLEXIBILITY, ANGLE_SIN_COS, JOINTINDEX, @@ -56,6 +57,29 @@ export enum VertexType { CUBETEXCOORD3, } +export const vertexAttributeVectorSizes = { + [VertexAttribute.VERTEX]: 3, + [VertexAttribute.NORMAL]: 3, + [VertexAttribute.COLOR]: 1, + [VertexAttribute.TEXCOORD0]: 2, + [VertexAttribute.TEXCOORD1]: 2, + [VertexAttribute.TEXCOORD2]: 2, + [VertexAttribute.TEXCOORD3]: 2, + [VertexAttribute.TANGENT]: 3, + [VertexAttribute.BINORMAL]: 3, + [VertexAttribute.HARD_JOINTINDEX]: 1, + [VertexAttribute.CUBETEXCOORD0]: 3, + [VertexAttribute.CUBETEXCOORD1]: 3, + [VertexAttribute.CUBETEXCOORD2]: 3, + [VertexAttribute.CUBETEXCOORD3]: 3, + [VertexAttribute.PIVOT4]: 4, + [VertexAttribute.PIVOT_DEPRECATED]: 3, + [VertexAttribute.FLEXIBILITY]: 1, + [VertexAttribute.ANGLE_SIN_COS]: 2, + [VertexAttribute.JOINTINDEX]: 4, + [VertexAttribute.JOINTWEIGHT]: 4, +} as const; + interface PolygonGroupRaw { '##name': 'PolygonGroup'; '#id': Buffer; @@ -71,39 +95,13 @@ interface PolygonGroupRaw { vertexFormat: number; vertices: Buffer; } -type BlitzkriegVertex = { type: VertexType; value: number[] }[]; +type BlitzkriegVertex = { type: VertexAttribute; value: number[] }[]; interface BlitzkriegPolygonGroup { id: bigint; vertices: BlitzkriegVertex[]; indices: number[]; } -const VECTOR_SIZES = [ - [VertexType.COLOR, VertexType.FLEXIBILITY, VertexType.HARD_JOINTINDEX], - [ - VertexType.TEXCOORD0, - VertexType.TEXCOORD1, - VertexType.TEXCOORD2, - VertexType.TEXCOORD3, - VertexType.ANGLE_SIN_COS, - ], - [ - VertexType.VERTEX, - VertexType.NORMAL, - VertexType.BINORMAL, - VertexType.CUBETEXCOORD0, - VertexType.CUBETEXCOORD1, - VertexType.CUBETEXCOORD2, - VertexType.CUBETEXCOORD3, - ], - [ - VertexType.PIVOT4, - VertexType.JOINTINDEX, - VertexType.JOINTWEIGHT, - VertexType.TANGENT, - ], -]; - export class SCPGStream { public index = 0; constructor(public buffer: Buffer) {} @@ -197,6 +195,21 @@ export class SCPGStream { return this.consumeMatrixN(4); } + getVertexStride(vertexFormat: number) { + const flags = vertexFormat.toString(2); + + return Object.values(VertexAttribute) + .filter((value) => typeof value === 'number') + .reduce( + (accumulator, attribute) => + flags[attribute as VertexAttribute] === '1' + ? accumulator + + vertexAttributeVectorSizes[attribute as VertexAttribute] * 4 + : accumulator, + 0, + ); + } + consumeSCGHeader() { return { name: this.consumeAscii(4), @@ -228,36 +241,25 @@ export class SCPGStream { const verticesStream = new SCPGStream(polygonGroupRaw.vertices); const vertices: BlitzkriegVertex[] = []; - const vertexFormatBuffer = Buffer.alloc(4); - vertexFormatBuffer.writeUInt32LE(polygonGroupRaw.vertexFormat, 0); - const vertexFormat = [...vertexFormatBuffer] - .map((number) => number.toString(2)) - .join('') - .split('') - .map((bitString, index) => (bitString === '1' ? index : null)) - .filter((type) => type !== null) as VertexType[]; + + const vertexFormat = ( + polygonGroupRaw.vertexFormat + .toString(2) + .split('') + .map((bitString, index) => + bitString === '1' ? index : null, + ) as VertexAttribute[] + ).filter((type) => type !== null); for (let index = 0; index < polygonGroupRaw.vertexCount; index++) { - vertexFormat.forEach((type) => { - const resolved = VECTOR_SIZES.some((types, size) => { - if (types.includes(type)) { - if (!(index in vertices)) vertices[index] = []; - vertices[index].push({ - type, - value: verticesStream.consumeVectorN(size + 1), - }); - return true; - } - }); - - if (!resolved) { - throw new TypeError(`Unknown vertex type: ${type}`); - } - }); + vertices[index] = vertexFormat.map((type) => ({ + type, + value: verticesStream.consumeVectorN( + vertexAttributeVectorSizes[type], + ), + })); } - console.log(verticesStream.readRemaining().length); - return { id: polygonGroupRaw['#id'].readBigUInt64LE(), vertices, diff --git a/src/types/sc2.ts b/src/types/sc2.ts index 602647669..77f6db2ea 100644 --- a/src/types/sc2.ts +++ b/src/types/sc2.ts @@ -11,10 +11,16 @@ type DataNode = { '#id': Buffer; materialName: string; parentMaterialKey?: string; - configCount?: number; qualityGroup?: string; fxName?: string; -} & Record<`configArchive_${number}`, ConfigArchive>; +} & ( + | ({ + configCount: number; + } & Record<`configArchive_${number}`, ConfigArchive>) + | { + configCount: undefined; + } +); interface ConfigArchive { configName: string; @@ -103,7 +109,7 @@ interface RbAabbox { maximum: number[]; } -type Component = +export type Component = | { 'comp.typename': 'TransformComponent'; 'tc.localRotation': Vector4Tuple; diff --git a/yarn.lock b/yarn.lock index 9725b6fe6..fb5b4c5a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2387,6 +2387,18 @@ resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz#90267db13f64d6e9ccb5ae3eac92786a7c77a516" integrity sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A== +"@types/parse-dds@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/parse-dds/-/parse-dds-1.0.3.tgz#a1bb00421906094510d88c64af978564d3c35c9b" + integrity sha512-LD9BruIpfP6Z9aAJ488h+PwtSwLzM0NO+Rs1l/zjcPLBTqR31OnIGmAvJYhGiHhbOn7R0YXw1gL5bImNiK6HNw== + +"@types/pngjs@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.4.tgz#9a457aebabd944efde1a773a0fa1d74933e8021b" + integrity sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ== + dependencies: + "@types/node" "*" + "@types/prop-types@*": version "15.7.11" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" @@ -4907,6 +4919,11 @@ parse-css-color@^0.2.1: color-name "^1.1.4" hex-rgb "^4.1.0" +parse-dds@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/parse-dds/-/parse-dds-1.2.1.tgz#b16533e00c4db91178b3460271361df4cb328164" + integrity sha512-oE88N8mzUi9d26tAaKVc0clxi6AjeTSWpLwgtcEVjXTYY1MW58w8FctmM9gGgvFd6YGu1/eFFYVeDVnlMsKBtA== + parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -5032,6 +5049,11 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" +pngjs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== + postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"