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.
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.
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 |
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.
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.
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.
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:
program -- the main lit shader
(vrml97_lighting.vert/frag), implementing the VRML97 Phong
lighting model.unlit_program -- unlit drawing, selection and text
(vrml97_unlit.*).vertex_color_program -- per-vertex coloured geometry such as
NURBS output (vrml97_vertex_color.*).point_program -- PointSet / particles with
per-vertex colour (vrml97_point.*).line_program -- IndexedLineSet
(vrml97_line.*).depth_program -- position-only, used for the shadow depth
passes (shadow_depth.*).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.
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.
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:
#include "file.glsl" directives are resolved recursively from
the shader directory, each file spliced at most once per program (an
include guard). Included files carry no #version of their own;
the top-level shader owns it.#defines are injected immediately after the
#version line. This is how per-driver limits are baked in --
for example the number of shadow-casting lights the driver can support
(MAX_SHADOW_LIGHTS) and whether cube-map arrays are available.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.
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.