import { ChannelName, WithRequiredChannels } from "../materials/channels.js"; import { EffectConstants, EffectField, EffectSchema, EffectSchemaValue, EffectValues, SchemaToNodeType, UniformKeys } from "../materials/MaterialEffect.js"; import { ColorTransformContext, ColorTransformFn } from "../materials/EffectMaterial.js"; import "../materials/Sprite2DMaterial.js"; import { Light2D } from "./Light2D.js"; import { LightStore } from "./LightStore.js"; import { SDFGenerator } from "./SDFGenerator.js"; import { OrthographicCamera, Texture, Vector2, Vector3, Vector4 } from "three"; import { WebGPURenderer } from "three/webgpu"; import { Entity, Trait } from "koota"; import Node from "three/src/nodes/core/Node.js"; import UniformNode from "three/src/nodes/core/UniformNode.js"; //#region src/lights/LightEffect.d.ts /** Compile-time context — passed to buildLightFn (called once on attach). */ interface LightEffectBuildContext { /** TSL uniform nodes for each uniform schema field, keyed by field name. */ uniforms: { [K in UniformKeys]: SchemaToNodeType; }; /** Read-only constants from factory function fields. */ constants: EffectConstants; /** The LightStore providing light data textures and count. */ lightStore: LightStore; /** * Stable reference to the scene's SDF texture. Non-null only when the * effect's class declared `needsShadows = true` — in that case Flatland * eagerly allocates the SDFGenerator before calling buildLightFn so the * reference is bindable in TSL `texture()` calls. The texture's RTs are * 1×1 placeholders at build time; the shadow pipeline system resizes * them on first frame and refreshes contents each subsequent frame, * without ever changing the reference. * * Null when the active effect doesn't declare `needsShadows` — shaders * should compile out the shadow path in that case (JS-level `if`, not * a GPU branch). */ sdfTexture: Texture | null; /** * Camera frustum width/height uniform node, updated each frame from the * camera bounds. Effects that map between world and UV space * (shadow sampling, radiance cascades, any other screen-projected * sampler) consume this instead of rolling their own uniform. */ worldSizeNode: UniformNode<'vec2', Vector2>; /** * Camera frustum bottom-left offset uniform node, updated each frame. */ worldOffsetNode: UniformNode<'vec2', Vector2>; } /** Runtime context — passed to init/update each frame. */ interface LightEffectRuntimeContext { renderer: WebGPURenderer; camera: OrthographicCamera; lightStore: LightStore; sdfGenerator: SDFGenerator | null; lights: readonly Light2D[]; worldSize: Vector2; worldOffset: Vector2; } interface FlatlandLike { _markLightingDirty(): void; _markLightingResizeDirty?(): void; /** * Re-run the attached LightEffect's `_buildLightFn` to capture * fresh constant values, re-wrap the result, and push it to every * lit material. Called from a writable LightEffect constant's * setter — does nothing if no LightEffect is currently attached. * Coalesced (one rebuild per microtask flush) inside the * implementation. */ _rebuildLightFn?: () => void; } type UniformNodeValue = UniformNode<'float', number> | UniformNode<'vec2', Vector2> | UniformNode<'vec3', Vector3> | UniformNode<'vec4', Vector4>; /** * Base class for lighting effects applied to Flatland sprites. * * Mirrors the PassEffect pattern: class-based, schema-driven, with property * accessors. Uses TSL `uniform()` nodes for zero-cost runtime parameter updates. * * LightEffect produces a `ColorTransformFn` that is automatically assigned to * all lit sprites. The transform runs in the material shader, reading light data * from shared DataTextures managed by LightStore. * * Subclasses may also override lifecycle methods (init/update/resize/dispose) * to manage GPU resources like Forward+ tiling or Radiance Cascades. * * @example Class-based definition: * ```typescript * class DefaultLightEffect extends LightEffect { * static readonly lightName = 'defaultLight' * static readonly lightSchema = { ambientIntensity: 0.2 } as const * static readonly needsShadows = false * declare ambientIntensity: number * * static buildLightFn({ uniforms, lightStore }: LightEffectBuildContext): ColorTransformFn { * // build shader using lightStore.readLightData() * } * } * ``` */ declare abstract class LightEffect { /** Unique light effect name. Must be overridden by subclass. */ static readonly lightName: string; /** Per-effect data schema with default values. Must be overridden by subclass. */ static readonly lightSchema: EffectSchema; /** Whether this effect needs the shadow/SDF pipeline. */ static readonly needsShadows: boolean; /** Per-fragment channels this effect requires (e.g., ['normal']). */ static readonly requires: readonly ChannelName[]; /** @internal Auto-generated Koota trait from schema. */ static _trait: Trait; /** @internal Computed field metadata from schema. */ static _fields: EffectField[]; /** @internal Total float slots needed for this effect's data. */ static _totalFloats: number; /** @internal Whether static initialization has been performed. */ static _initialized: boolean; /** * Build the lighting ColorTransformFn. Must be overridden by subclass. * Called once when the effect is attached to Flatland. The returned function * closes over uniform nodes for zero-cost parameter updates. */ static buildLightFn(_context: LightEffectBuildContext): ColorTransformFn; /** @internal Factory functions for constant fields (keyed by field name). */ static _constantFactories: Record unknown>; /** * Initialize static metadata from the schema (called once per subclass, lazily). * @internal */ static _initialize(): void; /** Effect name (from static). */ readonly name: string; /** @internal The Flatland instance this effect is attached to. */ _flatland: FlatlandLike | null; /** @internal The ECS entity for this effect. */ _entity: Entity | null; /** @internal Snapshot defaults for pre-enrollment staging. */ _defaults: Record; /** @internal Per-instance constant values (from factory function schema fields). */ _constants: Record; /** @internal TSL uniform nodes — one per uniform schema field. */ _uniforms: Record; /** @internal Cached result of buildLightFn(). */ _lightFn: ColorTransformFn | null; /** @internal Whether this effect is enabled. */ private _enabled; /** @internal Scale applied to the physical surface before resize(). */ private _resolutionScale; /** @internal Whether init() has been called. */ _initialized: boolean; /** @internal Whether uniform/structural state changed since last clearDirty(). */ _dirty: boolean; /** @internal Callback invoked when dirty state changes (set by _attach). */ _onDirty: (() => void) | null; constructor(); /** Whether this effect is enabled. */ get enabled(): boolean; /** Toggle enabled state. Structural change — marks lighting dirty. */ set enabled(value: boolean); /** * Processing-resolution multiplier for resources owned by this effect. * The default `1` receives the full physical drawing-buffer size; `0.5` * receives half-width/half-height dimensions while camera framing and the * renderer output stay unchanged. Values must be finite and greater than 0. */ get resolutionScale(): number; set resolutionScale(value: number); /** Whether this effect has been marked dirty since last clearDirty(). */ get dirty(): boolean; /** Clear the dirty flag. Called by lighting systems after processing. */ clearDirty(): void; /** Initialize GPU resources. Called lazily before the first resize and update. */ init(_ctx: LightEffectRuntimeContext): void; /** Per-frame GPU passes (tiling, SDF, radiance cascades). */ update(_ctx: LightEffectRuntimeContext): void; /** * Handle processing-surface resize. Flatland derives this from the physical * render surface multiplied by {@link resolutionScale}. Once a valid size * exists, this is called after init and before the next update. An initial * update may run first while a newly mounted canvas still reports 0×0. */ resize(_width: number, _height: number): void; /** * Attach this effect to a Flatland instance. * @internal Called by Flatland.setLighting() */ _attach(flatland: FlatlandLike, onDirty?: () => void): void; /** * Detach this effect from its Flatland instance. * @internal Called by Flatland when lighting is replaced */ _detach(): void; /** * Build the ColorTransformFn for standalone use (no Flatland, no ECS). * * Returns the lighting function that can be assigned directly to a * sprite material's `colorTransform`. For batched sprites, wrap with * `wrapWithLightFlags()` to gate per-instance. * * @example * ```typescript * const lightStore = new LightStore() * const lighting = new DefaultLightEffect() * const lightFn = lighting.build(lightStore) * * sprite.material.colorTransform = lightFn * sprite.material.requiredChannels = new Set(DefaultLightEffect.requires) * ``` */ build(lightStore: LightStore, worldSizeNode: UniformNode<'vec2', Vector2>, worldOffsetNode: UniformNode<'vec2', Vector2>, sdfTexture?: Texture | null): ColorTransformFn; /** * Build and cache the light function by calling the static buildLightFn() once. * The returned function closes over uniform nodes, constants, world bounds, * and the stable SDF texture reference (if shadows are needed). * @internal */ _buildLightFn(lightStore: LightStore, worldSizeNode: UniformNode<'vec2', Vector2>, worldOffsetNode: UniformNode<'vec2', Vector2>, sdfTexture?: Texture | null): ColorTransformFn; /** * Dispose GPU resources owned by this effect. * Override in subclasses that own ForwardPlusLighting or other GPU resources. */ dispose(): void; /** * Read a field value. * @internal */ _getField(name: string): number | number[]; /** * Write a field value. * Updates ECS trait, uniform value, and snapshot defaults. * @internal */ _setField(name: string, value: number | number[]): void; } /** Instance type for lifecycle hook `this` binding. */ type LightEffectInstance = LightEffect & EffectValues & EffectConstants; /** Configuration passed to createLightEffect(). */ interface LightEffectConfig { /** Unique name for this light effect. */ name: string; /** Per-effect data schema — default values define types and initial values. */ schema: S; /** Whether this effect needs the shadow/SDF pipeline. */ needsShadows?: boolean; /** Per-fragment channels this effect requires (e.g., ['normal'] as const). */ requires?: C; /** * Light builder: receives uniform nodes + light store, returns a ColorTransformFn. * The returned callback's context is narrowed based on `requires` — * e.g., `requires: ['normal']` guarantees `ctx.normal` is `Node<'vec3'>`. */ light: (context: LightEffectBuildContext) => (ctx: ColorTransformContext & WithRequiredChannels) => Node<'vec4'>; /** Initialize GPU resources. Called lazily on first render. */ init?: (this: LightEffectInstance, ctx: LightEffectRuntimeContext) => void; /** Per-frame GPU passes (tiling, SDF, radiance cascades). */ update?: (this: LightEffectInstance, ctx: LightEffectRuntimeContext) => void; /** Handle resize. */ resize?: (this: LightEffectInstance, width: number, height: number) => void; /** Dispose GPU resources. */ dispose?: (this: LightEffectInstance) => void; } /** * Type for a LightEffect class created by the factory. * Instances have typed properties matching the schema. */ type LightEffectClass = { new (): LightEffect & EffectValues & EffectConstants; readonly lightName: string; readonly lightSchema: S; readonly needsShadows: boolean; readonly requires: readonly ChannelName[]; readonly _trait: Trait; readonly _fields: EffectField[]; readonly _totalFloats: number; readonly _constantFactories: Record unknown>; readonly _initialized: boolean; _initialize(): void; buildLightFn(context: LightEffectBuildContext): ColorTransformFn; }; /** * Create a LightEffect class from a configuration object. * * Supports lifecycle hooks (init/update/resize/dispose) for effects that * manage GPU resources. Use factory function fields in the schema for * per-instance constants (e.g., ForwardPlusLighting). * * @example * ```typescript * const DefaultLightEffect = createLightEffect({ * name: 'defaultLight', * schema: { ambientIntensity: 0.2 }, * light: ({ uniforms, lightStore }) => { * // build light loop using lightStore.readLightData() * }, * }) * * const lighting = new DefaultLightEffect() * flatland.setLighting(lighting) * lighting.ambientIntensity = 0.4 // zero-cost uniform update * ``` */ declare function createLightEffect(config: LightEffectConfig): LightEffectClass; //#endregion export { type ChannelName, type EffectConstants, type EffectField, type EffectSchema, type EffectSchemaValue, type EffectValues, LightEffect, LightEffectBuildContext, LightEffectClass, LightEffectRuntimeContext, type UniformKeys, type WithRequiredChannels, createLightEffect }; //# sourceMappingURL=LightEffect.d.ts.map