Your first rendered scene: Cornell box¶
Learn the everyday Ludus workflow by opening a working sample, running it, changing a material, moving an object and adjusting its lighting. Each lesson makes one small edit and tells you what to look for before continuing. You only edit the sample; the engine stays unchanged.

The starting scene is an open room with a red left wall, green right wall, two white blocks and a ceiling light. Geometry and direct lighting live in a Slang shader drawn through Ludus's public fullscreen API. Mesh loading, general mesh rendering and path tracing are future work. There is no indirect illumination, color bleeding or progressive accumulation; a small constant ambient term keeps unlit faces visible. Sixteen fixed light samples give stable but stepped shadows.
Before you start¶
The desktop editor path below uses Ubuntu 24.04, Linux x64, with a Wayland session and a Vulkan-capable driver. Follow Install and launch once to prepare and open the editor. Use Building Ludus for system prerequisites. Keep a code editor available for C++ and Slang files; Ludus's current desktop editor manages projects and builds rather than editing those source documents.
On macOS, use the manual setup below, then follow the same numbered lessons using its build/run commands. The native editor is currently Linux-only. The screenshots here are actual macOS Metal runtime captures, not screenshots of the Linux editor. Window decoration and small GPU differences can vary.
Use a Ludus checkout that contains examples/cornell-box. This sample is not yet
an option in New Project; that dialog creates the minimal native template.
For this lesson, open the included sample. It consumes an installed SDK and does
not compile engine sources into the application.
1. Open and prepare the sample¶
- On Welcome, select Open Project and choose
examples/cornell-box/ludus.project.jsoninside your Ludus checkout. - Review Project settings. They should contain these values:
| Setting | Value |
|---|---|
| Name | Cornell Box |
| Provider | cmake |
| Source directory | . |
| Preset | linux-clang-development |
| Executable target | cornell_box |
| Run working directory | . |
| Run arguments | Empty |
- Select Project → Repair Project Setup. For this repository sample's first setup, choose Build and install the engine used by this editor under Engine for desktop builds. Leave Also set up browser builds unchecked.
- Choose Repair Project and follow Output. This explicit action prepares the engine and shader tools, installs the SDK, writes this machine's local project settings, then configures, builds and tests the sample. First-time preparation may download dependencies and take several minutes.
Checkpoint: setup finishes successfully and all five sample tests pass. Opening a project alone does not perform setup. If it reports missing setup, continue with Repair; if Repair fails, use the first error in Output and the recovery notes rather than treating the project as ready.
The sample's release lock is unresolved because it is part of this source checkout, not a published engine package. Repair records your local SDK override and tool paths in ignored files. You do not need to export SDK paths, choose a CMake executable or bootstrap shader tools manually for this editor route. See project setup for those contracts.
2. Run your first frame¶
- Choose Build → Build and Run, or Build and Run on the toolbar.
- Wait for the application window. Compare it with the opening screenshot: red left wall, green right wall, two white blocks and shadows on the floor.
- Resize the window. The scene keeps its proportions as the framebuffer aspect ratio changes; the camera stays fixed.
- Close the application window before the next lesson. You can also use Build → Stop to stop work owned by the editor.
Checkpoint: you see the rendered room and Output includes
Cornell box rendered. Close the window to exit. A process starting is not proof
that a frame appeared. This sample runs in its own native window. Use Build and
Run throughout this tutorial; its fullscreen application does not implement
GameHost's Build and Play or live gameplay editing workflow.
3. Separate materials from lighting¶
- In Project settings → Run arguments, select Add argument, enter
--flatas a single item, and press Enter. Do not add quotes. - Use File → Save Project Settings (Ctrl+S), then Build and Run.
- Compare the result below with the lit scene.

