commit 153e26c329a5b987400f283023326da21fc88838 Author: type-two Date: Mon Aug 3 19:47:34 2026 +1000 park_kit v0.1 — 27 textured park props + 18 MODELBEAST surfaces + manifest propkit.py: textured-GLB writer (TEXCOORD_0, shared external textures, MIRRORED_REPEAT). prop_author.py: parametric generators — ramps, rails, furniture, trees, dressing — each with local-space grind metadata and skatemakerpro element/collider hints. Co-Authored-By: Claude Opus 5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..76ef476 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# park_kit — GODVERSE park object library + +Reusable, textured, license-clean park objects for **bookquoy** and **skatemakerpro** +(and whatever comes next). 27 props, 18 photoreal surfaces, all original: geometry +authored parametrically in `tools/prop_author.py`, textures generated on the +MODELBEAST farm (flux_local, local GPU, no cloud spend). + +## The contract + +**`props/` and `textures/` ship together.** GLBs are geometry-only (2–24KB) and +reference `../textures/*.jpg` by relative URI, so every prop shares one texture set +and the browser caches each surface once. Move a GLB without the textures folder +and it loads grey. + +`manifest.json` is the catalog. Per prop: + +| field | meaning | +|---|---| +| `src` | `props/.glb` — drop straight into a skatemakerpro `props[]` entry | +| `grinds` | grindable edges in **prop-local metres** `{a:[x,z], b:[x,z], ya, yb, kind}` — rotate/translate with the prop, append to the park `rails[]` | +| `element` | matching skatemakerpro terrain kind (`quarter`/`bank`/`funbox`/`spine`/`ledge`/`mogul`) with params — place alongside ramp props for real collision from the heightfield | +| `collider` | simple blocker hint for furniture/trees (`box`/`cylinder`/`none`) | +| `size` | world-space bounding box `[x, y, z]` | + +Sampler is MIRRORED_REPEAT — tileable-intent textures never show a hard seam. +UVs are world-scale (metres per tile fixed per material), so texture density +matches across every prop. + +## Catalog + +- **obstacles (14):** qp_24, qp_18, bank_12, kicker, funbox_ply, spine_ply, + manual_pad, ledge_concrete, grindbox_ply, rail_flat_3m, rail_flat_6m, + rail_kink, rail_rainbow, pipe_full (graffiti-bombed concrete full pipe) +- **furniture (7):** bench_park, bench_concrete, picnic_table, bin_council, + bollard_steel, railing_ped_2m, planter_box (with gum sapling) +- **vegetation (2):** tree_fig (Moreton Bay, buttress roots), tree_gum (eucalypt) +- **dressing (4):** kerb_3m (slappy central), wall_graffiti (wallride bait), + shade_sail, mound_dirt + +## View it + +```bash +python3 serve.py 8151 # then open http://localhost:8151 +``` + +Dropdown per prop or the full grid; red lines = grind metadata (toggle). +`window.__view(id)` / `window.__ready()` hooks for headless checks. + +## Regenerate + +```bash +python3 tools/gen_textures.py # MODELBEAST textures (idempotent; --force name to redo) +python3 tools/prop_author.py # all GLBs + manifest.json +``` + +`tools/propkit.py` is the textured-GLB writer (UVs + external images — the +character_kit writers' upgrade). `fan(..., out=)` auto-orients windings; lathe +UVs wrap girth-first to match tube (bark ridges stay vertical). + +## Provenance + +Everything here is original GODVERSE work. Textures: flux2-klein-4b on the farm, +prompts in `tools/gen_textures.py`. No game assets, no trademarks, no scans. diff --git a/index.html b/index.html new file mode 100644 index 0000000..33c25ad --- /dev/null +++ b/index.html @@ -0,0 +1,140 @@ + + + + +park_kit — catalog + + + +
+ park_kit + + +
+
+ + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..6c84dcd --- /dev/null +++ b/manifest.json @@ -0,0 +1,914 @@ +{ + "name": "park_kit", + "version": "0.1", + "note": "Original GODVERSE park objects. props/ and textures/ must ship together (GLBs reference ../textures/*.jpg). grinds are prop-local segments \u2014 rotate/translate with the prop and append to the park rails list. element = matching skatemakerpro terrain kind for real collision.", + "textures": "MODELBEAST flux_local (flux2-klein-4b), original generations", + "props": [ + { + "id": "qp_24", + "src": "props/qp_24.glb", + "category": "obstacle", + "desc": "plywood quarter pipe 4ft, steel coping", + "size": [ + 2.4, + 1.25, + 2.297 + ], + "tris": 124, + "grinds": [ + { + "a": [ + -1.2, + -1.697056274847714 + ], + "b": [ + 1.2, + -1.697056274847714 + ], + "ya": 1.22, + "yb": 1.22, + "kind": "rail" + } + ], + "element": { + "kind": "quarter", + "len": 2.4, + "r0": 1.8, + "r1": 1.8, + "h": 1.2 + } + }, + { + "id": "qp_18", + "src": "props/qp_18.glb", + "category": "obstacle", + "desc": "mini quarter 3ft", + "size": [ + 1.8, + 0.95, + 1.873 + ], + "tris": 124, + "grinds": [ + { + "a": [ + -0.9, + -1.2727922061357857 + ], + "b": [ + 0.9, + -1.2727922061357857 + ], + "ya": 0.92, + "yb": 0.92, + "kind": "rail" + } + ], + "element": { + "kind": "quarter", + "len": 1.8, + "r0": 1.35, + "r1": 1.35, + "h": 0.9 + } + }, + { + "id": "bank_12", + "src": "props/bank_12.glb", + "category": "obstacle", + "desc": "plywood wedge bank", + "size": [ + 2.4, + 0.9, + 2.2 + ], + "tris": 54, + "grinds": [ + { + "a": [ + -1.2, + 0 + ], + "b": [ + 1.2, + 0 + ], + "ya": 0.9, + "yb": 0.9, + "kind": "ledge" + } + ], + "element": { + "kind": "bank", + "len": 2.4, + "h": 0.9, + "run": 1.8 + } + }, + { + "id": "kicker", + "src": "props/kicker.glb", + "category": "obstacle", + "desc": "curved plywood launch kicker", + "size": [ + 1.2, + 0.45, + 1.04 + ], + "tris": 56, + "element": { + "kind": "bank", + "len": 1.2, + "h": 0.45, + "run": 1.0 + } + }, + { + "id": "funbox_ply", + "src": "props/funbox_ply.glb", + "category": "obstacle", + "desc": "plywood funbox, banks both sides, steel edge caps", + "size": [ + 2.4, + 0.6, + 3.2 + ], + "tris": 68, + "grinds": [ + { + "a": [ + -1.2, + 0.6 + ], + "b": [ + 1.2, + 0.6 + ], + "ya": 0.6, + "yb": 0.6, + "kind": "ledge" + }, + { + "a": [ + -1.2, + -0.6 + ], + "b": [ + 1.2, + -0.6 + ], + "ya": 0.6, + "yb": 0.6, + "kind": "ledge" + } + ], + "element": { + "kind": "funbox", + "hw": 1.2, + "hd": 0.6, + "h": 0.6, + "skirt": 1.0 + } + }, + { + "id": "spine_ply", + "src": "props/spine_ply.glb", + "category": "obstacle", + "desc": "plywood spine, double coping", + "size": [ + 2.4, + 0.95, + 2.746 + ], + "tris": 188, + "grinds": [ + { + "a": [ + -1.2, + 0.1 + ], + "b": [ + 1.2, + 0.1 + ], + "ya": 0.92, + "yb": 0.92, + "kind": "rail" + }, + { + "a": [ + -1.2, + -0.1 + ], + "b": [ + 1.2, + -0.1 + ], + "ya": 0.92, + "yb": 0.92, + "kind": "rail" + } + ], + "element": { + "kind": "spine", + "len": 2.4, + "r": 1.35, + "h": 0.9 + } + }, + { + "id": "manual_pad", + "src": "props/manual_pad.glb", + "category": "obstacle", + "desc": "low concrete manual pad, steel edges", + "size": [ + 2.0, + 0.28, + 1.658 + ], + "tris": 60, + "grinds": [ + { + "a": [ + -1.0, + 0.8 + ], + "b": [ + 1.0, + 0.8 + ], + "ya": 0.28, + "yb": 0.28, + "kind": "ledge" + }, + { + "a": [ + -1.0, + -0.8 + ], + "b": [ + 1.0, + -0.8 + ], + "ya": 0.28, + "yb": 0.28, + "kind": "ledge" + } + ], + "element": { + "kind": "funbox", + "hw": 1.0, + "hd": 0.8, + "h": 0.28, + "skirt": 0.3 + } + }, + { + "id": "ledge_concrete", + "src": "props/ledge_concrete.glb", + "category": "obstacle", + "desc": "concrete grind ledge, tagged side", + "size": [ + 2.4, + 0.42, + 0.508 + ], + "tris": 62, + "grinds": [ + { + "a": [ + -1.2, + 0.225 + ], + "b": [ + 1.2, + 0.225 + ], + "ya": 0.42, + "yb": 0.42, + "kind": "ledge" + }, + { + "a": [ + -1.2, + -0.225 + ], + "b": [ + 1.2, + -0.225 + ], + "ya": 0.42, + "yb": 0.42, + "kind": "ledge" + } + ], + "collider": { + "type": "box", + "hw": 1.2, + "hd": 0.225, + "h": 0.42 + }, + "element": { + "kind": "ledge", + "hw": 1.2, + "hd": 0.225, + "h": 0.42 + } + }, + { + "id": "grindbox_ply", + "src": "props/grindbox_ply.glb", + "category": "obstacle", + "desc": "plywood grindbox: round bar one edge, angle the other", + "size": [ + 1.8, + 0.448, + 0.553 + ], + "tris": 68, + "grinds": [ + { + "a": [ + -0.9, + 0.25 + ], + "b": [ + 0.9, + 0.25 + ], + "ya": 0.42400000000000004, + "yb": 0.42400000000000004, + "kind": "rail" + }, + { + "a": [ + -0.9, + -0.25 + ], + "b": [ + 0.9, + -0.25 + ], + "ya": 0.4, + "yb": 0.4, + "kind": "ledge" + } + ], + "element": { + "kind": "ledge", + "hw": 0.9, + "hd": 0.25, + "h": 0.4 + } + }, + { + "id": "rail_flat_3m", + "src": "props/rail_flat_3m.glb", + "category": "obstacle", + "desc": "round flat rail, 3m", + "size": [ + 3.0, + 0.37, + 0.12 + ], + "tris": 128, + "grinds": [ + { + "a": [ + -1.5, + 0 + ], + "b": [ + 1.5, + 0 + ], + "ya": 0.35, + "yb": 0.35, + "kind": "rail" + } + ], + "collider": { + "type": "none" + } + }, + { + "id": "rail_flat_6m", + "src": "props/rail_flat_6m.glb", + "category": "obstacle", + "desc": "round flat rail, 6m", + "size": [ + 6.0, + 0.47, + 0.12 + ], + "tris": 172, + "grinds": [ + { + "a": [ + -3.0, + 0 + ], + "b": [ + 3.0, + 0 + ], + "ya": 0.45, + "yb": 0.45, + "kind": "rail" + } + ], + "collider": { + "type": "none" + } + }, + { + "id": "rail_kink", + "src": "props/rail_kink.glb", + "category": "obstacle", + "desc": "kinked down rail: flat then slope", + "size": [ + 3.206, + 0.57, + 0.12 + ], + "tris": 192, + "grinds": [ + { + "a": [ + -1.6, + 0 + ], + "b": [ + 0.2, + 0 + ], + "ya": 0.55, + "yb": 0.55, + "kind": "rail" + }, + { + "a": [ + 0.2, + 0 + ], + "b": [ + 1.6, + 0 + ], + "ya": 0.55, + "yb": 0.1, + "kind": "rail" + } + ], + "collider": { + "type": "none" + } + }, + { + "id": "rail_rainbow", + "src": "props/rail_rainbow.glb", + "category": "obstacle", + "desc": "rainbow rail arc", + "size": [ + 3.4, + 0.82, + 0.14 + ], + "tris": 244, + "grinds": [ + { + "a": [ + -1.6, + 0 + ], + "b": [ + 0, + 0 + ], + "ya": 0.06, + "yb": 0.8, + "kind": "rail" + }, + { + "a": [ + 0, + 0 + ], + "b": [ + 1.6, + 0 + ], + "ya": 0.8, + "yb": 0.06, + "kind": "rail" + } + ], + "collider": { + "type": "none" + } + }, + { + "id": "pipe_full", + "src": "props/pipe_full.glb", + "category": "obstacle", + "desc": "concrete full pipe, graffiti bombed outside", + "size": [ + 3.0, + 3.65, + 3.8 + ], + "tris": 192, + "collider": { + "type": "box", + "hw": 1.5, + "hd": 1.9, + "h": 3.8 + } + }, + { + "id": "bench_park", + "src": "props/bench_park.glb", + "category": "furniture", + "desc": "timber slat park bench, steel frame", + "size": [ + 1.8, + 0.866, + 0.638 + ], + "tris": 276, + "grinds": [ + { + "a": [ + -0.9, + 0.25 + ], + "b": [ + 0.9, + 0.25 + ], + "ya": 0.45, + "yb": 0.45, + "kind": "ledge" + }, + { + "a": [ + -0.9, + -0.36 + ], + "b": [ + 0.9, + -0.36 + ], + "ya": 0.86, + "yb": 0.86, + "kind": "rail" + } + ], + "collider": { + "type": "box", + "hw": 0.9, + "hd": 0.35, + "h": 0.45 + } + }, + { + "id": "bench_concrete", + "src": "props/bench_concrete.glb", + "category": "furniture", + "desc": "concrete bench block, tagged", + "size": [ + 1.8, + 0.45, + 0.452 + ], + "tris": 14, + "grinds": [ + { + "a": [ + -0.9, + 0.225 + ], + "b": [ + 0.9, + 0.225 + ], + "ya": 0.45, + "yb": 0.45, + "kind": "ledge" + }, + { + "a": [ + -0.9, + -0.225 + ], + "b": [ + 0.9, + -0.225 + ], + "ya": 0.45, + "yb": 0.45, + "kind": "ledge" + } + ], + "collider": { + "type": "box", + "hw": 0.9, + "hd": 0.225, + "h": 0.45 + }, + "element": { + "kind": "ledge", + "hw": 0.9, + "hd": 0.225, + "h": 0.45 + } + }, + { + "id": "picnic_table", + "src": "props/picnic_table.glb", + "category": "furniture", + "desc": "timber A-frame picnic table", + "size": [ + 1.8, + 0.78, + 2.325 + ], + "tris": 240, + "grinds": [ + { + "a": [ + -0.9, + 0.4 + ], + "b": [ + 0.9, + 0.4 + ], + "ya": 0.78, + "yb": 0.78, + "kind": "ledge" + }, + { + "a": [ + -0.9, + -0.4 + ], + "b": [ + 0.9, + -0.4 + ], + "ya": 0.78, + "yb": 0.78, + "kind": "ledge" + } + ], + "collider": { + "type": "box", + "hw": 0.9, + "hd": 0.85, + "h": 0.78 + } + }, + { + "id": "bin_council", + "src": "props/bin_council.glb", + "category": "furniture", + "desc": "council park bin, dark green", + "size": [ + 0.61, + 1.02, + 0.595 + ], + "tris": 252, + "collider": { + "type": "cylinder", + "r": 0.32, + "h": 1.02 + } + }, + { + "id": "bollard_steel", + "src": "props/bollard_steel.glb", + "category": "furniture", + "desc": "galvanised bollard", + "size": [ + 0.16, + 0.93, + 0.16 + ], + "tris": 96, + "collider": { + "type": "cylinder", + "r": 0.1, + "h": 0.93 + } + }, + { + "id": "railing_ped_2m", + "src": "props/railing_ped_2m.glb", + "category": "furniture", + "desc": "pedestrian railing, 2m module", + "size": [ + 2.04, + 1.024, + 0.14 + ], + "tris": 152, + "grinds": [ + { + "a": [ + -1.0, + 0 + ], + "b": [ + 1.0, + 0 + ], + "ya": 1.0, + "yb": 1.0, + "kind": "rail" + } + ], + "collider": { + "type": "none" + } + }, + { + "id": "planter_box", + "src": "props/planter_box.glb", + "category": "furniture", + "desc": "brick planter, concrete cap, gum sapling", + "size": [ + 1.28, + 1.95, + 0.989 + ], + "tris": 140, + "grinds": [ + { + "a": [ + -0.64, + 0.29 + ], + "b": [ + 0.64, + 0.29 + ], + "ya": 0.5, + "yb": 0.5, + "kind": "ledge" + }, + { + "a": [ + -0.64, + -0.29 + ], + "b": [ + 0.64, + -0.29 + ], + "ya": 0.5, + "yb": 0.5, + "kind": "ledge" + } + ], + "collider": { + "type": "box", + "hw": 0.64, + "hd": 0.29, + "h": 0.5 + } + }, + { + "id": "kerb_3m", + "src": "props/kerb_3m.glb", + "category": "dressing", + "desc": "kerb + gutter strip \u2014 slappy central", + "size": [ + 3.0, + 0.15, + 1.65 + ], + "tris": 36, + "grinds": [ + { + "a": [ + -1.5, + 0 + ], + "b": [ + 1.5, + 0 + ], + "ya": 0.15, + "yb": 0.15, + "kind": "ledge" + } + ], + "collider": { + "type": "none" + } + }, + { + "id": "wall_graffiti", + "src": "props/wall_graffiti.glb", + "category": "dressing", + "desc": "freestanding graffiti wall \u2014 wallride bait", + "size": [ + 3.6, + 1.8, + 0.256 + ], + "tris": 16, + "grinds": [ + { + "a": [ + -1.8, + 0 + ], + "b": [ + 1.8, + 0 + ], + "ya": 1.8, + "yb": 1.8, + "kind": "ledge" + } + ], + "collider": { + "type": "box", + "hw": 1.8, + "hd": 0.125, + "h": 1.8 + } + }, + { + "id": "shade_sail", + "src": "props/shade_sail.glb", + "category": "dressing", + "desc": "council shade sail on galv posts", + "size": [ + 4.71, + 3.403, + 3.91 + ], + "tris": 168, + "collider": { + "type": "cylinder", + "r": 0.1, + "h": 3.2 + } + }, + { + "id": "tree_fig", + "src": "props/tree_fig.glb", + "category": "vegetation", + "desc": "Moreton Bay fig \u2014 buttress roots, huge canopy", + "size": [ + 9.36, + 5.9, + 7.57 + ], + "tris": 686, + "collider": { + "type": "cylinder", + "r": 0.85, + "h": 2.8 + } + }, + { + "id": "tree_gum", + "src": "props/tree_gum.glb", + "category": "vegetation", + "desc": "eucalypt \u2014 pale mottled trunk, sparse crown", + "size": [ + 5.339, + 7.106, + 3.848 + ], + "tris": 498, + "collider": { + "type": "cylinder", + "r": 0.32, + "h": 5.6 + } + }, + { + "id": "mound_dirt", + "src": "props/mound_dirt.glb", + "category": "dressing", + "desc": "dirt jump mound \u2014 ridable", + "size": [ + 4.4, + 0.55, + 4.4 + ], + "tris": 200, + "element": { + "kind": "mogul", + "h": 0.55, + "s": 1.4 + } + } + ] +} \ No newline at end of file diff --git a/props/bank_12.glb b/props/bank_12.glb new file mode 100644 index 0000000..84a748f Binary files /dev/null and b/props/bank_12.glb differ diff --git a/props/bench_concrete.glb b/props/bench_concrete.glb new file mode 100644 index 0000000..6bfdea6 Binary files /dev/null and b/props/bench_concrete.glb differ diff --git a/props/bench_park.glb b/props/bench_park.glb new file mode 100644 index 0000000..0981c20 Binary files /dev/null and b/props/bench_park.glb differ diff --git a/props/bin_council.glb b/props/bin_council.glb new file mode 100644 index 0000000..d067335 Binary files /dev/null and b/props/bin_council.glb differ diff --git a/props/bollard_steel.glb b/props/bollard_steel.glb new file mode 100644 index 0000000..fe04afa Binary files /dev/null and b/props/bollard_steel.glb differ diff --git a/props/funbox_ply.glb b/props/funbox_ply.glb new file mode 100644 index 0000000..807a7e9 Binary files /dev/null and b/props/funbox_ply.glb differ diff --git a/props/grindbox_ply.glb b/props/grindbox_ply.glb new file mode 100644 index 0000000..78e323a Binary files /dev/null and b/props/grindbox_ply.glb differ diff --git a/props/kerb_3m.glb b/props/kerb_3m.glb new file mode 100644 index 0000000..3368aa3 Binary files /dev/null and b/props/kerb_3m.glb differ diff --git a/props/kicker.glb b/props/kicker.glb new file mode 100644 index 0000000..73c2585 Binary files /dev/null and b/props/kicker.glb differ diff --git a/props/ledge_concrete.glb b/props/ledge_concrete.glb new file mode 100644 index 0000000..711b80b Binary files /dev/null and b/props/ledge_concrete.glb differ diff --git a/props/manual_pad.glb b/props/manual_pad.glb new file mode 100644 index 0000000..7da23b8 Binary files /dev/null and b/props/manual_pad.glb differ diff --git a/props/mound_dirt.glb b/props/mound_dirt.glb new file mode 100644 index 0000000..d5b813f Binary files /dev/null and b/props/mound_dirt.glb differ diff --git a/props/picnic_table.glb b/props/picnic_table.glb new file mode 100644 index 0000000..6483d02 Binary files /dev/null and b/props/picnic_table.glb differ diff --git a/props/pipe_full.glb b/props/pipe_full.glb new file mode 100644 index 0000000..f9465f7 Binary files /dev/null and b/props/pipe_full.glb differ diff --git a/props/planter_box.glb b/props/planter_box.glb new file mode 100644 index 0000000..b88f209 Binary files /dev/null and b/props/planter_box.glb differ diff --git a/props/qp_18.glb b/props/qp_18.glb new file mode 100644 index 0000000..02b8b84 Binary files /dev/null and b/props/qp_18.glb differ diff --git a/props/qp_24.glb b/props/qp_24.glb new file mode 100644 index 0000000..f088ad7 Binary files /dev/null and b/props/qp_24.glb differ diff --git a/props/rail_flat_3m.glb b/props/rail_flat_3m.glb new file mode 100644 index 0000000..fba9cee Binary files /dev/null and b/props/rail_flat_3m.glb differ diff --git a/props/rail_flat_6m.glb b/props/rail_flat_6m.glb new file mode 100644 index 0000000..74de7e9 Binary files /dev/null and b/props/rail_flat_6m.glb differ diff --git a/props/rail_kink.glb b/props/rail_kink.glb new file mode 100644 index 0000000..6786e27 Binary files /dev/null and b/props/rail_kink.glb differ diff --git a/props/rail_rainbow.glb b/props/rail_rainbow.glb new file mode 100644 index 0000000..a7d03cd Binary files /dev/null and b/props/rail_rainbow.glb differ diff --git a/props/railing_ped_2m.glb b/props/railing_ped_2m.glb new file mode 100644 index 0000000..6df6023 Binary files /dev/null and b/props/railing_ped_2m.glb differ diff --git a/props/shade_sail.glb b/props/shade_sail.glb new file mode 100644 index 0000000..22c55cb Binary files /dev/null and b/props/shade_sail.glb differ diff --git a/props/spine_ply.glb b/props/spine_ply.glb new file mode 100644 index 0000000..7ed040b Binary files /dev/null and b/props/spine_ply.glb differ diff --git a/props/tree_fig.glb b/props/tree_fig.glb new file mode 100644 index 0000000..0d1e408 Binary files /dev/null and b/props/tree_fig.glb differ diff --git a/props/tree_gum.glb b/props/tree_gum.glb new file mode 100644 index 0000000..9595e3f Binary files /dev/null and b/props/tree_gum.glb differ diff --git a/props/wall_graffiti.glb b/props/wall_graffiti.glb new file mode 100644 index 0000000..52987c5 Binary files /dev/null and b/props/wall_graffiti.glb differ diff --git a/serve.py b/serve.py new file mode 100644 index 0000000..fb33699 --- /dev/null +++ b/serve.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""BOOKQUOY dev server — http.server with caching disabled so module edits show up.""" +import http.server, functools, sys + +class NoCache(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header('Cache-Control', 'no-store, must-revalidate') + super().end_headers() + +port = int(sys.argv[1]) if len(sys.argv) > 1 else 8141 +http.server.ThreadingHTTPServer(('', port), NoCache).serve_forever() diff --git a/textures/bark_fig.jpg b/textures/bark_fig.jpg new file mode 100644 index 0000000..9bc2110 Binary files /dev/null and b/textures/bark_fig.jpg differ diff --git a/textures/bark_gum.jpg b/textures/bark_gum.jpg new file mode 100644 index 0000000..ee7860b Binary files /dev/null and b/textures/bark_gum.jpg differ diff --git a/textures/brick_qld.jpg b/textures/brick_qld.jpg new file mode 100644 index 0000000..7a6eeed Binary files /dev/null and b/textures/brick_qld.jpg differ diff --git a/textures/concrete_green.jpg b/textures/concrete_green.jpg new file mode 100644 index 0000000..5ec690b Binary files /dev/null and b/textures/concrete_green.jpg differ diff --git a/textures/concrete_rough.jpg b/textures/concrete_rough.jpg new file mode 100644 index 0000000..966dc8e Binary files /dev/null and b/textures/concrete_rough.jpg differ diff --git a/textures/concrete_smooth.jpg b/textures/concrete_smooth.jpg new file mode 100644 index 0000000..743345c Binary files /dev/null and b/textures/concrete_smooth.jpg differ diff --git a/textures/dirt_mulch.jpg b/textures/dirt_mulch.jpg new file mode 100644 index 0000000..d40a325 Binary files /dev/null and b/textures/dirt_mulch.jpg differ diff --git a/textures/graffiti_a.jpg b/textures/graffiti_a.jpg new file mode 100644 index 0000000..6d16044 Binary files /dev/null and b/textures/graffiti_a.jpg differ diff --git a/textures/graffiti_b.jpg b/textures/graffiti_b.jpg new file mode 100644 index 0000000..420283b Binary files /dev/null and b/textures/graffiti_b.jpg differ diff --git a/textures/grass_dry.jpg b/textures/grass_dry.jpg new file mode 100644 index 0000000..7123193 Binary files /dev/null and b/textures/grass_dry.jpg differ diff --git a/textures/leaves_fig.jpg b/textures/leaves_fig.jpg new file mode 100644 index 0000000..e0100ed Binary files /dev/null and b/textures/leaves_fig.jpg differ diff --git a/textures/leaves_gum.jpg b/textures/leaves_gum.jpg new file mode 100644 index 0000000..ce4cb3f Binary files /dev/null and b/textures/leaves_gum.jpg differ diff --git a/textures/plywood_ramp.jpg b/textures/plywood_ramp.jpg new file mode 100644 index 0000000..fda0496 Binary files /dev/null and b/textures/plywood_ramp.jpg differ diff --git a/textures/sky_day.jpg b/textures/sky_day.jpg new file mode 100644 index 0000000..2b0bd98 Binary files /dev/null and b/textures/sky_day.jpg differ diff --git a/textures/steel_galv.jpg b/textures/steel_galv.jpg new file mode 100644 index 0000000..31d50e9 Binary files /dev/null and b/textures/steel_galv.jpg differ diff --git a/textures/steel_scratch.jpg b/textures/steel_scratch.jpg new file mode 100644 index 0000000..d2cc65b Binary files /dev/null and b/textures/steel_scratch.jpg differ diff --git a/textures/tarmac.jpg b/textures/tarmac.jpg new file mode 100644 index 0000000..e563a74 Binary files /dev/null and b/textures/tarmac.jpg differ diff --git a/textures/wood_slats.jpg b/textures/wood_slats.jpg new file mode 100644 index 0000000..06b3a38 Binary files /dev/null and b/textures/wood_slats.jpg differ diff --git a/tools/__pycache__/propkit.cpython-314.pyc b/tools/__pycache__/propkit.cpython-314.pyc new file mode 100644 index 0000000..cbbb31b Binary files /dev/null and b/tools/__pycache__/propkit.cpython-314.pyc differ diff --git a/tools/gen_textures.py b/tools/gen_textures.py new file mode 100644 index 0000000..6cc161a --- /dev/null +++ b/tools/gen_textures.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""gen_textures.py — batch park-surface textures from MODELBEAST flux_local. + +Guest token allows 4 active jobs: submit in a rolling window, poll, download +each job's own output (parent_job match — newest-asset raced once, 2026-07-16). +Token from ~/Documents/backnforth/.env (never printed). Re-runs skip textures +that already exist on disk, so the script is idempotent. + +Usage: python3 tools/gen_textures.py [--force name ...] +""" +import json, os, sys, time, urllib.request + +HOST = os.environ.get('MB_HOST', 'http://100.89.131.57:8777') +OUT = os.path.join(os.path.dirname(__file__), '..', 'textures') + +# Every prompt asks for flat, even, shadow-free top-down macro photography — +# these get tiled with MIRRORED_REPEAT in the GLB sampler, which hides seams, +# but baked-in lighting gradients would still show. 1024px, klein-4b, 4 steps. +STYLE = ('seamless tileable texture, photographed straight down, perfectly flat, ' + 'even diffuse lighting, no shadows, no vignette, full-frame surface detail, ' + 'photorealistic macro material photograph') + +TEXTURES = { + 'concrete_smooth': 'smooth grey skatepark concrete, steel-trowel finish, faint swirl marks, fine aggregate flecks, light wear', + 'concrete_green': 'green tinted polished skatepark concrete, smooth burnished surface, subtle colour variation, small speckle aggregate', + 'concrete_rough': 'broom finished pale concrete footpath surface, fine parallel brush lines, weathered', + 'plywood_ramp': 'worn skate ramp plywood, birch surface scuffed with black urethane wheel marks and scratches, faded grain', + 'steel_galv': 'galvanized steel sheet metal, crystalline spangle pattern, dull grey zinc coating', + 'steel_scratch': 'steel surface covered in fine bright grind scratches and wax residue, well used skateboard rail metal', + 'wood_slats': 'weathered grey-brown hardwood park bench timber, deep grain, small cracks, sun faded', + 'bark_fig': 'Moreton Bay fig tree bark, grey-brown, rough woven fibrous ridges', + 'leaves_fig': 'dense dark green glossy fig tree canopy foliage seen from below, layered leaves', + 'bark_gum': 'eucalyptus gum tree bark, smooth mottled cream grey and salmon patches, peeling strips', + 'leaves_gum': 'dense eucalyptus foliage filling the entire frame, layered slender sage-green curved gum leaves, no sky visible', + 'grass_dry': 'dry australian park grass, green-brown patchy couch lawn, some bare dirt showing', + 'tarmac': 'grey asphalt road surface, fine bitumen aggregate, small stones, light wear', + 'dirt_mulch': 'garden bed surface of brown dirt and eucalyptus bark mulch chips', + 'brick_qld': 'reddish brown queensland house bricks with pale mortar joints, running bond', + 'graffiti_a': 'colorful graffiti burner piece with bold wildstyle letters on a concrete skatepark wall, aerosol art, blues oranges purples', + 'graffiti_b': 'concrete wall covered in layered spray paint tags and simple throw-up graffiti letters, black silver white paint', + 'sky_day': 'clear bright queensland blue sky with a few small white cumulus clouds, wide even gradient', +} + +def token(): + for line in open(os.path.expanduser('~/Documents/backnforth/.env')): + if line.startswith('MB_TOKEN='): + return line.split('=', 1)[1].strip() + sys.exit('no MB_TOKEN') + +def req(path, data=None, headers=None, raw=False): + h = {'Authorization': f'Bearer {token()}'} + h.update(headers or {}) + r = urllib.request.Request(HOST + path, data=data, headers=h) + body = urllib.request.urlopen(r, timeout=180).read() + if raw: + return body + s = body.decode('utf-8', 'replace') + return json.loads(''.join(c if c >= ' ' or c in '\t' else ' ' for c in s)) + +def submit(name, desc, seed): + p = {'operator': 'flux_local', + 'params': {'prompt': f'{desc}, {STYLE}', 'model': 'flux2-klein-4b', + 'steps': 4, 'width': 1024, 'height': 1024, 'seed': seed}} + j = req('/api/jobs', data=json.dumps(p).encode(), + headers={'Content-Type': 'application/json'}) + print(f'[tx] {name}: job {j["id"]}', flush=True) + return j['id'] + +def fetch(jid, name): + items = req('/api/assets?limit=100') + items = items if isinstance(items, list) else items.get('items', []) + mine = [x for x in items if x.get('parent_job') == jid] + imgs = [x for x in mine if str(x.get('filename', x.get('name', ''))).lower() + .endswith(('.png', '.jpg', '.jpeg', '.webp'))] + if not imgs: + print(f'[tx] {name}: NO OUTPUT ASSET for job {jid}', flush=True) + return False + ext = os.path.splitext(str(imgs[0].get('filename', imgs[0].get('name', 'x.png'))))[1] or '.png' + data = req(f'/api/assets/{imgs[0]["id"]}/file', raw=True) + out = os.path.join(OUT, name + ext) + open(out, 'wb').write(data) + print(f'[tx] {name}: saved {len(data)//1024}KB -> {out}', flush=True) + return True + +def main(): + force = set(sys.argv[sys.argv.index('--force') + 1:]) if '--force' in sys.argv else set() + todo = [] + for i, (name, desc) in enumerate(TEXTURES.items()): + have = [f for f in os.listdir(OUT) if f.startswith(name + '.')] + if have and name not in force: + print(f'[tx] {name}: exists, skip', flush=True) + continue + todo.append((name, desc, 1000 + i)) + active = {} # jid -> name + fails = [] + while todo or active: + while todo and len(active) < 4: + name, desc, seed = todo.pop(0) + try: + active[submit(name, desc, seed)] = name + except Exception as e: + print(f'[tx] {name}: submit failed {e}', flush=True) + fails.append(name) + time.sleep(7) + for jid in list(active): + try: + st = req(f'/api/jobs/{jid}').get('status') + except Exception: + continue + if st in ('done', 'error', 'cancelled'): + name = active.pop(jid) + if st == 'done': + if not fetch(jid, name): + fails.append(name) + else: + print(f'[tx] {name}: job {st}', flush=True) + fails.append(name) + print(f'[tx] COMPLETE. fails: {fails or "none"}', flush=True) + +if __name__ == '__main__': + main() diff --git a/tools/prop_author.py b/tools/prop_author.py new file mode 100644 index 0000000..b8ab59c --- /dev/null +++ b/tools/prop_author.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python3 +"""prop_author.py — the park object catalog. Every prop is parametric, textured +with the MODELBEAST surface set, and ships manifest metadata the skatemakerpro +editor can consume directly: + + src props/.glb (editor prop `src` field) + grinds local-space rail segments {a,b,ya,yb,kind} — translate/rotate with + the prop and append to the park's rails list + element a matching terrain-kind hint for ramps (quarter/bank/funbox/spine) + so placed ramps get real collision from the editor's heightfield + collider simple blocker hint for furniture/trees + +Origin: ground centre. Metres, Y up. Ramps face +z (rider approaches from +z). +Run: python3 tools/prop_author.py -> props/*.glb + manifest.json +""" +import json, math, os, sys +sys.path.insert(0, os.path.dirname(__file__)) +from propkit import (Mat, Prop, box, sheet, lathe, tube, blob, merge, translate, + rotate_y, scale, compute_normals, _p) + +ROOT = os.path.join(os.path.dirname(__file__), '..') +PROPS = os.path.join(ROOT, 'props') +TEX = os.path.join(ROOT, 'textures') + +# ---------------------------------------------------------------- materials +CONC = Mat('concrete', tex='concrete_smooth', uv=1.6) +CONC_R = Mat('concrete_rough', tex='concrete_rough', uv=1.4) +PLY = Mat('plywood', tex='plywood_ramp', uv=1.2) +GALV = Mat('galv', tex='steel_galv', uv=0.8, rough=0.5, metal=0.85) +STEEL = Mat('steel_worn', tex='steel_scratch', uv=0.45, rough=0.35, metal=0.9) +SLATS = Mat('slats', tex='wood_slats', uv=0.9) +BARKF = Mat('bark_fig', tex='bark_fig', uv=1.1) +BARKG = Mat('bark_gum', tex='bark_gum', uv=1.2) +LEAFF = Mat('leaves_fig', tex='leaves_fig', uv=1.6, double=True) +LEAFG = Mat('leaves_gum', tex='leaves_gum', uv=1.4, double=True, + tint=(0.78, 0.9, 0.7, 1)) # push the pale crown toward sage +DIRT = Mat('dirt', tex='dirt_mulch', uv=1.0) +BRICK = Mat('brick', tex='brick_qld', uv=1.2) +TARMAC = Mat('tarmac', tex='tarmac', uv=1.5) +GRAF_A = Mat('graffiti_a', tex='graffiti_a', uv=1.0) # uv_fit parts only +GRAF_B = Mat('graffiti_b', tex='graffiti_b', uv=1.0) +BIN_G = Mat('bin_green', tint=(0.13, 0.28, 0.16, 1), rough=0.7) +SAIL = Mat('sail', tint=(0.75, 0.42, 0.28, 1), rough=0.85, double=True) + +# ---------------------------------------------------------------- helpers +def uv_fit(part, u0=0.0, v0=0.0, u1=1.0, v1=1.0): + """Remap a part's UVs to [u0..u1]x[v0..v1] — for decal faces (graffiti).""" + us = [u for u, v in part['UV']]; vs = [v for u, v in part['UV']] + du = (max(us) - min(us)) or 1; dv = (max(vs) - min(vs)) or 1 + part['UV'] = [(u0 + (u - min(us)) / du * (u1 - u0), + v0 + (v - min(vs)) / dv * (v1 - v0)) for u, v in part['UV']] + return part + +def quad(a, b, c, d): + """Single quad a->b->c->d ccw, UV 0..1.""" + P = [a, b, c, d] + I = [0, 1, 2, 0, 2, 3] + N = compute_normals(P, I) + return _p(P, N, [(0, 0), (1, 0), (1, 1), (0, 1)], I) + +def fan(pts, uvf, out=None): + """Triangle fan from pts[0] over a planar boundary. uvf(p) -> (u, v). + out: desired outward normal — winding auto-reverses to face it (no more + hand-reasoning CCW per plate; qp_24 shipped invisible the first time).""" + P = list(pts) + I = [] + for i in range(1, len(pts) - 1): + I += [0, i, i + 1] + N = compute_normals(P, I) + if out is not None: + d = sum(n[k] * out[k] for n in N for k in range(3)) + if d < 0: + I = [I[i + o] for i in range(0, len(I), 3) for o in (0, 2, 1)] + N = [(-x, -y, -z) for x, y, z in N] + return _p(P, N, [uvf(p) for p in P], I) + +def angle_cap(x0, x1, y, z, t=0.05, th=0.008): + """Steel L-angle capping a ledge top edge running along x at (y, z).""" + horiz = box((x0 + x1) / 2, y - th / 2, z, x1 - x0, th, t) + vert = box((x0 + x1) / 2, y - t / 2, z + (t / 2 if z >= 0 else -t / 2), + x1 - x0, t, th) + return merge(horiz, vert) + +def arc_pts(r, th0, th1, n): + return [(r - r * math.cos(t), -r * math.sin(t)) + for t in [th0 + (th1 - th0) * i / n for i in range(n + 1)]] + +# =================================================================== ramps +def quarter(pid, w, h, r, desc): + th = math.acos(1 - h / r) + depth = r * math.sin(th) + n = 14 + ys_zs = [(r - r * math.cos(t), -r * math.sin(t)) + for t in [th * i / n for i in range(n + 1)]] + p = Prop(pid, 'obstacle', desc) + # riding surface (two columns across width) + rows = [[(-w / 2, y, z), (w / 2, y, z)] for y, z in ys_zs] + p.add(PLY, sheet(rows)) + # deck platform + coping + deck_d = 0.6 + p.add(PLY, box(0, h - 0.04, -depth - deck_d / 2, w, 0.08, deck_d)) + p.add(STEEL, tube([(-w / 2, h + 0.02, -depth), (w / 2, h + 0.02, -depth)], + 0.032, seg=10)) + # side plates: arc boundary + deck + back-down (fan from back-bottom corner) + prof = [( -depth - deck_d, 0)] + [(z, y) for y, z in ys_zs] + \ + [(-depth, h), (-depth - deck_d, h)] + for sx in (-w / 2, w / 2): + pts = [(sx, y, z) for z, y in prof] + p.add(PLY, fan(pts, lambda pp: (pp[2], pp[1]), out=(sx, 0, 0))) + # back wall + p.add(GALV, box(0, h / 2, -depth - deck_d + 0.02, w, h, 0.04)) + p.grind(-w / 2, -depth, w / 2, -depth, h + 0.02, h + 0.02, 'rail') + p.element = {'kind': 'quarter', 'len': w, 'r0': r, 'r1': r, 'h': h} + return p + +def bank(pid, w, h, run, desc): + p = Prop(pid, 'obstacle', desc) + rows = [[(-w / 2, 0, run), (w / 2, 0, run)], [(-w / 2, h, 0), (w / 2, h, 0)]] + p.add(PLY, sheet(rows)) + p.add(PLY, box(0, h - 0.02, -0.2, w, 0.04, 0.4)) # top plate + for sx in (-w / 2, w / 2): + p.add(PLY, fan([(sx, 0, -0.4), (sx, 0, run), (sx, h, 0), (sx, h, -0.4)], + lambda pp: (pp[2], pp[1]), out=(sx, 0, 0))) + p.add(GALV, box(0, h / 2, -0.38, w, h, 0.04)) + p.add(STEEL, angle_cap(-w / 2, w / 2, h, 0)) + p.grind(-w / 2, 0, w / 2, 0, h, h, 'ledge') + p.element = {'kind': 'bank', 'len': w, 'h': h, 'run': run} + return p + +def kicker(): + p = Prop('kicker', 'obstacle', 'curved plywood launch kicker') + w, h, run, n = 1.2, 0.45, 1.0, 8 + rows = [] + for i in range(n + 1): + t = i / n + y = h * t * t # quadratic curve + z = run * (1 - t) + rows.append([(-w / 2, y, z), (w / 2, y, z)]) + p.add(PLY, sheet(rows)) + for sx in (-w / 2, w / 2): + pts = [(sx, 0, run)] + [(sx, h * (i / n) ** 2, run * (1 - i / n)) for i in range(1, n + 1)] + [(sx, 0, 0)] + p.add(PLY, fan(pts, lambda pp: (pp[2], pp[1]), out=(sx, 0, 0))) + p.add(GALV, box(0, h / 2, -0.02, w, h, 0.04)) + p.add(STEEL, box(0, h - 0.005, 0.02, w, 0.01, 0.06)) # lip strip + p.element = {'kind': 'bank', 'len': w, 'h': h, 'run': run} + return p + +def funbox_ply(): + p = Prop('funbox_ply', 'obstacle', 'plywood funbox, banks both sides, steel edge caps') + w, d, h, skirt = 2.4, 1.2, 0.6, 1.0 + p.add(PLY, box(0, h - 0.04, 0, w, 0.08, d)) # top + for sz in (1, -1): # bank skirts + rows = [[(-w / 2, 0, sz * (d / 2 + skirt)), (w / 2, 0, sz * (d / 2 + skirt))], + [(-w / 2, h, sz * d / 2), (w / 2, h, sz * d / 2)]] + p.add(PLY, sheet(rows, flip=(sz < 0))) + p.add(STEEL, angle_cap(-w / 2, w / 2, h, sz * d / 2)) + p.grind(-w / 2, sz * d / 2, w / 2, sz * d / 2, h, h, 'ledge') + for sx in (-w / 2, w / 2): # side triangles + p.add(GALV, fan([(sx, 0, -(d / 2 + skirt)), (sx, 0, d / 2 + skirt), + (sx, h, d / 2), (sx, h, -d / 2)], + lambda pp: (pp[2], pp[1]), out=(sx, 0, 0))) + p.element = {'kind': 'funbox', 'hw': w / 2, 'hd': d / 2, 'h': h, 'skirt': skirt} + return p + +def spine_ply(): + p = Prop('spine_ply', 'obstacle', 'plywood spine, double coping') + w, h, r = 2.4, 0.9, 1.35 + th = math.acos(1 - h / r) + depth = r * math.sin(th) + n = 12 + for sz in (1, -1): + arc = [(r - r * math.cos(t), r * math.sin(th) - r * math.sin(t)) + for t in [th * i / n for i in range(n + 1)]] + rows = [[(-w / 2, y, sz * (z + 0.1)), (w / 2, y, sz * (z + 0.1))] for y, z in arc] + p.add(PLY, sheet(rows, flip=(sz < 0))) + p.add(STEEL, tube([(-w / 2, h + 0.02, sz * 0.1), (w / 2, h + 0.02, sz * 0.1)], + 0.032, seg=10)) + p.grind(-w / 2, sz * 0.1, w / 2, sz * 0.1, h + 0.02, h + 0.02, 'rail') + p.add(PLY, box(0, h - 0.01, 0, w, 0.02, 0.2)) # spine cap between copings + for sx in (-w / 2, w / 2): # side plates + arc = [(r - r * math.cos(t), r * math.sin(th) - r * math.sin(t)) + for t in [th * i / n for i in range(n + 1)]] + pts = [(sx, y, z + 0.1) for y, z in arc] + \ + [(sx, y, -(z + 0.1)) for y, z in reversed(arc)] + p.add(PLY, fan(pts, lambda pp: (pp[2], pp[1]), out=(sx, 0, 0))) + p.element = {'kind': 'spine', 'len': w, 'r': r, 'h': h} + return p + +def manual_pad(): + p = Prop('manual_pad', 'obstacle', 'low concrete manual pad, steel edges') + w, d, h = 2.0, 1.6, 0.28 + p.add(CONC, box(0, h / 2, 0, w, h, d)) + for sz in (1, -1): + p.add(STEEL, angle_cap(-w / 2, w / 2, h, sz * d / 2)) + p.grind(-w / 2, sz * d / 2, w / 2, sz * d / 2, h, h, 'ledge') + p.element = {'kind': 'funbox', 'hw': w / 2, 'hd': d / 2, 'h': h, 'skirt': 0.3} + return p + +def ledge_concrete(): + p = Prop('ledge_concrete', 'obstacle', 'concrete grind ledge, tagged side') + w, d, h = 2.4, 0.45, 0.42 + p.add(CONC, box(0, h / 2, 0, w, h, d)) + side = uv_fit(quad((-w / 2, 0, d / 2 + 0.002), (w / 2, 0, d / 2 + 0.002), + (w / 2, h, d / 2 + 0.002), (-w / 2, h, d / 2 + 0.002))) + p.add(GRAF_B, side) + for sz in (1, -1): + p.add(STEEL, angle_cap(-w / 2, w / 2, h, sz * d / 2)) + p.grind(-w / 2, sz * d / 2, w / 2, sz * d / 2, h, h, 'ledge') + p.collider = {'type': 'box', 'hw': w / 2, 'hd': d / 2, 'h': h} + p.element = {'kind': 'ledge', 'hw': w / 2, 'hd': d / 2, 'h': h} + return p + +def grindbox_ply(): + p = Prop('grindbox_ply', 'obstacle', 'plywood grindbox: round bar one edge, angle the other') + w, d, h = 1.8, 0.5, 0.4 + p.add(PLY, box(0, h / 2, 0, w, h, d)) + p.add(STEEL, tube([(-w / 2, h + 0.024, d / 2), (w / 2, h + 0.024, d / 2)], 0.024, seg=8)) + p.grind(-w / 2, d / 2, w / 2, d / 2, h + 0.024, h + 0.024, 'rail') + p.add(STEEL, angle_cap(-w / 2, w / 2, h, -d / 2)) + p.grind(-w / 2, -d / 2, w / 2, -d / 2, h, h, 'ledge') + p.element = {'kind': 'ledge', 'hw': w / 2, 'hd': d / 2, 'h': h} + return p + +# =================================================================== rails +def flat_rail(pid, length, h, feet, desc): + p = Prop(pid, 'obstacle', desc) + r = 0.021 + p.add(STEEL, tube([(-length / 2, h, 0), (length / 2, h, 0)], r, seg=10)) + for i in range(feet): + fx = -length / 2 + 0.25 + (length - 0.5) * (i / max(1, feet - 1)) + p.add(GALV, tube([(fx, 0, 0), (fx, h - r, 0)], 0.018, seg=8)) + p.add(GALV, box(fx, 0.006, 0, 0.16, 0.012, 0.12)) + p.grind(-length / 2, 0, length / 2, 0, h, h, 'rail') + p.collider = {'type': 'none'} # thin — ride under is fine + return p + +def rail_kink(): + p = Prop('rail_kink', 'obstacle', 'kinked down rail: flat then slope') + h1, h2 = 0.55, 0.10 + pts = [(-1.6, h1, 0), (0.2, h1, 0), (1.6, h2, 0)] + p.add(STEEL, tube(pts, 0.021, seg=10)) + for fx, fy in ((-1.35, h1), (0.1, h1), (1.45, h2 + 0.06)): + p.add(GALV, tube([(fx, 0, 0), (fx, fy - 0.02, 0)], 0.018, seg=8)) + p.add(GALV, box(fx, 0.006, 0, 0.16, 0.012, 0.12)) + p.grind(-1.6, 0, 0.2, 0, h1, h1, 'rail') + p.grind(0.2, 0, 1.6, 0, h1, h2, 'rail') + p.collider = {'type': 'none'} + return p + +def rail_rainbow(): + p = Prop('rail_rainbow', 'obstacle', 'rainbow rail arc') + span, peak, n = 3.2, 0.8, 10 + pts = [] + for i in range(n + 1): + t = i / n + x = -span / 2 + span * t + y = 0.06 + (peak - 0.06) * math.sin(math.pi * t) + pts.append((x, y, 0)) + p.add(STEEL, tube(pts, 0.021, seg=10)) + for sx in (-span / 2, span / 2): + p.add(GALV, box(sx, 0.03, 0, 0.2, 0.06, 0.14)) + p.grind(-span / 2, 0, 0, 0, 0.06, peak, 'rail') + p.grind(0, 0, span / 2, 0, peak, 0.06, 'rail') + p.collider = {'type': 'none'} + return p + +def pipe_full(): + p = Prop('pipe_full', 'obstacle', 'concrete full pipe, graffiti bombed outside') + ri, ro, L, seg = 1.75, 1.9, 3.0, 24 + cy = ri # inner floor kisses ground + def ring(rr): + return [(math.cos(2 * math.pi * i / seg) * rr, + cy + math.sin(2 * math.pi * i / seg) * rr) for i in range(seg + 1)] + inner = sheet([[(-L / 2, y, x), (L / 2, y, x)] for x, y in ring(ri)], flip=True) + outer = sheet([[(-L / 2, y, x), (L / 2, y, x)] for x, y in ring(ro)]) + p.add(CONC, inner) + p.add(Mat('graffiti_wrap', tex='graffiti_a', uv=3.5), outer) # murals round the girth + for sx in (-1, 1): # end rings + P, N, UV, I = [], [], [], [] + for i in range(seg + 1): + a = 2 * math.pi * i / seg + P += [(sx * L / 2, cy + math.sin(a) * ri, math.cos(a) * ri), + (sx * L / 2, cy + math.sin(a) * ro, math.cos(a) * ro)] + N += [(sx, 0, 0)] * 2 + UV += [(math.cos(a) * ri, cy + math.sin(a) * ri), + (math.cos(a) * ro, cy + math.sin(a) * ro)] + for i in range(seg): + b = i * 2 + tri = [b, b + 2, b + 1, b + 1, b + 2, b + 3] + if sx < 0: tri = [b, b + 1, b + 2, b + 1, b + 3, b + 2] + I += tri + p.add(CONC_R, _p(P, N, UV, I)) + p.collider = {'type': 'box', 'hw': L / 2, 'hd': ro, 'h': 2 * ro} + return p + +# ================================================================ furniture +def bench_park(): + p = Prop('bench_park', 'furniture', 'timber slat park bench, steel frame') + L, seat_h, seat_d = 1.8, 0.45, 0.5 + for i in range(5): # seat slats + z = -seat_d / 2 + 0.05 + i * 0.1 + p.add(SLATS, box(0, seat_h - 0.02, z, L, 0.04, 0.085)) + for i in range(2): # back slats + y = 0.62 + i * 0.13 + p.add(SLATS, box(0, y, -seat_d / 2 - 0.05 - (y - seat_h) * 0.25, L, 0.1, 0.04)) + for sx in (-L / 2 + 0.12, L / 2 - 0.12): # steel loop legs + p.add(GALV, tube([(sx, 0, seat_d / 2 - 0.04), (sx, seat_h - 0.04, seat_d / 2 - 0.06), + (sx, seat_h - 0.04, -seat_d / 2 + 0.02), + (sx, 0.86, -seat_d / 2 - 0.11)], 0.02, seg=8)) + p.add(GALV, tube([(sx, 0.02, seat_d / 2 - 0.04), (sx, 0.02, -seat_d / 2 + 0.02)], + 0.018, seg=8)) + p.grind(-L / 2, seat_d / 2, L / 2, seat_d / 2, seat_h, seat_h, 'ledge') + p.grind(-L / 2, -seat_d / 2 - 0.11, L / 2, -seat_d / 2 - 0.11, 0.86, 0.86, 'rail') + p.collider = {'type': 'box', 'hw': L / 2, 'hd': 0.35, 'h': seat_h} + return p + +def bench_concrete(): + p = Prop('bench_concrete', 'furniture', 'concrete bench block, tagged') + L, h, d = 1.8, 0.45, 0.45 + p.add(CONC, box(0, h / 2, 0, L, h, d)) + p.add(GRAF_B, uv_fit(quad((-L / 2, 0, d / 2 + 0.002), (L / 2, 0, d / 2 + 0.002), + (L / 2, h, d / 2 + 0.002), (-L / 2, h, d / 2 + 0.002)))) + for sz in (1, -1): + p.grind(-L / 2, sz * d / 2, L / 2, sz * d / 2, h, h, 'ledge') + p.collider = {'type': 'box', 'hw': L / 2, 'hd': d / 2, 'h': h} + p.element = {'kind': 'ledge', 'hw': L / 2, 'hd': d / 2, 'h': h} + return p + +def picnic_table(): + p = Prop('picnic_table', 'furniture', 'timber A-frame picnic table') + L, top_h, seat_h = 1.8, 0.78, 0.45 + for i in range(4): # tabletop planks + p.add(SLATS, box(0, top_h - 0.025, -0.3 + i * 0.2, L, 0.05, 0.18)) + for sz in (1, -1): # seats + for i in range(2): + p.add(SLATS, box(0, seat_h - 0.025, sz * (0.62 + 0.0) + (i - 0.5) * 0.16 * sz * 0 + sz * (i * 0.16 - 0.08), L, 0.05, 0.14)) + for sx in (-L / 2 + 0.2, L / 2 - 0.2): # A-frame legs + p.add(SLATS, box(sx, top_h / 2, 0.55, 0.09, 0.05, 2.0)) + for sz in (1, -1): + leg = tube([(sx, 0, sz * 0.72), (sx, top_h - 0.05, sz * 0.28)], 0.032, seg=6) + p.add(SLATS, leg) + p.add(SLATS, box(sx, seat_h - 0.06, 0, 0.09, 0.05, 1.55)) + for sz in (1, -1): + p.grind(-L / 2, sz * 0.4, L / 2, sz * 0.4, top_h, top_h, 'ledge') + p.collider = {'type': 'box', 'hw': L / 2, 'hd': 0.85, 'h': top_h} + return p + +def bin_council(): + p = Prop('bin_council', 'furniture', 'council park bin, dark green') + prof = [(0.26, 0.0), (0.28, 0.05), (0.28, 0.75), (0.30, 0.8), + (0.30, 0.92), (0.22, 1.0), (0.10, 1.02), (0.0, 1.02)] + p.add(BIN_G, lathe(prof, seg=14)) + p.add(GALV, lathe([(0.30, 0.78), (0.305, 0.8), (0.30, 0.82)], seg=14, + cap_top=False)) + p.collider = {'type': 'cylinder', 'r': 0.32, 'h': 1.02} + return p + +def bollard_steel(): + p = Prop('bollard_steel', 'furniture', 'galvanised bollard') + p.add(GALV, lathe([(0.08, 0.0), (0.08, 0.82), (0.075, 0.88), (0.04, 0.92), (0.0, 0.93)], + seg=12)) + p.collider = {'type': 'cylinder', 'r': 0.1, 'h': 0.93} + return p + +def railing_ped(): + p = Prop('railing_ped_2m', 'furniture', 'pedestrian railing, 2m module') + L, H = 2.0, 1.0 + p.add(GALV, tube([(-L / 2, H, 0), (L / 2, H, 0)], 0.024, seg=8)) + p.add(GALV, tube([(-L / 2, 0.55, 0), (L / 2, 0.55, 0)], 0.018, seg=8)) + for sx in (-L / 2 + 0.05, L / 2 - 0.05): + p.add(GALV, tube([(sx, 0, 0), (sx, H - 0.02, 0)], 0.024, seg=8)) + p.add(GALV, box(sx, 0.006, 0, 0.14, 0.012, 0.14)) + p.grind(-L / 2, 0, L / 2, 0, H, H, 'rail') + p.collider = {'type': 'none'} + return p + +def planter_box(): + p = Prop('planter_box', 'furniture', 'brick planter, concrete cap, gum sapling') + L, D, H = 1.2, 0.5, 0.5 + p.add(BRICK, box(0, (H - 0.06) / 2, 0, L, H - 0.06, D)) + p.add(CONC, box(0, H - 0.03, 0, L + 0.08, 0.06, D + 0.08)) + p.add(DIRT, box(0, H - 0.055, 0, L - 0.12, 0.02, D - 0.12)) + p.add(BARKG, tube([(0.1, H, 0), (0.05, H + 0.9, 0.05)], 0.035, seg=6)) + p.add(LEAFG, translate(blob(0.45, 0.4, 0.45, seg=8, rings=5, amp=0.22, seed=3.0), + 0.02, H + 1.05, 0.04)) + for sz in (1, -1): + p.grind(-L / 2 - 0.04, sz * (D / 2 + 0.04), L / 2 + 0.04, sz * (D / 2 + 0.04), + H, H, 'ledge') + p.collider = {'type': 'box', 'hw': L / 2 + 0.04, 'hd': D / 2 + 0.04, 'h': H} + return p + +def kerb_3m(): + p = Prop('kerb_3m', 'dressing', 'kerb + gutter strip — slappy central') + L, kh = 3.0, 0.15 + p.add(TARMAC, box(0, 0.005, 0.45, L, 0.01, 0.9)) # road gutter + p.add(CONC_R, box(0, kh / 2, -0.075, L, kh, 0.15)) # kerb stone + p.add(CONC_R, box(0, kh - 0.005, -0.45, L, 0.01, 0.6)) # footpath strip + p.grind(-L / 2, 0, L / 2, 0, kh, kh, 'ledge') + p.collider = {'type': 'none'} + return p + +def wall_graffiti(): + p = Prop('wall_graffiti', 'dressing', 'freestanding graffiti wall — wallride bait') + W, H, T = 3.6, 1.8, 0.25 + p.add(CONC_R, box(0, H / 2, 0, W, H, T)) + p.add(GRAF_A, uv_fit(quad((-W / 2, 0, T / 2 + 0.003), (W / 2, 0, T / 2 + 0.003), + (W / 2, H, T / 2 + 0.003), (-W / 2, H, T / 2 + 0.003)))) + p.add(GRAF_B, uv_fit(quad((W / 2, 0, -T / 2 - 0.003), (-W / 2, 0, -T / 2 - 0.003), + (-W / 2, H, -T / 2 - 0.003), (W / 2, H, -T / 2 - 0.003)))) + p.grind(-W / 2, 0, W / 2, 0, H, H, 'ledge') + p.collider = {'type': 'box', 'hw': W / 2, 'hd': T / 2, 'h': H} + return p + +def shade_sail(): + p = Prop('shade_sail', 'dressing', 'council shade sail on galv posts') + posts = [(-2.2, 3.2, -1.6), (2.4, 3.4, -1.2), (0.0, 3.0, 2.2)] + for x, h, z in posts: + p.add(GALV, tube([(x, 0, z), (x * 0.92, h, z * 0.92)], 0.055, seg=8)) + n = 6 + a, b, c = [(x * 0.92, h - 0.05, z * 0.92) for x, h, z in posts] + rows = [] + for i in range(n + 1): + u = i / n + pa = tuple(a[k] + (b[k] - a[k]) * u for k in range(3)) + pb = tuple(c[k] + (b[k] - c[k]) * u for k in range(3)) + row = [] + for j in range(n + 1): + v = j / n + pt = tuple(pa[k] + (pb[k] - pa[k]) * v for k in range(3)) + sag = 0.5 * math.sin(math.pi * u) * math.sin(math.pi * v) + row.append((pt[0], pt[1] - sag, pt[2])) + rows.append(row) + p.add(SAIL, sheet(rows)) + p.collider = {'type': 'cylinder', 'r': 0.1, 'h': 3.2} + return p + +# =============================================================== vegetation +def tree_fig(): + p = Prop('tree_fig', 'vegetation', 'Moreton Bay fig — buttress roots, huge canopy') + h = 2.8 + prof = [(0.95, 0.0), (0.62, 0.35), (0.5, 0.9), (0.46, 1.8), (0.5, h)] + def buttress(iu, iv, r): + fl = max(0.0, 1.0 - iv / 2.5) ** 2 + return r * (1 + fl * 0.5 * abs(math.sin(iu * 2.2 + 0.7))) + p.add(BARKF, lathe(prof, seg=14, jitter=buttress, cap_top=True)) + for dx, dy, dz, rx, ry, rz, sd in [(0, 4.0, 0, 4.4, 1.9, 3.8, 1.0), + (2.1, 3.4, 1.2, 2.6, 1.4, 2.2, 5.0), + (-2.3, 3.6, -0.8, 2.4, 1.3, 2.4, 9.0)]: + p.add(LEAFF, translate(blob(rx, ry, rz, seg=12, rings=7, amp=0.18, seed=sd), + dx, dy, dz)) + for bx, bz, tx, ty, tz in [(0.2, 0.1, 1.9, 3.5, 1.1), (-0.2, -0.1, -2.1, 3.7, -0.7)]: + p.add(BARKF, tube([(bx, h - 0.4, bz), (tx, ty, tz)], 0.16, seg=7)) + p.collider = {'type': 'cylinder', 'r': 0.85, 'h': 2.8} + return p + +def tree_gum(): + p = Prop('tree_gum', 'vegetation', 'eucalypt — pale mottled trunk, sparse crown') + h = 5.6 + prof = [(0.30, 0.0), (0.24, 0.5), (0.18, 2.5), (0.14, 4.2), (0.11, h)] + p.add(BARKG, lathe(prof, seg=10, cap_top=True)) + p.add(BARKG, tube([(0, 4.1, 0), (1.3, 5.4, 0.6)], 0.09, seg=6)) + p.add(BARKG, tube([(0, 4.8, 0), (-1.1, 5.9, -0.5)], 0.08, seg=6)) + for dx, dy, dz, r, sd in [(0.3, 6.1, 0.2, 1.5, 2.0), (1.5, 5.8, 0.7, 1.2, 6.0), + (-1.3, 6.3, -0.6, 1.3, 11.0)]: + p.add(LEAFG, translate(blob(r, r * 0.62, r, seg=10, rings=6, amp=0.25, seed=sd), + dx, dy, dz)) + p.collider = {'type': 'cylinder', 'r': 0.32, 'h': 5.6} + return p + +def mound_dirt(): + p = Prop('mound_dirt', 'dressing', 'dirt jump mound — ridable') + R, H, n = 2.2, 0.55, 10 + rows = [] + for i in range(n + 1): + zr = -R + 2 * R * i / n + row = [] + for j in range(n + 1): + xr = -R + 2 * R * j / n + d2 = (xr * xr + zr * zr) / (R * R) + y = H * math.exp(-3.2 * d2) * max(0.0, 1 - d2 * 0.55) + row.append((xr, y, zr)) + rows.append(row) + p.add(DIRT, sheet(rows, flip=True)) + p.element = {'kind': 'mogul', 'h': H, 's': 1.4} + return p + +# ---------------------------------------------------------------- build all +BUILDERS = [ + lambda: quarter('qp_24', 2.4, 1.2, 1.8, 'plywood quarter pipe 4ft, steel coping'), + lambda: quarter('qp_18', 1.8, 0.9, 1.35, 'mini quarter 3ft'), + lambda: bank('bank_12', 2.4, 0.9, 1.8, 'plywood wedge bank'), + kicker, funbox_ply, spine_ply, manual_pad, ledge_concrete, grindbox_ply, + lambda: flat_rail('rail_flat_3m', 3.0, 0.35, 2, 'round flat rail, 3m'), + lambda: flat_rail('rail_flat_6m', 6.0, 0.45, 3, 'round flat rail, 6m'), + rail_kink, rail_rainbow, pipe_full, + bench_park, bench_concrete, picnic_table, bin_council, bollard_steel, + railing_ped, planter_box, kerb_3m, wall_graffiti, shade_sail, + tree_fig, tree_gum, mound_dirt, +] + +def main(): + os.makedirs(PROPS, exist_ok=True) + entries = [] + for b in BUILDERS: + entries.append(b().save(PROPS, TEX)) + manifest = { + 'name': 'park_kit', + 'version': '0.1', + 'note': 'Original GODVERSE park objects. props/ and textures/ must ship together ' + '(GLBs reference ../textures/*.jpg). grinds are prop-local segments — ' + 'rotate/translate with the prop and append to the park rails list. ' + 'element = matching skatemakerpro terrain kind for real collision.', + 'textures': 'MODELBEAST flux_local (flux2-klein-4b), original generations', + 'props': entries, + } + with open(os.path.join(ROOT, 'manifest.json'), 'w') as f: + json.dump(manifest, f, indent=1) + cats = {} + for e in entries: cats[e['category']] = cats.get(e['category'], 0) + 1 + print(f"manifest.json: {len(entries)} props {cats}") + +if __name__ == '__main__': + main() diff --git a/tools/propkit.py b/tools/propkit.py new file mode 100644 index 0000000..99ae3ee --- /dev/null +++ b/tools/propkit.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +"""propkit.py — textured-GLB authoring core for the park object library. + +Upgrade over the character_kit writers: TEXCOORD_0 on every primitive and real +PBR textures. Images are EXTERNAL URIs (../textures/*.png) so every prop shares +one texture set — GLBs stay geometry-sized and the browser caches each surface +once. Keep props/ and textures/ folders side by side (bookquoy + skatemakerpro +both vendor the pair together). + +Sampler uses MIRRORED_REPEAT: flux textures are tileable-*intent*, mirroring +kills the seam without any image post-processing. + +Conventions: metres, Y up, prop origin at ground centre. UVs are WORLD scale — +`uv` on a Mat is metres-per-tile, so texture density matches across every prop. +""" +import json, math, os, struct + +# ---------------------------------------------------------------- glb buffer +class Buf: + def __init__(self): self.b = bytearray() + def add(self, fmt, vals): + off = len(self.b) + for v in vals: + if isinstance(v, (list, tuple)): self.b += struct.pack("<" + fmt * len(v), *v) + else: self.b += struct.pack("<" + fmt, v) + while len(self.b) % 4: self.b += b"\0" + return off + +def write_glb(path, gjson, bin_): + j = json.dumps(gjson, separators=(",", ":")).encode() + while len(j) % 4: j += b" " + while len(bin_) % 4: bin_ += b"\0" + length = 12 + 8 + len(j) + 8 + len(bin_) + with open(path, "wb") as f: + f.write(struct.pack("uv) + ((0, 0, 1), [(x0, y0, z1), (x1, y0, z1), (x1, y1, z1), (x0, y1, z1)], lambda p: (p[0], p[1])), + ((0, 0, -1), [(x1, y0, z0), (x0, y0, z0), (x0, y1, z0), (x1, y1, z0)], lambda p: (-p[0], p[1])), + ((1, 0, 0), [(x1, y0, z1), (x1, y0, z0), (x1, y1, z0), (x1, y1, z1)], lambda p: (-p[2], p[1])), + ((-1, 0, 0), [(x0, y0, z0), (x0, y0, z1), (x0, y1, z1), (x0, y1, z0)], lambda p: (p[2], p[1])), + ((0, 1, 0), [(x0, y1, z1), (x1, y1, z1), (x1, y1, z0), (x0, y1, z0)], lambda p: (p[0], -p[2])), + ((0, -1, 0), [(x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1)], lambda p: (p[0], p[2])), + ] + P, N, UV, I = [], [], [], [] + for n, corners, uvf in F: + b = len(P) + P += corners; N += [n] * 4; UV += [uvf(p) for p in corners] + I += [b, b + 1, b + 2, b, b + 2, b + 3] + return _p(P, N, UV, I) + +def sheet(rows, flip=False, smooth=True): + """Grid surface from rows of (x,y,z). UVs by accumulated arc length.""" + nr, nc = len(rows), len(rows[0]) + P = [p for row in rows for p in row] + # arc-length UVs + us = [[0.0] * nc for _ in range(nr)] + vs = [[0.0] * nc for _ in range(nr)] + for r in range(nr): + for c in range(1, nc): + d = math.dist(rows[r][c], rows[r][c - 1]) + us[r][c] = us[r][c - 1] + d + for c in range(nc): + for r in range(1, nr): + d = math.dist(rows[r][c], rows[r - 1][c]) + vs[r][c] = vs[r - 1][c] + d + UV = [(us[r][c], vs[r][c]) for r in range(nr) for c in range(nc)] + I = [] + for r in range(nr - 1): + for c in range(nc - 1): + a = r * nc + c; b = a + 1; d = a + nc; e = d + 1 + quad = [a, b, e, a, e, d] if not flip else [a, e, b, a, d, e] + I += quad + N = compute_normals(P, I) + return _p(P, N, UV, I) + +def lathe(profile, seg=16, jitter=None, cap_top=True, cap_bot=False): + """Revolve profile [(radius, y), ...] around Y. jitter(iu, iv, r) -> r.""" + rows = [] + for iu in range(seg + 1): + th = 2 * math.pi * iu / seg + row = [] + for iv, (r, y) in enumerate(profile): + rr = jitter(iu % seg, iv, r) if jitter and r > 0 else r + row.append((rr * math.cos(th), y, rr * math.sin(th))) + rows.append(row) + # weld the seam ring so jitter matches + rows[-1] = rows[0] + pt = sheet(rows, flip=True) + # match tube(): u wraps the girth, v climbs the profile (bark ridges stay + # vertical — the transposed mapping chevroned the fig trunk) + pt["UV"] = [(v, u) for (u, v) in pt["UV"]] + caps = [] + if cap_top and profile[-1][0] > 0.001: + r, y = profile[-1] + caps.append(_disc(r, y, seg, up=True)) + if cap_bot and profile[0][0] > 0.001: + r, y = profile[0] + caps.append(_disc(r, y, seg, up=False)) + return merge(pt, *caps) + +def _disc(r, y, seg, up=True): + P = [(0, y, 0)] + [(r * math.cos(2 * math.pi * i / seg), y, + r * math.sin(2 * math.pi * i / seg)) for i in range(seg)] + N = [(0, 1 if up else -1, 0)] * (seg + 1) + UV = [(p[0], p[2]) for p in P] + I = [] + for i in range(seg): + j = 1 + i; k = 1 + (i + 1) % seg + I += [0, k, j] if up else [0, j, k] + return _p(P, N, UV, I) + +def tube(pts, r, seg=10, caps=True): + """Round tube along 3D polyline. Cylindrical UVs: u=around (girth m), v=along.""" + def frame(d): + up = (0, 1, 0) if abs(d[1]) < 0.95 else (1, 0, 0) + sx = _cross(d, up); sx = _norm(sx) + sy = _cross(sx, d) + return sx, sy + P, N, UV, I = [], [], [], [] + ring_n = seg + 1 + v = 0.0 + for i, p in enumerate(pts): + if i == 0: d = _norm(_sub(pts[1], pts[0])) + elif i == len(pts) - 1: d = _norm(_sub(pts[-1], pts[-2])) + else: d = _norm(_add(_norm(_sub(pts[i], pts[i - 1])), _norm(_sub(pts[i + 1], pts[i])))) + sx, sy = frame(d) + if i > 0: v += math.dist(pts[i], pts[i - 1]) + for s in range(ring_n): + th = 2 * math.pi * s / seg + n = _add(_mul(sx, math.cos(th)), _mul(sy, math.sin(th))) + P.append(_add(p, _mul(n, r))); N.append(tuple(n)) + UV.append((th * r, v)) + for i in range(len(pts) - 1): + for s in range(seg): + a = i * ring_n + s; b = a + 1; c = a + ring_n; d2 = c + 1 + I += [a, c, b, b, c, d2] + part = _p(P, N, UV, I) + if caps: + for idx, up in ((0, False), (len(pts) - 1, True)): + centre = pts[idx] + if idx == 0: d = _norm(_sub(pts[1], pts[0])) + else: d = _norm(_sub(pts[-1], pts[-2])) + nrm = d if up else _mul(d, -1) + b = len(part["P"]) + ring = [part["P"][idx * ring_n + s] for s in range(seg)] + part["P"] += [centre] + ring + part["N"] += [tuple(nrm)] * (seg + 1) + part["UV"] += [(0, 0)] + [(math.cos(2 * math.pi * s / seg) * r, + math.sin(2 * math.pi * s / seg) * r) for s in range(seg)] + for s in range(seg): + j = b + 1 + s; k = b + 1 + (s + 1) % seg + part["I"] += [b, j, k] if up else [b, k, j] + return part + +def _sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) +def _add(a, b): return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) +def _mul(a, s): return (a[0] * s, a[1] * s, a[2] * s) +def _cross(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) +def _norm(a): + l = math.sqrt(sum(c * c for c in a)) or 1 + return (a[0] / l, a[1] / l, a[2] / l) + +def blob(rx, ry, rz, seg=12, rings=8, amp=0.16, freq=5.0, seed=0.0): + """Noisy ellipsoid (tree canopy). Deterministic sin-hash displacement.""" + prof = [] + for iv in range(rings + 1): + ph = math.pi * iv / rings # 0..pi pole to pole + prof.append((math.sin(ph), -math.cos(ph))) # (r, y) unit sphere + def jit(iu, iv, r): + n = (math.sin(iu * freq * 0.71 + iv * 1.7 + seed) + + math.sin(iu * 1.3 + iv * freq * 0.53 + seed * 2.1) * 0.6) + return r * (1 + amp * n / 1.6) + pt = lathe([(r, y) for r, y in prof], seg=seg, jitter=jit, + cap_top=False, cap_bot=False) + return scale(pt, rx, ry, rz) + +# ---------------------------------------------------------------- prop builder +GLTF_MIRRORED = 33648 + +class Prop: + def __init__(self, pid, category, desc): + self.id, self.category, self.desc = pid, category, desc + self.parts = {} # mat.key -> (mat, merged part) + self.grinds = [] + self.collider = None + self.element = None + + def add(self, mat, *parts): + cur = self.parts.get(mat.key()) + p = merge(*parts) + self.parts[mat.key()] = (mat, merge(cur[1], p) if cur else p) + return self + + def grind(self, ax, az, bx, bz, ya, yb, kind="rail"): + """Grindable edge in PROP-LOCAL metres (pre-rotation). Editor transforms.""" + self.grinds.append({"a": [ax, az], "b": [bx, bz], "ya": ya, "yb": yb, "kind": kind}) + return self + + def save(self, props_dir, textures_dir): + buf = Buf(); accessors = []; views = []; prims = [] + materials = []; images = []; textures = []; samplers = [] + teximg = {} + def acc(fmt, ctype, count, atype, vals, minmax=False): + off = buf.add(fmt, vals) + comp = {"SCALAR": 1, "VEC2": 2, "VEC3": 3}[atype] + views.append({"buffer": 0, "byteOffset": off, + "byteLength": count * comp * (4 if ctype == 5126 else 4)}) + a = {"bufferView": len(views) - 1, "componentType": ctype, + "count": count, "type": atype} + if minmax: + a["min"] = [min(v[i] for v in vals) for i in range(comp)] + a["max"] = [max(v[i] for v in vals) for i in range(comp)] + accessors.append(a) + return len(accessors) - 1 + + for key, (mat, part) in self.parts.items(): + m = {"name": mat.name, + "pbrMetallicRoughness": {"baseColorFactor": list(mat.tint), + "metallicFactor": mat.metal, + "roughnessFactor": mat.rough}} + if mat.double: m["doubleSided"] = True + if mat.tex: + if mat.tex not in teximg: + ext = None + for e in (".png", ".jpg", ".jpeg", ".webp"): + if os.path.exists(os.path.join(textures_dir, mat.tex + e)): + ext = e; break + if ext is None: + raise SystemExit(f"{self.id}: texture missing: {mat.tex}") + if not samplers: + samplers.append({"magFilter": 9729, "minFilter": 9987, + "wrapS": GLTF_MIRRORED, "wrapT": GLTF_MIRRORED}) + images.append({"uri": "../textures/" + mat.tex + ext}) + textures.append({"sampler": 0, "source": len(images) - 1}) + teximg[mat.tex] = len(textures) - 1 + m["pbrMetallicRoughness"]["baseColorTexture"] = {"index": teximg[mat.tex]} + materials.append(m) + uvs = [(u / mat.uv, v / mat.uv) for u, v in part["UV"]] + pa = acc("f", 5126, len(part["P"]), "VEC3", [list(p) for p in part["P"]], minmax=True) + na = acc("f", 5126, len(part["N"]), "VEC3", [list(n) for n in part["N"]]) + ta = acc("f", 5126, len(uvs), "VEC2", [list(u) for u in uvs]) + ia = acc("I", 5125, len(part["I"]), "SCALAR", part["I"]) + prims.append({"attributes": {"POSITION": pa, "NORMAL": na, "TEXCOORD_0": ta}, + "indices": ia, "material": len(materials) - 1}) + + g = {"asset": {"version": "2.0", "generator": "park_kit propkit (original, GODVERSE)"}, + "scene": 0, "scenes": [{"nodes": [0], "name": self.id}], + "nodes": [{"name": "Prop_" + self.id, "mesh": 0}], + "meshes": [{"name": self.id + "_mesh", "primitives": prims}], + "materials": materials, + "accessors": accessors, "bufferViews": views, + "buffers": [{"byteLength": len(buf.b)}]} + if samplers: + g["samplers"] = samplers; g["images"] = images; g["textures"] = textures + out = os.path.join(props_dir, self.id + ".glb") + write_glb(out, g, bytes(buf.b)) + + xs = [p[0] for _, (m, pt) in self.parts.items() for p in pt["P"]] + ys = [p[1] for _, (m, pt) in self.parts.items() for p in pt["P"]] + zs = [p[2] for _, (m, pt) in self.parts.items() for p in pt["P"]] + tris = sum(len(pt["I"]) for _, (m, pt) in self.parts.items()) // 3 + entry = {"id": self.id, "src": "props/" + self.id + ".glb", + "category": self.category, "desc": self.desc, + "size": [round(max(xs) - min(xs), 3), round(max(ys), 3), + round(max(zs) - min(zs), 3)], + "tris": tris} + if self.grinds: entry["grinds"] = self.grinds + if self.collider: entry["collider"] = self.collider + if self.element: entry["element"] = self.element + print(f" {self.id}.glb {tris} tris {os.path.getsize(out)//1024}KB") + return entry