# These functions are the same as the ones found in the GDNative library. # They are used if the user's platform is not supported. const Util = preload("../util/util.gd") var _blur_buffer : Image func get_red_range(im: Image, rect: Rect2) -> Vector2: rect = rect.clip(Rect2(0, 0, im.get_width(), im.get_height())) var min_x := int(rect.position.x) var min_y := int(rect.position.y) var max_x := min_x + int(rect.size.x) var max_y := min_y + int(rect.size.y) im.lock() var min_height := im.get_pixel(min_x, min_y).r var max_height := min_height for y in range(min_y, max_y): for x in range(min_x, max_x): var h = im.get_pixel(x, y).r if h < min_height: min_height = h elif h > max_height: max_height = h im.unlock() return Vector2(min_height, max_height) func get_red_sum(im: Image, rect: Rect2) -> float: rect = rect.clip(Rect2(0, 0, im.get_width(), im.get_height())) var min_x := int(rect.position.x) var min_y := int(rect.position.y) var max_x := min_x + int(rect.size.x) var max_y := min_y + int(rect.size.y) var sum := 0.0 im.lock() for y in range(min_y, max_y): for x in range(min_x, max_x): sum += im.get_pixel(x, y).r im.unlock() return sum func get_red_sum_weighted(im: Image, brush: Image, pos: Vector2, var factor: float) -> float: var min_x = int(pos.x) var min_y = int(pos.y) var max_x = min_x + brush.get_width() var max_y = min_y + brush.get_height() var min_noclamp_x = min_x var min_noclamp_y = min_y min_x = Util.clamp_int(min_x, 0, im.get_width()) min_y = Util.clamp_int(min_y, 0, im.get_height()) max_x = Util.clamp_int(max_x, 0, im.get_width()) max_y = Util.clamp_int(max_y, 0, im.get_height()) var sum = 0.0 im.lock() brush.lock() for y in range(min_y, max_y): var by = y - min_noclamp_y for x in range(min_x, max_x): var bx = x - min_noclamp_x var shape_value = brush.get_pixel(bx, by).r sum += im.get_pixel(x, y).r * shape_value * factor im.lock() brush.unlock() return sum func add_red_brush(im: Image, brush: Image, pos: Vector2, var factor: float): var min_x = int(pos.x) var min_y = int(pos.y) var max_x = min_x + brush.get_width() var max_y = min_y + brush.get_height() var min_noclamp_x = min_x var min_noclamp_y = min_y min_x = Util.clamp_int(min_x, 0, im.get_width()) min_y = Util.clamp_int(min_y, 0, im.get_height()) max_x = Util.clamp_int(max_x, 0, im.get_width()) max_y = Util.clamp_int(max_y, 0, im.get_height()) im.lock() brush.lock() for y in range(min_y, max_y): var by = y - min_noclamp_y for x in range(min_x, max_x): var bx = x - min_noclamp_x var shape_value = brush.get_pixel(bx, by).r var r = im.get_pixel(x, y).r + shape_value * factor im.set_pixel(x, y, Color(r, r, r)) im.lock() brush.unlock() func lerp_channel_brush(im: Image, brush: Image, pos: Vector2, factor: float, target_value: float, channel: int): var min_x = int(pos.x) var min_y = int(pos.y) var max_x = min_x + brush.get_width() var max_y = min_y + brush.get_height() var min_noclamp_x = min_x var min_noclamp_y = min_y min_x = Util.clamp_int(min_x, 0, im.get_width()) min_y = Util.clamp_int(min_y, 0, im.get_height()) max_x = Util.clamp_int(max_x, 0, im.get_width()) max_y = Util.clamp_int(max_y, 0, im.get_height()) im.lock() brush.lock() for y in range(min_y, max_y): var by = y - min_noclamp_y for x in range(min_x, max_x): var bx = x - min_noclamp_x var shape_value = brush.get_pixel(bx, by).r var c = im.get_pixel(x, y) c[channel] = lerp(c[channel], target_value, shape_value * factor) im.set_pixel(x, y, c) im.lock() brush.unlock() func lerp_color_brush(im: Image, brush: Image, pos: Vector2, factor: float, target_value: Color): var min_x = int(pos.x) var min_y = int(pos.y) var max_x = min_x + brush.get_width() var max_y = min_y + brush.get_height() var min_noclamp_x = min_x var min_noclamp_y = min_y min_x = Util.clamp_int(min_x, 0, im.get_width()) min_y = Util.clamp_int(min_y, 0, im.get_height()) max_x = Util.clamp_int(max_x, 0, im.get_width()) max_y = Util.clamp_int(max_y, 0, im.get_height()) im.lock() brush.lock() for y in range(min_y, max_y): var by = y - min_noclamp_y for x in range(min_x, max_x): var bx = x - min_noclamp_x var shape_value = brush.get_pixel(bx, by).r var c = im.get_pixel(x, y).linear_interpolate(target_value, factor * shape_value) im.set_pixel(x, y, c) im.lock() brush.unlock() func generate_gaussian_brush(im: Image) -> float: var sum := 0.0 var center := Vector2(im.get_width() / 2, im.get_height() / 2) var radius := min(im.get_width(), im.get_height()) / 2.0 im.lock() for y in im.get_height(): for x in im.get_width(): var d := Vector2(x, y).distance_to(center) / radius var v := clamp(1.0 - d * d * d, 0.0, 1.0) im.set_pixel(x, y, Color(v, v, v)) sum += v; im.unlock() return sum func blur_red_brush(im: Image, brush: Image, pos: Vector2, factor: float): factor = clamp(factor, 0.0, 1.0) if _blur_buffer == null: _blur_buffer = Image.new() var buffer := _blur_buffer var buffer_width := brush.get_width() + 2 var buffer_height := brush.get_height() + 2 if buffer_width != buffer.get_width() or buffer_height != buffer.get_height(): buffer.create(buffer_width, buffer_height, false, Image.FORMAT_RF) im.lock() buffer.lock() var min_x := int(pos.x) - 1 var min_y := int(pos.y) - 1 var max_x := min_x + buffer.get_width() var max_y := min_y + buffer.get_height() var im_clamp_w = im.get_width() - 1 var im_clamp_h = im.get_height() - 1 # Copy pixels to temporary buffer for y in range(min_y, max_y): for x in range(min_x, max_x): var ix := clamp(x, 0, im_clamp_w) var iy := clamp(y, 0, im_clamp_h) var c = im.get_pixel(ix, iy) buffer.set_pixel(x - min_x, y - min_y, c) min_x = int(pos.x) min_y = int(pos.y) max_x = min_x + brush.get_width() max_y = min_y + brush.get_height() var min_noclamp_x := min_x var min_noclamp_y := min_y min_x = Util.clamp_int(min_x, 0, im.get_width()) min_y = Util.clamp_int(min_y, 0, im.get_height()) max_x = Util.clamp_int(max_x, 0, im.get_width()) max_y = Util.clamp_int(max_y, 0, im.get_height()) brush.lock() # Apply blur for y in range(min_y, max_y): var by := y - min_noclamp_y for x in range(min_x, max_x): var bx := x - min_noclamp_x var shape_value := brush.get_pixel(bx, by).r * factor var p10 = buffer.get_pixel(bx + 1, by ).r var p01 = buffer.get_pixel(bx, by + 1).r var p11 = buffer.get_pixel(bx + 1, by + 1).r var p21 = buffer.get_pixel(bx + 2, by + 1).r var p12 = buffer.get_pixel(bx + 1, by + 2).r var m = (p10 + p01 + p11 + p21 + p12) * 0.2 var p = lerp(p11, m, shape_value * factor) im.set_pixel(x, y, Color(p, p, p)) im.unlock() buffer.unlock() brush.unlock() func paint_indexed_splat(index_map: Image, weight_map: Image, brush: Image, pos: Vector2, \ texture_index: int, factor: float): var min_x := pos.x var min_y := pos.y var max_x := min_x + brush.get_width() var max_y := min_y + brush.get_height() var min_noclamp_x := min_x var min_noclamp_y := min_y min_x = Util.clamp_int(min_x, 0, index_map.get_width()) min_y = Util.clamp_int(min_y, 0, index_map.get_height()) max_x = Util.clamp_int(max_x, 0, index_map.get_width()) max_y = Util.clamp_int(max_y, 0, index_map.get_height()) var texture_index_f := float(texture_index) / 255.0 var all_texture_index_f := Color(texture_index_f, texture_index_f, texture_index_f) var ci := texture_index % 3 var cm := Color(-1, -1, -1) cm[ci] = 1 index_map.lock() weight_map.lock() brush.lock() for y in range(min_y, max_y): var by := y - min_noclamp_y for x in range(min_x, max_x): var bx := x - min_noclamp_x var shape_value := brush.get_pixel(bx, by).r * factor if shape_value == 0.0: continue var i := index_map.get_pixel(x, y) var w := weight_map.get_pixel(x, y) # Decompress third weight to make computations easier w[2] = 1.0 - w[0] - w[1] # The index map tells which textures to blend. # The weight map tells their blending amounts. # This brings the limitation that up to 3 textures can blend at a time in a given pixel. # Painting this in real time can be a challenge. # The approach here is a compromise for simplicity. # Each texture is associated a fixed component of the index map (R, G or B), # so two neighbor pixels having the same component won't be guaranteed to blend. # In other words, texture T will not be able to blend with T + N * k, # where k is an integer, and N is the number of components in the index map (up to 4). # It might still be able to blend due to a special case when an area is uniform, # but not otherwise. # Dynamic component assignment sounds like the alternative, however I wasn't able # to find a painting algorithm that wasn't confusing, at least the current one is # predictable. # Need to use approximation because Color is float but GDScript uses doubles... if abs(i[ci] - texture_index_f) > 0.001: # Pixel does not have our texture index, # transfer its weight to other components first if w[ci] > shape_value: w -= cm * shape_value elif w[ci] >= 0.0: w[ci] = 0.0 i[ci] = texture_index_f else: # Pixel has our texture index, increase its weight if w[ci] + shape_value < 1.0: w += cm * shape_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] = clamp(w[0], 0.0, 1.0) w[1] = clamp(w[1], 0.0, 1.0) w[2] = clamp(w[2], 0.0, 1.0) # Renormalize w /= w[0] + w[1] + w[2] index_map.set_pixel(x, y, i) weight_map.set_pixel(x, y, w) index_map.lock() weight_map.lock() brush.unlock()