Checkpoint: the wall colors remain, while shading and cast shadows disappear. The blocks and room share the same white material, so their interiors blend in this mode; silhouettes against the colored walls still distinguish them. The ceiling patch remains emissive.
Close the window. Select the --flat item, choose Remove argument, save the
settings and run again. Checkpoint: lighting and shadows return. Leave Run
arguments empty for the following lessons. Run arguments are saved project
metadata; remove experimental arguments before sharing your changes.
4. Find the files you will edit¶
Open the sample folder in your code editor. Start with these files:
| File | What you use it for |
|---|---|
ludus.project.json |
The editor's build target, preset and run arguments |
CMakeLists.txt |
SDK components, executable and shader compilation |
src/main.cpp |
Logging, window creation, camera publication, RHI startup, frames and shutdown |
src/camera.cpp |
Authored C0 eye/lens and checked framebuffer ray extraction |
shaders/cornell.slang |
Room, blocks, supplied camera rays, materials and lighting |
CMakePresets.json |
Shared Debug, Development and Release profiles |
CMakeUserPresets.json |
Ignored settings generated for this machine |
For each exercise: close the running sample → edit source → save → Build and
Run → inspect the result. CMake recompiles a changed shader and regenerates its
application-owned header. The running application does not reload shader edits
automatically. Do not edit generated cornell.h or files under out/.
5. Change a wall material¶
In shaders/cornell.slang, find Hit scene(...). The first rectangle call
creates the left wall; its last float3 before hit is the wall's RGB material.
Change just that value:
// Before:
float3(1.0, 0.0, 0.0), float3(0.63, 0.065, 0.05), hit);
// After:
float3(1.0, 0.0, 0.0), float3(0.05, 0.12, 0.63), hit);
Save, then Build and Run.

Checkpoint: only the left wall's material changes from red to blue; the right wall stays green and the geometry stays in place. These are linear RGB reflectance values, not display hex colors. White surfaces do not become blue: this renderer does not simulate light bouncing from the wall.
Restore float3(0.63, 0.065, 0.05) before the next exercise.
6. Move a block and its shadow¶
Find void blocks(...). In its first block call, change only the center:
// Before:
float3(-0.40, 0.36, 0.35)
// After:
float3(-0.15, 0.36, 0.35)
The center is (x, y, z) in this sample's normalized world units. Positive x
moves the short block toward the right. The next float3(0.30, 0.36, 0.30) gives
its half extents, not its full dimensions. Its y center equals its y half extent,
so its bottom still rests on the floor at y = 0. Save, then Build and Run.

Checkpoint: the short block moves right and its floor shadow moves with it;
the tall block stays in place. Both visible geometry and shadow rays use
blocks, so one edit updates both. This is procedural shader geometry rather
than an editor transform or a mesh entity.
Restore the original center before continuing.
7. Adjust light strength¶
At the end of shade(...), change the direct-light multiplier from 12.0 to
6.0, leaving the other values alone:
// Before:
return hit.albedo * (0.035 + illumination * (0.64 * 0.56 * 12.0 / (16.0 * 3.141593)));
// After:
return hit.albedo * (0.035 + illumination * (0.64 * 0.56 * 6.0 / (16.0 * 3.141593)));
Save, then Build and Run. Checkpoint: lit surfaces become dimmer, while the light's shape, block positions and shadow locations stay the same. The image will not become exactly half as bright because the shader also adds ambient light and applies tone mapping and display encoding. The visible ceiling patch uses a separate emissive value and keeps its brightness in this exercise.

Restore 12.0.
8. Understand what creates shadows¶
Inside the light-sampling loop in shade, temporarily comment out just this
call:
// blocks(point, toLight, shadow);
Save, then Build and Run. Checkpoint: the blocks still render, but their cast shadows disappear. Surface-facing light still shades their faces. You have removed the visibility test between each surface and each ceiling-light sample; you have not removed the blocks from the camera's scene query.

