feat(world): triplanar seabed shader with procedural normal maps and detail shading

Rock is normal-mapped triplanar in world space at two scales, sand carries an
oriented ripple normal map on the xz plane, and a grain map drives detail
occlusion, albedo mottling and roughness. The three maps are seamless,
seeded, mipmapped textures built synchronously by SeabedDetailTextures, so
no asset is needed and builds are deterministic. The mesh has no tangents:
perturbed normals are computed in world space and written to NORMAL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Fable 5.1 committed 2026-10-06 23:05:33 +02:00
1 parent f22a5e69aa
commit 17f58912c1
12 files changed
+208 -16

No files matched your search

@@ -2,14 +2,17 @@ class_name BathymetricTerrainActor
extends StaticBody3D
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
const SeabedDetailTextures = preload("res://game/world/sites/seabed_detail_textures.gd")
const TERRAIN_SHADER = preload("res://game/world/sites/seabed_terrain.gdshader")
const TILE_CELLS := 64
const VISIBILITY_RANGE_END_M := 420.0
const VISIBILITY_RANGE_MARGIN_M := 60.0
const DETAIL_TEXTURE_SEED := 4102
var _field: TerrainHeightField
var _material: ShaderMaterial
var _detail_textures: SeabedDetailTextures
var _tiles_root: Node3D
var _collision: CollisionShape3D
@@ -19,8 +22,13 @@ func configure(field: TerrainHeightField) -> String:
return "terrain field is missing"
_clear()
_field = field
if _detail_textures == null:
_detail_textures = SeabedDetailTextures.build(DETAIL_TEXTURE_SEED)
_material = ShaderMaterial.new()
_material.shader = TERRAIN_SHADER
_material.set_shader_parameter(&"sand_normal_map", _detail_textures.sand_normal)
_material.set_shader_parameter(&"rock_normal_map", _detail_textures.rock_normal)
_material.set_shader_parameter(&"rock_grain_map", _detail_textures.rock_grain)
_build_tiles()
_build_collision()
return ""
@@ -30,6 +38,10 @@ func height_field() -> TerrainHeightField:
return _field
func terrain_material() -> ShaderMaterial:
return _material
func tile_count() -> int:
return 0 if _tiles_root == null else _tiles_root.get_child_count()