Repository navigation
Renderer
renderers/renderer.mojo. The Renderer turns a scene, its assets and a camera into an image. prepare makes screen-space triangles, prepare_lines makes screen-space segments and prepare_points makes screen-space points. render also fills them on the CPU.

three.js: WebGLRenderer.render(scene, camera), plus the assets argument that Mojo needs.
var renderer = Renderer(320, 240)
var fast = Renderer(1280, 720, workers=available_workers())| Member | Meaning |
|---|---|
Renderer(width, height, workers=1) |
An image size and a thread count. |
set_workers(workers) |
Change the thread count. At least one. |
set_antialias(enabled) |
Supersample render, render_array and render_cube. See below. |
supersampled() -> Renderer |
A renderer SUPERSAMPLE times this one's size each way, viewport, scissor and render_scale scaled. |
scaled(factor) -> Renderer |
The same, factor times the size. |
multisampled(samples) -> Renderer |
scaled(sample_grid(samples)): what a draw into a multisampled target takes its samples with. |
set_background(color) |
The clear color. |
set_shading(mode) |
What a fragment's color comes from. See below. |
set_tone_mapping(mode, exposure=1.0) |
The curve that compresses the light for a display. See below. |
set_depth_mode(mode) |
How the depth is stored: STANDARD_DEPTH, LOGARITHMIC_DEPTH or REVERSED_DEPTH. See Rasterization. |
depth_mode_for(camera) -> DepthMode |
The mode a frame through camera is drawn in. A logarithmic depth is standard under an orthographic camera. |
prepare(scene, assets, camera) -> List[RasterVertex] |
Transform, clip and project every mesh. |
prepare_lines(scene, assets, camera) -> List[RasterVertex] |
The same for the scene's lines and wireframes. |
prepare_points(scene, assets, camera) -> List[RasterVertex] |
The same for the scene's points. A sprite is two triangles, and prepare makes them. |
prepare_frame(scene, assets, camera) -> Frame |
All three lists, and the one order both rasterizers draw them in. See Lines. The triangles are slim corners and their surfaces; whole_corners() gives them whole. See Slim corners. |
render(scene, assets, camera) -> Framebuffer |
Every pass, then rasterize and resolve. |
render_into(target, scene, assets, camera) |
The same into a target of the renderer's size, cleared first, resolved by the caller. A target with samples is drawn at its sample grid and resolved into its pixels. See below. |
render_with(hooks, scene, assets, camera), render_into_with(hooks, target, scene, assets, camera)
|
The same, with render hooks. See Renderer hooks and material flags. |
set_opaque_sort(method), set_transparent_sort(method)
|
Your order for the opaque and the translucent runs. See Renderer hooks and material flags. |
auto_clear, auto_clear_color, auto_clear_depth, auto_clear_stencil, clear(target)
|
What render_into clears. See Renderer hooks and material flags. |
info() -> RenderInfo, reset_info(), info_auto_reset
|
What the renderer drew. See Renderer hooks and material flags. |
custom_tone_mapping, curve_program(assets)
|
The node program of CUSTOM_TONE_MAPPING. See Renderer hooks and material flags. |
render_array(scene, assets, array) -> Framebuffer |
Once per camera of an ArrayCamera, each into its own rectangle. |
render_array_into(target, scene, assets, array) |
The same into a target you hold. |
render_cube(scene, assets, camera) -> CubeTexture |
Six faces through a CubeCamera, as a cube texture. |
render_into_layer(target, layer, level, scene, assets, camera) |
render_into one layer and one level of a layered target. three.js's setRenderTarget(target, activeCubeFace, activeMipmapLevel). |
render_cube_into(target, scene, assets, camera) |
Six faces through a CubeCamera, into level zero of a cube target. |
clear_color(scene) -> Color |
What a frame is cleared to: the scene's color background, or background. |
backdrop(scene, assets, camera) -> Optional[Framebuffer] |
The scene's image background as the camera sees it, or none. See Scene graph. |
tone_curve() -> ToneMapping |
The curve render resolves through: the one set, or none in the uv view. |
set_viewport(rect) |
Where the image lands on the target. See below. |
set_scissor(rect), set_scissor_test(enabled)
|
Which pixels a draw may touch, and whether that is enforced. See below. |
available_workers() -> Int |
One per logical core. |
camera_position(scene, camera) -> Vector3 |
Where the camera stands, in world space. |
toward_camera(scene, camera) -> Vector3 |
The one direction toward it, or PERSPECTIVE_VIEW. |
camera_up(scene, camera) -> Vector3 |
Which way is up for it, in world space. |
The two passes are separate because almost nothing a triangle carries applies to a line. render fills the triangles and then draws the segments over them, depth tested against them. See Lines.
scene.update() must run before prepare or render. The renderer reads world matrices and does not recompute them.
One worker emits each draw before it prepares the next draw. Several workers hold at most 65,536 prepared vertices across pending draws. A larger draw runs on its own. Triangle pieces stay in draw order when the batch is flushed. The limit bounds temporary vertex arrays, not the final frame or its assets.
render_into_keeping_shadows returns the maps drawn by a frame. To reuse them,
pass the maps to render_into_reusing_shadows. Transfer the list with ^ and
keep the returned list to avoid copying its texels between views:
maps = renderer.render_into_reusing_shadows(target, scene, assets, camera, maps^)Views can share maps when the lights, casters and visible layers are the same.
The view's layers filter both lights and casters, including point-light faces.
Scene cube captures use the same layer mask for their maps and all six faces.
render_into_with_shadows copies a borrowed list so the caller keeps its independent maps.
| Mode | A fragment's color is |
|---|---|
SHADE_TEXTURE |
The material color, times its texture, times the light, plus the highlight and the emissive times its map. The default. |
SHADE_LIT |
The material color times the light, plus the highlight and the emissive. Textures are ignored. |
SHADE_UV |
The texture coordinates, as red and green. A debug view. |
A NORMALS or DEPTH material writes data under either lit mode. See Materials.
set_shading refuses a mode that is none of the three.
Cube captures keep the renderer's clipping planes, light probe grid, LTC tables, shadow settings, material override and node time. Each face uses its own full viewport and no tone mapping. A probe bake uses only the grid supplied to scene_cube, so it does not bake the renderer's grid into itself.
set_antialias(True) is three.js's antialias. render, render_array and render_cube then draw the frame at SUPERSAMPLE times the size each way, two, and average every block of four pixels into one.
The large frame is drawn into a RenderTarget, which holds premultiplied linear light. RenderTarget.downsampled(factor) averages the blocks there, and resolve converts the small target once. Tone mapping and the sRGB encode come after the average.
The order is not a detail. Encoding first clamps each sample and bends it through the tone curve, and the mean of a curve is not the curve of a mean.
Four subsamples holding linear 4, 0, 0, 0 average to a radiance of 1. Encode them first and the bright one saturates to byte 255, which decodes to 1. The average is then a quarter of the light. It comes back as byte 137 with no curve, where 255 is correct. Under Reinhard it is 123 against 188.
The depth of an output pixel is the nearest of its four.
scaled and resized keep the draw filter, selected OIT draw, vertex snapping and UV-space mesh selection. They also keep the light probe grid. resized draws across its new target, with a full viewport and no scissor test. It keeps the clear flags and the anti-aliasing setting.
A point's PointsMaterial size and a line's one-pixel thickness are measured in the pixels of the finished image. The renderer carries a render_scale: one usually, and SUPERSAMPLE in the renderer supersampled() returns. attenuated_size converts a point's size with it, and rasterize_frame draws each line that many raster pixels wide.
Without it, turning anti-aliasing on shrank both. An eight-pixel point covered 64 pixels with the setting off and 16 with it on. A one-pixel line resolved to half coverage, which is a gray line where a white one was asked for. Anti-aliasing must change how cleanly an edge is drawn, not how large an object is.
A world-space length is not scaled: a triangle is projected onto whatever grid it is drawn on and comes out the right size either way.
Supersampling rather than a multisampled fill rule keeps both backends on one coverage rule. render/antialias.mojo holds downsample(Framebuffer, factor), which resizes a finished picture: it decodes bytes, averages premultiplied and encodes once. That is what a caller driving GpuRenderer must use, because the GPU hands back a resolved image rather than the linear target behind it.
That path loses the range described above. It is the best that can be done with bytes, and it is named here rather than left to be found. Prepare with supersampled(), pass that renderer's render_scale as the draw's line_width, draw at its size, and downsample. Giving the GPU the same linear resolve means keeping its target long enough to average it. A multisampled target does that: GpuRenderer.read_back_target resolves its samples in linear light, on the device. See GPU backend.
render_cube averages each face in linear light before it makes the sRGB byte image. The finished cube still clamps HDR light. render_cube_into with a FLOAT_TARGET keeps that range.
The viewport and the scissor are given in output pixels and scaled with the frame. render_into, render_array_into, render_into_layer and render_cube_into use the target's samples. The renderer's antialias does not add samples to these draws.
The two cube capture APIs have different sampling owners. This is intentional.
-
render_cubeusescamera.sizeand the renderer'santialias. With the setting off, it takes one sample per pixel. With the setting on, it takes four. It averages linear light before it creates each sRGB byte face. -
render_cube_intouses each target face's size andsamples. Zero or one takes one sample. Use 4, 9 or 16 for a regular sample grid. The camera's size and the renderer'santialiasdo not change that grid. The samples are averaged once into the target, with no tone mapping or sRGB encode.
For a four-sample HDR capture, pass cube_render_target(size, Color(0, 0, 0), FLOAT_TARGET, samples=4) to render_cube_into. Changing the renderer's anti-aliasing setting does not change that capture or sample it twice. See Layered render targets.
This ownership follows three.js r186: WebGLRenderer supplies antialias to the context, and RenderTarget owns a separate samples count. CubeCamera.update selects the target's faces. This port uses regular sample grids instead of hardware MSAA. The byte-returning render_cube convenience API also honors the renderer setting because it has no caller-owned target.
Both capture APIs keep full face viewports and separate capture history. They do not change the main renderer's viewport, scissor or history. The scene settings described above still apply.
set_tone_mapping picks one of the seven curves in render/tonemap.mojo and an exposure. NO_TONE_MAPPING and an exposure of one are the defaults, as in three.js. render applies the curve in RenderTarget.resolve, once per pixel, after every fragment is composited. The SHADE_UV view is never tone mapped, and nor is a pixel a data material wrote. See Render target.
set_tone_mapping refuses a curve that is none of the seven, and an exposure that is negative or not finite.
renderer.set_output_color_space(LINEAR_SRGB_COLOR_SPACE)
renderer.set_output_color_space(DISPLAY_P3_COLOR_SPACE)set_output_color_space is three.js's outputColorSpace. sRGB is the default, as in three.js. resolve writes each pixel's light in that space, after the tone mapping curve. Both backends use the same OutputEncoding, from render/color_spaces.mojo.
The light is multiplied by three.js's matrix, then goes through the space's transfer function. A linear space has no transfer function, so its bytes are the light itself. The matrix is ColorManagement._getMatrix, rounded to four places, as WebGLProgram writes it into the shader. The shader multiplies the color by the matrix from the left, value.rgb * mat3(...), and this does the same.
A space with sRGB's primaries has the identity matrix, and the matrix is skipped. A pixel that a normal or depth material wrote is data. three.js writes it without linearToOutputTexel, and so does this, in every space.
set_output_color_space refuses NO_COLOR_SPACE, which three.js cannot write out, and a value that names no space. A caller that resolves its own target, such as a stereo effect or a composer, passes renderer.output_encoding() to resolve.
First, read the scene as draws. A mesh is one draw. An instanced or batched mesh is one draw per instance, kept together as one group. An LOD is one draw, the level its distance from the camera picks. See Meshes and assets.
Leave out every group whose node shares no layer with the camera. Then leave out every draw whose bounding sphere lies wholly outside the camera's frustum. Then sort the groups into draw order. Then, for each draw in that order:
- Transform the positions to world space and camera space.
- Carry the raw texture coordinates,
uvanduv1. Each map moves them at the fragment by its own transform. See Textures. - Transform the normals with the normal matrix, or compute a face normal. Carry them into view space for a
NORMALSmaterial. - Clip each triangle against the near and far planes and the four sides of the view. See Rasterization.
- Project each corner to pixels and keep
1 / w. - Cull faces that the material's
sidedoes not draw. - Flip the normal of a
BACK_SIDEface, and of aDOUBLE_SIDEface seen from behind, as three.js'sFLIP_SIDEDandfaceDirectiondo.
The output is one flat list, three RasterVertex per triangle. Both rasterizers consume it. See Rasterization.
Opaque draws come first, nearest first. Translucent draws follow, furthest first. The order is per draw, by the depth of its own placed origin. An instance sorts where it is, not where its node is, so a translucent mesh between two instances of a group falls between them. Only the draws the camera makes are sorted. See Why transparency is sorted.
Equal sort keys keep their input order. Lists of up to 32 draws use insertion sort without extra storage. Larger lists use a stable merge sort with O(n log n) comparisons. An already ordered list needs one linear scan and no extra storage. Custom sorts use the same stable sorter.
A custom comparator must define a consistent strict ordering. For a tie, it must return False in both directions.
bench/renderer_sort_bench.mojo compares the draw sort with its previous implementation and checks that the results match.
prepare_frame sorts lines and wireframes into the same order. A translucent line falls between the translucent surfaces on either side of it. See Lines.
prepare skips a mesh whose bounding sphere lies wholly outside the view. The frustum's four sides come from the camera's projection matrix times its view matrix, read as planes in world space. Its near and far planes come from the camera's distances and the view matrix, the same numbers the clipper uses. See Math.
The sphere is the geometry's, computed once per geometry per frame and reused by every mesh that draws it. It is carried through each node's world matrix. A sphere within a millionth of the far distance of a plane is kept. The clipper decides such ties.
The image does not change. Every triangle of such a mesh is clipped away or lands off the image. What the test saves is the transform, the clip and the projection of those triangles. The bound itself reads the positions, twice.
Mesh(geometry, material, node, frustum_culled=False) opts a mesh out. three.js: Object3D.frustumCulled. A mesh that is left out is not read. Its material and its index buffer are checked when it is drawn.
Each instance of an instanced or batched mesh is tested on its own, with the instance's matrix folded into the node's. three.js tests the whole group by one bound. An LOD's shown level is tested as a mesh is.
render calls prepare_frame, then resolves the lights once for the frame with Lighting(scene, camera.visible_layers(), camera_position(scene, camera), toward_camera(scene, camera)). A light on a layer the camera does not watch lights nothing. The camera's position goes with the lights because a PHONG material measures its highlight from there. See Lights.
toward_camera goes with it because a parallel projection has one direction toward the camera for every surface. It asks Matrix4.is_affine() of the projection matrix. A converging projection has a bottom row that is not (0, 0, 0, 1), and toward_camera returns PERSPECTIVE_VIEW for it. A parallel one returns the camera's own world +z axis, made unit length. See Lights.
camera_up goes with them for a MATCAP surface, which is looked up in the camera's own frame. It is the view space +y axis carried back into the world. Every projection answers the same way. See Materials.
It resolves the scene's fog for the camera with FogView(scene.fog, view). See Fog. It clears the target to clear_color(scene) and paints backdrop(scene, assets, camera) under the scene, where the scene has an image background. Then it rasterizes the frame with rasterize_frame, in the frame's order, with the assets' cube textures for the surfaces that reflect one. Last, it resolves the image through the tone mapping curve.
A clipping plane cuts away what lies behind it. three.js: WebGLRenderer.clippingPlanes, localClippingEnabled, and Material.clippingPlanes, clipIntersection and clipShadows.
renderer.clipping_planes = [Plane(Vector3(1, 0, 0), 0)] # keep x > 0
renderer.local_clipping_enabled = True
material.set_clipping_planes(planes, intersection=True, shadows=True)| Member | Meaning |
|---|---|
Renderer.clipping_planes |
World-space planes that cut every mesh, line, point set and sprite. None by default. |
Renderer.local_clipping_enabled |
True to let each material's own planes cut it. False by default, as in three.js. |
Material.set_clipping_planes(planes, intersection, shadows) |
The material's own planes, at most MAX_CLIPPING_PLANES, which is eight. |
Material.clip_intersection |
False cuts what is behind any plane. True cuts only what is behind every plane. |
Material.clip_shadows |
True to cut the material's shadow with its planes too. |
A point is kept when its signed distance to each plane is zero or more. The renderer's planes always use the first rule. They never cut a shadow, as three.js's do not.
The planes cut triangles before they are projected, with the same clipper that cuts at the near, far and side planes. See Rasterization. A cut triangle is the same surface a per-fragment test keeps, so both rasterizers draw it without knowing about the planes.
What survives clip_intersection is not convex, so it is cut into convex pieces that do not overlap. Piece i is in front of plane i and behind every plane before it. A line is cut the same way. A point is kept or dropped whole.
A material holds its planes inline, so that it stays a plain value. three.js has no limit on the count.
set_viewport(rect) puts the camera's image in a rectangle of the target, three.js's setViewport. set_scissor(rect) and set_scissor_test(True) keep every draw inside a rectangle, three.js's setScissor and setScissorTest. A Rect is a corner and a size, and the corner counts up from the bottom left, as three.js's does.
from render.rect import Rect
renderer.set_viewport(Rect(0, 0, 160, 240))
renderer.set_scissor(Rect(0, 0, 160, 240))
renderer.set_scissor_test(True)The viewport is folded into the screen matrix by prepare, prepare_lines and prepare_points. The projection is mapped onto the rectangle rather than onto the whole target. The image is squeezed or stretched to the rectangle's size, as three.js's is. A viewport hanging off the target is allowed: the pixels it puts outside are not drawn. A viewport must hold at least one pixel.
A viewport is a mapping, not a scissor. Every triangle and segment is clipped against the camera's four side planes first, so nothing lands outside the camera's image. With the scissor test off, the whole target is cleared and the geometry outside the view produces no pixel.
The scissor is enforced by the render target on the CPU and by the kernel on the GPU. Both ask Rect.contains_pixel, so they agree about every edge. A pixel outside is neither cleared nor drawn. With the test off, the default, the scissor is kept and ignored. A scissor must lie wholly inside the target.
A split screen is two cameras, two viewports and two scissors drawing into one target. render_into draws into a target you hold, clearing the scissor alone to the background, so a second draw leaves the first's pixels. Resolve the target once, through tone_curve:
var target = RenderTarget(WIDTH, HEIGHT, Color(0, 0, 0))
renderer.set_scissor_test(True)
renderer.set_viewport(left)
renderer.set_scissor(left)
renderer.render_into(target, scene, assets, perspective)
renderer.set_viewport(right)
renderer.set_scissor(right)
renderer.render_into(target, scene, assets, top_view)
var image = target.resolve(renderer.workers, renderer.tone_curve(), renderer.tone_mapping_exposure)GpuRenderer.draw takes the same scissor, and its device target keeps its pixels between draws the same way. See GPU backend. examples/split.mojo draws a split screen.

