Core-Profile Rendering

OpenGLContext can render the same scenegraph two ways: through the legacy fixed-function pipeline (the compatibility profile), or through GLSL shaders on an OpenGL 3.3 core-profile context. This document describes the core-profile path: how it is selected, the passes it runs, the shader programs it manages, and the conventions a geometry node must follow to draw through it. The physically based renderer is a specialization of this same path.

Choosing a Profile

The profile is read once, from the environment, when a context is created:

OPENGLCONTEXT_PROFILE=core   python your_script.py     # shader pipeline
OPENGLCONTEXT_PROFILE=compatibility python your_script.py  # fixed-function (default)

core requests an OpenGL 3.3 core-profile context and uses the shader-based render pass. compatibility (the default) uses the fixed-function pipeline. Core profile is the only path that works on platforms that have dropped fixed-function support, such as macOS.

The GLFW backend creates core-profile contexts cleanly and is the recommended choice for core / PBR work:

OPENGLCONTEXT_PROFILE=core OPENGLCONTEXT_BACKEND=glfw python your_script.py

The dispatch lives in passes/renderpass.py. When the context definition reports profile == 'core', the renderer instantiates the core FlatPass (from passes/flatcore.py); otherwise it uses the compatibility FlatPass (from passes/flatcompat.py). The choice is made per context, not per frame.

Compatibility vs. Core, side by side

Both profiles run through a FlatPass (see Flat Rendering). The base class in passes/_flat.py carries both code paths; the single flag use_shaders selects between them. The core pass sets use_shaders = True; the compatibility pass leaves it False.

  Compatibility
(flatcompat.py)
Core
(flatcore.py)
Selected by default OPENGLCONTEXT_PROFILE=core
Lighting fixed-function glLight*, glEnable(GL_LIGHTING) VRML97 lighting model in GLSL; light properties uploaded as uniforms
Materials glMaterial*, glColorMaterial material uniforms set on the shader program
Matrices glMatrixMode / glLoadMatrixf / glPushMatrix client-side node-path matrices uploaded as mat4 uniforms
Geometry vertex pointers, display lists VAOs and VBOs at fixed attribute locations
Selection / picking glColor4ubv back-buffer colour codes object-id written to a second render target (MRT), or the unlit program

Watching the Scenegraph

The flat pass does not traverse the scenegraph every frame. It observes the graph and keeps a flat list of paths to every renderable node. The SGObserver base class (in passes/_flat.py) connects to the scenegraph's change signals -- child added, child removed, Switch changed -- and rebuilds a NodePath for each affected node. A NodePath caches the combined transform matrix for the route from the root to that node, so per-frame rendering is a small number of iterations over a prepared list rather than a recursive traversal. This is the same mechanism described in Flat Rendering; the core pass reuses it unchanged.

The Passes

On each visible frame the core FlatPass runs these steps in order:

The compatibility pass runs the analogous fixed-function sequence: legacy background, legacy lights, opaque, transparent.

sRGB Output

The shaders write colour that is already sRGB-encoded, so the framebuffer must not encode it a second time. On the first frame (with the context current) the pass disables GL_FRAMEBUFFER_SRGB once and leaves it off. It is a one-time call: nothing in OpenGLContext ever enables it, and some backends hand the application an sRGB-capable default framebuffer that would otherwise double-encode and wash the frame out.

Shader Programs

Core-profile rendering is managed by VRML97ShaderProgram (in passes/shaderpass.py), which compiles and owns a small family of programs, each for a different kind of geometry:

Background nodes carry their own small shader (vrml97_background.*). The PBR pass subclasses VRML97ShaderProgram and replaces only the main lit program with its Cook-Torrance shader, inheriting the rest.

Fixed Attribute Locations

Geometry binds vertex attributes to fixed locations so a cached VAO stays valid across programs. Two conventions are in use, by shader family:

Location Lit / unlit / vertex-colour / depth Point / line / background
0 aTexCoord aPosition
1 aNormal aColor
2 aPosition
3 aColor (vertex-colour program only)

The two conventions differ because the lit family carries a normal and texture coordinate while the point/line family does not. The shadow-depth vertex shader deliberately binds position at location 2 to match the lit shader, so a VAO built for lit drawing can be reused for the depth pass without rebinding.

How Shaders Are Assembled

The shaders are not monolithic files. Common code lives in include files (_common_inc.glsl, _brdf_inc.glsl, _lights_inc.glsl, _shadow_inc.glsl, _cubemap_inc.glsl) that are spliced in at compile time. The preprocessing in shaderpass.py does two things:

Assembling shaders this way keeps a single source of truth for the lighting math and lets the same shader compile correctly across drivers with very different texture-unit budgets. See Physically Based Rendering for the BRDF include and the shadow budget in detail.

Drawing Through the Core Pass (for geometry nodes)

A geometry node checks mode.shader_mode and, when true, draws with VAOs/VBOs instead of fixed-function calls:

def render(self, mode=None, **kwargs):
    if getattr(mode, 'shader_mode', False):
        return self._render_shader(mode)
    # ... legacy fixed-function path ...

def _render_shader(self, mode):
    program = mode.shader_program
    program.use(lit=True)                 # or use_point(), use(lit=False), ...
    program.set_matrices(mode.matrix, mode.projection)
    # bind a VAO with attributes at the fixed locations above, then:
    glDrawArrays(GL_TRIANGLES, 0, count)

The Shape node sets up material and texture before calling the geometry's render(), so the geometry only has to supply vertex data and issue the draw. See the structural overview for how Shape, Appearance and geometry interact.