tfrere HF Staff Cursor commited on
Commit
5a6547c
Β·
1 Parent(s): d202596

feat: replace jukebox with a row of 3 arcade cabinets

Browse files

New arcade.glb (from the user's FBX, texture downscaled to 1024, plus a
500-tri wire stand-in for the hologram pass). Three clones line up
against the back wall facing the arena, staggered in the ceremony, with
one static collision box covering the row.

Co-authored-by: Cursor <cursoragent@cursor.com>

README.md CHANGED
@@ -102,8 +102,8 @@ UI intents in).
102
  - `app/src/game/game.js` fetches the MJCF (`robot_allcollisions.xml`, or
103
  `robot_allcollisions_rollers.xml` for the roller variant), strips the
104
  visual geoms, injects a floor, arena walls, a ball, a collision box for
105
- the jukebox prop and a STAND keyframe, and compiles it with the official
106
- `@mujoco/mujoco` WASM bindings.
107
  - Both variants share the exact same policy interface: 61D observation
108
  (gyro, projected gravity, 14 joint pos/vel, last action, 13D command)
109
  and 14 position-targets, matching `mjlab_microduck/scripts/infer_policy.py`.
 
102
  - `app/src/game/game.js` fetches the MJCF (`robot_allcollisions.xml`, or
103
  `robot_allcollisions_rollers.xml` for the roller variant), strips the
104
  visual geoms, injects a floor, arena walls, a ball, a collision box for
105
+ the arcade cabinet row and a STAND keyframe, and compiles it with the
106
+ official `@mujoco/mujoco` WASM bindings.
107
  - Both variants share the exact same policy interface: 61D observation
108
  (gyro, projected gravity, 14 joint pos/vel, last action, 13D command)
109
  and 14 position-targets, matching `mjlab_microduck/scripts/infer_policy.py`.
app/public/assets/props/{jukebox.glb β†’ arcade.glb} RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:c57bd4676e7f3889692b1f9afe9e799fc4dfe2c97405e53cd1e5ed9b30d83059
3
- size 1759760
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1343e6a6851f8f09f91d746c2e0ff030fbf2abeacc3d06b41906fe3200a7704e
3
+ size 498180
app/src/game/constants.js CHANGED
@@ -65,14 +65,16 @@ export const ARENA_WALL_T = 0.05;
65
  // column/row of cells exists; the lattice is shifted half a cell in the
66
  // shaders so the walls land exactly on section lines).
67
  export const GRID_SECTION = (2 * ARENA_HALF) / 5; // 0.6 m
68
- // Jukebox corner prop: shared between the visual (game.js places the GLB)
69
- // and the physics (buildPhysicsXml adds a matching static collision box).
70
- export const JUKEBOX_H = 0.42; // target height, m
71
- export const JUKEBOX_MARGIN = 0.24; // center distance from each wall inner face
72
- export const JUKEBOX_YAW = Math.PI / 4; // angled toward the arena center
73
- // Collision half-extents in MJCF axes (x, y, z-up). x/y measured from the
74
- // GLB footprint once normalized to JUKEBOX_H (0.216 x 0.261 m).
75
- export const JUKEBOX_HALF = [0.109, 0.131, JUKEBOX_H / 2];
 
 
76
 
77
  // Spawn: center of the middle section cell in the SECOND ROW FROM THE
78
  // BACK wall. The duck faces +X (identity freejoint quat, walks toward
 
65
  // column/row of cells exists; the lattice is shifted half a cell in the
66
  // shaders so the walls land exactly on section lines).
67
  export const GRID_SECTION = (2 * ARENA_HALF) / 5; // 0.6 m
