Skip to content

Commit b6d0c65

Browse files
committed
fix(render): stop the procedural figure flashing before the character loads
The landing hero and playground both pass showProceduralWhileLoading:false to suppress the crude procedural figure while the 702 KB character.glb loads, but the option was never consumed — the procedural mannequin was added visible from frame 0 and only hidden once the character resolved, so it flashed on every page load for the model's fetch time. Honor the flag: when a characterUrl is set and the flag is false, hide the procedural meshes at init (the skeleton still drives animation + grounding, matching the post-load swap) and reveal them only if the character load fails, so the scene still degrades to the working fallback and never blanks. - implement showProceduralWhileLoading; correct its now-stale doc comment - add setMeshVisibility helper (also de-dupes the post-load hide) Verified in-browser with a throttled model load: mid-load shows no procedural figure (empty stage, skeleton still animating), the skinned character appears on load, and an aborted model reveals the procedural fallback. typecheck, build, and all tests pass.
1 parent 2bd43e0 commit b6d0c65

1 file changed

Lines changed: 35 additions & 11 deletions

File tree

packages/posecode-render/src/index.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,12 @@ export interface ViewerOptions {
9393
clips?: Record<string, string>;
9494
/**
9595
* Keep the procedural figure visible while a skinned `characterUrl` loads.
96-
* This viewer always shows the procedural figure during load (and on load
97-
* failure), so the flag is accepted for API compatibility; the default
98-
* behavior already matches `true`.
96+
* Defaults to `true` (the procedural figure poses the scene during the load,
97+
* matching callers that never set this). Set `false` alongside a
98+
* `characterUrl` to hide the procedural meshes until the character resolves —
99+
* so a page load shows the skinned figure or nothing, never a blink of the
100+
* crude procedural figure. On load failure the procedural figure is revealed
101+
* regardless, so the scene never stays blank.
99102
*/
100103
showProceduralWhileLoading?: boolean;
101104
}
@@ -182,10 +185,20 @@ export function createViewer(
182185
enableShadows(mannequin.root);
183186
scene.add(mannequin.root);
184187

188+
// When a skinned character is requested and the caller opted out of the
189+
// procedural fallback during load, hide the procedural meshes up front so the
190+
// crude figure never flashes for the character's fetch time on a page load.
191+
// The skeleton still drives animation and grounding; only the meshes hide
192+
// (same as the post-load swap). Revealed again if the character fails to load.
193+
const deferProceduralMeshes =
194+
Boolean(opts.characterUrl) && opts.showProceduralWhileLoading === false;
195+
if (deferProceduralMeshes) setMeshVisibility(mannequin.root, false);
196+
185197
// Skinned character layer (optional). While loading (and on failure) the
186-
// procedural figure stays; once ready, the driver skeleton is rebuilt with
187-
// the character's proportions, its meshes are hidden (they keep feeding the
188-
// bounding-box grounding), and the character mirrors it every frame.
198+
// procedural figure stays — unless deferred above; once ready, the driver
199+
// skeleton is rebuilt with the character's proportions, its meshes are hidden
200+
// (they keep feeding the bounding-box grounding), and the character mirrors it
201+
// every frame.
189202
let character: Character | null = null;
190203

191204
// Mocap-clip layer (optional, character-only). When the loaded document
@@ -826,9 +839,7 @@ export function createViewer(
826839
scene.remove(mannequin.root);
827840
disposeTree(mannequin.root);
828841
mannequin = buildMannequin(undefined, char.proportions);
829-
mannequin.root.traverse((obj) => {
830-
if ((obj as THREE.Mesh).isMesh) obj.visible = false;
831-
});
842+
setMeshVisibility(mannequin.root, false);
832843
scene.add(mannequin.root);
833844
scene.add(char.group);
834845
character = char;
@@ -840,14 +851,27 @@ export function createViewer(
840851
else char.sync(mannequin);
841852
})
842853
.catch(() => {
843-
// Keep the procedural figure. Deliberately silent: an offline embed
844-
// or a blocked CDN should degrade, not error.
854+
// Character failed (offline embed, blocked/404 CDN): reveal the
855+
// procedural figure we may have hidden, so the scene degrades to the
856+
// working fallback instead of staying blank. Deliberately silent.
857+
if (deferProceduralMeshes) setMeshVisibility(mannequin.root, true);
845858
});
846859
}
847860

848861
return api;
849862
}
850863

864+
/**
865+
* Show or hide a figure's meshes. The skeleton keeps driving animation and
866+
* bounding-box grounding regardless, so hiding only the meshes lets a hidden
867+
* procedural figure still pose the scene while its character stand-in loads.
868+
*/
869+
function setMeshVisibility(root: THREE.Object3D, visible: boolean): void {
870+
root.traverse((obj) => {
871+
if ((obj as THREE.Mesh).isMesh) obj.visible = visible;
872+
});
873+
}
874+
851875
/** True for a finger bone id (thumb/index/middle/ring/pinky_left|right). */
852876
function isFingerId(id: string): boolean {
853877
return /^(thumb|index|middle|ring|pinky)_/.test(id);

0 commit comments

Comments
 (0)