Skip to main content

API Summary

This page summarizes the public API exposed by the Genies Android SDK.

Integration Paths

PathYou writeSDK handles
Integrated — drop in NafViewView placement, optional pick / gizmo callbacksLoading, equip / unequip, LODs, animation, shaders, picking, camera, pacing
Custom — own the Vulkan pipelineRenderer codeLoading, animation, shader-state writes
info

The Custom path is scaffold only today — typed BYO-renderer protocol not yet defined; consumers walk the engine's naf_* C-API.

ModuleWhat it ships
:sdk:naf-coreHeadless session + builder, container, filesystem init, libnafcore.so JNI
:sdk:naf-configBundled resolver-config + texture-settings + default-asset JSON
:sdk:naf-toolsRenderer-less helpers (camera, picker, transform gizmo)
:sdk:naf-uiNafView + Vulkan renderer (libnafrenderer.so) + shader manager + Compose bridge

Integrated Path

One-time process init (typically in your Application class):

NafContainerApi.init(this, NafKitConfig.normalizedResolverConfig(this))
NafDefaultAssets.ensureLoaded(this)

Drop a NafView and load an avatar:

val view = NafView(this)
setContentView(view)
view.onSurfaceReady = {
lifecycleScope.launch {
val avatar = NafAvatar.fromJson(assets.open("avatar.json").bufferedReader().readText())
view.session.loadAvatar(avatar) // default mode = Animated()
}
}
tip

loadAvatar defaults mode to NafLoadMode.Animated() — the idle clip from NafDefaultAssets.idle pumped at real dt. Pass NafLoadMode.StaticPose for a thumbnail-style load (idle bound, pumped at dt=0 so it holds the first frame). NafLoadMode.Behavior(...) is exposed for cross-platform parity but throws IllegalArgumentException today — see Status.

Common follow-ups:

view.applyDefaultToonPreset()                                  // toon shader, bundled preset
// WARNING: Equipping wearables is not currently functional (pending auth + inventory integration)
view.session.equip(NafAssetEntry("hat_001")) // wearables
view.session.unequip("hat_001")
view.cameraDistance = 2.5f // camera
view.frameAvatar()
view.lighting.setKeyLightDirection(45f, 10f)
view.lighting.setExposure(1.2f)
view.clearColor = floatArrayOf(0.95f, 0.55f, 0.50f, 1f) // alpha < 1 needs isOpaque = false
view.isPickingEnabled = true // opt-in pick + gizmo
view.isGizmoEnabled = true
view.onPick = { hit -> view.isGizmoVisible = (hit != null) }
view.adaptiveFrameRate = true // 60 active / 24 idle
view.preferredFramesPerSecond = 60 // ceiling
view.renderScale = 0.75f // 0.25–1.0 drawable scale
view.unloadAvatar() // teardown

Compose

@Composable
fun AvatarScreen(avatar: NafAvatar) {
val view = rememberNafView()
LaunchedEffect(avatar) { view.session.loadAvatar(avatar) }
NafAvatarView(view, modifier = Modifier.fillMaxSize())
}

rememberNafView() survives recomposition and auto-unloads on dispose. Use NafAvatarView(modifier, onCreated = { … }) for the "just drop a view in" case.


tip

The integrated path above covers majority of consumer use. The sections below are the per-type API reference for when you need more.

danger

There are some exposed APIs that currently do not work. Equipping assets, behavior, gestures, and emotions are not currently functional.

Please check out the Troubleshooting page for more information.

NafView

SurfaceView subclass. Owns a NafAvatarSession, NafCameraControls, NafTransformGizmo, NafLighting. Forwards render config to the NafRenderer singleton.

Camera

Property / methodNotes
cameraTarget: FloatArray[x, y, z]
cameraDistance: FloatJVM-renamed nafCameraDistance to avoid View.setCameraDistance
cameraYaw, cameraPitch, cameraMin/MaxDistance, cameraMin/MaxPitch, cameraRotate/PanSensitivity: FloatPassthroughs to cameraControls
cameraGesturesEnabled: BooleanDefault true; disable to drive the camera programmatically
pushCamera()Push current cameraControls state to the renderer
frameAvatar()Re-fit orbit camera to meshSpaceBounds (10% padding)