68
+ // Arcade cabinet row: three cabinets side by side against the back (-X)
69
+ // wall, screens facing the arena. Shared between the visual (game.js
70
+ // places the GLB clones) and the physics (buildPhysicsXml adds ONE static
71
+ // collision box covering the whole row). Proportions measured from the
72
+ // GLB's natural size (0.524 x 0.587 x 1.0 m, w x d x h).
73
+ export const ARCADE_H = 0.42; // target height, m
74
+ export const ARCADE_W = ARCADE_H * 0.524; // footprint width, ~0.22 m
75
+ export const ARCADE_D = ARCADE_H * 0.5876; // footprint depth, ~0.247 m
76
+ export const ARCADE_GAP = 0.02; // gap between neighbouring cabinets
77
+ export const ARCADE_WALL_GAP = 0.01; // clearance between backs and the wall
78
 
79
  // Spawn: center of the middle section cell in the SECOND ROW FROM THE
80
  // BACK wall. The duck faces +X (identity freejoint quat, walks toward
app/src/game/game.js CHANGED
@@ -31,7 +31,7 @@ import {
31
  VEL_FWD, VEL_BACK, VEL_ANG, RVEL_FWD, RVEL_BACK, RVEL_ANG,
32
  CROUCH_PERIOD_S, CROUCH_END_PHASE,
33
  BALL_RADIUS, BALL_PARK_POS, ARENA_HALF, SPAWN_X, SPAWN_Y,
34
- JUKEBOX_H, JUKEBOX_MARGIN, JUKEBOX_YAW, JUKEBOX_HALF,
35
  } from "./constants.js";
36
  import {
37
  buildRig, cloneRig, loadKinematics, setJoint, setJawOpen, MODEL_DIR, MESH_VERSION,
@@ -175,18 +175,15 @@ async function boot({ scene, camera, renderer }) {
175
  el("geom", { name: w.name, type: "box", pos: w.pos, size: w.size }),
176
  );
177
  }
178
- // Jukebox corner prop: one static box matching the visual's footprint
179
- // so the duck and ball can't clip through it. Visual placement is in
180
- // three.js coords (x, 0, z) with rotation.y = yaw; MJCF is z-up with
181
- // x -> x, y -> -z and the same yaw angle.
182
- const jOff = ARENA_HALF - JUKEBOX_MARGIN;
183
- const jw = Math.cos(JUKEBOX_YAW / 2), jz = Math.sin(JUKEBOX_YAW / 2);
184
  doc.querySelector("worldbody").appendChild(
185
  el("geom", {
186
- name: "jukebox", type: "box",
187
- pos: `${-jOff} ${jOff} ${JUKEBOX_H / 2}`,
188
- quat: `${jw} 0 0 ${jz}`,
189
- size: JUKEBOX_HALF.join(" "),
190
  }),
191
  );
192
  // Kickable ball: a light free sphere (beach-ball feel). MuJoCo has no
@@ -619,7 +616,7 @@ async function boot({ scene, camera, renderer }) {
619
  }
620
  })();
621
 
622
- // ── Scene wiring (grid, walls, rig, ball, jukebox) ───────────────────
623
  // The grid/walls carry ceremony-driven uReveal uniforms and per-frame
624
  // focus updates, so the game owns them; lights and environment live in
625
  // the R3F layer.
@@ -743,44 +740,54 @@ async function boot({ scene, camera, renderer }) {
743
  THREE, scene, camera, renderer, fxModule: fx, mesh: ballMesh, group: ballGroup,
744
  });
745
 
746
- // ── Jukebox prop (corner dressing + static collision box) ───────────
747
- // assets/props/jukebox.glb carries two meshes exported from Blender: the
748
- // ~39k-tri render mesh and a 500-tri "jukebox_wire" stand-in used only by
749
- // the hologram pass (same fxWireGeometry escape hatch as the ball). It
750
- // materializes with the duck's ceremony, staggered a beat behind, and
751
- // replays on every respawn. Physics-side, buildPhysicsXml plants a
752
- // matching static box (constants JUKEBOX_*) so it can't be clipped.
753
- let jukeboxGroup = null;
 
