Avatar SDK Guide AI Skill

VIVERSE Avatar SDK Guide

Embed VIVERSE Avatar selection and creation into your own site or game. This guide organizes every SDK feature, setting, event, and the optional Avatar Controller into searchable categories. Use the search box to jump straight to a keyword, attribute, or method.

Overview

The VIVERSE Avatar SDK lets a third-party host site embed the Happy Path character flow so visitors can pick or create an avatar without calling account APIs directly from the host page.

A host page owns its own app experience and loads the SDK as an embed layer. In Happy Path mode, the SDK owns the VIVERSE Avatar iframe, character picker, Avatar Creator launch, developer-owned anonymous storage, and avatar asset API calls. The stable public entry is https://viverse-me.com/sdk.js.

What the host page should do

  • Load https://viverse-me.com/sdk.js.
  • Pass a registered SDK Integration ID with data-ac3-partner-id or partnerId.
  • Set data-ac3-mode="sdk-happy-path".
  • Listen for SDK lifecycle and avatar events.
  • Use returned signed URLs for immediate preview only.
  • Keep durable avatar state in a backend, not in browser-only storage.
Do not call account routes such as /api/ac3/me, /api/ac3/files, /api/ac3/download-url, /api/ac3/upload-vrm, or /api/ac3/claim-draft directly from the host page. The embedded SDK and VIVERSE Avatar pages own those flows.

Quick Start

Add a target element and the SDK script, then listen for avatar selections.

<div id="viverse-me-button-slot"></div>
<div id="host-sign-in-button-slot"></div>
<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-target="#viverse-me-button-slot"
  data-ac3-host-sign-in-target="#host-sign-in-button-slot"
  data-ac3-partner-id="partner_..."
  data-ac3-label="Start"
  async
></script>

Then listen for avatar selections:

window.addEventListener('viverse-me:avatar-selected', (event) => {
  const avatar = event.detail.avatar;

  hostScene.loadAvatar(avatar.vrmUrl);
  partnerApi.saveSelectedAvatarReference({
    partnerId: avatar.partnerId,
    userId: avatar.userId,
    tenantId: avatar.tenantId,
    key: avatar.key
  });
});
vrmUrl and thumbnailUrl are short-lived signed URLs for immediate preview. Store stable reference fields such as tenantId, key, and partnerId for durable use.

Getting your snippet

This public guide shows a partner_... placeholder everywhere a real SDK Integration ID is required. You never paste your ID from documentation — you copy a ready-made snippet.

  1. Sign in at VIVERSE Avatar and open Account Settings → My SDK.
  2. Add the website origins where the SDK may run under Allowed Websites.
  3. Use Copy in the SDK Code Snippet box. The copied snippet already contains your SDK Integration ID.
  4. Paste it into your host page and adjust the Happy Path attributes described in this guide.
The My SDK tab also shows a Usage panel with the total number of avatars your end users have created through your integration and the total storage they use, plus a per-website (origin) breakdown.
Keep the SDK Integration ID that comes with the copied snippet. It identifies your account's integration settings and is checked against your Allowed Websites. Wherever this guide shows partner_..., substitute the value from your own snippet.

Requirements

Before a production host can initialize the SDK:

  • The signed-in VIVERSE Avatar account must have an SDK Integration ID.
  • The host website must be saved in that account's Allowed Websites.
  • The production snippet must include data-ac3-partner-id or a manual partnerId.
  • The host CSP must allow VIVERSE Avatar frames, for example frame-src https://viverse-me.com.
  • To use the landing-matched Roboto button exactly, the host CSP must allow style-src https://fonts.googleapis.com and font-src https://fonts.gstatic.com; otherwise the browser uses its sans-serif fallback.
  • Three.js hosts that load the selected VRM with GLTFLoader must also allow the avatar transport in the host page CSP: add https://api.viverse-me.com to connect-src for the signed VRM and thumbnail fetch, add blob: to connect-src and img-src because GLTFLoader reads the VRM's embedded textures through generated Blob URLs, and add https://viverse-me.com to connect-src for the Avatar Controller module and VRMA animations. The import-map CDN origins (for example https://unpkg.com and https://esm.sh) must be allowed under script-src and connect-src. The SDK iframe has its own separate CSP that the host cannot change.

Local development origins such as http://localhost:<port> are supported when added to Allowed Websites.

Website Authorization

The SDK verifies the current window.location.origin before creating its iframe UI.

Production snippets must include a valid partnerId, and that partner ID must allow the host origin. Users manage Allowed Websites from Account settings. Each allowed website is an exact origin, such as https://www.example.com. It must not include paths, query strings, hashes, wildcards, usernames, or passwords.

If authorization fails

  • ViverseMeSDK.init() resolves to null.
  • No SDK iframe UI is created.
  • The SDK logs a warning.
  • viverse-me:authorization-denied is dispatched.

SDK Happy Path

No-login default Developer-owned storage

SDK Happy Path is the game-start embed mode. It uses a no-login anonymous flow with developer-owned storage. Hosts that need an external identity can add the separate Host Sign In flow described below.

