Added automatic procedural level of detail to planets

This commit is contained in:
Adog64 2024-02-29 09:00:00 -05:00
parent c2399094c4
commit ec869732ba
14 changed files with 326 additions and 237 deletions

View File

@ -0,0 +1,8 @@
[gd_resource type="GradientTexture1D" load_steps=2 format=3 uid="uid://0evqagsrs13i"]
[sub_resource type="Gradient" id="Gradient_xk23k"]
offsets = PackedFloat32Array(0.431734, 0.468635, 0.527675, 0.665505, 0.763066, 0.938224)
colors = PackedColorArray(0.197937, 0.197937, 0.197937, 1, 0.797212, 0.660532, 0.353345, 1, 0.063932, 0.201311, 0.030396, 1, 0.128088, 0.234932, 0.104546, 1, 0.309235, 0.329862, 0.313911, 1, 1, 1, 1, 1)
[resource]
gradient = SubResource("Gradient_xk23k")

13
godot/Global.gd Normal file
View File

@ -0,0 +1,13 @@
@tool
extends Node
var camera_position : Vector3
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass

View File

@ -0,0 +1,7 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://b6k6wbhtpun32"]
[ext_resource type="Shader" path="res://Shaders/Planet/PlanetShading.gdshader" id="1_cymf1"]
[resource]
render_priority = 0
shader = ExtResource("1_cymf1")

File diff suppressed because one or more lines are too long

View File

@ -1,77 +0,0 @@
@tool
extends MeshInstance3D
class_name PlantetMeshFace
@export var normal : Vector3
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 resolution : int = planet_data.resolution
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
var percent := Vector2(x,y) / (resolution-1)
var point_on_cube : Vector3 = normal + (percent.x - 0.5)*2.0*a_axis + (percent.y - 0.5)*2.0*b_axis
var point_on_sphere = point_on_cube.normalized()
var point_on_planet = planet_data.point_on_planet(point_on_sphere)
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)
func _update_mesh(arrays : Array):
var _mesh := ArrayMesh.new()
_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
self.mesh = _mesh

View File

@ -1,14 +0,0 @@
shader_type spatial;
void vertex() {
// Called for every vertex the material is visible on.
}
void fragment() {
// Called for every pixel the material is visible on.
}
//void light() {
// Called for every pixel for every light affecting the material.
// Uncomment to replace the default light processing function with this one.
//}

View File

@ -4,20 +4,13 @@ extends Resource
class_name PlanetData
@export var radius : float = 1:
@export var radius : float = 50:
get:
return radius
set(value):
radius = value
emit_signal("changed")
@export var resolution : int = 50:
get:
return resolution
set(value):
resolution = value
emit_signal("changed")
@export var noise_map : FastNoiseLite = FastNoiseLite.new():
get:
return noise_map
@ -27,19 +20,24 @@ class_name PlanetData
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 = 1.0:
@export var amplitude : float = 0.1:
get:
return amplitude
set(value):
amplitude = value
emit_signal("changed")
@export var min_height : float = 0.9:
var min_elevation := 99999.0
var max_elevation := 0.0
@export var planet_color : GradientTexture1D:
get:
return min_height
return planet_color
set(value):
min_height = 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:
@ -53,6 +51,6 @@ func on_data_changed():
func point_on_planet(point_on_sphere : Vector3) -> Vector3:
assert(noise_map != null, "Noise map not set")
var elevation = 1 / (1 + exp(noise_map.get_noise_3dv(point_on_sphere) * -flatness))
elevation = max(0.0, (elevation + 1)/2 - min_height)
return point_on_sphere * radius * (elevation + 1)
var elevation = noise_map.get_noise_3dv(point_on_sphere) / flatness
elevation = elevation/2 + 1
return point_on_sphere * radius * elevation

View File

@ -1,6 +1,7 @@
@tool
extends Node3D
class_name Planet
@export var planet_data : Resource = PlanetData.new():
get:
@ -16,9 +17,23 @@ func _ready():
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():
var face := child as PlantetMeshFace
face.regenerate_mesh(planet_data)
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

