mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 20:19:49 +02:00
sk
This commit is contained in:
1 parent
0ee9c99a28
commit
b540ab7814
6 files changed
+255
-6
No files matched your search
@@ -27,7 +27,7 @@ async function loadSkin(path: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function skinPath(id: number, skin: number) {
|
function skinPath(id: number, skin: number) {
|
||||||
return alias("api", `/tanks/${id}/skins/${skin}.glb`);
|
return alias("models_legendary", `/${id}/skins/${skin}.glb`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSkin(id: number) {
|
export function useSkin(id: number) {
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|||||||
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
|
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
|
||||||
import type { APIContext } from "astro";
|
import type { APIContext } from "astro";
|
||||||
import type { Vector3Tuple, Vector4Tuple } from "three";
|
import type { Vector3Tuple, Vector4Tuple } from "three";
|
||||||
import { SKIN_NUMBER } from "../../../../../constants/skinNumber";
|
import { SKIN_NUMBER } from "../../../../constants/skinNumber";
|
||||||
import { api } from "../../../../../core/blitzkit/api";
|
import { api } from "../../../../core/blitzkit/api";
|
||||||
import { mixStaticPaths } from "../../../../../core/blitzkit/mixStaticPaths";
|
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
||||||
import { vfs } from "../../../../../core/blitzkit/vfs";
|
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||||
import { getStaticPaths as _getStaticPaths } from "../../../_index";
|
import { getStaticPaths as _getStaticPaths } from "../../../[...chunk_api]/_index";
|
||||||
import { extractMaterials, extractModel } from "../model.glb";
|
import { extractMaterials, extractModel } from "../model.glb";
|
||||||
|
|
||||||
const SKIN_NUMBER_SLOTS = 5;
|
const SKIN_NUMBER_SLOTS = 5;
|
||||||
|
|||||||
Whitespace-only changes.
@@ -0,0 +1,12 @@
|
|||||||
|
import { api } from "../../../core/blitzkit/api";
|
||||||
|
import { mixStaticPaths } from "../../../core/blitzkit/mixStaticPaths";
|
||||||
|
import { getStaticPaths as _getStaticPaths } from "../_index";
|
||||||
|
|
||||||
|
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
||||||
|
const tanks = await api.tankDefinitions();
|
||||||
|
|
||||||
|
return Object.values(tanks.tanks).map((tank) => ({
|
||||||
|
params: { id: tank.id },
|
||||||
|
props: { id: tank.id },
|
||||||
|
}));
|
||||||
|
});
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import {
|
||||||
|
parseHierarchies,
|
||||||
|
Sc2ReadStream,
|
||||||
|
type CamouflagesXml,
|
||||||
|
type CamouflagesYaml,
|
||||||
|
type TankParameters,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import {
|
||||||
|
Document,
|
||||||
|
Node,
|
||||||
|
NodeIO,
|
||||||
|
PropertyType,
|
||||||
|
Scene,
|
||||||
|
} from "@gltf-transform/core";
|
||||||
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
|
import { dedup, mergeDocuments, unpartition } from "@gltf-transform/functions";
|
||||||
|
import type { APIContext } from "astro";
|
||||||
|
import type { Vector3Tuple, Vector4Tuple } from "three";
|
||||||
|
import { SKIN_NUMBER } from "../../../../constants/skinNumber";
|
||||||
|
import { api } from "../../../../core/blitzkit/api";
|
||||||
|
import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
|
||||||
|
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||||
|
import {
|
||||||
|
extractMaterials,
|
||||||
|
extractModel,
|
||||||
|
} from "../../../[...chunk_models_tanks]/[id]/model.glb";
|
||||||
|
import { getStaticPaths as _getStaticPaths } from "../_index";
|
||||||
|
|
||||||
|
const SKIN_NUMBER_SLOTS = 5;
|
||||||
|
const DIGIT_COLUMNS = 5;
|
||||||
|
const DIGIT_ROWS = 2;
|
||||||
|
|
||||||
|
export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
|
||||||
|
const camouflages = await api.camouflageDefinitions();
|
||||||
|
|
||||||
|
return Object.values(camouflages.camouflages)
|
||||||
|
.filter((camouflage) => camouflage.tank_id)
|
||||||
|
.map((camouflage) => ({
|
||||||
|
params: { id: camouflage.tank_id!, skin: camouflage.id },
|
||||||
|
props: { skin: camouflage.id },
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const camouflagesXml = await vfs.xml<{ root: CamouflagesXml }>(
|
||||||
|
"Data/XML/item_defs/vehicles/common/camouflages.xml",
|
||||||
|
);
|
||||||
|
const camouflagesYaml = await vfs.yaml<CamouflagesYaml>(
|
||||||
|
"Data/camouflages.yaml",
|
||||||
|
);
|
||||||
|
const customization = await vfs.yaml<
|
||||||
|
Record<string, { Name: string; Path: string }>
|
||||||
|
>("Data/3d/Customization.yaml");
|
||||||
|
|
||||||
|
export async function GET({ props }: APIContext<{ skin: number }>) {
|
||||||
|
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||||
|
|
||||||
|
const items = new Map(
|
||||||
|
Object.values(customization).map(({ Name, Path }) => [
|
||||||
|
Name,
|
||||||
|
Path.replace("~res:/3d/", "").replace(/\.sc2$/, ""),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const key = Object.keys(camouflagesXml.root.camouflages).find(
|
||||||
|
(key) => camouflagesXml.root.camouflages[key].id === props.skin,
|
||||||
|
)!;
|
||||||
|
const include = camouflagesXml.root.camouflages[key].vehicleFilter.include;
|
||||||
|
const [nation, tankKey] = (
|
||||||
|
include as { vehicle: { name: string } }
|
||||||
|
).vehicle.name.split(":");
|
||||||
|
const { preset, customEntities } = camouflagesYaml[key];
|
||||||
|
const skinNumber = Number(preset?.match(/(\d+)$/)?.[1] ?? 1);
|
||||||
|
const parameters = await vfs.yaml<TankParameters>(
|
||||||
|
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||||
|
);
|
||||||
|
const basePath = parameters.resourcesPath.blitzModelPath.replace(
|
||||||
|
/\.sc2$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const sc2 = new Sc2ReadStream(
|
||||||
|
(await vfs.file(`Data/3d/${basePath}.sc2`)).buffer as ArrayBuffer,
|
||||||
|
).sc2();
|
||||||
|
const slots: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
target: string;
|
||||||
|
position: Vector3Tuple;
|
||||||
|
rotation: Vector4Tuple;
|
||||||
|
scale: Vector3Tuple;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
const states: Record<string, string | null> = {};
|
||||||
|
|
||||||
|
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
||||||
|
for (const component of components) {
|
||||||
|
if (component["comp.typename"] === "NewSlotComponent") {
|
||||||
|
for (const slot of Object.values(component.slots)) {
|
||||||
|
slots[slot.name] = {
|
||||||
|
target: hierarchy.name,
|
||||||
|
position: slot.transform.position,
|
||||||
|
rotation: slot.transform.rotation,
|
||||||
|
scale: slot.transform.scale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component["comp.typename"] === "SlotComponent") {
|
||||||
|
slots[component["sc.slotName"]] = {
|
||||||
|
target: hierarchy.name,
|
||||||
|
position: component["sc.attachmentTranslation"] as Vector3Tuple,
|
||||||
|
rotation: component["sc.attachmentRotation"] as Vector4Tuple,
|
||||||
|
scale: component["sc.attachmentScale"] as Vector3Tuple,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component["comp.typename"] !== "StateSwitcherComponent") continue;
|
||||||
|
|
||||||
|
const active = component[`ssc.state${component["ssc.activeState"]}`];
|
||||||
|
// camos without their own variant keep the default piece, but groups
|
||||||
|
// of elements to hide stay hidden
|
||||||
|
let state: string | null =
|
||||||
|
active && !/skin\d*/.test(active) && !active.includes("hide")
|
||||||
|
? active
|
||||||
|
: null;
|
||||||
|
|
||||||
|
for (let index = 0; index < component["ssc.statesCount"]; index++) {
|
||||||
|
const name = component[`ssc.state${index}`];
|
||||||
|
const match = name.match(/skin(\d*)/);
|
||||||
|
|
||||||
|
if (
|
||||||
|
match &&
|
||||||
|
!name.includes("hide") &&
|
||||||
|
Number(match[1] || 1) === skinNumber
|
||||||
|
) {
|
||||||
|
state = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
states[hierarchy.name] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = new Document();
|
||||||
|
const scene = document.createScene().setExtras({ states });
|
||||||
|
const targets = new Map<string, Node>();
|
||||||
|
|
||||||
|
document.createBuffer();
|
||||||
|
|
||||||
|
if (preset) await extractMaterials(document, sc2, basePath, preset);
|
||||||
|
|
||||||
|
for (const name in customEntities) {
|
||||||
|
const slot = slots[name];
|
||||||
|
const path = items.get(customEntities[name].item);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!slot ||
|
||||||
|
!path ||
|
||||||
|
!(await vfs.resolve(`Data/3d/${path}.sc2`)) ||
|
||||||
|
!(await vfs.resolve(`Data/3d/${path}.scg`))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const part = await extractModel(vfs, path, true);
|
||||||
|
const map = mergeDocuments(document, part);
|
||||||
|
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
||||||
|
const node = document
|
||||||
|
.createNode(name)
|
||||||
|
.setTranslation(slot.position)
|
||||||
|
.setRotation(slot.rotation)
|
||||||
|
.setScale(slot.scale);
|
||||||
|
let target = targets.get(slot.target);
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
target = document.createNode(slot.target);
|
||||||
|
targets.set(slot.target, target);
|
||||||
|
scene.addChild(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of partScene.listChildren()) {
|
||||||
|
node.addChild(child as Node);
|
||||||
|
}
|
||||||
|
|
||||||
|
partScene.dispose();
|
||||||
|
target.addChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
applySkinNumber(document, SKIN_NUMBER);
|
||||||
|
|
||||||
|
await document.transform(
|
||||||
|
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.TEXTURE] }),
|
||||||
|
unpartition(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Response(await nodeIO.writeBinary(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySkinNumber(document: Document, number: string) {
|
||||||
|
const start = Math.ceil((SKIN_NUMBER_SLOTS - number.length) / 2) + 1;
|
||||||
|
|
||||||
|
for (const node of document.getRoot().listNodes()) {
|
||||||
|
const match = node.getName().match(/_number_\d+_(\d+)$/);
|
||||||
|
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const digit = number[Number(match[1]) - start];
|
||||||
|
|
||||||
|
if (digit === undefined) {
|
||||||
|
node.dispose();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = (Number(digit) + 9) % 10;
|
||||||
|
const column = cell % DIGIT_COLUMNS;
|
||||||
|
const row = Math.floor(cell / DIGIT_COLUMNS);
|
||||||
|
|
||||||
|
node.traverse((child) => {
|
||||||
|
for (const primitive of child.getMesh()?.listPrimitives() ?? []) {
|
||||||
|
const uvs = primitive.getAttribute("TEXCOORD_0")?.clone();
|
||||||
|
|
||||||
|
if (!uvs) continue;
|
||||||
|
|
||||||
|
for (let index = 0; index < uvs.getCount(); index++) {
|
||||||
|
const [u, v] = uvs.getElement(index, [0, 0]) as [number, number];
|
||||||
|
|
||||||
|
uvs.setElement(index, [
|
||||||
|
(column + u) / DIGIT_COLUMNS,
|
||||||
|
(row + v) / DIGIT_ROWS,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
primitive.setAttribute("TEXCOORD_0", uvs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Whitespace-only changes.
Reference in new issue
Block a user