<div id="viverse-me-button-slot"></div>
<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-target="#viverse-me-button-slot"
  data-ac3-partner-id="partner_..."
  data-ac3-label="Start"
  async
></script>

When the SDK launch button (or instance.open()) is triggered, a centered modal opens with a character picker. By default the panel lists the public default avatars plus a Create entry. If this browser previously created avatars for the same SDK Integration ID, the panel also displays those anonymous saved avatars (all of them by default):

  • Pick a default → emits viverse-me:avatar-selected with the public default vrmUrl and closes the modal.
  • Create → opens Avatar Creator with a developer-owned anonymous session (no sign-in). On finish it saves the avatar in that developer's storage namespace, generates a thumbnail, emits viverse-me:avatar-selected with short-lived signed vrmUrl/thumbnailUrl, and closes the modal.
  • Saved avatar → requests fresh signed asset URLs and restores an avatar created previously in the same browser.
  • Delete a saved avatar → hover a saved avatar card and click the × button, then confirm in the dialog. The avatar is removed from the developer-owned anonymous storage, freeing a slot.

Anonymous creation is capped at the same avatar limit as accounts (8 by default). When the limit is reached, Happy Path blocks creation and shows the same limit dialog until the end user deletes an avatar.

The SDK stores an opaque anonymous ID in the host browser's localStorage. Clearing site data, changing browser profiles, or changing devices loses the anonymous link unless the host adds an authenticated recovery flow. On mobile, the embedded creator releases the Unity WebGL runtime and restores a thumbnail-only viewer before generating the saved avatar thumbnail.

Host Sign In

If the host has its own account system, keep the default Start button as the anonymous flow and add a separate SDK-owned Host Sign In button.

Provide a button slot with data-ac3-host-sign-in-target; the SDK renders the button with the same definition used by Start. For a Host Sign In only entry, set data-ac3-show-anonymous-button="false". To skip the prompt when the host already knows the user, set data-ac3-remember-host-sign-in="true".

<div id="viverse-me-button-slot"></div>
<div id="host-sign-in-button-slot"></div>
<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-target="#viverse-me-button-slot"
  data-ac3-host-sign-in-target="#host-sign-in-button-slot"
  data-ac3-host-sign-in-label="Host Sign In"
  data-ac3-show-anonymous-button="false"
  data-ac3-remember-host-sign-in="true"
  data-ac3-partner-id="partner_..."
  async
></script>

When the SDK-owned Host Sign In button is clicked, the SDK dispatches viverse-me:host-sign-in-click and calls onHostSignInClick for manual init users. Open your own sign-in dialog from that event, then call openWithHostSignIn(account) after the host identity is known.

window.addEventListener('viverse-me:sdk-ready', (event) => {
  const sdk = event.detail.instance;
  window.addEventListener('viverse-me:host-sign-in-click', () => {
    hostSignInDialog.hidden = false;
  });
  window.addEventListener('viverse-me:host-identity-request', (identityEvent) => {
    identityEvent.detail.respond(hostAuth.currentUserId || '');
  });
  hostSignInForm.addEventListener('submit', async (submitEvent) => {
    submitEvent.preventDefault();
    const account = hostAccountInput.value;
    await sdk.openWithHostSignIn(account);
  });
});
For production, do not trust a browser text field as the account identity. Replace the mock Host Sign In UI with the partner's own authentication flow, verify the user on the partner backend, and only then request a short-lived AC3 host session for that verified external user. The anonymous browser ID used by Start is not sent, so anonymous avatars are not automatically merged into the host account.

Entry combinations

Default to Anonymous Start only. Add Host Sign In only when the host explicitly needs an external identity; the same script supports three entry layouts by adding or removing attributes.

Start + Host Sign In (both):

<div id="viverse-me-button-slot"></div>
<div id="host-sign-in-button-slot"></div>
<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-target="#viverse-me-button-slot"
  data-ac3-host-sign-in-target="#host-sign-in-button-slot"
  data-ac3-partner-id="partner_..."
  data-ac3-label="Start"
  async
></script>

Anonymous Start only: omit the Host Sign In slot and attribute.

<div id="viverse-me-button-slot"></div>
<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-target="#viverse-me-button-slot"
  data-ac3-partner-id="partner_..."
  data-ac3-label="Start"
  async
></script>

Host Sign In only: set data-ac3-show-anonymous-button="false".

<div id="host-sign-in-button-slot"></div>
<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-host-sign-in-target="#host-sign-in-button-slot"
  data-ac3-show-anonymous-button="false"
  data-ac3-partner-id="partner_..."
  async
></script>

Button styling

The default button follows the VIVERSE Avatar landing primary CTA: Roboto at 1.12rem, 700 weight, 0.04em letter spacing, a 48px minimum height, and a pill radius. Hosts can override its CSS custom properties without replacing the SDK-owned button:

:root {
  --viverse-me-button-background: #111;
  --viverse-me-button-color: #fff;
  --viverse-me-button-font: 700 1.12rem/1.2 Roboto, sans-serif;
  --viverse-me-button-letter-spacing: 0.04em;
  --viverse-me-button-min-height: 48px;
  --viverse-me-button-padding: 0 24px;
  --viverse-me-button-border-radius: 999px;
}

