Appearance
@carrot/engine-renderer-graph ​
ts
Render graph and pipeline orchestration for the Carrot WebGL2 engine - multi-pass, multi-camera rendering with extensible features.
Installation ​
bash
npm install @carrot/engine-renderer-graphArchitecture / How It Works ​
This package sits on top of @carrot/engine-renderer (low-level draw API) and @carrot/engine-renderer-shaders (shader compilation). It provides the high-level rendering pipeline that most game code interacts with.
RenderPipeline ​
The central orchestrator. Given a Renderer and a ShaderRegistry, it manages an ordered list of RenderPass instances and a set of RenderFeature plugins.
Frame execution (pipeline.execute(cameras, deltaTime)):
- Cameras are sorted by
renderOrder renderer.beginFrame()is called once- For each camera:
- Bind render target (or default framebuffer)
- Set viewport from camera's normalised viewport rect
- Clear per camera's
clearFlagsandclearColor - Notify all features (
onFrameBegin) - Execute each enabled pass in order:
- Apply pass render state (merged with defaults)
- Filter draw calls by combined pass + camera layer mask
- Sort draw calls per pass
sortMode - Call pass lifecycle:
onBegin/execute/onEnd
- Notify all features (
onFrameEnd) - Unbind render target
renderer.endFrame()is called once
executeDraw() is the helper that render passes call for each draw call. It resolves the shader from the material's shaderKey, binds it via the state cache, sets built-in uniforms (u_ViewProjection, u_Model, u_Time), calls bindMaterial() to map material properties to uniforms, uploads the mesh if dirty, and issues the draw.
RenderPass ​
Abstract base class. Subclass it to define custom rendering logic. Each pass declares:
- name - unique identifier for lookup/insertion
- sortMode - how to sort draw calls (
front-to-back,back-to-front, ornone) - layerMask - which draw call layers to include
- stateOverride - blend, depth, cull settings for this pass
- target - optional render-to-texture target
- shaderOverride - force all draw calls to use a specific shader
Three built-in passes are provided: OpaquePass, TransparentPass, and UIOverlayPass.
RenderFeature ​
Plugin interface for extending the pipeline. Features can:
- Inject passes at specific positions (
addPassAfter,addPassBefore) - Submit additional draw calls
- Hook into frame begin/end for per-frame work
Use cases: particle systems, post-processing chains, shadow map generation, debug visualisation.
bindMaterial ​
Resolves a MaterialInstance's property schema into shader uniform calls. Iterates the material's property definitions, reads values by key, and calls the appropriate ShaderProgram setter (setFloat, setColor, setVec3, setTexture, etc.). Handles texture unit assignment automatically.
Dependencies ​
| Package | Role |
|---|---|
@carrot/engine-renderer | Core renderer, Camera, DrawList, RenderStateCache, DrawCall |
@carrot/engine-renderer-shaders | ShaderRegistry, ShaderProgram for shader resolution |
@carrot/engine-materials | MaterialInstance, MaterialPropertyType, MaterialPropertyDef for material binding |
@carrot/engine-meshes | Mesh type (via DrawCall) |
@carrot/engine-textures | RenderTexture for pass targets, Texture for material binding |
@carrot/maths-geometry | Vector/matrix types for uniform binding |
@carrot/colors | ColorRgbLike for colour uniforms |
@carrot/signals | Signal for pass added/removed events |
Build ​
bash
npm run build # tscOutput: dist/ (ESM + CJS, TypeScript declarations).
Usage Guide ​
High-level render pipeline with multi-pass, multi-camera rendering and extensible features.
Import ​
ts
import {
RenderPipeline, RenderPass, bindMaterial,
OpaquePass, TransparentPass, UIOverlayPass, UI_LAYER,
} from '@carrot/engine-renderer-graph';
import type { RenderFeature } from '@carrot/engine-renderer-graph';Common Patterns ​
1. Set up a standard render pipeline ​
ts
import { Renderer, Camera } from '@carrot/engine-renderer';
import { ShaderRegistry } from '@carrot/engine-renderer-shaders';
const renderer = new Renderer(canvas);
const shaders = new ShaderRegistry(renderer.gl);
const pipeline = new RenderPipeline(renderer, shaders);
// Add standard passes
const opaque = new OpaquePass();
opaque.attachPipeline(pipeline);
pipeline.addPass(opaque);
const transparent = new TransparentPass();
transparent.attachPipeline(pipeline);
pipeline.addPass(transparent);2. Add a UI overlay pass ​
ts
import { createDrawCall } from '@carrot/engine-renderer';
const uiPass = new UIOverlayPass();
uiPass.attachPipeline(pipeline);
pipeline.addPass(uiPass);
// Submit UI draw calls on UI_LAYER
const uiCall = createDrawCall(uiMesh, uiMaterial, uiTransform, UI_LAYER);
renderer.submit(uiCall);3. Execute the pipeline each frame ​
ts
function gameLoop(deltaTime: number) {
// Submit draw calls
for (const entity of scene.entities) {
renderer.submit(createDrawCall(entity.mesh, entity.material, entity.transform));
}
// Execute all passes for all cameras
pipeline.execute([mainCamera, uiCamera], deltaTime);
}4. Insert passes at specific positions ​
ts
// Insert a shadow pass before opaques
pipeline.addPassBefore('opaque', shadowPass);
// Insert a particle pass after transparents
pipeline.addPassAfter('transparent', particlePass);5. Create a custom render pass ​
ts
class WireframePass extends RenderPass {
readonly name = 'wireframe';
sortMode = 'none' as const;
private pipeline?: RenderPipeline;
attachPipeline(pipeline: RenderPipeline): void {
this.pipeline = pipeline;
}
stateOverride = {
blend: BlendMode.None,
depthTest: true,
depthWrite: false,
cullFace: CullFace.None,
scissorTest: false,
};
execute(renderer: Renderer, context: RenderContext, drawCalls: readonly DrawCall[]): void {
if (!this.pipeline) return;
const wireShader = this.pipeline.shaders.get('debug.wireframe');
for (const call of drawCalls) {
this.pipeline.executeDraw(call, context, wireShader);
}
}
}6. Create a render feature (plugin) ​
ts
class ParticleRenderFeature implements RenderFeature {
readonly name = 'Particles';
private pass = new ParticlePass();
onRegister(pipeline: RenderPipeline): void {
this.pass.attachPipeline(pipeline);
pipeline.addPassAfter('transparent', this.pass);
}
onUnregister(pipeline: RenderPipeline): void {
pipeline.removePass('particles');
}
onFrameBegin(renderer: Renderer, context: RenderContext): void {
// Submit particle draw calls each frame
for (const emitter of this.emitters) {
renderer.submit(createDrawCall(emitter.mesh, emitter.material, emitter.transform));
}
}
}
pipeline.addFeature(new ParticleRenderFeature());7. Enable/disable passes at runtime ​
ts
const debugPass = pipeline.getPass('wireframe');
if (debugPass) {
debugPass.enabled = !debugPass.enabled;
}8. Render to texture (off-screen pass) ​
ts
const shadowPass = new ShadowPass();
shadowPass.target = shadowMapTexture; // RenderTexture
pipeline.addPassBefore('opaque', shadowPass);9. Shader override for a pass ​
ts
// Force all draw calls in a pass to use a specific shader
const depthOnlyPass = new OpaquePass();
depthOnlyPass.shaderOverride = shaders.get('core.depth.only');10. Layer masking ​
ts
// Camera only sees layers 0 and 1
mainCamera.layerMask = (1 << 0) | (1 << 1);
// Pass only processes layer 0
opaquePass.layerMask = 1 << 0;
// Combined mask: pipeline ANDs camera.layerMask with pass.layerMask11. Pipeline lifecycle signals ​
ts
pipeline.onPassAdded.listen((pass) => {
console.log(`Pass added: ${pass.name}`);
});
pipeline.onPassRemoved.listen((pass) => {
console.log(`Pass removed: ${pass.name}`);
});12. Cleanup ​
ts
pipeline.dispose(); // disposes all shader programs