OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / proc_erosion_sunshine_test.gd

399 lines · gd ref:
@tool
extends Node3D

## DC Terrain with SunshineClouds2 full-sky volumetric clouds + Sky3D Extended atmosphere.
## Same terrain as proc_erosion_dc_test but with SunshineClouds2 instead of Nubis hero volumes.
##
## Controls:
##   T / Shift+T  — advance / rewind time by 1 hour
##   F            — fast forward (1min cycle)
##   G            — normal speed (15min cycle)
##   P            — pause / resume time
##   R            — rebuild terrain
##   Right-click  — dig box, Shift+Right — add box
##   Shift+Scroll — adjust edit size
##   F2           — stamp fandisk at crosshair
##   F12          — screenshot

@export var terrain_size: float = 12.0

var _gen: DCTerrainGeneratorProcNodes
var _driver: SunshineCloudsDriverGD
var _nubis_comp: NubisCompositor

func _ready():
	var sky3d := $Sky3DExtended as Sky3DExtended
	if sky3d and sky3d.sky:
		sky3d.sky.moon_phase = 0.5
		sky3d.sky.moon_light_energy = 1.5
	# Let Sky3D handle fog naturally — no overrides

	# SunshineClouds2 setup — deferred so Sky3D's lights exist
	call_deferred("_setup_sunshine_clouds")

	var terrain = get_node_or_null("DCTerrain")
	if terrain == null:
		push_warning("proc_erosion_sunshine_test: no DCTerrain child")
		return

	var g = ProcNodeGraph.new()

	var noise = g.add_node(GradientNoiseNode.new())
	g.get_node(noise).set_input_default(1, 3.0)
	g.get_node(noise).set_input_default(2, 3)
	g.get_node(noise).set_input_default(3, 0.1)

	var erosion = g.add_node(ErosionNoiseNode.new())
	g.get_node(erosion).set_input_default(1, 5.0)
	g.get_node(erosion).set_input_default(2, 0.16)
	g.get_node(erosion).set_input_default(3, 0.6)
	g.get_node(erosion).set_input_default(4, 1.0)
	g.get_node(erosion).set_input_default(5, 5)
	g.get_node(erosion).set_input_default(6, 0.5)

	g.connect_sockets(noise, 0, erosion, 7)
	g.connect_sockets(noise, 1, erosion, 8)

	_gen = DCTerrainGeneratorProcNodes.new()
	_gen.set_terrain_height(1000.0)
	_gen.set_floor_y(-500.0)
	_gen.set_ceiling_y(500.0)
	_gen.set_mountain_scale(0.5)
	_gen.set_mountain_height(0.8)
	_gen.set_mountain_octaves(3)
	_gen.set_graph(g)

	var glsl = g.compile_to_glsl_function("terrain_density", true)
	if glsl.contains("erosion_filter"):
		print("[SunshineErosion] PASS: Compiled ", glsl.length(), " chars of GLSL")

	var vs: float = 2.0
	var depth: int = 10

	terrain.set("voxel_size", vs)
	terrain.set("octree_depth", depth)
	terrain.set("use_gpu", true)
	terrain.set("use_noise", true)
	terrain.set("generator", _gen)
	terrain.set("debug_logging", false)

	var shader = load("res://scenes/maps/dc_erosion_color.gdshader") as Shader
	if shader:
		var mat = ShaderMaterial.new()
		mat.shader = shader
		mat.set_shader_parameter("terrain_params", Vector3(-500.0, 500.0, 0.465))
		terrain.set("custom_material", mat)

	var player = get_node_or_null("Player")
	if player:
		player.position = Vector3(100, 1000, 100)
		player.set_multiplayer_authority(1)

	if not Engine.is_editor_hint():
		Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

	var half_ext: float = (1 << (depth - 1)) * 28 * vs
	print("[SunshineErosion] voxel=", vs, "m depth=", depth, " extent=", half_ext * 2, "m",
		" Y range=[-500, 500] terrain_height=1000m")