Panel Customization

Happy Path panel customization is an allowlisted client option. It does not require an API key and does not accept arbitrary CSS, HTML, JavaScript, or external stylesheet URLs. The SDK sanitizes the options and applies them as copy values, CSS variables, card visibility flags, and saved-avatar UI limits.

The saved avatar card, default avatar cards, and Create card can each be hidden independently. By default the panel fetches and displays all saved avatars; hosts can cap how many are fetched and shown with savedAvatarFetchLimit / savedAvatarDisplayLimit without changing backend retention rules.

<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-target="#viverse-me-button-slot"
  data-ac3-partner-id="partner_..."
  data-ac3-label="Start"
  data-ac3-hp-show-saved-avatar="false"
  data-ac3-hp-show-default-avatars="false"
  data-ac3-hp-show-create-avatar="false"
  data-ac3-hp-saved-avatar-fetch-limit="3"
  data-ac3-hp-saved-avatar-display-limit="2"
  data-ac3-hp-panel-background="rgba(255, 255, 255, 0.92)"
  data-ac3-hp-title-color="#0f172a"
  data-ac3-hp-card-background="rgba(16, 24, 40, 0.82)"
  data-ac3-hp-create-label="Create"
  async
></script>

The same settings can be passed during manual initialization under the nested happyPath option:

const instance = await window.ViverseMeSDK.init({
  mode: 'sdk-happy-path',
  target: '#viverse-me-button-slot',
  partnerId: 'partner_...',
  label: 'Start',
  happyPath: {
    cards: {
      showSavedAvatar: false,
      showDefaultAvatars: false,
      showCreateAvatar: false,
      savedAvatarFetchLimit: 3,
      savedAvatarDisplayLimit: 2
    },
    layout: { maxVisibleRowsBeforeScroll: 2, disableDynamicPanelWidth: false },
    copy: { title: 'Choose a character', createLabel: 'Create' },
    theme: {
      panelBackground: 'rgba(255, 255, 255, 0.92)',
      titleColor: '#0f172a',
      cardBackground: 'rgba(16, 24, 40, 0.82)'
    }
  }
});

Copy & card attributes

Script attributeInit optionDescription
data-ac3-hp-titlehappyPath.copy.titlePanel title.
data-ac3-hp-subtitlehappyPath.copy.subtitleSubtitle before a saved avatar exists.
data-ac3-hp-subtitle-savedhappyPath.copy.subtitleSavedSubtitle when a saved avatar exists.
data-ac3-hp-saved-labelhappyPath.copy.savedLabelSaved avatar card label.
data-ac3-hp-create-labelhappyPath.copy.createLabelCreate card label. Defaults to Create.
data-ac3-hp-default-avatar-1-labelhappyPath.copy.defaultAvatar1LabelFirst default avatar card label.
data-ac3-hp-default-avatar-2-labelhappyPath.copy.defaultAvatar2LabelSecond default avatar card label.
data-ac3-hp-show-saved-avatarhappyPath.cards.showSavedAvatarSet false to hide the saved avatar card.
data-ac3-hp-show-default-avatarshappyPath.cards.showDefaultAvatarsSet false to hide default avatar cards.
data-ac3-hp-show-create-avatarhappyPath.cards.showCreateAvatarSet false to hide the Create card.
data-ac3-hp-saved-avatar-fetch-limithappyPath.cards.savedAvatarFetchLimitHow many saved avatars the panel fetches. Defaults to 1.
data-ac3-hp-saved-avatar-display-limithappyPath.cards.savedAvatarDisplayLimitHow many fetched saved avatars render. Defaults to 1; cannot exceed the fetch limit.
data-ac3-hp-max-visible-rows-before-scrollhappyPath.layout.maxVisibleRowsBeforeScrollGrid rows visible before the grid scrolls. Defaults to 2.
data-ac3-hp-disable-dynamic-panel-widthhappyPath.layout.disableDynamicPanelWidthSet true to rely on your own panelWidth/grid styling.

Theme attributes

A large set of data-ac3-hp-* theme attributes map to happyPath.theme.* — including overlay, panel, title, subtitle, close-button, grid, and card values. Common ones are listed here.

