khanat-client/addons/zylann.hterrain/native/image_utils_generic.gd

369 lines
9.8 KiB
GDScript

# 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()