Spaces:
Running
Running
Muse Engine 32 WebGL2 release candidate
Browse files- game/muse-authored/phase30/FIXES-P30.md +51 -0
- game/muse-authored/phase30/README.md +7 -0
- game/muse-authored/phase30/build_p30_request.py +48 -0
- game/muse-authored/phase30/call_muse_p30.py +29 -0
- game/muse-authored/phase30/p30-call.log +3 -0
- game/muse-authored/phase30/p30-content.txt +21 -0
- game/muse-authored/phase30/p30-reasoning.txt +71 -0
- game/muse-authored/phase30/p30-request.json +15 -0
- game/muse-authored/phase30/p30-response.json +1 -0
game/muse-authored/phase30/FIXES-P30.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 30 — Muse Glimmer output: review and coach fix log
|
| 2 |
+
|
| 3 |
+
**What Muse was asked for** (`p30-request.json`, built by `build_p30_request.py`, sent by `call_muse_p30.py` from the studio PC to Paul's local llama.cpp server; the endpoint comes from an env var and is not stored anywhere):
|
| 4 |
+
two plain-JS modules, `engine/physics/mesh-collider.js` (grid-hashed triangle collider: ground, walls, push-out, raycast, spawn search) and `engine/import/scene-units.js` (FBX/glTF units + up-axis decision and conversion matrix), plus short notes.
|
| 5 |
+
|
| 6 |
+
**Call:** model `muse-glimmer-30b`, temperature 0.3, 27 Sep 2026 11:14:06 → 11:36:50 AEST (1364 s), 1573 prompt / 6613 completion tokens, `finish=stop`. Files: `p30-response.json` (full API response), `p30-content.txt` (the reply), `p30-reasoning.txt` (the model's visible reasoning), `p30-call.log`.
|
| 7 |
+
**Raw files** (split verbatim from the reply by the `===FILE:` markers): `raw/engine/physics/mesh-collider.js`, `raw/engine/import/scene-units.js`, `raw/PHASE30-NOTES.md`.
|
| 8 |
+
|
| 9 |
+
**Shipped files:** `engine/physics/mesh-collider.js` and `engine/import/scene-units.js` are Muse's code **pretty-printed with prettier (formatting only; the harness shows identical behaviour before/after)** plus the fixes below, each marked in the code as `[coach fix Pn]` / `[coach fix Un]`.
|
| 10 |
+
Muse's structure and API were kept: the uniform XZ grid hash keyed "ix,iz", per-triangle normal/bounds arrays, walkable-by-normal classification, the barycentric `groundAt`, the 5-sample `groundAtDisc`, sphere-stacked `wallHit`, the stats object, the spawn validity rules (ground, disc agreement, wall-free cylinder, 1.9 m headroom ray) and the decide() rules (glTF = m/Y-up, FBX UnitScaleFactor/100, size-based guess, forced overrides).
|
| 11 |
+
|
| 12 |
+
## How it was checked
|
| 13 |
+
`harness/harness.js` (coach) runs every query against brute-force references on a synthetic scene (floor, a 2 m wall, a 0.5 m step, a low deck) and on the real Kenney Yard collision mesh (17,376 triangles), plus the units/matrix rules.
|
| 14 |
+
|
| 15 |
+
| Stage | Result |
|
| 16 |
+
|---|---|
|
| 17 |
+
| Muse raw + P0 only (it doesn't parse at all without P0) | **hangs**: every triangle dropped → bounds stay ±Infinity → `findSpawn` loops forever (`results-stage0-…txt`) |
|
| 18 |
+
| Muse raw + P0 + P1 | **14/28** (`results-stage1-muse-P0-P1.txt`) |
|
| 19 |
+
| Muse + all coach fixes (shipped) | **28/28** (`results-final-muse-plus-coach-fixes.txt`) |
|
| 20 |
+
|
| 21 |
+
## mesh-collider.js
|
| 22 |
+
| Fix | Problem in Muse's output | Fix |
|
| 23 |
+
|---|---|---|
|
| 24 |
+
| **P0** | Syntax error: the object literal `window.MuseMeshCollider = { build: function…` was closed with `};` inside the literal (`return collider;};})();`), so the file did not parse. | Added the missing `}` (the formatted copy shows the normal closing). |
|
| 25 |
+
| **P1** | "Degenerate" filter `if (x0===x1 \|\| y0===y1 \|\| z0===z1) skip` dropped every triangle with two equal x, y **or** z values, i.e. every flat floor and axis-aligned wall (the test scene lost all 17k triangles). | Removed; the area test (`len < 1e-10`) that Muse also wrote is kept. |
|
| 26 |
+
| **P2** | `groundAt` accepted any triangle under the point, including ceilings/undersides (normal −Y) and steep slopes, so the underside of a deck read as floor. | Only walkable triangles; skip triangles whose lowest point is above maxY; 1e-5 tolerance on maxY. |
|
| 27 |
+
| **P3** | `closestPointTri` was not Ericson 5.1.5 (vertex-B region used bc·bp, edge regions guessed), so a player touching a wall face read as "not touching" (48/400 wall tests wrong). | Replaced with the textbook routine (same signature). |
|
| 28 |
+
| **P4** | `pushOut` moved a fixed 0.5 m per pass in the averaged direction and re-added the running total to x/z, i.e. a teleport whenever you brushed a wall; it also counted sphere heights as iterations. | Rewritten: up to 4 passes over all stacked spheres, resolve the deepest horizontal penetration by exactly its depth. |
|
| 29 |
+
| **P5** | `raycast` Möller–Trumbore algebra was wrong (u lost the dx terms, v was e1·(s×e2), t = e1·(e1×s) ≡ 0), so rays **never hit**; it sampled one cell per step (diagonal rays skip cells), allocated the stamp array per call and changed the stamp id every step. | Rewritten: textbook Möller–Trumbore, samples every cell/2 and visits the 3×3 cells around each sample, one reusable stamp per call, early exit after the nearest hit. |
|
| 30 |
+
| **P6** | `findSpawn` tested only 4 points per ring (the ±X/±Z axes), and its ring count came from the scene size, not the distance from the hint, so floor off those lines or beyond the hint's reach was never found; it also scanned every ring even after finding a spot. | Full square ring of 0.5 m grid points; rings reach the far side of the scene from the hint; stop at the first ring with a valid point. |
|
| 31 |
+
| **P7** | Spawn "disc agreement" used `groundAtDisc`, which returns the **max** of its samples, so a 0.4 m beam or a wall top counted as standable. | All 4 support points at r = 0.35 m must have floor within 0.25 m of the centre. |
|
| 32 |
+
| **P8** | `wallHit` returned false for a cylinder shorter than 2r and could leave the top of the cylinder untested; degenerate triangles were counted as walls in stats. | Test one sphere at the middle of a short cylinder and always test a last sphere at the top; stats count only valid triangles. |
|
| 33 |
+
|
| 34 |
+
## scene-units.js
|
| 35 |
+
| Fix | Problem in Muse's output | Fix |
|
| 36 |
+
|---|---|---|
|
| 37 |
+
| **U1** | `units = usf === 1 ? 'cm' : …` — three.js reads UnitScaleFactor 1 as 0.99999997…, so a centimetre file was labelled "m" (the scale itself was right). | Tolerant comparison; also mm/ft/other labels. |
|
| 38 |
+
| **U2** | `upAxis === 2 \|\| upAxis === undefined && format === 'fbx'` made every FBX **without** UpAxis metadata Z-up; the brief says unknown → keep Y-up. | Explicit cases: Z (±), Y (±), X (unsupported → Y-up + hint), unknown → Y-up. |
|
| 39 |
+
| **U3** | Reason text said "UpAxis 1 -> rotate" for Y-up files and repeated the Z-up text. | One clear up-axis phrase. |
|
| 40 |
+
| **U4** | `matrix()` started from diag(s, 0, 0, 1): with zUp the Y and Z columns came out **all zero** (the scene collapsed onto a line); the rotation it used (y′ = −z, z′ = −y) is a mirror that also turns the scene upside down; `upSign` flipped single entries. | Rewritten as rotation then uniform scale: +Z-up → (x, z, −y)·s; −Z-up → (x, −z, y)·s; −Y-up → (x, −y, −z)·s; otherwise s·I. Determinant > 0 (no mirroring). |
|
| 41 |
+
|
| 42 |
+
`raw/PHASE30-NOTES.md` is kept as Muse wrote it. Where it differs from the engine: the engine probes the floor with its own 9-point footprint at r + 3 cm (see below), not `groundAtDisc`.
|
| 43 |
+
|
| 44 |
+
## Coach-written Phase 30 parts (not Muse)
|
| 45 |
+
- `engine/ui/model-import.js`: `toLevelMeshes` (world transforms, mirrored-node winding, naming conventions `UCX_`/`_col`/`nocol`/foliage/`PlayerStart`, far-origin recentring), `importInto` level mode.
|
| 46 |
+
- `_dev/e30_block.js` → `engine/engine.js` `[e30]`: wiring the collider into Phase 28/29 movement, AI, shooting and draw; level activate/deactivate + fallback toggle; spawn placement; kill-plane respawn; **footprint probe** (Muse's `groundAtDisc` samples at 0.7 r, inside the wall radius, so a riser blocked the player before the step top was seen and step-up/mantle only worked when a long frame jumped past it; the engine probes centre + 8 points at r + 3 cm).
|
| 47 |
+
- Engine 29 bug fixes: crosshair + hit marker follow the canvas centre (`syncCrosshair`); exact pyramid ray test (`pyramidRayT`) instead of its square AABB.
|
| 48 |
+
- Vendored FBXLoader: 1 small addition that copies GlobalSettings UpAxis/UpAxisSign/… into `userData` (marked `[e30 muse-engine]`).
|
| 49 |
+
- Config panel (Use as level, Units, Up, CC0 test buttons, Scene colliders toggle), sidebar hide (H), test pack build script.
|
| 50 |
+
|
| 51 |
+
**Transparency note:** while the 23-minute Muse call was running, the coach wrote a stand-in with the same API (`_dev/coach-ref/`, dev-only, never shipped or loaded by the app) to wire up and test the engine side. When Muse's output arrived it replaced the stand-in; the shipped modules are Muse's code with the fixes above. (For the record the stand-in scored 26/28 on the same harness.)
|
game/muse-authored/phase30/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 30 provenance (Muse Glimmer)
|
| 2 |
+
- `build_p30_request.py` → `p30-request.json`: the exact prompt (system + user) sent to Muse Glimmer.
|
| 3 |
+
- `call_muse_p30.py`: the caller (endpoint from the `MUSE_BASE_URL` env var; nothing stored).
|
| 4 |
+
- `p30-response.json`, `p30-content.txt`, `p30-reasoning.txt`, `p30-call.log`: Muse's raw output and call log (27 Sep 2026, 1364 s, 6613 tokens).
|
| 5 |
+
- `raw/`: the three files split verbatim from the reply.
|
| 6 |
+
- `FIXES-P30.md`: review + every coach fix (P0–P8, U1–U4) and what the coach wrote instead.
|
| 7 |
+
- `harness/`: the review harness, the intermediate Muse stages and their results (stage1 14/28 → shipped 28/28).
|
game/muse-authored/phase30/build_p30_request.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Builds the Muse Glimmer request for Phase 30 (scene loading: mesh colliders + units/up-axis). Muse Engine 30.
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
HERE = Path(__file__).resolve().parent
|
| 5 |
+
|
| 6 |
+
SYSTEM = """You are Muse Glimmer, author of Muse Breaker Engine (homemade raw WebGL1, NO Three.js at runtime, no libraries).
|
| 7 |
+
Task: Phase 30 "proper scene loading". Author two small plain-JS modules that let a user's own imported 3D scene become the playable level.
|
| 8 |
+
Plain browser JS (ES2015), each file one IIFE that sets a window global, no imports, no DOM, no WebGL, no Math.random. Keep them compact and fast (a scene can have 200k triangles).
|
| 9 |
+
Reply ONLY in this exact format, complete files, no commentary outside the files:
|
| 10 |
+
===FILE: engine/physics/mesh-collider.js===
|
| 11 |
+
...
|
| 12 |
+
===FILE: engine/import/scene-units.js===
|
| 13 |
+
...
|
| 14 |
+
===FILE: PHASE30-NOTES.md===
|
| 15 |
+
...
|
| 16 |
+
===END==="""
|
| 17 |
+
|
| 18 |
+
USER = r"""MUSE ENGINE 30 - SCENE LOADING. Engine facts: world units are METRES, +Y is up, player is a vertical cylinder radius 0.35 m, standing height 1.7 m (eye 1.6), step-up 0.55 m, mantle up to 1.15 m. Player feet height is called feetY. The imported scene arrives as world-space triangles (already converted to metres and Y-up by scene-units.js).
|
| 19 |
+
|
| 20 |
+
FILE 1: engine/physics/mesh-collider.js -> window.MuseMeshCollider = { build }
|
| 21 |
+
build(positions, opts) where positions is a Float32Array of non-indexed triangles [x0,y0,z0, x1,y1,z1, x2,y2,z2, ...] (metres, Y-up).
|
| 22 |
+
opts: { cell: 2.0 (grid cell size in metres, XZ), walkableNy: 0.7 (a triangle is WALKABLE if its unit normal y >= this, else it is a WALL/obstacle; ignore degenerate triangles) }
|
| 23 |
+
Use a uniform XZ grid hash (Map or object keyed "ix,iz") storing triangle indices; a triangle goes into every cell its XZ bounding box overlaps. Precompute per-triangle normal and y-range.
|
| 24 |
+
Returns a collider object with:
|
| 25 |
+
- stats: { tris, walkable, walls, cells, min:[x,y,z], max:[x,y,z] }
|
| 26 |
+
- groundAt(x, z, maxY): highest y where a vertical line at (x,z) hits a WALKABLE triangle with y <= maxY; null if none. (point-in-triangle in XZ with barycentrics, then interpolate y)
|
| 27 |
+
- groundAtDisc(x, z, r, maxY): max of groundAt at the centre and at 4 points at distance r*0.7 (N/E/S/W) so the player can stand on edges; null if all null.
|
| 28 |
+
- wallHit(x, z, r, yMin, yMax): true if any NON-walkable triangle intersects the vertical cylinder (centre x,z, radius r, from yMin to yMax). Approximate the cylinder with spheres of radius r whose centres are stacked from yMin+r to yMax-r (spacing <= r); sphere vs triangle via closest-point-on-triangle (Ericson, Real-Time Collision Detection 5.1.5). Only test triangles in grid cells overlapping the circle and whose y-range overlaps [yMin, yMax].
|
| 29 |
+
- pushOut(x, z, r, yMin, yMax): return [dx, dz], the horizontal push that moves the cylinder out of penetrating NON-walkable triangles (sum/max of per-sphere horizontal penetration vectors; iterate up to 4 times); [0,0] if none.
|
| 30 |
+
- raycast(ox, oy, oz, dx, dy, dz, maxT): nearest triangle hit of ANY triangle (walkable or wall) along the normalised ray within maxT: { t, x, y, z, nx, ny, nz, tri } or null. Moller-Trumbore. Walk the grid cells along the ray in XZ (2D DDA, or step along the ray every cell/2 metres) and test each triangle once (use a stamp array to avoid retesting).
|
| 31 |
+
- findSpawn(hintX, hintZ, opts): a valid standing point [x, y, z] (y = feet height on a walkable surface) or null. Search outward from (hintX,hintZ) on a spiral/ring grid with 0.5 m spacing up to the scene bounds; at each (x,z) take ground = groundAt(x, z, stats.max[1] + 1); valid if ground != null, groundAtDisc agrees within 0.25 m, wallHit(x, z, 0.35, ground + 0.3, ground + 1.8) is false, and there is headroom (raycast straight up from ground+0.1 finds nothing within 1.9 m). Prefer the first valid point (closest to the hint).
|
| 32 |
+
Numbers must be robust: epsilon 1e-7 in Moller-Trumbore, skip triangles with area < 1e-10.
|
| 33 |
+
|
| 34 |
+
FILE 2: engine/import/scene-units.js -> window.MuseSceneUnits = { decide, matrix }
|
| 35 |
+
decide(meta, size) chooses how to convert an imported file to metres/Y-up.
|
| 36 |
+
meta: { format: 'fbx'|'glb'|'gltf', unitScaleFactor (FBX GlobalSettings: centimetres per file unit, e.g. 1 = cm, 100 = m, 2.54 = inch; may be undefined), upAxis (FBX GlobalSettings UpAxis: 0=X,1=Y,2=Z; may be undefined), upAxisSign (1 or -1), forcedUnits ('auto'|'m'|'cm'|'mm'|'in'|'ft'), forcedUp ('auto'|'y'|'z') }
|
| 37 |
+
size: [sx, sy, sz] bounding-box size in raw file units as parsed (before any conversion).
|
| 38 |
+
Rules: glTF/GLB is metres and +Y up by spec. FBX: metres = fileUnits * unitScaleFactor / 100 when unitScaleFactor is known; FBX upAxis 2 means Z-up (convert with a -90 degree rotation about X so +Z becomes +Y; respect upAxisSign). Forced options override everything. If units are unknown (auto with no metadata), guess from the biggest dimension: > 400 file units -> cm, > 4000 -> mm, else metres. If up is unknown, keep Y-up.
|
| 39 |
+
Return { scale (metres per file unit), zUp (bool), upSign (1|-1), units: 'm'|'cm'|..., reason: '<short human text, e.g. "FBX UnitScaleFactor 1 (cm) -> x0.01; UpAxis Z -> rotate -90 X">' }
|
| 40 |
+
matrix(decision): a 4x4 column-major Float32Array(16) (WebGL layout) that applies the rotation (if zUp) then the uniform scale.
|
| 41 |
+
|
| 42 |
+
FILE 3: PHASE30-NOTES.md: 10-20 lines: approach, limits, and what the engine must call each frame (e.g. how to use groundAt/wallHit for step-up: if the new position has a ground top higher than feetY by <= 0.55 step up, walls above that top block).
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
req = {"model": "muse-glimmer-30b", "temperature": 0.3, "max_tokens": 14000,
|
| 46 |
+
"messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": USER}]}
|
| 47 |
+
(HERE / "p30-request.json").write_text(json.dumps(req, indent=1), encoding="utf-8")
|
| 48 |
+
print("wrote p30-request.json", len(json.dumps(req)))
|
game/muse-authored/phase30/call_muse_p30.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Calls Muse Glimmer (OpenAI-compatible llama.cpp) with p30-request.json; saves raw response for provenance.
|
| 2 |
+
# Endpoint comes from env MUSE_BASE_URL (not stored in this file).
|
| 3 |
+
import json, os, sys, time, urllib.request
|
| 4 |
+
from datetime import datetime, timezone, timedelta
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
HERE = Path(__file__).resolve().parent
|
| 7 |
+
BASE = os.environ.get("MUSE_BASE_URL", "").rstrip("/")
|
| 8 |
+
SYD = timezone(timedelta(hours=10))
|
| 9 |
+
LOG = HERE / "p30-call.log"
|
| 10 |
+
def log(m):
|
| 11 |
+
with LOG.open("a", encoding="utf-8") as f: f.write(m + "\n")
|
| 12 |
+
print(m, flush=True)
|
| 13 |
+
if not BASE: log("MUSE_BASE_URL not set"); sys.exit(2)
|
| 14 |
+
body = (HERE / "p30-request.json").read_bytes()
|
| 15 |
+
log(f"START {datetime.now(SYD).isoformat()} bytes={len(body)}")
|
| 16 |
+
t0 = time.perf_counter()
|
| 17 |
+
try:
|
| 18 |
+
req = urllib.request.Request(BASE + "/chat/completions", data=body, headers={"Content-Type": "application/json"}, method="POST")
|
| 19 |
+
with urllib.request.urlopen(req, timeout=3000) as r: raw = r.read()
|
| 20 |
+
(HERE / "p30-response.json").write_bytes(raw)
|
| 21 |
+
d = json.loads(raw.decode("utf-8"))
|
| 22 |
+
msg = d["choices"][0]["message"]
|
| 23 |
+
(HERE / "p30-content.txt").write_text(msg.get("content") or "", encoding="utf-8")
|
| 24 |
+
(HERE / "p30-reasoning.txt").write_text(msg.get("reasoning_content") or "", encoding="utf-8")
|
| 25 |
+
log(f"OK finish={d['choices'][0].get('finish_reason')} usage={d.get('usage')} elapsed={time.perf_counter()-t0:.0f}s model={d.get('model')}")
|
| 26 |
+
except Exception as e:
|
| 27 |
+
log(f"FAIL after {time.perf_counter()-t0:.0f}s {type(e).__name__}: {e}")
|
| 28 |
+
sys.exit(1)
|
| 29 |
+
log(f"DONE {datetime.now(SYD).isoformat()}")
|
game/muse-authored/phase30/p30-call.log
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
START 2026-09-27T11:14:06.501562+10:00 bytes=5509
|
| 2 |
+
OK finish=stop usage={'completion_tokens': 6613, 'prompt_tokens': 1573, 'total_tokens': 8186, 'prompt_tokens_details': {'cached_tokens': 0}} elapsed=1364s model=muse-glimmer-30b
|
| 3 |
+
DONE 2026-09-27T11:36:50.542864+10:00
|
game/muse-authored/phase30/p30-content.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
===FILE: engine/physics/mesh-collider.js===
|
| 2 |
+
(function(){
|
| 3 |
+
function barycentricXZ(px,pz,ax,az,bx,bz,cx,cz){var v0x=bx-ax,v0z=bz-az,v1x=cx-ax,v1z=cz-az,v2x=px-ax,v2z=pz-az;var d00=v0x*v0x+v0z*v0z;var d01=v0x*v1x+v0z*v1z;var d11=v1x*v1x+v1z*v1z;var d20=v2x*v0x+v2z*v0z;var d21=v2x*v1x+v2z*v1z;var denom=d00*d11-d01*d01;if(Math.abs(denom)<1e-12)return null;var inv=1/denom;var u=(d11*d20-d01*d21)*inv;var v=(d00*d21-d01*d20)*inv;if(u>= -1e-7 && v>= -1e-7 && u+v<=1+1e-7)return [u,v];return null;}
|
| 4 |
+
function closestPointTri(px,py,pz,ax,ay,az,bx,by,bz,cx,cy,cz){var abx=bx-ax,aby=by-ay,abz=bz-az;var acx=cx-ax,acy=cy-ay,acz=cz-az;var apx=px-ax,apy=py-ay,apz=pz-az;var d1=abx*apx+aby*apy+abz*apz;var d2=acx*apx+acy*apy+acz*apz;if(d1<=0 && d2<=0)return [ax,ay,az];var bcx=cx-bx,bcy=cy-by,bcz=cz-bz;var bpx=px-bx,bpy=py-by,bpz=pz-bz;var d3=bcx*bpx+bcy*bpy+bcz*bpz;if(d3>=0 && d2<=d3)return [bx,by,bz];var ccx=ax-cx,ccy=ay-cy,ccz=az-cz;var cpx=px-cx,cpy=py-cy,cpz=pz-cz;var d4=ccx*cpx+ccy*cpy+ccz*cpz;if(d4>=0 && d1<=d4)return [cx,cy,cz];var vc=acx*abx+acy*aby+acz*abz;var d00=abx*abx+aby*aby+abz*abz;var d11=acx*acx+acy*acy+acz*acz;var d20=d1;var d21=d2;var denom=d00*d11-vc*vc;if(Math.abs(denom)<1e-12)denom=1e-12;var v=d11*d20-vc*d21;var w=vc*d20-d00*d21;if(v>0 && w>0 && v+w<=d00*d11){var inv=1/denom;v*=inv;w*=inv;return [ax+abx*v+acx*w, ay+aby*v+acy*w, az+abz*v+acz*w];}var v=d1/d00;if(v>=0 && v<=1){var px1=ax+abx*v,py1=ay+aby*v,pz1=az+abz*v;return [px1,py1,pz1];}var w=-d4/(ccx*ccx+ccy*ccy+ccz*ccz);if(w>=0 && w<=1){var px1=cx+ccx*w,py1=cy+ccy*w,pz1=cz+ccz*w;return [px1,py1,pz1];}var w=-d3/(bcx*bcx+bcy*bcy+bcz*bcz);if(w>=0 && w<=1){var px1=bx+bcx*w,py1=by+bcy*w,pz1=bz+bcz*w;return [px1,py1,pz1];}return [ax,ay,az];}
|
| 5 |
+
window.MuseMeshCollider={build:function(positions,opts){opts=opts||{};var cell=opts.cell||2.0;var walkableNy=opts.walkableNy!==undefined?opts.walkableNy:0.7;var n=positions.length/9|0;var triN=new Float32Array(n*3),triMinY=new Float32Array(n),triMaxY=new Float32Array(n),triMinX=new Float32Array(n),triMaxX=new Float32Array(n),triMinZ=new Float32Array(n),triMaxZ=new Float32Array(n),walkable=new Uint8Array(n);var min=[Infinity,Infinity,Infinity],max=[-Infinity,-Infinity,-Infinity];for(var i=0;i<n;i++){var b=i*9;var x0=positions[b],y0=positions[b+1],z0=positions[b+2];var x1=positions[b+3],y1=positions[b+4],z1=positions[b+5];var x2=positions[b+6],y2=positions[b+7],z2=positions[b+8];if(x0===x1||y0===y1||z0===z1){triN[i*3+1]=-2;continue;}var e1x=x1-x0,e1y=y1-y0,e1z=z1-z0;var e2x=x2-x0,e2y=y2-y0,e2z=z2-z0;var nx=e1y*e2z-e1z*e2y,ny=e1z*e2x-e1x*e2z,nz=e1x*e2y-e1y*e2x;var len=Math.hypot(nx,ny,nz);if(len<1e-10){triN[i*3+1]=-2;continue;}nx/=len;ny/=len;nz/=len;triN[i*3]=nx;triN[i*3+1]=ny;triN[i*3+2]=nz;var mnx=Math.min(x0,x1,x2),mxx=Math.max(x0,x1,x2);var mnz=Math.min(z0,z1,z2),mxz=Math.max(z0,z1,z2);var mny=Math.min(y0,y1,y2),mxy=Math.max(y0,y1,y2);triMinX[i]=mnx;triMaxX[i]=mxx;triMinZ[i]=mnz;triMaxZ[i]=mxz;triMinY[i]=mny;triMaxY[i]=mxy;walkable[i]=ny>=walkableNy?1:0;min[0]=Math.min(min[0],mnx);min[1]=Math.min(min[1],mny);min[2]=Math.min(min[2],mnz);max[0]=Math.max(max[0],mxx);max[1]=Math.max(max[1],mxy);max[2]=Math.max(max[2],mxz);}var grid={};for(var i=0;i<n;i++){if(triN[i*3+1]===-2)continue;var ix0=Math.floor(triMinX[i]/cell),ix1=Math.floor(triMaxX[i]/cell);var iz0=Math.floor(triMinZ[i]/cell),iz1=Math.floor(triMaxZ[i]/cell);for(var ix=ix0;ix<=ix1;ix++){for(var iz=iz0;iz<=iz1;iz++){var k=ix+','+iz;var arr=grid[k];if(!arr){arr=[];grid[k]=arr;}arr.push(i);}}}var cells=0;for(var k in grid)cells++;var collider={stats:{tris:n,walkable:0,walls:0,cells:cells,min:min,max:max},_positions:positions,_triN:triN,_triMinY:triMinY,_triMaxY:triMaxY,_triMinX:triMinX,_triMaxX:triMaxX,_triMinZ:triMinZ,_triMaxZ:triMaxZ,_walkable:walkable,_grid:grid,_cell:cell,_walkableNy:walkableNy};for(var i=0;i<n;i++)if(walkable[i])collider.stats.walkable++;else collider.stats.walls++;collider.groundAt=function(x,z,maxY){var ix=Math.floor(x/cell),iz=Math.floor(z/cell);var k=ix+','+iz;var arr=grid[k];if(!arr)return null;var best=null;for(var j=0;j<arr.length;j++){var i=arr[j];var b=i*9;var x0=positions[b],y0=positions[b+1],z0=positions[b+2];var x1=positions[b+3],y1=positions[b+4],z1=positions[b+5];var x2=positions[b+6],y2=positions[b+7],z2=positions[b+8];var bc=barycentricXZ(x,z,x0,z0,x1,z1,x2,z2);if(!bc)continue;var u=bc[0],v=bc[1];var w=1-u-v;var y=y0*w+y1*u+y2*v;if(y>maxY)continue;if(best===null||y>best)best=y;}return best;};collider.groundAtDisc=function(x,z,r,maxY){var off=r*0.7;var ys=[];var g=collider.groundAt(x,z,maxY);if(g!==null)ys.push(g);var pts=[[x+off,z],[x-off,z],[x,z+off],[x,z-off]];for(var p=0;p<pts.length;p++){var gx=pts[p][0],gz=pts[p][1];var gg=collider.groundAt(gx,gz,maxY);if(gg!==null)ys.push(gg);}if(ys.length===0)return null;var m=ys[0];for(var k=1;k<ys.length;k++)if(ys[k]>m)m=ys[k];return m;};collider.wallHit=function(x,z,r,yMin,yMax){var step=Math.max(0.01,r*0.9);var y=yMin+r;var end=yMax-r;if(y>end)return false;while(y<=end){var ix0=Math.floor((x-r)/cell),ix1=Math.floor((x+r)/cell);var iz0=Math.floor((z-r)/cell),iz1=Math.floor((z+r)/cell);for(var ix=ix0;ix<=ix1;ix++){for(var iz=iz0;iz<=iz1;iz++){var k=ix+','+iz;var arr=grid[k];if(!arr)continue;for(var j=0;j<arr.length;j++){var i=arr[j];if(walkable[i])continue;if(triMaxY[i]<y-r||triMinY[i]>y+r)continue;var b=i*9;var ax=positions[b],ay=positions[b+1],az=positions[b+2];var bx=positions[b+3],by=positions[b+4],bz=positions[b+5];var cx=positions[b+6],cy=positions[b+7],cz=positions[b+8];var cp=closestPointTri(x,y,z,ax,ay,az,bx,by,bz,cx,cy,cz);var dx=x-cp[0],dy=y-cp[1],dz=z-cp[2];if(dx*dx+dy*dy+dz*dz < (r-1e-7)*(r-1e-7))return true;}}}y+=step;}return false;};collider.pushOut=function(x,z,r,yMin,yMax){var dx=0,dz=0;var step=Math.max(0.01,r*0.9);var y=yMin+r;var end=yMax-r;var iter=0;while(y<=end && iter<4){var ix0=Math.floor((x-r)/cell),ix1=Math.floor((x+r)/cell);var iz0=Math.floor((z-r)/cell),iz1=Math.floor((z+r)/cell);var accX=0,accZ=0;for(var ix=ix0;ix<=ix1;ix++){for(var iz=iz0;iz<=iz1;iz++){var k=ix+','+iz;var arr=grid[k];if(!arr)continue;for(var j=0;j<arr.length;j++){var i=arr[j];if(walkable[i])continue;if(triMaxY[i]<y-r||triMinY[i]>y+r)continue;var b=i*9;var ax=positions[b],ay=positions[b+1],az=positions[b+2];var bx=positions[b+3],by=positions[b+4],bz=positions[b+5];var cx=positions[b+6],cy=positions[b+7],cz=positions[b+8];var cp=closestPointTri(x,y,z,ax,ay,az,bx,by,bz,cx,cy,cz);var ddx=x-cp[0],ddz=z-cp[2];var dist=Math.hypot(ddx,ddz);if(dist<r-1e-7){var pen=r-dist;var nx=ddx/(dist||1),nz=ddz/(dist||1);accX+=nx*pen;accZ+=nz*pen;}}}}var len=Math.hypot(accX,accZ);if(len>1e-7){dx+=accX/len*0.5;dz+=accZ/len*0.5;x+=dx;z+=dz;}else break;y+=step;iter++;}return [dx,dz];};collider.raycast=function(ox,oy,oz,dx,dy,dz,maxT){var len=Math.hypot(dx,dy,dz);if(len<1e-12)return null;dx/=len;dy/=len;dz/=len;var step=Math.max(0.01,cell/2);var t=0;var best=null;var stamp=new Int32Array(n);var stampId=1;var tested=0;while(t<=maxT){var px=ox+dx*t,py=oy+dy*t,pz=oz+dz*t;var ix=Math.floor(px/cell),iz=Math.floor(pz/cell);var k=ix+','+iz;var arr=grid[k];if(arr){for(var j=0;j<arr.length;j++){var i=arr[j];if(stamp[i]===stampId)continue;stamp[i]=stampId;var b=i*9;var x0=positions[b],y0=positions[b+1],z0=positions[b+2];var x1=positions[b+3],y1=positions[b+4],z1=positions[b+5];var x2=positions[b+6],y2=positions[b+7],z2=positions[b+8];var e1x=x1-x0,e1y=y1-y0,e1z=z1-z0;var e2x=x2-x0,e2y=y2-y0,e2z=z2-z0;var pxv=ox-x0,pyv=oy-y0,pzv=oz-z0;var tvec=[0,0,0];var pvec=[dx,dy,dz];var qvec=[e1x,e1y,e1z];var det=qvec[0]*pvec[1]*e2z - qvec[0]*pvec[2]*e2y - qvec[1]*pvec[0]*e2z + qvec[1]*pvec[2]*e2x + qvec[2]*pvec[0]*e2y - qvec[2]*pvec[1]*e2x;if(Math.abs(det)<1e-7)continue;var inv=1/det;var u=(pxv*e2y - pyv*e2x)*pvec[2] - (pxv*e2z - pzv*e2x)*pvec[1];u*=inv;if(u<0||u>1)continue;var v=(qvec[0]*(pyv*e2z - pzv*e2y) - qvec[1]*(pxv*e2z - pzv*e2x) + qvec[2]*(pxv*e2y - pyv*e2x))*inv;if(v<0||u+v>1)continue;var tt=(qvec[0]*(e1y*pzv - e1z*pyv) + qvec[1]*(e1z*pxv - e1x*pzv) + qvec[2]*(e1x*pyv - e1y*pxv))*inv;if(tt<1e-7||tt>maxT)continue;if(!best||tt<best.t){var hx=ox+dx*tt,hy=oy+dy*tt,hz=oz+dz*tt;var nx=e1y*e2z-e1z*e2y,ny=e1z*e2x-e1x*e2z,nz=e1x*e2y-e1y*e2x;var nlen=Math.hypot(nx,ny,nz)||1;nx/=nlen;ny/=nlen;nz/=nlen;best={t:tt,x:hx,y:hy,z:hz,nx:nx,ny:ny,nz:nz,tri:i};}}}t+=step;stampId++;if(stampId===2147483647)for(var s=0;s<n;s++)stamp[s]=0,stampId=1;}return best;};collider.findSpawn=function(hintX,hintZ,opts){opts=opts||{};var maxY=collider.stats.max[1]+1;var best=null,bestD=Infinity;var step=0.5;var maxR= Math.max(Math.abs(collider.stats.max[0]-collider.stats.min[0]),Math.abs(collider.stats.max[2]-collider.stats.min[2]));var rings=Math.ceil(maxR/step);for(var r=0;r<=rings;r++){var rad=r*step;for(var a=0;a<4;a++){var ang=a*Math.PI/2;var x=hintX+Math.cos(ang)*rad;var z=hintZ+Math.sin(ang)*rad;if(x<collider.stats.min[0]||x>collider.stats.max[0]||z<collider.stats.min[2]||z>collider.stats.max[2])continue;var ground=collider.groundAt(x,z,maxY);if(ground===null)continue;var disc=collider.groundAtDisc(x,z,0.35,maxY);if(disc===null||Math.abs(disc-ground)>0.25)continue;if(collider.wallHit(x,z,0.35,ground+0.3,ground+1.8))continue;var head=collider.raycast(x,ground+0.1,z,0,1,0,1.9);if(head)continue;var d=Math.hypot(x-hintX,z-hintZ);if(d<bestD){bestD=d;best=[x,ground,z];}}}return best;};return collider;};})();
|
| 6 |
+
===FILE: engine/import/scene-units.js===
|
| 7 |
+
(function(){
|
| 8 |
+
function guessScale(size){var m=Math.max(size[0],size[1],size[2]);if(m>4000)return 0.001;if(m>400)return 0.01;return 1;}
|
| 9 |
+
function guessUnits(s){if(s===0.001)return 'mm';if(s===0.01)return 'cm';return 'm';}
|
| 10 |
+
window.MuseSceneUnits={decide:function(meta,size){var format=(meta&&meta.format)||'glb';var forcedUnits=(meta&&meta.forcedUnits)||'auto';var forcedUp=(meta&&meta.forcedUp)||'auto';var scale=1, zUp=false, upSign=1, units='m', reason='';if(forcedUnits!=='auto'){var map={'m':1,'cm':0.01,'mm':0.001,'in':0.0254,'ft':0.3048};scale=map[forcedUnits]||1;units=forcedUnits;reason='Forced units '+forcedUnits; }else if(format==='glb'||format==='gltf'){scale=1;units='m';reason='glTF/GLB is metres +Y up by spec';}else if(format==='fbx'){var usf=meta&&meta.unitScaleFactor;if(typeof usf==='number'&&usf>0){scale=usf/100;units=usf===1?'cm':usf===100?'m':usf===2.54?'in':'m';reason='FBX UnitScaleFactor '+usf+' -> x'+scale.toFixed(5)+'; UpAxis '+(meta.upAxis!==undefined?meta.upAxis:'?')+' -> rotate';}else{scale=guessScale(size);units=guessUnits(scale);reason='FBX units unknown, guessed from size '+Math.max(size[0],size[1],size[2]).toFixed(1)+' -> '+units;}var upAxis=meta&&meta.upAxis;if(upAxis===2||upAxis===undefined && format==='fbx'){zUp=true;reason+='; UpAxis Z -> rotate -90 X';}upSign=(meta&&meta.upAxisSign)||1;}else{scale=guessScale(size);units=guessUnits(scale);reason='Unknown format, guessed '+units;}if(forcedUp!=='auto'){zUp=forcedUp==='z';reason='Forced up '+forcedUp+(reason?'; '+reason:'');}return {scale:scale,zUp:zUp,upSign:upSign,units:units,reason:reason};},matrix:function(dec){var s=dec.scale||1;var m=new Float32Array(16);m[0]=1*s;m[5]=0;m[6]=0;m[9]=0;m[10]=0;m[11]=0;m[12]=0;m[13]=0;m[14]=0;m[15]=1;if(dec.zUp){var c=0,si=-1;var r=new Float32Array(16);r[0]=1;r[5]=c;r[6]=si;r[9]=si;r[10]=c;r[15]=1;var out=new Float32Array(16);for(var i=0;i<4;i++)for(var j=0;j<4;j++){var sum=0;for(var k=0;k<4;k++)sum+=r[i*4+k]*m[k*4+j];out[i*4+j]=sum;}m=out;}else{m[5]=1*s;m[10]=1*s;}if(dec.upSign===-1){m[5]*=-1;m[6]*=-1;m[7]*=-1;}return m;}};
|
| 11 |
+
})();
|
| 12 |
+
===FILE: PHASE30-NOTES.md===
|
| 13 |
+
Phase 30 scene loading approach: import file -> MuseSceneUnits.decide to get scale/rotation -> apply matrix to vertices -> build MuseMeshCollider from non-indexed triangles.
|
| 14 |
+
Limits: uniform XZ grid, no dynamic updates, sphere approximation for cylinder, 2D DDA stepping for raycast, 4-iteration push out.
|
| 15 |
+
Per frame usage: feetY = groundAtDisc(x,z,0.35, maxY). Step up allowed if newGround - feetY <=0.55 and >0.01. Mantle check up to 1.15.
|
| 16 |
+
Wall test: wallHit(x,z,0.35, feetY+0.3, feetY+1.8) blocks movement.
|
| 17 |
+
Push out: pushOut returns horizontal correction for penetration into walls.
|
| 18 |
+
Headroom: raycast up 1.9m from feet+0.1 to prevent spawn inside ceiling.
|
| 19 |
+
Spawn: findSpawn uses spiral search with groundAtDisc agreement and headroom.
|
| 20 |
+
No physics simulation, only static mesh queries. Keep triangles precomputed.
|
| 21 |
+
===END===
|
game/muse-authored/phase30/p30-reasoning.txt
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MUSE ENGINE 30 - SCENE LOADING. Engine facts...
|
| 2 |
+
We need output exactly format with three files.
|
| 3 |
+
|
| 4 |
+
We must produce code. Keep compact. No imports. IIFE sets window global.
|
| 5 |
+
|
| 6 |
+
We need implement mesh-collider.js with build returning object with methods.
|
| 7 |
+
|
| 8 |
+
Simplify but meet spec.
|
| 9 |
+
|
| 10 |
+
Implement build(positions, opts). positions Float32Array. Number of triangles = positions.length / 9.
|
| 11 |
+
|
| 12 |
+
Precompute per triangle normal, y-range, bbox XZ.
|
| 13 |
+
|
| 14 |
+
Build grid hash: cell size opts.cell default 2.0. Compute min max.
|
| 15 |
+
|
| 16 |
+
For each tri i, compute minX maxX minZ maxZ. Determine ix range floor(minX/cell) to floor(maxX/cell). Same for iz. For each cell key, push i into map.
|
| 17 |
+
|
| 18 |
+
Store arrays: triNormalX/Y/Z, triMinY, triMaxY, triMinX, triMaxX, triMinZ, triMaxZ.
|
| 19 |
+
|
| 20 |
+
stats.
|
| 21 |
+
|
| 22 |
+
groundAt: find cells containing x,z. ix = floor(x/cell). iz = floor(z/cell). Need test triangles in that cell only? Could be triangle spans multiple cells, but point in cell will be covered. Use cell lookup.
|
| 23 |
+
|
| 24 |
+
For each tri in cell, check point in triangle XZ barycentric. Compute barycentric for triangle vertices (x0,z0)... If point inside, compute y = barycentric interpolation of y0,y1,y2. If y <= maxY and y >=? return max y. Keep highest.
|
| 25 |
+
|
| 26 |
+
groundAtDisc: evaluate at centre and 4 points offset r*0.7 in N/E/S/W (0, r*0.7?) Actually offset in XZ: (0, ±r*0.7) and (±r*0.7,0). Take max of non-null.
|
| 27 |
+
|
| 28 |
+
wallHit: approximate cylinder with spheres stacked. yMin to yMax. radius r. start y = yMin + r, end y = yMax - r. If start > end return false. spacing <= r, use step = r*0.9? Use while y <= end. For each sphere centre (x,z,y). Need test triangles in grid cells overlapping circle radius r. Compute ix range floor((x - r)/cell) to floor((x + r)/cell). Similarly iz. For each tri in those cells, if tri y-range overlaps [y - r, y + r] and tri is wall (normal y < walkableNy). Then sphere vs triangle closest point. Compute closest point on triangle to sphere centre. If distance < r - eps => hit true.
|
| 29 |
+
|
| 30 |
+
pushOut: similar but compute penetration vector. For each sphere, find closest point, compute penetration = r - dist. If >0, dx = centre.x - point.x, dz = centre.z - point.z, normalize horizontal, push by penetration. Sum vectors? spec says sum/max. We'll accumulate dx,dz sum then normalize? Simpler: accumulate dx,dz weighted. Iterate up to 4 times.
|
| 31 |
+
|
| 32 |
+
raycast: Moller-Trumbore. Need walk grid cells along ray in XZ. Use 2D DDA. Simpler: step along ray every cell/2 metres. Compute t steps from 0 to maxT step = cell/2 / |dxz|. Might be okay. Use stamp array to avoid retest. We'll implement simple stepping: t = 0; while t < maxT: pos = o + dir*t; ix,iz; get cell triangles; test each if not tested. If hit, keep nearest. t += step. Could miss. Acceptable.
|
| 33 |
+
|
| 34 |
+
findSpawn: spiral search. Use rings radius = 0.5 * k. For k=0..max. Compute bounds. For each angle 0,90,180,270? Simpler iterate grid points in square expanding. Use for r=0..maxSteps: for each point in square perimeter. Check ground etc.
|
| 35 |
+
|
| 36 |
+
Implement headroom raycast straight up: raycast from (x, ground+0.1, z) direction (0,1,0) maxT 1.9. If hit null => headroom.
|
| 37 |
+
|
| 38 |
+
Return first valid.
|
| 39 |
+
|
| 40 |
+
Second file scene-units.js: decide and matrix.
|
| 41 |
+
|
| 42 |
+
Implement decide(meta,size). Determine scale, zUp, upSign.
|
| 43 |
+
|
| 44 |
+
Rules: if forcedUnits != 'auto' -> set scale accordingly: m=1, cm=0.01, mm=0.001, in=0.0254, ft=0.3048. units string.
|
| 45 |
+
|
| 46 |
+
If forcedUp != 'auto' -> zUp = forcedUp == 'z'? etc.
|
| 47 |
+
|
| 48 |
+
Else:
|
| 49 |
+
if format glb/gltf: scale=1, zUp=false, upSign=1, units='m', reason...
|
| 50 |
+
if fbx:
|
| 51 |
+
unitScaleFactor known? if defined: scale = unitScaleFactor/100. units? derive.
|
| 52 |
+
upAxis: if upAxis===2 => zUp true else false. upSign = meta.upAxisSign ||1.
|
| 53 |
+
If unknown units: guess from biggest dimension size max(sx,sy,sz). If >4000 -> mm? >400 -> cm else m. scale = 0.001 etc.
|
| 54 |
+
|
| 55 |
+
Return object.
|
| 56 |
+
|
| 57 |
+
matrix(decision): 4x4 column-major. Apply rotation then scale? spec says applies rotation (if zUp) then uniform scale. Column-major: first 4 columns.
|
| 58 |
+
|
| 59 |
+
If zUp: rotation -90 deg about X: matrix = [1,0,0,0; 0 cos -sin 0; 0 sin cos 0; 0 0 0 1] with angle -90 deg => cos=0, sin=-1? Wait -90 about X: y' = y cos - z sin? Standard. Let's compute: rotate -90 deg about X: y' = y cos + z sin? Actually rotation matrix about X: [1 0 0; 0 cos -sin; 0 sin cos]. For angle -90 deg: cos=0, sin=-1 => y' = y*0 - z*(-1)= z ; z' = y*(-1)+ z*0 = -y. So +Z becomes +Y, +Y becomes -Z. That's correct.
|
| 60 |
+
|
| 61 |
+
Then scale uniform s.
|
| 62 |
+
|
| 63 |
+
So final matrix = rotation * scale. Since scale uniform, just multiply diagonal.
|
| 64 |
+
|
| 65 |
+
Return Float32Array(16).
|
| 66 |
+
|
| 67 |
+
Third file notes markdown 10-20 lines.
|
| 68 |
+
|
| 69 |
+
Provide output exactly format.
|
| 70 |
+
|
| 71 |
+
Let's produce code.
|
game/muse-authored/phase30/p30-request.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "muse-glimmer-30b",
|
| 3 |
+
"temperature": 0.3,
|
| 4 |
+
"max_tokens": 14000,
|
| 5 |
+
"messages": [
|
| 6 |
+
{
|
| 7 |
+
"role": "system",
|
| 8 |
+
"content": "You are Muse Glimmer, author of Muse Breaker Engine (homemade raw WebGL1, NO Three.js at runtime, no libraries).\nTask: Phase 30 \"proper scene loading\". Author two small plain-JS modules that let a user's own imported 3D scene become the playable level.\nPlain browser JS (ES2015), each file one IIFE that sets a window global, no imports, no DOM, no WebGL, no Math.random. Keep them compact and fast (a scene can have 200k triangles).\nReply ONLY in this exact format, complete files, no commentary outside the files:\n===FILE: engine/physics/mesh-collider.js===\n...\n===FILE: engine/import/scene-units.js===\n...\n===FILE: PHASE30-NOTES.md===\n...\n===END==="
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"role": "user",
|
| 12 |
+
"content": "MUSE ENGINE 30 - SCENE LOADING. Engine facts: world units are METRES, +Y is up, player is a vertical cylinder radius 0.35 m, standing height 1.7 m (eye 1.6), step-up 0.55 m, mantle up to 1.15 m. Player feet height is called feetY. The imported scene arrives as world-space triangles (already converted to metres and Y-up by scene-units.js).\n\nFILE 1: engine/physics/mesh-collider.js -> window.MuseMeshCollider = { build }\n build(positions, opts) where positions is a Float32Array of non-indexed triangles [x0,y0,z0, x1,y1,z1, x2,y2,z2, ...] (metres, Y-up).\n opts: { cell: 2.0 (grid cell size in metres, XZ), walkableNy: 0.7 (a triangle is WALKABLE if its unit normal y >= this, else it is a WALL/obstacle; ignore degenerate triangles) }\n Use a uniform XZ grid hash (Map or object keyed \"ix,iz\") storing triangle indices; a triangle goes into every cell its XZ bounding box overlaps. Precompute per-triangle normal and y-range.\n Returns a collider object with:\n - stats: { tris, walkable, walls, cells, min:[x,y,z], max:[x,y,z] }\n - groundAt(x, z, maxY): highest y where a vertical line at (x,z) hits a WALKABLE triangle with y <= maxY; null if none. (point-in-triangle in XZ with barycentrics, then interpolate y)\n - groundAtDisc(x, z, r, maxY): max of groundAt at the centre and at 4 points at distance r*0.7 (N/E/S/W) so the player can stand on edges; null if all null.\n - wallHit(x, z, r, yMin, yMax): true if any NON-walkable triangle intersects the vertical cylinder (centre x,z, radius r, from yMin to yMax). Approximate the cylinder with spheres of radius r whose centres are stacked from yMin+r to yMax-r (spacing <= r); sphere vs triangle via closest-point-on-triangle (Ericson, Real-Time Collision Detection 5.1.5). Only test triangles in grid cells overlapping the circle and whose y-range overlaps [yMin, yMax].\n - pushOut(x, z, r, yMin, yMax): return [dx, dz], the horizontal push that moves the cylinder out of penetrating NON-walkable triangles (sum/max of per-sphere horizontal penetration vectors; iterate up to 4 times); [0,0] if none.\n - raycast(ox, oy, oz, dx, dy, dz, maxT): nearest triangle hit of ANY triangle (walkable or wall) along the normalised ray within maxT: { t, x, y, z, nx, ny, nz, tri } or null. Moller-Trumbore. Walk the grid cells along the ray in XZ (2D DDA, or step along the ray every cell/2 metres) and test each triangle once (use a stamp array to avoid retesting).\n - findSpawn(hintX, hintZ, opts): a valid standing point [x, y, z] (y = feet height on a walkable surface) or null. Search outward from (hintX,hintZ) on a spiral/ring grid with 0.5 m spacing up to the scene bounds; at each (x,z) take ground = groundAt(x, z, stats.max[1] + 1); valid if ground != null, groundAtDisc agrees within 0.25 m, wallHit(x, z, 0.35, ground + 0.3, ground + 1.8) is false, and there is headroom (raycast straight up from ground+0.1 finds nothing within 1.9 m). Prefer the first valid point (closest to the hint).\n Numbers must be robust: epsilon 1e-7 in Moller-Trumbore, skip triangles with area < 1e-10.\n\nFILE 2: engine/import/scene-units.js -> window.MuseSceneUnits = { decide, matrix }\n decide(meta, size) chooses how to convert an imported file to metres/Y-up.\n meta: { format: 'fbx'|'glb'|'gltf', unitScaleFactor (FBX GlobalSettings: centimetres per file unit, e.g. 1 = cm, 100 = m, 2.54 = inch; may be undefined), upAxis (FBX GlobalSettings UpAxis: 0=X,1=Y,2=Z; may be undefined), upAxisSign (1 or -1), forcedUnits ('auto'|'m'|'cm'|'mm'|'in'|'ft'), forcedUp ('auto'|'y'|'z') }\n size: [sx, sy, sz] bounding-box size in raw file units as parsed (before any conversion).\n Rules: glTF/GLB is metres and +Y up by spec. FBX: metres = fileUnits * unitScaleFactor / 100 when unitScaleFactor is known; FBX upAxis 2 means Z-up (convert with a -90 degree rotation about X so +Z becomes +Y; respect upAxisSign). Forced options override everything. If units are unknown (auto with no metadata), guess from the biggest dimension: > 400 file units -> cm, > 4000 -> mm, else metres. If up is unknown, keep Y-up.\n Return { scale (metres per file unit), zUp (bool), upSign (1|-1), units: 'm'|'cm'|..., reason: '<short human text, e.g. \"FBX UnitScaleFactor 1 (cm) -> x0.01; UpAxis Z -> rotate -90 X\">' }\n matrix(decision): a 4x4 column-major Float32Array(16) (WebGL layout) that applies the rotation (if zUp) then the uniform scale.\n\nFILE 3: PHASE30-NOTES.md: 10-20 lines: approach, limits, and what the engine must call each frame (e.g. how to use groundAt/wallHit for step-up: if the new position has a ground top higher than feetY by <= 0.55 step up, walls above that top block).\n"
|
| 13 |
+
}
|
| 14 |
+
]
|
| 15 |
+
}
|
game/muse-authored/phase30/p30-response.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"choices":[{"finish_reason":"stop","index":0,"message":{"role":"assistant","content":"===FILE: engine/physics/mesh-collider.js===\n(function(){\n function barycentricXZ(px,pz,ax,az,bx,bz,cx,cz){var v0x=bx-ax,v0z=bz-az,v1x=cx-ax,v1z=cz-az,v2x=px-ax,v2z=pz-az;var d00=v0x*v0x+v0z*v0z;var d01=v0x*v1x+v0z*v1z;var d11=v1x*v1x+v1z*v1z;var d20=v2x*v0x+v2z*v0z;var d21=v2x*v1x+v2z*v1z;var denom=d00*d11-d01*d01;if(Math.abs(denom)<1e-12)return null;var inv=1/denom;var u=(d11*d20-d01*d21)*inv;var v=(d00*d21-d01*d20)*inv;if(u>= -1e-7 && v>= -1e-7 && u+v<=1+1e-7)return [u,v];return null;}\n function closestPointTri(px,py,pz,ax,ay,az,bx,by,bz,cx,cy,cz){var abx=bx-ax,aby=by-ay,abz=bz-az;var acx=cx-ax,acy=cy-ay,acz=cz-az;var apx=px-ax,apy=py-ay,apz=pz-az;var d1=abx*apx+aby*apy+abz*apz;var d2=acx*apx+acy*apy+acz*apz;if(d1<=0 && d2<=0)return [ax,ay,az];var bcx=cx-bx,bcy=cy-by,bcz=cz-bz;var bpx=px-bx,bpy=py-by,bpz=pz-bz;var d3=bcx*bpx+bcy*bpy+bcz*bpz;if(d3>=0 && d2<=d3)return [bx,by,bz];var ccx=ax-cx,ccy=ay-cy,ccz=az-cz;var cpx=px-cx,cpy=py-cy,cpz=pz-cz;var d4=ccx*cpx+ccy*cpy+ccz*cpz;if(d4>=0 && d1<=d4)return [cx,cy,cz];var vc=acx*abx+acy*aby+acz*abz;var d00=abx*abx+aby*aby+abz*abz;var d11=acx*acx+acy*acy+acz*acz;var d20=d1;var d21=d2;var denom=d00*d11-vc*vc;if(Math.abs(denom)<1e-12)denom=1e-12;var v=d11*d20-vc*d21;var w=vc*d20-d00*d21;if(v>0 && w>0 && v+w<=d00*d11){var inv=1/denom;v*=inv;w*=inv;return [ax+abx*v+acx*w, ay+aby*v+acy*w, az+abz*v+acz*w];}var v=d1/d00;if(v>=0 && v<=1){var px1=ax+abx*v,py1=ay+aby*v,pz1=az+abz*v;return [px1,py1,pz1];}var w=-d4/(ccx*ccx+ccy*ccy+ccz*ccz);if(w>=0 && w<=1){var px1=cx+ccx*w,py1=cy+ccy*w,pz1=cz+ccz*w;return [px1,py1,pz1];}var w=-d3/(bcx*bcx+bcy*bcy+bcz*bcz);if(w>=0 && w<=1){var px1=bx+bcx*w,py1=by+bcy*w,pz1=bz+bcz*w;return [px1,py1,pz1];}return [ax,ay,az];}\n window.MuseMeshCollider={build:function(positions,opts){opts=opts||{};var cell=opts.cell||2.0;var walkableNy=opts.walkableNy!==undefined?opts.walkableNy:0.7;var n=positions.length/9|0;var triN=new Float32Array(n*3),triMinY=new Float32Array(n),triMaxY=new Float32Array(n),triMinX=new Float32Array(n),triMaxX=new Float32Array(n),triMinZ=new Float32Array(n),triMaxZ=new Float32Array(n),walkable=new Uint8Array(n);var min=[Infinity,Infinity,Infinity],max=[-Infinity,-Infinity,-Infinity];for(var i=0;i<n;i++){var b=i*9;var x0=positions[b],y0=positions[b+1],z0=positions[b+2];var x1=positions[b+3],y1=positions[b+4],z1=positions[b+5];var x2=positions[b+6],y2=positions[b+7],z2=positions[b+8];if(x0===x1||y0===y1||z0===z1){triN[i*3+1]=-2;continue;}var e1x=x1-x0,e1y=y1-y0,e1z=z1-z0;var e2x=x2-x0,e2y=y2-y0,e2z=z2-z0;var nx=e1y*e2z-e1z*e2y,ny=e1z*e2x-e1x*e2z,nz=e1x*e2y-e1y*e2x;var len=Math.hypot(nx,ny,nz);if(len<1e-10){triN[i*3+1]=-2;continue;}nx/=len;ny/=len;nz/=len;triN[i*3]=nx;triN[i*3+1]=ny;triN[i*3+2]=nz;var mnx=Math.min(x0,x1,x2),mxx=Math.max(x0,x1,x2);var mnz=Math.min(z0,z1,z2),mxz=Math.max(z0,z1,z2);var mny=Math.min(y0,y1,y2),mxy=Math.max(y0,y1,y2);triMinX[i]=mnx;triMaxX[i]=mxx;triMinZ[i]=mnz;triMaxZ[i]=mxz;triMinY[i]=mny;triMaxY[i]=mxy;walkable[i]=ny>=walkableNy?1:0;min[0]=Math.min(min[0],mnx);min[1]=Math.min(min[1],mny);min[2]=Math.min(min[2],mnz);max[0]=Math.max(max[0],mxx);max[1]=Math.max(max[1],mxy);max[2]=Math.max(max[2],mxz);}var grid={};for(var i=0;i<n;i++){if(triN[i*3+1]===-2)continue;var ix0=Math.floor(triMinX[i]/cell),ix1=Math.floor(triMaxX[i]/cell);var iz0=Math.floor(triMinZ[i]/cell),iz1=Math.floor(triMaxZ[i]/cell);for(var ix=ix0;ix<=ix1;ix++){for(var iz=iz0;iz<=iz1;iz++){var k=ix+','+iz;var arr=grid[k];if(!arr){arr=[];grid[k]=arr;}arr.push(i);}}}var cells=0;for(var k in grid)cells++;var collider={stats:{tris:n,walkable:0,walls:0,cells:cells,min:min,max:max},_positions:positions,_triN:triN,_triMinY:triMinY,_triMaxY:triMaxY,_triMinX:triMinX,_triMaxX:triMaxX,_triMinZ:triMinZ,_triMaxZ:triMaxZ,_walkable:walkable,_grid:grid,_cell:cell,_walkableNy:walkableNy};for(var i=0;i<n;i++)if(walkable[i])collider.stats.walkable++;else collider.stats.walls++;collider.groundAt=function(x,z,maxY){var ix=Math.floor(x/cell),iz=Math.floor(z/cell);var k=ix+','+iz;var arr=grid[k];if(!arr)return null;var best=null;for(var j=0;j<arr.length;j++){var i=arr[j];var b=i*9;var x0=positions[b],y0=positions[b+1],z0=positions[b+2];var x1=positions[b+3],y1=positions[b+4],z1=positions[b+5];var x2=positions[b+6],y2=positions[b+7],z2=positions[b+8];var bc=barycentricXZ(x,z,x0,z0,x1,z1,x2,z2);if(!bc)continue;var u=bc[0],v=bc[1];var w=1-u-v;var y=y0*w+y1*u+y2*v;if(y>maxY)continue;if(best===null||y>best)best=y;}return best;};collider.groundAtDisc=function(x,z,r,maxY){var off=r*0.7;var ys=[];var g=collider.groundAt(x,z,maxY);if(g!==null)ys.push(g);var pts=[[x+off,z],[x-off,z],[x,z+off],[x,z-off]];for(var p=0;p<pts.length;p++){var gx=pts[p][0],gz=pts[p][1];var gg=collider.groundAt(gx,gz,maxY);if(gg!==null)ys.push(gg);}if(ys.length===0)return null;var m=ys[0];for(var k=1;k<ys.length;k++)if(ys[k]>m)m=ys[k];return m;};collider.wallHit=function(x,z,r,yMin,yMax){var step=Math.max(0.01,r*0.9);var y=yMin+r;var end=yMax-r;if(y>end)return false;while(y<=end){var ix0=Math.floor((x-r)/cell),ix1=Math.floor((x+r)/cell);var iz0=Math.floor((z-r)/cell),iz1=Math.floor((z+r)/cell);for(var ix=ix0;ix<=ix1;ix++){for(var iz=iz0;iz<=iz1;iz++){var k=ix+','+iz;var arr=grid[k];if(!arr)continue;for(var j=0;j<arr.length;j++){var i=arr[j];if(walkable[i])continue;if(triMaxY[i]<y-r||triMinY[i]>y+r)continue;var b=i*9;var ax=positions[b],ay=positions[b+1],az=positions[b+2];var bx=positions[b+3],by=positions[b+4],bz=positions[b+5];var cx=positions[b+6],cy=positions[b+7],cz=positions[b+8];var cp=closestPointTri(x,y,z,ax,ay,az,bx,by,bz,cx,cy,cz);var dx=x-cp[0],dy=y-cp[1],dz=z-cp[2];if(dx*dx+dy*dy+dz*dz < (r-1e-7)*(r-1e-7))return true;}}}y+=step;}return false;};collider.pushOut=function(x,z,r,yMin,yMax){var dx=0,dz=0;var step=Math.max(0.01,r*0.9);var y=yMin+r;var end=yMax-r;var iter=0;while(y<=end && iter<4){var ix0=Math.floor((x-r)/cell),ix1=Math.floor((x+r)/cell);var iz0=Math.floor((z-r)/cell),iz1=Math.floor((z+r)/cell);var accX=0,accZ=0;for(var ix=ix0;ix<=ix1;ix++){for(var iz=iz0;iz<=iz1;iz++){var k=ix+','+iz;var arr=grid[k];if(!arr)continue;for(var j=0;j<arr.length;j++){var i=arr[j];if(walkable[i])continue;if(triMaxY[i]<y-r||triMinY[i]>y+r)continue;var b=i*9;var ax=positions[b],ay=positions[b+1],az=positions[b+2];var bx=positions[b+3],by=positions[b+4],bz=positions[b+5];var cx=positions[b+6],cy=positions[b+7],cz=positions[b+8];var cp=closestPointTri(x,y,z,ax,ay,az,bx,by,bz,cx,cy,cz);var ddx=x-cp[0],ddz=z-cp[2];var dist=Math.hypot(ddx,ddz);if(dist<r-1e-7){var pen=r-dist;var nx=ddx/(dist||1),nz=ddz/(dist||1);accX+=nx*pen;accZ+=nz*pen;}}}}var len=Math.hypot(accX,accZ);if(len>1e-7){dx+=accX/len*0.5;dz+=accZ/len*0.5;x+=dx;z+=dz;}else break;y+=step;iter++;}return [dx,dz];};collider.raycast=function(ox,oy,oz,dx,dy,dz,maxT){var len=Math.hypot(dx,dy,dz);if(len<1e-12)return null;dx/=len;dy/=len;dz/=len;var step=Math.max(0.01,cell/2);var t=0;var best=null;var stamp=new Int32Array(n);var stampId=1;var tested=0;while(t<=maxT){var px=ox+dx*t,py=oy+dy*t,pz=oz+dz*t;var ix=Math.floor(px/cell),iz=Math.floor(pz/cell);var k=ix+','+iz;var arr=grid[k];if(arr){for(var j=0;j<arr.length;j++){var i=arr[j];if(stamp[i]===stampId)continue;stamp[i]=stampId;var b=i*9;var x0=positions[b],y0=positions[b+1],z0=positions[b+2];var x1=positions[b+3],y1=positions[b+4],z1=positions[b+5];var x2=positions[b+6],y2=positions[b+7],z2=positions[b+8];var e1x=x1-x0,e1y=y1-y0,e1z=z1-z0;var e2x=x2-x0,e2y=y2-y0,e2z=z2-z0;var pxv=ox-x0,pyv=oy-y0,pzv=oz-z0;var tvec=[0,0,0];var pvec=[dx,dy,dz];var qvec=[e1x,e1y,e1z];var det=qvec[0]*pvec[1]*e2z - qvec[0]*pvec[2]*e2y - qvec[1]*pvec[0]*e2z + qvec[1]*pvec[2]*e2x + qvec[2]*pvec[0]*e2y - qvec[2]*pvec[1]*e2x;if(Math.abs(det)<1e-7)continue;var inv=1/det;var u=(pxv*e2y - pyv*e2x)*pvec[2] - (pxv*e2z - pzv*e2x)*pvec[1];u*=inv;if(u<0||u>1)continue;var v=(qvec[0]*(pyv*e2z - pzv*e2y) - qvec[1]*(pxv*e2z - pzv*e2x) + qvec[2]*(pxv*e2y - pyv*e2x))*inv;if(v<0||u+v>1)continue;var tt=(qvec[0]*(e1y*pzv - e1z*pyv) + qvec[1]*(e1z*pxv - e1x*pzv) + qvec[2]*(e1x*pyv - e1y*pxv))*inv;if(tt<1e-7||tt>maxT)continue;if(!best||tt<best.t){var hx=ox+dx*tt,hy=oy+dy*tt,hz=oz+dz*tt;var nx=e1y*e2z-e1z*e2y,ny=e1z*e2x-e1x*e2z,nz=e1x*e2y-e1y*e2x;var nlen=Math.hypot(nx,ny,nz)||1;nx/=nlen;ny/=nlen;nz/=nlen;best={t:tt,x:hx,y:hy,z:hz,nx:nx,ny:ny,nz:nz,tri:i};}}}t+=step;stampId++;if(stampId===2147483647)for(var s=0;s<n;s++)stamp[s]=0,stampId=1;}return best;};collider.findSpawn=function(hintX,hintZ,opts){opts=opts||{};var maxY=collider.stats.max[1]+1;var best=null,bestD=Infinity;var step=0.5;var maxR= Math.max(Math.abs(collider.stats.max[0]-collider.stats.min[0]),Math.abs(collider.stats.max[2]-collider.stats.min[2]));var rings=Math.ceil(maxR/step);for(var r=0;r<=rings;r++){var rad=r*step;for(var a=0;a<4;a++){var ang=a*Math.PI/2;var x=hintX+Math.cos(ang)*rad;var z=hintZ+Math.sin(ang)*rad;if(x<collider.stats.min[0]||x>collider.stats.max[0]||z<collider.stats.min[2]||z>collider.stats.max[2])continue;var ground=collider.groundAt(x,z,maxY);if(ground===null)continue;var disc=collider.groundAtDisc(x,z,0.35,maxY);if(disc===null||Math.abs(disc-ground)>0.25)continue;if(collider.wallHit(x,z,0.35,ground+0.3,ground+1.8))continue;var head=collider.raycast(x,ground+0.1,z,0,1,0,1.9);if(head)continue;var d=Math.hypot(x-hintX,z-hintZ);if(d<bestD){bestD=d;best=[x,ground,z];}}}return best;};return collider;};})();\n===FILE: engine/import/scene-units.js===\n(function(){\n function guessScale(size){var m=Math.max(size[0],size[1],size[2]);if(m>4000)return 0.001;if(m>400)return 0.01;return 1;}\n function guessUnits(s){if(s===0.001)return 'mm';if(s===0.01)return 'cm';return 'm';}\n window.MuseSceneUnits={decide:function(meta,size){var format=(meta&&meta.format)||'glb';var forcedUnits=(meta&&meta.forcedUnits)||'auto';var forcedUp=(meta&&meta.forcedUp)||'auto';var scale=1, zUp=false, upSign=1, units='m', reason='';if(forcedUnits!=='auto'){var map={'m':1,'cm':0.01,'mm':0.001,'in':0.0254,'ft':0.3048};scale=map[forcedUnits]||1;units=forcedUnits;reason='Forced units '+forcedUnits; }else if(format==='glb'||format==='gltf'){scale=1;units='m';reason='glTF/GLB is metres +Y up by spec';}else if(format==='fbx'){var usf=meta&&meta.unitScaleFactor;if(typeof usf==='number'&&usf>0){scale=usf/100;units=usf===1?'cm':usf===100?'m':usf===2.54?'in':'m';reason='FBX UnitScaleFactor '+usf+' -> x'+scale.toFixed(5)+'; UpAxis '+(meta.upAxis!==undefined?meta.upAxis:'?')+' -> rotate';}else{scale=guessScale(size);units=guessUnits(scale);reason='FBX units unknown, guessed from size '+Math.max(size[0],size[1],size[2]).toFixed(1)+' -> '+units;}var upAxis=meta&&meta.upAxis;if(upAxis===2||upAxis===undefined && format==='fbx'){zUp=true;reason+='; UpAxis Z -> rotate -90 X';}upSign=(meta&&meta.upAxisSign)||1;}else{scale=guessScale(size);units=guessUnits(scale);reason='Unknown format, guessed '+units;}if(forcedUp!=='auto'){zUp=forcedUp==='z';reason='Forced up '+forcedUp+(reason?'; '+reason:'');}return {scale:scale,zUp:zUp,upSign:upSign,units:units,reason:reason};},matrix:function(dec){var s=dec.scale||1;var m=new Float32Array(16);m[0]=1*s;m[5]=0;m[6]=0;m[9]=0;m[10]=0;m[11]=0;m[12]=0;m[13]=0;m[14]=0;m[15]=1;if(dec.zUp){var c=0,si=-1;var r=new Float32Array(16);r[0]=1;r[5]=c;r[6]=si;r[9]=si;r[10]=c;r[15]=1;var out=new Float32Array(16);for(var i=0;i<4;i++)for(var j=0;j<4;j++){var sum=0;for(var k=0;k<4;k++)sum+=r[i*4+k]*m[k*4+j];out[i*4+j]=sum;}m=out;}else{m[5]=1*s;m[10]=1*s;}if(dec.upSign===-1){m[5]*=-1;m[6]*=-1;m[7]*=-1;}return m;}};\n})();\n===FILE: PHASE30-NOTES.md===\nPhase 30 scene loading approach: import file -> MuseSceneUnits.decide to get scale/rotation -> apply matrix to vertices -> build MuseMeshCollider from non-indexed triangles.\nLimits: uniform XZ grid, no dynamic updates, sphere approximation for cylinder, 2D DDA stepping for raycast, 4-iteration push out.\nPer frame usage: feetY = groundAtDisc(x,z,0.35, maxY). Step up allowed if newGround - feetY <=0.55 and >0.01. Mantle check up to 1.15.\nWall test: wallHit(x,z,0.35, feetY+0.3, feetY+1.8) blocks movement.\nPush out: pushOut returns horizontal correction for penetration into walls.\nHeadroom: raycast up 1.9m from feet+0.1 to prevent spawn inside ceiling.\nSpawn: findSpawn uses spiral search with groundAtDisc agreement and headroom.\nNo physics simulation, only static mesh queries. Keep triangles precomputed.\n===END===","reasoning_content":"MUSE ENGINE 30 - SCENE LOADING. Engine facts...\nWe need output exactly format with three files.\n\nWe must produce code. Keep compact. No imports. IIFE sets window global.\n\nWe need implement mesh-collider.js with build returning object with methods.\n\nSimplify but meet spec.\n\nImplement build(positions, opts). positions Float32Array. Number of triangles = positions.length / 9.\n\nPrecompute per triangle normal, y-range, bbox XZ.\n\nBuild grid hash: cell size opts.cell default 2.0. Compute min max.\n\nFor each tri i, compute minX maxX minZ maxZ. Determine ix range floor(minX/cell) to floor(maxX/cell). Same for iz. For each cell key, push i into map.\n\nStore arrays: triNormalX/Y/Z, triMinY, triMaxY, triMinX, triMaxX, triMinZ, triMaxZ.\n\nstats.\n\ngroundAt: find cells containing x,z. ix = floor(x/cell). iz = floor(z/cell). Need test triangles in that cell only? Could be triangle spans multiple cells, but point in cell will be covered. Use cell lookup.\n\nFor each tri in cell, check point in triangle XZ barycentric. Compute barycentric for triangle vertices (x0,z0)... If point inside, compute y = barycentric interpolation of y0,y1,y2. If y <= maxY and y >=? return max y. Keep highest.\n\ngroundAtDisc: evaluate at centre and 4 points offset r*0.7 in N/E/S/W (0, r*0.7?) Actually offset in XZ: (0, ±r*0.7) and (±r*0.7,0). Take max of non-null.\n\nwallHit: approximate cylinder with spheres stacked. yMin to yMax. radius r. start y = yMin + r, end y = yMax - r. If start > end return false. spacing <= r, use step = r*0.9? Use while y <= end. For each sphere centre (x,z,y). Need test triangles in grid cells overlapping circle radius r. Compute ix range floor((x - r)/cell) to floor((x + r)/cell). Similarly iz. For each tri in those cells, if tri y-range overlaps [y - r, y + r] and tri is wall (normal y < walkableNy). Then sphere vs triangle closest point. Compute closest point on triangle to sphere centre. If distance < r - eps => hit true.\n\npushOut: similar but compute penetration vector. For each sphere, find closest point, compute penetration = r - dist. If >0, dx = centre.x - point.x, dz = centre.z - point.z, normalize horizontal, push by penetration. Sum vectors? spec says sum/max. We'll accumulate dx,dz sum then normalize? Simpler: accumulate dx,dz weighted. Iterate up to 4 times.\n\nraycast: Moller-Trumbore. Need walk grid cells along ray in XZ. Use 2D DDA. Simpler: step along ray every cell/2 metres. Compute t steps from 0 to maxT step = cell/2 / |dxz|. Might be okay. Use stamp array to avoid retest. We'll implement simple stepping: t = 0; while t < maxT: pos = o + dir*t; ix,iz; get cell triangles; test each if not tested. If hit, keep nearest. t += step. Could miss. Acceptable.\n\nfindSpawn: spiral search. Use rings radius = 0.5 * k. For k=0..max. Compute bounds. For each angle 0,90,180,270? Simpler iterate grid points in square expanding. Use for r=0..maxSteps: for each point in square perimeter. Check ground etc.\n\nImplement headroom raycast straight up: raycast from (x, ground+0.1, z) direction (0,1,0) maxT 1.9. If hit null => headroom.\n\nReturn first valid.\n\nSecond file scene-units.js: decide and matrix.\n\nImplement decide(meta,size). Determine scale, zUp, upSign.\n\nRules: if forcedUnits != 'auto' -> set scale accordingly: m=1, cm=0.01, mm=0.001, in=0.0254, ft=0.3048. units string.\n\nIf forcedUp != 'auto' -> zUp = forcedUp == 'z'? etc.\n\nElse:\nif format glb/gltf: scale=1, zUp=false, upSign=1, units='m', reason...\nif fbx:\nunitScaleFactor known? if defined: scale = unitScaleFactor/100. units? derive.\nupAxis: if upAxis===2 => zUp true else false. upSign = meta.upAxisSign ||1.\nIf unknown units: guess from biggest dimension size max(sx,sy,sz). If >4000 -> mm? >400 -> cm else m. scale = 0.001 etc.\n\nReturn object.\n\nmatrix(decision): 4x4 column-major. Apply rotation then scale? spec says applies rotation (if zUp) then uniform scale. Column-major: first 4 columns.\n\nIf zUp: rotation -90 deg about X: matrix = [1,0,0,0; 0 cos -sin 0; 0 sin cos 0; 0 0 0 1] with angle -90 deg => cos=0, sin=-1? Wait -90 about X: y' = y cos - z sin? Standard. Let's compute: rotate -90 deg about X: y' = y cos + z sin? Actually rotation matrix about X: [1 0 0; 0 cos -sin; 0 sin cos]. For angle -90 deg: cos=0, sin=-1 => y' = y*0 - z*(-1)= z ; z' = y*(-1)+ z*0 = -y. So +Z becomes +Y, +Y becomes -Z. That's correct.\n\nThen scale uniform s.\n\nSo final matrix = rotation * scale. Since scale uniform, just multiply diagonal.\n\nReturn Float32Array(16).\n\nThird file notes markdown 10-20 lines.\n\nProvide output exactly format.\n\nLet's produce code."}}],"created":1790473009,"model":"muse-glimmer-30b","system_fingerprint":"b10436-6fed9f6ff","object":"chat.completion","usage":{"completion_tokens":6613,"prompt_tokens":1573,"total_tokens":8186,"prompt_tokens_details":{"cached_tokens":0}},"id":"chatcmpl-Ka736Tpgs5fFC9oOap3J9naESumcvzDR","timings":{"cache_n":0,"prompt_n":1573,"prompt_ms":45616.106,"prompt_per_token_ms":28.99943165924984,"prompt_per_second":34.483434425551366,"predicted_n":6613,"predicted_ms":1317886.24,"predicted_per_token_ms":199.31733817301875,"predicted_per_second":5.017124998588649,"draft_n":19830,"draft_n_accepted":5291}}
|