Skip to main content

API Summary

This page summarizes the public API exposed by the Genies Web SDK, organized by functional layers. Each layer builds on the previous one — developers can stop at whichever level matches their needs.

What is NAF?

NAF (Native Avatar Framework) is a WebGL SDK for loading, animating, and rendering Genies avatars in the browser. It wraps a C++ animation engine compiled to WebAssembly (via Emscripten) and exposes a clean JavaScript API on top of Three.js.

Initialize and Instance

The SDK is returned as a NafInstance from initialize(). The APIs listed here are also available as standalone imports from the naf entry point. TypeScript definitions live in sdk/src/naf.d.ts. This guide intentionally focuses on consumer-facing APIs; internal runtime hooks may exist in the package for sandbox/build integration but are not part of the recommended app integration surface.

import { initialize } from 'naf';
const naf = await initialize({ canvas, initScene });

Layer 0 — Core & Runtime

Runtime bootstrap, global config, and the per-frame tick. Required by every other layer.

Lifecycle

APIPurpose
initialize(options?)Loads WASM, mounts filesystem, loads asset-resolver + texture configs, optionally runs initScene(canvas, glContext). Returns a NafInstance. Idempotent.
update(deltaTime, renderer)Per-frame SDK tick — processes texture uploads, resets GL state, runs first-frame init. Call once per frame before rendering.
getCanvas() / getContext()NAF-managed canvas element and WebGL2 context.
Initialize Options

These are the initialize options:

  • canvas?: HTMLCanvasElement;
  • initScene?: (canvas: HTMLCanvasElement, glContext: WebGL2RenderingContext) => void;
  • assetResolverConfigPath?: string;
  • textureSettingsPath?: string;

Configuration

APIPurpose
setBearerToken(token)Auth token used by the native REST service (smart avatar behavior, asset resolver).
setEnvironment(env)'dev' \| 'prod'. Reloads the asset resolver config for the requested environment. Async.
getEnvironment()Current environment.
setWasmBasePath(path)Override WASM asset URL (auto-detected by default).

Diagnostics

APIPurpose
createLogger(tag)Tagged logger used throughout the SDK.
setLogLevel(LogLevel.X)Global log verbosity. Values: LogLevel.DEBUG \| INFO \| WARN \| ERROR \| NONE.
showDebugWindow() / hideDebugWindow() / toggleDebugWindow()On-screen log window.

Events

All lifecycle events are emitted through a built-in pub/sub bus:

naf.on('avatar:ready', ({ avatarId, mesh, controller }) => {...});
naf.off('avatar:ready', handler);
EventPayload
initialized{ instance }
avatar:silhouette{ avatarId?, mesh, controller? } (Layer 3 only)
avatar:ready{ avatarId, mesh, controller?, previousMesh } — fires from both loadAvatar (Layer 1) and loadAvatarProgressive (Layer 3). previousMesh is the silhouette mesh in Layer 3, null in Layer 1.
avatar:loaded{ avatarId, handle } — fires from both loadAvatar (Layer 1) and loadAvatarProgressive (Layer 3). For Layer 1, handle.controller is null — attach animation separately via attachAnimation.
avatar:lod-upgrade{ avatarId, targetLod }
avatar:rebuild-start{ avatarId, mesh } (fired before the old mesh / controller are torn down during equipAsset/unequipAsset)
avatar:rebuilt{ avatarId, mesh, controller } (fired after equipAsset/unequipAsset)
avatar:unloaded{ avatarId }
avatar:behavior-loaded{ avatarId, controller }
avatar:channel-start{ avatarId, channel, guid, duration, isLooping } — fires when a channel enters TransitionIn.
avatar:channel-end{ avatarId, channel, guid, completed } — fires when a channel empties. completed: true if it ran through its transition-out naturally; false if pre-empted.
avatar:error{ avatarId?, avatarDefinition?, error, phase }
Bus Methods

These are the bus methods:

  • on(event, handler) (returns unsubscribe)
  • once(event, handler)
  • off(event, handler)
  • emit(event, data)

The emit method exported for adapters/tests and sandbox tooling; normal app code should listen to SDK lifecycle events rather than synthesize them.

Event Error Phases

The phase field on avatar:error identifies where in the pipeline the error occurred:

