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 gathers one RenderPlan while preserving two distinct
rendering paths. Legacy 2D components submit RenderItems that are grouped
into RenderBatch and RenderSortLayer objects. Opt-in ModelRenderer
components submit ModelRenderItems to dedicated forward-rendered 3D passes.
Effekseer may submit a native 2D command or a depth-aware native 3D command.
Render Flow¶
Scenegathers activeDrawablecomponents.- Components call
submit,submitModel,submitNative, orsubmitNative3DonRenderQueue. RenderQueue::buildBatches()batches compatible 2D items, preserves the existing 2D sort/compositing plan, frustum-culls eligible meshes, and sorts opaque and transparent 3D submissions independently.- When 3D content exists, the renderer executes the sun-shadow pass, opaque and masked 3D front-to-back, transparent 3D back-to-front, then legacy 2D and pixel-space UI/overlay. Opaque draws write depth; transparent draws do not.
Depth is cleared once for the 3D world. With no 3D submissions, the established 2D render plan, ordering, compositing, and coordinate behavior are unchanged. See Forward-rendered 3D for the material, lighting, shadow, custom shader, and debug-draw contracts.
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 persistent mesh data, a model matrix, optional GPU bone
palette, and a Material3D snapshot. ModelRenderer is the normal component
interface; FbxRenderer is a deprecated compatibility wrapper. Built-in
materials use metallic-roughness PBR or unlit shading and support opaque, mask,
and blend alpha modes.
submitNative(sortOrder, callback) inserts a depth-disabled callback into the
legacy 2D sequence. submitNative3D(sortOrder, callback, depthTest, depthWrite)
inserts a callback into the perspective world pass. Effekseer selects between
these paths with its renderIn3D setting; the native particle system remains
2D.
Each scene supplies a SceneLighting value to the renderer:
ambientColorandambientIntensity- a
DirectionalLight3Dsnapshot - all active
PointLight3DandSpotLight3Dcandidates
The forward renderer selects one directional, up to eight point, and up to four
spot lights for each draw by priority and bounds influence. One directional
light may cast a stabilized orthographic shadow. The legacy directional fields
remain as a source-compatible fallback when no DirectionalLight3D component
is present. 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 2D shaders:
RenderItem::shaderId and RenderItem::shaderParams select the shader and any custom parameters.
With RENDERING3D ON, use registerShader3D(Shader3DDesc) for portable mesh
shaders. Custom 3D shaders follow the same frame, object, material, light, and
skinning bindings as the built-in PBR, unlit, and shadow variants; the complete
binding table is in Forward-rendered 3D.
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"));