Avatar transform

Property / methodNotes
avatarPosition: FloatArray[x, y, z] world translation
avatarModelTransform: FloatArray16-float column-major; only translation column honored today
setAvatarPosition(x, y, z)3-float convenience setter
meshSpaceBounds: Pair<FloatArray, FloatArray>?Combined bind-pose AABB, or null when nothing loaded

Picking

PropertyDefaultNotes
isPickingEnabled: BooleanfalseOpt in to wire the tap recognizer
pickPrecision: NafPickPrecision.Bounds.Bounds (constant-time AABB) or .Mesh (CPU-skinned tri)
pickScope: NafPickScope.Avatar.Avatar (any submesh → one result) or .Mesh (returns submesh id)
onPick: ((NafPickResult?) -> Unit)?nullFires on tap; null result = miss
runPick(screenX, screenY): NafPickResult?Programmatic pick (no touch)

Gizmo

Property / methodDefaultNotes
transformGizmo: NafTransformGizmoThe underlying math object
gizmoAxisLength: FloatPassthrough to transformGizmo.axisLength
isGizmoEnabled: BooleanfalseOpt in to wire the axis-drag recognizer
isGizmoVisible: BooleanfalseDraws the overlay; typically toggled from onPick
onGizmoMove: ((x, y, z) -> Unit)?nullFires on every drag step

Render config

Property / methodDefaultNotes
clearColor: FloatArray[0.04, 0.04, 0.05, 1][r, g, b, a]; alpha < 1 needs isOpaque = false
isOpaque: BooleantrueJVM-renamed nafIsOpaque to avoid View.isOpaque()
setTransparent(Boolean)Underlying surface format flip
showSkeleton: BooleanfalseDebug overlay
lighting: NafLightingSee Rendering section below

Power & thermal

Property / methodDefaultNotes
isRenderingPaused: BooleanfalseHalts the per-vsync drive
rendersOnDemand: BooleanfalseOnly submit when state changes; call setNeedsRedraw() for consumer-driven mutations
adaptiveFrameRate: BooleanfalseModulate between active/idle FPS tiers
adaptiveActiveFPS / IdleFPS: Int60 / 24Tier values
preferredFramesPerSecond: Int0Ceiling FPS; combines as min(tier, ceiling); 0 = uncapped
renderScale: Float1.0Clamped [0.25, 1.0]; lower = fewer fragments
setNeedsRedraw()Wake the on-demand loop
currentFps: Float0EMA across actual ticks

The "active" signal: any input or setNeedsRedraw() within the last 250ms. Auto-fired from every SDK state write (camera, setEntity, etc.).

Lifecycle

Property / methodNotes
session: NafAvatarSessionThe owned session
isSurfaceReady: Boolean + isSurfaceReadyFlow: StateFlow<Boolean>True between surfaceCreated and surfaceDestroyed; gate the first load on this
onSurfaceReady: (() -> Unit)?One-shot callback equivalent
unloadAvatar()Tear down the loaded avatar (idempotent)

NafAvatarSession

Headless lifecycle. NafView.session is one; use directly for BYO renderer or testing.

APINotes
NafAvatarSession(context: Context)Constructor; one process-wide NafContainerApi.init required first
suspend loadAvatar(avatar, mode = Animated(), lods = [0], trackingId = null)Loads + builds; idle clip from NafDefaultAssets.idle
suspend equip(asset: NafAssetEntry, lod = 0, trackingId = null)Add a wearable. Not currently functional — pending auth + inventory integration
unequip(assetId: String)Remove a wearable. Not currently functional — pending auth + inventory integration
unloadAvatar()Cancels any in-flight load, tears down native handles
clearLodCache()Drop the LOD upgrade caches (process-wide)
equippedAssetIds: List<String>Currently-equipped ids
mergedEntityPtr / controllerHandle: LongRAW pointers for BYO renderer use
State flowsisAvatarLoadedFlow, isLoadingFlow, isSilhouetteActiveFlow, currentModeFlow (each with a snapshot val alias)
CallbacksonAvatarReady(mergedPtr, controllerHandle), onAvatarUnload(), onSilhouetteModeChanged(active), onLoadingChange(loading)