Script attributeInit optionDescription
data-ac3-hp-font-familytheme.fontFamilyPanel font family.
data-ac3-hp-overlay-backgroundtheme.overlayBackgroundModal overlay background.
data-ac3-hp-overlay-backdrop-filtertheme.overlayBackdropFilterModal overlay backdrop filter.
data-ac3-hp-panel-widththeme.panelWidthPanel width.
data-ac3-hp-panel-paddingtheme.panelPaddingPanel padding.
data-ac3-hp-panel-radiustheme.panelRadiusPanel border radius.
data-ac3-hp-panel-backgroundtheme.panelBackgroundPanel background.
data-ac3-hp-panel-bordertheme.panelBorderPanel border color/value.
data-ac3-hp-title-colortheme.titleColorPanel title color.
data-ac3-hp-subtitle-colortheme.subtitleColorPanel subtitle color.
data-ac3-hp-card-backgroundtheme.cardBackgroundAll card backgrounds.
data-ac3-hp-card-bordertheme.cardBorderAll card borders.
data-ac3-hp-card-text-colortheme.cardTextColorAll card text colors.
data-ac3-hp-card-hover-backgroundtheme.cardHoverBackgroundCard hover/focus backgrounds.
data-ac3-hp-card-hover-bordertheme.cardHoverBorderCard hover/focus borders.
data-ac3-hp-grid-template-columnstheme.gridTemplateColumnsCard grid columns.
data-ac3-hp-grid-gaptheme.gridGapCard grid gap.
data-ac3-hp-create-icon-backgroundtheme.createIconBackgroundCreate icon background.
Happy Path only supports vertical grid overflow. Horizontal scrolling is intentionally unsupported in the public SDK surface.

Script Attributes & Init Options

Every script attribute has an equivalent manual init option.

Script attributeInit optionDescription
data-ac3-modemodeUse sdk-happy-path for the no-login game-start picker + developer-owned anonymous storage.
data-ac3-targettargetCSS selector for the host button slot.
data-ac3-partner-idpartnerIdRequired SDK Integration ID.
data-ac3-user-iduserIdStable host user ID. Recommended for production.
data-ac3-session-idsessionIdOptional host session ID.
data-ac3-character-idcharacterIdOptional host character ID.
data-ac3-localelocaleOptional locale forwarded to embedded flows. Defaults to en-US.
data-ac3-labellabelOptional host launch button label.
data-ac3-button-classbuttonClassOptional class added to the host launch button.
data-ac3-show-anonymous-buttonshowAnonymousButtonSet false to hide the anonymous Start button.
data-ac3-host-sign-in-targethostSignInTargetCSS selector for an SDK-owned Host Sign In button slot.
data-ac3-host-sign-in-labelhostSignInLabelHost Sign In button label. Defaults to Host Sign In.
data-ac3-host-sign-in-button-classhostSignInButtonClassClass added to the SDK-owned Host Sign In button.
data-ac3-remember-host-sign-inrememberHostSignInSet true to request a verified host identity before showing sign-in UI.
data-ac3-auto-initn/aSet false for manual initialization.
At least one stable host subject field should be provided for production, usually userId. The backend uses the subject with partnerId to maintain partner active-avatar records.

Manual Initialization

Manual initialization is useful when the host needs to load the SDK dynamically or wire callbacks directly. Set data-ac3-auto-init="false" on the script tag, then call window.ViverseMeSDK.init().

async function loadViverseMeSdk() {
  const script = document.createElement('script');
  script.src = 'https://viverse-me.com/sdk.js';
  script.async = true;
  script.dataset.ac3AutoInit = 'false';
  document.head.append(script);

  await new Promise((resolve, reject) => {
    script.addEventListener('load', resolve, { once: true });
    script.addEventListener('error', reject, { once: true });
  });

  return window.ViverseMeSDK;
}

const sdk = await loadViverseMeSdk();
const instance = await sdk.init({
  mode: 'sdk-happy-path',
  target: '#viverse-me-button-slot',
  partnerId: 'partner_...',
  label: 'Start',
  locale: 'en-US',
  onOpen() { hostScene.pause(); },
  onClose() { hostScene.resume(); },
  onAvatarSelected(event) { hostScene.loadAvatar(event.avatar.vrmUrl); },
  onAuthCleared() { hostScene.clearSelectedAvatar(); },
  onAuthorizationDenied(event) { console.warn(event.message); }
});

The resolved instance exposes:

  • instance.open() — open the picker programmatically.
  • instance.close() — close the modal or embedded flow.
  • instance.openWithHostSignIn(account) — open with a host-verified external user (see Host Sign In).
  • instance.destroy() — remove the SDK button, modal, and all SDK listeners.
If website authorization fails, init() resolves to null, the SDK does not create its iframe UI, and it dispatches viverse-me:authorization-denied.

Events

The SDK dispatches browser events on the host window and also supports equivalent callback options in manual initialization.

EventWhen it firesPayload
viverse-me:sdk-readyThe SDK finished auto-initialization and the instance is ready.{ instance }
viverse-me:openThe SDK opens a modal or embedded flow.SDK state details.
viverse-me:closeThe SDK closes the modal or embedded flow.SDK state details.
viverse-me:avatar-selectedA Happy Path or default avatar is selected.{ avatar } (also flattened on detail) plus animations and controlsHint.
viverse-me:controls-changedSDK control settings change.{ animationEnabled, controlEnabled, controllerSize }
viverse-me:selector-stateThe panel mounts, collapses, expands, or changes placement.{ mode, collapsed, placement, expanded }
viverse-me:host-sign-in-clickThe SDK-owned Host Sign In button is clicked.{ instance, button }
viverse-me:host-identity-requestThe SDK checks whether the host has a verified signed-in user.{ instance, respond }; call respond(externalUserId) or respond('').
viverse-me:auth-clearedA Host Sign In session is cleared.SDK state details with identityType: 'host'.
viverse-me:authorization-deniedThe partner ID and origin are not authorized.partnerId, origin, code, message

