Source code for simvx.core.render_view
"""RenderView: render the MAIN scene from a second camera into a texture.
Where :class:`~simvx.core.SubViewport` renders its *own subtree* with its own
camera, a :class:`RenderView` renders the **main scene tree** through any
:class:`~simvx.core.Camera3D` placed in it (design D6, render_modernization.md).
The rendered colour buffer is published as a bindless texture index via
:attr:`RenderView.texture`, sampled exactly like a SubViewport feed: pass the
node as ``Material(albedo_map=render_view)`` for a security-monitor slab, or
read ``render_view.texture`` directly.
The render-to-texture integration is driven from the graphics backend
(``simvx.graphics.renderer.render_view.RenderViewManager``); core stays
rendering-agnostic. RenderViews are scheduled with the SubViewports by the
:class:`~simvx.core.scene_target_graph.SceneTargetGraph`, so a RenderView that
samples another offscreen target (or feeds one) renders in producer-first
order within the same frame; genuine cycles degrade to a one-frame lag on the
broken edge, never an error.
Pipelined render mode (``render_mode="pipelined"``) defers RenderView capture
exactly like reflection-probe capture: the texture slot stays valid but never
updates, and the backend logs a one-time warning.
"""
import logging
from .descriptors import Property
from .nodes_3d.camera import Camera3D
from .nodes_3d.node3d import Node3D
from .properties import NodePath
log = logging.getLogger(__name__)
__all__ = ["RenderView"]
[docs]
class RenderView(Node3D):
"""Renders the main scene from ``camera`` into an offscreen texture.
Unlike ``SubViewport`` this node has no offscreen subtree: its children (if
any) are ordinary main-scene content, and the view it captures is the main
world itself, seen from the ``Camera3D`` that ``camera`` points at. The
referenced camera does not need to be (and normally is not) the scene's
active camera.
Properties:
camera: Node path to the ``Camera3D`` to render from, resolved relative
to this node (e.g. ``"../SecurityCam"``). Empty or unresolvable
paths skip rendering with a one-time warning.
size: Offscreen target dimensions in pixels.
render_target_update_mode: ``"always"`` (default, render every frame),
``"when_visible"`` (render only while this node is visible in the
hierarchy), ``"once"`` (render a single frame then freeze), or
``"disabled"`` (never render; the slot stays valid but stale).
Same name as ``SubViewport``'s property for the same dial (the
plain ``update_mode`` of design D6 is taken: every ``Node`` carries
the pause-processing ``update_mode``).
use_occlusion: Run the two-phase Hi-Z occlusion cull for this view's
own camera (desktop renderer; the web Hi-Z is single-phase and has
no per-view hook yet). Off by default: an offscreen view is
usually small enough that the occlusion pass costs more than it
saves. In the pipelined render mode RenderView capture (and with
it this cull) is deferred entirely, like probe capture.
Note:
Avoid pointing ``camera`` at a surface that displays this RenderView's
own texture: sampling an image while rendering into it is a feedback
loop the GPU does not order (the same caveat applies to a SubViewport
showing its own feed).
"""
# Stable structural marker, duck-typed by backends and by the scene-target
# ordering helper (mirrors ``SubViewport._is_subviewport``). Unlike that
# marker it does NOT prune this node's subtree from the main pass: a
# RenderView's children are ordinary main-scene content.
_is_renderview: bool = True
camera = NodePath("", type_filter=Camera3D, hint="Path to the Camera3D this view renders from")
size = Property((512, 512), hint="Offscreen target dimensions in pixels")
render_target_update_mode = Property(
"always",
enum=["disabled", "once", "when_visible", "always"],
hint="When the offscreen view re-renders",
)
use_occlusion = Property(False, hint="Run occlusion culling for this view's pass")
def __init__(self, name="RenderView", **kwargs):
super().__init__(name=name, **kwargs)
self._texture_id: int = -1
self._rendered_once: bool = False
self._cached_camera: Camera3D | None = None
self._cached_camera_path: str = ""
# Explicit render-ordering hint: offscreen target(s) (SubViewport or
# RenderView) this view consumes and must render *after*. A runtime
# attribute, NOT a Property (node references do not serialise into a
# ``.py`` scene); mirrors ``SubViewport.feeds_from``. Accepts a single
# node, an iterable of them, or None.
self.feeds_from = None
[docs]
@property
def texture(self) -> int:
"""The rendered texture's bindless index; ``-1`` until first rendered."""
return self._texture_id
[docs]
@property
def texture_size(self) -> tuple[int, int]:
"""Pixel dimensions of the underlying render target."""
return tuple(self.size)
[docs]
def resolve_camera(self) -> Camera3D | None:
"""Resolve :attr:`camera` to a live ``Camera3D``, caching the result.
Mirrors ``RemoteTransform3D``: re-resolves when the path changes or the
cached node leaves the tree; warns once per distinct invalid path.
"""
path = self.camera
if not path:
self._cached_camera = None
self._cached_camera_path = ""
return None
if path == self._cached_camera_path and self._cached_camera is not None:
if self._cached_camera._tree is not None:
return self._cached_camera
try:
node = self.get_node(path)
except ValueError, KeyError, AttributeError:
if path != self._cached_camera_path:
log.warning("RenderView '%s': invalid camera path '%s'", self.name, path)
self._cached_camera = None
self._cached_camera_path = path
return None
if not isinstance(node, Camera3D):
if path != self._cached_camera_path:
log.warning("RenderView '%s': target '%s' is not a Camera3D", self.name, path)
self._cached_camera = None
self._cached_camera_path = path
return None
self._cached_camera = node
self._cached_camera_path = path
return node