Production art v1: portable rigged character and PBR package pipeline - #121
Production art v1: portable rigged character and PBR package pipeline#121BrandDead wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Interrupted clips leave idle running
- Added a check to ensure the animation group is still active before starting idle, preventing nested idle animations when transient clips are interrupted.
- ✅ Fixed: Downed hit proxies stay pickable
- Added pickability toggles for hitMeshes in both syncSnapshot and updateCamera to ensure downed actors' collision proxies become unpickable.
Or push these changes by commenting:
@cursor push 256ef8a86b
Preview (256ef8a86b)
diff --git a/frontend/src/game/ops/OpsPackagedAssetLoader.ts b/frontend/src/game/ops/OpsPackagedAssetLoader.ts
--- a/frontend/src/game/ops/OpsPackagedAssetLoader.ts
+++ b/frontend/src/game/ops/OpsPackagedAssetLoader.ts
@@ -233,9 +233,11 @@
nextGroup.start(loop);
if (transient) {
nextGroup.onAnimationGroupEndObservable.addOnce(() => {
- transient = false;
- activeState = undefined;
- playAnimation('idle');
+ if (activeGroup === nextGroup) {
+ transient = false;
+ activeState = undefined;
+ playAnimation('idle');
+ }
});
}
};
diff --git a/frontend/src/game/ops/OpsWorld.ts b/frontend/src/game/ops/OpsWorld.ts
--- a/frontend/src/game/ops/OpsWorld.ts
+++ b/frontend/src/game/ops/OpsWorld.ts
@@ -525,6 +525,9 @@
mesh.visibility = actor.isDown ? 0.42 : 1;
mesh.isPickable = !actor.isDown;
});
+ visual.hitMeshes?.forEach((mesh) => {
+ mesh.isPickable = !actor.isDown;
+ });
});
}
@@ -780,6 +783,9 @@
mesh.visibility = selected.isDown ? 0.42 : 1;
mesh.isPickable = !selected.isDown;
});
+ actor.hitMeshes?.forEach((mesh) => {
+ mesh.isPickable = !selected.isDown;
+ });
return;
}
@@ -790,6 +796,9 @@
mesh.visibility = 0;
mesh.isPickable = false;
});
+ actor.hitMeshes?.forEach((mesh) => {
+ mesh.isPickable = false;
+ });
return;
}
@@ -803,6 +812,9 @@
mesh.visibility = selected.isDown ? 0.42 : 1;
mesh.isPickable = !selected.isDown;
});
+ actor.hitMeshes?.forEach((mesh) => {
+ mesh.isPickable = !selected.isDown;
+ });
}
private updateEffects(delta: number): void {You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 285ce57. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 285ce57619
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| visual.meshes.forEach((mesh) => { | ||
| mesh.visibility = actor.isDown ? 0.42 : 1; | ||
| mesh.isPickable = !actor.isDown; |
There was a problem hiding this comment.
Disable packaged hit proxies when actors go down
When a production-GLB actor is downed, this updates only visual.meshes; the separate visual.hitMeshes created by createHitProxies() remain pickable. In FPS/TPS, rays therefore continue selecting the invisible hit boxes around a corpse, and validateAimFireRay() rejects the downed actor candidate instead of firing at a living target behind it. This differs from the articulated fallback, whose hit geometry is disabled here, so the packaged path should also disable its hit proxies.
Useful? React with 👍 / 👎.
| return HIT_PROXY_SPECS.flatMap((spec) => { | ||
| const bone = findBone(entries, packageDefinition.hitZones[spec.packageKey]); | ||
| if (!bone) return []; |
There was a problem hiding this comment.
Reject packages without every physical hit bone
For any schema-valid replacement package with a misspelled hit-zone bone—or no skinned mesh—this silently omits some or all hit proxies while still accepting the template and marking the package active. Because the visible GLB meshes are deliberately non-pickable, a package missing all proxies makes every actor impossible to shoot in FPS/TPS. Validate the skinned mesh and every declared hit-zone bone while loading the template so malformed packages trigger the articulated fallback instead.
Useful? React with 👍 / 👎.
| ]; | ||
|
|
||
| const io = new NodeIO(); | ||
| const targetDocument = await io.read(baseModelPath); |
There was a problem hiding this comment.
Normalize the official texture URIs before reading the model
With the exact Quaternius archive this pipeline is documented to consume, docs/PRODUCTION_ART_SOURCES.md records that the source glTF requests T_Hair_1_Normal_png.png and T_Eye_Normal_png.png, while the archive contains differently named files. The script calls io.read(baseModelPath) before performing any URI repair or creating aliases, so its advertised command cannot rebuild the asset directly from the official sources without an undocumented manual pre-step. Automate that normalization before reading the model.
Useful? React with 👍 / 👎.
| // budget. Excluding them let ~31 MB of raw PNG land on main while | ||
| // the gate still reported 5.35/20 MB. | ||
| const isPackageFile = p.startsWith(PACKAGE_DIR + path.sep); | ||
| if (!isPackageFile) warn('W_ORPHAN', `Runtime file not in manifest: ${path.relative(FRONTEND, p)}`); |
There was a problem hiding this comment.
Run package validation in the checked CI workflow
In the checked .github/workflows/ci.yml frontend job, only npm run assets:audit and the Vite build run; npm run assets:packages is never called. This new branch suppresses orphan warnings for everything under public/assets/packages on the assumption that validate-packages.mjs handles it, while Vite merely copies public files, so a malformed package manifest or missing referenced GLB can pass CI and deploy with the runtime silently falling back. Add the package validator to that workflow rather than relying on the unused aggregate validate script.
Useful? React with 👍 / 👎.
| const albedo = new Texture(`${PBR_ROOT}/${surfaceId}-color.webp`, scene, true, false); | ||
| const normal = new Texture(`${PBR_ROOT}/${surfaceId}-normal.webp`, scene, true, false); | ||
| const roughness = new Texture(`${PBR_ROOT}/${surfaceId}-roughness.webp`, scene, true, false); |
There was a problem hiding this comment.
Enable mipmaps for the tiled PBR surfaces
The third Texture constructor argument is noMipmap, so passing true disables mip generation for every new PBR map. These textures are tiled up to 10× and are heavily minified in the tactical and oblique cameras; without mipmaps, the albedo, roughness, and normal detail will alias and shimmer during camera or actor movement. Pass false or omit noMipmap so the production street materials use mipmapped sampling.
Useful? React with 👍 / 👎.
| await world.initializeProductionAssets(); | ||
| await scene.whenReadyAsync(); |
There was a problem hiding this comment.
Bound or cancel the blocking production-asset load
The render loop and onReady callback are not started until this awaited package load and the subsequent scene-ready wait complete, but neither operation has a timeout or an abort signal. If the package JSON, 1.36 MB GLB, or one of its embedded resources stalls rather than rejecting, Modern Ops remains on its loading state indefinitely and the articulated fallback is never displayed. Apply a bounded abort signal or start rendering the fallback while production assets load asynchronously.
Useful? React with 👍 / 👎.


Summary
This PR begins the next production-art milestone after merged PRs #119 and #120 without making a paid purchase. It proves the portable character and block-material contracts with legally redistributable CC0 sources while preserving the shared combat, possession, camera, and result authority.
What changed
AssetContainer, then instantiates independent skeletons and animation groups for every combatant.hand_r.Verification
Visual boundary
The free standard character is intentionally a pipeline stand-in, not final member art. Its superhero costume does not match the streetwear vision. Two procedural bone-attached clothing experiments were tested and removed because they degraded the live TPS silhouette. This PR therefore proves that a commissioned or licensed clothed hero/rival can replace the GLB without rewriting combat, cameras, possession, hit resolution, or persistence; it does not claim target-image fidelity.
The next asset decision is defined in
docs/PRODUCTION_ART_PACKAGE_V1.md: approve one authored clothed hero and one rival only after they pass the same strict package, TPS/FPS/tactical, and physical-hit gates.Evidence
See:
docs/PRODUCTION_ART_PACKAGE_V1.mddocs/PRODUCTION_ART_SOURCES.mddocs/PRODUCTION_ART_V1_VERIFICATION.mddocs/PRODUCTION_ART_VISUAL_REVIEW.mddocs/evidence/production-art-v1/Note
Medium Risk
Touches 3D combat presentation, physical hit proxies, and asset loading with a fallback path; scope is large but gated by tests, schema validation, and unchanged gameplay contracts.
Overview
Introduces production art package v1: a strict, schema-validated character pipeline and CC0 PBR street materials for the 1208 Las Olas Modern Ops client, without changing combat, possession, or result authority.
Character runtime loads
character.universal-male.pipeline-v1before the scene is ready—a Quaternius CC0 base plus 12 gameplay clips merged into one ~1.36 MB GLB viabuild-character-package.mjs. The loader parses the GLB once into a BabylonAssetContainer, clones rigs per combatant, attaches bone-local hit proxies and ahand_rweapon, and falls back to the articulated procedural actor if validation fails. Environment swaps flat street colors for ambientCG asphalt/concrete/brick WebP maps with apbr-sources.jsonprovenance lock.Tooling and gates add
@gltf-transformdev deps, PBR prep script, package files in the global 20 MB asset audit, and gauntlet checks that require the production package ID. Docs (ASSETS.md, package spec, sources, verification, visual review) record CC0 acquisition, explicit “pipeline stand-in not final art,” and acceptance criteria for commissioned heroes.Lifecycle fixes from review: transient animation end callbacks only restart idle if the group is still active; downed, tactical, and first-person camera sync now toggles hit proxy pickability with visible meshes so rays cannot hit hidden bodies.
Reviewed by Cursor Bugbot for commit 20a610a. Bugbot is set up for automated code reviews on this repo. Configure here.