Added automatic procedural level of detail to planets
This commit is contained in:
56
godot/Scripts/PlanetGeneration/PlanetData.gd
Normal file
56
godot/Scripts/PlanetGeneration/PlanetData.gd
Normal file
@ -0,0 +1,56 @@
|
||||
@tool
|
||||
|
||||
extends Resource
|
||||
|
||||
class_name PlanetData
|
||||
|
||||
@export var radius : float = 50:
|
||||
get:
|
||||
return radius
|
||||
set(value):
|
||||
radius = value
|
||||
emit_signal("changed")
|
||||
|
||||
@export var noise_map : FastNoiseLite = FastNoiseLite.new():
|
||||
get:
|
||||
return noise_map
|
||||
set(value):
|
||||
noise_map = value
|
||||
emit_signal("changed")
|
||||
if noise_map != null and not noise_map.is_connected("changed", on_data_changed):
|
||||
noise_map.connect("changed", on_data_changed)
|
||||
|
||||
@export var amplitude : float = 0.1:
|
||||
get:
|
||||
return amplitude
|
||||
set(value):
|
||||
amplitude = value
|
||||
emit_signal("changed")
|
||||
|
||||
var min_elevation := 99999.0
|
||||
var max_elevation := 0.0
|
||||
|
||||
@export var planet_color : GradientTexture1D:
|
||||
get:
|
||||
return planet_color
|
||||
set(value):
|
||||
planet_color = value
|
||||
emit_signal("changed")
|
||||
if planet_color != null and not planet_color.is_connected("changed", on_data_changed):
|
||||
planet_color.connect("changed", on_data_changed)
|
||||
|
||||
@export var flatness : float = 6:
|
||||
get:
|
||||
return flatness
|
||||
set(value):
|
||||
flatness = value
|
||||
emit_signal("changed")
|
||||
|
||||
func on_data_changed():
|
||||
emit_signal("changed")
|
||||
|
||||
func point_on_planet(point_on_sphere : Vector3) -> Vector3:
|
||||
assert(noise_map != null, "Noise map not set")
|
||||
var elevation = noise_map.get_noise_3dv(point_on_sphere) / flatness
|
||||
elevation = elevation/2 + 1
|
||||
return point_on_sphere * radius * elevation
|
39
godot/Scripts/PlanetGeneration/PlanetGeneration.gd
Normal file
39
godot/Scripts/PlanetGeneration/PlanetGeneration.gd
Normal file
@ -0,0 +1,39 @@
|
||||
@tool
|
||||
|
||||
extends Node3D
|
||||
class_name Planet
|
||||
|
||||
@export var planet_data : Resource = PlanetData.new():
|
||||
get:
|
||||
return planet_data
|
||||
set(value):
|
||||
planet_data = value
|
||||
on_data_changed()
|
||||
if planet_data != null and not planet_data.is_connected("changed", on_data_changed):
|
||||
planet_data.connect("changed", on_data_changed)
|
||||
|
||||
func _ready():
|
||||
planet_data = PlanetData.new()
|
||||
planet_data.noise_map = FastNoiseLite.new()
|
||||
planet_data.noise_map.set_noise_type(FastNoiseLite.NoiseType.TYPE_SIMPLEX)
|
||||
planet_data.noise_map.frequency = 1
|
||||
planet_data.noise_map.fractal_octaves = 10
|
||||
planet_data.planet_color = load("res://Earth-LikeGradient.tres")
|
||||
on_data_changed()
|
||||
|
||||
func on_data_changed():
|
||||
var i : int = 0
|
||||
for child in get_children():
|
||||
if i < 6:
|
||||
(child as PlanetMeshFace).regenerate_mesh(planet_data)
|
||||
else:
|
||||
(child as CSGSphere3D).radius = planet_data.radius
|
||||
i += 1
|
||||
|
||||
func _process(delta):
|
||||
var i : int = 0
|
||||
for child in get_children():
|
||||
if i < 6:
|
||||
var face := child as PlanetMeshFace
|
||||
face.update_from_camera_pos(planet_data)
|
||||
i += 1
|
7
godot/Scripts/PlanetGeneration/PlanetMeshFace.gd
Normal file
7
godot/Scripts/PlanetGeneration/PlanetMeshFace.gd
Normal file
@ -0,0 +1,7 @@
|
||||
@tool
|
||||
|
||||
extends QuadTreeNode
|
||||
class_name PlanetMeshFace
|
||||
|
||||
func _ready():
|
||||
self.is_root = true
|
184
godot/Scripts/PlanetGeneration/QuadTreeNode.gd
Normal file
184
godot/Scripts/PlanetGeneration/QuadTreeNode.gd
Normal file
@ -0,0 +1,184 @@
|
||||
extends Node
|
||||
class_name QuadTreeNode
|
||||
|
||||
var is_root : bool = false:
|
||||
get:
|
||||
return is_root
|
||||
set(value):
|
||||
is_root = value
|
||||
|
||||
var is_leaf : bool = true:
|
||||
get:
|
||||
return is_leaf
|
||||
set(value):
|
||||
is_leaf = value
|
||||
|
||||
var level : int
|
||||
var radius : float = 1.0
|
||||
|
||||
var children : Array
|
||||
|
||||
var mesh_instance : MeshInstance3D = MeshInstance3D.new()
|
||||
|
||||
var material = load("res://PlanetColoring.tres")
|
||||
|
||||
@export var normal : Vector3
|
||||
@export var center : Vector2
|
||||
|
||||
var center_of_mass : Vector3
|
||||
|
||||
const THRESHOLDS = [512*512, 128*128, 64*64, 32*32]
|
||||
const RESOLUTION = 17
|
||||
|
||||
func split(planet_data : PlanetData):
|
||||
remove_child(mesh_instance)
|
||||
mesh_instance = null
|
||||
|
||||
children[0] = QuadTreeNode.new()
|
||||
children[0].center = center + Vector2(radius/2, radius/2)
|
||||
|
||||
children[1] = QuadTreeNode.new()
|
||||
children[1].center = center + Vector2(-radius/2, radius/2)
|
||||
|
||||
children[2] = QuadTreeNode.new()
|
||||
children[2].center = center + Vector2(radius/2, -radius/2)
|
||||
|
||||
children[3] = QuadTreeNode.new()
|
||||
children[3].center = center + Vector2(-radius/2, -radius/2)
|
||||
|
||||
for child in children:
|
||||
call_deferred("add_child", child)
|
||||
child.level = level + 1
|
||||
child.radius = radius/2
|
||||
child.normal = normal
|
||||
child.material = material
|
||||
child.mesh_instance = MeshInstance3D.new()
|
||||
child.regenerate_mesh(planet_data)
|
||||
child.call_deferred("add_child", child.mesh_instance)
|
||||
|
||||
self.is_leaf = false
|
||||
|
||||
func join():
|
||||
for child in children:
|
||||
if (child != null):
|
||||
child.join()
|
||||
child.free()
|
||||
child = null
|
||||
self.is_leaf = true
|
||||
mesh_instance = MeshInstance3D.new()
|
||||
call_deferred("add_child", mesh_instance)
|
||||
|
||||
|
||||
func update_from_camera_pos(planet_data : PlanetData, camera : Camera3D = null):
|
||||
if (camera == null):
|
||||
camera = get_viewport().get_camera_3d()
|
||||
if (camera == null):
|
||||
return
|
||||
var camera_pos = camera.global_position
|
||||
|
||||
var dist = (camera_pos - center_of_mass).length_squared()
|
||||
|
||||
if (self.level < (THRESHOLDS.size() - 1) and dist < THRESHOLDS[self.level] and self.is_leaf):
|
||||
split(planet_data)
|
||||
elif (dist > THRESHOLDS[self.level] + 4 and not self.is_leaf):
|
||||
join()
|
||||
regenerate_mesh(planet_data)
|
||||
|
||||
if (children[0] != null):
|
||||
for child in children:
|
||||
child.update_from_camera_pos(planet_data, camera)
|
||||
|
||||
func regenerate_mesh(planet_data):
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
|
||||
var vertex_array := PackedVector3Array()
|
||||
var uv_array := PackedVector2Array()
|
||||
var normal_array := PackedVector3Array()
|
||||
|
||||
var index_array := PackedInt32Array()
|
||||
var num_vertices : int = RESOLUTION * RESOLUTION
|
||||
var num_indices : int = (RESOLUTION-1) * (RESOLUTION-1) * 6
|
||||
|
||||
vertex_array.resize(num_vertices)
|
||||
normal_array.resize(num_vertices)
|
||||
uv_array.resize(num_vertices)
|
||||
index_array.resize(num_indices)
|
||||
|
||||
var tri_index : int = 0
|
||||
var a_axis := Vector3(normal.y, normal.z, normal.x)
|
||||
var b_axis : Vector3 = normal.cross(a_axis)
|
||||
for y in range(RESOLUTION):
|
||||
for x in range(RESOLUTION):
|
||||
var i : int = x + y*RESOLUTION
|
||||
|
||||
# Relative position of the current point inside the tile
|
||||
var percent := Vector2(x,y) / (RESOLUTION-1)
|
||||
|
||||
# Calculate the point on a cube, normalize it to a sphere, and map it to a point on the planet's surface.
|
||||
var point_on_cube : Vector3 = normal + (center.x + (percent.x - 0.5)*2.0*radius)*a_axis + (center.y + (percent.y - 0.5)*2.0*radius)*b_axis
|
||||
var point_on_sphere = point_on_cube.normalized()
|
||||
var point_on_planet = planet_data.point_on_planet(point_on_sphere)
|
||||
|
||||
# Update the max and min elevations of the planet's surface.
|
||||
var elevation = point_on_planet.length()
|
||||
if (elevation < planet_data.min_elevation):
|
||||
planet_data.min_elevation = elevation
|
||||
if (elevation > planet_data.max_elevation):
|
||||
planet_data.max_elevation = elevation
|
||||
|
||||
# Calculate center of mass of current tile.
|
||||
if (percent.x == 0.5 and percent.y == 0.5):
|
||||
center_of_mass = point_on_planet
|
||||
|
||||
vertex_array[i] = point_on_planet
|
||||
if x != RESOLUTION-1 and y != RESOLUTION-1:
|
||||
index_array[tri_index + 2] = i
|
||||
index_array[tri_index + 1] = i+RESOLUTION+1
|
||||
index_array[tri_index] = i+RESOLUTION
|
||||
|
||||
index_array[tri_index + 5] = i
|
||||
index_array[tri_index + 4] = i+1
|
||||
index_array[tri_index + 3] = i+RESOLUTION+1
|
||||
tri_index += 6
|
||||
|
||||
for a in range(0, index_array.size(), 3):
|
||||
var b : int = a + 1
|
||||
var c : int = a + 2
|
||||
|
||||
var ab : Vector3 = vertex_array[index_array[b]] - vertex_array[index_array[a]]
|
||||
var bc : Vector3 = vertex_array[index_array[c]] - vertex_array[index_array[b]]
|
||||
var ca : Vector3 = vertex_array[index_array[a]] - vertex_array[index_array[c]]
|
||||
|
||||
var cross_ab_bc : Vector3 = bc.cross(ab)
|
||||
var cross_bc_ca : Vector3 = ca.cross(bc)
|
||||
var cross_ca_ab : Vector3 = ab.cross(ca)
|
||||
|
||||
normal_array[index_array[a]] += cross_ab_bc + cross_bc_ca + cross_ca_ab
|
||||
normal_array[index_array[b]] += cross_ab_bc + cross_bc_ca + cross_ca_ab
|
||||
normal_array[index_array[c]] += cross_ab_bc + cross_bc_ca + cross_ca_ab
|
||||
|
||||
for i in range(normal_array.size()):
|
||||
normal_array[i] = normal_array[i].normalized()
|
||||
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertex_array
|
||||
arrays[Mesh.ARRAY_NORMAL] = normal_array
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uv_array
|
||||
arrays[Mesh.ARRAY_INDEX] = index_array
|
||||
|
||||
call_deferred("_update_mesh", arrays, planet_data)
|
||||
|
||||
func _update_mesh(arrays : Array, planet_data):
|
||||
var _mesh := ArrayMesh.new()
|
||||
_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
if (mesh_instance == null):
|
||||
return
|
||||
|
||||
mesh_instance.mesh = _mesh
|
||||
mesh_instance.material_override = material
|
||||
mesh_instance.material_override.set_shader_parameter("min_elevation", planet_data.min_elevation - 0.2)
|
||||
mesh_instance.material_override.set_shader_parameter("max_elevation", planet_data.max_elevation)
|
||||
mesh_instance.material_override.set_shader_parameter("elevation_color", planet_data.planet_color)
|
||||
|
||||
func _init():
|
||||
children.resize(4)
|
20
godot/Scripts/Player/Camera3D.gd
Normal file
20
godot/Scripts/Player/Camera3D.gd
Normal file
@ -0,0 +1,20 @@
|
||||
extends Camera3D
|
||||
|
||||
@export var velocity: Vector3
|
||||
@export var acceleration: Vector3
|
||||
var g : float = 0.01
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
global_position = Vector3(0,0,-150)
|
||||
acceleration = -global_position.normalized()
|
||||
velocity = Vector3(0.5, 0.5, 0)
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta):
|
||||
acceleration = -global_position.normalized() * g
|
||||
velocity += acceleration
|
||||
global_translate(velocity)
|
||||
Global.camera_position = global_position
|
||||
look_at(Vector3(0,0,0))
|
Reference in New Issue
Block a user