feat: support transparency

This commit is contained in:
TrèsAbhi committed 2025-10-11 21:22:48 -05:00
1 parent 06a3791ddd
commit 4ab03e56a2
7 files changed
+190 -184

No files matched your search

+101 -102
View File
@@ -1,20 +1,19 @@
import { Vector3Tuple, Vector4Tuple } from 'three'; import { Vector3Tuple, Vector4Tuple } from "three";
import { ScpgReadStream } from './scpg'; import { ScpgReadStream } from "./scpg";
export interface SC2 { export interface SC2 {
'#dataNodes': DataNode[]; "#dataNodes": DataNode[];
'#hierarchy': Hierarchy[]; "#hierarchy": Hierarchy[];
'#sceneComponents': SceneComponents; "#sceneComponents": SceneComponents;
} }
type DataNode = { type DataNode = {
'##name': 'NMaterial'; "##name": "NMaterial";
'#id': ArrayBuffer; "#id": ArrayBuffer;
materialName: string; materialName: string;
parentMaterialKey?: bigint; parentMaterialKey?: bigint;
qualityGroup?: string; qualityGroup?: string;
fxName?: string; fxName?: string;
textures?: Textures; textures?: Textures;
} & ( } & (
| ({ | ({
@@ -80,8 +79,8 @@ export interface Textures {
} }
export interface Hierarchy { export interface Hierarchy {
'##name': 'Entity'; "##name": "Entity";
'#hierarchy'?: Hierarchy[]; "#hierarchy"?: Hierarchy[];
components: Components; components: Components;
flags: number; flags: number;
id: bigint; id: bigint;
@@ -97,12 +96,12 @@ type Components = {
type RoBatches = Record< type RoBatches = Record<
string, string,
{ {
'##name': 'RenderBatch'; "##name": "RenderBatch";
'rb.aabbox': RbAabbox; "rb.aabbox": RbAabbox;
'rb.classname': 'RenderBatch'; "rb.classname": "RenderBatch";
'rb.datasource': bigint; "rb.datasource": bigint;
'rb.nmatname': bigint; "rb.nmatname": bigint;
'rb.sortingKey': number; "rb.sortingKey": number;
} }
>; >;
@@ -113,50 +112,50 @@ interface RbAabbox {
export type Component = export type Component =
| { | {
'comp.typename': 'TransformComponent'; "comp.typename": "TransformComponent";
'tc.localRotation': Vector4Tuple; "tc.localRotation": Vector4Tuple;
'tc.localScale': Vector3Tuple; "tc.localScale": Vector3Tuple;
'tc.localTranslation': Vector3Tuple; "tc.localTranslation": Vector3Tuple;
'tc.worldRotation': Vector4Tuple; "tc.worldRotation": Vector4Tuple;
'tc.worldScale': Vector3Tuple; "tc.worldScale": Vector3Tuple;
'tc.worldTranslation': Vector3Tuple; "tc.worldTranslation": Vector3Tuple;
} }
| { | {
'comp.typename': 'RenderComponent'; "comp.typename": "RenderComponent";
'rc.renderObj': { "rc.renderObj": {
'##name': 'Mesh'; "##name": "Mesh";
'ro.batchCount': number; "ro.batchCount": number;
'ro.batches': RoBatches; "ro.batches": RoBatches;
'ro.debugflags': number; "ro.debugflags": number;
'ro.flags': number; "ro.flags": number;
'ro.notShadowOnly': boolean; "ro.notShadowOnly": boolean;
'ro.sOclCull': boolean; "ro.sOclCull": boolean;
'ro.sOclIndex': number; "ro.sOclIndex": number;
} & Record<`rb${number}.${'lodIndex' | 'switchIndex'}`, number>; } & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number>;
} }
| { | {
'comp.typename': 'SlotComponent'; "comp.typename": "SlotComponent";
'sc.attachmentRotation': number[]; "sc.attachmentRotation": number[];
'sc.attachmentScale': number[]; "sc.attachmentScale": number[];
'sc.attachmentTranslation': number[]; "sc.attachmentTranslation": number[];
'sc.configFilePath': string; "sc.configFilePath": string;
'sc.slotName': string; "sc.slotName": string;
'sc.template': string; "sc.template": string;
'sc.typeFiltersCount': number; "sc.typeFiltersCount": number;
} }
| { | {
'comp.typename': 'LodComponent'; "comp.typename": "LodComponent";
'lc.loddist': Record<`distance${number}`, number>; "lc.loddist": Record<`distance${number}`, number>;
} }
| { | {
'comp.typename': 'DecorItemComponent'; "comp.typename": "DecorItemComponent";
drawOrder: number; drawOrder: number;
shouldApplyCamo: boolean; shouldApplyCamo: boolean;
vanishTargetAlpha: number; vanishTargetAlpha: number;
vanishWhenArmorShown: boolean; vanishWhenArmorShown: boolean;
} }
| { | {
'comp.typename': 'NewSlotComponent'; "comp.typename": "NewSlotComponent";
slotItemsCount: number; slotItemsCount: number;
slots: { slots: {
[key: string]: { [key: string]: {
@@ -171,109 +170,109 @@ export type Component =
}; };
} }
| { | {
'comp.typename': 'ScenarioComponent'; "comp.typename": "ScenarioComponent";
'scenario.scriptId': string; "scenario.scriptId": string;
} }
| ({ | ({
'comp.typename': 'StateSwitcherComponent'; "comp.typename": "StateSwitcherComponent";
'ssc.activeState': number; "ssc.activeState": number;
'ssc.statesCount': number; "ssc.statesCount": number;
} & Record<`ssc.state${number}`, string>) } & Record<`ssc.state${number}`, string>)
| { | {
'comp.typename': 'CustomPropertiesComponent'; "comp.typename": "CustomPropertiesComponent";
'cpc.properties.archive': { "cpc.properties.archive": {
'editor.donotremove': number; "editor.donotremove": number;
}; };
} }
| ({ | ({
'comp.typename': 'ActionComponent'; "comp.typename": "ActionComponent";
'ac.actionCount': number; "ac.actionCount": number;
} & Record< } & Record<
string, // 0000, 0001, ... string, // 0000, 0001, ...
{ {
'act.delay': number; "act.delay": number;
'act.delayVariation': number; "act.delayVariation": number;
'act.entityName': string; "act.entityName": string;
'act.event': number; "act.event": number;
'act.eventToTrigger': string; "act.eventToTrigger": string;
'act.motionFrameTime': number; "act.motionFrameTime": number;
'act.motionSpeed': number; "act.motionSpeed": number;
'act.stateActivated': number; "act.stateActivated": number;
'act.stopAfterNRepeats': number; "act.stopAfterNRepeats": number;
'act.stopWhenEmpty': boolean; "act.stopWhenEmpty": boolean;
'act.switchIndex': number; "act.switchIndex": number;
'act.type': number; "act.type": number;
'act.userEventId': string; "act.userEventId": string;
} }
>) >)
| { | {
'comp.typename': 'ParticleEffectComponent'; "comp.typename": "ParticleEffectComponent";
'pe.clearOnRestart': boolean; "pe.clearOnRestart": boolean;
'pe.effectDuration': number; "pe.effectDuration": number;
'pe.emitters': { "pe.emitters": {
'##name': string; "##name": string;
'emitter.data': { "emitter.data": {
'emitter.id': `${number}`; "emitter.id": `${number}`;
'emitter.quality': string; "emitter.quality": string;
}[]; }[];
'emitter.position': Vector3Tuple; "emitter.position": Vector3Tuple;
'emitter.rotation': Vector4Tuple; "emitter.rotation": Vector4Tuple;
'emitter.scale': Vector3Tuple; "emitter.scale": Vector3Tuple;
}[]; }[];
'pe.inheritScale': boolean; "pe.inheritScale": boolean;
'pe.nestedEmitters': boolean; "pe.nestedEmitters": boolean;
'pe.repeatsCount': number; "pe.repeatsCount": number;
'pe.startFromTime': number; "pe.startFromTime": number;
'pe.stopWhenEmpty': boolean; "pe.stopWhenEmpty": boolean;
'pe.version': number; "pe.version": number;
'ro.flags': number; "ro.flags": number;
} }
| { | {
'comp.typename': 'TankElementComponent'; "comp.typename": "TankElementComponent";
isDestructible: boolean; isDestructible: boolean;
shouldCastShadow: boolean; shouldCastShadow: boolean;
shouldGenerateSilhouette: boolean; shouldGenerateSilhouette: boolean;
} }
| { | {
'comp.typename': 'MotionComponent'; "comp.typename": "MotionComponent";
'motion.filepath': string; "motion.filepath": string;
'motion.playbackRate': number; "motion.playbackRate": number;
'simpleMotion.repeatsCount': number; "simpleMotion.repeatsCount": number;
} }
| { | {
'comp.typename': 'SkeletonComponent'; "comp.typename": "SkeletonComponent";
joints: Record< joints: Record<
string, // 0000, 0001, ... string, // 0000, 0001, ...
{ {
'joint.bbox.max': Vector3Tuple; "joint.bbox.max": Vector3Tuple;
'joint.bbox.min': Vector3Tuple; "joint.bbox.min": Vector3Tuple;
'joint.bindPose': [ "joint.bindPose": [
Vector4Tuple, Vector4Tuple,
Vector4Tuple, Vector4Tuple,
Vector4Tuple, Vector4Tuple,
Vector4Tuple, Vector4Tuple,
]; ];
'joint.invBindPose': [ "joint.invBindPose": [
Vector4Tuple, Vector4Tuple,
Vector4Tuple, Vector4Tuple,
Vector4Tuple, Vector4Tuple,
Vector4Tuple, Vector4Tuple,
]; ];
'joint.name': string; "joint.name": string;
'joint.parentIndex': number; "joint.parentIndex": number;
'joint.uid': string; "joint.uid": string;
} }
>; >;
jointsCount: number; jointsCount: number;
} }
| { | {
'comp.typename': 'AnimationComponent'; "comp.typename": "AnimationComponent";
animation: bigint; animation: bigint;
animationTimeScale: number; animationTimeScale: number;
repeatsCount: number; repeatsCount: number;
} }
| { | {
'comp.typename': 'SkeletonJointBindingComponent'; "comp.typename": "SkeletonJointBindingComponent";
count: number; count: number;
map: Record< map: Record<
string, // 0000, 0001, ... string, // 0000, 0001, ...
+1 -5
View File
@@ -1,7 +1,3 @@
import { assertSecret } from "@blitzkit/core";
const dev = assertSecret(import.meta.env.DEV);
export function literals( export function literals(
string: string, string: string,
literals?: Record<string, string | number | undefined> literals?: Record<string, string | number | undefined>
@@ -9,7 +5,7 @@ export function literals(
for (const key in literals) { for (const key in literals) {
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`); 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}"`); console.warn(`Could not find literal {${key}} in string "${string}"`);
} }
@@ -31,12 +31,12 @@ export async function tankModels() {
let index = 0; let index = 0;
for (const [tankKey, tank] of entries) { for (const [tankKey, tank] of entries) {
console.log(` ${++index} / ${entries.length} : ${tankKey}`);
if (tankKey.includes("tutorial_bot")) continue; if (tankKey.includes("tutorial_bot")) continue;
const id = toUniqueId(nation, tank.id); const id = toUniqueId(nation, tank.id);
console.log(` ${++index} / ${entries.length} : ${tankKey}`);
const parameters = await readYAMLDVPL<TankParameters>( const parameters = await readYAMLDVPL<TankParameters>(
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml` `${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
); );
@@ -39,82 +39,95 @@ export async function extractModel(data: string, path: string) {
const buffer = document.createBuffer(); const buffer = document.createBuffer();
const materials = new Map<bigint, Material | bigint | undefined>(); const materials = new Map<bigint, Material | bigint | undefined>();
// create materials for (const node of sc2["#dataNodes"]) {
await Promise.all( const id = new DataView(node["#id"]).getBigUint64(0, true);
sc2["#dataNodes"].map(async (node) => {
const id = new DataView(node["#id"]).getBigUint64(0, true);
if (node.parentMaterialKey !== undefined) { if (node.parentMaterialKey !== undefined) {
/** /**
* material depends on a parent and doesn't seem to have any * material depends on a parent and doesn't seem to have any
* properties so we just point it to the parent * properties so we just point it to the parent
*/ */
materials.set(id, node.parentMaterialKey); materials.set(id, node.parentMaterialKey);
return; 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); if (textures.baseRMMap) {
let textures: Textures | undefined = undefined; material.setMetallicRoughnessTexture(
if (node.textures) {
textures = node.textures;
} else if (typeof node.configCount === "number") {
textures = node.configArchive_0.textures;
}
if (textures) {
material.setBaseColorTexture(
document document
.createTexture(node.materialName) .createTexture(node.materialName)
.setMimeType("image/jpeg") .setMimeType("image/webp")
.setImage( .setImage(
await readBaseColor( await readRoughnessMetallic(
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`, `${data}/3d/${dirname(path)}/${textures.baseRMMap}`
textures.miscMap
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
: undefined
) )
) )
); );
}
if (textures.baseRMMap) { if (textures.baseNormalMap ?? textures.normalmap) {
material.setMetallicRoughnessTexture( const isBase = textures.baseNormalMap !== undefined;
document
.createTexture(node.materialName) material.setNormalTexture(
.setMimeType("image/jpeg") document
.setImage( .createTexture(node.materialName)
await readRoughnessMetallic( .setMimeType("image/webp")
`${data}/3d/${dirname(path)}/${textures.baseRMMap}` .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 // replace children materials with parents
materials.forEach((material, id) => { materials.forEach((material, id) => {
@@ -6,10 +6,7 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
const occlusionRaw = occlusionPath const occlusionRaw = occlusionPath
? await readTexture(occlusionPath) ? await readTexture(occlusionPath)
: undefined; : undefined;
const base = await sharp(baseRaw.data, { raw: baseRaw }) const base = await sharp(baseRaw.data, { raw: baseRaw }).raw().toBuffer();
.removeAlpha()
.raw()
.toBuffer();
const occlusion = occlusionRaw const occlusion = occlusionRaw
? await sharp(occlusionRaw.data, { raw: occlusionRaw }) ? await sharp(occlusionRaw.data, { raw: occlusionRaw })
.extractChannel(3) .extractChannel(3)
@@ -17,7 +14,7 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
.toBuffer() .toBuffer()
: undefined; : 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++) { for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
let c = 1; let c = 1;
@@ -33,15 +30,16 @@ export async function readBaseColor(path: string, occlusionPath?: string) {
c = occlusion![occlusionI] / 255; c = occlusion![occlusionI] / 255;
} }
combined[i * 3 + 0] = Math.round(base[i * 3 + 0] * c); combined[i * 4 + 0] = Math.round(base[i * 4 + 0] * c);
combined[i * 3 + 1] = Math.round(base[i * 3 + 1] * c); combined[i * 4 + 1] = Math.round(base[i * 4 + 1] * c);
combined[i * 3 + 2] = Math.round(base[i * 3 + 2] * 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, { 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(); .toBuffer();
return image; return image;
@@ -25,5 +25,5 @@ export async function readNormal(path: string, isBase: boolean) {
raw.data[index + 3] = 255; raw.data[index + 3] = 255;
} }
return await sharp(raw.data, { raw }).jpeg().toBuffer(); return await sharp(raw.data, { raw }).webp().toBuffer();
} }
@@ -17,7 +17,7 @@ export async function readRoughnessMetallic(path: string) {
const image = await sharp(combined, { const image = await sharp(combined, {
raw: { width: raw.width, height: raw.height, channels: 3 }, raw: { width: raw.width, height: raw.height, channels: 3 },
}) })
.jpeg() .webp()
.toBuffer(); .toBuffer();
return image; return image;