aboutsummaryrefslogtreecommitdiff
path: root/addons/voxel-core/classes
diff options
context:
space:
mode:
authorjacopograndi <jacopo.grandi@outlook.it>2022-01-15 16:29:02 +0100
committerjacopograndi <jacopo.grandi@outlook.it>2022-01-15 16:29:02 +0100
commitc9c5b5d7c2a238310ce7bde336f76d2d1b6f9f29 (patch)
treec6e935fea995752a0b307e56892e8422fb734c0e /addons/voxel-core/classes
parent98f356e68b759bf84300290315d4ada09c41f79b (diff)
moved shapes to loading area & fixed asset paths
Diffstat (limited to 'addons/voxel-core/classes')
-rw-r--r--addons/voxel-core/classes/reader.gd21
-rw-r--r--addons/voxel-core/classes/readers/gpl.gd48
-rw-r--r--addons/voxel-core/classes/readers/image.gd40
-rw-r--r--addons/voxel-core/classes/readers/vox.gd86
-rw-r--r--addons/voxel-core/classes/voxel.gd330
-rw-r--r--addons/voxel-core/classes/voxel_mesh.gd124
-rw-r--r--addons/voxel-core/classes/voxel_object.gd501
-rw-r--r--addons/voxel-core/classes/voxel_set.gd234
-rw-r--r--addons/voxel-core/classes/voxel_tool.gd227
9 files changed, 1611 insertions, 0 deletions
diff --git a/addons/voxel-core/classes/reader.gd b/addons/voxel-core/classes/reader.gd
new file mode 100644
index 0000000..7c17b8a
--- /dev/null
+++ b/addons/voxel-core/classes/reader.gd
@@ -0,0 +1,21 @@
+class_name Reader
+extends Reference
+# Makeshift interface class inhereted by all file readers.
+
+
+
+## Public Methods
+# Calls on appropriate file reader according to file_path's extension.
+# file_path : String : path to file to be read
+# return : Dictionary : read results, contains: { error : int, voxels : Dictionary<Vec3, int>, palette : Dictionary<int, Dictionary<String, Variant> }
+static func read_file(file_path : String) -> Dictionary:
+ var result = { "error": ERR_FILE_UNRECOGNIZED }
+ match file_path.get_extension():
+ "png", "bmp", "dds", "exr", "hdr", "jpg", "jpeg", "tga", "svg", "svgz", "webp":
+ result = ImageReader.read_file(file_path)
+ "vox": result = VoxReader.read_file(file_path)
+ "qb": continue
+ "qbt": continue
+ "vxm": continue
+ "gpl": result = GPLReader.read_file(file_path)
+ return result
diff --git a/addons/voxel-core/classes/readers/gpl.gd b/addons/voxel-core/classes/readers/gpl.gd
new file mode 100644
index 0000000..4a6b943
--- /dev/null
+++ b/addons/voxel-core/classes/readers/gpl.gd
@@ -0,0 +1,48 @@
+class_name GPLReader
+extends Reference
+# GIMP palette file reader
+
+
+
+# Public Methods
+# Reads GPL file, and returns voxel palette
+static func read(gpl_file : File) -> Dictionary:
+ var result := {
+ "error": OK,
+ "voxels": {},
+ "palette": [],
+ }
+
+ if gpl_file.get_line() == "GIMP Palette":
+ while not gpl_file.eof_reached():
+ var line = gpl_file.get_line()
+ if typeof(line) == TYPE_STRING and not line.empty() and (line[0].is_valid_integer() or line[0] == " "):
+ var tokens = line.split("\t")
+ var name = ""
+ var color = tokens[0].split_floats(" ")
+ color = Color(color[0] / 255, color[1] / 255, color[2] / 255)
+ if tokens.size() > 1:
+ name = tokens[1]
+ var end = name.find("(")
+ name = name.substr(0, end)
+
+ if not result["palette"].has(color):
+ var voxel := Voxel.colored(color)
+ result["palette"].append(voxel)
+ if not name.empty():
+ Voxel.set_name(voxel, name.strip_edges())
+ else:
+ result["error"] = ERR_FILE_UNRECOGNIZED
+
+ return result
+
+
+static func read_file(gpl_path : String) -> Dictionary:
+ var result := { "error": OK }
+ var file := File.new()
+ var error = file.open(gpl_path, File.READ)
+ if error == OK:
+ result = read(file)
+ if file.is_open():
+ file.close()
+ return result
diff --git a/addons/voxel-core/classes/readers/image.gd b/addons/voxel-core/classes/readers/image.gd
new file mode 100644
index 0000000..3c19d2f
--- /dev/null
+++ b/addons/voxel-core/classes/readers/image.gd
@@ -0,0 +1,40 @@
+class_name ImageReader, "res://addons/voxel-core/assets/logos/MagicaVoxel.png"
+extends Reference
+# Image file reader
+
+
+
+## Public Methods
+# Reads images pixels, returns voxel content and voxel palette
+static func read(image : Image) -> Dictionary:
+ var result := {
+ "error": OK,
+ "voxels": {},
+ "palette": [],
+ }
+
+ image.lock()
+ for x in range(image.get_width()):
+ for y in range(image.get_height()):
+ if image.get_pixel(x, y).a > 0:
+ var color := image.get_pixel(x, y)
+ color.a = 1.0
+ var index = result["palette"].find(color)
+ if index == -1:
+ index = result["palette"].size()
+ result["palette"].append(color)
+ result["voxels"][Vector3(x, -y, 0).round()] = index
+ image.unlock()
+
+ for index in range(result["palette"].size()):
+ result["palette"][index] = Voxel.colored(result["palette"][index])
+
+ return result
+
+
+static func read_file(image_path : String) -> Dictionary:
+ var image := Image.new()
+ var err = image.load(image_path)
+ if err == OK:
+ return read(image)
+ return { "error": err }
diff --git a/addons/voxel-core/classes/readers/vox.gd b/addons/voxel-core/classes/readers/vox.gd
new file mode 100644
index 0000000..51dfa86
--- /dev/null
+++ b/addons/voxel-core/classes/readers/vox.gd
@@ -0,0 +1,86 @@
+class_name VoxReader, "res://addons/voxel-core/assets/logos/MagicaVoxel.png"
+extends Reference
+# MagicaVoxel file reader
+
+
+
+# Constants
+const magicavoxel_default_palette := [
+ Color("00000000"), Color("ffffffff"), Color("ffccffff"), Color("ff99ffff"), Color("ff66ffff"), Color("ff33ffff"), Color("ff00ffff"), Color("ffffccff"), Color("ffccccff"), Color("ff99ccff"), Color("ff66ccff"), Color("ff33ccff"), Color("ff00ccff"), Color("ffff99ff"), Color("ffcc99ff"), Color("ff9999ff"),
+ Color("ff6699ff"), Color("ff3399ff"), Color("ff0099ff"), Color("ffff66ff"), Color("ffcc66ff"), Color("ff9966ff"), Color("ff6666ff"), Color("ff3366ff"), Color("ff0066ff"), Color("ffff33ff"), Color("ffcc33ff"), Color("ff9933ff"), Color("ff6633ff"), Color("ff3333ff"), Color("ff0033ff"), Color("ffff00ff"),
+ Color("ffcc00ff"), Color("ff9900ff"), Color("ff6600ff"), Color("ff3300ff"), Color("ff0000ff"), Color("ffffffcc"), Color("ffccffcc"), Color("ff99ffcc"), Color("ff66ffcc"), Color("ff33ffcc"), Color("ff00ffcc"), Color("ffffcccc"), Color("ffcccccc"), Color("ff99cccc"), Color("ff66cccc"), Color("ff33cccc"),
+ Color("ff00cccc"), Color("ffff99cc"), Color("ffcc99cc"), Color("ff9999cc"), Color("ff6699cc"), Color("ff3399cc"), Color("ff0099cc"), Color("ffff66cc"), Color("ffcc66cc"), Color("ff9966cc"), Color("ff6666cc"), Color("ff3366cc"), Color("ff0066cc"), Color("ffff33cc"), Color("ffcc33cc"), Color("ff9933cc"),
+ Color("ff6633cc"), Color("ff3333cc"), Color("ff0033cc"), Color("ffff00cc"), Color("ffcc00cc"), Color("ff9900cc"), Color("ff6600cc"), Color("ff3300cc"), Color("ff0000cc"), Color("ffffff99"), Color("ffccff99"), Color("ff99ff99"), Color("ff66ff99"), Color("ff33ff99"), Color("ff00ff99"), Color("ffffcc99"),
+ Color("ffcccc99"), Color("ff99cc99"), Color("ff66cc99"), Color("ff33cc99"), Color("ff00cc99"), Color("ffff9999"), Color("ffcc9999"), Color("ff999999"), Color("ff669999"), Color("ff339999"), Color("ff009999"), Color("ffff6699"), Color("ffcc6699"), Color("ff996699"), Color("ff666699"), Color("ff336699"),
+ Color("ff006699"), Color("ffff3399"), Color("ffcc3399"), Color("ff993399"), Color("ff663399"), Color("ff333399"), Color("ff003399"), Color("ffff0099"), Color("ffcc0099"), Color("ff990099"), Color("ff660099"), Color("ff330099"), Color("ff000099"), Color("ffffff66"), Color("ffccff66"), Color("ff99ff66"),
+ Color("ff66ff66"), Color("ff33ff66"), Color("ff00ff66"), Color("ffffcc66"), Color("ffcccc66"), Color("ff99cc66"), Color("ff66cc66"), Color("ff33cc66"), Color("ff00cc66"), Color("ffff9966"), Color("ffcc9966"), Color("ff999966"), Color("ff669966"), Color("ff339966"), Color("ff009966"), Color("ffff6666"),
+ Color("ffcc6666"), Color("ff996666"), Color("ff666666"), Color("ff336666"), Color("ff006666"), Color("ffff3366"), Color("ffcc3366"), Color("ff993366"), Color("ff663366"), Color("ff333366"), Color("ff003366"), Color("ffff0066"), Color("ffcc0066"), Color("ff990066"), Color("ff660066"), Color("ff330066"),
+ Color("ff000066"), Color("ffffff33"), Color("ffccff33"), Color("ff99ff33"), Color("ff66ff33"), Color("ff33ff33"), Color("ff00ff33"), Color("ffffcc33"), Color("ffcccc33"), Color("ff99cc33"), Color("ff66cc33"), Color("ff33cc33"), Color("ff00cc33"), Color("ffff9933"), Color("ffcc9933"), Color("ff999933"),
+ Color("ff669933"), Color("ff339933"), Color("ff009933"), Color("ffff6633"), Color("ffcc6633"), Color("ff996633"), Color("ff666633"), Color("ff336633"), Color("ff006633"), Color("ffff3333"), Color("ffcc3333"), Color("ff993333"), Color("ff663333"), Color("ff333333"), Color("ff003333"), Color("ffff0033"),
+ Color("ffcc0033"), Color("ff990033"), Color("ff660033"), Color("ff330033"), Color("ff000033"), Color("ffffff00"), Color("ffccff00"), Color("ff99ff00"), Color("ff66ff00"), Color("ff33ff00"), Color("ff00ff00"), Color("ffffcc00"), Color("ffcccc00"), Color("ff99cc00"), Color("ff66cc00"), Color("ff33cc00"),
+ Color("ff00cc00"), Color("ffff9900"), Color("ffcc9900"), Color("ff999900"), Color("ff669900"), Color("ff339900"), Color("ff009900"), Color("ffff6600"), Color("ffcc6600"), Color("ff996600"), Color("ff666600"), Color("ff336600"), Color("ff006600"), Color("ffff3300"), Color("ffcc3300"), Color("ff993300"),
+ Color("ff663300"), Color("ff333300"), Color("ff003300"), Color("ffff0000"), Color("ffcc0000"), Color("ff990000"), Color("ff660000"), Color("ff330000"), Color("ff0000ee"), Color("ff0000dd"), Color("ff0000bb"), Color("ff0000aa"), Color("ff000088"), Color("ff000077"), Color("ff000055"), Color("ff000044"),
+ Color("ff000022"), Color("ff000011"), Color("ff00ee00"), Color("ff00dd00"), Color("ff00bb00"), Color("ff00aa00"), Color("ff008800"), Color("ff007700"), Color("ff005500"), Color("ff004400"), Color("ff002200"), Color("ff001100"), Color("ffee0000"), Color("ffdd0000"), Color("ffbb0000"), Color("ffaa0000"),
+ Color("ff880000"), Color("ff770000"), Color("ff550000"), Color("ff440000"), Color("ff220000"), Color("ff110000"), Color("ffeeeeee"), Color("ffdddddd"), Color("ffbbbbbb"), Color("ffaaaaaa"), Color("ff888888"), Color("ff777777"), Color("ff555555"), Color("ff444444"), Color("ff222222"), Color("ff111111")
+]
+
+
+
+# Public Methods
+# Reads vox file, returns voxel content and voxel palette
+static func read(vox_file : File) -> Dictionary:
+ var result := {
+ "error": OK,
+ "voxels": {},
+ "palette": [],
+ }
+
+ var magic := vox_file.get_buffer(4).get_string_from_ascii()
+ var magic_version := vox_file.get_32()
+ if magic == "VOX " and magic_version == 150:
+ var nodes := {}
+ while vox_file.get_position() < vox_file.get_len():
+ var chunk_name = vox_file.get_buffer(4).get_string_from_ascii()
+ var chunk_size = vox_file.get_32()
+ var chunk_children = vox_file.get_32()
+
+ match chunk_name:
+ "XYZI":
+ for i in range(0, vox_file.get_32()):
+ var x := vox_file.get_8()
+ var z := -vox_file.get_8()
+ var y := vox_file.get_8()
+ result["voxels"][Vector3(
+ x, y, z).floor()] = vox_file.get_8() - 1
+ "RGBA":
+ for i in range(0,256):
+ var color := Color(
+ float(vox_file.get_8() / 255.0),
+ float(vox_file.get_8() / 255.0),
+ float(vox_file.get_8() / 255.0),
+ float(vox_file.get_8() / 255.0))
+ color.a = 1.0
+ result["palette"].append(color)
+ _:
+ vox_file.get_buffer(chunk_size)
+
+ if result["palette"].empty():
+ result["palette"] = magicavoxel_default_palette.duplicate()
+ else:
+ result["error"] = ERR_FILE_UNRECOGNIZED
+
+ for index in range(result["palette"].size()):
+ result["palette"][index] = Voxel.colored(result["palette"][index])
+
+ return result
+
+
+static func read_file(vox_path : String) -> Dictionary:
+ var result := { "error": OK }
+ var vox_file := File.new()
+ var error = vox_file.open(vox_path, File.READ)
+ if error == OK:
+ result = read(vox_file)
+ if vox_file.is_open():
+ vox_file.close()
+ return result
diff --git a/addons/voxel-core/classes/voxel.gd b/addons/voxel-core/classes/voxel.gd
new file mode 100644
index 0000000..f9bc7bb
--- /dev/null
+++ b/addons/voxel-core/classes/voxel.gd
@@ -0,0 +1,330 @@
+tool
+class_name Voxel, "res://addons/voxel-core/assets/classes/voxel.png"
+extends Object
+# Utility class containing various properties and methods to do with voxels.
+#
+# Voxel Schema:
+# Every voxel is a Dictionary, not every Dictionary is a voxel, only by following
+# the voxel scheme indicated below can wide varieties of voxels be produced.
+# Note that voxel dictionaries may have additions, but they must be done in such
+# a way as to respect the original structure so as to avoid conflicts.
+#
+# {
+# name : String ~ Used in VoxelSets, should always be lowercase
+# color : Color ~ Default color used for all voxel faces, if not present fallback is Transparent color
+# colors : Dictionary = { ~ Color used on a per face bases, if not present fallback is voxel color
+# Vector3.UP : Color
+# Vector3.DOWN : Color
+# Vector3.RIGHT : Color
+# Vector3.LEFT : Color
+# Vector3.FORWARD : Color
+# Vector3.BACK : Color
+# }
+# uv : Vector2 ~ Default uv position used for all voxel faces, if not present fallback is (-1.0, -1.0)
+# uvs : Dictionary = { ~ uv position used on a per face bases, if not present fallback to voxel uv
+# Vector3.UP : Vector2
+# Vector3.DOWN : Vector2
+# Vector3.RIGHT : Vector2
+# Vector3.LEFT : Vector2
+# Vector3.FORWARD : Vector2
+# Vector3.BACK : Vector2
+# }
+# metallic : float ~ Metallic value used for all voxel face's material, must be between 0.0 and 1.0 and if not present fallback is 0.0
+# specular : float ~ Specular value used for all voxel faces, must be between 0.0 and 1.0 and if not present fallback is 0.5
+# roughness : float ~ Roughness value used for all voxel faces, must be between 0.0 and 1.0 and if not present fallback is 1.0
+# energy : float ~ Emission energy value used for all voxel faces, must be between 0.0 and 16.0 and if not present fallback is 0.0
+# energy_color : Color ~ Emission color used for all voxel faces, if not present fallback is white
+# material : int ~ ID of the VoxelSet material used for this voxel, if not present fallback is -1
+# }
+#
+
+
+
+## Constants
+# Lists of all voxel faces, and their adjacent directions
+const Faces := {
+ Vector3.RIGHT: [ Vector3.FORWARD, Vector3.BACK, Vector3.DOWN, Vector3.UP ],
+ Vector3.UP: [ Vector3.LEFT, Vector3.RIGHT, Vector3.FORWARD, Vector3.BACK ],
+ Vector3.FORWARD: [ Vector3.LEFT, Vector3.RIGHT, Vector3.DOWN, Vector3.UP ],
+ Vector3.LEFT: [ Vector3.FORWARD, Vector3.BACK, Vector3.DOWN, Vector3.UP ],
+ Vector3.DOWN: [ Vector3.LEFT, Vector3.RIGHT, Vector3.FORWARD, Vector3.BACK ],
+ Vector3.BACK: [ Vector3.LEFT, Vector3.RIGHT, Vector3.DOWN, Vector3.UP ],
+}
+
+# 0.5 means that voxels will have the dimensions of 0.5 x 0.5 x 0.5
+const VoxelWorldSize := 0.5
+
+
+
+## Public Methods
+# Returns Dictionary representation of a colored voxel
+# color : Color : color to set
+# colors : Dictionary<Vector3, Color> : face colors to set
+# return : Dictionary<String, Variant> : Dictionary representing voxel
+static func colored(color : Color, colors := {}) -> Dictionary:
+ var voxel = {}
+ voxel["color"] = color
+ if not colors.empty():
+ voxel["colors"] = colors.duplicate()
+ return voxel
+
+
+# Returns true if voxel has color defined
+static func has_color(voxel : Dictionary) -> bool:
+ return voxel.has("color")
+
+
+# Returns the defined color within given voxel if present, otherwise returns transparent color
+static func get_color(voxel : Dictionary) -> Color:
+ return voxel.get("color", Color.transparent)
+
+
+# Sets the given color to the given voxel
+static func set_color(voxel : Dictionary, color : Color) -> void:
+ voxel["color"] = color
+
+
+# Returns true if voxel has specified color at given face
+static func has_face_color(voxel : Dictionary, face : Vector3) -> bool:
+ return voxel.has("colors") and voxel["colors"].has(face)
+
+
+# Returns the defined color at given face if present, otherwise returns color
+static func get_face_color(voxel : Dictionary, face : Vector3) -> Color:
+ return voxel["colors"].get(face, get_color(voxel)) if voxel.has("colors") else get_color(voxel)
+
+
+# Sets the given color at the given face to the given voxel
+static func set_face_color(voxel : Dictionary, face : Vector3, color : Color) -> void:
+ if not voxel.has("colors"): voxel["colors"] = {}
+ voxel["colors"][face] = color
+
+
+# Removes color at given face from given voxel
+static func remove_face_color(voxel : Dictionary, face : Vector3) -> void:
+ if voxel.has("colors"):
+ voxel["colors"].erase(face)
+ if voxel["colors"].empty():
+ voxel.erase("colors")
+
+
+# Returns Dictionary representation of a uvd voxel
+# uv : Vector2 : uv to set
+# uvs : Dictionary<Vector3, Vector2> : face uv to set
+# color : Color : color to set
+# colors : Dictionary<Vector3, Color> : face colors to set
+# return : Dictionary<String, Variant> : Dictionary representing voxel
+static func uvd(uv : Vector2, uvs := {}, color := Color.white, colors := {}) -> Dictionary:
+ var voxel = colored(color, colors)
+ voxel["uv"] = uv
+ if not uvs.empty():
+ voxel["uvs"] = uvs
+ return voxel
+
+
+# Returns true if voxel has uv defined
+static func has_uv(voxel : Dictionary) -> bool:
+ return voxel.has("uv")
+
+
+# Returns the defined uv within given voxel if present, otherwise returns a negative vector
+static func get_uv(voxel : Dictionary) -> Vector2:
+ return voxel.get("uv", -Vector2.ONE)
+
+
+# Sets the given uv to the given voxel
+static func set_uv(voxel : Dictionary, uv : Vector2) -> void:
+ voxel["uv"] = uv
+
+
+# Removes uv from given voxel
+static func remove_uv(voxel : Dictionary) -> void:
+ voxel.erase("uv")
+
+
+# Returns true if voxel has specified uv at given face
+static func has_face_uv(voxel : Dictionary, face : Vector3) -> bool:
+ return voxel.has("uvs") and voxel["uvs"].has(face)
+
+
+# Returns the defined uv at given face if present, otherwise returns uv
+static func get_face_uv(voxel : Dictionary, face : Vector3) -> Vector2:
+ return voxel["uvs"].get(face, get_uv(voxel)) if voxel.has("uvs") else get_uv(voxel)
+
+
+# Sets the given uv at the given face to the given voxel
+static func set_face_uv(voxel : Dictionary, face : Vector3, uv : Vector2) -> void:
+ if not voxel.has("uvs"):
+ voxel["uvs"] = {}
+ voxel["uvs"][face] = uv
+
+
+# Removes uv at given face from given voxel
+static func remove_face_uv(voxel : Dictionary, face : Vector3) -> void:
+ if voxel.has("uvs"):
+ voxel["uvs"].erase(face)
+ if voxel["uvs"].empty():
+ voxel.erase("uvs")
+
+
+# Returns true if given name is valid
+static func is_valid_name(name : String) -> bool:
+ return not name.empty()
+
+
+# Returns the defined name within given voxel if present, otherwise returns an empty string
+static func get_name(voxel : Dictionary) -> String:
+ return voxel.get("name", "")
+
+
+# Sets the given name to the given voxel
+static func set_name(voxel : Dictionary, name : String) -> void:
+ voxel["name"] = name.to_lower()
+
+
+# Removes name from given voxel
+static func remove_name(voxel : Dictionary) -> void:
+ voxel.erase("name")
+
+
+# Returns the defined metallic within given voxel if present, otherwise returns 0
+static func get_metallic(voxel : Dictionary) -> float:
+ return voxel.get("metallic", 0.0)
+
+
+# Sets the given metallic to the given voxel
+static func set_metallic(voxel : Dictionary, metallic : float) -> void:
+ voxel["metallic"] = metallic
+
+
+# Removes metallic from given voxel
+static func remove_metallic(voxel : Dictionary) -> void:
+ voxel.erase("metallic")
+
+
+# Returns the defined specular within given voxel if present, otherwise returns 0.5
+static func get_specular(voxel : Dictionary) -> float:
+ return voxel.get("specular", 0.5)
+
+
+# Sets the given specular to the given voxel
+static func set_specular(voxel : Dictionary, specular : float) -> void:
+ voxel["specular"] = specular
+
+
+# Removes specular from given voxel
+static func remove_specular(voxel : Dictionary) -> void:
+ voxel.erase("specular")
+
+
+# Returns the defined roughness within given voxel if present, otherwise returns 1
+static func get_roughness(voxel : Dictionary) -> float:
+ return voxel.get("roughness", 1.0)
+
+
+# Sets the given roughness to the given voxel
+static func set_roughness(voxel : Dictionary, roughness : float) -> void:
+ voxel["roughness"] = roughness
+
+
+# Removes roughness from given voxel
+static func remove_roughness(voxel : Dictionary) -> void:
+ voxel.erase("roughness")
+
+
+# Returns the defined energy within given voxel if present, otherwise returns 0
+static func get_energy(voxel : Dictionary) -> float:
+ return voxel.get("energy", 0.0)
+
+
+# Sets the given energy to the given voxel
+static func set_energy(voxel : Dictionary, energy : float) -> void:
+ voxel["energy"] = energy
+
+
+# Removes energy from given voxel
+static func remove_energy(voxel : Dictionary) -> void:
+ voxel.erase("energy")
+
+
+# Returns the defined energy_color within given voxel if present, otherwise returns Color.white
+static func get_energy_color(voxel : Dictionary) -> Color:
+ return voxel.get("energy_color", Color.white)
+
+
+# Sets the given energy_color to the given voxel
+static func set_energy_color(voxel : Dictionary, energy_color : Color) -> void:
+ voxel["energy_color"] = energy_color
+
+
+# Removes energy_color from given voxel
+static func remove_energy_color(voxel : Dictionary) -> void:
+ voxel.erase("energy_color")
+
+
+# Returns the defined material within given voxel if present, otherwise returns -1
+static func get_material(voxel : Dictionary) -> int:
+ return voxel.get("material", -1)
+
+
+# Sets the given material to the given voxel
+static func set_material(voxel : Dictionary, material : int) -> void:
+ voxel["material"] = material
+
+
+# Removes material from given voxel
+static func remove_material(voxel : Dictionary) -> void:
+ voxel.erase("material")
+
+
+# Removes unnecessary properties of given voxel in accordance to Voxel schema
+static func clean(voxel : Dictionary) -> void:
+ if not is_valid_name(get_name(voxel)):
+ remove_name(voxel)
+
+ if get_uv(voxel) == get_uv({}):
+ remove_uv(voxel)
+
+ for face in Faces:
+ if get_face_color(voxel, face) == get_color({}):
+ remove_face_color(voxel, face)
+ if get_face_uv(voxel, face) == get_uv({}):
+ remove_face_uv(voxel, face)
+
+ if get_material(voxel) == get_material({}):
+ remove_material(voxel)
+
+ if get_metallic(voxel) == get_metallic({}):
+ remove_metallic(voxel)
+
+ if get_specular(voxel) == get_specular({}):
+ remove_specular(voxel)
+
+ if get_roughness(voxel) == get_roughness({}):
+ remove_roughness(voxel)
+
+ if get_energy(voxel) == get_energy({}):
+ remove_energy(voxel)
+
+ if get_energy_color(voxel) == get_energy_color({}):
+ remove_energy_color(voxel)
+
+
+# Returns the world position as snapped world position
+static func world_to_snapped(world : Vector3) -> Vector3:
+ return (world / VoxelWorldSize).floor() * VoxelWorldSize
+
+
+# Returns the snapped world position as voxel grid position
+static func snapped_to_grid(snapped : Vector3) -> Vector3:
+ return snapped / VoxelWorldSize
+
+
+# Returns world position as voxel grid position
+static func world_to_grid(world : Vector3) -> Vector3:
+ return snapped_to_grid(world_to_snapped(world))
+
+
+# Returns voxel grid position as snapped world position
+static func grid_to_snapped(grid : Vector3) -> Vector3:
+ return grid * VoxelWorldSize
diff --git a/addons/voxel-core/classes/voxel_mesh.gd b/addons/voxel-core/classes/voxel_mesh.gd
new file mode 100644
index 0000000..ae07549
--- /dev/null
+++ b/addons/voxel-core/classes/voxel_mesh.gd
@@ -0,0 +1,124 @@
+tool
+class_name VoxelMesh
+extends "res://addons/voxel-core/classes/voxel_object.gd"
+# The most basic voxel visualization object, for a moderate amount of voxels.
+
+
+
+## Private Variables
+# Used voxels, Dictionary<Vector3, int>
+var _voxels := {}
+
+
+
+## Built-In Virtual Methods
+func _get(property : String):
+ if property == "VOXELS":
+ return _voxels
+
+
+func _set(property : String, value):
+ if property == "VOXELS":
+ _voxels = value
+ return true
+ return false
+
+
+func _get_property_list():
+ var properties = []
+
+ properties.append({
+ "name": "VOXELS",
+ "type": TYPE_DICTIONARY,
+ "hint": PROPERTY_HINT_NONE,
+ "usage": PROPERTY_USAGE_STORAGE,
+ })
+
+ return properties
+
+
+
+## Public Methods
+func empty() -> bool:
+ return _voxels.empty()
+
+
+func set_voxel(grid : Vector3, voxel : int) -> void:
+ _voxels[grid] = voxel
+
+
+func set_voxels(voxels : Dictionary) -> void:
+ erase_voxels()
+ _voxels = voxels
+
+
+func get_voxel_id(grid : Vector3) -> int:
+ return _voxels.get(grid, -1)
+
+
+func get_voxels() -> Array:
+ return _voxels.keys()
+
+
+func erase_voxel(grid : Vector3) -> void:
+ _voxels.erase(grid)
+
+
+func erase_voxels() -> void:
+ _voxels.clear()
+
+
+func update_mesh() -> void:
+ if not _voxels.empty():
+ var vt := VoxelTool.new()
+ var materials := {}
+ if is_instance_valid(mesh) and mesh is ArrayMesh:
+ for index in get_surface_material_count():
+ var material := get_surface_material(index)
+ if is_instance_valid(material):
+ materials[mesh.surface_get_name(index)] = material
+
+ match MeshModes.NAIVE if edit_hint > 0 else mesh_mode:
+ MeshModes.GREEDY:
+ mesh = greed_volume(_voxels.keys(), vt)
+ _:
+ mesh = naive_volume(_voxels.keys(), vt)
+
+ for material_name in materials:
+ var material_index = mesh.surface_find_by_name(material_name)
+ if material_index > -1:
+ set_surface_material(material_index, materials[material_name])
+ else:
+ mesh = null
+ .update_mesh()
+
+
+func update_static_body() -> void:
+ var staticBody = get_node_or_null("StaticBody")
+
+ if (edit_hint >= 2 or static_body) and is_instance_valid(mesh):
+ if not is_instance_valid(staticBody):
+ staticBody = StaticBody.new()
+ staticBody.set_name("StaticBody")
+ add_child(staticBody)
+
+ var collisionShape
+ if staticBody.has_node("CollisionShape"):
+ collisionShape = staticBody.get_node("CollisionShape")
+ else:
+ collisionShape = CollisionShape.new()
+ collisionShape.set_name("CollisionShape")
+ staticBody.add_child(collisionShape)
+ collisionShape.shape = mesh.create_trimesh_shape()
+
+ if static_body and not staticBody.owner:
+ staticBody.set_owner(get_tree().get_edited_scene_root())
+ elif not static_body and staticBody.owner:
+ staticBody.set_owner(null)
+ if static_body and not collisionShape.owner:
+ collisionShape.set_owner(get_tree().get_edited_scene_root())
+ elif not static_body and staticBody.owner:
+ collisionShape.set_owner(null)
+ elif is_instance_valid(staticBody):
+ remove_child(staticBody)
+ staticBody.queue_free()
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
diff --git a/addons/voxel-core/classes/voxel_set.gd b/addons/voxel-core/classes/voxel_set.gd
new file mode 100644
index 0000000..d0bccac
--- /dev/null
+++ b/addons/voxel-core/classes/voxel_set.gd
@@ -0,0 +1,234 @@
+tool
+class_name VoxelSet, "res://addons/voxel-core/assets/classes/voxel_set.png"
+extends Resource
+# Library of Voxels used by VoxelObjects.
+
+
+
+## Signals
+# Emitted on request_refresh
+signal requested_refresh
+
+
+
+## Exported Variables
+# Size of each tile in tiles in pixels
+export var tile_size := Vector2(32.0, 32.0) setget set_tile_size
+
+# Texture used for tiles / uv mapping
+export var tiles : Texture = null setget set_tiles
+
+# Materials used by voxels
+export(Array, Material) var materials := [
+ SpatialMaterial.new(),
+] setget set_materials
+
+
+
+## Private Variables
+# Voxels stored by their id
+var _voxels := []
+
+# Flag indicating whether _uv_scale, tile_size and tiles texture is set
+var _uv_ready := false
+
+# World UV Scale, calculated on request_refresh
+var _uv_scale := Vector2.ONE
+
+
+
+## Built-In Virtual Methods
+func _get(property : String):
+ if property == "VOXELS":
+ return _voxels
+
+
+func _set(property : String, value) -> bool:
+ if property == "VOXELS":
+ if typeof(value) == TYPE_DICTIONARY:
+ for key in value:
+ var voxel : Dictionary = value[key]
+ if voxel.has("vsn"):
+ Voxel.set_name(voxel, voxel["vsn"])
+ voxel.erase("vsn")
+ _voxels.append(value[key])
+ else:
+ _voxels = value
+ return true
+ return false
+
+
+func _get_property_list():
+ var properties = []
+
+ properties.append({
+ "name": "VOXELS",
+ "type": TYPE_ARRAY,
+ "hint": PROPERTY_HINT_NONE,
+ "usage": PROPERTY_USAGE_STORAGE,
+ })
+
+ return properties
+
+
+
+## Public Methods
+# Sets tile_size, calls on request_refresh by default
+func set_tile_size(value : Vector2, refresh := true) -> void:
+ tile_size = Vector2(
+ floor(clamp(value.x, 1, 256)),
+ floor(clamp(value.y, 1, 256)))
+
+ if refresh:
+ request_refresh()
+
+
+# Sets tiles, calls on request_refresh by default
+func set_tiles(value : Texture, refresh := true) -> void:
+ tiles = value
+
+ if refresh:
+ request_refresh()
+
+
+# Sets materials used by voxels in VoxelSet
+func set_materials(values : Array, refresh := true) -> void:
+ for index in range(values.size()):
+ var material = values[index]
+ if is_instance_valid(material) and not (material is SpatialMaterial or material is ShaderMaterial):
+ printerr("VoxelSet : Expected Spatial or Shader material got " + str(material))
+ values[index] = null
+
+ if values.empty():
+ materials.resize(1)
+ else:
+ materials = values
+ property_list_changed_notify()
+
+ if refresh:
+ request_refresh()
+
+
+# Returns VoxelSet material with id if present, otherwise returns null
+func get_material(id : int) -> Material:
+ return materials[id] if id > -1 and id < materials.size() else null
+
+
+# Returns number of voxels in VoxelSet
+func size() -> int:
+ return _voxels.size()
+
+
+# Returns true if VoxelSet has no voxels
+func empty() -> bool:
+ return _voxels.empty()
+
+
+# Returns true if VoxelSet has voxel with given id
+func has_id(id : int) -> bool:
+ return id > -1 and id < _voxels.size()
+
+
+# Returns true if VoxelSet has everything necessary for uv mapping
+func uv_ready() -> bool:
+ return _uv_ready
+
+
+# Returns the uv scale
+func uv_scale() -> Vector2:
+ return _uv_scale
+
+
+# Returns true if given id is valid
+static func is_valid_id(id : int) -> bool:
+ return id > -1
+
+
+# Returns a list of all the voxel ids
+# returns : Array<int> : contained voxel ids
+func get_ids() -> Array:
+ return range(_voxels.size())
+
+
+# Returns name associated with the given id, returns a empty string if id isn't found
+func id_to_name(id : int) -> String:
+ return Voxel.get_name(get_voxel(id))
+
+
+# Returns the id of the voxel with the given name, returns -1 if not found
+func name_to_id(name : String) -> int:
+ name = name.to_lower()
+ for id in get_ids():
+ if id_to_name(id) == name:
+ return id
+ return -1
+
+
+# Appends voxel to end of VoxelSet
+func add_voxel(voxel : Dictionary) -> void:
+ _voxels.append(voxel)
+
+
+# Sets the voxel at given id
+func set_voxel(id : int, voxel : Dictionary) -> void:
+ if not has_id(id):
+ printerr("VoxelSet : given id `" + str(id) + "` is out of range")
+ return
+
+ _voxels[id] = voxel
+
+
+# Insert voxel with given id
+func insert_voxel(id : int, voxel : Dictionary) -> void:
+ if id < -1 and id > _voxels.size():
+ printerr("VoxelSet : given id `" + str(id) + "` is out of range")
+ return
+
+ _voxels.insert(id, voxel)
+
+
+# Replaces all _voxels
+func set_voxels(voxels : Array) -> void:
+ _voxels = voxels
+
+
+# Gets voxel Dictionary by their id, returns an empty Dictionary if not found
+func get_voxel(id : int) -> Dictionary:
+ return _voxels[id] if has_id(id) else {}
+
+
+# Erase voxel from VoxelSet
+func erase_voxel(id : int) -> void:
+ _voxels.remove(id)
+
+
+# Erases all voxels in VoxelSet
+func erase_voxels() -> void:
+ _voxels.clear()
+
+
+# Should be called when noticable changes have been committed to voxels.
+# Emits requested_refresh, calculates _uv_scale and updates _uv_ready
+func request_refresh() -> void:
+ _uv_ready = is_instance_valid(tiles)
+ if _uv_ready:
+ _uv_scale = Vector2.ONE / (tiles.get_size() / tile_size)
+ else:
+ _uv_scale = Vector2.ONE
+ emit_signal("requested_refresh")
+
+
+# Loads file's content as voxels
+# NOTE: Reference Reader.gd for valid file imports
+# source_file : String : Path to file to be loaded
+# return int : int : Error code
+func load_file(source_file : String, append := false) -> int:
+ var read := Reader.read_file(source_file)
+ var error : int = read.get("error", FAILED)
+ if error == OK:
+ if append:
+ for voxel in read["palette"]:
+ add_voxel(voxel)
+ else:
+ set_voxels(read["palette"])
+ return error
diff --git a/addons/voxel-core/classes/voxel_tool.gd b/addons/voxel-core/classes/voxel_tool.gd
new file mode 100644
index 0000000..bba14aa
--- /dev/null
+++ b/addons/voxel-core/classes/voxel_tool.gd
@@ -0,0 +1,227 @@
+tool
+class_name VoxelTool
+extends Reference
+# Used to construct a Mesh with provided VoxelSet
+# and by specifying voxel faces individually.
+
+
+
+class Surface:
+ ## Public Variables
+ # Index of the last vertex in Mesh being constructed
+ var index : int
+
+ var material : SpatialMaterial
+
+ # SurfaceTool used to construct Mesh
+ var surface_tool : SurfaceTool
+
+
+
+ ## Built-In Virtual Methods
+ func _init() -> void:
+ index = 0
+ surface_tool = SurfaceTool.new()
+ surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES)
+
+
+
+## Private Variables
+# Flag indicating whether uv mapping should be applied to constructed mesh
+var _uv_voxels := false
+
+# Contains Surfaces being constructed
+var _surfaces := {}
+
+# VoxelSet used when constructing Mesh, is set on begin
+var _voxel_set : VoxelSet = null
+
+
+
+## Public Methods
+# Called before constructing mesh, takes the VoxelSet with which Mesh will be constructed
+func begin(voxel_set : VoxelSet = null, uv_voxels := false) -> void:
+ clear()
+ _uv_voxels = uv_voxels and voxel_set.uv_ready()
+ _voxel_set = voxel_set
+
+
+# Clear all information
+func clear() -> void:
+ _uv_voxels = false
+ _surfaces.clear()
+ _voxel_set = null
+
+
+# Returns a constructed ArrayMesh
+func commit() -> ArrayMesh:
+ var mesh := ArrayMesh.new()
+ for surface_id in _surfaces:
+ var surface : Surface = _surfaces[surface_id]
+ var submesh = surface.surface_tool.commit_to_arrays()
+ mesh.add_surface_from_arrays(
+ Mesh.PRIMITIVE_TRIANGLES,
+ submesh)
+ mesh.surface_set_name(mesh.get_surface_count() - 1, surface_id)
+ mesh.surface_set_material(mesh.get_surface_count() - 1, surface.material)
+ clear()
+ return mesh
+
+
+# Adds a voxel face to Mesh with given vertex positions and voxel data
+# voxel : Dictioanry<String, Variant> : voxel data to use
+# face : Vector3 : face of voxel to generate
+# bottom_right : Vector3 : grid position of bottom right vertex pertaining to face
+# bottom_left : Vector3 : grid position of bottom left vertex pertaining to face, if not given botttom right is used
+# top_right : Vector3 : grid position of top right vertex pertaining to face, if not given botttom right is used
+# top_left : Vector3 : grid position of top left vertex pertaining to face, if not given botttom right is used
+func add_face(
+ voxel : Dictionary,
+ face : Vector3,
+ bottom_right : Vector3,
+ bottom_left := Vector3.INF,
+ top_right := Vector3.INF,
+ top_left := Vector3.INF) -> void:
+ bottom_right = bottom_right
+ if bottom_left == Vector3.INF: bottom_left = bottom_right
+ if top_right == Vector3.INF: top_right = bottom_right
+ if top_left == Vector3.INF: top_left = bottom_right
+
+ var color := Voxel.get_face_color(voxel, face)
+ var uv := Voxel.get_face_uv(voxel, face) if _uv_voxels else -Vector2.ONE
+ var uv_surface := uv != -Vector2.ONE
+
+ var material := Voxel.get_material(voxel)
+
+ var metal := Voxel.get_metallic(voxel)
+ var specular := Voxel.get_specular(voxel)
+ var rough := Voxel.get_roughness(voxel)
+ var energy := Voxel.get_energy(voxel)
+ var energy_color := Voxel.get_energy_color(voxel)
+
+ var surface_id := str(material) if material > -1 else (str(metal) + "," + str(specular) + "," + str(rough) + "," + str(energy) + "," + str(energy_color))
+ if uv_surface:
+ surface_id += "_uv"
+ var surface : Surface = _surfaces.get(surface_id)
+ if not is_instance_valid(surface):
+ surface = Surface.new()
+
+ surface.material = _voxel_set.get_material(material) if is_instance_valid(_voxel_set) else null
+ if is_instance_valid(surface.material):
+ surface.material = surface.material.duplicate()
+ else:
+ surface.material = SpatialMaterial.new()
+
+ surface.material.metallic = metal
+ surface.material.metallic_specular = specular
+ surface.material.roughness = rough
+ if energy > 0.0:
+ surface.material.emission_enabled = true
+ surface.material.emission = energy_color
+ surface.material.emission_energy = energy
+
+ if surface.material is SpatialMaterial:
+ surface.material.vertex_color_use_as_albedo = true
+ if uv_surface:
+ surface.material.albedo_texture = _voxel_set.tiles
+
+ _surfaces[surface_id] = surface
+
+ surface.surface_tool.add_normal(face)
+ surface.surface_tool.add_color(color)
+
+ match face:
+ Vector3.RIGHT:
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.RIGHT) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_left + Vector3.RIGHT + Vector3.UP) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.ONE) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_left + Vector3.RIGHT) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_right + Vector3.ONE) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.DOWN) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_right + Vector3.RIGHT + Vector3.BACK) * Voxel.VoxelWorldSize)
+ Vector3.LEFT:
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.DOWN) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_left) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_left + Vector3.UP) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.ONE) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_right + Vector3.BACK) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.RIGHT) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_right + Vector3.UP + Vector3.BACK) * Voxel.VoxelWorldSize)
+ Vector3.UP:
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.DOWN) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_left + Vector3.UP + Vector3.BACK) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_left + Vector3.UP) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.ONE) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_right + Vector3.ONE) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.RIGHT) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_right + Vector3.RIGHT + Vector3.UP) * Voxel.VoxelWorldSize)
+ Vector3.DOWN:
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.DOWN) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_right + Vector3.RIGHT + Vector3.BACK) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_right + Vector3.RIGHT) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.ONE) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_left + Vector3.BACK) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.RIGHT) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_left) * Voxel.VoxelWorldSize)
+ Vector3.FORWARD:
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.ONE) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_right + Vector3.RIGHT) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.RIGHT) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_right + Vector3.RIGHT + Vector3.UP) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.DOWN) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_left) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_left + Vector3.UP) * Voxel.VoxelWorldSize)
+ Vector3.BACK:
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.RIGHT) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_right + Vector3.ONE) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.ONE) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_right + Vector3.RIGHT + Vector3.BACK) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((top_left + Vector3.UP + Vector3.BACK) * Voxel.VoxelWorldSize)
+ if uv_surface:
+ surface.surface_tool.add_uv((uv + Vector2.DOWN) * _voxel_set.uv_scale())
+ surface.surface_tool.add_vertex((bottom_left + Vector3.BACK) * Voxel.VoxelWorldSize)
+
+ surface.index += 4
+ surface.surface_tool.add_index(surface.index - 4)
+ surface.surface_tool.add_index(surface.index - 3)
+ surface.surface_tool.add_index(surface.index - 2)
+ surface.surface_tool.add_index(surface.index - 3)
+ surface.surface_tool.add_index(surface.index - 1)
+ surface.surface_tool.add_index(surface.index - 2)
+
+
+# Adds all the faces of a voxel to Mesh at given position and with voxel data
+# voxel : Dictioanry<String, Variant> : voxel data to use
+# grid : Vector3 : voxel grid position of voxel
+func add_faces(voxel : Dictionary, grid : Vector3) -> void:
+ for face in Voxel.Faces:
+ add_face(voxel, face, grid)