From c9c5b5d7c2a238310ce7bde336f76d2d1b6f9f29 Mon Sep 17 00:00:00 2001 From: jacopograndi Date: Sat, 15 Jan 2022 16:29:02 +0100 Subject: moved shapes to loading area & fixed asset paths --- addons/voxel-core/classes/reader.gd | 21 ++ addons/voxel-core/classes/readers/gpl.gd | 48 +++ addons/voxel-core/classes/readers/image.gd | 40 +++ addons/voxel-core/classes/readers/vox.gd | 86 +++++ addons/voxel-core/classes/voxel.gd | 330 +++++++++++++++++++ addons/voxel-core/classes/voxel_mesh.gd | 124 +++++++ addons/voxel-core/classes/voxel_object.gd | 501 +++++++++++++++++++++++++++++ addons/voxel-core/classes/voxel_set.gd | 234 ++++++++++++++ addons/voxel-core/classes/voxel_tool.gd | 227 +++++++++++++ 9 files changed, 1611 insertions(+) create mode 100644 addons/voxel-core/classes/reader.gd create mode 100644 addons/voxel-core/classes/readers/gpl.gd create mode 100644 addons/voxel-core/classes/readers/image.gd create mode 100644 addons/voxel-core/classes/readers/vox.gd create mode 100644 addons/voxel-core/classes/voxel.gd create mode 100644 addons/voxel-core/classes/voxel_mesh.gd create mode 100644 addons/voxel-core/classes/voxel_object.gd create mode 100644 addons/voxel-core/classes/voxel_set.gd create mode 100644 addons/voxel-core/classes/voxel_tool.gd (limited to 'addons/voxel-core/classes') 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, palette : Dictionary } +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 : face colors to set +# return : Dictionary : 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 : face uv to set +# color : Color : color to set +# colors : Dictionary : face colors to set +# return : Dictionary : 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 +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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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 : 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) -- cgit v1.2.3-54-g00ecf