"""No-UI PlotOptiX raytracer (output to numpy array only).
https://github.com/rnd-team-dev/plotoptix/blob/master/LICENSE.txt
Have a look at examples on GitHub: https://github.com/rnd-team-dev/plotoptix.
"""
import json, math, logging, os, threading, time
import numpy as np
from ctypes import byref, c_ubyte, c_float, c_uint, c_int, c_longlong, c_void_p
from typing import List, Tuple, Callable, Optional, Union, Any
from plotoptix.singleton import Singleton
from plotoptix.geometry import GeometryMeta
from plotoptix._load_lib import load_optix, PARAM_NONE_CALLBACK, PARAM_INT_CALLBACK
from plotoptix.utils import _make_contiguous_vector, _make_contiguous_3d
from plotoptix.enums import *
[docs]class NpOptiX(threading.Thread, metaclass=Singleton):
"""No-UI raytracer, output to numpy array only.
Base, headless interface to the `RnD.SharpOptiX` raytracing engine. Provides
infrastructure for running the raytracing and compute threads and exposes
their callbacks to the user. Outputs raytraced image to numpy array.
In derived UI classes, implement in overriden methods:
- start and run UI event loop in: :meth:`plotoptix.NpOptiX._run_event_loop`
- raise UI close event in: :meth:`plotoptix.NpOptiX.close`
- update image in UI in: :meth:`plotoptix.NpOptiX._launch_finished_callback`
- optionally apply UI edits in: :meth:`plotoptix.NpOptiX._scene_rt_starting_callback`
Parameters
----------
src : string or dict, optional
Scene description, file name or dictionary. Empty scene is prepared
if the default ``None`` value is used.
on_initialization : callable or list, optional
Callable or list of callables to execute upon starting the raytracing
thread. These callbacks are executed on the main thread.
on_scene_compute : callable or list, optional
Callable or list of callables to execute upon starting the new frame.
Callbacks are executed in a thread parallel to the raytracing.
on_rt_completed : callable or list, optional
Callable or list of callables to execute when the frame raytracing
is completed (execution may be paused with pause_compute() method).
Callbacks are executed in a thread parallel to the raytracing.
on_launch_finished : callable or list, optional
Callable or list of callables to execute when the frame raytracing
is completed. These callbacks are executed on the raytracing thread.
on_rt_accum_done : callable or list, optional
Callable or list of callables to execute when the last accumulation
frame is finished. These callbacks are executed on the raytracing thread.
width : int, optional
Pixel width of the raytracing output. Default value is 16.
height : int, optional
Pixel height of the raytracing output. Default value is 16.
start_now : bool, optional
Start raytracing thread immediately. If set to ``False``, then user should
call ``start()`` method. Default is ``False``.
devices : list, optional
List of selected devices, with the primary device at index 0. Empty list
is default, resulting with all compatible devices selected for processing.
log_level : int or string, optional
Log output level. Default is ``WARN``.
"""
_img_rgba = None
"""Ray-tracing output, 8bps color.
Shape: ``(height, width, 4)``, ``dtype = np.uint8``, contains RGBA data
(alpha channel is now constant, ``255``).
A ndarray wrapped aroud the gpu bufffer. It enables reading the image with no
additional memory copy. Access the buffer in the ``on_launch_finished`` callback
or in/after the ``on_rt_accum_done`` callback to avoid reading while the buffer
content is being updated.
"""
_raw_rgba = None
"""Ray-tracing output, raw floating point data.
Shape: ``(height, width, 4)``, ``dtype = np.float32``, contains RGBA data
(alpha channel is now constant, ``1.0``).
A ndarray wrapped aroud the gpu bufffer. It enables reading the image with no
additional memory copy. Access the buffer in the ``on_launch_finished`` callback
or in/after the ``on_rt_accum_done`` callback to avoid reading while the buffer
content is being updated.
"""
_hit_pos = None
"""Hit position.
Shape: ``(height, width, 4)``, ``dtype = np.float32``, contains ``[X, Y, Z, D]`` data, where
``XYZ`` is the hit 3D position and ``D`` is the hit distance to the camera plane.
Note, this buffer height and width are 2x smaller than rt output if AI upscaler is used.
"""
_geo_id = None
"""Object info.
Encodes the object handle and primitive index (or vertex/face index for meshes)
for each pixel in the output image.
Shape: ``(height, width, 2)``, ``dtype = np.int32``, contains:
- ``_geo_id[h, w, 0] = handle | (vtx_id << 30)``, where ``handle`` is the object
handle, ``vtx_id`` is the vertex index for the triangular face that was hit
(values are ``0``, ``1``, ``2``);
- ``_geo_id[h, w, 1] = prim_idx``, where ``prim_idx`` is the primitive index in
a data set, or face index of a mesh.
Note, this buffer height and width are 2x smaller than rt output if AI upscaler is used.
"""
_albedo = None
"""Surface albedo.
Shape: ``(height, width, 4)``, ``dtype = np.float32``, contains RGBA data
(alpha channel is now constant, ``0.0``).
Available when denoiser is enabled (:attr:`plotoptix.enums.Postprocessing.Denoiser`),
and set to :attr:`plotoptix.enums.DenoiserKind.RgbAlbedo`
or :attr:`plotoptix.enums.DenoiserKind.RgbAlbedoNormal` mode, or when ``save_albedo``
parameter is set to ``True`` (see :meth:`plotoptix.NpOptiX.set_param`).
Note, this buffer height and width are 2x smaller than rt output if AI upscaler is used.
"""
_normal = None
"""Surface normal.
Shape: ``(height, width, 4)``, ``dtype = np.float32``, contains XYZ0 data
(4'th channel is constant, ``0.0``).
Surface normal vector in camera space. Available only when the denoiser is enabled
(:attr:`plotoptix.enums.Postprocessing.Denoiser`), and set to
:attr:`plotoptix.enums.DenoiserKind.RgbAlbedoNormal` mode, or when ``save_normals``
parameter is set to ``True`` (see :meth:`plotoptix.NpOptiX.set_param`).
Note, this buffer height and width are 2x smaller than rt output if AI upscaler is used.
"""
def __init__(self,
src: Optional[Union[str, dict]] = None,
on_initialization = None,
on_scene_compute = None,
on_rt_completed = None,
on_launch_finished = None,
on_rt_accum_done = None,
width: int = -1,
height: int = -1,
start_now: bool = False,
devices: List = [],
log_level: Union[int, str] = logging.WARN) -> None:
"""NpOptiX constructor.
"""
super().__init__()
self._cupy = None
self._torch = None
self._raise_on_error = False
self._logger = logging.getLogger(__name__ + "-NpOptiX")
self._logger.setLevel(log_level)
self._started_event = threading.Event()
self._padlock = threading.RLock()
self._is_scene_created = False
self._is_started = False
self._is_closed = False
rt_log = 0
if isinstance(log_level, int):
if log_level == logging.ERROR: rt_log = 1
elif log_level == logging.WARNING: rt_log = 2
elif log_level == logging.INFO: rt_log = 3
elif log_level == logging.DEBUG: rt_log = 4
if isinstance(log_level, str):
if log_level == 'ERROR': rt_log = 1
elif log_level == 'WARNING': rt_log = 2
elif log_level == 'WARN': rt_log = 2
elif log_level == 'INFO': rt_log = 3
elif log_level == 'DEBUG': rt_log = 4
# load SharpOptiX library, configure paths ####################
self._logger.info("Configure RnD.SharpOptiX library...")
self._optix = load_optix()
self._logger.info("...done.")
###############################################################
# setup SharpOptiX interface ##################################
self._logger.info("Preparing empty scene...")
self._width = 0
self._height = 0
if width < 2: width = 2
if height < 2: height = 2
self.resize(width, height)
self.geometry_data = {} # geometry name to metadata dictionary
self.geometry_names = {} # geometry handle to name dictionary
self.camera_handles = {} # camera name to handle dictionary
self.camera_names = {} # camera handle to name dictionary
self.light_handles = {} # light name to handle dictionary
self.light_names = {} # light handle to name dictionary
# scene initialization / compute / upload / accumulation done callbacks:
if on_initialization is not None: self._initialization_cb = self._make_list_of_callable(on_initialization)
elif src is None: self._initialization_cb = [self._default_initialization]
else: self._initialization_cb = []
self.set_scene_compute_cb(on_scene_compute)
self.set_rt_completed_cb(on_rt_completed)
self.set_rt_starting_cb(cb=None)
self.set_launch_finished_cb(on_launch_finished)
self.set_accum_done_cb(on_rt_accum_done)
device_ptr = 0
device_count = 0
if len(devices) > 0:
self._logger.info("Configure selected devices.")
device_idx = [int(d) for d in devices]
device_idx = np.ascontiguousarray(device_idx, dtype=np.int32)
device_ptr = device_idx.ctypes.data
device_count = device_idx.shape[0]
if src is None: # create empty scene
self._logger.info(" - ray-tracer initialization")
self._is_scene_created = self._optix.create_empty_scene(self._width, self._height, device_ptr, device_count, rt_log)
if self._is_scene_created: self._logger.info("Empty scene ready.")
elif isinstance(src, str): # create scene from file
if not os.path.isfile(src):
msg = "File %s not found." % src
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
wd = os.getcwd()
if os.path.isabs(src):
d, f = os.path.split(src)
os.chdir(d)
else: f = src
self._is_scene_created = self._optix.create_scene_from_file(f, self._width, self._height, device_ptr, device_count)
self._is_scene_created &= self._init_scene_metadata()
if self._is_scene_created:
self._logger.info("Scene loaded correctly.")
os.chdir(wd)
elif isinstance(src, dict): # create scene from dictionary
s = json.dumps(src)
self._is_scene_created = self._optix.create_scene_from_json(s, self._width, self._height, device_ptr, device_count)
self._is_scene_created &= self._init_scene_metadata()
if self._is_scene_created: self._logger.info("Scene loaded correctly.")
else:
msg = "Scene source type not supported."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
if self._is_scene_created:
# optionally start raytracing thread:
if start_now: self.start()
else: self._logger.info("Use start() to start raytracing.")
else:
msg = "Initial setup failed, see errors above."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
###############################################################
def __del__(self):
"""Release resources.
"""
if self._is_scene_created and not self._is_closed:
if self._is_started: self.close()
else: self._optix.destroy_scene()
[docs] def enable_cupy(self):
"""Enable CuPy features.
Required before any call to CuPy related methods:
:meth:`plotoptix.NpOptiX.set_cupy_texture_1d`, :meth:`plotoptix.NpOptiX.set_cupy_texture_2d`,
or :meth:`plotoptix.NpOptiX.update_raw_data` with CuPy array used.
"""
import importlib
try:
self._cupy = importlib.import_module("cupy")
except:
self._logger.error("CuPy is not available.")
self._cupy = None
[docs] def enable_torch(self):
"""Enable PyTorch features.
Required before any call to PyTorch related methods:
:meth:`plotoptix.NpOptiX.set_torch_texture_1d`, :meth:`plotoptix.NpOptiX.set_torch_texture_2d`,
or :meth:`plotoptix.NpOptiX.update_raw_data` with torch tensor used.
"""
import importlib
try:
self._torch = importlib.import_module("torch")
except:
self._logger.error("Torch is not available.")
self._torch = None
[docs] def get_gpu_architecture(self, ordinal: int) -> Optional[GpuArchitecture]:
"""Get SM architecture of selected GPU.
Returns architecture of selected GPU.
Parameters
----------
ordinal : int
CUDA ordinal of the GPU.
Returns
-------
out : GpuArchitecture or None
SM architecture or ``None`` if not recognized.
See Also
--------
:py:mod:`plotoptix.enums.GpuArchitecture`
"""
cfg = self._optix.get_n_gpu_architecture(ordinal)
if cfg >= 0: return GpuArchitecture(cfg)
else: return None
def _make_list_of_callable(self, items) -> List[Callable[["NpOptiX"], None]]:
if callable(items): return [items]
else:
for item in items:
assert callable(item), "Expected callable or list of callable items."
return items
[docs] def start(self) -> None:
"""Start the raytracing, compute, and UI threads.
Actions provided with ``on_initialization`` parameter of NpOptiX
constructor are executed by this method on the main thread,
before starting the ratracing thread.
"""
if self._is_closed:
self._logger.warn("Raytracing output was closed, cannot re-open.")
return
if self._is_started:
self._logger.warn("Raytracing output already running.")
return
for c in self._initialization_cb: c(self)
self._logger.info("Initialization done.")
self._optix.start_rt()
self._logger.info("RT loop ready.")
super().start()
if self._started_event.wait(600):
self._logger.info("Raytracing started.")
self._is_started = True
else:
msg = "Raytracing output startup timed out."
self._logger.error(msg)
self._is_started = False
if self._raise_on_error: raise TimeoutError(msg)
def update_device_buffers(self):
"""Update buffer pointers.
Use after changing denoiser mode since albedo and normal
buffer wrappers are not updated automatically.
"""
c_buf = c_longlong()
c_len = c_int()
r_buf = c_longlong()
r_len = c_int()
h_buf = c_longlong()
h_len = c_int()
g_buf = c_longlong()
g_len = c_int()
a_buf = c_longlong()
a_len = c_int()
n_buf = c_longlong()
n_len = c_int()
if self._optix.get_device_buffers(
byref(c_buf), byref(c_len),
byref(r_buf), byref(r_len),
byref(h_buf), byref(h_len),
byref(g_buf), byref(g_len),
byref(a_buf), byref(a_len),
byref(n_buf), byref(n_len)):
buf = (((c_ubyte * 4) * self._width) * self._height).from_address(c_buf.value)
self._img_rgba = np.ctypeslib.as_array(buf)
buf = (((c_float * 4) * self._width) * self._height).from_address(r_buf.value)
self._raw_rgba = np.ctypeslib.as_array(buf)
buf = (((c_float * 4) * self._width) * self._height).from_address(h_buf.value)
self._hit_pos = np.ctypeslib.as_array(buf)
buf = (((c_uint * 2) * self._width) * self._height).from_address(g_buf.value)
self._geo_id = np.ctypeslib.as_array(buf)
if a_len.value > 0:
buf = (((c_float * 4) * self._width) * self._height).from_address(a_buf.value)
self._albedo = np.ctypeslib.as_array(buf)
else: self._albedo = None
if n_len.value > 0:
buf = (((c_float * 4) * self._width) * self._height).from_address(n_buf.value)
self._normal = np.ctypeslib.as_array(buf)
else: self._normal = None
else:
msg = "Image buffers setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def run(self):
"""Starts UI event loop.
Derived from `threading.Thread <https://docs.python.org/3/library/threading.html>`__.
Use :meth:`plotoptix.NpOptiX.start` to perform complete initialization.
**Do not override**, use :meth:`plotoptix.NpOptiX._run_event_loop` instead.
"""
assert self._is_scene_created, "Scene is not ready, see initialization messages."
c_buf = c_longlong()
c_len = c_int()
r_buf = c_longlong()
r_len = c_int()
h_buf = c_longlong()
h_len = c_int()
g_buf = c_longlong()
g_len = c_int()
a_buf = c_longlong()
a_len = c_int()
n_buf = c_longlong()
n_len = c_int()
if self._optix.resize_scene(self._width, self._height,
byref(c_buf), byref(c_len),
byref(r_buf), byref(r_len),
byref(h_buf), byref(h_len),
byref(g_buf), byref(g_len),
byref(a_buf), byref(a_len),
byref(n_buf), byref(n_len)):
buf = (((c_ubyte * 4) * self._width) * self._height).from_address(c_buf.value)
self._img_rgba = np.ctypeslib.as_array(buf)
buf = (((c_float * 4) * self._width) * self._height).from_address(r_buf.value)
self._raw_rgba = np.ctypeslib.as_array(buf)
w = self._width
h = self._height
# take into account 4x smaller buffers when AI upscaler is used
# TODO: read buffer width and height directly from the engine
if h_len.value == 4 * 4 * (w // 2) * (h // 2):
w = w // 2
h = h // 2
buf = (((c_float * 4) * w) * h).from_address(h_buf.value)
self._hit_pos = np.ctypeslib.as_array(buf)
buf = (((c_uint * 2) * w) * h).from_address(g_buf.value)
self._geo_id = np.ctypeslib.as_array(buf)
if a_len.value > 0:
buf = (((c_float * 4) * w) * h).from_address(a_buf.value)
self._albedo = np.ctypeslib.as_array(buf)
else: self._albedo = None
if n_len.value > 0:
buf = (((c_float * 4) * w) * h).from_address(n_buf.value)
self._normal = np.ctypeslib.as_array(buf)
else: self._normal = None
else:
msg = "Image buffers setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
c1_ptr = self._get_launch_finished_callback()
r1 = self._optix.register_launch_finished_callback(c1_ptr)
c2_ptr = self._get_accum_done_callback()
r2 = self._optix.register_accum_done_callback(c2_ptr)
c3_ptr = self._get_scene_rt_starting_callback()
r3 = self._optix.register_scene_rt_starting_callback(c3_ptr)
c4_ptr = self._get_start_scene_compute_callback()
r4 = self._optix.register_start_scene_compute_callback(c4_ptr)
c5_ptr = self._get_scene_rt_completed_callback()
r5 = self._optix.register_scene_rt_completed_callback(c5_ptr)
if r1 & r2 & r3 & r4 & r5: self._logger.info("Callbacks registered.")
else:
msg = "Callbacks setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
self._run_event_loop()
###########################################################################
[docs] def _run_event_loop(self):
"""Internal method for running the UI event loop.
This method should be overriden in derived UI class (but **do not call
this base implementation**).
Remember to set self._started_event after all your UI initialization.
"""
self._started_event.set()
while not self._is_closed: time.sleep(0.5)
###########################################################################
###########################################################################
[docs] def close(self) -> None:
"""Stop the raytracing thread, release resources.
Raytracing cannot be restarted after this method is called.
Override in UI class, call this base implementation (or raise a close
event for your UI and call this base implementation there).
"""
assert not self._is_closed, "Raytracing output already closed."
assert self._is_started, "Raytracing output not yet running."
with self._padlock:
self._logger.info("Stopping raytracing output.")
self._is_scene_created = False
self._is_started = False
self._optix.stop_rt()
self._optix.destroy_scene()
self._is_closed = True
###########################################################################
def is_started(self) -> bool: return self._is_started
def is_closed(self) -> bool: return self._is_closed
[docs] def get_rt_output(self,
bps: Union[ChannelDepth, str] = ChannelDepth.Bps8,
channels: Union[ChannelOrder, str] = ChannelOrder.RGBA) -> Optional[np.ndarray]:
"""Return a copy of the output image.
The image data type is specified with the ``bps`` argument. 8 bit per channel data,
``numpy.uint8``, is returned by default. Use ``Bps16`` value to read the image in
16 bit per channel depth, ``numpy.uint16``. Use ``Bps32`` value to read the HDR image
in 32 bit per channel format, ``numpy.float32``.
If channels ordering includes alpha channel then it is a constant, 100% opaque value,
to be used in the future releases.
Safe to call at any time, from any thread.
Parameters
----------
bps : ChannelDepth enum or string, optional
Color depth.
channels : ChannelOrder enum or string, optional
Color channels ordering.
Returns
-------
out : ndarray
RGB(A) array of shape (height, width, 3) or (height, width, 4) and type corresponding
to ``bps`` argument. ``None`` in case of errors.
See Also
--------
:class:`plotoptix.enums.ChannelDepth`, :class:`plotoptix.enums.ChannelOrder`
"""
assert self._is_started, "Raytracing output not running."
if isinstance(bps, str): bps = ChannelDepth[bps]
if isinstance(channels, str): channels = ChannelOrder[channels]
a = None
try:
self._padlock.acquire()
if bps == ChannelDepth.Bps8 and channels == ChannelOrder.RGBA:
if self._img_rgba is not None:
return self._img_rgba.copy()
else:
a = np.ascontiguousarray(np.zeros((self._height, self._width, 4), dtype=np.uint8))
if bps == ChannelDepth.Bps8:
if channels == ChannelOrder.BGRA:
a = np.ascontiguousarray(np.zeros((self._height, self._width, 4), dtype=np.uint8))
elif channels == ChannelOrder.RGB or channels == ChannelOrder.BGR:
a = np.ascontiguousarray(np.zeros((self._height, self._width, 3), dtype=np.uint8))
elif bps == ChannelDepth.Bps16:
if channels == ChannelOrder.RGBA or channels == ChannelOrder.BGRA:
a = np.ascontiguousarray(np.zeros((self._height, self._width, 4), dtype=np.uint16))
elif channels == ChannelOrder.RGB or channels == ChannelOrder.BGR:
a = np.ascontiguousarray(np.zeros((self._height, self._width, 3), dtype=np.uint16))
elif bps == ChannelDepth.Bps32:
if channels == ChannelOrder.RGBA or channels == ChannelOrder.BGRA:
a = np.ascontiguousarray(np.zeros((self._height, self._width, 4), dtype=np.float32))
elif channels == ChannelOrder.RGB or channels == ChannelOrder.BGR:
a = np.ascontiguousarray(np.zeros((self._height, self._width, 3), dtype=np.float32))
else: return a
if not self._optix.get_output(a.ctypes.data, a.nbytes, bps.value, channels.value):
msg = "Image not copied."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
return a
def get_size(self) -> Tuple[int, int]:
"""Get size of the ray-tracing output image.
Get current dimensions of the output image.
Returns
-------
out : tuple (int, int)
Output image size, ``(width, height)``.
"""
return self._width, self._height
[docs] def resize(self, width: Optional[int] = None, height: Optional[int] = None) -> None:
"""Change dimensions of the raytracing output.
Both or one of the dimensions may be provided. No effect if width and height
is same as of the current output.
Parameters
----------
width : int, optional
New width of the raytracing output.
height : int, optional
New height of the raytracing output.
"""
if width is None: width = self._width
if height is None: height = self._height
if (width == self._width) and (height == self._height): return
with self._padlock:
self._width = width
self._height = height
# resize the scene, update gpu memory address
c_buf = c_longlong()
c_len = c_int()
r_buf = c_longlong()
r_len = c_int()
h_buf = c_longlong()
h_len = c_int()
g_buf = c_longlong()
g_len = c_int()
a_buf = c_longlong()
a_len = c_int()
n_buf = c_longlong()
n_len = c_int()
if self._optix.resize_scene(self._width, self._height,
byref(c_buf), byref(c_len),
byref(r_buf), byref(r_len),
byref(h_buf), byref(h_len),
byref(g_buf), byref(g_len),
byref(a_buf), byref(a_len),
byref(n_buf), byref(n_len)):
buf = (((c_ubyte * 4) * self._width) * self._height).from_address(c_buf.value)
#buf_from_mem = ctypes.pythonapi.PyMemoryView_FromMemory
#buf_from_mem.restype = ctypes.py_object
#buf_from_mem.argtypes = (ctypes.c_void_p, ctypes.c_int, ctypes.c_int)
#buf = buf_from_mem(c_buf.value, c_len.value, 0x100)
self._img_rgba = np.ctypeslib.as_array(buf)
#self._img_rgba = np.ndarray((height, width, 4), np.uint8, buf, order='C')
#print(self._img_rgba.shape, self._img_rgba.__array_interface__)
#print(self._img_rgba[int(height/2), int(width/2)])
buf = (((c_float * 4) * self._width) * self._height).from_address(r_buf.value)
self._raw_rgba = np.ctypeslib.as_array(buf)
w = self._width
h = self._height
# take into account 4x smaller buffers when AI upscaler is used
# TODO: read buffer width and height directly from the engine
if h_len.value == 4 * 4 * (w // 2) * (h // 2):
w = w // 2
h = h // 2
buf = (((c_float * 4) * w) * h).from_address(h_buf.value)
self._hit_pos = np.ctypeslib.as_array(buf)
buf = (((c_uint * 2) * w) * h).from_address(g_buf.value)
self._geo_id = np.ctypeslib.as_array(buf)
if a_len.value > 0:
buf = (((c_float * 4) * w) * h).from_address(a_buf.value)
self._albedo = np.ctypeslib.as_array(buf)
else: self._albedo = None
if n_len.value > 0:
buf = (((c_float * 4) * w) * h).from_address(n_buf.value)
self._normal = np.ctypeslib.as_array(buf)
else: self._normal = None
else:
self._img_rgba = None
self._hit_pos = None
self._geo_id = None
self._albedo = None
self._normal = None
@staticmethod
def _default_initialization(wnd) -> None:
wnd._logger.info("Default scene initialization.")
if wnd._optix.get_current_camera() == 0:
wnd.setup_camera("default", [0, 0, 10], [0, 0, 0])
###########################################################################
[docs] def set_launch_finished_cb(self, cb) -> None:
"""Set callback function(s) executed after each finished frame.
Parameters
----------
cb : callable or list
Callable or list of callables to set as the launch finished callback.
"""
with self._padlock:
if cb is not None: self._launch_finished_cb = self._make_list_of_callable(cb)
else: self._launch_finished_cb = []
[docs] def _launch_finished_callback(self, rt_result: int) -> None:
"""
Callback executed after each finished frame (``min_accumulation_step``
accumulation frames are raytraced together). This callback is
executed in the raytracing thread and should not compute extensively
(get/save the image data here but calculate scene etc in another thread).
Override this method in the UI class, call this base implementation
and update image in UI (or raise an event to do so).
Actions provided with ``on_launch_finished`` parameter of NpOptiX
constructor are executed here.
Parameters
----------
rt_result : int
Raytracing result code corresponding to :class:`plotoptix.enums.RtResult`.
"""
if self._is_started:
if rt_result < RtResult.NoUpdates.value:
#self._logger.info("Launch finished.")
with self._padlock:
for c in self._launch_finished_cb: c(self)
def _get_launch_finished_callback(self):
def func(rt_result: int): self._launch_finished_callback(rt_result)
return PARAM_INT_CALLBACK(func)
###########################################################################
###########################################################################
def set_rt_starting_cb(self, cb) -> None:
"""Set callback function(s) executed before each frame raytracing.
Parameters
----------
cb : callable or list
Callable or list of callables to set as the rt starting callback.
"""
with self._padlock:
if cb is not None: self._rt_starting_cb = self._make_list_of_callable(cb)
else: self._rt_starting_cb = []
[docs] def _scene_rt_starting_callback(self) -> None:
"""
Callback executed before starting frame raytracing. Appropriate to
override in UI class and apply scene edits (or raise an event to do
so) like camera rotations, etc. made by a user in UI.
This callback is executed in the raytracing thread and should not
compute extensively.
"""
for c in self._rt_starting_cb: c(self)
def _get_scene_rt_starting_callback(self):
def func(): self._scene_rt_starting_callback()
return PARAM_NONE_CALLBACK(func)
###########################################################################
###########################################################################
[docs] def set_accum_done_cb(self, cb) -> None:
"""Set callback function(s) executed when all accumulation frames
are completed.
Parameters
----------
cb : callable or list
Callable or list of callables to set as the accum done callback.
"""
with self._padlock:
if cb is not None: self._rt_accum_done_cb = self._make_list_of_callable(cb)
else: self._rt_accum_done_cb = []
[docs] def _accum_done_callback(self) -> None:
"""
Callback executed when all accumulation frames are completed.
**Do not override**, it is intended to launch ``on_rt_accum_done``
actions provided with NpOptiX constructor parameters.
Executed in the raytracing thread, so do not compute or write files
(make a copy of the image data and process it in another thread).
"""
if self._is_started:
self._logger.info("RT accumulation finished.")
with self._padlock:
for c in self._rt_accum_done_cb: c(self)
def _get_accum_done_callback(self):
def func(): self._accum_done_callback()
return PARAM_NONE_CALLBACK(func)
###########################################################################
###########################################################################
[docs] def set_scene_compute_cb(self, cb) -> None:
"""Set callback function(s) executed on each frame ray tracing start.
Callback(s) executed in parallel to the raytracing and intended for
CPU intensive computations. Note, set ``compute_timeout`` to appropriate
value if your computations are longer than single frame ray tracing, see
:meth:`plotoptix.NpOptiX.set_param`.
Parameters
----------
cb : callable or list
Callable or list of callables to set as the scene compute callback.
"""
with self._padlock:
if cb is not None: self._scene_compute_cb = self._make_list_of_callable(cb)
else: self._scene_compute_cb = []
[docs] def _start_scene_compute_callback(self, n_frames : int) -> None:
"""
Compute callback executed together with the start of each frame raytracing.
This callback is executed in parallel to the raytracing and is intended
for CPU intensive computations. Do not set, update data, cameras, lights,
etc. here, as it will block until the end of raytracing in the parallel
thread.
Callback execution can be suspended / resumed with :meth:`plotoptix.NpOptiX.pause_compute` /
:meth:`plotoptix.NpOptiX.resume_compute` methods.
**Do not override**, this method is intended to launch ``on_scene_compute``
actions provided with NpOptiX constructor parameters.
Parameters
----------
n_frames : int
Number of the raytraced frames since the last call (excluding paused
cycles).
"""
if self._is_started:
self._logger.info("Compute, delta %d frames.", n_frames)
for c in self._scene_compute_cb: c(self, n_frames)
def _get_start_scene_compute_callback(self):
def func(n_frames : int): self._start_scene_compute_callback(n_frames)
return PARAM_INT_CALLBACK(func)
[docs] def set_rt_completed_cb(self, cb) -> None:
"""Set callback function(s) executed on each frame ray tracing finished.
Callback(s) executed in the same thread as the scene compute callback. Note,
set ``compute_timeout`` to appropriate value if your computations are longer
than single frame ray tracing, see :meth:`plotoptix.NpOptiX.set_param`.
Parameters
----------
cb : callable or list
Callable or list of callables to set as the RT completed callback.
"""
with self._padlock:
if cb is not None: self._rt_completed_cb = self._make_list_of_callable(cb)
else: self._rt_completed_cb = []
[docs] def _scene_rt_completed_callback(self, rt_result : int) -> None:
"""
Callback executed in the same thread as _start_scene_compute_callback,
after it finishes computations.
This callback is synchronized also with the raytracing thread and should
be used for any uploads of the updated scene to GPU: data, cameras, lights
setup or updates. Image updates in UI are also possible here, but note that
callback execution can be suspended / resumed with pause_compute() /
resume_compute() methods.
**Do not override**, this method is intended to launch on_rt_completed
actions provided with __init__ method parameters.
Parameters
----------
rt_result : int
Raytracing result code corresponding to RtResult enum.
"""
if self._is_started and rt_result <= RtResult.NoUpdates.value:
self._logger.info("RT completed, result %s.", RtResult(rt_result))
for c in self._rt_completed_cb: c(self)
def _get_scene_rt_completed_callback(self):
def func(rt_result : int): self._scene_rt_completed_callback(rt_result)
return PARAM_INT_CALLBACK(func)
###########################################################################
[docs] def pause_compute(self) -> None:
"""Suspend execution of ``on_scene_compute`` / ``on_rt_completed`` actions.
"""
if self._optix.set_compute_paused(True):
self._logger.info("Compute thread paused.")
else:
self._logger.warn("Pausing compute thread had no effect.")
[docs] def resume_compute(self) -> None:
"""Resume execution of ``on_scene_compute`` / ``on_rt_completed actions``.
"""
if self._optix.set_compute_paused(False):
self._logger.info("Compute thread resumed.")
else:
msg = "Resuming compute thread had no effect."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def refresh_scene(self) -> None:
"""Refresh scene
Starts raytracing accumulation from scratch.
"""
self._optix.refresh_scene()
[docs] def get_float(self, name: str) -> Optional[float]:
"""Get shader ``float`` variable with given ``name``.
Parameters
----------
name : string
Variable name.
Returns
-------
out : float
Value of the variable or ``None`` if variable not found.
"""
if not isinstance(name, str): name = str(name)
c_x = c_float()
if self._optix.get_float(name, byref(c_x)):
self._logger.info("Variable float %s = %f", name, c_x.value)
return c_x.value
else:
msg = "Variable float %s not found." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
[docs] def get_float2(self, name: str) -> Tuple[Optional[float], Optional[float]]:
"""Get shader ``float2`` variable with given ``name``.
Parameters
----------
name : string
Variable name.
Returns
-------
out : tuple (float, float)
Value (x, y) of the variable or ``(None, None)`` if variable not found.
"""
if not isinstance(name, str): name = str(name)
c_x = c_float()
c_y = c_float()
if self._optix.get_float2(name, byref(c_x), byref(c_y)):
self._logger.info("Variable float2 %s = (%f, %f)", name, c_x.value, c_y.value)
return c_x.value, c_y.value
else:
msg = "Variable float2 %s not found." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, None
[docs] def get_float3(self, name: str) -> Tuple[Optional[float], Optional[float], Optional[float]]:
"""Get shader ``float3`` variable with given ``name``.
Parameters
----------
name : string
Variable name.
Returns
-------
out : tuple (float, float, float)
Value (x, y, z) of the variable or ``(None, None, None)`` if variable not found.
"""
if not isinstance(name, str): name = str(name)
c_x = c_float()
c_y = c_float()
c_z = c_float()
if self._optix.get_float3(name, byref(c_x), byref( c_y), byref(c_z)):
self._logger.info("Variable float3 %s = (%f, %f, %f)", name, c_x.value, c_y.value, c_z.value)
return c_x.value, c_y.value, c_z.value
else:
msg = "Variable float3 %s not found." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, None, None
[docs] def set_float(self, name: str, x: float, y: Optional[float] = None, z: Optional[float] = None, refresh: bool = False) -> None:
"""Set shader variable.
Set shader variable with given ``name`` and of the type ``float``, ``float2``
(if y provided), or ``float3`` (if y and z provided). Raytrace the whole
scene if refresh is set to ``True``.
Parameters
----------
name : string
Vairable name.
x : float
Variable value (x component in case of ``float2`` and ``float3``).
y : float, optional
Y component value for ``float2`` and ``float3`` variables.
z : float, optional
Z component value for ``float3`` variables.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
Examples
--------
>>> optix = TkOptiX()
>>> optix.set_float("tonemap_exposure", 0.8)
>>> optix.set_float("tonemap_gamma", 2.2)
"""
if not isinstance(name, str): name = str(name)
if not isinstance(x, float): x = float(x)
if z is not None: # expect float3
if not isinstance(z, float): z = float(z)
if not isinstance(y, float): y = float(y)
self._optix.set_float3(name, x, y, z, refresh)
return
if y is not None: # expect float2
if not isinstance(y, float): y = float(y)
self._optix.set_float2(name, x, y, refresh)
return
self._optix.set_float(name, x, refresh)
[docs] def get_uint(self, name: str) -> Optional[int]:
"""Get shader ``uint`` variable with given ``name``.
Parameters
----------
name : string
Variable name.
Returns
-------
out : int
Value of the variable or ``None`` if variable not found.
"""
if not isinstance(name, str): name = str(name)
c_x = c_uint()
if self._optix.get_uint(name, byref(c_x)):
self._logger.info("Variable uint %s = %d", name, c_x.value)
return c_x.value
else:
msg = "Variable uint %s not found." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
[docs] def get_uint2(self, name: str) -> Tuple[Optional[int], Optional[int]]:
"""Get shader ``uint2`` variable with given ``name``.
Parameters
----------
name : string
Variable name.
Returns
-------
out : tuple (int, int)
Value (x, y) of the variable or ``(None, None)`` if variable not found.
"""
if not isinstance(name, str): name = str(name)
c_x = c_uint()
c_y = c_uint()
if self._optix.get_uint2(name, byref(c_x), byref(c_y)):
self._logger.info("Variable uint2 %s = (%d, %d)", name, c_x.value, c_y.value)
return c_x.value, c_y.value
else:
msg = "Variable uint2 %s not found." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, None
[docs] def set_uint(self, name: str, x: int, y: Optional[int] = None, refresh: bool = False) -> None:
"""Set shader variable.
Set shader variable with given ``name`` and of the type ``uint`` or ``uint2``
(if y provided). Raytrace the whole scene if refresh is set to ``True``.
Note, shader variables distinguish ``int`` and ``uint`` while the type
provided by Python methods is ``int`` in both cases.
Parameters
----------
name : string
Variable name.
x : int
Variable value (x component in case of ``uint2``).
y : int, optional
Y component value for ``uint2`` variable.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
Examples
--------
>>> optix = TkOptiX()
>>> optix.set_uint("path_seg_range", 4, 16) # set longer range of traced path segments
"""
if not isinstance(name, str): name = str(name)
if not isinstance(x, int): x = int(x)
if y is not None: # expect uint2
if not isinstance(y, int): y = int(y)
self._optix.set_uint2(name, x, y, refresh)
return
self._optix.set_uint(name, x, refresh)
[docs] def get_int(self, name: str) -> Optional[int]:
"""Get shader ``int`` variable with given ``name``.
Parameters
----------
name : string
Variable name.
Returns
-------
out : int
Value of the variable or ``None`` if variable not found.
"""
if not isinstance(name, str): name = str(name)
c_x = c_int()
if self._optix.get_int(name, byref(c_x)):
self._logger.info("Variable int %s = %d", name, c_x.value)
return c_x.value
else:
msg = "Variable int %s not found." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
[docs] def set_int(self, name: str, x: int, refresh: bool = False) -> None:
"""Set shader variable.
Set shader variable with given ``name`` and of the type ``int``. Raytrace
the whole scene if refresh is set to ``True``.
Note, shader variables distinguish ``int`` and ``uint`` while the type
provided by Python methods is ``int`` in both cases.
Parameters
----------
name : string
Variable name.
x : int
Variable value.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
"""
if not isinstance(name, str): name = str(name)
if not isinstance(x, int): x = int(x)
self._optix.set_int(name, x, refresh)
def _get_contiguous_tex_mem(self, data: Any) -> Tuple[Any, bool, c_void_p, bool]:
if data is None:
return None, False, 0, False
is_ubyte = False
is_cuda = False
data_ptr = 0
if self._cupy is not None and isinstance(data, self._cupy.ndarray):
is_ubyte = data.dtype == self._cupy.uint8
if not is_ubyte and data.dtype != self._cupy.float32:
data = self._cupy.ascontiguousarray(data, dtype=self._cupy.float32)
if not data.flags['C_CONTIGUOUS']: data = self._cupy.ascontiguousarray(data)
data_ptr = data.data.ptr
is_cuda = True
elif self._torch is not None and self._torch.is_tensor(data):
is_ubyte = data.dtype == self._torch.uint8
if not is_ubyte: # everything not explicitly given as uin8 upload as float32
data = data.type(self._torch.float32) # no copy if already float32
data = data.contiguous() # no copy if already contiguous
data_ptr = data.data_ptr()
is_cuda = data.is_cuda
else:
is_ubyte = data.dtype == np.uint8
if not is_ubyte and data.dtype != np.float32:
data = np.ascontiguousarray(data, dtype=np.float32)
if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data)
data_ptr = data.ctypes.data
is_cuda = False
return data, is_ubyte, data_ptr, is_cuda
def _get_contiguous_tex1d_mem(self, data: Any) -> Tuple[Any, RtFormat, c_void_p, bool]:
if data is None:
return None, RtFormat.Float, 0, False
data, is_ubyte, data_ptr, is_cuda = self._get_contiguous_tex_mem(data)
if is_ubyte:
if len(data.shape) == 1: rt_format = RtFormat.UByte
elif len(data.shape) == 2:
if data.shape[1] == 1: rt_format = RtFormat.UByte
elif data.shape[1] == 2: rt_format = RtFormat.UByte2
elif data.shape[1] == 4: rt_format = RtFormat.UByte4
else:
msg = "Texture 1D shape should be (length,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.UByte, 0, False
else:
msg = "Texture 1D shape should be (length,) or (length,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.UByte, 0, False
else:
if len(data.shape) == 1: rt_format = RtFormat.Float
elif len(data.shape) == 2:
if data.shape[1] == 1: rt_format = RtFormat.Float
elif data.shape[1] == 2: rt_format = RtFormat.Float2
elif data.shape[1] == 4: rt_format = RtFormat.Float4
else:
msg = "Texture 1D shape should be (length,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.Float, 0, False
else:
msg = "Texture 1D shape should be (length,) or (length,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.Float, 0, False
return data, rt_format, data_ptr, is_cuda
# *** Remove in the next release ***
def set_torch_texture_1d(self, name: str, data: Any,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Clamp,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
keep_on_host: bool = False,
refresh: bool = False) -> None:
raise RuntimeError("Method removed. Use set_texture_1d() for any array type.")
[docs] def set_texture_1d(self, name: str, data: Any,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Clamp,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
keep_on_host: bool = False,
refresh: bool = False) -> None:
"""Set texture data.
Set texture ``name`` data. Texture format (float, float2, float4 or byte, byte2, byte4)
and length are deduced from the ``data`` array shape and data type. CPU arrays (Numpy)
and GPU arrays/tensors (PyTorch, CuPy) are supported. Use ``keep_on_host=True`` to make
a copy of data in the host memory (in addition to GPU memory), this option is required
when (small) textures are going to be saved to JSON description of the scene.
Parameters
----------
name : string
Texture name.
data : array_like
Texture data.
addr_mode : TextureAddressMode or string, optional
Texture addressing mode on edge crossing.
filter_mode : TextureFilterMode or string, optional
Texture interpolation mode: nearest neighbor or trilinear.
keep_on_host : bool, optional
Store texture data copy in the host memory.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
See Also
--------
:meth:`plotoptix.NpOptiX.enable_torch`
:meth:`plotoptix.NpOptiX.enable_cupy`
"""
if not isinstance(name, str): name = str(name)
if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode]
if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode]
data, rt_format, data_ptr, is_gpu = self._get_contiguous_tex1d_mem(data)
self._logger.info(
"Set texture 1D %s: length=%d, format=%s, device=%s.",
name, data.shape[0], rt_format.name,
"GPU" if is_gpu else "CPU"
)
if not self._optix.set_texture_1d(name,
data_ptr, is_gpu,
data.shape[0], rt_format.value,
addr_mode.value, filter_mode.value,
keep_on_host, refresh
):
msg = "Texture 1D %s not uploaded." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
def _get_contiguous_tex2d_mem(self, data: Any) -> Tuple[Any, RtFormat, c_void_p, bool]:
if data is None:
return None, RtFormat.Float, 0, False
data, is_ubyte, data_ptr, is_cuda = self._get_contiguous_tex_mem(data)
if is_ubyte:
if len(data.shape) == 2: rt_format = RtFormat.UByte
elif len(data.shape) == 3:
if data.shape[2] == 1: rt_format = RtFormat.UByte
elif data.shape[2] == 2: rt_format = RtFormat.UByte2
elif data.shape[2] == 4: rt_format = RtFormat.UByte4
else:
msg = "Texture 2D shape should be (height,width,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.UByte, 0, False
else:
msg = "Texture 2D shape should be (height,width) or (height,width,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.UByte, 0, False
else:
if len(data.shape) == 2: rt_format = RtFormat.Float
elif len(data.shape) == 3:
if data.shape[2] == 1: rt_format = RtFormat.Float
elif data.shape[2] == 2: rt_format = RtFormat.Float2
elif data.shape[2] == 4: rt_format = RtFormat.Float4
else:
msg = "Texture 2D shape should be (height,width,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.Float, 0, False
else:
msg = "Texture 2D shape should be (height,width) or (height,width,n), where n=1,2,4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, RtFormat.Float, 0, False
return data, rt_format, data_ptr, is_cuda
# *** Remove in the next release ***
def set_torch_texture_2d(self, name: str, data: Any,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Wrap,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
keep_on_host: bool = False,
refresh: bool = False) -> None:
raise RuntimeError("Method removed. Use set_texture_2d() for any array type.")
[docs] def set_texture_2d(self, name: str, data: Any,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Wrap,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
keep_on_host: bool = False,
refresh: bool = False) -> None:
"""Set texture data.
Set texture ``name`` data. Texture format (float, float2, float4 or byte, byte2, byte4)
and width/height are deduced from the ``data`` array shape and dtype. CPU arrays (Numpy)
and GPU arrays/tensors (PyTorch, CuPy) are supported. Use ``keep_on_host=True``
to make a copy of data in the host memory (in addition to GPU memory), this
option is required when (small) textures are going to be saved to JSON description
of the scene.
Parameters
----------
name : string
Texture name.
data : array_like
Texture data.
addr_mode : TextureAddressMode or string, optional
Texture addressing mode on edge crossing.
filter_mode : TextureFilterMode or string, optional
Texture interpolation mode: nearest neighbor or trilinear.
keep_on_host : bool, optional
Store texture data copy in the host memory.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
See Also
--------
:meth:`plotoptix.NpOptiX.enable_torch`
:meth:`plotoptix.NpOptiX.enable_cupy`
"""
if not isinstance(name, str): name = str(name)
if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode]
if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode]
data, rt_format, data_ptr, is_gpu = self._get_contiguous_tex2d_mem(data)
self._logger.info(
"Set texture 2D %s: %d x %d, format=%s, device=%s.",
name, data.shape[1], data.shape[0], rt_format.name,
"GPU" if is_gpu else "CPU"
)
if not self._optix.set_texture_2d(name,
data_ptr, is_gpu,
data.shape[1], data.shape[0], rt_format.value,
addr_mode.value, filter_mode.value,
keep_on_host, refresh
):
msg = "Texture 2D %s not uploaded." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def load_texture(self, tex_name: str, file_name: str,
rt_format: RtFormat = RtFormat.Float4,
prescale: float = 1.0,
baseline: float = 0.0,
exposure: float = 1.0,
gamma: float = 1.0,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Wrap,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
keep_on_host: bool = False,
refresh: bool = False) -> None:
"""Load texture from file.
Parameters
----------
tex_name : string
Texture name.
file_name : string
Source image file.
rt_format: RtFormat, optional
Target format of the texture.
prescale : float, optional
Scaling factor for color values.
baseline : float, optional
Baseline added to color values.
exposure : float, optional
Exposure value used in the postprocessing.
gamma : float, optional
Gamma value used in the postprocessing.
addr_mode : TextureAddressMode or string, optional
Texture addressing mode on edge crossing.
filter_mode : TextureFilterMode or string, optional
Texture interpolation mode: nearest neighbor or trilinear.
keep_on_host : bool, optional
Store texture data copy in the host memory.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
Examples
--------
>>> optix = TkOptiX()
>>> optix.load_texture("rainbow", "data/rainbow.jpg") # set gray background
"""
if isinstance(rt_format, str): rt_format = RtFormat[rt_format]
if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode]
if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode]
if not self._optix.load_texture_2d(tex_name, file_name, prescale, baseline, exposure, gamma, rt_format.value, addr_mode.value, filter_mode.value, refresh):
msg = "Failed on reading texture from file %s." % file_name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
[docs] def set_normal_tilt(self, name: str, data: Any,
mapping: Union[TextureMapping, str] = TextureMapping.Flat,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Wrap,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
keep_on_host: bool = False,
refresh: bool = False) -> None:
"""Set normal tilt data.
Set shading normal tilt according to displacement data for the material ``name``. The ``data``
has to be a 2D array containing displacement mapping. ``mapping`` determines how the normal tilt
is calculated from the displacement map (see :class:`plotoptix.enums.TextureMapping`).
Use ``keep_on_host=True`` to make a copy of data in the host memory (in addition to GPU
memory), this option is required when (small) arrays are going to be saved to JSON
description of the scene.
Parameters
----------
name : string
Object name.
data : array_like
Displacement map data.
mapping : TextureMapping or string, optional
Mapping mode (see :class:`plotoptix.enums.TextureMapping`).
addr_mode : TextureAddressMode or string, optional
Texture addressing mode on edge crossing.
filter_mode : TextureFilterMode or string, optional
Texture interpolation mode: nearest neighbor or trilinear.
keep_on_host : bool, optional
Store texture data copy in the host memory.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
"""
if not isinstance(name, str): name = str(name)
if not isinstance(data, np.ndarray): data = np.ascontiguousarray(data, dtype=np.float32)
if isinstance(mapping, str): mapping = TextureMapping[mapping]
if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode]
if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode]
if len(data.shape) != 2:
msg = "Data shape should be (height,width)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if data.dtype != np.float32: data = np.ascontiguousarray(data, dtype=np.float32)
if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data, dtype=np.float32)
self._logger.info("Set shading normal tilt map for %s: %d x %d.", name, data.shape[1], data.shape[0])
if not self._optix.set_normal_tilt(name, data.ctypes.data, data.shape[1], data.shape[0],
mapping.value, addr_mode.value, filter_mode.value,
keep_on_host, refresh):
msg = "%s normal tilt map not uploaded." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def load_normal_tilt(self, name: str, file_name: str,
mapping: Union[TextureMapping, str] = TextureMapping.Flat,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Wrap,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
prescale: float = 1.0,
baseline: float = 0.0,
refresh: bool = False) -> None:
"""Set normal tilt data.
Set shading normal tilt according to displacement map loaded from an image file. ``mapping``
determines how the normal tilt is calculated from the displacement data
(see :class:`plotoptix.enums.TextureMapping`). Tilt data is stored in the device memory only
(there is no host copy).
Parameters
----------
name : string
Material name.
file_name : string
Image file name with the displacement data.
mapping : TextureMapping or string, optional
Mapping mode (see :class:`plotoptix.enums.TextureMapping`).
addr_mode : TextureAddressMode or string, optional
Texture addressing mode on edge crossing.
filter_mode : TextureFilterMode or string, optional
Texture interpolation mode: nearest neighbor or trilinear.
prescale : float, optional
Scaling factor for displacement values.
baseline : float, optional
Baseline added to displacement values.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
"""
if not isinstance(name, str): name = str(name)
if not isinstance(file_name, str): name = str(file_name)
if isinstance(mapping, str): mapping = TextureMapping[mapping]
if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode]
if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode]
self._logger.info("Set shading normal tilt map for %s using %s.", name, file_name)
if not self._optix.load_normal_tilt(name, file_name, mapping.value, addr_mode.value, filter_mode.value, prescale, baseline, refresh):
msg = "%s normal tilt map not uploaded." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def set_displacement(self, name: str, data: Any,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Wrap,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
keep_on_host: bool = False,
refresh: bool = False) -> None:
"""Set surface displacement data.
Set displacement data for the object ``name``. Geometry attribute program of the object
has to be set to :attr:`plotoptix.enums.GeomAttributeProgram.DisplacedSurface`. The ``data``
has to be a 2D array containing displacement map.
Use ``keep_on_host=True`` to make a copy of data in the host memory (in addition to GPU
memory), this option is required when (small) arrays are going to be saved to JSON
description of the scene.
Parameters
----------
name : string
Object name.
data : array_like
Displacement map data.
addr_mode : TextureAddressMode or string, optional
Texture addressing mode on edge crossing.
keep_on_host : bool, optional
Store texture data copy in the host memory.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
"""
if not isinstance(name, str): name = str(name)
if not isinstance(data, np.ndarray): data = np.ascontiguousarray(data, dtype=np.float32)
if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode]
if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode]
if len(data.shape) != 2:
msg = "Data shape should be (height,width)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if data.dtype != np.float32: data = np.ascontiguousarray(data, dtype=np.float32)
if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data, dtype=np.float32)
self._logger.info("Set displacement map for %s: %d x %d.", name, data.shape[1], data.shape[0])
if not self._optix.set_displacement(name,
data.ctypes.data, False,
data.shape[1], data.shape[0],
addr_mode.value, filter_mode.value,
keep_on_host, refresh
):
msg = "%s displacement map not uploaded." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def load_displacement(self, name: str, file_name: str,
prescale: float = 1.0,
baseline: float = 0.0,
addr_mode: Union[TextureAddressMode, str] = TextureAddressMode.Wrap,
filter_mode: Union[TextureFilterMode, str] = TextureFilterMode.Trilinear,
refresh: bool = False) -> None:
"""Load surface displacement data from file.
Load displacement data for the object ``name`` from an image file. Geometry attribute
program of the object has to be set to :attr:`plotoptix.enums.GeomAttributeProgram.DisplacedSurface`.
Tilt data is stored in the device memory only (there is no host copy).
Parameters
----------
name : string
Object name.
file_name : string
Image file name with the displacement data.
prescale : float, optional
Scaling factor for displacement values.
baseline : float, optional
Baseline added to displacement values.
addr_mode : TextureAddressMode or string, optional
Texture addressing mode on edge crossing.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
"""
if not isinstance(name, str): name = str(name)
if not isinstance(file_name, str): name = str(file_name)
if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode]
if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode]
self._logger.info("Set displacement map for %s using %s.", name, file_name)
if not self._optix.load_displacement(name, file_name, prescale, baseline, addr_mode.value, filter_mode.value, refresh):
msg = "%s displacement map not uploaded." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def get_background_mode(self) -> Optional[MissProgram]:
"""Get currently configured miss program.
Returns
-------
out : MissProgram or None
Miss program, see :py:mod:`plotoptix.enums.MissProgram`, or
`None` if reading the mode failed.
See Also
--------
:py:mod:`plotoptix.enums.MissProgram`
"""
miss = self._optix.get_miss_program()
if miss >= 0:
mode = MissProgram(miss)
self._logger.info("Current miss program is: %s", mode.name)
return mode
else:
msg = "Failed on reading the miss program."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
return None
[docs] def set_background_mode(self, mode: Union[MissProgram, str], refresh: bool = False) -> None:
"""Set miss program.
Parameters
----------
mode : MissProgram enum or string
Miss program, see :py:mod:`plotoptix.enums.MissProgram`.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
See Also
--------
:py:mod:`plotoptix.enums.MissProgram`
"""
if isinstance(mode, str): mode = MissProgram[mode]
if self._optix.set_miss_program(mode.value, refresh):
self._logger.info("Miss program %s is selected.", mode.name)
else:
msg = "Miss program setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def get_background(self) -> Tuple[float, float, float]:
"""Get background color.
**Note**, currently returns constant background color also in texture
based modes.
Returns
-------
out : tuple (float, float, float)
Color values (r, g, b) of the background color.
"""
return self.get_float3("bg_color")
[docs] def set_background(self, bg: Any,
rt_format: Union[RtFormat, str] = RtFormat.Float4,
prescale: float = 1.0,
baseline: float = 0.0,
exposure: float = 1.0,
gamma: float = 1.0,
keep_on_host: bool = False,
refresh: bool = False) -> None:
"""Set background color.
Set background color or texture (shader variable ``bg_color``, texture
``bg_texture`` or ``bg_texture8``, depending on the ``rt_format`` value). Run
raytracing if refresh is set to ``True``. Texture should be provided as
an array of shape ``(height, width, n)``, where ``n`` is 3 or 4. 3-component
RGB arrays are extended to 4-component RGBA shape (alpha channel is reserved
for future implementations).
Function attempts to load texture from file if ``bg`` is a string.
Color values are corrected to account for the postprocessing tone
mapping if ``exposure`` and ``gamma`` values are provided.
Use ``keep_on_host=True`` to make a copy of data in the host memory (in addition
to GPU memory), this option is required when (small) textures are going to be saved
to JSON description of the scene.
Note, color components range is <0; 1>.
Parameters
----------
bg : Any
New backgroud color or texture data; single value is a grayscale level,
RGB color components can be provided as an array-like values, texture
is provided as an array of shape ``(height, width, n)`` (Numpy, CuPy,
PyTorch) or string with the source image file path.
rt_format: RtFormat, optional
Target format of the texture.
prescale : float, optional
Scaling factor for color values.
baseline : float, optional
Baseline added to color values.
exposure : float, optional
Exposure value used in the postprocessing.
gamma : float, optional
Gamma value used in the postprocessing.
keep_on_host : bool, optional
Store texture data copy in the host memory.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
Examples
--------
>>> optix = TkOptiX()
>>> optix.set_background(0.5) # set gray background
>>> optix.set_background([0.5, 0.7, 0.9]) # set light bluish background
See Also
--------
:meth:`plotoptix.NpOptiX.enable_torch`
:meth:`plotoptix.NpOptiX.enable_cupy`
"""
if isinstance(rt_format, str): rt_format = RtFormat[rt_format]
if rt_format == RtFormat.Float4:
bg_name = "bg_texture"
elif rt_format == RtFormat.UByte4:
bg_name = "bg_texture8"
else:
msg = "Background texture format should be Float4 or UByte4."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
# set bkg from file
if isinstance(bg, str):
if self._optix.load_texture_2d(bg_name, bg,
prescale, baseline, exposure, gamma,
rt_format.value,
TextureAddressMode.Mirror.value,
TextureFilterMode.Trilinear.value,
False):
if not self._optix.set_bg_texture(bg_name, refresh):
msg = "Background texture %s not set." % bg_name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
self._logger.info("Background texture loaded from file.")
else:
msg = "Failed on reading background texture."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
e = 1.0 / exposure
# set const grayscale bkg color
if isinstance(bg, float) or isinstance(bg, int):
x = float(bg); x = e * np.power(x, gamma)
y = float(bg); y = e * np.power(y, gamma)
z = float(bg); z = e * np.power(z, gamma)
if self._optix.set_float3("bg_color", x, y, z, refresh):
self._logger.info("Background constant gray level updated.")
else:
msg = "Failed on updating background color."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
# set const color bkg
if (isinstance(bg, (list, tuple)) and len(bg) == 3) or (hasattr(bg, "shape") and len(bg.shape) == 1 and (bg.shape[0] == 3)):
x = e * np.power(float(bg[0]), gamma)
y = e * np.power(float(bg[1]), gamma)
z = e * np.power(float(bg[2]), gamma)
if self._optix.set_float3("bg_color", x, y, z, refresh):
self._logger.info("Background constant color updated.")
else:
msg = "Failed on updating background color."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
# set texture bkg
# CuPy
if self._cupy is not None and isinstance(bg, self._cupy.ndarray):
if len(bg.shape) == 2:
bg = self._cupy.stack([bg, bg, bg], axis=-1)
if len(bg.shape) == 3:
if bg.shape[-1] == 1:
bg = self._cupy.concatenate([bg, bg, bg], axis=-1)
if bg.shape[-1] == 3:
b = self._cupy.zeros((bg.shape[0], bg.shape[1], 4), dtype=bg.dtype)
b[...,:-1] = bg
bg = b
if gamma != 1.0 or e != 1.0:
if bg.dtype == self._cupy.uint8:
bg = bg.astype(self._cupy.float32)
bg *= 1.0/255.0
if gamma != 1.0: bg[..., :3] = self._cupy.power(bg[..., :3], gamma)
if e != 1.0: bg[..., :3] *= e
if rt_format == RtFormat.Float4 and bg.dtype != self._cupy.float32:
if bg.dtype == self._cupy.uint8:
bg = bg.astype(dtype=self._cupy.float32)
bg *= 1.0/255.0
else:
bg = bg.astype(dtype=self._cupy.float32)
elif rt_format == RtFormat.UByte4 and bg.dtype != self._cupy.uint8:
if bg.dtype in [self._cupy.float16, self._cupy.float32, self._cupy.float64]:
bg *= 255.0
self._cupy.clip(bg, 0.0, 255.0, out=bg)
bg = bg.astype(dtype=self._cupy.uint8)
# PyTorch
elif self._torch is not None and self._torch.is_tensor(bg):
if len(bg.shape) == 2:
bg = bg.unsqueeze(-1)
if len(bg.shape) == 3:
if bg.shape[-1] == 1:
bg = self._torch.cat([bg, bg, bg], dim=-1)
if bg.shape[-1] == 3:
b = self._torch.zeros((bg.shape[0], bg.shape[1], 4), dtype=bg.dtype)
b[...,:-1] = bg
bg = b
if gamma != 1.0 or e != 1.0:
if bg.dtype == self._torch.uint8:
bg = bg.type(self._torch.float32)
bg *= 1.0/255.0
if gamma != 1.0: bg[..., :3] = self._torch.pow(bg[..., :3], gamma)
if e != 1.0: bg[..., :3] *= e
if rt_format == RtFormat.Float4 and bg.dtype != self._torch.float32:
if bg.dtype == self._torch.uint8:
bg = bg.type(self._torch.float32)
bg *= 1.0/255.0
else:
bg = bg.type(self._torch.float32)
elif rt_format == RtFormat.UByte4 and bg.dtype != self._torch.uint8:
if bg.dtype in [self._torch.float16, self._torch.float32, self._torch.float64]:
bg *= 255.0
self._torch.clip(bg, 0.0, 255.0, out=bg)
bg = bg.type(dtype=self._torch.uint8)
# Numpy
else:
if not isinstance(bg, np.ndarray):
bg = np.ascontiguousarray(bg)
if len(bg.shape) == 2:
bg = np.stack([bg, bg, bg], axis=-1)
if len(bg.shape) == 3:
if bg.shape[-1] == 1:
bg = np.concatenate([bg, bg, bg], axis=-1)
if bg.shape[-1] == 3:
b = np.zeros((bg.shape[0], bg.shape[1], 4), dtype=bg.dtype)
b[...,:-1] = bg
bg = b
if gamma != 1.0 or e != 1.0:
if bg.dtype == np.uint8:
bg = bg.astype(np.float32)
bg *= 1.0/255.0
if gamma != 1.0: bg[..., :3] = np.power(bg[..., :3], gamma)
if e != 1.0: bg[..., :3] *= e
if rt_format == RtFormat.Float4 and bg.dtype != np.float32:
if bg.dtype == np.uint8:
bg = bg.astype(dtype=np.float32)
bg *= 1.0/255.0
else:
bg = bg.astype(dtype=np.float32)
elif rt_format == RtFormat.UByte4 and bg.dtype != np.uint8:
if bg.dtype in [np.float16, np.float32, np.float64]:
bg *= 255.0
np.clip(bg, 0.0, 255.0, out=bg)
bg = bg.astype(dtype=np.uint8)
if len(bg.shape) == 3 and bg.shape[-1] == 4:
self.set_texture_2d(bg_name, bg,
addr_mode=TextureAddressMode.Mirror,
filter_mode=TextureFilterMode.Trilinear,
keep_on_host=keep_on_host,
refresh=False
)
if not self._optix.set_bg_texture(bg_name, refresh):
msg = "Background texture %s not set." % bg_name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
self._logger.info("Background texture %s updated." % bg_name)
else:
msg = "Background should be a single gray level or [r,g,b] array_like or 2D array_like of gray/[r,g,b]/[r,g,b,a] values."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
[docs] def get_ambient(self) -> Tuple[float, float, float]:
"""Get ambient color.
Returns
-------
out : tuple (float, float, float)
Color values (r, g, b) of the ambient light color.
"""
return self.get_float3("ambient_color")
[docs] def set_ambient(self, color: Any, refresh: bool = False) -> None:
"""Set ambient light color.
Set ambient light color of the scene (shader variable ``ambient_color``,
default value is [0.86, 0.89, 0.94]). Raytrace the whole scene if
refresh is set to ``True``.
Note, color components range is <0; 1>.
Parameters
----------
color : Any
New ambient light color value; single value is a grayscale level,
RGB color components can be provided as array-like values.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
Examples
--------
>>> optix = TkOptiX()
>>> optix.set_ambient(0.5) # set dim gray light
>>> optix.set_ambient([0.1, 0.2, 0.3]) # set dim bluish light
"""
if isinstance(color, float) or isinstance(color, int):
x = float(color)
y = float(color)
z = float(color)
else:
if not isinstance(color, np.ndarray):
color = np.asarray(color, dtype=np.float32)
if (len(color.shape) != 1) or (color.shape[0] != 3):
msg = "Color should be a single value or 3-element array/list/tupe."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
x = color[0]
y = color[1]
z = color[2]
self._optix.set_float3("ambient_color", x, y, z, refresh)
self._logger.info("Ambient color updated.")
[docs] def get_param(self, name: str) -> Optional[Any]:
"""Get raytracer parameter.
Available parameters:
- ``compute_timeout``
- ``light_shading``
- ``work_distribution``
- ``max_accumulation_frames``
- ``min_accumulation_step``
- ``noise_threshold``
- ``rt_timeout``
- ``save_albedo``
- ``save_normals``
Parameters
----------
name : string
Parameter name.
Returns
-------
out : Any, optional
Value of the parameter or ``None`` if parameter not found.
Examples
--------
>>> optix = TkOptiX()
>>> print(optix.get_param("max_accumulation_frames"))
See Also
--------
:meth:`plotoptix.NpOptiX.set_param`
"""
try:
v = None
self._padlock.acquire()
if name == "min_accumulation_step":
v = self._optix.get_min_accumulation_step()
elif name == "max_accumulation_frames":
v = self._optix.get_max_accumulation_frames()
elif name == "work_distribution":
wdistr = self._optix.get_work_distribution()
if wdistr >= 0: v = WorkDistribution(wdistr)
elif name == "noise_threshold":
v = self._optix.get_noise_threshold()
elif name == "light_shading":
shading = self._optix.get_light_shading()
if shading >= 0: v = LightShading(shading)
elif name == "compute_timeout":
v = self._optix.get_compute_timeout()
elif name == "rt_timeout":
v = self._optix.get_rt_timeout()
elif name == "save_albedo":
v = self._optix.get_save_albedo()
elif name == "save_normals":
v = self._optix.get_save_normals()
else:
msg = "Unknown parameter " + name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
self._logger.info("Value of %s is %s", name, v)
return v
[docs] def set_param(self, **kwargs) -> None:
"""Set raytracer parameter(s).
Available parameters:
- ``compute_timeout``: timeout for the computation thread
Set this parameter if the computations performed in the scene_compute
callback are longer than the frame ray tracing. See also
:meth:`plotoptix.NpOptiX.set_scene_compute_cb`.
- ``light_shading``: light shading mode.
Use :attr:`plotoptix.enums.LightShading.Hard` for best caustics or
:attr:`plotoptix.enums.LightShading.Soft` for fast convergence. String
names ``"Hard"`` and ``"Soft"`` are accepted.
Set mode before adding lights.
- ``max_accumulation_frames``
Number of accumulation frames computed for the scene.
- ``min_accumulation_step``
Number of accumulation frames computed in a single step (before each
image refresh).
- ``noise_threshold``
Average noise threshold for automatic stop of ray tracing in
:attr:`plotoptix.enums.WorkDistribution.NoiseBalanced` mode. Default
value is ``0`` which effectively disables automatic stopping. Noise
is calculated as average relative error of the estimated pixel brightness.
- ``rt_timeout``
Ray tracing timeout. Default value is 30000 (30s).
- ``save_albedo``
Allocate buffer and collect albedo information if set to `True`.
If set to `False` then buffer is allocated only if denoiser requires it.
- ``save_normals``
Allocate buffer and collect normals if set to `True`. If set to `False`
then buffer is allocated only if denoiser requires it.
Parameters
----------
kwargs : Any
Values of parameters corresponding to provided names.
Examples
--------
>>> optix = TkOptiX()
>>> optix.set_param(min_accumulation_step=4, max_accumulation_frames=200)
"""
try:
self._padlock.acquire()
for key, value in kwargs.items():
self._logger.info("Set %s to %s", key, value)
if key == "min_accumulation_step":
self._optix.set_min_accumulation_step(int(value))
elif key == "max_accumulation_frames":
self._optix.set_max_accumulation_frames(int(value))
elif key == "noise_threshold":
self._optix.set_noise_threshold(float(value))
elif key == "light_shading":
if len(self.light_handles) > 0:
msg = "Light shading has to be selected before adding lights."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
continue
if isinstance(value, str): mode = LightShading[value]
else: mode = value
self._optix.set_light_shading(mode.value)
elif key == "compute_timeout":
self._optix.set_compute_timeout(int(value))
elif key == "rt_timeout":
self._optix.set_rt_timeout(int(value))
elif key == "save_albedo":
self._optix.set_save_albedo(bool(value))
elif key == "save_normals":
self._optix.set_save_normals(bool(value))
else:
msg = "Unknown parameter " + key
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def get_scene(self) -> dict:
"""Get dictionary with the scene description.
Returns a dictionary with the scene description. Geometry objects,
materials, lights, texture data or file names, cameras, postprocessing
and scene parameters are included. Callback functions and vieport dimensions
are not saved. Existing files are overwritten.
Returns
-------
out : dict, optional
Dictionary with the scene description.
"""
try:
self._padlock.acquire()
s = self._optix.save_scene_to_json()
if len(s) > 2: return json.loads(s)
else: return {}
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
def _init_scene_metadata(self) -> bool:
s = self._optix.get_scene_metadata()
if len(s) > 2: meta = json.loads(s)
else:
self._logger.error("Scene loading failed.")
return False
self.geometry_data = {} # geometry name to handle dictionary
self.geometry_names = {} # geometry handle to name dictionary
if "Geometry" in meta:
for key, value in meta["Geometry"].items():
self.geometry_data[key] = GeometryMeta(key, value["Handle"], value["Size"], Geometry(value["Type"]))
self.geometry_names[value["Handle"]] = key
else: return False
self.camera_handles = {} # camera name to handle dictionary
self.camera_names = {} # camera handle to name dictionary
if "Cameras" in meta:
for key, value in meta["Cameras"].items():
self.camera_handles[key] = value
self.camera_names[value] = key
else: return False
self.light_handles = {} # light name to handle dictionary
self.light_names = {} # light handle to name dictionary
if "Lights" in meta:
for key, value in meta["Lights"].items():
self.light_handles[key] = value
self.light_names[value] = key
else: return False
return True
[docs] def set_scene(self, scene: dict) -> None:
"""Setup scene using description in provided dictionary.
Set new scene using provided description (and destroy current scene). Geometry
objects, materials, lights, texture data or file names, cameras, postprocessing
and scene parameters are replaced. Callback functions and vieport dimensions are
preserved.
Note: locations of external resources loaded from files (e.g. textures) are saved
as relative paths, ensure your working directory matches these locations.
Parameters
----------
scene : dict
Dictionary with the scene description.
"""
s = json.dumps(scene)
with self._padlock:
self._logger.info("Loading new scene from dictionary.")
if self._optix.load_scene_from_json(s) and self._init_scene_metadata():
self.update_device_buffers()
self._logger.info("New scene ready.")
else:
msg = "Scene loading failed."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
[docs] def load_scene(self, file_name: str) -> None:
"""Load scene description from JSON file.
Load new scene from JSON file (and destroy current scene). Geometry objects,
materials, lights, texture data or file names, cameras, postprocessing and
scene parameters are replaced. Callback functions and vieport dimensions are
preserved.
Parameters
----------
file_name : str
Input file name.
"""
if not os.path.isfile(file_name):
msg = "File %s not found." % file_name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
wd = os.getcwd()
if os.path.isabs(file_name):
d, f = os.path.split(file_name)
os.chdir(d)
else:
f = file_name
with self._padlock:
self._logger.info("Loading new scene from file %s.", file_name)
if self._optix.load_scene_from_file(f) and self._init_scene_metadata():
self.update_device_buffers()
self._logger.info("New scene ready.")
else:
msg = "Scene loading failed."
self._logger.error(msg)
if self._raise_on_error:
os.chdir(wd)
raise ValueError(msg)
os.chdir(wd)
[docs] def save_scene(self, file_name: str) -> None:
"""Save scene description to JSON file.
Save description of the scene to file. Geometry objects, materials, lights,
texture data or file names, cameras, postprocessing and scene parameters
are included. Callback functions and vieport dimensions are not saved.
Existing files are overwritten.
Parameters
----------
file_name : str
Output file name.
"""
try:
self._padlock.acquire()
if not self._optix.save_scene_to_file(file_name):
msg = "Scene not saved."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def save_image(self, file_name: str,
bps: Union[ChannelDepth, str] = ChannelDepth.Bps8) -> None:
"""Save current image to file.
Save current content of the image buffer to a file. Accepted formats,
recognized by the extension used in the ``file_name``, are:
- bmp, gif, png, jpg, and tif for 8bps color depth,
- png (Windows only), and tif for 16bps color depth,
- tif for 32bps hdr images.
Existing files are overwritten.
Parameters
----------
file_name : str
Output file name.
bps : ChannelDepth enum or string, optional
Color depth.
See Also
--------
:class:`plotoptix.enums.ChannelDepth`
"""
if isinstance(bps, str): bps = ChannelDepth[bps]
try:
self._padlock.acquire()
if bps == ChannelDepth.Bps8:
ok = self._optix.save_image_to_file(file_name)
elif bps == ChannelDepth.Bps16:
ok = self._optix.save_image_to_file_16bps(file_name)
elif bps == ChannelDepth.Bps32:
ok = self._optix.save_image_to_file_32bps(file_name)
else:
ok = False
if not ok:
msg = "Image not saved."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def encoder_create(self, fps: int, bitrate: float = 2,
idrrate: Optional[int] = None,
profile: Union[NvEncProfile, str] = NvEncProfile.Default,
preset: Union[NvEncPreset, str] = NvEncPreset.Default) -> None:
"""Create video encoder.
Create and configure video encoder for this raytracer instance. Only one encoder
per raytracer instance is supported now. Specifying ``preset`` overrides ``bitrate``
settings. Beware that some combinations are not supported by all players
(e.g. lossless encoding is not playable in Windows Media Player).
Parameters
----------
fps : int
Frames per second assumed in the output file.
bitrate : float, optional
Constant bitrate of the encoded stream, in Mbits to save you typing 0's.
idrrate : int, optional
Instantaneous Decode Refresh frame interval. 2 seconds interval is used if
``idrrate`` is not provided.
profile : NvEncProfile enum or string, optional
H.264 encoding profile.
preset : NvEncPreset enum or string, optional
H.264 encoding preset, overrides ``bitrate`` settings.
See Also
--------
:class:`plotoptix.enums.NvEncProfile`, :class:`plotoptix.enums.NvEncPreset`
"""
if idrrate is None: idrrate = 2 * fps
if isinstance(profile, str): profile = NvEncProfile[profile]
if isinstance(preset, str): preset = NvEncPreset[preset]
try:
self._padlock.acquire()
if not self._optix.encoder_create(fps, int(1000000 * bitrate), idrrate, profile.value, preset.value):
msg = "Encoder not created."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def encoder_start(self, out_name: str, n_frames: int = 0) -> None:
"""Start video encoding.
Start encoding to MP4 file with provided name. Total number of frames
can be optionally limited. Output file is overwritten if it already exists.
New file is created and encoding is restarted if method is launched
during previously started encoding.
Parameters
----------
out_name : str
Output file name.
n_frames : int, optional
Maximum number of frames to encode if ``n_frames`` or unlimited
encoding when default value is used.
"""
try:
self._padlock.acquire()
if not self._optix.encoder_start(out_name, n_frames):
msg = "Encoder not started."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def encoder_stop(self) -> None:
"""Stop video encoding.
Stop encoding and close the output file (can happen before configured
total number of frames to encode).
"""
try:
self._padlock.acquire()
if not self._optix.encoder_stop():
msg = "Encoder not stopped."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def encoder_is_open(self) -> bool:
"""Encoder is encoding.
Returns
-------
out : bool
``True`` if encoder is encoding.
"""
return self._optix.encoder_is_open()
[docs] def encoded_frames(self) -> int:
"""Number of encoded video frames.
Returns
-------
out : int
Number of frames.
"""
n = self._optix.encoded_frames()
if n < 0:
msg = "Number of encoded frames unavailable."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return n
[docs] def encoding_frames(self) -> int:
"""Number of frames to encode.
Returns
-------
out : int
Number of frames.
"""
n = self._optix.encoding_frames()
if n < 0:
msg = "Number of frames to encode unavailable."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return n
[docs] def get_camera_names(self) -> list:
"""Return list of cameras' names.
"""
return list(self.camera_handles.keys())
[docs] def get_camera_name_handle(self, name: Optional[str] = None) -> Tuple[Optional[str], Optional[int]]:
"""Get camera name and handle.
Mostly for the internal use.
Parameters
----------
name : string, optional
Camera name; current camera is used if name not provided.
Returns
-------
out : tuple (name, handle)
Name and handle of the camera or ``(None, None)`` if camera not found.
"""
cam_handle = 0
if name is None: # try current camera
cam_handle = self._optix.get_current_camera()
if cam_handle == 0:
msg = "Current camera is not set."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, None
for n, h in self.camera_handles.items():
if h == cam_handle:
name = n
break
else: # try camera by name
if not isinstance(name, str): name = str(name)
if name in self.camera_handles:
cam_handle = self.camera_handles[name]
else:
msg = "Camera %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, None
return name, cam_handle
[docs] def get_camera(self, name: Optional[str] = None) -> Optional[dict]:
"""Get camera parameters.
Parameters
----------
name : string, optional
Name of the camera, use current camera if name not provided.
Returns
-------
out : dict, optional
Dictionary of the camera parameters or ``None`` if failed on
accessing camera data.
"""
name, cam_handle = self.get_camera_name_handle(name)
if name is None: return None
s = self._optix.get_camera(cam_handle)
if len(s) > 2: return json.loads(s)
else:
msg = "Failed on reading camera %s." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
[docs] def get_camera_eye(self, name: Optional[str] = None) -> Optional[np.ndarray]:
"""Get camera eye coordinates.
Parameters
----------
name : string, optional
Name of the camera, use current camera if name not provided.
Returns
-------
out : np.ndarray, optional
3D coordinates of the camera eye or None if failed on
accessing camera data.
"""
if name is not None and not isinstance(name, str): name = str(name)
name, cam_handle = self.get_camera_name_handle(name)
if name is None: return None
eye = np.ascontiguousarray([0, 0, 0], dtype=np.float32)
self._optix.get_camera_eye(cam_handle, eye.ctypes.data)
return eye
[docs] def get_camera_target(self, name: Optional[str] = None) -> Optional[np.ndarray]:
"""Get camera target coordinates.
Parameters
----------
name : string, optional
Name of the camera, use current camera if name not provided.
Returns
-------
out : np.ndarray, optional
3D coordinates of the camera target or ``None`` if failed on
accessing camera data.
"""
if name is not None and not isinstance(name, str): name = str(name)
name, cam_handle = self.get_camera_name_handle(name)
if name is None: return None
target = np.ascontiguousarray([0, 0, 0], dtype=np.float32)
self._optix.get_camera_target(cam_handle, target.ctypes.data)
return target
[docs] def get_camera_glock(self, name: Optional[str] = None) -> Optional[bool]:
"""Get camera gimbal lock state.
Parameters
----------
name : string, optional
Name of the camera, use current camera if name not provided.
Returns
-------
out : bool, optional
Gimbal lock state of the camera or ``None`` if failed on
accessing camera data.
"""
if name is not None and not isinstance(name, str): name = str(name)
name, cam_handle = self.get_camera_name_handle(name)
if name is None: return None
return self._optix.get_camera_glock(cam_handle)
[docs] def set_camera_glock(self, state: bool) -> None:
"""Set current camera's gimbal lock.
Parameters
----------
state : bool
Gimbal lock state.
"""
if not self._optix.set_camera_glock(state):
msg = "Camera gimbal lock not set."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def setup_camera(self, name: str,
eye: Optional[Any] = None,
target: Optional[Any] = None,
up: Optional[Any] = None,
cam_type: Union[Camera, str] = Camera.Pinhole,
work_distribution: Union[WorkDistribution, str] = WorkDistribution.Uniform,
aperture_radius: float = -1,
aperture_fract: float = 0.15,
focal_scale: float = -1,
chroma_l: float = 0.05,
chroma_t: float = 0.01,
fov: float = -1,
camera_matrix: Optional[Any] = None,
#distort_coeffs: Optional[Any] = None,
sensor_height: float = -1,
blur: float = 1,
glock: bool = False,
textures: Optional[Any] = None,
make_current: bool = True) -> None:
"""Setup new or update existing camera.
Note, parameters possible to update with this method are:
``eye``, ``target``, ``up``, ``aperture_radius``,
``focal_scale``, and ``fov``.
Parameters
----------
name : string
Name of the new camera.
eye : array_like, optional
Eye 3D position. Best fit for the current scene is computed if
argument is not provided. Ignored in camera modes with ray origins
stored in a texture.
target : array_like, optional
Target 3D position. Center of all geometries if argument not provided.
Ignored in camera modes with ray targets or directions stored in a texture.
up : array_like, optional
Up (vertical) direction. Y axis if argument not provided. Ignored in camera
modes with ray origins stored in a texture.
cam_type : Camera enum or string, optional
Type (pinhole, depth of field, ...), see :class:`plotoptix.enums.Camera`.
Cannot be changed after construction.
work_distribution :
How rays per pixel are distributed. Default value is :attr:`plotoptix.enums.WorkDistribution.Uniform`,
shooting constant number of rays per pixel (though no. of rays may
differ vor various materials of the primary hit).
See :class:`plotoptix.enums.WorkDistribution` for dynamic
distribution of rays based on the estimated per pixel noise.
aperture_radius : float, optional
Aperture radius (increases focus blur for depth of field cameras). Default
`-1` is internally reset to `0.1`.
aperture_fract : float, optional
Fraction of blind central spot of the aperture (results with ring-like
bokeh if > 0). Cannot be changed after construction.
focal_scale : float, optional
Focusing distance, relative to ``eye - target`` length. Default `-1` is internally
reset to `1.0`, that is focus is set at the target point.
chroma_l : float, optional
Longitudinal chromatic aberration strength, relative variation of the focusing
distance for different wavelengths. Use be a small positive value << 1.0. Default
is ``0.05``, use ``0.0`` for no aberration.
chroma_t : float, optional
Transverse chromatic aberration strength, relative variation of the lens
magnification for different wavelengths. Use be a small positive value << 1.0.
Default is ``0.01``, use ``0.0`` for no aberration.
fov : float, optional
Field of view in degrees. Default `-1` is internally reset to `35.0` (corresponding
to a ~70mm lens in a typical 35mm frame camera).
camera_matrix : array_like, optional
Camera intrinsic matrix in OpenCV convention: `[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]`.
Only `fs`, `fy`, `cx`, `cy` values are used, unit is [mm]; values at positions of constant
`0`s and `1` are ignored. **Note**: camera matrix parameters override `fov` and require
`sensor_height` argument.
sensor_height : float, optional
Height of the sensor, [mm]. Used only if `camera_matrix` is provided.
Default `-1` is internally reset to `24.0` (35mm camera film size).
blur : float, optional
Weight of the new frame in averaging with already accumulated frames.
Range is (0; 1>, lower values result with a higher motion blur, value
1.0 turns off the blur (default). Cannot be changed after construction.
glock : bool, optional
Gimbal lock state of the new camera.
textures : array_like, optional
List of textures used by the camera ray generation program.
make_current : bool, optional
Automatically switch to this camera if set to ``True``.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if isinstance(cam_type, str): cam_type = Camera[cam_type]
if isinstance(work_distribution, str): work_distribution = WorkDistribution[work_distribution]
if name in self.camera_handles:
self.update_camera(name=name, eye=eye, target=target, up=up,
aperture_radius=aperture_radius,
focal_scale=focal_scale,
fov=fov,
camera_matrix=camera_matrix,
#distort_coeffs=distort_coeffs,
sensor_height=sensor_height
)
return
if up is None: up = np.ascontiguousarray([0, 1, 0], dtype=np.float32)
if aperture_radius <= 0: aperture_radius = 0.1
if focal_scale <= 0: focal_scale = 1.0
if fov <= 0: fov = 35.0
if sensor_height <= 0: sensor_height = 24.0
if camera_matrix is not None:
camera_matrix = np.asarray(camera_matrix, dtype=np.float32)
if camera_matrix.shape != (3, 3):
msg = "Need 3x3 camera matrix in OpenCV convention."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
fx = max(0.001, camera_matrix[0, 0])
fy = max(0.001, camera_matrix[1, 1])
fov = 180 * 2*np.arctan(sensor_height/(2*fy)) / np.pi
rxy = fx / fy
cx = 0.5 - camera_matrix[0, 2] / sensor_height
cy = 0.5 + camera_matrix[1, 2] / sensor_height
else:
rxy = 1.0
cx = 0.5
cy = 0.5
#distort_coeffs_ptr = 0
#distort_coeffs = _make_contiguous_vector(eye, 5)
#if distort_coeffs is not None: distort_coeffs_ptr = distort_coeffs.ctypes.data
eye_ptr = 0
eye = _make_contiguous_vector(eye, 3)
if eye is not None: eye_ptr = eye.ctypes.data
target_ptr = 0
target = _make_contiguous_vector(target, 3)
if target is not None: target_ptr = target.ctypes.data
up = _make_contiguous_vector(up, 3)
if up is None:
msg = "Need 3D camera up vector."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
tex_list = ""
if textures is not None: tex_list = ";".join(textures)
h = self._optix.setup_camera(name, cam_type.value, work_distribution.value,
eye_ptr, target_ptr, up.ctypes.data,
aperture_radius, aperture_fract,
focal_scale, chroma_l, chroma_t,
fov, rxy, cx, cy, sensor_height, blur, glock,
tex_list, make_current
)
if h > 0:
self._logger.info("Camera %s handle: %d.", name, h)
self.camera_handles[name] = h
self.camera_names[h] = name
else:
msg = "Camera setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def update_camera(self, name: Optional[str] = None,
eye: Optional[Any] = None,
target: Optional[Any] = None,
up: Optional[Any] = None,
aperture_radius: float = -1.0,
focal_scale: float = -1.0,
fov: float = -1.0,
camera_matrix: Optional[Any] = None,
#distort_coeffs: Optional[Any] = None,
sensor_height: float = -1) -> None:
"""Update camera parameters.
Parameters
----------
name : string
Name of the camera to update.
eye : array_like, optional
Eye 3D position.
target : array_like, optional
Target 3D position.
up : array_like, optional
Up (vertical) direction.
aperture_radius : float, optional
Aperture radius (increases focus blur for depth of field cameras).
focal_scale : float, optional
Focus distance / (eye - target).length.
fov : float, optional
Field of view in degrees.
camera_matrix : array_like, optional
Camera intrinsic matrix in OpenCV convention: `[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]`.
Only `fs`, `fy`, `cx`, `cy` values are used, unit is [mm]; values at positions of constant
`0`s and `1` are ignored. **Note**: camera matrix parameters override `fov` and require
`sensor_height` value (if `sensor_height` is not provided then the previously set value
is used).
sensor_height : float, optional
Height of the sensor, [mm]. Used only if `camera_matrix` is provided.
Value set previously is not updated if default `-1` is used.
"""
name, cam_handle = self.get_camera_name_handle(name)
if (name is None) or (cam_handle == 0): return
eye = _make_contiguous_vector(eye, 3)
if eye is not None: eye_ptr = eye.ctypes.data
else: eye_ptr = 0
target = _make_contiguous_vector(target, 3)
if target is not None: target_ptr = target.ctypes.data
else: target_ptr = 0
up = _make_contiguous_vector(up, 3)
if up is not None: up_ptr = up.ctypes.data
else: up_ptr = 0
if camera_matrix is not None:
if sensor_height <= 0: sensor_height = self._optix.get_camera_sensor_h(cam_handle)
camera_matrix = np.asarray(camera_matrix, dtype=np.float32)
if camera_matrix.shape != (3, 3):
msg = "Need 3x3 camera matrix in OpenCV convention."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
fx = max(0.001, camera_matrix[0, 0])
fy = max(0.001, camera_matrix[1, 1])
fov = 180 * 2*np.arctan(sensor_height/(2*fy)) / np.pi
rxy = fx / fy
cx = 0.5 - camera_matrix[0, 2] / sensor_height
cy = 0.5 + camera_matrix[1, 2] / sensor_height
else:
rxy = -1.0
cx = 0.5
cy = 0.5
#distort_coeffs = _make_contiguous_vector(eye, 5)
#if distort_coeffs is not None: distort_coeffs_ptr = distort_coeffs.ctypes.data
#else: distort_coeffs_ptr = 0
if self._optix.update_camera(name, eye_ptr, target_ptr, up_ptr,
aperture_radius, focal_scale, fov, rxy, cx, cy,
sensor_height):
self._logger.info("Camera %s updated.", name)
else:
msg = "Camera %s update failed." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def get_current_camera(self) -> Optional[str]:
"""Get current camera name.
Returns
-------
out : string, optional
Name of the current camera or ``None`` if camera not set.
"""
cam_handle = self._optix.get_current_camera()
if cam_handle == 0:
msg = "Current camera is not set."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
if cam_handle not in self.camera_names:
msg = "Camera handle %d does not exists." % cam_handle
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
return self.camera_names[cam_handle]
[docs] def set_current_camera(self, name: str) -> None:
"""Switch to another camera.
Parameters
----------
name : string
Name of the new current camera.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.camera_handles:
msg = "Camera %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if self._optix.set_current_camera(name):
self._logger.info("Current camera: %s", name)
else:
msg = "Current camera not changed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_fit(self,
camera: Optional[str] = None,
geometry: Optional[str] = None,
scale: float = 2.5) -> None:
"""Fit the camera eye and target to contain geometry in the field of view.
Parameters
----------
camera : string, optional
Name of the new camera to fit; current camera if name not provided.
geometry : string, optional
Name of the geometry to fit in view; all geometries if not provided.
scale : float, optional
Adjustment of the prefered distance (useful for wide angle cameras).
"""
camera, cam_handle = self.get_camera_name_handle(camera)
if camera is None: return
if geometry is not None:
if not isinstance(geometry, str): geometry = str(geometry)
else: geometry = ""
self._optix.fit_camera(cam_handle, geometry, scale)
[docs] def camera_move_by(self, shift: Tuple[float, float, float]) -> None:
"""Move current camera in the world coordinates.
Parameters
----------
shift : tuple (float, float, float)
(X, Y, Z) shift vector.
"""
if not self._optix.move_camera_by(shift[0], shift[1], shift[2]):
msg = "Camera move failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_move_by_local(self, shift: Tuple[float, float, float]) -> None:
"""Move current camera in the camera coordinates.
Camera coordinates are: X to the right, Y up, Z towards camera.
Parameters
----------
shift : tuple (float, float, float)
(X, Y, Z) shift vector.
"""
if not self._optix.move_camera_by_local(shift[0], shift[1], shift[2]):
msg = "Camera move failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_rotate_by(self,
rot: Tuple[float, float, float],
center: Tuple[float, float, float]) -> None:
"""Rotate current camera in the world coordinates about the center.
Rotation is done the world coordinates about Y, X, and then Z axis,
by the angles provided with ``rot = (rx, ry, rz)`` parameter.
Parameters
----------
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
center : tuple (float, float, float)
Rotation center.
"""
if not self._optix.rotate_camera_by(rot[0], rot[1], rot[2], center[0], center[1], center[2]):
msg = "Camera rotate failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_rotate_by_local(self,
rot: Tuple[float, float, float],
center: Tuple[float, float, float]) -> None:
"""Rotate current camera in the camera coordinates about the center.
Rotation is done the camera coordinates about Y (camera up, yaw),
X (camera right, pitch), and then Z (towards camera, roll) axis,
by the angles provided with ``rot = (rx, ry, rz)`` parameter.
Parameters
----------
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
center : tuple (float, float, float)
Rotation center.
"""
if not self._optix.rotate_camera_by_local(rot[0], rot[1], rot[2], center[0], center[1], center[2]):
msg = "Camera rotate local failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_rotate_eye(self, rot: Tuple[float, float, float]) -> None:
"""Rotate current camera eye about the target point in the world coordinates.
Rotation is done the world coordinates about Y, X, and then Z axis,
by the angles provided with ``rot = (rx, ry, rz)`` parameter.
Parameters
----------
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
"""
if not self._optix.rotate_camera_eye_by(rot[0], rot[1], rot[2]):
msg = "Camera rotate eye failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_rotate_eye_local(self, rot: Tuple[float, float, float]) -> None:
"""Rotate current camera eye about the target point in the camera coordinates.
Rotation is done the camera coordinates about Y (camera up, yaw),
X (camera right, pitch), and then Z (towards camera, roll) axis,
by the angles provided with ``rot = (rx, ry, rz)`` parameter.
Parameters
----------
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
"""
if not self._optix.rotate_camera_eye_by_local(rot[0], rot[1], rot[2]):
msg = "Camera rotate eye local failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_rotate_target(self, rot: Tuple[float, float, float]) -> None:
"""Rotate current camera target about the eye point in the world coordinates.
Rotation is done the world coordinates about Y, X, and then Z axis,
by the angles provided with ``rot = (rx, ry, rz)`` parameter.
Parameters
----------
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
"""
if not self._optix.rotate_camera_tgt_by(rot[0], rot[1], rot[2]):
msg = "Camera rotate target failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def camera_rotate_target_local(self, rot: Tuple[float, float, float]) -> None:
"""Rotate current camera target about the eye point in the camera coordinates.
Rotation is done the camera coordinates about Y (camera up, yaw),
X (camera right, pitch), and then Z (towards camera, roll) axis,
by the angles provided with ``rot = (rx, ry, rz)`` parameter.
Parameters
----------
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
"""
if not self._optix.rotate_camera_tgt_by_local(rot[0], rot[1], rot[2]):
msg = "Camera rotate target failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def get_light_names(self) -> list:
"""Return list of lights' names.
"""
return list(self.light_handles.keys())
[docs] def get_light_shading(self) -> Optional[LightShading]:
"""Get light shading mode.
Deprecated, use ``get_param("light_shading")`` instead.
Returns
----------
out : LightShading or None
Light shading mode. ``None`` is returned if function could
not read the mode from the raytracer.
See Also
--------
:meth:`plotoptix.NpOptiX.get_param`
"""
self._logger.warn("Deprecated, use get_param(\"light_shading\") instead.")
return self.get_param("light_shading")
[docs] def set_light_shading(self, mode: Union[LightShading, str]) -> None:
"""Set light shading mode.
Deprecated, use ``set_param(light_shading=mode)`` instead.
See Also
--------
:meth:`plotoptix.NpOptiX.set_param`
"""
self._logger.warn("Deprecated, use set_param(light_shading=mode) instead.")
self.set_param(light_shading=mode)
[docs] def get_light_pos(self, name: Optional[str] = None) -> Optional[np.ndarray]:
"""Get light 3D position.
Parameters
----------
name : string, optional
Name of the light (last added light if ``None``).
Returns
-------
out : np.ndarray, optional
3D of the light or ``None`` if failed on accessing light data.
"""
if name is None:
if len(self.light_handles) > 0: name = list(self.light_handles.keys())[-1]
else: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
pos = np.ascontiguousarray([0, 0, 0], dtype=np.float32)
self._optix.get_light_pos(name, pos.ctypes.data)
return pos
[docs] def get_light_color(self, name: Optional[str] = None) -> Optional[np.ndarray]:
"""Get light color.
Parameters
----------
name : string, optional
Name of the light (last added light if ``None``).
Returns
-------
out : np.ndarray, optional
Light color RGB or ``None`` if failed on accessing light data.
"""
if name is None:
if len(self.light_handles) > 0: name = list(self.light_handles.keys())[-1]
else: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
col = np.ascontiguousarray([0, 0, 0], dtype=np.float32)
self._optix.get_light_color(name, col.ctypes.data)
return col
[docs] def get_light_u(self, name: Optional[str] = None) -> Optional[np.ndarray]:
"""Get parallelogram light U vector.
Parameters
----------
name : string, optional
Name of the light (last added light if ``None``).
Returns
-------
out : np.ndarray, optional
Light U vector or ``None`` if failed on accessing light data.
"""
if name is None:
if len(self.light_handles) > 0: name = list(self.light_handles.keys())[-1]
else: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
u = np.ascontiguousarray([0, 0, 0], dtype=np.float32)
self._optix.get_light_u(name, u.ctypes.data)
return u
[docs] def get_light_v(self, name: Optional[str] = None) -> Optional[np.ndarray]:
"""Get parallelogram light V vector.
Parameters
----------
name : string, optional
Name of the light (last added light if ``None``).
Returns
-------
out : np.ndarray, optional
Light V vector or ``None`` if failed on accessing light data.
"""
if name is None:
if len(self.light_handles) > 0: name = list(self.light_handles.keys())[-1]
else: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
v = np.ascontiguousarray([0, 0, 0], dtype=np.float32)
self._optix.get_light_v(name, v.ctypes.data)
return v
[docs] def get_light_r(self, name: Optional[str] = None) -> Optional[float]:
"""Get spherical light radius.
Parameters
----------
name : string, optional
Name of the light (last added light if ``None``).
Returns
-------
out : float, optional
Light readius or ``None`` if failed on accessing light data.
"""
if name is None:
if len(self.light_handles) > 0: name = list(self.light_handles.keys())[-1]
else: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
return self._optix.get_light_r(name)
[docs] def get_light(self, name: str) -> Optional[dict]:
"""Get light source parameters.
Parameters
----------
name : string
Name of the light source.
Returns
-------
out : dict, optional
Dictionary of the light source parameters or ``None`` if
failed on accessing the data.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
s = self._optix.get_light(name)
if len(s) > 2: return json.loads(s)
else:
msg = "Failed on reading light %s." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
return None
[docs] def setup_spherical_light(self, name: str, pos: Optional[Any] = None,
autofit_camera: Optional[str] = None,
color: Optional[Any] = None,
radius: float = -1,
in_geometry: bool = True) -> None:
"""Setup new or update existing spherical light.
Updating an existing light with this method will not change its visibility.
Only ``pos``, ``color``, and ``radius`` values can be updated.
Parameters
----------
name : string
Name of the new light.
pos : array_like, optional
3D position.
autofit_camera : string, optional
Name of the camera used to compute light position automatically.
color : Any, optional
RGB color of the light; single value is gray, array_like is RGB
color components. Color value range is (0; inf) as it means the
light intensity.
radius : float, optional
Sphere radius.
in_geometry: bool, optional
Visible in the scene if set to ``True``.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name in self.light_handles:
self.update_light(name, pos=pos, color=color, radius=radius)
return
if color is None: color = 10 * np.ascontiguousarray([1, 1, 1], dtype=np.float32)
if radius <= 0: radius = 1.0
autofit = False
pos = _make_contiguous_vector(pos, 3)
if pos is None:
cam_name, _ = self.get_camera_name_handle(autofit_camera)
if cam_name is None:
msg = "Need 3D coordinates for the new light."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
pos = np.ascontiguousarray([0, 0, 0])
autofit = True
color = _make_contiguous_vector(color, 3)
if color is None:
msg = "Need color (single value or 3-element array/list/tuple)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
h = self._optix.setup_spherical_light(name, pos.ctypes.data, color.ctypes.data,
radius, in_geometry)
if h != 0:
self._logger.info("Light %s handle: %d.", name, h)
self.light_handles[name] = h
self.light_names[h] = name
if autofit:
self.light_fit(name, camera=cam_name)
else:
msg = "Light %s setup failed." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
[docs] def setup_parallelogram_light(self, name: str, pos: Optional[Any] = None,
autofit_camera: Optional[str] = None,
color: Optional[Any] = None,
u: Optional[Any] = None,
v: Optional[Any] = None,
in_geometry: bool = True) -> None:
"""Setup new or update existing parallelogram light.
Note, the light direction is UxV, the back side is black.
Properties that can be updated: ``pos``, ``color``, ``u``, ``v``.
Parameters
----------
name : string
Name of the new light.
pos : array_like, optional
3D position.
autofit_camera : string, optional
Name of the camera used to compute light position automatically.
color : Any, optional
RGB color of the light; single value is gray, array_like is RGB
color components. Color value range is (0; inf) as it means the
light intensity.
u : array_like, optional
Parallelogram U vector.
v : array_like, optional
Parallelogram V vector.
in_geometry: bool, optional
Visible in the scene if set to ``True``.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name in self.light_handles:
self.update_light(name, pos=pos, color=color, u=u, v=v)
return
if color is None: color = 10 * np.ascontiguousarray([1, 1, 1], dtype=np.float32)
if u is None: u = np.ascontiguousarray([0, 1, 0], dtype=np.float32)
if v is None: v = np.ascontiguousarray([-1, 0, 0], dtype=np.float32)
autofit = False
pos = _make_contiguous_vector(pos, 3)
if pos is None:
cam_name, _ = self.get_camera_name_handle(autofit_camera)
if cam_name is None:
msg = "Need 3D coordinates for the new light."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
pos = np.ascontiguousarray([0, 0, 0])
autofit = True
color = _make_contiguous_vector(color, 3)
if color is None:
msg = "Need color (single value or 3-element array/list/tuple)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
u = _make_contiguous_vector(u, 3)
if u is None:
msg = "Need 3D vector U."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
v = _make_contiguous_vector(v, 3)
if v is None:
msg = "Need 3D vector V."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
h = self._optix.setup_parallelogram_light(name, pos.ctypes.data, color.ctypes.data,
u.ctypes.data, v.ctypes.data, in_geometry)
if h != 0:
self._logger.info("Light %s handle: %d.", name, h)
self.light_handles[name] = h
self.light_names[h] = name
if autofit:
self.light_fit(name, camera=cam_name)
else:
msg = "Light %s setup failed." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def setup_area_light(self, name: str,
center: Optional[Any] = None, target: Optional[Any] = None,
u: Optional[float] = None, v: Optional[float] = None,
color: Optional[Any] = None,
in_geometry: bool = True) -> None:
"""Setup new or update existing area (parallelogram) light.
Convenience method to setup parallelogram light with ``center`` and ``target`` 3D points,
and scalar lengths of sides ``u`` and ``v``.
Parameters
----------
name : string
Name of the new light.
center : array_like
3D position of the light center.
target : array_like
3D position of the light target.
u : float
Horizontal side length.
v : float
Vertical side length.
color : Any, optional
RGB color of the light; single value is gray, array_like is RGB
color components. Color value range is (0; inf) as it means the
light intensity.
in_geometry: bool, optional
Visible in the scene if set to ``True``.
"""
if name in self.light_handles:
self.update_area_light(name, center, target, u, v, color)
return
if center is None or target is None or u is None or v is None:
msg = "Need ceter, target, u, and v for the new light."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if color is None:
color = 10 * np.ascontiguousarray([1, 1, 1], dtype=np.float32)
center = _make_contiguous_vector(center, 3)
target = _make_contiguous_vector(target, 3)
n = target - center
n = n / np.linalg.norm(n)
uvec = np.cross(n, [0, 1, 0])
uvec = uvec / np.linalg.norm(uvec)
vvec = np.cross(uvec, n)
vvec = vvec / np.linalg.norm(vvec)
uvec *= -u
vvec *= v
pos = center - 0.5 * (vvec + uvec)
self.setup_parallelogram_light(name, pos=pos, color=color, u=uvec, v=vvec, in_geometry=in_geometry)
[docs] def setup_light(self, name: str,
light_type: Union[Light, str] = Light.Spherical,
pos: Optional[Any] = None,
autofit_camera: Optional[str] = None,
color: Optional[Any] = None,
u: Optional[Any] = None,
v: Optional[Any] = None,
radius: float = -1,
in_geometry: bool = True) -> None:
"""Setup a new light or update an existing light.
Note, the parallelogram light direction is UxV, the back side is black.
Updating an existing light with this method will not change the type of light,
nor its visibility. Only ``pos``, ``color``, ``radius``, ``u``, and ``v`` values
can be updated.
Parameters
----------
name : string
Name of the new light.
light_type : Light enum or string
Light type (parallelogram, spherical, ...), see :class:`plotoptix.enums.Light` enum.
pos : array_like, optional
3D position.
autofit_camera : string, optional
Name of the camera used to compute light position automatically.
color : Any, optional
RGB color of the light; single value is gray, array_like is RGB
color components. Color value range is (0; inf) as it means the
light intensity.
u : array_like, optional
Parallelogram U vector.
v : array_like, optional
Parallelogram V vector.
radius : float, optional
Sphere radius.
in_geometry: bool, optional
Visible in the scene if set to ``True``.
"""
if name is None: raise ValueError()
if name in self.light_handles:
self.update_light(name, pos=pos, color=color,
radius=radius, u=u, v=v)
return
if color is None: color = 10 * np.ascontiguousarray([1, 1, 1], dtype=np.float32)
if u is None: u = np.ascontiguousarray([0, 1, 0], dtype=np.float32)
if v is None: v = np.ascontiguousarray([-1, 0, 0], dtype=np.float32)
if radius <= 0: radius = 1.0
if isinstance(light_type, str): light_type = Light[light_type]
if light_type == Light.Spherical:
self.setup_spherical_light(name, pos=pos,
autofit_camera=autofit_camera,
color=color, radius=radius,
in_geometry=in_geometry)
elif light_type == Light.Parallelogram:
self.setup_parallelogram_light(name, pos=pos,
autofit_camera=autofit_camera,
color=color, u=u, v=v,
in_geometry=in_geometry)
[docs] def update_light(self, name: str,
pos: Optional[Any] = None,
color: Optional[Any] = None,
radius: float = -1,
u: Optional[Any] = None,
v: Optional[Any] = None) -> None:
"""Update light parameters.
Note, the parallelogram light direction is UxV, the back side is black.
Parameters
----------
name : string
Name of the light.
pos : array_like, optional
3D position.
color : Any, optional
RGB color of the light; single value is gray, array_like is RGB
color components. Color value range is (0; inf) as it means the
light intensity.
radius : float, optional
Sphere radius.
u : array_like, optional
Parallelogram U vector.
v : array_like, optional
Parallelogram V vector.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
pos = _make_contiguous_vector(pos, 3)
if pos is not None: pos_ptr = pos.ctypes.data
else: pos_ptr = 0
color = _make_contiguous_vector(color, 3)
if color is not None: color_ptr = color.ctypes.data
else: color_ptr = 0
u = _make_contiguous_vector(u, 3)
if u is not None: u_ptr = u.ctypes.data
else: u_ptr = 0
v = _make_contiguous_vector(v, 3)
if v is not None: v_ptr = v.ctypes.data
else: v_ptr = 0
if self._optix.update_light(name,
pos_ptr, color_ptr,
radius, u_ptr, v_ptr):
self._logger.info("Light %s updated.", name)
else:
msg = "Light %s update failed." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def update_area_light(self, name: str,
center: Optional[Any] = None, target: Optional[Any] = None,
u: Optional[float] = None, v: Optional[float] = None,
color: Optional[Any] = None) -> None:
"""Update area (parallelogram) light.
Convenience method to update parallelogram light with ``center`` and ``target`` 3D points,
and scalar lengths of sides ``u`` and ``v``.
Parameters
----------
name : string
Name of the new light.
center : array_like, optional
3D position of the light center.
target : array_like, optional
3D position of the light target.
u : float, optional
Horizontal side length.
v : float, optional
Vertical side length.
color : Any, optional
RGB color of the light; single value is gray, array_like is RGB
color components. Color value range is (0; inf) as it means the
light intensity.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name not in self.light_handles:
msg = "Light %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if u is None:
u = np.linalg.norm(self.get_light_u(name))
if v is None:
v = np.linalg.norm(self.get_light_v(name))
if center is None:
center = self.get_light_pos(name) + 0.5 * (self.get_light_u(name) + self.get_light_v(name))
if target is None:
n = np.cross(self.get_light_u(name), self.get_light_v(name))
target = center + 100 * n
center = _make_contiguous_vector(center, 3)
target = _make_contiguous_vector(target, 3)
n = target - center
n = n / np.linalg.norm(n)
uvec = np.cross(n, [0, 1, 0])
uvec = uvec / np.linalg.norm(uvec)
vvec = np.cross(uvec, n)
vvec = vvec / np.linalg.norm(vvec)
uvec *= -u
vvec *= v
pos = center - 0.5 * (vvec + uvec)
self.update_light(name, pos=pos, color=color, u=uvec, v=vvec)
[docs] def light_fit(self, light: str,
camera: Optional[str] = None,
horizontal_rot: float = 45,
vertical_rot: float = 25,
dist_scale: float = 1.5) -> None:
"""Fit light position and direction to the camera.
Parameters
----------
name : string
Name of the light.
camera : string, optional
Name of the camera; current camera is used if not provided.
horizontal_rot : float, optional
Angle: eye - target - light in the camera horizontal plane.
vertical_rot : float, optional
Angle: eye - target - light in the camera vertical plane.
dist_scale : float, optional
Light to target distance with reespect to the eye to target distance.
"""
if light is None: raise ValueError()
if not isinstance(light, str): light = str(light)
if not light in self.light_handles:
msg = "Light %s not found." % light
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
cam_handle = 0
if camera is not None:
if not isinstance(camera, str): camera = str(camera)
if camera in self.camera_handles:
cam_handle = self.camera_handles[camera]
horizontal_rot = math.pi * horizontal_rot / 180.0
vertical_rot = math.pi * vertical_rot / 180.0
self._optix.fit_light(light, cam_handle, horizontal_rot, vertical_rot, dist_scale)
[docs] def get_material(self, name: str) -> Optional[dict]:
"""Get material parameters.
Parameters
----------
name : string
Name of the material.
Returns
-------
out : dict, optional
Dictionary of the material parameters or ``None`` if failed on
accessing material data.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
s = self._optix.get_material(name)
if len(s) > 2: return json.loads(s)
else:
msg = "Failed on reading material %s." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
return None
[docs] def setup_material(self, name: str, data: dict) -> None:
"""Setup new or update existing material.
Note: for maximum performance, setup only those materials
you need in the scene.
Parameters
----------
name : string
Name of the material.
data : dict
Parameters of the material.
See Also
--------
:py:mod:`plotoptix.materials`
"""
if name is None or data is None: raise ValueError()
if self._optix.setup_material(name, json.dumps(data)):
self._logger.info("Configured material %s.", name)
else:
msg = "Material %s not configured." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def update_material(self, name: str, data: dict, refresh: bool = False) -> None:
"""Update material properties.
Update material properties and optionally refresh the scene.
Parameters
----------
name : string
Name of the material.
data : dict
Parameters of the material.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
See Also
--------
:py:mod:`plotoptix.materials`
"""
self.setup_material(name, data)
if refresh: self._optix.refresh_scene()
def update_material_texture(self, name: str, data: Any, idx: int = 0, keep_on_host: bool = False, refresh: bool = False) -> None:
"""Update material texture data.
Update texture content/size for material ``name`` data. Texture format has to be RGBA,
width/height are deduced from the ``data`` array shape. Use ``keep_on_host=True``
to make a copy of data in the host memory (in addition to GPU memory), this
option is required when (small) textures are going to be saved to JSON description
of the scene.
Parameters
----------
name : string
Material name.
data : array_like
Texture data.
idx : int, optional
Texture index, the first texture if the default ``0`` is used.
keep_on_host : bool, optional
Store texture data copy in the host memory.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
"""
if not isinstance(name, str): name = str(name)
if not isinstance(data, np.ndarray): data = np.ascontiguousarray(data, dtype=np.float32)
if len(data.shape) != 3 or data.shape[-1] != 4:
msg = "Material texture shape should be (height,width,4)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if data.dtype != np.float32: data = np.ascontiguousarray(data, dtype=np.float32)
if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data, dtype=np.float32)
self._logger.info("Set material %s texture %d: %d x %d.", name, idx, data.shape[1], data.shape[0])
if not self._optix.set_material_texture(name, idx, data.ctypes.data, data.shape[1], data.shape[0], RtFormat.Float4.value, keep_on_host, refresh):
msg = "Material %s texture not uploaded." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def set_correction_curve(self, ctrl_points: Any,
channel: Union[Channel, str] = Channel.Gray,
n_points: int = 256,
range: float = 255,
refresh: bool = False) -> None:
"""Set correction curve.
Calculate and setup a color correction curve using control points provided with
``ctrl_points``. Curve is applied in 2D postprocessing stage to the selected
``channel``. Control points should be an array_like set of input-output values
(array shape is ``(m,2)``). Control point input and output maximum value can be
provided with the ``range`` parameter. Control points are scaled to the range
<0;1>, extreme values (0,0) and (1,1) are added if not present in ``ctrl_points``
(use :meth:`plotoptix.NpOptiX.set_texture_1d` if custom correction curve should
e.g. start above 0 or saturate at a level lower than 1).
Smooth bezier curve is calculated from the control points and stored in 1D texture
with ``n_points`` length.
Parameters
----------
ctrl_points : array_like
Control points to construct curve.
channel : Channel or string, optional
Destination color for the correction curve.
n_points : int, optional
Number of curve points to be stored in texture.
range : float, optional
Maximum input / output value corresponding to provided ``ctrl_points``.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
See Also
--------
:py:mod:`plotoptix.enums.Postprocessing`
:py:mod:`plotoptix.enums.Channel`
"""
if isinstance(channel, str): channel = Channel[channel]
if not isinstance(ctrl_points, np.ndarray): ctrl_points = np.ascontiguousarray(ctrl_points, dtype=np.float32)
if len(ctrl_points.shape) != 2 or ctrl_points.shape[1] != 2:
msg = "Control points shape should be (n,2)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if ctrl_points.dtype != np.float32: ctrl_points = np.ascontiguousarray(ctrl_points, dtype=np.float32)
if not ctrl_points.flags['C_CONTIGUOUS']: ctrl_points = np.ascontiguousarray(ctrl_points, dtype=np.float32)
self._logger.info("Set correction curve in %s channel.", channel.name)
if not self._optix.set_correction_curve(ctrl_points.ctypes.data, ctrl_points.shape[0], n_points, channel.value, range, refresh):
msg = "Correction curve setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def add_postproc(self, stage: Union[Postprocessing, str], refresh: bool = False) -> None:
"""Add 2D postprocessing stage.
Stages are applied to image in the order they are added with this
method. Each stage algorithm has its own variables that should be
configured before adding the postprocessing stage. Configuration
can be updated at any time, but stages cannot be disabled after
adding. See :py:mod:`plotoptix.enums.Postprocessing` for algorithms
configuration examples.
Parameters
----------
stage : Postprocessing or string
Postprocessing algorithm to add.
refresh : bool, optional
Set to ``True`` if the image should be re-computed.
See Also
--------
:py:mod:`plotoptix.enums.Postprocessing`
"""
if isinstance(stage, str): stage = Postprocessing[stage]
self._logger.info("Add postprocessing stage: %s.", stage.name)
if not self._optix.add_postproc(stage.value, refresh):
msg = "Configuration of postprocessing stage %s failed." % stage.name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def set_data(self, name: str, pos: Optional[Any] = None,
r: Optional[Any] = None, c: Optional[Any] = None,
u: Optional[Any] = None, v: Optional[Any] = None, w: Optional[Any] = None,
geom: Union[Geometry, str] = Geometry.ParticleSet,
geom_attr: Union[GeomAttributeProgram, str] = GeomAttributeProgram.Default,
mat: Optional[str] = None,
rnd: bool = True) -> None:
"""Create new or update existing geometry for the dataset.
Data is provided as an array of 3D positions of data points, with the shape ``(n, 3)``.
Additional features can be visualized as a color and size/thickness of the primitives.
Note: not all arguments are used to update existing geeometry. Update is available for:
``mat``, ``pos``, ``c``, ``r``, ``u``, ``v``, and ``w`` data.
Parameters
----------
name : string
Name of the geometry.
pos : array_like, optional
Positions of data points.
c : Any, optional
Colors of the primitives. Single value means a constant gray level.
3-component array means constant RGB color. Array with the shape[0]
equal to the number of primitives will set individual gray/color for
each primitive.
r : Any, optional
Radii of particles / bezier primitives or U / V / W lengths of
parallelograms / parallelepipeds / tetrahedrons (if u / v / w not provided).
Single value sets const. size for all primitives.
u : array_like, optional
U vector(s) of parallelograms / parallelepipeds / tetrahedrons / textured particles.
Single vector sets const. value for all primitives.
v : array_like, optional
V vector(s) of parallelograms / parallelepipeds / tetrahedrons / textured particles.
Single vector sets const. value for all primitives.
w : array_like, optional
W vector(s) of parallelepipeds / tetrahedrons. Single vector sets const.
value for all primitives.
geom : Geometry enum or string, optional
Geometry of primitives (ParticleSet, Tetrahedrons, ...). See :class:`plotoptix.enums.Geometry`
enum.
geom_attr : GeomAttributeProgram enum or string, optional
Geometry attributes program. See :class:`plotoptix.enums.GeomAttributeProgram` enum.
mat : string, optional
Material name.
rnd : bool, optional
Randomize not provided U / V / W vectors so regular but randomly rotated
primitives are generated using available vectors (default). If set to
``False`` all primitives are aligned in the same direction.
See Also
--------
:meth:`plotoptix.NpOptiX.update_data`
:class:`plotoptix.enums.Geometry`
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if isinstance(geom, str): geom = Geometry[geom]
if isinstance(geom_attr, str): geom_attr = GeomAttributeProgram[geom_attr]
constSize = "ConstSize" in geom.name
if name in self.geometry_data:
self.update_data(name, mat=mat, pos=pos, c=c, r=r, u=u, v=v, w=w)
return
if pos is None:
msg = "pos argument required for new geometries."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if r is None and not constSize: r = np.ascontiguousarray([0.05], dtype=np.float32)
if c is None: c = np.ascontiguousarray([0.95, 0.95, 0.95], dtype=np.float32)
if mat is None: mat = "diffuse"
n_primitives = -1
# Prepare positions data
pos = _make_contiguous_3d(pos)
if pos is None:
msg = "Positions (pos) are required for the new instances and cannot be left as None."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if (len(pos.shape) != 2) or (pos.shape[0] < 1) or (pos.shape[1] != 3):
msg = "Positions (pos) should be an array of shape (n, 3)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
n_primitives = pos.shape[0]
pos_ptr = pos.ctypes.data
# Prepare colors data
c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (1,):
c = np.ascontiguousarray([c[0], c[0], c[0]], dtype=np.float32)
col_const_ptr = c.ctypes.data
col_ptr = 0
elif c.shape == (3,):
col_const_ptr = c.ctypes.data
col_ptr = 0
else:
c = _make_contiguous_3d(c, n=n_primitives, extend_scalars=True)
assert c.shape == pos.shape, "Colors and data points shapes must be the same."
if c is not None: col_ptr = c.ctypes.data
else: col_ptr = 0
col_const_ptr = 0
ruvw_len = n_primitives if not constSize else 1
# Prepare radii data
if r is not None:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1: r = r.flatten()
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if r is not None:
if r.shape[0] == 1:
if ruvw_len > 0:
if (ruvw_len != r.shape[0]): r = np.full(ruvw_len, r[0], dtype=np.float32)
else:
msg = "Cannot resolve proper radii (r) shape from preceding data arguments."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if (ruvw_len > 0) and (ruvw_len != r.shape[0]):
msg = "Radii (r) shape does not match shape of preceding data arguments."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if not constSize: n_primitives = r.shape[0]
radii_ptr = r.ctypes.data
else: radii_ptr = 0
# Prepare U vectors
u = _make_contiguous_3d(u, n=ruvw_len)
u_ptr = u.ctypes.data if u is not None else 0
# Prepare V vectors
v = _make_contiguous_3d(v, n=ruvw_len)
v_ptr = v.ctypes.data if v is not None else 0
# Prepare W vectors
w = _make_contiguous_3d(w, n=ruvw_len)
w_ptr = w.ctypes.data if w is not None else 0
if n_primitives == -1:
msg = "Could not figure out proper data shapes."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
# Configure according to selected geometry
is_ok = True
if geom in [Geometry.ParticleSet, Geometry.ParticleSetConstSize]:
if r is None:
msg = "ParticleSet setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.ParticleSetTextured:
if r is None:
msg = "ParticleSetTextured setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if (u is None) or (v is None):
if r is None:
msg = "ParticleSetTextured setup failed, need U / V vectors or radii data."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.Parallelograms:
if (u is None) or (v is None):
if r is None:
msg = "Parallelograms setup failed, need U / V vectors or radii data."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.ParallelogramsConstSize:
if (u is None) and (v is None):
if r is None:
msg = "Plot setup failed, need U, V vectors or radius to set parallelogram edge length."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
else:
if (u is not None and u.shape != (1,3)) or (v is not None and v.shape != (1,3)) or (r is not None and r.shape != (1,)):
msg = "Plot setup failed, need single 3D vector for each of U, V or single radius to set parallelogram edge length."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom in [Geometry.Parallelepipeds, Geometry.Tetrahedrons]:
if (u is None) or (v is None) or (w is None):
if r is None:
msg = "Plot setup failed, need U, V, W vectors or radii data."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.ParallelepipedsConstSize: # or (geom == Geometry.Tetrahedrons):
if (u is None) and (v is None) and (w is None):
if r is None:
msg = "Plot setup failed, need U, V, W vectors or radius to set cube edge length."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
else:
if (u is not None and u.shape != (1,3)) or (v is not None and v.shape != (1,3)) or (w is not None and w.shape != (1,3)) or (r is not None and r.shape != (1,)):
msg = "Plot setup failed, need single 3D vector for each of U, V, W or single radius to set cube edge length."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.BezierChain:
if r is None:
msg = "BezierChain setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.SegmentChain:
if n_primitives < 2:
msg = "SegmentChain requires at least 2 data points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if r is None:
msg = "SegmentChain setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.BSplineQuad:
if n_primitives < 3:
msg = "BSplineQuad requires at least 3 data points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if r is None:
msg = "BSplineQuad setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.Ribbon:
if n_primitives < 3:
msg = "Ribbon requires at least 3 data points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if r is None:
msg = "Ribbon setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.BSplineCubic:
if n_primitives < 4:
msg = "BSplineCubic requires at least 4 data points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if r is None:
msg = "BSplineCubic setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.Beziers:
if n_primitives < 4:
msg = "Bezier requires at least 4 data points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if r is None:
msg = "Bezier setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
elif geom == Geometry.CatmullRom:
if n_primitives < 4:
msg = "CatmullRom requires at least 4 data points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if r is None:
msg = "CatmullRom setup failed, radii data is missing."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
else:
msg = "Unknown geometry"
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
is_ok = False
if is_ok:
try:
self._padlock.acquire()
self._logger.info("Create %s %s, %d primitives...", geom.name, name, n_primitives)
g_handle = self._optix.setup_geometry(geom.value, geom_attr.value, name, mat, rnd, n_primitives,
pos_ptr, col_const_ptr, col_ptr, radii_ptr, u_ptr, v_ptr, w_ptr)
if g_handle > 0:
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name] = GeometryMeta(name, g_handle, n_primitives, geom)
self.geometry_names[g_handle] = name
else:
msg = "Geometry setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
def _get_contiguous_mem(self, data: Any, n: int, dim: int) -> Tuple[Any, c_void_p, bool]:
if data is None:
return None, 0, False
depth = 2 if dim > 1 else 1
if (len(data.shape) != depth) or (data.shape[0] != n) or (dim > 1 and data.shape[1] != dim):
msg = f"Tensor/array shape should be (%d, 3)." % n
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None, 0, False
data_ptr = 0
is_cuda = False
if self._cupy is not None and isinstance(data, self._cupy.ndarray):
if data.dtype != self._cupy.float32: data = self._cupy.ascontiguousarray(data, dtype=self._cupy.float32)
if not data.flags['C_CONTIGUOUS']: data = self._cupy.ascontiguousarray(data, dtype=self._cupy.float32)
data_ptr = data.data.ptr
is_cuda = True
elif self._torch is not None and self._torch.is_tensor(data):
data = data.type(self._torch.float32).contiguous()
data_ptr = data.data_ptr()
is_cuda = data.is_cuda
else:
if not isinstance(data, np.ndarray): data = np.ascontiguousarray(data, dtype=np.float32)
if data.dtype != np.float32: data = np.ascontiguousarray(data, dtype=np.float32)
if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data, dtype=np.float32)
data_ptr = data.ctypes.data
is_cuda = False
return data, data_ptr, is_cuda
[docs] def sync_raw_data(self, name: str) -> None:
"""Synchronize geometry raw data.
This method updates CPU geometry data buffers if GPU copies were modified directly with :meth:`plotoptix.NpOptiX.update_raw_data`.
"""
if not self._optix.sync_geometry_data(name):
msg = "CPU data not synced to GPU copies."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
[docs] def get_data(self, name: str, buffer: Union[GeomBuffer, str]) -> Optional[np.ndarray]:
"""Clone geometry data and return as numpy array.
Parameters
----------
name : string
Name of the geometry.
buffer : GeomBuffer or string
Geometry data type that will be cloned.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if not name in self.geometry_data:
msg = "Geometry %s does not exists." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return None
self.sync_raw_data(name)
return self.geometry_data[name].copy_buffer(buffer)
[docs] def update_raw_data(self, name: str,
pos: Optional[Any] = None, c: Optional[Any] = None, r: Optional[Any] = None,
u: Optional[Any] = None, v: Optional[Any] = None, w: Optional[Any] = None) -> None:
"""Update raw data of an existing geometry.
Fast and direct copy of geometry data from a source array or tensor. CPU to GPU and GPU to GPU transfers are
supported. Local CPU copy is not updated by this method, however data in modified buffers can be synchronized
with :meth:`plotoptix.NpOptiX.sync_raw_data`.
Note: number of primitives of the geometry cannot be changed and not all properties are possible to update with
this function, use :meth:`plotoptix.NpOptiX.set_data` or :meth:`plotoptix.NpOptiX.update_data` for more generic
changes.
ParticleSet, Parallelogram, Parallepiped, and BSpline geometries: all data updates are supported.
Tetrahedrons: only colors can be updated.
BezierChain: updates are not implemented (geometry properties require preprocessing, and cannot update directly).
Use BSplines or CatmullRom instead.
Graph, surface and wireframe geometries also require preprocessing and are not supported now.
Mesh: only vertex, color and normal data updates are supported.
Parameters
----------
name : string
Name of the geometry.
pos : array_like, optional
Positions of data points or mesh vertices.
c : array_like, optional
Colors of the primitives.
r : Any, optional
Radii of particles / bezier primitives.
u : array_like, optional
U vectors of parallelograms / parallelepipeds / tetrahedrons / textured particles.
Normal vectors of meshes.
v : array_like, optional
V vectors of parallelograms / parallelepipeds / tetrahedrons / textured particles.
w : array_like, optional
W vectors of parallelepipeds / tetrahedrons.
See Also
--------
:meth:`plotoptix.NpOptiX.enable_torch`
:meth:`plotoptix.NpOptiX.update_data`
:meth:`plotoptix.NpOptiX.get_data`
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if not name in self.geometry_data:
msg = "Geometry %s does not exists yet, use set_data() instead." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
constSize = "ConstSize" in self.geometry_data[name]._geom.name
n_primitives = self.geometry_data[name]._size
ruvw_len = n_primitives if not constSize else 1
p, p_ptr, p_gpu = self._get_contiguous_mem(pos, n_primitives, 3)
c, c_ptr, c_gpu = self._get_contiguous_mem(c, n_primitives, 3)
r, r_ptr, r_gpu = self._get_contiguous_mem(r, ruvw_len, 1)
u, u_ptr, u_gpu = self._get_contiguous_mem(u, ruvw_len, 3)
v, v_ptr, v_gpu = self._get_contiguous_mem(v, ruvw_len, 3)
w, w_ptr, w_gpu = self._get_contiguous_mem(w, ruvw_len, 3)
try:
self._padlock.acquire()
self._logger.info("Update %s, %d primitives...", name, n_primitives)
g_handle = self._optix.update_geometry_raw(name, n_primitives,
p_ptr, p_gpu, c_ptr, c_gpu, r_ptr, r_gpu,
u_ptr, u_gpu, v_ptr, v_gpu, w_ptr, w_gpu
)
if (g_handle > 0) and (g_handle == self.geometry_data[name]._handle):
self._logger.info("...done, handle: %d", g_handle)
else:
msg = "Raw data update failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def update_data(self, name: str,
mat: Optional[str] = None,
pos: Optional[Any] = None, c: Optional[Any] = None, r: Optional[Any] = None,
u: Optional[Any] = None, v: Optional[Any] = None, w: Optional[Any] = None) -> None:
"""Update data of an existing geometry.
Note that on data size changes (``pos`` array size different than provided with :meth:`plotoptix.NpOptiX.set_data`)
also other properties must be provided matching the new size, otherwise default values are used.
Parameters
----------
name : string
Name of the geometry.
mat : string, optional
Material name.
pos : array_like, optional
Positions of data points.
c : Any, optional
Colors of the primitives. Single value means a constant gray level.
3-component array means constant RGB color. Array with the shape[0]
equal to the number of primitives will set individual grey/color for
each primitive.
r : Any, optional
Radii of particles / bezier primitives. Single value sets constant
radius for all primitives.
u : array_like, optional
U vector(s) of parallelograms / parallelepipeds / tetrahedrons / textured particles.
Single vector sets const. value for all primitives.
v : array_like, optional
V vector(s) of parallelograms / parallelepipeds / tetrahedrons / textured particles.
Single vector sets const. value for all primitives.
w : array_like, optional
W vector(s) of parallelepipeds / tetrahedrons. Single vector sets const.
value for all primitives.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if mat is None: mat = ""
if not name in self.geometry_data:
msg = "Geometry %s does not exists yet, use set_data() instead." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
n_primitives = self.geometry_data[name]._size
constSize = "ConstSize" in self.geometry_data[name]._geom.name
size_changed = False
# Prepare positions data
pos = _make_contiguous_3d(pos)
pos_ptr = 0
if pos is not None:
if (len(pos.shape) != 2) or (pos.shape[0] < 1) or (pos.shape[1] != 3):
msg = "Positions (pos) should be an array of shape (n, 3)."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
n_primitives = pos.shape[0]
size_changed = (n_primitives != self.geometry_data[name]._size)
pos_ptr = pos.ctypes.data
# Prepare colors data
col_const_ptr = 0
col_ptr = 0
if size_changed and c is None:
c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
elif c is not None:
c = np.ascontiguousarray(c, dtype=np.float32)
if c is not None:
if c.shape == (1,):
c = np.ascontiguousarray([c[0], c[0], c[0]], dtype=np.float32)
col_const_ptr = c.ctypes.data
elif c.shape == (3,):
col_const_ptr = c.ctypes.data
else:
c = _make_contiguous_3d(c, n=n_primitives, extend_scalars=True)
if c is not None: col_ptr = c.ctypes.data
ruvw_len = n_primitives if not constSize else 1
# Prepare radii data
if size_changed and not constSize and r is None:
r = np.ascontiguousarray([0.05], dtype=np.float32)
if r is not None:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1: r = r.flatten()
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
radii_ptr = 0
if r is not None:
if not constSize:
if r.shape[0] == 1:
r = np.full(n_primitives, r[0], dtype=np.float32)
if n_primitives != r.shape[0]:
msg = "Radii (r) shape does not match shape of preceding data arguments."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
radii_ptr = r.ctypes.data
# Prepare U vectors
u = _make_contiguous_3d(u, n=ruvw_len)
u_ptr = u.ctypes.data if u is not None else 0
# Prepare V vectors
v = _make_contiguous_3d(v, n=ruvw_len)
v_ptr = v.ctypes.data if v is not None else 0
# Prepare W vectors
w = _make_contiguous_3d(w, n=ruvw_len)
w_ptr = w.ctypes.data if w is not None else 0
try:
self._padlock.acquire()
self._logger.info("Update %s, %d primitives...", name, n_primitives)
g_handle = self._optix.update_geometry(name, mat, n_primitives,
pos_ptr, col_const_ptr, col_ptr, radii_ptr,
u_ptr, v_ptr, w_ptr)
if (g_handle > 0) and (g_handle == self.geometry_data[name]._handle):
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name]._size = n_primitives
else:
msg = "Geometry update failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def set_data_2d(self, name: str, pos: Optional[Any] = None,
r: Optional[Any] = None, c: Optional[Any] = None,
normals: Optional[Any] = None,
range_x: Optional[Tuple[float, float]] = None,
range_z: Optional[Tuple[float, float]] = None,
floor_y: Optional[float] = None,
floor_c: Optional[Any] = None,
geom: Union[Geometry, str] = Geometry.Mesh,
mat: Optional[str] = None,
make_normals: bool = False) -> None:
"""Create new or update existing surface geometry for the 2D dataset.
Data is provided as 2D array of :math:`z = f(x, y)` values, with the shape ``(n, m)``,
where ``n`` and ``m`` are at least 2. Additional data features can be
visualized with color (array of RGB values, shape ``(n, m, 3)``).
Convention of vertical Y and horizontal XZ plane is adopted.
Note: not all arguments are used to update existing geeometry. Update is available for:
``mat``, ``pos``, ``c``, ``r``, ``normals``, ``range_x``, ``range_z``, ``floor_y``,
and ``floor_c`` data.
Parameters
----------
name : string
Name of the new surface geometry.
pos : array_like, optional
Z values of data points.
r : Any, optional
Radii of vertices for the :attr:`plotoptix.enums.Geometry.Graph` geometry,
interpolated along the wireframe edges. Single value sets constant radius
for all vertices.
c : Any, optional
Colors of data points. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n, m, 3)`` will set individual color for each data point,
interpolated between points; ``n`` and ``m`` have to be the same
as in data points shape.
normals : array_like, optional
Surface normal vectors at data points. Array shape has to be ``(n, m, 3)``,
with ``n`` and ``m`` the same as in data points shape.
range_x : tuple (float, float), optional
Data range along X axis. Data array indexes are used if range is
not provided.
range_z : tuple (float, float), optional
Data range along Z axis. Data array indexes are used if range is
not provided.
floor_y : float, optional
Y level of XZ plane forming the base of the new geometry. Surface
only is created if ``floor_y`` is not provided.
floor_c: Any, optional
Color of the base volume. Single value or array_like RGB color values.
geom : Geometry enum or string, optional
Geometry of the surface, only :attr:`plotoptix.enums.Geometry.Mesh` or
:attr:`plotoptix.enums.Geometry.Graph` are supported.
mat : string, optional
Material name.
make_normals : bool, optional
Calculate normals for data points, only if not provided with ``normals``
argument. Normals of all triangles attached to the point are averaged.
See Also
--------
:meth:`plotoptix.NpOptiX.update_data_2d`
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name in self.geometry_data:
self.update_data_2d(name,
mat=mat, pos=pos, r=r, c=c, normals=normals,
range_x=range_x, range_z=range_x,
floor_y=floor_y, floor_c=floor_c)
return
if pos is None:
msg = "pos argument required for new geometries."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if r is None: r = np.ascontiguousarray([0.05], dtype=np.float32)
if c is None: c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
if mat is None: mat = "diffuse"
if isinstance(geom, str): geom = Geometry[geom]
if not geom in [Geometry.Mesh, Geometry.Graph]:
msg = "Geometry type %s not supported by the surface plot." % geom.name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 2 and pos.shape[0] > 1 and pos.shape[1] > 1, "Required vertex data shape is (z,x), where z >= 2 and x >= 2."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
pos_ptr = pos.ctypes.data
if r is not None and geom == Geometry.Graph:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1 or r.shape[0] > 1:
assert r.shape == pos.shape[:2], "Radii shape must be (v,u), with u and v matching the surface points shape."
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if r is not None and geom == Geometry.Graph:
if r.shape[0] == 1:
r = np.full(pos.shape[:2], r[0], dtype=np.float32)
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if r.shape != pos.shape[:2]:
msg = "Radii (r) shape does not match the shape of preceding data arguments."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
radii_ptr = r.ctypes.data
else: radii_ptr = 0
n_ptr = 0
if normals is not None:
if not isinstance(normals, np.ndarray): normals = np.ascontiguousarray(normals, dtype=np.float32)
assert len(normals.shape) == 3 and normals.shape == pos.shape + (3,), "Normals shape must be (z,x,3), where (z,x) id the vertex data shape."
if normals.dtype != np.float32: normals = np.ascontiguousarray(normals, dtype=np.float32)
if not normals.flags['C_CONTIGUOUS']: normals = np.ascontiguousarray(normals, dtype=np.float32)
n_ptr = normals.ctypes.data
make_normals = False
c_ptr = 0
c_const = None
if c is not None:
if isinstance(c, float) or isinstance(c, int): c = np.full(3, c, dtype=np.float32)
if not isinstance(c, np.ndarray): c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (3,):
c_const = c
cm = np.zeros(pos.shape + (3,), dtype=np.float32)
cm[:,:] = c
c = cm
assert len(c.shape) == 3 and c.shape == pos.shape + (3,), "Colors shape must be (m,n,3), where (m,n) id the vertex data shape."
if c.dtype != np.float32: c = np.ascontiguousarray(c, dtype=np.float32)
if not c.flags['C_CONTIGUOUS']: c = np.ascontiguousarray(c, dtype=np.float32)
c_ptr = c.ctypes.data
make_floor = floor_y is not None
if not make_floor: floor_y = np.float32(np.nan)
cl_ptr = 0
if make_floor:
if floor_c is not None:
if isinstance(floor_c, float) or isinstance(floor_c, int): floor_c = np.full(3, floor_c, dtype=np.float32)
if not isinstance(floor_c, np.ndarray): floor_c = np.ascontiguousarray(floor_c, dtype=np.float32)
if floor_c.shape == (3,):
if floor_c.dtype != np.float32: floor_c = np.ascontiguousarray(floor_c, dtype=np.float32)
if not floor_c.flags['C_CONTIGUOUS']: floor_c = np.ascontiguousarray(floor_c, dtype=np.float32)
cl_ptr = floor_c.ctypes.data
else:
self._logger.warn("Floor color should be a single value or RGB array.")
elif c_const is not None:
floor_c = np.ascontiguousarray(c_const, dtype=np.float32)
cl_ptr = floor_c.ctypes.data
if range_x is None: range_x = (np.float32(np.nan), np.float32(np.nan))
if range_z is None: range_z = (np.float32(np.nan), np.float32(np.nan))
try:
self._padlock.acquire()
self._logger.info("Setup surface %s...", name)
g_handle = self._optix.setup_surface(geom.value, name, mat, pos.shape[1], pos.shape[0], pos_ptr, radii_ptr, n_ptr, c_ptr, cl_ptr,
range_x[0], range_x[1], range_z[0], range_z[1], floor_y, make_normals)
if g_handle > 0:
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name] = GeometryMeta(name, g_handle, pos.shape[0] * pos.shape[1], geom)
self.geometry_names[g_handle] = name
else:
msg = "Surface setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def update_data_2d(self, name: str,
mat: Optional[str] = None,
pos: Optional[Any] = None,
r: Optional[Any] = None,
c: Optional[Any] = None,
normals: Optional[Any] = None,
range_x: Optional[Tuple[float, float]] = None,
range_z: Optional[Tuple[float, float]] = None,
floor_y: Optional[float] = None,
floor_c: Optional[Any] = None) -> None:
"""Update surface geometry data or properties.
Parameters
----------
name : string
Name of the surface geometry.
mat : string, optional
Material name.
pos : array_like, optional
Z values of data points.
r : Any, optional
Radii of vertices for the :attr:`plotoptix.enums.Geometry.Graph` geometry,
interpolated along the wireframe edges. Single value sets constant radius
for all vertices.
c : Any, optional
Colors of data points. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n,m,3)`` will set individual color for each data point,
interpolated between points; ``n`` and ``m`` have to be the same
as in data points shape.
normals : array_like, optional
Surface normal vectors at data points. Array shape has to be
``(n,m,3)``, with ``n`` and``m`` the same as in data points shape.
range_x : tuple (float, float), optional
Data range along X axis.
range_z : tuple (float, float), optional
Data range along Z axis.
floor_y : float, optional
Y level of XZ plane forming the base of the geometry.
floor_c: Any, optional
Color of the base volume. Single value or array_like RGB color values.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if mat is None: mat = ""
if not name in self.geometry_data:
msg = "Surface %s does not exists yet, use set_data_2d() instead." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
s_x = c_uint()
s_z = c_uint()
if not self._optix.get_surface_size(name, byref(s_x), byref(s_z)):
msg = "Cannot get surface %s size." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
size_xz = (s_z.value, s_x.value)
size_changed = False
pos_ptr = 0
if pos is not None:
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 2 and pos.shape[0] > 1 and pos.shape[1] > 1, "Required vertex data shape is (z,x), where z >= 2 and x >= 2."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
if pos.shape != size_xz: size_changed = True
size_xz = pos.shape
pos_ptr = pos.ctypes.data
if size_changed and r is None:
r = np.ascontiguousarray([0.05], dtype=np.float32)
if r is not None:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1 or r.shape[0] > 1:
assert r.shape == size_xz, "Radii shape must be (x,z), with x and z matching the data points shape."
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
radii_ptr = 0
if r is not None:
if r.shape[0] == 1:
r = np.full(size_xz, r[0], dtype=np.float32)
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if size_xz != r.shape:
msg = "Radii (r) shape does not match the number of data points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
radii_ptr = r.ctypes.data
c_ptr = 0
c_const = None
if size_changed and c is None: c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
if c is not None:
if isinstance(c, float) or isinstance(c, int): c = np.full(3, c, dtype=np.float32)
if not isinstance(c, np.ndarray): c = np.ascontiguousarray(c, dtype=np.float32)
if len(c.shape) == 1 and c.shape[0] == 3:
c_const = c
cm = np.zeros(size_xz + (3,), dtype=np.float32)
cm[:,:] = c
c = cm
assert len(c.shape) == 3 and c.shape == size_xz + (3,), "Colors shape must be (m,n,3), where (m,n) id the vertex data shape."
if c.dtype != np.float32: c = np.ascontiguousarray(c, dtype=np.float32)
if not c.flags['C_CONTIGUOUS']: c = np.ascontiguousarray(c, dtype=np.float32)
c_ptr = c.ctypes.data
n_ptr = 0
if normals is not None:
if not isinstance(normals, np.ndarray): normals = np.ascontiguousarray(normals, dtype=np.float32)
assert len(normals.shape) == 3 and normals.shape == size_xz + (3,), "Normals shape must be (z,x,3), where (z,x) id the vertex data shape."
if normals.dtype != np.float32: normals = np.ascontiguousarray(normals, dtype=np.float32)
if not normals.flags['C_CONTIGUOUS']: normals = np.ascontiguousarray(normals, dtype=np.float32)
n_ptr = normals.ctypes.data
cl_ptr = 0
if floor_c is not None:
if isinstance(floor_c, float) or isinstance(floor_c, int): floor_c = np.full(3, floor_c, dtype=np.float32)
if not isinstance(floor_c, np.ndarray): floor_c = np.ascontiguousarray(floor_c, dtype=np.float32)
if len(floor_c.shape) == 1 and floor_c.shape[0] == 3:
if floor_c.dtype != np.float32: floor_c = np.ascontiguousarray(floor_c, dtype=np.float32)
if not floor_c.flags['C_CONTIGUOUS']: floor_c = np.ascontiguousarray(floor_c, dtype=np.float32)
cl_ptr = floor_c.ctypes.data
else:
self._logger.warn("Floor color should be a single value or RGB array.")
if range_x is None: range_x = (np.float32(np.nan), np.float32(np.nan))
if range_z is None: range_z = (np.float32(np.nan), np.float32(np.nan))
if floor_y is None: floor_y = np.float32(np.nan)
try:
self._padlock.acquire()
self._logger.info("Update surface %s, size (%d, %d)...", name, size_xz[1], size_xz[0])
g_handle = self._optix.update_surface(name, mat, size_xz[1], size_xz[0],
pos_ptr, radii_ptr, n_ptr, c_ptr, cl_ptr,
range_x[0], range_x[1], range_z[0], range_z[1],
floor_y)
if (g_handle > 0) and (g_handle == self.geometry_data[name]._handle):
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name]._size = size_xz[0] * size_xz[1]
else:
msg = "Geometry update failed."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def set_surface(self, name: str, pos: Optional[Any] = None,
r: Optional[Any] = None, c: Optional[Any] = None,
normals: Optional[Any] = None,
geom: Union[Geometry, str] = Geometry.Mesh,
mat: Optional[str] = None,
wrap_u: bool = False,
wrap_v: bool = False,
make_normals: bool = False) -> None:
"""Create new or update existing parametric surface geometry.
Data is provided as 2D array of :math:`[x, y, z] = f(u, v)` values, with the shape
``(n, m, 3)``, where ``n`` and ``m`` are at least 2. Additional data features can be
visualized with color (array of RGB values, shape ``(n, m, 3)``) or wireframe thickness
if the :attr:`plotoptix.enums.Geometry.Graph` geometry is used.
Note: not all arguments are used to update existing geeometry. Update is available for:
``mat``, ``pos``, ``c``, ``r``, and ``normals`` data.
Parameters
----------
name : string
Name of the new surface geometry.
pos : array_like, optional
XYZ values of surface points.
r : Any, optional
Radii of vertices for the :attr:`plotoptix.enums.Geometry.Graph` geometry,
interpolated along the wireframe edges. Single value sets constant radius
for all vertices.
c : Any, optional
Colors of surface points. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n, m, 3)`` will set individual color for each surface point,
interpolated between points; ``n`` and ``m`` have to be the same
as in the surface points shape.
normals : array_like, optional
Normal vectors at provided surface points. Array shape has to be ``(n, m, 3)``,
with ``n`` and ``m`` the same as in the surface points shape.
geom : Geometry enum or string, optional
Geometry of the surface, only :attr:`plotoptix.enums.Geometry.Mesh` or
:attr:`plotoptix.enums.Geometry.Graph` are supported.
mat : string, optional
Material name.
wrap_u : bool, optional
Stitch surface edges making U axis continuous.
wrap_v : bool, optional
Stitch surface edges making V axis continuous.
make_normals : bool, optional
Calculate normals for surface points, only if not provided with ``normals``
argument. Normals of all triangles attached to the point are averaged.
See Also
--------
:meth:`plotoptix.NpOptiX.update_surface`
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name in self.geometry_data:
self.update_surface(name, mat=mat, pos=pos, r=r, c=c, normals=normals)
return
if pos is None:
msg = "pos argument required for new geometries."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if r is None: r = np.ascontiguousarray([0.05], dtype=np.float32)
if c is None: c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
if mat is None: mat = "diffuse"
if isinstance(geom, str): geom = Geometry[geom]
if not geom in [Geometry.Mesh, Geometry.Graph]:
msg = "Geometry type %s not supported by the parametric surface." % geom.name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 3 and pos.shape[0] > 1 and pos.shape[1] > 1 and pos.shape[2] == 3, "Required surface points shape is (v,u,3), where u >= 2 and v >= 2."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
pos_ptr = pos.ctypes.data
if r is not None and geom == Geometry.Graph:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1 or r.shape[0] > 1:
assert r.shape == pos.shape[:2], "Radii shape must be (v,u), with u and v matching the surface points shape."
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if r is not None and geom == Geometry.Graph:
if r.shape[0] == 1:
r = np.full(pos.shape[:2], r[0], dtype=np.float32)
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if r.shape != pos.shape[:2]:
msg = "Radii (r) shape does not match the shape of preceding data arguments."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
radii_ptr = r.ctypes.data
else: radii_ptr = 0
n_ptr = 0
if normals is not None and geom == Geometry.Mesh:
if not isinstance(normals, np.ndarray): normals = np.ascontiguousarray(normals, dtype=np.float32)
assert normals.shape == pos.shape, "Normals shape must be (v,u,3), with u and v matching the surface points shape."
if normals.dtype != np.float32: normals = np.ascontiguousarray(normals, dtype=np.float32)
if not normals.flags['C_CONTIGUOUS']: normals = np.ascontiguousarray(normals, dtype=np.float32)
n_ptr = normals.ctypes.data
make_normals = False
c_ptr = 0
c_const_ptr = 0
if c is not None:
if isinstance(c, float) or isinstance(c, int): c = np.full(3, c, dtype=np.float32)
if not isinstance(c, np.ndarray): c = np.ascontiguousarray(c, dtype=np.float32)
if c.dtype != np.float32: c = np.ascontiguousarray(c, dtype=np.float32)
if not c.flags['C_CONTIGUOUS']: c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (3,):
c_const_ptr = c.ctypes.data
elif c.shape == pos.shape:
c_ptr = c.ctypes.data
else:
msg = "Colors shape must be (3,) or (v,u,3), with u and v matching the surface points shape."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
try:
self._padlock.acquire()
self._logger.info("Setup surface %s...", name)
g_handle = self._optix.setup_psurface(geom.value, name, mat, pos.shape[1], pos.shape[0], pos_ptr, radii_ptr, n_ptr, c_const_ptr, c_ptr, wrap_u, wrap_v, make_normals)
if g_handle > 0:
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name] = GeometryMeta(name, g_handle, pos.shape[0] * pos.shape[1], geom)
self.geometry_names[g_handle] = name
else:
msg = "Surface setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def update_surface(self, name: str,
mat: Optional[str] = None,
pos: Optional[Any] = None,
r: Optional[Any] = None,
c: Optional[Any] = None,
normals: Optional[Any] = None) -> None:
"""Update surface geometry data or properties.
Parameters
----------
name : string
Name of the surface geometry.
mat : string, optional
Material name.
pos : array_like, optional
XYZ values of surface points.
r : Any, optional
Radii of vertices for the :attr:`plotoptix.enums.Geometry.Graph` geometry,
interpolated along the edges. Single value sets constant radius for all vertices.
c : Any, optional
Colors of surface points. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n, m, 3)`` will set individual color for each surface point,
interpolated between points; ``n`` and ``m`` have to be the same
as in the surface points shape.
normals : array_like, optional
Normal vectors at provided surface points. Array shape has to be ``(n, m, 3)``,
with ``n`` and ``m`` the same as in the surface points shape.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if mat is None: mat = ""
if not name in self.geometry_data:
msg = "Surface %s does not exists yet, use set_surface() instead." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
s_u = c_uint()
s_v = c_uint()
if not self._optix.get_surface_size(name, byref(s_u), byref(s_v)):
msg = "Cannot get surface %s size." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
size_uv3 = (s_v.value, s_u.value, 3)
size_uv1 = (s_v.value, s_u.value)
size_changed = False
pos_ptr = 0
if pos is not None:
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 3 and pos.shape[0] > 1 and pos.shape[1] > 1 and pos.shape[2] == 3, "Required vertex data shape is (v,u,3), where u >= 2 and v >= 2."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
if pos.shape != size_uv3: size_changed = True
size_uv3 = pos.shape
pos_ptr = pos.ctypes.data
if size_changed and r is None:
r = np.ascontiguousarray([0.05], dtype=np.float32)
if r is not None:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1 or r.shape[0] > 1:
assert r.shape == size_uv1, "Radii shape must be (v,u), with u and v matching the surface points shape."
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
radii_ptr = 0
if r is not None:
if r.shape[0] == 1:
r = np.full(size_uv1, r[0], dtype=np.float32)
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if size_uv1 != r.shape:
msg = "Radii (r) shape does not match the number of surface points."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
radii_ptr = r.ctypes.data
c_ptr = 0
c_const_ptr = 0
if size_changed and c is None: c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
if c is not None:
if isinstance(c, float) or isinstance(c, int): c = np.full(3, c, dtype=np.float32)
if not isinstance(c, np.ndarray): c = np.ascontiguousarray(c, dtype=np.float32)
if c.dtype != np.float32: c = np.ascontiguousarray(c, dtype=np.float32)
if not c.flags['C_CONTIGUOUS']: c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (3,):
c_const_ptr = c.ctypes.data
elif c.shape == size_uv3:
c_ptr = c.ctypes.data
else:
msg = "Colors shape must be (3,) or (v,u,3), with u and v matching the surface points shape."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
n_ptr = 0
if normals is not None:
if not isinstance(normals, np.ndarray): normals = np.ascontiguousarray(normals, dtype=np.float32)
assert normals.shape == size_uv3, "Normals shape must be (v,u,3), with u and v matching the surface points shape."
if normals.dtype != np.float32: normals = np.ascontiguousarray(normals, dtype=np.float32)
if not normals.flags['C_CONTIGUOUS']: normals = np.ascontiguousarray(normals, dtype=np.float32)
n_ptr = normals.ctypes.data
try:
self._padlock.acquire()
self._logger.info("Update surface %s, size (%d, %d)...", name, size_uv1[1], size_uv1[0])
g_handle = self._optix.update_psurface(name, mat, size_uv1[1], size_uv1[0], pos_ptr, radii_ptr, n_ptr, c_const_ptr, c_ptr)
if (g_handle > 0) and (g_handle == self.geometry_data[name]._handle):
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name]._size = size_uv1[0] * size_uv1[1]
else:
msg = "Geometry update failed."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
def set_graph(self, name: str,
pos: Optional[Any] = None, edges: Optional[Any] = None,
r: Optional[Any] = None, c: Optional[Any] = None,
mat: Optional[str] = None) -> None:
"""Create new or update existing graph (mesh wireframe) geometry.
Data is provided as vertices :math:`[x, y, z]`, with the shape ``(n, 3)``, and edges
(doublets of vertex indices), with the shape ``(n, 2)`` or ``(m)`` where :math:`m = 2*n`.
Data features can be visualized with colors (array of RGB values assigned to the graph
vertices, shape ``(n, 3)``) and/or vertex radii.
Note: not all arguments are used to update existing geeometry. Update is available for:
``mat``, ``pos``, ``edges``, ``r``, and ``c`` data.
Parameters
----------
name : string
Name of the new graph geometry.
pos : array_like, optional
XYZ values of the graph vertices.
edges : array_like, optional
Graph edges as indices (doublets) to vertices in the ``pos`` array.
r : Any, optional
Radii of vertices, interpolated along the edges. Single value sets constant
radius for all vertices.
c : Any, optional
Colors of the graph vertices. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n, 3)`` will set individual color for each vertex, interpolated along
the edges; ``n`` has to be equal to the vertex number in ``pos`` array.
mat : string, optional
Material name.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name in self.geometry_data:
self.update_graph(name, mat=mat, pos=pos, edges=edges, r=r, c=c)
return
if pos is None or edges is None:
msg = "pos and edges arguments required for new geometries."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if r is None: r = np.ascontiguousarray([0.05], dtype=np.float32)
if c is None: c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
if mat is None: mat = "diffuse"
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 2 and pos.shape[0] > 1 and pos.shape[1] == 3, "Required vertex data shape is (n,3), where n >= 2."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
pos_ptr = pos.ctypes.data
n_vertices = pos.shape[0]
if not isinstance(edges, np.ndarray): edges = np.ascontiguousarray(edges, dtype=np.int32)
if edges.dtype != np.int32: edges = np.ascontiguousarray(edges, dtype=np.int32)
if not edges.flags['C_CONTIGUOUS']: edges = np.ascontiguousarray(edges, dtype=np.int32)
assert (len(edges.shape) == 2 and edges.shape[1] == 2) or (len(edges.shape) == 1 and (edges.shape[0] % 2 == 0)), "Required index shape is (n,2) or (m), where m is a multiple of 2."
edges_ptr = edges.ctypes.data
n_edges = edges.size // 2
if r is not None:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1: r = r.flatten()
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if r is not None:
if r.shape[0] == 1:
if n_vertices > 0: r = np.full(n_vertices, r[0], dtype=np.float32)
else:
msg = "Cannot resolve proper radii (r) shape from preceding data arguments."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if (n_vertices > 0) and (n_vertices != r.shape[0]):
msg = "Radii (r) shape does not match the shape of preceding data arguments."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
radii_ptr = r.ctypes.data
else: radii_ptr = 0
c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (1,):
c = np.ascontiguousarray([c[0], c[0], c[0]], dtype=np.float32)
col_const_ptr = c.ctypes.data
col_ptr = 0
elif c.shape == (3,):
col_const_ptr = c.ctypes.data
col_ptr = 0
else:
c = _make_contiguous_3d(c, n=n_vertices, extend_scalars=True)
assert c.shape == pos.shape, "Colors shape must be (n,3), with n matching the number of graph vertices."
if c is not None: col_ptr = c.ctypes.data
else: col_ptr = 0
col_const_ptr = 0
try:
self._padlock.acquire()
self._logger.info("Setup graph %s...", name)
g_handle = self._optix.setup_graph(name, mat, n_vertices, n_edges, pos_ptr, radii_ptr, edges_ptr, col_const_ptr, col_ptr)
if g_handle > 0:
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name] = GeometryMeta(name, g_handle, n_vertices, Geometry.Graph)
self.geometry_names[g_handle] = name
else:
msg = "Graph setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
def update_graph(self, name: str,
mat: Optional[str] = None,
pos: Optional[Any] = None,
edges: Optional[Any] = None,
r: Optional[Any] = None,
c: Optional[Any] = None) -> None:
"""Update data of an existing graph (mesh wireframe) geometry.
All data or only selected arrays may be uptated. If vertices and edges are left
unchanged then ``color`` and ``r`` array sizes should match the size of the graph,
i.e. existing ``pos`` shape.
Parameters
----------
name : string
Name of the graph geometry.
mat : string, optional
Material name.
pos : array_like, optional
XYZ values of the graph vertices.
edges : array_like, optional
Graph edges as indices (doublets) to the ``pos`` array.
r : Any, optional
Radii of vertices, interpolated along the edges. Single value sets
constant radius for all vertices.
c : Any, optional
Colors of graph vertices. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n, 3)`` will set individual color for each vertex,
interpolated along edges; ``n`` has to be equal to the vertex
number in ``pos`` array.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if mat is None: mat = ""
if not name in self.geometry_data:
msg = "Graph %s does not exists yet, use set_graph() instead." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
m_vertices = self._optix.get_geometry_size(name)
#m_edges = self._optix.get_edges_count(name)
size_changed = False
pos_ptr = 0
n_vertices = 0
if pos is not None:
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 2 and pos.shape[0] > 1 and pos.shape[1] == 3, "Required vertex data shape is (n,3), where n >= 2."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
if pos.shape[0] != m_vertices: size_changed = True
pos_ptr = pos.ctypes.data
n_vertices = pos.shape[0]
m_vertices = n_vertices
edges_ptr = 0
n_edges = 0
if edges is not None:
if not isinstance(edges, np.ndarray): edges = np.ascontiguousarray(edges, dtype=np.int32)
if edges.dtype != np.int32: edges = np.ascontiguousarray(edges, dtype=np.int32)
if not edges.flags['C_CONTIGUOUS']: edges = np.ascontiguousarray(edges, dtype=np.int32)
assert (len(edges.shape) == 2 and edges.shape[1] == 3) or (len(edges.shape) == 1 and (edges.shape[0] % 2 == 0)), "Required index shape is (n,3) or (m), where m is a multiple of 2."
edges_ptr = edges.ctypes.data
n_edges = edges.size // 2
#m_edges = n_edges
if size_changed and r is None:
r = np.ascontiguousarray([0.05], dtype=np.float32)
if r is not None:
if not isinstance(r, np.ndarray): r = np.ascontiguousarray(r, dtype=np.float32)
if r.dtype != np.float32: r = np.ascontiguousarray(r, dtype=np.float32)
if len(r.shape) > 1: r = r.flatten()
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
radii_ptr = 0
if r is not None:
if r.shape[0] == 1:
r = np.full(m_vertices, r[0], dtype=np.float32)
if not r.flags['C_CONTIGUOUS']: r = np.ascontiguousarray(r, dtype=np.float32)
if m_vertices != r.shape[0]:
msg = "Radii (r) shape does not match the number of graph vertices."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
radii_ptr = r.ctypes.data
c_ptr = 0
c_const_ptr = 0
if size_changed and c is None: c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
if c is not None:
if isinstance(c, float) or isinstance(c, int): c = np.full(3, c, dtype=np.float32)
if not isinstance(c, np.ndarray): c = np.ascontiguousarray(c, dtype=np.float32)
if c.dtype != np.float32: c = np.ascontiguousarray(c, dtype=np.float32)
if not c.flags['C_CONTIGUOUS']: c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (3,):
c_const_ptr = c.ctypes.data
elif c.shape == (m_vertices, 3):
c_ptr = c.ctypes.data
else:
msg = "Colors shape must be (n,3), with n matching the number of graph vertices."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
try:
self._padlock.acquire()
self._logger.info("Update graph %s...", name)
g_handle = self._optix.update_graph(name, mat, m_vertices, n_edges, pos_ptr, radii_ptr, edges_ptr, c_const_ptr, c_ptr)
if (g_handle > 0) and (g_handle == self.geometry_data[name]._handle):
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name]._size = m_vertices
else:
msg = "Graph update failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def set_mesh(self, name: str, pos: Optional[Any] = None, faces: Optional[Any] = None,
c: Any = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32),
normals: Optional[Any] = None,
nidx: Optional[Any] = None,
uvmap: Optional[Any] = None,
uvidx: Optional[Any] = None,
mat: str = "diffuse",
make_normals: bool = False) -> None:
"""Create new or update existing mesh geometry.
Data is provided as vertices :math:`[x, y, z]`, with the shape ``(n, 3)``, and faces
(triplets of vertex indices), with the shape ``(n, 3)`` or ``(m)`` where :math:`m = 3*n`.
Data features can be visualized with color (array of RGB values assigned to the mesh
vertices, shape ``(n, 3)``).
Mesh ``normals`` can be provided as an array of 3D vectors. Mappng of normals to
faces can be provided as an array of ``nidx`` indexes. If mapping is not provided
then face vertex data is used (requires same number of vertices and normal vectors).
Smooth shading normals can be pre-calculated if ``make_normals=True`` and normals
data is not provided.
Texture UV mapping ``uvmap`` can be provided as an array of 2D vectors. Mappng of
UV coordinates to faces can be provided as an array of ``uvidx`` indexes. If mapping
is not provided then face vertex data is used (requires same number of vertices
and UV points).
Note: not all arguments are used to update existing geeometry. Update is available for:
``mat``, ``pos``, ``faces``, ``c``, ``normals``, ``nidx``, ``uvmap`` and ``uvidx`` data.
Parameters
----------
name : string
Name of the new mesh geometry.
pos : array_like, optional
XYZ values of the mesh vertices.
faces : array_like, optional
Mesh faces as indices (triplets) to the ``pos`` array.
c : Any, optional
Colors of mesh vertices. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n, 3)`` will set individual color for each vertex,
interpolated on face surfaces; ``n`` has to be equal to the vertex
number in ``pos`` array.
normals : array_like, optional
Normal vectors.
nidx : array_like, optional
Normal to face mapping, ``faces`` is used if not provided.
uvmap : array_like, optional
Texture UV coordinates.
uvidx : array_like, optional
Texture UV to face mapping, ``faces`` is used if not provided.
mat : string, optional
Material name.
make_normals : bool, optional
Calculate smooth shading of the mesh, only if ``normals`` are not provided.
Normals of all triangles attached to the mesh vertex are averaged.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if name in self.geometry_data:
self.update_mesh(name, mat=mat, pos=pos, faces=faces,
c=c, normals=normals, nidx=nidx, uvmap=uvmap,
uvidx=uvidx)
return
if pos is None or faces is None:
msg = "pos and faces arguments required for new geometries."
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 2 and pos.shape[0] > 2 and pos.shape[1] == 3, "Required vertex data shape is (n,3), where n >= 3."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
pos_ptr = pos.ctypes.data
n_vertices = pos.shape[0]
if not isinstance(faces, np.ndarray): faces = np.ascontiguousarray(faces, dtype=np.int32)
if faces.dtype != np.int32: faces = np.ascontiguousarray(faces, dtype=np.int32)
if not faces.flags['C_CONTIGUOUS']: faces = np.ascontiguousarray(faces, dtype=np.int32)
assert (len(faces.shape) == 2 and faces.shape[1] == 3) or (len(faces.shape) == 1 and (faces.shape[0] % 3 == 0)), "Required index shape is (n,3) or (m), where m is a multiple of 3."
faces_ptr = faces.ctypes.data
n_faces = faces.size // 3
n_colors = 0
c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (1,):
c = np.ascontiguousarray([c[0], c[0], c[0]], dtype=np.float32)
col_const_ptr = c.ctypes.data
col_ptr = 0
elif c.shape == (3,):
col_const_ptr = c.ctypes.data
col_ptr = 0
else:
c = _make_contiguous_3d(c, n=n_vertices, extend_scalars=True)
n_colors = c.shape[0]
assert n_colors == n_vertices or n_colors == n_faces, "Colors shape must be (n,3), with n matching the number of mesh vertices or faces."
if c is not None: col_ptr = c.ctypes.data
else: col_ptr = 0
col_const_ptr = 0
n_ptr = 0
n_normals = 0
if normals is not None:
if not isinstance(normals, np.ndarray): normals = np.ascontiguousarray(normals, dtype=np.float32)
if nidx is None:
assert normals.shape == pos.shape, "If normal index data not provided, normals shape must be (n,3), with n matching the mesh vertex positions shape."
else:
assert len(normals.shape) == 2 and normals.shape[0] > 2 and normals.shape[1] == 3, "Required normals data shape is (n,3), where n >= 3."
if normals.dtype != np.float32: normals = np.ascontiguousarray(normals, dtype=np.float32)
if not normals.flags['C_CONTIGUOUS']: normals = np.ascontiguousarray(normals, dtype=np.float32)
n_ptr = normals.ctypes.data
n_normals = normals.shape[0]
make_normals = False
nidx_ptr = 0
if nidx is not None:
if not isinstance(nidx, np.ndarray): nidx = np.ascontiguousarray(nidx, dtype=np.int32)
if nidx.dtype != np.int32: nidx = np.ascontiguousarray(nidx, dtype=np.int32)
if not nidx.flags['C_CONTIGUOUS']: nidx = np.ascontiguousarray(nidx, dtype=np.int32)
assert np.array_equal(nidx.shape, faces.shape), "Required same shape of normal index and face index arrays."
nidx_ptr = nidx.ctypes.data
uv_ptr = 0
n_uv = 0
if uvmap is not None:
if not isinstance(uvmap, np.ndarray): uvmap = np.ascontiguousarray(uvmap, dtype=np.float32)
if uvidx is None:
assert uvmap.shape[0] == pos.shape[0], "If UV index data not provided, uvmap shape must be (n,2), with n matching the number of mesh vertices."
else:
assert len(uvmap.shape) == 2 and uvmap.shape[0] > 2 and uvmap.shape[1] == 2, "Required UV data shape is (n,2), where n >= 3."
if uvmap.dtype != np.float32: uvmap = np.ascontiguousarray(uvmap, dtype=np.float32)
if not uvmap.flags['C_CONTIGUOUS']: uvmap = np.ascontiguousarray(uvmap, dtype=np.float32)
uv_ptr = uvmap.ctypes.data
n_uv = uvmap.shape[0]
uvidx_ptr = 0
if uvidx is not None:
if not isinstance(uvidx, np.ndarray): uvidx = np.ascontiguousarray(uvidx, dtype=np.int32)
if uvidx.dtype != np.int32: uvidx = np.ascontiguousarray(uvidx, dtype=np.int32)
if not uvidx.flags['C_CONTIGUOUS']: uvidx = np.ascontiguousarray(uvidx, dtype=np.int32)
assert np.array_equal(uvidx.shape, faces.shape), "Required same shape of UV index and face index arrays."
uvidx_ptr = uvidx.ctypes.data
try:
self._padlock.acquire()
self._logger.info("Setup mesh %s...", name)
g_handle = self._optix.setup_mesh(
name, mat,
n_vertices, n_faces, n_colors, n_normals, n_uv,
pos_ptr, faces_ptr, col_const_ptr, col_ptr, n_ptr, nidx_ptr, uv_ptr, uvidx_ptr,
make_normals
)
if g_handle > 0:
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name] = GeometryMeta(name, g_handle, n_vertices, Geometry.Mesh)
self.geometry_names[g_handle] = name
else:
msg = "Mesh setup failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def update_mesh(self, name: str,
mat: Optional[str] = None,
pos: Optional[Any] = None,
faces: Optional[Any] = None,
c: Optional[Any] = None,
normals: Optional[Any] = None,
nidx: Optional[Any] = None,
uvmap: Optional[Any] = None,
uvidx: Optional[Any] = None) -> None:
"""Update data of an existing mesh geometry.
All data or only some of arrays may be uptated. If vertices and faces are left
unchanged then other arrays sizes should match the sizes of the mesh, i.e. ``c``
shape should match existing ``pos`` shape, ``nidx`` and ``uvidx`` shapes should
match ``faces`` shape or if index mapping is not provided then ``normals`` and
``uvmap`` shapes should match ``pos`` shape.
Parameters
----------
name : string
Name of the mesh geometry.
mat : string, optional
Material name.
pos : array_like, optional
XYZ values of the mesh vertices.
faces : array_like, optional
Mesh faces as indices (triplets) to the ``pos`` array.
c : Any, optional
Colors of mesh vertices. Single value means a constant gray level.
3-component array means a constant RGB color. Array of the shape
``(n, 3)`` will set individual color for each vertex,
interpolated on face surfaces; ``n`` has to be equal to the vertex
number in ``pos`` array.
normals : array_like, optional
Normal vectors.
nidx : array_like, optional
Normal to face mapping, existing mesh ``faces`` is used if not provided.
uvmap : array_like, optional
Texture UV coordinates.
uvidx : array_like, optional
Texture UV to face mapping, existing mesh ``faces`` is used if not provided.
"""
if name is None: raise ValueError()
if not isinstance(name, str): name = str(name)
if mat is None: mat = ""
if not name in self.geometry_data:
msg = "Mesh %s does not exists yet, use set_mesh() instead." % name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
m_vertices = self._optix.get_geometry_size(name)
m_faces = self._optix.get_faces_count(name)
size_changed = False
pos_ptr = 0
n_vertices = 0
if pos is not None:
if not isinstance(pos, np.ndarray): pos = np.ascontiguousarray(pos, dtype=np.float32)
assert len(pos.shape) == 2 and pos.shape[0] > 2 and pos.shape[1] == 3, "Required vertex data shape is (n,3), where n >= 3."
if pos.dtype != np.float32: pos = np.ascontiguousarray(pos, dtype=np.float32)
if not pos.flags['C_CONTIGUOUS']: pos = np.ascontiguousarray(pos, dtype=np.float32)
if pos.shape[0] != m_vertices: size_changed = True
pos_ptr = pos.ctypes.data
n_vertices = pos.shape[0]
m_vertices = n_vertices
faces_ptr = 0
n_faces = 0
if faces is not None:
if not isinstance(faces, np.ndarray): faces = np.ascontiguousarray(faces, dtype=np.int32)
if faces.dtype != np.int32: faces = np.ascontiguousarray(faces, dtype=np.int32)
if not faces.flags['C_CONTIGUOUS']: faces = np.ascontiguousarray(faces, dtype=np.int32)
assert (len(faces.shape) == 2 and faces.shape[1] == 3) or (len(faces.shape) == 1 and (faces.shape[0] % 3 == 0)), "Required index shape is (n,3) or (m), where m is a multiple of 3."
faces_ptr = faces.ctypes.data
n_faces = faces.size // 3
m_faces = n_faces
c_ptr = 0
c_const_ptr = 0
if size_changed and c is None: c = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32)
if c is not None:
if isinstance(c, float) or isinstance(c, int): c = np.full(3, c, dtype=np.float32)
if not isinstance(c, np.ndarray): c = np.ascontiguousarray(c, dtype=np.float32)
if c.dtype != np.float32: c = np.ascontiguousarray(c, dtype=np.float32)
if not c.flags['C_CONTIGUOUS']: c = np.ascontiguousarray(c, dtype=np.float32)
if c.shape == (3,):
c_const_ptr = c.ctypes.data
elif c.shape == (m_vertices, 3):
c_ptr = c.ctypes.data
else:
msg = "Colors shape must be (n,3), with n matching the number of mesh vertices."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
n_ptr = 0
n_normals = 0
if normals is not None:
if not isinstance(normals, np.ndarray): normals = np.ascontiguousarray(normals, dtype=np.float32)
if nidx is None:
assert normals.shape[0] == m_vertices, "If normal index data not provided, normals shape must be (n,3), with n matching the mesh vertex positions shape."
else:
assert len(normals.shape) == 2 and normals.shape[0] > 2 and normals.shape[1] == 3, "Required normals data shape is (n,3), where n >= 3."
if normals.dtype != np.float32: normals = np.ascontiguousarray(normals, dtype=np.float32)
if not normals.flags['C_CONTIGUOUS']: normals = np.ascontiguousarray(normals, dtype=np.float32)
n_ptr = normals.ctypes.data
n_normals = normals.shape[0]
make_normals = False
nidx_ptr = 0
if nidx is not None:
if not isinstance(nidx, np.ndarray): nidx = np.ascontiguousarray(nidx, dtype=np.int32)
if nidx.dtype != np.int32: nidx = np.ascontiguousarray(nidx, dtype=np.int32)
if not nidx.flags['C_CONTIGUOUS']: nidx = np.ascontiguousarray(nidx, dtype=np.int32)
assert (len(nidx.shape) == 2 and nidx.shape[0] == m_faces) or (len(nidx.shape) == 1 and nidx.shape[0] == 3 * m_faces), "Required same shape of normal index and face index arrays."
nidx_ptr = nidx.ctypes.data
uv_ptr = 0
n_uv = 0
if uvmap is not None:
if not isinstance(uvmap, np.ndarray): uvmap = np.ascontiguousarray(uvmap, dtype=np.float32)
if uvidx is None:
assert uvmap.shape[0] == m_vertices, "If UV index data not provided, uvmap shape must be (n,2), with n matching the number of mesh vertices."
else:
assert len(uvmap.shape) == 2 and uvmap.shape[0] > 2 and uvmap.shape[1] == 2, "Required UV data shape is (n,2), where n >= 3."
if uvmap.dtype != np.float32: uvmap = np.ascontiguousarray(uvmap, dtype=np.float32)
if not uvmap.flags['C_CONTIGUOUS']: uvmap = np.ascontiguousarray(uvmap, dtype=np.float32)
uv_ptr = uvmap.ctypes.data
n_uv = uvmap.shape[0]
uvidx_ptr = 0
if uvidx is not None:
if not isinstance(uvidx, np.ndarray): uvidx = np.ascontiguousarray(uvidx, dtype=np.int32)
if uvidx.dtype != np.int32: uvidx = np.ascontiguousarray(uvidx, dtype=np.int32)
if not uvidx.flags['C_CONTIGUOUS']: uvidx = np.ascontiguousarray(uvidx, dtype=np.int32)
assert (len(uvidx.shape) == 2 and uvidx.shape[0] == m_faces) or (len(uvidx.shape) == 1 and uvidx.shape[0] == 3 * m_faces), "Required same shape of UV index and face index arrays."
uvidx_ptr = uvidx.ctypes.data
try:
self._padlock.acquire()
self._logger.info("Update mesh %s...", name)
g_handle = self._optix.update_mesh(name, mat, n_vertices, n_faces, n_normals, n_uv, pos_ptr, faces_ptr, c_const_ptr, c_ptr, n_ptr, nidx_ptr, uv_ptr, uvidx_ptr)
if (g_handle > 0) and (g_handle == self.geometry_data[name]._handle):
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[name]._size = m_vertices
else:
msg = "Mesh update failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def load_mesh_obj(self, file_name: str, mesh_name: Optional[str] = None, parent: Optional[str] = None,
c: Any = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32),
mat: str = "diffuse",
make_normals: bool = False) -> None:
"""Load mesh geometry from Wavefront .obj file.
Note: this method can read files with named objects only. Use :meth:`plotoptix.NpOptiX.load_merged_mesh_obj`
for reading files with raw, unnamed mesh.
Parameters
----------
file_name : string
File name (local file path or url) to read from.
mesh_name : string, optional
Name of the mesh to import from the file. All meshes are imported
if ``None`` value or empty string is used.
parent : string, optional
Optional name of a mesh to set as a parent of all other meshes loaded from the file. All transformations
applied to the parent will be applied to children meshes as well.
c : Any, optional
Color of the mesh. Single value means a constant gray level.
3-component array means constant RGB color.
mat : string, optional
Material name.
make_normals : bool, optional
If set to ``True`` and .obj file does not contain normals then normals
are calculated for each vertex by averaging normals of connected
triangles. If set to ``False`` (default) then normals from the .obj file
are used or precalculated normals are not used (normals are calculated
from the mesh triangles during ray tracing).
"""
if file_name is None: raise ValueError()
if not isinstance(file_name, str): file_name = str(file_name)
if mesh_name is None: mesh_name = ""
if not isinstance(mesh_name, str): mesh_name = str(mesh_name)
if parent is None: parent = ""
if not isinstance(parent, str): parent = str(parent)
if mesh_name in self.geometry_data:
msg = "Geometry %s already exists, use update_mesh() instead." % mesh_name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
c = _make_contiguous_vector(c, n_dim=3)
if c is not None: col_ptr = c.ctypes.data
else: col_ptr = 0
try:
self._padlock.acquire()
self._logger.info("Load mesh from file %s ...", file_name)
s = self._optix.load_mesh_obj(file_name, mesh_name, parent, mat, col_ptr, make_normals)
if len(s) > 2:
meta = json.loads(s)
for key, value in meta.items():
self.geometry_data[key] = GeometryMeta(key, value["Handle"], value["Size"], Geometry(value["Type"]))
self.geometry_names[value["Handle"]] = key
self._logger.info("...loaded: %s %s (%d vertices)", key, value["Size"], value["Type"])
else:
msg = "Mesh loading failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def load_multiple_mesh_obj(self, file_name: str, mat: dict, default: str = "diffuse",
parent: Optional[str] = None) -> None:
"""Load meshesh from Wavefront .obj file, assign materials from dictrionary.
Note: this method can read files with named objects only. Use :meth:`plotoptix.NpOptiX.load_merged_mesh_obj`
for reading files with a raw, unnamed mesh.
Parameters
----------
file_name : string
File name (local file path or url) to read from.
mat : dict
Mesh name to material name dictionary. All meshes with names starting with keys in ``dict`` will have
corresponding material assigned.
default : string, optional
Default material name, assigned if mesh name not fount in ``mat``.
parent : string, optional
Optional full name of a mesh to set as a parent of all other meshes loaded from the file. All transformations
applied to the parent will be applied to children meshes as well.
"""
if file_name is None: raise ValueError()
if not isinstance(file_name, str): file_name = str(file_name)
if parent is None: parent = ""
if not isinstance(parent, str): parent = str(parent)
for n in mat:
if not isinstance(mat[n], str):
mat[n] = str(mat[n])
try:
self._padlock.acquire()
self._logger.info("Load mesh from file %s ...", file_name)
s = self._optix.load_multiple_mesh_obj(file_name, json.dumps(mat), default, parent)
if len(s) > 2:
meta = json.loads(s)
for key, value in meta.items():
self.geometry_data[key] = GeometryMeta(key, value["Handle"], value["Size"], Geometry(value["Type"]))
self.geometry_names[value["Handle"]] = key
self._logger.info("...loaded: %s %s (%d vertices)", key, value["Type"], value["Size"])
else:
msg = "Mesh loading failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def load_merged_mesh_obj(self, file_name: str, mesh_name: str,
c: Any = np.ascontiguousarray([0.94, 0.94, 0.94], dtype=np.float32),
mat: str = "diffuse", make_normals: bool = False) -> None:
"""Load and merge mesh geometries from Wavefront .obj file.
All objects are imported from file and merged in a single PlotOptiX mesh. This method
can read files with no named objects specified.
Parameters
----------
file_name : string
File name (local file path or url) to read from.
mesh_name : string
Name of the new mesh geometry.
c : Any, optional
Color of the mesh. Single value means a constant gray level.
3-component array means constant RGB color.
mat : string, optional
Material name.
make_normals : bool, optional
If set to ``True`` and .obj file does not contain normals then normals
are calculated for each vertex by averaging normals of connected
triangles. If set to ``False`` (default) then normals from the .obj file
are used or precalculated normals are not used (normals are calculated
from the mesh triangles during ray tracing).
"""
if file_name is None or mesh_name is None: raise ValueError()
if not isinstance(file_name, str): file_name = str(file_name)
if not isinstance(mesh_name, str): mesh_name = str(mesh_name)
if mesh_name in self.geometry_data:
msg = "Geometry %s already exists, use update_mesh() instead." % mesh_name
self._logger.error(msg)
if self._raise_on_error: raise ValueError(msg)
return
c = _make_contiguous_vector(c, n_dim=3)
if c is not None: col_ptr = c.ctypes.data
else: col_ptr = 0
try:
self._padlock.acquire()
self._logger.info("Load and merge meshes from file %s ...", file_name)
g_handle = self._optix.load_merged_mesh_obj(file_name, mesh_name, mat, col_ptr, make_normals)
if g_handle > 0:
self._logger.info("...done, handle: %d", g_handle)
self.geometry_data[mesh_name] = GeometryMeta(mesh_name, g_handle, self._optix.get_geometry_size(mesh_name))
self.geometry_names[g_handle] = mesh_name
else:
msg = "Mesh loading failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
except Exception as e:
self._logger.error(str(e))
if self._raise_on_error: raise
finally:
self._padlock.release()
[docs] def get_geometry_names(self) -> list:
"""Return list of geometries' names.
"""
return list(self.geometry_data.keys())
[docs] def move_geometry(self, name: str, v: Tuple[float, float, float],
update: bool = True) -> None:
"""Move all primitives by (x, y, z) vector.
Updates GPU buffers immediately if update is set to ``True`` (default),
otherwise update should be made using :meth:`plotoptix.NpOptiX.update_geom_buffers`
after all geometry modifications are finished.
Parameters
----------
name : string
Name of the geometry.
v : tuple (float, float, float)
(X, Y, Z) shift.
update : bool, optional
Update GPU buffer.
"""
if name is None: raise ValueError()
if not self._optix.move_geometry(name, v[0], v[1], v[2], update):
msg = "Geometry move failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def move_primitive(self, name: str, idx: int, v: Tuple[float, float, float],
update: bool = True) -> None:
"""Move selected primitive by (x, y, z) vector.
Updates GPU buffers immediately if update is set to ``True`` (default),
otherwise update should be made using :meth:`plotoptix.NpOptiX.update_geom_buffers`
after all geometry modifications are finished.
Parameters
----------
name : string
Name of the geometry.
idx : int
Primitive index.
v : tuple (float, float, float)
(X, Y, Z) shift.
update : bool, optional
Update GPU buffer.
"""
if name is None: raise ValueError()
if not self._optix.move_primitive(name, idx, v[0], v[1], v[2], update):
msg = "Primitive move failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def rotate_geometry(self, name: str, rot: Tuple[float, float, float],
center: Optional[Tuple[float, float, float]] = None,
update: bool = True) -> None:
"""Rotate all primitives by specified degrees.
Rotate all primitives by specified degrees around x, y, z axis, with
respect to the center of the geometry. Update GPU buffers immediately
if update is set to ``True`` (default), otherwise update should be made using
:meth:`plotoptix.NpOptiX.update_geom_buffers` after all geometry modifications
are finished.
Parameters
----------
name : string
Name of the geometry.
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
center : tuple (float, float, float), optional
Rotation center. If not provided, rotation is made about the geometry
center.
update : bool, optional
Update GPU buffer.
"""
if name is None: raise ValueError()
if center is None:
if not self._optix.rotate_geometry(name, rot[0], rot[1], rot[2], update):
msg = "Geometry rotate failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(center, tuple): center = tuple(center)
if not self._optix.rotate_geometry_about(name, rot[0], rot[1], rot[2], center[0], center[1], center[2], update):
msg = "Geometry rotate failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def rotate_primitive(self, name: str, idx: int, rot: Tuple[float, float, float],
center: Optional[Tuple[float, float, float]] = None,
update: bool = True) -> None:
"""Rotate selected primitive by specified degrees.
Rotate selected primitive by specified degrees around x, y, z axis, with
respect to the center of the selected primitive. Update GPU buffers
immediately if update is set to ``True`` (default), otherwise update should be
made using :meth:`plotoptix.NpOptiX.update_geom_buffers` after all geometry
modifications are finished.
Parameters
----------
name : string
Name of the geometry.
idx : int
Primitive index.
rot : tuple (float, float, float)
Rotation around (X, Y, Z) axis.
center : tuple (float, float, float), optional
Rotation center. If not provided, rotation is made about the primitive
center.
update : bool, optional
Update GPU buffer.
"""
if name is None: raise ValueError()
if center is None:
if not self._optix.rotate_primitive(name, idx, rot[0], rot[1], rot[2], update):
msg = "Primitive rotate failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(center, tuple): center = tuple(center)
if not self._optix.rotate_primitive_about(name, idx, rot[0], rot[1], rot[2], center[0], center[1], center[2], update):
msg = "Geometry rotate failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def scale_geometry(self, name: str, s: Union[float, Tuple[float, float, float]],
center: Optional[Tuple[float, float, float]] = None,
update: bool = True) -> None:
"""Scale all primitive's positions and sizes.
Scale all primitive's positions and sizes by specified factor, with respect
to the center of the geometry. Update GPU buffers immediately if update is
set to ``True`` (default), otherwise update should be made using
:meth:`plotoptix.NpOptiX.update_geom_buffers` after all geometry modifications
are finished.
Parameters
----------
name : string
Name of the geometry.
s : float, tuple (float, float, float)
Scaling factor, single value or (x, y, z) scales.
center : tuple (float, float, float), optional
Scaling center. If not provided, scaling is made w.r.t. the primitive center.
update : bool, optional
Update GPU buffer.
"""
if name is None: raise ValueError()
if isinstance(s, float) or isinstance(s, int):
s = float(s)
if center is None:
if not self._optix.scale_geometry(name, s, update):
msg = "Geometry scale by scalar failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(center, tuple): center = tuple(center)
if not self._optix.scale_geometry_c(name, s, center[0], center[1], center[2], update):
msg = "Geometry scale by scalar w.r.t. the center failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(s, tuple): s = tuple(s)
if center is None:
if not self._optix.scale_geometry_xyz(name, s[0], s[1], s[2], update):
msg = "Geometry scale by vector failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(center, tuple): center = tuple(center)
if not self._optix.scale_geometry_xyz_c(name, s[0], s[1], s[2], center[0], center[1], center[2], update):
msg = "Geometry scale by vector w.r.t. the center failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def scale_primitive(self, name: str, idx: int, s: Union[float, Tuple[float, float, float]],
center: Optional[Tuple[float, float, float]] = None,
update: bool = True) -> None:
"""Scale selected primitive.
Scale selected primitive by specified factor, with respect to the center of
the selected primitive. Update GPU buffers immediately if update is set to
``True`` (default), otherwise update should be made using
:meth:`plotoptix.NpOptiX.update_geom_buffers` after all geometry modifications
are finished.
Parameters
----------
name : string
Name of the geometry.
idx : int
Primitive index.
s : float, tuple (float, float, float)
Scaling factor, single value or (x, y, z) scales.
center : tuple (float, float, float), optional
Scaling center. If not provided, scaling is made w.r.t. the primitive center.
update : bool, optional
Update GPU buffer.
"""
if name is None: raise ValueError()
if isinstance(s, float) or isinstance(s, int):
s = float(s)
if center is None:
if not self._optix.scale_primitive(name, idx, s, update):
msg = "Primitive scale by scalar failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(center, tuple): center = tuple(center)
if not self._optix.scale_primitive_c(name, idx, s, center[0], center[1], center[2], update):
msg = "Primitive scale by scalar w.r.t. the center failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(s, tuple): s = tuple(s)
if center is None:
if not self._optix.scale_primitive_xyz(name, idx, s[0], s[1], s[2], update):
msg = "Primitive scale by vector failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
else:
if not isinstance(center, tuple): center = tuple(center)
if not self._optix.scale_primitive_xyz_c(name, idx, s[0], s[1], s[2], center[0], center[1], center[2], update):
msg = "Primitive scale by vector w.r.t. the center failed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def update_geom_buffers(self, name: str,
mask: Union[GeomBuffer, str] = GeomBuffer.All,
forced: bool = False) -> None:
"""Update geometry buffers.
Update geometry buffers in GPU after modifications made with
:meth:`plotoptix.NpOptiX.move_geometry`, :meth:`plotoptix.NpOptiX.move_primitive`,
and similar methods.
Parameters
----------
name : string
Name of the geometry.
mask : GeomBuffer or string, optional
Which buffers to update. All buffers if not specified.
forced : bool, optional
Update even if the object was not tagged as outdated. Operations like rotations,
scaling, shifts, are setting "out of date" flag automatically, but you'll need
forced update after direct modifications of memory buffers accessed with via
:class:`plotoptix.geometry.PinnedBuffer`.
"""
if name is None: raise ValueError()
if isinstance(mask, str): mask = GeomBuffer[mask]
if not self._optix.update_geom_buffers(name, mask.value, forced):
msg = "Geometry %s buffers update failed." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
[docs] def delete_geometry(self, name: str) -> None:
"""Remove geometry from the scene.
Parameters
----------
name : string
Name of the geometry.
"""
if name is None: raise ValueError()
if not name in self.geometry_data:
msg = "Geometry %s not found." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
return
if not self._optix.delete_geometry(name):
msg = "Geometry %s not removed." % name
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)
return
handle = self.geometry_data[name]._handle
del self.geometry_names[handle]
del self.geometry_data[name]
[docs] def set_coordinates(self, mode: Union[Coordinates, str] = Coordinates.Box, thickness: float = 1.0) -> None:
"""Set style of the coordinate system geometry (or hide it).
Parameters
----------
mode : Coordinates enum or string, optional
Style of the coordinate system geometry.
thickness : float, optional
Thickness of lines.
See Also
--------
:class:`plotoptix.enums.Coordinates`
"""
if mode is None: raise ValueError()
if isinstance(mode, str): mode = Coordinates[mode]
if self._optix.set_coordinates_geom(mode.value, thickness):
self._logger.info("Coordinate system mode set to: %s.", mode.name)
else:
msg = "Coordinate system mode not changed."
self._logger.error(msg)
if self._raise_on_error: raise RuntimeError(msg)