NafLoadMode

ModeWhat it does
StaticPoseIdle clip bound, pumped at dt=0 so it stays on the clip's first frame
Animated(silhouette: Boolean = false)Idle pumped at real dt; silhouette = true + multi-LOD shows a silhouette while LODs stream
Behavior(silhouette)Not implemented — throws IllegalArgumentException (smart-avatar pipeline lands later)

Definitions, asset refs, defaults

data class NafAssetEntry(val assetId: String, val version: String = "1")

data class NafAvatar(
val assets: List<NafAssetEntry>, // assets[0] = rig; rest = wearables
val shapeAttributes: Map<String, Float> = emptyMap(),
val slug: String? = null,
) {
fun toJson(): String
companion object { @JvmStatic fun fromJson(json: String): NafAvatar }
}

data class NafLodUpgrade(
val avatarId: Int,
val targetLod: Int,
val assetId: String? = null,
val mergedEntityPtr: Long = 0L,
)
fun NafEvent.toLodUpgrade(): NafLodUpgrade? // null when not LodUpgrade*

object NafDefaultAssets {
@Volatile var idle: NafAssetEntry?
@Volatile var talking: NafAssetEntry? // not yet pumped
@Volatile var breathing: NafAssetEntry? // not yet pumped
fun ensureLoaded(context: Context)
}

JSON shape mirrors the cross-platform NAF wire format.

note

To swap the default idle clip globally, override NafDefaultAssets.idle once at app startup — every subsequent loadAvatar picks it up.


NafShaderManager (toon + outline)

note

NafShaderManager is an internal singleton — most consumers should use the NafView convenience methods (apply(preset), applyDefaultToonPreset(), disableToonShader()) instead of driving it directly. The reference below is for advanced use cases like custom multi-avatar management or fine-grained per-mesh control.

object NafShaderManager {
val outline: OutlineNamespace

fun attachAvatar(id: Long); fun detachAvatar(id: Long); fun setActiveAvatar(id: Long?)
fun setActiveMesh(meshName: String?) // null = all meshes
val meshNames: List<String>

fun setToonEnabled(enabled: Boolean); val isToonEnabled: Boolean
val toonUniformNames: List<String>
fun setToonFloat / setToonVec3 / setToonVec4 / getToonFloat / getToonVec3 / getToonVec4

fun applyShaderPreset(preset: ToonPreset)
fun applyShaderPreset(json: String): Boolean
fun applyDefaultToonPreset(context: Context): Boolean

fun toonParams(forMesh: String? = null): Map<String, ToonPreset.ParamValue>
fun outlineParams(forMesh: String? = null): Map<String, ToonPreset.ParamValue>
}

The outline namespace mirrors the toon side (setEnabled, setThickness, setColor, setUseAlbedo, setConstantWidth, setDepthBias, setAlphaTest, plus getters, plus setMeshDisabled(meshName, disabled) for per-mesh opt-out).

Per-mesh overrides — call setActiveMesh("avatar_headOnly_geo"), then every setter writes only to that mesh. Reverting to null writes to base + all existing per-mesh overrides. Preset JSON expresses the same via a "multiMesh" block whose keys are mesh ids:

{ "name": "char-x", "toonParams": {
"cartoonMix": 1,
"multiMesh": {
"avatar_eyelashes_geo": { "outlineEnabled": false }
}
}}

NafView shortcuts — view-level convenience over the singleton:

fun NafView.apply(preset: ToonPreset)
fun NafView.applyDefaultToonPreset(): Boolean
fun NafView.attachToonShader()
fun NafView.disableToonShader()

Rendering (lighting, background, color space)

Lighting — single key-light + exposure today. Routes through the toon shader's cartoonAzimuth / cartoonElevation / albedoMult uniforms; textured-only meshes don't see exposure changes.