Hosts with active rendering, audio, physics, or expensive polling should pause work on open and resume on close.

window.addEventListener('viverse-me:open', () => { hostScene.pause(); });
window.addEventListener('viverse-me:close', () => { hostScene.resume(); });

Host entry panel lifecycle

The host owns the entry panel that mounts the launch button (and any Host Sign In button). The SDK owns avatar confirmation: the Happy Path panel shows its own Enter Game button once an avatar is picked, and clicking it is what dispatches viverse-me:avatar-selected. The host should not add its own confirm or “enter” button — it reacts to that event instead.

  • Show the host entry panel first, with the launch button slot mounted inside it.
  • On viverse-me:avatar-selected, hide the entry panel and enter the experience. If the avatar fails to load, restore the entry panel so the user can retry.
  • On viverse-me:auth-cleared after Host Sign Out, restore the entry panel.
  • Show touch controls only after an avatar is loaded and the SDK panel is closed.
const entryPanel = document.querySelector('.host-entry-panel');
let hasAvatar = false;

window.addEventListener('viverse-me:avatar-selected', async (event) => {
  entryPanel.hidden = true;
  try {
    await hostScene.loadAvatar(event.detail.avatar.vrmUrl, event.detail.avatar.animations);
    hasAvatar = true;
  } catch (error) {
    if (!hasAvatar) entryPanel.hidden = false;
  }
});

window.addEventListener('viverse-me:auth-cleared', () => {
  entryPanel.hidden = false;
});

Avatar Selection Payload

A selected Happy Path avatar payload can include:

{
  tenantId: '[email protected]',
  key: 'accounts/user-example-com/avatars/avatar.vrm',
  fileName: 'avatar.vrm',
  partnerId: 'partner_...',
  userId: 'host-user-123',
  sessionId: 'optional-host-session',
  characterId: 'optional-character',
  vrmUrl: 'https://api.viverse-me.com/api/ac3/download?...',
  thumbnailUrl: 'https://api.viverse-me.com/api/ac3/download?...',
  vrmExpires: 1770000000,
  thumbnailExpires: 1770000000,
  animations: {
    idle: 'https://viverse-me.com/sdk/animations/idle.vrma',
    walk: 'https://viverse-me.com/sdk/animations/walk.vrma',
    run: 'https://viverse-me.com/sdk/animations/run.vrma',
    jump_start: 'https://viverse-me.com/sdk/animations/jump_start.vrma',
    jump_up: 'https://viverse-me.com/sdk/animations/jump_up.vrma',
    jump_loop: 'https://viverse-me.com/sdk/animations/jump_loop.vrma',
    jump_down: 'https://viverse-me.com/sdk/animations/jump_down.vrma'
  },
  controlsHint: { animationEnabled: true, controlEnabled: true, controllerSize: 'default' }
}

Default avatar selections are returned through the Worker sdk/default-avatar proxy, so third-party hosts do not need direct R2 bucket CORS access for shared default assets. If a host manually builds a default-avatar proxy URL, include both partnerId and the current host origin — this is especially important for thumbnails rendered with <img>, because image requests may not include an Origin header.

const url = new URL('/api/ac3/sdk/default-avatar', apiBase);
url.searchParams.set('partnerId', partnerId);
url.searchParams.set('origin', window.location.origin);
url.searchParams.set('file', 'default-avatar-1.png');

Animations & Integration Tiers

The SDK ships a complete set of optional VRMA motion clips. Pick one of two integration tiers depending on whether you want the SDK to run animations and jump physics for you, or you only want the raw assets.

QuestionPick Tier APick Tier B
Do you already have a game loop / physics engine?No — SDK runs everythingYes — SDK only loads VRM + VRMA
Do you need ground/wall collision, slopes, platforms, multi-floor?No (flat ground is fine)Yes (Tier A is flat-plane only)
Want default Space-jump + WASD + orbit camera + mobile joystick?YesYou'll build your own
Embedding into an existing Three.js / Unity / Unreal world?NoYes

Tier A — bundled avatar controller (recommended)

Load avatar-controller.js through window.ViverseMeSDK.avatarControllerUrl and you get walk / run / jump out of the box, including a gravity-driven jump state machine, the Space key, and an on-screen Jump button on touch devices. The controller also creates and drives a perspective camera for you. Tier A is optimised for a single avatar on flat ground and does not include world collision, floor cutouts, slopes/stairs, ceilings, or NPC interaction.

Tier B — clips with your own engine

If you already have your own Three.js / Unity / Unreal pipeline, take the URLs and ignore the controller. The SDK never modifies position.y on its own when you don't use the controller.

window.ViverseMeSDK.animations
// { idle, walk, run, jump_start, jump_up, jump_loop, jump_down } → absolute VRMA URLs