func _setup_sunshine_clouds() -> void:
	var sky3d := $Sky3DExtended as Sky3DExtended
	if not sky3d:
		push_error("[SunshineErosion] Sky3DExtended not found")
		return

	# Create SunshineClouds2 CompositorEffect
	var cloud_res := SunshineCloudsGD.new()
	# Chunky cumulus clouds — defined shapes, good lighting, minimal streaking
	cloud_res.clouds_coverage = 0.68
	cloud_res.clouds_density = 0.2
	cloud_res.atmospheric_density = 0.5
	cloud_res.lighting_density = 1.0
	cloud_res.clouds_anisotropy = 0.2           # Subtle silver lining
	cloud_res.clouds_powder = 0.5               # Moderate powder
	cloud_res.clouds_sharpness = 1.0            # Sharp defined edges
	cloud_res.clouds_detail_power = 1.1         # Good detail without noise
	cloud_res.curl_noise_strength = 1500.0      # Subtle curl — wisps without streaking
	cloud_res.wind_swept_range = 0.2            # Minimal wind shearing
	cloud_res.wind_swept_strength = 500.0       # Very light displacement
	cloud_res.lighting_sharpness = 0.4          # Defined shadows
	cloud_res.ambient_occlusion_color = Color(0.3, 0.3, 0.4, 1.0)
	cloud_res.cloud_ambient_color = Color(0.5, 0.6, 0.75, 1.0)
	cloud_res.atmosphere_color = Color(0.7, 0.8, 0.9, 1.0)
	# Noise scales — tighter range for chunkier shapes
	cloud_res.extra_large_noise_scale = 320000.0
	cloud_res.large_noise_scale = 120000.0
	cloud_res.medium_noise_scale = 20000.0
	cloud_res.small_noise_scale = 8500.0
	# Cloud layer
	cloud_res.cloud_floor = 300.0
	cloud_res.cloud_ceiling = 10000.0
	# Quality — high accumulation to kill dithering
	cloud_res.max_step_count = 300.0
	cloud_res.max_lighting_steps = 24.0
	cloud_res.resolution_scale = 1              # Half res
	cloud_res.accumulation_decay = 0.9          # 90% previous frame — smooth, no dithering
	cloud_res.min_step_distance = 100.0
	cloud_res.max_step_distance = 400.0
	cloud_res.lighting_travel_distance = 10000.0
	cloud_res.lod_bias = 1.0
	cloud_res.dither_speed = 100.0              # Fast dither cycling
	cloud_res.blur_power = 2.5                  # Soften edges
	# SunshineClouds 2.5 (Overhaul) replaced blur_quality with blur_passes (+ blur_edge_sharpness).
	cloud_res.blur_passes = 1
	cloud_res.fog_effect_ground = 0.0
	cloud_res.use_environment_fog = 0.0

	# Add to Sky3D's compositor
	if not sky3d.compositor:
		sky3d.compositor = Compositor.new()
	var effects := sky3d.compositor.compositor_effects.duplicate()
	effects.append(cloud_res)
	sky3d.compositor.compositor_effects = effects

	# Create driver
	_driver = SunshineCloudsDriverGD.new()
	_driver.name = "SunshineCloudsDriver"
	_driver.clouds_resource = cloud_res
	_driver.wind_direction = Vector3(1, 0, 0.5)
	_driver.extra_large_structures_wind_speed = 30.0
	_driver.large_structures_wind_speed = 20.0
	_driver.medium_structures_wind_speed = 10.0
	_driver.small_structures_wind_speed = 5.0

	# Track Sky3D's lights
	var lights: Array[DirectionalLight3D] = []
	if sky3d.sun:
		lights.append(sky3d.sun)
	if sky3d.moon:
		lights.append(sky3d.moon)
	_driver.tracked_directional_lights = lights
	_driver.tracked_directional_light_shadow_steps = [32, 12]

	# Sample fog color from Sky3D's environment
	if sky3d.environment:
		_driver.ambience_sample_environment = sky3d.environment

	_driver.update_continuously = true
	add_child(_driver)

	print("[SunshineErosion] SunshineClouds2 active — coverage=%.2f, floor=%d, ceiling=%d, tracking %d lights" % [
		cloud_res.clouds_coverage, int(cloud_res.cloud_floor), int(cloud_res.cloud_ceiling), lights.size()])

	# --- Nubis hero volumes around mountain peaks (POST_TRANSPARENT — occludes player) ---
	_nubis_comp = NubisCompositor.new()
	var nubis_comp := _nubis_comp
	nubis_comp.density_scale = 80.0
	nubis_comp.noise_freq = 0.00005
	nubis_comp.erosion_strength = 0.9
	# Add nubis AFTER sunshine so it renders on top (POST_TRANSPARENT over PRE_TRANSPARENT)
	effects = sky3d.compositor.compositor_effects.duplicate()
	effects.append(nubis_comp)
	sky3d.compositor.compositor_effects = effects

	var shared := _make_schneider_sdf(32, 42, 0.5)

	# Hero clouds hugging mountain peaks — lowered to ~200m altitude
	var hero_configs := [
		# Near peaks
		{"pos": Vector3(0, 200, 0), "size": Vector3(800, 400, 800), "density": 75.0},
		{"pos": Vector3(-200, 180, -300), "size": Vector3(600, 300, 700), "density": 70.0},
		{"pos": Vector3(300, 160, 200), "size": Vector3(700, 350, 600), "density": 80.0},
		# Mid-range ridgelines
		{"pos": Vector3(-600, 250, -800), "size": Vector3(1000, 500, 900), "density": 85.0},
		{"pos": Vector3(800, 220, -500), "size": Vector3(900, 400, 800), "density": 75.0},
		{"pos": Vector3(500, 200, 700), "size": Vector3(800, 350, 900), "density": 70.0},
		{"pos": Vector3(-700, 230, 500), "size": Vector3(1000, 450, 800), "density": 80.0},
		# Distant formations
		{"pos": Vector3(1500, 280, -1200), "size": Vector3(1200, 600, 1100), "density": 85.0},
		{"pos": Vector3(-1300, 260, -1000), "size": Vector3(1100, 500, 1000), "density": 80.0},
		{"pos": Vector3(0, 200, 1500), "size": Vector3(1400, 500, 1200), "density": 75.0},
		{"pos": Vector3(-1500, 240, 800), "size": Vector3(1000, 450, 1100), "density": 80.0},
		{"pos": Vector3(1200, 220, 1000), "size": Vector3(900, 400, 1000), "density": 70.0},
	]

	for cfg in hero_configs:
		var c := CloudVolume.new()
		c.shared_sdf = shared
		c.position = cfg["pos"]
		c.cloud_size = cfg["size"]
		c.density_scale = cfg["density"]
		c.wind_speed = 0.01
		add_child(c)

	print("[SunshineErosion] %d Nubis hero volumes at mountain peaks" % hero_configs.size())


