import { Light2D } from "./Light2D.js"; import { DataTexture, Vector2, Vector3 } from "three"; import Node from "three/src/nodes/core/Node.js"; //#region src/lights/ForwardPlusLighting.d.ts /** * Screen-space tile edge in pixels. * * 16px gives finer-grained coverage of small fill lights (slime glows * ~40 world units) so the tile's closest-point score stays * representative of what the fragment would see at the tile center. * 32px showed visible blockiness on small fill lights because the * tile AABB could contain the light while its effective reach * barely extends to the tile's opposite corner — the scoring would * keep the light on the light's own side of the tile but evict it * by the far side, producing grid-aligned intensity falloff. * * CPU cost at 16px doubles vs 32px (more tiles), but dedup + the * reservoir path keep per-tile work bounded, and the per-fragment * shader loop is unchanged either way (MAX_LIGHTS_PER_TILE caps it). */ declare const TILE_SIZE = 16; /** * Max lights any single tile can hold. Each tile dedicates * `MAX_LIGHTS_PER_TILE / 4 = 4` RGBA texels to light indices. Caps * the per-fragment shader loop iteration count — the inner loop * breaks early on the empty-slot sentinel, so tiles with fewer lights * pay only for what they hold. * * Raising this increases tile-texture footprint and the saturated- * tile shader worst case. Lowering it makes reservoir eviction kick * in sooner in dense scenes. */ declare const MAX_LIGHTS_PER_TILE = 16; /** * Default per-tile fill-light quota (sprites with `castsShadow: false`, * e.g. slime glows, atmospheric ambience). Fill lights saturating a * tile get deduplicated to the top-K by score within their category * bucket. Keeps 1000-slime scenes from drowning hero lights (torches) * in tile competition while preserving the "lots of soft fill" * visual read. * * Per-category overrides via `ForwardPlusLighting.setFillQuota(category, * n)`. This constant is the value all 4 buckets initialize to. */ declare const MAX_FILL_LIGHTS_PER_TILE = 2; /** * Number of fill-light categories with independent per-tile quotas. * Category 0 is the default for lights without a `category` string * set. */ declare const FILL_CATEGORY_COUNT = 4; /** * Light-index blocks per tile — each block is one RGBA texel holding * 4 light indices. With MAX_LIGHTS_PER_TILE = 16 this is 4 blocks. */ declare const BLOCKS_PER_TILE: number; /** * Total RGBA texels consumed per tile. Sized to align each tile to a * 128-byte cache line on every target GPU class (mobile, desktop, * console) — stride 8 × 16 bytes/RGBA32F = 128 bytes. The light-index * blocks occupy the first {@link BLOCKS_PER_TILE} texels; the * remaining texels are reserved for future per-tile scalars without * needing another stride refactor. */ declare const TILE_STRIDE = 8; /** * Fixed side-length of the tile-index DataTexture. Allocated once at * construction and NEVER resized — the previous tall-narrow layout * (`width = blocksPerTile`, `height = tileCount`) hit WebGPU's 8192 * 2D-texture dimension limit at fullscreen (e.g. 1440p / 16-px tiles * ≈ 10k–14k rows), causing the GPU texture to allocate at a clipped * size and the shader to read zeros — visible as "lights disappear on * fullscreen, only ambient survives." * * 512 × 512 × RGBA32F = 1 MB GPU + CPU. Capacity is * `512² / TILE_STRIDE = 32,768` tiles — covers up to 4K CSS canvas * (3840×2160 → 32,400 tiles, 99% utilization) at TILE_SIZE=16. * Beyond 4K, bump TILE_SIZE to 32 or TILE_TEXTURE_DIM to 1024. */ declare const TILE_TEXTURE_DIM = 512; /** * Forward+ tiled light culler for 2D scenes. * * Subdivides the screen into {@link TILE_SIZE}-pixel tiles and assigns * each light to every tile its bounding box overlaps. Per-fragment * shading then iterates only the lights bound to its tile (capped at * {@link MAX_LIGHTS_PER_TILE}) instead of every light in the scene — * the cost flips from O(total_lights) to O(lights_per_tile). * * Tile data is uploaded each frame via a single {@link DataTexture} * (light indices + per-tile meta scalars). Sprite-friendly extras: * * - **Reservoir eviction** — when a tile saturates, lower-scored lights * lose slots to higher-scored newcomers, keeping the visible set * close to the "would-be-brightest-K" ideal. * - **Per-category fill quotas** — `castsShadow: false` lights compete * only within their own {@link Light2D.category} bucket, so a * thousand cosmetic slime glows can't drown out hero torches. Tunable * per-bucket via {@link setFillQuota} / {@link getFillQuota} / * {@link resetFillQuotas}. * - **Importance bias** — {@link Light2D.importance} multiplies the * tile-rank score, letting hero lights resist eviction. * * Owned by {@link DefaultLightEffect} (and other tiled-Forward+ effects); * end users rarely instantiate this directly. */ declare class ForwardPlusLighting { private _tileCountX; private _tileCountY; private _tileCount; private _tileData; private _tileTexture; /** * Per-tile light count (`length = _tileCount`). Promoted to a * persistent member so (a) no per-frame allocation, and (b) the * devtools registry can hold a stable reference to visualise which * tiles are saturated / empty. */ private _lightCounts; /** * Per-slot reservoir scores (`length = _tileCount * MAX_LIGHTS_PER_TILE`). * Same rationale — also exposed for debugging (per-tile quality). */ private _tileScores; /** * Per-slot category marker: * * -1 hero slot (`castsShadow: true` light — never evicted by fills) * 0..3 fill slot holding a light with the given category bucket * * Lets the reservoir-eviction path compete each light against its * own category (fills in bucket 0 only displace other bucket-0 * fills; buckets 1/2/3 likewise; heroes compete only among heroes). * * Length = `_tileCount * MAX_LIGHTS_PER_TILE`. `Int8Array` because * signed (need -1 for the hero marker). */ private _tileSlotCategory; /** * Per-tile per-category count of fill-light slots currently * claimed. Bounded by `MAX_FILL_LIGHTS_PER_TILE` per bucket. * Length = `_tileCount * FILL_CATEGORY_COUNT`. */ private _tileFillCount; /** * Per-bucket quota for fill lights — each category's max * concurrent slots in any single tile. Initialized to * `MAX_FILL_LIGHTS_PER_TILE` (2) for all four buckets. Tunable at * runtime via {@link setFillQuota}; readable via * {@link getFillQuota}. * * Range: `[0, MAX_LIGHTS_PER_TILE]`. Setting to 0 disables that * bucket entirely (no fills in that category claim slots). */ private _fillQuotas; private _screenSize; private _worldSize; private _worldOffset; readonly tileCountXNode: import("three/webgpu").UniformNode<"int", number>; readonly screenSizeNode: import("three/webgpu").UniformNode<"vec2", Vector2>; readonly worldSizeNode: import("three/webgpu").UniformNode<"vec2", Vector2>; readonly worldOffsetNode: import("three/webgpu").UniformNode<"vec2", Vector2>; readonly ambientNode: import("three/webgpu").UniformNode<"vec3", Vector3>; constructor(); get tileTexture(): DataTexture; get tileCountX(): number; init(screenWidth: number, screenHeight: number): void; resize(screenWidth: number, screenHeight: number): void; setWorldBounds(worldSize: Vector2, worldOffset: Vector2): void; /** * Set the per-tile quota for a fill-light category. Lights in this * bucket will be capped at `quota` concurrent slots per tile; the * `(quota + 1)`-th and later in-range fills lose to score-based * eviction (or get rejected if the tile is otherwise full). * * Raising a quota lets more fills of that type contribute exact * lighting in dense clusters at the cost of more shader-loop * iterations per fragment in saturated tiles. Lowering tightens * the cap (cheaper, dimmer dense areas). Setting to 0 disables * fills in that bucket entirely. * * @param category - Either a category string (hashed via the same * djb2 used by `Light2D.category`) or a raw bucket index 0..3. * @param quota - Slots per tile, clamped to `[0, MAX_LIGHTS_PER_TILE]`. * * @example * ```typescript * forwardPlus.setFillQuota('slime', 4) // up to 4 slime fills per tile * forwardPlus.setFillQuota('water', 0) // disable water fills entirely * ``` */ setFillQuota(category: string | number, quota: number): void; /** * Reset every per-bucket fill quota back to the default * (`MAX_FILL_LIGHTS_PER_TILE`). Used by the declarative * `categoryQuotas` prop on light effects so that removing a key * from the prop record actually clears that bucket's override * (otherwise stale per-bucket values would linger across * re-renders). */ resetFillQuotas(): void; /** * Read the per-tile quota currently configured for a fill-light * category. Mirrors {@link setFillQuota} — accepts either a string * (hashed) or a raw bucket index. */ getFillQuota(category: string | number): number; update(lights: Light2D[], maxLights?: number): void; createTileLookup(): (tileIndex: Node<"int">, slotIndex: Node<"int">) => import("three/webgpu").VarNode<"int", import("three/webgpu").ConvertNode<"int">>; dispose(): void; } //#endregion export { BLOCKS_PER_TILE, FILL_CATEGORY_COUNT, ForwardPlusLighting, MAX_FILL_LIGHTS_PER_TILE, MAX_LIGHTS_PER_TILE, TILE_SIZE, TILE_STRIDE, TILE_TEXTURE_DIM }; //# sourceMappingURL=ForwardPlusLighting.d.ts.map