simvx.core.nodes_3d.lights¶

Light3D, DirectionalLight3D, PointLight3D, SpotLight3D – 3D light nodes.

Module Contents¶

Classes¶

Light3D

Base class for the 3D light nodes.

DirectionalLight3D

Infinitely distant light: a sun. Every fragment is lit from one direction.

PointLight3D

Omnidirectional light at the node’s world position, with a hard range.

SpotLight3D

Cone-shaped light aimed along the node’s forward vector.

API¶

class simvx.core.nodes_3d.lights.Light3D(**kwargs)[source]¶

Bases: simvx.core.nodes_3d.node3d.Node3D

Base class for the 3D light nodes.

Holds the properties every light shares: colour, intensity, shadows and light_cull_mask. Add one of the concrete subclasses (:class:DirectionalLight3D, :class:PointLight3D, :class:SpotLight3D) to a scene rather than this class.

The renderer multiplies colour by intensity to get the light’s radiance and feeds that into the Cook-Torrance BRDF, so intensity is a plain linear multiplier: it is not lumens, watts or any photometric unit, and 2.0 is exactly twice as bright as 1.0. Values stay linear all the way to the tonemapper, so overdriving a light past 1.0 is normal and produces bloom rather than clipping.

A light only lights a mesh when light_cull_mask and the mesh’s render layer share a bit, and only when the active camera’s cull mask also overlaps light_cull_mask: use this to keep, say, a UI-only rim light off the world geometry.

Initialization

colour¶

‘Colour(…)’

intensity¶

‘Property(…)’

shadows¶

‘Property(…)’

light_cull_mask¶

‘Bitmask(…)’

set_light_cull_mask_layer(index: int, enabled: bool = True) None[source]¶

Enable or disable a specific light cull mask layer (0-31).

is_light_cull_mask_layer_enabled(index: int) bool[source]¶

Check if a specific light cull mask layer is enabled (0-31).

position¶

‘_SpatialVecProperty(…)’

rotation¶

‘Property(…)’

scale¶

‘_SpatialVecProperty(…)’

render_layer¶

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3¶
property world_position: simvx.core.math.types.Vec3¶
property world_rotation: simvx.core.math.types.Quat¶
property world_scale: simvx.core.math.types.Vec3¶
property forward: simvx.core.math.types.Vec3¶
property right: simvx.core.math.types.Vec3¶
property up: simvx.core.math.types.Vec3¶
translate(offset: tuple[float, float, float] | numpy.ndarray)¶
translate_global(offset: tuple[float, float, float] | numpy.ndarray)¶
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)¶
rotate_x(angle: float)¶
rotate_y(angle: float)¶
rotate_z(angle: float)¶
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)¶
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None¶
set_render_layer(index: int, enabled: bool = True) None¶
is_on_render_layer(index: int) bool¶
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)¶
strict_errors: ClassVar[bool]¶

True

dev_checks: ClassVar[bool]¶

None

script_error_raised¶

‘Signal(…)’

dynamic: bool¶

False

visible¶

‘Property(…)’

update_mode¶

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]¶

None

classmethod __init_subclass__(**kwargs)¶
property name: str¶
property visible_in_tree: bool¶
reset_error() None¶
add_child(node: simvx.core.node.T) simvx.core.node.T¶
remove_child(node: simvx.core.node.Node) None¶
reparent(new_parent: simvx.core.node.Node)¶
node_at(path, default=_NO_DEFAULT)¶
find(target, *, direct: bool = False)¶
find_all(target, *, direct: bool = False)¶
expect(target, *, direct: bool = False)¶
ancestor(target)¶
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]¶
property path: str¶
property is_scene_root: bool¶
add_to_group(group: str)¶
remove_from_group(group: str)¶
is_in_group(group: str) bool¶
on_ready() None¶
on_enter_tree() None¶
on_exit_tree() None¶
on_update(dt: float) None¶
on_fixed_update(dt: float) None¶
on_draw(renderer) None¶
on_picked(event: simvx.core.events.InputEvent) None¶
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None¶
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle¶
stop_coroutine(gen_or_handle)¶
queue_redraw() None¶
property render_dirty: bool¶
clear_children()¶
destroy()¶
property destroying: bool¶
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None¶
property app¶
property tree: simvx.core.scene_tree.SceneTree¶
property physics¶
property physics_2d¶
__getitem__(key: str)¶
classmethod get_properties() dict[str, simvx.core.descriptors.Property]¶
__repr__()¶
class simvx.core.nodes_3d.lights.DirectionalLight3D(**kwargs)[source]¶

