diff --git a/.changeset/harden-avatar-selection.md b/.changeset/harden-avatar-selection.md index c5cae8d..ce79909 100644 --- a/.changeset/harden-avatar-selection.md +++ b/.changeset/harden-avatar-selection.md @@ -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. diff --git a/README.md b/README.md index f4cd48a..9ab280e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/packages/posecode-embed/README.md b/packages/posecode-embed/README.md index f026018..2157b3c 100644 --- a/packages/posecode-embed/README.md +++ b/packages/posecode-embed/README.md @@ -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 diff --git a/packages/posecode-embed/src/options.ts b/packages/posecode-embed/src/options.ts index e8329c7..b8dfab7 100644 --- a/packages/posecode-embed/src/options.ts +++ b/packages/posecode-embed/src/options.ts @@ -31,7 +31,8 @@ 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; } @@ -39,12 +40,10 @@ export interface PlayerOptions { /** 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 = { 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 = { diff --git a/packages/posecode-embed/test/options.test.ts b/packages/posecode-embed/test/options.test.ts index a6885c2..fb3bdd2 100644 --- a/packages/posecode-embed/test/options.test.ts +++ b/packages/posecode-embed/test/options.test.ts @@ -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", () => { diff --git a/packages/posecode-render/README.md b/packages/posecode-render/README.md index b1a31ac..e1a8625 100644 --- a/packages/posecode-render/README.md +++ b/packages/posecode-render/README.md @@ -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", // }, }); diff --git a/playground/public/models/avatar2.glb b/playground/public/models/avatar2.glb deleted file mode 100644 index 14af548..0000000 Binary files a/playground/public/models/avatar2.glb and /dev/null differ diff --git a/playground/public/models/avatar3.glb b/playground/public/models/avatar3.glb deleted file mode 100644 index b8ce2e5..0000000 Binary files a/playground/public/models/avatar3.glb and /dev/null differ diff --git a/playground/src/main.ts b/playground/src/main.ts index 82a48ee..fa895c0 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -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 = { +// 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> = { 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";