PhaseFatal?avatarId present?Description
avatar-loadYesNo (avatarDefinition included instead)loadAvatarRaw failed — all asset resolver fallbacks exhausted. The promise rejects.
nativeNoNoNative-side error forwarded during load (e.g. a single resolver endpoint failing before fallback). Informational only.
silhouetteNoNoSilhouette mesh construction failed. Avatar load continues.
silhouette-animationNoNoFailed to animate the silhouette. Avatar load continues.
animationNoYesAnimation controller or idle clip setup failed after avatar loaded.
behaviorNoYesSmart avatar behavior activation failed.
lod-upgradeNoYesA background LOD texture upgrade failed. Avatar remains at previous LOD.
rebuildYesYesFull rebuild (equipAsset/unequipAsset) failed. The promise rejects and the definition is rolled back.
rebuild-animationNoYesAnimation controller failed during rebuild staging. Rebuild continues without animation.
rebuild-behaviorNoYesBehavior re-activation failed after rebuild.
note

Fatal means the calling promise rejects. Non-fatal errors are emitted for observability but the pipeline continues.

Authentication

The SDK supports two authentication modes:

  • External Auth: lets the developers manage the token via setBearerToken().
  • Native Auth: lets the SDK manage everything automatically.
import { setBearerToken } from '@geniesinc/genies-naf-webgl';

// Set token before loading avatars
setBearerToken(myAccessToken);

// Update when your token refreshes
onTokenRefresh((newToken) => {
setBearerToken(newToken);
});

// Clear to switch back to native auth
setBearerToken('');

Layer 1 — Basic Avatar

Load a single avatar at a fixed LOD. No animation, no progressive loading. Use this when you just need a static mesh.

APIPurpose
loadAvatar(avatarDefinition, options)Loads avatar assets and returns a NafAvatarHandle.
preloadAssets(avatarDefinition, lodArray?)Warm the asset cache without building a mesh.
getLoadedAvatars()Array of currently-loaded handles.
pickAvatar(raycaster, options?)Raycast-pick the topmost loaded avatar under a pre-built THREE.Raycaster. Skips handles whose status is not 'ready'. Returns { handle, intersection } or null.
Load Avatar Options

These are the loadAvatar options:

  • renderer: WebGLRenderer;
  • meshOptions?: CreateMeshOptions;
  • lod?: number; (0 = best detail)
  • signal?

The progressive loader takes an LOD progression array such as [2, 0]. Pass an AbortSignal to cancel a stale load before it publishes scene/native resources.

Avatar Definition

Avatar definitions are JSONs that describes an avatar's assets:

{
"assets": [
{ "id": "asset-abc-123", "version": "1" },
{ "id": "asset-def-456", "version": "1" }
],
"shapeAttributes": { ... }
}

NAF Avatar Handle

NafAvatarHandle is returned by every load function. It owns the native entities and the Three.js mesh.

MemberPurpose
avatarIdNumeric ID assigned by native.
meshTHREE.Group containing the skinned meshes.
controllerAnimation controller (set once attachAnimation or progressive idle is used).
statusLifecycle state: 'loading' \| 'ready' \| 'unloaded'.
isReadyTrue once setup is complete and the handle is safe to drive each frame.
isUnloadedTrue once unload() has been called.
userDataConsumer-defined metadata bag (Record<string, unknown>). The SDK never reads or writes it.
getEquippedAssets()Returns a shallow copy of the avatar's current asset list (first entry is the base rig).
getDefinition()Returns the avatar definition reflecting current state — each asset's lod is overridden to the actually-loaded LOD, which can lag behind the originally requested array if an upgrade hasn't completed or failed.
getRefitReport()Utility mesh diagnostics: { hasUtilityMesh, utilityMeshCount, cageCount, deformedMeshCount }, or null if refitting didn't run. Counts utility meshes present — including ones not actively refitting
getUtilityMeshData()Returns the geometry ({ vertexCount, indices, positions, normals, uvs }[]) of the avatar's utility meshes — normally filtered out of the rendered output. Returns null if refitting didn't run.
currentLodLOD currently rendering (number, or null if not tracked).
equipAsset({ id, version?, lod? })Adds a combinable wearable asset and rebuilds the avatar in place. version defaults to '1'; lod defaults to the avatar's current LOD so the wearable matches the body resolution. lod may also be an array (load-order, e.g. [2, 0]) for progressive equip: the wearable appears at the first LOD immediately, then the remaining LODs stream in the background as texture-only upgrades (no mesh rebuild — the animation keeps playing). Resolves once the first LOD is equipped.
unequipAsset(assetId)Removes a previously-equipped asset by id and rebuilds the avatar. The base rig (first asset) cannot be unequipped.
replaceAssets(assetIdsToRemove, assetEntry?)Removes one or more equipped assets and optionally adds a replacement in a single rebuild. Useful for mutually-exclusive presets.
setColor(colorId, color, options?)Sets a runtime material color through native ColorEditor without equipping a material-data asset or rebuilding the avatar mesh.
unsetColor(colorId, options?)Clears a runtime ColorEditor color override.
unload()Frees native entities, disposes the controller, unregisters LOD callbacks. Idempotent.
note

