godot-nishita-sky-with-volu.../NishitaSky.gd
MMqd a86eb99df7 # Moon
* Added a realistically lit moon influenced by the sun, resulting in different moon phases, including Earth blocking moon (new moon phase, and blood moon).
* Moving camera very high on the Y axis, or changing the `Height` parameter brings the moon closer

# Misc
* Added support for moon and ground texture, accurate textures included.
* Moving the camera on the X and Z axis (very far) changes the sky and ground texture position.
* Cloud coverage now affects the brightness of the sky and sun.

# Regression
* Issues with cloud alpha at edges at high sun brightness, currently cutting out the sun from the clouds
2023-06-30 17:38:17 -04:00

461 lines
12 KiB
GDScript

@tool
extends Node3D
var sun_color := Color.BLACK
@export var sun_enabled := true
@export var light_color := Color.WHITE
@export var sky_material: Material = null
@export var sun_object_path: NodePath
@export var moon_object_path: NodePath
@export var sun_ground_Height := 1000.0
@export var sun_saturation_scale := 100.0
@export var sun_saturation_mult := 0.3
@export_range(0.0000001, 1.0) var sun_desaturation_height := 0.25
@export var sun_gradient: GradientTexture1D = null
@export var sun_cloud_gradient: GradientTexture1D = null
@export var sun_cloud_ambient_gradient: GradientTexture1D = null
@export var sun_cloud_ground_gradient: GradientTexture1D = null
@export var compute_gradient_toggle := false:
get:
return compute_gradient_toggle
set(value):
if value:
compute_gradient_toggle = false
var cloud_height = (
(
(
get_param("cloud_bottom")
+ get_param("cloud_top")
)
* 0.5
)
+ get_param("Height")
)
var sun_min_angle_mult := 1.0
var min_sun_y := (
sun_min_angle_mult
* sin( acos(
get_param("earthRadius") / (get_param("earthRadius") + sun_ground_Height)
)
)
)
var min_cloud_sun_y := (
sun_min_angle_mult
* sin( acos(
get_param("earthRadius") / (get_param("earthRadius") + cloud_height)
)
)
)
sun_gradient = compute_sun_gradient(sun_ground_Height, min_sun_y)
sun_cloud_gradient = compute_sun_gradient(min_cloud_sun_y, cloud_height)
sun_cloud_ambient_gradient = compute_sun_gradient(
min_cloud_sun_y, get_param("cloud_top"), true
)
sun_cloud_ground_gradient = compute_sun_gradient(
min_cloud_sun_y, get_param("cloud_bottom")
)
func set_param(param: String, value):
sky_material.set_shader_parameter(param, value)
func get_param(param: String):
return sky_material.get_shader_parameter(param)
func compute_sun_gradient(h: float, min_sun_y: float, ambient: bool = false):
var gradient := GradientTexture1D.new()
gradient.gradient = Gradient.new()
var sample_count := 256
var max_col := 0.0
var cols: Array[Color] = []
var poss: Array[float] = []
var max_sky: Vector4
if ambient:
max_sky = sample_sky(
Basis.from_euler(Vector3(PI * 0.5, PI * 0.5, 0.0)).z, Vector3.UP * h, Vector3.UP
)
else:
max_sky = sample_sky(Vector3.UP, Vector3.UP * h, Vector3.UP)
for i in range(sample_count):
var new_i: float = i / (sample_count + 1.0)
var dir: float = lerp(-0.5 * PI, 0.5 * PI, new_i)
var b_sun: Basis
var sun_rot := Vector3(dir, 0.0, 0.0)
sun_rot.x = min(Vector3(dir, 0.0, 0.0).x, asin(min_sun_y))
b_sun = Basis.from_euler(sun_rot)
var b_sample: Basis = Basis.from_euler(Vector3(dir, 0.0, 0.0))
if ambient:
b_sample = Basis.from_euler(Vector3(PI * 0.5, PI * 0.5, 0.0))
var sky: Vector4 = sample_sky(b_sample.z, Vector3.UP * h, b_sun.z)
var col: Color = Color(sky.x, sky.y, sky.z).srgb_to_linear()
if not ambient:
col = saturate(
col,
clamp((sun_desaturation_height - b_sun.z.y) / sun_desaturation_height, 0.0, 1.0)
)
max_col = max(max_col, col.r, col.g, col.b)
cols.append(col)
poss.append(new_i)
for i in range(sample_count):
var new_i: float = i / (sample_count + 1.0)
cols[i] /= max_col
cols[i].r *= light_color.r
cols[i].g *= light_color.g
cols[i].b *= light_color.b
cols[i].a = 1.0
if i > 0 and cols[i] == cols[i - 1]:
continue
gradient.gradient.add_point(poss[i], cols[i])
gradient.gradient.remove_point(len(gradient.gradient.offsets) - 1)
gradient.gradient.remove_point(0)
return gradient
#func rot_to_gradient(rot: float) -> float:
# if rot > 0.5*PI:
# return fmod(rot, 0.5*PI)/PI - 0.5
# elif rot < -0.5*PI:
# return 0.5-fmod(rot, 0.5*PI)/PI
# return rot/PI
func rot_to_gradient(rot: float) -> float:
return (1.0 - rot) * 0.5
func normalized_color(col: Vector4) -> Vector4:
if max(col.x, col.y, col.z) == 0.0:
col = Vector4.ZERO
else:
col = col / max(col.x, col.y, col.z)
return col
func saturate(col: Color, saturation: float) -> Color:
return Color.from_hsv(
col.h,
clamp(log(col.s * saturation * sun_saturation_scale + 1.0) * sun_saturation_mult, 0.0, 1.0),
col.v
)
func loop(val: float, val_range: float) -> float:
if val > val_range:
return fmod(val, val_range) - val_range
if val < -val_range:
return fmod(val, -val_range) + val_range
return val
func _process(delta):
var cloud_height = (
(get_param("cloud_bottom") + get_param("cloud_top"))
* 0.5 + get_param("Height")
)
var sun_dir: Vector3 = global_transform.basis.z
var sun_min_angle_mult := 1.0
var min_sun_y := (
sun_min_angle_mult
* sin( acos(
get_param("earthRadius") / (get_param("earthRadius") + sun_ground_Height)
)
)
)
var min_cloud_sun_y := (
sun_min_angle_mult
* sin( acos(
get_param("earthRadius") / (get_param("earthRadius") + cloud_height)
)
)
)
var sun_object = get_node(sun_object_path)
rotation.x = loop(rotation.x, PI)
rotation.y = loop(rotation.y, PI)
rotation.z = loop(rotation.z, PI)
var moon_object = get_node(moon_object_path)
set_param("precomputed_moon_dir", moon_object.global_transform.basis)
set_param(
"precomputed_sun_size", deg_to_rad(sun_object.light_angular_distance)
)
var precomputed_sun_size : float = deg_to_rad(sun_object.light_angular_distance)
var moonRadius : float = get_param("moonRadius")
var moonDistance : float = get_param("moonDistance")
var earthRadius : float = get_param("earthRadius")
var moon_dir : Vector3 = moon_object.global_transform.basis.z
var moon_size : float = (moonRadius /
((moonDistance + earthRadius) * moon_dir -
Vector3.UP * (get_viewport().get_camera_3d().global_position.y + earthRadius + get_param("Height"))).length() *
2.0) * get_param("moon_size_mult")
var sun_passthrough := 1.0
if (moon_size > 0.0):
var sun_atten_range := sin(precomputed_sun_size)
var moon_atten_range := sin(deg_to_rad(moon_size)) * 0.5
sun_passthrough = pow(clamp(1.0 - clamp(min(
moon_object.global_transform.basis.z.dot(sun_dir),
1.0) -
(1.0 - moon_atten_range),
0.0, 1.0) /
moon_atten_range,
0.0, 1.0),
2.0)
sun_object.light_energy = sun_passthrough * lerp(1.0, 0.0, pow(clamp((get_param("cloud_coverage") - 0.25) / 0.75, 0.0, 1.0), 0.5));
set_param(
"precomputed_sun_energy",
sun_object.light_intensity_lux / get_world_3d().get_environment().background_intensity
)
set_param("precomputed_background_intensity", get_world_3d().get_environment().background_intensity)
sun_object.rotation = rotation
sun_object.rotation.x = (
max(rotation.x, PI - asin(min_sun_y))
if (rotation.x > PI * 0.5)
else min(rotation.x, asin(min_sun_y))
)
if sun_enabled:
sun_object.visible = (
sun_dir.y > -sin(
deg_to_rad(sun_object.light_angular_distance)
+ acos(
get_param("earthRadius")
/ (
get_param("earthRadius")
+ (
get_param("cloud_top")
* float(get_param("clouds"))
)
)
)
)
)
set_param("precomputed_sun_visible", sun_object.visible)
set_param("precomputed_sun_enabled", sun_enabled)
else:
sun_object.visible = false
set_param("precomputed_sun_visible", false)
set_param("precomputed_sun_enabled", false)
var gradient_pos := rot_to_gradient(sun_dir.y)
var sun_ratio := asin(deg_to_rad(sun_object.light_angular_distance)) / PI
var sun_gradient_offset: float = -clamp(1.0 - sun_dir.y / sun_ratio, 0.0, 1.0) * sun_ratio
sun_object.light_color = sun_gradient.gradient.sample(gradient_pos + sun_gradient_offset)
set_param("precomputed_sun_dir", sun_dir)
set_param("precomputed_sun_color", light_color)
#Precomputed cloud lighting
if get_param("clouds"):
var cloud_sun_rot := rotation
cloud_sun_rot.x = min(rotation.x, asin(min_cloud_sun_y))
set_param(
"precomputed_Atmosphere_sun",
sun_cloud_gradient.gradient.sample(gradient_pos + sun_gradient_offset)
)
set_param(
"precomputed_Atmosphere_ambient",
sun_cloud_ambient_gradient.gradient.sample(gradient_pos)
)
set_param(
"precomputed_Atmosphere_ground", sun_cloud_ground_gradient.gradient.sample(gradient_pos)
)
var ground_color: Vector3 = Vector3(0.1, 0.07, 0.034)
var ground_brightness: float = 1.0
func solve_quadratic(origin: Vector3, dir: Vector3, Radius: float) -> Vector3:
var b := 2.0 * dir.dot(origin)
var c := origin.dot(origin) - Radius * Radius
var d := b * b - 4.0 * c
var det := sqrt(d)
return Vector3((-b + det) * 0.5, (-b - det) * 0.5, d)
func atmosphere(
Direction: Vector3, pos: Vector3, SunDirection: Vector3, intensity: float = 1.0
) -> Array[Vector3]:
var shader_Height := 1.0
# var intensity : float = get_param("intensity")
var Re: float = get_param("earthRadius")
var Ra: float = get_param("atmosphereRadius")
var Hr: float = get_param("rayleighScaleHeight")
var Hm: float = get_param("mieScaleHeight")
var mie_eccentricity: float = get_param("mie_eccentricity")
var turbidity: float = get_param("turbidity")
var ground := 0.0
var mu := Direction.dot(SunDirection)
var phaseR := (3.0 / (16.0 * PI)) * (1.0 + mu * mu)
var phaseM := (
(3.0 / (8.0 * PI))
* (
(1.0 - mie_eccentricity * mie_eccentricity)
* (1.0 + mu * mu)
/ (
(2.0 + mie_eccentricity * mie_eccentricity)
* pow(1.0 + mie_eccentricity * mie_eccentricity - 2.0 * mie_eccentricity * mu, 1.5)
)
)
)
var SumR := Vector3.ZERO
var SumM := Vector3.ZERO
var begin := Vector3.ZERO
var end := Vector3.ZERO
var cameraPos := Vector3(0, Re + sun_ground_Height + max(0.0, pos.y), 0)
begin = cameraPos
var d1 := solve_quadratic(cameraPos, Direction, Ra)
if d1.x > d1.y && d1.x > 0.0:
end = cameraPos + Direction * d1.x
if d1.y > 0.0:
begin = cameraPos + Direction * d1.y
else:
return [Vector3.ZERO, Vector3.ONE, Vector3.ONE]
var d2 = solve_quadratic(cameraPos, Direction, Re)
if d2.x > 0.0 && d2.y > 0.0:
end = begin + Direction * d2.y
ground = 1.0
var numSamples := 16 * 16
var numSamplesL := 8 * 2
var segmentLength := begin.distance_to(end) / float(numSamples)
var opticalDepthR := 0.0
var opticalDepthM := 0.0
var atmosphere_atten := Vector3.ZERO
var BetaR: Vector3 = (
get_param("rayleigh_color")
* 22.4e-6
* get_param("rayleigh")
)
var BetaM: Vector3 = (
get_param("mie_color")
* 20e-6
* get_param("mie")
)
for i in range(numSamples):
var Px := begin + Direction * segmentLength * (float(i) + 0.5)
var sampleHeight := Px.length() - Re
var Hr_sample := exp(-sampleHeight / (Hr * turbidity)) * segmentLength
var Hm_sample := exp(-sampleHeight / (Hm * turbidity)) * segmentLength
opticalDepthR += Hr_sample
opticalDepthM += Hm_sample
var opticalDepthLR := 0.0
var opticalDepthLM := 0.0
var d3 = solve_quadratic(Px, SunDirection, Ra)
var d4 = solve_quadratic(Px, SunDirection, Re)
if d4.x > 0.0 and d4.y > 0.0:
continue
var j2 := 0
var segmentLengthL: float = max(d3.x, d3.y) / float(numSamplesL)
for j in range(numSamplesL):
var Pl: Vector3 = Px + SunDirection * segmentLengthL * (j + 0.5)
var sampleHeightL: float = Pl.length() - Re
if sampleHeightL < 0.0:
break
opticalDepthLR += exp(-sampleHeightL / (Hr * turbidity))
opticalDepthLM += exp(-sampleHeightL / (Hm * turbidity))
j2 += 1
if j2 == numSamplesL:
opticalDepthLR *= segmentLengthL
opticalDepthLM *= segmentLengthL
var tau := (
BetaR * (opticalDepthR + opticalDepthLR)
+ BetaM * 1.1 * (opticalDepthM + opticalDepthLM)
)
var attenuation := v3exp(-tau)
atmosphere_atten += tau
SumR += Hr_sample * attenuation
SumM += Hm_sample * attenuation
var sky := SumR * phaseR * BetaR + SumM * phaseM * BetaM
return [
sky,
atmosphere_atten * (1.0 - ground),
v3exp(-(opticalDepthR * BetaR + opticalDepthM * BetaM))
]
func v3exp(input: Vector3) -> Vector3:
return Vector3(exp(input.x), exp(input.y), exp(input.z))
func sample_sky(
dir: Vector3,
pos: Vector3,
sun_dir: Vector3,
LIGHT0_ENERGY: Vector3 = Vector3.ONE,
LIGHT0_COLOR: Vector3 = Vector3.ONE
) -> Vector4:
var sun_object = get_node(sun_object_path)
var sky: Array[Vector3] = atmosphere(dir, pos, sun_dir)
var skyxyz: Vector3 = sky[0]
var sun: Vector3 = Vector3.ZERO
sun = (
(Vector3.ONE - v3exp(-sky[1]))
* (
(
Vector3.ONE
* max(
(
max(dir.dot(sun_dir), 0.0)
- (cos(deg_to_rad(sun_object.light_angular_distance)))
),
0.0
)
* get_param("sun_brightness")
)
+ (
(Vector3.ONE - v3exp(-sky[2]))
* ground_color
* max(sun_dir.y, 0.0)
* sky[2].x
* ground_brightness
)
)
* LIGHT0_ENERGY
)
var col := skyxyz + sun
return Vector4(col.x, col.y, col.z, 1.0)
func mix(start: Vector3, end: Vector3, factor: float):
return lerp(start, end, factor)