Skip to content

Commit bec1c7e

Browse files
committed
avatar eklendi
1 parent 579d986 commit bec1c7e

21 files changed

Lines changed: 320 additions & 100 deletions

File tree

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 rigs (`rig humanoid` / `avatar1` / `avatar2` / `avatar3`)
823+
824+
A `.posecode` document's `rig` directive isn't just `humanoid``avatar1`,
825+
`avatar2`, and `avatar3` are also valid rig names (see
826+
[`spec/SPEC.md`](spec/SPEC.md)). Pass `characterUrls` (a rig name → GLB URL
827+
map) to `createViewer` instead of a single `characterUrl`, and each loaded
828+
document's `rig` value picks its character automatically — switching
829+
documents, or editing one to declare a different `rig`, swaps the visible
830+
character. A rig with no entry in the map (or any load failure) falls back to
831+
the procedural figure, same as an unset `characterUrl`. 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 `rig`).
836+
837+
### Bringing your own character rig
838+
839+
Pass a `characterUrl` (fixed) or `characterUrls` (per-rig, 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

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ These are the unlocks, roughly in order of leverage:
2828

2929
1. ~~**Hip / waist hinge primitive**~~: **shipped (v0.1).** `pelvis: hinge <deg>`
3030
tips the torso forward over the hips while the legs stay planted (the renderer
31-
counter-rotates the hips). Powers `deadlift`, `bent-over-row`, `good-morning`,
31+
counter-rotates the hips). Powers `deadlift`, `bent-over-row`, `good-morning`,rig
3232
and `bow`. Next: hinge with a loaded-bar prop.
3333
2. ~~**Reach-IK (reach a world target)**~~: **shipped, now ROM-constrained.**
3434
`reach: <effector> <target>` drives a hand/foot to a body landmark, the

editors/vscode/syntaxes/posecode.tmLanguage.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 34 additions & 34 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` | *(rig-driven)* | Realistic figure. Absent: the loaded document's `rig` directive (`humanoid`, `avatar1`, `avatar2`, `avatar3`) picks the hosted character. Set to a GLB URL (Mixamo rig) to pin one character regardless of `rig`, 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 (rig
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+
* Rig name (the loaded document's `rig` directive) → character GLB URL,
33+
* applied when `characterUrl` is unset and the character isn't disabled.
34+
* Defaults to the hosted characters for every built-in rig name.
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 rig name, keyed by posecode-parser's RigName. */
43+
export const DEFAULT_CHARACTER_URLS: Record<string, string> = {
44+
humanoid: DEFAULT_CHARACTER_URL,
45+
avatar1: "https://posecode.org/models/avatar1.glb",
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 each
90+
// loaded document's `rig` 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/options.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
import { describe, it, expect } from "vitest";
2-
import { parseOptions, DEFAULT_CHARACTER_URL, DEFAULT_OPTIONS } from "../src/options.js";
2+
import {
3+
parseOptions,
4+
DEFAULT_CHARACTER_URL,
5+
DEFAULT_CHARACTER_URLS,
6+
DEFAULT_OPTIONS,
7+
} from "../src/options.js";
38

49
describe("parseOptions", () => {
510
it("returns sensible defaults for an element with no attributes", () => {
611
expect(parseOptions({})).toEqual(DEFAULT_OPTIONS);
712
expect(DEFAULT_CHARACTER_URL).toBe("https://posecode.org/models/xbot.glb");
13+
// No explicit `character` attribute: rig-driven, not pinned to one URL.
14+
expect(DEFAULT_OPTIONS.characterUrl).toBe("");
15+
expect(DEFAULT_OPTIONS.characterDisabled).toBe(false);
16+
expect(DEFAULT_OPTIONS.characterUrls).toBe(DEFAULT_CHARACTER_URLS);
17+
expect(DEFAULT_CHARACTER_URLS.humanoid).toBe(DEFAULT_CHARACTER_URL);
18+
});
19+
20+
it("pins an explicit character URL and disables rig-driven selection", () => {
21+
const o = parseOptions({ character: "https://example.com/me.glb" });
22+
expect(o.characterUrl).toBe("https://example.com/me.glb");
23+
expect(o.characterDisabled).toBe(false);
24+
});
25+
26+
it("disables the character entirely on a falsey word", () => {
27+
const o = parseOptions({ character: "off" });
28+
expect(o.characterUrl).toBe("");
29+
expect(o.characterDisabled).toBe(true);
830
});
931

1032
it("treats boolean attributes as present-means-true", () => {

0 commit comments

Comments
 (0)