Per-frame loops iterating getLoadedAvatars() should skip handles where isReady is false — a handle is registered as soon as the native entities exist, but animation/behavior/LOD wiring may still be in progress.


Layer 2 — Animation

Attach an animation controller to an existing handle and play clips, gesture tags, emotions.

APIPurpose
attachAnimation(handle, { syncOptions? })Creates an AvatarAnimationController and stores it on handle.controller. Returns the controller.
loadAnimation(animationId, version?)Fetch a raw animation buffer from the asset resolver.
categorizeClips(clipNames)Split clip names into { idleClips, gestureClips }.
Attach Animation Sync Options

These are the attachAnimation syncOptions:

  • enableSkeleton?: boolean;
  • enableBlendShapes?: boolean;
  • convertUnityToThreejs?: boolean;

Avatar Animation Controller

The AvatarAnimationController is a high-level controller for handling Avatar animations.

Static Factory

MethodPurpose
AvatarAnimationController.create(mesh, options?).Create and initialize a AvatarAnimationController.
hasAnimationTrue while the controller has animation and has not been disposed.
isPausedWhen true, update() and syncPose() short-circuit without advancing native playback.
isDisposedTrue after dispose() has released the native controller and pose resources.

Clip Loading

MethodPurpose
loadClip(path)Load from a local file path.
loadClipFromAssetResolver(animationId, version?)Load via asset resolver.
loadClipFromBuffer(buffer, cacheKey)Load from a Uint8Array.

Playback

MethodPurpose
setIdle(clip)Set the looping base clip.
getIdleTime() / setIdleTime(t)Get/set idle animation playback time. SDK uses this internally for silhouette → avatar handoff.
playGesture(clip, options?)One-shot gesture. Options: onComplete, blendInTime, blendOutTime, holdTime, isSpecialGesture.
setTalking(clip)Set the talking layer clip.
setEmotion(clipMin, clipMax)Set emotion layer clips (min/max intensity).
playEmotionBody(clip, options?)Play full-body emotion.
playGestureTag(tag, onComplete?, startTime?)Play a gesture by behavior tag. startTime (seconds, default 0) begins the clip mid-way.
playEmotion(tag, intensity)Play an emotion by tag.
playGUID(guidString, startTime?)Play a clip by GUID. startTime (seconds, default 0) begins the clip mid-way.

Smart Avatar

MethodPurpose
loadSmartAvatarBehavior(slug, useRH?, idleAssetId?, talkingAssetId?, breathingAssetId?, onLoaded?)Native REST fetch + activation. Returns false if the controller has already been disposed.
loadSmartAvatarBehaviorFromConfig(config, useRH?, idleAssetId?, talkingAssetId?, breathingAssetId?, onLoaded?)Use a pre-fetched smart-avatars JSON. Returns false if the controller has already been disposed.
getAvailableAnimTags(){ gestures, emotions } from the loaded behavior, or null.

Weights

MethodPurpose
setIdleWeight(w)Idle layer weight.
setTalkingWeight(w)Talking layer weight.
setEmotionWeight(w, intensity)Emotion layer weight + intensity.
setBoredomWeight(w)Boredom layer weight.
setGestureWeight(w)Gesture layer weight.
setLookWeight(w)Head look-at weight (independent of eye gaze).
setGazeWeight(w)Eye-gaze weight (independent of head look-at; defaults fully on).
setBreathWeight(w, speed?)Breathing layer weight and speed (default 1.0). Requires a breathing clip loaded via behavior.
setAutoBoredom(enabled)Enable/disable automatic boredom idle variations.
getChannelWeights()Returns { boredom, gesture, specialGesture }.
getLookWeight()Current head look-at weight.
getGazeWeight()Current eye-gaze weight.
getTalkingWeight()Current talking (lipsync) blend weight.

Debug

MethodPurpose
getCurrentBoredGUID()GUID of the currently playing bored animation (empty if none).
getCurrentSecondaryGUID()GUID of the currently playing secondary/emotion-body animation.
getCurrentGestureGUID()GUID of the currently playing gesture animation.
getCurrentEmotion()Name of the current active emotion (empty if none).