754
  try {
755
  const gltf = await new GLTFLoader().loadAsync(
756
- signed(`./assets/props/jukebox.glb?v=1`),
757
  );
758
- const jukebox = gltf.scene;
759
- const jukeWire = jukebox.getObjectByName("jukebox_wire");
760
- jukeWire.removeFromParent();
761
- const jukeBox3 = new THREE.Box3().setFromObject(jukebox);
762
- const jukeScale = JUKEBOX_H / (jukeBox3.max.y - jukeBox3.min.y);
763
- jukebox.scale.setScalar(jukeScale);
764
- jukebox.position.y = -jukeBox3.min.y * jukeScale;
765
- jukebox.traverse((o) => {
766
- if (o.isMesh) o.userData.fxWireGeometry = jukeWire.geometry;
767
- });
768
- const jukeGroup = new THREE.Group();
769
- jukeGroup.add(jukebox);
770
- // Back corner, behind the spawn (the duck faces +X), angled toward the
771
- // center of the arena so the front panel reads from the play area.
772
- jukeGroup.position.set(
773
- -(ARENA_HALF - JUKEBOX_MARGIN), 0, -(ARENA_HALF - JUKEBOX_MARGIN),
774
- );
775
- jukeGroup.rotation.y = JUKEBOX_YAW;
776
- scene.add(jukeGroup);
777
- jukeboxGroup = jukeGroup;
778
- const jukeFx = fx.createWireframeFx();
779
- jukeFx.init({ THREE, scene, root: jukeGroup, camera, renderer, hidden: true });
780
- ceremony.addPropFx(jukeFx, 0.25);
 
 
 
 
 
 
 
 
 
781
  } catch (err) {
782
  // Decorative only: a missing/broken GLB must never halt the boot.
783
- console.warn("[game] jukebox prop disabled:", err);
784
  }
785
 
786
  // ── Camera: orbit controls + chase cam + reset glide ─────────────────
@@ -1254,7 +1261,7 @@ async function boot({ scene, camera, renderer }) {
1254
  get ballQposAdr() { return ballQposAdr; },
1255
  get chaseCam() { return chaseCam; },
1256
  set chaseCam(v) { chaseCam = !!v; },
1257
- get jukebox() { return jukeboxGroup; },
1258
  get camResetActive() { return camResetT0 !== null; },
1259
  get respawnActive() { return ceremony?.respawnActive ?? false; },
1260
  get camPose() {
 
31
  VEL_FWD, VEL_BACK, VEL_ANG, RVEL_FWD, RVEL_BACK, RVEL_ANG,
32
  CROUCH_PERIOD_S, CROUCH_END_PHASE,
33
  BALL_RADIUS, BALL_PARK_POS, ARENA_HALF, SPAWN_X, SPAWN_Y,
34
+ ARCADE_H, ARCADE_W, ARCADE_D, ARCADE_GAP, ARCADE_WALL_GAP,
35
  } from "./constants.js";
36
  import {
37
  buildRig, cloneRig, loadKinematics, setJoint, setJawOpen, MODEL_DIR, MESH_VERSION,
 
175
  el("geom", { name: w.name, type: "box", pos: w.pos, size: w.size }),
176
  );
177
  }
178
+ // Arcade cabinet row: one static box covering all three cabinets so
179
+ // the duck and ball can't clip through them. The row sits against the
180
+ // back (-X) wall, centered on y = 0 (three.js z = 0), axis-aligned.
181
+ const rowHalfW = (3 * ARCADE_W + 2 * ARCADE_GAP) / 2;
 
 
182
  doc.querySelector("worldbody").appendChild(
183
  el("geom", {
184
+ name: "arcade_row", type: "box",
185
+ pos: `${-(ARENA_HALF - ARCADE_WALL_GAP - ARCADE_D / 2)} 0 ${ARCADE_H / 2}`,
186
+ size: `${ARCADE_D / 2} ${rowHalfW} ${ARCADE_H / 2}`,
 
187
  }),
188
  );
