From 74ffed0a3409d24abc48852500e9009b92d94fd7 Mon Sep 17 00:00:00 2001 From: Christoph Neuhauser Date: Wed, 6 May 2026 10:34:23 +0200 Subject: [PATCH] Cycles: Add denoising backward motion vector pass with depth delta A new backward motion vector denoising pass is added to Cycles. Its x and y component contain the same screen space motion vector data as the vector data pass, but the z component additionally stores the linear depth delta in camera space. A future version of OIDN plans to use this for viewport denoising. Thus, only backward motion vectors with linear depth delta, but not forward motion vectors are needed for now. Pull Request: https://projects.blender.org/blender/blender/pulls/157787 --- intern/cycles/blender/addon/engine.py | 1 + intern/cycles/blender/addon/properties.py | 1 + intern/cycles/blender/sync.cpp | 1 + intern/cycles/device/denoise.h | 1 + intern/cycles/kernel/data_template.h | 2 + intern/cycles/kernel/film/denoising_passes.h | 6 + intern/cycles/kernel/geom/primitive.h | 119 ++++++++++++------- intern/cycles/kernel/types.h | 16 ++- intern/cycles/scene/camera.cpp | 16 ++- intern/cycles/scene/film.cpp | 13 +- intern/cycles/scene/pass.cpp | 2 + intern/cycles/scene/scene.cpp | 4 +- 12 files changed, 125 insertions(+), 57 deletions(-) diff --git a/intern/cycles/blender/addon/engine.py b/intern/cycles/blender/addon/engine.py index 77b6832f74b..3c9f2ac0217 100644 --- a/intern/cycles/blender/addon/engine.py +++ b/intern/cycles/blender/addon/engine.py @@ -251,6 +251,7 @@ def list_render_passes(scene, srl): yield (n_("Denoising Normal"), "XYZ", 'VECTOR') yield (n_("Denoising Roughness"), "X", 'VALUE') yield (n_("Denoising Depth"), "Z", 'VALUE') + yield (n_("Denoising Backward Motion"), "XYZ", 'VECTOR') # Custom AOV passes. for aov in srl.aovs: diff --git a/intern/cycles/blender/addon/properties.py b/intern/cycles/blender/addon/properties.py index f67ce451daf..c4e085ef26f 100644 --- a/intern/cycles/blender/addon/properties.py +++ b/intern/cycles/blender/addon/properties.py @@ -223,6 +223,7 @@ enum_view3d_shading_render_pass = ( ('DENOISING_SPECULAR_ALBEDO', "Denoising Specular Albedo", "Specular albedo pass used by denoiser"), ('DENOISING_NORMAL', "Denoising Normal", "Normal pass used by denoiser"), ('DENOISING_ROUGHNESS', "Denoising Roughness", "Roughness pass used by denoiser"), + ('DENOISING_BACKWARD_MOTION', "Denoising Backward Motion", "Backward motion pass used by denoiser"), ('SAMPLE_COUNT', "Sample Count", "Per-pixel number of samples"), ) diff --git a/intern/cycles/blender/sync.cpp b/intern/cycles/blender/sync.cpp index c8d665395b6..c85002c186f 100644 --- a/intern/cycles/blender/sync.cpp +++ b/intern/cycles/blender/sync.cpp @@ -742,6 +742,7 @@ static bool get_known_pass_type(blender::RenderPass &b_pass, PassType &type, Pas MAP_PASS("Denoising Normal", PASS_DENOISING_NORMAL, true); MAP_PASS("Denoising Roughness", PASS_DENOISING_ROUGHNESS, true); MAP_PASS("Denoising Depth", PASS_DENOISING_DEPTH, true); + MAP_PASS("Denoising Backward Motion", PASS_DENOISING_BACKWARD_MOTION, true); MAP_PASS("Shadow Catcher", PASS_SHADOW_CATCHER, false); MAP_PASS("Noisy Shadow Catcher", PASS_SHADOW_CATCHER, true); diff --git a/intern/cycles/device/denoise.h b/intern/cycles/device/denoise.h index 2b84f12188c..6c327a0b9da 100644 --- a/intern/cycles/device/denoise.h +++ b/intern/cycles/device/denoise.h @@ -30,6 +30,7 @@ enum DenoiserPass { DENOISER_PASS_ROUGHNESS = 1 << 3, DENOISER_PASS_DEPTH = 1 << 4, DENOISER_PASS_MOTION = 1 << 5, + DENOISER_PASS_BACKWARD_MOTION = 1 << 6, }; using DenoiserPassMask = int; diff --git a/intern/cycles/kernel/data_template.h b/intern/cycles/kernel/data_template.h index 7b4cd8c23c2..b670f0df021 100644 --- a/intern/cycles/kernel/data_template.h +++ b/intern/cycles/kernel/data_template.h @@ -77,6 +77,7 @@ KERNEL_STRUCT_MEMBER(film, int, is_rec709) KERNEL_STRUCT_MEMBER(film, float, exposure) /* Passed used. */ KERNEL_STRUCT_MEMBER(film, int, pass_flag) +KERNEL_STRUCT_MEMBER(film, int, denoising_pass_flag) KERNEL_STRUCT_MEMBER(film, int, light_pass_flag) /* Pass offsets. */ KERNEL_STRUCT_MEMBER(film, int, pass_stride) @@ -133,6 +134,7 @@ KERNEL_STRUCT_MEMBER(film, int, pass_denoising_specular_albedo) KERNEL_STRUCT_MEMBER(film, int, pass_denoising_normal) KERNEL_STRUCT_MEMBER(film, int, pass_denoising_roughness) KERNEL_STRUCT_MEMBER(film, int, pass_denoising_depth) +KERNEL_STRUCT_MEMBER(film, int, pass_denoising_backward_motion) /* AOVs. */ KERNEL_STRUCT_MEMBER(film, int, pass_aov_color) KERNEL_STRUCT_MEMBER(film, int, pass_aov_value) diff --git a/intern/cycles/kernel/film/denoising_passes.h b/intern/cycles/kernel/film/denoising_passes.h index f741584fddc..ab2b6b5d236 100644 --- a/intern/cycles/kernel/film/denoising_passes.h +++ b/intern/cycles/kernel/film/denoising_passes.h @@ -134,6 +134,12 @@ ccl_device_forceinline void film_write_denoising_features_surface(KernelGlobals film_write_pass_spectrum(buffer + kernel_data.film.pass_denoising_specular_albedo, denoising_specular_albedo); } + + if (kernel_data.film.pass_denoising_backward_motion != PASS_UNUSED) { + const float3 backward_motion = primitive_motion_vector_backward_depth_delta(kg, sd); + film_write_pass_float3(buffer + kernel_data.film.pass_denoising_backward_motion, + backward_motion); + } } /* Portion deferred to the next bounce. Specularity uses the feature weight, transparent diff --git a/intern/cycles/kernel/geom/primitive.h b/intern/cycles/kernel/geom/primitive.h index 7c018e86ee6..7835461d65d 100644 --- a/intern/cycles/kernel/geom/primitive.h +++ b/intern/cycles/kernel/geom/primitive.h @@ -184,42 +184,42 @@ ccl_device Float3Type primitive_tangent(KernelGlobals kg, ccl_private ShaderData #endif } -/* Motion vector for motion pass */ +/* Motion vector common */ -ccl_device_forceinline float4 primitive_motion_vector(KernelGlobals kg, - const ccl_private ShaderData *sd) +ccl_device_forceinline void primitive_motion_data_without_camera(KernelGlobals kg, + const ccl_private ShaderData *sd, + ccl_private float3 *motion_center, + ccl_private float3 *motion_pre, + ccl_private float3 *motion_post) { - /* center position */ - float3 center; - #if defined(__HAIR__) || defined(__POINTCLOUD__) const bool is_curve_or_point = sd->type & (PRIMITIVE_CURVE | PRIMITIVE_POINT); if (is_curve_or_point) { - center = make_float3(0.0f, 0.0f, 0.0f); + *motion_center = make_float3(0.0f, 0.0f, 0.0f); if (sd->type & PRIMITIVE_CURVE) { # if defined(__HAIR__) - center = curve_motion_center_location(kg, sd); + *motion_center = curve_motion_center_location(kg, sd); # endif } else if (sd->type & PRIMITIVE_POINT) { # if defined(__POINTCLOUD__) - center = point_motion_center_location(kg, sd); + *motion_center = point_motion_center_location(kg, sd); # endif } if (!(sd->object_flag & SD_OBJECT_TRANSFORM_APPLIED)) { - object_position_transform(kg, sd, ¢er); + object_position_transform(kg, sd, motion_center); } } else #endif { - center = sd->P; + *motion_center = sd->P; } - float3 motion_pre = center; - float3 motion_post = center; + *motion_pre = *motion_center; + *motion_post = *motion_center; /* deformation motion */ AttributeDescriptor desc = find_attribute(kg, sd, ATTR_STD_MOTION_VERTEX_POSITION); @@ -230,14 +230,14 @@ ccl_device_forceinline float4 primitive_motion_vector(KernelGlobals kg, #if defined(__HAIR__) || defined(__POINTCLOUD__) if (is_curve_or_point) { - motion_pre = make_float3(primitive_surface_attribute(kg, sd, desc)); + *motion_pre = make_float3(primitive_surface_attribute(kg, sd, desc)); desc.offset += numverts; - motion_post = make_float3(primitive_surface_attribute(kg, sd, desc)); + *motion_post = make_float3(primitive_surface_attribute(kg, sd, desc)); /* Curve */ if ((sd->object_flag & SD_OBJECT_HAS_VERTEX_MOTION) == 0) { - object_position_transform(kg, sd, &motion_pre); - object_position_transform(kg, sd, &motion_post); + object_position_transform(kg, sd, motion_pre); + object_position_transform(kg, sd, motion_post); } } else @@ -245,9 +245,9 @@ ccl_device_forceinline float4 primitive_motion_vector(KernelGlobals kg, if (sd->type & PRIMITIVE_TRIANGLE) { /* Triangle */ - motion_pre = triangle_attribute(kg, sd, desc); + *motion_pre = triangle_attribute(kg, sd, desc); desc.offset += numverts; - motion_post = triangle_attribute(kg, sd, desc); + *motion_post = triangle_attribute(kg, sd, desc); } } @@ -256,12 +256,18 @@ ccl_device_forceinline float4 primitive_motion_vector(KernelGlobals kg, Transform tfm; tfm = object_fetch_motion_pass_transform(kg, sd->object, OBJECT_PASS_MOTION_PRE); - motion_pre = transform_point(&tfm, motion_pre); + *motion_pre = transform_point(&tfm, *motion_pre); tfm = object_fetch_motion_pass_transform(kg, sd->object, OBJECT_PASS_MOTION_POST); - motion_post = transform_point(&tfm, motion_post); + *motion_post = transform_point(&tfm, *motion_post); +} - float3 motion_center; +ccl_device_forceinline void primitive_motion_data_camera_step(KernelGlobals kg, + ccl_private float3 *motion_center, + ccl_private float3 *motion_pre, + ccl_private float3 *motion_post) +{ + Transform tfm; /* camera motion, for perspective/orthographic motion.pre/post will be a * world-to-raster matrix, for panorama it's world-to-camera, for custom @@ -270,45 +276,55 @@ ccl_device_forceinline float4 primitive_motion_vector(KernelGlobals kg, /* TODO: Custom cameras don't have inverse mappings yet, so we fall back to * camera-space vectors here for now. */ tfm = kernel_data.cam.worldtocamera; - motion_center = normalize(transform_point(&tfm, center)); + *motion_center = normalize(transform_point(&tfm, *motion_center)); tfm = kernel_data.cam.motion_pass_pre; - motion_pre = normalize(transform_point(&tfm, motion_pre)); + *motion_pre = normalize(transform_point(&tfm, *motion_pre)); tfm = kernel_data.cam.motion_pass_post; - motion_post = normalize(transform_point(&tfm, motion_post)); + *motion_post = normalize(transform_point(&tfm, *motion_post)); } else if (kernel_data.cam.type != CAMERA_PANORAMA) { /* Perspective and orthographics camera use the world-to-raster matrix. */ ProjectionTransform projection = kernel_data.cam.worldtoraster; - motion_center = transform_perspective(&projection, center); + *motion_center = transform_perspective(&projection, *motion_center); projection = kernel_data.cam.perspective_pre; - motion_pre = transform_perspective(&projection, motion_pre); + *motion_pre = transform_perspective(&projection, *motion_pre); projection = kernel_data.cam.perspective_post; - motion_post = transform_perspective(&projection, motion_post); + *motion_post = transform_perspective(&projection, *motion_post); } else { /* Panorama cameras have their own inverse mappings. */ tfm = kernel_data.cam.worldtocamera; - motion_center = normalize(transform_point(&tfm, center)); - motion_center = make_float3(direction_to_panorama(&kernel_data.cam, motion_center)); - motion_center.x *= kernel_data.cam.width; - motion_center.y *= kernel_data.cam.height; + *motion_center = normalize(transform_point(&tfm, *motion_center)); + *motion_center = make_float3(direction_to_panorama(&kernel_data.cam, *motion_center)); + motion_center->x *= kernel_data.cam.width; + motion_center->y *= kernel_data.cam.height; tfm = kernel_data.cam.motion_pass_pre; - motion_pre = normalize(transform_point(&tfm, motion_pre)); - motion_pre = make_float3(direction_to_panorama(&kernel_data.cam, motion_pre)); - motion_pre.x *= kernel_data.cam.width; - motion_pre.y *= kernel_data.cam.height; + *motion_pre = normalize(transform_point(&tfm, *motion_pre)); + *motion_pre = make_float3(direction_to_panorama(&kernel_data.cam, *motion_pre)); + motion_pre->x *= kernel_data.cam.width; + motion_pre->y *= kernel_data.cam.height; tfm = kernel_data.cam.motion_pass_post; - motion_post = normalize(transform_point(&tfm, motion_post)); - motion_post = make_float3(direction_to_panorama(&kernel_data.cam, motion_post)); - motion_post.x *= kernel_data.cam.width; - motion_post.y *= kernel_data.cam.height; + *motion_post = normalize(transform_point(&tfm, *motion_post)); + *motion_post = make_float3(direction_to_panorama(&kernel_data.cam, *motion_post)); + motion_post->x *= kernel_data.cam.width; + motion_post->y *= kernel_data.cam.height; } +} + +/* Motion vector for motion pass */ + +ccl_device_forceinline float4 primitive_motion_vector(KernelGlobals kg, + const ccl_private ShaderData *sd) +{ + float3 motion_center, motion_pre, motion_post; + primitive_motion_data_without_camera(kg, sd, &motion_center, &motion_pre, &motion_post); + primitive_motion_data_camera_step(kg, &motion_center, &motion_pre, &motion_post); motion_pre = motion_pre - motion_center; motion_post = motion_center - motion_post; @@ -316,4 +332,27 @@ ccl_device_forceinline float4 primitive_motion_vector(KernelGlobals kg, return make_float4(motion_pre.x, motion_pre.y, motion_post.x, motion_post.y); } +/* Motion vector for denoising backward motion pass */ + +ccl_device_forceinline float3 +primitive_motion_vector_backward_depth_delta(KernelGlobals kg, const ccl_private ShaderData *sd) +{ + Transform tfm; + float3 motion_center, motion_pre, motion_post; + primitive_motion_data_without_camera(kg, sd, &motion_center, &motion_pre, &motion_post); + + /* Get camera-space vectors for linear depth delta. */ + tfm = kernel_data.cam.worldtocamera; + float3 motion_center_cam = transform_point(&tfm, motion_center); + tfm = kernel_data.cam.motion_pass_pre; + float3 motion_pre_cam = transform_point(&tfm, motion_pre); + + primitive_motion_data_camera_step(kg, &motion_center, &motion_pre, &motion_post); + + motion_pre = motion_pre - motion_center; + float linear_depth_delta_pre = motion_pre_cam.z - motion_center_cam.z; + + return make_float3(motion_pre.x, motion_pre.y, linear_depth_delta_pre); +} + CCL_NAMESPACE_END diff --git a/intern/cycles/kernel/types.h b/intern/cycles/kernel/types.h index 3853d7b39e5..e53b6b3e5db 100644 --- a/intern/cycles/kernel/types.h +++ b/intern/cycles/kernel/types.h @@ -339,11 +339,6 @@ enum PassType { PASS_TRANSMISSION_COLOR, /* No Scatter color since it's tricky to define what it would even mean. */ PASS_MIST, - PASS_DENOISING_ALBEDO, - PASS_DENOISING_SPECULAR_ALBEDO, - PASS_DENOISING_NORMAL, - PASS_DENOISING_ROUGHNESS, - PASS_DENOISING_DEPTH, PASS_RENDER_TIME, /* PASS_SHADOW_CATCHER accumulates contribution of shadow catcher object which is not affected by @@ -374,10 +369,19 @@ enum PassType { PASS_VOLUME_MAJORANT_SAMPLE_COUNT, PASS_CATEGORY_DATA_END = 63, + /* Denoising passes */ + PASS_DENOISING_ALBEDO, + PASS_DENOISING_SPECULAR_ALBEDO, + PASS_DENOISING_NORMAL, + PASS_DENOISING_ROUGHNESS, + PASS_DENOISING_DEPTH, + PASS_DENOISING_BACKWARD_MOTION, + PASS_CATEGORY_DENOISING_END = 95, + PASS_BAKE_PRIMITIVE, PASS_BAKE_SEED, PASS_BAKE_DIFFERENTIAL, - PASS_CATEGORY_BAKE_END = 95, + PASS_CATEGORY_BAKE_END = 127, PASS_DENOISING_PREVIOUS, diff --git a/intern/cycles/scene/camera.cpp b/intern/cycles/scene/camera.cpp index c05cd4b4fae..1587413956c 100644 --- a/intern/cycles/scene/camera.cpp +++ b/intern/cycles/scene/camera.cpp @@ -362,17 +362,15 @@ void Camera::update(Scene *scene) } if (need_motion == Scene::MOTION_PASS) { - if (camera_type == CAMERA_PANORAMA || camera_type == CAMERA_CUSTOM) { - if (have_motion) { - kcam->motion_pass_pre = transform_inverse(motion[0]); - kcam->motion_pass_post = transform_inverse(motion[motion.size() - 1]); - } - else { - kcam->motion_pass_pre = kcam->worldtocamera; - kcam->motion_pass_post = kcam->worldtocamera; - } + if (have_motion) { + kcam->motion_pass_pre = transform_inverse(motion[0]); + kcam->motion_pass_post = transform_inverse(motion[motion.size() - 1]); } else { + kcam->motion_pass_pre = kcam->worldtocamera; + kcam->motion_pass_post = kcam->worldtocamera; + } + if (camera_type != CAMERA_PANORAMA && camera_type != CAMERA_CUSTOM) { if (have_motion || fov != fov_pre || fov != fov_post) { /* Note the values for perspective_pre/perspective_post calculated for MOTION_PASS are * different to those calculated for MOTION_BLUR below, so the code has not been combined. diff --git a/intern/cycles/scene/film.cpp b/intern/cycles/scene/film.cpp index 9ffcc0bbda6..1126ca72f87 100644 --- a/intern/cycles/scene/film.cpp +++ b/intern/cycles/scene/film.cpp @@ -156,6 +156,7 @@ void Film::device_update(Device *device, DeviceScene *dscene, Scene *scene) kfilm->exposure = exposure; kfilm->pass_alpha_threshold = pass_alpha_threshold; kfilm->pass_flag = 0; + kfilm->denoising_pass_flag = 0; kfilm->use_approximate_shadow_catcher = get_use_approximate_shadow_catcher(); @@ -200,6 +201,7 @@ void Film::device_update(Device *device, DeviceScene *dscene, Scene *scene) kfilm->pass_denoising_normal = PASS_UNUSED; kfilm->pass_denoising_roughness = PASS_UNUSED; kfilm->pass_denoising_depth = PASS_UNUSED; + kfilm->pass_denoising_backward_motion = PASS_UNUSED; kfilm->pass_sample_count = PASS_UNUSED; kfilm->pass_render_time = PASS_UNUSED; kfilm->pass_adaptive_aux_buffer = PASS_UNUSED; @@ -251,6 +253,9 @@ void Film::device_update(Device *device, DeviceScene *dscene, Scene *scene) else if (pass->get_type() <= PASS_CATEGORY_DATA_END) { kfilm->pass_flag |= pass_flag; } + else if (pass->get_type() <= PASS_CATEGORY_DENOISING_END) { + kfilm->denoising_pass_flag |= pass_flag; + } else { assert(pass->get_type() <= PASS_CATEGORY_BAKE_END); } @@ -387,6 +392,9 @@ void Film::device_update(Device *device, DeviceScene *dscene, Scene *scene) case PASS_DENOISING_DEPTH: kfilm->pass_denoising_depth = kfilm->pass_stride; break; + case PASS_DENOISING_BACKWARD_MOTION: + kfilm->pass_denoising_backward_motion = kfilm->pass_stride; + break; case PASS_SHADOW_CATCHER: kfilm->pass_shadow_catcher = kfilm->pass_stride; @@ -563,6 +571,9 @@ void Film::update_passes(Scene *scene) if (denoiser_passes & DENOISER_PASS_MOTION) { add_auto_pass(scene, PASS_MOTION); } + if (denoiser_passes & DENOISER_PASS_BACKWARD_MOTION) { + add_auto_pass(scene, PASS_DENOISING_BACKWARD_MOTION); + } } /* Create passes for shadow catcher. */ @@ -789,7 +800,7 @@ uint Film::get_kernel_features(const Scene *scene) const !is_volume_guiding_pass(pass_type); if (has_denoise_pass || - (pass_type >= PASS_DENOISING_ALBEDO && pass_type <= PASS_DENOISING_DEPTH)) + (pass_type >= PASS_DENOISING_ALBEDO && pass_type <= PASS_DENOISING_BACKWARD_MOTION)) { kernel_features |= KERNEL_FEATURE_DENOISING; } diff --git a/intern/cycles/scene/pass.cpp b/intern/cycles/scene/pass.cpp index f6c8bd64bf3..5abbbc09531 100644 --- a/intern/cycles/scene/pass.cpp +++ b/intern/cycles/scene/pass.cpp @@ -92,6 +92,7 @@ const NodeEnum *Pass::get_type_enum() pass_type_enum.insert("denoising_normal", PASS_DENOISING_NORMAL); pass_type_enum.insert("denoising_roughness", PASS_DENOISING_ROUGHNESS); pass_type_enum.insert("denoising_depth", PASS_DENOISING_DEPTH); + pass_type_enum.insert("denoising_backward_motion", PASS_DENOISING_BACKWARD_MOTION); pass_type_enum.insert("denoising_previous", PASS_DENOISING_PREVIOUS); pass_type_enum.insert("volume_majorant", PASS_VOLUME_MAJORANT); pass_type_enum.insert("volume_majorant_sample_count", PASS_VOLUME_MAJORANT_SAMPLE_COUNT); @@ -311,6 +312,7 @@ PassInfo Pass::get_info(const PassType type, case PASS_DENOISING_ALBEDO: case PASS_DENOISING_SPECULAR_ALBEDO: case PASS_DENOISING_NORMAL: + case PASS_DENOISING_BACKWARD_MOTION: pass_info.num_components = 3; break; case PASS_DENOISING_ROUGHNESS: diff --git a/intern/cycles/scene/scene.cpp b/intern/cycles/scene/scene.cpp index ac509c000e2..b2d26ee9285 100644 --- a/intern/cycles/scene/scene.cpp +++ b/intern/cycles/scene/scene.cpp @@ -412,7 +412,9 @@ Scene::MotionType Scene::need_motion() const if (integrator->get_motion_blur()) { return MOTION_BLUR; } - if (Pass::contains(passes, PASS_MOTION)) { + if (Pass::contains(passes, PASS_MOTION) || + Pass::contains(passes, PASS_DENOISING_BACKWARD_MOTION)) + { return MOTION_PASS; } return MOTION_NONE;