func _make_schneider_sdf(res: int, seed_val: int, coverage: float) -> ImageTexture3D:
	var perlin := FastNoiseLite.new()
	perlin.noise_type = FastNoiseLite.TYPE_PERLIN
	perlin.frequency = 0.08
	perlin.fractal_octaves = 4
	perlin.fractal_gain = 0.5
	perlin.seed = seed_val
	var worley := FastNoiseLite.new()
	worley.noise_type = FastNoiseLite.TYPE_CELLULAR
	worley.cellular_return_type = FastNoiseLite.RETURN_DISTANCE
	worley.frequency = 0.12
	worley.fractal_octaves = 3
	worley.seed = seed_val + 10
	var images: Array[Image] = []
	for z in res:
		var img := Image.create(res, res, false, Image.FORMAT_RF)
		for y in res:
			for x in res:
				var uvw := Vector3(float(x) / float(res), 1.0 - float(y) / float(res), float(z) / float(res))
				var sp := Vector3(float(x), float(y), float(z))
				var p_val := perlin.get_noise_3d(sp.x, sp.y, sp.z) * 0.5 + 0.5
				var w_val := 1.0 - (worley.get_noise_3d(sp.x, sp.y, sp.z) * 0.5 + 0.5)
				var lo := -(1.0 - w_val)
				var base_cloud := clampf((p_val - lo) / (1.0 - lo), 0.0, 1.0)
				var hf := uvw.y
				var gradient := _smoothstep(0.01, 0.15, hf) * (1.0 - _smoothstep(0.6, 1.0, hf))
				base_cloud *= gradient
				var threshold := 1.0 - coverage
				var shaped := clampf((base_cloud - threshold) / maxf(1.0 - threshold, 0.001), 0.0, 1.0)
				shaped *= _smoothstep(threshold - 0.05, threshold + 0.05, base_cloud)
				var ex := _smoothstep(0.0, 0.08, uvw.x) * _smoothstep(0.0, 0.08, 1.0 - uvw.x)
				var ey := _smoothstep(0.0, 0.08, 1.0 - uvw.y)
				var ez := _smoothstep(0.0, 0.08, uvw.z) * _smoothstep(0.0, 0.08, 1.0 - uvw.z)
				shaped *= ex * ey * ez
				var sdf := pow(clampf(shaped, 0.0, 1.0), (1.0 - hf) * 0.8 + 0.5)
				img.set_pixel(x, y, Color(sdf, 0, 0, 1))
		images.append(img)
	var tex := ImageTexture3D.new()
	tex.create(Image.FORMAT_RF, res, res, res, false, images)
	return tex


func _smoothstep(edge0: float, edge1: float, x: float) -> float:
	var t := clampf((x - edge0) / (edge1 - edge0), 0.0, 1.0)
	return t * t * (3.0 - 2.0 * t)


var _edit_radius: float = 3.0