View File

@ -0,0 +1,7 @@
@tool
extends QuadTreeNode
class_name PlanetMeshFace
func _ready():
self.is_root = true

View 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)

View File

@ -1,19 +1,20 @@
extends Camera3D
var velocity: Vector3
var acceleration: Vector3
var g : float = 0.1
@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.05, 0, 0)
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 / global_position.length_squared()
acceleration = -global_position.normalized() * g
velocity += acceleration
global_translate(velocity)
Global.camera_position = global_position
look_at(Vector3(0,0,0))

View File

@ -0,0 +1,18 @@
shader_type spatial;
uniform float min_elevation;
uniform float max_elevation;
uniform sampler2D elevation_color;
varying float height;
void vertex() {
height = length(VERTEX);
}
void fragment() {
float t = height/(max_elevation-min_elevation)- (min_elevation / (max_elevation-min_elevation));
vec3 color = texture(elevation_color, vec2(t, 0.0)).rgb;
ALBEDO = color;
}

View File

@ -1,9 +1,10 @@
[gd_scene load_steps=10 format=3 uid="uid://b4ssaqxtmkfpq"]
[gd_scene load_steps=11 format=3 uid="uid://b4ssaqxtmkfpq"]
[ext_resource type="PackedScene" uid="uid://clkrjexk1mje0" path="res://PlanetGeneration.tscn" id="1_65in3"]
[ext_resource type="Texture2D" uid="uid://osf4dpcksyxa" path="res://cc71d02a-1e5a-4aa3-a8f2-44f262a17d38_scaled.jpg" id="1_vooe1"]
[ext_resource type="Script" path="res://Camera3D.gd" id="2_4ip6j"]
[ext_resource type="Script" path="res://PlanetData.gd" id="2_gy4ke"]
[ext_resource type="Script" path="res://Scripts/Player/Camera3D.gd" id="2_4ip6j"]
[ext_resource type="Script" path="res://Scripts/PlanetGeneration/PlanetData.gd" id="2_gy4ke"]
[ext_resource type="Texture2D" uid="uid://0evqagsrs13i" path="res://Earth-LikeGradient.tres" id="3_orhq2"]
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_vl6wy"]
panorama = ExtResource("1_vooe1")
@ -21,18 +22,17 @@ ambient_light_energy = 2.59
tonemap_mode = 2
glow_enabled = true
[sub_resource type="FastNoiseLite" id="FastNoiseLite_huf6x"]
[sub_resource type="FastNoiseLite" id="FastNoiseLite_ubgvy"]
noise_type = 0
frequency = 1.0
fractal_octaves = 10
[sub_resource type="Resource" id="Resource_aso5l"]
[sub_resource type="Resource" id="Resource_12q14"]
script = ExtResource("2_gy4ke")
radius = 1.0
resolution = 100
noise_map = SubResource("FastNoiseLite_huf6x")
amplitude = 1.0
min_height = 0.9
radius = 50.0
noise_map = SubResource("FastNoiseLite_ubgvy")
amplitude = 0.1
planet_color = ExtResource("3_orhq2")
flatness = 6.0
[node name="Main" type="Node3D"]
@ -44,9 +44,11 @@ shadow_enabled = true
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_qj44i")
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, -13.8584)
script = ExtResource("2_4ip6j")
[node name="Planet" parent="." instance=ExtResource("1_65in3")]
planet_data = SubResource("Resource_aso5l")
planet_data = SubResource("Resource_12q14")
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(0.90061, -0.173246, -0.398607, 0, 0.917122, -0.398607, 0.434628, 0.358989, 0.825969, -35.8229, -10.2513, 382.498)
script = ExtResource("2_4ip6j")
velocity = Vector3(-0.469469, -0.469469, -1.12083)
acceleration = Vector3(0.0039095, 0.0039095, -0.00833256)

View File

@ -14,3 +14,7 @@ config/name="PlanetMiner-Godot"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.2", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
Global="*res://Global.gd"