This page walks through common engine tasks using concrete code.
- Note
- The sample code uses the base
Scene class from the engine and imaginary types SceneTree, SceneInstance and SceneMeshInstance to represent the scene graph. You can use the Lysa Nodes library which provide a Node-based scene graph.
Table of contents
1. Bootstrapping the engine
Every application starts by creating a lysa::ContextConfiguration, instantiating lysa::Lysa, and calling run(). The configuration covers the graphics backend, resource pool capacities, the logging system, and optional physics and scripting settings.
.displayFPS = true,
},
};
int lysaMain() {
if constexpr (vireo::getPlatform() == vireo::Platform::WINDOWS) {
contextConfiguration.backendConfiguration.backend = vireo::Backend::DIRECTX;
}
return 0;
}
The entry function must be named lysaMain() — the platform layer provides the real main() / WinMain() and calls lysaMain() after performing platform-specific initialization.
Sanity-check the VFS before doing any asset work:
if (!
lysa::ctx().fs.directoryExists(
"app://shaders")) {
"or build the 'shaders' target first");
return;
}
2. Creating the window
Declare a lysa::RenderingWindowConfiguration and extend lysa::RenderingWindow. The nested rendererConfiguration selects the rendering path, anti-aliasing, tone-mapping, and optional effects (bloom, AO, TAA…).
.width = 1280,
.height = 720,
.renderTargetConfiguration = {
.presentMode = vireo::PresentMode::VSYNC,
.rendererConfiguration = {
.depthStencilFormat = vireo::ImageFormat::D32_SFLOAT,
.clearColor = lysa::float3{0.0f, 0.2f, 0.4f},
.bloomEnabled = true,
.taaEnabled = false,
}
}
};
public:
MainWindow() : RenderingWindow(renderingWindowConfiguration) {
});
});
}
};
Renderer type options are lysa::RendererType::FORWARD and lysa::RendererType::DEFERRED (the latter requires the DEFERRED_RENDERER CMake flag, which is ON by default).
3. Setting up a scene
Derive from lysa::Scene and override the lifecycle hooks:
public:
MyScene() : Scene(
lysa::SceneConfiguration{}) {
}
};
Wire the scene into the render target and connect the main-loop events in your application class:
window.getRenderTarget().addView(view);
const auto delta = std::any_cast<double>(evt.
payload);
scene.onPhysicsProcess(delta);
});
const auto delta = std::any_cast<double>(evt.
payload);
window.getRenderTarget().render();
});
window.getRenderTarget().id,
view.viewport = {};
view.scissors = {};
window.getRenderTarget().updateView(view);
});
4. Creating meshes and instances
Build a lysa::Mesh from raw vertex/index data, create a lysa::MeshInstance to place it in the world, and add it to the scene:
const std::vector<lysa::Vertex> vertices {
{.position = { 0.0f, 0.5f, 0.0f}, .uv = {0.5f, 0.25f}},
{.position = { 0.5f, -0.5f, 0.0f}, .uv = {0.75f, 0.75f}},
{.position = {-0.5f, -0.5f, 0.0f}, .uv = {0.25f, 0.75f}},
};
const std::vector<lysa::uint32> indices { 0, 1, 2 };
const std::vector<lysa::MeshSurface> surfaces {
};
lysa::Mesh&
mesh = meshManager.create(vertices, indices, surfaces,
"MyTriangle");
instance.setTransform(lysa::float4x4::translation(1.0f, 0.0f, 0.0f));
scene.addInstance(instance);
Build a scene graph (parent → child transforms):
auto parent = std::make_shared<SceneMeshInstance>(meshA, "Parent");
auto child = std::make_shared<SceneMeshInstance>(meshB, "Child",
lysa::float4x4::translation(0.5f, 0.0f, 0.0f));
parent->addChild(child);
root.addChild(parent);
addInstance(root);
- Note
- The Lysa Nodes library provides a Node-bases scene graph with assets pack loading and animation support.
Updating transforms every frame:
instance->setTransform(
lysa::mul(lysa::float4x4::rotation_y(angle),
instance->getTransform()));
5. Materials
Standard material — PBR surface with albedo, diffuse texture, and transparency:
Shader material — custom Slang vertex/fragment shaders with per-material parameters:
"uv_gradient.frag",
"scale.vert"
);
Assign a material to a mesh surface:
mesh.setSurfaceMaterial(0, mat.
id);
Override a material on a specific instance without modifying the mesh:
instance.meshInstance.setSurfaceOverrideMaterial(0, otherMat.id);
instance.meshInstance.removeSurfaceOverrideMaterial(0);
Update shader parameters every frame (e.g., in onPhysicsProcess):
void MyScene::onPhysicsProcess(const double delta) {
gradient = std::clamp(gradient + gradientSpeed * static_cast<float>(delta), 0.0f, 1.0f);
if (gradient == 1.0f || gradient == 0.0f) gradientSpeed = -gradientSpeed;
}
6. Lights
Three light types are available. All lights can optionally cast shadow maps.
Directional light:
{1.0f, 1.0f, 1.0f},
2.0f,
};
sun.castShadows = true;
sun.shadowMapSize = 4096;
sun.shadowMapCascadesCount = 3;
sun.shadowMapCascadesSplitLambda = 0.80f;
addLight(sun);
Omni (point) light:
{1.0f, 1.0f, 1.0f},
1.0f,
lysa::float4x4::translation(0.0f, 2.0f, 0.0f),
10.0f
};
omni.castShadows = true;
omni.shadowMapSize = 1024;
addLight(omni);
Spot light:
{1.0f, 1.0f, 1.0f},
1.5f,
lysa::float4x4::translation(-7.0f, 1.8f, 0.0f))),
10.0f,
};
addLight(spot);
The maximum number of simultaneous lights per scene is set through SceneConfiguration::maxLights.
7. Loading assets packs
Pre-exported .assets files (produced by the Blender add-on or the gltf2lysa converter) contain a list of all the assets in the scene, are loaded through the VFS and hydrated into your scene-graph node types via AssetsPack::load<>():
std::shared_ptr<SceneInstance> load(const std::string& fileURI) {
return lysa::AssetsPack::load<SceneInstance, SceneMeshInstance, DummyAnimationPlayer>(fileURI);
}
Usage in a scene:
const auto model = load("app://res/models/sponza.assets");
root.addChild(model);
addInstance(root);
Cloning an already-loaded asset (useful for placing multiple copies without reloading from disk):
std::shared_ptr<SceneInstance> clone(const std::shared_ptr<SceneInstance>& orig) {
std::shared_ptr<SceneInstance> copy;
if (const auto& mi = std::dynamic_pointer_cast<SceneMeshInstance>(orig)) {
copy = std::make_shared<SceneMeshInstance>(*mi);
} else {
copy = std::make_shared<SceneInstance>(*orig);
}
for (const auto& child : orig->children) {
copy->addChild(clone(child));
}
return copy;
}
for (int i = 0; i < 5; ++i) {
const auto instance = clone(model);
instance->setTransform(lysa::float4x4::translation(
0.0f,
root.addChild(instance);
}
addInstance(root);
8. Camera
Construct a lysa::Camera with a view transform, a perspective projection, and near/far planes, then pass it to lysa::RenderView :
constexpr float near = 0.01f;
constexpr float far = 100.0f;
lysa::float4x4::identity(),
near, far);
camera.transform = lysa::float4x4::translation(0.0f, 1.8f, 8.0f);
Rebuild the projection matrix when the window is resized:
window.getRenderTarget().id,
fov, window.getRenderTarget().getAspectRatio(), near, far);
});
For a fly camera with mouse look, keyboard movement, and gamepad support subscribes to PHYSICS_PROCESS for movement and PROCESS for mouse-delta look.
const auto mousePos = window.getMousePosition();
auto offset = (lastMousePos - mousePos) * mouseSensitivity;
rotate(offset.y * invertY, offset.x);
camera.transform = pivot->globalTransform;
});
9. Handling input
Input events arrive through the event system. Subscribe to RenderingWindowEvent::INPUT on your window's id:
const auto& inputEvent = std::any_cast<const lysa::InputEvent&>(evt.payload);
scene.onInput(inputEvent);
});
Inside your scene, dispatch on the event type:
const auto& key = std::get<lysa::InputEventKey>(
event.data);
rotating = !rotating;
}
spawnInstance();
}
}
const auto& btn = std::get<lysa::InputEventMouseButton>(
event.data);
if (!btn.pressed) captureMouse();
}
}
Polling helpers (call inside PHYSICS_PROCESS):
Mouse capture:
window.resetMousePosition();
auto pos = window.getMousePosition();
10. The event system
Subscribe to any event by type (and optional source id):
const double delta = std::any_cast<double>(evt.
payload);
update(delta);
});
renderTarget.id,
const auto extent = std::any_cast<vireo::Extent>(evt.payload);
onResize(extent);
});
Always store and unsubscribe event from objects with a shorter lifetime than the engine (e.g., in destructors of camera or scene helpers):
11. Instance visibility and dynamic add/remove
Toggle visibility on an instance (and all its children recursively):
instance->setVisible(false);
instance->setVisible(true);
Remove an instance from the scene without destroying it, then add it back:
if (
scene.haveInstance(meshInstance)) {
scene.removeInstance(meshInstance);
}
scene.addInstance(meshInstance);
Check presence before operating:
mesh.meshInstance.setVisible(!
mesh.meshInstance.isVisible());
}
12. Post-processing passes
Custom compute passes can be added to the renderer's post-processing chain. Pass a shader name and an optional push-constant data pointer:
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat(),
"blur",
&blurData,
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat(),
"greyscale");
renderTarget.getRenderer().addPostprocessing(blur);
renderTarget.getRenderer().addPostprocessing(greyscale);
Update push-constant data when the window is resized:
void MyScene::onResize(const vireo::Extent& extent) {
blurData.
update(extent, 2.0f);
}
Debug: display an internal attachment on screen using lysa::DisplayAttachment to inspect G-Buffer layers, AO, shadow maps, TAA, or any intermediate pass:
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat());
renderTarget.getRenderer().addPostprocessing(display);
13. 2D and 3D vector rendering
Add a lysa::Vector3DRenderer for world-space primitives and a lysa::Vector2DRenderer for screen-space UI overlays:
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat());
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat());
renderTarget.addSceneRenderer(renderer3D);
renderTarget.addUIRenderer(renderer2D);
Call restart() at the start of each onProcess frame, then issue draw commands:
void MyScene::onProcess(const double alpha) {
SceneTree::onProcess(alpha);
renderer3D.restart();
renderer2D.restart();
renderer3D.drawLine(from, to, {1.0f, 0.0f, 0.0f, 1.0f});
renderer2D.drawText(screenFont, "Hello!", {10.0f, 10.0f}, {1.0f, 1.0f, 1.0f, 1.0f});
}
Load a lysa::Font for text rendering:
lysa::Font screenFont(
"app://res/fonts/Signwood");
screenFont.setOutlineWidthAbsolute(1.0f / 4.0f);
screenFont.setOutlineWidthRelative(1.0f / 2.0f);
screenFont.setOutlineThreshold(0.2f);