Look-at

MethodPurpose
getHeadBone()Cached head bone.
setLookAtCamera(cameraLocalPosition, headPosition)Drive head/eye look-at.

Dynamics (GENIES_dynamics chains)

Configs can be baked into the asset (applied natively at build time) or supplied at runtime from JSON:

MethodPurpose
appendDynamicsGroup(config)Add a dynamics chain from a GENIES_dynamics JSON object or string. Returns true when the native side accepts it.
getLastDynamicsGroupError()Last native parse/build error from appendDynamicsGroup (empty string if none).
clearDynamics()Remove all runtime dynamics groups and disable simulation.
resetSimulation()Snap the simulated state back to the driven pose.
setDynamicsEnabled(enabled)Enable/disable dynamics updates.
setDynamicsWeight(w) / getDynamicsWeight()Blend weight between driven pose and simulation, [0, 1].

DynamicsConfigLoader wraps appendDynamicsGroup in the extension-config-loader pattern for loading configs from a URL, local file, or object; clear() maps to clearDynamics():

const loader = new DynamicsConfigLoader(controller);
await loader.loadFromUrl('/configs/skirt-chains.json'); // or loader.loadFromObject(json)

Rig recipe (GENIES_rig_recipe)

MethodPurpose
setRigRecipeConfig(config)Replace the active rig recipe from a GENIES_rig_recipe JSON object or string. Full replacement, not a merge — it overrides whatever was active, including a recipe baked into the asset's extension. Returns true when the native side accepts it.
getLastRigRecipeConfigError()Last native parse/build error from setRigRecipeConfig (empty string if none).

RigRecipeConfigLoader is the same loader pattern for rig recipes:

const loader = new RigRecipeConfigLoader(controller);
loader.applyConfig(rigRecipeJson); // → boolean

Both loaders share the exported ExtensionConfigLoader base.

Frame Update

MethodPurpose
update(deltaTime)Advance the controller.
syncPose()Sync computed pose to the Three.js mesh.
isPausedGetter/setter. When true, update() and syncPose() short-circuit without touching native state or the mesh. Native playback time does not advance while paused. Intended for off-screen avatars.
hasAnimationTrue while animation is active and the controller has not been disposed.
isDisposedTrue after dispose() releases native resources.
poseSynchronizerExposes enableSkeleton / enableBlendShapes runtime toggles.
dispose()Release native resources.

Channel introspection

Every playback layer (idle, boredom, gesture, breath, baseEmotion, emote1–4, talking, secondary) runs its own state machine: Empty → TransitionIn → Playing → TransitionOut → Empty. Two APIs expose this state.

MethodPurpose
getChannelInfo(channel)Returns { guid, status, duration, currentTime, speedMultiplier, isLooping } for a channel. Accepts lowercase string names ('gesture', 'boredom', 'idle', 'secondary', 'breath', 'baseEmotion', 'emote1'..'emote4', 'talking'). Useful when a clip was started via a tag-based API (e.g. playGestureTag) and you never held a clip reference.
getChannelCurrentTime(channel)Returns the channel's current playback time in seconds. Equivalent to getChannelInfo(channel).currentTime but skips the struct allocation — preferred for tight render-loop polling.
setChannelCurrentTime(channel, time)Seek a channel forward or backward, in seconds. The per-frame update(dt) only advances time, so any backward scrub or jump-to-frame must go through this API. No clamping: caller is responsible for keeping time within [0, duration) (or wrapping for looping channels). The new time takes effect on the next pose evaluation.

Lifecycle events are emitted on the global bus (see Events table above):

  • avatar:channel-start — channel entered TransitionIn.
  • avatar:channel-end — channel returned to Empty. completed: true if it ran through its transition-out naturally; false if cut short (pre-empted by a higher-priority gesture, or cleared).

A consecutive channel-endchannel-start on the same channel is the "clip changed" signal (idle cycling, boredom variants). Playback progress is not emitted — poll getChannelInfo(channel).currentTime from your render loop instead.

currentTime semantics. For looping channels (idle, breath, baseEmotion, talking) currentTime is wrapped into [0, duration) so consumers see a loop-local phase. For non-looping channels (gesture, boredom, emotionBody, emote1–4) it advances 0 → duration and then the channel transitions out.

