Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/harden-avatar-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"posecode-embed": minor
---

Add an optional avatar selector separate from humanoid rig topology, safely hot-swap document-selected characters with procedural fallback, and add hosted avatar defaults.
Add an optional avatar selector separate from humanoid rig topology, safely hot-swap document-selected characters with procedural fallback, and support host-provided avatar maps.

Keep the renderer peer range compatible with the parser's additive language/IR update.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -821,8 +821,8 @@ The renderer also includes a zero-asset procedural figure and accepts compatible

### Multiple character appearances (`avatar avatar1` / `avatar2` / `avatar3`)

All built-in characters use the same `rig humanoid` skeleton topology. An
optional `avatar` directive selects appearance without redefining that rig (see
All avatar selectors use the same `rig humanoid` skeleton topology. An optional
`avatar` directive selects appearance without redefining that rig (see
[`spec/SPEC.md`](spec/SPEC.md)). Pass `characterUrls` (selector → GLB URL map)
to `createViewer` instead of a single `characterUrl`; `ir.avatar` is used when
present and `ir.rig` supplies the default selector otherwise. Switching
Expand All @@ -831,8 +831,10 @@ selector with no entry in the map (or any load failure) falls back to the
procedural figure. See
[`packages/posecode-render/README.md`](packages/posecode-render/README.md#usage)
for the option, and `packages/posecode-embed`'s `character` attribute docs for
the same behavior in the web component (absent by default; set an explicit URL
to pin one character regardless of `avatar`).
the same behavior in the web component. The built-in map currently serves XBot
for `humanoid` and `avatar1`; `avatar2` and `avatar3` intentionally use the
procedural fallback until assets with documented redistribution rights are
provided. Set an explicit URL to pin one character regardless of `avatar`.

### Bringing your own character rig

Expand Down Expand Up @@ -860,6 +862,10 @@ export a character in T-pose with "skin with skeleton," then convert
FBX → GLB (e.g. with Blender's glTF exporter or `FBX2glTF`). Bone names come
out Mixamo-compatible automatically.

Before publishing or committing a character, verify that its license permits
redistribution of the asset itself. Compatibility with a Mixamo rig does not
by itself grant redistribution rights.

The bone map and retarget/calibration logic live in
[`packages/posecode-render/src/character.ts`](packages/posecode-render/src/character.ts).
Supporting a different naming convention (e.g. VRM humanoid bones) means
Expand Down
2 changes: 1 addition & 1 deletion packages/posecode-embed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ definePosecodePlayer(); // idempotent
| `controls` | `true` | Show the play/pause bar. |
| `autorotate` | `true` | Slowly orbit the camera when idle. |
| `speed` | `1` | Playback multiplier (`0.1`–`4`). |
| `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. |
| `character` | *(document-driven)* | Realistic figure. Absent: the humanoid default and `avatar1` use hosted XBot; unmapped selectors such as `avatar2`/`avatar3` use the procedural mannequin until the host supplies licensed assets. Set to a GLB URL to pin one character regardless of `avatar`, or `off` for the procedural mannequin. Load failures also fall back to the mannequin. |
| `playground` | `https://posecode.org/play` | Base URL for the "Edit ↗" link. |

Boolean attributes accept `false` / `0` / `no` / `off` to turn them off, so
Expand Down
7 changes: 3 additions & 4 deletions packages/posecode-embed/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,19 @@ export interface PlayerOptions {
/**
* Document selector (`avatar` when present, otherwise `rig`) → GLB URL,
* applied when `characterUrl` is unset and the character isn't disabled.
* Defaults to the hosted character choices and the humanoid default.
* The built-in map only contains assets with documented provenance;
* selectors absent from it use the renderer's procedural fallback.
*/
characterUrls: Record<string, string>;
}

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

/** Hosted character per built-in selector. Avatar1 intentionally reuses XBot. */
/** Hosted characters with documented provenance. Avatar1 intentionally reuses XBot. */
export const DEFAULT_CHARACTER_URLS: Record<string, string> = {
humanoid: DEFAULT_CHARACTER_URL,
avatar1: DEFAULT_CHARACTER_URL,
avatar2: "https://posecode.org/models/avatar2.glb",
avatar3: "https://posecode.org/models/avatar3.glb",
};

export const DEFAULT_OPTIONS: PlayerOptions = {
Expand Down
2 changes: 2 additions & 0 deletions packages/posecode-embed/test/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ describe("parseOptions", () => {
expect(DEFAULT_OPTIONS.characterUrls).toBe(DEFAULT_CHARACTER_URLS);
expect(DEFAULT_CHARACTER_URLS.humanoid).toBe(DEFAULT_CHARACTER_URL);
expect(DEFAULT_CHARACTER_URLS.avatar1).toBe(DEFAULT_CHARACTER_URL);
expect(DEFAULT_CHARACTER_URLS.avatar2).toBeUndefined();
expect(DEFAULT_CHARACTER_URLS.avatar3).toBeUndefined();
});

it("pins an explicit character URL and disables document-driven selection", () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/posecode-render/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const viewer = createViewer(canvas, {
// characterUrls: {
// humanoid: "https://posecode.org/models/xbot.glb",
// avatar1: "https://posecode.org/models/xbot.glb",
// avatar2: "https://posecode.org/models/avatar2.glb",
// avatar3: "https://posecode.org/models/avatar3.glb",
// avatar2: "https://assets.example.com/licensed-avatar2.glb",
// avatar3: "https://assets.example.com/licensed-avatar3.glb",
// },
});

Expand Down
Binary file removed playground/public/models/avatar2.glb
Binary file not shown.
Binary file removed playground/public/models/avatar3.glb
Binary file not shown.
11 changes: 5 additions & 6 deletions playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,13 @@ type InteractiveViewer = Viewer & {
const DEFAULT_PRESET =
PRESETS.find((p) => p.id === "superhero-landing") ?? PRESETS[0]!;

// Character appearance is independent of skeleton topology. Documents without
// an `avatar` directive use the humanoid default; avatar1 deliberately reuses
// XBot instead of committing a duplicate binary.
const CHARACTER_URLS: Record<AvatarName | "humanoid", string> = {
// Character appearance is independent of skeleton topology. Only assets with
// documented redistribution provenance are mapped here. Documents without an
// `avatar` directive use the humanoid default; avatar1 deliberately reuses
// XBot, while unmapped selectors fall back to the procedural figure.
const CHARACTER_URLS: Partial<Record<AvatarName | "humanoid", string>> = {
humanoid: "/models/xbot.glb",
avatar1: "/models/xbot.glb",
avatar2: "/models/avatar2.glb",
avatar3: "/models/avatar3.glb",
};
import { renderWarnings } from "./warnings.js";
import llmPrompt from "../../spec/llm-authoring.md?raw";
Expand Down