mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +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",
|
"name": "blitzkit",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gltf-transform/extensions": "^4.4.2",
|
||||||
"lz4js": "^0.2.0",
|
"lz4js": "^0.2.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
@@ -106,6 +107,8 @@
|
|||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@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/colors": "^3.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/themes": "^3.2.1",
|
"@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/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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"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/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=="],
|
"@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gltf-transform/extensions": "^4.4.2",
|
||||||
"lz4js": "^0.2.0",
|
"lz4js": "^0.2.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { fetchPB } from "../../protobuf";
|
|||||||
import {
|
import {
|
||||||
CamouflageDefinitions,
|
CamouflageDefinitions,
|
||||||
ConsumableDefinitions,
|
ConsumableDefinitions,
|
||||||
|
EquipmentDefinitions,
|
||||||
GameDefinitions,
|
GameDefinitions,
|
||||||
ModelDefinitions,
|
ModelDefinitions,
|
||||||
ProvisionDefinitions,
|
ProvisionDefinitions,
|
||||||
|
SkillDefinitions,
|
||||||
TankDefinitions,
|
TankDefinitions,
|
||||||
} from "../../protos";
|
} from "../../protos";
|
||||||
import { BlitzKitAPI } from "./base";
|
import { BlitzKitAPI } from "./base";
|
||||||
@@ -33,4 +35,12 @@ export class ClientBlitzKitAPI extends BlitzKitAPI {
|
|||||||
modelDefinitions() {
|
modelDefinitions() {
|
||||||
return fetchPB("/api/definitions/models.pb", 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/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@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/colors": "^3.0.0",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/themes": "^3.2.1",
|
"@radix-ui/themes": "^3.2.1",
|
||||||
|
|||||||
+2
-3
@@ -1,4 +1,3 @@
|
|||||||
import { asset } from "@blitzkit/core";
|
|
||||||
import { IconButton } from "@radix-ui/themes";
|
import { IconButton } from "@radix-ui/themes";
|
||||||
import { useArmor } from "../../../../../../hooks/useArmor";
|
import { useArmor } from "../../../../../../hooks/useArmor";
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
@@ -7,7 +6,7 @@ import { Tankopedia } from "../../../../../../stores/tankopedia";
|
|||||||
export function DynamicArmorSwitcher() {
|
export function DynamicArmorSwitcher() {
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73)
|
state.protagonist.consumables.includes(73),
|
||||||
);
|
);
|
||||||
const { hasDynamicArmor } = useArmor(tank.id);
|
const { hasDynamicArmor } = useArmor(tank.id);
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ export function DynamicArmorSwitcher() {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Dynamic Armor"
|
alt="Dynamic Armor"
|
||||||
src={asset("icons/consumables/73.webp")}
|
src={"/api/icons/consumables/73.webp"}
|
||||||
style={{
|
style={{
|
||||||
width: "50%",
|
width: "50%",
|
||||||
height: "50%",
|
height: "50%",
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt={unwrap(tank.name!)}
|
alt={unwrap(tank.name!)}
|
||||||
src={asset(`icons/tanks/big/${id}.webp`)}
|
src={`/api/tanks/${id}/icons/big.webp`}
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
style={{
|
style={{
|
||||||
@@ -99,7 +99,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
|||||||
<Flex gap="1" align="center">
|
<Flex gap="1" align="center">
|
||||||
<img
|
<img
|
||||||
alt="XP"
|
alt="XP"
|
||||||
src={asset("icons/currencies/xp.webp")}
|
src={"/api/icons/currencies/xp.webp"}
|
||||||
style={{
|
style={{
|
||||||
width: "1em",
|
width: "1em",
|
||||||
height: "1em",
|
height: "1em",
|
||||||
@@ -114,7 +114,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
|||||||
<Flex gap="1" align="center">
|
<Flex gap="1" align="center">
|
||||||
<img
|
<img
|
||||||
alt="Silver"
|
alt="Silver"
|
||||||
src={asset("icons/currencies/silver.webp")}
|
src={"/api/icons/currencies/silver.webp"}
|
||||||
style={{
|
style={{
|
||||||
width: "1em",
|
width: "1em",
|
||||||
height: "1em",
|
height: "1em",
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
<Flex gap="1" align="center">
|
<Flex gap="1" align="center">
|
||||||
<img
|
<img
|
||||||
alt="Silver"
|
alt="Silver"
|
||||||
src={asset("icons/currencies/silver.webp")}
|
src={"/api/icons/currencies/silver.webp"}
|
||||||
style={{
|
style={{
|
||||||
width: "1em",
|
width: "1em",
|
||||||
height: "1em",
|
height: "1em",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { asset } from "@blitzkit/core";
|
|
||||||
import { useLoader } from "@react-three/fiber";
|
import { useLoader } from "@react-three/fiber";
|
||||||
import { GLTFLoader } from "three-stdlib";
|
import { GLTFLoader } from "three-stdlib";
|
||||||
import { useDispose } from "./useDispose";
|
import { useDispose } from "./useDispose";
|
||||||
|
|
||||||
export function useArmor(id: number) {
|
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);
|
const gltf = useLoader(GLTFLoader, path);
|
||||||
|
|
||||||
useDispose(gltf, 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 {
|
import {
|
||||||
Hierarchy,
|
|
||||||
Sc2ReadStream,
|
Sc2ReadStream,
|
||||||
ScgReadStream,
|
ScgReadStream,
|
||||||
|
toUniqueId,
|
||||||
VertexAttribute,
|
VertexAttribute,
|
||||||
|
vertexAttributeVectorSizes,
|
||||||
|
type Hierarchy,
|
||||||
|
type VehicleDefinitionList,
|
||||||
} from "@blitzkit/core";
|
} 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 { times } from "lodash-es";
|
||||||
import { AbstractVFS } from "../vfs/abstract";
|
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||||
import {
|
|
||||||
vertexAttributeGLTFName,
|
|
||||||
vertexAttributeGltfVectorSizes,
|
|
||||||
} from "./extractModel/constants";
|
|
||||||
|
|
||||||
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 sc2Path = `Data/3d/Tanks/CollisionMeshes/${fileName}.sc2`;
|
||||||
const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
|
const scgPath = `Data/3d/Tanks/CollisionMeshes/${fileName}.scg`;
|
||||||
const sc2 = new Sc2ReadStream(
|
const sc2 = new Sc2ReadStream(
|
||||||
@@ -141,4 +172,18 @@ export async function extractArmor(vfs: AbstractVFS, fileName: string) {
|
|||||||
parseHierarchies(sc2["#hierarchy"], scene);
|
parseHierarchies(sc2["#hierarchy"], scene);
|
||||||
|
|
||||||
return document;
|
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 {
|
import {
|
||||||
bufferToBigInt,
|
bufferToBigInt,
|
||||||
ConfigArchive,
|
DdsReadStream,
|
||||||
Hierarchy,
|
PvrReadStream,
|
||||||
Sc2ReadStream,
|
Sc2ReadStream,
|
||||||
ScgReadStream,
|
ScgReadStream,
|
||||||
Textures,
|
toUniqueId,
|
||||||
VertexAttribute,
|
VertexAttribute,
|
||||||
|
type ConfigArchive,
|
||||||
|
type Hierarchy,
|
||||||
|
type TankParameters,
|
||||||
|
type Textures,
|
||||||
|
type VehicleDefinitionList,
|
||||||
} from "@blitzkit/core";
|
} 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 { dedup, prune } from "@gltf-transform/functions";
|
||||||
|
import type { APIContext } from "astro";
|
||||||
import { times } from "lodash-es";
|
import { times } from "lodash-es";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import { AbstractVFS } from "../../vfs/abstract";
|
import sharp from "sharp";
|
||||||
import { readBaseColor } from "../readBaseColor";
|
import { vfs } from "../../../../core/blitzkit/vfs";
|
||||||
import { readNormal } from "../readNormal";
|
|
||||||
import { readRoughnessMetallic } from "../readRoughnessMetallic";
|
|
||||||
import {
|
import {
|
||||||
vertexAttributeGLTFName,
|
vertexAttributeGLTFName,
|
||||||
vertexAttributeGltfVectorSizes,
|
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;
|
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
||||||
|
|
||||||
@@ -27,7 +69,7 @@ const omitMeshNames = {
|
|||||||
end: ["_POINT"],
|
end: ["_POINT"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function extractModel(vfs: AbstractVFS, path: string) {
|
async function extractModel(vfs: AbstractVFS, path: string) {
|
||||||
const sc2Path = `Data/3d/${path}.sc2`;
|
const sc2Path = `Data/3d/${path}.sc2`;
|
||||||
const scgPath = `Data/3d/${path}.scg`;
|
const scgPath = `Data/3d/${path}.scg`;
|
||||||
const sc2 = new Sc2ReadStream(
|
const sc2 = new Sc2ReadStream(
|
||||||
@@ -325,3 +367,115 @@ export async function extractModel(vfs: AbstractVFS, path: string) {
|
|||||||
|
|
||||||
return document;
|
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