aboutsummaryrefslogtreecommitdiff
path: root/assets/addons/voxel-core/classes/voxel_object.gd
blob: 8d126338366fd3e73165995404cea0c2714f02a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
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