diff options
Diffstat (limited to 'addons/voxel-core/classes/voxel_object.gd')
-rw-r--r-- | addons/voxel-core/classes/voxel_object.gd | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/addons/voxel-core/classes/voxel_object.gd b/addons/voxel-core/classes/voxel_object.gd new file mode 100644 index 0000000..8d12633 --- /dev/null +++ b/addons/voxel-core/classes/voxel_object.gd @@ -0,0 +1,501 @@ +tool +extends MeshInstance +# Makeshift interface class inhereted by all voxel visualization objects. + + + +## Signals +# Emitted when VoxelSet is changed +signal set_voxel_set(voxel_set) + + + +## Enums +# Defines the modes in which Mesh can be constructed +enum MeshModes { + # Naive meshing, simple culling of voxel faces; http://web.archive.org/web/20200428085802/https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/ + NAIVE, + # Greedy meshing, culls and merges similar voxel faces; http://web.archive.org/web/20201112011204/https://www.gedge.ca/dev/2014/08/17/greedy-voxel-meshing + GREEDY, + # Marching Cubes meshing, https://en.wikipedia.org/wiki/Marching_cubes + #MARCHING_CUBES, + # Transvoxel meshing, http://web.archive.org/web/20201112033736/http://transvoxel.org/ + #TRANSVOXEL, +} + + + +## Exported Variables +# The meshing mode by which Mesh is generated +export(MeshModes) var mesh_mode := MeshModes.NAIVE setget set_mesh_mode + +# Flag indicating that UV Mapping should be applied when generating meshes if applicable +export var uv_map := false setget set_uv_map + +# Flag indicating the persitant attachment and maintenance of a StaticBody +export var static_body := false setget set_static_body + +# The VoxelSet for this VoxelObject +export(Resource) var voxel_set = null setget set_voxel_set + + + +## Public Variables +# Flag indicating that edits to voxel data will be frequent +# NOTE: When true will only allow naive meshing +var edit_hint := 0 setget set_edit_hint + + + +# Public Methods +# Sets the EditHint flag, calls update_mesh if needed and not told otherwise +func set_edit_hint(value : int, update := is_inside_tree()) -> void: + edit_hint = value + + if update: + update_mesh() + + +# Sets the mesh_mode, calls update_mesh if needed and not told otherwise +func set_mesh_mode(value : int, update := is_inside_tree()) -> void: + mesh_mode = value + + if update: + update_mesh() + + +# Sets the uv_map, calls update_mesh if needed and not told otherwise +func set_uv_map(value : bool, update := is_inside_tree()) -> void: + uv_map = value + + if update: + update_mesh() + + +# Sets static_body, calls update_static_body if needed and not told otherwise +func set_static_body(value : bool, update := is_inside_tree()) -> void: + static_body = value + + if update: + update_static_body() + + +# Sets voxel_set, calls update_mesh if needed and not told otherwise +func set_voxel_set(value : Resource, update := is_inside_tree()) -> void: + if not (typeof(value) == TYPE_NIL or value is VoxelSet): + printerr("Invalid Resource given expected VoxelSet") + return + + if is_instance_valid(voxel_set): + if voxel_set.is_connected("requested_refresh", self, "update_mesh"): + voxel_set.disconnect("requested_refresh", self, "update_mesh") + + voxel_set = value + if is_instance_valid(voxel_set): + if not voxel_set.is_connected("requested_refresh", self, "update_mesh"): + voxel_set.connect("requested_refresh", self, "update_mesh") + + if update: + update_mesh() + emit_signal("set_voxel_set", voxel_set) + + +# Return true if no voxels are present +func empty() -> bool: + return true + + +# Sets given voxel id at the given grid position +func set_voxel(grid : Vector3, voxel_id : int) -> void: + pass + + +# Replace current voxel data with given voxel data +# voxels : Dictionary<Vector3, int> : voxels to set +func set_voxels(voxels : Dictionary) -> void: + erase_voxels() + for grid in voxels: + set_voxel(grid, voxels[grid]) + + +# Returns voxel id at given grid position if present; otherwise returns -1 +func get_voxel_id(grid : Vector3) -> int: + return -1 + + +# Returns voxel Dictionary representing voxel id at given grid position +func get_voxel(grid : Vector3) -> Dictionary: + return voxel_set.get_voxel(get_voxel_id(grid)) + + +# Returns Array of all voxel grid positions +# return : Array<Vector3> : Array of Vector3 each represents a grid position of a voxel +func get_voxels() -> Array: + return [] + + +# Erase voxel id at given grid position +func erase_voxel(grid : Vector3) -> void: + pass + + +# Erase all voxels +func erase_voxels() -> void: + for grid in get_voxels(): + erase_voxel(grid) + + +# Returns 3D axis-aligned bounding box +# volume : Array<Vector3> : Array of grid positions from which to calculate bounds +# return : Dictionary : bounding box, contains: { position : Vector3, size: Vector3 } +func get_box(volume := get_voxels()) -> Dictionary: + var box := { "position": Vector3.ZERO, "size": Vector3.ZERO } + + if not volume.empty(): + box["position"] = Vector3.INF + box["size"] = -Vector3.INF + + for voxel_grid in volume: + if voxel_grid.x < box["position"].x: + box["position"].x = voxel_grid.x + if voxel_grid.y < box["position"].y: + box["position"].y = voxel_grid.y + if voxel_grid.z < box["position"].z: + box["position"].z = voxel_grid.z + + if voxel_grid.x > box["size"].x: + box["size"].x = voxel_grid.x + if voxel_grid.y > box["size"].y: + box["size"].y = voxel_grid.y + if voxel_grid.z > box["size"].z: + box["size"].z = voxel_grid.z + + box["size"] = (box["size"] - box["position"]).abs() + Vector3.ONE + + return box + + +# Moves voxels in given volume by given translation +# translation : Vector3 : translation to move voxels by +# volume : Array<Vector3> : Array of grid positions representing voxels to move +func move(translation := Vector3(), volume := get_voxels()) -> void: + var translated := {} + for voxel_grid in volume: + translated[voxel_grid + translation] = get_voxel_id(voxel_grid) + erase_voxel(voxel_grid) + for voxel_grid in translated: + set_voxel(voxel_grid, translated[voxel_grid]) + + +# Centers voxels in given volume with respect to axis origin with the given alignment +# alignment : Vector3 : Alignment to center voxels by +# volume : Array<Vector3> : Array of grid positions representing voxels to center +func center(alignment := Vector3(0.5, 0.5, 0.5), volume := get_voxels()) -> void: + move(vec_to_center(alignment, volume), volume) + + +# Flips voxels in given volume over set axis +func flip(x : bool, y : bool, z : bool, volume := get_voxels()) -> void: + var flipped := {} + for voxel_grid in volume: + flipped[Vector3( + (voxel_grid.x + (1 if z else 0)) * (-1 if z else 1), + (voxel_grid.y + (1 if y else 0)) * (-1 if y else 1), + (voxel_grid.z + (1 if x else 0)) * (-1 if x else 1))] = get_voxel_id(voxel_grid) + erase_voxel(voxel_grid) + for voxel_grid in flipped: + set_voxel(voxel_grid, flipped[voxel_grid]) + + +# Returns the translation necessary to center given volume by +# alignment : Vector3 : Alignment to center voxels by +# volume : Array<Vector3> : Array of grid positions representing voxels to center +# return : Vector3 : Translation necessary to center +func vec_to_center(alignment := Vector3(0.5, 0.5, 0.5), volume := get_voxels()) -> Vector3: + var box := get_box(volume) + alignment = Vector3.ONE - Vector3( + clamp(alignment.x, 0.0, 1.0), + clamp(alignment.y, 0.0, 1.0), + clamp(alignment.z, 0.0, 1.0)) + return -box["position"] - (box["size"] * alignment).floor() + +# A Fast Voxel Traversal Algorithm for Ray Tracing, by John Amanatides +# Algorithm paper: https://web.archive.org/web/20201108160724/http://www.cse.chalmers.se/edu/year/2010/course/TDA361/grid.pdf +# from : Vector3 : World position from which to start raycast +# direction : Vector3 : Direction of raycast +# max_distance : int : Maximum distance of ray cast +# stop : FuncRef : Calls on function, that receives "hit" and returns bool, as raycast is projected, if it returns true raycast is returned +# return : Dictionary<String, Vector3> : If voxel is "hit", returns Dictionary with grid position and face normal; else empty +func intersect_ray( + from : Vector3, + direction : Vector3, + max_distance := 64, + stop : FuncRef = null) -> Dictionary: + var hit := { + "normal": Vector3(), + } + var grid := Voxel.world_to_grid(from) + var step := Vector3( + 1 if direction.x > 0 else -1, + 1 if direction.y > 0 else -1, + 1 if direction.z > 0 else -1) + var t_delta := direction.inverse().abs() + var dist := from.distance_to(Voxel.world_to_snapped(from)) + var t_max := t_delta * dist + var step_index := -1 + + var t = 0.0 + var valid := false + while t < max_distance: + hit["position"] = grid + hit["normal"].x = -step.x if step_index == 0 else 0 + hit["normal"].y = -step.y if step_index == 1 else 0 + hit["normal"].z = -step.z if step_index == 2 else 0 + if get_voxel_id(grid) > -1 or (is_instance_valid(stop) and stop.call_func(hit)): + valid = true + break + + match t_max.min_axis(): + Vector3.AXIS_X: + grid.x += step.x + t = t_max.x + t_max.x += t_delta.x + step_index = 0 + Vector3.AXIS_Y: + grid.y += step.y + t = t_max.y + t_max.y += t_delta.y + step_index = 1 + Vector3.AXIS_Z: + grid.z += step.z + t = t_max.z + t_max.z += t_delta.z + step_index = 2 + if not valid: + hit.clear() + return hit + + +# Returns Array of all voxel grid positions connected to given target +# target : Vector3 : Grid position at which to start flood select +# selected : Array : Array to add selected voxel grid positions to +# return : Array<Vector3> : Array of all voxel grid positions connected to given target +func select_flood(target : Vector3, selected := []) -> Array: + selected.append(get_voxel_id(target)) + + for direction in Voxel.Faces: + var next = target + direction + if get_voxel_id(next) == get_voxel_id(selected[0]): + if not selected.has(next): + select_flood(next, selected) + + return selected + + +# Returns Array of all voxel grid positions connected to given target that aren't obstructed at the given face normal +# target : Vector3 : Grid position at which to start flood select +# face_normal : Vector3 : Normal of face to check for obstruction +# selected : Array : Array to add selected voxel grid positions to +# return : Array<Vector3> : Array of all voxel grid positions connected to given target +func select_face(target : Vector3, face_normal : Vector3, selected := []) -> Array: + selected.append(target) + + for direction in Voxel.Faces[face_normal]: + var next = target + direction + if get_voxel_id(next) > -1: + if get_voxel_id(next + face_normal) == -1: + if not selected.has(next): + select_face(next, face_normal, selected) + + return selected + + +# Returns Array of all voxel grid positions connected to given target that are similar and aren't obstructed at the given face normal +# target : Vector3 : Grid position at which to start flood select +# face_normal : Vector3 : Normal of face to check for obstruction +# selected : Array : Array to add selected voxel grid positions to +# return : Array<Vector3> : Array of all voxel grid positions connected to given target +func select_face_similar(target : Vector3, face_normal : Vector3, selected := []) -> Array: + selected.append(target) + + for direction in Voxel.Faces[face_normal]: + var next = target + direction + if get_voxel_id(next) == get_voxel_id(selected[0]): + if get_voxel_id(next + face_normal) == -1: + if not selected.has(next): + select_face_similar(next, face_normal, selected) + + return selected + + +# Loads and sets voxels and replaces VoxelSet with given file +# NOTE: Reference Reader.gd for valid file imports +# source_file : String : Path to file to be loaded +# new_voxel_set : bool : If true new VoxelSet is created, else overwrite current one +# return int : int : Error code +func load_file(source_file : String, new_voxel_set := true) -> int: + var read := Reader.read_file(source_file) + var error : int = read.get("error", FAILED) + if error == OK: + if new_voxel_set or not is_instance_valid(voxel_set): + set_voxel_set(VoxelSet.new(), false) + voxel_set.set_voxels(read["palette"]) + + set_voxels(read["voxels"]) + return error + + +# Makes a naive mesh out of volume of voxels given +# volume : Array<Vector3> : Array of grid positions representing volume of voxels from which to buid ArrayMesh +# vt : VoxelTool : VoxelTool with which ArrayMesh will be built +# return : ArrayMesh : Naive voxel mesh +func naive_volume(volume : Array, vt := VoxelTool.new()) -> ArrayMesh: + if not is_instance_valid(voxel_set): + return null + + vt.begin(voxel_set, uv_map) + + for position in volume: + for direction in Voxel.Faces: + if get_voxel_id(position + direction) == -1: + vt.add_face(get_voxel(position), direction, position) + + return vt.commit() + + +# Greedy meshing +# volume : Array<Vector3> : Array of grid positions representing volume of voxels from which to buid ArrayMesh +# vt : VoxelTool : VoxelTool with which ArrayMesh will be built +# return : ArrayMesh : Greedy voxel mesh +func greed_volume(volume : Array, vt := VoxelTool.new()) -> ArrayMesh: + if not is_instance_valid(voxel_set): + return null + + vt.begin(voxel_set, uv_map) + + var faces = Voxel.Faces.duplicate() + for face in faces: + faces[face] = [] + for position in volume: + if get_voxel_id(position + face) == -1: + faces[face].append(position) + + for face in faces: + while not faces[face].empty(): + var bottom_right : Vector3 = faces[face].pop_front() + var bottom_left : Vector3 = bottom_right + var top_right : Vector3 = bottom_right + var top_left : Vector3 = bottom_right + var voxel : Dictionary = get_voxel(bottom_right) + + + if not uv_map or Voxel.get_face_uv(voxel, face) == -Vector2.ONE: + var width := 1 + + while true: + var index = faces[face].find(top_right + Voxel.Faces[face][1]) + if index > -1: + var _voxel = get_voxel(faces[face][index]) + if Voxel.get_face_color(_voxel, face) == Voxel.get_face_color(voxel, face) and (not uv_map or Voxel.get_face_uv(_voxel, face) == -Vector2.ONE): + width += 1 + faces[face].remove(index) + top_right += Voxel.Faces[face][1] + bottom_right += Voxel.Faces[face][1] + else: + break + else: + break + + while true: + var index = faces[face].find(top_left + Voxel.Faces[face][0]) + if index > -1: + var _voxel = get_voxel(faces[face][index]) + if Voxel.get_face_color(_voxel, face) == Voxel.get_face_color(voxel, face) and (not uv_map or Voxel.get_face_uv(_voxel, face) == -Vector2.ONE): + width += 1 + faces[face].remove(index) + top_left += Voxel.Faces[face][0] + bottom_left += Voxel.Faces[face][0] + else: + break + else: + break + + while true: + var used := [] + var current := top_right + var index = faces[face].find(current + Voxel.Faces[face][3]) + if index > -1: + var _voxel = get_voxel(faces[face][index]) + if Voxel.get_face_color(_voxel, face) == Voxel.get_face_color(voxel, face) and (not uv_map or Voxel.get_face_uv(_voxel, face) == -Vector2.ONE): + current += Voxel.Faces[face][3] + used.append(current) + while true: + index = faces[face].find(current + Voxel.Faces[face][0]) + if index > -1: + _voxel = get_voxel(faces[face][index]) + if Voxel.get_face_color(_voxel, face) == Voxel.get_face_color(voxel, face) and (not uv_map or Voxel.get_face_uv(_voxel, face) == -Vector2.ONE): + current += Voxel.Faces[face][0] + used.append(current) + else: + break + else: + break + if used.size() == width: + top_right += Voxel.Faces[face][3] + top_left += Voxel.Faces[face][3] + for use in used: + faces[face].erase(use) + else: + break + else: + break + else: + break + + while true: + var used := [] + var current := bottom_right + var index = faces[face].find(current + Voxel.Faces[face][2]) + if index > -1: + var _voxel = get_voxel(faces[face][index]) + if Voxel.get_face_color(_voxel, face) == Voxel.get_face_color(voxel, face) and (not uv_map or Voxel.get_face_uv(_voxel, face) == -Vector2.ONE): + current += Voxel.Faces[face][2] + used.append(current) + while true: + index = faces[face].find(current + Voxel.Faces[face][0]) + if index > -1: + _voxel = get_voxel(faces[face][index]) + if Voxel.get_face_color(_voxel, face) == Voxel.get_face_color(voxel, face) and (not uv_map or Voxel.get_face_uv(_voxel, face) == -Vector2.ONE): + current += Voxel.Faces[face][0] + used.append(current) + else: + break + else: + break + if used.size() == width: + bottom_right += Voxel.Faces[face][2] + bottom_left += Voxel.Faces[face][2] + for use in used: + faces[face].erase(use) + else: + break + else: + break + else: + break + + vt.add_face(voxel,face, + bottom_right, bottom_left, top_right, top_left) + + return vt.commit() + + +# Updates Mesh and calls on save and update_static_body if needed +# save : bool : Save voxels on update +func update_mesh() -> void: + update_static_body() + + +# Sets and updates StaticMesh if demanded +func update_static_body() -> void: + pass |