Clockwork

Clockwork is a TypeScript-first, modular game engine workspace focused on ECS-driven runtime systems and a WebGL2 renderer stack.

qti-clockwork-lighting

qti-clockwork-lighting

qti-clockwork-lighting provides a deferred 2D lighting pipeline for Clockwork WebGL2 renderer stacks.

It includes:

  • Point/spot/ambient light component schemas
  • Shadow occluder support with CPU-generated shadow atlas strips
  • Deferred light accumulation and composite passes
  • Post-processing stages (bloom, vignette, colour LUT)
  • Render graph integration via plugin-managed pass registration

Clockwork JVM equivalent uses renderer queue light draw calls and render-stage systems.


Install

JavaScript/TypeScript:

npm i qti-clockwork-lighting

Maven (Clockwork JVM — renderer is in clockwork-jvm-renderer):

<dependency>
  <groupId>com.quietterminal</groupId>
  <artifactId>clockwork-jvm-renderer</artifactId>
  <version>1.0.0</version>
</dependency>

Gradle:

dependencies {
    implementation("com.quietterminal:clockwork-jvm-renderer:1.0.0")
}

Register the Plugin

TypeScript:

import { AppBuilder } from 'qti-clockwork-app'
import { createLightingPlugin } from 'qti-clockwork-lighting'

const app = new AppBuilder()
  .use(
    createLightingPlugin({
      config: {
        bloomEnabled: true,
        shadowAtlasSize: 2048,
        maxLights: 64
      }
    })
  )
  .build()

JavaScript:

import { AppBuilder } from 'qti-clockwork-app'
import { createLightingPlugin } from 'qti-clockwork-lighting'

const app = new AppBuilder()
  .use(
    createLightingPlugin({
      config: {
        bloomEnabled: true,
        shadowAtlasSize: 2048,
        maxLights: 64
      }
    })
  )
  .build()

Clockwork JVM example:

import com.quietterminal.clockwork.ClockworkApp;
import com.quietterminal.clockwork.renderer.ClockworkRendererPlugin;
import com.quietterminal.clockwork.renderer.WindowConfig;

ClockworkApp app = new ClockworkApp()
    .use(new ClockworkRendererPlugin(WindowConfig.builder().build()))
    .build();

Light Components

TypeScript:

import {
  POINT_LIGHT,
  SPOT_LIGHT,
  AMBIENT_LIGHT,
  SHADOW_OCCLUDER
} from 'qti-clockwork-lighting'

const point = app.world.spawnEntity()
app.world.addComponent(point, POINT_LIGHT, {
  position: { x: 8, y: 6 },
  colour: { r: 1, g: 0.9, b: 0.7 },
  intensity: 2.0,
  radius: 12,
  falloff: 'quadratic',
  castsShadows: true,
  shadowSoftness: 0.5
})

const ambient = app.world.spawnEntity()
app.world.addComponent(ambient, AMBIENT_LIGHT, {
  colour: { r: 0.1, g: 0.1, b: 0.15 },
  intensity: 1.0,
  skyColour: { r: 0.14, g: 0.14, b: 0.2 },
  groundColour: { r: 0.04, g: 0.04, b: 0.05 }
})

const occluder = app.world.spawnEntity()
app.world.addComponent(occluder, SHADOW_OCCLUDER, {
  segments: [
    [{ x: 0, y: 0 }, { x: 10, y: 0 }],
    [{ x: 10, y: 0 }, { x: 10, y: 5 }]
  ]
})

JavaScript:

import { POINT_LIGHT, AMBIENT_LIGHT, SHADOW_OCCLUDER } from 'qti-clockwork-lighting'

const point = app.world.spawnEntity()
app.world.addComponent(point, POINT_LIGHT, {
  position: { x: 8, y: 6 },
  colour: { r: 1, g: 0.9, b: 0.7 },
  intensity: 2.0,
  radius: 12,
  falloff: 'quadratic',
  castsShadows: true,
  shadowSoftness: 0.5
})

const ambient = app.world.spawnEntity()
app.world.addComponent(ambient, AMBIENT_LIGHT, {
  colour: { r: 0.1, g: 0.1, b: 0.15 },
  intensity: 1.0,
  skyColour: { r: 0.14, g: 0.14, b: 0.2 },
  groundColour: { r: 0.04, g: 0.04, b: 0.05 }
})

const occluder = app.world.spawnEntity()
app.world.addComponent(occluder, SHADOW_OCCLUDER, {
  segments: [
    [{ x: 0, y: 0 }, { x: 10, y: 0 }],
    [{ x: 10, y: 0 }, { x: 10, y: 5 }]
  ]
})

Clockwork JVM equivalent:

import com.quietterminal.clockwork.scheduler.Stage;
import com.quietterminal.clockwork.renderer.ClockworkRendererPlugin;
import com.quietterminal.clockwork.renderer.RenderQueue;

app.addSystem(Stage.RENDER, ctx -> {
    RenderQueue queue = (RenderQueue) ctx.resources()
        .get(ClockworkRendererPlugin.RENDER_QUEUE_KEY)
        .orElseThrow();
    queue.lights().add(new RenderQueue.LightDrawCall(8.0f, 6.0f, 12.0f, 2.0f));
});

World Resources

TypeScript:

import { LIGHTING_WORLD_KEY, LIGHTING_CONFIG_KEY, type LightingWorld } from 'qti-clockwork-lighting'

const lightingWorld = app.world.getResource(LIGHTING_WORLD_KEY) as LightingWorld
const lightingConfig = app.world.getResource(LIGHTING_CONFIG_KEY)

void lightingWorld
void lightingConfig

JavaScript:

import { LIGHTING_WORLD_KEY, LIGHTING_CONFIG_KEY } from 'qti-clockwork-lighting'

const lightingWorld = app.world.getResource(LIGHTING_WORLD_KEY)
const lightingConfig = app.world.getResource(LIGHTING_CONFIG_KEY)

void lightingWorld
void lightingConfig

Clockwork JVM equivalent:

import com.quietterminal.clockwork.renderer.ClockworkRendererPlugin;
import com.quietterminal.clockwork.renderer.RenderQueue;

RenderQueue queue = (RenderQueue) app.world().resources()
    .get(ClockworkRendererPlugin.RENDER_QUEUE_KEY)
    .orElseThrow();
System.out.println(queue.snapshot().lights().size());

Notes

  • The plugin registers a deferred pass chain (shadowMap, lightingAccum, composite, postProcess, output) into the render graph.
  • SHADOW_OCCLUDER segments are ray-cast on CPU, then uploaded to the shadow atlas texture for shading.
  • LIGHTING_CAMERA_KEY (renderer:camera) should be present so light culling and world-space reconstruction are accurate.
Last updated: March 05, 2026