simvx.graphics.renderer.caster_slots¶
Which point and spot lights get an atlas row, and which row.
Backend-neutral: this is numpy over the shared LIGHT_DTYPE array both
renderers build, with no GPU call in it, so the Vulkan shadow pass and the
browser runtime apply one budget by one rule. It lives apart from
- mod:
.shadow_rendererbecause that module importsvulkanat module level and the web bundle refuses such a module outright.
Module Contents¶
Classes¶
Which lights held which atlas rows at the previous selection, per kind. |
Functions¶
Give each shadow-casting point/spot light a caster slot, in |
Data¶
API¶
- simvx.graphics.renderer.caster_slots.__all__¶
[‘CASTER_HYSTERESIS’, ‘CasterRows’, ‘assign_caster_slots’]
- simvx.graphics.renderer.caster_slots.CASTER_HYSTERESIS¶
0.1
- class simvx.graphics.renderer.caster_slots.CasterRows¶
Which lights held which atlas rows at the previous selection, per kind.
Each array lists winning light indices in atlas-row order, so
points[2]is the light that owned point caster row 2. Handing the same instance back to :func:assign_caster_slotsevery frame is what makes selection sticky: an incumbent is only unseated by a clearly stronger light (:data:CASTER_HYSTERESIS), and one that survives keeps the row it had – whether or not the set was over budget – so a light joining or leaving it does not move the rows of the lights around it. A budget that shrinks past a row re-seats its holder, who then queues like a newcomer. Without a carrier every call decides from scratch and rows follow array order.Lights are identified by their index in the array the caller built, which is the only identity a light has here; anything that changes the array – adding or removing a light, hiding one, or editing a cull mask – shifts the later indices and forfeits one frame of stickiness.
- points: numpy.ndarray | None¶
None
- spots: numpy.ndarray | None¶
None
- simvx.graphics.renderer.caster_slots.assign_caster_slots(lights: numpy.ndarray | None, budget: int, camera_view: numpy.ndarray | None = None, rows: simvx.graphics.renderer.caster_slots.CasterRows | None = None) tuple[int, int, int, int]¶
Give each shadow-casting point/spot light a caster slot, in
params.w.For a positional light
params.wreads:0asked for no shadow,slot + 1owns that atlas row, and-1asked for one but did not fit the budget. The forward shader darkens a light with its OWN row and with no other, and the demoted marker keeps the request visible, so raising the budget later re-slots the same light. Directional lights are left alone: theirparams.wis the CSM opt-in flag, and there is one CSM.A light whose range (or, for a spot, outer cone) is zero lights nothing and is not a candidate. When more lights of a kind qualify than
budgetcan hold, the slots go to the highestshadow_priority(carried indirection.w), then to the lights with the strongest influence at the camera camera_view describes –intensity * range / distance, so a lamp is judged by what it lights and not only by how close it is. A light already holding a row in rows has its influence multiplied by1 +- Data:
CASTER_HYSTERESISfirst, so it is the sitting incumbent and not array order that takes a tie – unless the incumbent is worth nothing at all, which scaling cannot lift. Array order, the order the caller built the light array in, decides only what priority and influence leave equal. Without a camera there is no influence to measure and every candidate weighs the same, which leaves those same three keys: priority, then the incumbent, then array order. Returns(point_casters, spot_casters, point_candidates, spot_candidates)so the caller can report a budget that could not be honoured.
Pass the same :class:
CasterRowsas rows every frame to make selection sticky: it is read for the previous winners and rewritten with the new ones. Without it each call decides afresh, which is right for a one-shot caller and wrong for a render loop, where it would churn the caster set and the atlas rows as the camera drifts.Written against whole columns rather than per light: within budget the column form costs a flat ~12 us, and a few microseconds more on the one frame the caster set changes and the rows are re-seated; once the ranking runs it grows slowly with the candidate count (~31 us at 8 candidates, ~57 us at 512, measured), where the per-light loop passes it at about six candidates and is some forty times slower at 512. The adapter rebuilds the light array and calls
set_lightsunconditionally, so a light-heavy scene pays this every frame; the ranking above is skipped entirely while the scene fits its budget, which is what a one-light scene always does.