Add sunlit shallows site with localized sun shafts
Quality Gate / Quality Gate (pull_request) Successful in 6m55s
Quality Gate / Quality Gate (pull_request) Successful in 6m55s
New visual test site under game/world/sites: flat sandy floor at -40 m, calm surface at 0 m, fixed daytime sun ahead of and above the submarine. Light shafts come from a shadow-only surface occluder whose alpha-scissor ripples open only inside a pool a few tens of metres ahead; a subtractive FogVolume cancels the global volumetric fog above the water, and an unshaded ceiling mesh shows the lit underside of the surface inside that pool. A composed world (sunlit_shallows_world.tscn) mounts the submarine, cockpit and camera rig in it, with the projectors kept out of the fog. Cockpit: the porthole glass moves to its own render layer with a rougher material and the porthole fill light excludes that layer, so interior lamps no longer mirror as hard specular points. Quality gate: new integration suite for the site (42 checks), three glass assertions in the foundation suite, and the Input-access architecture rule now matches both path separators so the gate runs on Windows. project.godot raises the volumetric fog froxel grid to 128 so the shafts stay readable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c1ea50aba
commit
f0072a3e11
14 files changed
+666
-2
No files matched your search
@@ -0,0 +1,28 @@
|
||||
// Flat sandy floor with slowly drifting caustic lines. The pattern is purely
|
||||
// presentational and time-driven by the shader clock; no gameplay reads it.
|
||||
shader_type spatial;
|
||||
|
||||
uniform vec3 sand_color : source_color = vec3(0.34, 0.32, 0.25);
|
||||
uniform vec3 caustic_tint : source_color = vec3(0.55, 0.82, 0.92);
|
||||
uniform sampler2D caustic_noise : repeat_enable, filter_linear_mipmap;
|
||||
uniform float caustic_scale = 0.055;
|
||||
uniform float caustic_speed = 0.018;
|
||||
uniform float caustic_strength : hint_range(0.0, 2.0) = 0.25;
|
||||
|
||||
varying vec2 world_xz;
|
||||
|
||||
void vertex() {
|
||||
world_xz = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 drift_a = TIME * caustic_speed * vec2(1.0, 0.6);
|
||||
vec2 drift_b = TIME * caustic_speed * vec2(-0.7, 1.0);
|
||||
float layer_a = texture(caustic_noise, world_xz * caustic_scale + drift_a).r;
|
||||
float layer_b = texture(caustic_noise, world_xz * caustic_scale * 1.35 + drift_b).r;
|
||||
float caustic = pow(clamp(min(layer_a, layer_b) * 1.9, 0.0, 1.0), 4.0);
|
||||
ALBEDO = sand_color;
|
||||
ROUGHNESS = 0.95;
|
||||
METALLIC = 0.0;
|
||||
EMISSION = caustic_tint * caustic * caustic_strength;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
class_name SunlitShallowsEnvironment
|
||||
extends Node3D
|
||||
|
||||
## Visual test site: flat floor, calm water, fixed daytime sun.
|
||||
## The hour never changes, so the sun transform is a constant and the only
|
||||
## motion is the slow ripple drift inside the two shaders.
|
||||
|
||||
const SURFACE_LEVEL_M := 0.0
|
||||
const FLOOR_DEPTH_M := -40.0
|
||||
## Air above the calm surface holds no scattering medium: a subtractive
|
||||
## FogVolume cancels the global volumetric fog there so the view never shows
|
||||
## an unshadowed glow above the water.
|
||||
const AIR_VOLUME_HEIGHT_M := 80.0
|
||||
const VOLUMETRIC_FOG_DENSITY := 0.035
|
||||
const SITE_EXTENT_M := 240.0
|
||||
## Sun sits ahead of the observer (world -Z) and high: shafts travel down
|
||||
## toward the submarine, as in the cockpit visual target.
|
||||
const SUN_ROTATION_DEGREES := Vector3(-44.0, 162.0, 0.0)
|
||||
const NOISE_SEED := 7
|
||||
## Calm water: ripple drift stays well under 0.05 texture units per second.
|
||||
const SURFACE_DRIFT_SPEED := 0.012
|
||||
## Sunlight only breaks through a pool of surface a few tens of metres ahead of
|
||||
## the submarine (world -Z), so the shafts land in front of the porthole.
|
||||
const SHAFT_POOL_CENTER_XZ_M := Vector2(-14.0, -31.0)
|
||||
const SHAFT_POOL_RADIUS_M := 22.0
|
||||
const SHAFT_POOL_EDGE_M := 12.0
|
||||
## Coarser ripples give a few broken oblique shafts rather than a dense fan.
|
||||
const SURFACE_RIPPLE_SCALE := 0.025
|
||||
## The underside of the surface is drawn dark, lit only inside the pool.
|
||||
const SURFACE_CEILING_LIT_SCALE := 0.3
|
||||
|
||||
const SURFACE_OCCLUDER_SHADER := preload(
|
||||
"res://game/world/sites/sunlit_shallows_surface_occluder.gdshader"
|
||||
)
|
||||
const CAUSTIC_FLOOR_SHADER := preload(
|
||||
"res://game/world/sites/sunlit_shallows_caustic_floor.gdshader"
|
||||
)
|
||||
const SURFACE_CEILING_SHADER := preload(
|
||||
"res://game/world/sites/sunlit_shallows_surface_ceiling.gdshader"
|
||||
)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_fix_sun()
|
||||
_add_flat_floor()
|
||||
_add_surface_occluder()
|
||||
_add_surface_ceiling()
|
||||
_add_air_fog_cut()
|
||||
|
||||
|
||||
func _fix_sun() -> void:
|
||||
var sun := get_node_or_null("Sun") as DirectionalLight3D
|
||||
if sun == null:
|
||||
return
|
||||
sun.rotation_degrees = SUN_ROTATION_DEGREES
|
||||
|
||||
|
||||
func _add_flat_floor() -> void:
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(SITE_EXTENT_M, SITE_EXTENT_M)
|
||||
plane.subdivide_width = 1
|
||||
plane.subdivide_depth = 1
|
||||
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = CAUSTIC_FLOOR_SHADER
|
||||
material.set_shader_parameter("caustic_noise", _make_ripple_noise(0.007))
|
||||
|
||||
var body := StaticBody3D.new()
|
||||
body.name = "FlatFloor"
|
||||
body.position = Vector3(0.0, FLOOR_DEPTH_M, 0.0)
|
||||
body.set_meta("sonar_material", 1)
|
||||
body.set_meta("sonar_reflectivity", 0.6)
|
||||
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = "Mesh"
|
||||
mesh_instance.mesh = plane
|
||||
mesh_instance.material_override = material
|
||||
body.add_child(mesh_instance)
|
||||
|
||||
var collision := CollisionShape3D.new()
|
||||
collision.name = "Collision"
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = Vector3(SITE_EXTENT_M, 1.0, SITE_EXTENT_M)
|
||||
collision.shape = shape
|
||||
collision.position = Vector3(0.0, -0.5, 0.0)
|
||||
body.add_child(collision)
|
||||
add_child(body)
|
||||
|
||||
|
||||
func _add_surface_occluder() -> void:
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(SITE_EXTENT_M, SITE_EXTENT_M)
|
||||
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = SURFACE_OCCLUDER_SHADER
|
||||
material.set_shader_parameter("ripple_noise", _make_ripple_noise(0.009))
|
||||
material.set_shader_parameter("drift_speed", SURFACE_DRIFT_SPEED)
|
||||
material.set_shader_parameter("ripple_scale", SURFACE_RIPPLE_SCALE)
|
||||
material.set_shader_parameter("pool_center_xz", SHAFT_POOL_CENTER_XZ_M)
|
||||
material.set_shader_parameter("pool_radius", SHAFT_POOL_RADIUS_M)
|
||||
material.set_shader_parameter("pool_edge", SHAFT_POOL_EDGE_M)
|
||||
|
||||
var occluder := MeshInstance3D.new()
|
||||
occluder.name = "CalmSurfaceOccluder"
|
||||
occluder.position = Vector3(0.0, SURFACE_LEVEL_M, 0.0)
|
||||
occluder.mesh = plane
|
||||
occluder.material_override = material
|
||||
occluder.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
|
||||
add_child(occluder)
|
||||
|
||||
|
||||
func _add_surface_ceiling() -> void:
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(SITE_EXTENT_M * 1.5, SITE_EXTENT_M * 1.5)
|
||||
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = SURFACE_CEILING_SHADER
|
||||
material.set_shader_parameter("ripple_noise", _make_ripple_noise(0.009))
|
||||
material.set_shader_parameter("drift_speed", SURFACE_DRIFT_SPEED)
|
||||
material.set_shader_parameter("pool_center_xz", SHAFT_POOL_CENTER_XZ_M)
|
||||
material.set_shader_parameter("pool_radius", SHAFT_POOL_RADIUS_M)
|
||||
material.set_shader_parameter("lit_scale", SURFACE_CEILING_LIT_SCALE)
|
||||
|
||||
var ceiling := MeshInstance3D.new()
|
||||
ceiling.name = "SurfaceCeiling"
|
||||
ceiling.position = Vector3(0.0, SURFACE_LEVEL_M, 0.0)
|
||||
ceiling.mesh = plane
|
||||
ceiling.material_override = material
|
||||
ceiling.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(ceiling)
|
||||
|
||||
|
||||
func _add_air_fog_cut() -> void:
|
||||
var air := FogVolume.new()
|
||||
air.name = "AirFogCut"
|
||||
air.shape = RenderingServer.FOG_VOLUME_SHAPE_BOX
|
||||
air.size = Vector3(SITE_EXTENT_M * 1.5, AIR_VOLUME_HEIGHT_M, SITE_EXTENT_M * 1.5)
|
||||
air.position = Vector3(0.0, SURFACE_LEVEL_M + AIR_VOLUME_HEIGHT_M * 0.5, 0.0)
|
||||
var material := FogMaterial.new()
|
||||
material.density = -VOLUMETRIC_FOG_DENSITY
|
||||
material.edge_fade = 0.0
|
||||
air.material = material
|
||||
add_child(air)
|
||||
|
||||
|
||||
func _make_ripple_noise(frequency: float) -> NoiseTexture2D:
|
||||
var noise := FastNoiseLite.new()
|
||||
noise.seed = NOISE_SEED
|
||||
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||
noise.frequency = frequency
|
||||
noise.fractal_octaves = 3
|
||||
noise.fractal_lacunarity = 2.1
|
||||
noise.fractal_gain = 0.45
|
||||
|
||||
var texture := NoiseTexture2D.new()
|
||||
texture.width = 512
|
||||
texture.height = 512
|
||||
texture.seamless = true
|
||||
texture.generate_mipmaps = true
|
||||
texture.noise = noise
|
||||
return texture
|
||||
@@ -0,0 +1,64 @@
|
||||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://game/world/sites/sunlit_shallows_environment.gd" id="1_environment"]
|
||||
|
||||
[sub_resource type="Environment" id="EnvironmentSunlitShallows"]
|
||||
background_mode = 1
|
||||
background_color = Color(0.005, 0.03, 0.06, 1)
|
||||
ambient_light_source = 3
|
||||
ambient_light_color = Color(0.14, 0.32, 0.42, 1)
|
||||
ambient_light_energy = 0.22
|
||||
tonemap_mode = 2
|
||||
tonemap_exposure = 0.75
|
||||
glow_enabled = true
|
||||
glow_levels/1 = 0.6
|
||||
glow_levels/2 = 1.0
|
||||
glow_levels/3 = 0.8
|
||||
glow_levels/4 = 0.3
|
||||
glow_intensity = 0.35
|
||||
glow_blend_mode = 0
|
||||
glow_hdr_threshold = 1.6
|
||||
fog_enabled = true
|
||||
fog_light_color = Color(0.04, 0.16, 0.24, 1)
|
||||
fog_light_energy = 0.12
|
||||
fog_sun_scatter = 0.0
|
||||
fog_density = 0.003
|
||||
fog_aerial_perspective = 0.6
|
||||
fog_sky_affect = 1.0
|
||||
volumetric_fog_enabled = true
|
||||
volumetric_fog_density = 0.035
|
||||
volumetric_fog_albedo = Color(0.26, 0.52, 0.7, 1)
|
||||
volumetric_fog_emission_energy = 0.0
|
||||
volumetric_fog_anisotropy = 0.55
|
||||
volumetric_fog_length = 150.0
|
||||
volumetric_fog_detail_spread = 1.6
|
||||
volumetric_fog_ambient_inject = 0.015
|
||||
volumetric_fog_sky_affect = 1.0
|
||||
volumetric_fog_temporal_reprojection_enabled = true
|
||||
volumetric_fog_temporal_reprojection_amount = 0.92
|
||||
|
||||
[node name="SunlitShallowsEnvironment" type="Node3D"]
|
||||
script = ExtResource("1_environment")
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("EnvironmentSunlitShallows")
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="."]
|
||||
rotation_degrees = Vector3(-44, 162, 0)
|
||||
light_color = Color(0.86, 0.95, 1, 1)
|
||||
light_energy = 1.0
|
||||
light_volumetric_fog_energy = 1.4
|
||||
shadow_enabled = true
|
||||
shadow_bias = 0.04
|
||||
shadow_normal_bias = 1.5
|
||||
shadow_blur = 2.5
|
||||
directional_shadow_mode = 1
|
||||
directional_shadow_split_1 = 0.2
|
||||
directional_shadow_blend_splits = true
|
||||
directional_shadow_max_distance = 160.0
|
||||
|
||||
[node name="ObserverCamera" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9962, 0.0872, 0, -0.0872, 0.9962, 0, -30, 0)
|
||||
fov = 72.0
|
||||
near = 0.1
|
||||
far = 400.0
|
||||
@@ -0,0 +1,32 @@
|
||||
// Underside of the calm surface as seen from below: dark everywhere, lit with
|
||||
// drifting crests only inside the pool where the occluder lets the sun through.
|
||||
// Purely visual; shadows and shafts come from the occluder mesh.
|
||||
shader_type spatial;
|
||||
render_mode unshaded, cull_disabled;
|
||||
|
||||
uniform sampler2D ripple_noise : repeat_enable, filter_linear_mipmap;
|
||||
uniform vec2 pool_center_xz = vec2(-14.0, -31.0);
|
||||
uniform float pool_radius = 22.0;
|
||||
uniform float ripple_scale = 0.03;
|
||||
uniform float drift_speed = 0.012;
|
||||
uniform float lit_scale : hint_range(0.0, 1.0) = 0.3;
|
||||
uniform vec3 dark_color : source_color = vec3(0.01, 0.05, 0.09);
|
||||
uniform vec3 lit_color : source_color = vec3(0.5, 0.85, 1.0);
|
||||
|
||||
varying vec2 world_xz;
|
||||
|
||||
void vertex() {
|
||||
world_xz = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 slow_drift = vec2(TIME * drift_speed);
|
||||
vec2 cross_drift = vec2(-TIME * drift_speed * 0.7);
|
||||
float broad = texture(ripple_noise, world_xz * ripple_scale + slow_drift).r;
|
||||
float fine = texture(ripple_noise, world_xz * ripple_scale * 1.9 + cross_drift).r;
|
||||
float crest = smoothstep(0.45, 0.7, broad * 0.65 + fine * 0.35);
|
||||
float pool = 1.0 - smoothstep(
|
||||
pool_radius * 0.6, pool_radius * 1.6, distance(world_xz, pool_center_xz)
|
||||
);
|
||||
ALBEDO = mix(dark_color, lit_color * (0.35 + crest) * lit_scale, pool);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Calm-water surface seen only through its shadow: the mesh is invisible
|
||||
// (SHADOW_ONLY) and the alpha-scissor cut-outs let the fixed daytime sun carve
|
||||
// light shafts into the volumetric fog below.
|
||||
shader_type spatial;
|
||||
render_mode cull_disabled, unshaded, depth_prepass_alpha;
|
||||
|
||||
uniform sampler2D ripple_noise : repeat_enable, filter_linear_mipmap;
|
||||
uniform float ripple_scale = 0.025;
|
||||
uniform float drift_speed = 0.012;
|
||||
uniform float coverage : hint_range(0.0, 1.0) = 0.30;
|
||||
// World-space pool where the surface lets sunlight through. Outside it the
|
||||
// surface is fully opaque, so shafts only form in that patch of water.
|
||||
uniform vec2 pool_center_xz = vec2(-14.0, -31.0);
|
||||
uniform float pool_radius = 22.0;
|
||||
uniform float pool_edge = 12.0;
|
||||
|
||||
varying vec2 world_xz;
|
||||
|
||||
void vertex() {
|
||||
world_xz = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xz;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 slow_drift = TIME * drift_speed * vec2(1.0, 0.35);
|
||||
vec2 cross_drift = TIME * drift_speed * vec2(-0.4, 0.9);
|
||||
float broad = texture(ripple_noise, world_xz * ripple_scale + slow_drift).r;
|
||||
float fine = texture(ripple_noise, world_xz * ripple_scale * 1.9 + cross_drift).r;
|
||||
float raw_ripple = broad * 0.65 + fine * 0.35;
|
||||
// Inside the pool the crests are sharpened so the gaps cut crisp shafts.
|
||||
float pool_ripple = smoothstep(0.35, 0.65, raw_ripple);
|
||||
// Outside the pool only the deepest troughs open: a dim dapple, not darkness.
|
||||
float dim_ripple = smoothstep(0.16, 0.30, raw_ripple);
|
||||
float pool = 1.0 - smoothstep(
|
||||
pool_radius - pool_edge, pool_radius, distance(world_xz, pool_center_xz)
|
||||
);
|
||||
ALBEDO = vec3(0.0);
|
||||
ALPHA = mix(dim_ripple, pool_ripple, pool);
|
||||
ALPHA_SCISSOR_THRESHOLD = coverage;
|
||||
}
|
||||
Reference in new issue
Block a user