Restore the call, save and run once more. Checkpoint: your result matches the starting scene. Restore all exercise edits before using the baseline as a reference or submitting unrelated project changes.
9. Follow the engine frame lifecycle¶
Open src/main.cpp and follow these functions in order:
Runcreates the Platform window and owns the event/frame loop.Renderer::Startstarts RHI and creates the shaders, uniform and pipeline.Renderer::Drawbegins a frame, evaluates the fixed C0 camera, extracts rays for the acquired physical framebuffer, uploads its dimensions and ray basis, draws a fullscreen triangle and ends the frame.- Renderer shutdown releases GPU resources before the window is destroyed.
The host uploads five aligned float32[4] rows: framebuffer dimensions and
display flags, then eye, right, up and forward ray vectors. Slang receives the
same 80-byte layout at binding 0; generated reflection and C++ assertions check
its size and offsets. If the attachment encodes
sRGB, the shader leaves its output linear. Otherwise the shader encodes it after
tone mapping. Errors return explicit statuses and use Ludus logging.
src/camera.cpp owns the authored fixed shot: eye (0, 1, 3.6), identity
orientation and vertical FOV 0.76101275 radians (about 43.6 degrees). It uses
GameplayCamera C0 to
publish an owning pose/lens sample and copied trace. The private adapter derives
ray scales from FoundationMath's canonical perspective projection using the
actual framebuffer aspect. Camera or extraction failures stop the frame before
uploading an invalid basis. The ray renderer owns intersection distances; this
integration adds no depth attachment or general mesh pipeline.
To change the view, edit DefaultCamera() in src/camera.cpp, then build and run.
Increasing eye Z from 3.6 to 4.2 moves back; increasing vertical FOV from
0.76101275F to 0.9F widens the view. Resize again to check the composition.
Restore the defaults to reproduce the opening screenshot. Camera values now
live in C++ rather than as separate constants in the shader.
The fragment shader constructs a camera ray for each pixel, finds the closest room plane or rotated box, then samples sixteen points on the ceiling light. The host provides the window and GPU lifecycle; this sample owns the scene and lighting algorithm. Read Rendering and shaders when you are ready to create your own shader/uniform layout.
You can now repeat the same edit–build–run loop in your own application. Use Create your first project for a new minimal project, and Build and debug for the normal workspace workflow. This lesson does not provide a general scene editor, mesh pipeline or path tracer.
Manual setup and command-line workflow¶
This route is for macOS, terminal users and explicit SDK/tool selection. Finish it before lesson 2, then use the commands here wherever a lesson says Build and Run. The editor route above performs its project preparation through Repair; it does not require these shell steps too.
Prepare the engine once¶
Start with a Ludus checkout containing examples/cornell-box. Follow
Build and initialize for platform prerequisites.
The tutorial uses C++23, upstream Clang 23, CMake 3.29.6, Ninja 1.11.1.3,
Python 3.10+ and Slang 2026.1.2. Linux also uses the pinned SPIRV-Tools validator.
The pins live in config/tool_versions.json and config/shader_toolchain.json.
Run these commands from the engine checkout root. Choose the preset for your host; these commands explicitly prepare and build the engine once as a prerequisite. The sample itself subsequently links the installed libraries.
# Ubuntu 24.04, Vulkan:
engine_preset=linux-clang-development
# On macOS instead, use:
# engine_preset=macos-clang-development
./init.sh --cli "$engine_preset" --preset-only --locked --with-tests
./scripts/shader-probe bootstrap
./scripts/build "$engine_preset"
out/host-tools/venv/bin/cmake --install "out/build/$engine_preset" \
--prefix "$PWD/out/install/$engine_preset"
init and shader bootstrap may download pinned tools/dependencies. Sample
configure, build and test do not download anything. Keep preparation separate
from normal project builds. A previous SDK that predates Metal/fullscreen
rendering is insufficient even if it has the same 0.1.0 version string.
Select your tools and SDK¶
Still at the engine checkout root, set up this shell:
export PATH="$PWD/out/host-tools/bin:$PWD/out/host-tools/venv/bin:$PATH"
export LUDUS_SDK_PREFIX="$PWD/out/install/$engine_preset;$PWD/out/conan/$engine_preset"
export LUDUS_SLANG_COMPILER="$PWD/out/shader-tools/slang/bin/slangc"
# Linux only:
export LUDUS_SPIRV_VALIDATOR="$PWD/out/shader-tools/spirv-tools/usr/bin/spirv-val"
# macOS only:
export LUDUS_OSX_SYSROOT="$PWD/out/host-tools/macos-sdk"
export LUDUS_LIBCXX_INCLUDE="$PWD/out/host-tools/libcxx-include"
The dependency-generator prefix accompanies this local cmake --install SDK.
For a complete bundled SDK, LUDUS_SDK_PREFIX can instead contain only its
absolute install prefix. See SDK installation and project setup
for the shared store and supported release workflow.
All paths above are local environment settings. Do not commit machine paths;
use ignored CMakeUserPresets.json for persistent overrides. If using VS Code
CMake Tools, select this prepared CMake executable and enable preset mode, then
select the sample's host preset. Opening/checking a project should not run the
preparation commands automatically.
Configure, build and test¶
cd examples/cornell-box
cmake --version
clang++ --version
ninja --version
cmake --list-presets=configure
cmake --list-presets=build
cmake --list-presets=test
cmake --preset "$engine_preset"
cmake --build --preset "$engine_preset"
ctest --preset "$engine_preset"
The configure listing must show three selectable host presets: Debug, Development and Release. Build/test listings may also show the other host's names; configure conditions prevent selecting those on this machine. Use an SDK with the same build flavor as the sample. To select Debug or Release, prepare that engine preset first and update the environment; changing only the sample preset does not change the selected SDK.
The build compiles src/main.cpp and src/camera.cpp and generates the
application-owned cornell.h from shaders/cornell.slang using the installed
ludus_compile_shader helper. Linux emits validated SPIR-V (and WGSL as part of
the existing helper); macOS emits MSL. Only native execution is covered here.
The sample neither adds the engine as a subdirectory nor includes private RHI
or backend headers.
CTest runs five checks: camera/projection agreement and invalid-input preservation, command-line help, rejection of an unbounded headless run, three frames of direct lighting, and three frames of flat materials. The render tests create real shaders, uniforms and a pipeline, upload the layout, submit frames and shut down. A missing/unusable GPU fails the tests; it does not silently pass. These checks establish rendering execution, not pixel accuracy.
Run and repeat a lesson¶
From examples/cornell-box, rebuild after saving each source edit:
cmake --build --preset "$engine_preset"
# Linux:
out/build/linux-clang-development/cornell_box
# macOS:
out/build/macos-clang-development/cornell_box.app/Contents/MacOS/cornell_box
For lesson 3, append --flat to your host's executable command. Remove it for
later lessons. Close the running window before rebuilding and starting again.
For a bounded GPU check without a desktop window:
# Linux (requires a Vulkan adapter; software Vulkan is suitable for CI):
out/build/linux-clang-development/cornell_box --headless --frames=3
# macOS (requires a Metal adapter):
out/build/macos-clang-development/cornell_box.app/Contents/MacOS/cornell_box \
--headless --frames=3
--frames=N accepts 1..10000 successfully submitted frames. --headless
requires it; invalid arguments return 2, startup/draw failures and incomplete
bounded runs return 1, and normal completion returns 0. Linux interactive runs
need a Wayland desktop and a windowed Platform SDK. The executable does not
export images; these screenshots were captured from the real native window on
October 7, 2026 at its default square size.
Validation and recovery¶
CI builds the independent sample against the freshly installed SDK in the existing macOS/Metal and Linux/Vulkan jobs, runs CTest, and runs Clang 23 static analysis on its source. A separate sample build enables ASan/UBSan. The Linux job also starts without local presets and verifies the shared CLI/editor Repair backend, its read-only setup check and repeated repair. These checks cover the setup operations, not an end-to-end GUI interaction. The macOS job also submits three frames through a live Cocoa window.
To run the sample sanitizer configuration locally:
cmake --preset "$engine_preset" -B out/sanitized -DCORNELL_ENABLE_SANITIZERS=ON
cmake --build out/sanitized
ctest --test-dir out/sanitized --output-on-failure --no-tests=error
The SDK in this command stays the selected matching SDK; instrumentation covers the sample translation unit. Engine sanitizer validation remains in Ludus's existing sanitizer jobs. Clang 18 ASan cannot start on the macOS 26.6 development host; the macOS 14 CI job supplies that runtime validation.
In the editor, use Project → Repair Project Setup after moving tools or the
SDK. For the manual route, update the environment or ignored local presets and
run cmake --fresh --preset "$engine_preset" before rebuilding/testing. A missing
Slang compiler/validator fails during configure; shader build errors include the
exact compiler command. A native window failure means checking the WindowServer
or Wayland session and the SDK's Platform backend. GPU-startup errors are reported
through the sample's logger. Repeated configure/build/test reuses the project
without regenerating or replacing its source.
Attribution¶
Thanks to Cornell University Program of Computer Graphics, The Cornell Box, for the scene concept. This sample uses original normalized dimensions and approximate RGB colors; it does not copy measured data or implement the cited radiosity methods. The shader carries the same concise attribution beside the implementation. Mesh rendering/loading and path tracing remain future engine work, and can become later tutorials without changing this first sample's scope.