API Summary
This page summarizes the public API exposed by the Genies Android SDK.
Integration Paths
| Path | You write | SDK handles |
|---|---|---|
Integrated — drop in NafView | View placement, optional pick / gizmo callbacks | Loading, equip / unequip, LODs, animation, shaders, picking, camera, pacing |
| Custom — own the Vulkan pipeline | Renderer code | Loading, animation, shader-state writes |
The Custom path is scaffold only today — typed BYO-renderer protocol not yet defined; consumers walk the engine's naf_* C-API.
| Module | What it ships |
|---|---|
:sdk:naf-core | Headless session + builder, container, filesystem init, libnafcore.so JNI |
:sdk:naf-config | Bundled resolver-config + texture-settings + default-asset JSON |
:sdk:naf-tools | Renderer-less helpers (camera, picker, transform gizmo) |
:sdk:naf-ui | NafView + 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()
}
}
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.
The integrated path above covers majority of consumer use. The sections below are the per-type API reference for when you need more.
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 / method | Notes |
|---|---|
cameraTarget: FloatArray | [x, y, z] |
cameraDistance: Float | JVM-renamed nafCameraDistance to avoid View.setCameraDistance |
cameraYaw, cameraPitch, cameraMin/MaxDistance, cameraMin/MaxPitch, cameraRotate/PanSensitivity: Float | Passthroughs to cameraControls |
cameraGesturesEnabled: Boolean | Default 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 / method | Notes |
|---|---|
avatarPosition: FloatArray | [x, y, z] world translation |
avatarModelTransform: FloatArray | 16-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
| Property | Default | Notes |
|---|---|---|
isPickingEnabled: Boolean | false | Opt 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)? | null | Fires on tap; null result = miss |
runPick(screenX, screenY): NafPickResult? | — | Programmatic pick (no touch) |
Gizmo
| Property / method | Default | Notes |
|---|---|---|
transformGizmo: NafTransformGizmo | — | The underlying math object |
gizmoAxisLength: Float | — | Passthrough to transformGizmo.axisLength |
isGizmoEnabled: Boolean | false | Opt in to wire the axis-drag recognizer |
isGizmoVisible: Boolean | false | Draws the overlay; typically toggled from onPick |
onGizmoMove: ((x, y, z) -> Unit)? | null | Fires on every drag step |
Render config
| Property / method | Default | Notes |
|---|---|---|
clearColor: FloatArray | [0.04, 0.04, 0.05, 1] | [r, g, b, a]; alpha < 1 needs isOpaque = false |
isOpaque: Boolean | true | JVM-renamed nafIsOpaque to avoid View.isOpaque() |
setTransparent(Boolean) | — | Underlying surface format flip |
showSkeleton: Boolean | false | Debug overlay |
lighting: NafLighting | — | See Rendering section below |
Power & thermal
| Property / method | Default | Notes |
|---|---|---|
isRenderingPaused: Boolean | false | Halts the per-vsync drive |
rendersOnDemand: Boolean | false | Only submit when state changes; call setNeedsRedraw() for consumer-driven mutations |
adaptiveFrameRate: Boolean | false | Modulate between active/idle FPS tiers |
adaptiveActiveFPS / IdleFPS: Int | 60 / 24 | Tier values |
preferredFramesPerSecond: Int | 0 | Ceiling FPS; combines as min(tier, ceiling); 0 = uncapped |
renderScale: Float | 1.0 | Clamped [0.25, 1.0]; lower = fewer fragments |
setNeedsRedraw() | — | Wake the on-demand loop |
currentFps: Float | 0 | EMA 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 / method | Notes |
|---|---|
session: NafAvatarSession | The 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.
| API | Notes |
|---|---|
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: Long | RAW pointers for BYO renderer use |
| State flows | isAvatarLoadedFlow, isLoadingFlow, isSilhouetteActiveFlow, currentModeFlow (each with a snapshot val alias) |
| Callbacks | onAvatarReady(mergedPtr, controllerHandle), onAvatarUnload(), onSilhouetteModeChanged(active), onLoadingChange(loading) |
NafLoadMode
| Mode | What it does |
|---|---|
StaticPose | Idle 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.
To swap the default idle clip globally, override NafDefaultAssets.idle once at app startup — every subsequent loadAvatar picks it up.
NafShaderManager (toon + outline)
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 / transparency — view.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?
}
| Code | NafEventType | Fires today? |
|---|---|---|
| 100 / 101 / 102 | AssetLoadStarted / Completed / Failed | ✅ |
| 200 / 201 | AvatarReady / SilhouetteReady | ✅ |
| 202 / 203 | LodUpgradeStart / LodUpgradeCompleted | ✅ (use NafEvent.toLodUpgrade()) |
| 300–306 | Gesture* / Emotion* / Boredom* / VoiceAnim* | ❌ — lands with behavior mode |
| 400 / 401 / 402 | IdleEntered / TalkingStarted / TalkingStopped | ❌ |
| 900 | Error (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)
| Area | Status |
|---|---|
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 | ❌ |