Skip to content

Commit 71e7226

Browse files
Merge pull request #130 from posecode-dev/feat/avatar-selection
Support document-selected avatar appearances alongside the humanoid rig
2 parents f226fe6 + 76ff5ad commit 71e7226

35 files changed

Lines changed: 627 additions & 96 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"posecode-parser": minor
3+
"posecode-render": minor
4+
"posecode-embed": minor
5+
---
6+
7+
Add an optional avatar selector separate from humanoid rig topology, safely hot-swap document-selected characters with procedural fallback, and add hosted avatar defaults.
8+
9+
Keep the renderer peer range compatible with the parser's additive language/IR update.

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,52 @@ The hosted playground currently uses an Adobe Mixamo character and one showcase
819819

820820
The renderer also includes a zero-asset procedural figure and accepts compatible humanoid GLB characters through `characterUrl`.
821821

822+
### Multiple character appearances (`avatar avatar1` / `avatar2` / `avatar3`)
823+
824+
All built-in characters use the same `rig humanoid` skeleton topology. An
825+
optional `avatar` directive selects appearance without redefining that rig (see
826+
[`spec/SPEC.md`](spec/SPEC.md)). Pass `characterUrls` (selector → GLB URL map)
827+
to `createViewer` instead of a single `characterUrl`; `ir.avatar` is used when
828+
present and `ir.rig` supplies the default selector otherwise. Switching
829+
documents, or editing the `avatar` directive, swaps the visible character. A
830+
selector with no entry in the map (or any load failure) falls back to the
831+
procedural figure. See
832+
[`packages/posecode-render/README.md`](packages/posecode-render/README.md#usage)
833+
for the option, and `packages/posecode-embed`'s `character` attribute docs for
834+
the same behavior in the web component (absent by default; set an explicit URL
835+
to pin one character regardless of `avatar`).
836+
837+
### Bringing your own character rig
838+
839+
Pass a `characterUrl` (fixed) or `characterUrls` (per-selector, see above) pointing
840+
to a skinned GLB to replace the bundled Mixamo character. Requirements:
841+
842+
- **Format:** glTF binary (`.glb`) containing a `THREE.SkinnedMesh`.
843+
- **Rest pose:** T-pose.
844+
- **Bone naming:** Mixamo convention. Names may carry the `mixamorig:` /
845+
`mixamorigN:` namespace prefix — it's stripped automatically. These bones
846+
must all be present:
847+
- Torso/head: `Hips`, `Spine`, `Spine2`, `Neck`, `Head`
848+
- Arms: `LeftArm`, `LeftForeArm`, `LeftHand`, `RightArm`, `RightForeArm`, `RightHand`
849+
- Legs: `LeftUpLeg`, `LeftLeg`, `LeftFoot`, `RightUpLeg`, `RightLeg`, `RightFoot`
850+
- Fingers (first phalanx only): `LeftHandThumb1`, `LeftHandIndex1`,
851+
`LeftHandMiddle1`, `LeftHandRing1`, `LeftHandPinky1`, and the
852+
`RightHand*1` equivalents
853+
854+
If any required bone is missing, loading the character rejects and the
855+
viewer silently falls back to the zero-asset procedural figure — a bad rig
856+
never breaks the scene.
857+
858+
The simplest way to source a compatible rig is [mixamo.com](https://www.mixamo.com):
859+
export a character in T-pose with "skin with skeleton," then convert
860+
FBX → GLB (e.g. with Blender's glTF exporter or `FBX2glTF`). Bone names come
861+
out Mixamo-compatible automatically.
862+
863+
The bone map and retarget/calibration logic live in
864+
[`packages/posecode-render/src/character.ts`](packages/posecode-render/src/character.ts).
865+
Supporting a different naming convention (e.g. VRM humanoid bones) means
866+
editing the `BONE_MAP` table and `plainName()` prefix-stripping there.
867+
822868
---
823869

824870
## Licensing

docs/legal/LEGAL_REVIEW.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ This is an engineering audit checklist, not legal advice. Resolve these points w
2323

2424
## Third-party materials
2525

26-
- Recheck the current Adobe Mixamo terms for redistribution of `xbot.glb` and `jumping-jacks.fbx`, especially for npm, source archives, hosted demos, white-label offerings, and customer redistribution.
26+
- Recheck the current Adobe Mixamo terms for redistribution of `xbot.glb`, `avatar2.glb`, `avatar3.glb`, and `jumping-jacks.fbx`, especially for npm, source archives, hosted demos, white-label offerings, and customer redistribution.
2727
- Replace the remaining Mixamo-derived files with owned or clearly redistributable assets if the intended distribution is not covered.
2828
- Run a dependency and source-provenance audit for every release artifact, including bundled JavaScript, model files, fonts, images, and generated content. Preserve all required notices.

docs/legal/THIRD_PARTY_NOTICES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ Posecode npm packages use dependencies under permissive licenses including MIT,
77
The following committed files originate from Adobe Mixamo character or animation exports and are governed by the applicable Adobe terms, not Apache-2.0 or AGPL-3.0:
88

99
- `playground/public/models/xbot.glb`
10+
- `playground/public/models/avatar2.glb` (Mixamo "Beta" character)
11+
- `playground/public/models/avatar3.glb` (Mixamo "Remy" character)
1012
- `playground/public/clips/jumping-jacks.fbx`
1113

14+
Provenance for the two avatar characters is established by their glTF contents:
15+
both carry the `mixamorig:` skeleton, a `mixamo.com` animation track, and Mixamo
16+
mesh and texture names (`Beta_Joints`/`Beta_Surface` and `Remy_Body_*`).
17+
1218
The software licenses in this repository do not grant independent rights to extract, redistribute, or relicense these assets. Review the current Adobe Mixamo terms before redistribution. Posecode also provides a code-generated procedural figure that does not require these assets.
1319

1420
`playground/public/models/character.glb`, an older unused Mixamo-derived character, was removed before the 0.3.0 release. The hosted embed default now uses the same active XBot asset as the playground.

editors/vscode/syntaxes/posecode.tmLanguage.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"keywords": {
3131
"name": "keyword.control.posecode",
32-
"match": "\\b(posecode|rig|prop|pose|start|step|repeat|clip|ground-lock|reach|pin|grip|turn|travel|cue|hold)\\b"
32+
"match": "\\b(posecode|rig|avatar|prop|pose|start|step|repeat|clip|ground-lock|reach|pin|grip|turn|travel|cue|hold)\\b"
3333
},
3434
"kinds": {
3535
"name": "storage.type.posecode",
@@ -45,7 +45,7 @@
4545
},
4646
"constants": {
4747
"name": "constant.language.posecode",
48-
"match": "\\b(flow|settle|drive|snap|linear|ease-in-out|ease-in|ease-out|neutral|standing|plank|hands|feet|humanoid)\\b"
48+
"match": "\\b(flow|settle|drive|snap|linear|ease-in-out|ease-in|ease-out|neutral|standing|plank|hands|feet|humanoid|avatar1|avatar2|avatar3)\\b"
4949
},
5050
"numbers": {
5151
"name": "constant.numeric.posecode",

package-lock.json

Lines changed: 17 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/posecode-embed/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ definePosecodePlayer(); // idempotent
6868
| `controls` | `true` | Show the play/pause bar. |
6969
| `autorotate` | `true` | Slowly orbit the camera when idle. |
7070
| `speed` | `1` | Playback multiplier (`0.1``4`). |
71-
| `character` | *(hosted default)* | Realistic figure: a GLB URL (Mixamo rig), or `off` for the procedural mannequin. Load failures fall back to the mannequin. |
71+
| `character` | *(document-driven)* | Realistic figure. Absent: optional `avatar avatar1|avatar2|avatar3` selects a hosted appearance; documents without it use the humanoid XBot default. Set to a GLB URL to pin one character regardless of `avatar`, or `off` for the procedural mannequin. Load failures fall back to the mannequin. |
7272
| `playground` | `https://posecode.org/play` | Base URL for the "Edit ↗" link. |
7373

7474
Boolean attributes accept `false` / `0` / `no` / `off` to turn them off, so

packages/posecode-embed/src/element.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,11 @@ export class PosecodePlayerElement extends HTMLElement {
227227
const { createViewer } = await import("posecode-render");
228228
const viewer = createViewer(this.#canvas, {
229229
autoRotate: opts.autoRotate && !reduceMotion,
230-
...(opts.characterUrl ? { characterUrl: opts.characterUrl } : {}),
230+
...(opts.characterDisabled
231+
? {}
232+
: opts.characterUrl
233+
? { characterUrl: opts.characterUrl }
234+
: { characterUrls: opts.characterUrls }),
231235
});
232236
this.#viewer = viewer;
233237
viewer.onPhase(({ phaseName }) => {

packages/posecode-embed/src/options.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,43 @@ export interface PlayerOptions {
1919
/** Playback speed multiplier (0.1–4). */
2020
speed: number;
2121
/**
22-
* Realistic skinned figure: a GLB URL, the default hosted character when
23-
* absent, or `""` (attribute `character="off"`) for the procedural figure.
24-
* Load failures fall back to the procedural figure, so an offline page
25-
* degrades instead of blanking.
22+
* Realistic skinned figure pinned to one GLB URL, from an explicit
23+
* `character="<url>"` attribute. `""` when the attribute is absent (the host
24+
* picks the character from `characterUrls` instead) or the character is
25+
* disabled. Load failures fall back to the procedural figure, so an offline
26+
* page degrades instead of blanking.
2627
*/
2728
characterUrl: string;
29+
/** True when `character="off"` (or another falsey word) explicitly disables any skinned character. */
30+
characterDisabled: boolean;
31+
/**
32+
* Document selector (`avatar` when present, otherwise `rig`) → GLB URL,
33+
* applied when `characterUrl` is unset and the character isn't disabled.
34+
* Defaults to the hosted character choices and the humanoid default.
35+
*/
36+
characterUrls: Record<string, string>;
2837
}
2938

3039
/** The character the hosted playground uses, served from the same origin. */
3140
export const DEFAULT_CHARACTER_URL = "https://posecode.org/models/xbot.glb";
3241

42+
/** Hosted character per built-in selector. Avatar1 intentionally reuses XBot. */
43+
export const DEFAULT_CHARACTER_URLS: Record<string, string> = {
44+
humanoid: DEFAULT_CHARACTER_URL,
45+
avatar1: DEFAULT_CHARACTER_URL,
46+
avatar2: "https://posecode.org/models/avatar2.glb",
47+
avatar3: "https://posecode.org/models/avatar3.glb",
48+
};
49+
3350
export const DEFAULT_OPTIONS: PlayerOptions = {
3451
autoplay: true,
3552
loop: true,
3653
controls: true,
3754
autoRotate: true,
3855
speed: 1,
39-
characterUrl: DEFAULT_CHARACTER_URL,
56+
characterUrl: "",
57+
characterDisabled: false,
58+
characterUrls: DEFAULT_CHARACTER_URLS,
4059
};
4160

4261
const SPEED_MIN = 0.1;
@@ -66,15 +85,13 @@ function clamp(n: number, lo: number, hi: number): number {
6685

6786
export function parseOptions(attrs: RawAttributes): PlayerOptions {
6887
const speedRaw = attrs.speed != null ? Number(attrs.speed) : NaN;
69-
// `character` accepts a GLB URL, a falsey word to opt out, or absent for
70-
// the hosted default.
88+
// `character` accepts a GLB URL (pinned regardless of the document's rig),
89+
// a falsey word to disable any skinned character, or absent to let the
90+
// document's optional `avatar` directive pick from characterUrls.
7191
const characterRaw = attrs.character?.trim();
72-
const characterUrl =
73-
characterRaw === undefined || characterRaw === null
74-
? DEFAULT_OPTIONS.characterUrl
75-
: FALSEY.has(characterRaw.toLowerCase())
76-
? ""
77-
: characterRaw;
92+
const characterDisabled =
93+
characterRaw !== undefined && characterRaw !== null && FALSEY.has(characterRaw.toLowerCase());
94+
const characterUrl = characterRaw && !characterDisabled ? characterRaw : "";
7895
return {
7996
autoplay: boolAttr(attrs.autoplay, DEFAULT_OPTIONS.autoplay),
8097
loop: boolAttr(attrs.loop, DEFAULT_OPTIONS.loop),
@@ -84,5 +101,7 @@ export function parseOptions(attrs: RawAttributes): PlayerOptions {
84101
? clamp(speedRaw, SPEED_MIN, SPEED_MAX)
85102
: DEFAULT_OPTIONS.speed,
86103
characterUrl,
104+
characterDisabled,
105+
characterUrls: DEFAULT_OPTIONS.characterUrls,
87106
};
88107
}

packages/posecode-embed/test/compat.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ describe("embed compatibility contract", () => {
2828
readFileSync(resolve(import.meta.dirname, "../package.json"), "utf8"),
2929
) as { version: string };
3030
expect(version).toBe(pkg.version);
31-
expect(languageVersion).toBe("0.3");
31+
expect(languageVersion).toBe("0.4");
3232
});
3333
});

0 commit comments

Comments
 (0)