view.lighting.setKeyLightDirection(45f, 10f)
view.lighting.setExposure(1.2f)

Structural lighting (ambient + multi-directional array) and a proper post-process pass are not yet implemented.

Background / transparencyview.clearColor = floatArrayOf(r, g, b, a). Alpha < 1 needs view.isOpaque = false (flips the SurfaceView to TRANSLUCENT + zOrderOnTop); the renderer rebinds the cached entity automatically on the resulting surface recreate.

Color space caveat — the pipeline is currently gamma-space (UNORM swapchain, no sRGB decode on textures, no encode on output, no HDR / tone map). Bundled toon presets are hand-tuned to compensate. Switching to proper linear lighting or ACES Filmic will require a per-character preset re-tune.


:sdk:naf-tools

Renderer-less helpers. Drop in alongside :sdk:naf-core when you want the camera + picker + gizmo math without :sdk:naf-ui's Vulkan dependency.

class NafCameraControls {
var targetX/Y/Z, distance, yaw, pitch: Float
var minDistance, maxDistance, minPitch, maxPitch: Float
var rotateSensitivity, panSensitivity, fovYRadians: Float

fun handlePan(dx, dy, fingerCount); fun handlePinch(scaleFactor)
fun frame(minBound, maxBound, fovY, aspect)
fun screenToRay(screenX, screenY, vpW, vpH): NafRay
fun worldToScreen(x, y, z, vpW, vpH): FloatArray?
}

data class NafRay(val ox, oy, oz, dx, dy, dz: Float)

enum class NafPickPrecision { Bounds, Mesh }
enum class NafPickScope { Avatar, Mesh }

data class NafPickCandidate(val id: String, val worldAabb: List<Float>)
data class NafPickResult(val id: String, val distance: Float, val worldX/Y/Z: Float)

object NafAvatarPicker {
fun pick(screenX, screenY, vpW, vpH, camera, candidates): NafPickResult?
}

class NafTransformGizmo {
enum class Axis { X, Y, Z }
data class DragState(val axis: Axis, /* … */)
var centerX/Y/Z, axisLength, hitWidthPx: Float

fun beginDrag(screenX, screenY, vpW, vpH, camera): DragState?
fun updateDrag(state, screenX, screenY, vpW, vpH, camera)
}

NafView wires these into onTouchEvent already — you only need them directly for BYO touch surfaces.


:sdk:naf-config

object NafKitConfig {
var resolverConfigOverride: String?
var resolverConfigDevOverride: String?
var resolverConfigProdOverride: String?
var textureSettingsOverride: String?
var shaderPresetsOverride: String?

fun normalizedResolverConfig(context: Context): String // dev/prod by build flavor
fun textureSettings(context: Context): String?
fun shaderPresets(context: Context): String?
}

Bundled JSON lives in this AAR under assets/config/. Override any field once at app startup.


NafContainerApi

object NafContainerApi {
val isInitialized: Boolean
fun init(context: Context, resolverConfig: String)
fun loadCombinableAssetSync(assetId, version, lod = 0): Entity
}

init is idempotent — first call wins. Pass NafKitConfig.normalizedResolverConfig(context) for the default.


NafRenderer

Singleton wrapping libnafrenderer.so. NafView calls into it for the integrated path; consumers usually don't touch this directly.

object NafRenderer {
fun setEntity(mergedEntityPtr: Long); fun clearEntity()
fun attachController(handle: Long, frozen: Boolean = false)
fun setCameraOrbit(yaw, pitch, distance, targetX, targetY, targetZ)
fun setShowSkeleton(enabled: Boolean); fun setSilhouetteMode(enabled: Boolean)
fun setGizmo(visible, x, y, z, length); fun setAvatarPosition(x, y, z)
fun setClearColor(r, g, b, a)
fun meshBounds(): FloatArray // flat [6 * meshCount]
fun pickMesh(ox, oy, oz, dx, dy, dz): FloatArray? // null on miss

// Pacing — see NafView's "Power & thermal" section for the full table.
var isRenderingPaused, rendersOnDemand, adaptiveFrameRate: Boolean
var adaptiveActiveFPS, adaptiveIdleFPS, preferredFramesPerSecond: Int
fun setNeedsRedraw()
val isFrameSuppressed: Boolean; val currentFps: Float
}

