Integrating Zernikalos
Learn how to initialize the engine, load resources, and implement your first scene on Android, iOS, and Web.
1. Engine & View Setup
The first step is to prepare the surface where the engine will render. Each platform uses its own native view.
Zernikalos provides a custom ZernikalosView (which wraps a GLSurfaceView). Add it to your layout:
<zernikalos.ui.ZernikalosView
android:id="@+id/render_surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />Then, initialize the engine in your Activity:
val engine = Zernikalos()
val renderSurface = findViewById<ZernikalosView>(R.id.render_surface)Zernikalos uses MetalKit (MTKView) for rendering on iOS.
import MetalKit
import Zernikalos
// Inside your UIViewController
guard let mtkView = view as? MTKView else { return }
guard let device = MTLCreateSystemDefaultDevice() else { return }
mtkView.device = device
let engine = Zernikalos()In the browser, Zernikalos renders to a standard HTMLCanvasElement using WebGPU.
import { zernikalos } from '@zernikalos/zernikalos';
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
const engine = new zernikalos.Zernikalos();2. Core Concepts
Before diving into code, here are the three pillars of a Zernikalos application:
Loading Resources (.zko)
All 3D assets (models, textures, animations) must be bundled into a .zko file using the Nest App.
Place your .zko files in the src/main/assets/ directory of your Android project.
Add your .zko files to your Xcode project resources and ensure they are included in the "Copy Bundle Resources" build phase.
Place your .zko files in the public/ folder of your web project (or any directory served as static content).
The ZContext
The ZContext is passed to all lifecycle methods. It gives you access to the active scene, the activeCamera, and the underlying rendering state.
Lifecycle Methods
You control the engine via the ZSceneStateHandler interface:
onReady: Called once when the engine is initialized. Ideal for loading models.onUpdate: Called every frame. Use this for game logic and animations.onResize: Called when the view dimensions change.onRender: Called every frame for the final draw pass.
3. Basic State Handler Snippets
These snippets show the minimal logic required to get the engine running.
engine.initialize(renderSurface, object : ZSceneStateHandler {
override fun onReady(context: ZContext, done: () -> Unit) {
val bytes = assets.open("model.zko").use { it.readBytes() }
val zko = loadFromProto(bytes)
val scene = ZScene()
scene.addChild(zko.root)
context.scene = scene
done()
}
override fun onUpdate(context: ZContext, done: () -> Unit) = done()
})class MyHandler: ZSceneStateHandler {
func onReady(context: ZContext, done: () -> Void) {
let zko = ZkoLoader.companion.loadFromMainBundlePathSync(fileName: "model")
let scene = ZScene()
if let root = zko?.root { scene.addChild(child: root) }
context.scene = scene
done()
}
}
engine.initialize(view: mtkView, stateHandler: MyHandler())engine.initializeWithCanvas(canvas, {
onReady: async (context, done) => {
const res = await fetch('/model.zko');
const bytes = new Int8Array(await res.arrayBuffer());
const zko = await zernikalos.loader.loadFromProto(bytes);
const scene = new zernikalos.objects.ZScene();
scene.addChild(zko.root);
context.scene = scene;
done();
}
});4. Complete Samples
Finally, here are full, copy-pasteable examples for each platform including camera setup and a basic rotation.
import android.os.Bundle
import androidx.activity.ComponentActivity
import kotlinx.coroutines.*
import zernikalos.*
import zernikalos.context.ZContext
import zernikalos.loader.loadFromProto
import zernikalos.objects.*
import zernikalos.scenestatehandler.ZSceneStateHandler
import zernikalos.search.findFirstCamera
import zernikalos.ui.ZernikalosView
class MainActivity : ComponentActivity() {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val renderSurface = ZernikalosView(this)
setContentView(renderSurface)
val engine = Zernikalos()
engine.initialize(renderSurface, object : ZSceneStateHandler {
private var rootGroup: ZGroup? = null
override fun onReady(context: ZContext, done: () -> Unit) {
scope.launch {
val bytes = assets.open("scene.zko").use { it.readBytes() }
val zko = loadFromProto(bytes)
rootGroup = zko.root as? ZGroup
val scene = ZScene()
val camera = ZCamera()
scene.addChild(camera)
scene.addChild(zko.root)
context.scene = scene
context.activeCamera = findFirstCamera(scene)
done()
}
}
override fun onUpdate(context: ZContext, done: () -> Unit) {
rootGroup?.transform?.rotate(0.1f, 0f, 1f, 0f)
done()
}
})
}
}import MetalKit
import Zernikalos
class StateHandler: ZSceneStateHandler {
private var root: ZObject?
func onReady(context: ZContext, done: () -> Void) {
let zko = ZkoLoader.companion.loadFromMainBundlePathSync(fileName: "scene")
self.root = zko?.root
let scene = ZScene()
let camera = ZCamera.companion.DefaultPerspectiveCamera
if let r = root { scene.addChild(child: r) }
scene.addChild(child: camera)
context.scene = scene
context.activeCamera = camera
done()
}
func onUpdate(context: ZContext, done: @escaping () -> Void) {
root?.transform.rotate(angle: 0.2, x: 0, y: 1, z: 0)
done()
}
}import { zernikalos } from '@zernikalos/zernikalos';
async function start() {
const canvas = document.createElement('canvas');
canvas.style.width = '100vw'; canvas.style.height = '100vh';
document.body.appendChild(canvas);
const engine = new zernikalos.Zernikalos();
const res = await fetch('/scene.zko');
const bytes = new Int8Array(await res.arrayBuffer());
const zko = await zernikalos.loader.loadFromProto(bytes);
engine.initializeWithCanvas(canvas, {
onReady: (ctx, done) => {
const scene = new zernikalos.objects.ZScene();
const camera = new zernikalos.objects.ZCamera();
scene.addChild(zko.root);
scene.addChild(camera);
camera.transform?.translate(0, 0, -10);
ctx.scene = scene;
ctx.activeCamera = camera;
done();
},
onUpdate: (ctx, done) => {
zko.root.transform?.rotate(0.1, 0, 1, 0);
done();
}
});
}
start();