Repository navigation
Add Tr2Sprite2dEffect, a sprite that draws an effect over its rect - #135
Merged
Merged
Conversation
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.
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
approved these changes
Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
Tr2Sprite2dEffect, a leaf sprite that draws oneTr2Effectover 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
Tr2Sprite2dRenderJobper 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
Tr2Sprite2dEffectsends 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'scolor,glowBrightnessandspriteTargetride 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.