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