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
| API | Purpose |
|---|---|
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. |
These are the initialize options:
canvas?: HTMLCanvasElement;initScene?: (canvas: HTMLCanvasElement, glContext: WebGL2RenderingContext) => void;assetResolverConfigPath?: string;textureSettingsPath?: string;
Configuration
| API | Purpose |
|---|---|
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
| API | Purpose |
|---|---|
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);
| Event | Payload |
|---|---|
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). Layer 1 normally has no controller, but the SDK creates one when automatic dynamics requires it. |
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:dynamics-changed | { avatarId, controller, source, active, error, overrideError } — the automatic dynamics source changed or was reapplied after a rebuild. |
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 } |
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:
| Phase | Fatal? | avatarId present? | Description |
|---|---|---|---|
avatar-load | Yes | No (avatarDefinition included instead) | loadAvatarRaw failed — all asset resolver fallbacks exhausted. The promise rejects. |
native | No | No | Native-side error forwarded during load (e.g. a single resolver endpoint failing before fallback). Informational only. |
silhouette | No | No | Silhouette mesh construction failed. Avatar load continues. |
silhouette-animation | No | No | Failed to animate the silhouette. Avatar load continues. |
animation | No | Yes | Animation controller or idle clip setup failed after avatar loaded. |
behavior | No | Yes | Smart avatar behavior activation failed. |
lod-upgrade | No | Yes | A background LOD texture upgrade failed. Avatar remains at previous LOD. |
rebuild | Yes | Yes | Full rebuild (equipAsset/unequipAsset) failed. The promise rejects and the definition is rolled back. |
rebuild-animation | No | Yes | Animation controller failed during rebuild staging. Rebuild continues without animation. |
rebuild-behavior | No | Yes | Behavior re-activation failed after rebuild. |
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 with no progressive loading. It normally returns a static mesh — but when the asset ships dynamics (GENIES_dynamics — physics-based secondary motion like hair and cloth that sways as the avatar moves), the SDK creates the animation controller it needs automatically, unless you pass dynamics: false. See the Dynamics section for the full picture.
| API | Purpose |
|---|---|
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. |
These are the loadAvatar options:
renderer: WebGLRenderer;meshOptions?: CreateMeshOptions;lod?: number;(0 = best detail)syncOptions?dynamics?— override or disable automatic dynamics (see Dynamics).proceduralTextures?: boolean;— procedural textures are generated on the GPU as the avatar loads. Set tofalseto skip that work (native procedural shader loading, render-target allocation, and rendering); supported output-0 materials then use the procedural set's authoredbaseColorinput directly. This is a load-time setting, so unload and reload the avatar to change it.signal?
Here lod is a single number (0 = best), not an array — the progressive loader is the one that 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.
| Member | Purpose |
|---|---|
avatarId | Numeric ID assigned by native. |
mesh | THREE.Group containing the skinned meshes. |
controller | Animation controller (set by attachAnimation, progressive idle, or automatic dynamics). |
status | Lifecycle state: 'loading' \| 'ready' \| 'unloaded'. |
isReady | True once setup is complete and the handle is safe to drive each frame. |
isUnloaded | True once unload() has been called. |
proceduralTexturesEnabled | Current native procedural-texture load mode (see the proceduralTextures load option). |
userData | Consumer-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. |
getDynamicsState() | Returns { source, active, error, overrideError } describing how automatic dynamics resolved for this avatar. |
configureDynamics(options?) | Reconfigures dynamics. Omitted options use the embedded GENIES_dynamics; false explicitly disables dynamics. |
setDynamicsOverride(override, options?) | Applies a local override — { config }, { url }, or an inline object/string — falling back to the embedded asset config by default. |
clearDynamicsOverride() | Removes the local override and returns to the embedded GENIES_dynamics source. |
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. |
currentLod | LOD 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. By default the color applies avatar-wide (attribute ids are shared across assets by authoring convention); pass options.assetId to tint only that one equipped asset, even when several assets author the same attribute id. |
unsetColor(colorId, options?) | Clears a runtime ColorEditor color override. Pass options.assetId to clear an asset-scoped color. |
getColorAttributes() | Enumerates the editable color attribute ids on the built avatar: { global: string[], byAsset: { [assetId]: string[] } }. global ids work with a plain setColor; byAsset lists the ids you can scope per equipped asset. Lets a UI discover what's editable instead of hardcoding id lists. |
unload() | Frees native entities, disposes the controller, unregisters LOD callbacks. Idempotent. |
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.
| API | Purpose |
|---|---|
attachAnimation(handle, { syncOptions?, dynamics? }) | Creates an AvatarAnimationController, applies automatic dynamics, 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 }. |
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
| Method | Purpose |
|---|---|
AvatarAnimationController.create(mesh, options?). | Create and initialize a AvatarAnimationController. |
hasAnimation | True while the controller has animation and has not been disposed. |
isPaused | When true, update() and syncPose() short-circuit without advancing native playback. |
isDisposed | True after dispose() has released the native controller and pose resources. |
Clip Loading
| Method | Purpose |
|---|---|
loadClip(path) | Load from a local file path. |
loadClipFromAssetResolver(animationId, version?) | Load via asset resolver. |
loadClipFromBuffer(buffer, cacheKey) | Load from a Uint8Array. |
Playback
| Method | Purpose |
|---|---|
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
| Method | Purpose |
|---|---|
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
| Method | Purpose |
|---|---|
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
| Method | Purpose |
|---|---|
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
| Method | Purpose |
|---|---|
getHeadBone() | Cached head bone. |
setLookAtCamera(cameraLocalPosition, headPosition) | Drive head/eye look-at. |
Dynamics (GENIES_dynamics chains)
Dynamics is physics-based secondary motion — hair, cloth, and accessories that sway and settle as the avatar moves. Its configuration ships with the asset in the GENIES_dynamics extension.
Dynamics is automatic — no consumer call is required. loadAvatar() and loadAvatarProgressive() read GENIES_dynamics from the combined asset, create an animation controller when one is needed, and apply the configuration once the final PoseContext exists:
const handle = await naf.loadAvatarProgressive(definition, { renderer });
Local JSON is an optional override. The SDK tries the override first and, by default, falls back to the embedded asset configuration if loading or native validation fails:
const handle = await naf.loadAvatarProgressive(definition, {
renderer,
dynamics: {
override: { url: '/configs/avatar_dynamics.json' }, // or { config: json }
fallbackToAsset: true,
},
});
await handle.setDynamicsOverride({ config: editedJson });
await handle.clearDynamicsOverride(); // back to GENIES_dynamics
Omit dynamics to use the embedded extension automatically. A successful override replaces the asset configuration — the two sources are not combined. An empty override URL is treated as no override, and dynamics: false disables all dynamics. Equip/unequip rebuilds re-resolve the source after refreshing the controller's pose context.
The controller methods below remain available as the low-level API — configs can be baked into the asset (applied natively at build time) or supplied at runtime from JSON:
| Method | Purpose |
|---|---|
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 remains available when you deliberately want to bypass automatic source resolution and apply JSON directly. It 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)
| Method | Purpose |
|---|---|
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
| Method | Purpose |
|---|---|
update(deltaTime) | Advance the controller. |
syncPose() | Sync computed pose to the Three.js mesh. |
isPaused | Getter/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. |
hasAnimation | True while animation is active and the controller has not been disposed. |
isDisposed | True after dispose() releases native resources. |
poseSynchronizer | Exposes 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.
| Method | Purpose |
|---|---|
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 enteredTransitionIn.avatar:channel-end— channel returned toEmpty.completed: trueif it ran through its transition-out naturally;falseif cut short (pre-empted by a higher-priority gesture, or cleared).
A consecutive channel-end → channel-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 point | holdTime source | JS-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.
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 },
dynamics: { override: { url: '/configs/avatar.json' }, fallbackToAsset: true }, // optional — omit for automatic
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
signalto 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 lifecycle —
onReadyis awaited (flow control); events are always emitted (observation). - Composable — omit
idleand useattachAnimation+activateBehaviormanually if you need finer control.
Callbacks have event equivalents:
avatar:silhouetteavatar:readyavatar:lod-upgradeavatar: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');
- The first asset in the avatar definition is the base rig and cannot be unequipped.
equipAssetrejects if the asset id is already equipped.- Only one rebuild may be in flight per handle — concurrent calls reject.
- Incremental rebuilds reuse the active animation controller and stage the replacement mesh at the current channel times before the visible swap, so playback stays continuous through equip and unequip.
- 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.
| API | Purpose |
|---|---|
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 overslug).slug— let native fetch the config via REST (requiressetBearerToken).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— optionalAbortSignal; late completions are ignored after abort.isCurrent— optional guard callback; returnfalsewhen the controller no longer belongs to the active avatar/load.
For most developers this is wired automatically via:
loadAvatarProgressive({ behavior }).
SDK Module — NAF Content Tools
The APIs above are for running avatars. Content Tools is a separate, optional toolset for authoring them — inspecting, editing, decoding, and regenerating the NAF FlatBuffer assets themselves (materials, textures, RGBK masks, shader flavors, and the Dynamics / rig-recipe extensions). Most apps never need it; reach for it when you're building content pipelines or editor tooling.
It ships in the same npm package as the runtime, under separate subpath modules so the runtime entry point stays lean and never bundles the authoring code:
@geniesinc/genies-naf-webgl/content-tools— browser-safe. Inspect and edit decoded assets in memory.@geniesinc/genies-naf-webgl/content-tools/node— adds filesystem/process access for the nativeflatcandnafshcexecutables.
The native executables are opt-in and are not installed with the runtime SDK. When you need to author, add the companion package:
npm install --save-dev @geniesinc/genies-naf-content-tools
It selects the platform-specific build containing flatc, nafshc, and the Content Schemas. After it's installed, new NafContentToolsHost() needs no paths. Explicit constructor paths — or the NAF_FLATC_PATH, NAF_NAFSHC_PATH, and NAF_SCHEMA_ROOT_PATH environment variables — override package resolution.
How it fits together
Browsers can't run flatc or nafshc, so any browser authoring flow talks to a small Node endpoint. Expose createContentToolsBridgeHandler() from your Node host, and use NafContentToolsClient as the typed browser transport — it handles the bridge actions and Base64 conversion for you, so you don't reimplement them. Request parsing, authentication, size limits, and HTTP status handling stay application-owned. Node applications and CLIs can skip the bridge and use NafContentToolsHost directly.
A typical edit is: decode an asset to an editable JSON document → inspect or mutate it in memory → encodeAsset() to persist it as a verified CombinableAsset .bin, or encodeRevision() to create a new asset version with matching manifest references. Unrelated texture references are left untouched.
Browser-safe API
| API | Purpose |
|---|---|
NafContentToolsClient | Typed client for host-backed health, decode, manifest inspection, Slang compilation, encode, revision, and local-fixture persistence operations. Accepts a custom endpoint, headers, fetch, and per-request AbortSignal. |
client.saveLocalFixtureAsset(fixture) | Ask the configured host to merge one generated revision into its local fixture manifest without replacing definitions or unrelated fixture records. |
inspectAvatarDefinition(definition) | Enumerate definition assets, versions, and LODs. |
inspectCombinableAsset(document) | Enumerate stable material JSON paths, legacy texture keys, procedural sets, and shader flavors. |
inspectMaterialTextures(document, selector) | Enumerate physical legacy, glTF, and procedural textures with typed semantic roles, channel tags, source slots, and complete flavor metadata. Reports glTF ORM (R: Occlusion, G: Roughness, B: Metalness) and Unity metallic-smoothness (R: Metalness, A: Smoothness). |
upsertShaderProgram(...) / upsertShaderFlavor(...) | Add or replace procedural shader data. |
getEmbeddedShaderFlavorData(...) / upsertShaderSourceFlavor(...) | Read an embedded flavor as bytes, or replace one from editable text while preserving sibling flavors. |
upsertRgbkMask(...) | Add or replace an embedded RGBK mask. |
getRgbkMask(...) / texture source helpers | Retrieve a material's RGBK texture and inspect embedded or URI-backed flavor data for preview. |
configureRgbkMaterial(...) | Create/update GeniesRGBK, including legacy Unity-material migration. |
inspectJsonExtension(...) / getJsonExtension(...) | Decode an application/json entry from ext.extensions. |
upsertJsonExtension(...) / removeExtension(...) | Add, replace, or remove JSON extensions such as GENIES_dynamics and GENIES_rig_recipe. |
getDynamicsConfig(...) / upsertDynamicsConfig(...) | Strongly typed DynamicsChainConfig access and mutation. |
getRigRecipeConfig(...) / upsertRigRecipeConfig(...) | Strongly typed RigRecipeConfig access and mutation. |
retargetCombinableAssetRevision(...) | Retarget both manifest and CombinableAssetConfig URIs for a generated version. |
createLocalFixtureManifest(...) / createLocalAvatarDefinition(...) | Build local iteration metadata. |
Node API
| API | Purpose |
|---|---|
new NafContentToolsHost(options?) | Create the host API using explicit paths, environment overrides, the opt-in companion package, or staged monorepo tools. |
new NafContentToolsBridge(options?) | Implement the typed browser bridge contract using NafContentToolsHost, with optional filesystem or callback-based local-fixture persistence. |
createContentToolsBridgeHandler(options?) | Return a framework-neutral async request handler you can connect to Express, Next.js, Electron, or another Node host. |
resolveContentToolsHost(options?) | Inspect the resolved executable and schema paths without creating a host. |
NafContentToolsHost.decodeCombinableAsset(...) | Decode an asset .bin to editable JSON through flatc. |
encodeCombinableAsset(...) | Encode JSON and verify it by decoding the result. |
decodeContentManifest(...) / decodeEmbeddedCombinableAssetConfig(...) | Inspect the manifest and its embedded asset config. |
createCombinableAssetRevision(...) | Regenerate a versioned asset and matching manifest. |
ShaderCompiler.compile(...) / compileSource(...) | Invoke nafshc and return shader flavors plus reflection metadata. Produces slang-ir and glsl-es flavors: a Slang-disabled runtime selects glsl-es directly, a Slang-enabled runtime selects and transcodes slang-ir. |
Cross-cutting APIs
Cache Persistence
| API | Purpose |
|---|---|
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/...). |
readFileFromVFS(path) | Read a copied Uint8Array from an absolute VFS path, or null when the file is missing. |
listFilesInVFS(path) | List the direct child entry names in a VFS directory. |
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.
| API | Purpose |
|---|---|
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.
Procedural shader flavors
Procedural materials select their shader flavor natively; consumers continue to use the material and ColorEditor APIs without passing shader source to JavaScript:
- Slang-enabled builds select
shader/slang-ir(orshader/slang) and transcode it through SPIR-V and SPIRV-Cross to GLSL ES at runtime. - Slang-disabled builds select the asset's precompiled
shader/glsl-esflavor and use its embedded reflection metadata. - If the flavor required by the active build is absent, procedural material creation fails instead of silently selecting a flavor for a different backend path.
| Material field | Purpose |
|---|---|
material.nafTextures?.rgbaMask | Optional THREE.Texture for the authored RGBX/RGBA mask. It is exposed from either a legacy _RGBMask slot or a native procedural texture parameter such as rgbkMask. |
material.userData.nafTextureSlots?.rgbaMask | Metadata for the bound extra texture: { name, width, height, format, pointer, index, texCoord }. |
material.userData.materialModel | Which 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.
| API | Purpose |
|---|---|
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:
- Use the built-in effects — enable toon/outline, apply presets, tweak params.
- 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.
| API | Purpose |
|---|---|
getShaderManager() | Singleton ShaderManager with toon (mega) and outline effects. |
MegaShaderMaterial / OutlineMaterial | Custom 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.
| Method | Purpose |
|---|---|
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 overrides keyed by THREE.Mesh.name and multiMaterial overrides keyed by material name. |
setActiveMesh(meshName \| null, materialIndex?) | Scope subsequent toon + outline setters to one mesh on the active avatar. null reverts to all meshes. Optional materialIndex scopes further to a single submesh material slot (toon only — outline is per source mesh). Per-effect toon.setActiveMesh(name, matIdx?) / outline.setActiveMesh(name) are also available for divergent scoping. |
getMeshNames() | Mesh names on the active avatar. |
getMeshMaterials() | Enumerate submeshes: [{ meshName, matIdx, matName, materialModel, count, sourceMeshIds }]. count is the number of materials on the parent mesh (1 for single-material). sourceMeshIds traces each primitive back to the source RuntimeMesh id(s) it came from before the combine. Useful for identifying which wearable/part landed in a merged bucket like Default. |
toon.getMaterialModels() | Material models present on the active avatar: gltf, legacy, or unknown. Mixed avatars return more than one value. |
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
Two effects come built in and are controlled through the manager: Toon (cel / stylized shading) and Outline (a drawn outline around the avatar). You can adjust either one live with individual setter calls, or describe a whole look at once with a JSON preset (see Presets below).
Toon (mgr.toon) — stylized cel shading.
- Runtime:
enable(bool),setParam(name, v),getParam(name). - Presets:
applyPreset({ base, perMesh, perMaterial })— apply values to everything (base), to named meshes (perMesh), or to named materials (perMaterial). - Scope + read-back:
setActiveMesh(name, matIdx?),getMeshNames(),getMeshMaterials(),getMeshParams(name?, matIdx?). - Inspection:
getMaterialModels().
A few of the toon parameters you might use:
aoAlbedoDarkendarkens the base color in ambient-occlusion areas — crevices and contact shadows — for a more hand-painted look.0leaves the color untouched; higher values deepen it.useOrmAlpha(0–1) applies to glTF materials only; it's ignored (forced to0) on legacy Unity-URP materials. Set it globally or per material viamultiMaterial.
Surface state — transparency and culling. These optional fields control how a material draws:
alphaMode—OPAQUE,MASK(a hard cutout usingalphaCutoff), orBLEND(soft transparency).alphaCutoff— the threshold used byMASK.opacity— overall opacity. Note that settingopacityon its own does not turn on transparency; usealphaMode: "BLEND"for that.doubleSided— render both faces of a surface instead of culling the back.depthWrite— whether the material writes to the depth buffer.BLENDdefaults this tofalse.
Any field you leave out is restored to the asset's original ("canonical") value — so a preset only changes what it explicitly names, even after you swap to a different preset. Outlines inherit the material's coverage and render state.
Presets are JSON
A preset is just a plain JSON object describing the look you want, which you hand to applyShaderPreset(). Top-level fields apply to the whole avatar; multiMesh overrides by mesh name and multiMaterial overrides by material name, with the most specific match winning. For example — make everything a hard cutout, but let the hair blend softly:
{
"alphaMode": "MASK",
"alphaCutoff": 0.5,
"doubleSided": true,
"opacity": 1,
"depthWrite": true,
"useOrmAlpha": 0,
"multiMaterial": {
"HairMaterial": {
"alphaMode": "BLEND",
"opacity": 0.8,
"depthWrite": false,
"useOrmAlpha": 1
}
}
}
For convenience — and to keep older presets working — a few fields accept alternate names that map onto the canonical field:
alphaOpacity→opacitytwoSided→doubleSidedalphaEnabled→alphaMode:falsebecomesOPAQUE;truebecomesMASKwhen a positivealphaCutoffis set, otherwiseBLEND.
Using presets with TypeScript. If you work in TypeScript, the SDK ships types so your editor can autocomplete preset fields and catch typos before you run. Import MegaStylizerPreset for the JSON shape accepted by applyShaderPreset(), or MegaStylizerParameters for the canonical toon.setParam() / toon.applyPreset() values. Supporting types include MegaStylizerParameterName, MegaStylizerAlphaMode, MegaStylizerMaterialModel, and MegaStylizerMeshMaterial. Applying satisfies MegaStylizerPreset to a preset object gives you full IntelliSense and rejects unknown fields:
import { getShaderManager, type MegaStylizerPreset } from '@geniesinc/genies-naf-webgl';
const preset = {
cartoonMix: 0.8,
albedoSaturation: 1.1,
albedoLuminanceGamma: 0.95,
alphaMode: 'MASK',
alphaCutoff: 0.5,
outlineEnabled: true,
outlineSilhouetteGate: 0.2,
multiMaterial: {
HairMaterial: { alphaMode: 'BLEND', opacity: 0.8, depthWrite: false }
}
} satisfies MegaStylizerPreset;
getShaderManager().applyShaderPreset(preset);
Outline (mgr.outline) — draws an outline around the avatar (an inverted-hull outline). Its methods fall into three groups:
- Appearance:
setThickness(v)/getThickness(),setColor(c)/getColor(),setUseAlbedo(bool)/getUseAlbedo(),setConstantWidth(bool)/getConstantWidth(),setDepthBias(v),setAlphaTest(v)/getAlphaTest(),setResolution(w, h)/getResolution(). - Silhouette gating:
setSilhouetteGate(v),setGateGeomMix(v),setGateStrictness(v). - Scope + read-back:
setActiveMesh(name),getMeshNames(),getMeshParams(name?).
Silhouette gating restricts the outline to the model's silhouette — its contour (rim) edges — instead of drawing it across flat, front-facing surfaces. setSilhouetteGate(0) disables gating so the outline is drawn everywhere; any value above 0 turns it on and trims the non-silhouette parts. setGateGeomMix and setGateStrictness tune how that silhouette edge is detected. In a preset, the matching fields are outlineSilhouetteGate, outlineGateGeomMix, and outlineGateStrictness.
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"
// — Per-submesh scoping (when a mesh has multiple materials)
mgr.setActiveMesh('chest_plate', 1); // second material slot only
mgr.toon.setParam('cartoonMix', 0.3);
mgr.setActiveMesh(null);
// 4. Inspect (optional)
mgr.getMeshNames(); // → ['head', 'chest_plate', 'arm_l', ...]
mgr.toon.getMeshParams('chest_plate'); // → { cartoonMix: 0.8, shadowColor: [...], ... }
mgr.getMeshMaterials(); // → [{ meshName: 'Default', matIdx: 0, matName: 'URPMaterial', sourceMeshIds: ['hair_Mesh'], ... }, ...]
// 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.
| API | Purpose |
|---|---|
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
| API | Purpose |
|---|---|
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).
| API | Purpose |
|---|---|
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. |