diff options
Diffstat (limited to 'source/blender/imbuf')
-rw-r--r-- | source/blender/imbuf/CMakeLists.txt | 14 | ||||
-rw-r--r-- | source/blender/imbuf/IMB_rasterizer.hh | 710 | ||||
-rw-r--r-- | source/blender/imbuf/intern/rasterizer_blending.hh | 32 | ||||
-rw-r--r-- | source/blender/imbuf/intern/rasterizer_clamping.hh | 82 | ||||
-rw-r--r-- | source/blender/imbuf/intern/rasterizer_stats.hh | 89 | ||||
-rw-r--r-- | source/blender/imbuf/intern/rasterizer_target.hh | 74 | ||||
-rw-r--r-- | source/blender/imbuf/intern/rasterizer_test.cc | 429 |
7 files changed, 1430 insertions, 0 deletions
diff --git a/source/blender/imbuf/CMakeLists.txt b/source/blender/imbuf/CMakeLists.txt index c97eead0159..ef996eb2288 100644 --- a/source/blender/imbuf/CMakeLists.txt +++ b/source/blender/imbuf/CMakeLists.txt @@ -190,3 +190,17 @@ set_source_files_properties( ) blender_add_lib(bf_imbuf "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") + + +if(WITH_GTESTS) + set(TEST_SRC + intern/rasterizer_test.cc + ) + set(TEST_INC + ) + set(TEST_LIB + ) + include(GTestTesting) + blender_add_test_lib(bf_imbuf_tests "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}") +endif() + diff --git a/source/blender/imbuf/IMB_rasterizer.hh b/source/blender/imbuf/IMB_rasterizer.hh new file mode 100644 index 00000000000..09e0e54e52e --- /dev/null +++ b/source/blender/imbuf/IMB_rasterizer.hh @@ -0,0 +1,710 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * Copyright 2022 Blender Foundation. All rights reserved. */ + +/** \file + * \ingroup imbuf + * + * Rasterizer to render triangles onto an image buffer. + * + * The implementation is template based and follows a (very limited) OpenGL pipeline. + * + * ## Basic usage + * + * In order to use it you have to define the data structure for a single vertex. + * + * \code{.cc} + * struct VertexInput { + * float2 uv; + * }; + * \endcode + * + * A vertex shader is required to transfer the vertices to actual coordinates in the image buffer. + * The vertex shader will store vertex specific data in a VertexOutInterface. + * + * \code{.cc} + * class MyVertexShader : public AbstractVertexShader<VertexInput, float> { + * public: + * float4x4 mat; + * void vertex(const VertexInputType &input, VertexOutputType *r_output) override + * { + * float2 coord = float2(mat * float3(input.uv[0], input.uv[1], 0.0)); + * r_output->coord = coord * image_size; + * r_output->data = 1.0; + * } + * }; + * \endcode + * + * A fragment shader is required to actually generate the pixel that will be stored in the buffer. + * + * \code{.cc} + * class FragmentShader : public AbstractFragmentShader<float, float4> { + * public: + * void fragment(const FragmentInputType &input, FragmentOutputType *r_output) override + * { + * *r_output = float4(input, input, input, 1.0); + * } + * }; + * \endcode + * + * Create a rasterizer with the vertex and fragment shader and start drawing. + * It is required to call flush to make sure that all triangles are drawn to the image buffer. + * + * \code{.cc} + * Rasterizer<MyVertexShader, MyFragmentShader> rasterizer(&image_buffer); + * rasterizer.activate_drawing_target(&image_buffer); + * rasterizer.get_vertex_shader().mat = float4x4::identity(); + * rasterizer.draw_triangle( + * VertexInput{float2(0.0, 1.0)}, + * VertexInput{float2(1.0, 1.0)}, + * VertexInput{float2(1.0, 0.0)}, + * ); + * rasterizer.flush(); + * \endcode + */ + +#pragma once + +#include "BLI_math.h" +#include "BLI_math_vec_types.hh" +#include "BLI_vector.hh" + +#include "IMB_imbuf.h" +#include "IMB_imbuf_types.h" + +#include "intern/rasterizer_blending.hh" +#include "intern/rasterizer_clamping.hh" +#include "intern/rasterizer_stats.hh" +#include "intern/rasterizer_target.hh" + +#include <optional> + +// #define DEBUG_PRINT + +namespace blender::imbuf::rasterizer { + +/** The default number of rasterlines to buffer before flushing to the image buffer. */ +constexpr int64_t DefaultRasterlinesBufferSize = 4096; + +/** + * Interface data of the vertex stage. + */ +template< + /** + * Data type per vertex generated by the vertex shader and transferred to the fragment shader. + * + * The data type should implement the +=, +, =, -, / and * operator. + */ + typename Inner> +class VertexOutInterface { + public: + using InnerType = Inner; + using Self = VertexOutInterface<InnerType>; + /** Coordinate of a vertex inside the image buffer. (0..image_buffer.x, 0..image_buffer.y). */ + float2 coord; + InnerType data; + + Self &operator+=(const Self &other) + { + coord += other.coord; + data += other.data; + return *this; + } + + Self &operator=(const Self &other) + { + coord = other.coord; + data = other.data; + return *this; + } + + Self operator-(const Self &other) const + { + Self result; + result.coord = coord - other.coord; + result.data = data - other.data; + return result; + } + + Self operator+(const Self &other) const + { + Self result; + result.coord = coord + other.coord; + result.data = data + other.data; + return result; + } + + Self operator/(const float divider) const + { + Self result; + result.coord = coord / divider; + result.data = data / divider; + return result; + } + + Self operator*(const float multiplier) const + { + Self result; + result.coord = coord * multiplier; + result.data = data * multiplier; + return result; + } +}; + +/** + * Vertex shader + */ +template<typename VertexInput, typename VertexOutput> class AbstractVertexShader { + public: + using VertexInputType = VertexInput; + using VertexOutputType = VertexOutInterface<VertexOutput>; + + virtual void vertex(const VertexInputType &input, VertexOutputType *r_output) = 0; +}; + +/** + * Fragment shader will render a single fragment onto the ImageBuffer. + * FragmentInput - The input data from the vertex stage. + * FragmentOutput points to the memory location to write to in the image buffer. + */ +template<typename FragmentInput, typename FragmentOutput> class AbstractFragmentShader { + public: + using FragmentInputType = FragmentInput; + using FragmentOutputType = FragmentOutput; + + virtual void fragment(const FragmentInputType &input, FragmentOutputType *r_output) = 0; +}; + +/** + * RasterLine - data to render a single rasterline of a triangle. + */ +template<typename FragmentInput> class Rasterline { + public: + /** Row where this rasterline will be rendered. */ + uint32_t y; + /** Starting X coordinate of the rasterline. */ + uint32_t start_x; + /** Ending X coordinate of the rasterline. */ + uint32_t end_x; + /** Input data for the fragment shader on (start_x, y). */ + FragmentInput start_data; + /** Delta to add to the start_input to create the data for the next fragment. */ + FragmentInput fragment_add; + + Rasterline(uint32_t y, + uint32_t start_x, + uint32_t end_x, + FragmentInput start_data, + FragmentInput fragment_add) + : y(y), start_x(start_x), end_x(end_x), start_data(start_data), fragment_add(fragment_add) + { + } +}; + +template<typename Rasterline, int64_t BufferSize> class Rasterlines { + public: + Vector<Rasterline, BufferSize> buffer; + + explicit Rasterlines() + { + buffer.reserve(BufferSize); + } + + virtual ~Rasterlines() = default; + + void append(const Rasterline &value) + { + buffer.append(value); + BLI_assert(buffer.capacity() == BufferSize); + } + + bool is_empty() const + { + return buffer.is_empty(); + } + + bool has_items() const + { + return buffer.has_items(); + } + + bool is_full() const + { + return buffer.size() == BufferSize; + } + + void clear() + { + buffer.clear(); + BLI_assert(buffer.size() == 0); + BLI_assert(buffer.capacity() == BufferSize); + } +}; + +template<typename VertexShader, + typename FragmentShader, + /** + * A blend mode integrates the result of the fragment shader with the drawing target. + */ + typename BlendMode = CopyBlendMode, + typename DrawingTarget = ImageBufferDrawingTarget, + + /** + * To improve branching performance the rasterlines are buffered and flushed when this + * treshold is reached. + */ + int64_t RasterlinesSize = DefaultRasterlinesBufferSize, + + /** + * Statistic collector. Should be a subclass of AbstractStats or implement the same + * interface. + * + * Is used in test cases to check what decision was made. + */ + typename Statistics = NullStats> +class Rasterizer { + public: + using InterfaceInnerType = typename VertexShader::VertexOutputType::InnerType; + using RasterlineType = Rasterline<InterfaceInnerType>; + using VertexInputType = typename VertexShader::VertexInputType; + using VertexOutputType = typename VertexShader::VertexOutputType; + using FragmentInputType = typename FragmentShader::FragmentInputType; + using FragmentOutputType = typename FragmentShader::FragmentOutputType; + using TargetBufferType = typename DrawingTarget::InnerType; + + /** Check if the vertex shader and the fragment shader can be linked together. */ + static_assert(std::is_same_v<InterfaceInnerType, FragmentInputType>); + /** Check if the output of the fragment shader can be used as source of the Blend Mode. */ + static_assert(std::is_same_v<FragmentOutputType, typename BlendMode::SourceType>); + + private: + VertexShader vertex_shader_; + FragmentShader fragment_shader_; + Rasterlines<RasterlineType, RasterlinesSize> rasterlines_; + const CenterPixelClampingMethod clamping_method; + const BlendMode blending_mode_; + DrawingTarget drawing_target_; + + public: + Statistics stats; + + explicit Rasterizer() + { + } + + /** Activate the giver image buffer to be used as the active drawing target. */ + void activate_drawing_target(TargetBufferType *new_drawing_target) + { + deactivate_drawing_target(); + drawing_target_.activate(new_drawing_target); + } + + /** + * Deactivate active drawing target. + * + * Will flush any rasterlines before deactivating. + */ + void deactivate_drawing_target() + { + if (has_active_drawing_target()) { + flush(); + } + drawing_target_.deactivate(); + BLI_assert(!has_active_drawing_target()); + } + + bool has_active_drawing_target() const + { + return drawing_target_.has_active_target(); + } + + virtual ~Rasterizer() = default; + + VertexShader &vertex_shader() + { + return vertex_shader_; + } + VertexShader &fragment_shader() + { + return fragment_shader_; + } + + void draw_triangle(const VertexInputType &p1, + const VertexInputType &p2, + const VertexInputType &p3) + { + BLI_assert_msg(has_active_drawing_target(), + "Drawing requires an active drawing target. Use `activate_drawing_target` to " + "activate a drawing target."); + stats.increase_triangles(); + + std::array<VertexOutputType, 3> vertex_out; + + vertex_shader_.vertex(p1, &vertex_out[0]); + vertex_shader_.vertex(p2, &vertex_out[1]); + vertex_shader_.vertex(p3, &vertex_out[2]); + + /* Early check if all coordinates are on a single of the buffer it is imposible to render into + * the buffer*/ + const VertexOutputType &p1_out = vertex_out[0]; + const VertexOutputType &p2_out = vertex_out[1]; + const VertexOutputType &p3_out = vertex_out[2]; + const bool triangle_not_visible = (p1_out.coord[0] < 0.0 && p2_out.coord[0] < 0.0 && + p3_out.coord[0] < 0.0) || + (p1_out.coord[1] < 0.0 && p2_out.coord[1] < 0.0 && + p3_out.coord[1] < 0.0) || + (p1_out.coord[0] >= drawing_target_.get_width() && + p2_out.coord[0] >= drawing_target_.get_width() && + p3_out.coord[0] >= drawing_target_.get_width()) || + (p1_out.coord[1] >= drawing_target_.get_height() && + p2_out.coord[1] >= drawing_target_.get_height() && + p3_out.coord[1] >= drawing_target_.get_height()); + if (triangle_not_visible) { + stats.increase_discarded_triangles(); + return; + } + + rasterize_triangle(vertex_out); + } + + /** + * Flush any not drawn rasterlines onto the active drawing target. + */ + void flush() + { + if (rasterlines_.is_empty()) { + return; + } + + stats.increase_flushes(); + for (const RasterlineType &rasterline : rasterlines_.buffer) { + render_rasterline(rasterline); + } + rasterlines_.clear(); + } + + private: + void rasterize_triangle(std::array<VertexOutputType, 3> &vertex_out) + { +#ifdef DEBUG_PRINT + printf("%s 1: (%.4f,%.4f) 2: (%.4f,%.4f) 3: (%.4f %.4f)\n", + __func__, + vertex_out[0].coord[0], + vertex_out[0].coord[1], + vertex_out[1].coord[0], + vertex_out[1].coord[1], + vertex_out[2].coord[0], + vertex_out[2].coord[1]); +#endif + std::array<VertexOutputType *, 3> sorted_vertices = order_triangle_vertices(vertex_out); + + const int min_rasterline_y = clamping_method.scanline_for(sorted_vertices[0]->coord[1]); + const int mid_rasterline_y = clamping_method.scanline_for(sorted_vertices[1]->coord[1]); + const int max_rasterline_y = clamping_method.scanline_for(sorted_vertices[2]->coord[1]) - 1; + + /* left and right branch. */ + VertexOutputType left = *sorted_vertices[0]; + VertexOutputType right = *sorted_vertices[0]; + + VertexOutputType *left_target; + VertexOutputType *right_target; + if (sorted_vertices[1]->coord[0] < sorted_vertices[2]->coord[0]) { + left_target = sorted_vertices[1]; + right_target = sorted_vertices[2]; + } + else { + left_target = sorted_vertices[2]; + right_target = sorted_vertices[1]; + } + + VertexOutputType left_add = calc_branch_delta(left, *left_target); + VertexOutputType right_add = calc_branch_delta(right, *right_target); + + /* Change winding order to match the steepness of the edges. */ + if (right_add.coord[0] < left_add.coord[0]) { + std::swap(left_add, right_add); + std::swap(left_target, right_target); + } + + /* Calculate the adder for each x pixel. This is constant for the whole triangle. It is + * calculated at the midline to reduce edge cases. */ + const InterfaceInnerType fragment_add = calc_fragment_delta( + sorted_vertices, left, right, left_add, right_add, left_target); + + /* Perform a substep to make sure that the data of left and right match the data on the anchor + * point (center of the pixel). */ + update_branches_to_min_anchor_line(*sorted_vertices[0], left, right, left_add, right_add); + + /* Add rasterlines from min_rasterline_y to mid_rasterline_y. */ + rasterize_loop( + min_rasterline_y, mid_rasterline_y, left, right, left_add, right_add, fragment_add); + + /* Special case when mid vertex is on the same rasterline as the min vertex. + * In this case we need to split the right/left branches. Comparing the x coordinate to find + * the branch that should hold the mid vertex. + */ + if (min_rasterline_y == mid_rasterline_y) { + update_branch_for_flat_bottom(*sorted_vertices[0], *sorted_vertices[1], left, right); + } + + update_branches_at_mid_anchor_line( + *sorted_vertices[1], *sorted_vertices[2], left, right, left_add, right_add); + + /* Add rasterlines from mid_rasterline_y to max_rasterline_y. */ + rasterize_loop( + mid_rasterline_y, max_rasterline_y, left, right, left_add, right_add, fragment_add); + } + + /** + * Rasterize multiple sequential lines. + * + * Create and buffer rasterlines between #from_y and #to_y. + * The #left and #right branches are incremented for each rasterline. + */ + void rasterize_loop(int32_t from_y, + int32_t to_y, + VertexOutputType &left, + VertexOutputType &right, + const VertexOutputType &left_add, + const VertexOutputType &right_add, + const InterfaceInnerType &fragment_add) + { + for (int y = from_y; y < to_y; y++) { + if (y >= 0 && y < drawing_target_.get_height()) { + std::optional<RasterlineType> rasterline = clamped_rasterline( + y, left.coord[0], right.coord[0], left.data, fragment_add); + if (rasterline) { + append(*rasterline); + } + } + left += left_add; + right += right_add; + } + } + + /** + * Update the left or right branch for when the mid vertex is on the same rasterline as the min + * vertex. + */ + void update_branch_for_flat_bottom(const VertexOutputType &min_vertex, + const VertexOutputType &mid_vertex, + VertexOutputType &r_left, + VertexOutputType &r_right) const + { + if (min_vertex.coord[0] > mid_vertex.coord[0]) { + r_left = mid_vertex; + } + else { + r_right = mid_vertex; + } + } + + void update_branches_to_min_anchor_line(const VertexOutputType &min_vertex, + VertexOutputType &r_left, + VertexOutputType &r_right, + const VertexOutputType &left_add, + const VertexOutputType &right_add) + { + const float distance_to_minline_anchor_point = clamping_method.distance_to_scanline_anchor( + min_vertex.coord[1]); + r_left += left_add * distance_to_minline_anchor_point; + r_right += right_add * distance_to_minline_anchor_point; + } + + void update_branches_at_mid_anchor_line(const VertexOutputType &mid_vertex, + const VertexOutputType &max_vertex, + VertexOutputType &r_left, + VertexOutputType &r_right, + VertexOutputType &r_left_add, + VertexOutputType &r_right_add) + { + /* When both are the same we should the left/right branches are the same. */ + const float distance_to_midline_anchor_point = clamping_method.distance_to_scanline_anchor( + mid_vertex.coord[1]); + /* Use the x coordinate to identify which branch should be modified. */ + const float distance_to_left = abs(r_left.coord[0] - mid_vertex.coord[0]); + const float distance_to_right = abs(r_right.coord[0] - mid_vertex.coord[0]); + if (distance_to_left < distance_to_right) { + r_left = mid_vertex; + r_left_add = calc_branch_delta(r_left, max_vertex); + r_left += r_left_add * distance_to_midline_anchor_point; + } + else { + r_right = mid_vertex; + r_right_add = calc_branch_delta(r_right, max_vertex); + r_right += r_right_add * distance_to_midline_anchor_point; + } + } + + /** + * Calculate the delta adder between two sequential fragments in the x-direction. + * + * Fragment adder is constant and can be calculated once and reused for each rasterline of + * the same triangle. However the calculation requires a distance that might not be known at + * the first scanline that is added. Therefore this method uses the mid scanline as there is + * the max x_distance. + * + * \returns the adder that can be added the previous fragment data. + */ + InterfaceInnerType calc_fragment_delta(const std::array<VertexOutputType *, 3> &sorted_vertices, + const VertexOutputType &left, + const VertexOutputType &right, + const VertexOutputType &left_add, + const VertexOutputType &right_add, + const VertexOutputType *left_target) + { + const float distance_min_to_mid = sorted_vertices[1]->coord[1] - sorted_vertices[0]->coord[1]; + if (distance_min_to_mid == 0.0f) { + VertexOutputType *mid_left = (sorted_vertices[1]->coord[0] < sorted_vertices[0]->coord[0]) ? + sorted_vertices[1] : + sorted_vertices[0]; + VertexOutputType *mid_right = (sorted_vertices[1]->coord[0] < sorted_vertices[0]->coord[0]) ? + sorted_vertices[0] : + sorted_vertices[1]; + return (mid_right->data - mid_left->data) / (mid_right->coord[0] - mid_left->coord[0]); + } + + if (left_target == sorted_vertices[1]) { + VertexOutputType mid_right = right + right_add * distance_min_to_mid; + return (mid_right.data - sorted_vertices[1]->data) / + (mid_right.coord[0] - sorted_vertices[1]->coord[0]); + } + + VertexOutputType mid_left = left + left_add * distance_min_to_mid; + return (sorted_vertices[1]->data - mid_left.data) / + (sorted_vertices[1]->coord[0] - mid_left.coord[0]); + } + + /** + * Calculate the delta adder between two rasterlines for the given edge. + */ + VertexOutputType calc_branch_delta(const VertexOutputType &from, const VertexOutputType &to) + { + const float num_rasterlines = max_ff(to.coord[1] - from.coord[1], 1.0f); + VertexOutputType result = (to - from) / num_rasterlines; + return result; + } + + std::array<VertexOutputType *, 3> order_triangle_vertices( + std::array<VertexOutputType, 3> &vertex_out) + { + std::array<VertexOutputType *, 3> sorted; + /* Find min v-coordinate and store at index 0. */ + sorted[0] = &vertex_out[0]; + for (int i = 1; i < 3; i++) { + if (vertex_out[i].coord[1] < sorted[0]->coord[1]) { + sorted[0] = &vertex_out[i]; + } + } + + /* Find max v-coordinate and store at index 2. */ + sorted[2] = &vertex_out[0]; + for (int i = 1; i < 3; i++) { + if (vertex_out[i].coord[1] > sorted[2]->coord[1]) { + sorted[2] = &vertex_out[i]; + } + } + + /* Exit when all 3 have the same v coordinate. Use the original input order. */ + if (sorted[0]->coord[1] == sorted[2]->coord[1]) { + for (int i = 0; i < 3; i++) { + sorted[i] = &vertex_out[i]; + } + BLI_assert(sorted[0] != sorted[1] && sorted[0] != sorted[2] && sorted[1] != sorted[2]); + return sorted; + } + + /* Find mid v-coordinate and store at index 1. */ + sorted[1] = &vertex_out[0]; + for (int i = 0; i < 3; i++) { + if (sorted[0] != &vertex_out[i] && sorted[2] != &vertex_out[i]) { + sorted[1] = &vertex_out[i]; + break; + } + } + + BLI_assert(sorted[0] != sorted[1] && sorted[0] != sorted[2] && sorted[1] != sorted[2]); + BLI_assert(sorted[0]->coord[1] <= sorted[1]->coord[1]); + BLI_assert(sorted[0]->coord[1] < sorted[2]->coord[1]); + BLI_assert(sorted[1]->coord[1] <= sorted[2]->coord[1]); + return sorted; + } + + std::optional<RasterlineType> clamped_rasterline(int32_t y, + float start_x, + float end_x, + InterfaceInnerType start_data, + const InterfaceInnerType fragment_add) + { + BLI_assert(y >= 0 && y < drawing_target_.get_height()); + + stats.increase_rasterlines(); + if (start_x >= end_x) { + stats.increase_discarded_rasterlines(); + return std::nullopt; + } + if (end_x < 0) { + stats.increase_discarded_rasterlines(); + return std::nullopt; + } + if (start_x >= drawing_target_.get_width()) { + stats.increase_discarded_rasterlines(); + return std::nullopt; + } + + /* Is created rasterline clamped and should be added to the statistics. */ + bool is_clamped = false; + + /* Clamp the start_x to the first visible column anchor. */ + int32_t start_xi = clamping_method.column_for(start_x); + float delta_to_anchor = clamping_method.distance_to_column_anchor(start_x); + if (start_xi < 0) { + delta_to_anchor += -start_xi; + start_xi = 0; + is_clamped = true; + } + start_data += fragment_add * delta_to_anchor; + + uint32_t end_xi = clamping_method.column_for(end_x); + if (end_xi > drawing_target_.get_width()) { + end_xi = drawing_target_.get_width(); + is_clamped = true; + } + + if (is_clamped) { + stats.increase_clamped_rasterlines(); + } + +#ifdef DEBUG_PRINT + printf("%s y(%d) x(%d-%u)\n", __func__, y, start_xi, end_xi); +#endif + + return RasterlineType(y, (uint32_t)start_xi, end_xi, start_data, fragment_add); + } + + void render_rasterline(const RasterlineType &rasterline) + { + FragmentInputType data = rasterline.start_data; + float *pixel_ptr = drawing_target_.get_pixel_ptr(rasterline.start_x, rasterline.y); + for (uint32_t x = rasterline.start_x; x < rasterline.end_x; x++) { + + FragmentOutputType fragment_out; + fragment_shader_.fragment(data, &fragment_out); + blending_mode_.blend(pixel_ptr, fragment_out); + + data += rasterline.fragment_add; + pixel_ptr += drawing_target_.get_pixel_stride(); + } + + stats.increase_drawn_fragments(rasterline.end_x - rasterline.start_x); + } + + void append(const RasterlineType &rasterline) + { + rasterlines_.append(rasterline); + if (rasterlines_.is_full()) { + flush(); + } + } +}; + +} // namespace blender::imbuf::rasterizer diff --git a/source/blender/imbuf/intern/rasterizer_blending.hh b/source/blender/imbuf/intern/rasterizer_blending.hh new file mode 100644 index 00000000000..14dbaadfc95 --- /dev/null +++ b/source/blender/imbuf/intern/rasterizer_blending.hh @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * Copyright 2022 Blender Foundation. All rights reserved. */ + +#pragma once + +#include "BLI_math.h" +#include "BLI_math_vec_types.hh" +#include "BLI_sys_types.h" + +namespace blender::imbuf::rasterizer { + +/** How to integrate the result of a fragment shader into its drawing target. */ +template<typename Source, typename Destination> class AbstractBlendMode { + public: + using SourceType = Source; + using DestinationType = Destination; + + virtual void blend(Destination *dest, const Source &source) const = 0; +}; + +/** + * Copy the result of the fragment shader into float[4] without any modifications. + */ +class CopyBlendMode : public AbstractBlendMode<float4, float> { + public: + void blend(float *dest, const float4 &source) const override + { + copy_v4_v4(dest, source); + } +}; + +} // namespace blender::imbuf::rasterizer diff --git a/source/blender/imbuf/intern/rasterizer_clamping.hh b/source/blender/imbuf/intern/rasterizer_clamping.hh new file mode 100644 index 00000000000..30aa9daec36 --- /dev/null +++ b/source/blender/imbuf/intern/rasterizer_clamping.hh @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * Copyright 2022 Blender Foundation. All rights reserved. */ + +#pragma once + +/** \file + * \ingroup imbuf + * + * Pixel clamping determines how the edges of geometry is clamped to pixels. + */ + +#include "BLI_sys_types.h" + +namespace blender::imbuf::rasterizer { + +class AbstractPixelClampingMethod { + public: + virtual float distance_to_scanline_anchor(float y) const = 0; + virtual float distance_to_column_anchor(float y) const = 0; + virtual int scanline_for(float y) const = 0; + virtual int column_for(float x) const = 0; +}; + +class CenterPixelClampingMethod : public AbstractPixelClampingMethod { + public: + float distance_to_scanline_anchor(float y) const override + { + return distance_to_anchor(y); + } + float distance_to_column_anchor(float x) const override + { + return distance_to_anchor(x); + } + + int scanline_for(float y) const override + { + return this->round(y); + } + + int column_for(float x) const override + { + return this->round(x); + } + + private: + float distance_to_anchor(float value) const + { + float fract = to_fract(value); + float result; + if (fract <= 0.5f) { + result = 0.5f - fract; + } + else { + result = 1.5f - fract; + } + BLI_assert(result >= 0.0f); + BLI_assert(result < 1.0f); + return result; + } + + int round(float value) const + { + /* Cannot use std::round as it rounds away from 0. */ + float fract = to_fract(value); + int result; + + if (fract > 0.5f) { + result = ceilf(value); + } + else { + result = floorf(value); + } + return result; + } + + float to_fract(float value) const + { + return value - floor(value); + } +}; + +} // namespace blender::imbuf::rasterizer diff --git a/source/blender/imbuf/intern/rasterizer_stats.hh b/source/blender/imbuf/intern/rasterizer_stats.hh new file mode 100644 index 00000000000..4a8ad4bff73 --- /dev/null +++ b/source/blender/imbuf/intern/rasterizer_stats.hh @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * Copyright 2022 Blender Foundation. All rights reserved. */ + +#pragma once + +#include "BLI_sys_types.h" + +namespace blender::imbuf::rasterizer { + +class AbstractStats { + public: + virtual void increase_triangles() = 0; + virtual void increase_discarded_triangles() = 0; + virtual void increase_flushes() = 0; + virtual void increase_rasterlines() = 0; + virtual void increase_clamped_rasterlines() = 0; + virtual void increase_discarded_rasterlines() = 0; + virtual void increase_drawn_fragments(uint64_t fragments_drawn) = 0; +}; + +class Stats : public AbstractStats { + public: + int64_t triangles = 0; + int64_t discarded_triangles = 0; + int64_t flushes = 0; + int64_t rasterlines = 0; + int64_t clamped_rasterlines = 0; + int64_t discarded_rasterlines = 0; + int64_t drawn_fragments = 0; + + void increase_triangles() override + { + triangles += 1; + } + + void increase_discarded_triangles() override + { + discarded_triangles += 1; + } + + void increase_flushes() override + { + flushes += 1; + } + + void increase_rasterlines() override + { + rasterlines += 1; + } + + void increase_clamped_rasterlines() override + { + clamped_rasterlines += 1; + } + void increase_discarded_rasterlines() override + { + discarded_rasterlines += 1; + } + void increase_drawn_fragments(uint64_t fragments_drawn) override + { + drawn_fragments += fragments_drawn; + } + + void reset() + { + triangles = 0; + discarded_triangles = 0; + flushes = 0; + rasterlines = 0; + clamped_rasterlines = 0; + discarded_rasterlines = 0; + drawn_fragments = 0; + } +}; + +class NullStats : public AbstractStats { + public: + void increase_triangles() override{}; + void increase_discarded_triangles() override{}; + void increase_flushes() override{}; + void increase_rasterlines() override{}; + void increase_clamped_rasterlines() override{}; + void increase_discarded_rasterlines() override{}; + void increase_drawn_fragments(uint64_t UNUSED(fragments_drawn)) override + { + } +}; + +} // namespace blender::imbuf::rasterizer diff --git a/source/blender/imbuf/intern/rasterizer_target.hh b/source/blender/imbuf/intern/rasterizer_target.hh new file mode 100644 index 00000000000..25830b4006a --- /dev/null +++ b/source/blender/imbuf/intern/rasterizer_target.hh @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * Copyright 2022 Blender Foundation. All rights reserved. */ + +#pragma once + +/** \file + * \ingroup imbuf + * + * Rasterizer drawing target. + */ + +#include "BLI_sys_types.h" + +namespace blender::imbuf::rasterizer { + +/** + * An abstract implementation of a drawing target. Will make it possible to switch to other render + * targets then only ImBuf types. + */ +template<typename Inner> class AbstractDrawingTarget { + public: + using InnerType = Inner; + virtual uint64_t get_width() const = 0; + virtual uint64_t get_height() const = 0; + virtual float *get_pixel_ptr(uint64_t x, uint64_t y) = 0; + virtual int64_t get_pixel_stride() const = 0; + virtual bool has_active_target() const = 0; + virtual void activate(Inner *instance) = 0; + virtual void deactivate() = 0; +}; + +class ImageBufferDrawingTarget : public AbstractDrawingTarget<ImBuf> { + private: + ImBuf *image_buffer_ = nullptr; + + public: + bool has_active_target() const override + { + return image_buffer_ != nullptr; + } + + void activate(ImBuf *image_buffer) override + { + image_buffer_ = image_buffer; + } + + void deactivate() override + { + image_buffer_ = nullptr; + } + + uint64_t get_width() const override + { + return image_buffer_->x; + }; + + uint64_t get_height() const override + { + return image_buffer_->y; + } + + float *get_pixel_ptr(uint64_t x, uint64_t y) override + { + BLI_assert(has_active_target()); + uint64_t pixel_index = y * image_buffer_->x + x; + return &image_buffer_->rect_float[pixel_index * 4]; + } + int64_t get_pixel_stride() const override + { + return 4; + } +}; + +} // namespace blender::imbuf::rasterizer diff --git a/source/blender/imbuf/intern/rasterizer_test.cc b/source/blender/imbuf/intern/rasterizer_test.cc new file mode 100644 index 00000000000..ccc61d55ed9 --- /dev/null +++ b/source/blender/imbuf/intern/rasterizer_test.cc @@ -0,0 +1,429 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "testing/testing.h" + +#include "BLI_float4x4.hh" +#include "BLI_path_util.h" + +#include "IMB_rasterizer.hh" + +namespace blender::imbuf::rasterizer::tests { + +const uint32_t IMBUF_SIZE = 256; + +struct VertexInput { + float2 uv; + float value; + + VertexInput(float2 uv, float value) : uv(uv), value(value) + { + } +}; + +class VertexShader : public AbstractVertexShader<VertexInput, float4> { + public: + float2 image_size; + float4x4 vp_mat; + void vertex(const VertexInputType &input, VertexOutputType *r_output) override + { + float2 coord = float2(vp_mat * float3(input.uv[0], input.uv[1], 0.0)); + r_output->coord = coord * image_size; + r_output->data = float4(input.value, input.value, input.value, 1.0); + } +}; + +class FragmentShader : public AbstractFragmentShader<float4, float4> { + public: + void fragment(const FragmentInputType &input, FragmentOutputType *r_output) override + { + *r_output = input; + } +}; + +using RasterizerType = Rasterizer<VertexShader, + FragmentShader, + CopyBlendMode, + ImageBufferDrawingTarget, + DefaultRasterlinesBufferSize, + Stats>; + +/* Draw 2 triangles that fills the entire image buffer and see if each pixel is touched. */ +TEST(imbuf_rasterizer, draw_triangle_edge_alignment_quality) +{ + ImBuf image_buffer; + IMB_initImBuf(&image_buffer, IMBUF_SIZE, IMBUF_SIZE, 32, IB_rectfloat); + + RasterizerType rasterizer; + rasterizer.activate_drawing_target(&image_buffer); + + VertexShader &vertex_shader = rasterizer.vertex_shader(); + vertex_shader.image_size = float2(image_buffer.x, image_buffer.y); + + float clear_color[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + float3 location(0.5, 0.5, 0.0); + float3 rotation(0.0, 0.0, 0.0); + float3 scale(1.0, 1.0, 1.0); + + for (int i = 0; i < 1000; i++) { + rasterizer.stats.reset(); + + IMB_rectfill(&image_buffer, clear_color); + rotation[2] = (i / 1000.0) * M_PI * 2; + + vertex_shader.vp_mat = float4x4::from_loc_eul_scale(location, rotation, scale); + rasterizer.draw_triangle(VertexInput(float2(-1.0, -1.0), 0.2), + VertexInput(float2(-1.0, 1.0), 0.5), + VertexInput(float2(1.0, -1.0), 1.0)); + rasterizer.draw_triangle(VertexInput(float2(1.0, 1.0), 0.2), + VertexInput(float2(-1.0, 1.0), 0.5), + VertexInput(float2(1.0, -1.0), 1.0)); + rasterizer.flush(); + + /* Check if each pixel has been drawn exactly once. */ + EXPECT_EQ(rasterizer.stats.drawn_fragments, IMBUF_SIZE * IMBUF_SIZE) << i; + +#ifdef DEBUG_SAVE + char file_name[FILE_MAX]; + BLI_path_sequence_encode(file_name, "/tmp/test_", ".png", 4, i); + IMB_saveiff(&image_buffer, file_name, IB_rectfloat); + imb_freerectImBuf(&image_buffer); +#endif + } + + imb_freerectImbuf_all(&image_buffer); +} + +/** + * This test case renders 3 images that should have the same coverage. But using a different edge. + * + * The results should be identical. + */ +TEST(imbuf_rasterizer, edge_pixel_clamping) +{ + float clear_color[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + ImBuf image_buffer_a; + ImBuf image_buffer_b; + ImBuf image_buffer_c; + int fragments_drawn_a; + int fragments_drawn_b; + int fragments_drawn_c; + + RasterizerType rasterizer; + + { + IMB_initImBuf(&image_buffer_a, IMBUF_SIZE, IMBUF_SIZE, 32, IB_rectfloat); + + rasterizer.stats.reset(); + rasterizer.activate_drawing_target(&image_buffer_a); + VertexShader &vertex_shader = rasterizer.vertex_shader(); + vertex_shader.image_size = float2(image_buffer_a.x, image_buffer_a.y); + vertex_shader.vp_mat = float4x4::identity(); + IMB_rectfill(&image_buffer_a, clear_color); + rasterizer.draw_triangle(VertexInput(float2(0.2, -0.2), 1.0), + VertexInput(float2(1.2, 1.2), 1.0), + VertexInput(float2(1.5, -0.3), 1.0)); + rasterizer.flush(); + fragments_drawn_a = rasterizer.stats.drawn_fragments; + } + { + IMB_initImBuf(&image_buffer_b, IMBUF_SIZE, IMBUF_SIZE, 32, IB_rectfloat); + + rasterizer.stats.reset(); + rasterizer.activate_drawing_target(&image_buffer_b); + VertexShader &vertex_shader = rasterizer.vertex_shader(); + vertex_shader.image_size = float2(image_buffer_b.x, image_buffer_b.y); + vertex_shader.vp_mat = float4x4::identity(); + IMB_rectfill(&image_buffer_b, clear_color); + rasterizer.draw_triangle(VertexInput(float2(0.2, -0.2), 1.0), + VertexInput(float2(1.2, 1.2), 1.0), + VertexInput(float2(1.5, -0.3), 1.0)); + rasterizer.flush(); + fragments_drawn_b = rasterizer.stats.drawn_fragments; + } + + { + IMB_initImBuf(&image_buffer_c, IMBUF_SIZE, IMBUF_SIZE, 32, IB_rectfloat); + + rasterizer.stats.reset(); + rasterizer.activate_drawing_target(&image_buffer_c); + VertexShader &vertex_shader = rasterizer.vertex_shader(); + vertex_shader.image_size = float2(image_buffer_c.x, image_buffer_c.y); + vertex_shader.vp_mat = float4x4::identity(); + IMB_rectfill(&image_buffer_c, clear_color); + rasterizer.draw_triangle(VertexInput(float2(0.2, -0.2), 1.0), + VertexInput(float2(1.2, 1.2), 1.0), + VertexInput(float2(10.0, 1.3), 1.0)); + rasterizer.flush(); + fragments_drawn_c = rasterizer.stats.drawn_fragments; + } + + EXPECT_EQ(fragments_drawn_a, fragments_drawn_b); + EXPECT_EQ(memcmp(image_buffer_a.rect_float, + image_buffer_b.rect_float, + sizeof(float) * 4 * IMBUF_SIZE * IMBUF_SIZE), + 0); + EXPECT_EQ(fragments_drawn_a, fragments_drawn_c); + EXPECT_EQ(memcmp(image_buffer_a.rect_float, + image_buffer_c.rect_float, + sizeof(float) * 4 * IMBUF_SIZE * IMBUF_SIZE), + 0); + EXPECT_EQ(fragments_drawn_b, fragments_drawn_c); + EXPECT_EQ(memcmp(image_buffer_b.rect_float, + image_buffer_c.rect_float, + sizeof(float) * 4 * IMBUF_SIZE * IMBUF_SIZE), + 0); + + imb_freerectImbuf_all(&image_buffer_a); + imb_freerectImbuf_all(&image_buffer_b); + imb_freerectImbuf_all(&image_buffer_c); +} + +/** Use one rasterizer and switch between multiple drawing targets. */ +TEST(imbuf_rasterizer, switch_drawing_target) +{ + float clear_color[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + ImBuf image_buffer_a; + ImBuf image_buffer_b; + ImBuf image_buffer_c; + + RasterizerType rasterizer; + + IMB_initImBuf(&image_buffer_a, IMBUF_SIZE, IMBUF_SIZE, 32, IB_rectfloat); + IMB_rectfill(&image_buffer_a, clear_color); + + VertexShader &vertex_shader = rasterizer.vertex_shader(); + vertex_shader.image_size = float2(image_buffer_a.x, image_buffer_a.y); + vertex_shader.vp_mat = float4x4::identity(); + + rasterizer.activate_drawing_target(&image_buffer_a); + rasterizer.draw_triangle(VertexInput(float2(0.2, -0.2), 1.0), + VertexInput(float2(1.2, 1.2), 1.0), + VertexInput(float2(1.5, -0.3), 1.0)); + + IMB_initImBuf(&image_buffer_b, IMBUF_SIZE, IMBUF_SIZE, 32, IB_rectfloat); + IMB_rectfill(&image_buffer_b, clear_color); + rasterizer.activate_drawing_target(&image_buffer_b); + rasterizer.draw_triangle(VertexInput(float2(0.2, -0.2), 1.0), + VertexInput(float2(1.2, 1.2), 1.0), + VertexInput(float2(1.5, -0.3), 1.0)); + + IMB_initImBuf(&image_buffer_c, IMBUF_SIZE, IMBUF_SIZE, 32, IB_rectfloat); + IMB_rectfill(&image_buffer_c, clear_color); + rasterizer.activate_drawing_target(&image_buffer_c); + rasterizer.draw_triangle(VertexInput(float2(0.2, -0.2), 1.0), + VertexInput(float2(1.2, 1.2), 1.0), + VertexInput(float2(10.0, 1.3), 1.0)); + rasterizer.flush(); + + EXPECT_EQ(memcmp(image_buffer_a.rect_float, + image_buffer_b.rect_float, + sizeof(float) * 4 * IMBUF_SIZE * IMBUF_SIZE), + 0); + EXPECT_EQ(memcmp(image_buffer_a.rect_float, + image_buffer_c.rect_float, + sizeof(float) * 4 * IMBUF_SIZE * IMBUF_SIZE), + 0); + EXPECT_EQ(memcmp(image_buffer_b.rect_float, + image_buffer_c.rect_float, + sizeof(float) * 4 * IMBUF_SIZE * IMBUF_SIZE), + 0); + + imb_freerectImbuf_all(&image_buffer_a); + imb_freerectImbuf_all(&image_buffer_b); + imb_freerectImbuf_all(&image_buffer_c); +} + +TEST(imbuf_rasterizer, center_pixel_clamper_scanline_for) +{ + CenterPixelClampingMethod clamper; + + EXPECT_EQ(clamper.scanline_for(-2.0f), -2); + EXPECT_EQ(clamper.scanline_for(-1.9f), -2); + EXPECT_EQ(clamper.scanline_for(-1.8f), -2); + EXPECT_EQ(clamper.scanline_for(-1.7f), -2); + EXPECT_EQ(clamper.scanline_for(-1.6f), -2); + EXPECT_EQ(clamper.scanline_for(-1.5f), -2); + EXPECT_EQ(clamper.scanline_for(-1.4f), -1); + EXPECT_EQ(clamper.scanline_for(-1.3f), -1); + EXPECT_EQ(clamper.scanline_for(-1.2f), -1); + EXPECT_EQ(clamper.scanline_for(-1.1f), -1); + EXPECT_EQ(clamper.scanline_for(-1.0f), -1); + EXPECT_EQ(clamper.scanline_for(-0.9f), -1); + EXPECT_EQ(clamper.scanline_for(-0.8f), -1); + EXPECT_EQ(clamper.scanline_for(-0.7f), -1); + EXPECT_EQ(clamper.scanline_for(-0.6f), -1); + EXPECT_EQ(clamper.scanline_for(-0.5f), -1); + EXPECT_EQ(clamper.scanline_for(-0.4f), 0); + EXPECT_EQ(clamper.scanline_for(-0.3f), 0); + EXPECT_EQ(clamper.scanline_for(-0.2f), 0); + EXPECT_EQ(clamper.scanline_for(-0.1f), 0); + EXPECT_EQ(clamper.scanline_for(0.0f), 0); + EXPECT_EQ(clamper.scanline_for(0.1f), 0); + EXPECT_EQ(clamper.scanline_for(0.2f), 0); + EXPECT_EQ(clamper.scanline_for(0.3f), 0); + EXPECT_EQ(clamper.scanline_for(0.4f), 0); + EXPECT_EQ(clamper.scanline_for(0.5f), 0); + EXPECT_EQ(clamper.scanline_for(0.6f), 1); + EXPECT_EQ(clamper.scanline_for(0.7f), 1); + EXPECT_EQ(clamper.scanline_for(0.8f), 1); + EXPECT_EQ(clamper.scanline_for(0.9f), 1); + EXPECT_EQ(clamper.scanline_for(1.0f), 1); + EXPECT_EQ(clamper.scanline_for(1.0f), 1); + EXPECT_EQ(clamper.scanline_for(1.1f), 1); + EXPECT_EQ(clamper.scanline_for(1.2f), 1); + EXPECT_EQ(clamper.scanline_for(1.3f), 1); + EXPECT_EQ(clamper.scanline_for(1.4f), 1); + EXPECT_EQ(clamper.scanline_for(1.5f), 1); + EXPECT_EQ(clamper.scanline_for(1.6f), 2); + EXPECT_EQ(clamper.scanline_for(1.7f), 2); + EXPECT_EQ(clamper.scanline_for(1.8f), 2); + EXPECT_EQ(clamper.scanline_for(1.9f), 2); + EXPECT_EQ(clamper.scanline_for(2.0f), 2); +} + +TEST(imbuf_rasterizer, center_pixel_clamper_column_for) +{ + CenterPixelClampingMethod clamper; + + EXPECT_EQ(clamper.column_for(-2.0f), -2); + EXPECT_EQ(clamper.column_for(-1.9f), -2); + EXPECT_EQ(clamper.column_for(-1.8f), -2); + EXPECT_EQ(clamper.column_for(-1.7f), -2); + EXPECT_EQ(clamper.column_for(-1.6f), -2); + EXPECT_EQ(clamper.column_for(-1.5f), -2); + EXPECT_EQ(clamper.column_for(-1.4f), -1); + EXPECT_EQ(clamper.column_for(-1.3f), -1); + EXPECT_EQ(clamper.column_for(-1.2f), -1); + EXPECT_EQ(clamper.column_for(-1.1f), -1); + EXPECT_EQ(clamper.column_for(-1.0f), -1); + EXPECT_EQ(clamper.column_for(-0.9f), -1); + EXPECT_EQ(clamper.column_for(-0.8f), -1); + EXPECT_EQ(clamper.column_for(-0.7f), -1); + EXPECT_EQ(clamper.column_for(-0.6f), -1); + EXPECT_EQ(clamper.column_for(-0.5f), -1); + EXPECT_EQ(clamper.column_for(-0.4f), 0); + EXPECT_EQ(clamper.column_for(-0.3f), 0); + EXPECT_EQ(clamper.column_for(-0.2f), 0); + EXPECT_EQ(clamper.column_for(-0.1f), 0); + EXPECT_EQ(clamper.column_for(0.0f), 0); + EXPECT_EQ(clamper.column_for(0.1f), 0); + EXPECT_EQ(clamper.column_for(0.2f), 0); + EXPECT_EQ(clamper.column_for(0.3f), 0); + EXPECT_EQ(clamper.column_for(0.4f), 0); + EXPECT_EQ(clamper.column_for(0.5f), 0); + EXPECT_EQ(clamper.column_for(0.6f), 1); + EXPECT_EQ(clamper.column_for(0.7f), 1); + EXPECT_EQ(clamper.column_for(0.8f), 1); + EXPECT_EQ(clamper.column_for(0.9f), 1); + EXPECT_EQ(clamper.column_for(1.0f), 1); + EXPECT_EQ(clamper.column_for(1.0f), 1); + EXPECT_EQ(clamper.column_for(1.1f), 1); + EXPECT_EQ(clamper.column_for(1.2f), 1); + EXPECT_EQ(clamper.column_for(1.3f), 1); + EXPECT_EQ(clamper.column_for(1.4f), 1); + EXPECT_EQ(clamper.column_for(1.5f), 1); + EXPECT_EQ(clamper.column_for(1.6f), 2); + EXPECT_EQ(clamper.column_for(1.7f), 2); + EXPECT_EQ(clamper.column_for(1.8f), 2); + EXPECT_EQ(clamper.column_for(1.9f), 2); + EXPECT_EQ(clamper.column_for(2.0f), 2); +} + +TEST(imbuf_rasterizer, center_pixel_clamper_distance_to_scanline_anchorpoint) +{ + CenterPixelClampingMethod clamper; + + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-2.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.9f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.8f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.7f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.6f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.4f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.3f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.2f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.1f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-1.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.9f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.8f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.7f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.6f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.4f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.3f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.2f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(-0.1f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.1f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.2f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.3f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.4f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.6f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.7f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.8f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(0.9f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.1f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.2f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.3f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.4f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.6f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.7f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.8f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(1.9f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_scanline_anchor(2.0f), 0.5f); +} + +TEST(imbuf_rasterizer, center_pixel_clamper_distance_to_column_anchorpoint) +{ + CenterPixelClampingMethod clamper; + + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-2.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.9f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.8f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.7f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.6f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.4f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.3f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.2f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.1f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-1.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.9f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.8f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.7f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.6f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.4f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.3f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.2f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(-0.1f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.1f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.2f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.3f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.4f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.6f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.7f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.8f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(0.9f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.0f), 0.5f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.1f), 0.4f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.2f), 0.3f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.3f), 0.2f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.4f), 0.1f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.5f), 0.0f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.6f), 0.9f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.7f), 0.8f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.8f), 0.7f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(1.9f), 0.6f); + EXPECT_FLOAT_EQ(clamper.distance_to_column_anchor(2.0f), 0.5f); +} + +} // namespace blender::imbuf::rasterizer::tests |