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:
1 parent
f22a5e69aa
commit
17f58912c1
12 files changed
+208
-16
No files matched your search
@@ -9,6 +9,7 @@ const BathymetricTerrainActor = preload(
|
||||
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
||||
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
||||
const DiveSite = preload("res://game/world/sites/dive_site.gd")
|
||||
const SeabedDetailTextures = preload("res://game/world/sites/seabed_detail_textures.gd")
|
||||
const SonarCaptureRequest = preload("res://game/application/sonar/sonar_capture_request.gd")
|
||||
const ReferenceBathymetryAcquisition = preload(
|
||||
"res://game/world/sonar/reference_bathymetry_acquisition.gd"
|
||||
@@ -18,7 +19,7 @@ const DIVE_SITE_SCENE = preload("res://game/world/sites/dive_site.tscn")
|
||||
const ZONE1_SITE_PATH := "res://content/sites/zone1_vigilant.tres"
|
||||
const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres"
|
||||
const GROOVE_PROBE_OFFSET_M := 25.0
|
||||
const TEST_COUNT := 36
|
||||
const TEST_COUNT := 40
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
@@ -74,6 +75,30 @@ func _test_actor_matches_field() -> void:
|
||||
await physics_frame
|
||||
|
||||
_expect(actor.tile_count() == 2, "80 x 64 cells split into 2 x 1 tiles of at most 64 cells")
|
||||
var material := actor.terrain_material()
|
||||
_expect(
|
||||
material != null
|
||||
and material.get_shader_parameter(&"sand_normal_map") is ImageTexture
|
||||
and material.get_shader_parameter(&"rock_normal_map") is ImageTexture
|
||||
and material.get_shader_parameter(&"rock_grain_map") is ImageTexture,
|
||||
"terrain material carries the three procedural detail maps"
|
||||
)
|
||||
var first_textures := SeabedDetailTextures.build(7)
|
||||
var second_textures := SeabedDetailTextures.build(7)
|
||||
_expect(
|
||||
first_textures.rock_normal.get_image().get_data() == second_textures.rock_normal.get_image().get_data()
|
||||
and first_textures.sand_normal.get_image().get_data() == second_textures.sand_normal.get_image().get_data(),
|
||||
"detail maps are deterministic for a seed"
|
||||
)
|
||||
_expect(
|
||||
first_textures.rock_normal.get_width() == SeabedDetailTextures.TEXTURE_SIZE
|
||||
and first_textures.rock_normal.get_image().has_mipmaps(),
|
||||
"detail maps are tile-sized with mipmaps"
|
||||
)
|
||||
var sand_image := first_textures.sand_normal.get_image()
|
||||
var left := sand_image.get_pixel(0, 100)
|
||||
var right := sand_image.get_pixel(SeabedDetailTextures.TEXTURE_SIZE - 1, 100)
|
||||
_expect(absf(left.r - right.r) < 0.25 and absf(left.g - right.g) < 0.25, "sand ripple map wraps across its seam")
|
||||
var first_tile := actor.get_node("Tiles").get_child(0) as MeshInstance3D
|
||||
var arrays: Array = first_tile.mesh.surface_get_arrays(0)
|
||||
var tile_vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
|
||||
Reference in new issue
Block a user