Build Your Own Engine III - Rendering Improvements
Opening
The first version of Ditto had a simple rendering goal: draw the scene and put the editor UI on top. OpenGL was the easiest choice at that stage. The API was accessible, the debugging cost was low, and it was enough for an early editor prototype.
As the engine grew, a single OpenGL renderer became a liability. Materials, render targets, Game View, Scene View, post effects, and different graphics APIs all become harder to manage if every feature is written directly against OpenGL.
Ditto later moved rendering behind an RHI, a Rendering Hardware Interface. Higher-level systems no longer call OpenGL, Vulkan, or DirectX 12 directly. They create resources, configure pipelines, and submit draws through IRenderer.
Rendering Abstraction
RHI Boundary
The goal of the RHI is not to pretend every graphics API is identical. The goal is to give engine code a stable boundary. Ditto mainly needs these capabilities:
- create textures, buffers, and render targets
- create pipelines
- bind materials and textures
- draw meshes, sprites, and UI
- support editor Scene View and Game View
- render ImGui
- choose DX12, Vulkan, or OpenGL at runtime
Upper layers describe what they want to draw. Backend layers decide how that maps to a specific API.
1 | Scene / UI / Editor |
This split is useful even for a personal project. It separates engine rendering logic from API details, which makes debugging, backend replacement, and testing much easier.
Backend Selection
Ditto currently tries runtime backends in this order:
1 | DirectX 12 -> Vulkan -> OpenGL |
The backend can also be forced with an environment variable:
1 | $env:DITTO_RHI = "dx12" |
DX12 is the main Windows target. Vulkan depends on the Vulkan SDK; the current shader pipeline also uses SDK tools such as dxc.exe and spirv-cross.exe. OpenGL remains useful as a compatibility and debugging backend.
Android is visible in the build UI as a future target, but the exporter and runtime support are not implemented yet. It should be described as planned work, not as a supported platform.
Shader Assetization
Shader and Material
After the RHI change, a material can no longer assume that the backend is an OpenGL program. Upper layers should describe the shader, textures, parameters, and render state. The backend then turns that description into its own pipeline object.
In Ditto, materials and shaders are assets. A material references a shader and stores texture, color, float, and similar parameters. During rendering, the renderer reads those assets and creates or reuses the corresponding pipeline through the RHI.
The key benefit is that the material system does not care whether the shader eventually becomes DXIL, SPIR-V, or GLSL. The material says “use this shader with these parameters”; the backend handles compilation and binding details.
Shader Files
The older shader path was basically a GLSL loader: read a file, compile a program, find uniforms, and set them during draw. The current path is closer to a small ShaderLab-style format.
A .shader file can contain HLSLPROGRAM or CGPROGRAM blocks. ShaderAsset parses the source, extracts properties and shader code, then builds the engine HLSL used by the renderer.
1 | .shader |
Using HLSL as the shared source keeps the upper-level shader authoring model consistent. The tradeoff is a more complex compilation pipeline.
Compilation Pipeline
Different backends compile shaders differently:
- DirectX 12: compile HLSL directly with dxc, usually with VSMain and PSMain
- Vulkan: compile HLSL to SPIR-V with dxc -spirv, then create shader modules
- OpenGL: generate SPIR-V first, then convert it to GLSL with spirv-cross
This path looks indirect, but it avoids maintaining separate HLSL, GLSL, and SPIR-V source files for every shader.
Diagnostics are important. ShaderCompiler checks whether tools such as dxc.exe and spirv-cross.exe are available under the Vulkan SDK path, and returns compiler errors to the caller. In the current code, Vulkan and OpenGL mainly use this shared compiler path, while the DX12 renderer calls dxc directly and can fall back to fxc. It is not the cleanest possible abstraction, but it keeps the backends practical.
In an editor, shader failure cannot just become a black screen. Users need to know whether the issue is a missing tool, invalid syntax, or backend pipeline creation failure.
Editor Rendering
RenderTarget Views
An editor does not render only to the main window. It has at least a Scene View and a Game View. They may use different cameras and sizes, and they often need to display rendered textures inside UI panels.
That makes RenderTarget a key RHI object. Game View can render into an offscreen target, then hand the result to an ImGui panel. Scene View can do the same while adding gizmos and selection overlays.
This is a major difference between drawing a game and building an editor. A game presents the final image. An editor manages rendered images as part of its tool UI.
ImGui Integration
Ditto’s editor UI is based on ImGui. Once rendering is behind the RHI, ImGui also cannot depend on only one OpenGL backend. It has to integrate with the currently selected renderer.
This work is less visible than the shader pipeline, but it affects stability every frame. If ImGui resources, framebuffers, or command submission fall out of sync with the main rendering path, the editor can flicker, go black, or break on resize.
Closing
The rendering improvements mainly solve a boundary problem. The early OpenGL renderer was enough to start the engine; the RHI gives the rendering system room to grow.
At this stage, Ditto’s renderer is no longer just “draw some geometry.” It has a more modern structure: multiple backends, shader assets, material assets, render targets, editor views, and a diagnosable compiler toolchain.
The next post covers asset saving. Once scripting and rendering work, the project needs stable persistence: scenes, prefabs, materials, shaders, textures, audio, and scripts cannot keep referencing each other through fragile file paths.






