Skip to content

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
  1. 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.
  2. If a shadow-casting DirectionalLight3D exists, its shadow map is rendered first (a depth-only pass from the light's own view/projection
  3. see _render_shadow_map()) so step 3 can sample it.
  4. 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).
  5. 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.
  6. '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.