Source code for simvx.core.decal

"""Decal3D — projects textures onto surfaces within a box volume."""


from __future__ import annotations

import logging

from .descriptors import Property
from .math.types import Vec3
from .nodes_3d.node3d import Node3D

log = logging.getLogger(__name__)


[docs] class Decal3D(Node3D): """Projects a texture onto nearby geometry within a box-shaped volume. The decal projects along the node's local **-Y** axis. The ``size`` property defines the half-extents of the projection box in local space (width, height, depth). Only geometry within this box and matching the ``cull_mask`` receives the projected texture. Properties: size: Half-extents of the projection box (Vec3). texture: Path to the albedo/colour texture to project. normal_texture: Path to the normal-map texture to project. albedo_mix: Blend factor for the albedo texture (0 = invisible, 1 = full). normal_mix: Blend factor for the normal-map texture (0 = none, 1 = full). modulate: Colour tint applied to the projected texture (RGBA tuple). distance_fade_begin: Distance from camera at which the decal starts fading. distance_fade_length: Fade range — the decal is fully invisible at ``distance_fade_begin + distance_fade_length``. cull_mask: Bitmask selecting which render layers receive the projection. upper_fade: Fade at the top (+Y) edge of the projection box (0 = hard, 1 = full fade). lower_fade: Fade at the bottom (-Y) edge of the projection box (0 = hard, 1 = full fade). Example:: decal = Decal3D(position=(0, 2, 0), name="BloodSplat") decal.texture = "res://textures/blood.png" decal.size = Vec3(1, 0.5, 1) decal.albedo_mix = 0.8 """ size = Property(Vec3(1, 1, 1), hint="Projection box half-extents (width, height, depth)", group="Decal") texture = Property("", hint="Albedo/colour texture path", group="Decal") normal_texture = Property("", hint="Normal-map texture path", group="Decal") albedo_mix = Property(1.0, range=(0, 1), hint="Albedo texture blend factor", group="Decal") normal_mix = Property(0.0, range=(0, 1), hint="Normal-map blend factor", group="Decal") modulate = Property((1.0, 1.0, 1.0, 1.0), hint="Colour tint (RGBA)", group="Decal") distance_fade_begin = Property(0.0, range=(0, 1000), hint="Distance at which fade starts", group="Decal") distance_fade_length = Property(0.0, range=(0, 1000), hint="Fade range length", group="Decal") cull_mask = Property(0xFFFFFFFF, range=(0, 0xFFFFFFFF), hint="Render layer bitmask (32 layers)", group="Decal") upper_fade = Property(0.3, range=(0, 1), hint="Fade at upper (+Y) edge of projection box", group="Decal") lower_fade = Property(0.3, range=(0, 1), hint="Fade at lower (-Y) edge of projection box", group="Decal") gizmo_colour = Property((0.4, 0.8, 1.0, 0.5), hint="Editor gizmo colour") @property def projection_direction(self) -> Vec3: """World-space projection direction (local -Y axis).""" return Vec3(*(self.world_rotation * Vec3(0, -1, 0)))
[docs] def set_cull_mask_layer(self, index: int, enabled: bool = True) -> None: """Enable or disable a specific cull mask layer (0-31).""" if not 0 <= index < 32: raise ValueError(f"Cull mask layer index must be 0-31, got {index}") if enabled: self.cull_mask = self.cull_mask | (1 << index) else: self.cull_mask = self.cull_mask & ~(1 << index)
[docs] def is_cull_mask_layer_enabled(self, index: int) -> bool: """Check if a specific cull mask layer is enabled (0-31).""" if not 0 <= index < 32: raise ValueError(f"Cull mask layer index must be 0-31, got {index}") return bool(self.cull_mask & (1 << index))
[docs] def get_gizmo_lines(self) -> list[tuple[Vec3, Vec3]]: """Return wireframe edges of the projection box in world space. The box is centred at the node's global position, oriented by the node's global rotation, with half-extents given by ``size``. An additional arrow along the projection direction (-Y) is included. """ p = self.world_position rot = self.world_rotation sz = self.size if isinstance(self.size, Vec3) else Vec3(self.size) hx, hy, hz = float(sz[0]), float(sz[1]), float(sz[2]) # Local-space axes scaled by half-extents rx = Vec3(*(rot * Vec3(hx, 0, 0))) ry = Vec3(*(rot * Vec3(0, hy, 0))) rz = Vec3(*(rot * Vec3(0, 0, hz))) # 8 corners of the oriented box corners = [ p + sx * rx + sy * ry + sz_ * rz for sx in (-1, 1) for sy in (-1, 1) for sz_ in (-1, 1) ] # 12 edges: connect corners that differ in exactly one axis lines: list[tuple[Vec3, Vec3]] = [] for i in range(8): for j in range(i + 1, 8): # Indices encode (sx, sy, sz) each as 0 or 1 across 3 bits diff = i ^ j if diff in (1, 2, 4): # differ in exactly one bit lines.append((Vec3(*corners[i]), Vec3(*corners[j]))) # Arrow along projection direction (-Y) arrow_len = hy * 0.6 arrow_tip = p + Vec3(*(rot * Vec3(0, -hy - arrow_len, 0))) arrow_base = p + Vec3(*(rot * Vec3(0, -hy, 0))) lines.append((arrow_base, arrow_tip)) return lines