simvx.core.planar_reflection

Planar reflections: the main scene mirrored across a plane (design RM-B4).

Built on :class:~simvx.core.RenderView: a :class:PlanarReflection3D node renders the MAIN scene through a camera mirrored across its own local XZ plane (the node’s +Y axis is the plane normal) into an offscreen texture. A floor, mirror or water Material samples that texture with a projective UV derived from the fragment’s clip position, so pass the node straight to Material(albedo_map=planar_reflection) and the surface shows the mirrored world.

Two building blocks, usable separately:

  • func:

    planar_reflection_camera: reflect any Camera3D across a world plane, returning a mirrored camera whose projection carries an oblique near-clip plane (Lengyel) so nothing behind the reflection plane is drawn.

  • class:

    PlanarReflection3D: the node that owns a mirrored camera, keeps it in sync with the active main camera every frame (the render backends call

    meth:

    ~PlanarReflection3D.sync_reflection while preparing offscreen targets), and publishes the reflection texture via RenderView.texture.

The mirrored camera is built as a PROPER rotation (a reflected orthonormal basis has determinant -1, which a Node3D cannot represent): reflecting the camera’s forward and up vectors and rebuilding the basis flips the camera-space X axis relative to the true mirror image. The material shaders compensate by mirroring the horizontal projective UV, so the composition is exact for points on the reflection plane, which is precisely the surface being shaded.

Pipelined render mode defers RenderView capture entirely (probe-capture precedent), so a PlanarReflection3D freezes there like any RenderView.

Module Contents

Classes

PlanarReflection3D

Mirrors the main scene across this node’s local XZ plane into a texture.

Functions

planar_reflection_camera

Return a Camera3D mirroring main_camera across a world plane.

Data

API

simvx.core.planar_reflection.__all__

[‘PlanarReflection3D’, ‘planar_reflection_camera’]

simvx.core.planar_reflection.planar_reflection_camera(plane: tuple[float, float, float, float], main_camera: simvx.core.nodes_3d.camera.Camera3D, *, into: simvx.core.nodes_3d.camera.Camera3D | None = None) simvx.core.nodes_3d.camera.Camera3D[source]

Return a Camera3D mirroring main_camera across a world plane.

The result views the scene from the far side of the plane (position, forward and up reflected) with main_camera’s fov/near/far, and carries clip_plane = plane so its projection obliquely clips everything behind the reflection plane (:func:~simvx.core.math.oblique_near_plane).

Args: plane: World-space plane (nx, ny, nz, d), equation n . x + d = 0 with unit n; the kept side is n . x + d > 0. main_camera: The camera to mirror (typically the scene’s active one). into: Optional existing camera to update in place (per-frame reuse without allocation). A new standalone Camera3D when None.

Returns: The mirrored camera (into when given). Its basis is a proper rotation: the rendered image equals the true mirror image flipped horizontally, which the planar-reflection material samples away by mirroring the horizontal projective UV.

class simvx.core.planar_reflection.PlanarReflection3D(name='PlanarReflection3D', **kwargs)[source]

Bases: simvx.core.render_view.RenderView

Mirrors the main scene across this node’s local XZ plane into a texture.

Place the node ON the reflective surface: its world position is a point on the reflection plane and its world +Y axis is the plane normal. Every frame the backends re-mirror the scene’s active camera across that plane (with an oblique near clip, so nothing below the surface leaks into the reflection) and render the main tree from it, sized to the main viewport times resolution_scale so the projective UV stays aligned at any window size.

Sample the reflection by passing the node as a Material albedo::

water = MeshInstance3D(
    mesh=Mesh.plane(...),
    material=Material(albedo_map=reflection, unlit=True),
)

A Material whose albedo_map is a PlanarReflection3D samples the texture with a mirrored projective UV from the fragment’s clip position (not the mesh UV), which is exact for fragments lying on the reflection plane.

Properties: resolution_scale: Reflection resolution as a fraction of the main viewport (0.05 to 1.0). Half resolution by default: reflections tolerate softness well and the pass renders the whole scene again. clip_bias: World-space offset of the oblique clip plane along the normal. Keeps the reflective surface itself (and z-fighting slivers on it) out of its own reflection.

RenderView.camera is unused (the mirrored camera is internal and derived); size is driven from resolution_scale each frame.

Initialization

resolution_scale

‘Property(…)’

clip_bias

‘Property(…)’

resolve_camera() simvx.core.nodes_3d.camera.Camera3D | None[source]

The internal mirrored camera; None until first synced.

world_plane() tuple[float, float, float, float][source]

This node’s reflection plane in world space, (nx, ny, nz, d).

The plane passes through the node’s world position with the node’s world +Y axis as its (unit) normal; n . x + d = 0.

sync_reflection(main_viewport_size: tuple[float, float]) bool[source]

Re-mirror the active main camera across this node’s plane.

Called by the render backends while preparing offscreen targets (after all node updates, before any offscreen render). Resizes the target to main_viewport_size * resolution_scale (the projective UV requires the reflection aspect to match the main viewport) and rebuilds the internal mirrored camera. Returns False (view skipped, one-time warning in the manager) when no main camera exists.

camera

‘NodePath(…)’

size

‘Property(…)’

render_target_update_mode

‘Property(…)’

use_occlusion

‘Property(…)’

property texture: int
property texture_size: tuple[int, int]
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

script_error_raised

‘Signal(…)’

dynamic: bool

False

classmethod __init_subclass__(**kwargs)
property name: str
property update_mode: simvx.core.descriptors.UpdateMode
property visible: bool
reset_error() None
add_child(node: simvx.core.node.Node) simvx.core.node.Node
remove_child(node: simvx.core.node.Node)
reparent(new_parent: simvx.core.node.Node)
get_node(path: str) simvx.core.node.Node
get_node_or_none(path: str) simvx.core.node.Node | None
find(target, *, direct: bool = False)
find_all(target, *, direct: bool = False)
walk(*, include_self: bool = True) collections.abc.Iterator[simvx.core.node.Node]
property path: str
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()
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__()