Keel’s 3D renderer runs entirely inside the ECS. You register it withDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/VKSFY/keel/llms.txt
Use this file to discover all available pages before exploring further.
setup_renderer_3d(app), upload meshes and materials to the GPU through the registries it returns, then spawn entities with Transform3D and MeshRenderer components. Each frame the renderer builds a view-projection matrix, extracts frustum planes for cheap sphere-based culling, uploads per-frame light uniforms, and issues one draw call per visible mesh.
Setup
setup_renderer_3d(app) registers a system at Phase.RENDER and returns a Renderer3DSetup dataclass:
| Field | Type | Description |
|---|---|---|
mesh_registry | MeshRegistry | Upload meshes; get back integer mesh IDs. |
material_registry | MaterialRegistry | Register PBR-lite materials; get back integer material IDs. |
renderer3d | Renderer3D | The renderer instance (holds culling stats, draw-call counts). |
shader_cache | ShaderCache3D | The compiled PBR-lite shader. |
render_system | Callable | The registered render function (for reference). |
If both
setup_renderer_2d and setup_renderer_3d are active in the same app, the 3D renderer skips its own framebuffer clear and lets the 2D system’s clear serve as the shared clear. Depth testing is enabled for the 3D mesh pass and disabled on exit, so 2D sprites and text overlays that follow see no depth state.Meshes
Registering a Mesh
Upload aMesh to the GPU and receive an integer mesh ID:
MeshRegistry.add(mesh) uploads vertex and index buffers to a MeshBuffer (VBO + EBO) and binds it against the PBR shader. The returned integer is stable for the lifetime of the app.
Built-in Primitives
make_cube()
Unit cube centred at the origin. 24 vertices, 36 indices, per-face flat normals.
make_sphere(subdivisions=N)
UV sphere of radius 0.5. Higher
subdivisions values produce smoother geometry. subdivisions=1 is coarse; subdivisions=4 is smooth.make_plane(width, depth)
Y-up flat quad on the XZ plane. Normal points +Y.
width and depth default to 1.0.Loading OBJ Files
v, vn, vt, and f lines. N-gon faces are triangulated via fan decomposition. When normals are absent from the file, the loader generates per-triangle flat normals from the cross product of the face edges. Material (mtllib, usemtl), group (g, o), and smoothing (s) directives are intentionally ignored.
The CPU-side Mesh format is an (N, 8) float32 vertex array (position xyz, normal xyz, UV xy) plus a (M,) uint32 index array in triangle-list order.
Materials
Keel’s v1 material model is PBR-lite: scalar fields only, no texture maps.The Material Dataclass
Registering a Material
material_registry.add(material) returns a stable integer material ID. Material ID 0 is always a pre-allocated default mid-gray surface — MeshRenderer(material_id=0) is always safe to use without registering a material first.
The MeshRenderer Component
Transform3D to place a mesh in the world:
visible=False on a MeshRenderer skips the draw call for that entity without removing it from the world. Use world.set(entity_id, keel.MeshRenderer, visible=False) to toggle at runtime.
Lights
Directional Light
A sun-like light with a world-space direction and no falloff. The renderer reads the firstDirectionalLight in the world and ignores any extras.
DirectionalLight does not need a Transform3D — direction is stored directly on the component.
Point Lights
Point lights illuminate nearby geometry with a distance-based falloff controlled byradius. Their position is read from a co-located Transform3D on the same entity.
Ambient Light
Ambient light is a world resource, not a component. Insert it once to override the default(0.1, 0.1, 0.1) ambient term:
Frustum Culling
The renderer uses Gribb/Hartmann plane extraction from the view-projection matrix to build six frustum planes each frame. Before issuing a draw call for an entity, it tests a bounding sphere centred on the entity’sTransform3D position against all six planes. Entities whose sphere is fully outside any plane are culled — their draw call is skipped entirely.
The bounding sphere radius defaults to 2.0 world units. For very large or very small meshes this may produce conservative culling results (false negatives — meshes culled when they should be visible). A per-mesh bounding radius API is planned for v0.2.
Transform3D Hierarchy
Transform3D supports parent-child chains. Set a parent entity ID on a child transform to make it inherit its parent’s world matrix:
Full Example
The example below iscube_demo.py. A red-orange cube spins on two axes; an emissive lamp sphere orbits it and casts coloured point-light illumination.
Call setup_renderer_3d(app)
This wires the PBR-lite shader, creates mesh and material registries, and registers the Phase.RENDER system.
Upload meshes and materials
Use
mesh_registry.add(make_cube()) (or OBJLoader.load(path)) and material_registry.add(Material(...)) to get stable integer IDs.Spawn Transform3D + MeshRenderer entities
Pair the two components so the renderer can locate each entity in space and look up its geometry and shading parameters.
Add lights
Spawn at least one
DirectionalLight entity. Add PointLight entities (with a co-located Transform3D) for local illumination — up to 8 are active per frame.