With more than one worker, the image is cut into horizontal bands. Each band is drawn on its own thread. The result is byte for byte the same as one thread. See Why the CPU renderer uses bands.
prepare also uses the workers. Each draw's vertices are transformed on one thread. The draw's triangles are then cut into pieces of at most 2,048. The workers emit the pieces in parallel, each into a list of its own. The lists are copied into the frame in draw order, so the corners are the same as on one thread.
The default is one worker. The coverage tool needs probe records in order.
prepare raises in five cases:
- A mesh names a node, geometry or material that does not exist.
- A material names a texture, an emissive map or an alpha map that does not exist.
- An emissive map reads its alpha as coverage.
- An alpha map is not stored as data, which means
LINEARandIGNORED. - A material names a map whose channel is neither
UV_CHANNEL_0norUV_CHANNEL_1. - A geometry has no positions.
A mesh the camera's layers or frustum leave out is not checked.
render also raises when scene.fog holds an unknown kind or an inside-out range.
set_viewport raises for a rectangle with no pixel. set_scissor raises for one that reaches outside the target. render_into raises for a target that is not the renderer's size.
make bench-scene times each stage on its own on a sphere of twelve thousand triangles. At 1280 by 720 with 16 workers a frame takes about 6 milliseconds on an Apple M4 Max. The rasterizer is the largest stage. prepare takes about half a millisecond. A triangle wholly inside the depth range skips the clipper, and the corner list is sized once per draw.
The resolve encodes the clear color once and copies it to every pixel that still holds it. A frame that is mostly background pays for the pixels that are not.
Scene.add_gaussian_splat adds an owned splat object to the transparent list.
Its RenderItem.kind is DRAW_SPLATS. Its geometry and material IDs are -1,
because splat data belongs to the object. Hooks must check the kind before
looking up those IDs in asset stores. See Gaussian splats.
Tutorials
How-to
Reference
- Scene graph
- Rotations
- Cameras
- Geometry
- Geometry addons
- Lofts and clipping groups
- Sculptor
- Procedural generators
- Meshes and assets
- Lines
- Points and sprites
- Helpers
- Scene objects
- Animated objects
- Raycasting
- Curves and paths
- Animation
- Skinning
- Materials
- Node materials
- TSL functions
- Lights
- Lighting addons
- Fog
- Textures
- Renderer
- Renderer hooks and material flags
- Rasterization
- GPU backend
- Render target
- Post-processing
- GPU computation
- Compute nodes
- Progressive light map
- Gaussian splats
- Voxel global illumination
- SVG renderer
- Image files
- Image decode queue
- Model files
- More model files
- Exporters
- Scene JSON
- Windowing and controls
- Math
- Norm consumers
- Math addons
- Units
- Coverage tool
- Commands
- Examples
- Benchmarks
- Extensions
- Shared physics
- Physics query snapshots
- Continuous collision
- Tick force restoration
- Water
- Animals
- Buildings
- CARLA
- Femur
- Tibia
- Fibula
- Patella
- Knee
- Muscles
- Vessels
- Lymph
- Nerves
- Integument
- Leg
- Foot
- Pelvis
- Torso
- Arm
- Hand
- Head
- Genome
- Game humanoid
- Humanoid fidelity
- Audio-aligned game faces
- Converted assets
- Mesh quality
- Segment inertia
Explanation
- Software rasterizer
- Scene graph as an array
- Linear color
- Normals are not colors
- Perspective correction
- Fixed-point coverage
- Per-fragment shading
- Mipmaps
- Sorted transparency
- Types and checks
- Render hooks as a trait
- Shared CPU and GPU code
- Bands and threads
- PNG structure
- Extensions beside the port
- CUDA teardown hang
- Mojo compiler hang