#include "image_utils.h" #include "int_range_2d.h" #include "math_funcs.h" namespace godot { template inline void generic_brush_op(Image &image, Image &brush, Vector2 p_pos, float factor, F op) { IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size()); int min_x_noclamp = range.min_x; int min_y_noclamp = range.min_y; range.clip(Vector2i(image.get_size())); image.lock(); brush.lock(); for (int y = range.min_y; y < range.max_y; ++y) { int by = y - min_y_noclamp; for (int x = range.min_x; x < range.max_x; ++x) { int bx = x - min_x_noclamp; float b = brush.get_pixel(bx, by).r * factor; op(image, x, y, b); } } image.unlock(); brush.unlock(); } ImageUtils::ImageUtils() { #ifdef _DEBUG Godot::print("Constructing ImageUtils"); #endif } ImageUtils::~ImageUtils() { #ifdef _DEBUG // TODO Cannot print shit here, see https://github.com/godotengine/godot/issues/37417 // Means only the console will print this //Godot::print("Destructing ImageUtils"); printf("Destructing ImageUtils\n"); #endif } void ImageUtils::_init() { } Vector2 ImageUtils::get_red_range(Ref image_ref, Rect2 rect) const { ERR_FAIL_COND_V(image_ref.is_null(), Vector2()); Image &image = **image_ref; IntRange2D range(rect); range.clip(Vector2i(image.get_size())); image.lock(); float min_value = image.get_pixel(range.min_x, range.min_y).r; float max_value = min_value; for (int y = range.min_y; y < range.max_y; ++y) { for (int x = range.min_x; x < range.max_x; ++x) { float v = image.get_pixel(x, y).r; if (v > max_value) { max_value = v; } else if (v < min_value) { min_value = v; } } } image.unlock(); return Vector2(min_value, max_value); } float ImageUtils::get_red_sum(Ref image_ref, Rect2 rect) const { ERR_FAIL_COND_V(image_ref.is_null(), 0.f); Image &image = **image_ref; IntRange2D range(rect); range.clip(Vector2i(image.get_size())); image.lock(); float sum = 0.f; for (int y = range.min_y; y < range.max_y; ++y) { for (int x = range.min_x; x < range.max_x; ++x) { sum += image.get_pixel(x, y).r; } } image.unlock(); return sum; } float ImageUtils::get_red_sum_weighted(Ref image_ref, Ref brush_ref, Vector2 p_pos, float factor) const { ERR_FAIL_COND_V(image_ref.is_null(), 0.f); ERR_FAIL_COND_V(brush_ref.is_null(), 0.f); Image &image = **image_ref; Image &brush = **brush_ref; float sum = 0.f; generic_brush_op(image, brush, p_pos, factor, [&sum](Image &image, int x, int y, float b) { sum += image.get_pixel(x, y).r * b; }); return sum; } void ImageUtils::add_red_brush(Ref image_ref, Ref brush_ref, Vector2 p_pos, float factor) const { ERR_FAIL_COND(image_ref.is_null()); ERR_FAIL_COND(brush_ref.is_null()); Image &image = **image_ref; Image &brush = **brush_ref; generic_brush_op(image, brush, p_pos, factor, [](Image &image, int x, int y, float b) { float r = image.get_pixel(x, y).r + b; image.set_pixel(x, y, Color(r, r, r)); }); } void ImageUtils::lerp_channel_brush(Ref image_ref, Ref brush_ref, Vector2 p_pos, float factor, float target_value, int channel) const { ERR_FAIL_COND(image_ref.is_null()); ERR_FAIL_COND(brush_ref.is_null()); Image &image = **image_ref; Image &brush = **brush_ref; generic_brush_op(image, brush, p_pos, factor, [target_value, channel](Image &image, int x, int y, float b) { Color c = image.get_pixel(x, y); c[channel] = Math::lerp(c[channel], target_value, b); image.set_pixel(x, y, c); }); } void ImageUtils::lerp_color_brush(Ref image_ref, Ref brush_ref, Vector2 p_pos, float factor, Color target_value) const { ERR_FAIL_COND(image_ref.is_null()); ERR_FAIL_COND(brush_ref.is_null()); Image &image = **image_ref; Image &brush = **brush_ref; generic_brush_op(image, brush, p_pos, factor, [target_value](Image &image, int x, int y, float b) { const Color c = image.get_pixel(x, y).linear_interpolate(target_value, b); image.set_pixel(x, y, c); }); } // TODO Smooth (each pixel being box-filtered, contrary to the existing smooth) float ImageUtils::generate_gaussian_brush(Ref image_ref) const { ERR_FAIL_COND_V(image_ref.is_null(), 0.f); Image &image = **image_ref; int w = static_cast(image.get_width()); int h = static_cast(image.get_height()); Vector2 center(w / 2, h / 2); float radius = Math::min(w, h) / 2; ERR_FAIL_COND_V(radius <= 0.1f, 0.f); float sum = 0.f; image.lock(); for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { float d = Vector2(x, y).distance_to(center) / radius; float v = Math::clamp(1.f - d * d * d, 0.f, 1.f); image.set_pixel(x, y, Color(v, v, v)); sum += v; } } image.unlock(); return sum; } void ImageUtils::blur_red_brush(Ref image_ref, Ref brush_ref, Vector2 p_pos, float factor) { ERR_FAIL_COND(image_ref.is_null()); ERR_FAIL_COND(brush_ref.is_null()); Image &image = **image_ref; Image &brush = **brush_ref; factor = Math::clamp(factor, 0.f, 1.f); // Relative to the image IntRange2D buffer_range = IntRange2D::from_pos_size(p_pos, brush.get_size()); buffer_range.pad(1); const int image_width = static_cast(image.get_width()); const int image_height = static_cast(image.get_height()); const int buffer_width = static_cast(buffer_range.get_width()); const int buffer_height = static_cast(buffer_range.get_height()); _blur_buffer.resize(buffer_width * buffer_height); image.lock(); // Cache pixels, because they will be queried more than once and written to later int buffer_i = 0; for (int y = buffer_range.min_y; y < buffer_range.max_y; ++y) { for (int x = buffer_range.min_x; x < buffer_range.max_x; ++x) { const int ix = Math::clamp(x, 0, image_width - 1); const int iy = Math::clamp(y, 0, image_height - 1); _blur_buffer[buffer_i] = image.get_pixel(ix, iy).r; ++buffer_i; } } IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size()); const int min_x_noclamp = range.min_x; const int min_y_noclamp = range.min_y; range.clip(Vector2i(image.get_size())); const int buffer_offset_left = -1; const int buffer_offset_right = 1; const int buffer_offset_top = -buffer_width; const int buffer_offset_bottom = buffer_width; brush.lock(); // Apply blur for (int y = range.min_y; y < range.max_y; ++y) { const int brush_y = y - min_y_noclamp; for (int x = range.min_x; x < range.max_x; ++x) { const int brush_x = x - min_x_noclamp; const float brush_value = brush.get_pixel(brush_x, brush_y).r * factor; buffer_i = (brush_x + 1) + (brush_y + 1) * buffer_width; const float p10 = _blur_buffer[buffer_i + buffer_offset_top]; const float p01 = _blur_buffer[buffer_i + buffer_offset_left]; const float p11 = _blur_buffer[buffer_i]; const float p21 = _blur_buffer[buffer_i + buffer_offset_right]; const float p12 = _blur_buffer[buffer_i + buffer_offset_bottom]; // Average float m = (p10 + p01 + p11 + p21 + p12) * 0.2f; float p = Math::lerp(p11, m, brush_value); image.set_pixel(x, y, Color(p, p, p)); } } image.unlock(); brush.unlock(); } void ImageUtils::paint_indexed_splat(Ref index_map_ref, Ref weight_map_ref, Ref brush_ref, Vector2 p_pos, int texture_index, float factor) { ERR_FAIL_COND(index_map_ref.is_null()); ERR_FAIL_COND(weight_map_ref.is_null()); ERR_FAIL_COND(brush_ref.is_null()); Image &index_map = **index_map_ref; Image &weight_map = **weight_map_ref; Image &brush = **brush_ref; ERR_FAIL_COND(index_map.get_size() != weight_map.get_size()); factor = Math::clamp(factor, 0.f, 1.f); IntRange2D range = IntRange2D::from_min_max(p_pos, brush.get_size()); const int min_x_noclamp = range.min_x; const int min_y_noclamp = range.min_y; range.clip(Vector2i(index_map.get_size())); const float texture_index_f = float(texture_index) / 255.f; const Color all_texture_index_f(texture_index_f, texture_index_f, texture_index_f); const int ci = texture_index % 3; Color cm(-1, -1, -1); cm[ci] = 1; brush.lock(); index_map.lock(); weight_map.lock(); for (int y = range.min_y; y < range.max_y; ++y) { const int brush_y = y - min_y_noclamp; for (int x = range.min_x; x < range.max_x; ++x) { const int brush_x = x - min_x_noclamp; const float brush_value = brush.get_pixel(brush_x, brush_y).r * factor; if (brush_value == 0.f) { continue; } Color i = index_map.get_pixel(x, y); Color w = weight_map.get_pixel(x, y); // Decompress third weight to make computations easier w[2] = 1.f - w[0] - w[1]; if (std::abs(i[ci] - texture_index_f) > 0.001f) { // Pixel does not have our texture index, // transfer its weight to other components first if (w[ci] > brush_value) { w[0] -= cm[0] * brush_value; w[1] -= cm[1] * brush_value; w[2] -= cm[2] * brush_value; } else if (w[ci] >= 0.f) { w[ci] = 0.f; i[ci] = texture_index_f; } } else { // Pixel has our texture index, increase its weight if (w[ci] + brush_value < 1.f) { w[0] += cm[0] * brush_value; w[1] += cm[1] * brush_value; w[2] += cm[2] * brush_value; } else { // Pixel weight is full, we can set all components to the same index. // Need to nullify other weights because they would otherwise never reach // zero due to normalization w = Color(0, 0, 0); w[ci] = 1.0; i = all_texture_index_f; } } // No `saturate` function in Color?? w[0] = Math::clamp(w[0], 0.f, 1.f); w[1] = Math::clamp(w[1], 0.f, 1.f); w[2] = Math::clamp(w[2], 0.f, 1.f); // Renormalize const float sum = w[0] + w[1] + w[2]; w[0] /= sum; w[1] /= sum; w[2] /= sum; index_map.set_pixel(x, y, i); weight_map.set_pixel(x, y, w); } } brush.lock(); index_map.unlock(); weight_map.unlock(); } void ImageUtils::_register_methods() { register_method("get_red_range", &ImageUtils::get_red_range); register_method("get_red_sum", &ImageUtils::get_red_sum); register_method("get_red_sum_weighted", &ImageUtils::get_red_sum_weighted); register_method("add_red_brush", &ImageUtils::add_red_brush); register_method("lerp_channel_brush", &ImageUtils::lerp_channel_brush); register_method("lerp_color_brush", &ImageUtils::lerp_color_brush); register_method("generate_gaussian_brush", &ImageUtils::generate_gaussian_brush); register_method("blur_red_brush", &ImageUtils::blur_red_brush); register_method("paint_indexed_splat", &ImageUtils::paint_indexed_splat); } } // namespace godot