OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / test_nubis.gd

236 lines · gd ref:
extends Node3D

## Nubis sky shader cloud test.
## F12: screenshot, F1: toggle coverage

var _sky_mat: ShaderMaterial

func _ready() -> void:
	var shader := load("res://addons/nubis_clouds/shaders/cloud_sky.gdshader") as Shader
	if shader == null:
		push_error("Failed to load cloud_sky.gdshader")
		return

	_sky_mat = ShaderMaterial.new()
	_sky_mat.shader = shader

	# Load pre-baked 3D noise textures (from Clay John's Nubis demo)
	var pw_tex = load("res://addons/nubis_clouds/textures/perlworlnoise.png")
	var worl_tex = load("res://addons/nubis_clouds/textures/worlnoise.webp")
	var weather_tex = load("res://addons/nubis_clouds/textures/weather.webp")
	if pw_tex: _sky_mat.set_shader_parameter("perlworlnoise", pw_tex)
	if worl_tex: _sky_mat.set_shader_parameter("worlnoise", worl_tex)
	if weather_tex: _sky_mat.set_shader_parameter("weather_map", weather_tex)
	_sky_mat.set_shader_parameter("cloud_coverage", 0.35)

	var sky := Sky.new()
	sky.sky_material = _sky_mat
	sky.process_mode = Sky.PROCESS_MODE_REALTIME

	var env: Environment = $WorldEnvironment.environment
	env.background_mode = Environment.BG_SKY
	env.sky = sky

	_sky_mat.set_shader_parameter("cloud_time_scale", 2.0)
	# Ground cloud shadows
	var shadow_shader := load("res://addons/nubis_clouds/shaders/cloud_shadow_ground.gdshader") as Shader
	if shadow_shader and pw_tex and weather_tex:
		var ground_mat := ShaderMaterial.new()
		ground_mat.shader = shadow_shader
		ground_mat.set_shader_parameter("perlworlnoise", pw_tex)
		ground_mat.set_shader_parameter("weather_map", weather_tex)
		ground_mat.set_shader_parameter("cloud_coverage", 0.35)
		ground_mat.set_shader_parameter("shadow_strength", 0.5)
		var ground := get_node_or_null("Ground") as MeshInstance3D
		if ground:
			ground.material_override = ground_mat

	# Create NubisCompositor for volumetric cloud rendering with proper depth
	var compositor := Compositor.new()
	var nubis_comp := NubisCompositor.new()
	nubis_comp.density_scale = 80.0
	nubis_comp.noise_freq = 0.00005     # lower = bigger bumps, higher = finer detail
	nubis_comp.erosion_strength = 0.9   # 0 = smooth sphere, 1 = heavily eroded
	compositor.compositor_effects = [nubis_comp]
	var we := $WorldEnvironment as WorldEnvironment
	we.compositor = compositor

	# === SHAPE TEST: change number to test different SDF generators ===
	var sdf_func := 1
	var shared: ImageTexture3D
	match sdf_func:
		1: shared = _make_schneider_sdf(32, 42, 0.5)
		2: shared = _make_cloud_sdf(32, 42, 0.06, 0.04, 0.25)  # cellular
		_: shared = _make_schneider_sdf(32, 42, 0.5)

	# Fill the sky: wide coverage with varied heights
	var rng := RandomNumberGenerator.new()
	rng.seed = 42

	# Ring of large volumes around the player, some towering
	var configs := [
		# Nearby towering cumulus
		{"pos": Vector3(0, 300, -800), "size": Vector3(1500, 800, 1500), "density": 80.0},
		{"pos": Vector3(1000, 350, -400), "size": Vector3(1200, 600, 1200), "density": 75.0},
		{"pos": Vector3(-900, 280, 500), "size": Vector3(1400, 700, 1300), "density": 85.0},
		# Mid-range wide formations
		{"pos": Vector3(600, 250, 900), "size": Vector3(1800, 400, 1600), "density": 70.0},
		{"pos": Vector3(-1200, 300, -600), "size": Vector3(1600, 500, 1400), "density": 75.0},
		# Distant tall towers
		{"pos": Vector3(2000, 400, -1500), "size": Vector3(1000, 900, 1000), "density": 90.0},
		{"pos": Vector3(-1800, 350, -1800), "size": Vector3(1200, 700, 1100), "density": 80.0},
		{"pos": Vector3(0, 320, 2000), "size": Vector3(1500, 600, 1500), "density": 70.0},
		# Low flat stratus
		{"pos": Vector3(500, 150, -200), "size": Vector3(2000, 200, 2000), "density": 60.0},
		{"pos": Vector3(-800, 180, 1200), "size": Vector3(1800, 250, 1800), "density": 55.0},
	]

	for cfg in 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.02
		add_child(c)

	print("[Nubis] %d clouds (shape test #%d)" % [configs.size(), sdf_func])


