diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f2be95d3..818f6084 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,20 @@ Release history =============== +`v0.16.0` - 2023-05-17 +---------------------- + +Added +~~~~~ + +- set texture data from pytorch tensors +- texture filter modes enabled, default is *trilinear*, optionally can be changed to *nearest* + +- set geometry data directly from numpy array or pytorch tensor (no host copy is mada) +- synchronization method to update host copy with data from gpu: NpOptiX.sync_raw_data() + +- convenience method to get a copy of geometry data (more simple than PinnedBuffer, though returns a copy): NpOptiX.get_data() + `v0.15.1` - 2023-03-16 ---------------------- @@ -568,6 +582,7 @@ Added - this changelog, markdown description content type tag for PyPI - use [Semantic Versioning](https://semver.org/spec/v2.0.0.html) +.. _`v0.16.0`: https://github.com/rnd-team-dev/plotoptix/releases/tag/v0.16.0 .. _`v0.15.1`: https://github.com/rnd-team-dev/plotoptix/releases/tag/v0.15.1 .. _`v0.15.0`: https://github.com/rnd-team-dev/plotoptix/releases/tag/v0.15.0 .. _`v0.14.4`: https://github.com/rnd-team-dev/plotoptix/releases/tag/v0.14.4 diff --git a/README.rst b/README.rst index beeed88e..7faf3ee6 100644 --- a/README.rst +++ b/README.rst @@ -15,13 +15,14 @@ PlotOptiX `Docs `__ -- Check what we are doing with PlotOptiX on `Behance `__, `Facebook `__, and `Instagram `__. +- Have a look what is possible with PlotOptiX: `Behance `__, `Instagram `__, and `Facebook `__. - Join us on `Patreon `__ for news, release plans and hi-res content. PlotOptiX is a 3D `ray tracing `__ package for Python, aimed at easy and aesthetic visualization of large datasets (and small as well). Data features can be represented in images as a position, size/thickness and color of primitives of several basic shapes, or projected onto surfaces of objects in form of a color textures and displacement maps. Triangular meshes, -generated in the code or loaded from a file, are supported as well. All is finished with a photorealistic lighting, depth of field, and many other physically based effects simulated with a high quality. +generated in the code or loaded from a file, are supported as well. All is finished with a photorealistic lighting, depth of field, and many other +physically based effects simulated with a high quality. No need to write shaders, intersection algorithms, handle 3D scene technicalities. Basic usage is even more simple than with `matplotlib `__: @@ -65,8 +66,9 @@ Features - *light sources*: spherical and parallelogram, light emission in volumes, uniform environmental light or environment map - *post-processing*: tonal correction curves, levels adjustment, apply mask/overlay, AI denoiser and upsampler - *callbacks*: at the scene initialization, start and end of each frame raytracing, end of progressive accumulation -- 8/16/32bps(hdr) image output to `numpy `__ array, or save to popular image file formats +- 8/16/32bps(hdr) image output to `numpy `__ array, or save to popular image file formats - zero-copy access to GPU buffers wrapped in ndarrays: 8/32bpc image, hit and object info, albedo, normals +- direct access to `PyTorch `__ tensors data stored on GPU (and CPU as well) for texture and geometry updates - GPU acceleration using RT Cores and everything else what comes with `OptiX `__ - hardware accelerated video output to MP4 file format using `NVENC 9.0 `__ - Tkinter based simple GUI window or a headless raytracer @@ -95,7 +97,7 @@ What's Included Installation ============ -**Note**, at this point, PlotOptiX binaries are tested in: Windows 10/11 (any Python 3), and Linux (Python 3.8 recommended): Ubuntu 18.04, CentOS 7. +**Note**, at this point, PlotOptiX binaries are tested in: Windows 10/11 (any Python 3), and Linux (Python 3.8-3.10 recommended): Ubuntu 18.04, CentOS 7. PlotOptiX was also successfully tested on the `Google Cloud Platform `__, using Compute Engine instance with 2x V100 GPU's and Ubuntu 18.04 image. Here are the `installation steps `__ so you can save some precious seconds (FFmpeg not included). @@ -107,15 +109,7 @@ Windows prerequisites *.NET Framework:* -Most likely you already got the right version with your Windows installation. Just in case of doubts, here is the command verifying this:: - - C:\>reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\full" /v version - - HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\full - version REG_SZ 4.8.04084 - -If the number in your output is < 4.8, visit `download page `__ and -install the most recent release. +You have it built in your Windows. Go ahead and install PlotOptiX. Linux prerequisites ------------------- @@ -191,21 +185,14 @@ Then, try running code from the top of this readme, or one of the examples. You Development path ================ -This is still an early version. There are some important features not available yet, eg. ticks and labels on plot axes. +This is still an experimental version in many aspects. PlotOptiX is basically an interface to RnD.SharpOptiX library which we are developing and using in our Studio. RnD.SharpOptiX offers -much more functionality than it is now available through PlotOptiX. We'll progressively add more to PlotOptiX if there is interest in -this project (download, star, and `become our Patron `__ -if you like it!). - -The idea for development is: - -1. Binaries for Linux (done in v0.3.0). -2. Migrate to OptiX 7.0 (done in v0.7.0). -3. Complete the plot layout / cover more raytracing features. -4. Convenience functions for various plot styles. Other GUI's. +much more functionality than it is now available through PlotOptiX. We progressively add more to PlotOptiX based mostly on the interest +of our patrons and applications that this project supports. - *Here, the community input is possible and warmly welcome!* +Download, star, and `become our Patron `__ if you like the project. Get in touch, share your use case, +we are always happy to help and take part in exciting ideas of our users. Examples ======== diff --git a/docs/npoptix.rst b/docs/npoptix.rst index 2f92e8bd..c873a432 100644 --- a/docs/npoptix.rst +++ b/docs/npoptix.rst @@ -25,7 +25,7 @@ basic rules are: .. image:: images/flow_1.png :alt: PlotOptiX compute flow -Fig. 1. PlotOptiX computations flow. Details of the OptiX tast are ommited for clarity (i.e. scene compilation and multi-gpu management). +Fig. 1. PlotOptiX computations flow. Details of the OptiX task are ommited for clarity (i.e. scene compilation and multi-gpu management). .. image:: images/flow_2.png :alt: PlotOptiX compute flow diff --git a/docs/npoptix_geometry.rst b/docs/npoptix_geometry.rst index 57d871f2..beef218c 100644 --- a/docs/npoptix_geometry.rst +++ b/docs/npoptix_geometry.rst @@ -1,10 +1,11 @@ -Plot geometry -============= +Geometry +======== Create, load, update, and remove a plot --------------------------------------- .. automethod:: plotoptix.NpOptiX.get_geometry_names +.. automethod:: plotoptix.NpOptiX.get_data .. automethod:: plotoptix.NpOptiX.set_data .. automethod:: plotoptix.NpOptiX.update_data .. automethod:: plotoptix.NpOptiX.set_data_2d @@ -19,10 +20,17 @@ Create, load, update, and remove a plot .. automethod:: plotoptix.NpOptiX.load_displacement .. automethod:: plotoptix.NpOptiX.delete_geometry -Direct modifications of data ----------------------------- +Direct access and modifications of data +--------------------------------------- + +**Fast updates:** + +.. automethod:: plotoptix.NpOptiX.update_raw_data +.. automethod:: plotoptix.NpOptiX.sync_raw_data + +**Geometry modifications:** -These methods allow making changes to properties of data points +Following methods allow making changes to properties of data points stored internally in the raytracer, without re-sending whole data arrays from the Python code. diff --git a/examples/2_animations_and_callbacks/5_pytorch_texture.py b/examples/2_animations_and_callbacks/5_pytorch_texture.py new file mode 100644 index 00000000..48f17e31 --- /dev/null +++ b/examples/2_animations_and_callbacks/5_pytorch_texture.py @@ -0,0 +1,73 @@ +""" +Pytorch texture source - game of life. +""" + +import torch +import torch.nn.functional as F + +from plotoptix import TkOptiX +from plotoptix.materials import m_flat + + +class params(): + if torch.cuda.is_available: + device = torch.device('cuda') + dtype = torch.float16 + else: + device = torch.device('cpu') + dtype = torch.float32 + w = torch.tensor( + [[[[1.0,1.0,1.0], [1.0,0.0,1.0], [1.0,1.0,1.0]]]], + dtype=dtype, device=device, requires_grad=False + ) + cells = torch.rand((1,1,128,128), dtype=dtype, device=device, requires_grad=False) + cells[cells > 0.995] = 1.0 + cells[cells < 1.0] = 0.0 + tex2D = torch.unsqueeze(cells[0, 0].type(torch.float32), -1).expand(-1, -1, 4).contiguous() + + +# Update texture data with a simple "game of life" rules. +def compute(rt, delta): + params.cells = F.conv2d(params.cells, weight=params.w, stride=1, padding=1) + params.cells[params.cells < 2.0] = 0.0 + params.cells[params.cells > 3.0] = 0.0 + params.cells[params.cells != 0.0] = 1.0 + + # Conversion to float32 and to contiguous memoty layout is ensured by plotoptix, + # though you may wamt to make it explicit like here, eg for performance reasons. + params.tex2D = torch.unsqueeze(params.cells[0, 0].type(torch.float32), -1).expand(-1, -1, 4).contiguous() + + +# Copy texture data to plotoptix scene. +def update_data(rt): + rt.set_torch_texture_2d("tex2d", params.tex2D, refresh=True) + + +def main(): + rt = TkOptiX( + on_scene_compute=compute, + on_rt_completed=update_data + ) + rt.set_param(min_accumulation_step=1) + rt.set_background(0) + rt.set_ambient(0) + + # NOTE: pytorch features are not enabled by default. You need + # to call this method before using anything related to pytorch. + rt.enable_torch() + + rt.set_torch_texture_2d("tex2d", params.tex2D, addr_mode="Clamp", filter_mode="Nearest") + m_flat["ColorTextures"] = [ "tex2d" ] + rt.setup_material("flat", m_flat) + + rt.setup_camera("cam1", eye=[0, 0, 3], target=[0, 0, 0], fov=35, glock=True) + + rt.set_data("plane", geom="Parallelograms", mat="flat", + pos=[-1, -1, 0], u=[2, 0, 0], v=[0, 2, 0], c=0.9 + ) + + rt.start() + +if __name__ == '__main__': + main() + diff --git a/examples/2_animations_and_callbacks/6_direct_data_update.py b/examples/2_animations_and_callbacks/6_direct_data_update.py new file mode 100644 index 00000000..8b325fd1 --- /dev/null +++ b/examples/2_animations_and_callbacks/6_direct_data_update.py @@ -0,0 +1,64 @@ +""" +Direct update of geometry data on device. + +This example uses numpy arrays, but pytorch tensors are supported as well, +just note that rt.enable_torch() call is required before using pytorch. +""" + +import numpy as np + +from plotoptix import TkOptiX +from plotoptix.utils import simplex + + +class params(): + n = 100 + rx = (-20, 20) + r = 0.85 * 0.5 * (rx[1] - rx[0]) / (n - 1) + + x = np.linspace(rx[0], rx[1], n) + z = np.linspace(rx[0], rx[1], n) + X, Z = np.meshgrid(x, z) + + data = np.stack([X.flatten(), np.zeros(n*n), Z.flatten()], axis=1) + t = 0 + +# Compute updated geometry data. +def compute(rt, delta): + row = np.ones((params.data.shape[0], 1)) + xn = simplex(np.concatenate([params.data, params.t * row], axis=1)) + yn = simplex(np.concatenate([params.data, (params.t + 20) * row], axis=1)) + zn = simplex(np.concatenate([params.data, (params.t - 20) * row], axis=1)) + dv = np.stack([xn, yn, zn], axis=1) + params.data += 0.02 * dv + params.t += 0.05 + +# Fast copy to geometry buffer on device, without making a host copy. +def update_data(rt): + rt.update_raw_data("balls", pos=params.data) + + +def main(): + rt = TkOptiX( + on_scene_compute=compute, + on_rt_completed=update_data + ) + rt.set_param(min_accumulation_step=10, max_accumulation_frames=16) + rt.set_background(0) + rt.set_ambient(0) + + rt.setup_camera("cam1", cam_type="ThinLens", eye=[3.5, 1.27, 3.5], target=[0, 0, 0], fov=30, glock=True) + rt.setup_light("light1", pos=[4, 5, 5], color=18, radius=1.0) + + exposure = 1.0; gamma = 2.2 + rt.set_float("tonemap_exposure", exposure) + rt.set_float("tonemap_gamma", gamma) + rt.add_postproc("Gamma") + + rt.set_data("balls", pos=params.data, c=0.82, r=params.r) + + rt.start() + +if __name__ == '__main__': + main() + diff --git a/plotoptix/__init__.py b/plotoptix/__init__.py index 3ae12358..c084435d 100644 --- a/plotoptix/__init__.py +++ b/plotoptix/__init__.py @@ -12,8 +12,8 @@ __author__ = "Robert Sulej, R&D Team " __status__ = "beta" -__version__ = "0.15.1" -__date__ = "16 March 2023" +__version__ = "0.16.0" +__date__ = "17 May 2023" import logging diff --git a/plotoptix/_load_lib.py b/plotoptix/_load_lib.py index b9fe7a49..3451d7f4 100644 --- a/plotoptix/_load_lib.py +++ b/plotoptix/_load_lib.py @@ -122,19 +122,19 @@ def _load_optix_win(): optix.set_bg_texture.argtypes = [c_wchar_p, c_bool] optix.set_bg_texture.restype = c_bool - optix.set_texture_1d.argtypes = [c_wchar_p, c_void_p, c_int, c_uint, c_int, c_bool, c_bool] + optix.set_texture_1d.argtypes = [c_wchar_p, c_void_p, c_bool, c_int, c_uint, c_int, c_int, c_bool, c_bool] optix.set_texture_1d.restype = c_bool - optix.set_texture_2d.argtypes = [c_wchar_p, c_void_p, c_int, c_int, c_uint, c_int, c_bool, c_bool] + optix.set_texture_2d.argtypes = [c_wchar_p, c_void_p, c_bool, c_int, c_int, c_uint, c_int, c_int, c_bool, c_bool] optix.set_texture_2d.restype = c_bool - optix.load_texture_2d.argtypes = [c_wchar_p, c_wchar_p, c_float, c_float, c_float, c_float, c_uint, c_int, c_bool] + optix.load_texture_2d.argtypes = [c_wchar_p, c_wchar_p, c_float, c_float, c_float, c_float, c_uint, c_int, c_int, c_bool] optix.load_texture_2d.restype = c_bool - optix.set_displacement.argtypes = [c_wchar_p, c_void_p, c_int, c_int, c_int, c_bool, c_bool] + optix.set_displacement.argtypes = [c_wchar_p, c_void_p, c_bool, c_int, c_int, c_int, c_int, c_bool, c_bool] optix.set_displacement.restype = c_bool - optix.load_displacement.argtypes = [c_wchar_p, c_wchar_p, c_float, c_float, c_int, c_bool] + optix.load_displacement.argtypes = [c_wchar_p, c_wchar_p, c_float, c_float, c_int, c_int, c_bool] optix.load_displacement.restype = c_bool optix.resize_scene.argtypes = [c_int, c_int, POINTER(c_longlong), POINTER(c_int), POINTER(c_longlong), POINTER(c_int), POINTER(c_longlong), POINTER(c_int), POINTER(c_longlong), POINTER(c_int), POINTER(c_longlong), POINTER(c_int), POINTER(c_longlong), POINTER(c_int)] @@ -158,10 +158,10 @@ def _load_optix_win(): optix.setup_material.argtypes = [c_wchar_p, c_wchar_p] optix.setup_material.restype = c_bool - optix.set_normal_tilt.argtypes = [c_wchar_p, c_void_p, c_int, c_int, c_int, c_int, c_bool, c_bool] + optix.set_normal_tilt.argtypes = [c_wchar_p, c_void_p, c_int, c_int, c_int, c_int, c_int, c_bool, c_bool] optix.set_normal_tilt.restype = c_bool - optix.load_normal_tilt.argtypes = [c_wchar_p, c_wchar_p, c_int, c_int, c_float, c_float, c_bool] + optix.load_normal_tilt.argtypes = [c_wchar_p, c_wchar_p, c_int, c_int, c_int, c_float, c_float, c_bool] optix.load_normal_tilt.restype = c_bool optix.set_correction_curve.argtypes = [c_void_p, c_int, c_int, c_int, c_float, c_bool] @@ -176,6 +176,12 @@ def _load_optix_win(): optix.update_geometry.argtypes = [c_wchar_p, c_wchar_p, c_int, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p] optix.update_geometry.restype = c_uint + optix.update_geometry_raw.argtypes = [c_wchar_p, c_int, c_void_p, c_bool, c_void_p, c_bool, c_void_p, c_bool, c_void_p, c_bool, c_void_p, c_bool, c_void_p, c_bool] + optix.update_geometry_raw.restype = c_uint + + optix.sync_geometry_data.argtypes = [c_wchar_p] + optix.sync_geometry_data.restype = c_bool + optix.get_geometry_size.argtypes = [c_wchar_p] optix.get_geometry_size.restype = c_int @@ -658,26 +664,26 @@ def set_float3(self, name, x, y, z, refresh): return self._optix.set_float3(name def set_bg_texture(self, name, refresh): return self._optix.set_bg_texture(name, refresh) - def set_texture_1d(self, name, data_ptr, length, tformat, addr_mode, keep_on_host, refresh): + def set_texture_1d(self, name, data_ptr, is_gpu, length, tformat, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_texture_1d_ptr(name, - IntPtr.__overloads__[Int64](data_ptr), - length, tformat, addr_mode, keep_on_host, refresh) + IntPtr.__overloads__[Int64](data_ptr), is_gpu, + length, tformat, addr_mode, filter_mode, keep_on_host, refresh) - def set_texture_2d(self, name, data_ptr, width, height, tformat, addr_mode, keep_on_host, refresh): + def set_texture_2d(self, name, data_ptr, is_gpu, width, height, tformat, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_texture_2d_ptr(name, - IntPtr.__overloads__[Int64](data_ptr), - width, height, tformat, addr_mode, keep_on_host, refresh) + IntPtr.__overloads__[Int64](data_ptr), is_gpu, + width, height, tformat, addr_mode, filter_mode, keep_on_host, refresh) - def load_texture_2d(self, tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, refresh): - return self._optix.load_texture_2d(tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, refresh) + def load_texture_2d(self, tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, filter_mode, refresh): + return self._optix.load_texture_2d(tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, filter_mode, refresh) - def set_displacement(self, obj_name, data_ptr, width, height, addr_mode, keep_on_host, refresh): + def set_displacement(self, obj_name, data_ptr, is_gpu, width, height, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_displacement_ptr(obj_name, - IntPtr.__overloads__[Int64](data_ptr), - width, height, addr_mode, keep_on_host, refresh) + IntPtr.__overloads__[Int64](data_ptr), is_gpu, + width, height, addr_mode, filter_mode, keep_on_host, refresh) - def load_displacement(self, obj_name, file_name, prescale, baseline, addr_mode, refresh): - return self._optix.load_displacement(obj_name, file_name, prescale, baseline, addr_mode, refresh) + def load_displacement(self, obj_name, file_name, prescale, baseline, addr_mode, filter_mode, refresh): + return self._optix.load_displacement(obj_name, file_name, prescale, baseline, addr_mode, filter_mode, refresh) def resize_scene(self, width, height, @@ -739,13 +745,13 @@ def get_material(self, name): return self._optix.get_material(name) def setup_material(self, name, jstr): return self._optix.setup_material(name, jstr) - def set_normal_tilt(self, mat_name, data_ptr, width, height, mapping, addr_mode, keep_on_host, refresh): + def set_normal_tilt(self, mat_name, data_ptr, width, height, mapping, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_normal_tilt_ptr(mat_name, IntPtr.__overloads__[Int64](data_ptr), - width, height, mapping, addr_mode, keep_on_host, refresh) + width, height, mapping, addr_mode, filter_mode, keep_on_host, refresh) - def load_normal_tilt(self, mat_name, file_name, mapping, addr_mode, prescale, baseline, refresh): - return self._optix.load_normal_tilt(mat_name, file_name, mapping, addr_mode, prescale, baseline, refresh) + def load_normal_tilt(self, mat_name, file_name, mapping, addr_mode, filter_mode, prescale, baseline, refresh): + return self._optix.load_normal_tilt(mat_name, file_name, mapping, addr_mode, filter_mode, prescale, baseline, refresh) def set_correction_curve(self, data_ptr, n_ctrl_points, n_curve_points, channel, vrange, refresh): @@ -774,6 +780,17 @@ def update_geometry(self, name, material, n_primitives, pos, cconst, c, r, u, v, IntPtr.__overloads__[Int64](v), IntPtr.__overloads__[Int64](w)) + def update_geometry_raw(self, name, n_primitives, pos, is_pos_gpu, c, is_c_gpu, r, is_r_gpu, u, is_u_gpu, v, is_v_gpu, w, is_w_gpu): + return self._optix.update_geometry_raw_ptr(name, n_primitives, + IntPtr.__overloads__[Int64](pos), is_pos_gpu, + IntPtr.__overloads__[Int64](c), is_c_gpu, + IntPtr.__overloads__[Int64](r), is_r_gpu, + IntPtr.__overloads__[Int64](u), is_u_gpu, + IntPtr.__overloads__[Int64](v), is_v_gpu, + IntPtr.__overloads__[Int64](w), is_w_gpu) + + def sync_geometry_data(self, name): return self._optix.sync_geometry_data(name) + def get_geometry_size(self, name): return self._optix.get_geometry_size(name) def get_faces_count(self, name): return self._optix.get_faces_count(name) @@ -1306,26 +1323,26 @@ def set_float3(self, name, x, y, z, refresh): return self._optix.set_float3(name def set_bg_texture(self, name, refresh): return self._optix.set_bg_texture(name, refresh) - def set_texture_1d(self, name, data_ptr, length, tformat, addr_mode, keep_on_host, refresh): + def set_texture_1d(self, name, data_ptr, is_gpu, length, tformat, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_texture_1d_ptr(name, - IntPtr(data_ptr), - length, tformat, addr_mode, keep_on_host, refresh) + IntPtr(data_ptr), is_gpu, + length, tformat, addr_mode, filter_mode, keep_on_host, refresh) - def set_texture_2d(self, name, data_ptr, width, height, tformat, addr_mode, keep_on_host, refresh): + def set_texture_2d(self, name, data_ptr, is_gpu, width, height, tformat, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_texture_2d_ptr(name, - IntPtr(data_ptr), - width, height, tformat, addr_mode, keep_on_host, refresh) + IntPtr(data_ptr), is_gpu, + width, height, tformat, addr_mode, filter_mode, keep_on_host, refresh) - def load_texture_2d(self, tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, refresh): - return self._optix.load_texture_2d(tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, refresh) + def load_texture_2d(self, tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, filter_mode, refresh): + return self._optix.load_texture_2d(tex_name, file_name, prescale, baseline, exposure, gamma, tformat, addr_mode, filter_mode, refresh) - def set_displacement(self, obj_name, data_ptr, width, height, addr_mode, keep_on_host, refresh): + def set_displacement(self, obj_name, data_ptr, is_gpu, width, height, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_displacement_ptr(obj_name, - IntPtr(data_ptr), - width, height, addr_mode, keep_on_host, refresh) + IntPtr(data_ptr), is_gpu, + width, height, addr_mode, filter_mode, keep_on_host, refresh) - def load_displacement(self, obj_name, file_name, prescale, baseline, addr_mode, refresh): - return self._optix.load_displacement(obj_name, file_name, prescale, baseline, addr_mode, refresh) + def load_displacement(self, obj_name, file_name, prescale, baseline, addr_mode, filter_mode, refresh): + return self._optix.load_displacement(obj_name, file_name, prescale, baseline, addr_mode, filter_mode, refresh) def resize_scene(self, width, height, @@ -1387,13 +1404,13 @@ def get_material(self, name): return self._optix.get_material(name) def setup_material(self, name, jstr): return self._optix.setup_material(name, jstr) - def set_normal_tilt(self, mat_name, data_ptr, width, height, mapping, addr_mode, keep_on_host, refresh): + def set_normal_tilt(self, mat_name, data_ptr, width, height, mapping, addr_mode, filter_mode, keep_on_host, refresh): return self._optix.set_normal_tilt_ptr(mat_name, IntPtr(data_ptr), - width, height, mapping, addr_mode, keep_on_host, refresh) + width, height, mapping, addr_mode, filter_mode, keep_on_host, refresh) - def load_normal_tilt(self, mat_name, file_name, mapping, addr_mode, prescale, baseline, refresh): - return self._optix.load_normal_tilt(mat_name, file_name, mapping, addr_mode, prescale, baseline, refresh) + def load_normal_tilt(self, mat_name, file_name, mapping, addr_mode, filter_mode, prescale, baseline, refresh): + return self._optix.load_normal_tilt(mat_name, file_name, mapping, addr_mode, filter_mode, prescale, baseline, refresh) def set_correction_curve(self, data_ptr, n_ctrl_points, n_curve_points, channel, vrange, refresh): @@ -1422,6 +1439,17 @@ def update_geometry(self, name, material, n_primitives, pos, cconst, c, r, u, v, IntPtr(v), IntPtr(w)) + def update_geometry_raw(self, name, n_primitives, pos, is_pos_gpu, c, is_c_gpu, r, is_r_gpu, u, is_u_gpu, v, is_v_gpu, w, is_w_gpu): + return self._optix.update_geometry_raw_ptr(name, n_primitives, + IntPtr(pos), is_pos_gpu, + IntPtr(c), is_c_gpu, + IntPtr(r), is_r_gpu, + IntPtr(u), is_u_gpu, + IntPtr(v), is_v_gpu, + IntPtr(w), is_w_gpu) + + def sync_geometry_data(self, name): return self._optix.sync_geometry_data(name) + def get_geometry_size(self, name): return self._optix.get_geometry_size(name) def get_faces_count(self, name): return self._optix.get_faces_count(name) diff --git a/plotoptix/bin/RnD.SharpEncoder.dll b/plotoptix/bin/RnD.SharpEncoder.dll index 08f89645..16ca6387 100644 Binary files a/plotoptix/bin/RnD.SharpEncoder.dll and b/plotoptix/bin/RnD.SharpEncoder.dll differ diff --git a/plotoptix/bin/RnD.SharpOptiX.dll b/plotoptix/bin/RnD.SharpOptiX.dll index 91c66cdc..bdd9dee1 100644 Binary files a/plotoptix/bin/RnD.SharpOptiX.dll and b/plotoptix/bin/RnD.SharpOptiX.dll differ diff --git a/plotoptix/bin/librndSharpOptiX7.so b/plotoptix/bin/librndSharpOptiX7.so index 33c39c51..84ad2322 100755 Binary files a/plotoptix/bin/librndSharpOptiX7.so and b/plotoptix/bin/librndSharpOptiX7.so differ diff --git a/plotoptix/bin/rndSharpOptiX7.dll b/plotoptix/bin/rndSharpOptiX7.dll index 697aa027..adb8d68c 100644 Binary files a/plotoptix/bin/rndSharpOptiX7.dll and b/plotoptix/bin/rndSharpOptiX7.dll differ diff --git a/plotoptix/enums.py b/plotoptix/enums.py index b9a54cca..aefc35af 100644 --- a/plotoptix/enums.py +++ b/plotoptix/enums.py @@ -145,6 +145,14 @@ class TextureAddressMode(Enum): Border = 3 +class TextureFilterMode(Enum): + """Texture sampling mode. + """ + + Nearest = 0 + + Trilinear = 1 + class MaterialType(Enum): """Type of the material shader. diff --git a/plotoptix/geometry.py b/plotoptix/geometry.py index d9a53678..dfb79ae9 100644 --- a/plotoptix/geometry.py +++ b/plotoptix/geometry.py @@ -76,18 +76,28 @@ def _pin_buffer(self, buffer: Union[GeomBuffer, str]) -> Optional[np.ndarray]: msg = "Buffer not pinned." raise RuntimeError(msg) - return None - def _release_buffer(self, buffer: GeomBuffer) -> None: if isinstance(buffer, str): buffer = GeomBuffer[buffer] - - if not self._optix.unpin_geometry_buffer(self._name, buffer.value): msg = "Buffer not released." raise RuntimeError(msg) + def copy_buffer(self, buffer: Union[GeomBuffer, str]) -> Optional[np.ndarray]: + """Return a copy of geometry buffer contents. + """ + + try: + b = self._pin_buffer(buffer) + data = np.ctypeslib.as_array(b).copy() if b is not None else None + except: + data = None + finally: + self._release_buffer(buffer) + + return data + class PinnedBuffer: """Pins an internal buffer memory and exposes it as an ``np.ndarray``. diff --git a/plotoptix/npoptix.py b/plotoptix/npoptix.py index 12f3e251..d0c9b690 100644 --- a/plotoptix/npoptix.py +++ b/plotoptix/npoptix.py @@ -8,7 +8,7 @@ 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 +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 @@ -157,6 +157,8 @@ def __init__(self, super().__init__() + self._torch = None + self._raise_on_error = False self._logger = logging.getLogger(__name__ + "-NpOptiX") self._logger.setLevel(log_level) @@ -274,6 +276,16 @@ def __del__(self): if self._is_started: self.close() else: self._optix.destroy_scene() + def enable_torch(self): + """Enable pytorch features. + """ + import importlib + try: + self._torch = importlib.import_module("torch") + except: + self._logger.error("Torch is not available.") + self._torch = None + def get_gpu_architecture(self, ordinal: int) -> Optional[GpuArchitecture]: """Get SM architecture of selected GPU. @@ -1158,8 +1170,100 @@ def set_int(self, name: str, x: int, refresh: bool = False) -> None: self._optix.set_int(name, x, refresh) + 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: + """Set texture data from pytorch tensor. + + Set texture ``name`` data. Texture format (float, float2, float4 or byte, byte2, byte4) + and length are deduced from the ``data`` tensor shape and dtype. Tensor can reside in + the GPU or CPU memory. 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 : torch.Tensor + 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. + """ + if self._torch is None: + self._logger.error("Torch features are not enabled, use rt.enable_torch() first.") + return + + if not self._torch.is_tensor(data): + msg = "Use this function with torch tensore only." + self._logger.error(msg) + if self._raise_on_error: raise ValueError(msg) + return + + 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] + + if data.dtype != self._torch.uint8: # everything not explicitly given as uin8 upload as float32 + data = data.type(self._torch.float32) # no copy if already float32 + + 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 + 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 + + else: + 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 + 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 + + self._logger.info("Set torch texture 1D %s: length=%d, format=%s.", name, data.shape[0], rt_format.name) + + if not self._optix.set_texture_1d(name, + data.contiguous().data_ptr(), data.is_cuda, + data.shape[0], rt_format.value, + addr_mode.value, filter_mode.value, + keep_on_host, refresh + ): + msg = "Torch texture 1D %s not uploaded." % name + self._logger.error(msg) + if self._raise_on_error: raise RuntimeError(msg) + + 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. @@ -1178,15 +1282,26 @@ def set_texture_1d(self, name: str, data: Any, 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. """ + if self._torch is not None and self._torch.is_tensor(data): + self.set_torch_texture_1d( + name=name, data=data, + addr_mode=addr_mode, + keep_on_host=keep_on_host, + refresh=refresh + ) + if not isinstance(name, str): name = str(name) if not isinstance(data, np.ndarray): data = np.ascontiguousarray(data) if isinstance(addr_mode, str): addr_mode = TextureAddressMode[addr_mode] + if isinstance(filter_mode, str): filter_mode = TextureFilterMode[filter_mode] if data.dtype != np.uint8: # everything not explicitly given as uin8 upload as float32 @@ -1230,13 +1345,109 @@ def set_texture_1d(self, name: str, data: Any, if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data, dtype=np.uint8) self._logger.info("Set texture 1D %s: length=%d, format=%s.", name, data.shape[0], rt_format.name) - if not self._optix.set_texture_1d(name, data.ctypes.data, data.shape[0], rt_format.value, addr_mode.value, keep_on_host, refresh): + if not self._optix.set_texture_1d(name, + data.ctypes.data, False, + 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 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: + """Set texture data from pytorch tensor. + + Set texture ``name`` data. Texture format (float, float2, float4 or byte, byte2, byte4) + and width/height are deduced from the ``data`` tensor shape and dtype. Tensor can reside in + the GPU or CPU memory. 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 : torch.Tensor + 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. + """ + + if self._torch is None: + self._logger.error("Torch features are not enabled, use rt.enable_torch() first.") + return + + if not self._torch.is_tensor(data): + msg = "Use this function with torch tensore only." + self._logger.error(msg) + if self._raise_on_error: raise ValueError(msg) + return + + 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] + + if data.dtype != self._torch.uint8: # everything not explicitly given as uin8 upload as float32 + data = data.type(self._torch.float32) # no copy if already float32 + + 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 + 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 + + else: + 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 + 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 + + self._logger.info("Set torch texture 2D %s: %d x %d, format=%s.", name, data.shape[1], data.shape[0], rt_format.name) + + if not self._optix.set_texture_2d(name, + data.contiguous().data_ptr(), data.is_cuda, data.shape[1], data.shape[0], + rt_format.value, addr_mode.value, filter_mode.value, keep_on_host, refresh + ): + msg = "Torch texture 2D %s not set." % name + self._logger.error(msg) + if self._raise_on_error: raise RuntimeError(msg) + + 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. @@ -1255,15 +1466,26 @@ def set_texture_2d(self, name: str, data: Any, 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. """ - if not isinstance(name, str): name = str(name) - if not isinstance(data, np.ndarray): data = np.ascontiguousarray(data) + if self._torch is not None and self._torch.is_tensor(data): + self.set_torch_texture_2d( + name=name, data=data, + addr_mode=addr_mode, + keep_on_host=keep_on_host, + refresh=refresh + ) + 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] + + if not isinstance(data, np.ndarray): data = np.ascontiguousarray(data) if data.dtype != np.uint8: # everything not explicitly given as uin8 upload as float32 @@ -1307,7 +1529,12 @@ def set_texture_2d(self, name: str, data: Any, if not data.flags['C_CONTIGUOUS']: data = np.ascontiguousarray(data, dtype=np.uint8) self._logger.info("Set texture 2D %s: %d x %d, format=%s.", name, data.shape[1], data.shape[0], rt_format.name) - if not self._optix.set_texture_2d(name, data.ctypes.data, data.shape[1], data.shape[0], rt_format.value, addr_mode.value, keep_on_host, refresh): + if not self._optix.set_texture_2d(name, + data.ctypes.data, False, + 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) @@ -1319,6 +1546,7 @@ def load_texture(self, tex_name: str, file_name: str, 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. @@ -1341,6 +1569,8 @@ def load_texture(self, tex_name: str, file_name: str, 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 @@ -1354,8 +1584,9 @@ def load_texture(self, tex_name: str, file_name: str, 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, refresh): + 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) @@ -1363,6 +1594,7 @@ def load_texture(self, tex_name: str, file_name: str, 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. @@ -1385,6 +1617,8 @@ def set_normal_tilt(self, name: str, data: Any, 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 @@ -1396,6 +1630,7 @@ def set_normal_tilt(self, name: str, data: Any, 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)." @@ -1408,7 +1643,8 @@ def set_normal_tilt(self, name: str, data: Any, 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, keep_on_host, refresh): + 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) @@ -1416,6 +1652,7 @@ def set_normal_tilt(self, name: str, data: Any, 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: @@ -1436,6 +1673,8 @@ def load_normal_tilt(self, name: str, file_name: str, 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 @@ -1449,15 +1688,17 @@ def load_normal_tilt(self, name: str, file_name: str, 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, prescale, baseline, refresh): + 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) 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. @@ -1487,6 +1728,7 @@ def set_displacement(self, name: str, data: Any, 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)." @@ -1498,8 +1740,12 @@ def set_displacement(self, name: str, data: Any, 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, data.shape[1], data.shape[0], - addr_mode.value, keep_on_host, refresh): + 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) @@ -1508,6 +1754,7 @@ 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. @@ -1534,9 +1781,10 @@ def load_displacement(self, name: str, file_name: str, 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, refresh): + 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) @@ -1673,6 +1921,7 @@ def set_background(self, bg: Any, 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 @@ -1742,7 +1991,12 @@ def set_background(self, bg: Any, np.clip(bg, 0.0, 255.0, out=bg) bg = bg.astype(dtype=np.uint8) - self.set_texture_2d(bg_name, bg, addr_mode=TextureAddressMode.Mirror, keep_on_host=keep_on_host, refresh=False) + 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 @@ -4131,6 +4385,156 @@ def set_data(self, name: str, pos: Optional[Any] = None, 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 self._torch is not None and self._torch.is_tensor(data): + if (len(data.shape) == depth) and (data.shape[0] == n) and (dim == 1 or data.shape[1] == dim): + cdata = data.type(self._torch.float32).contiguous() + return cdata, cdata.data_ptr(), cdata.is_cuda + else: + msg = "Tensor should be an array of shape (n, 3)." + self._logger.error(msg) + if self._raise_on_error: raise ValueError(msg) + return None, 0, False + + 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) + + if (len(data.shape) == depth) and (data.shape[0] == n) and (dim == 1 or data.shape[1] == dim): + return data, data.ctypes.data, False + else: + msg = "Array shape should be (n, 3)." + self._logger.error(msg) + if self._raise_on_error: raise ValueError(msg) + return None, 0, False + + 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) + + 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) + + + 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.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 + + n_primitives = self.geometry_data[name]._size + + 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, n_primitives, 1) + u, u_ptr, u_gpu = self._get_contiguous_mem(u, n_primitives, 3) + v, v_ptr, v_gpu = self._get_contiguous_mem(v, n_primitives, 3) + w, w_ptr, w_gpu = self._get_contiguous_mem(w, n_primitives, 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() def update_data(self, name: str, mat: Optional[str] = None, diff --git a/plotoptix/tests/test_020_scene.py b/plotoptix/tests/test_020_scene.py index df13adb7..181b5926 100644 --- a/plotoptix/tests/test_020_scene.py +++ b/plotoptix/tests/test_020_scene.py @@ -195,7 +195,7 @@ def test070_start_rt(self): TestScene.scene.start() self.assertTrue(TestScene.scene.is_started(), msg="Scene did not flip to _is_started=True state.") - self.assertTrue(TestScene.scene.isAlive(), msg="Raytracing thread is not alive.") + self.assertTrue(TestScene.scene.is_alive(), msg="Raytracing thread is not alive.") TestScene.is_alive = True def test080_camera(self): @@ -263,7 +263,7 @@ def test999_close(self): TestScene.scene.close() TestScene.scene.join(10) self.assertTrue(TestScene.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") - self.assertFalse(TestScene.scene.isAlive(), msg="Raytracing thread closing timed out.") + self.assertFalse(TestScene.scene.is_alive(), msg="Raytracing thread closing timed out.") TestScene.is_alive = False @classmethod diff --git a/plotoptix/tests/test_030_callbacks.py b/plotoptix/tests/test_030_callbacks.py index de27134a..0a56647a 100644 --- a/plotoptix/tests/test_030_callbacks.py +++ b/plotoptix/tests/test_030_callbacks.py @@ -85,7 +85,7 @@ def test010_setup_and_start(self): TestCallbacks.scene.start() self.assertTrue(TestCallbacks.scene.is_started(), msg="Scene did not flip to _is_started=True state.") - self.assertTrue(TestCallbacks.scene.isAlive(), msg="Raytracing thread is not alive.") + self.assertTrue(TestCallbacks.scene.is_alive(), msg="Raytracing thread is not alive.") TestCallbacks.is_alive = True self.assertTrue(TestCallbacks.initialization_trigs == 1, @@ -145,7 +145,7 @@ def test999_close(self): TestCallbacks.scene.close() TestCallbacks.scene.join(10) self.assertTrue(TestCallbacks.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") - self.assertFalse(TestCallbacks.scene.isAlive(), msg="Raytracing thread closing timed out.") + self.assertFalse(TestCallbacks.scene.is_alive(), msg="Raytracing thread closing timed out.") TestCallbacks.is_alive = False @classmethod diff --git a/plotoptix/tests/test_040_postprocessing.py b/plotoptix/tests/test_040_postprocessing.py index 58104de8..7e998599 100644 --- a/plotoptix/tests/test_040_postprocessing.py +++ b/plotoptix/tests/test_040_postprocessing.py @@ -92,7 +92,7 @@ def test998_start_rt(self): TestScene.scene.start() self.assertTrue(TestScene.scene.is_started(), msg="Scene did not flip to _is_started=True state.") - self.assertTrue(TestScene.scene.isAlive(), msg="Raytracing thread is not alive.") + self.assertTrue(TestScene.scene.is_alive(), msg="Raytracing thread is not alive.") TestScene.is_alive = True @@ -102,7 +102,7 @@ def test999_close(self): TestScene.scene.close() TestScene.scene.join(10) self.assertTrue(TestScene.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") - self.assertFalse(TestScene.scene.isAlive(), msg="Raytracing thread closing timed out.") + self.assertFalse(TestScene.scene.is_alive(), msg="Raytracing thread closing timed out.") TestScene.is_alive = False @classmethod diff --git a/plotoptix/tests/test_050_save_output.py b/plotoptix/tests/test_050_save_output.py index ca79184f..a41469a2 100644 --- a/plotoptix/tests/test_050_save_output.py +++ b/plotoptix/tests/test_050_save_output.py @@ -53,7 +53,7 @@ def test010_setup_and_start(self): TestOutput.scene.start() self.assertTrue(TestOutput.scene.is_started(), msg="Scene did not flip to _is_started=True state.") - self.assertTrue(TestOutput.scene.isAlive(), msg="Raytracing thread is not alive.") + self.assertTrue(TestOutput.scene.is_alive(), msg="Raytracing thread is not alive.") TestOutput.is_alive = True self.assertFalse(TestOutput.scene.encoder_is_open(), msg="Encoder is_open is True on startup.") @@ -102,7 +102,7 @@ def test999_close(self): TestOutput.scene.close() TestOutput.scene.join(10) self.assertTrue(TestOutput.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") - self.assertFalse(TestOutput.scene.isAlive(), msg="Raytracing thread closing timed out.") + self.assertFalse(TestOutput.scene.is_alive(), msg="Raytracing thread closing timed out.") TestOutput.is_alive = False @classmethod diff --git a/plotoptix/tests/test_070_scene_io.py b/plotoptix/tests/test_070_scene_io.py index 1808a0b8..7538fd8a 100644 --- a/plotoptix/tests/test_070_scene_io.py +++ b/plotoptix/tests/test_070_scene_io.py @@ -46,7 +46,7 @@ def test010_setup_and_start(self): TestOutput.scene.start() self.assertTrue(TestOutput.scene.is_started(), msg="Scene did not flip to _is_started=True state.") - self.assertTrue(TestOutput.scene.isAlive(), msg="Raytracing thread is not alive.") + self.assertTrue(TestOutput.scene.is_alive(), msg="Raytracing thread is not alive.") TestOutput.is_alive = True def test020_save_scene(self): @@ -95,7 +95,7 @@ def test999_close(self): TestOutput.scene.close() TestOutput.scene.join(10) self.assertTrue(TestOutput.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") - self.assertFalse(TestOutput.scene.isAlive(), msg="Raytracing thread closing timed out.") + self.assertFalse(TestOutput.scene.is_alive(), msg="Raytracing thread closing timed out.") TestOutput.is_alive = False @classmethod diff --git a/plotoptix/tests/test_080_meshes.py b/plotoptix/tests/test_080_meshes.py index 247da1e6..329bca65 100644 --- a/plotoptix/tests/test_080_meshes.py +++ b/plotoptix/tests/test_080_meshes.py @@ -50,7 +50,7 @@ def test010_setup_and_start(self): TestOutput.scene.start() self.assertTrue(TestOutput.scene.is_started(), msg="Scene did not flip to _is_started=True state.") - self.assertTrue(TestOutput.scene.isAlive(), msg="Raytracing thread is not alive.") + self.assertTrue(TestOutput.scene.is_alive(), msg="Raytracing thread is not alive.") TestOutput.is_alive = True def test999_close(self): @@ -59,7 +59,7 @@ def test999_close(self): TestOutput.scene.close() TestOutput.scene.join(10) self.assertTrue(TestOutput.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") - self.assertFalse(TestOutput.scene.isAlive(), msg="Raytracing thread closing timed out.") + self.assertFalse(TestOutput.scene.is_alive(), msg="Raytracing thread closing timed out.") TestOutput.is_alive = False @classmethod diff --git a/plotoptix/tests/test_090_buffers_readback.py b/plotoptix/tests/test_090_buffers_readback.py index a968b866..9cc14ed3 100644 --- a/plotoptix/tests/test_090_buffers_readback.py +++ b/plotoptix/tests/test_090_buffers_readback.py @@ -67,7 +67,7 @@ def test010_setup_and_start(self): TestCallbacks.scene.start() self.assertTrue(TestCallbacks.scene.is_started(), msg="Scene did not flip to _is_started=True state.") - self.assertTrue(TestCallbacks.scene.isAlive(), msg="Raytracing thread is not alive.") + self.assertTrue(TestCallbacks.scene.is_alive(), msg="Raytracing thread is not alive.") TestCallbacks.is_alive = True t = 0; @@ -87,7 +87,7 @@ def test999_close(self): TestCallbacks.scene.close() TestCallbacks.scene.join(10) self.assertTrue(TestCallbacks.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") - self.assertFalse(TestCallbacks.scene.isAlive(), msg="Raytracing thread closing timed out.") + self.assertFalse(TestCallbacks.scene.is_alive(), msg="Raytracing thread closing timed out.") TestCallbacks.is_alive = False @classmethod diff --git a/plotoptix/tests/test_100_direct_access.py b/plotoptix/tests/test_100_direct_access.py new file mode 100644 index 00000000..7ea10020 --- /dev/null +++ b/plotoptix/tests/test_100_direct_access.py @@ -0,0 +1,78 @@ +from unittest import TestCase + +from plotoptix import NpOptiX + +import numpy as np + +class AccTestOptiX(NpOptiX): + + def __init__(self): + + super().__init__( + width=128, height=96, + start_now=False, + log_level='INFO' + ) + +class TestAccess(TestCase): + + scene = None + data = None + r = None + is_alive = False + + @classmethod + def setUpClass(cls): + print("################ Test 100: direct access. ###################################") + n = 100 + rx = (-10, 10) + + TestAccess.r = 0.85 * 0.5 * (rx[1] - rx[0]) / (n - 1) + + x = np.linspace(rx[0], rx[1], n) + z = np.linspace(rx[0], rx[1], n) + X, Z = np.meshgrid(x, z) + + TestAccess.data = np.stack([X.flatten(), np.zeros(n*n), Z.flatten()], axis=1) + + def test010_setup_and_start(self): + TestAccess.scene = AccTestOptiX() + TestAccess.scene.set_param(min_accumulation_step=2, max_accumulation_frames=6) + + TestAccess.scene.set_data("balls", pos=TestAccess.data, c=0.82, r=TestAccess.r) + + TestAccess.scene.setup_camera("cam1") + TestAccess.scene.setup_light("light1", color=10, radius=3) + TestAccess.scene.set_background(0) + TestAccess.scene.set_ambient(0) + + TestAccess.scene.start() + self.assertTrue(TestAccess.scene.is_started(), msg="Scene did not flip to _is_started=True state.") + self.assertTrue(TestAccess.scene.is_alive(), msg="Raytracing thread is not alive.") + TestAccess.is_alive = True + + def test020_write_read_geom(self): + rb_data = TestAccess.scene.get_data("balls", "Positions") + self.assertTrue(np.allclose(TestAccess.data, rb_data), msg="Incorrect values in data readback.") + + shift = np.linspace(0, 1, TestAccess.data.shape[0]) + mod_data = TestAccess.data.copy() + mod_data[:,1] += shift + TestAccess.scene.update_raw_data("balls", pos=mod_data) + rb_data = TestAccess.scene.get_data("balls", "Positions") + self.assertTrue(np.allclose(mod_data, rb_data), msg="Incorrect values in modified data readback.") + + def test999_close(self): + self.assertTrue(TestAccess.scene is not None and TestAccess.is_alive, msg="Wrong state of the test class.") + + TestAccess.scene.close() + TestAccess.scene.join(10) + self.assertTrue(TestAccess.scene.is_closed(), msg="Scene did not flip to _is_closed=True state.") + self.assertFalse(TestAccess.scene.is_alive(), msg="Raytracing thread closing timed out.") + TestAccess.is_alive = False + + @classmethod + def tearDownClass(cls): + cls.assertFalse(cls, cls.is_alive, msg="Wrong state of the test class.") + print("Test 100: completed.") + diff --git a/plotoptix/tkoptix.py b/plotoptix/tkoptix.py index 5ad67595..c9bdd41e 100644 --- a/plotoptix/tkoptix.py +++ b/plotoptix/tkoptix.py @@ -623,6 +623,8 @@ def _gui_apply_scene_edits(self, *args): else: # manipulate selected ogject name = self.geometry_names[self._selection_handle] + if not self._optix.sync_geometry_data(name): + self._logger.error("CPU data not synced to GPU copies.") if self._left_mouse: if not self._any_key: rx = np.pi * (self._mouse_to_y - self._mouse_from_y) / self._height diff --git a/plotoptix/utils.py b/plotoptix/utils.py index fda66caa..2c2c65ab 100644 --- a/plotoptix/utils.py +++ b/plotoptix/utils.py @@ -51,7 +51,6 @@ def set_gpu_architecture(arch: Union[GpuArchitecture, str]) -> None: if isinstance(arch, str): arch = GpuArchitecture[arch] _optix.set_gpu_architecture(arch.value) - def _make_contiguous_vector(a: Optional[Any], n_dim: int) -> Optional[np.ndarray]: if a is None: return None diff --git a/setup.py b/setup.py index fae0044a..3c73d7f2 100644 --- a/setup.py +++ b/setup.py @@ -148,7 +148,7 @@ def get_tag(self): setup(name='plotoptix', - version='0.15.1', + version='0.16.0', url='https://rnd.team/plotoptix', project_urls={ 'Documentation': 'https://plotoptix.rnd.team',