diff --git a/Changelog.md b/Changelog.md index 95f1154d..fc6f9f84 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,38 @@ +# Version 3.2 +## New Features: +- Subsurface Scattering and Volume shaders now work in RPR 2.0. This allows the rendering of organic materials, such as skin, which absorb light into their interior. Volume shaders can now also be used for simple fog boxes. Also the Volume Scatter node is supported. +- Viewport denoising and upscaling improves the interactivity and speed of Viewport rendering. With the use of the Radeon Image Filter Library, this allows Radeon ProRender to render at half resolution faster, and then upscale to the full size of the Viewport. +- Deformation motion blur gives accurate motion blur to objects which are being deformed, for example, a flag flapping in the wind or a ball squashing. Besides, motion blur export has been optimized, and a setting for disabling deformation motion blur has been added. +- A new RPR Toon Shader has been added. This enables cartoon-style shading for a non-photorealistic look. Toon shaders can be used in a “simple” mode for just setting a color or a gradient of different colors for shadow vs lit areas of the object. +- Support for Blender 2.93 has been added. +- The look of “blocky” displacements has been significantly improved by enabling subdivision by default on objects with displacement shaders. However, this does render slower, and can be overridden with the RPR Object Subdivision settings. +- Support has been added for Reflection Catcher and Transparent background in the Viewport in the Full mode. +- Outline rendering (formerly called Contour rendering) is now moved to the view layer AOV section. Outline rendering can be enabled just as the other AOVs. The rendering process with Outline rendering enabled will take two passes per view layer, with the second doing the Outline rendering. +- Support for (Shutter) Position in the Motion Blur settings has been added. This uses the cycles setting to control the shutter opening around the frame number. +- Support for the Voronoi Texture node is added. + +## Issues Fixed: +- Improve prop name readability in Object visibility UI. +- Texture compression was causing artifacts in some scenes. A “texture compression” setting has been added and defaulted to False. You can enable this setting for faster renders, but make sure that there are no texture artifacts. +- The issue with the add-on not loading in versions of Blender downloaded from the Windows app store has been fixed. +- Objects set as Reflection Catchers now work in the Full mode. +- Overbright edges of objects with Uber shaders in metalness mode ― fixed. +- Shaders with high roughness could have artifacts with reflection or refraction ― fixed. +- Tiled rendering with a transparent background in the Full render quality has been fixed. +- Occasional issues in starting the add-on in certain OSs have been fixed. +- The option "Viewport Denoising and Upscaling" is saved to the scene settings. +- Memory leak in Viewport rendering with the Upscale filter enabled has been fixed. +- Image filters on Ubuntu20 have been fixed. +- Iterating all of view layers when baking all objects have been fixed. +- Fixed a crash in the viewport render if an object with hair and modifiers was not enabled for viewport display. +- Fixed an error with math nodes set to “Smooth min” or “Compare” modes. + +## Known Issues: +- In RPR 2.0, heterogenous volumes, smoke and fire simulations or VDB files are not yet supported. +- Subsurface scattering and volume shader are currently disabled on macOS due to long compile times. +- Some AOVs may have artifacts on AMD cards with drivers earlier than 21.6.1 + + # Version 3.1 ## New Features: - Support for AMD Radeon™ RX 6700 XT graphics cards has been added. diff --git a/RPRBlenderHelper/CMakeLists.txt b/RPRBlenderHelper/CMakeLists.txt index 3ba0fe85..9b44fc71 100644 --- a/RPRBlenderHelper/CMakeLists.txt +++ b/RPRBlenderHelper/CMakeLists.txt @@ -8,7 +8,6 @@ set (CMAKE_CXX_STANDARD 11) set(RPR_SDK_DIR ${CMAKE_SOURCE_DIR}/../.sdk/rpr) set(RPRTOOLS_DIR ${RPR_SDK_DIR}/rprTools) set(SHARED_DIR ${CMAKE_SOURCE_DIR}/../RadeonProRenderSharedComponents) -set(OPENVDB_SDK_PATH ${SHARED_DIR}/OpenVdb) set(SOURCES RPRBlenderHelper/RPRBlenderHelper.cpp @@ -26,32 +25,52 @@ include_directories( if(WIN32) list(APPEND SOURCES - RPRBlenderHelper/OpenVdb.cpp RPRBlenderHelper/dllmain.cpp ) include_directories( - ${OPENVDB_SDK_PATH}/include ${SHARED_DIR}/RadeonProRenderLibs/rprLibs ) - set(LIBS ${RPR_SDK_DIR}/lib/RadeonProRender64.lib - ${OPENVDB_SDK_PATH}/Windows/lib/openvdb.lib - ${OPENVDB_SDK_PATH}/Windows/lib/Half-2_3.lib - ${OPENVDB_SDK_PATH}/Windows/lib/tbb.lib ) elseif(${APPLE}) - list(APPEND SOURCES - RPRBlenderHelper/OpenVdb.cpp - ) include_directories( - ${OPENVDB_SDK_PATH}/include ${SHARED_DIR}/RadeonProRenderLibs/rprLibs ) - set(LIBS ${RPR_SDK_DIR}/bin/libRadeonProRender64.dylib + ) + +else() # Linux + set(LIBS ${RPR_SDK_DIR}/bin/libRadeonProRender64.so) + +endif() + +add_library(RPRBlenderHelper SHARED ${SOURCES}) +add_definitions(-DBLENDER_PLUGIN) +target_link_libraries(RPRBlenderHelper ${LIBS}) + + +# Building RPRBlenderHelper with OPENVDB support +set(OPENVDB_SDK_PATH ${SHARED_DIR}/OpenVdb) + +list(APPEND SOURCES + RPRBlenderHelper/OpenVdb.cpp +) +include_directories( + ${OPENVDB_SDK_PATH}/include +) + +if(WIN32) + list(APPEND LIBS + ${OPENVDB_SDK_PATH}/Windows/lib/openvdb.lib + ${OPENVDB_SDK_PATH}/Windows/lib/Half-2_3.lib + ${OPENVDB_SDK_PATH}/Windows/lib/tbb.lib + ) + +elseif(${APPLE}) + list(APPEND LIBS ${OPENVDB_SDK_PATH}/OSX/lib/libopenvdb.a ${OPENVDB_SDK_PATH}/OSX/lib/libz.a ${OPENVDB_SDK_PATH}/OSX/lib/libblosc.a @@ -60,13 +79,11 @@ elseif(${APPLE}) ${OPENVDB_SDK_PATH}/OSX/lib/libtbb.a ) -else() # Linux - set(LIBS ${RPR_SDK_DIR}/bin/libRadeonProRender64.so) +else() + return() endif() -add_library(RPRBlenderHelper SHARED ${SOURCES}) - +add_library(RPRBlenderHelper_vdb SHARED ${SOURCES}) add_definitions(-DBLENDER_PLUGIN) - -target_link_libraries(RPRBlenderHelper ${LIBS}) +target_link_libraries(RPRBlenderHelper_vdb ${LIBS}) diff --git a/RadeonProImageProcessingSDK b/RadeonProImageProcessingSDK index 00b1f056..9dc9175c 160000 --- a/RadeonProImageProcessingSDK +++ b/RadeonProImageProcessingSDK @@ -1 +1 @@ -Subproject commit 00b1f056d13050ebff02d212b2eff8ac7a4ee2f7 +Subproject commit 9dc9175cd7b2ab99227ed50a76338f35e3c927dd diff --git a/RadeonProRenderSDK b/RadeonProRenderSDK index 5ca51cc4..7afdadba 160000 --- a/RadeonProRenderSDK +++ b/RadeonProRenderSDK @@ -1 +1 @@ -Subproject commit 5ca51cc43b41f04e812b393ffcb402f9a6b56f9f +Subproject commit 7afdadba35695f36054a314ccbfec646291fbe39 diff --git a/build.cmd b/build.cmd index fce396c3..f5241c8b 100644 --- a/build.cmd +++ b/build.cmd @@ -76,6 +76,7 @@ py -3.7 src\bindings\pyrpr\src\pyrprapi.py %castxml% set bindingsOk=.\bindings-ok if exist %bindingsOk% ( py -3.7 build.py + py -3.9 build.py ) else ( echo Compiling bindings failed ) diff --git a/build.py b/build.py index afa18c1f..348a9966 100644 --- a/build.py +++ b/build.py @@ -20,7 +20,6 @@ import platform import subprocess from pathlib import Path -import shutil arch = platform.architecture() @@ -37,18 +36,17 @@ os.chdir(str(pyrpr_path)) pyrpr_build_dir = Path('.build') -if Path('.build').exists(): - shutil.rmtree(str(pyrpr_build_dir)) - subprocess.check_call([sys.executable, 'rpr.py']) subprocess.check_call([sys.executable, 'rpr_load_store.py']) os.chdir(cwd) -os.chdir('RPRBlenderHelper') -os.makedirs('.build', exist_ok=True) -os.chdir('.build') -if 'Windows' == platform.system(): - subprocess.check_call(['cmake', '-G', 'Visual Studio 14 2015 Win64', '..']) -else: - subprocess.check_call(['cmake', '..']) -subprocess.check_call(['cmake', '--build', '.', '--config', 'Release', '--clean-first']) +if sys.version_info.major == 3 and sys.version_info.minor == 7: + # we are going to build RPRBlenderHelper only for python 3.7 + os.chdir('RPRBlenderHelper') + os.makedirs('.build', exist_ok=True) + os.chdir('.build') + if 'Windows' == platform.system(): + subprocess.check_call(['cmake', '-G', 'Visual Studio 14 2015 Win64', '..']) + else: + subprocess.check_call(['cmake', '..']) + subprocess.check_call(['cmake', '--build', '.', '--config', 'Release', '--clean-first']) diff --git a/build.sh b/build.sh index 452d41b7..2e1e9d53 100755 --- a/build.sh +++ b/build.sh @@ -4,6 +4,7 @@ if [ -f "$cxml" ]; then python3.7 src/bindings/pyrpr/src/pyrprapi.py $cxml if [ -f "./bindings-ok" ]; then python3.7 build.py + python3.9 build.py else echo Compiling bindings failed fi diff --git a/build_osx.sh b/build_osx.sh index c2513dd9..f811fcef 100755 --- a/build_osx.sh +++ b/build_osx.sh @@ -17,6 +17,7 @@ if [ -f "$cxml" ]; then python3.7 src/bindings/pyrpr/src/pyrprapi.py $cxml if [ -f "./bindings-ok" ]; then python3.7 build.py + python3.9 build.py #sh osx/postbuild.sh else echo Compiling bindings failed diff --git a/cmd_tools/create_sdk.py b/cmd_tools/create_sdk.py index 6f321224..cc4d0103 100644 --- a/cmd_tools/create_sdk.py +++ b/cmd_tools/create_sdk.py @@ -37,6 +37,10 @@ def recreate_sdk(): copy_rif_sdk() +def find_file(path, glob): + return next(f for f in path.glob(glob) if not f.is_symlink()) + + def copy_rpr_sdk(): rpr_dir = Path("RadeonProRenderSDK/RadeonProRender") @@ -85,7 +89,7 @@ def copy_rif_sdk(): # getting rif bin_dir os_str = { 'Windows': "Windows", - 'Linux': "Ubuntu18", + 'Linux': "Ubuntu20", 'Darwin': "OSX" }[OS] bin_dir = rif_dir / os_str / "Dynamic" @@ -109,27 +113,32 @@ def copy_rif_sdk(): shutil.copy(str(lib), str(sdk_lib_dir)) elif OS == 'Linux': - shutil.copy(str(bin_dir / "libRadeonImageFilters.so.1.6.1"), + shutil.copy(str(find_file(bin_dir, "libRadeonImageFilters.so*")), str(sdk_bin_dir / "libRadeonImageFilters.so")) - shutil.copy(str(bin_dir / "libRadeonML_MIOpen.so.0.9.8"), + shutil.copy(str(find_file(bin_dir, "libRadeonML_MIOpen.so*")), str(sdk_bin_dir / "libRadeonML_MIOpen.so")) - shutil.copy(str(bin_dir / "libOpenImageDenoise.so.0.9.0"), + shutil.copy(str(find_file(bin_dir, "libOpenImageDenoise.so*")), str(sdk_bin_dir / "libOpenImageDenoise.so")) - shutil.copy(str(bin_dir / "libMIOpen.so.2.0.4"), + shutil.copy(str(find_file(bin_dir, "libMIOpen.so.2*")), str(sdk_bin_dir / "libMIOpen.so.2")) + shutil.copy(str(find_file(bin_dir, "libRadeonML.so.0*")), + str(sdk_bin_dir / "libRadeonML.so.0")) elif OS == 'Darwin': - shutil.copy(str(bin_dir / "libRadeonImageFilters.1.6.1.dylib"), + shutil.copy(str(find_file(bin_dir, "libRadeonImageFilters*.dylib")), str(sdk_bin_dir / "libRadeonImageFilters.dylib")) - shutil.copy(str(bin_dir / "libOpenImageDenoise.0.9.0.dylib"), + shutil.copy(str(find_file(bin_dir, "libOpenImageDenoise*.dylib")), str(sdk_bin_dir / "libOpenImageDenoise.dylib")) - shutil.copy(str(bin_dir / "libRadeonML_MPS.0.9.8.dylib"), + shutil.copy(str(find_file(bin_dir, "libRadeonML_MPS*.dylib")), str(sdk_bin_dir / "libRadeonML_MPS.dylib")) + shutil.copy(str(find_file(bin_dir, "libRadeonML.0*.dylib")), + str(sdk_bin_dir / "libRadeonML.0.dylib")) # adjusting id of RIF libs install_name_tool('-id', "@rpath/libRadeonImageFilters.dylib", sdk_bin_dir / "libRadeonImageFilters.dylib") install_name_tool('-id', "@rpath/libOpenImageDenoise.dylib", sdk_bin_dir / "libOpenImageDenoise.dylib") install_name_tool('-id', "@rpath/libRadeonML_MPS.dylib", sdk_bin_dir / "libRadeonML_MPS.dylib") + install_name_tool('-id', "@rpath/libRadeonML.0.dylib", sdk_bin_dir / "libRadeonML.0.dylib") else: raise KeyError("Unsupported OS", OS) diff --git a/run_blender_with_rpr.cmd b/run_blender_with_rpr.cmd index 468daa08..bdfdc536 100644 --- a/run_blender_with_rpr.cmd +++ b/run_blender_with_rpr.cmd @@ -19,6 +19,9 @@ REM ******************************************************************* if ""=="%BLENDER_EXE%" goto error +REM set Debug Mode flag +set RPR_BLENDER_DEBUG=1 + py cmd_tools/run_blender.py "%BLENDER_EXE%" cmd_tools/test_rpr.py pause REM it's much easier to get issue traceback on crash if pause is present; remove if not needed diff --git a/run_blender_with_rpr_Ubuntu.sh b/run_blender_with_rpr_Ubuntu.sh index 8399eb37..f4dbb175 100755 --- a/run_blender_with_rpr_Ubuntu.sh +++ b/run_blender_with_rpr_Ubuntu.sh @@ -65,6 +65,7 @@ function main() { init + export RPR_BLENDER_DEBUG=1 export LD_LIBRARY_PATH="$WORK_DIR:$LD_LIBRARY_PATH" python3 cmd_tools/run_blender.py "$BLENDER_EXE" cmd_tools/test_rpr.py diff --git a/run_blender_with_rpr_osx.sh b/run_blender_with_rpr_osx.sh index c3cf3d51..cb15be03 100755 --- a/run_blender_with_rpr_osx.sh +++ b/run_blender_with_rpr_osx.sh @@ -30,6 +30,8 @@ if [ -x "${BLENDER_EXE}" ]; then CDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DIST_LIB="$CDIR/distlib" + # set Debug Mode flag + export RPR_BLENDER_DEBUG=1 export LD_LIBRARY_PATH="$DIST_LIB" python3 cmd_tools/run_blender.py "$BLENDER_EXE" cmd_tools/test_rpr.py "$DEBUGGER_EXE" diff --git a/src/RPRBlender.pyproj b/src/RPRBlender.pyproj index a340ab1e..18ca9474 100644 --- a/src/RPRBlender.pyproj +++ b/src/RPRBlender.pyproj @@ -47,6 +47,7 @@ + diff --git a/src/bindings/pyrpr/rpr.py b/src/bindings/pyrpr/rpr.py index 2773b101..ef8e0210 100644 --- a/src/bindings/pyrpr/rpr.py +++ b/src/bindings/pyrpr/rpr.py @@ -42,7 +42,9 @@ def export(json_file_name, dependencies, header_file_name, cffi_name, output_nam ffi.cdef(Path('rprapi.h').read_text()) - lib_names = ['RadeonProRender64', 'RadeonImageFilters', 'python37'] + + lib_names = ['RadeonProRender64', 'RadeonImageFilters', + f'python{sys.version_info.major}{sys.version_info.minor}'] inc_dir = [str(base / "src/bindings/pyrpr"), str(rpr_sdk['inc']), @@ -89,7 +91,11 @@ def export(json_file_name, dependencies, header_file_name, cffi_name, output_nam shutil.copy(_cffi_backend.__file__, str(build_dir)) if 'Linux' == platform.system(): - for path in (Path(_cffi_backend.__file__).parent / '.libs_cffi_backend').iterdir(): + cffi_libs_dir = Path(_cffi_backend.__file__).parent / '.libs_cffi_backend' + if not cffi_libs_dir.is_dir(): + cffi_libs_dir = Path(_cffi_backend.__file__).parent / 'cffi.libs' + + for path in cffi_libs_dir.iterdir(): if '.so' in path.suffixes: # copy library needed for cffi backend ffi_lib = str(path) @@ -133,10 +139,8 @@ def write_api(api_desc_fpath, f, abi_mode): else: print('typedef ', t.type, name, ';', file=f) for name, t in api.functions.items(): - if 'rprxGetLog' == name: - continue if 'rifContextExecuteCommandQueue' == name: - print('rif_int rifContextExecuteCommandQueue(rif_context context, rif_command_queue command_queue, void *executeFinishedCallbackFunction(void* userdata), void* data, float* time);', file=f) + print('rif_int rifContextExecuteCommandQueue(rif_context context, rif_command_queue command_queue, void *executeFinishedCallbackFunction(void* userdata), void* data, rif_performance_statistic* statistics);', file=f) continue print(name, [(arg.name, arg.type) for arg in t.args]) print(t.restype, name, '(' + ', '.join(arg.type + ' ' + arg.name for arg in t.args) + ');', file=f) diff --git a/src/bindings/pyrpr/src/pyhybrid.py b/src/bindings/pyrpr/src/pyhybrid.py index a1e338c0..8f34aab2 100644 --- a/src/bindings/pyrpr/src/pyhybrid.py +++ b/src/bindings/pyrpr/src/pyhybrid.py @@ -132,6 +132,16 @@ class Camera(pyrpr.Camera): pass +@class_ignore_unsupported +class ImageData(pyrpr.ImageData): + pass + + +@class_ignore_unsupported +class ImageFile(pyrpr.ImageFile): + pass + + @class_ignore_unsupported class MaterialNode(pyrpr.MaterialNode): def set_input(self, name, value): diff --git a/src/bindings/pyrpr/src/pyrpr.py b/src/bindings/pyrpr/src/pyrpr.py index e3bd96d2..5f80cd03 100644 --- a/src/bindings/pyrpr/src/pyrpr.py +++ b/src/bindings/pyrpr/src/pyrpr.py @@ -12,26 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. #******************************************************************** -import math import platform import traceback import inspect import ctypes -import os import time import functools import sys import numpy as np -import bgl from typing import List +import bgl + import pyrprwrap from pyrprwrap import * -lib_wrapped_log_calls = False - - class CoreError(Exception): def __init__(self, status, func_name, argv, module_name): super().__init__() @@ -95,67 +91,34 @@ def wrapped(*argv): class _init_data: - _log_fun = None + log_fun = None + lib_wrapped_log_calls = False -def init(log_fun, rprsdk_bin_path=None): +def init(lib_dir, log_fun, lib_wrapped_log_calls): + _init_data.log_fun = log_fun + _init_data.lib_wrapped_log_calls = lib_wrapped_log_calls - _module = __import__(__name__) + lib_name = { + 'Windows': "RadeonProRender64.dll", + 'Linux': "libRadeonProRender64.so", + 'Darwin': "libRadeonProRender64.dylib" + }[platform.system()] - _init_data._log_fun = log_fun - - alternate_relative_paths = [] - if platform.system() == "Windows": - alternate_relative_paths += ["../../rif/bin"] - lib_names = [ - 'RadeonProRender64.dll', - 'RadeonImageFilters.dll', - ] - - elif platform.system() == "Linux": - lib_names = [ - 'libRadeonProRender64.so', - ] - - elif platform.system() == "Darwin": - lib_names = [ - 'libRadeonProRender64.dylib', - ] - - else: - raise ValueError("Not supported OS", platform.system()) - - for lib_name in lib_names: - rpr_lib_path = rprsdk_bin_path / lib_name - if os.path.isfile(str(rpr_lib_path)): - ctypes.CDLL(str(rpr_lib_path)) - else: - found = False - for relpath in alternate_relative_paths: - rpr_lib_path = rprsdk_bin_path / relpath / lib_name - if os.path.isfile(str(rpr_lib_path)): - try: - ctypes.CDLL(str(rpr_lib_path)) - except OSError as e: - print(f"Failed to load '{rpr_lib_path}': {str(e)}") - raise - found = True - break - - if not found: - print("Shared lib does not exists \"%s\"\n" % lib_name) - assert False + ctypes.CDLL(str(lib_dir / lib_name)) import __rpr try: lib = __rpr.lib except AttributeError: - lib = __rpr.ffi.dlopen(str(rprsdk_bin_path/lib_names[0])) + lib = __rpr.ffi.dlopen(str(lib_dir / lib_name)) pyrprwrap.lib = lib pyrprwrap.ffi = __rpr.ffi global ffi ffi = __rpr.ffi + _module = __import__(__name__) + for name in pyrprwrap._constants_names: setattr(_module, name, getattr(pyrprwrap, name)) @@ -197,38 +160,6 @@ def get_first_gpu_id_used(creation_flags): raise IndexError("GPU is not used", creation_flags) -class array: - def __init__(self, a: np.array): - self.array = a if a.flags['C_CONTIGUOUS'] else np.ascontiguousarray(a) - - def __eq__(self, other): - return np.array_equal(self.array, other.array) - - @property - def nbytes(self): - return self.array[0].nbytes - - @property - def len(self): - return len(self.array) - - @property - def data(self): - if self.array.dtype == np.float32: - return ffi.cast('float*', self.array.ctypes.data) - - if self.array.dtype == np.int32: - return ffi.cast('rpr_int*', self.array.ctypes.data) - - if self.array.dtype == np.int64: - return ffi.cast('size_t*', self.array.ctypes.data) - - raise KeyError("Not correct dtype of np.array", self.array.dtype) - - def __repr__(self): - return 'pyrpr.' + repr(self.array) - - class Object: core_type_name = 'void*' @@ -240,11 +171,11 @@ def __del__(self): try: self.delete() except: - _init_data._log_fun('EXCEPTION:', traceback.format_exc()) + _init_data.log_fun('EXCEPTION:', traceback.format_exc()) def delete(self): - if lib_wrapped_log_calls: - _init_data._log_fun('delete: ', self.name, self) + if _init_data.lib_wrapped_log_calls: + _init_data.log_fun('delete: ', self.name, self) if self._get_handle(): ObjectDelete(self._get_handle()) @@ -644,6 +575,9 @@ def set_vertex_colors(self, colors): def set_id(self, id): ShapeSetObjectID(self, id) + def set_contour_ignore(self, ignore_in_contour): + ShapeSetContourIgnore(self, ignore_in_contour) + class Curve(Object): core_type_name = 'rpr_curve' @@ -721,11 +655,11 @@ def set_transform(self, transform:np.array, transpose=True): # Blender needs mat class Mesh(Shape): def __init__(self, context, vertices, normals, uvs: List[np.array], vertex_indices, normal_indices, uv_indices: List[np.array], - num_face_vertices): + num_face_vertices, mesh_info): super().__init__(context) self.poly_count = len(num_face_vertices) - if len(uvs) > 1: + if len(uvs) > 1 or mesh_info: # several UVs set present texcoords_layers_num = len(uvs) texcoords_uvs = ffi.new("float *[]", texcoords_layers_num) @@ -741,7 +675,15 @@ def __init__(self, context, vertices, normals, uvs: List[np.array], texcoords_ind[i] = ffi.cast('rpr_int *', uv_indices[i].ctypes.data) texcoords_ind_nbytes[i] = uv_indices[i][0].nbytes - ContextCreateMeshEx( + mesh_info_ptr = ffi.new(f"rpr_mesh_info[{2 * len(mesh_info) + 1}]") + i = 0 + for key, val in mesh_info.items(): + mesh_info_ptr[i] = key + mesh_info_ptr[i + 1] = val + i += 2 + mesh_info_ptr[i] = 0 + + ContextCreateMeshEx2( self.context, ffi.cast("float *", vertices.ctypes.data), len(vertices), vertices[0].nbytes, ffi.cast("float *", normals.ctypes.data), len(normals), normals[0].nbytes, @@ -753,6 +695,7 @@ def __init__(self, context, vertices, normals, uvs: List[np.array], ffi.cast('rpr_int*', normal_indices.ctypes.data), normal_indices[0].nbytes, texcoords_ind, ffi.cast('rpr_int*', texcoords_ind_nbytes.ctypes.data), ffi.cast('rpr_int*', num_face_vertices.ctypes.data), len(num_face_vertices), + mesh_info_ptr, self ) @@ -1391,6 +1334,9 @@ def set_wrap(self, wrap_type): def set_colorspace(self, colorspace): ImageSetOcioColorspace(self, encode(colorspace)) + def set_compression(self, compression): + ImageSetInternalCompression(self, compression) + @property def size_byte(self): if self._size_byte is None: diff --git a/src/bindings/pyrpr/src/pyrpr2.py b/src/bindings/pyrpr/src/pyrpr2.py index 01307515..53845ce0 100644 --- a/src/bindings/pyrpr/src/pyrpr2.py +++ b/src/bindings/pyrpr/src/pyrpr2.py @@ -39,6 +39,12 @@ def render_update_callback(progress, data): self.render_update_callback = None +class Camera(pyrpr.Camera): + def set_motion_transform(self, transform, transpose=True): # Blender needs matrix to be transposed + pyrpr.CameraSetMotionTransformCount(self, 1) + pyrpr.CameraSetMotionTransform(self, transpose, pyrpr.ffi.cast('float*', transform.ctypes.data), 1) + + class Shape(pyrpr.Shape): def set_motion_transform(self, transform, transpose=True): # Blender needs matrix to be transposed pyrpr.ShapeSetMotionTransformCount(self, 1) diff --git a/src/bindings/pyrpr/src/pyrpr_load_store.py b/src/bindings/pyrpr/src/pyrpr_load_store.py index e420a9ea..333d9427 100644 --- a/src/bindings/pyrpr/src/pyrpr_load_store.py +++ b/src/bindings/pyrpr/src/pyrpr_load_store.py @@ -19,12 +19,16 @@ lib = None -def init(rpr_sdk_bin_path): - +def init(lib_dir): global lib - path = get_library_path(rpr_sdk_bin_path) - lib = ffi.dlopen(path) + lib_name = { + 'Windows': "RprLoadStore64.dll", + 'Linux': "libRprLoadStore64.so", + 'Darwin': "libRprLoadStore64.dylib" + }[platform.system()] + + lib = ffi.dlopen(str(lib_dir / lib_name)) def export(name, context, scene, flags): @@ -36,17 +40,3 @@ def export(name, context, scene, flags): # note: without any of above flags images will not be exported. return lib.rprsExport(pyrpr.encode(name), context._get_handle(), scene._get_handle(), 0, ffi.NULL, ffi.NULL, 0, ffi.NULL, ffi.NULL, flags) - - -def get_library_path(rpr_sdk_bin_path): - - os = platform.system() - - if os == "Windows": - return str(rpr_sdk_bin_path / 'RprLoadStore64.dll') - elif os == "Linux": - return str(rpr_sdk_bin_path / 'libRprLoadStore64.so') - elif os == "Darwin": - return str(rpr_sdk_bin_path / 'libRprLoadStore64.dylib') - else: - assert False diff --git a/src/bindings/pyrpr/src/pyrprapi.py b/src/bindings/pyrpr/src/pyrprapi.py index 9fa6ddf3..c2a90155 100644 --- a/src/bindings/pyrpr/src/pyrprapi.py +++ b/src/bindings/pyrpr/src/pyrprapi.py @@ -670,7 +670,12 @@ def get_rif_sdk(base=Path()): 'rprContextCreateCompressedImage_func', 'rpr_compressed_format', 'rpr_comressed_image_desc', - 'RPR_CONTEXT_CREATE_COMPRESSED_IMAGE',] + 'RPR_CONTEXT_CREATE_COMPRESSED_IMAGE', + 'rpr_framebuffer_type', + 'RPR_UV_CAMERA_SET_CHART_INDEX_FUNC_NAME', + 'RPR_CONTEXT_CREATE_FRAMEBUFFER_TYPED_FUNC_NAME', + 'RPR_MATERIAL_SET_INPUT_BY_S_KEY_FUNC_NAME', + 'RPR_MATERIALX_SET_ADDRESS_FUNC_NAME',] ) export( @@ -681,7 +686,9 @@ def get_rif_sdk(base=Path()): 'constant': ['RIF_', 'VERSION_', 'COMMIT_'], }, castxml, - exclude=['RIF_DEPRECATED', 'RIF_MAKE_VERSION', 'RIF_API_VERSION', 'VERSION_BUILD'] + exclude=['RIF_DEPRECATED', 'RIF_MAKE_VERSION', 'RIF_API_VERSION', 'VERSION_BUILD', + 'RIF_STRINGIFY2(s)', 'RIF_STRINGIFY(s)', + 'rif_logger_desc', 'rifLoggerAttach'] ) # export(rpr_header_gltf, includes_gltf, json_file_name_gltf, diff --git a/src/bindings/pyrpr/src/pyrprimagefilters.py b/src/bindings/pyrpr/src/pyrprimagefilters.py index d49b6d8c..012b94f1 100644 --- a/src/bindings/pyrpr/src/pyrprimagefilters.py +++ b/src/bindings/pyrpr/src/pyrprimagefilters.py @@ -14,8 +14,8 @@ #******************************************************************** import platform import traceback -import os -from abc import ABCMeta, abstractmethod +import ctypes +from abc import ABCMeta import numpy as np import pyrprimagefilterswrap @@ -25,19 +25,15 @@ import bgl -lib_wrapped_log_calls = False - class _init_data: - _log_fun = None - - -def init(log_fun, rprsdk_bin_path): - _module = __import__(__name__) + log_fun = None + lib_wrapped_log_calls = False - _init_data._log_fun = log_fun - rel_path = "../../rif/bin" +def init(lib_dir, log_fun, lib_wrapped_log_calls): + _init_data.log_fun = log_fun + _init_data.lib_wrapped_log_calls = lib_wrapped_log_calls lib_name = { 'Windows': "RadeonImageFilters.dll", @@ -45,21 +41,23 @@ def init(log_fun, rprsdk_bin_path): 'Darwin': "libRadeonImageFilters.dylib" }[platform.system()] - import __imagefilters + if platform.system() == 'Windows': + ctypes.CDLL(str(lib_dir / "RadeonML.dll")) + ctypes.CDLL(str(lib_dir / lib_name)) + import __imagefilters try: lib = __imagefilters.lib except AttributeError: - lib_path = str(rprsdk_bin_path / lib_name) - if not os.path.isfile(lib_path): - lib_path = str(rprsdk_bin_path / rel_path / lib_name) - lib = __imagefilters.ffi.dlopen(lib_path) + lib = __imagefilters.ffi.dlopen(str(lib_dir / lib_name)) pyrprimagefilterswrap.lib = lib pyrprimagefilterswrap.ffi = __imagefilters.ffi global ffi ffi = __imagefilters.ffi + _module = __import__(__name__) + for name in pyrprimagefilterswrap._constants_names: setattr(_module, name, getattr(pyrprimagefilterswrap, name)) @@ -89,13 +87,13 @@ def __del__(self): try: self.delete() except: - _init_data._log_fun('EXCEPTION:', traceback.format_exc()) + _init_data.log_fun('EXCEPTION:', traceback.format_exc()) def delete(self): if self._handle_ptr and self._get_handle(): - if lib_wrapped_log_calls: - assert _init_data._log_fun - _init_data._log_fun('delete: ', self) + if _init_data.lib_wrapped_log_calls: + assert _init_data.log_fun + _init_data.log_fun('delete: ', self) ObjectDelete(self._get_handle()) self._reset_handle() diff --git a/src/rprblender/__init__.py b/src/rprblender/__init__.py index 12ca7aec..a45511dd 100644 --- a/src/rprblender/__init__.py +++ b/src/rprblender/__init__.py @@ -20,7 +20,7 @@ bl_info = { "name": "Radeon ProRender", "author": "AMD", - "version": (3, 1, 0), + "version": (3, 2, 2), "blender": (2, 80, 0), "location": "Info header, render engine menu", "description": "Radeon ProRender rendering plugin for Blender 2.8x", @@ -211,6 +211,9 @@ def do_register_pass(aov): for i in range(3,6): do_register_pass(cryptomatte_aovs[i]) + if layer.rpr.use_contour_render: + do_register_pass(layer.rpr.contour_info) + @bpy.app.handlers.persistent def on_version_update(*args, **kwargs): diff --git a/src/rprblender/config.py b/src/rprblender/config.py index b466aa45..40be584c 100644 --- a/src/rprblender/config.py +++ b/src/rprblender/config.py @@ -28,8 +28,6 @@ disable_athena_report = False clean_athena_files = True -disable_athena_report = False -clean_athena_files = True try: # configdev.py example for logging setup: diff --git a/src/rprblender/engine/__init__.py b/src/rprblender/engine/__init__.py index a4d7a9cf..997002d8 100644 --- a/src/rprblender/engine/__init__.py +++ b/src/rprblender/engine/__init__.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. #******************************************************************** - +import os import sys -import traceback from rprblender import config from rprblender import utils @@ -23,80 +22,53 @@ log = logging.Log(tag='engine.init') -def pyrpr_init(bindings_import_path, rprsdk_bin_path): - log("pyrpr_init: bindings_path=%s, rpr_bin_path=%s" % (bindings_import_path, rprsdk_bin_path)) - - if bindings_import_path not in sys.path: - sys.path.append(bindings_import_path) - - try: - import pyrpr - import pyhybrid - import pyrpr2 - - rpr_version = utils.core_ver_str(full=True) - - log.info(f"RPR Core version: {rpr_version}") - pyrpr.lib_wrapped_log_calls = config.pyrpr_log_calls - pyrpr.init(logging.Log(tag='core'), rprsdk_bin_path=rprsdk_bin_path) - - import pyrpr_load_store - pyrpr_load_store.init(rprsdk_bin_path) - - import pyrprimagefilters - rif_version = utils.rif_ver_str(full=True) - log.info(f"Image Filters version {rif_version}") - pyrprimagefilters.lib_wrapped_log_calls = config.pyrprimagefilters_log_calls - pyrprimagefilters.init(log, rprsdk_bin_path=rprsdk_bin_path) - - # import pyrprgltf - # pyrprgltf.lib_wrapped_log_calls = config.pyrprgltf_log_calls - # pyrprgltf.init(log, rprsdk_bin_path=rprsdk_bin_path) - - except: - logging.critical(traceback.format_exc(), tag='') - return False - - finally: - sys.path.remove(bindings_import_path) +if utils.IS_DEBUG_MODE: + project_root = utils.package_root_dir().parent.parent + rpr_lib_dir = project_root / '.sdk/rpr/bin' + rif_lib_dir = project_root / '.sdk/rif/bin' - return True + if utils.IS_WIN: + os.environ['PATH'] = f"{rpr_lib_dir};{rif_lib_dir};" \ + f"{os.environ.get('PATH', '')}" + else: + os.environ['LD_LIBRARY_PATH'] = f"{rpr_lib_dir}:{rif_lib_dir}:" \ + f"{os.environ.get('LD_LIBRARY_PATH', '')}" + sys.path.append(str(project_root / "src/bindings/pyrpr/.build")) + sys.path.append(str(project_root / "src/bindings/pyrpr/src")) -if 'pyrpr' not in sys.modules: +else: + rpr_lib_dir = rif_lib_dir = utils.package_root_dir() + if utils.IS_WIN: + os.environ['PATH'] = f"{rpr_lib_dir};{os.environ.get('PATH', '')}" + else: + os.environ['LD_LIBRARY_PATH'] = f"{rpr_lib_dir}:{os.environ.get('LD_LIBRARY_PATH', '')}" - # try loading pyrpr for installed addon - bindings_import_path = str(utils.package_root_dir()) - rprsdk_bin_path = utils.package_root_dir() - if not pyrpr_init(bindings_import_path, rprsdk_bin_path): - logging.warn("Failed to load rpr from %s. One more attempt will be provided." % bindings_import_path) + sys.path.append(str(utils.package_root_dir())) - # try loading pyrpr from source - src = utils.package_root_dir().parent - project_root = src.parent - rprsdk_bin_path = project_root / ".sdk/rpr/bin" +import pyrpr +import pyhybrid +import pyrpr2 - bindings_import_path = str(src / 'bindings/pyrpr/.build') - pyrpr_import_path = str(src / 'bindings/pyrpr/src') +pyrpr.init(rpr_lib_dir, logging.Log(tag='core'), config.pyrpr_log_calls) +log.info("Core version:", utils.core_ver_str(full=True)) - if bindings_import_path not in sys.path: - sys.path.append(pyrpr_import_path) +import pyrpr_load_store +pyrpr_load_store.init(rpr_lib_dir) - try: - assert pyrpr_init(bindings_import_path, rprsdk_bin_path) - finally: - sys.path.remove(pyrpr_import_path) +import pyrprimagefilters +pyrprimagefilters.init(rif_lib_dir, logging.Log(tag='rif'), config.pyrprimagefilters_log_calls) +log.info("RIF version:", utils.rif_ver_str(full=True)) - logging.info('rprsdk_bin_path:', rprsdk_bin_path) - - -import pyrpr -import pyhybrid -import pyrpr2 +from rprblender.utils import helper_lib +helper_lib.init() def register_plugins(): + rprsdk_bin_path = utils.package_root_dir() if not utils.IS_DEBUG_MODE else \ + utils.package_root_dir().parent.parent / '.sdk/rpr/bin' + def register_plugin(ContextCls, lib_name, cache_path): lib_path = rprsdk_bin_path / lib_name ContextCls.register_plugin(lib_path, cache_path) @@ -133,9 +105,6 @@ def register_plugin(ContextCls, lib_name, cache_path): except RuntimeError as err: log.warn(err) -# we do import of helper_lib just to load RPRBlenderHelper.dll at this stage -import rprblender.utils.helper_lib - register_plugins() diff --git a/src/rprblender/engine/context.py b/src/rprblender/engine/context.py index 366eaf1c..4ee58ed1 100644 --- a/src/rprblender/engine/context.py +++ b/src/rprblender/engine/context.py @@ -25,6 +25,8 @@ class RPRContext: _Scene = pyrpr.Scene _MaterialNode = pyrpr.MaterialNode + _ImageData = pyrpr.ImageData + _ImageFile = pyrpr.ImageFile _PointLight = pyrpr.PointLight _SphereLight = pyrpr.PointLight # RPR 2.0 only feature, use PointLight instead @@ -63,6 +65,10 @@ def __init__(self): self.do_motion_blur = False self.engine_type = None + + # motion data cache. each object key has a {transform: ... , deformation: ...} + self.transform_cache = {} + self.deformation_cache = {} # TODO: probably better make nodes more close to materials in one data structure self.material_nodes = {} @@ -80,7 +86,10 @@ def __init__(self): self.use_reflection_catcher = False self.use_transparent_background = False - def init(self, context_flags, context_props, use_contour_integrator=False): + # texture compression used when images created + self.texture_compression = False + + def init(self, context_flags, context_props): self.context = self._Context(context_flags, context_props) self.material_system = pyrpr.MaterialSystem(self.context) self.gl_interop = pyrpr.CREATION_FLAGS_ENABLE_GL_INTEROP in context_flags @@ -94,9 +103,6 @@ def init(self, context_flags, context_props, use_contour_integrator=False): # self.context.set_parameter('metalperformanceshader', True) #self.context.set_parameter('ooctexcache', helpers.get_ooc_cache_size(is_preview)) - if use_contour_integrator: - self.context.set_parameter(pyrpr.CONTEXT_GPUINTEGRATOR, "gpucontour") - self.post_effect = self._PostEffect(self.context, pyrpr.POST_EFFECT_NORMALIZATION) self.scene = self._Scene(self.context) @@ -122,6 +128,9 @@ def clear_scene(self): self.images = {} + self.transform_cache = {} + self.deformation_cache = {} + def render(self, restart=False, tile=None): if restart: self.clear_frame_buffers() @@ -137,6 +146,10 @@ def abort_render(self): def get_image(self, aov_type=None): return self.get_frame_buffer(aov_type).get_data() + def set_integrator(self, use_contour_integrator): + integrator = "gpucontour" if use_contour_integrator else "gpusimple" + self.context.set_parameter(pyrpr.CONTEXT_GPUINTEGRATOR, integrator) + def get_frame_buffer(self, aov_type=None): if aov_type is not None: return self.frame_buffers_aovs[aov_type]['res'] @@ -158,14 +171,7 @@ def resolve(self, aovs=None): for aov, fbs in self.frame_buffers_aovs.items(): fbs['aov'].resolve(fbs['res'], aov != pyrpr.AOV_SHADOW_CATCHER) - if self.composite: - if aovs and pyrpr.AOV_COLOR not in aovs: - return - - color_aov = self.frame_buffers_aovs[pyrpr.AOV_COLOR] - self.composite.compute(color_aov['composite']) - if self.gl_interop: - color_aov['composite'].resolve(color_aov['gl']) + self.apply_filters() def enable_aov(self, aov_type): if self.is_aov_enabled(aov_type): @@ -245,10 +251,10 @@ def sync_catchers(self, use_transparent_background=None): return False def _enable_catchers(self): - # Experimentally found the max value of shadow catcher, - # we'll need it to normalize shadow catcher AOV - SHADOW_CATCHER_MAX_VALUE = 2.0 + self.enable_catcher_aovs() + self.create_filter_composite() + def enable_catcher_aovs(self): # Enable required AOVs self.enable_aov(pyrpr.AOV_COLOR) self.enable_aov(pyrpr.AOV_OPACITY) @@ -258,6 +264,11 @@ def _enable_catchers(self): if self.use_reflection_catcher: self.enable_aov(pyrpr.AOV_REFLECTION_CATCHER) + def create_filter_composite(self): + # Experimentally found the max value of shadow catcher, + # we'll need it to normalize shadow catcher AOV + SHADOW_CATCHER_MAX_VALUE = 2.0 + # Composite frame buffer self.frame_buffers_aovs[pyrpr.AOV_COLOR]['composite'] = pyrpr.FrameBuffer( self.context, self.width, self.height) @@ -267,17 +278,14 @@ def _enable_catchers(self): self.frame_buffers_aovs[pyrpr.AOV_COLOR]['res'] = pyrpr.FrameBuffer( self.context, self.width, self.height) self.frame_buffers_aovs[pyrpr.AOV_COLOR]['res'].set_name('default_res') - # Composite calculation elements frame buffers color = self.create_composite(pyrpr.COMPOSITE_FRAMEBUFFER, { 'framebuffer.input': self.frame_buffers_aovs[pyrpr.AOV_COLOR]['res'] }) - alpha = self.create_composite(pyrpr.COMPOSITE_FRAMEBUFFER, { 'framebuffer.input': self.frame_buffers_aovs[pyrpr.AOV_OPACITY]['res'] }).get_channel(0) full_alpha = alpha - if self.use_reflection_catcher or self.use_shadow_catcher: if self.use_reflection_catcher: reflection_catcher = self.create_composite(pyrpr.COMPOSITE_FRAMEBUFFER, { @@ -399,7 +407,8 @@ def create_area_light( self.context, vertices, normals, uvs, vertex_indices, normal_indices, uv_indices, - num_face_vertices + num_face_vertices, + {} ) light = self._AreaLight(mesh, self.material_system) self.objects[key] = light @@ -409,13 +418,15 @@ def create_mesh( self, key, vertices, normals, uvs, vertex_indices, normal_indices, uv_indices, - num_face_vertices + num_face_vertices, + mesh_info={} ): mesh = self._Mesh( self.context, vertices, normals, uvs, vertex_indices, normal_indices, uv_indices, - num_face_vertices + num_face_vertices, + mesh_info ) self.objects[key] = mesh return mesh @@ -451,13 +462,15 @@ def set_material_node_as_material(self, key, material_node): self.materials[key] = material_node def create_image_file(self, key, filepath): - image = pyrpr.ImageFile(self.context, filepath) + image = self._ImageFile(self.context, filepath) + image.set_compression(self.texture_compression) if key: self.images[key] = image return image def create_image_data(self, key, data): - image = pyrpr.ImageData(self.context, data) + image = self._ImageData(self.context, data) + image.set_compression(self.texture_compression) if key: self.images[key] = image return image @@ -564,6 +577,13 @@ def remove_material(self, key): del self.materials[key] + def apply_filters(self): + if self.composite: + color_aov = self.frame_buffers_aovs[pyrpr.AOV_COLOR] + self.composite.compute(color_aov['composite']) + if self.gl_interop: + color_aov['composite'].resolve(color_aov['gl']) + class RPRContext2(RPRContext): """ Manager of pyrpr calls """ @@ -571,6 +591,8 @@ class RPRContext2(RPRContext): # Classes _Context = pyrpr2.Context + _Camera = pyrpr2.Camera + _Mesh = pyrpr2.Mesh _Instance = pyrpr2.Instance @@ -579,11 +601,17 @@ class RPRContext2(RPRContext): _DiskLight = pyrpr2.DiskLight _PostEffect = pyrpr2.PostEffect - def init(self, context_flags, context_props, use_contour_integrator=False): + def init(self, context_flags, context_props): context_flags -= {pyrpr.CREATION_FLAGS_ENABLE_GL_INTEROP} - super().init(context_flags, context_props, use_contour_integrator) + super().init(context_flags, context_props) + + def _enable_catchers(self): + pass + + def _disable_catchers(self): + pass - def sync_catchers(self, use_transparent_background=False): + def apply_filters(self): pass def sync_auto_adapt_subdivision(self, width=0, height=0): diff --git a/src/rprblender/engine/context_hybrid.py b/src/rprblender/engine/context_hybrid.py index dcf0aef5..5e639aee 100644 --- a/src/rprblender/engine/context_hybrid.py +++ b/src/rprblender/engine/context_hybrid.py @@ -29,6 +29,8 @@ class RPRContext(context.RPRContext): _Scene = pyhybrid.Scene _MaterialNode = pyhybrid.MaterialNode + _ImageData = pyhybrid.ImageData + _ImageFile = pyhybrid.ImageFile _PointLight = pyhybrid.PointLight _SphereLight = pyhybrid.PointLight @@ -49,7 +51,7 @@ class RPRContext(context.RPRContext): _PostEffect = pyhybrid.PostEffect - def init(self, context_flags, context_props, use_contour_integrator=False): + def init(self, context_flags, context_props): context_flags -= {pyrpr.CREATION_FLAGS_ENABLE_GL_INTEROP} if context_props[0] == pyrpr.CONTEXT_SAMPLER_TYPE: context_props = context_props[2:] diff --git a/src/rprblender/engine/engine.py b/src/rprblender/engine/engine.py index 66da230b..418b2fb4 100644 --- a/src/rprblender/engine/engine.py +++ b/src/rprblender/engine/engine.py @@ -21,15 +21,12 @@ ''' main Render object ''' import weakref -import numpy as np import bpy -import mathutils import pyrpr from .context import RPRContext from rprblender.export import object, instance -from rprblender.properties.view_layer import RPR_ViewLayerProperites from . import image_filter from rprblender.utils import logging @@ -61,69 +58,7 @@ def stop_render(self): self.rpr_context = None self.image_filter = None self.background_filter = None - - def _set_render_result(self, render_passes: bpy.types.RenderPasses, apply_image_filter): - """ - Sets render result to render passes - :param render_passes: render passes to collect - :return: images - """ - def zeros_image(channels): - return np.zeros((self.rpr_context.height, self.rpr_context.width, channels), dtype=np.float32) - - images = [] - - for p in render_passes: - # finding corresponded aov - - if p.name == "Combined": - if apply_image_filter and self.image_filter: - image = self.image_filter.get_data() - - if self.background_filter: - self.update_background_filter_inputs(color_image=image) - self.background_filter.run() - image = self.background_filter.get_data() - else: - # copying alpha component from rendered image to final denoised image, - # because image filter changes it to 1.0 - image[:, :, 3] = self.rpr_context.get_image()[:, :, 3] - - elif self.background_filter: - self.update_background_filter_inputs() - self.background_filter.run() - image = self.background_filter.get_data() - else: - image = self.rpr_context.get_image() - - elif p.name == "Color": - image = self.rpr_context.get_image(pyrpr.AOV_COLOR) - - else: - aovs_info = RPR_ViewLayerProperites.cryptomatte_aovs_info \ - if "Cryptomatte" in p.name else RPR_ViewLayerProperites.aovs_info - aov = next((aov for aov in aovs_info - if aov['name'] == p.name), None) - if aov and self.rpr_context.is_aov_enabled(aov['rpr']): - image = self.rpr_context.get_image(aov['rpr']) - else: - log.warn(f"AOV '{p.name}' is not enabled in rpr_context " - f"or not found in aovs_info") - image = zeros_image(p.channels) - - if p.channels != image.shape[2]: - image = image[:, :, 0:p.channels] - - images.append(image.flatten()) - - # efficient way to copy all AOV images - render_passes.foreach_set('rect', np.concatenate(images)) - - def update_render_result(self, tile_pos, tile_size, layer_name="", - apply_image_filter=False): - result = self.rpr_engine.begin_result(*tile_pos, *tile_size, layer=layer_name) - self._set_render_result(result.layers[0].passes, apply_image_filter) - self.rpr_engine.end_result(result) + self.upscale_filter = None def depsgraph_objects(self, depsgraph: bpy.types.Depsgraph, with_camera=False): """ Iterates evaluated objects in depsgraph with ITERATED_OBJECT_TYPES """ @@ -143,81 +78,33 @@ def depsgraph_instances(self, depsgraph: bpy.types.Depsgraph): if instance.is_instance and instance.object.type in ITERATED_OBJECT_TYPES: yield instance - def sync_motion_blur(self, depsgraph: bpy.types.Depsgraph): - - def set_motion_blur(rpr_object, prev_matrix, cur_matrix): - if hasattr(rpr_object, 'set_motion_transform'): - rpr_object.set_motion_transform( - np.array(prev_matrix, dtype=np.float32).reshape(4, 4)) - else: - velocity = (prev_matrix - cur_matrix).to_translation() - rpr_object.set_linear_motion(*velocity) - - mul_diff = prev_matrix @ cur_matrix.inverted() - - quaternion = mul_diff.to_quaternion() - if quaternion.axis.length > 0.5: - rpr_object.set_angular_motion(*quaternion.axis, quaternion.angle) - else: - rpr_object.set_angular_motion(1.0, 0.0, 0.0, 0.0) - - if not isinstance(rpr_object, pyrpr.Camera): - scale_motion = mul_diff.to_scale() - mathutils.Vector((1, 1, 1)) - rpr_object.set_scale_motion(*scale_motion) - - cur_matrices = {} - - # getting current frame matrices - for obj in self.depsgraph_objects(depsgraph, with_camera=True): - if not obj.rpr.motion_blur: - continue + def cache_blur_data(self, depsgraph: bpy.types.Depsgraph): + scene = depsgraph.scene + position = scene.cycles.motion_blur_position - key = object.key(obj) - rpr_object = self.rpr_context.objects.get(key, None) - if not rpr_object or not isinstance(rpr_object, (pyrpr.Shape, pyrpr.AreaLight, pyrpr.Camera)): - continue + if position == 'END': # shutter closes at the current frame, so [N-1 .. N] + start_frame = scene.frame_current - 1 + subframe = 0.0 + elif position == 'START': # shutter opens at the current frame, [N .. N+1] + start_frame = scene.frame_current + subframe = 0.0 + else: # 'CENTER' # shutter is opened during current frame, [N-0.5 .. N+0.5] + start_frame = scene.frame_current - 1 + subframe = 0.5 + end_frame = start_frame + 1 - cur_matrices[key] = obj.matrix_world.copy() + # set to next frame and cache blur data + self._set_scene_frame(scene, end_frame, subframe) - for inst in self.depsgraph_instances(depsgraph): - if not inst.parent.rpr.motion_blur: - continue - - key = instance.key(inst) - rpr_object = self.rpr_context.objects.get(key, None) - if not rpr_object or not isinstance(rpr_object, (pyrpr.Shape, pyrpr.AreaLight)): - continue - - cur_matrices[key] = inst.matrix_world.copy() - - if not cur_matrices: - return - - cur_frame = depsgraph.scene.frame_current - prev_frame = cur_frame - 1 - - # set to previous frame and calculate motion blur data - self._set_scene_frame(depsgraph.scene, prev_frame, 0.0) try: for obj in self.depsgraph_objects(depsgraph, with_camera=True): - key = object.key(obj) - cur_matrix = cur_matrices.get(key, None) - if cur_matrix is None: - continue - - set_motion_blur(self.rpr_context.objects[key], obj.matrix_world, cur_matrix) + object.cache_blur_data(self.rpr_context, obj) for inst in self.depsgraph_instances(depsgraph): - key = instance.key(inst) - cur_matrix = cur_matrices.get(key, None) - if cur_matrix is None: - continue - - set_motion_blur(self.rpr_context.objects[key], inst.matrix_world, cur_matrix) + instance.cache_blur_data(self.rpr_context, inst) finally: - # restore current frame - self._set_scene_frame(depsgraph.scene, cur_frame, 0.0) + self._set_scene_frame(scene, start_frame, subframe) def _set_scene_frame(self, scene, frame, subframe=0.0): self.rpr_engine.frame_set(frame, subframe) @@ -431,29 +318,67 @@ def setup_background_filter(self, settings): def _enable_background_filter(self, settings): width, height = settings['resolution'] + use_background = settings['use_background'] + use_shadow = settings['use_shadow'] + use_reflection = settings['use_reflection'] self.rpr_context.enable_aov(pyrpr.AOV_COLOR) self.rpr_context.enable_aov(pyrpr.AOV_OPACITY) inputs = {'color', 'opacity'} - self.background_filter = image_filter.ImageFilterTransparentBackground( - self.rpr_context.context, inputs, {}, {}, width, height) + if not use_background and not use_reflection: + # The RPR2 applies a lonely Shadow catcher as a part of Color AOV, nothing to do here + return + + if use_shadow: + self.rpr_context.enable_aov(pyrpr.AOV_SHADOW_CATCHER) + inputs.add('shadow_catcher') + if use_reflection: + self.rpr_context.enable_aov(pyrpr.AOV_REFLECTION_CATCHER) + inputs.add('reflection_catcher') + if use_reflection or use_shadow: + self.rpr_context.enable_aov(pyrpr.AOV_BACKGROUND) + inputs.add('background') + + params = {'use_background': use_background, 'use_shadow': use_shadow, 'use_reflection': use_reflection} + + self.background_filter = image_filter.ImageFilterTransparentShadowReflectionCatcher( + self.rpr_context.context, inputs, {}, params, width, height + ) self.background_filter.settings = settings def _disable_background_filter(self): self.background_filter = None - def update_background_filter_inputs(self, tile_pos=(0, 0), color_image=None, opacity_image=None): + def update_background_filter_inputs( + self, tile_pos=(0, 0), + color_image=None, opacity_image=None): + """ + Update background filter input images. + Use color_image and opacity_image as source if passed, get from AOV otherwise. + Update catchers from AOVs if usage flags are set. + """ if color_image is None: color_image = self.rpr_context.get_image(pyrpr.AOV_COLOR) + self.background_filter.update_input('color', color_image, tile_pos) + if opacity_image is None: opacity_image = self.rpr_context.get_image(pyrpr.AOV_OPACITY) - - self.background_filter.update_input('color', color_image, tile_pos) self.background_filter.update_input('opacity', opacity_image, tile_pos) + # Catchers are taken directly from AOVs only when needed + if self.rpr_context.use_shadow_catcher: + shadow_catcher_image = self.rpr_context.get_image(pyrpr.AOV_SHADOW_CATCHER) + self.background_filter.update_input('shadow_catcher', shadow_catcher_image, tile_pos) + if self.rpr_context.use_reflection_catcher: + reflection_catcher_image = self.rpr_context.get_image(pyrpr.AOV_REFLECTION_CATCHER) + self.background_filter.update_input('reflection_catcher', reflection_catcher_image, tile_pos) + if self.rpr_context.use_shadow_catcher or self.rpr_context.use_reflection_catcher: + background_image = self.rpr_context.get_image(pyrpr.AOV_BACKGROUND) + self.background_filter.update_input('background', background_image, tile_pos) + def setup_upscale_filter(self, settings): if self.upscale_filter and self.upscale_filter.settings == settings: return False diff --git a/src/rprblender/engine/export_engine.py b/src/rprblender/engine/export_engine.py index 9dd2f79a..351cf8e3 100644 --- a/src/rprblender/engine/export_engine.py +++ b/src/rprblender/engine/export_engine.py @@ -49,9 +49,7 @@ def sync(self, context): self.rpr_context.blender_data['depsgraph'] = depsgraph scene = depsgraph.scene - use_contour = scene.rpr.is_contour_used() - - scene.rpr.init_rpr_context(self.rpr_context, use_contour_integrator=use_contour) + scene.rpr.init_rpr_context(self.rpr_context) self.rpr_context.scene.set_name(scene.name) self.rpr_context.width = int(scene.render.resolution_x * scene.render.resolution_percentage / 100) @@ -59,6 +57,13 @@ def sync(self, context): world.sync(self.rpr_context, scene.world) + # cache blur data + self.rpr_context.do_motion_blur = scene.render.use_motion_blur and \ + not math.isclose(scene.camera.data.rpr.motion_blur_exposure, 0.0) + if self.rpr_context.do_motion_blur: + self.cache_blur_data(depsgraph) + self.set_motion_blur_mode(scene) + # camera, objects, particles for obj in self.depsgraph_objects(depsgraph, with_camera=True): indirect_only = obj.original.indirect_only_get(view_layer=depsgraph.view_layer) @@ -74,6 +79,7 @@ def sync(self, context): # rpr_context parameters self.rpr_context.set_parameter(pyrpr.CONTEXT_PREVIEW, False) scene.rpr.export_ray_depth(self.rpr_context) + self.rpr_context.texture_compression = scene.rpr.texture_compression # EXPORT CAMERA camera_key = object.key(scene.camera) # current camera key @@ -87,15 +93,11 @@ def sync(self, context): self.rpr_context.width / self.rpr_context.height) camera_data.export(rpr_camera) - # sync Motion Blur - self.rpr_context.do_motion_blur = scene.render.use_motion_blur and \ - not math.isclose(scene.camera.data.rpr.motion_blur_exposure, 0.0) - if self.rpr_context.do_motion_blur: - self.sync_motion_blur(depsgraph) rpr_camera.set_exposure(scene.camera.data.rpr.motion_blur_exposure) - self.set_motion_blur_mode(scene) - + object.export_motion_blur(self.rpr_context, camera_key, + object.get_transform(camera_obj)) + # adaptive subdivision will be limited to the current scene render size self.rpr_context.enable_aov(pyrpr.AOV_COLOR) self.rpr_context.sync_auto_adapt_subdivision() diff --git a/src/rprblender/engine/image_filter.py b/src/rprblender/engine/image_filter.py index 624526c3..abf24768 100644 --- a/src/rprblender/engine/image_filter.py +++ b/src/rprblender/engine/image_filter.py @@ -122,9 +122,7 @@ def setup_alpha_filter(self, alpha): GET_COORD_OR_RETURN(coord, GET_BUFFER_SIZE(outputImage)); vec4 pixel = ReadPixelTyped(inputImage, coord.x, coord.y); vec4 pixel_alpha = ReadPixelTyped(alphaBuf, coord.x, coord.y); - pixel.x *= pixel_alpha.x; - pixel.y *= pixel_alpha.x; - pixel.z *= pixel_alpha.x; + pixel.xyz *= pixel_alpha.x; pixel.w = pixel_alpha.x; WritePixelTyped(outputImage, coord.x, coord.y, pixel); """ @@ -200,7 +198,7 @@ def _create_filter(self): if not models_path.is_dir(): # set alternative path models_path = utils.package_root_dir() / '../../.sdk/rif/models' - self.filter.set_parameter('modelPath', str(models_path)) + self.filter.set_parameter('modelPath', str(models_path.resolve())) ml_output_image = self.context.create_image(self.width, self.height, 3) @@ -310,9 +308,75 @@ def _create_filter(self): if not models_path.is_dir(): # set alternative path models_path = utils.package_root_dir() / '../../.sdk/rif/models' - self.filter.set_parameter('modelPath', str(models_path)) + self.filter.set_parameter('modelPath', str(models_path.resolve())) - self.filter.set_parameter('mode', rif.AI_UPSCALE_MODE_BEST_2X) + self.filter.set_parameter('mode', rif.AI_UPSCALE_MODE_FAST_2X) + self.filter.set_parameter('useHDR', True) self.output_image = self.context.create_image(self.width * 2, self.height * 2) self.command_queue.attach_image_filter(self.filter, self.inputs['color'], self.output_image) + + +class ImageFilterTransparentShadowReflectionCatcher(ImageFilter): + """ Calculate combination of shadow and reflection catchers, applies transparent background if needed """ + + def _create_filter(self): + """ Calculate reflection using reflection catcher and integrate it to color result """ + + use_background = self.params.get('use_background', False) + use_shadow = self.params.get('use_shadow', False) + use_reflection = self.params.get('use_reflection', False) + + self.filter = self.context.create_filter(rif.IMAGE_FILTER_USER_DEFINED) + + # only the outputImage is opened for writing in the USER_DEFINED filter, so work will be done in a single pass + # for this to work the filter code multi-string is combined here + code = """ +int2 coord; +GET_COORD_OR_RETURN(coord, GET_BUFFER_SIZE(outputImage)); +vec4 pixel = ReadPixelTyped(inputImage, coord.x, coord.y); +vec4 alpha = ReadPixelTyped(alphaImage, coord.x, coord.y); + """ + self.filter.set_parameter('alphaImage', self.inputs['opacity']) + + if use_reflection or use_shadow: + code += """ +vec4 background = ReadPixelTyped(backgroundImage, coord.x, coord.y); + """ + self.filter.set_parameter('backgroundImage', self.inputs['background']) + + if use_reflection: + code += """ +vec4 reflection = ReadPixelTyped(reflectionImage, coord.x, coord.y); +alpha.x += reflection.x; + """ + self.filter.set_parameter('reflectionImage', self.inputs['reflection_catcher']) + + code += """ +pixel.xyz = background.xyz * (1.0f - alpha.x) + pixel.xyz * alpha.x; + """ + + if use_shadow: + # note: "shadow.x / 2.0f" doesn't work correctly, used "* 0.5f" instead + code += """ +vec4 shadow = ReadPixelTyped(shadowImage, coord.x, coord.y); +float normalized = min(shadow.x * 0.5f, 1.0f); +pixel.xyz = pixel.xyz * (1.0f - normalized); +alpha.x = min(alpha.x + normalized, 1.0f); + """ + self.filter.set_parameter('shadowImage', self.inputs['shadow_catcher']) + + # apply transparent background if needed + if use_background: + code += """ +pixel.xyz *= alpha.x; +pixel.w = alpha.x; + """ + + # save calculations result to output + code += """ +WritePixelTyped(outputImage, coord.x, coord.y, pixel); + """ + + self.filter.set_parameter('code', code) + self.command_queue.attach_image_filter(self.filter, self.inputs['color'], self.output_image) diff --git a/src/rprblender/engine/preview_engine.py b/src/rprblender/engine/preview_engine.py index 212420a9..701b3e4e 100644 --- a/src/rprblender/engine/preview_engine.py +++ b/src/rprblender/engine/preview_engine.py @@ -22,6 +22,7 @@ log = logging.Log(tag='PreviewEngine') + CONTEXT_LIFETIME = 300.0 # 5 minutes in seconds @@ -61,21 +62,28 @@ def render(self): return log(f"Start render [{self.rpr_context.width}, {self.rpr_context.height}]") + result = self.rpr_engine.begin_result(0, 0, self.rpr_context.width, self.rpr_context.height) sample = 0 - while sample < self.render_samples: - if self.rpr_engine.test_break(): - break - update_samples = min(self.render_update_samples, self.render_samples - sample) + try: + while sample < self.render_samples: + if self.rpr_engine.test_break(): + break + + update_samples = min(self.render_update_samples, self.render_samples - sample) + + log(f" samples: {sample} +{update_samples} / {self.render_samples}") + self.rpr_context.set_parameter(pyrpr.CONTEXT_ITERATIONS, update_samples) + self.rpr_context.render(restart=(sample == 0)) + self.rpr_context.resolve() - log(f" samples: {sample} +{update_samples} / {self.render_samples}") - self.rpr_context.set_parameter(pyrpr.CONTEXT_ITERATIONS, update_samples) - self.rpr_context.render(restart=(sample == 0)) - self.rpr_context.resolve() - self.update_render_result((0, 0), (self.rpr_context.width, - self.rpr_context.height)) + image = self.rpr_context.get_image() + result.layers[0].passes.foreach_set('rect', image.flatten()) + self.rpr_engine.update_result(result) - sample += update_samples + sample += update_samples + finally: + self.rpr_engine.end_result(result) # clearing scene after finishing render self.rpr_context.clear_scene() @@ -113,6 +121,7 @@ def sync(self, depsgraph): self.rpr_context.set_parameter(pyrpr.CONTEXT_PREVIEW, True) settings_scene.rpr.export_ray_depth(self.rpr_context) settings_scene.rpr.export_pixel_filter(self.rpr_context) + self.rpr_context.texture_compression = settings_scene.rpr.texture_compression self.render_samples = settings_scene.rpr.viewport_limits.preview_samples self.render_update_samples = settings_scene.rpr.viewport_limits.preview_update_samples diff --git a/src/rprblender/engine/render_engine.py b/src/rprblender/engine/render_engine.py index f7985856..282e511c 100644 --- a/src/rprblender/engine/render_engine.py +++ b/src/rprblender/engine/render_engine.py @@ -17,6 +17,7 @@ import datetime import math import numpy as np +import bpy import pyrpr @@ -27,6 +28,8 @@ from rprblender.utils.conversion import perfcounter_to_str from rprblender.utils.user_settings import get_user_settings from rprblender import bl_info +from rprblender.properties.view_layer import RPR_ViewLayerProperites + from rprblender.utils import logging log = logging.Log(tag='RenderEngine') @@ -62,7 +65,11 @@ def __init__(self, rpr_engine): self.camera_data: camera.CameraData = None self.tile_order = None - self.use_contour = False + # settings to crontrol the contour render pass + # needs_contour_pass means this engine should execute it + self.needs_contour_pass = False + self.cached_rendered_images = {} + self.contour_pass_samples = 0 self.world_backplate = None @@ -76,6 +83,110 @@ def notify_status(self, progress, info): self.rpr_engine.update_progress(progress) self.rpr_engine.update_stats(self.status_title, info) + def _update_render_result(self, tile_pos, tile_size, layer_name="", + apply_image_filter=False): + + def zeros_image(channels): + return np.zeros((self.rpr_context.height, self.rpr_context.width, channels), + dtype=np.float32) + + def set_render_result(render_passes: bpy.types.RenderPasses): + images = [] + + x1, y1 = tile_pos + x2, y2 = x1 + tile_size[0], y1 + tile_size[1] + + for p in render_passes: + if p.name == "Combined": + if apply_image_filter and self.image_filter: + image = self.image_filter.get_data() + + if self.background_filter: + # calculate background effects on denoised image and cut out by tile size + self.update_background_filter_inputs(tile_pos=tile_pos, + color_image=image) + self.background_filter.run() + image = self.background_filter.get_data()[y1:y2, x1:x2, :] + else: + # copying alpha component from rendered image to final denoised image, + # because image filter changes it to 1.0 + image[:, :, 3] = self.rpr_context.get_image()[:, :, 3] + + elif self.background_filter: + # calculate background effects and cut out by tile size + self.update_background_filter_inputs(tile_pos=tile_pos) + self.background_filter.run() + image = self.background_filter.get_data()[y1:y2, x1:x2, :] + else: + image = self.rpr_context.get_image() + + elif p.name == "Color": + image = self.rpr_context.get_image(pyrpr.AOV_COLOR) + + elif p.name == "Outline": + image = zeros_image(p.channels) + + else: + aovs_info = RPR_ViewLayerProperites.cryptomatte_aovs_info \ + if "Cryptomatte" in p.name else RPR_ViewLayerProperites.aovs_info + aov = next((aov for aov in aovs_info + if aov['name'] == p.name), None) + if aov and self.rpr_context.is_aov_enabled(aov['rpr']): + image = self.rpr_context.get_image(aov['rpr']) + elif p.name != 'Outline': + log.warn(f"AOV '{p.name}' is not enabled in rpr_context " + f"or not found in aovs_info") + image = zeros_image(p.channels) + + if p.channels != image.shape[2]: + image = image[:, :, 0:p.channels] + + if self.needs_contour_pass: + # saving rendered image into cache_rendered_images + if p.name not in self.cached_rendered_images: + self.cached_rendered_images[p.name] = np.zeros( + (self.height, self.width, p.channels), dtype=np.float32) + + self.cached_rendered_images[p.name][y1:y2, x1:x2] = image + + images.append(image.flatten()) + + # efficient way to copy all AOV images + render_passes.foreach_set('rect', np.concatenate(images)) + + result = self.rpr_engine.begin_result(*tile_pos, *tile_size, layer=layer_name, view="") + try: + set_render_result(result.layers[0].passes) + + finally: + self.rpr_engine.end_result(result) + + def _update_render_result_contour(self, tile_pos, tile_size, layer_name=""): + def set_render_result(render_passes: bpy.types.RenderPasses): + images = [] + + x1, y1 = tile_pos + x2, y2 = x1 + tile_size[0], y1 + tile_size[1] + + for p in render_passes: + if p.name == "Outline": + image = self.rpr_context.get_image(pyrpr.AOV_COLOR) + else: + # getting required rendered image from cached_rendered_images + image = self.cached_rendered_images[p.name][y1:y2, x1:x2] + + images.append(image.flatten()) + + # efficient way to copy all AOV images + render_passes.foreach_set('rect', np.concatenate(images)) + + result = self.rpr_engine.begin_result(*tile_pos, *tile_size, layer=layer_name, view="") + try: + set_render_result(result.layers[0].passes) + + finally: + self.rpr_engine.end_result(result) + def _render(self): athena_data = {} @@ -88,6 +199,8 @@ def _render(self): if is_adaptive: all_pixels = active_pixels = self.rpr_context.width * self.rpr_context.height + render_update_samples = self.render_update_samples + while True: if self.rpr_engine.test_break(): athena_data['End Status'] = "cancelled" @@ -98,7 +211,7 @@ def _render(self): self.rpr_context.get_parameter(pyrpr.CONTEXT_ADAPTIVE_SAMPLING_MIN_SPP) # if less than update_samples left, use the remainder - update_samples = min(self.render_update_samples, + update_samples = min(render_update_samples, self.render_samples - self.current_sample) # we report time/iterations left as fractions if limit enabled @@ -135,8 +248,8 @@ def _render(self): if self.background_filter: self.update_background_filter_inputs() self.background_filter.run() - self.update_render_result((0, 0), (self.width, self.height), - layer_name=self.render_layer_name) + self._update_render_result((0, 0), (self.width, self.height), + layer_name=self.render_layer_name) # stop at whichever comes first: # max samples or max time if enabled or active_pixels == 0 @@ -152,17 +265,25 @@ def _render(self): break self.render_iteration += 1 - if self.render_iteration > 1 and self.render_update_samples < MAX_RENDER_ITERATIONS and not self.use_contour: + if self.render_iteration > 1 and render_update_samples < MAX_RENDER_ITERATIONS: # progressively increase update samples up to 32 - self.render_update_samples *= 2 + render_update_samples *= 2 if self.image_filter: - self.notify_status(1.0, "Applying denoising final image") + self.notify_status(1.0, "Denoising final image") self.update_image_filter_inputs() self.image_filter.run() - self.update_render_result((0, 0), (self.width, self.height), - layer_name=self.render_layer_name, - apply_image_filter=True) + color_source = self.image_filter.get_data() + + # restore alpha channel + alpha_source = self.rpr_context.get_image() + color_source[:, :, 3] = alpha_source[:, :, 3] + if self.background_filter: + self.update_background_filter_inputs(color_image=color_source) + self.background_filter.run() + self._update_render_result((0, 0), (self.width, self.height), + layer_name=self.render_layer_name, + apply_image_filter=True) self.apply_render_stamp_to_image() @@ -187,6 +308,8 @@ def _render_tiles(self): athena_data['End Status'] = "successful" progress = 0.0 + render_update_samples = self.render_update_samples + for tile_index, (tile_pos, tile_size) in enumerate(tile_iterator()): if self.rpr_engine.test_break(): athena_data['End Status'] = "cancelled" @@ -213,7 +336,7 @@ def _render_tiles(self): if self.rpr_engine.test_break(): break - update_samples = min(self.render_update_samples, self.render_samples - sample) + update_samples = min(render_update_samples, self.render_samples - sample) self.current_render_time = time.perf_counter() - time_begin progress = (tile_index + sample/self.render_samples) / tiles_number info_str = f"Render Time: {self.current_render_time:.1f} sec"\ @@ -240,8 +363,8 @@ def _render_tiles(self): sample += update_samples self.rpr_context.resolve() - self.update_render_result(tile_pos, tile_size, - layer_name=self.render_layer_name) + self._update_render_result(tile_pos, tile_size, + layer_name=self.render_layer_name) # store maximum actual number of used samples for render stamp info self.current_sample = max(self.current_sample, sample) @@ -255,14 +378,17 @@ def _render_tiles(self): break render_iteration += 1 - if render_iteration > 1 and self.render_update_samples < MAX_RENDER_ITERATIONS and not self.use_contour: + if render_iteration > 1 and render_update_samples < MAX_RENDER_ITERATIONS: # progressively increase update samples up to 32 - self.render_update_samples *= 2 + render_update_samples *= 2 - if self.image_filter and not self.rpr_engine.test_break(): - self.update_image_filter_inputs(tile_pos) + if not self.rpr_engine.test_break(): + if self.image_filter: + self.update_image_filter_inputs(tile_pos=tile_pos) + if self.background_filter: + self.update_background_filter_inputs(tile_pos=tile_pos) - if self.image_filter and not self.rpr_engine.test_break(): + if (self.image_filter or self.background_filter) and not self.rpr_engine.test_break(): self.notify_status(1.0, "Applying denoising final image") # getting already rendered images for every render pass @@ -284,9 +410,19 @@ def _render_tiles(self): # we will update only Combined pass if p.name == "Combined": - self.image_filter.run() - image = self.image_filter.get_data() - images[pos: pos + length] = image.flatten() + image = None + if self.image_filter: + self.image_filter.run() + image = self.image_filter.get_data() + + if self.background_filter: + if image is not None: + self.background_filter.update_input('color', image) + self.background_filter.run() + image = self.background_filter.get_data() + + if image is not None: + images[pos: pos + length] = image.flatten() break pos += length @@ -306,6 +442,85 @@ def _render_tiles(self): self.athena_send(athena_data) + def _render_contour(self): + log(f"Doing Outline Pass") + + # set contour settings + self.rpr_context.set_parameter(pyrpr.CONTEXT_GPUINTEGRATOR, "gpucontour") + + # enable contour aovs + self.rpr_context.disable_aovs() + self.rpr_context.resize(self.width, self.height) + + self.rpr_context.enable_aov(pyrpr.AOV_COLOR) + self.rpr_context.enable_aov(pyrpr.AOV_OBJECT_ID) + self.rpr_context.enable_aov(pyrpr.AOV_MATERIAL_ID) + self.rpr_context.enable_aov(pyrpr.AOV_SHADING_NORMAL) + + # setting camera + self.camera_data.export(self.rpr_context.scene.camera) + + athena_data = {} + + time_begin = time.perf_counter() + athena_data['Start Time'] = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f") + athena_data['End Status'] = "successful" + + self.current_sample = 0 + + while True: + if self.rpr_engine.test_break(): + athena_data['End Status'] = "cancelled" + break + + self.current_render_time = time.perf_counter() - time_begin + + # if less than update_samples left, use the remainder + update_samples = 1 + + # we report time/iterations left as fractions if limit enabled + time_str = f"{self.current_render_time:.1f}/{self.render_time}" if self.render_time \ + else f"{self.current_render_time:.1f}" + + # percent done is one of percent iterations or percent time so pick whichever is greater + progress = max( + self.current_sample / self.contour_pass_samples, + self.current_render_time / self.render_time if self.render_time else 0 + ) + info_str = f"Outline Pass | Render Time: {time_str} sec | "\ + f"Samples: {self.current_sample}/{self.contour_pass_samples}" + log_str = f" samples: {self.current_sample} +{update_samples} / {self.contour_pass_samples}"\ + f", progress: {progress * 100:.1f}%, time: {self.current_render_time:.2f}" + + self.notify_status(progress, info_str) + + log(log_str) + + self.rpr_context.set_parameter(pyrpr.CONTEXT_ITERATIONS, update_samples) + self.rpr_context.set_parameter(pyrpr.CONTEXT_FRAMECOUNT, self.render_iteration) + self.rpr_context.render(restart=(self.current_sample == 0)) + + self.current_sample += update_samples + + self.rpr_context.resolve() + self._update_render_result_contour((0, 0), (self.width, self.height), + layer_name=self.render_layer_name) + + if self.current_sample == self.contour_pass_samples: + break + + if self.render_time and self.current_render_time >= self.render_time: + break + + self.render_iteration += 1 + + athena_data['Stop Time'] = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f") + athena_data['Samples'] = self.current_sample + + log.info(f"Scene synchronization time:", perfcounter_to_str(self.sync_time)) + log.info(f"Render time:", perfcounter_to_str(self.current_render_time)) + self.athena_send(athena_data) + def render(self): if not self.is_synced: return @@ -321,11 +536,15 @@ def render(self): else: self._render() + # contour or "Outline" rendering is done as a separate render pass. + if self.needs_contour_pass: + self._render_contour() + self.notify_status(1, "Finish render") log('Finish render') def _init_rpr_context(self, scene): - scene.rpr.init_rpr_context(self.rpr_context, use_contour_integrator=self.use_contour) + scene.rpr.init_rpr_context(self.rpr_context) self.rpr_context.scene.set_name(scene.name) @@ -346,7 +565,6 @@ def sync(self, depsgraph): self.notify_status(0, "Start syncing") - self.use_contour = scene.rpr.is_contour_used() self._init_rpr_context(scene) border = ((0, 0), (1, 1)) if not scene.render.use_border else \ @@ -361,45 +579,58 @@ def sync(self, depsgraph): self.rpr_context.resize(self.width, self.height) - if self.use_contour: - scene.rpr.export_contour_mode(self.rpr_context) + self.needs_contour_pass = view_layer.rpr.use_contour_render and scene.rpr.render_quality == 'FULL2' + if self.needs_contour_pass: + view_layer.rpr.contour.export_contour_settings(self.rpr_context) self.rpr_context.blender_data['depsgraph'] = depsgraph - # EXPORT OBJECTS - objects_len = len(depsgraph.objects) - for i, obj in enumerate(self.depsgraph_objects(depsgraph)): - self.notify_status(0, "Syncing object (%d/%d): %s" % (i, objects_len, obj.name)) + # CACHE BLUR DATA + self.rpr_context.do_motion_blur = scene.render.use_motion_blur and \ + not math.isclose(scene.camera.data.rpr.motion_blur_exposure, 0.0) - # the correct collection visibility info is stored in original object - indirect_only = obj.original.indirect_only_get(view_layer=view_layer) - object.sync(self.rpr_context, obj, - indirect_only=indirect_only, material_override=material_override, - frame_current=scene.frame_current, use_contour=self.use_contour) + cur_frame = scene.frame_current + try: + if self.rpr_context.do_motion_blur: + self.cache_blur_data(depsgraph) + self.set_motion_blur_mode(scene) - if self.rpr_engine.test_break(): - log.warn("Syncing stopped by user termination") - return + # EXPORT OBJECTS + objects_len = len(depsgraph.objects) + for i, obj in enumerate(self.depsgraph_objects(depsgraph)): + self.notify_status(0, "Syncing object (%d/%d): %s" % (i, objects_len, obj.name)) + + # the correct collection visibility info is stored in original object + indirect_only = obj.original.indirect_only_get(view_layer=view_layer) + object.sync(self.rpr_context, obj, + indirect_only=indirect_only, material_override=material_override, + frame_current=scene.frame_current) - # EXPORT INSTANCES - instances_len = len(depsgraph.object_instances) - last_instances_percent = 0 - self.notify_status(0, "Syncing instances 0%") + if self.rpr_engine.test_break(): + log.warn("Syncing stopped by user termination") + return - for i, inst in enumerate(self.depsgraph_instances(depsgraph)): - instances_percent = (i * 100) // instances_len - if instances_percent > last_instances_percent: - self.notify_status(0, f"Syncing instances {instances_percent}%") - last_instances_percent = instances_percent + # EXPORT INSTANCES + instances_len = len(depsgraph.object_instances) + last_instances_percent = 0 + self.notify_status(0, "Syncing instances 0%") - indirect_only = inst.parent.original.indirect_only_get(view_layer=view_layer) - instance.sync(self.rpr_context, inst, - indirect_only=indirect_only, material_override=material_override, - frame_current=scene.frame_current, use_contour=self.use_contour) + for i, inst in enumerate(self.depsgraph_instances(depsgraph)): + instances_percent = (i * 100) // instances_len + if instances_percent > last_instances_percent: + self.notify_status(0, f"Syncing instances {instances_percent}%") + last_instances_percent = instances_percent - if self.rpr_engine.test_break(): - log.warn("Syncing stopped by user termination") - return + indirect_only = inst.parent.original.indirect_only_get(view_layer=view_layer) + instance.sync(self.rpr_context, inst, + indirect_only=indirect_only, material_override=material_override, + frame_current=scene.frame_current) + + if self.rpr_engine.test_break(): + log.warn("Syncing stopped by user termination") + return + finally: + self._set_scene_frame(scene, cur_frame, 0.0) self.notify_status(0, "Syncing instances 100%") @@ -414,8 +645,13 @@ def sync(self, depsgraph): if not camera_obj: camera_obj = scene.camera - self.camera_data = camera.CameraData.init_from_camera(camera_obj.data, camera_obj.matrix_world, - screen_width / screen_height, border) + self.camera_data = camera.CameraData.init_from_camera( + camera_obj.data, camera_obj.matrix_world, screen_width / screen_height, border) + + if self.rpr_context.do_motion_blur: + rpr_camera.set_exposure(scene.camera.data.rpr.motion_blur_exposure) + object.export_motion_blur(self.rpr_context, camera_key, + object.get_transform(camera_obj)) if scene.rpr.is_tile_render_available: if scene.camera.data.type == 'PANO': @@ -443,25 +679,22 @@ def sync(self, depsgraph): world_settings = world.sync(self.rpr_context, world_data) self.world_backplate = world_settings.backplate - # SYNC MOTION BLUR - self.rpr_context.do_motion_blur = scene.render.use_motion_blur and \ - not math.isclose(scene.camera.data.rpr.motion_blur_exposure, 0.0) - - if self.rpr_context.do_motion_blur: - self.sync_motion_blur(depsgraph) - rpr_camera.set_exposure(scene.camera.data.rpr.motion_blur_exposure) - self.set_motion_blur_mode(scene) - # EXPORT PARTICLES # Note: particles should be exported after motion blur, # otherwise prev_location of particle will be (0, 0, 0) self.notify_status(0, "Syncing particles") for obj in self.depsgraph_objects(depsgraph): particle.sync(self.rpr_context, obj) + if self.rpr_engine.test_break(): + log.warn("Syncing stopped by user termination") + return # objects linked to scene as a collection are instanced, so walk thru them for particles for entry in self.depsgraph_instances(depsgraph): particle.sync(self.rpr_context, entry.instance_object) + if self.rpr_engine.test_break(): + log.warn("Syncing stopped by user termination") + return # EXPORT: AOVS, adaptive sampling, shadow catcher, denoiser enable_adaptive = scene.rpr.limits.noise_threshold > 0.0 @@ -480,8 +713,12 @@ def sync(self, depsgraph): # Shadow catcher if scene.rpr.render_quality != 'FULL': self.rpr_context.sync_catchers(False) + bg_filter_enabled = scene.render.film_transparent or self.rpr_context.use_reflection_catcher # single Shadow Catcher AOV is handled by core background_filter_settings = { - 'enable': scene.render.film_transparent, + 'enable': bg_filter_enabled, + 'use_background': scene.render.film_transparent, + 'use_shadow': self.rpr_context.use_shadow_catcher, + 'use_reflection': self.rpr_context.use_reflection_catcher, 'resolution': (self.width, self.height), } self.setup_background_filter(background_filter_settings) @@ -492,14 +729,13 @@ def sync(self, depsgraph): self.rpr_context.set_parameter(pyrpr.CONTEXT_PREVIEW, False) scene.rpr.export_ray_depth(self.rpr_context) scene.rpr.export_pixel_filter(self.rpr_context) + self.rpr_context.texture_compression = scene.rpr.texture_compression self.render_samples, self.render_time = (scene.rpr.limits.max_samples, scene.rpr.limits.seconds) + self.contour_pass_samples = scene.rpr.limits.contour_render_samples if scene.rpr.render_quality == 'FULL2': - if self.use_contour: - self.render_update_samples = 1 - else: - self.render_update_samples = scene.rpr.limits.update_samples_rpr2 + self.render_update_samples = scene.rpr.limits.update_samples_rpr2 else: self.render_update_samples = scene.rpr.limits.update_samples diff --git a/src/rprblender/engine/render_engine_2.py b/src/rprblender/engine/render_engine_2.py index b5fd33bd..3360f422 100644 --- a/src/rprblender/engine/render_engine_2.py +++ b/src/rprblender/engine/render_engine_2.py @@ -75,12 +75,13 @@ def do_resolve(): break self.rpr_context.resolve() - self.update_render_result((0, 0), (self.width, self.height), - layer_name=self.render_layer_name) + self._update_render_result((0, 0), (self.width, self.height), + layer_name=self.render_layer_name) log('Finish do_resolve') self.rpr_context.set_render_update_callback(render_update_callback) + resolve_thread = threading.Thread(target=do_resolve) resolve_thread.start() diff --git a/src/rprblender/engine/viewport_engine.py b/src/rprblender/engine/viewport_engine.py index 7657e8a1..df3d64de 100644 --- a/src/rprblender/engine/viewport_engine.py +++ b/src/rprblender/engine/viewport_engine.py @@ -214,7 +214,6 @@ def __init__(self, rpr_engine): self.render_time = 0 self.view_mode = None - self.use_contour = False self.space_data = None self.selected_objects = None @@ -227,6 +226,7 @@ def stop_render(self): self.rpr_context = None self.image_filter = None + self.upscale_filter = None def _resolve(self): self.rpr_context.resolve() @@ -245,8 +245,6 @@ def _do_sync(self, depsgraph): self.notify_status("Starting...", "Sync") time_begin = time.perf_counter() - self.use_contour = depsgraph.scene.rpr.is_contour_used(is_final_engine=False) - # exporting objects frame_current = depsgraph.scene.frame_current material_override = depsgraph.view_layer.material_override @@ -262,7 +260,7 @@ def _do_sync(self, depsgraph): indirect_only = obj.original.indirect_only_get(view_layer=depsgraph.view_layer) object.sync(self.rpr_context, obj, indirect_only=indirect_only, material_override=material_override, - frame_current=frame_current, use_contour=self.use_contour) + frame_current=frame_current) # exporting instances instances_len = len(depsgraph.object_instances) @@ -281,10 +279,22 @@ def _do_sync(self, depsgraph): indirect_only = inst.parent.original.indirect_only_get(view_layer=depsgraph.view_layer) instance.sync(self.rpr_context, inst, indirect_only=indirect_only, material_override=material_override, - frame_current=frame_current, use_contour=self.use_contour) + frame_current=frame_current) # shadow catcher - self.rpr_context.sync_catchers(depsgraph.scene.render.film_transparent) + if depsgraph.scene.rpr.render_quality != 'FULL': # non-Legacy modes + self.rpr_context.sync_catchers(False) + bg_filter_enabled = self.rpr_context.use_reflection_catcher or self.rpr_context.use_shadow_catcher + background_filter_settings = { + 'enable': bg_filter_enabled, + 'use_background': depsgraph.scene.render.film_transparent, + 'use_shadow': self.rpr_context.use_shadow_catcher, + 'use_reflection': self.rpr_context.use_reflection_catcher, + 'resolution': (self.width, self.height), + } + self.setup_background_filter(background_filter_settings) + else: + self.rpr_context.sync_catchers(depsgraph.scene.render.film_transparent) self.is_synced = True @@ -469,10 +479,8 @@ def sync(self, context, depsgraph): settings = get_user_settings() use_gl_interop = settings.use_gl_interop and not scene.render.film_transparent - use_contour = scene.rpr.is_contour_used(is_final_engine=False) scene.rpr.init_rpr_context(self.rpr_context, is_final_engine=False, - use_gl_interop=use_gl_interop, - use_contour_integrator=use_contour) + use_gl_interop=use_gl_interop) self.rpr_context.blender_data['depsgraph'] = depsgraph @@ -495,19 +503,16 @@ def sync(self, context, depsgraph): self.world_settings = self._get_world_settings(depsgraph) self.world_settings.export(self.rpr_context) - if scene.rpr.is_contour_used(is_final_engine=False): - scene.rpr.export_contour_mode(self.rpr_context) - rpr_camera = self.rpr_context.create_camera() rpr_camera.set_name("Camera") self.rpr_context.scene.set_camera(rpr_camera) # image filter - self.setup_image_filter(self._get_image_filter_settings()) + self.setup_image_filter(self._get_image_filter_settings(scene)) # upscale filter self.setup_upscale_filter({ - 'enable': settings.viewport_denoiser_upscale, + 'enable': scene.rpr.viewport_upscale, 'resolution': (self.width, self.height), }) @@ -516,6 +521,7 @@ def sync(self, context, depsgraph): self.rpr_context.set_parameter(pyrpr.CONTEXT_ITERATIONS, 1) scene.rpr.export_render_mode(self.rpr_context) scene.rpr.export_ray_depth(self.rpr_context) + self.rpr_context.texture_compression = scene.rpr.texture_compression scene.rpr.export_pixel_filter(self.rpr_context) self.render_iterations, self.render_time = (viewport_limits.max_samples, 0) @@ -524,7 +530,6 @@ def sync(self, context, depsgraph): self.restart_render_event.clear() self.view_mode = context.mode - self.use_contour = scene.rpr.is_contour_used(is_final_engine=False) self.space_data = context.space_data self.selected_objects = context.selected_objects self.sync_render_thread = threading.Thread(target=self._do_sync_render, args=(depsgraph,)) @@ -570,11 +575,9 @@ def sync_update(self, context, depsgraph): self.rpr_context.blender_data['depsgraph'] = depsgraph # if view mode changed need to sync collections - use_contour = depsgraph.scene.rpr.is_contour_used(is_final_engine=False) mode_updated = False - if self.view_mode != context.mode or self.use_contour != use_contour: + if self.view_mode != context.mode: self.view_mode = context.mode - self.use_contour = use_contour mode_updated = True if not updates and not sync_world and not sync_collection: @@ -612,8 +615,7 @@ def sync_update(self, context, depsgraph): update.is_updated_transform, indirect_only=indirect_only, material_override=material_override, - frame_current=frame_current, - use_contour=self.use_contour) + frame_current=frame_current) is_obj_updated |= is_updated continue @@ -641,7 +643,16 @@ def sync_update(self, context, depsgraph): is_updated |= self.sync_objects_collection(depsgraph) if is_obj_updated: - self.rpr_context.sync_catchers() + if self.background_filter: + self.rpr_context.sync_catchers(False) + bg_filter_enabled = self.rpr_context.use_reflection_catcher or self.rpr_context.use_shadow_catcher + background_filter_settings = {'enable': bg_filter_enabled, 'use_background': False, + 'use_shadow': self.rpr_context.use_shadow_catcher, + 'use_reflection': self.rpr_context.use_reflection_catcher, + 'resolution': (self.width, self.height)} + self.setup_background_filter(background_filter_settings) + else: + self.rpr_context.sync_catchers() if is_updated: self.restart_render_event.set() @@ -932,8 +943,6 @@ def sync_collection_objects(self, depsgraph, object_keys_to_export, material_ove res = False frame_current = depsgraph.scene.frame_current - use_contour = depsgraph.scene.rpr.is_contour_used(is_final_engine=False) - for obj in self.depsgraph_objects(depsgraph): obj_key = object.key(obj) if obj_key not in object_keys_to_export: @@ -944,7 +953,7 @@ def sync_collection_objects(self, depsgraph, object_keys_to_export, material_ove indirect_only = obj.original.indirect_only_get(view_layer=depsgraph.view_layer) object.sync(self.rpr_context, obj, indirect_only=indirect_only, material_override=material_override, - frame_current=frame_current, use_contour=use_contour) + frame_current=frame_current) else: assign_materials(self.rpr_context, rpr_obj, obj, material_override) @@ -957,8 +966,6 @@ def sync_collection_instances(self, depsgraph, object_keys_to_export, material_o res = False frame_current = depsgraph.scene.frame_current - use_contour = depsgraph.scene.rpr.is_contour_used(is_final_engine=False) - for inst in self.depsgraph_instances(depsgraph): instance_key = instance.key(inst) if instance_key not in object_keys_to_export: @@ -969,7 +976,7 @@ def sync_collection_instances(self, depsgraph, object_keys_to_export, material_o indirect_only = inst.parent.original.indirect_only_get(view_layer=depsgraph.view_layer) instance.sync(self.rpr_context, inst, indirect_only=indirect_only, material_override=material_override, - frame_current=frame_current, use_contour=use_contour) + frame_current=frame_current) else: assign_materials(self.rpr_context, inst_obj, inst.object, material_override=material_override) @@ -982,8 +989,6 @@ def update_material_on_scene_objects(self, mat, depsgraph): material_override = depsgraph.view_layer.material_override frame_current = depsgraph.scene.frame_current - use_contour = depsgraph.scene.rpr.is_contour_used(is_final_engine=False) - if material_override and material_override.name == mat.name: objects = self.depsgraph_objects(depsgraph) active_mat = material_override @@ -1005,15 +1010,14 @@ def update_material_on_scene_objects(self, mat, depsgraph): if object.key(obj) not in self.rpr_context.objects: object.sync(self.rpr_context, obj, indirect_only=indirect_only, - frame_current=frame_current, use_contour=use_contour) + frame_current=frame_current) updated = True continue updated |= object.sync_update(self.rpr_context, obj, False, False, indirect_only=indirect_only, material_override=material_override, - frame_current=frame_current, - use_contour=use_contour) + frame_current=frame_current) return updated @@ -1033,12 +1037,12 @@ def update_render(self, scene: bpy.types.Scene, view_layer: bpy.types.ViewLayer) restart |= scene.rpr.viewport_limits.set_adaptive_params(self.rpr_context) # image filter - if self.setup_image_filter(self._get_image_filter_settings()): + if self.setup_image_filter(self._get_image_filter_settings(scene)): self.denoised_image = None restart = True restart |= self.setup_upscale_filter({ - 'enable': get_user_settings().viewport_denoiser_upscale, + 'enable': scene.rpr.viewport_upscale, 'resolution': (self.width, self.height), }) @@ -1050,9 +1054,9 @@ def _get_world_settings(self, depsgraph): return world.WorldData.init_from_shading_data(self.shading_data) - def _get_image_filter_settings(self): + def _get_image_filter_settings(self, scene): return { - 'enable': get_user_settings().viewport_denoiser_upscale, + 'enable': scene.rpr.viewport_denoiser, 'resolution': (self.width, self.height), 'filter_type': 'ML', 'ml_color_only': False, diff --git a/src/rprblender/engine/viewport_engine_2.py b/src/rprblender/engine/viewport_engine_2.py index 1f3fa6bf..c810c53b 100644 --- a/src/rprblender/engine/viewport_engine_2.py +++ b/src/rprblender/engine/viewport_engine_2.py @@ -47,6 +47,7 @@ def stop_render(self): self.rpr_context.set_render_update_callback(None) self.rpr_context = None self.image_filter = None + self.upscale_filter = None def _resolve(self): self.rpr_context.resolve(None if self.image_filter and self.is_last_iteration else @@ -69,6 +70,11 @@ def _resize(self, width, height): image_filter_settings['resolution'] = self.width, self.height self.setup_image_filter(image_filter_settings) + if self.background_filter: + background_filter_settings = self.background_filter.settings.copy() + background_filter_settings['resolution'] = self.width, self.height + self.setup_background_filter(background_filter_settings) + if self.upscale_filter: upscale_filter_settings = self.upscale_filter.settings.copy() upscale_filter_settings['resolution'] = self.width, self.height @@ -150,7 +156,7 @@ def render_update(progress): self.rpr_context.set_parameter(pyrpr.CONTEXT_FRAMECOUNT, iteration) update_iterations = 1 - if not self.use_contour and iteration > 1: + if iteration > 1: update_iterations = min(32, self.render_iterations - iteration) self.rpr_context.set_parameter(pyrpr.CONTEXT_ITERATIONS, update_iterations) @@ -222,26 +228,33 @@ def render_update(progress): self._resolve() time_render = time.perf_counter() - time_begin - if self.image_filter: - self.notify_status(f"Time: {time_render:.1f} sec | Iteration: {iteration}" - f" | Denoising...", "Render") + with self.render_lock: + if self.image_filter: + self.notify_status(f"Time: {time_render:.1f} sec | Iteration: {iteration}" + f" | Denoising...", "Render") - # applying denoising - self.update_image_filter_inputs() - self.image_filter.run() - self.rendered_image = self.image_filter.get_data() + # applying denoising + self.update_image_filter_inputs() + self.image_filter.run() + image = self.image_filter.get_data() - time_render = time.perf_counter() - time_begin - status_str = f"Time: {time_render:.1f} sec | Iteration: {iteration} | Denoised" - else: - self.rendered_image = self.rpr_context.get_image() - status_str = f"Time: {time_render:.1f} sec | Iteration: {iteration}" + time_render = time.perf_counter() - time_begin + status_str = f"Time: {time_render:.1f} sec | Iteration: {iteration} | Denoised" + else: + image = self.rpr_context.get_image() + status_str = f"Time: {time_render:.1f} sec | Iteration: {iteration}" + + if self.background_filter: + with self.resolve_lock: + self.rendered_image = self.resolve_background_aovs(self.rendered_image) + else: + self.rendered_image = image - if self.upscale_filter: - self.upscale_filter.update_input('color', self.rendered_image) - self.upscale_filter.run() - self.rendered_image = self.upscale_filter.get_data() - status_str += " | Upscaled" + if self.upscale_filter: + self.upscale_filter.update_input('color', self.rendered_image) + self.upscale_filter.run() + self.rendered_image = self.upscale_filter.get_data() + status_str += " | Upscaled" self.notify_status(status_str, "Rendering Done") @@ -260,10 +273,30 @@ def _do_resolve(self): with self.resolve_lock: self._resolve() - self.rendered_image = self.rpr_context.get_image() + image = self.rpr_context.get_image() + + if self.background_filter: + image = self.resolve_background_aovs(image) + self.rendered_image = image + else: + self.rendered_image = image log("Finish _do_resolve") + def resolve_background_aovs(self, color_image): + settings = self.background_filter.settings + self.rpr_context.resolve((pyrpr.AOV_OPACITY,)) + alpha = self.rpr_context.get_image(pyrpr.AOV_OPACITY) + if settings['use_shadow']: + self.rpr_context.resolve((pyrpr.AOV_SHADOW_CATCHER,)) + if settings['use_reflection']: + self.rpr_context.resolve((pyrpr.AOV_REFLECTION_CATCHER,)) + if settings['use_shadow'] or settings['use_reflection']: + self.rpr_context.resolve((pyrpr.AOV_BACKGROUND,)) + self.update_background_filter_inputs(color_image=color_image, opacity_image=alpha) + self.background_filter.run() + return self.background_filter.get_data() + def draw(self, context): log("Draw") diff --git a/src/rprblender/export/camera.py b/src/rprblender/export/camera.py index cb2b4bab..72d34fed 100644 --- a/src/rprblender/export/camera.py +++ b/src/rprblender/export/camera.py @@ -224,8 +224,10 @@ def export(self, rpr_camera: pyrpr.Camera, tile=((0.0, 0.0), (1.0, 1.0))): def sync(rpr_context: RPRContext, obj: bpy.types.Object): - """ Creates pyrpr.Camera from obj.data: bpy.types.Camera. Created camera sets to scene as default """ - + """ + Creates pyrpr.Camera from obj.data: bpy.types.Camera. + Created camera sets to scene as default + """ camera = obj.data log("sync", camera) @@ -237,3 +239,7 @@ def sync(rpr_context: RPRContext, obj: bpy.types.Object): # set scene's camera rpr_context.scene.set_camera(rpr_camera) + + +def cache_blur_data(rpr_context, obj: bpy.types.Object): + rpr_context.transform_cache[object.key(obj)] = object.get_transform(obj) diff --git a/src/rprblender/export/hair.py b/src/rprblender/export/hair.py index d74190b0..c4cb26c3 100644 --- a/src/rprblender/export/hair.py +++ b/src/rprblender/export/hair.py @@ -100,8 +100,9 @@ def shape_f(x, shape): # finding corresponded active ParticleSystemModifier p_modifier = next((modifier for modifier in obj.modifiers if modifier.type == 'PARTICLE_SYSTEM' and - modifier.show_render and - modifier.particle_system.name == p_sys.name), + (modifier.show_render if use_final_settings + else modifier.show_viewport) + and modifier.particle_system.name == p_sys.name), None) if not p_modifier: diff --git a/src/rprblender/export/instance.py b/src/rprblender/export/instance.py index c686ed9c..b5df22c8 100644 --- a/src/rprblender/export/instance.py +++ b/src/rprblender/export/instance.py @@ -52,7 +52,10 @@ def sync(rpr_context, instance: bpy.types.DepsgraphObjectInstance, **kwargs): rpr_shape = rpr_context.create_instance(instance_key, rpr_mesh) rpr_shape.set_name(str(instance_key)) - rpr_shape.set_transform(get_transform(instance)) + + transform = get_transform(instance) + rpr_shape.set_transform(transform) + object.export_motion_blur(rpr_context, instance_key, transform) # exporting visibility from source object indirect_only = kwargs.get("indirect_only", False) @@ -68,3 +71,8 @@ def sync(rpr_context, instance: bpy.types.DepsgraphObjectInstance, **kwargs): else: raise ValueError("Unsupported object type for instance", instance, obj, obj.type) + + +def cache_blur_data(rpr_context, inst: bpy.types.DepsgraphObjectInstance): + if inst.parent.rpr.motion_blur: + rpr_context.transform_cache[key(inst)] = get_transform(inst) diff --git a/src/rprblender/export/mesh.py b/src/rprblender/export/mesh.py index bb29d3e1..c711b889 100644 --- a/src/rprblender/export/mesh.py +++ b/src/rprblender/export/mesh.py @@ -21,7 +21,7 @@ import mathutils import pyrpr -from rprblender.engine.context import RPRContext +from rprblender.engine.context import RPRContext, RPRContext2 from . import object, material, volume from rprblender.utils import get_data_from_collection @@ -82,12 +82,13 @@ def init_from_mesh(mesh: bpy.types.Mesh, calc_area=False, obj=None): data.uvs.append(uvs) data.uv_indices.append(uv_indices) - secondary_uv = mesh.rpr.secondary_uv_layer(obj) - if secondary_uv: - uvs = get_data_from_collection(secondary_uv.data, 'uv', (len(secondary_uv.data), 2)) - if len(uvs) > 0: - data.uvs.append(uvs) - data.uv_indices.append(uv_indices) + if obj: + secondary_uv = mesh.rpr.secondary_uv_layer(obj) + if secondary_uv: + uvs = get_data_from_collection(secondary_uv.data, 'uv', (len(secondary_uv.data), 2)) + if len(uvs) > 0: + data.uvs.append(uvs) + data.uv_indices.append(uv_indices) data.num_face_vertices = np.full((tris_len,), 3, dtype=np.int32) data.vertex_indices = get_data_from_collection(mesh.loop_triangles, 'vertices', @@ -241,6 +242,14 @@ def assign_materials(rpr_context: RPRContext, rpr_shape: pyrpr.Shape, obj: bpy.t if mat.cycles.displacement_method in {'DISPLACEMENT', 'BOTH'}: rpr_displacement = material.sync(rpr_context, mat, 'Displacement', obj=obj) rpr_shape.set_displacement_material(rpr_displacement) + # if no subdivision set that up to 'high' so displacement looks good + # note subdivision is capped to resolution + if rpr_shape.subdivision is None: + rpr_shape.subdivision = { + 'level': 10, + 'boundary': pyrpr.SUBDIV_BOUNDARY_INTERFOP_TYPE_EDGE_AND_CORNER, + 'crease_weight': 10 + } else: rpr_shape.set_displacement_material(None) @@ -274,7 +283,7 @@ def export_visibility(obj, rpr_shape, indirect_only): obj.rpr.set_catchers(rpr_shape) -def sync_visibility(rpr_context, obj: bpy.types.Object, rpr_shape: pyrpr.Shape, indirect_only: bool = False, use_contour: bool = False): +def sync_visibility(rpr_context, obj: bpy.types.Object, rpr_shape: pyrpr.Shape, indirect_only: bool = False): from rprblender.engine.viewport_engine import ViewportEngine rpr_shape.set_visibility( @@ -287,8 +296,7 @@ def sync_visibility(rpr_context, obj: bpy.types.Object, rpr_shape: pyrpr.Shape, export_visibility(obj, rpr_shape, indirect_only) obj.rpr.export_subdivision(rpr_shape) - if use_contour: - pyrpr.ShapeSetContourIgnore(rpr_shape, not obj.rpr.visibility_contour) + rpr_shape.set_contour_ignore(not obj.rpr.visibility_contour) if obj.rpr.portal_light: # Register mesh as a portal light, set "Environment" light group @@ -306,7 +314,6 @@ def sync(rpr_context: RPRContext, obj: bpy.types.Object, **kwargs): mesh = kwargs.get("mesh", obj.data) material_override = kwargs.get("material_override", None) indirect_only = kwargs.get("indirect_only", False) - use_contour = kwargs.get("use_contour", False) log("sync", mesh, obj, "IndirectOnly" if indirect_only else "") obj_key = object.key(obj) @@ -315,12 +322,26 @@ def sync(rpr_context: RPRContext, obj: bpy.types.Object, **kwargs): rpr_context.create_empty_object(obj_key) return - rpr_shape = rpr_context.create_mesh( - obj_key, - data.vertices, data.normals, data.uvs, - data.vertex_indices, data.normal_indices, data.uv_indices, - data.num_face_vertices - ) + deformation_data = rpr_context.deformation_cache.get(obj_key) + if deformation_data and np.any(data.vertices != deformation_data.vertices) and \ + np.any(data.normals != deformation_data.normals): + vertices = np.concatenate((data.vertices, deformation_data.vertices)) + normals = np.concatenate((data.normals, deformation_data.normals)) + rpr_shape = rpr_context.create_mesh( + obj_key, + np.ascontiguousarray(vertices), np.ascontiguousarray(normals), data.uvs, + data.vertex_indices, data.normal_indices, data.uv_indices, + data.num_face_vertices, + {pyrpr.MESH_MOTION_DIMENSION: 2} + ) + else: + rpr_shape = rpr_context.create_mesh( + obj_key, + data.vertices, data.normals, data.uvs, + data.vertex_indices, data.normal_indices, data.uv_indices, + data.num_face_vertices + ) + rpr_shape.set_name(obj.name) rpr_shape.set_id(obj.pass_index) rpr_context.set_aov_index_lookup(obj.pass_index, obj.pass_index, @@ -332,9 +353,12 @@ def sync(rpr_context: RPRContext, obj: bpy.types.Object, **kwargs): assign_materials(rpr_context, rpr_shape, obj, material_override) rpr_context.scene.attach(rpr_shape) - rpr_shape.set_transform(object.get_transform(obj)) - sync_visibility(rpr_context, obj, rpr_shape, indirect_only=indirect_only, use_contour=use_contour) + transform = object.get_transform(obj) + rpr_shape.set_transform(transform) + object.export_motion_blur(rpr_context, obj_key, transform) + + sync_visibility(rpr_context, obj, rpr_shape, indirect_only=indirect_only) def sync_update(rpr_context: RPRContext, obj: bpy.types.Object, is_updated_geometry, is_updated_transform, **kwargs): @@ -356,11 +380,19 @@ def sync_update(rpr_context: RPRContext, obj: bpy.types.Object, is_updated_geome indirect_only = kwargs.get("indirect_only", False) material_override = kwargs.get("material_override", None) - use_contour = kwargs.get("use_contour", False) - - sync_visibility(rpr_context, obj, rpr_shape, indirect_only=indirect_only, use_contour=use_contour) + + sync_visibility(rpr_context, obj, rpr_shape, indirect_only=indirect_only) assign_materials(rpr_context, rpr_shape, obj, material_override) return True sync(rpr_context, obj, **kwargs) return True + + +def cache_blur_data(rpr_context, obj: bpy.types.Object, mesh=None): + obj_key = object.key(obj) + if obj.rpr.motion_blur: + rpr_context.transform_cache[obj_key] = object.get_transform(obj) + + if obj.rpr.deformation_blur and isinstance(rpr_context, RPRContext2): + rpr_context.deformation_cache[obj_key] = MeshData.init_from_mesh(mesh if mesh else obj.data) diff --git a/src/rprblender/export/object.py b/src/rprblender/export/object.py index 31a1b8ba..fcee5ec4 100644 --- a/src/rprblender/export/object.py +++ b/src/rprblender/export/object.py @@ -16,6 +16,7 @@ import numpy as np import bpy +import mathutils from . import mesh, light, camera, to_mesh, volume, openvdb, particle, hair from rprblender.utils import logging @@ -106,3 +107,47 @@ def sync_update(rpr_context, obj: bpy.types.Object, is_updated_geometry, is_upda updated |= particle.sync_update(rpr_context, obj, is_updated_geometry, is_updated_transform) return updated + + +def cache_blur_data(rpr_context, obj: bpy.types.Object): + if obj.type == 'MESH': + if obj.mode == 'OBJECT': + # if in edit mode use to_mesh + mesh.cache_blur_data(rpr_context, obj) + else: + to_mesh.cache_blur_data(rpr_context, obj) + + elif obj.type == 'CAMERA': + camera.cache_blur_data(rpr_context, obj) + + elif obj.type in ('CURVE', 'FONT', 'SURFACE', 'META'): + to_mesh.cache_blur_data(rpr_context, obj) + + +def export_motion_blur(rpr_context, obj_key, transform): + """Use the motion_blur_cache to set the transform motion""" + next_transform = rpr_context.transform_cache.get(obj_key) + if next_transform is None or np.all(transform == next_transform): + return + + rpr_object = rpr_context.objects[obj_key] + if hasattr(rpr_object, 'set_motion_transform'): + rpr_object.set_motion_transform(next_transform) + + else: + m_from = mathutils.Matrix(transform) + m_to = mathutils.Matrix(next_transform) + sub = m_to - m_from + div = m_from @ m_to.inverted() + quat = div.to_quaternion() + + linear = sub.to_translation() + angular = (*quat.axis, quat.angle) if quat.axis.length > 0.5 else (1.0, 0.0, 0.0, 0.0) + scale = div.to_scale() - mathutils.Vector((1, 1, 1)) + + rpr_object.set_linear_motion(*linear) + rpr_object.set_angular_motion(*angular) + if hasattr(rpr_object, 'set_scale_motion'): + rpr_object.set_scale_motion(*scale) + + diff --git a/src/rprblender/export/openvdb.py b/src/rprblender/export/openvdb.py index e3bf4376..b54a242d 100644 --- a/src/rprblender/export/openvdb.py +++ b/src/rprblender/export/openvdb.py @@ -19,7 +19,7 @@ import bpy import pyrpr -from rprblender.utils import helper_lib, IS_WIN, IS_MAC, is_zero +from rprblender.utils import helper_lib, IS_OPENVDB_SUPPORT, is_zero from rprblender.utils import get_sequence_frame_file_path from . import mesh, object, material @@ -102,7 +102,8 @@ def get_volume_file_path(volume, scene_frame): def sync(rpr_context, obj: bpy.types.Object, **kwargs): - if not (IS_WIN or IS_MAC): + if not IS_OPENVDB_SUPPORT: + log.warn("OpenVDB is not supported") return # getting openvdb grid data diff --git a/src/rprblender/export/particle.py b/src/rprblender/export/particle.py index 67e9ea7b..1a7c6ec4 100644 --- a/src/rprblender/export/particle.py +++ b/src/rprblender/export/particle.py @@ -103,8 +103,8 @@ def sync(rpr_context, emitter: bpy.types.Object): if rpr_context.do_motion_blur: if hasattr(instance, 'set_motion_transform'): prev_loc = mathutils.Matrix.Translation(particle.prev_location) - prev_mat = np.array(prev_loc @ rot.to_matrix().to_4x4() @ scale, dtype=np.float32)\ - .reshape(4, 4) + prev_mat = np.array(prev_loc @ rot.to_matrix().to_4x4() @ scale, + dtype=np.float32).reshape(4, 4) instance.set_motion_transform(prev_mat) else: velocity = (particle.location[i] - particle.prev_location[i] for i in range(3)) diff --git a/src/rprblender/export/to_mesh.py b/src/rprblender/export/to_mesh.py index 9fab3702..b924ecd2 100644 --- a/src/rprblender/export/to_mesh.py +++ b/src/rprblender/export/to_mesh.py @@ -19,6 +19,7 @@ import bpy +from rprblender.engine.context import RPRContext2 from . import object, mesh from rprblender.utils import logging @@ -29,8 +30,8 @@ def sync(rpr_context, obj: bpy.types.Object, **kwargs): """ Converts object into blender's mesh and exports it as mesh """ try: - # This operation adds new mesh into bpy.data.meshes, that's why it should be removed after usage. - # obj.to_mesh() could also return None for META objects. + # This operation adds new mesh into bpy.data.meshes, that's why it should be removed + # after usage. obj.to_mesh() could also return None for META objects. new_mesh = obj.to_mesh() log("sync", obj, new_mesh) @@ -67,3 +68,25 @@ def sync_update(rpr_context, obj: bpy.types.Object, is_updated_geometry, is_upda material_override = kwargs.get('material_override', None) return mesh.assign_materials(rpr_context, rpr_shape, obj, material_override=material_override) + + +def cache_blur_data(rpr_context, obj: bpy.types.Object): + if obj.rpr.deformation_blur and isinstance(rpr_context, RPRContext2): + try: + # This operation adds new mesh into bpy.data.meshes, that's why it should be removed + # after usage. obj.to_mesh() could also return None for META objects. + new_mesh = obj.to_mesh() + log("sync", obj, new_mesh) + + if new_mesh: + mesh.cache_blur_data(rpr_context, obj, new_mesh) + return True + + return False + + finally: + # it's important to clear created mesh + obj.to_mesh_clear() + + else: + mesh.cache_blur_data(rpr_context, obj) diff --git a/src/rprblender/nodes/__init__.py b/src/rprblender/nodes/__init__.py index 11f97be2..421b2c7c 100644 --- a/src/rprblender/nodes/__init__.py +++ b/src/rprblender/nodes/__init__.py @@ -61,12 +61,14 @@ def poll(cls, context): NodeItem('RPRShaderNodeUber'), NodeItem('RPRShaderNodePassthrough'), NodeItem('RPRShaderNodeLayered'), + NodeItem('RPRShaderNodeToon'), ]), RPR_ShaderNodeCategory("RPR_TEXTURES", "Texture", items=[ NodeItem('ShaderNodeTexChecker'), NodeItem('ShaderNodeTexGradient'), NodeItem('ShaderNodeTexImage'), NodeItem('ShaderNodeTexNoise'), + NodeItem('ShaderNodeTexVoronoi'), NodeItem('RPRTextureNodeLayered'), ],), RPR_ShaderNodeCategory('RPR_COLOR', "Color", items=[ @@ -139,6 +141,7 @@ def func(cls, context): rpr_nodes.RPRShaderProceduralUVNode, rpr_nodes.RPRShaderNodeLayered, rpr_nodes.RPRTextureNodeLayered, + rpr_nodes.RPRShaderNodeToon, ]) diff --git a/src/rprblender/nodes/blender_nodes.py b/src/rprblender/nodes/blender_nodes.py index 2dc9ae6f..f0d2371b 100644 --- a/src/rprblender/nodes/blender_nodes.py +++ b/src/rprblender/nodes/blender_nodes.py @@ -1383,12 +1383,29 @@ def export(self): res = in1 > in2 elif op == 'MODULO': res = self.create_arithmetic(pyrpr.MATERIAL_NODE_OP_MOD, in1, in2) + else: in3 = self.get_input_value(2) if op == 'MULTIPLY_ADD': res = in1 * in2 + in3 + elif op == 'COMPARE': + # Descriptions from Cycles: Outputs 1.0 if the difference + # between the two input values is less than or equal to Epsilon. + res = abs(in1 - in2) <= in3 + elif op == 'SMOOTH_MIN': + # Descriptions from Cycles: https://en.wikipedia.org/wiki/Smooth_maximum + f1 = math.e ** (in1 * in3) + f2 = math.e ** (in2 * in3) + res = (in1 * f2 + in2 * f1) / (f1 + f2) + elif op == 'SMOOTH_MAX': + # Descriptions from Cycles: https://en.wikipedia.org/wiki/Smooth_maximum + f1 = math.e ** (in1 * in3) + f2 = math.e ** (in2 * in3) + res = (in1 * f1 + in2 * f2) / (f1 + f2) + else: - raise ValueError("Incorrect math operation", op) + res = in1 + log.warn("Unsupported math operation", op) if self.node.use_clamp: res = res.clamp() @@ -1884,6 +1901,57 @@ def export_hybrid(self): return None +class ShaderNodeTexVoronoi(NodeParser): + """Create RPR Voronoi node""" + + def export_rpr2(self): + if self.node.voronoi_dimensions in ('1D', '4D'): + log.warn("Unsupported dimension type", self.node.voronoi_dimensions, self.node, + self.material) + return None + + if self.node.feature != 'F1': + log.warn("Unsupported feature type", self.node.feature, self.node, self.material) + return None + + if self.node.distance != 'EUCLIDEAN': + log.warn("Unsupported distance type", self.node.distance, self.node, self.material) + return None + + scale = self.get_input_value('Scale') + scale *= 3.5 # RPR Voronoi texture visually is about 350% of Blender Voronoi + randomness = self.get_input_value('Randomness') + dimensions = 2 if self.node.voronoi_dimensions == '2D' else 3 + + mapping = self.get_input_link('Vector') + if not mapping: # use default mapping if no external mapping nodes attached + mapping = self.create_node(pyrpr.MATERIAL_NODE_INPUT_LOOKUP, { + pyrpr.MATERIAL_INPUT_VALUE: pyrpr.MATERIAL_NODE_LOOKUP_UV + }) + + out_type = { + 'Distance': pyrpr.VORONOI_OUT_TYPE_DISTANCE, + 'Color': pyrpr.VORONOI_OUT_TYPE_COLOR, + 'Position': pyrpr.VORONOI_OUT_TYPE_POSITION + }[self.socket_out.name] + + voronoi = self.create_node(pyrpr.MATERIAL_NODE_VORONOI_TEXTURE, { + pyrpr.MATERIAL_INPUT_UV: mapping, + pyrpr.MATERIAL_INPUT_SCALE: scale, + pyrpr.MATERIAL_INPUT_RANDOMNESS: randomness, + pyrpr.MATERIAL_INPUT_DIMENSION: dimensions, + pyrpr.MATERIAL_INPUT_OUTTYPE: out_type, + }) + + return voronoi + + def export(self): + return None + + def export_hybrid(self): + return None + + class ShaderNodeMapping(NodeParser): """Creating mix of lookup and math nodes to adjust texture coordinates mapping in a way Cycles do""" diff --git a/src/rprblender/nodes/rpr_nodes.py b/src/rprblender/nodes/rpr_nodes.py index 18e8c139..7f4252f9 100644 --- a/src/rprblender/nodes/rpr_nodes.py +++ b/src/rprblender/nodes/rpr_nodes.py @@ -1513,3 +1513,98 @@ def export_hybrid(self): return None return self.export() + + +class RPRShaderNodeToon(RPRShaderNode): + ''' A toon shader using both the RPR Toon Shader and Ramp node ''' + bl_label = 'RPR Toon' + + def advanced_changed(self, context): + ramp_sockets = ['Shadow Color', "Mid Level","Mid Color", "Highlight Level", "Highlight Color"] + mix_sockets = ["Mid Level Mix", "Highlight Level Mix"] + if self.show_advanced: + self.inputs['Color'].enabled = False + for socket in ramp_sockets: + self.inputs[socket].enabled = True + for socket in mix_sockets: + self.inputs[socket].enabled = self.show_mix_levels + else: + for socket in ramp_sockets + mix_sockets: + self.inputs[socket].enabled = False + self.inputs['Color'].enabled = True + + show_advanced: BoolProperty(name="Advanced", default=False, update=advanced_changed) + show_mix_levels: BoolProperty(name="Mix Levels", default=False, update=advanced_changed) + + def init(self, context): + # Adding input sockets with default_value or hide_value properties. + # Here we use Blender's native node sockets + self.inputs.new('rpr_socket_color', "Color").default_value = (0.8, 0.8, 0.8, 1.0) # Corresponds to Cycles diffuse + self.inputs.new('rpr_socket_weight', "Roughness").default_value = 1.0 + self.inputs.new('NodeSocketVector', "Normal").hide_value = True + + inp = self.inputs.new('rpr_socket_color', "Shadow Color") + inp.default_value = (0.0, 0.0, 0.0, 1.0) # Corresponds to Cycles diffuse + inp.enabled = False + inp = self.inputs.new('rpr_socket_weight', "Mid Level") + inp.default_value = 0.5 + inp.enabled = False + inp = self.inputs.new('rpr_socket_weight', "Mid Level Mix") + inp.default_value = 0.05 + inp.enabled = False + inp = self.inputs.new('rpr_socket_color', "Mid Color") + inp.default_value = (0.4, 0.4, 0.4, 1.0) # Corresponds to Cycles diffuse + inp.enabled = False + inp = self.inputs.new('rpr_socket_weight', "Highlight Level") + inp.default_value = 0.8 + inp.enabled = False + inp = self.inputs.new('rpr_socket_weight', "Highlight Level Mix") + inp.default_value = 0.05 + inp.enabled = False + inp = self.inputs.new('rpr_socket_color', "Highlight Color") + inp.default_value = (0.8, 0.8, 0.8, 1.0) # Corresponds to Cycles diffuse + inp.enabled = False + + # adding output socket + self.outputs.new('NodeSocketShader', "Shader") + + def draw_buttons(self, context, layout): + col = layout.column() + + col.prop(self, 'show_advanced') + if self.show_advanced: + col.prop(self, 'show_mix_levels') + + class Exporter(RuleNodeParser): + def export(self): + if self.node.show_advanced: + # build the toon ramp node + interpolation_mode = pyrpr.INTERPOLATION_MODE_LINEAR if self.node.show_mix_levels \ + else pyrpr.INTERPOLATION_MODE_NONE + ramp = self.create_node(pyrpr.MATERIAL_NODE_TOON_RAMP, { + pyrpr.MATERIAL_INPUT_SHADOW: self.get_input_value('Shadow Color'), + pyrpr.MATERIAL_INPUT_MID: self.get_input_value('Mid Color'), + pyrpr.MATERIAL_INPUT_HIGHLIGHT: self.get_input_value('Highlight Color'), + pyrpr.MATERIAL_INPUT_POSITION1: self.get_input_value('Mid Level'), + pyrpr.MATERIAL_INPUT_POSITION2: self.get_input_value('Highlight Level'), + pyrpr.MATERIAL_INPUT_RANGE1: self.get_input_value('Mid Level Mix'), + pyrpr.MATERIAL_INPUT_RANGE2: self.get_input_value('Highlight Level Mix'), + pyrpr.MATERIAL_INPUT_INTERPOLATION: interpolation_mode, + }) + + toon_shader = self.create_node(pyrpr.MATERIAL_NODE_TOON_CLOSURE, { + pyrpr.MATERIAL_INPUT_COLOR: (1.0, 1.0, 1.0, 1.0), + pyrpr.MATERIAL_INPUT_ROUGHNESS: self.get_input_value('Roughness'), + pyrpr.MATERIAL_INPUT_DIFFUSE_RAMP: ramp + }) + else: + toon_shader = self.create_node(pyrpr.MATERIAL_NODE_TOON_CLOSURE, { + pyrpr.MATERIAL_INPUT_COLOR: self.get_input_value('Color'), + pyrpr.MATERIAL_INPUT_ROUGHNESS: self.get_input_value('Roughness') + }) + + normal = self.get_input_link('Normal') + if normal: + toon_shader.set_input(pyrpr.MATERIAL_INPUT_NORMAL, normal) + + return toon_shader diff --git a/src/rprblender/operators/export_scene.py b/src/rprblender/operators/export_scene.py index 8e9737ed..ee59bea3 100644 --- a/src/rprblender/operators/export_scene.py +++ b/src/rprblender/operators/export_scene.py @@ -175,7 +175,7 @@ def save_json(self, filepath, scene, view_layer, engine_lib_name): output_base = os.path.splitext(filepath)[0] devices = get_user_settings().final_devices - use_contour = scene.rpr.is_contour_used() and not devices.cpu_state + use_contour = view_layer.rpr.use_contour_render and not devices.cpu_state data = { 'width': int(scene.render.resolution_x * scene.render.resolution_percentage / 100), @@ -239,16 +239,16 @@ def save_json(self, filepath, scene, view_layer, engine_lib_name): device_settings[f'gpu{i}'] = int(gpu_state) if use_contour: + contour = view_layer.rpr.contour data['contour'] = { - "object.id": int(scene.rpr.contour_use_object_id), - "material.id": int(scene.rpr.contour_use_material_id), - "normal": int(scene.rpr.contour_use_shading_normal), - "threshold.normal": scene.rpr.contour_normal_threshold, - "linewidth.objid": scene.rpr.contour_object_id_line_width, - "linewidth.matid": scene.rpr.contour_material_id_line_width, - "linewidth.normal": scene.rpr.contour_shading_normal_line_width, - "antialiasing": scene.rpr.contour_antialiasing, - "debug": int(scene.rpr.contour_use_shading_normal) + "object.id": int(contour.use_object_id), + "material.id": int(contour.use_material_id), + "normal": int(contour.use_shading_normal), + "threshold.normal": contour.normal_threshold, + "linewidth.objid": contour.object_id_line_width, + "linewidth.matid": contour.material_id_line_width, + "linewidth.normal": contour.shading_normal_line_width, + "antialiasing": contour.antialiasing, } data['context'] = device_settings diff --git a/src/rprblender/operators/nodes.py b/src/rprblender/operators/nodes.py index 4226b9a4..14f9ff14 100644 --- a/src/rprblender/operators/nodes.py +++ b/src/rprblender/operators/nodes.py @@ -110,37 +110,43 @@ def poll(cls, context): def execute(self, context): # iterate over all objects and find unsupported nodes baked_materials = [] + baked_objs = [] selected_object = context.active_object + selected_layer = context.window.view_layer - for obj in context.scene.objects: - if obj.type != 'MESH': - continue - - for material_slot in obj.material_slots: - if material_slot.material.name in baked_materials: - continue - nt = material_slot.material.node_tree - if nt is None: + for layer in context.scene.view_layers: + context.window.view_layer = layer + for obj in layer.objects: + if obj.type != 'MESH' or obj.name in baked_objs: continue - nodes_to_bake = [] - for node in nt.nodes: - if not get_node_parser_class(node.bl_idname): - nodes_to_bake.append(node) + baked_objs.append(obj.name) - settings = get_user_settings() - resolution = settings.bake_resolution + for material_slot in obj.material_slots: + if material_slot.material.name in baked_materials: + continue + nt = material_slot.material.node_tree + if nt is None: + continue - old_selection = obj.select_get() - obj.select_set(True) - bake_nodes(nt, nodes_to_bake, material_slot.material, int(resolution), obj) - obj.select_set(old_selection) + nodes_to_bake = [] + for node in nt.nodes: + if not get_node_parser_class(node.bl_idname): + nodes_to_bake.append(node) - baked_materials.append(material_slot.material.name) + settings = get_user_settings() + resolution = settings.bake_resolution - selected_object.select_set(True) + old_selection = obj.select_get() + obj.select_set(True) + bake_nodes(nt, nodes_to_bake, material_slot.material, int(resolution), obj) + obj.select_set(old_selection) - return {'FINISHED'} + baked_materials.append(material_slot.material.name) + + context.window.view_layer = selected_layer + selected_object.select_set(True) + return {'FINISHED'} class RPR_NODE_OP_bake_selected_nodes(RPR_Operator): diff --git a/src/rprblender/properties/__init__.py b/src/rprblender/properties/__init__.py index 4e29c7e6..956e3952 100644 --- a/src/rprblender/properties/__init__.py +++ b/src/rprblender/properties/__init__.py @@ -71,6 +71,7 @@ def sync(self, rpr_context): world.RPR_EnvironmentProperties, view_layer.RPR_DenoiserProperties, + view_layer.RPR_ContourProperties, view_layer.RPR_ViewLayerProperites, material_browser.RPR_MaterialBrowserProperties, diff --git a/src/rprblender/properties/object.py b/src/rprblender/properties/object.py index a69d1171..98f99f67 100644 --- a/src/rprblender/properties/object.py +++ b/src/rprblender/properties/object.py @@ -79,12 +79,17 @@ class RPR_ObjectProperites(RPR_Properties): default=True, ) - # Motion Blur + # Motion and Deformation Blur motion_blur: BoolProperty( name="Motion Blur", description="Enable Motion Blur", default=True, ) + deformation_blur: BoolProperty( + name="Deformation Blur", + description="Enable Deformation Blur", + default=False, + ) # Subdivision subdivision: BoolProperty( diff --git a/src/rprblender/properties/render.py b/src/rprblender/properties/render.py index 74b31efe..de2a4a95 100644 --- a/src/rprblender/properties/render.py +++ b/src/rprblender/properties/render.py @@ -61,6 +61,12 @@ class RPR_RenderLimits(bpy.types.PropertyGroup): min=16, default=128, ) + contour_render_samples: IntProperty( + name="Outline Samples", + description="Number of samples for Outline Rendering", + min=1, default=16, + ) + noise_threshold: FloatProperty( name="Noise Threshold", description="Cutoff for adaptive sampling. Once pixels are below this amount of noise, " @@ -466,70 +472,29 @@ def update_render_quality(self, context): default=False, ) + texture_compression: BoolProperty( + name="Texture Compression", + description="Enables Texture compression for faster rendering (with lossier textures)", + default=False, + ) + motion_blur_in_velocity_aov: BoolProperty( name="Only in Velocity AOV", description="Apply Motion Blur in Velocity AOV only\nOnly for Full render quality", default=False, ) - # CONTOUR render mode settings - use_contour_render: BoolProperty( - name="Contour", - description="Use Contour rendering mode. Final render only", - default=False - ) - - contour_use_object_id: BoolProperty( - name="Use Object ID", - description="Use Object ID for Contour rendering", - default=True, - ) - contour_use_material_id: BoolProperty( - name="Use Material Index", - description="Use Material Index for Contour rendering", + viewport_denoiser: BoolProperty( + name="Viewport Denoising", + description="Denoise rendered image with Machine Learning denoiser", default=True, ) - contour_use_shading_normal: BoolProperty( - name="Use Shading Normal", - description="Use Shading Normal for Contour rendering", - default=True, - ) - - contour_object_id_line_width: FloatProperty( - name="Line Width Object", - description="Line width for Object ID contours", - min=1.0, max=10.0, - default=1.0, - ) - contour_material_id_line_width: FloatProperty( - name="Line Width Material", - description="Line width for Material Index contours", - min=1.0, max=10.0, - default=1.0, - ) - contour_shading_normal_line_width: FloatProperty( - name="Line Width Normal", - description="Line width for Shading Normal contours", - min=1.0, max=10.0, - default=1.0, - ) - contour_normal_threshold: FloatProperty( - name="Normal Threshold", - description="Threshold for normals, in degrees", - subtype='ANGLE', - min=0.0, max=math.radians(180.0), - default=math.radians(45.0), - ) - contour_antialiasing: FloatProperty( - name="Antialiasing", - min=0.0, max=1.0, - default=1.0, - ) - - contour_debug_flag: BoolProperty( - name="Feature Debug", - default=False, + viewport_upscale: BoolProperty( + name="Viewport Upscaling", + description="Rendering at 2 times lower resoluting then upscaling rendered image " + "in the end of render", + default=True, ) def init_rpr_context(self, rpr_context, is_final_engine=True, use_gl_interop=False, use_contour_integrator=False): @@ -580,7 +545,7 @@ def init_rpr_context(self, rpr_context, is_final_engine=True, use_gl_interop=Fal else: pyrpr.Context.set_parameter(None, pyrpr.CONTEXT_TRACING_ENABLED, False) - rpr_context.init(context_flags, context_props, use_contour_integrator=use_contour_integrator) + rpr_context.init(context_flags, context_props) if metal_enabled: mac_vers_major = platform.mac_ver()[0].split('.')[1] @@ -637,28 +602,6 @@ def is_contour_available(self, is_final_engine): devices = self.get_devices(is_final_engine=is_final_engine) return self.render_quality == 'FULL2' and not devices.cpu_state - def is_contour_used(self, is_final_engine=True): - return self.is_contour_available(is_final_engine) and self.use_contour_render - - def export_contour_mode(self, rpr_context): - """ set Contour render mode parameters """ - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_USE_OBJECTID, self.contour_use_object_id) - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_USE_MATERIALID, self.contour_use_material_id) - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_USE_NORMAL, self.contour_use_shading_normal) - - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_LINEWIDTH_OBJECTID, self.contour_object_id_line_width) - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_LINEWIDTH_MATERIALID, self.contour_material_id_line_width) - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_LINEWIDTH_NORMAL, self.contour_shading_normal_line_width) - - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_NORMAL_THRESHOLD, math.degrees(self.contour_normal_threshold)) - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_ANTIALIASING, self.contour_antialiasing) - - rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_DEBUG_ENABLED, self.contour_debug_flag) - - rpr_context.enable_aov(pyrpr.AOV_OBJECT_ID) - rpr_context.enable_aov(pyrpr.AOV_MATERIAL_ID) - rpr_context.enable_aov(pyrpr.AOV_SHADING_NORMAL) - def export_pixel_filter(self, rpr_context): """ Exports pixel filter settings """ filter_type = getattr(pyrpr, f"FILTER_{self.pixel_filter}") diff --git a/src/rprblender/properties/view_layer.py b/src/rprblender/properties/view_layer.py index 894acab4..f65d526b 100644 --- a/src/rprblender/properties/view_layer.py +++ b/src/rprblender/properties/view_layer.py @@ -23,6 +23,7 @@ ) import pyrpr +import math from rprblender.utils import logging from . import RPR_Properties @@ -31,6 +32,77 @@ log = logging.Log(tag='properties.view_layer') +class RPR_ContourProperties(RPR_Properties): + """ Propoerties to do a contour pass """ + # CONTOUR render mode settings + + use_object_id: BoolProperty( + name="Use Object ID", + description="Use Object ID for Contour rendering", + default=True, + ) + + use_material_id: BoolProperty( + name="Use Material Index", + description="Use Material Index for Contour rendering", + default=True, + ) + + use_shading_normal: BoolProperty( + name="Use Shading Normal", + description="Use Shading Normal for Contour rendering", + default=True, + ) + + object_id_line_width: FloatProperty( + name="Line Width Object", + description="Line width for Object ID contours", + min=1.0, max=10.0, + default=1.0, + ) + + material_id_line_width: FloatProperty( + name="Line Width Material", + description="Line width for Material Index contours", + min=1.0, max=10.0, + default=1.0, + ) + + shading_normal_line_width: FloatProperty( + name="Line Width Normal", + description="Line width for Shading Normal contours", + min=1.0, max=10.0, + default=1.0, + ) + + normal_threshold: FloatProperty( + name="Normal Threshold", + description="Threshold for normals, in degrees", + subtype='ANGLE', + min=0.0, max=math.radians(180.0), + default=math.radians(45.0), + ) + + antialiasing: FloatProperty( + name="Antialiasing", + min=0.0, max=1.0, + default=1.0, + ) + + def export_contour_settings(self, rpr_context): + """ set Contour render mode parameters """ + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_USE_OBJECTID, self.use_object_id) + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_USE_MATERIALID, self.use_material_id) + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_USE_NORMAL, self.use_shading_normal) + + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_LINEWIDTH_OBJECTID, self.object_id_line_width) + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_LINEWIDTH_MATERIALID, self.material_id_line_width) + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_LINEWIDTH_NORMAL, self.shading_normal_line_width) + + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_NORMAL_THRESHOLD, math.degrees(self.normal_threshold)) + rpr_context.set_parameter(pyrpr.CONTEXT_CONTOUR_ANTIALIASING, self.antialiasing) + + class RPR_DenoiserProperties(RPR_Properties): """ Denoiser properties. This is a child property in RPR_ViewLayerProperties """ enable: BoolProperty( @@ -342,6 +414,13 @@ class RPR_ViewLayerProperites(RPR_Properties): }, ) + contour_info = { + 'rpr': pyrpr.AOV_COLOR, + 'name': "Outline", + 'channel': 'RGBA' + } + + def aov_enabled_changed(self, context): """ Request update of active render passes for Render Layers compositor input node """ context.view_layer.update_render_passes() @@ -370,6 +449,13 @@ def aov_enabled_changed(self, context): # TODO: Probably better to create each aov separately like: aov_depth: BoolProperty(...) denoiser: PointerProperty(type=RPR_DenoiserProperties) + + use_contour_render: BoolProperty( + name="Contour", + description="Use Contour rendering mode. Final render only", + default=False + ) + contour: PointerProperty(type=RPR_ContourProperties) def export_aovs(self, view_layer: bpy.types.ViewLayer, rpr_context, rpr_engine, enable_adaptive, cryptomatte_allowed): """ @@ -410,6 +496,10 @@ def export_aovs(self, view_layer: bpy.types.ViewLayer, rpr_context, rpr_engine, aov = self.cryptomatte_aovs_info[i] rpr_engine.add_pass(aov['name'], len(aov['channel']), aov['channel'], layer=view_layer.name) rpr_context.enable_aov(aov['rpr']) + + if self.use_contour_render: + aov = self.contour_info + rpr_engine.add_pass(aov['name'], len(aov['channel']), aov['channel'], layer=view_layer.name) def enable_aov_by_name(self, name): ''' Enables a give aov name ''' diff --git a/src/rprblender/ui/__init__.py b/src/rprblender/ui/__init__.py index 2997a322..7ba1b9df 100644 --- a/src/rprblender/ui/__init__.py +++ b/src/rprblender/ui/__init__.py @@ -134,7 +134,6 @@ def get_panels(): render.RPR_RENDER_PT_viewport_limits, render.RPR_RENDER_PT_quality, render.RPR_RENDER_PT_max_ray_depth, - render.RPR_RENDER_PT_contour_rendering, render.RPR_RENDER_PT_pixel_filter, render.RPR_RENDER_PT_light_clamping, render.RPR_RENDER_PT_bake_textures, @@ -176,6 +175,7 @@ def get_panels(): view_layer.RPR_VIEWLAYER_PT_aovs, view_layer.RPR_RENDER_PT_override, view_layer.RPR_RENDER_PT_denoiser, + view_layer.RPR_RENDER_PT_contour_rendering, view3d.RPR_VIEW3D_MT_menu, view3d.RPR_VIEW3D_PT_panel, diff --git a/src/rprblender/ui/object.py b/src/rprblender/ui/object.py index 6b0f9992..79bbabd2 100644 --- a/src/rprblender/ui/object.py +++ b/src/rprblender/ui/object.py @@ -41,6 +41,7 @@ def draw(self, context): col = self.layout.column() col.active = context.scene.render.use_motion_blur col.prop(rpr, "motion_blur") + col.prop(rpr, "deformation_blur") class RPR_OBJECT_PT_visibility(RPR_Panel): @@ -54,13 +55,12 @@ def draw(self, context): rpr = context.object.rpr cycles_vis = context.object.cycles_visibility - flow = self.layout.grid_flow(row_major=True, even_columns=True) - flow.column().prop(cycles_vis, 'camera') - flow.column().prop(cycles_vis, 'glossy') - flow.column().prop(cycles_vis, 'transmission') - flow.column().prop(cycles_vis, 'diffuse') - flow.column().prop(cycles_vis, 'shadow') - flow.column().prop(rpr, 'visibility_contour') + self.layout.prop(cycles_vis, 'camera') + self.layout.prop(cycles_vis, 'diffuse') + self.layout.prop(cycles_vis, 'glossy') + self.layout.prop(cycles_vis, 'transmission') + self.layout.prop(cycles_vis, 'shadow') + self.layout.prop(rpr, 'visibility_contour') class RPR_OBJECT_PT_subdivision(RPR_Panel): diff --git a/src/rprblender/ui/render.py b/src/rprblender/ui/render.py index cd77eec4..d7f22c2f 100644 --- a/src/rprblender/ui/render.py +++ b/src/rprblender/ui/render.py @@ -136,6 +136,10 @@ def draw(self, context): col.prop(rpr, 'tile_y') col.prop(rpr, 'tile_order') + col = self.layout.column(align=True) + col.enabled = context.view_layer.rpr.use_contour_render and context.scene.rpr.render_quality == 'FULL2' + col.prop(limits, 'contour_render_samples', slider=False) + class RPR_RENDER_PT_viewport_limits(RPR_Panel): bl_label = "Viewport & Preview Sampling" @@ -170,7 +174,8 @@ def draw(self, context): col.prop(settings, 'use_gl_interop') - col.prop(settings, 'viewport_denoiser_upscale') + col.prop(context.scene.rpr, 'viewport_denoiser') + col.prop(context.scene.rpr, 'viewport_upscale') col.separator() col.prop(limits, 'preview_samples') @@ -196,6 +201,9 @@ def draw(self, context): if rpr.render_quality in ('LOW', 'MEDIUM', 'HIGH'): self.layout.prop(rpr, 'hybrid_low_mem') + if rpr.render_quality == 'FULL2': + self.layout.prop(rpr, 'texture_compression') + class RPR_RENDER_PT_max_ray_depth(RPR_Panel): bl_label = "Max Ray Depth" @@ -219,48 +227,6 @@ def draw(self, context): self.layout.prop(rpr_scene, 'ray_cast_epsilon', slider=True) -class RPR_RENDER_PT_contour_rendering(RPR_Panel): - bl_label = "Contour Rendering" - bl_options = {'DEFAULT_CLOSED'} - - def draw_header(self, context): - self.layout.prop(context.scene.rpr, 'use_contour_render', text="") - self.layout.enabled = context.scene.rpr.render_quality == 'FULL2' - - def draw(self, context): - self.layout.use_property_split = True - self.layout.use_property_decorate = False - - rpr_scene = context.scene.rpr - - main_column = self.layout.column() - main_column.enabled = context.scene.rpr.is_contour_used() - - col = main_column.column(align=True) - col.prop(rpr_scene, 'contour_use_object_id') - args = col.column(align=True) - args.enabled = rpr_scene.contour_use_object_id - args.prop(rpr_scene, 'contour_object_id_line_width', slider=True) - - col = main_column.column(align=True) - col.prop(rpr_scene, 'contour_use_material_id') - args = col.column(align=True) - args.enabled = rpr_scene.contour_use_material_id - args.prop(rpr_scene, 'contour_material_id_line_width', slider=True) - - col = main_column.column(align=True) - col.prop(rpr_scene, 'contour_use_shading_normal') - args = col.column(align=True) - args.enabled = rpr_scene.contour_use_shading_normal - args.prop(rpr_scene, 'contour_normal_threshold', slider=True) - args.prop(rpr_scene, 'contour_shading_normal_line_width', slider=True) - - col = main_column.column(align=True) - col.prop(rpr_scene, 'contour_antialiasing', slider=True) - - main_column.prop(rpr_scene, 'contour_debug_flag') - - class RPR_RENDER_PT_bake_textures(RPR_Panel): bl_label = "Node Baking" bl_parent_id = 'RPR_RENDER_PT_quality' @@ -349,6 +315,7 @@ def draw(self, context): col = layout.column() col.enabled = context.scene.render.use_motion_blur + col.prop(context.scene.cycles, 'motion_blur_position', text="Position", slider=True) col.prop(context.scene.camera.data.rpr, 'motion_blur_exposure', text="Shutter Opening ratio", slider=True) col = layout.column() diff --git a/src/rprblender/ui/view_layer.py b/src/rprblender/ui/view_layer.py index edb6224c..91b0ac90 100644 --- a/src/rprblender/ui/view_layer.py +++ b/src/rprblender/ui/view_layer.py @@ -100,3 +100,46 @@ def draw(self, context): view_layer = context.view_layer layout.prop(view_layer, "material_override") + + +class RPR_RENDER_PT_contour_rendering(RPR_Panel): + bl_label = "Outline Rendering" + bl_options = {'DEFAULT_CLOSED'} + bl_context = "view_layer" + + def draw_header(self, context): + self.layout.prop(context.view_layer.rpr, 'use_contour_render', text="") + self.layout.enabled = context.scene.rpr.render_quality == 'FULL2' + + def draw(self, context): + self.layout.use_property_split = True + self.layout.use_property_decorate = False + + contour_settings = context.view_layer.rpr.contour + + main_column = self.layout.column() + main_column.enabled = context.view_layer.rpr.use_contour_render and context.scene.rpr.render_quality == 'FULL2' + + col = main_column.column(align=True) + col.prop(contour_settings, 'use_object_id') + args = col.column(align=True) + args.enabled = contour_settings.use_object_id + args.prop(contour_settings, 'object_id_line_width', slider=True) + + col = main_column.column(align=True) + col.prop(contour_settings, 'use_material_id') + args = col.column(align=True) + args.enabled = contour_settings.use_material_id + args.prop(contour_settings, 'material_id_line_width', slider=True) + + col = main_column.column(align=True) + col.prop(contour_settings, 'use_shading_normal') + args = col.column(align=True) + args.enabled = contour_settings.use_shading_normal + args.prop(contour_settings, 'normal_threshold', slider=True) + args.prop(contour_settings, 'shading_normal_line_width', slider=True) + + col = main_column.column(align=True) + col.prop(contour_settings, 'antialiasing', slider=True) + + #main_column.prop(view_layer, 'contour_debug_flag') \ No newline at end of file diff --git a/src/rprblender/utils/__init__.py b/src/rprblender/utils/__init__.py index 0df086b9..9611e65e 100644 --- a/src/rprblender/utils/__init__.py +++ b/src/rprblender/utils/__init__.py @@ -187,6 +187,11 @@ def get_tiles_number(): BLENDER_VERSION = f'{bpy.app.version[0]}.{bpy.app.version[1]}' +IS_DEBUG_MODE = bool(int(os.environ.get('RPR_BLENDER_DEBUG', 0))) + +IS_OPENVDB_SUPPORT = (BLENDER_VERSION <= "2.92") and (IS_WIN or IS_MAC) + + from . import logging log = logging.Log(tag='utils') diff --git a/src/rprblender/utils/helper_lib.py b/src/rprblender/utils/helper_lib.py index e7e4b3a1..7c92b7ed 100644 --- a/src/rprblender/utils/helper_lib.py +++ b/src/rprblender/utils/helper_lib.py @@ -13,13 +13,11 @@ # limitations under the License. #******************************************************************** import ctypes -import platform import numpy as np import math import os -from . import package_root_dir, IS_WIN, IS_MAC, core_ver_str, rif_ver_str -from .. import bl_info +from . import package_root_dir, OS, IS_WIN, IS_DEBUG_MODE, IS_OPENVDB_SUPPORT from . import logging log = logging.Log(tag='utils.helper_lib') @@ -36,40 +34,50 @@ class VdbGridData(ctypes.Structure): def init(): global lib - root_dir = package_root_dir() - OS = platform.system() - - paths = [root_dir] - if OS == 'Windows': - lib_name = "RPRBlenderHelper.dll" - paths.append(root_dir / "../../RPRBlenderHelper/.build/Release") + if IS_OPENVDB_SUPPORT: + lib_name = { + 'Windows': "RPRBlenderHelper_vdb.dll", + 'Linux': "libRPRBlenderHelper.so", + 'Darwin': "libRPRBlenderHelper_vdb.dylib" + }[OS] + else: + lib_name = { + 'Windows': "RPRBlenderHelper.dll", + 'Linux': "libRPRBlenderHelper.so", + 'Darwin': "libRPRBlenderHelper.dylib" + }[OS] + + if IS_DEBUG_MODE: + root_dir = package_root_dir().parent.parent + if IS_WIN: + lib_dir = root_dir / 'RPRBlenderHelper/.build/Release' + if IS_OPENVDB_SUPPORT: + os.environ['PATH'] = \ + f"{root_dir / 'RadeonProRenderSharedComponents/OpenVdb/Windows/bin'};" \ + f"{os.environ.get('PATH', '')}" + # NOTE for python 3.8+ we have to use os.add_dll_directory() + # https://docs.python.org/3.8/library/os.html#os.add_dll_directory - if (root_dir / "openvdb.dll").is_file(): - os.environ['PATH'] += ";" + str(root_dir) else: - os.environ['PATH'] += ";" + str((root_dir / "../../RadeonProRenderSharedComponents/OpenVdb/Windows/bin") - .absolute()) - - elif OS == 'Darwin': - lib_name = "libRPRBlenderHelper.dylib" - paths.append(root_dir / "../../RPRBlenderHelper/.build") + lib_dir = root_dir / 'RPRBlenderHelper/.build' + if IS_OPENVDB_SUPPORT: + os.environ['LD_LIBRARY_PATH'] = \ + f"{root_dir / 'RadeonProRenderSharedComponents/OpenVdb/OSX/lib'}:" \ + f"{os.environ.get('LD_LIBRARY_PATH', '')}" else: - lib_name = "libRPRBlenderHelper.so" - paths.append(root_dir / "../../RPRBlenderHelper/.build") - - lib_path = next(p / lib_name for p in paths if (p / lib_name).is_file()) - log('Load lib', lib_path) - try: - lib = ctypes.cdll.LoadLibrary(str(lib_path)) - except OSError as e: # expand the traceback info with the exact addon version and library name - raise Exception(f"Failed to load library {lib_path}", - f"addon version {bl_info['version']}", - f"core {core_ver_str(True)}", - f"rif {rif_ver_str(True)}", - str(e)) \ - from e + root_dir = package_root_dir() + lib_dir = package_root_dir() + + if IS_WIN and IS_OPENVDB_SUPPORT: + if IS_DEBUG_MODE: + ctypes.CDLL(str(root_dir / + 'RadeonProRenderSharedComponents/OpenVdb/Windows/bin/Half-2_3.dll')) + else: + ctypes.CDLL(str(root_dir / 'Half-2_3.dll')) + + lib = ctypes.CDLL(str(lib_dir / lib_name)) # Sun & Sky functions lib.set_sun_horizontal_coordinate.argtypes = [ctypes.c_float, ctypes.c_float] @@ -89,7 +97,7 @@ def init(): lib.get_sun_azimuth.restype = ctypes.c_float lib.get_sun_altitude.restype = ctypes.c_float - if IS_WIN or IS_MAC: + if IS_OPENVDB_SUPPORT: # OpenVdb functions lib.vdb_read_grids_list.argtypes = [ctypes.c_char_p] lib.vdb_read_grids_list.restype = ctypes.c_char_p @@ -178,6 +186,3 @@ def vdb_read_grid_data(vdb_file, grid_name): lib.vdb_free_grid_data(ctypes.byref(data)) return res - - -init() diff --git a/src/rprblender/utils/install_libs.py b/src/rprblender/utils/install_libs.py index e2b0e97c..f9c4db64 100644 --- a/src/rprblender/utils/install_libs.py +++ b/src/rprblender/utils/install_libs.py @@ -15,14 +15,20 @@ import sys import site import subprocess +from datetime import datetime, timedelta import bpy -from . import IS_MAC, IS_LINUX +from . import IS_MAC, IS_LINUX, package_root_dir +from rprblender import config from rprblender.utils.logging import Log log = Log(tag="install_libs") +PIP_CHECK_FILENAME = "pip_check.txt" +NEXT_TIME_CHECK_DELTA = 5 # 5 days + + # adding user site-packages path to sys.path if site.getusersitepackages() not in sys.path: sys.path.append(site.getusersitepackages()) @@ -47,10 +53,20 @@ def ensure_boto3(): Use this snippet to install boto3 library with all the dependencies if absent at the addon launch time Note: still it will be available at the next Blender launch only """ + pip_check_file = package_root_dir() / PIP_CHECK_FILENAME + try: import boto3 except ImportError: + # checking if we need to install boto3 + if pip_check_file.is_file(): + str_time = pip_check_file.read_text() + next_time_check = datetime.fromisoformat(str_time) + if datetime.now() < next_time_check: + config.disable_athena_report = True + return + log.info("Installing boto3 library...") try: if IS_MAC or IS_LINUX: @@ -64,3 +80,12 @@ def ensure_boto3(): except subprocess.SubprocessError as e: log.warn("Something went wrong, unable to install boto3 library.", e) + + # after failing installation of boto3 set next date to try install boto3 + next_time_check = datetime.now() + timedelta(NEXT_TIME_CHECK_DELTA) + pip_check_file.write_text(next_time_check.isoformat()) + config.disable_athena_report = True + return + + if pip_check_file.is_file(): + pip_check_file.unlink()