window.ViverseMeSDK.getAnimationUrl('idle');
// → absolute URL to the bundled idle VRMA

Clip roles:

  • idle, walk, run — locomotion loops.
  • jump_start — one-shot crouch / take-off before leaving the ground.
  • jump_up — one-shot rising phase right after take-off.
  • jump_loop — looping in-air pose for variable airtime.
  • jump_down — one-shot landing phase.

The recommended jump sequence is jump_startjump_upjump_loopjump_down. The animations are hosted with permissive CORS so they can be fetched directly from any host origin.

Avatar Controller

The SDK ships an optional ES module that wires VRMA playback (idle / walk / run plus optional gravity jump), pointer / touch orbit, wheel and pinch zoom, WASD or arrow-key movement, a touch-device virtual joystick, the Space key, and a Jump button onto a host-provided Three.js scene.

ModeWhen to useWhat the controller does
physics: 'builtin' (default)Tier A simple integrations.Owns position.y, applies gravity, plays the jump state machine, binds Space, mounts a Jump button on touch, and creates + drives a perspective camera.
physics: 'none'Tier B integrations with their own physics.Never modifies position.y, never binds Space, never mounts a Jump button, never touches the camera, never installs orbit/zoom listeners. Only handles VRM + mixer + idle/walk/run.

Peer dependencies & importmap

The controller never bundles Three.js or the VRM libraries. The host must provide them through an importmap so a single shared Three.js instance is used.

<script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/[email protected]/build/three.module.js",
      "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/",
      "@pixiv/three-vrm": "https://esm.sh/@pixiv/[email protected]?bundle&external=three",
      "@pixiv/three-vrm-animation": "https://esm.sh/@pixiv/[email protected]?bundle&external=three"
    }
  }
</script>
const { createAvatarController } = await import(window.ViverseMeSDK.avatarControllerUrl);

const controller = createAvatarController({
  scene,                    // THREE.Scene (host owns)
  domElement: canvas,       // input target; touch-action set to 'none'
  animations: window.ViverseMeSDK.animations
  // physics: 'builtin', enableJump: true, bindSpaceKey: true — all default on
});

controller.setAvatar(vrm);  // call again on avatar change

function loop() {
  controller.update(clock.getDelta());
  renderer.render(scene, controller.camera); // SDK created the camera for you
  requestAnimationFrame(loop);
}

Complete Happy Path + Three.js setup

For a complete game-ready integration, use Happy Path for avatar selection and let the Avatar Controller own the single-avatar flat-ground camera, movement, VRMA playback, jump physics, desktop input, and touch controls. The host still owns the Three.js scene, renderer, lights, render loop, and VRM loading.

Start with a full-viewport canvas, the shared import map above, and the Happy Path launcher:

<canvas id="game-canvas"></canvas>
<div id="viverse-me-button-slot"></div>

<script
  src="https://viverse-me.com/sdk.js"
  data-ac3-mode="sdk-happy-path"
  data-ac3-target="#viverse-me-button-slot"
  data-ac3-partner-id="partner_..."
  data-ac3-show-anonymous-button="true"
  async
></script>
<script type="module" src="./game.js"></script>

Then use this controller and event wiring in game.js:

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';

const canvas = document.querySelector('#game-canvas');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);

const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75));

scene.add(new THREE.HemisphereLight(0xffffff, 0xe8e8e8, 2.8));
const keyLight = new THREE.DirectionalLight(0xffffff, 1.35);
keyLight.position.set(4, 7, 5);
scene.add(keyLight);

const loader = new GLTFLoader();
loader.crossOrigin = 'anonymous';
loader.register((parser) => new VRMLoaderPlugin(parser));

let controller;
let currentVrm;
let hasAvatar = false;
let avatarLoading = false;
let pausedBySdk = false;
let controlsHint = {};
let frameId = 0;
const clock = new THREE.Clock();

const sdk = window.ViverseMeSDK?.instance || await new Promise((resolve) => {
  window.addEventListener('viverse-me:sdk-ready', (event) => {
    resolve(event.detail.instance);
  }, { once: true });
});

const { createAvatarController } = await import(
  window.ViverseMeSDK.avatarControllerUrl
);

controller = createAvatarController({
  scene,
  domElement: canvas,
  animations: window.ViverseMeSDK.animations,
  enableAnimation: true,
  enableControl: true,
  physics: 'builtin'
});
controller.setJoystickVisible(false);
resize();
resume();

window.addEventListener('viverse-me:open', () => {
  pausedBySdk = true;
  controller.setEnableControl(false);
  controller.setJoystickVisible(false);
  cancelAnimationFrame(frameId);
  frameId = 0;
});

window.addEventListener('viverse-me:close', () => {
  pausedBySdk = false;
  applyControls();
  controller.setJoystickVisible(hasAvatar && !avatarLoading);
  resume();
});

