Compositor: Add interpolation options to displace node

This commit introduces interpolation methods (Nearest, Bilinear,
Bicubic) for the Displace node in the compositor:

- Exposes a dropdown selector for interpolation selection.
- Introduces different methods for Anisotropic and other interpolation
  methods.

Reference: #119592.

Pull Request: https://projects.blender.org/blender/blender/pulls/139802
This commit is contained in:
Benjamin Beilharz
2025-06-12 14:53:15 +02:00
committed by Omar Emara
parent d626bf6311
commit 8b67050575
9 changed files with 285 additions and 85 deletions

View File

@@ -27,7 +27,7 @@
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 7
#define BLENDER_FILE_SUBVERSION 8
/* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to

View File

@@ -146,6 +146,26 @@ void blo_do_versions_500(FileData * /*fd*/, Library * /*lib*/, Main *bmain)
}
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 500, 8)) {
FOREACH_NODETREE_BEGIN (bmain, ntree, id) {
if (ntree->type != NTREE_COMPOSIT) {
continue;
}
LISTBASE_FOREACH (bNode *, node, &ntree->nodes) {
if (node->type_legacy != CMP_NODE_DISPLACE) {
continue;
}
if (node->storage != nullptr) {
continue;
}
NodeDisplaceData *data = MEM_callocN<NodeDisplaceData>(__func__);
data->interpolation = CMP_NODE_INTERPOLATION_ANISOTROPIC;
node->storage = data;
}
}
FOREACH_NODETREE_END;
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

View File

@@ -182,6 +182,7 @@ set(GLSL_SRC
shaders/compositor_deriche_gaussian_blur_sum.glsl
shaders/compositor_directional_blur.glsl
shaders/compositor_displace.glsl
shaders/compositor_displace_anisotropic.glsl
shaders/compositor_double_edge_mask_compute_boundary.glsl
shaders/compositor_double_edge_mask_compute_gradient.glsl
shaders/compositor_edge_filter.glsl

View File

@@ -2,13 +2,9 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_bicubic_sampler_lib.glsl"
#include "gpu_shader_compositor_texture_utilities.glsl"
/* A shared table that stores the displaced coordinates of all pixels in the work group. This is
* necessary to avoid recomputing displaced coordinates when computing the gradients necessary for
* anisotropic filtering, see the implementation for more information. */
shared float2 displaced_coordinates_table[gl_WorkGroupSize.x][gl_WorkGroupSize.y];
void main()
{
int2 texel = int2(gl_GlobalInvocationID.xy);
@@ -24,39 +20,5 @@ void main()
float2 displacement = texture_load(displacement_tx, texel).xy * scale / float2(input_size);
float2 displaced_coordinates = coordinates - displacement;
/* Store the displaced coordinates into the shared table and issue a barrier to later compute the
* gradients from the table. */
int2 table_index = int2(gl_LocalInvocationID.xy);
displaced_coordinates_table[table_index.x][table_index.y] = displaced_coordinates;
barrier();
/* Compute the partial derivative of the displaced coordinates along the x direction using a
* finite difference approximation. Odd invocations use a forward finite difference equation
* while even invocations use a backward finite difference equation. This is done such that
* invocations at the edges of the work group wouldn't need access to pixels that are outside of
* the work group.
*
* The x_step value is 1 for even invocations and when added to the x table index and multiplied
* by the result yields a standard forward finite difference equation. The x_step value is -1 for
* odd invocations and when added to the x table index and multiplied by the result yields a
* standard backward finite difference equation, because multiplication by -1 flips the order of
* subtraction.
*
* Divide by the input size since textureGrad assumes derivatives with respect to texel
* coordinates. */
int x_step = (table_index.x % 2) * -2 + 1;
float2 x_neighbor = displaced_coordinates_table[table_index.x + x_step][table_index.y];
float2 x_gradient = (x_neighbor - displaced_coordinates) * x_step / input_size.x;
/* Compute the partial derivative of the displaced coordinates along the y direction using a
* finite difference approximation. See the previous code section for more information. */
int y_step = (table_index.y % 2) * -2 + 1;
float2 y_neighbor = displaced_coordinates_table[table_index.x][table_index.y + y_step];
float2 y_gradient = (y_neighbor - displaced_coordinates) * y_step / input_size.y;
/* Sample the input using the displaced coordinates passing in the computed gradients in order to
* utilize the anisotropic filtering capabilities of the sampler. */
float4 displaced_color = textureGrad(input_tx, displaced_coordinates, x_gradient, y_gradient);
imageStore(output_img, texel, displaced_color);
imageStore(output_img, texel, SAMPLER_FUNCTION(input_tx, displaced_coordinates));
}

View File

@@ -0,0 +1,62 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "gpu_shader_compositor_texture_utilities.glsl"
/* A shared table that stores the displaced coordinates of all pixels in the work group. This is
* necessary to avoid recomputing displaced coordinates when computing the gradients necessary for
* anisotropic filtering, see the implementation for more information. */
shared float2 displaced_coordinates_table[gl_WorkGroupSize.x][gl_WorkGroupSize.y];
void main()
{
int2 texel = int2(gl_GlobalInvocationID.xy);
int2 input_size = texture_size(input_tx);
/* Add 0.5 to evaluate the input sampler at the center of the pixel and divide by the image size
* to get the coordinates into the sampler's expected [0, 1] range. */
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(input_size);
/* Note that the input displacement is in pixel space, so divide by the input size to transform
* it into the normalized sampler space. */
float2 scale = float2(texture_load(x_scale_tx, texel).x, texture_load(y_scale_tx, texel).x);
float2 displacement = texture_load(displacement_tx, texel).xy * scale / float2(input_size);
float2 displaced_coordinates = coordinates - displacement;
/* Store the displaced coordinates into the shared table and issue a barrier to later compute the
* gradients from the table. */
int2 table_index = int2(gl_LocalInvocationID.xy);
displaced_coordinates_table[table_index.x][table_index.y] = displaced_coordinates;
barrier();
/* Compute the partial derivative of the displaced coordinates along the x direction using a
* finite difference approximation. Odd invocations use a forward finite difference equation
* while even invocations use a backward finite difference equation. This is done such that
* invocations at the edges of the work group wouldn't need access to pixels that are outside of
* the work group.
*
* The x_step value is 1 for even invocations and when added to the x table index and multiplied
* by the result yields a standard forward finite difference equation. The x_step value is -1 for
* odd invocations and when added to the x table index and multiplied by the result yields a
* standard backward finite difference equation, because multiplication by -1 flips the order of
* subtraction.
*
* Divide by the input size since textureGrad assumes derivatives with respect to texel
* coordinates. */
int x_step = (table_index.x % 2) * -2 + 1;
float2 x_neighbor = displaced_coordinates_table[table_index.x + x_step][table_index.y];
float2 x_gradient = (x_neighbor - displaced_coordinates) * x_step / input_size.x;
/* Compute the partial derivative of the displaced coordinates along the y direction using a
* finite difference approximation. See the previous code section for more information. */
int y_step = (table_index.y % 2) * -2 + 1;
float2 y_neighbor = displaced_coordinates_table[table_index.x][table_index.y + y_step];
float2 y_gradient = (y_neighbor - displaced_coordinates) * y_step / input_size.y;
/* Sample the input using the displaced coordinates passing in the computed gradients in order to
* utilize the anisotropic filtering capabilities of the sampler. */
float4 displaced_color = textureGrad(input_tx, displaced_coordinates, x_gradient, y_gradient);
imageStore(output_img, texel, displaced_color);
}

View File

@@ -4,13 +4,31 @@
#include "gpu_shader_create_info.hh"
GPU_SHADER_CREATE_INFO(compositor_displace)
GPU_SHADER_CREATE_INFO(compositor_displace_shared)
LOCAL_GROUP_SIZE(16, 16)
SAMPLER(0, sampler2D, input_tx)
SAMPLER(1, sampler2D, displacement_tx)
SAMPLER(2, sampler2D, x_scale_tx)
SAMPLER(3, sampler2D, y_scale_tx)
IMAGE(0, GPU_RGBA16F, write, image2D, output_img)
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_displace)
ADDITIONAL_INFO(compositor_displace_shared)
COMPUTE_SOURCE("compositor_displace.glsl")
DEFINE_VALUE("SAMPLER_FUNCTION", "texture")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_displace_bicubic)
ADDITIONAL_INFO(compositor_displace_shared)
COMPUTE_SOURCE("compositor_displace.glsl")
DEFINE_VALUE("SAMPLER_FUNCTION", "texture_bicubic")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_displace_anisotropic)
ADDITIONAL_INFO(compositor_displace_shared)
COMPUTE_SOURCE("compositor_displace_anisotropic.glsl")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()

