Skip to content

@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-renderer

Architecture / 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 DrawCall objects, supports layer-mask filtering, and multi-criteria sorting (shader key, material key, depth) for optimal batching.

Frame Lifecycle โ€‹

  1. renderer.beginFrame(delta, total) - increments frame counter, updates timing
  2. Game code submits draw calls via renderer.submit(call)
  3. The render graph (or manual code) iterates draw calls, binding shaders/materials/state and calling renderer.draw(gpuMesh) - draw calls submitted before pipeline.execute() survive into pass execution because beginFrame does not clear the draw list
  4. 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 โ€‹

PackageRole
@carrot/engine-meshesMesh type used in draw calls and mesh upload
@carrot/engine-buffersFloatBuffer, IndexBuffer used by MeshUploader for GPU upload
@carrot/engine-materialsMaterialInstance type referenced in DrawCall
@carrot/engine-texturesRender target interface on Camera
@carrot/maths-geometryMatrix4x4, Vector3, vector/matrix types for transforms and projections
@carrot/colorsColorRgb, ColorRgbLike for clear colour
@carrot/signalsSignal for frame lifecycle events

Build โ€‹

bash
npm run build    # tsc

Output: 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 camera

4. 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 โ€‹

ClassDescription
ShaderProgramCompiled WebGL shader program - wraps a linked vertex + fragment program with cached uniform locations and typed setters
ShaderRegistryCompiles and caches ShaderSource to ShaderProgram - auto-registers core shaders on construction

Exports โ€‹

ExportTypeDescription
coreShadersShaderSource[]Pre-baked ShaderSource objects for the four built-in core shaders

ShaderProgram Members โ€‹

MemberTypeDescription
glWebGL2RenderingContextThe GL context
keystringShader identifier (matches ShaderSource.id)
programWebGLProgramThe compiled WebGL program
bind()methodBind this program (useProgram)
getUniformLocation()methodGet cached uniform location by name
hasUniform()methodCheck if a uniform exists
setFloat()methodSet a float uniform
setInt()methodSet an int uniform
setBool()methodSet a bool uniform (as int 0/1)
setVec2()methodSet a vec2 uniform from Vector2Like
setVec3()methodSet a vec3 uniform from Vector3Like
setVec4()methodSet a vec4 uniform from Vector4Like
setColor()methodSet a vec4 uniform from ColorRgbLike
setMat4()methodSet a mat4 uniform from Matrix4x4Like (converts to column-major)
setMat4Array()methodSet a mat4 uniform from raw Float32Array
setTexture()methodSet a sampler uniform to a texture unit
dispose()methodDelete the GL program

ShaderRegistry Members โ€‹

MemberTypeDescription
onRegisteredSignal<{ key, program }>Fired when a new shader is compiled
countnumberNumber of registered programs (getter)
register()methodCompile and cache a ShaderSource (idempotent)
registerFile()methodRegister from a parsed ShaderFile
registerAll()methodBulk register multiple ShaderSource objects
registerXml()methodParse and register a .shader XML string
registerGlob()methodRegister all shaders from a Vite import.meta.glob result (Record<string, string>)
get()methodGet compiled program by key (throws if missing)
tryGet()methodGet compiled program by key (returns undefined if missing)
has()methodCheck if a key is registered
getByPrefix()methodGet all programs matching a key prefix
override()methodReplace an existing shader (hot-reload / runtime swap)
disposeAll()methodDispose all compiled programs

Built-in Shader Library โ€‹

Shader IDCategoryDescription
core.sprite.defaultSpriteTextured quad with tint colour (u_MainTexture, u_TintColor)
core.fullscreen.blitFullscreenFullscreen triangle blit - no vertex buffer needed (u_MainTexture)
core.unlit.colorUnlitFlat colour from vertex colours multiplied by u_Color
core.unlit.texturedUnlitTextured 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 โ€‹

ClassDescription
RenderPipelineOrchestrates the full render graph - owns pass list, feature list, executes the frame for each camera
RenderPassAbstract base for render passes - defines state, target, layer mask, sort mode, and execute method
OpaquePassBuilt-in pass for non-transparent geometry - front-to-back sort, depth write ON, blend OFF
TransparentPassBuilt-in pass for transparent geometry - back-to-front sort, depth write OFF, alpha blend ON
UIOverlayPassBuilt-in pass for UI elements - no depth test, alpha blend, filters to UI_LAYER

Interfaces โ€‹

InterfaceDescription
RenderFeaturePlugin interface for custom rendering behaviour - can inject passes, submit draw calls, hook frame events

Functions โ€‹

FunctionSignatureDescription
bindMaterial(program, material, gl) => voidResolves material properties into shader uniform bindings

Constants โ€‹

ConstantValueDescription
UI_LAYER1 << 8 (0x100)Default layer bitmask for UI draw calls

RenderPipeline Members โ€‹

MemberTypeDescription
rendererRendererThe core renderer
shadersShaderRegistryShader compilation registry
passesreadonly RenderPass[]Current pass list (getter)
onPassAddedSignal<RenderPass>Fired when a pass is added
onPassRemovedSignal<RenderPass>Fired when a pass is removed
addPass()methodAppend a pass to the end
addPassAfter()methodInsert a pass after a named pass
addPassBefore()methodInsert a pass before a named pass
removePass()methodRemove a pass by name
getPass()methodFind a pass by name
addFeature()methodRegister a render feature
removeFeature()methodRemove a feature by name
getFeature()methodFind a feature by name
execute()methodExecute the full pipeline for all cameras
executeDraw()methodExecute a single draw call (resolve shader, bind material, upload mesh, draw)
dispose()methodDispose all shader programs

RenderPass Members โ€‹

MemberTypeDescription
namestringPass identifier (abstract)
enabledbooleanWhether the pass is active (default: true)
targetRenderTexture | nullRender target (null = default framebuffer)
layerMasknumberLayer filter bitmask (default: all)
sortModeDrawSortModeDraw call sort mode (default: none)
shaderOverrideShaderProgram | nullForce all draw calls to use this shader
stateOverridePartial<RenderStateDescriptor> | nullRender state override
execute()abstract methodExecute the pass - called by pipeline per camera
onSetup()optional methodCalled once when pass is added to pipeline
onBegin()optional methodCalled before execute each frame
onEnd()optional methodCalled after execute each frame

RenderFeature Members โ€‹

MemberTypeDescription
namestringFeature identifier
onRegister()methodCalled when registered - add passes here
onUnregister()optional methodCalled when removed from pipeline
onFrameBegin()optional methodCalled at start of each camera's frame
onFrameEnd()optional methodCalled at end of each camera's frame

Built-in Pass State Overrides โ€‹

PassBlendDepth TestDepth WriteDepth FuncCull Face
OpaquePassNonetruetrueLessBack
TransparentPassAlphatruefalseLessBack
UIOverlayPassAlphafalsefalse-None

Carrot