Source code for plotoptix.npoptix

"""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)