View File

@@ -1577,6 +1577,10 @@ typedef struct NodeScaleData {
short interpolation;
} NodeScaleData;
typedef struct NodeDisplaceData {
short interpolation;
} NodeDisplaceData;
typedef struct NodePlaneTrackDeformData {
char tracking_object[64];
char plane_track_name[64];

View File

@@ -8066,6 +8066,19 @@ static void def_cmp_dilate_erode(BlenderRNA * /*brna*/, StructRNA *srna)
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_cmp_displace(BlenderRNA * /*brna*/, StructRNA *srna)
{
PropertyRNA *prop;
RNA_def_struct_sdna_from(srna, "NodeDisplaceData", "storage");
prop = RNA_def_property(srna, "interpolation", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "interpolation");
RNA_def_property_enum_items(prop, cmp_interpolation_items);
RNA_def_property_ui_text(prop, "Interpolation", "Interpolation method");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
}
static void def_cmp_inpaint(BlenderRNA * /*brna*/, StructRNA *srna)
{
PropertyRNA *prop;
@@ -13995,7 +14008,7 @@ static void rna_def_nodes(BlenderRNA *brna)
define("CompositorNode", "CompositorNodeDespeckle", def_cmp_despeckle);
define("CompositorNode", "CompositorNodeDiffMatte", def_cmp_diff_matte);
define("CompositorNode", "CompositorNodeDilateErode", def_cmp_dilate_erode);
define("CompositorNode", "CompositorNodeDisplace");
define("CompositorNode", "CompositorNodeDisplace", def_cmp_displace);
define("CompositorNode", "CompositorNodeDistanceMatte", def_cmp_distance_matte);
define("CompositorNode", "CompositorNodeDoubleEdgeMask", def_cmp_double_edge_mask);
define("CompositorNode", "CompositorNodeEllipseMask", def_cmp_ellipsemask);

View File

@@ -6,21 +6,34 @@
* \ingroup cmpnodes
*/
#include "MEM_guardedalloc.h"
#include "BKE_node.hh"
#include "BLI_assert.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
#include "RNA_types.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "node_composite_util.hh"
/* **************** Displace ******************** */
namespace blender::nodes::node_composite_displace_cc {
NODE_STORAGE_FUNCS(NodeDisplaceData)
static void cmp_node_displace_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Color>("Image")
@@ -46,6 +59,18 @@ static void cmp_node_displace_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Color>("Image");
}
static void cmp_node_init_displace(bNodeTree * /*ntree*/, bNode *node)
{
NodeDisplaceData *data = MEM_callocN<NodeDisplaceData>(__func__);
data->interpolation = CMP_NODE_INTERPOLATION_ANISOTROPIC;
node->storage = data;
}
static void cmp_buts_displace(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
layout->prop(ptr, "interpolation", UI_ITEM_R_SPLIT_EMPTY_NAME, "", ICON_NONE);
}
using namespace blender::compositor;
class DisplaceOperation : public NodeOperation {
@@ -71,12 +96,20 @@ class DisplaceOperation : public NodeOperation {
void execute_gpu()
{
GPUShader *shader = context().get_shader("compositor_displace");
GPUShader *shader = context().get_shader(this->get_shader_name());
GPU_shader_bind(shader);
const Result &input_image = get_input("Image");
GPU_texture_mipmap_mode(input_image, true, true);
GPU_texture_anisotropic_filter(input_image, true);
const Interpolation interpolation = this->get_interpolation();
if (interpolation == Interpolation::Anisotropic) {
GPU_texture_anisotropic_filter(input_image, true);
GPU_texture_mipmap_mode(input_image, true, true);
}
else {
const bool use_bilinear = ELEM(
interpolation, Interpolation::Bilinear, Interpolation::Bicubic);
GPU_texture_filter_mode(input_image, use_bilinear);
}
GPU_texture_extend_mode(input_image, GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER);
input_image.bind_as_texture(shader, "input_tx");
@@ -109,18 +142,76 @@ class DisplaceOperation : public NodeOperation {
const Result &x_scale = get_input("X Scale");
const Result &y_scale = get_input("Y Scale");
const Interpolation interpolation = this->get_interpolation();
const Domain domain = compute_domain();
Result &output = get_result("Image");
output.allocate_texture(domain);
/* In order to perform EWA sampling, we need to compute the partial derivative of the displaced
* coordinates along the x and y directions using a finite difference approximation. But in
* order to avoid loading multiple neighboring displacement values for each pixel, we operate
* on the image in 2x2 blocks of pixels, where the derivatives are computed horizontally and
* vertically across the 2x2 block such that odd texels use a forward finite difference
* equation while even invocations use a backward finite difference equation. */
const int2 size = domain.size;
if (interpolation == Interpolation::Anisotropic) {
this->compute_anisotropic(size, image, output, input_displacement, x_scale, y_scale);
}
else {
this->compute_interpolation(
interpolation, size, image, output, input_displacement, x_scale, y_scale);
}
}
void compute_interpolation(const Interpolation &interpolation,
const int2 &size,
const Result &image,
Result &output,
const Result &input_displacement,
const Result &x_scale,
const Result &y_scale) const
{
parallel_for(size, [&](const int2 base_texel) {
const float2 coordinates = compute_coordinates(
base_texel, size, input_displacement, x_scale, y_scale);
switch (interpolation) {
/* The anisotropic case requires gradient computation and is handled separately. */
case Interpolation::Anisotropic:
BLI_assert_unreachable();
break;
case Interpolation::Nearest:
output.store_pixel(base_texel, image.sample_nearest_zero(coordinates));
break;
case Interpolation::Bilinear:
output.store_pixel(base_texel, image.sample_bilinear_zero(coordinates));
break;
case Interpolation::Bicubic:
output.store_pixel(base_texel, image.sample_cubic_wrap(coordinates, false, false));
break;
}
});
}
/* In order to perform EWA sampling, we need to compute the partial derivative of the
* displaced coordinates along the x and y directions using a finite difference
* approximation. But in order to avoid loading multiple neighboring displacement values for
* each pixel, we operate on the image in 2x2 blocks of pixels, where the derivatives are
* computed horizontally and vertically across the 2x2 block such that odd texels use a
* forward finite difference equation while even invocations use a backward finite difference
* equation. */
void compute_anisotropic(const int2 &size,
const Result &image,
Result &output,
const Result &input_displacement,
const Result &x_scale,
const Result &y_scale) const
{
auto compute_anisotropic_pixel = [&](const int2 &texel,
const float2 &coordinates,
const float2 &x_gradient,
const float2 &y_gradient) {
/* Sample the input using the displaced coordinates passing in the computed gradients in
* order to utilize the anisotropic filtering capabilities of the sampler. */
output.store_pixel(texel, image.sample_ewa_zero(coordinates, x_gradient, y_gradient));
};
parallel_for(math::divide_ceil(size, int2(2)), [&](const int2 base_texel) {
/* Compute each of the pixels in the 2x2 block, making sure to exempt out of bounds right
* and upper pixels. */
const int x = base_texel.x * 2;
const int y = base_texel.y * 2;
@@ -129,24 +220,14 @@ class DisplaceOperation : public NodeOperation {
const int2 upper_left_texel = int2(x, y + 1);
const int2 upper_right_texel = int2(x + 1, y + 1);
auto compute_coordinates = [&](const int2 &texel) -> float2 {
/* Add 0.5 to evaluate the sampler at the center of the pixel and divide by the image size
* to get the coordinates into the sampler's expected [0, 1] range. */
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
/* Note that the input displacement is in pixel space, so divide by the input size to
* transform it into the normalized sampler space. */
float2 scale = float2(x_scale.load_pixel_extended<float, true>(texel),
y_scale.load_pixel_extended<float, true>(texel));
float2 displacement = input_displacement.load_pixel_extended<float3, true>(texel).xy() *
scale / float2(size);
return coordinates - displacement;
};
const float2 lower_left_coordinates = compute_coordinates(lower_left_texel);
const float2 lower_right_coordinates = compute_coordinates(lower_right_texel);
const float2 upper_left_coordinates = compute_coordinates(upper_left_texel);
const float2 upper_right_coordinates = compute_coordinates(upper_right_texel);
const float2 lower_left_coordinates = compute_coordinates(
lower_left_texel, size, input_displacement, x_scale, y_scale);
const float2 lower_right_coordinates = compute_coordinates(
lower_right_texel, size, input_displacement, x_scale, y_scale);
const float2 upper_left_coordinates = compute_coordinates(
upper_left_texel, size, input_displacement, x_scale, y_scale);
const float2 upper_right_coordinates = compute_coordinates(
upper_right_texel, size, input_displacement, x_scale, y_scale);
/* Compute the partial derivatives using finite difference. Divide by the input size since
* sample_ewa_zero assumes derivatives with respect to texel coordinates. */
@@ -156,33 +237,68 @@ class DisplaceOperation : public NodeOperation {
const float2 upper_x_gradient = (upper_right_coordinates - upper_left_coordinates) / size.x;
/* Computes one of the 2x2 pixels given its texel location, coordinates, and gradients. */
auto compute_pixel = [&](const int2 &texel,
const float2 &coordinates,
const float2 &x_gradient,
const float2 &y_gradient) {
/* Sample the input using the displaced coordinates passing in the computed gradients in
* order to utilize the anisotropic filtering capabilities of the sampler. */
float4 displaced_color = image.sample_ewa_zero(coordinates, x_gradient, y_gradient);
output.store_pixel(texel, displaced_color);
};
/* Compute each of the pixels in the 2x2 block, making sure to exempt out of bounds right
* and upper pixels. */
compute_pixel(lower_left_texel, lower_left_coordinates, lower_x_gradient, left_y_gradient);
compute_anisotropic_pixel(
lower_left_texel, lower_left_coordinates, lower_x_gradient, left_y_gradient);
if (lower_right_texel.x != size.x) {
compute_pixel(
compute_anisotropic_pixel(
lower_right_texel, lower_right_coordinates, lower_x_gradient, right_y_gradient);
}
if (upper_left_texel.y != size.y) {
compute_pixel(upper_left_texel, upper_left_coordinates, upper_x_gradient, left_y_gradient);
compute_anisotropic_pixel(
upper_left_texel, upper_left_coordinates, upper_x_gradient, left_y_gradient);
}
if (upper_right_texel.x != size.x && upper_right_texel.y != size.y) {
compute_pixel(
compute_anisotropic_pixel(
upper_right_texel, upper_right_coordinates, upper_x_gradient, right_y_gradient);
}
});
}
float2 compute_coordinates(const int2 &texel,
const int2 &size,
const Result &input_displacement,
const Result &x_scale,
const Result &y_scale) const
{
/* Add 0.5 to evaluate the sampler at the center of the pixel and divide by the image
* size to get the coordinates into the sampler's expected [0, 1] range. */
float2 coordinates = (float2(texel) + float2(0.5f)) / float2(size);
/* Note that the input displacement is in pixel space, so divide by the input size to
* transform it into the normalized sampler space. */
float2 scale = float2(x_scale.load_pixel_extended<float, true>(texel),
y_scale.load_pixel_extended<float, true>(texel));
float2 displacement = input_displacement.load_pixel_extended<float3, true>(texel).xy() *
scale / float2(size);
return coordinates - displacement;
}
const char *get_shader_name() const
{
if (this->get_interpolation() == Interpolation::Anisotropic) {
return "compositor_displace_anisotropic";
}
return "compositor_displace";
}
Interpolation get_interpolation() const
{
switch (node_storage(bnode()).interpolation) {
case CMP_NODE_INTERPOLATION_ANISOTROPIC:
return Interpolation::Anisotropic;
case CMP_NODE_INTERPOLATION_NEAREST:
return Interpolation::Nearest;
case CMP_NODE_INTERPOLATION_BILINEAR:
return Interpolation::Bilinear;
case CMP_NODE_INTERPOLATION_BICUBIC:
return Interpolation::Bicubic;
}
BLI_assert_unreachable();
return Interpolation::Nearest;
}
bool is_identity()
{
const Result &input_image = get_input("Image");
@@ -228,6 +344,10 @@ static void register_node_type_cmp_displace()
ntype.enum_name_legacy = "DISPLACE";
ntype.nclass = NODE_CLASS_DISTORT;
ntype.declare = file_ns::cmp_node_displace_declare;
ntype.draw_buttons = file_ns::cmp_buts_displace;
ntype.initfunc = file_ns::cmp_node_init_displace;
blender::bke::node_type_storage(
ntype, "NodeDisplaceData", node_free_standard_storage, node_copy_standard_storage);
ntype.get_compositor_operation = file_ns::get_compositor_operation;
blender::bke::node_register_type(ntype);