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
This commit is contained in:
Christoph Neuhauser 2026-05-06 10:34:23 +02:00
parent c6dddc25d3
commit 74ffed0a34
12 changed files with 125 additions and 57 deletions

View file

@ -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:

View file

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

View file

@ -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);

View file

@ -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;

View file

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

View file

@ -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

View file

@ -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, &center);
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<float4>(kg, sd, desc));
*motion_pre = make_float3(primitive_surface_attribute<float4>(kg, sd, desc));
desc.offset += numverts;
motion_post = make_float3(primitive_surface_attribute<float4>(kg, sd, desc));
*motion_post = make_float3(primitive_surface_attribute<float4>(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<float3>(kg, sd, desc);
*motion_pre = triangle_attribute<float3>(kg, sd, desc);
desc.offset += numverts;
motion_post = triangle_attribute<float3>(kg, sd, desc);
*motion_post = triangle_attribute<float3>(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

View file

@ -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,

View file

@ -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.

View file

@ -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;
}

View file

@ -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:

View file

@ -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;