func _make_cloud_sdf(res: int, seed_val: int, cell_freq: float, warp_freq: float, bottom_clip: float) -> ImageTexture3D:
	# Noise-based SDF with configurable parameters for different cloud types
	var noise_shape := FastNoiseLite.new()
	noise_shape.noise_type = FastNoiseLite.TYPE_CELLULAR
	noise_shape.cellular_distance_function = FastNoiseLite.DISTANCE_EUCLIDEAN
	noise_shape.cellular_return_type = FastNoiseLite.RETURN_DISTANCE
	noise_shape.frequency = cell_freq
	noise_shape.fractal_octaves = 3
	noise_shape.fractal_gain = 0.5
	noise_shape.seed = seed_val

	var noise_warp := FastNoiseLite.new()
	noise_warp.noise_type = FastNoiseLite.TYPE_PERLIN
	noise_warp.frequency = warp_freq
	noise_warp.seed = seed_val + 81

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

				# Sample position with domain warp for organic shapes
				var sp := Vector3(float(x), float(y), float(z))
				var wx := noise_warp.get_noise_3d(sp.x, sp.y, sp.z) * 5.0
				var wz := noise_warp.get_noise_3d(sp.x + 100, sp.y, sp.z + 100) * 5.0
				sp.x += wx
				sp.z += wz

				# Inverted cellular noise: cell centers = 1 (cloud), edges = 0 (gap)
				var cell := noise_shape.get_noise_3d(sp.x, sp.y, sp.z)
				# Remap from [-1,1] to [0,1], then invert for puffy shapes
				var sdf := 1.0 - clampf((cell * 0.5 + 0.5), 0.0, 1.0)

				# Soft sphere boundary to contain within volume
				var dist := (uvw - Vector3(0.5, 0.5, 0.5)).length()
				var boundary := 1.0 - smoothstep(0.0, 0.48, dist)
				sdf *= boundary

				# Flat bottom: fade out below clip height
				sdf *= smoothstep(bottom_clip - 0.1, bottom_clip, uvw.y)

				# Height-dependent density: thinner at top
				sdf *= smoothstep(0.0, 0.15, 1.0 - uvw.y)

				img.set_pixel(x, y, Color(clampf(sdf, 0.0, 1.0), 0, 0, 1))
		images.append(img)
	var tex := ImageTexture3D.new()
	tex.create(Image.FORMAT_RF, res, res, res, false, images)
	return tex


func _make_schneider_sdf(res: int, seed_val: int, coverage: float) -> ImageTexture3D:
	# Schneider/HZD density function baked to 3D texture
	# Same math as nubis_sky.gdshader cloud_density_fn but at volume scale
	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))

				# Base Perlin noise [0,1]
				var p_val := perlin.get_noise_3d(sp.x, sp.y, sp.z) * 0.5 + 0.5

				# Worley FBM for remap [0,1]
				var w_val := 1.0 - (worley.get_noise_3d(sp.x, sp.y, sp.z) * 0.5 + 0.5)

				# Schneider remap: Perlin shaped by Worley
				# base_cloud = remap(perlin, -(1-worley_fbm), 1, 0, 1)
				var lo := -(1.0 - w_val)
				var base_cloud := clampf((p_val - lo) / (1.0 - lo), 0.0, 1.0)

				# Height gradient — cumulus profile
				var hf := uvw.y
				var gradient := smoothstep(0.01, 0.15, hf) * (1.0 - smoothstep(0.6, 1.0, hf))
				base_cloud *= gradient

				# Coverage threshold — creates natural gaps between cloud formations
				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)

				# Edge fade at volume boundaries (soft, no sphere constraint)
				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

				# Power curve: compress density at base, expand at top
				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)


func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed:
		match event.keycode:
			KEY_F1:
				var c: float = _sky_mat.get_shader_parameter("cloud_coverage")
				c = fmod(c + 0.1, 1.0)
				_sky_mat.set_shader_parameter("cloud_coverage", c)
				print("[Nubis] coverage = %.1f" % c)
			KEY_F12:
				var img := get_viewport().get_texture().get_image()
				var ts := Time.get_datetime_string_from_system().replace(":", "-")
				var dir := "/home/fleeky/Desktop/hyper-tps"
				DirAccess.make_dir_recursive_absolute(dir)
				var path := "%s/nubis_%s.png" % [dir, ts]
				img.save_png(path)
				print("[Nubis] Screenshot: %s" % path)