pipeline
FreeBodyEngine.graphics.pbr.pipeline
#
PBRPipeline: PBRPipeline draws the active scene's tilemaps/sprites/debug draws/3D models into a multi-attachment G-buffer, runs a deferred lighting composite pass over it (see graphics/pbr/lighting.py for the Light node types and graphics/pbr/shaders.py for the composite/forward shader source), then forward-shades any transparent objects on top, and finally presents the result.
Frame structure
- Opaque/additive geometry (blend_mode OPAQUE/ADDITIVE - see graphics/material.py's BlendMode) is queued via Renderer.submit() and drawn into the G-buffer via Renderer.flush_opaque() - state-minimizing batching (see graphics/instancing.py), not GPU instancing.
- If a shadow-casting DirectionalLight3D exists, its shadow map is rendered first (a depth-only pass from the light's own view/projection
- see _render_shadow_map()) so step 3 can sample it.
- A single fullscreen composite pass (_draw_composite) reads the G-buffer back and accumulates every active light (up to graphics.pbr.shaders.MAX_LIGHTS) into the 'lit' attachment - one fragment-shader loop over the whole screen, not one draw call per light. This is what keeps lighting itself cheap regardless of light count (within MAX_LIGHTS).
- Transparent geometry (blend_mode TRANSPARENT) is forward-shaded directly onto 'lit' via Renderer.flush_transparent(), since a deferred G-buffer can only hold one opaque surface per pixel and can't represent a blended one at all.
- 'lit' (now
output_channel's default) is blitted to the window.
Why light data is MAX_LIGHTS flat uniform slots, not a real uniform array or
an FBUSL @buffer block: @buffer blocks exist in FBUSL's grammar but
GL33Generator's own docstring says GL33 "has no real SSBOs" and implements
them via buffer-texture reads restricted to readonly - written for the
raytrace/compute-kernel path (see graphics/gl33/compute.py), not proven for
an ordinary vertex/fragment Shader. A true GLSL uniform array (vec4[32])
is representable in FBUSL's type grammar, but GLShader.set_uniform's
GL-call dispatch (graphics/gl33/shader.py's set_gl_uniform) only issues
single-element glUniform/glUniformMatrix calls, never the *v variants an
array uniform needs. Flat per-slot uniforms (Light0_Color, Light1_
Color, ...) use only already-proven scalar/vector uniform plumbing - lower
risk than exercising either untested path for this rebuild, at the cost of
MAX_LIGHTS being a hard cap that costs a shader recompile to raise (see
graphics/pbr/shaders.py).
SHADOW_MAP_SIZE = 2048
module-attribute
#
PBRPipeline()
#
Bases: GraphicsPipeline
GraphicsPipeline for physically-based rendering - see module docstring for the full frame structure.
Adds 'scene_manager' as a service dependency, alongside GraphicsPipeline's own 'renderer' dependency.
create_material(data, injector)
#
Builds a PBRMaterial from data, using injector to resolve FBUSL builtins.
draw()
#
Renders one frame - see module docstring for the full frame structure. Falls back to just clearing the window to opaque black if the active scene has no camera.
draw_world_mesh(mesh, material, transform, camera, instances=1)
#
Queues mesh/material/transform for drawing against camera
via Renderer.submit() (see Renderer.flush_opaque()/
flush_transparent() for when queued calls actually get drawn).
instances is accepted but unused - see graphics/instancing.py's
module docstring for why automatic GPU instancing isn't part of
this queue.
on_initialize()
#
Fetches the 'scene_manager' service, creates main_framebuffer
(the G-buffer plus a 'lit' attachment the composite/forward passes
write into), compiles the internal composite/shadow shaders, and
creates the shadow-map framebuffer. output_channel defaults to
'lit' - still overridable to any other G-buffer channel for
debugging (e.g. 'albedo', 'gWorldNormal').
resize(size)
#
Resizes main_framebuffer to match the new framebuffer size.