mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
tank model and collision
This commit is contained in:
1 parent
9968b2c83d
commit
9e4d9c7968
19 files changed
+278
-206
No files matched your search
@@ -5,6 +5,7 @@
|
||||
"": {
|
||||
"name": "blitzkit",
|
||||
"dependencies": {
|
||||
"@gltf-transform/extensions": "^4.4.2",
|
||||
"lz4js": "^0.2.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -106,6 +107,8 @@
|
||||
"@blitzkit/core": "workspace:*",
|
||||
"@blitzkit/i18n": "workspace:*",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@gltf-transform/core": "^4.4.2",
|
||||
"@gltf-transform/functions": "^4.4.2",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
@@ -455,7 +458,7 @@
|
||||
|
||||
"@gltf-transform/core": ["@gltf-transform/[email protected]", "", { "dependencies": { "property-graph": "^3.0.0" } }, "sha512-qKhrQ29KJ9K6sz7xGoGRllqPBlROjPJMQPvCLBOfgMPH3S/Ph5E0e6fD5WmwTY5QPyBsDMRqKd1Vl0ncThTXGw=="],
|
||||
|
||||
"@gltf-transform/extensions": ["@gltf-transform/extensions@4.2.1", "", { "dependencies": { "@gltf-transform/core": "^4.2.1", "ktx-parse": "^1.0.1" } }, "sha512-ieHSJU9qvb3ucWDxgHFcff+C7fLtFPa6G0Wtc/ki7GsN6Rh7o9eoyqi7Ggj2LO4i3ynCwLLPb/onbJKw2fBtsA=="],
|
||||
"@gltf-transform/extensions": ["@gltf-transform/extensions@4.4.2", "", { "dependencies": { "@gltf-transform/core": "^4.4.2", "ktx-parse": "^1.1.0" } }, "sha512-HJH1FM+edC5eNvl6xO0SOXJ/j/3oDoIpSu150OTdJaLBoM3TgCCGIfh4wyhgWAqZrkvgHKVGiZKxcKV5LkgPCQ=="],
|
||||
|
||||
"@gltf-transform/functions": ["@gltf-transform/[email protected]", "", { "dependencies": { "@gltf-transform/core": "^4.2.1", "@gltf-transform/extensions": "^4.2.1", "ktx-parse": "^1.0.1", "ndarray": "^1.0.19", "ndarray-lanczos": "^0.3.0", "ndarray-pixels": "^5.0.1" } }, "sha512-hFCI80N6zt7yHlqXbzlYEYQbFwK+2OOZNvacrRtrWiCWKI0k1Ad5+nZFsmuIZZV03aXb5XG6UF7JINT3wETS1Q=="],
|
||||
|
||||
@@ -2521,6 +2524,16 @@
|
||||
|
||||
"@babel/preset-env/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@blitzkit/scripts/@gltf-transform/extensions": ["@gltf-transform/[email protected]", "", { "dependencies": { "@gltf-transform/core": "^4.2.1", "ktx-parse": "^1.0.1" } }, "sha512-ieHSJU9qvb3ucWDxgHFcff+C7fLtFPa6G0Wtc/ki7GsN6Rh7o9eoyqi7Ggj2LO4i3ynCwLLPb/onbJKw2fBtsA=="],
|
||||
|
||||
"@blitzkit/website/@gltf-transform/core": ["@gltf-transform/[email protected]", "", { "dependencies": { "property-graph": "^4.1.0" } }, "sha512-qsWKwNSwK+2s834Mt4xbYcyHqCrgNFP7hIv5s487JxebngRfDgelpghNF+kSswGb2/NuapasfK3UViFoSJJoMg=="],
|
||||
|
||||
"@blitzkit/website/@gltf-transform/functions": ["@gltf-transform/[email protected]", "", { "dependencies": { "@gltf-transform/core": "^4.4.2", "@gltf-transform/extensions": "^4.4.2", "ktx-parse": "^1.1.0", "ndarray": "^1.0.19", "ndarray-lanczos": "^0.3.0", "ndarray-pixels": "^5.0.1" } }, "sha512-dclXgv9TshMaWBqPDUYd4xTwBQ2PpuR8p0Y9pokrRzGQDUPXRP6lTDzbqT0UmEmxFSvRyPJjvOWUmzeiRafpvw=="],
|
||||
|
||||
"@gltf-transform/extensions/@gltf-transform/core": ["@gltf-transform/[email protected]", "", { "dependencies": { "property-graph": "^4.1.0" } }, "sha512-qsWKwNSwK+2s834Mt4xbYcyHqCrgNFP7hIv5s487JxebngRfDgelpghNF+kSswGb2/NuapasfK3UViFoSJJoMg=="],
|
||||
|
||||
"@gltf-transform/functions/@gltf-transform/extensions": ["@gltf-transform/[email protected]", "", { "dependencies": { "@gltf-transform/core": "^4.2.1", "ktx-parse": "^1.0.1" } }, "sha512-ieHSJU9qvb3ucWDxgHFcff+C7fLtFPa6G0Wtc/ki7GsN6Rh7o9eoyqi7Ggj2LO4i3ynCwLLPb/onbJKw2fBtsA=="],
|
||||
|
||||
"@jimp/core/file-type": ["[email protected]", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
@@ -2701,6 +2714,10 @@
|
||||
|
||||
"yaml-language-server/yaml": ["[email protected]", "", {}, "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA=="],
|
||||
|
||||
"@blitzkit/website/@gltf-transform/core/property-graph": ["[email protected]", "", {}, "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ=="],
|
||||
|
||||
"@gltf-transform/extensions/@gltf-transform/core/property-graph": ["[email protected]", "", {}, "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@gltf-transform/extensions": "^4.4.2",
|
||||
"lz4js": "^0.2.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -2,9 +2,11 @@ import { fetchPB } from "../../protobuf";
|
||||
import {
|
||||
CamouflageDefinitions,
|
||||
ConsumableDefinitions,
|
||||
EquipmentDefinitions,
|
||||
GameDefinitions,
|
||||
ModelDefinitions,
|
||||
ProvisionDefinitions,
|
||||
SkillDefinitions,
|
||||
TankDefinitions,
|
||||
} from "../../protos";
|
||||
import { BlitzKitAPI } from "./base";
|
||||
@@ -33,4 +35,12 @@ export class ClientBlitzKitAPI extends BlitzKitAPI {
|
||||
modelDefinitions() {
|
||||
return fetchPB("/api/definitions/models.pb", ModelDefinitions);
|
||||
}
|
||||
|
||||
skillDefinitions() {
|
||||
return fetchPB("/api/definitions/skills.pb", SkillDefinitions);
|
||||
}
|
||||
|
||||
equipmentDefinitions() {
|
||||
return fetchPB("/api/definitions/equipment.pb", EquipmentDefinitions);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { toUniqueId } from "@blitzkit/core";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { extractArmor } from "../core/blitz/extractArmor";
|
||||
import { AssetUploader } from "../core/github/assetUploader";
|
||||
import { vfs } from "./constants";
|
||||
import { VehicleDefinitionList } from "./definitions";
|
||||
|
||||
export async function tankArmors() {
|
||||
console.log("Building tank armors...");
|
||||
|
||||
using uploader = new AssetUploader("tank armors");
|
||||
const nodeIO = new NodeIO();
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nation of nations) {
|
||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tanks.root) {
|
||||
const tank = tanks.root[tankKey];
|
||||
|
||||
if (tankKey.includes("tutorial_bot")) continue;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
const model = await extractArmor(vfs, `${nation}-${tankKey}`);
|
||||
const content = await nodeIO.writeBinary(model);
|
||||
|
||||
await uploader.add({ path: `3d/tanks/armor/${id}.glb`, content });
|
||||
}
|
||||
}
|
||||
|
||||
await uploader.flush();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { VertexAttribute, vertexAttributeVectorSizes } from "@blitzkit/core";
|
||||
|
||||
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
||||
{
|
||||
[VertexAttribute.VERTEX]: "POSITION",
|
||||
[VertexAttribute.NORMAL]: "NORMAL",
|
||||
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
|
||||
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
|
||||
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
|
||||
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
|
||||
};
|
||||
export const vertexAttributeGltfVectorSizes = {
|
||||
...vertexAttributeVectorSizes,
|
||||
[VertexAttribute.TANGENT]: 4,
|
||||
} as const;
|
||||
@@ -1,50 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
import { readTexture } from "./readTexture";
|
||||
|
||||
export async function readBaseColor(path: string, occlusionPath?: string) {
|
||||
let hasAlpha = false;
|
||||
const baseRaw = await readTexture(path);
|
||||
const occlusionRaw = occlusionPath
|
||||
? await readTexture(occlusionPath)
|
||||
: undefined;
|
||||
const base = await sharp(baseRaw.data, { raw: baseRaw }).raw().toBuffer();
|
||||
const occlusion = occlusionRaw
|
||||
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
|
||||
.extractChannel(3)
|
||||
.raw()
|
||||
.toBuffer()
|
||||
: undefined;
|
||||
|
||||
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 4);
|
||||
|
||||
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
|
||||
let c = 1;
|
||||
|
||||
if (occlusionRaw) {
|
||||
const x = i % baseRaw.width;
|
||||
const y = Math.floor(i / baseRaw.width);
|
||||
const u = Math.floor(x * (occlusionRaw.width / baseRaw.width));
|
||||
const v = Math.floor(y * (occlusionRaw.height / baseRaw.height));
|
||||
|
||||
const occlusionI = u + v * occlusionRaw.width;
|
||||
|
||||
c = occlusion![occlusionI] / 255;
|
||||
}
|
||||
|
||||
const alpha = base[i * 4 + 3];
|
||||
hasAlpha ||= alpha < 255;
|
||||
|
||||
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] = alpha;
|
||||
}
|
||||
|
||||
const image = await sharp(combined, {
|
||||
raw: { width: baseRaw.width, height: baseRaw.height, channels: 4 },
|
||||
})
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return { hasAlpha, image };
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
import { readTexture } from "./readTexture";
|
||||
|
||||
export async function readNormal(path: string, isBase: boolean) {
|
||||
const raw = await readTexture(path);
|
||||
|
||||
if (!isBase) {
|
||||
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||
}
|
||||
|
||||
const bytes = 4 * raw.width * raw.height;
|
||||
|
||||
for (let index = 0; index < bytes; index += 4) {
|
||||
/**
|
||||
* Red is always 255 and blue is always 0. Only alpha and green contain any
|
||||
* sort of information.
|
||||
*/
|
||||
let x = raw.data[index + 3] * (2 / 255) - 1;
|
||||
let y = raw.data[index + 1] * (2 / 255) - 1;
|
||||
let z = Math.sqrt(Math.max(0, 1 - x ** 2 - y ** 2));
|
||||
|
||||
raw.data[index] = Math.round((x + 1) * (255 / 2));
|
||||
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
||||
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
||||
raw.data[index + 3] = 255;
|
||||
}
|
||||
|
||||
return await sharp(raw.data, { raw }).webp().toBuffer();
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import sharp from "sharp";
|
||||
import { readTexture } from "./readTexture";
|
||||
|
||||
export async function readRoughnessMetallic(path: string) {
|
||||
const raw = await readTexture(path);
|
||||
const blitz = sharp(raw.data, { raw: raw });
|
||||
const metallicness = await blitz?.extractChannel(1).raw().toBuffer();
|
||||
const roughness = await blitz?.extractChannel(3).raw().toBuffer();
|
||||
|
||||
const combined = Buffer.alloc(raw.width * raw.height * 3);
|
||||
|
||||
for (let i = 0; i < raw.width * raw.height; i++) {
|
||||
combined[i * 3 + 1] = roughness[i];
|
||||
combined[i * 3 + 2] = metallicness[i];
|
||||
}
|
||||
|
||||
const image = await sharp(combined, {
|
||||
raw: { width: raw.width, height: raw.height, channels: 3 },
|
||||
})
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return image;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum TextureMutation {
|
||||
Normal,
|
||||
RoughnessMetallicness,
|
||||
Albedo,
|
||||
Miscellaneous,
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { DdsReadStream, PvrReadStream } from "@blitzkit/core";
|
||||
import { vfs } from "../../../buildAssets/constants";
|
||||
|
||||
export async function readTexture(path: string) {
|
||||
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
|
||||
const isDds = await vfs.resolve(ddsTexturePath);
|
||||
const resolvedTexturePath = isDds
|
||||
? ddsTexturePath
|
||||
: ddsTexturePath.replace(".dds", ".pvr");
|
||||
const file = await vfs.file(resolvedTexturePath);
|
||||
|
||||
const raw = isDds
|
||||
? await new DdsReadStream(file.buffer as ArrayBuffer).dds()
|
||||
: new PvrReadStream(file.buffer as ArrayBuffer).pvr();
|
||||
|
||||
return raw;
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
"@blitzkit/core": "workspace:*",
|
||||
"@blitzkit/i18n": "workspace:*",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@gltf-transform/core": "^4.4.2",
|
||||
"@gltf-transform/functions": "^4.4.2",
|
||||
"@radix-ui/colors": "^3.0.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
import { asset } from "@blitzkit/core";
|
||||
import { IconButton } from "@radix-ui/themes";
|
||||
import { useArmor } from "../../../../../../hooks/useArmor";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
@@ -7,7 +6,7 @@ import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
export function DynamicArmorSwitcher() {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const isDynamicArmorActive = Duel.use((state) =>
|
||||
state.protagonist.consumables.includes(73)
|
||||
state.protagonist.consumables.includes(73),
|
||||
);
|
||||
const { hasDynamicArmor } = useArmor(tank.id);
|
||||
|
||||
@@ -38,7 +37,7 @@ export function DynamicArmorSwitcher() {
|
||||
>
|
||||
<img
|
||||
alt="Dynamic Armor"
|
||||
src={asset("icons/consumables/73.webp")}
|
||||
src={"/api/icons/consumables/73.webp"}
|
||||
style={{
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
|
||||
@@ -79,7 +79,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
>
|
||||
<img
|
||||
alt={unwrap(tank.name!)}
|
||||
src={asset(`icons/tanks/big/${id}.webp`)}
|
||||
src={`/api/tanks/${id}/icons/big.webp`}
|
||||
width={64}
|
||||
height={64}
|
||||
style={{
|
||||
@@ -99,7 +99,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="XP"
|
||||
src={asset("icons/currencies/xp.webp")}
|
||||
src={"/api/icons/currencies/xp.webp"}
|
||||
style={{
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
@@ -114,7 +114,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="Silver"
|
||||
src={asset("icons/currencies/silver.webp")}
|
||||
src={"/api/icons/currencies/silver.webp"}
|
||||
style={{
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
|
||||
@@ -121,7 +121,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="Silver"
|
||||
src={asset("icons/currencies/silver.webp")}
|
||||
src={"/api/icons/currencies/silver.webp"}
|
||||
style={{
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { asset } from "@blitzkit/core";
|
||||
import { useLoader } from "@react-three/fiber";
|
||||
import { GLTFLoader } from "three-stdlib";
|
||||
import { useDispose } from "./useDispose";
|
||||
|
||||
export function useArmor(id: number) {
|
||||
const path = asset(`3d/tanks/armor/${id}.glb`);
|
||||
const path = `/api/tanks/${id}/collision.glb`;
|
||||
const gltf = useLoader(GLTFLoader, path);
|
||||
|
||||
useDispose(gltf, path);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { EquipmentDefinitions } from "@blitzkit/core";
|
||||
import { api } from "../../../core/blitzkit/api";
|
||||
|
||||
export { getStaticPaths } from "../_index";
|
||||
|
||||
export async function GET() {
|
||||
const definitions = await api.equipmentDefinitions();
|
||||
const bytes = EquipmentDefinitions.encode(definitions).finish();
|
||||
|
||||
return new Response(bytes);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SkillDefinitions } from "@blitzkit/core";
|
||||
import { api } from "../../../core/blitzkit/api";
|
||||
|
||||
export { getStaticPaths } from "../_index";
|
||||
|
||||
export async function GET() {
|
||||
const definitions = await api.skillDefinitions();
|
||||
const bytes = SkillDefinitions.encode(definitions).finish();
|
||||
|
||||
return new Response(bytes);
|
||||
}
|
||||
+54
-9
@@ -1,18 +1,49 @@
|
||||
import {
|
||||
Hierarchy,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
toUniqueId,
|
||||
VertexAttribute,
|
||||
vertexAttributeVectorSizes,
|
||||
type Hierarchy,
|
||||
type VehicleDefinitionList,
|
||||
} from "@blitzkit/core";
|
||||
import { Accessor, Document, Node, Scene } from "@gltf-transform/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 { AbstractVFS } from "../vfs/abstract";
|
||||
import {
|
||||
vertexAttributeGLTFName,
|
||||
vertexAttributeGltfVectorSizes,
|
||||
} from "./extractModel/constants";
|
||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||
|
||||
export async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
||||
export { getStaticPaths } from "./_index";
|
||||
|
||||
export async function GET({ props }: APIContext<{ id: number }>) {
|
||||
const nodeIO = new NodeIO();
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nation of nations) {
|
||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
|
||||
for (const tankKey in tanks.root) {
|
||||
const tank = tanks.root[tankKey];
|
||||
|
||||
if (tankKey.includes("tutorial_bot")) continue;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
if (id !== props.id) continue;
|
||||
|
||||
const model = await extractArmor(vfs, `${nation}-${tankKey}`);
|
||||
const bytes = await nodeIO.writeBinary(model);
|
||||
|
||||
return new Response(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
||||
const sc2Path = `Data/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
|
||||
const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
|
||||
const sc2 = new Sc2ReadStream(
|
||||
@@ -141,4 +172,18 @@ export async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
||||
parseHierarchies(sc2["#hierarchy"], scene);
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
||||
|
||||
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
||||
{
|
||||
[VertexAttribute.VERTEX]: "POSITION",
|
||||
[VertexAttribute.NORMAL]: "NORMAL",
|
||||
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
|
||||
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
|
||||
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
|
||||
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
|
||||
};
|
||||
export const vertexAttributeGltfVectorSizes = {
|
||||
...vertexAttributeVectorSizes,
|
||||
[VertexAttribute.TANGENT]: 4,
|
||||
} as const;
|
||||
+164
-10
@@ -1,24 +1,66 @@
|
||||
import {
|
||||
bufferToBigInt,
|
||||
ConfigArchive,
|
||||
Hierarchy,
|
||||
DdsReadStream,
|
||||
PvrReadStream,
|
||||
Sc2ReadStream,
|
||||
ScgReadStream,
|
||||
Textures,
|
||||
toUniqueId,
|
||||
VertexAttribute,
|
||||
type ConfigArchive,
|
||||
type Hierarchy,
|
||||
type TankParameters,
|
||||
type Textures,
|
||||
type VehicleDefinitionList,
|
||||
} from "@blitzkit/core";
|
||||
import { Document, Material, Node, Scene } from "@gltf-transform/core";
|
||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||
import { Document, Material, Node, NodeIO, Scene } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { dedup, prune } from "@gltf-transform/functions";
|
||||
import type { APIContext } from "astro";
|
||||
import { times } from "lodash-es";
|
||||
import { dirname } from "path";
|
||||
import { AbstractVFS } from "../../vfs/abstract";
|
||||
import { readBaseColor } from "../readBaseColor";
|
||||
import { readNormal } from "../readNormal";
|
||||
import { readRoughnessMetallic } from "../readRoughnessMetallic";
|
||||
import sharp from "sharp";
|
||||
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||
import {
|
||||
vertexAttributeGLTFName,
|
||||
vertexAttributeGltfVectorSizes,
|
||||
} from "./constants";
|
||||
} from "./collision.glb";
|
||||
|
||||
export { getStaticPaths } from "./_index";
|
||||
|
||||
export async function GET({ props }: APIContext<{ id: number }>) {
|
||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
const nations = await vfs
|
||||
.dir(`Data/XML/item_defs/vehicles`)
|
||||
.then((files) => files.filter((nation) => nation !== "common"));
|
||||
|
||||
for (const nationIndex in nations) {
|
||||
const nation = nations[nationIndex];
|
||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
||||
);
|
||||
const entries = Object.entries(tanks.root);
|
||||
|
||||
for (const [tankKey, tank] of entries) {
|
||||
if (tankKey.includes("tutorial_bot")) continue;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
if (id !== props.id) continue;
|
||||
|
||||
const parameters = await vfs.yaml<TankParameters>(
|
||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
const model = await extractModel(
|
||||
vfs,
|
||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ""),
|
||||
);
|
||||
const bytes = await nodeIO.writeBinary(model);
|
||||
|
||||
return new Response(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
||||
|
||||
@@ -27,7 +69,7 @@ const omitMeshNames = {
|
||||
end: ["_POINT"],
|
||||
};
|
||||
|
||||
export async function extractModel(vfs: AbstractVFS, path: string) {
|
||||
async function extractModel(vfs: AbstractVFS, path: string) {
|
||||
const sc2Path = `Data/3d/${path}.sc2`;
|
||||
const scgPath = `Data/3d/${path}.scg`;
|
||||
const sc2 = new Sc2ReadStream(
|
||||
@@ -325,3 +367,115 @@ export async function extractModel(vfs: AbstractVFS, path: string) {
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
async function readBaseColor(path: string, occlusionPath?: string) {
|
||||
let hasAlpha = false;
|
||||
const baseRaw = await readTexture(path);
|
||||
const occlusionRaw = occlusionPath
|
||||
? await readTexture(occlusionPath)
|
||||
: undefined;
|
||||
const base = await sharp(baseRaw.data, { raw: baseRaw }).raw().toBuffer();
|
||||
const occlusion = occlusionRaw
|
||||
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
|
||||
.extractChannel(3)
|
||||
.raw()
|
||||
.toBuffer()
|
||||
: undefined;
|
||||
|
||||
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 4);
|
||||
|
||||
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
|
||||
let c = 1;
|
||||
|
||||
if (occlusionRaw) {
|
||||
const x = i % baseRaw.width;
|
||||
const y = Math.floor(i / baseRaw.width);
|
||||
const u = Math.floor(x * (occlusionRaw.width / baseRaw.width));
|
||||
const v = Math.floor(y * (occlusionRaw.height / baseRaw.height));
|
||||
|
||||
const occlusionI = u + v * occlusionRaw.width;
|
||||
|
||||
c = occlusion![occlusionI] / 255;
|
||||
}
|
||||
|
||||
const alpha = base[i * 4 + 3];
|
||||
hasAlpha ||= alpha < 255;
|
||||
|
||||
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] = alpha;
|
||||
}
|
||||
|
||||
const image = await sharp(combined, {
|
||||
raw: { width: baseRaw.width, height: baseRaw.height, channels: 4 },
|
||||
})
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return { hasAlpha, image };
|
||||
}
|
||||
|
||||
async function readRoughnessMetallic(path: string) {
|
||||
const raw = await readTexture(path);
|
||||
const blitz = sharp(raw.data, { raw: raw });
|
||||
const metallicness = await blitz?.extractChannel(1).raw().toBuffer();
|
||||
const roughness = await blitz?.extractChannel(3).raw().toBuffer();
|
||||
|
||||
const combined = Buffer.alloc(raw.width * raw.height * 3);
|
||||
|
||||
for (let i = 0; i < raw.width * raw.height; i++) {
|
||||
combined[i * 3 + 1] = roughness[i];
|
||||
combined[i * 3 + 2] = metallicness[i];
|
||||
}
|
||||
|
||||
const image = await sharp(combined, {
|
||||
raw: { width: raw.width, height: raw.height, channels: 3 },
|
||||
})
|
||||
.webp()
|
||||
.toBuffer();
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
async function readNormal(path: string, isBase: boolean) {
|
||||
const raw = await readTexture(path);
|
||||
|
||||
if (!isBase) {
|
||||
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||
}
|
||||
|
||||
const bytes = 4 * raw.width * raw.height;
|
||||
|
||||
for (let index = 0; index < bytes; index += 4) {
|
||||
/**
|
||||
* Red is always 255 and blue is always 0. Only alpha and green contain any
|
||||
* sort of information.
|
||||
*/
|
||||
let x = raw.data[index + 3] * (2 / 255) - 1;
|
||||
let y = raw.data[index + 1] * (2 / 255) - 1;
|
||||
let z = Math.sqrt(Math.max(0, 1 - x ** 2 - y ** 2));
|
||||
|
||||
raw.data[index] = Math.round((x + 1) * (255 / 2));
|
||||
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
||||
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
||||
raw.data[index + 3] = 255;
|
||||
}
|
||||
|
||||
return await sharp(raw.data, { raw }).webp().toBuffer();
|
||||
}
|
||||
|
||||
async function readTexture(path: string) {
|
||||
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
|
||||
const isDds = await vfs.resolve(ddsTexturePath);
|
||||
const resolvedTexturePath = isDds
|
||||
? ddsTexturePath
|
||||
: ddsTexturePath.replace(".dds", ".pvr");
|
||||
const file = await vfs.file(resolvedTexturePath);
|
||||
|
||||
const raw = isDds
|
||||
? await new DdsReadStream(file.buffer as ArrayBuffer).dds()
|
||||
: new PvrReadStream(file.buffer as ArrayBuffer).pvr();
|
||||
|
||||
return raw;
|
||||
}
|
||||
Reference in new issue
Block a user