window.addEventListener('viverse-me:avatar-selected', async (selectionEvent) => {
  const avatar = selectionEvent.detail.avatar;
  if (!avatar?.vrmUrl) return;

  avatarLoading = true;
  controller.setJoystickVisible(false);
  await Promise.resolve();
  sdk.showLoading?.('Loading avatar...');
  try {
    const gltf = await loader.loadAsync(avatar.vrmUrl);
    const nextVrm = gltf.userData.vrm;
    if (!nextVrm) throw new Error('Selected asset is not a VRM.');

    VRMUtils.removeUnnecessaryVertices(gltf.scene);
    VRMUtils.removeUnnecessaryJoints(gltf.scene);
    controller.setAvatar(null);
    if (currentVrm) VRMUtils.deepDispose(currentVrm.scene);

    currentVrm = nextVrm;
    controller.setAvatar(nextVrm, avatar.animations || null);
    hasAvatar = true;
    applyControls(avatar.controlsHint);
  } catch (error) {
    console.error('Unable to load selected avatar:', error);
  } finally {
    avatarLoading = false;
    sdk.hideLoading?.();
    controller.setJoystickVisible(hasAvatar && !pausedBySdk);
  }
});

window.addEventListener('viverse-me:controls-changed', (controlsEvent) => {
  applyControls(controlsEvent.detail.controls || controlsEvent.detail);
});

function applyControls(hint = {}) {
  if (!controller) return;
  controlsHint = { ...controlsHint, ...hint };
  const animationEnabled = controlsHint.animationEnabled !== false;
  controller.setEnableAnimation(animationEnabled);
  controller.setEnableControl(
    !pausedBySdk && animationEnabled && controlsHint.controlEnabled !== false
  );
  if (controlsHint.controllerSize !== undefined) {
    controller.setControllerSize(controlsHint.controllerSize);
  }
}

function render() {
  frameId = 0;
  if (pausedBySdk || !controller) return;
  controller.update(Math.min(clock.getDelta(), 0.05));
  renderer.render(scene, controller.camera);
  frameId = requestAnimationFrame(render);
}

function resume() {
  if (frameId || pausedBySdk || !controller) return;
  clock.getDelta();
  frameId = requestAnimationFrame(render);
}

function resize() {
  if (!controller) return;
  const width = window.innerWidth;
  const height = window.innerHeight;
  renderer.setSize(width, height, false);
  controller.camera.aspect = width / height;
  controller.camera.updateProjectionMatrix();
}

window.addEventListener('resize', resize);
Keep the SDK lifecycle wiring. Pausing the loop and hiding controller UI while Happy Path is open prevents keyboard, pointer, touch, audio, and GPU work from continuing behind the embedded flow. Apply both avatar.animations and later viverse-me:controls-changed payloads so account and developer settings remain authoritative.
This recipe uses physics: 'builtin' and is intended for one avatar on a flat ground plane. Use physics: 'none' and your own camera, collisions, movement, jump, and control UI for a world with walls, slopes, stairs, platforms, multiple floors, or an existing physics engine.

Constructor options

OptionDefaultNotes
scenerequiredTHREE.Scene provided by the host.
domElementrequiredCanvas / element used for pointer, wheel, touch, and gesture listeners.
cameraundefinedOptional PerspectiveCamera. Tier A: omit to let the SDK create one; pass one to drive a host-owned camera. Tier B: SDK never touches it.
physics'builtin''builtin' owns position.y + jump; 'none' leaves both untouched.
enableJumptrueOnly meaningful when physics: 'builtin'. Disables Space binding, Jump button, and jump machine.
bindSpaceKeytrueBind Space to jump. Set false if Space is used elsewhere.
gravity18 m/s²Game-feel gravity. 9.81 matches Earth.
jumpVelocity6.5 m/sInitial upward velocity. Height ≈ v² / (2g).
variableJumpCutFactor0.45Rising velocity multiplier when the jump input releases early.
groundY0Y of the single ground plane. Tier A is flat-plane only.
enableAnimationtrueMixer master switch. Disabling also forces input off.
enableControltrueMaster switch for orbit / zoom / movement / Space / Jump.
input.joystick'auto''auto' mounts on touch devices; true always; false never.
moveSpeed2.4 m/sWalking speed. Run uses moveSpeed * runSpeedMultiplier.
runSpeedMultiplier2.0Multiplier when double-tap to run is active.
cameraOffset1.35Vertical look-at offset above the anchor (head height).
initialCameraDistance4.5Starting orbit distance, clamped to min/max.
minCameraDistance1.75Closest zoom.
maxCameraDistance6.5Farthest zoom.

Public methods

  • update(delta) — advance mixer, VRM, camera, movement, and jump physics; call once per frame.
  • setAvatar(vrm, animations?) — attach or replace the current VRM. Pass null to detach.
  • setAnimations({ idle, walk, run, jump_* }) — replace VRMA URLs and reload clips.
  • setEnableAnimation(boolean) — toggle clip playback.
  • setEnableControl(boolean) — toggle orbit / zoom / movement / Space / Jump input.
  • setEnableJump(boolean) — toggle the jump machine, Space binding, and Jump button. No-op when physics: 'none'.
  • jump() / cutJump() — programmatic jump trigger and variable-height release.
  • setCameraDistance(distance) — clamp the current orbit distance.
  • setJoystickVisible(boolean) — show/hide the virtual joystick and the Jump button together.
  • setControllerSize('default' | 'small') — switch on-screen control footprint.
  • dispose() — detach listeners, restore touch-action, stop the mixer, remove joystick / Jump button and the anchor.

