diff --git a/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts b/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts index 3e6f7f590..df5d3b48a 100644 --- a/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts +++ b/packages/core/src/blitzkit/api/server/3_modelDefinitions.ts @@ -4,7 +4,13 @@ import { ChassisDefinitionsList, Crew, GunDefinitionsList, + I_HAT, + J_HAT, + K_HAT, ModelDefinitions, + parseHierarchies, + Sc2ReadStream, + ScgReadStream, TankParameters, toUniqueId, TurretDefinitionsList, @@ -12,9 +18,10 @@ import { VehicleDefinitionArmor, VehicleDefinitionList, VehicleDefinitions, + VertexAttribute, } from "@blitzkit/core"; import { parse as parsePath } from "path"; -import { Vector3Tuple } from "three"; +import { MathUtils, Vector3 as THREEVector3, Vector3Tuple } from "three"; import { Cache } from "./0_base"; import { ServerBlitzKitAPI2 } from "./2_camouflageDefinitions"; @@ -23,6 +30,87 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 { return { x: tuple[0], y: tuple[1], z: tuple[2] } satisfies Vector3; } + private async collisionBoundingBoxes( + nation: string, + tankKey: string, + turretRotation?: { yaw: number; pitch: number; roll: number }, + ) { + const boundingBoxes: Record< + string, + { min: Vector3Tuple; max: Vector3Tuple } + > = {}; + const path = `Data/3d/Tanks/CollisionMeshes/${nation}-${tankKey}`; + + if (!(await this.vfs.resolve(`${path}.sc2`))) { + throw new Error(`Missing collision mesh for ${nation}-${tankKey}`); + } + + const sc2 = new Sc2ReadStream( + (await this.vfs.file(`${path}.sc2`)).buffer as ArrayBuffer, + ).sc2(); + const scg = new ScgReadStream( + (await this.vfs.file(`${path}.scg`)).buffer as ArrayBuffer, + ).scg(); + + parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => { + for (const component of components) { + if (component["comp.typename"] !== "RenderComponent") continue; + + const polygonGroup = scg.get( + component["rc.renderObj"]["ro.batches"]["0000"]["rb.datasource"], + ); + + if (!polygonGroup) continue; + + const min: Vector3Tuple = [Infinity, Infinity, Infinity]; + const max: Vector3Tuple = [-Infinity, -Infinity, -Infinity]; + const rotate = + turretRotation && hierarchy.name.toLowerCase().startsWith("turret"); + + polygonGroup.vertices.forEach((vertex) => { + vertex.forEach(({ attribute, value }) => { + if (attribute !== VertexAttribute.VERTEX) return; + + const position = new THREEVector3(...value); + + if (rotate) { + position + .applyAxisAngle( + I_HAT, + -MathUtils.degToRad(turretRotation.pitch), + ) + .applyAxisAngle( + J_HAT, + -MathUtils.degToRad(turretRotation.roll), + ) + .applyAxisAngle( + K_HAT, + -MathUtils.degToRad(turretRotation.yaw), + ); + } + + const coordinates = position.toArray(); + + for (let axis = 0; axis < 3; axis++) { + min[axis] = Math.min(min[axis], coordinates[axis]); + max[axis] = Math.max(max[axis], coordinates[axis]); + } + }); + }); + + boundingBoxes[hierarchy.name.toLowerCase()] = { min, max }; + } + + return null; + }); + + if (!boundingBoxes.hull) { + throw new Error(`Missing hull collision mesh for ${nation}-${tankKey}`); + } + + return boundingBoxes; + } + private assignArmor( raw: VehicleDefinitionArmor[string], id: number, @@ -67,6 +155,12 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 { const tankParameters = await this.vfs.yaml( `Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, ); + const collisionBoundingBoxes = await this.collisionBoundingBoxes( + nation, + tankKey, + tankDefinition.root.hull.turretInitialRotation, + ); + const hullBoundingBox = collisionBoundingBoxes.hull; const turretOrigin = tankDefinition.root.hull.turretPositions.turret .split(" ") .map(Number) as Vector3Tuple; @@ -141,12 +235,8 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 { } : undefined, bounding_box: { - min: this.vector3TupleToBlitzkit( - tankParameters.collision.hull.bbox.min, - ), - max: this.vector3TupleToBlitzkit( - tankParameters.collision.hull.bbox.max, - ), + min: this.vector3TupleToBlitzkit(hullBoundingBox.min), + max: this.vector3TupleToBlitzkit(hullBoundingBox.max), }, turrets: {}, tracks: {}, @@ -208,26 +298,35 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 { turret.userString; + const turretCollisionName = parsePath( + turret.hitTester.collisionModel, + ).name.toLowerCase(); + const isFullYaw = -turretYaw[0] + turretYaw[1] === 360; + + if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) { + throw new Error( + `Missing turret collision mesh for ${nation}-${tankKey} ${turretKey}`, + ); + } + + const turretBoundingBox = collisionBoundingBoxes[ + turretCollisionName + ] ?? { + min: [0, 0, 0] as Vector3Tuple, + max: [0, 0, 0] as Vector3Tuple, + }; + modelDefinitions.models[tankId].turrets[turretId] = { armor: turretArmor, gun_origin: this.vector3TupleToBlitzkit(gunOrigin), model_id: turretModel, - yaw: - -turretYaw[0] + turretYaw[1] === 360 - ? undefined - : { min: turretYaw[0], max: turretYaw[1] }, + yaw: isFullYaw + ? undefined + : { min: turretYaw[0], max: turretYaw[1] }, guns: {}, bounding_box: { - min: this.vector3TupleToBlitzkit( - tankParameters.collision[ - parsePath(turret.hitTester.collisionModel).name.toLowerCase() - ].bbox.min, - ), - max: this.vector3TupleToBlitzkit( - tankParameters.collision[ - parsePath(turret.hitTester.collisionModel).name.toLowerCase() - ].bbox.max, - ), + min: this.vector3TupleToBlitzkit(turretBoundingBox.min), + max: this.vector3TupleToBlitzkit(turretBoundingBox.max), }, }; diff --git a/packages/core/src/blitzkit/index.ts b/packages/core/src/blitzkit/index.ts index 700e57018..20298bcee 100644 --- a/packages/core/src/blitzkit/index.ts +++ b/packages/core/src/blitzkit/index.ts @@ -25,6 +25,7 @@ export * from "./sluggify"; export * from "./staticAsset"; export * from "./sum"; export * from "./tankDefinitions"; +export * from "./tankBoundingBox"; export * from "./tankIcon"; export * from "./tankNames"; export * from "./unionBoundingBox"; diff --git a/packages/core/src/blitzkit/tankBoundingBox.ts b/packages/core/src/blitzkit/tankBoundingBox.ts new file mode 100644 index 000000000..9e3408d74 --- /dev/null +++ b/packages/core/src/blitzkit/tankBoundingBox.ts @@ -0,0 +1,32 @@ +import { ModelDefinition, TurretModelDefinition } from "../protos"; +import { unionBoundingBox } from "./unionBoundingBox"; + +export function tankBoundingBox( + tankModelDefinition: ModelDefinition, + turretModelDefinition: TurretModelDefinition, +) { + const hull = tankModelDefinition.bounding_box!; + const turret = turretModelDefinition.bounding_box!; + const origin = tankModelDefinition.turret_origin!; + + if ( + turret.min!.x === turret.max!.x && + turret.min!.y === turret.max!.y && + turret.min!.z === turret.max!.z + ) { + return unionBoundingBox(hull, hull); + } + + return unionBoundingBox(hull, { + min: { + x: turret.min!.x + origin.x, + y: turret.min!.y + origin.z, + z: turret.min!.z + origin.y, + }, + max: { + x: turret.max!.x + origin.x, + y: turret.max!.y + origin.z, + z: turret.max!.z + origin.y, + }, + }); +} diff --git a/packages/core/src/streams/sc2.ts b/packages/core/src/streams/sc2.ts index 8b4837bab..73e2a2147 100644 --- a/packages/core/src/streams/sc2.ts +++ b/packages/core/src/streams/sc2.ts @@ -90,6 +90,28 @@ export interface Hierarchy { name: string; } +export function parseHierarchies( + hierarchies: Hierarchy[], + parent: Parent, + parseHierarchy: ( + hierarchy: Hierarchy, + components: Component[], + parent: Parent, + ) => Parent | undefined, +) { + for (const hierarchy of hierarchies) { + const components = Array.from( + { length: hierarchy.components.count }, + (_, index) => hierarchy.components[index.toString().padStart(4, "0")], + ); + const child = parseHierarchy(hierarchy, components, parent); + + if (child !== undefined && hierarchy["#hierarchy"]) { + parseHierarchies(hierarchy["#hierarchy"], child, parseHierarchy); + } + } +} + // 0001, 0002, 0003...: Component type Components = { count: number; diff --git a/packages/website/src/components/TankSearch/components/Card.tsx b/packages/website/src/components/TankSearch/components/Card.tsx index 97182487a..b210ef1c6 100644 --- a/packages/website/src/components/TankSearch/components/Card.tsx +++ b/packages/website/src/components/TankSearch/components/Card.tsx @@ -1,7 +1,7 @@ import { normalizeBoundingBox, resolveDpm, - unionBoundingBox, + tankBoundingBox, type TankDefinition, } from "@blitzkit/core"; import { useMemo } from "react"; @@ -112,10 +112,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) { return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0); case "survivability.volume": { const dimensions = normalizeBoundingBox( - unionBoundingBox( - tankModelDefinition.bounding_box!, - turretModelDefinition.bounding_box!, - ), + tankBoundingBox(tankModelDefinition, turretModelDefinition), ); return (dimensions.x * dimensions.y * dimensions.z).toFixed(0); @@ -123,10 +120,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) { case "survivability.length": { const bounds = normalizeBoundingBox( - unionBoundingBox( - tankModelDefinition.bounding_box!, - turretModelDefinition.bounding_box!, - ), + tankBoundingBox(tankModelDefinition, turretModelDefinition), ); return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0); diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx index 35321dc2c..eb8ac1ed9 100644 --- a/packages/website/src/components/TankSearch/index.tsx +++ b/packages/website/src/components/TankSearch/index.tsx @@ -3,7 +3,7 @@ import { normalizeBoundingBox, resolveDpm, SEARCH_KEYS, - unionBoundingBox, + tankBoundingBox, type TankDefinition, } from "@blitzkit/core"; import { literals } from "@blitzkit/i18n/src/literals"; @@ -137,16 +137,10 @@ export const TankSearch = memo( const bTurretModelDefinition = bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; const aSize = normalizeBoundingBox( - unionBoundingBox( - aTankModelDefinition.bounding_box!, - aTurretModelDefinition.bounding_box!, - ), + tankBoundingBox(aTankModelDefinition, aTurretModelDefinition), ); const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.bounding_box!, - bTurretModelDefinition.bounding_box!, - ), + tankBoundingBox(bTankModelDefinition, bTurretModelDefinition), ); const aVolume = aSize.x * aSize.y * aSize.z; const bVolume = bSize.x * bSize.y * bSize.z; @@ -164,16 +158,10 @@ export const TankSearch = memo( const bTurretModelDefinition = bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; const aSize = normalizeBoundingBox( - unionBoundingBox( - aTankModelDefinition.bounding_box!, - aTurretModelDefinition.bounding_box!, - ), + tankBoundingBox(aTankModelDefinition, aTurretModelDefinition), ); const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.bounding_box!, - bTurretModelDefinition.bounding_box!, - ), + tankBoundingBox(bTankModelDefinition, bTurretModelDefinition), ); const aLength = Math.max(aSize.x, aSize.y, aSize.z); const bLength = Math.max(bSize.x, bSize.y, bSize.z); diff --git a/packages/website/src/core/blitzkit/tankCharacteristics.ts b/packages/website/src/core/blitzkit/tankCharacteristics.ts index 51301190a..251581fc7 100644 --- a/packages/website/src/core/blitzkit/tankCharacteristics.ts +++ b/packages/website/src/core/blitzkit/tankCharacteristics.ts @@ -8,8 +8,8 @@ import { resolvePenetrationCoefficient, ShellType, sum, + tankBoundingBox, TankClass, - unionBoundingBox, type EngineDefinition, type EquipmentDefinitions, type GunDefinition, @@ -335,10 +335,7 @@ export function tankCharacteristics( ); const size = normalizeBoundingBox( - unionBoundingBox( - tankModelDefinition.bounding_box!, - turretModelDefinition.bounding_box!, - ), + tankBoundingBox(tankModelDefinition, turretModelDefinition), ); const weightKg = tank.weight + engine.weight + track.weight + turret.weight + gun.weight; diff --git a/packages/website/src/pages/[...chunk_api]/tanks/[id]/collision.glb.ts b/packages/website/src/pages/[...chunk_api]/tanks/[id]/collision.glb.ts index 7bff4a081..dbfd14255 100644 --- a/packages/website/src/pages/[...chunk_api]/tanks/[id]/collision.glb.ts +++ b/packages/website/src/pages/[...chunk_api]/tanks/[id]/collision.glb.ts @@ -1,16 +1,15 @@ import { + parseHierarchies, Sc2ReadStream, ScgReadStream, toUniqueId, VertexAttribute, vertexAttributeVectorSizes, - type Hierarchy, type VehicleDefinitionList, } from "@blitzkit/core"; import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract"; import { Accessor, Document, Node, NodeIO, Scene } from "@gltf-transform/core"; import type { APIContext } from "astro"; -import { times } from "lodash-es"; import { vfs } from "../../../../core/blitzkit/vfs"; export { getStaticPaths } from "./_index"; @@ -56,13 +55,10 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) { const scene = document.createScene(); const buffer = document.createBuffer(); - function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) { - hierarchies.forEach((hierarchy) => { - const components = times( - hierarchy.components.count, - (index) => hierarchy.components[index.toString().padStart(4, "0")], - ); - + parseHierarchies( + sc2["#hierarchy"], + scene, + (hierarchy, components, parent) => { components.forEach((component) => { switch (component["comp.typename"]) { case "TransformComponent": @@ -163,13 +159,9 @@ async function extractArmor(vfs: AbstractVFS, fileName: string) { } }); - if (hierarchy["#hierarchy"]) { - parseHierarchies(hierarchy["#hierarchy"], parent); - } - }); - } - - parseHierarchies(sc2["#hierarchy"], scene); + return parent; + }, + ); return document; } diff --git a/packages/website/src/pages/[...chunk_api]/tanks/[id]/model.glb.ts b/packages/website/src/pages/[...chunk_api]/tanks/[id]/model.glb.ts index 6eae18f85..3b531668c 100644 --- a/packages/website/src/pages/[...chunk_api]/tanks/[id]/model.glb.ts +++ b/packages/website/src/pages/[...chunk_api]/tanks/[id]/model.glb.ts @@ -1,13 +1,13 @@ import { bufferToBigInt, DdsReadStream, + parseHierarchies, PvrReadStream, Sc2ReadStream, ScgReadStream, toUniqueId, VertexAttribute, type ConfigArchive, - type Hierarchy, type TankParameters, type Textures, type VehicleDefinitionList, @@ -240,8 +240,10 @@ async function extractModel(vfs: AbstractVFS, path: string) { materials.set(id, resolvedMaterial); }); - function parseHierarchies(hierarchies: Hierarchy[], parent: Scene | Node) { - hierarchies.forEach((hierarchy) => { + parseHierarchies( + sc2["#hierarchy"], + scene, + (hierarchy, components, parent) => { if ( omitMeshNames.start.some((omit) => hierarchy.name.startsWith(omit)) || omitMeshNames.end.some((omit) => hierarchy.name.endsWith(omit)) @@ -249,10 +251,6 @@ async function extractModel(vfs: AbstractVFS, path: string) { return; 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"]) { @@ -361,15 +359,11 @@ async function extractModel(vfs: AbstractVFS, path: string) { } }); - if (hierarchy["#hierarchy"]) { - parseHierarchies(hierarchy["#hierarchy"], node); - } - parent.addChild(node); - }); - } - parseHierarchies(sc2["#hierarchy"], scene); + return node; + }, + ); scene.addChild(document.createNode("test"));