Skip to content

Add Tr2Sprite2dEffect, a sprite that draws an effect over its rect - #135

Merged
filipppavlov merged 2 commits into
carbonengine:mainfrom
evefrontier:sprite2d-effect
Oct 29, 2026
Merged

filipppavlov merged 2 commits into
carbonengine:mainfrom
evefrontier:sprite2d-effect

Conversation

@hinrikg

@hinrikg hinrikg commented Oct 28, 2026

Copy link
Copy Markdown

Summary

Adds Tr2Sprite2dEffect, a leaf sprite that draws one Tr2Effect over its rect, so a UI element can have a custom shader at a low per-element cost.

The problem

A sprite's look is otherwise fixed by the uber shader. The only way to draw a custom shader over a UI element has been a Tr2Sprite2dRenderJob per element, which runs its job outside the scene's batch: the scene flushes, leaves managed rendering, swaps the depth stencil, glow target and viewport around the job, and restores its states afterwards. The job also knows nothing of where its sprite is on screen or how opaque its ancestors are, so the caller has to work that out every frame and place a viewport. And since a viewport is axis-aligned and the glow target is unbound inside a job, the effect neither rotates with its ancestors nor writes glow.

How it works

 Tr2Sprite2dScene::Render
   GatherSprites, in tree order
-    Tr2Sprite2dRenderJob::GatherSprites
-      IssueDrawCall                 # flush the batch
-      RunJobHelper                  # state bracket around the job
-        job->Run                    # set viewport, render effect
-      restore states, vertex declaration, textures, index buffer
+    Tr2Sprite2dEffect::GatherSprites
+      build the quad's 4 vertices
+      RenderTriangleVerts( effect, verts )
+        IssueDrawCall               # flush the batch
+        batch the 4 vertices        # transform index and clip rect like any sprite
+        IssueDrawCall               # effect in place of the uber shader

Tr2Sprite2dEffect sends its quad through the scene's batched vertex path like any sprite's, so it is clipped, transformed, captured into display lists and drawn in 3D as other sprites are, with no per-frame work outside the scene. The effect replaces the uber shader for that one draw call, with the uber shader's vertex layout and constants. The sprite fills the vertex fields for its own purpose: texcoord 0 runs 0 to 1 over the quad, texcoord 1 holds the quad's size, and the sprite's color, glowBrightness and spriteTarget ride in the vertices. An override effect gets none of the scene's textures, since the scene would bind them over the effect's own.

Each effect sprite is still a draw call of its own, which breaks the scene's batch. That is the whole of its cost. A plain batch break is also cheaper than a render job's interruption, since a job saves and restores the scene's render state as well.

Performance

Measured in the client with a few hundred custom-shader sprites spread over different branches of a nested UI tree, against the same sprites drawn as render jobs placed from Python: the effect sprite costs about 10x less CPU per sprite. Even in the best case for the render-job path, with every sprite under one shared ancestor chain so its placement work is shared too, the effect sprite is still about 2.5x cheaper. What remains of the effect sprite's cost is its separate draw call; GPU time is at the noise floor on both paths.

A sprite's look is fixed by the uber shader, so drawing a custom shader
over one UI element has so far taken a Tr2Sprite2dRenderJob per
element. Each one runs its job outside the scene's batch: the scene
flushes what it has drawn, leaves managed rendering, swaps the depth
stencil, glow target and viewport around the job, and restores its
render states and vertex declaration after it. The job knows nothing of
where its sprite is on screen or how opaque its ancestors are, so the
caller has to work that out every frame and place a viewport. A
viewport is axis-aligned and the glow target is unbound inside a job,
so the effect neither rotates with its ancestors nor writes glow.

Tr2Sprite2dEffect is a leaf sprite that draws one effect over its quad,
in order with the other sprites. The quad goes through the scene's
batched path like any sprite's, so it is clipped, transformed, captured
into display lists and drawn in 3D as they are, with no per-frame work
outside the scene. The effect takes the place of the uber shader for
that one draw call, with the uber shader's vertex layout and constants.
The sprite fills the vertex fields for its own purpose: texcoord 0 runs
0 to 1 over the quad, texcoord 1 holds the quad's width and height, and
the sprite's color, glow brightness and sprite target ride in the
vertices. Each effect sprite is a draw call of its own, which breaks
the scene's batch, but that is the whole of its cost.

The sprite builds its quad itself and sets every vertex field, so the
quad doesn't depend on what earlier sprites drew. A new
Tr2Sprite2dScene::RenderTriangleVerts overload draws the vertices as a
draw call of their own, with the given effect overriding the scene's
shader while it does. An override effect gets none of the scene's
textures, as the scene binds them after the effect binds its own, so
neither the scene nor a display list entry captured from it binds them.
Comment thread trinity/Sprite2d/Tr2Sprite2dScene.cpp Resolved
Comment thread trinity/Sprite2d/Tr2Sprite2dEffect.h Resolved
Comment thread trinity/Sprite2d/Tr2Sprite2dEffect.cpp Resolved
Initialize bindTextures on the display list entry that RunJob captures
too, so every Entry sets it. Mark the ITr2SpriteObject overrides with
override instead of virtual, and drop the empty destructor.
GetVertexCount stays a plain method, as on every other sprite: no base
class declares it virtual.
@filipppavlov
filipppavlov merged commit 612e192 into carbonengine:main Oct 29, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants