Skip to content

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

  1. Scene gathers active Drawable components.
  2. Components call submit, submitModel, or submitNative on RenderQueue.
  3. RenderQueue::buildBatches() batches compatible 2D items and builds ordered sort layers.
  4. The platform renderer walks each layer's 2D batches, model items, and native commands.

Sorting and Filtering

  • RenderItem::sortOrder controls draw order. Larger values render first; smaller values render later and therefore appear in the foreground.
  • TextureFiltering can be Nearest, Linear, Anisotropic, or Inherit.
  • BlendMode can be Normal, Additive, Multiply, or Screen.
  • RenderItem::indices enables custom triangle geometry when submitting RenderItem directly.
  • RenderItem::premultipliedAlpha selects PMA blending when needed.
  • RenderItem::scissorEnabled/scissorRect provide 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:

  • ambientColor and ambientIntensity
  • directionalDirection, directionalColor, and directionalIntensity

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:

renderer->registerShader(myShaderId, vertexSrc, fragmentSrc, /*usesParams=*/true);

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