func _unhandled_input(event: InputEvent) -> void:
	var terrain = get_node_or_null("DCTerrain")
	var player = get_node_or_null("Player")

	if event.is_action_pressed("ui_cancel"):
		if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
			Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
		elif not Engine.is_editor_hint():
			Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

	if event is InputEventKey and event.pressed:
		match event.keycode:
			KEY_T:
				var sky3d := get_node_or_null("Sky3DExtended") as Sky3DExtended
				if sky3d:
					var delta := -1.0 if event.shift_pressed else 1.0
					sky3d.current_time = fmod(sky3d.current_time + delta + 24.0, 24.0)
					print("[SunshineErosion] Time: %.1f (%s)" % [sky3d.current_time, "day" if sky3d.is_day() else "night"])
			KEY_F:
				var sky3d := get_node_or_null("Sky3DExtended") as Sky3DExtended
				if sky3d:
					sky3d.minutes_per_day = 1.0
					print("[SunshineErosion] Fast forward (1min cycle)")
			KEY_G:
				var sky3d := get_node_or_null("Sky3DExtended") as Sky3DExtended
				if sky3d:
					sky3d.minutes_per_day = 15.0
					print("[SunshineErosion] Normal speed (15min cycle)")
			KEY_P:
				var sky3d := get_node_or_null("Sky3DExtended") as Sky3DExtended
				if sky3d:
					if sky3d.game_time_enabled:
						sky3d.pause()
						print("[SunshineErosion] Paused at %.1f" % sky3d.current_time)
					else:
						sky3d.resume()
						print("[SunshineErosion] Resumed")
			KEY_R:
				if terrain:
					terrain.rebuild()
					print("[SunshineErosion] Rebuild triggered")
			KEY_D:
				if _nubis_comp:
					_nubis_comp.debug_mode = (_nubis_comp.debug_mode + 1) % 6
					var names := ["Normal", "Sun Optical", "Beer Trans", "SDF Profile", "Density", "Beer-Powder"]
					print("[SunshineErosion] Debug: %s (%d)" % [names[_nubis_comp.debug_mode], _nubis_comp.debug_mode])
			KEY_F2:
				_stamp_fandisk(terrain, player)
			KEY_F12:
				var img: Image = get_viewport().get_texture().get_image()
				var ts: String = Time.get_datetime_string_from_system().replace(":", "-")
				var dir := "/home/fleeky/Desktop/hyper-tps"
				DirAccess.make_dir_recursive_absolute(dir)
				var path: String = "%s/sunshine_erosion_%s.png" % [dir, ts]
				img.save_png(path)
				print("[SunshineErosion] Screenshot: %s" % path)

	if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
		if event.shift_pressed and event.button_index == MOUSE_BUTTON_WHEEL_UP:
			_edit_radius = minf(_edit_radius + 0.5, 20.0)
			print("Edit size: %.1f" % _edit_radius)
			get_viewport().set_input_as_handled()
		elif event.shift_pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			_edit_radius = maxf(_edit_radius - 0.5, 1.0)
			print("Edit size: %.1f" % _edit_radius)
			get_viewport().set_input_as_handled()
		elif event.button_index == MOUSE_BUTTON_RIGHT:
			var camera: Camera3D = get_viewport().get_camera_3d()
			if not camera or not terrain:
				return
			var viewport_center: Vector2 = get_viewport().get_visible_rect().size * 0.5
			var from: Vector3 = camera.project_ray_origin(viewport_center)
			var dir: Vector3 = camera.project_ray_normal(viewport_center)
			var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
			var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, from + dir * 200.0)
			if player:
				query.exclude = [player.get_rid()]
			var result: Dictionary = space_state.intersect_ray(query)
			if result.size() > 0:
				var hit_pos: Vector3 = result["position"]
				var half_ext: Vector3 = Vector3(_edit_radius, _edit_radius, _edit_radius)
				var adding: bool = event.shift_pressed
				var strength: float = 3.0 if adding else -3.0
				terrain.edit_terrain_box(hit_pos, half_ext, strength)
				print("Box edit at %s (size=%.1f, strength=%.1f)" % [hit_pos, _edit_radius, strength])


func _stamp_fandisk(terrain, player) -> void:
	if not terrain:
		return
	var camera: Camera3D = get_viewport().get_camera_3d()
	if not camera:
		return
	var viewport_center: Vector2 = get_viewport().get_visible_rect().size * 0.5
	var from: Vector3 = camera.project_ray_origin(viewport_center)
	var dir: Vector3 = camera.project_ray_normal(viewport_center)
	var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
	var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, from + dir * 200.0)
	if player:
		query.exclude = [player.get_rid()]
	var result: Dictionary = space_state.intersect_ray(query)
	if result.size() > 0:
		var hit_pos: Vector3 = result["position"]
		var scene: PackedScene = load("res://fandisk.glb")
		if not scene:
			return
		var instance: Node3D = scene.instantiate()
		var mesh_inst: MeshInstance3D = null
		for child in instance.get_children():
			if child is MeshInstance3D:
				mesh_inst = child
				break
		if not mesh_inst or not mesh_inst.mesh:
			instance.queue_free()
			return
		var mesh: Mesh = mesh_inst.mesh
		var arrays: Array = mesh.surface_get_arrays(0)
		var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
		var idxs: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
		hit_pos.y += _edit_radius * 0.8
		terrain.stamp_mesh_arrays(verts, idxs, hit_pos, _edit_radius, 3.0)
		instance.queue_free()