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 <noreply@anthropic.com>
63
README.md
Normal file
@ -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/<id>.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.
|
||||||
140
index.html
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>park_kit — catalog</title>
|
||||||
|
<style>
|
||||||
|
html,body{margin:0;height:100%;overflow:hidden;background:#0d0f0e;font-family:ui-monospace,Menlo,monospace}
|
||||||
|
#hud{position:fixed;top:10px;left:10px;color:#cfe8c9;z-index:5;font-size:13px}
|
||||||
|
#hud select{background:#1b241c;color:#cfe8c9;border:1px solid #3c5c40;font:inherit;padding:3px 6px}
|
||||||
|
#hud label{margin-left:10px;user-select:none}
|
||||||
|
#info{position:fixed;bottom:10px;left:10px;color:#8fae89;z-index:5;font-size:12px;white-space:pre}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="hud">
|
||||||
|
<b>park_kit</b>
|
||||||
|
<select id="pick"><option value="__all__">— all props —</option></select>
|
||||||
|
<label><input type="checkbox" id="grinds" checked> grind edges</label>
|
||||||
|
</div>
|
||||||
|
<div id="info"></div>
|
||||||
|
<script type="importmap">
|
||||||
|
{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js",
|
||||||
|
"three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const tl = new THREE.TextureLoader();
|
||||||
|
const sky = tl.load('textures/sky_day.jpg');
|
||||||
|
sky.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
scene.background = sky;
|
||||||
|
|
||||||
|
const cam = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.05, 300);
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
||||||
|
renderer.setSize(innerWidth, innerHeight);
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
document.body.appendChild(renderer.domElement);
|
||||||
|
const ctl = new OrbitControls(cam, renderer.domElement);
|
||||||
|
|
||||||
|
scene.add(new THREE.HemisphereLight(0xdfeeff, 0x3a4a35, 1.05));
|
||||||
|
const sun = new THREE.DirectionalLight(0xfff2dd, 2.2);
|
||||||
|
sun.position.set(18, 30, 12); sun.castShadow = true;
|
||||||
|
sun.shadow.mapSize.set(2048, 2048);
|
||||||
|
Object.assign(sun.shadow.camera, { left: -30, right: 30, top: 30, bottom: -30 });
|
||||||
|
scene.add(sun);
|
||||||
|
|
||||||
|
// ground: green paddo concrete
|
||||||
|
const gtex = tl.load('textures/concrete_green.jpg');
|
||||||
|
gtex.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
gtex.wrapS = gtex.wrapT = THREE.MirroredRepeatWrapping;
|
||||||
|
gtex.repeat.set(30, 30);
|
||||||
|
const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100),
|
||||||
|
new THREE.MeshStandardMaterial({ map: gtex, roughness: 0.92 }));
|
||||||
|
ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true;
|
||||||
|
scene.add(ground);
|
||||||
|
|
||||||
|
const manifest = await (await fetch('manifest.json')).json();
|
||||||
|
const pick = document.getElementById('pick');
|
||||||
|
for (const p of manifest.props) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = p.id; o.textContent = `${p.id} (${p.category})`;
|
||||||
|
pick.appendChild(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const grp = new THREE.Group(); scene.add(grp);
|
||||||
|
const grindGrp = new THREE.Group(); scene.add(grindGrp);
|
||||||
|
const grindMat = new THREE.LineBasicMaterial({ color: 0xff2d55 });
|
||||||
|
|
||||||
|
function label(text, x, z) {
|
||||||
|
const c = document.createElement('canvas'); c.width = 512; c.height = 96;
|
||||||
|
const g = c.getContext('2d');
|
||||||
|
g.fillStyle = '#0d0f0e'; g.globalAlpha = 0.55; g.fillRect(0, 0, 512, 96);
|
||||||
|
g.globalAlpha = 1; g.fillStyle = '#d8f2cf';
|
||||||
|
g.font = 'bold 44px Menlo,monospace'; g.textAlign = 'center';
|
||||||
|
g.fillText(text, 256, 62);
|
||||||
|
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: t }));
|
||||||
|
s.scale.set(2.6, 0.5, 1); s.position.set(x, 0.28, z + 1.9);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGrinds(p, x, z, ry) {
|
||||||
|
for (const gr of p.grinds || []) {
|
||||||
|
const rot = ([px, pz]) => [px * Math.cos(ry) + pz * Math.sin(ry),
|
||||||
|
-px * Math.sin(ry) + pz * Math.cos(ry)];
|
||||||
|
const [ax, az] = rot(gr.a), [bx, bz] = rot(gr.b);
|
||||||
|
const geo = new THREE.BufferGeometry().setFromPoints([
|
||||||
|
new THREE.Vector3(x + ax, gr.ya + 0.015, z + az),
|
||||||
|
new THREE.Vector3(x + bx, gr.yb + 0.015, z + bz)]);
|
||||||
|
grindGrp.add(new THREE.Line(geo, grindMat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let placed = 0;
|
||||||
|
function show(id) {
|
||||||
|
grp.clear(); grindGrp.clear(); placed = 0;
|
||||||
|
const items = id === '__all__' ? manifest.props
|
||||||
|
: manifest.props.filter(p => p.id === id);
|
||||||
|
const cols = Math.ceil(Math.sqrt(items.length));
|
||||||
|
const pitch = 7.5;
|
||||||
|
items.forEach((p, i) => {
|
||||||
|
const x = id === '__all__' ? (i % cols - (cols - 1) / 2) * pitch : 0;
|
||||||
|
const z = id === '__all__' ? (Math.floor(i / cols) - (cols - 1) / 2) * pitch : 0;
|
||||||
|
loader.load(p.src, g => {
|
||||||
|
g.scene.traverse(o => { if (o.isMesh) { o.castShadow = o.receiveShadow = true; } });
|
||||||
|
g.scene.position.set(x, 0, z);
|
||||||
|
grp.add(g.scene);
|
||||||
|
grp.add(label(p.id, x, z));
|
||||||
|
addGrinds(p, x, z, 0);
|
||||||
|
placed++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const dim = id === '__all__' ? cols * pitch
|
||||||
|
: Math.max(...(items[0].size || [4]), 2.5) * 1.35 + 1.5;
|
||||||
|
cam.position.set(dim * 0.55, dim * 0.45, dim * 0.85);
|
||||||
|
ctl.target.set(0, id === '__all__' ? 0 : (items[0].size?.[1] || 1.6) * 0.45, 0);
|
||||||
|
document.getElementById('info').textContent =
|
||||||
|
id === '__all__' ? `${items.length} props — ${manifest.note.split('.')[0]}`
|
||||||
|
: JSON.stringify(items[0], null, 1);
|
||||||
|
}
|
||||||
|
pick.onchange = () => show(pick.value);
|
||||||
|
document.getElementById('grinds').onchange = e =>
|
||||||
|
grindGrp.visible = e.target.checked;
|
||||||
|
show('__all__');
|
||||||
|
|
||||||
|
window.__ready = () => placed;
|
||||||
|
window.__view = id => { pick.value = id; show(id); };
|
||||||
|
addEventListener('resize', () => {
|
||||||
|
cam.aspect = innerWidth / innerHeight; cam.updateProjectionMatrix();
|
||||||
|
renderer.setSize(innerWidth, innerHeight);
|
||||||
|
});
|
||||||
|
(function loop() { requestAnimationFrame(loop); ctl.update(); renderer.render(scene, cam); })();
|
||||||
|
window.__snap = () => { ctl.update(); renderer.render(scene, cam); return true; };
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
914
manifest.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
props/bank_12.glb
Normal file
BIN
props/bench_concrete.glb
Normal file
BIN
props/bench_park.glb
Normal file
BIN
props/bin_council.glb
Normal file
BIN
props/bollard_steel.glb
Normal file
BIN
props/funbox_ply.glb
Normal file
BIN
props/grindbox_ply.glb
Normal file
BIN
props/kerb_3m.glb
Normal file
BIN
props/kicker.glb
Normal file
BIN
props/ledge_concrete.glb
Normal file
BIN
props/manual_pad.glb
Normal file
BIN
props/mound_dirt.glb
Normal file
BIN
props/picnic_table.glb
Normal file
BIN
props/pipe_full.glb
Normal file
BIN
props/planter_box.glb
Normal file
BIN
props/qp_18.glb
Normal file
BIN
props/qp_24.glb
Normal file
BIN
props/rail_flat_3m.glb
Normal file
BIN
props/rail_flat_6m.glb
Normal file
BIN
props/rail_kink.glb
Normal file
BIN
props/rail_rainbow.glb
Normal file
BIN
props/railing_ped_2m.glb
Normal file
BIN
props/shade_sail.glb
Normal file
BIN
props/spine_ply.glb
Normal file
BIN
props/tree_fig.glb
Normal file
BIN
props/tree_gum.glb
Normal file
BIN
props/wall_graffiti.glb
Normal file
11
serve.py
Normal file
@ -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()
|
||||||
BIN
textures/bark_fig.jpg
Normal file
|
After Width: | Height: | Size: 424 KiB |
BIN
textures/bark_gum.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
textures/brick_qld.jpg
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
textures/concrete_green.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
textures/concrete_rough.jpg
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
textures/concrete_smooth.jpg
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
textures/dirt_mulch.jpg
Normal file
|
After Width: | Height: | Size: 503 KiB |
BIN
textures/graffiti_a.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
textures/graffiti_b.jpg
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
textures/grass_dry.jpg
Normal file
|
After Width: | Height: | Size: 506 KiB |
BIN
textures/leaves_fig.jpg
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
textures/leaves_gum.jpg
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
textures/plywood_ramp.jpg
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
textures/sky_day.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
textures/steel_galv.jpg
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
textures/steel_scratch.jpg
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
textures/tarmac.jpg
Normal file
|
After Width: | Height: | Size: 424 KiB |
BIN
textures/wood_slats.jpg
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
tools/__pycache__/propkit.cpython-314.pyc
Normal file
121
tools/gen_textures.py
Normal file
@ -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()
|
||||||
523
tools/prop_author.py
Normal file
@ -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/<id>.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()
|
||||||
335
tools/propkit.py
Normal file
@ -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("<III", 0x46546C67, 2, length))
|
||||||
|
f.write(struct.pack("<II", len(j), 0x4E4F534A)); f.write(j)
|
||||||
|
f.write(struct.pack("<II", len(bin_), 0x004E4942)); f.write(bin_)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- materials
|
||||||
|
class Mat:
|
||||||
|
"""tex: basename in textures/ (no ext) or None for flat colour."""
|
||||||
|
def __init__(self, name, tex=None, tint=(1, 1, 1, 1), rough=0.9, metal=0.0,
|
||||||
|
uv=1.0, double=False):
|
||||||
|
self.name, self.tex, self.tint = name, tex, tuple(tint)
|
||||||
|
self.rough, self.metal, self.uv, self.double = rough, metal, uv, double
|
||||||
|
def key(self): return self.name
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- part math
|
||||||
|
def _p(P=None, N=None, UV=None, I=None):
|
||||||
|
return {"P": P or [], "N": N or [], "UV": UV or [], "I": I or []}
|
||||||
|
|
||||||
|
def merge(*parts):
|
||||||
|
out = _p()
|
||||||
|
for pt in parts:
|
||||||
|
base = len(out["P"])
|
||||||
|
out["P"] += pt["P"]; out["N"] += pt["N"]; out["UV"] += pt["UV"]
|
||||||
|
out["I"] += [base + i for i in pt["I"]]
|
||||||
|
return out
|
||||||
|
|
||||||
|
def translate(pt, dx, dy, dz):
|
||||||
|
return _p([(x + dx, y + dy, z + dz) for x, y, z in pt["P"]],
|
||||||
|
list(pt["N"]), list(pt["UV"]), list(pt["I"]))
|
||||||
|
|
||||||
|
def rotate_y(pt, ang):
|
||||||
|
c, s = math.cos(ang), math.sin(ang)
|
||||||
|
rp = [(x * c + z * s, y, -x * s + z * c) for x, y, z in pt["P"]]
|
||||||
|
rn = [(x * c + z * s, y, -x * s + z * c) for x, y, z in pt["N"]]
|
||||||
|
return _p(rp, rn, list(pt["UV"]), list(pt["I"]))
|
||||||
|
|
||||||
|
def scale(pt, sx, sy, sz):
|
||||||
|
rp = [(x * sx, y * sy, z * sz) for x, y, z in pt["P"]]
|
||||||
|
rn = []
|
||||||
|
for x, y, z in pt["N"]: # inverse-transpose for normals
|
||||||
|
nx, ny, nz = x / sx, y / sy, z / sz
|
||||||
|
l = math.sqrt(nx * nx + ny * ny + nz * nz) or 1
|
||||||
|
rn.append((nx / l, ny / l, nz / l))
|
||||||
|
return _p(rp, rn, list(pt["UV"]), list(pt["I"]))
|
||||||
|
|
||||||
|
def compute_normals(P, I):
|
||||||
|
N = [[0.0, 0.0, 0.0] for _ in P]
|
||||||
|
for i in range(0, len(I), 3):
|
||||||
|
a, b, c = I[i], I[i + 1], I[i + 2]
|
||||||
|
ux, uy, uz = (P[b][k] - P[a][k] for k in range(3))
|
||||||
|
vx, vy, vz = (P[c][k] - P[a][k] for k in range(3))
|
||||||
|
n = (uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx)
|
||||||
|
for j in (a, b, c):
|
||||||
|
for k in range(3): N[j][k] += n[k]
|
||||||
|
out = []
|
||||||
|
for n in N:
|
||||||
|
l = math.sqrt(sum(c * c for c in n)) or 1
|
||||||
|
out.append((n[0] / l, n[1] / l, n[2] / l))
|
||||||
|
return out
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- primitives
|
||||||
|
def box(cx, cy, cz, sx, sy, sz):
|
||||||
|
"""Axis-aligned box, per-face planar UVs in world metres."""
|
||||||
|
x0, x1 = cx - sx / 2, cx + sx / 2
|
||||||
|
y0, y1 = cy - sy / 2, cy + sy / 2
|
||||||
|
z0, z1 = cz - sz / 2, cz + sz / 2
|
||||||
|
F = [ # (normal, corners ccw, uv picks: fn(corner)->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
|
||||||