Bases: simvx.core.nodes_3d.lights.Light3D

Infinitely distant light: a sun. Every fragment is lit from one direction.

The light has no position and no falloff: position only moves the editor gizmo. The direction is the node’s forward vector, so aim it either by rotating the node or by assigning :attr:direction (which rotates the node for you). range does not apply.

With shadows=True the light drives the cascaded shadow maps; the cascade count is set globally by WorldEnvironment.shadow_cascade_count. A WorldEnvironment with sky_mode="procedural" also treats the first DirectionalLight3D in the scene as the sun that generates the sky.

Initialization

gizmo_colour¶

‘Colour(…)’

direction¶

‘_DirectionProperty(…)’

get_gizmo_lines() list[tuple[simvx.core.math.types.Vec3, simvx.core.math.types.Vec3]][source]¶

Return direction arrow and parallel rays showing light direction.

colour¶

‘Colour(…)’

intensity¶

‘Property(…)’

shadows¶

‘Property(…)’

light_cull_mask¶

‘Bitmask(…)’

set_light_cull_mask_layer(index: int, enabled: bool = True) None¶
is_light_cull_mask_layer_enabled(index: int) bool¶
position¶

‘_SpatialVecProperty(…)’

rotation¶

‘Property(…)’

scale¶

‘_SpatialVecProperty(…)’

render_layer¶

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3¶
property world_position: simvx.core.math.types.Vec3¶
property world_rotation: simvx.core.math.types.Quat¶
property world_scale: simvx.core.math.types.Vec3¶
property forward: simvx.core.math.types.Vec3¶
property right: simvx.core.math.types.Vec3¶
property up: simvx.core.math.types.Vec3¶
translate(offset: tuple[float, float, float] | numpy.ndarray)¶
translate_global(offset: tuple[float, float, float] | numpy.ndarray)¶
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)¶
rotate_x(angle: float)¶
rotate_y(angle: float)¶
rotate_z(angle: float)¶
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)¶
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None¶
set_render_layer(index: int, enabled: bool = True) None¶
is_on_render_layer(index: int) bool¶
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)¶
strict_errors: ClassVar[bool]¶

True

dev_checks: ClassVar[bool]¶

None

script_error_raised¶

‘Signal(…)’

dynamic: bool¶

False

visible¶

‘Property(…)’

update_mode¶

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]¶

None

classmethod __init_subclass__(**kwargs)¶
property name: str¶
property visible_in_tree: bool¶
reset_error() None¶
add_child(node: simvx.core.node.T) simvx.core.node.T¶
remove_child(node: simvx.core.node.Node) None¶
reparent(new_parent: simvx.core.node.Node)¶
node_at(path, default=_NO_DEFAULT)¶
find(target, *, direct: bool = False)¶
find_all(target, *, direct: bool = False)¶
expect(target, *, direct: bool = False)¶
ancestor(target)¶
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]¶
property path: str¶
property is_scene_root: bool¶
add_to_group(group: str)¶
remove_from_group(group: str)¶
is_in_group(group: str) bool¶
on_ready() None¶
on_enter_tree() None¶
on_exit_tree() None¶
on_update(dt: float) None¶
on_fixed_update(dt: float) None¶
on_draw(renderer) None¶
on_picked(event: simvx.core.events.InputEvent) None¶
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None¶
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle¶
stop_coroutine(gen_or_handle)¶
queue_redraw() None¶
property render_dirty: bool¶
clear_children()¶
destroy()¶
property destroying: bool¶
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None¶
property app¶
property tree: simvx.core.scene_tree.SceneTree¶
property physics¶
property physics_2d¶
__getitem__(key: str)¶
classmethod get_properties() dict[str, simvx.core.descriptors.Property]¶
__repr__()¶
class simvx.core.nodes_3d.lights.PointLight3D(**kwargs)[source]¶

Bases: simvx.core.nodes_3d.lights.Light3D

Omnidirectional light at the node’s world position, with a hard range.

Attenuation is the windowed linear falloff max(1 - distance/range, 0) squared, so brightness reaches exactly zero at range and the light contributes nothing beyond it. That cutoff is what lets the renderer cull the light per tile, so keep range as tight as the look allows rather than using one huge light. It is not inverse-square: doubling range stretches the falloff instead of dimming the light, and intensity is the only brightness dial.

