Rendering¶
Most games draw through components attached to scene nodes. For example, this
adds a sprite using a path from the game's assets/ directory:
auto* player = createNode("player");
auto* sprite = player->addComponent<snowpulse::SpriteRenderer>();
sprite->sprite(appContext()->assetManager()->loadSprite("sprites/player.png"));
addChild(player);
Use Components for built-in renderers and the sections below when you need custom geometry, shaders, batching behavior, or precise draw ordering.
Internally, Snowpulse uses a layered render queue. Normal 2D components submit
RenderItems that are grouped into RenderBatch objects, while FBX components
submit ModelRenderItems and Effekseer uses native draw commands. All three
kinds are merged into RenderSortLayers so their sortOrder values compose in
one render sequence.
Render Flow¶
Scenegathers activeDrawablecomponents.- Components call
submit,submitModel, orsubmitNativeonRenderQueue. RenderQueue::buildBatches()batches compatible 2D items and builds ordered sort layers.- The platform renderer walks each layer's 2D batches, model items, and native commands.
Sorting and Filtering¶
RenderItem::sortOrdercontrols draw order. Larger values render first; smaller values render later and therefore appear in the foreground.TextureFilteringcan beNearest,Linear,Anisotropic, orInherit.BlendModecan beNormal,Additive,Multiply, orScreen.RenderItem::indicesenables custom triangle geometry when submittingRenderItemdirectly.RenderItem::premultipliedAlphaselects PMA blending when needed.RenderItem::scissorEnabled/scissorRectprovide clip rectangles (used by UI).
Screen is a lightening blend useful for glows and highlights; black leaves
the destination unchanged while brighter source colors move it toward white.
TextureFiltering::Inherit resolves to the queue default. The default is
Linear; the normal application render loop copies
AppContext::textureFiltering() to the frame queue before components draw:
scene->appContext()->textureFiltering(snowpulse::TextureFiltering::Nearest);
// Useful for a manually managed queue outside the normal application loop.
snowpulse::RenderQueue queue;
queue.defaultTextureFiltering(snowpulse::TextureFiltering::Anisotropic);
Set an individual component or RenderItem to Nearest, Linear, or
Anisotropic to override that default. The RenderQueue* supplied to
Drawable::draw() is the scene's queue for the current frame; submit to it and
let the engine clear it before gathering and call buildBatches() afterward.
RenderQueue::batches() exposes the 2D batches for statistics and diagnostics.
sortLayers() is the complete render input and is what IRenderer::render()
consumes.
RenderItem and Custom Drawable¶
Implement both Component and Drawable to add a renderable component. A
RenderItem with no explicit vertices submits a quad; the queue generates its
vertices from the transform, size, sprite-frame metadata, and UV rectangle.
This example shows every public RenderItem field:
class CustomDrawable final : public snowpulse::Component,
public snowpulse::Drawable {
public:
explicit CustomDrawable(snowpulse::SpriteRegion region)
: _region(region) {}
void draw(snowpulse::RenderQueue* queue) override {
if (!isActive || !queue || !owner()) {
return;
}
snowpulse::RenderItem item;
item.worldMatrix = owner()->transform().worldMatrix;
item.color = { 0.75f, 0.9f, 1.0f, 0.85f };
item.size = _region.hasSize ? _region.size : glm::vec2 { 64.0f, 64.0f };
item.uvRect = _region.uvRect;
item.sourceSize = _region.sourceSize;
item.frameSize = _region.frameSize;
item.frameOffset = _region.frameOffset;
item.rotated = _region.rotated;
item.primitive = snowpulse::RenderPrimitive::Triangles;
item.vertices = {}; // Empty selects generated-quad geometry.
item.indices = {};
item.shaderId = snowpulse::kShaderDefault;
item.shaderParams = { 0.0f, 0.0f, 0.0f, 0.0f };
item.sortOrder = sortOrder;
item.texture = _region.texture;
item.textureFiltering = snowpulse::TextureFiltering::Inherit;
item.blendMode = snowpulse::BlendMode::Screen;
item.premultipliedAlpha = true;
item.scissorEnabled = false;
item.scissorRect = { 0.0f, 0.0f, 0.0f, 0.0f };
queue->submit(std::move(item));
}
private:
snowpulse::SpriteRegion _region;
};
const auto region = assetManager()->loadSprite("visuals/region.png");
auto* visual = node->addComponent<CustomDrawable>(region);
visual->sortOrder = -20;
For custom triangles, populate vertices and optionally indices. Unlike the
generated-quad path, custom vertex positions and colors are copied as supplied,
so transform local positions before submitting them:
snowpulse::RenderItem item;
item.primitive = snowpulse::RenderPrimitive::Triangles;
item.sortOrder = sortOrder;
item.blendMode = snowpulse::BlendMode::Screen;
const auto& world = owner()->transform().worldMatrix;
const auto vertex = [&world](const glm::vec2& local) {
const glm::vec3 position(world * glm::vec4(local, 0.0f, 1.0f));
return snowpulse::RenderVertex(
position, glm::vec2 { 0.0f, 0.0f }, glm::vec4 { 1.0f });
};
item.vertices = {
vertex({ -24.0f, -16.0f }),
vertex({ 24.0f, -16.0f }),
vertex({ 0.0f, 28.0f }),
};
item.indices = { 0, 1, 2 };
queue->submit(std::move(item));
Models, Native Commands, and Lighting¶
submitModel() accepts ModelRenderItem geometry and PBR-style material data
used by FbxRenderer. submitNative(sortOrder, callback) inserts a backend
draw callback at the requested layer; Effekseer uses this path to keep native
effect rendering ordered with normal engine content.
Each scene supplies a SceneLighting value to the renderer:
ambientColorandambientIntensitydirectionalDirection,directionalColor, anddirectionalIntensity
These values affect model items. The built-in sprite, primitive, text, and UI shaders remain unlit.
Primitive Rendering¶
Use QuadRenderer, TriangleRenderer, and CircleRenderer for texture-free
solid-color shapes. TriangleRenderer submits a three-vertex
RenderPrimitive::Triangles item from local-space points, so it can represent
arrows, wedges, simple markers, and custom one-triangle debug geometry without
building a full RenderItem manually.
Shaders¶
The renderer supports registering custom shaders:
RenderItem::shaderId and RenderItem::shaderParams select the shader and any custom parameters.
Android uses the shared GLES3 renderer core with a thin EGL presentation
binding. It requests RGBA8/depth24/stencil8 and presents with Android Frame
Pacing (SwappyGL_swap) when vsync is enabled. Render-command execution is
separate from presentation, leaving a Vulkan seam without promising a Vulkan
backend in v1. Registered shaders, cached/dynamic textures, ImGui, and
Effekseer device objects are restored after EGL_CONTEXT_LOST.
Desktop Recording¶
Opt-in macOS marketing builds include an engine-level recorder shortcut
(Control + R) that captures the application scene to MP4. The
capture pass renders into an offscreen framebuffer sized exactly to
targetResolution, so
the video resolution is independent of the window size. The capture view is
centered when the window and target widths differ. ImGui/debug UI is drawn after
this capture pass and is not included in the recording.
See Platforms for application-level configuration.
Sample Usage¶
auto* sprite = node->addComponent<snowpulse::SpriteRenderer>();
sprite->sortOrder = 10;
sprite->textureFiltering = snowpulse::TextureFiltering::Nearest;
sprite->sprite(assetManager()->loadSprite("visuals/sprite.png"));