Zernikalos
Quick Start

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();

On this page