Source code for mini_arcade_core.engine.render.passes.postfx
"""
Post-processing effects render pass implementation.
"""
from __future__ import annotations
from dataclasses import dataclass
from mini_arcade_core.backend import Backend
from mini_arcade_core.engine.render.camera import viewport_transform_for_packet
from mini_arcade_core.engine.render.context import RenderContext
from mini_arcade_core.engine.render.effects.registry import EffectRegistry
from mini_arcade_core.engine.render.frame_packet import FramePacket
from mini_arcade_core.engine.render.packet import DrawOp, RenderPacket
[docs]
@dataclass
class PostFXPass:
"""
PostFX Render Pass.
This pass handles scene effect-layer draw ops and optional post-processing.
"""
name: str = "PostFXPass"
registry: EffectRegistry | None = None
[docs]
def run(
self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
):
"""Run the post-processing effects render pass."""
self._draw_effect_layer(backend, ctx, packets)
self._apply_screen_effects(backend, ctx)
def _draw_effect_layer(
self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
) -> None:
for fp in packets:
if fp.is_overlay:
continue
ops = self._layer_ops(fp.packet, "effects")
if ops is None or not ops:
continue
ctx.stats.packets += 1
ctx.stats.renderables += len(ops)
ctx.stats.draw_groups += 1
world_transform = viewport_transform_for_packet(
ctx.viewport,
fp.packet,
)
backend.set_viewport_transform(
ctx.viewport.offset_x,
ctx.viewport.offset_y,
ctx.viewport.scale,
)
backend.render.set_clip_rect(
0,
0,
ctx.viewport.virtual_w,
ctx.viewport.virtual_h,
)
try:
backend.set_viewport_transform(
world_transform.ox,
world_transform.oy,
world_transform.s,
)
for op in ops:
op(backend)
finally:
backend.render.clear_clip_rect()
backend.clear_viewport_transform()
def _apply_screen_effects(
self, backend: Backend, ctx: RenderContext
) -> None:
stack = ctx.meta.get("effects_stack")
if stack is None or not stack.is_active():
return
# Screen space: no transforms
backend.clear_viewport_transform()
backend.render.clear_clip_rect()
reg = self.registry
if reg is None:
return
for effect_id in list(stack.active):
effect = reg.get(effect_id)
if effect is None:
continue
effect.apply(backend, ctx)
@staticmethod
def _layer_ops(
packet: RenderPacket, key: str
) -> tuple[DrawOp, ...] | None:
raw = packet.meta.get("pass_ops")
if not isinstance(raw, dict):
return None
ops = raw.get(key)
if ops is None:
return tuple()
return tuple(ops)