189
  // Kickable ball: a light free sphere (beach-ball feel). MuJoCo has no
 
616
  }
617
  })();
618
 
619
+ // ── Scene wiring (grid, walls, rig, ball, arcade row) ────────────────
620
  // The grid/walls carry ceremony-driven uReveal uniforms and per-frame
621
  // focus updates, so the game owns them; lights and environment live in
622
  // the R3F layer.
 
740
  THREE, scene, camera, renderer, fxModule: fx, mesh: ballMesh, group: ballGroup,
741
  });
742
 
743
+ // ── Arcade cabinets (wall dressing + static collision box) ──────────
744
+ // assets/props/arcade.glb carries two meshes: the ~9k-tri render mesh
745
+ // and a 500-tri "arcade_wire" stand-in used only by the hologram pass
746
+ // (same fxWireGeometry escape hatch as the ball). Three clones line up
747
+ // against the back (-X) wall, backs to the wall, screens facing the
748
+ // arena; each materializes with the duck's ceremony, staggered, and
749
+ // replays on every respawn. Physics-side, buildPhysicsXml plants one
750
+ // static box covering the whole row (constants ARCADE_*).
751
+ let arcadeGroups = null;
752
  try {
753
  const gltf = await new GLTFLoader().loadAsync(
754
+ signed(`./assets/props/arcade.glb?v=1`),
755
  );
756
+ const proto = gltf.scene;
757
+ const wire = proto.getObjectByName("arcade_wire");
758
+ wire.removeFromParent();
759
+ const box3 = new THREE.Box3().setFromObject(proto);
760
+ const scale = ARCADE_H / (box3.max.y - box3.min.y);
761
+ proto.scale.setScalar(scale);
762
+ proto.position.y = -box3.min.y * scale;
763
+ arcadeGroups = [];
764
+ for (let i = 0; i < 3; i++) {
765
+ // clone(true) shares geometry/materials; userData doesn't survive
766
+ // Object3D.copy (JSON round-trip), so tag the wire geometry after.
767
+ const cab = proto.clone(true);
768
+ cab.traverse((o) => {
769
+ if (o.isMesh) o.userData.fxWireGeometry = wire.geometry;
770
+ });
771
+ const group = new THREE.Group();
772
+ group.add(cab);
773
+ // Row centered on the back wall's middle, one cabinet-width apart,
774
+ // back faces almost touching the wall. The GLB fronts +Z; the row
775
+ // must front +X (toward the spawn), hence the +90 deg yaw.
776
+ group.position.set(
777
+ -(ARENA_HALF - ARCADE_D / 2 - ARCADE_WALL_GAP),
778
+ 0,
779
+ (i - 1) * (ARCADE_W + ARCADE_GAP),
780
+ );
781
+ group.rotation.y = Math.PI / 2;
782
+ scene.add(group);
783
+ arcadeGroups.push(group);
784
+ const cabFx = fx.createWireframeFx();
785
+ cabFx.init({ THREE, scene, root: group, camera, renderer, hidden: true });
786
+ ceremony.addPropFx(cabFx, 0.25 + i * 0.12);
787
+ }
788
  } catch (err) {
789
  // Decorative only: a missing/broken GLB must never halt the boot.
790
+ console.warn("[game] arcade props disabled:", err);
791
  }
792
 
793
  // ── Camera: orbit controls + chase cam + reset glide ─────────────────
 
1261
  get ballQposAdr() { return ballQposAdr; },
1262
  get chaseCam() { return chaseCam; },
1263
  set chaseCam(v) { chaseCam = !!v; },
1264
+ get arcades() { return arcadeGroups; },
1265
  get camResetActive() { return camResetT0 !== null; },
1266
  get respawnActive() { return ceremony?.respawnActive ?? false; },
1267
  get camPose() {