Files
gta6/tools/assets/weapons.mjs
T

119 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Baut die Waffen aus dem ASIS Weapons Pack zu GLB um (docs/superpowers/specs/2026-09-17-animationen-design.md,
// "Umbau der Waffen"):
//
// node tools/assets/weapons.mjs <Ordner WeaponsPack> -> public/models/weapons/<id>.glb
//
// Das Pack bleibt außerhalb des Repos. Die Geometrie kommt über three.js' FBXLoader, die Materialien werden neu
// gebaut: T_ ist die Grundfarbe, N_ die Normalmap (schon im OpenGL-Format von glTF), M_ eine TGA-Maske mit
// Grau = Rauheit und Alpha = Metall (am AK47-Holz gemessen, s. Entwurf).
import { fileURLToPath } from "node:url";
import { Vector3 } from "three";
export const TEXTURE_SIZE = 1024;
/** Die fünf Waffen des Packs. length ist die erwartete Länge in Metern und fängt eine falsche Einheit ab. */
export const WEAPON_SOURCES = [
{ id: "pistol", dir: "Pistol", mesh: "SM_Pistol.fbx", base: "T_Pistol.png", normal: "N_Pistol.png", mask: "M_Pistol.tga", length: [0.15, 0.25] },
{ id: "ak47", dir: "AK47_Option2(Same Socketing)", mesh: "SM_AK47.fbx", base: "T_AK47.png", normal: "N_AK47.png", mask: "M_AK47.tga", length: [0.7, 1.3] },
{ id: "m4", dir: "M4 Carbine", mesh: "SM_M4_Carbine.fbx", base: "T_M4_Carbine.png", normal: "N_M4_Carbine.png", mask: "M_M4_Carbine.tga", length: [0.7, 1.3] },
{ id: "shotgun870", dir: "870 Shotgun", mesh: "SM_870_Shotgun.fbx", base: "T_870_Shotgun.png", normal: "N_870_Shotgun.png", mask: "M_870_Shotgun.tga", length: [0.7, 1.3] },
{ id: "l96", dir: "L96 Sniper Rifle", mesh: "SM_L96_Sniper_Rifle.fbx", base: "T_L96_Sniper_Rifle.png", normal: "N_L96_Sniper_Rifle.png", mask: "M_L96_Sniper_Rifle.tga", length: [0.7, 1.3] },
];
/**
* Liest ein unkomprimiertes 32-Bit-TGA (Typ 2) und gibt RGBA mit der obersten Bildzeile zuerst zurück. Mehr kann
* der Leser nicht, und mehr steckt auch nicht im Pack: alle fünf Masken sind Typ 2, 32 Bit, Ursprung unten links.
*/
export function readTga(bytes) {
const idLength = bytes[0];
const colorMapType = bytes[1];
const imageType = bytes[2];
const width = bytes[12] | (bytes[13] << 8);
const height = bytes[14] | (bytes[15] << 8);
const depth = bytes[16];
const flags = bytes[17];
if (colorMapType !== 0 || imageType !== 2 || depth !== 32) {
throw new Error(`TGA: nur unkomprimiert mit 32 Bit lesbar (Typ ${imageType}, ${depth} Bit, Farbtabelle ${colorMapType})`);
}
if (flags & 0x10) throw new Error("TGA: Zeilen von rechts nach links werden nicht unterstützt");
const start = 18 + idLength;
if (bytes.length < start + width * height * 4) throw new Error("TGA: Datei kürzer als ihre Bildgröße");
const originTop = (flags & 0x20) !== 0;
const data = new Uint8Array(width * height * 4);
for (let row = 0; row < height; row++) {
const sourceRow = originTop ? row : height - 1 - row;
for (let x = 0; x < width; x++) {
const from = start + (sourceRow * width + x) * 4;
const to = (row * width + x) * 4;
data[to] = bytes[from + 2]; // TGA speichert BGRA
data[to + 1] = bytes[from + 1];
data[to + 2] = bytes[from];
data[to + 3] = bytes[from + 3];
}
}
return { width, height, data };
}
/** Maske -> metallicRoughness von glTF: Rauheit nach G, Metall nach B. R bleibt ungenutzt. */
export function maskToMetallicRoughness({ width, height, data }) {
const rgb = new Uint8Array(width * height * 3);
for (let i = 0; i < width * height; i++) {
rgb[i * 3] = 255;
rgb[i * 3 + 1] = data[i * 4];
rgb[i * 3 + 2] = data[i * 4 + 3];
}
return rgb;
}
/**
* Eckpunkte, Normalen und UVs eines FBX-Meshes für glTF. Die Knotendrehung wird eingerechnet (die L96 liegt im
* Pack um 180° gedreht), der Knotenmaßstab nicht: Die Eckpunkte liegen schon in Metern, die Pistole trägt
* zusätzlich den Maßstab 100. Die v-Achse wird gespiegelt, weil FBX den UV-Ursprung unten links hat, glTF oben links.
*/
export function meshArrays(geometry, quaternion) {
const source = geometry.index ? geometry.toNonIndexed() : geometry;
const position = source.getAttribute("position");
const normal = source.getAttribute("normal");
const uv = source.getAttribute("uv");
if (!position || !normal || !uv) throw new Error("Mesh ohne position, normal oder uv");
const positions = new Float32Array(position.count * 3);
const normals = new Float32Array(position.count * 3);
const uvs = new Float32Array(position.count * 2);
const point = new Vector3();
for (let i = 0; i < position.count; i++) {
point.fromBufferAttribute(position, i).applyQuaternion(quaternion).toArray(positions, i * 3);
point.fromBufferAttribute(normal, i).applyQuaternion(quaternion).normalize().toArray(normals, i * 3);
uvs[i * 2] = uv.getX(i);
uvs[i * 2 + 1] = 1 - uv.getY(i);
}
return { positions, normals, uvs };
}
/** Längste Ausdehnung in Metern. */
export function longestExtent(positions) {
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
for (let i = 0; i < positions.length; i += 3) {
for (let axis = 0; axis < 3; axis++) {
min[axis] = Math.min(min[axis], positions[i + axis]);
max[axis] = Math.max(max[axis], positions[i + axis]);
}
}
return Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
}
/** Bricht ab, wenn die Waffe nicht die erwartete Länge hat — dann stimmt die Einheit nicht. */
export function checkLength(id, positions, [min, max]) {
const length = longestExtent(positions);
if (!(length >= min && length <= max)) {
throw new Error(`${id}: ${length.toFixed(3)} m lang, erwartet ${min}–${max} m — falsche Einheit?`);
}
return length;
}
/** Zielpfad einer Waffe, unabhängig vom Arbeitsverzeichnis (wie placeTarget in tools/assets/places.mjs). */
export function weaponTarget(id) {
if (!/^[a-z0-9]+$/.test(id)) throw new Error(`Ungültige Waffen-ID "${id}": nur Kleinbuchstaben und Ziffern.`);
return fileURLToPath(new URL(`../../public/models/weapons/${id}.glb`, import.meta.url));
}