aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/native/image_utils_generic.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/zylann.hterrain/native/image_utils_generic.gd')
-rw-r--r--game/addons/zylann.hterrain/native/image_utils_generic.gd316
1 files changed, 316 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/native/image_utils_generic.gd b/game/addons/zylann.hterrain/native/image_utils_generic.gd
new file mode 100644
index 0000000..2a8aed8
--- /dev/null
+++ b/game/addons/zylann.hterrain/native/image_utils_generic.gd
@@ -0,0 +1,316 @@
+@tool
+
+# 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 HT_Util = preload("../util/util.gd")
+
+var _blur_buffer : Image
+
+
+func get_red_range(im: Image, rect: Rect2) -> Vector2:
+ rect = rect.intersection(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 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
+
+ return Vector2(min_height, max_height)
+
+
+func get_red_sum(im: Image, rect: Rect2) -> float:
+ rect = rect.intersection(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
+
+ for y in range(min_y, max_y):
+ for x in range(min_x, max_x):
+ sum += im.get_pixel(x, y).r
+
+ return sum
+
+
+func get_red_sum_weighted(im: Image, brush: Image, pos: Vector2, 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 = clampi(min_x, 0, im.get_width())
+ min_y = clampi(min_y, 0, im.get_height())
+ max_x = clampi(max_x, 0, im.get_width())
+ max_y = clampi(max_y, 0, im.get_height())
+
+ var sum = 0.0
+
+ 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
+
+ return sum
+
+
+func add_red_brush(im: Image, brush: Image, pos: Vector2, 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 = clampi(min_x, 0, im.get_width())
+ min_y = clampi(min_y, 0, im.get_height())
+ max_x = clampi(max_x, 0, im.get_width())
+ max_y = clampi(max_y, 0, im.get_height())
+
+ 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))
+
+
+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 = clampi(min_x, 0, im.get_width())
+ min_y = clampi(min_y, 0, im.get_height())
+ max_x = clampi(max_x, 0, im.get_width())
+ max_y = clampi(max_y, 0, im.get_height())
+
+ 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)
+
+
+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 = clampi(min_x, 0, im.get_width())
+ min_y = clampi(min_y, 0, im.get_height())
+ max_x = clampi(max_x, 0, im.get_width())
+ max_y = clampi(max_y, 0, im.get_height())
+
+ 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).lerp(target_value, factor * shape_value)
+ im.set_pixel(x, y, c)
+
+
+func generate_gaussian_brush(im: Image) -> float:
+ var sum := 0.0
+ var center := Vector2(im.get_width() / 2, im.get_height() / 2)
+ var radius := minf(im.get_width(), im.get_height()) / 2.0
+
+ for y in im.get_height():
+ for x in im.get_width():
+ var d := Vector2(x, y).distance_to(center) / radius
+ var v := clampf(1.0 - d * d * d, 0.0, 1.0)
+ im.set_pixel(x, y, Color(v, v, v))
+ sum += v;
+
+ return sum
+
+
+func blur_red_brush(im: Image, brush: Image, pos: Vector2, factor: float):
+ factor = clampf(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)
+
+ 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 := clampi(x, 0, im_clamp_w)
+ var iy := clampi(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 = clampi(min_x, 0, im.get_width())
+ min_y = clampi(min_y, 0, im.get_height())
+ max_x = clampi(max_x, 0, im.get_width())
+ max_y = clampi(max_y, 0, im.get_height())
+
+ # 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 = lerpf(p11, m, shape_value * factor)
+
+ im.set_pixel(x, y, Color(p, p, p))
+
+
+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 = clampi(min_x, 0, index_map.get_width())
+ min_y = clampi(min_y, 0, index_map.get_height())
+ max_x = clampi(max_x, 0, index_map.get_width())
+ max_y = clampi(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
+
+ 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] = clampf(w[0], 0.0, 1.0)
+ w[1] = clampf(w[1], 0.0, 1.0)
+ w[2] = clampf(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)