Summary
Forge has no way to clip a sprite to a rectangle. Anything that needs to render "only the part of this inside that region" — scrolling lists, minimap viewports, fog-of-war reveals, wipe transitions, health bars that fill by revealing rather than scaling, cropped portraits — currently has no mechanism.
Raised by @stormmuller while reviewing #580, on the grounds that clipping (like text rendering) is generally useful and shouldn't be buried inside a UI module. Explicitly out of scope for the UI work; filed here so it can be designed on its own merits.
Why not gl.scissor
CameraEcsComponent.scissorRect already exists as a per-camera concept, but scissor is the wrong tool for per-sprite clipping: it is GL state, so changing it forces a draw-call break. Applied to a scrolling list, that means one draw call per clipped element — destroying batching for precisely the case with the most elements.
Suggested approach
Pass the clip rectangle as per-instance data and discard in the fragment shader, the way Unity's RectMask2D works:
- +4 floats per instance (
clipMin.xy, clipMax.xy) on the instance data segment.
- Fragment shader discards fragments outside the rect.
- Batching is preserved — a whole scrolling list stays in one draw call.
- Nested clips intersect their rectangles, so nesting costs nothing extra.
- Unclipped sprites pass an infinite rect, or the segment is opt-in via a separate
Renderable.
Worth considering during design:
- Soft edges. A one- or two-pixel feather is nearly free in the shader and looks markedly better than a hard
discard on scrolling text.
- Rotation. An axis-aligned clip rect in world space is the simple case. Clipping a rotated element correctly means either transforming the fragment into the clipper's local space, or accepting axis-aligned-only and documenting it.
- Arbitrary shapes. A stencil-buffer or alpha-cutoff mask (Unity's
Mask) is strictly more general but adds stencil state management to a renderer that currently has none. Probably a separate, later mechanism rather than part of this.
Relationship to the UI design (#580)
design/ui-system.md DL-09 covers the same ground and reached the same per-instance-clip conclusion. That design now defers to this issue rather than owning the work.
Note the dependency: ScrollRectEcsComponent (UI backlog 3.4) cannot ship before this lands. Scroll views are unusable without clipping — content spills past the viewport. Anything else in the UI backlog is unaffected.
Summary
Forge has no way to clip a sprite to a rectangle. Anything that needs to render "only the part of this inside that region" — scrolling lists, minimap viewports, fog-of-war reveals, wipe transitions, health bars that fill by revealing rather than scaling, cropped portraits — currently has no mechanism.
Raised by @stormmuller while reviewing #580, on the grounds that clipping (like text rendering) is generally useful and shouldn't be buried inside a UI module. Explicitly out of scope for the UI work; filed here so it can be designed on its own merits.
Why not
gl.scissorCameraEcsComponent.scissorRectalready exists as a per-camera concept, but scissor is the wrong tool for per-sprite clipping: it is GL state, so changing it forces a draw-call break. Applied to a scrolling list, that means one draw call per clipped element — destroying batching for precisely the case with the most elements.Suggested approach
Pass the clip rectangle as per-instance data and
discardin the fragment shader, the way Unity'sRectMask2Dworks:clipMin.xy,clipMax.xy) on the instance data segment.Renderable.Worth considering during design:
discardon scrolling text.Mask) is strictly more general but adds stencil state management to a renderer that currently has none. Probably a separate, later mechanism rather than part of this.Relationship to the UI design (#580)
design/ui-system.mdDL-09 covers the same ground and reached the same per-instance-clip conclusion. That design now defers to this issue rather than owning the work.Note the dependency:
ScrollRectEcsComponent(UI backlog 3.4) cannot ship before this lands. Scroll views are unusable without clipping — content spills past the viewport. Anything else in the UI backlog is unaffected.