Appearance
@carrot/engine-renderer โ
ts
Low-level WebGL2 rendering foundation - owns the GL context, GPU state cache, mesh upload pipeline, and frame lifecycle.
Installation โ
bash
npm install @carrot/engine-rendererArchitecture / How It Works โ
This package is the bottom layer of the rendering stack. It provides the raw draw API without any knowledge of render graphs, passes, or scene structure. Higher-level packages (@carrot/engine-renderer-graph) build on top of this.
Core Components โ
Renderer is the entry point. It creates a WebGL2 context from a canvas, and owns three subsystems:
- RenderStateCache - tracks current GPU state (blend mode, depth, cull face, scissor, bound shaders/textures) and only issues GL calls when state actually changes. This is critical for batching performance.
- MeshUploader - manages the CPU-to-GPU mesh upload lifecycle. Creates VAOs with attribute bindings (positions, normals, tangents, up to 6 UV channels, vertex colours), caches by
Mesh.uniqueId, and re-uploads when meshes are marked dirty. - DrawList - collects submitted
DrawCallobjects, supports layer-mask filtering, and multi-criteria sorting (shader key, material key, depth) for optimal batching.
Frame Lifecycle โ
renderer.beginFrame(delta, total)- increments frame counter, updates timing- Game code submits draw calls via
renderer.submit(call) - The render graph (or manual code) iterates draw calls, binding shaders/materials/state and calling
renderer.draw(gpuMesh)- draw calls submitted beforepipeline.execute()survive into pass execution becausebeginFramedoes not clear the draw list renderer.endFrame()- clears the draw list and performs post-frame cleanup
The draw list lifecycle is: submit โ beginFrame โ passes read โ endFrame clears. This ordering ensures that draw calls submitted between beginFrame and pipeline.execute() are visible to all render passes.
Camera โ
Camera defines a viewpoint - perspective or orthographic projection, view matrix (via lookAt or direct set), clear settings, viewport rect, render target, and layer mask. Multiple cameras are supported (e.g. main camera + minimap + shadow cameras).
DrawCall โ
The atomic unit of rendering: a mesh + material + model transform + layer mask. Created via createDrawCall(). Sort keys are computed by DrawList.sort() for batching.
RenderContext โ
A per-camera, per-frame snapshot passed to render passes: view/projection/VP matrices, target dimensions, frame number, delta time, total time.
Dependencies โ
| Package | Role |
|---|---|
@carrot/engine-meshes | Mesh type used in draw calls and mesh upload |
@carrot/engine-buffers | FloatBuffer, IndexBuffer used by MeshUploader for GPU upload |
@carrot/engine-materials | MaterialInstance type referenced in DrawCall |
@carrot/engine-textures | Render target interface on Camera |
@carrot/maths-geometry | Matrix4x4, Vector3, vector/matrix types for transforms and projections |
@carrot/colors | ColorRgb, ColorRgbLike for clear colour |
@carrot/signals | Signal for frame lifecycle events |
Build โ
bash
npm run build # tscOutput: dist/ (ESM + CJS, TypeScript declarations).
Using @carrot/engine-renderer โ
Low-level WebGL2 renderer - canvas context management, state caching, mesh upload, draw call submission.
Import โ
ts
import {
Renderer, Camera, ClearFlags,
RenderStateCache, BlendMode, DepthFunc, CullFace, defaultRenderState,
createDrawCall, DrawList, DrawSortMode,
createRenderContext, MeshUploader,
} from '@carrot/engine-renderer';
import type { RenderContext, DrawCall, RenderStateDescriptor, GpuMesh } from '@carrot/engine-renderer';Common Patterns โ
1. Create a renderer โ
ts
const canvas = document.getElementById('game') as HTMLCanvasElement;
const renderer = new Renderer(canvas, { antialias: true, alpha: false });2. Set up a camera โ
ts
const camera = new Camera();
camera.setPerspective(60, canvas.width / canvas.height, 0.1, 1000);
camera.lookAt({ x: 0, y: 5, z: 10 }, { x: 0, y: 0, z: 0 });
camera.clearFlags = ClearFlags.All;
camera.clearColor = { r: 0.1, g: 0.1, b: 0.15, a: 1 };3. Orthographic camera (minimap, UI) โ
ts
const uiCamera = new Camera();
uiCamera.setOrthographic(0, 1920, 0, 1080, -1, 1);
uiCamera.clearFlags = ClearFlags.None;
uiCamera.renderOrder = 10; // render after main camera4. Multi-camera with render targets โ
ts
const minimap = new Camera();
minimap.setOrthographic(-50, 50, -50, 50, 0.1, 100);
minimap.target = minimapRenderTexture; // renders to texture, not screen
minimap.renderOrder = 1;5. Submit draw calls โ
ts
const call = createDrawCall(mesh, materialInstance, modelMatrix);
renderer.submit(call);
// With a specific layer mask (e.g. layer 2)
const uiCall = createDrawCall(uiMesh, uiMaterial, uiTransform, 1 << 2);
renderer.submit(uiCall);6. Manual frame loop (without render graph) โ
ts
function frame(deltaTime: number, totalTime: number) {
renderer.beginFrame(deltaTime, totalTime);
// Submit draw calls
for (const entity of scene.entities) {
renderer.submit(createDrawCall(entity.mesh, entity.material, entity.transform));
}
// Build context
const ctx = renderer.createContext(camera, deltaTime);
// Clear and set viewport
renderer.clear(camera);
renderer.setViewport(camera, canvas.width, canvas.height);
// Sort and iterate
renderer.drawList.sort(DrawSortMode.FrontToBack);
for (const call of renderer.drawList) {
// Bind shader, set uniforms, then:
const gpuMesh = renderer.meshes.getOrUpload(call.mesh);
if (gpuMesh) renderer.draw(gpuMesh);
}
renderer.endFrame();
}7. Apply render state โ
ts
// Apply a full state descriptor (only diffs are sent to GPU)
renderer.state.apply({
blend: BlendMode.Alpha,
depthTest: true,
depthWrite: false,
depthFunc: DepthFunc.Less,
cullFace: CullFace.Back,
scissorTest: false,
});
// Or set individual states
renderer.state.setBlend(BlendMode.Additive);
renderer.state.setDepthWrite(false);
renderer.state.setCullFace(CullFace.None);
// Bind shader/texture (tracked to avoid redundant binds)
renderer.state.bindShader(program);
renderer.state.bindTexture(0, gl.TEXTURE_2D, texture);8. Filter draw calls by layer โ
ts
// Get only layer 0 draw calls
const layer0Calls = renderer.drawList.filter(1);
// Get layers 0 and 3
const filtered = renderer.drawList.filter((1 << 0) | (1 << 3));
// All layers (fast path - no filtering)
const all = renderer.drawList.filter(0xFFFFFFFF);9. Frame lifecycle signals โ
ts
renderer.onBeginFrame.listen((ctx) => {
console.log(`Frame ${ctx.frameNumber}, dt=${ctx.deltaTime}`);
});
renderer.onEndFrame.listen((ctx) => {
// post-frame work
});10. Cleanup โ
ts
renderer.dispose(); // disposes all uploaded meshes
// Dispose a single mesh by ID
renderer.meshes.dispose(mesh.uniqueId);Package Contents โ
@carrot/engine-renderer-shaders โ
Import: import { ... } from '@carrot/engine-renderer-shaders';
Classes โ
| Class | Description |
|---|---|
ShaderProgram | Compiled WebGL shader program - wraps a linked vertex + fragment program with cached uniform locations and typed setters |
ShaderRegistry | Compiles and caches ShaderSource to ShaderProgram - auto-registers core shaders on construction |
Exports โ
| Export | Type | Description |
|---|---|---|
coreShaders | ShaderSource[] | Pre-baked ShaderSource objects for the four built-in core shaders |
ShaderProgram Members โ
| Member | Type | Description |
|---|---|---|
gl | WebGL2RenderingContext | The GL context |
key | string | Shader identifier (matches ShaderSource.id) |
program | WebGLProgram | The compiled WebGL program |
bind() | method | Bind this program (useProgram) |
getUniformLocation() | method | Get cached uniform location by name |
hasUniform() | method | Check if a uniform exists |
setFloat() | method | Set a float uniform |
setInt() | method | Set an int uniform |
setBool() | method | Set a bool uniform (as int 0/1) |
setVec2() | method | Set a vec2 uniform from Vector2Like |
setVec3() | method | Set a vec3 uniform from Vector3Like |
setVec4() | method | Set a vec4 uniform from Vector4Like |
setColor() | method | Set a vec4 uniform from ColorRgbLike |
setMat4() | method | Set a mat4 uniform from Matrix4x4Like (converts to column-major) |
setMat4Array() | method | Set a mat4 uniform from raw Float32Array |
setTexture() | method | Set a sampler uniform to a texture unit |
dispose() | method | Delete the GL program |
ShaderRegistry Members โ
| Member | Type | Description |
|---|---|---|
onRegistered | Signal<{ key, program }> | Fired when a new shader is compiled |
count | number | Number of registered programs (getter) |
register() | method | Compile and cache a ShaderSource (idempotent) |
registerFile() | method | Register from a parsed ShaderFile |
registerAll() | method | Bulk register multiple ShaderSource objects |
registerXml() | method | Parse and register a .shader XML string |
registerGlob() | method | Register all shaders from a Vite import.meta.glob result (Record<string, string>) |
get() | method | Get compiled program by key (throws if missing) |
tryGet() | method | Get compiled program by key (returns undefined if missing) |
has() | method | Check if a key is registered |
getByPrefix() | method | Get all programs matching a key prefix |
override() | method | Replace an existing shader (hot-reload / runtime swap) |
disposeAll() | method | Dispose all compiled programs |
Built-in Shader Library โ
| Shader ID | Category | Description |
|---|---|---|
core.sprite.default | Sprite | Textured quad with tint colour (u_MainTexture, u_TintColor) |
core.fullscreen.blit | Fullscreen | Fullscreen triangle blit - no vertex buffer needed (u_MainTexture) |
core.unlit.color | Unlit | Flat colour from vertex colours multiplied by u_Color |
core.unlit.textured | Unlit | Textured with vertex colours, multiplied by u_Color |
Built-in Uniforms (set by pipeline) โ
u_ViewProjection (mat4) ยท u_Model (mat4) ยท u_Time (float)
@carrot/engine-renderer-graph โ
Import: import { ... } from '@carrot/engine-renderer-graph';
Classes โ
| Class | Description |
|---|---|
RenderPipeline | Orchestrates the full render graph - owns pass list, feature list, executes the frame for each camera |
RenderPass | Abstract base for render passes - defines state, target, layer mask, sort mode, and execute method |
OpaquePass | Built-in pass for non-transparent geometry - front-to-back sort, depth write ON, blend OFF |
TransparentPass | Built-in pass for transparent geometry - back-to-front sort, depth write OFF, alpha blend ON |
UIOverlayPass | Built-in pass for UI elements - no depth test, alpha blend, filters to UI_LAYER |
Interfaces โ
| Interface | Description |
|---|---|
RenderFeature | Plugin interface for custom rendering behaviour - can inject passes, submit draw calls, hook frame events |
Functions โ
| Function | Signature | Description |
|---|---|---|
bindMaterial | (program, material, gl) => void | Resolves material properties into shader uniform bindings |
Constants โ
| Constant | Value | Description |
|---|---|---|
UI_LAYER | 1 << 8 (0x100) | Default layer bitmask for UI draw calls |
RenderPipeline Members โ
| Member | Type | Description |
|---|---|---|
renderer | Renderer | The core renderer |
shaders | ShaderRegistry | Shader compilation registry |
passes | readonly RenderPass[] | Current pass list (getter) |
onPassAdded | Signal<RenderPass> | Fired when a pass is added |
onPassRemoved | Signal<RenderPass> | Fired when a pass is removed |
addPass() | method | Append a pass to the end |
addPassAfter() | method | Insert a pass after a named pass |
addPassBefore() | method | Insert a pass before a named pass |
removePass() | method | Remove a pass by name |
getPass() | method | Find a pass by name |
addFeature() | method | Register a render feature |
removeFeature() | method | Remove a feature by name |
getFeature() | method | Find a feature by name |
execute() | method | Execute the full pipeline for all cameras |
executeDraw() | method | Execute a single draw call (resolve shader, bind material, upload mesh, draw) |
dispose() | method | Dispose all shader programs |
RenderPass Members โ
| Member | Type | Description |
|---|---|---|
name | string | Pass identifier (abstract) |
enabled | boolean | Whether the pass is active (default: true) |
target | RenderTexture | null | Render target (null = default framebuffer) |
layerMask | number | Layer filter bitmask (default: all) |
sortMode | DrawSortMode | Draw call sort mode (default: none) |
shaderOverride | ShaderProgram | null | Force all draw calls to use this shader |
stateOverride | Partial<RenderStateDescriptor> | null | Render state override |
execute() | abstract method | Execute the pass - called by pipeline per camera |
onSetup() | optional method | Called once when pass is added to pipeline |
onBegin() | optional method | Called before execute each frame |
onEnd() | optional method | Called after execute each frame |
RenderFeature Members โ
| Member | Type | Description |
|---|---|---|
name | string | Feature identifier |
onRegister() | method | Called when registered - add passes here |
onUnregister() | optional method | Called when removed from pipeline |
onFrameBegin() | optional method | Called at start of each camera's frame |
onFrameEnd() | optional method | Called at end of each camera's frame |
Built-in Pass State Overrides โ
| Pass | Blend | Depth Test | Depth Write | Depth Func | Cull Face |
|---|---|---|---|---|---|
OpaquePass | None | true | true | Less | Back |
TransparentPass | Alpha | true | false | Less | Back |
UIOverlayPass | Alpha | false | false | - | None |