Read-only getters

  • camera — the camera the SDK drives in Tier A; the host-provided camera or null in Tier B.
  • anchor, visualRootTHREE.Group parents around the VRM. Move / rotate anchor from your own physics in Tier B.
  • cameraState{ yaw, pitch, distance, minDistance, maxDistance }.
  • enableAnimation, enableControl, enableJump, physicsMode, isGrounded, jumpPhase.

Jump Behavior

The jump state machine is gravity-driven: pressing Space (or the on-screen Jump button) sets vertical velocity to jumpVelocity, then each frame applies vy -= gravity * dt until the avatar returns to groundY. Releasing Space while still rising multiplies vy by variableJumpCutFactor for short hops.

  • Standing jump (no movement input on Space press) — plays the full jump_startjump_upjump_loopjump_down sequence. Horizontal movement stays disabled during jump_start and jump_down.
  • Running jump (movement input present) — skips jump_start and jump_down so the legs keep cycling. Mid-air control is at 60% of ground speed (steering only).

If the player provides movement input while a standing-jump jump_down crouch is still playing, the controller cuts the landing clip short and blends to walk / run immediately. On coarse-pointer devices the controller mounts a virtual joystick and — when physics: 'builtin' and enableJump: true — a Jump button. Both are hidden by default; toggle them with setJoystickVisible(true).

Avatar URL Lifecycle

Treat vrmUrl and thumbnailUrl as preview transport, not durable storage.

  • Use signed URLs immediately to render or preview the avatar.
  • Check expiry fields before reusing a cached URL.
  • Store stable references in the partner backend.
  • Resolve fresh signed URLs server-side when restoring a user's selected avatar later.

A browser-only integration may keep a short-lived local preview cache, but a production partner should not rely on browser storage as the source of truth.

Server-side Active Avatar Fetch

Partner backends can resolve the current selected avatar for a host subject through the protected partner API. Generate a partner-scoped API token for your SDK Integration ID and store it only on your backend.

Manage API tokens

  1. Sign in to VIVERSE Avatar and open Account Settings → My SDK.
  2. Select API Tokens, then Generate Token.
  3. Generate and copy the token when it appears. The full value is shown only once.
  4. Store it as a server-side secret. If it is lost, revoke it and generate a replacement.

You can keep up to five active tokens for rotation. Tokens remain active until revoked. To rotate without downtime, configure and verify the replacement on your backend before revoking the old token.

GET https://api.viverse-me.com/api/ac3/partner/active-avatar?userId=host-user-123
Authorization: Bearer <PARTNER_API_TOKEN>

The token determines which Partner ID can be accessed. The subject can be userId, sessionId, or characterId. The response returns the stable avatar reference plus fresh signed asset URLs when an active avatar exists.

async function getViverseMeActiveAvatar({ userId }) {
  const url = new URL('https://api.viverse-me.com/api/ac3/partner/active-avatar');
  url.searchParams.set('userId', userId);

  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.PARTNER_API_TOKEN}` }
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch active avatar (${response.status})`);
  }
  return response.json();
}
Never expose PARTNER_API_TOKEN in browser code. Revocation takes effect immediately and cannot be undone.

Security Notes

  • Do not put PARTNER_API_TOKEN or admin tokens in browser code.
  • Do not call account, draft, upload, claim, or download-url APIs directly from the third-party page.
  • Use SDK events for browser preview and partner backend APIs for durable restoration.
  • Keep production embeds on https://viverse-me.com/sdk.js unless a preview origin has been explicitly configured.
  • Allow VIVERSE Avatar in the host site's frame policy or CSP.
  • Safari, private browsing, and enterprise privacy settings can still block third-party cookies or storage used by embedded flows.

Troubleshooting

init() returns null

Check that partnerId is present and the current origin is saved as an Allowed Website.

The Happy Path panel does not show a saved avatar

The browser may not have a stored Happy Path anonymous ID for this SDK Integration ID, or site data may have been cleared. The panel should still show public default avatars unless the host hides them with happyPath.cards.showDefaultAvatars.

A cached avatar URL stops loading

Signed URLs expire. Use the partner backend active-avatar API to resolve a fresh URL, or wait for the SDK to emit a fresh viverse-me:avatar-selected event.

A default thumbnail URL returns SDK_ORIGIN_REQUIRED

Add origin=window.location.origin to the sdk/default-avatar proxy URL. This is usually needed for thumbnails loaded through <img>.

The avatar loads but has no textures or materials

The host page CSP connect-src is blocking blob:. GLTFLoader turns the VRM's embedded textures into Blob URLs and fetches them, so a missing blob: allowance loads the mesh geometry without materials. Add blob: to connect-src (and img-src). This is a host-page CSP fix; the SDK iframe keeps its own separate CSP that the host cannot change.