From 4ab03e56a2b0ba7282694a5952760f03552c7213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:22:48 -0500 Subject: [PATCH] feat: support transparency --- packages/core/src/streams/sc2.ts | 203 +++++++++--------- packages/i18n/src/literals.ts | 6 +- .../scripts/src/buildAssets/tankModels.ts | 4 +- .../src/core/blitz/extractModel/index.ts | 139 ++++++------ .../scripts/src/core/blitz/readBaseColor.ts | 18 +- packages/scripts/src/core/blitz/readNormal.ts | 2 +- .../src/core/blitz/readRoughnessMetallic.ts | 2 +- 7 files changed, 190 insertions(+), 184 deletions(-) diff --git a/packages/core/src/streams/sc2.ts b/packages/core/src/streams/sc2.ts index 506aee783..bfef4da51 100644 --- a/packages/core/src/streams/sc2.ts +++ b/packages/core/src/streams/sc2.ts @@ -1,20 +1,19 @@ -import { Vector3Tuple, Vector4Tuple } from 'three'; -import { ScpgReadStream } from './scpg'; +import { Vector3Tuple, Vector4Tuple } from "three"; +import { ScpgReadStream } from "./scpg"; export interface SC2 { - '#dataNodes': DataNode[]; - '#hierarchy': Hierarchy[]; - '#sceneComponents': SceneComponents; + "#dataNodes": DataNode[]; + "#hierarchy": Hierarchy[]; + "#sceneComponents": SceneComponents; } type DataNode = { - '##name': 'NMaterial'; - '#id': ArrayBuffer; + "##name": "NMaterial"; + "#id": ArrayBuffer; materialName: string; parentMaterialKey?: bigint; qualityGroup?: string; fxName?: string; - textures?: Textures; } & ( | ({ @@ -80,8 +79,8 @@ export interface Textures { } export interface Hierarchy { - '##name': 'Entity'; - '#hierarchy'?: Hierarchy[]; + "##name": "Entity"; + "#hierarchy"?: Hierarchy[]; components: Components; flags: number; id: bigint; @@ -97,12 +96,12 @@ type Components = { type RoBatches = Record< string, { - '##name': 'RenderBatch'; - 'rb.aabbox': RbAabbox; - 'rb.classname': 'RenderBatch'; - 'rb.datasource': bigint; - 'rb.nmatname': bigint; - 'rb.sortingKey': number; + "##name": "RenderBatch"; + "rb.aabbox": RbAabbox; + "rb.classname": "RenderBatch"; + "rb.datasource": bigint; + "rb.nmatname": bigint; + "rb.sortingKey": number; } >; @@ -113,50 +112,50 @@ interface RbAabbox { export type Component = | { - 'comp.typename': 'TransformComponent'; - 'tc.localRotation': Vector4Tuple; - 'tc.localScale': Vector3Tuple; - 'tc.localTranslation': Vector3Tuple; - 'tc.worldRotation': Vector4Tuple; - 'tc.worldScale': Vector3Tuple; - 'tc.worldTranslation': Vector3Tuple; + "comp.typename": "TransformComponent"; + "tc.localRotation": Vector4Tuple; + "tc.localScale": Vector3Tuple; + "tc.localTranslation": Vector3Tuple; + "tc.worldRotation": Vector4Tuple; + "tc.worldScale": Vector3Tuple; + "tc.worldTranslation": Vector3Tuple; } | { - 'comp.typename': 'RenderComponent'; - 'rc.renderObj': { - '##name': 'Mesh'; - 'ro.batchCount': number; - 'ro.batches': RoBatches; - 'ro.debugflags': number; - 'ro.flags': number; - 'ro.notShadowOnly': boolean; - 'ro.sOclCull': boolean; - 'ro.sOclIndex': number; - } & Record<`rb${number}.${'lodIndex' | 'switchIndex'}`, number>; + "comp.typename": "RenderComponent"; + "rc.renderObj": { + "##name": "Mesh"; + "ro.batchCount": number; + "ro.batches": RoBatches; + "ro.debugflags": number; + "ro.flags": number; + "ro.notShadowOnly": boolean; + "ro.sOclCull": boolean; + "ro.sOclIndex": number; + } & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number>; } | { - 'comp.typename': 'SlotComponent'; - 'sc.attachmentRotation': number[]; - 'sc.attachmentScale': number[]; - 'sc.attachmentTranslation': number[]; - 'sc.configFilePath': string; - 'sc.slotName': string; - 'sc.template': string; - 'sc.typeFiltersCount': number; + "comp.typename": "SlotComponent"; + "sc.attachmentRotation": number[]; + "sc.attachmentScale": number[]; + "sc.attachmentTranslation": number[]; + "sc.configFilePath": string; + "sc.slotName": string; + "sc.template": string; + "sc.typeFiltersCount": number; } | { - 'comp.typename': 'LodComponent'; - 'lc.loddist': Record<`distance${number}`, number>; + "comp.typename": "LodComponent"; + "lc.loddist": Record<`distance${number}`, number>; } | { - 'comp.typename': 'DecorItemComponent'; + "comp.typename": "DecorItemComponent"; drawOrder: number; shouldApplyCamo: boolean; vanishTargetAlpha: number; vanishWhenArmorShown: boolean; } | { - 'comp.typename': 'NewSlotComponent'; + "comp.typename": "NewSlotComponent"; slotItemsCount: number; slots: { [key: string]: { @@ -171,109 +170,109 @@ export type Component = }; } | { - 'comp.typename': 'ScenarioComponent'; - 'scenario.scriptId': string; + "comp.typename": "ScenarioComponent"; + "scenario.scriptId": string; } | ({ - 'comp.typename': 'StateSwitcherComponent'; - 'ssc.activeState': number; - 'ssc.statesCount': number; + "comp.typename": "StateSwitcherComponent"; + "ssc.activeState": number; + "ssc.statesCount": number; } & Record<`ssc.state${number}`, string>) | { - 'comp.typename': 'CustomPropertiesComponent'; - 'cpc.properties.archive': { - 'editor.donotremove': number; + "comp.typename": "CustomPropertiesComponent"; + "cpc.properties.archive": { + "editor.donotremove": number; }; } | ({ - 'comp.typename': 'ActionComponent'; - 'ac.actionCount': number; + "comp.typename": "ActionComponent"; + "ac.actionCount": number; } & Record< string, // 0000, 0001, ... { - 'act.delay': number; - 'act.delayVariation': number; - 'act.entityName': string; - 'act.event': number; - 'act.eventToTrigger': string; - 'act.motionFrameTime': number; - 'act.motionSpeed': number; - 'act.stateActivated': number; - 'act.stopAfterNRepeats': number; - 'act.stopWhenEmpty': boolean; - 'act.switchIndex': number; - 'act.type': number; - 'act.userEventId': string; + "act.delay": number; + "act.delayVariation": number; + "act.entityName": string; + "act.event": number; + "act.eventToTrigger": string; + "act.motionFrameTime": number; + "act.motionSpeed": number; + "act.stateActivated": number; + "act.stopAfterNRepeats": number; + "act.stopWhenEmpty": boolean; + "act.switchIndex": number; + "act.type": number; + "act.userEventId": string; } >) | { - 'comp.typename': 'ParticleEffectComponent'; - 'pe.clearOnRestart': boolean; - 'pe.effectDuration': number; - 'pe.emitters': { - '##name': string; - 'emitter.data': { - 'emitter.id': `${number}`; - 'emitter.quality': string; + "comp.typename": "ParticleEffectComponent"; + "pe.clearOnRestart": boolean; + "pe.effectDuration": number; + "pe.emitters": { + "##name": string; + "emitter.data": { + "emitter.id": `${number}`; + "emitter.quality": string; }[]; - 'emitter.position': Vector3Tuple; - 'emitter.rotation': Vector4Tuple; - 'emitter.scale': Vector3Tuple; + "emitter.position": Vector3Tuple; + "emitter.rotation": Vector4Tuple; + "emitter.scale": Vector3Tuple; }[]; - 'pe.inheritScale': boolean; - 'pe.nestedEmitters': boolean; - 'pe.repeatsCount': number; - 'pe.startFromTime': number; - 'pe.stopWhenEmpty': boolean; - 'pe.version': number; - 'ro.flags': number; + "pe.inheritScale": boolean; + "pe.nestedEmitters": boolean; + "pe.repeatsCount": number; + "pe.startFromTime": number; + "pe.stopWhenEmpty": boolean; + "pe.version": number; + "ro.flags": number; } | { - 'comp.typename': 'TankElementComponent'; + "comp.typename": "TankElementComponent"; isDestructible: boolean; shouldCastShadow: boolean; shouldGenerateSilhouette: boolean; } | { - 'comp.typename': 'MotionComponent'; - 'motion.filepath': string; - 'motion.playbackRate': number; - 'simpleMotion.repeatsCount': number; + "comp.typename": "MotionComponent"; + "motion.filepath": string; + "motion.playbackRate": number; + "simpleMotion.repeatsCount": number; } | { - 'comp.typename': 'SkeletonComponent'; + "comp.typename": "SkeletonComponent"; joints: Record< string, // 0000, 0001, ... { - 'joint.bbox.max': Vector3Tuple; - 'joint.bbox.min': Vector3Tuple; - 'joint.bindPose': [ + "joint.bbox.max": Vector3Tuple; + "joint.bbox.min": Vector3Tuple; + "joint.bindPose": [ Vector4Tuple, Vector4Tuple, Vector4Tuple, Vector4Tuple, ]; - 'joint.invBindPose': [ + "joint.invBindPose": [ Vector4Tuple, Vector4Tuple, Vector4Tuple, Vector4Tuple, ]; - 'joint.name': string; - 'joint.parentIndex': number; - 'joint.uid': string; + "joint.name": string; + "joint.parentIndex": number; + "joint.uid": string; } >; jointsCount: number; } | { - 'comp.typename': 'AnimationComponent'; + "comp.typename": "AnimationComponent"; animation: bigint; animationTimeScale: number; repeatsCount: number; } | { - 'comp.typename': 'SkeletonJointBindingComponent'; + "comp.typename": "SkeletonJointBindingComponent"; count: number; map: Record< string, // 0000, 0001, ... diff --git a/packages/i18n/src/literals.ts b/packages/i18n/src/literals.ts index 8f802dd46..e2efe5056 100644 --- a/packages/i18n/src/literals.ts +++ b/packages/i18n/src/literals.ts @@ -1,7 +1,3 @@ -import { assertSecret } from "@blitzkit/core"; - -const dev = assertSecret(import.meta.env.DEV); - export function literals( string: string, literals?: Record @@ -9,7 +5,7 @@ export function literals( for (const key in literals) { const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`); - if (dev && mutated === string) { + if (import.meta.env.DEV && mutated === string) { console.warn(`Could not find literal {${key}} in string "${string}"`); } diff --git a/packages/scripts/src/buildAssets/tankModels.ts b/packages/scripts/src/buildAssets/tankModels.ts index 97ea628c6..407d4dfbf 100644 --- a/packages/scripts/src/buildAssets/tankModels.ts +++ b/packages/scripts/src/buildAssets/tankModels.ts @@ -31,12 +31,12 @@ export async function tankModels() { let index = 0; for (const [tankKey, tank] of entries) { - console.log(` ${++index} / ${entries.length} : ${tankKey}`); - if (tankKey.includes("tutorial_bot")) continue; const id = toUniqueId(nation, tank.id); + console.log(` ${++index} / ${entries.length} : ${tankKey}`); + const parameters = await readYAMLDVPL( `${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml` ); diff --git a/packages/scripts/src/core/blitz/extractModel/index.ts b/packages/scripts/src/core/blitz/extractModel/index.ts index fa1ebecd5..2ee07ce58 100644 --- a/packages/scripts/src/core/blitz/extractModel/index.ts +++ b/packages/scripts/src/core/blitz/extractModel/index.ts @@ -39,82 +39,95 @@ export async function extractModel(data: string, path: string) { const buffer = document.createBuffer(); const materials = new Map(); - // create materials - await Promise.all( - sc2["#dataNodes"].map(async (node) => { - const id = new DataView(node["#id"]).getBigUint64(0, true); + for (const node of sc2["#dataNodes"]) { + const id = new DataView(node["#id"]).getBigUint64(0, true); - if (node.parentMaterialKey !== undefined) { - /** - * material depends on a parent and doesn't seem to have any - * properties so we just point it to the parent - */ - materials.set(id, node.parentMaterialKey); - return; + if (node.parentMaterialKey !== undefined) { + /** + * material depends on a parent and doesn't seem to have any + * properties so we just point it to the parent + */ + materials.set(id, node.parentMaterialKey); + continue; + } + + const material = document.createMaterial(node.materialName); + let textures: Textures | undefined = undefined; + + if (node.textures) { + textures = node.textures; + } else if (typeof node.configCount === "number") { + textures = node.configArchive_0.textures; + } + + if (textures) { + material.setBaseColorTexture( + document + .createTexture(node.materialName) + .setMimeType("image/webp") + .setImage( + await readBaseColor( + `${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`, + textures.miscMap + ? `${data}/3d/${dirname(path)}/${textures.miscMap}` + : undefined + ) + ) + ); + + if (node.configCount) { + for ( + let configIndex = 0; + configIndex < node.configCount; + configIndex++ + ) { + const archive = node[`configArchive_${configIndex}`]; + + if ( + archive.configName !== "Default" || + !archive.enabledPresets?.AlphaTest + ) { + continue; + } + + material.setAlphaMode("BLEND").setDoubleSided(true); + } } - const material = document.createMaterial(node.materialName); - let textures: Textures | undefined = undefined; - - if (node.textures) { - textures = node.textures; - } else if (typeof node.configCount === "number") { - textures = node.configArchive_0.textures; - } - - if (textures) { - material.setBaseColorTexture( + if (textures.baseRMMap) { + material.setMetallicRoughnessTexture( document .createTexture(node.materialName) - .setMimeType("image/jpeg") + .setMimeType("image/webp") .setImage( - await readBaseColor( - `${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`, - textures.miscMap - ? `${data}/3d/${dirname(path)}/${textures.miscMap}` - : undefined + await readRoughnessMetallic( + `${data}/3d/${dirname(path)}/${textures.baseRMMap}` ) ) ); + } - if (textures.baseRMMap) { - material.setMetallicRoughnessTexture( - document - .createTexture(node.materialName) - .setMimeType("image/jpeg") - .setImage( - await readRoughnessMetallic( - `${data}/3d/${dirname(path)}/${textures.baseRMMap}` - ) + if (textures.baseNormalMap ?? textures.normalmap) { + const isBase = textures.baseNormalMap !== undefined; + + material.setNormalTexture( + document + .createTexture(node.materialName) + .setMimeType("image/webp") + .setImage( + await readNormal( + `${data}/3d/${dirname(path)}/${ + textures.baseNormalMap ?? textures.normalmap + }`, + isBase ) - ); - } - - if (textures.baseNormalMap ?? textures.normalmap) { - const isBase = textures.baseNormalMap !== undefined; - - material.setNormalTexture( - document - .createTexture(node.materialName) - .setMimeType("image/jpeg") - .setImage( - await readNormal( - `${data}/3d/${dirname(path)}/${ - textures.baseNormalMap ?? textures.normalmap - }`, - isBase - ) - ) - ); - } - - materials.set( - new DataView(node["#id"]).getBigUint64(0, true), - material + ) ); } - }) - ); + + materials.set(new DataView(node["#id"]).getBigUint64(0, true), material); + } + } // replace children materials with parents materials.forEach((material, id) => { diff --git a/packages/scripts/src/core/blitz/readBaseColor.ts b/packages/scripts/src/core/blitz/readBaseColor.ts index 141286afa..0081b9cdc 100644 --- a/packages/scripts/src/core/blitz/readBaseColor.ts +++ b/packages/scripts/src/core/blitz/readBaseColor.ts @@ -6,10 +6,7 @@ export async function readBaseColor(path: string, occlusionPath?: string) { const occlusionRaw = occlusionPath ? await readTexture(occlusionPath) : undefined; - const base = await sharp(baseRaw.data, { raw: baseRaw }) - .removeAlpha() - .raw() - .toBuffer(); + const base = await sharp(baseRaw.data, { raw: baseRaw }).raw().toBuffer(); const occlusion = occlusionRaw ? await sharp(occlusionRaw.data, { raw: occlusionRaw }) .extractChannel(3) @@ -17,7 +14,7 @@ export async function readBaseColor(path: string, occlusionPath?: string) { .toBuffer() : undefined; - const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 3); + const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 4); for (let i = 0; i < baseRaw.width * baseRaw.height; i++) { let c = 1; @@ -33,15 +30,16 @@ export async function readBaseColor(path: string, occlusionPath?: string) { c = occlusion![occlusionI] / 255; } - combined[i * 3 + 0] = Math.round(base[i * 3 + 0] * c); - combined[i * 3 + 1] = Math.round(base[i * 3 + 1] * c); - combined[i * 3 + 2] = Math.round(base[i * 3 + 2] * c); + combined[i * 4 + 0] = Math.round(base[i * 4 + 0] * c); + combined[i * 4 + 1] = Math.round(base[i * 4 + 1] * c); + combined[i * 4 + 2] = Math.round(base[i * 4 + 2] * c); + combined[i * 4 + 3] = base[i * 4 + 3]; } const image = await sharp(combined, { - raw: { width: baseRaw.width, height: baseRaw.height, channels: 3 }, + raw: { width: baseRaw.width, height: baseRaw.height, channels: 4 }, }) - .jpeg() + .webp() .toBuffer(); return image; diff --git a/packages/scripts/src/core/blitz/readNormal.ts b/packages/scripts/src/core/blitz/readNormal.ts index 803f6712b..8744e4e7e 100644 --- a/packages/scripts/src/core/blitz/readNormal.ts +++ b/packages/scripts/src/core/blitz/readNormal.ts @@ -25,5 +25,5 @@ export async function readNormal(path: string, isBase: boolean) { raw.data[index + 3] = 255; } - return await sharp(raw.data, { raw }).jpeg().toBuffer(); + return await sharp(raw.data, { raw }).webp().toBuffer(); } diff --git a/packages/scripts/src/core/blitz/readRoughnessMetallic.ts b/packages/scripts/src/core/blitz/readRoughnessMetallic.ts index f262b93a5..6c56ec43a 100644 --- a/packages/scripts/src/core/blitz/readRoughnessMetallic.ts +++ b/packages/scripts/src/core/blitz/readRoughnessMetallic.ts @@ -17,7 +17,7 @@ export async function readRoughnessMetallic(path: string) { const image = await sharp(combined, { raw: { width: raw.width, height: raw.height, channels: 3 }, }) - .jpeg() + .webp() .toBuffer(); return image;