mergedEntityPtr is a raw GnCore::Entity* from NafAvatarBuilder.nativeLoadAsync / nativeEquipAsync / nativeUnequip. Lifetime is owned by AvatarBuilderHandle in libnaf.so keyed by avatarId.


Events

object NafEventBus {
fun subscribe(type: NafEventType, callback: (NafEvent) -> Unit): NafEventSubscription
fun subscribe(type: NafEventType, handler: Handler, callback): NafEventSubscription
fun publish(event: NafEvent)
fun events(type: NafEventType): Flow<NafEvent> // Compose-friendly
}

data class NafEvent(val type: NafEventType, val userInfo: Map<String, Any> = emptyMap()) {
inline fun <reified T> get(key: String): T?
}
CodeNafEventTypeFires today?
100 / 101 / 102AssetLoadStarted / Completed / Failed
200 / 201AvatarReady / SilhouetteReady
202 / 203LodUpgradeStart / LodUpgradeCompleted✅ (use NafEvent.toLodUpgrade())
300–306Gesture* / Emotion* / Boredom* / VoiceAnim*❌ — lands with behavior mode
400 / 401 / 402IdleEntered / TalkingStarted / TalkingStopped
900Error (userInfo: error: Throwable, trackingId, assetId?)

Common userInfo keys: trackingId: UUID, avatarId: Int, assetId: String, targetLod: Int, error: Throwable, mergedEntityPtr: Long.

NafEventBridge — string-keyed bus

Lower level; for events fired by C++ that don't have a typed NafEventType entry.

object NafEventBridge {
fun subscribe(eventName, callback): NafEventBridgeSubscription
fun subscribe(eventName, handler, callback): NafEventBridgeSubscription
fun subscribeAll(callback): NafEventBridgeSubscription
fun fire(eventName: String, payload: Map<String, Any?>? = null)

fun sendRequest(eventName, payload, timeoutMillis = 5_000L,
completion: (reply: Map<String, Any?>?) -> Unit)
}

sendRequest is the request-reply primitive — fires eventName with payload + _requestId, listens on "$eventName.response" for a reply with the matching id, resolves completion (or null on timeout). Responders subscribe to eventName, read payload["_requestId"], fire "$eventName.response" with the same id.


Status

✅ shipped · ⚠ partial · ❌ deferred (engine work needed)

AreaStatus
loadAvatar / unloadAvatar
equip / unequip⚠ (surface present; pending auth + inventory integration)
StaticPose / Animated modes (incl. silhouette pre-load on multi-LOD)
Behavior mode + playGesture / playEmotion + behavior weights
LOD streaming + cache gate
Toon + outline shader manager (per-mesh overrides, presets)
NafView.applyDefaultToonPreset() / apply(preset)
Picking (Bounds / Mesh × Avatar / Mesh)
Translate gizmo
frameAvatar() / meshSpaceBounds / avatarModelTransform (translation only)
clearColor / isOpaque solid backgrounds
Image / video-backed backgrounds
rendersOnDemand / setNeedsRedraw / currentFps
adaptiveFrameRate / preferredFramesPerSecond / renderScale
isRenderingPaused, showSkeleton
showBoundingBox overlay
Compose bridge (NafAvatarView + rememberNafView)
NafEventBus lifecycle codes (asset / avatar / LOD)
NafEventBus behavior codes (gesture / emotion / boredom / voice / talking)
NafEventBridge.sendRequest (request-reply)
Structural lighting (ambient + directional[])
Linear lighting / sRGB color management❌ (gamma-space; presets tuned to compensate)
HDR offscreen + ACES Filmic tone-map
MSAA toggle
Culling override (debug)
BYO renderer protocol (NafAvatarRenderer)
Mesh-data export (NafMeshData / NafMaterial / NafTexture)
NafError typed code domain❌ (errors surface as Throwable on NafEventType.Error)
Look-at / gaze, blink, voice anim, blendshape pumping