diff options
Diffstat (limited to 'intern/cycles/blender')
-rw-r--r-- | intern/cycles/blender/CMakeLists.txt | 1 | ||||
-rw-r--r-- | intern/cycles/blender/addon/__init__.py | 6 | ||||
-rw-r--r-- | intern/cycles/blender/addon/operators.py | 133 | ||||
-rw-r--r-- | intern/cycles/blender/addon/properties.py | 6 | ||||
-rw-r--r-- | intern/cycles/blender/addon/ui.py | 23 | ||||
-rw-r--r-- | intern/cycles/blender/blender_python.cpp | 121 | ||||
-rw-r--r-- | intern/cycles/blender/blender_session.cpp | 8 |
7 files changed, 271 insertions, 27 deletions
diff --git a/intern/cycles/blender/CMakeLists.txt b/intern/cycles/blender/CMakeLists.txt index 769bc788ab5..84e2690333e 100644 --- a/intern/cycles/blender/CMakeLists.txt +++ b/intern/cycles/blender/CMakeLists.txt @@ -41,6 +41,7 @@ set(SRC set(ADDON_FILES addon/__init__.py addon/engine.py + addon/operators.py addon/osl.py addon/presets.py addon/properties.py diff --git a/intern/cycles/blender/addon/__init__.py b/intern/cycles/blender/addon/__init__.py index 038126278aa..1f148538328 100644 --- a/intern/cycles/blender/addon/__init__.py +++ b/intern/cycles/blender/addon/__init__.py @@ -37,6 +37,8 @@ if "bpy" in locals(): importlib.reload(version_update) if "ui" in locals(): importlib.reload(ui) + if "operators" in locals(): + importlib.reload(operators) if "properties" in locals(): importlib.reload(properties) if "presets" in locals(): @@ -118,6 +120,7 @@ classes = ( def register(): from bpy.utils import register_class from . import ui + from . import operators from . import properties from . import presets import atexit @@ -130,6 +133,7 @@ def register(): properties.register() ui.register() + operators.register() presets.register() for cls in classes: @@ -141,6 +145,7 @@ def register(): def unregister(): from bpy.utils import unregister_class from . import ui + from . import operators from . import properties from . import presets import atexit @@ -148,6 +153,7 @@ def unregister(): bpy.app.handlers.version_update.remove(version_update.do_versions) ui.unregister() + operators.unregister() properties.unregister() presets.unregister() diff --git a/intern/cycles/blender/addon/operators.py b/intern/cycles/blender/addon/operators.py new file mode 100644 index 00000000000..c39aa386203 --- /dev/null +++ b/intern/cycles/blender/addon/operators.py @@ -0,0 +1,133 @@ +# +# Copyright 2011-2019 Blender Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# <pep8 compliant> + +import bpy +from bpy.types import Operator +from bpy.props import StringProperty + + +class CYCLES_OT_use_shading_nodes(Operator): + """Enable nodes on a material, world or light""" + bl_idname = "cycles.use_shading_nodes" + bl_label = "Use Nodes" + + @classmethod + def poll(cls, context): + return (getattr(context, "material", False) or getattr(context, "world", False) or + getattr(context, "light", False)) + + def execute(self, context): + if context.material: + context.material.use_nodes = True + elif context.world: + context.world.use_nodes = True + elif context.light: + context.light.use_nodes = True + + return {'FINISHED'} + + +class CYCLES_OT_denoise_animation(Operator): + """Denoise rendered animation sequence using current scene and view """ \ + """layer settings. Requires denoising data passes and output to """ \ + """OpenEXR multilayer files""" + bl_idname = "cycles.denoise_animation" + bl_label = "Denoise Animation" + + input_filepath = StringProperty( + name='Input Filepath', + description='File path for frames to denoise. If not specified, uses the render file path from the scene', + default='', + subtype='FILE_PATH') + + output_filepath = StringProperty( + name='Output Filepath', + description='If not specified, renders will be denoised in-place', + default='', + subtype='FILE_PATH') + + def execute(self, context): + import os + + preferences = context.user_preferences + scene = context.scene + render_layer = scene.render.layers.active + + in_filepath = self.input_filepath + out_filepath = self.output_filepath + + if in_filepath == '': + in_filepath = scene.render.filepath + if out_filepath == '': + out_filepath = in_filepath + + # Backup since we will overwrite the scene path temporarily + original_filepath = scene.render.filepath + + # Expand filepaths for each frame so we match Blender render output exactly. + in_filepaths = [] + out_filepaths = [] + + for frame in range(scene.frame_start, scene.frame_end + 1): + scene.render.filepath = in_filepath + filepath = scene.render.frame_path(frame=frame) + in_filepaths.append(filepath) + + if not os.path.isfile(filepath): + scene.render.filepath = original_filepath + self.report({'ERROR'}, f"Frame '{filepath}' not found, animation must be complete.") + return {'CANCELLED'} + + scene.render.filepath = out_filepath + filepath = scene.render.frame_path(frame=frame) + out_filepaths.append(filepath) + + scene.render.filepath = original_filepath + + # Run denoiser + # TODO: support cancel and progress reports. + import _cycles + try: + _cycles.denoise(preferences.as_pointer(), + scene.as_pointer(), + render_layer.as_pointer(), + input=in_filepaths, + output=out_filepaths) + except Exception as e: + self.report({'ERROR'}, str(e)) + return {'FINISHED'} + + self.report({'INFO'}, "Denoising completed.") + return {'FINISHED'} + + +classes = ( + CYCLES_OT_use_shading_nodes, + CYCLES_OT_denoise_animation +) + +def register(): + from bpy.utils import register_class + for cls in classes: + register_class(cls) + + +def unregister(): + from bpy.utils import unregister_class + for cls in classes: + unregister_class(cls) diff --git a/intern/cycles/blender/addon/properties.py b/intern/cycles/blender/addon/properties.py index 23ab1cf6a30..1106923f529 100644 --- a/intern/cycles/blender/addon/properties.py +++ b/intern/cycles/blender/addon/properties.py @@ -1344,6 +1344,12 @@ class CyclesRenderLayerSettings(bpy.types.PropertyGroup): default=False, update=update_render_passes, ) + denoising_neighbor_frames: IntProperty( + name="Neighbor Frames", + description="Number of neighboring frames to use for denoising animations (more frames produce smoother results at the cost of performance)", + min=0, max=7, + default=0, + ) cls.use_pass_crypto_object = BoolProperty( name="Cryptomatte Object", description="Render cryptomatte object pass, for isolating objects in compositing", diff --git a/intern/cycles/blender/addon/ui.py b/intern/cycles/blender/addon/ui.py index 2f1adfe4178..e372843d763 100644 --- a/intern/cycles/blender/addon/ui.py +++ b/intern/cycles/blender/addon/ui.py @@ -22,7 +22,6 @@ import _cycles from bpy.types import ( Panel, Menu, - Operator, ) @@ -912,27 +911,6 @@ class CYCLES_OBJECT_PT_cycles_settings(CyclesButtonsPanel, Panel): sub.prop(cob, "use_distance_cull") -class CYCLES_OT_use_shading_nodes(Operator): - """Enable nodes on a material, world or lamp""" - bl_idname = "cycles.use_shading_nodes" - bl_label = "Use Nodes" - - @classmethod - def poll(cls, context): - return (getattr(context, "material", False) or getattr(context, "world", False) or - getattr(context, "lamp", False)) - - def execute(self, context): - if context.material: - context.material.use_nodes = True - elif context.world: - context.world.use_nodes = True - elif context.lamp: - context.lamp.use_nodes = True - - return {'FINISHED'} - - def find_node(material, nodetype): if material and material.node_tree: ntree = material.node_tree @@ -1870,7 +1848,6 @@ classes = ( CYCLES_PT_context_material, CYCLES_OBJECT_PT_motion_blur, CYCLES_OBJECT_PT_cycles_settings, - CYCLES_OT_use_shading_nodes, CYCLES_LAMP_PT_preview, CYCLES_LAMP_PT_lamp, CYCLES_LAMP_PT_nodes, diff --git a/intern/cycles/blender/blender_python.cpp b/intern/cycles/blender/blender_python.cpp index 9dd0cd4c0bc..bf7605ed5b1 100644 --- a/intern/cycles/blender/blender_python.cpp +++ b/intern/cycles/blender/blender_python.cpp @@ -18,9 +18,12 @@ #include "blender/CCL_api.h" +#include "blender/blender_device.h" #include "blender/blender_sync.h" #include "blender/blender_session.h" +#include "render/denoising.h" + #include "util/util_debug.h" #include "util/util_foreach.h" #include "util/util_logging.h" @@ -623,6 +626,121 @@ static PyObject *opencl_disable_func(PyObject * /*self*/, PyObject * /*value*/) } #endif +static bool denoise_parse_filepaths(PyObject *pyfilepaths, vector<string>& filepaths) +{ + + if(PyUnicode_Check(pyfilepaths)) { + const char *filepath = PyUnicode_AsUTF8(pyfilepaths); + filepaths.push_back(filepath); + return true; + } + + PyObject *sequence = PySequence_Fast(pyfilepaths, "File paths must be a string or sequence of strings"); + if(sequence == NULL) { + return false; + } + + for(Py_ssize_t i = 0; i < PySequence_Fast_GET_SIZE(sequence); i++) { + PyObject *item = PySequence_Fast_GET_ITEM(sequence, i); + const char *filepath = PyUnicode_AsUTF8(item); + if(filepath == NULL) { + PyErr_SetString(PyExc_ValueError, "File paths must be a string or sequence of strings."); + Py_DECREF(sequence); + return false; + } + filepaths.push_back(filepath); + } + Py_DECREF(sequence); + + return true; +} + +static PyObject *denoise_func(PyObject * /*self*/, PyObject *args, PyObject *keywords) +{ + static const char *keyword_list[] = {"preferences", "scene", "view_layer", + "input", "output", + "tile_size", "samples", NULL}; + PyObject *pypreferences, *pyscene, *pyrenderlayer; + PyObject *pyinput, *pyoutput = NULL; + int tile_size = 0, samples = 0; + + if (!PyArg_ParseTupleAndKeywords(args, keywords, "OOOO|Oii", (char**)keyword_list, + &pypreferences, &pyscene, &pyrenderlayer, + &pyinput, &pyoutput, + &tile_size, &samples)) { + return NULL; + } + + /* Get device specification from preferences and scene. */ + PointerRNA preferencesptr; + RNA_pointer_create(NULL, &RNA_UserPreferences, (void*)PyLong_AsVoidPtr(pypreferences), &preferencesptr); + BL::UserPreferences b_preferences(preferencesptr); + + PointerRNA sceneptr; + RNA_id_pointer_create((ID*)PyLong_AsVoidPtr(pyscene), &sceneptr); + BL::Scene b_scene(sceneptr); + + DeviceInfo device = blender_device_info(b_preferences, b_scene, true); + + /* Get denoising parameters from view layer. */ + PointerRNA renderlayerptr; + RNA_pointer_create((ID*)PyLong_AsVoidPtr(pyscene), &RNA_SceneRenderLayer, PyLong_AsVoidPtr(pyrenderlayer), &renderlayerptr); + PointerRNA crenderlayer = RNA_pointer_get(&renderlayerptr, "cycles"); + + DenoiseParams params; + params.radius = get_int(crenderlayer, "denoising_radius"); + params.strength = get_float(crenderlayer, "denoising_strength"); + params.feature_strength = get_float(crenderlayer, "denoising_feature_strength"); + params.relative_pca = get_boolean(crenderlayer, "denoising_relative_pca"); + params.neighbor_frames = get_int(crenderlayer, "denoising_neighbor_frames"); + + /* Parse file paths list. */ + vector<string> input, output; + + if(!denoise_parse_filepaths(pyinput, input)) { + return NULL; + } + + if(pyoutput) { + if(!denoise_parse_filepaths(pyoutput, output)) { + return NULL; + } + } + else { + output = input; + } + + if(input.empty()) { + PyErr_SetString(PyExc_ValueError, "No input file paths specified."); + return NULL; + } + if(input.size() != output.size()) { + PyErr_SetString(PyExc_ValueError, "Number of input and output file paths does not match."); + return NULL; + } + + /* Create denoiser. */ + Denoiser denoiser(device); + denoiser.params = params; + denoiser.input = input; + denoiser.output = output; + + if (tile_size > 0) { + denoiser.tile_size = make_int2(tile_size, tile_size); + } + if (samples > 0) { + denoiser.samples_override = samples; + } + + /* Run denoiser. */ + if(!denoiser.run()) { + PyErr_SetString(PyExc_ValueError, denoiser.error.c_str()); + return NULL; + } + + Py_RETURN_NONE; +} + static PyObject *debug_flags_update_func(PyObject * /*self*/, PyObject *args) { PyObject *pyscene; @@ -783,6 +901,9 @@ static PyMethodDef methods[] = { {"opencl_disable", opencl_disable_func, METH_NOARGS, ""}, #endif + /* Standalone denoising */ + {"denoise", (PyCFunction)denoise_func, METH_VARARGS|METH_KEYWORDS, ""}, + /* Debugging routines */ {"debug_flags_update", debug_flags_update_func, METH_VARARGS, ""}, {"debug_flags_reset", debug_flags_reset_func, METH_NOARGS, ""}, diff --git a/intern/cycles/blender/blender_session.cpp b/intern/cycles/blender/blender_session.cpp index 50ac35069a9..43d7ff49a3b 100644 --- a/intern/cycles/blender/blender_session.cpp +++ b/intern/cycles/blender/blender_session.cpp @@ -431,10 +431,10 @@ void BlenderSession::render() session->params.run_denoising = run_denoising; session->params.full_denoising = full_denoising; session->params.write_denoising_passes = write_denoising_passes; - session->params.denoising_radius = get_int(crl, "denoising_radius"); - session->params.denoising_strength = get_float(crl, "denoising_strength"); - session->params.denoising_feature_strength = get_float(crl, "denoising_feature_strength"); - session->params.denoising_relative_pca = get_boolean(crl, "denoising_relative_pca"); + session->params.denoising.radius = get_int(crl, "denoising_radius"); + session->params.denoising.strength = get_float(crl, "denoising_strength"); + session->params.denoising.feature_strength = get_float(crl, "denoising_feature_strength"); + session->params.denoising.relative_pca = get_boolean(crl, "denoising_relative_pca"); scene->film->denoising_data_pass = buffer_params.denoising_data_pass; scene->film->denoising_clean_pass = buffer_params.denoising_clean_pass; |