With shadows=True the light casts a cubemap shadow. The Vulkan backend renders WorldEnvironment.shadow_caster_count point-light shadow cubes per frame (default 1); when more point lights ask than the budget holds, the highest :attr:shadow_priority wins, then the strongest influence at the camera (intensity * range / distance, so reach counts as much as closeness), and a light already casting keeps its map unless a challenger clearly beats it. Lights past that budget still light the scene, cast nothing, and are reported once. Each caster re-renders every shadow-casting mesh six times per frame, so raise the budget deliberately rather than lighting every lamp.

Initialization

range¶

‘Property(…)’

shadow_priority¶

‘Property(…)’

gizmo_colour¶

‘Colour(…)’

get_gizmo_lines() list[tuple[simvx.core.math.types.Vec3, simvx.core.math.types.Vec3]][source]¶

Return 3 circles showing the light range sphere.

colour¶

‘Colour(…)’

intensity¶

‘Property(…)’

shadows¶

‘Property(…)’

light_cull_mask¶

‘Bitmask(…)’

set_light_cull_mask_layer(index: int, enabled: bool = True) None¶
is_light_cull_mask_layer_enabled(index: int) bool¶
position¶

‘_SpatialVecProperty(…)’

rotation¶

‘Property(…)’

scale¶

‘_SpatialVecProperty(…)’

render_layer¶

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3¶
property world_position: simvx.core.math.types.Vec3¶
property world_rotation: simvx.core.math.types.Quat¶
property world_scale: simvx.core.math.types.Vec3¶
property forward: simvx.core.math.types.Vec3¶
property right: simvx.core.math.types.Vec3¶
property up: simvx.core.math.types.Vec3¶
translate(offset: tuple[float, float, float] | numpy.ndarray)¶
translate_global(offset: tuple[float, float, float] | numpy.ndarray)¶
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)¶
rotate_x(angle: float)¶
rotate_y(angle: float)¶
rotate_z(angle: float)¶
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)¶
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None¶
set_render_layer(index: int, enabled: bool = True) None¶
is_on_render_layer(index: int) bool¶
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)¶
strict_errors: ClassVar[bool]¶

True

dev_checks: ClassVar[bool]¶

None

script_error_raised¶

‘Signal(…)’

dynamic: bool¶

False

visible¶

‘Property(…)’

update_mode¶

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]¶

None

classmethod __init_subclass__(**kwargs)¶
property name: str¶
property visible_in_tree: bool¶
reset_error() None¶
add_child(node: simvx.core.node.T) simvx.core.node.T¶
remove_child(node: simvx.core.node.Node) None¶
reparent(new_parent: simvx.core.node.Node)¶
node_at(path, default=_NO_DEFAULT)¶
find(target, *, direct: bool = False)¶
find_all(target, *, direct: bool = False)¶
expect(target, *, direct: bool = False)¶
ancestor(target)¶
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]¶
property path: str¶
property is_scene_root: bool¶
add_to_group(group: str)¶
remove_from_group(group: str)¶
is_in_group(group: str) bool¶
on_ready() None¶
on_enter_tree() None¶
on_exit_tree() None¶
on_update(dt: float) None¶
on_fixed_update(dt: float) None¶
on_draw(renderer) None¶
on_picked(event: simvx.core.events.InputEvent) None¶
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None¶
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle¶
stop_coroutine(gen_or_handle)¶
queue_redraw() None¶
property render_dirty: bool¶
clear_children()¶
destroy()¶
property destroying: bool¶
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None¶
property app¶
property tree: simvx.core.scene_tree.SceneTree¶
property physics¶
property physics_2d¶
__getitem__(key: str)¶
classmethod get_properties() dict[str, simvx.core.descriptors.Property]¶
__repr__()¶
class simvx.core.nodes_3d.lights.SpotLight3D(**kwargs)[source]¶

Bases: simvx.core.nodes_3d.lights.Light3D

Cone-shaped light aimed along the node’s forward vector.

Combines the same distance falloff as :class:PointLight3D with a cone mask. Both cone angles are HALF-angles in degrees, measured from the cone axis to its edge, so the default outer_cone=45 is a 90-degree-wide beam. Fragments inside inner_cone get the full beam, fragments between inner_cone and outer_cone fade out linearly in cosine space, and fragments past outer_cone get nothing. Keep inner_cone below outer_cone: the gap between them is the soft edge, and narrowing it towards zero sharpens the beam.

Aim the light by rotating the node (the beam follows forward); the position is the cone apex.

