aboutsummaryrefslogtreecommitdiff
path: root/game/addons/zylann.hterrain/util/util.gd
diff options
context:
space:
mode:
Diffstat (limited to 'game/addons/zylann.hterrain/util/util.gd')
-rw-r--r--game/addons/zylann.hterrain/util/util.gd549
1 files changed, 549 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/util/util.gd b/game/addons/zylann.hterrain/util/util.gd
new file mode 100644
index 0000000..e2cd32c
--- /dev/null
+++ b/game/addons/zylann.hterrain/util/util.gd
@@ -0,0 +1,549 @@
+@tool
+
+const HT_Errors = preload("./errors.gd")
+
+
+# Godot has this internally but doesn't expose it
+static func next_power_of_two(x: int) -> int:
+ x -= 1
+ x |= x >> 1
+ x |= x >> 2
+ x |= x >> 4
+ x |= x >> 8
+ x |= x >> 16
+ x += 1
+ return x
+
+
+# CubeMesh doesn't have a wireframe option
+static func create_wirecube_mesh(color = Color(1,1,1)) -> Mesh:
+ var positions := PackedVector3Array([
+ Vector3(0, 0, 0),
+ Vector3(1, 0, 0),
+ Vector3(1, 0, 1),
+ Vector3(0, 0, 1),
+ Vector3(0, 1, 0),
+ Vector3(1, 1, 0),
+ Vector3(1, 1, 1),
+ Vector3(0, 1, 1),
+ ])
+ var colors := PackedColorArray([
+ color, color, color, color,
+ color, color, color, color,
+ ])
+ var indices := PackedInt32Array([
+ 0, 1,
+ 1, 2,
+ 2, 3,
+ 3, 0,
+
+ 4, 5,
+ 5, 6,
+ 6, 7,
+ 7, 4,
+
+ 0, 4,
+ 1, 5,
+ 2, 6,
+ 3, 7
+ ])
+ var arrays := []
+ arrays.resize(Mesh.ARRAY_MAX)
+ arrays[Mesh.ARRAY_VERTEX] = positions
+ arrays[Mesh.ARRAY_COLOR] = colors
+ arrays[Mesh.ARRAY_INDEX] = indices
+ var mesh := ArrayMesh.new()
+ mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
+ return mesh
+
+
+static func integer_square_root(x: int) -> int:
+ assert(typeof(x) == TYPE_INT)
+ var r := int(roundf(sqrt(x)))
+ if r * r == x:
+ return r
+ # Does not exist
+ return -1
+
+
+# Formats integer using a separator between each 3-digit group
+static func format_integer(n: int, sep := ",") -> String:
+ assert(typeof(n) == TYPE_INT)
+
+ var negative := false
+ if n < 0:
+ negative = true
+ n = -n
+
+ var s = ""
+ while n >= 1000:
+ s = str(sep, str(n % 1000).pad_zeros(3), s)
+ n /= 1000
+
+ if negative:
+ return str("-", str(n), s)
+ else:
+ return str(str(n), s)
+
+
+# Goes up all parents until a node of the given class is found
+static func get_node_in_parents(node: Node, klass) -> Node:
+ while node != null:
+ node = node.get_parent()
+ if node != null and is_instance_of(node, klass):
+ return node
+ return null
+
+
+# Goes down all children until a node of the given class is found
+static func find_first_node(node: Node, klass) -> Node:
+ if is_instance_of(node, klass):
+ return node
+ for i in node.get_child_count():
+ var child := node.get_child(i)
+ var found_node := find_first_node(child, klass)
+ if found_node != null:
+ return found_node
+ return null
+
+
+static func is_in_edited_scene(node: Node) -> bool:
+ if not node.is_inside_tree():
+ return false
+ var edited_scene := node.get_tree().edited_scene_root
+ if node == edited_scene:
+ return true
+ return edited_scene != null and edited_scene.is_ancestor_of(node)
+
+
+# Get an extended or cropped version of an image,
+# with optional anchoring to decide in which direction to extend or crop.
+# New pixels are filled with the provided fill color.
+static func get_cropped_image(src: Image, width: int, height: int,
+ fill_color=null, anchor=Vector2(-1, -1)) -> Image:
+
+ width = int(width)
+ height = int(height)
+ if width == src.get_width() and height == src.get_height():
+ return src
+ var im := Image.create(width, height, false, src.get_format())
+ if fill_color != null:
+ im.fill(fill_color)
+ var p = get_cropped_image_params(
+ src.get_width(), src.get_height(), width, height, anchor)
+ im.blit_rect(src, p.src_rect, p.dst_pos)
+ return im
+
+
+static func get_cropped_image_params(src_w: int, src_h: int, dst_w: int, dst_h: int,
+ anchor: Vector2) -> Dictionary:
+
+ var rel_anchor := (anchor + Vector2(1, 1)) / 2.0
+
+ var dst_x := (dst_w - src_w) * rel_anchor.x
+ var dst_y := (dst_h - src_h) * rel_anchor.y
+
+ var src_x := 0
+ var src_y := 0
+
+ if dst_x < 0:
+ src_x -= dst_x
+ src_w -= dst_x
+ dst_x = 0
+
+ if dst_y < 0:
+ src_y -= dst_y
+ src_h -= dst_y
+ dst_y = 0
+
+ if dst_x + src_w >= dst_w:
+ src_w = dst_w - dst_x
+
+ if dst_y + src_h >= dst_h:
+ src_h = dst_h - dst_y
+
+ return {
+ "src_rect": Rect2i(src_x, src_y, src_w, src_h),
+ "dst_pos": Vector2i(dst_x, dst_y)
+ }
+
+# TODO Workaround for https://github.com/godotengine/godot/issues/24488
+# TODO Simplify in Godot 3.1 if that's still not fixed,
+# using https://github.com/godotengine/godot/pull/21806
+# And actually that function does not even work.
+#static func get_shader_param_or_default(mat: Material, name: String):
+# assert(mat.shader != null)
+# var v = mat.get_shader_param(name)
+# if v != null:
+# return v
+# var params = VisualServer.shader_get_param_list(mat.shader)
+# for p in params:
+# if p.name == name:
+# match p.type:
+# TYPE_OBJECT:
+# return null
+# # I should normally check default values,
+# # however they are not accessible
+# TYPE_BOOL:
+# return false
+# TYPE_REAL:
+# return 0.0
+# TYPE_VECTOR2:
+# return Vector2()
+# TYPE_VECTOR3:
+# return Vector3()
+# TYPE_COLOR:
+# return Color()
+# return null
+
+
+# Generic way to apply editor scale to a plugin UI scene.
+# It is slower than doing it manually on specific controls.
+# Takes a node as root because since Godot 4 Window dialogs are no longer Controls.
+static func apply_dpi_scale(root: Node, dpi_scale: float):
+ if dpi_scale == 1.0:
+ return
+ var to_process := [root]
+ while len(to_process) > 0:
+ var node : Node = to_process[-1]
+ to_process.pop_back()
+ if node is Window:
+ node.size = Vector2(node.size) * dpi_scale
+ elif node is Viewport or node is SubViewport:
+ continue
+ elif node is Control:
+ if node.custom_minimum_size != Vector2(0, 0):
+ node.custom_minimum_size = node.custom_minimum_size * dpi_scale
+ var parent = node.get_parent()
+ if parent != null:
+ if not (parent is Container):
+ node.offset_bottom *= dpi_scale
+ node.offset_left *= dpi_scale
+ node.offset_top *= dpi_scale
+ node.offset_right *= dpi_scale
+ for i in node.get_child_count():
+ to_process.append(node.get_child(i))
+
+
+# TODO AABB has `intersects_segment` but doesn't provide the hit point
+# So we have to rely on a less efficient method.
+# Returns a list of intersections between an AABB and a segment, sorted
+# by distance to the beginning of the segment.
+static func get_aabb_intersection_with_segment(aabb: AABB,
+ segment_begin: Vector3, segment_end: Vector3) -> Array:
+
+ var hits := []
+
+ if not aabb.intersects_segment(segment_begin, segment_end):
+ return hits
+
+ var hit
+
+ var x_rect := Rect2(aabb.position.y, aabb.position.z, aabb.size.y, aabb.size.z)
+
+ hit = Plane(Vector3(1, 0, 0), aabb.position.x) \
+ .intersects_segment(segment_begin, segment_end)
+ if hit != null and x_rect.has_point(Vector2(hit.y, hit.z)):
+ hits.append(hit)
+
+ hit = Plane(Vector3(1, 0, 0), aabb.end.x) \
+ .intersects_segment(segment_begin, segment_end)
+ if hit != null and x_rect.has_point(Vector2(hit.y, hit.z)):
+ hits.append(hit)
+
+ var y_rect := Rect2(aabb.position.x, aabb.position.z, aabb.size.x, aabb.size.z)
+
+ hit = Plane(Vector3(0, 1, 0), aabb.position.y) \
+ .intersects_segment(segment_begin, segment_end)
+ if hit != null and y_rect.has_point(Vector2(hit.x, hit.z)):
+ hits.append(hit)
+
+ hit = Plane(Vector3(0, 1, 0), aabb.end.y) \
+ .intersects_segment(segment_begin, segment_end)
+ if hit != null and y_rect.has_point(Vector2(hit.x, hit.z)):
+ hits.append(hit)
+
+ var z_rect := Rect2(aabb.position.x, aabb.position.y, aabb.size.x, aabb.size.y)
+
+ hit = Plane(Vector3(0, 0, 1), aabb.position.z) \
+ .intersects_segment(segment_begin, segment_end)
+ if hit != null and z_rect.has_point(Vector2(hit.x, hit.y)):
+ hits.append(hit)
+
+ hit = Plane(Vector3(0, 0, 1), aabb.end.z) \
+ .intersects_segment(segment_begin, segment_end)
+ if hit != null and z_rect.has_point(Vector2(hit.x, hit.y)):
+ hits.append(hit)
+
+ if len(hits) == 2:
+ # The segment has two hit points. Sort them by distance
+ var d0 = hits[0].distance_squared_to(segment_begin)
+ var d1 = hits[1].distance_squared_to(segment_begin)
+ if d0 > d1:
+ var temp = hits[0]
+ hits[0] = hits[1]
+ hits[1] = temp
+ else:
+ assert(len(hits) < 2)
+
+ return hits
+
+
+class HT_GridRaytraceResult2D:
+ var hit_cell_pos: Vector2
+ var prev_cell_pos: Vector2
+
+
+# Iterates through a virtual 2D grid of unit-sized square cells,
+# and executes an action on each cell intersecting the given segment,
+# ordered from begin to end.
+# One of my most re-used pieces of code :)
+#
+# Initially inspired by http://www.cse.yorku.ca/~amana/research/grid.pdf
+#
+# Ported from https://github.com/bulletphysics/bullet3/blob/
+# 687780af6b491056700cfb22cab57e61aeec6ab8/src/BulletCollision/CollisionShapes/
+# btHeightfieldTerrainShape.cpp#L418
+#
+static func grid_raytrace_2d(ray_origin: Vector2, ray_direction: Vector2,
+ quad_predicate: Callable, max_distance: float) -> HT_GridRaytraceResult2D:
+
+ if max_distance < 0.0001:
+ # Consider the ray is too small to hit anything
+ return null
+
+ var xi_step := 0
+ if ray_direction.x > 0:
+ xi_step = 1
+ elif ray_direction.x < 0:
+ xi_step = -1
+
+ var yi_step := 0
+ if ray_direction.y > 0:
+ yi_step = 1
+ elif ray_direction.y < 0:
+ yi_step = -1
+
+ var infinite := 9999999.0
+
+ var param_delta_x := infinite
+ if xi_step != 0:
+ param_delta_x = 1.0 / absf(ray_direction.x)
+
+ var param_delta_y := infinite
+ if yi_step != 0:
+ param_delta_y = 1.0 / absf(ray_direction.y)
+
+ # pos = param * dir
+ # At which value of `param` we will cross a x-axis lane?
+ var param_cross_x := infinite
+ # At which value of `param` we will cross a y-axis lane?
+ var param_cross_y := infinite
+
+ # param_cross_x and param_cross_z are initialized as being the first cross
+ # X initialization
+ if xi_step != 0:
+ if xi_step == 1:
+ param_cross_x = (ceilf(ray_origin.x) - ray_origin.x) * param_delta_x
+ else:
+ param_cross_x = (ray_origin.x - floorf(ray_origin.x)) * param_delta_x
+ else:
+ # Will never cross on X
+ param_cross_x = infinite
+
+ # Y initialization
+ if yi_step != 0:
+ if yi_step == 1:
+ param_cross_y = (ceilf(ray_origin.y) - ray_origin.y) * param_delta_y
+ else:
+ param_cross_y = (ray_origin.y - floorf(ray_origin.y)) * param_delta_y
+ else:
+ # Will never cross on Y
+ param_cross_y = infinite
+
+ var x := int(floorf(ray_origin.x))
+ var y := int(floorf(ray_origin.y))
+
+ # Workaround cases where the ray starts at an integer position
+ if param_cross_x == 0.0:
+ param_cross_x += param_delta_x
+ # If going backwards, we should ignore the position we would get by the above flooring,
+ # because the ray is not heading in that direction
+ if xi_step == -1:
+ x -= 1
+
+ if param_cross_y == 0.0:
+ param_cross_y += param_delta_y
+ if yi_step == -1:
+ y -= 1
+
+ var prev_x := x
+ var prev_y := y
+ var param := 0.0
+ var prev_param := 0.0
+
+ while true:
+ prev_x = x
+ prev_y = y
+ prev_param = param
+
+ if param_cross_x < param_cross_y:
+ # X lane
+ x += xi_step
+ # Assign before advancing the param,
+ # to be in sync with the initialization step
+ param = param_cross_x
+ param_cross_x += param_delta_x
+
+ else:
+ # Y lane
+ y += yi_step
+ param = param_cross_y
+ param_cross_y += param_delta_y
+
+ if param > max_distance:
+ param = max_distance
+ # quad coordinates, enter param, exit/end param
+ if quad_predicate.call(prev_x, prev_y, prev_param, param):
+ var res := HT_GridRaytraceResult2D.new()
+ res.hit_cell_pos = Vector2(x, y)
+ res.prev_cell_pos = Vector2(prev_x, prev_y)
+ return res
+ else:
+ break
+
+ elif quad_predicate.call(prev_x, prev_y, prev_param, param):
+ var res := HT_GridRaytraceResult2D.new()
+ res.hit_cell_pos = Vector2(x, y)
+ res.prev_cell_pos = Vector2(prev_x, prev_y)
+ return res
+
+ return null
+
+
+static func get_segment_clipped_by_rect(rect: Rect2,
+ segment_begin: Vector2, segment_end: Vector2) -> Array:
+
+ # /
+ # A-----/---B A-----+---B
+ # | / | => | / |
+ # | / | | / |
+ # C--/------D C--+------D
+ # /
+
+ if rect.has_point(segment_begin) and rect.has_point(segment_end):
+ return [segment_begin, segment_end]
+
+ var a := rect.position
+ var b := Vector2(rect.end.x, rect.position.y)
+ var c := Vector2(rect.position.x, rect.end.y)
+ var d := rect.end
+
+ var ab = Geometry2D.segment_intersects_segment(segment_begin, segment_end, a, b)
+ var cd = Geometry2D.segment_intersects_segment(segment_begin, segment_end, c, d)
+ var ac = Geometry2D.segment_intersects_segment(segment_begin, segment_end, a, c)
+ var bd = Geometry2D.segment_intersects_segment(segment_begin, segment_end, b, d)
+
+ var hits = []
+ if ab != null:
+ hits.append(ab)
+ if cd != null:
+ hits.append(cd)
+ if ac != null:
+ hits.append(ac)
+ if bd != null:
+ hits.append(bd)
+
+ # Now we need to order the hits from begin to end
+ if len(hits) == 1:
+ if rect.has_point(segment_begin):
+ hits = [segment_begin, hits[0]]
+ elif rect.has_point(segment_end):
+ hits = [hits[0], segment_end]
+ else:
+ # TODO This has a tendency to happen with integer coordinates...
+ # How can you get only 1 hit and have no end of the segment
+ # inside of the rectangle? Float precision shit? Assume no hit...
+ return []
+
+ elif len(hits) == 2:
+ var d0 = hits[0].distance_squared_to(segment_begin)
+ var d1 = hits[1].distance_squared_to(segment_begin)
+ if d0 > d1:
+ hits = [hits[1], hits[0]]
+
+ return hits
+
+
+static func get_pixel_clamped(im: Image, x: int, y: int) -> Color:
+ x = clampi(x, 0, im.get_width() - 1)
+ y = clampi(y, 0, im.get_height() - 1)
+ return im.get_pixel(x, y)
+
+
+static func update_configuration_warning(node: Node, recursive: bool):
+ if not Engine.is_editor_hint():
+ return
+ node.update_configuration_warnings()
+ if recursive:
+ for i in node.get_child_count():
+ var child = node.get_child(i)
+ update_configuration_warning(child, true)
+
+
+static func write_import_file(settings: Dictionary, imp_fpath: String, logger) -> bool:
+ # TODO Should use ConfigFile instead
+ var f := FileAccess.open(imp_fpath, FileAccess.WRITE)
+ if f == null:
+ var err = FileAccess.get_open_error()
+ logger.error("Could not open '{0}' for write, error {1}" \
+ .format([imp_fpath, HT_Errors.get_message(err)]))
+ return false
+
+ for section in settings:
+ f.store_line(str("[", section, "]"))
+ f.store_line("")
+ var params = settings[section]
+ for key in params:
+ var v = params[key]
+ var sv
+ match typeof(v):
+ TYPE_STRING:
+ sv = str('"', v.replace('"', '\"'), '"')
+ TYPE_BOOL:
+ sv = "true" if v else "false"
+ _:
+ sv = str(v)
+ f.store_line(str(key, "=", sv))
+ f.store_line("")
+
+ return true
+
+
+static func update_texture_partial(
+ tex: ImageTexture, im: Image, src_rect: Rect2i, dst_pos: Vector2i):
+
+ # ..ooo@@@XXX%%%xx..
+ # .oo@@XXX%x%xxx.. ` .
+ # .o@XX%%xx.. ` .
+ # o@X%.. ..ooooooo
+ # .@X%x. ..o@@^^ ^^@@o
+ # .ooo@@@@@@ooo.. ..o@@^ @X%
+ # o@@^^^ ^^^@@@ooo.oo@@^ %
+ # xzI -*-- ^^^o^^ --*- %
+ # @@@o ooooooo^@@^o^@X^@oooooo .X%x
+ # I@@@@@@@@@XX%%xx ( o@o )X%x@ROMBASED@@@X%x
+ # I@@@@XX%%xx oo@@@@X% @@X%x ^^^@@@@@@@X%x
+ # @X%xx o@@@@@@@X% @@XX%%x ) ^^@X%x
+ # ^ xx o@@@@@@@@Xx ^ @XX%%x xxx
+ # o@@^^^ooo I^^ I^o ooo . x
+ # oo @^ IX I ^X @^ oo
+ # IX U . V IX
+ # V . . V
+ #
+
+ # TODO Optimize: Godot 4 has lost the ability to update textures partially!
+ var fuck = tex.get_image()
+ fuck.blit_rect(im, src_rect, dst_pos)
+ tex.update(fuck)
+