Container: offene Varianten (eine Tür offen / beide Türen offen / ohne Türen), 8 Farben, Innenseiten mit Paletten-Textur + Trimesh-Kollision

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
slyphax 2026-10-27 23:42:09 +01:00
parent 902181500a
commit 8345903ce7
50 changed files with 733 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5569,6 +5569,51 @@
"kategorie": "kisten",
"name_de": "Container 01"
},
"container_001_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_001_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_001_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 01 (eine Tür offen)"
},
"container_001_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_001_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_001_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 01 (beide Türen offen)"
},
"container_001_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_001_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_001_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 01 (ohne Türen)"
},
"container_002": {
"poly": 1816,
"collision": {
@ -5584,6 +5629,51 @@
"kategorie": "kisten",
"name_de": "Container 02"
},
"container_002_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_002_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_002_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 02 (eine Tür offen)"
},
"container_002_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_002_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_002_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 02 (beide Türen offen)"
},
"container_002_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_002_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_002_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 02 (ohne Türen)"
},
"container_003": {
"poly": 1816,
"collision": {
@ -5599,6 +5689,51 @@
"kategorie": "kisten",
"name_de": "Container 03"
},
"container_003_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_003_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_003_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 03 (eine Tür offen)"
},
"container_003_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_003_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_003_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 03 (beide Türen offen)"
},
"container_003_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_003_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_003_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 03 (ohne Türen)"
},
"container_004": {
"poly": 1816,
"collision": {
@ -5614,6 +5749,51 @@
"kategorie": "kisten",
"name_de": "Container 04"
},
"container_004_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_004_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_004_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 04 (eine Tür offen)"
},
"container_004_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_004_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_004_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 04 (beide Türen offen)"
},
"container_004_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_004_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_004_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 04 (ohne Türen)"
},
"container_005": {
"poly": 1816,
"collision": {
@ -5629,6 +5809,51 @@
"kategorie": "kisten",
"name_de": "Container 05"
},
"container_005_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_005_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_005_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 05 (eine Tür offen)"
},
"container_005_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_005_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_005_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 05 (beide Türen offen)"
},
"container_005_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_005_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_005_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 05 (ohne Türen)"
},
"container_006": {
"poly": 1816,
"collision": {
@ -5644,6 +5869,51 @@
"kategorie": "kisten",
"name_de": "Container 06"
},
"container_006_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_006_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_006_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 06 (eine Tür offen)"
},
"container_006_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_006_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_006_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 06 (beide Türen offen)"
},
"container_006_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_006_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_006_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 06 (ohne Türen)"
},
"container_007": {
"poly": 1816,
"collision": {
@ -5659,6 +5929,51 @@
"kategorie": "kisten",
"name_de": "Container 07"
},
"container_007_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_007_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_007_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 07 (eine Tür offen)"
},
"container_007_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_007_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_007_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 07 (beide Türen offen)"
},
"container_007_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_007_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_007_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 07 (ohne Türen)"
},
"container_008": {
"poly": 1816,
"collision": {
@ -5674,6 +5989,51 @@
"kategorie": "kisten",
"name_de": "Container 08"
},
"container_008_offen1": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
7.19
],
"quelle": "kollisions/container_008_offen1.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_008_offen1.glb",
"kategorie": "kisten",
"name_de": "Container 08 (eine Tür offen)"
},
"container_008_offen2": {
"poly": 1832,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
8.16
],
"quelle": "kollisions/container_008_offen2.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_008_offen2.glb",
"kategorie": "kisten",
"name_de": "Container 08 (beide Türen offen)"
},
"container_008_ohne": {
"poly": 488,
"collision": {
"shape": "trimesh",
"size": [
2.52,
2.7,
6.16
],
"quelle": "kollisions/container_008_ohne.json"
},
"glb": "res://assets/objekte/military_desert/kisten/container_008_ohne.glb",
"kategorie": "kisten",
"name_de": "Container 08 (ohne Türen)"
},
"deadwood_001": {
"poly": 1335,
"collision": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,349 @@
#!/usr/bin/env python3
"""Offene Container-Varianten (Military-Pack, container_001..008).
Aus jedem Pack-Container entstehen drei begehbare Varianten:
<oid>_offen1 eine Tuer (Stirnseite +Z) aufgeschwenkt, die andere zu
<oid>_offen2 beide Stirnseiten offen, Fluegel aufgeschwenkt
<oid>_ohne alle Tuer-Teile entfernt (freier Durchgang)
Das Pack-Original hat einseitige Waende (Innenseite wird weggecullt) — hier
bekommen Boden/Decke/Waende/Tuerblaetter eine umgedrehte Innenseite mit
Paletten-Textur (gleiche Pack-Textur, kein zweites Material -> keine Extra-
Draw-Calls auf der Quest). Die Kollision ist ein Trimesh aus derselben
Geometrie ohne Tuer-Beschlaege (Riegel/Scharniere/Griffe), damit Server,
Client und Editor dieselbe Form bauen.
Aufruf (aus dem Editor-Repo, Geschwister-Klone wie bei gen_bauteile.gd):
python3 tools/gen_container_varianten.py --client ../scopedvr-client \
--server ../scopedvr-match-server
Danach: im Match-Server python3 tools/merge_object_library.py und im Editor
tools/sync_objects.sh. Idempotent (gleiche Eingabe -> gleiche Dateien).
"""
from __future__ import annotations
import argparse
import json
import math
import os
import struct
import sys
import numpy as np
KATEGORIE = "kisten"
FARBEN = range(1, 9)
VARIANTEN = [
# suffix, offene Enden, Tuer-Teile entfernen?, Namenszusatz
("offen1", ("+z",), False, "eine Tür offen"),
("offen2", ("+z", "-z"), False, "beide Türen offen"),
("ohne", (), True, "ohne Türen"),
]
AUF_WINKEL = 100.0 # Aufschwenkwinkel der Fluegel (Grad)
RUND = 2 # Kollisions-JSON wie im Pack: 2 Nachkommastellen
# Paletten-Texel (Demonstration_Textures1.png: 19 Spalten x 10 Zeilen, je 51,2 px)
def _texel(spalte: int, zeile: int) -> tuple:
return ((spalte + 0.5) * 51.2 / 1024.0, (zeile + 0.5) * 51.2 / 1024.0)
UV_WAND = _texel(17, 5) # Grau
UV_DECKE = _texel(17, 3) # helleres Grau
UV_BODEN = _texel(16, 6) # Holzbraun
def lade_glb(pfad):
b = open(pfad, "rb").read()
jl = struct.unpack("<I", b[12:16])[0]
j = json.loads(b[20:20 + jl])
off = 20 + jl
bl = struct.unpack("<I", b[off:off + 4])[0]
return j, b[off + 8:off + 8 + bl]
def accessor(j, binb, i):
a = j["accessors"][i]
bv = j["bufferViews"][a["bufferView"]]
n = {"SCALAR": 1, "VEC2": 2, "VEC3": 3}[a["type"]]
dt = {5126: "<f4", 5123: "<u2", 5125: "<u4"}[a["componentType"]]
o = bv.get("byteOffset", 0) + a.get("byteOffset", 0)
return np.frombuffer(binb, dtype=dt, count=a["count"] * n, offset=o).reshape(-1, n)
def schreibe_glb(pfad, j_alt, pos, nor, uv, trans):
"""Indiziertes GLB (ein Mesh, ein Material) — Material/Textur wie im Original."""
key = np.concatenate([pos, nor, uv], axis=1).astype(np.float32)
_, first, inv = np.unique(key, axis=0, return_index=True, return_inverse=True)
inv = inv.ravel()
# stabile Reihenfolge: nach erstem Auftreten
order = np.argsort(first)
rank = np.empty_like(order)
rank[order] = np.arange(len(order))
first = first[order]
idx = rank[inv].astype(np.uint32)
P = pos[first].astype("<f4"); N = nor[first].astype("<f4"); U = uv[first].astype("<f4")
parts = [P.tobytes(), N.tobytes(), U.tobytes(), idx.astype("<u4").tobytes()]
views, offs, o = [], [], 0
for k, p in enumerate(parts):
views.append({"buffer": 0, "byteOffset": o, "byteLength": len(p),
"target": 34963 if k == 3 else 34962})
o += len(p)
binb = b"".join(parts)
n = len(P)
accs = [
{"bufferView": 0, "componentType": 5126, "count": n, "type": "VEC3",
"min": P.min(0).tolist(), "max": P.max(0).tolist()},
{"bufferView": 1, "componentType": 5126, "count": n, "type": "VEC3"},
{"bufferView": 2, "componentType": 5126, "count": n, "type": "VEC2"},
{"bufferView": 3, "componentType": 5125, "count": len(idx), "type": "SCALAR"},
]
name = os.path.splitext(os.path.basename(pfad))[0]
j = {
"asset": j_alt["asset"],
"scene": 0,
"scenes": [{"name": "Scene", "nodes": [0]}],
"nodes": [{"mesh": 0, "name": name, "translation": trans}],
"meshes": [{"name": name + "_mesh", "primitives": [
{"attributes": {"POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2}, "indices": 3, "material": 0}]}],
"materials": j_alt["materials"],
"textures": j_alt["textures"],
"images": j_alt["images"],
"samplers": j_alt.get("samplers", [{}]),
"accessors": accs,
"bufferViews": views,
"buffers": [{"byteLength": len(binb)}],
}
js = json.dumps(j, separators=(",", ":")).encode()
js += b" " * (-len(js) % 4)
binb += b"\0" * (-len(binb) % 4)
total = 12 + 8 + len(js) + 8 + len(binb)
with open(pfad, "wb") as f:
f.write(struct.pack("<III", 0x46546C67, 2, total))
f.write(struct.pack("<II", len(js), 0x4E4F534A) + js)
f.write(struct.pack("<II", len(binb), 0x004E4942) + binb)
# ------------------------------------------------------------- Geometrie ----
def komponenten(tris_pos):
"""Union-Find ueber gemeinsame (gerundete) Eckpunkte -> Komponenten-ID je Dreieck."""
v = np.round(tris_pos, 3)
n = len(v)
par = list(range(n))
def f(x):
while par[x] != x:
par[x] = par[par[x]]
x = par[x]
return x
first = {}
for i in range(n):
for p in v[i]:
k = tuple(p)
if k in first:
par[f(i)] = f(first[k])
else:
first[k] = i
return np.array([f(i) for i in range(n)])
def ist_tuerteil(box_min, box_max, zc):
"""Tuer-Hardware/-Blatt: an einer Stirnseite, aber kein Eck-Beschlag."""
if abs(zc) <= 2.85:
return False
ecke = (box_max[1] < 0.3 or box_min[1] > 2.4) and max(abs(box_min[0]), abs(box_max[0])) > 0.95
return not ecke
def rot_y(pos, nor, pivot, grad):
"""Drehung um die senkrechte Achse durch pivot=(x,z). (a,b) -> (a cos - b sin, a sin + b cos)."""
w = math.radians(grad)
c, s = math.cos(w), math.sin(w)
p = pos.copy(); n = nor.copy()
a = pos[..., 0] - pivot[0]; b = pos[..., 2] - pivot[1]
p[..., 0] = pivot[0] + a * c - b * s
p[..., 2] = pivot[1] + a * s + b * c
n[..., 0] = nor[..., 0] * c - nor[..., 2] * s
n[..., 2] = nor[..., 0] * s + nor[..., 2] * c
return p, n
def baue(j, binb):
prim = j["meshes"][0]["primitives"][0]
P = accessor(j, binb, prim["attributes"]["POSITION"]).astype(np.float64)
N = accessor(j, binb, prim["attributes"]["NORMAL"]).astype(np.float64)
U = accessor(j, binb, prim["attributes"]["TEXCOORD_0"]).astype(np.float64)
I = accessor(j, binb, prim["indices"]).ravel().reshape(-1, 3)
trans = np.array(j["nodes"][0].get("translation", [0, 0, 0]), dtype=np.float64)
tp = P[I] + trans # (n,3,3) im Objekt-Fusspunkt-Raum (= Kollisions-JSON)
tn = N[I]
tu = U[I]
comp = komponenten(tp)
tuer = np.zeros(len(tp), dtype=bool)
ende = np.zeros(len(tp), dtype=np.int8) # +1 / -1 je Tuer-Dreieck
hardware = np.zeros(len(tp), dtype=bool) # Riegel/Scharniere/Griffe (nicht Blatt)
global _COMP_N, _KOMP
_KOMP = comp
_COMP_N = np.array([(comp == c).sum() for c in comp])
for c in np.unique(comp):
m = comp == c
pts = tp[m].reshape(-1, 3)
lo, hi = pts.min(0), pts.max(0)
zc = (lo[2] + hi[2]) / 2
if ist_tuerteil(lo, hi, zc):
tuer |= m
ende[m] = 1 if zc > 0 else -1
# Tuerblaetter = grosse ebene Dreiecke senkrecht Z, Rest = Hardware
area = np.linalg.norm(np.cross(tp[:, 1] - tp[:, 0], tp[:, 2] - tp[:, 0]), axis=1) / 2
nz = np.abs(np.cross(tp[:, 1] - tp[:, 0], tp[:, 2] - tp[:, 0])[:, 2]) / np.maximum(area * 2, 1e-9)
blatt = tuer & (area > 0.5) & (nz > 0.99)
# 60-Tri-Schale (Blatt mit Rahmenprofil, beide Fluegel verschweisst) gehoert ebenfalls zum Blatt
hardware = tuer & ~blatt
return tp, tn, tu, tuer, ende, blatt, hardware, trans
_COMP_N = None
_KOMP = None
def scharniere(tp, tuer, ende, hardware):
"""Drehpunkt (x,z) je (Ende, Seite) aus den Scharnier-Manschetten (16 Dreiecke, x ~ +-1,05)."""
piv = {}
cen = tp.mean(1)
for e in (1, -1):
for seite in (-1, 1):
m = tuer & (ende == e) & hardware & (_COMP_N == 16) & (np.sign(cen[:, 0]) == seite) & (np.abs(cen[:, 0]) > 0.95)
if not m.any():
raise SystemExit("Scharnier nicht gefunden (Ende %d Seite %d)" % (e, seite))
pts = tp[m].reshape(-1, 3)
piv[(e, seite)] = ((pts[:, 0].min() + pts[:, 0].max()) / 2, (pts[:, 2].min() + pts[:, 2].max()) / 2)
return piv
def variante(tp, tn, tu, tuer, ende, blatt, hardware, offene_enden, ohne_tuer):
"""-> (pos, nor, uv) sichtbar und kollisions-Dreiecke (n,3,3)."""
piv = scharniere(tp, tuer, ende, hardware)
sicht_p, sicht_n, sicht_u = [], [], []
kol = []
cen = tp.mean(1)
# --- statischer Koerper (kein Tuerteil) ---
k = ~tuer
sicht_p.append(tp[k]); sicht_n.append(tn[k]); sicht_u.append(tu[k])
# Kollision: nur Rahmen (grosse Komponente) + Wand-/Boden-/Deckenflaechen; die 6-cm-
# Rippen und Eckbeschlaege bleiben sichtbar, kollidieren aber nicht (Dreiecks-Budget
# der Registry: test_trimesh_alle S2 <= 130000).
gruppe = np.array([(_KOMP == c).sum() for c in _KOMP])
kol.append(tp[k & ((gruppe >= 60) | (gruppe == 2))])
area = np.linalg.norm(np.cross(tp[:, 1] - tp[:, 0], tp[:, 2] - tp[:, 0]), axis=1) / 2
nrm = np.cross(tp[:, 1] - tp[:, 0], tp[:, 2] - tp[:, 0])
nrm = nrm / np.maximum(np.linalg.norm(nrm, axis=1), 1e-9)[:, None]
def innen(mask, uvfix):
"""Umgedrehte Kopie (Innenseite) der Dreiecke in mask, mit Paletten-UV."""
p = tp[mask][:, ::-1, :].copy()
n = -tn[mask][:, ::-1, :].copy()
u = np.tile(np.array(uvfix, dtype=np.float64), (len(p), 3, 1))
return p, n, u
gross = ~tuer & (area > 5) # Waende / Decke / Boden (einseitig)
for mask, uvfix in (
(gross & (np.abs(nrm[:, 0]) > 0.99), UV_WAND),
(gross & (nrm[:, 1] > 0.99), UV_DECKE),
(gross & (nrm[:, 1] < -0.99), UV_BODEN),
):
p, n, u = innen(mask, uvfix)
sicht_p.append(p); sicht_n.append(n); sicht_u.append(u)
kol.append(p)
# --- Tuer-Teile ---
for e in (1, -1):
m_e = tuer & (ende == e)
if ohne_tuer:
continue
if (("+z" if e > 0 else "-z") in offene_enden):
for seite in (-1, 1):
ms = m_e & (np.where(cen[:, 0] < 0, -1, 1) == seite)
# Blatt-Innenseite (flach, einseitig) mit anhaengen
bl = ms & blatt
ip, inn, iu = innen(bl, UV_WAND)
p = np.concatenate([tp[ms], ip]); n = np.concatenate([tn[ms], inn]); u = np.concatenate([tu[ms], iu])
# Aufschwenken nach aussen: linkes Blatt am +Z-Ende dreht +w, rechtes -w; am -Z-Ende umgekehrt
w = AUF_WINKEL * seite * -1 * e
p, n = rot_y(p, n, piv[(e, seite)], w)
sicht_p.append(p); sicht_n.append(n); sicht_u.append(u)
# Kollision: nur die Blatt-Platten (ohne Beschlaege), mit gedreht
kb = np.concatenate([tp[bl], ip])
kb, _ = rot_y(kb, np.zeros_like(kb), piv[(e, seite)], w)
kol.append(kb)
else:
bl = m_e & blatt
ip, inn, iu = innen(bl, UV_WAND)
sicht_p.append(tp[m_e]); sicht_n.append(tn[m_e]); sicht_u.append(tu[m_e])
sicht_p.append(ip); sicht_n.append(inn); sicht_u.append(iu)
kol.append(tp[bl]); kol.append(ip)
cat = lambda L: np.concatenate(L).reshape(-1, 3)
return cat(sicht_p), cat(sicht_n), np.concatenate(sicht_u).reshape(-1, 2), np.concatenate(kol)
def kollisions_json(oid, glb_pfad, tris):
v = np.round(tris.reshape(-1, 3), RUND)
# entartete Dreiecke (nach Rundung) raus
t = v.reshape(-1, 3, 3)
ok = ~((t[:, 0] == t[:, 1]).all(1) | (t[:, 1] == t[:, 2]).all(1) | (t[:, 0] == t[:, 2]).all(1))
t = t[ok]
v = t.reshape(-1, 3)
return {
"aabb_max": [round(float(x), RUND) for x in v.max(0)],
"aabb_min": [round(float(x), RUND) for x in v.min(0)],
"dreiecke": int(len(t)),
"oid": oid,
"quelle_glb": "res://assets/objekte/military_desert/%s/%s.glb" % (KATEGORIE, oid),
"vertices": [[float(x) for x in p] for p in v],
}, t
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--client", required=True)
ap.add_argument("--server", required=True)
ap.add_argument("--editor", default=os.path.join(os.path.dirname(__file__), ".."))
a = ap.parse_args()
glb_dir = os.path.join(a.client, "assets/objekte/military_desert", KATEGORIE)
kol_dirs = [os.path.join(d, "library/kollisions") for d in (a.server, a.client, a.editor)]
for d in kol_dirs:
os.makedirs(d, exist_ok=True)
fragment = {"objects": {}}
for n in FARBEN:
quelle = "container_%03d" % n
j, binb = lade_glb(os.path.join(glb_dir, quelle + ".glb"))
tp, tn, tu, tuer, ende, blatt, hardware, trans = baue(j, binb)
if n == 1:
print("Tuer-Dreiecke: %d (Blatt %d, Beschlaege %d) von %d" % (
tuer.sum(), blatt.sum(), hardware.sum(), len(tp)))
for suffix, enden, ohne, text in VARIANTEN:
oid = "%s_%s" % (quelle, suffix)
pos, nor, uv, kol = variante(tp, tn, tu, tuer, ende, blatt, hardware, enden, ohne)
# GLB im Fusspunkt-Raum (translation=0): Kollision == Sichtgeometrie
schreibe_glb(os.path.join(glb_dir, oid + ".glb"), j, pos.astype(np.float32),
nor.astype(np.float32), uv.astype(np.float32), [0.0, 0.0, 0.0])
kj, kt = kollisions_json(oid, None, kol)
s = json.dumps(kj, separators=(",", ":"), sort_keys=True)
for d in kol_dirs:
with open(os.path.join(d, oid + ".json"), "w") as f:
f.write(s)
size = [round(float(x), 2) for x in (kj["aabb_max"][i] - kj["aabb_min"][i] for i in range(3))]
fragment["objects"][oid] = {
"poly": int(len(pos) // 3),
"collision": {"shape": "trimesh", "size": size, "quelle": "kollisions/%s.json" % oid},
"glb": "res://assets/objekte/military_desert/%s/%s.glb" % (KATEGORIE, oid),
"kategorie": KATEGORIE,
"name_de": "Container %02d (%s)" % (n, text),
}
print("%s: %d Sicht-, %d Kollisions-Dreiecke" % (oid, len(pos) // 3, len(kt)))
frag = os.path.join(a.server, "library/objects.d/container_offen.json")
with open(frag, "w") as f:
f.write(json.dumps(fragment, indent=2, ensure_ascii=False) + "\n")
print("Fertig: %d Objekte." % len(fragment["objects"]))
if __name__ == "__main__":
sys.exit(main())