With shadows=True the light casts a projected shadow map. The Vulkan backend renders WorldEnvironment.shadow_caster_count spot shadows per frame (default 1); when more spot lights ask than the budget holds, the highest :attr:shadow_priority wins, then the strongest influence at the camera (intensity * range / distance, so reach counts as much as closeness), and a light already casting keeps its map unless a challenger clearly beats it. Lights past that budget still light the scene and cast nothing. The shadow frustum is 2 * outer_cone wide, so a cone approaching 90 degrees loses shadow precision.

Initialization

range¶

‘Property(…)’

inner_cone¶

‘Property(…)’

outer_cone¶

‘Property(…)’

shadow_priority¶

‘Property(…)’

gizmo_colour¶

‘Colour(…)’

get_gizmo_lines() list[tuple[simvx.core.math.types.Vec3, simvx.core.math.types.Vec3]][source]¶

Return cone wireframe showing spot light direction and angle.

colour¶

‘Colour(…)’

intensity¶

‘Property(…)’

shadows¶

‘Property(…)’

light_cull_mask¶

‘Bitmask(…)’

set_light_cull_mask_layer(index: int, enabled: bool = True) None¶
is_light_cull_mask_layer_enabled(index: int) bool¶
position¶

‘_SpatialVecProperty(…)’

rotation¶

‘Property(…)’

scale¶

‘_SpatialVecProperty(…)’

render_layer¶

‘Property(…)’

property rotation_degrees: simvx.core.math.types.Vec3¶
property world_position: simvx.core.math.types.Vec3¶
property world_rotation: simvx.core.math.types.Quat¶
property world_scale: simvx.core.math.types.Vec3¶
property forward: simvx.core.math.types.Vec3¶
property right: simvx.core.math.types.Vec3¶
property up: simvx.core.math.types.Vec3¶
translate(offset: tuple[float, float, float] | numpy.ndarray)¶
translate_global(offset: tuple[float, float, float] | numpy.ndarray)¶
rotate(axis: tuple[float, float, float] | numpy.ndarray, angle: float)¶
rotate_x(angle: float)¶
rotate_y(angle: float)¶
rotate_z(angle: float)¶
look_at(target: tuple[float, float, float] | numpy.ndarray, up=None)¶
face_along(forward: tuple[float, float, float] | numpy.ndarray, up: tuple[float, float, float] | numpy.ndarray | None = None) None¶
set_render_layer(index: int, enabled: bool = True) None¶
is_on_render_layer(index: int) bool¶
wrap_bounds(bounds: tuple[float, float, float] | numpy.ndarray, margin: float = 1.0)¶
strict_errors: ClassVar[bool]¶

True

dev_checks: ClassVar[bool]¶

None

script_error_raised¶

‘Signal(…)’

dynamic: bool¶

False

visible¶

‘Property(…)’

update_mode¶

‘Property(…)’

__properties__: ClassVar[dict[str, simvx.core.descriptors.Property]]¶

None

classmethod __init_subclass__(**kwargs)¶
property name: str¶
property visible_in_tree: bool¶
reset_error() None¶
add_child(node: simvx.core.node.T) simvx.core.node.T¶
remove_child(node: simvx.core.node.Node) None¶
reparent(new_parent: simvx.core.node.Node)¶
node_at(path, default=_NO_DEFAULT)¶
find(target, *, direct: bool = False)¶
find_all(target, *, direct: bool = False)¶
expect(target, *, direct: bool = False)¶
ancestor(target)¶
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]¶
property path: str¶
property is_scene_root: bool¶
add_to_group(group: str)¶
remove_from_group(group: str)¶
is_in_group(group: str) bool¶
on_ready() None¶
on_enter_tree() None¶
on_exit_tree() None¶
on_update(dt: float) None¶
on_fixed_update(dt: float) None¶
on_draw(renderer) None¶
on_picked(event: simvx.core.events.InputEvent) None¶
on_unhandled_input(event: simvx.core.events.TreeInputEvent) None¶
start_coroutine(gen: simvx.core.descriptors.Coroutine) simvx.core.descriptors.CoroutineHandle¶
stop_coroutine(gen_or_handle)¶
queue_redraw() None¶
property render_dirty: bool¶
clear_children()¶
destroy()¶
property destroying: bool¶
call_deferred(method: collections.abc.Callable[..., Any], *args: Any) None¶
property app¶
property tree: simvx.core.scene_tree.SceneTree¶
property physics¶
property physics_2d¶
__getitem__(key: str)¶
classmethod get_properties() dict[str, simvx.core.descriptors.Property]¶
__repr__()¶