duration semantics — gesture channel caveat. For most channels channelInfo.duration === clip.duration. The gesture channel is the exception: it reports clip.duration + holdTime, where holdTime is the period the clip's final pose is held before blending out. So for a 2 s gesture clip with holdTime = 15, channelInfo.duration === 17.

holdTime configurability.

Entry pointholdTime sourceJS-configurable?
playGesture(clip, { holdTime })Per-call argument (default 1.0 in the JS wrapper).Yes.
playGestureTag(tag)Hard-coded BehaviorTimingConfig::gestureHoldTime (15 s).No — not currently exposed. Resolve the tag → clip yourself and call playGesture(clip, { holdTime }) if you need a different value.

Layer 3 — Progressive Loading

The recommended entry point for most developers. Handles silhouette → LOD upgrade pipeline, animation attachment, idle-time transfer, and optional behavior activation in one call.

info

loadAvatarProgressive options use lod?: number[] for the progression order, for example [2, 0]. LOD upgrade batching uses avatarDefinition.assets automatically.

const handle = await naf.loadAvatarProgressive(avatarDefinition, {
renderer,
scene, // optional — SDK adds/removes meshes for you
lod: [2, 0], // LOD progression
meshOptions: { enableSkinning: true },
position: { x: 0, z: 2 }, // applied to both silhouette and avatar
rotation: { y: Math.PI }, // applied to both silhouette and avatar
idle: { id: 'idle_neutral', version: '1' },
syncOptions: { enableSkeleton: true, enableBlendShapes: true },
behavior: { slug: 'default', talking: 'talking_clip', breathing: 'breathing_clip', config, onLoaded },
signal: abortController.signal,
onSilhouette: ({ mesh, controller }) => {...},
onReady: ({ avatarId, mesh, controller, previousMesh }) => {...}, // awaited
onLodUpgrade: ({ targetLod }) => {...},
onError: ({ error, phase, avatarId, avatarDefinition }) => {...}, // see "avatar:error phases" table

});

Key Features

  • LOD cache awareness — skips silhouette when a usable LOD is already cached.
  • Gap-free mesh swap — silhouette is removed only after the final mesh is in the scene.
  • Idle time transfer — animation phase is continuous across the silhouette handoff.
  • Position/rotation — set once via options; SDK applies to both silhouette and avatar meshes. The consumer doesn't need to know they are different meshes.
  • Cancellation — pass signal to abort a stale progressive load and suppress late silhouette, LOD, texture upload, and behavior callbacks.
  • Shader lifecycle — SDK owns attachAvatar / detachAvatar / syncTextures / rebuild material swaps. Consumer just enables effects and sets params.
  • Hybrid lifecycleonReady is awaited (flow control); events are always emitted (observation).
  • Composable — omit idle and use attachAnimation + activateBehavior manually if you need finer control.
info

Callbacks have event equivalents:

  • avatar:silhouette
  • avatar:ready
  • avatar:lod-upgrade
  • avatar:error

Equip / Unequip (combinable wearables)

A progressively-loaded handle can swap its wearable assets at runtime:

await handle.equipAsset({ id: 'WardrobeGear/recAbC123', version: '1' });
// ...
await handle.unequipAsset('WardrobeGear/recAbC123');
Notes
  • The first asset in the avatar definition is the base rig and cannot be unequipped.
  • equipAsset rejects if the asset id is already equipped.
  • Only one rebuild may be in flight per handle — concurrent calls reject.
  • LOD on rebuild: an incremental equip downloads only the new wearable (at its lod, defaulting to the avatar's current LOD); the body is reused, not re-downloaded. A full rebuild reloads the whole avatar at the current LOD in a single shot — it does not replay the silhouette → upgrade progression.

Layer 4 — Behavior

Smart-avatar behavior system on top of the animation controller — drives idle selection, gesture triggers, and talking from a config or slug.

APIPurpose
activateBehavior(controller, { config?, slug?, talking?, breathing?, transferIdleTimeFrom?, onLoaded?, signal?, isCurrent? })Activate behavior on a controller. Returns Promise<boolean>; false means activation was skipped because the controller was already stale, disposed, or cancelled.
  • config — pre-fetched smart-avatars JSON (takes precedence over slug).
  • slug — let native fetch the config via REST (requires setBearerToken).
  • talking — talking animation asset ID.
  • breathing — breathing animation asset ID. Enables the breathing layer in the native SmartAvatarLoader.
  • transferIdleTimeFrom — controller to copy idle phase from (silhouette handoff).
  • onLoaded — fires when the behavior is fully ready.
  • signal — optional AbortSignal; late completions are ignored after abort.
  • isCurrent — optional guard callback; return false when the controller no longer belongs to the active avatar/load.
tip

For most developers this is wired automatically via:

loadAvatarProgressive({ behavior }).


Cross-cutting APIs

Cache Persistence

APIPurpose
syncFilesystem()Sync the WASM virtual FS to IndexedDB. Returns Promise<boolean>. Normal load/prefetch paths call this when needed; use it only after custom cache writes.
writeFileToVFS(data, path)Write binary data (Uint8Array) directly into the WASM virtual filesystem at an absolute path (e.g. /gnpath/localResolver/...).

Advanced runtime and low-level hooks

These are exported and typed, but most consumers should not need them. They exist for sandbox tooling, custom integrations, or code that bypasses the high-level load APIs.

APIPurpose
ensureInitialized()Throws if the SDK has not been initialized. SDK methods call this internally; useful only in wrappers/adapters.
resetRuntime()Clears native runtime caches/state. Call only after all avatars are unloaded or when deliberately resetting a custom integration.
setAssetResolverConfig(config)Install an asset resolver config object manually. initialize() / setEnvironment() handle this in normal apps.
setTextureConfig(textureSettings)Install texture format preferences manually. initialize() handles this in normal apps.
setTargetCoordSpace(forward, up, right)Declare the native target coordinate space. Called automatically at initialize() with the Three.js basis (forward +Z, up +Y, right −X), so meshes are delivered and animation clips are baked in Three.js space natively
registerLodCallbacks(avatarId, callbacks) / unregisterLodCallbacks(avatarId)Low-level native LOD callbacks. Prefer loadAvatarProgressive({ onLodUpgrade }) and SDK events unless building a custom loader.
createMultiMeshAvatar(meshDataArray, sharedSkeleton, renderer, options?)Low-level Three.js mesh construction from native mesh data. Valid for custom loaders/rendering paths.
updateMaterialTextures(mesh, updatedMeshData, renderer)Low-level texture/material refresh from native mesh data. Valid for custom LOD/rendering paths.

Native material textures

Materials come from one of two pipelines, identified by material.userData.materialModel:

  • 'gltf' — the current pipeline. Standard glTF fields map to their Three.js equivalents
  • 'legacy' — deprecated Unity-URP exports, kept for backwards compatibility.

No consumer action is needed to pick a pipeline — detection is automatic per material.

Material fieldPurpose
material.nafTextures?.rgbaMaskOptional THREE.Texture for the authored RGBX/RGBA mask slot (_RGBMask) when present in native material data.
material.userData.nafTextureSlots?.rgbaMaskMetadata for the bound extra texture: { name, width, height, format, pointer, index, texCoord }.
material.userData.materialModelWhich material pipeline produced the material: 'gltf' (current glTF pipeline) or 'legacy' (deprecated Unity-URP export). Useful for diagnostics or conditional shader logic.

Example:

mesh.traverse((node) => {
const materials = Array.isArray(node.material) ? node.material : [node.material];
for (const material of materials) {
const rgbaMask = material?.nafTextures?.rgbaMask;
if (rgbaMask) {
console.log('RGBA mask texture:', rgbaMask, material.userData.nafTextureSlots.rgbaMask);
}
}
});

Asset Prefetch

Type-agnostic preload — warms the resolver's local file cache for any asset (animation, combinable, …) without parsing or decoding. Subsequent load* calls hit the cache instead of the network.

APIPurpose
prefetchAsset(assetId, { version?, lod? })Prefetch one asset. Returns Promise<boolean>.
prefetchAssets(requests)Batch — Promise.allSettled + one trailing syncFilesystem(). Returns per-asset status (see below).

Batch return shape:

{
results: Array<{
key: unknown;
id: string;
version: string;
lod: string;
ok: boolean;
error?: Error;
}>;
succeeded: number;
failed: number;
synced: boolean; // true if a trailing syncFilesystem() ran
}

Fail-soft: one asset failing does not reject the batch — inspect results[i].ok per item.

Example — preload voice-tag gesture clips before playback:

import { prefetchAssets } from 'naf';

const tags = controller.getAvailableAnimTags();
const requests = Object.entries(tags.gestures).flatMap(([name, t]) =>
(t.animIds ?? []).map(id => ({ id, key: `gesture:${name}` }))
);

const { results, succeeded, failed } = await prefetchAssets(requests);
console.info(`Prefetched ${succeeded}/${results.length}, ${failed} failed`);

// playGestureTag is now a cache hit on every tag we prefetched.
controller.playGestureTag('wave');

Supported asset types today: animations and combinable assets. Other types (textures, icons, …) log a warning and return false until per-type flavor selection is wired up.

Shaders

The SDK ships a ShaderManager that orchestrates shader effects across multiple avatars. It comes with two built-in effects (toon and outline), both disabled by default. Consumers can:

  1. Use the built-in effects — enable toon/outline, apply presets, tweak params.
  2. Use their own materials — never enable the built-in effects; the mesh keeps its original PBR materials.

Lifecycle

The SDK owns the shader lifecycle internally — attachAvatar, detachAvatar, syncTextures, and rebuild material swaps are handled automatically during loadAvatar / loadAvatarProgressive / equipAsset / unequipAsset / unload. Consumers do not need to call these methods directly.

APIPurpose
getShaderManager()Singleton ShaderManager with toon (mega) and outline effects.
MegaShaderMaterial / OutlineMaterialCustom material classes used by the built-in effects.
applyUnityMetallicSmoothness(material)Convert Unity metallic-smoothness to PBR roughness.
setSmoothnessScale(v) / getSmoothnessScale()Global smoothness scale.
setMinRoughness(v) / getMinRoughness()Global minimum roughness clamp.

ShaderManager API

The manager maintains per-avatar state for every registered effect. Each avatar can independently have effects enabled or disabled. Avatar lifecycle (attach, detach, texture sync, rebuild) is handled automatically by the SDK — consumers only interact with the manager for selection, effects, and cleanup.

MethodPurpose
setActiveAvatar(avatarId)Route UI param changes (setParam, enable, setThickness, etc.) to this avatar.
applyShaderPreset(presetJson)Apply a preset blob (toon + outline) to the active avatar. Supports multiMesh per-mesh overrides keyed by THREE.Mesh.name.
setActiveMesh(meshName \| null)Scope subsequent toon + outline setters to one mesh on the active avatar. null reverts to all meshes. Per-effect toon.setActiveMesh / outline.setActiveMesh are also available for divergent scoping.
getMeshNames()Mesh names on the active avatar.
setRgbaMaskColors({ r?, g?, b?, a?, strength?, useAlpha? })Procedural RGBA/RGBX-mask shading: tint each mask channel with a color and blend over the diffuse by strength. The mask shader is injected lazily on first use and respects the active-mesh scope. Returns the number of materials affected. Presets may carry the same params under an rgbaMask key (applied by applyShaderPreset).
dispose()Dispose all effects for all avatars.

Multi-Avatar Shaders

All avatars hold their shader materials simultaneously — no detach/reattach needed. The SDK handles attachAvatar/detachAvatar internally during load and unload. Consumers only call setActiveAvatar to choose which avatar receives UI-driven changes.

const mgr = getShaderManager();

// Avatar A loads (SDK calls attachAvatar internally via loadAvatarProgressive)
// Select avatar A for editing
mgr.setActiveAvatar(avatarA.avatarId);
mgr.toon.enable(true);
mgr.toon.setParam('cartoonMix', 0.8);
mgr.outline.enable(true);
mgr.outline.setThickness(0.003);

// Avatar B loads (SDK calls attachAvatar internally)
// Switch editing target — avatar A keeps its materials untouched
mgr.setActiveAvatar(avatarB.avatarId);
mgr.toon.enable(true);
mgr.toon.setParam('cartoonMix', 0.5); // different value, only affects B

// Switch back to A — its toon/outline are still there, no cache needed
mgr.setActiveAvatar(avatarA.avatarId);
mgr.toon.setParam('cartoonMix', 1.0); // updates only A

// Unload B — A is unaffected (SDK calls detachAvatar internally)
avatarB.unload();

Built-in Effects

Toon (mgr.toon): enable(bool), setParam(name, v), getParam(name), applyPreset({ base, perMesh }) (single-pass; perMesh keyed by THREE.Mesh.name), setActiveMesh(name) / getMeshNames() / getMeshParams(name?) for per-mesh scoping + read-back.

Outline (mgr.outline): enable(bool), setThickness(v), getThickness(), setColor(c), getColor(), setUseAlbedo(bool), getUseAlbedo(), setConstantWidth(bool), getConstantWidth(), setDepthBias(v), setAlphaTest(v), getAlphaTest(), setResolution(w, h), getResolution(), setActiveMesh(name) / getMeshNames() / getMeshParams(name?) for per-mesh scoping + read-back.

Legacy convenience wrappers: enableToonShader, setToonParam, isToonEnabled, enableOutline, setOutlineThickness, setOutlineColor, setOutlineUseAlbedo, setOutlineConstantWidth, isOutlineEnabled, setMaterialParam, getMaterialParam, getMaterialParams. Prefer getShaderManager() for new code.

Apply MegaShader to external meshes

applyMegaShaderToMesh(object, { avatarId?, outline? }) applies the toon (and optionally outline) shader to any Three.js Object3D — useful for loaded glb/fbx models or procedurally-built meshes. Works on both static Mesh and SkinnedMesh. Returns { avatarId, dispose() }; the avatar id is registered with ShaderManager so applyShaderPreset / setParam / per-mesh scoping all work normally.

Coexists with regular NAF avatars — each gets its own avatar id in the manager. Use mgr.setActiveAvatar(id) to choose which one receives subsequent setter calls.

import {
initialize,
applyMegaShaderToMesh,
getShaderManager,
} from 'naf';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

// 1. Init NAF + load your external mesh
const naf = await initialize({ canvas, initScene });
const gltf = await new GLTFLoader().loadAsync('robot.glb');
scene.add(gltf.scene);

// 2. Apply MegaShader (toon + outline) — works for static Mesh AND SkinnedMesh
const { avatarId, dispose } = applyMegaShaderToMesh(gltf.scene, {
avatarId: 'my-robot', // optional; auto-generated if omitted
outline: true, // also build the inverted-hull outline
});

// 3. Drive it through the regular ShaderManager surface
const mgr = getShaderManager();
mgr.setActiveAvatar(avatarId);

// — Apply a full preset blob (same JSON shape as the avatar presets)
const preset = await fetch('/presets/myPreset.json').then(r => r.json());
mgr.applyShaderPreset(preset.toonParams);

// — Or set individual params
mgr.toon.setParam('cartoonMix', 0.8);
mgr.toon.setParam('shadowColor', [1, 0.286, 0, 1]);
mgr.outline.setThickness(0.003);
mgr.outline.setColor(0x000000);

// — Per-mesh scoping (by child mesh.name)
mgr.setActiveMesh('chest_plate');
mgr.toon.setParam('shadowColor', [0.5, 0, 0, 1]); // chest only
mgr.setActiveMesh(null); // back to "all meshes"

// 4. Inspect (optional)
mgr.getMeshNames(); // → ['head', 'chest_plate', 'arm_l', ...]
mgr.toon.getMeshParams('chest_plate'); // → { cartoonMix: 0.8, shadowColor: [...], ... }

// 5. When done — restores original materials + unregisters
dispose();
scene.remove(gltf.scene);

Voice (Data Helpers)

The SDK ships parsers — not playback. Scene integration (AudioBuffer, lip sync, etc.) is the developer's responsibility.

APIPurpose
parseVoiceBlendshapes(buffer)Parse blendshape timeline from a voice animation buffer.
parseVoiceTags(buffer)Parse gesture/emotion tags from a voice animation buffer.
extractVoiceAudio(buffer)Extract raw audio data.
loadVoiceAnimation(assetId, version?)Fetch voice animation bytes from the asset resolver.
loadVoiceConfig(assetId, version?)Fetch voice config JSON.

Logging & Debug

APIPurpose
createLogger(tag)Tagged logger.
LogLevel{ DEBUG, INFO, WARN, ERROR, NONE }.
setLogLevel(level)Global verbosity.
showDebugWindow() / hideDebugWindow() / toggleDebugWindow()On-screen log panel.
initAnimationDebug(mesh, poseSynchronizer, scene)Bone / weight visual overlays.

In-memory log buffer

Captures all four log levels (Debug/Info/Warning/Error).

APIPurpose
setLogMemoryBufferSize(n)Capacity. 0 clears + disables; >0 also enables capture.
setLogMemoryBufferEnabled(bool)Pause/resume without changing capacity.
isLogMemoryBufferEnabled()Whether capture is on.
getLogMemoryBufferSize() / clearLogMemoryBuffer()Current entry count / empty the buffer.
getLogMemoryBuffer()Snapshot array of { timestamp, level, caller, message }.
dumpLogMemoryBuffer()console.table the snapshot (also returns it).
setLogBufferFullCallback(fn)Fires the snapshot just before the oldest entry is dropped — flush-to-analytics hook. null to clear.