Terrain des sites de plongée : champ de hauteur authoré, Banc du Vigilant, formes variées et rendu sous-marin #4
No files matched your search
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 131 KiB |
@@ -251,10 +251,15 @@ de contacts existant les sépare du fond.
|
|||||||
- Brouillard, lumière directionnelle et particules de la scène de test
|
- Brouillard, lumière directionnelle et particules de la scène de test
|
||||||
actuelle sont conservés et réglés par site.
|
actuelle sont conservés et réglés par site.
|
||||||
|
|
||||||
**Écart d'implémentation (graybox)** : le shader livré n'est pas triplanaire.
|
**État du 2026-10-06 (demande d'Olivier)** : le shader mélange sable et roche
|
||||||
Il mélange sable et roche par la pente à partir de 20° (sable sous 20°, roche
|
par la pente à partir de 20° (sable sous 20°, roche au-dessus de 40°, fondu
|
||||||
au-dessus de 40°, fondu entre ; le matériau sonar bascule à 20°) et rend les
|
entre ; le matériau sonar bascule à 20°). La roche est en normal map
|
||||||
rides par modulation d'albédo, sans normal map. À revoir en tranche 6.
|
**triplanaire** monde (deux échelles, 7 m et 1,7 m) ; le sable porte une normal
|
||||||
|
map de rides orientée par la houle sur le plan xz ; une carte de grain sert
|
||||||
|
d'occlusion de détail et de marbrure d'albédo. Les trois cartes sont
|
||||||
|
procédurales, sans asset, construites par `SeabedDetailTextures` (bruit seedé,
|
||||||
|
sans couture, mipmaps) dans la couche monde. Le maillage n'a pas de tangentes :
|
||||||
|
les normales perturbées sont calculées en espace monde dans le shader.
|
||||||
|
|
||||||
## 8. Architecture proposée
|
## 8. Architecture proposée
|
||||||
|
|
||||||
|
|||||||
@@ -182,9 +182,10 @@ Plan de masse (nord en haut) :
|
|||||||
`terrain_showcase_canyon.png` (gradins et fond ridé), `_crevasse.png`,
|
`terrain_showcase_canyon.png` (gradins et fond ridé), `_crevasse.png`,
|
||||||
`_boulders.png`, `_spurs.png`, `_escarpment.png` (face de la falaise vue du
|
`_boulders.png`, `_spurs.png`, `_escarpment.png` (face de la falaise vue du
|
||||||
pied) ; pour le Banc du Vigilant recuit, `zone1_vigilant_tombant.png`
|
pied) ; pour le Banc du Vigilant recuit, `zone1_vigilant_tombant.png`
|
||||||
(gradins du tombant) et `zone1_vigilant_plateau.png` (plateau ondulé). Le
|
(gradins du tombant) et `zone1_vigilant_plateau.png` (plateau ondulé). Les
|
||||||
rendu (brouillard, rides en albédo, absence d'ombrage de détail) n'est pas
|
captures sont faites avec le shader à normal maps et ombrage de détail
|
||||||
l'objet de ce brief : à ce stade, le relief se lit surtout au sonar.
|
(brief du Banc du Vigilant, §7) ; brouillard et lumière restent ceux de la
|
||||||
|
scène de site.
|
||||||
|
|
||||||
## 8. Décisions laissées à Olivier
|
## 8. Décisions laissées à Olivier
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ class_name BathymetricTerrainActor
|
|||||||
extends StaticBody3D
|
extends StaticBody3D
|
||||||
|
|
||||||
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
|
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 TERRAIN_SHADER = preload("res://game/world/sites/seabed_terrain.gdshader")
|
||||||
|
|
||||||
const TILE_CELLS := 64
|
const TILE_CELLS := 64
|
||||||
const VISIBILITY_RANGE_END_M := 420.0
|
const VISIBILITY_RANGE_END_M := 420.0
|
||||||
const VISIBILITY_RANGE_MARGIN_M := 60.0
|
const VISIBILITY_RANGE_MARGIN_M := 60.0
|
||||||
|
const DETAIL_TEXTURE_SEED := 4102
|
||||||
|
|
||||||
var _field: TerrainHeightField
|
var _field: TerrainHeightField
|
||||||
var _material: ShaderMaterial
|
var _material: ShaderMaterial
|
||||||
|
var _detail_textures: SeabedDetailTextures
|
||||||
var _tiles_root: Node3D
|
var _tiles_root: Node3D
|
||||||
var _collision: CollisionShape3D
|
var _collision: CollisionShape3D
|
||||||
|
|
||||||
@@ -19,8 +22,13 @@ func configure(field: TerrainHeightField) -> String:
|
|||||||
return "terrain field is missing"
|
return "terrain field is missing"
|
||||||
_clear()
|
_clear()
|
||||||
_field = field
|
_field = field
|
||||||
|
if _detail_textures == null:
|
||||||
|
_detail_textures = SeabedDetailTextures.build(DETAIL_TEXTURE_SEED)
|
||||||
_material = ShaderMaterial.new()
|
_material = ShaderMaterial.new()
|
||||||
_material.shader = TERRAIN_SHADER
|
_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_tiles()
|
||||||
_build_collision()
|
_build_collision()
|
||||||
return ""
|
return ""
|
||||||
@@ -30,6 +38,10 @@ func height_field() -> TerrainHeightField:
|
|||||||
return _field
|
return _field
|
||||||
|
|
||||||
|
|
||||||
|
func terrain_material() -> ShaderMaterial:
|
||||||
|
return _material
|
||||||
|
|
||||||
|
|
||||||
func tile_count() -> int:
|
func tile_count() -> int:
|
||||||
return 0 if _tiles_root == null else _tiles_root.get_child_count()
|
return 0 if _tiles_root == null else _tiles_root.get_child_count()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
class_name SeabedDetailTextures
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Procedural, seamless detail maps for the seabed shader, built synchronously from
|
||||||
|
## seeded noise so no texture asset is needed and every build is identical:
|
||||||
|
## an oriented sand ripple normal map, a rock normal map and the rock grain map
|
||||||
|
## (height, also read as occlusion and albedo mottling).
|
||||||
|
|
||||||
|
const TEXTURE_SIZE := 256
|
||||||
|
const RIPPLES_PER_TILE := 2.0
|
||||||
|
const RIPPLE_WARP := 0.22
|
||||||
|
const ROCK_BUMP_SCALE := 6.0
|
||||||
|
const SAND_BUMP_SCALE := 3.0
|
||||||
|
|
||||||
|
var sand_normal: ImageTexture
|
||||||
|
var rock_normal: ImageTexture
|
||||||
|
var rock_grain: ImageTexture
|
||||||
|
|
||||||
|
|
||||||
|
static func build(seed: int) -> SeabedDetailTextures:
|
||||||
|
var result := SeabedDetailTextures.new()
|
||||||
|
var rock_height := _rock_height_image(seed)
|
||||||
|
result.rock_grain = _texture_from(rock_height)
|
||||||
|
var rock_bumps := rock_height.duplicate() as Image
|
||||||
|
rock_bumps.bump_map_to_normal_map(ROCK_BUMP_SCALE)
|
||||||
|
result.rock_normal = _texture_from(rock_bumps)
|
||||||
|
var sand_height := _sand_height_image(seed + 1)
|
||||||
|
sand_height.bump_map_to_normal_map(SAND_BUMP_SCALE)
|
||||||
|
result.sand_normal = _texture_from(sand_height)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
## Ridged fractal noise: sharp crests and pits, seamless across the tile.
|
||||||
|
static func _rock_height_image(seed: int) -> Image:
|
||||||
|
var noise := FastNoiseLite.new()
|
||||||
|
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||||
|
noise.seed = seed
|
||||||
|
noise.frequency = 0.012
|
||||||
|
noise.fractal_type = FastNoiseLite.FRACTAL_RIDGED
|
||||||
|
noise.fractal_octaves = 5
|
||||||
|
return noise.get_seamless_image(TEXTURE_SIZE, TEXTURE_SIZE, false, false, 0.1, true)
|
||||||
|
|
||||||
|
|
||||||
|
## Ripples across the tile's v axis, warped and grained by noise. Asymmetric profile
|
||||||
|
## (gentle stoss side, steep lee side) as on a swell-built sand bed.
|
||||||
|
static func _sand_height_image(seed: int) -> Image:
|
||||||
|
var warp := FastNoiseLite.new()
|
||||||
|
warp.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||||
|
warp.seed = seed
|
||||||
|
warp.frequency = 0.02
|
||||||
|
warp.fractal_octaves = 2
|
||||||
|
var warp_image := warp.get_seamless_image(TEXTURE_SIZE, TEXTURE_SIZE, false, false, 0.1, true)
|
||||||
|
var grain := FastNoiseLite.new()
|
||||||
|
grain.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||||
|
grain.seed = seed + 7
|
||||||
|
grain.frequency = 0.15
|
||||||
|
grain.fractal_octaves = 3
|
||||||
|
var grain_image := grain.get_seamless_image(TEXTURE_SIZE, TEXTURE_SIZE, false, false, 0.1, true)
|
||||||
|
var image := Image.create(TEXTURE_SIZE, TEXTURE_SIZE, false, Image.FORMAT_L8)
|
||||||
|
for y in TEXTURE_SIZE:
|
||||||
|
for x in TEXTURE_SIZE:
|
||||||
|
var phase := (float(y) / float(TEXTURE_SIZE)) * RIPPLES_PER_TILE
|
||||||
|
phase += (warp_image.get_pixel(x, y).r - 0.5) * RIPPLE_WARP
|
||||||
|
var fraction := phase - floorf(phase)
|
||||||
|
var crest := fraction / 0.7 if fraction < 0.7 else (1.0 - fraction) / 0.3
|
||||||
|
var height := crest * 0.8 + grain_image.get_pixel(x, y).r * 0.2
|
||||||
|
image.set_pixel(x, y, Color(height, height, height))
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
static func _texture_from(image: Image) -> ImageTexture:
|
||||||
|
image.generate_mipmaps()
|
||||||
|
return ImageTexture.create_from_image(image)
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
render_mode depth_draw_opaque;
|
render_mode depth_draw_opaque;
|
||||||
|
|
||||||
|
// Sand / rock split by slope, matching the sonar material rule (sand under 20 deg,
|
||||||
|
// rock over 40 deg). Rock is normal-mapped triplanar in world space so cliffs and
|
||||||
|
// ledges keep their detail; sand carries an oriented ripple normal map on the xz plane.
|
||||||
|
// The mesh has no tangents: perturbed normals are built in world space and written
|
||||||
|
// to NORMAL in view space. Detail shading comes from the normal maps, a grain map
|
||||||
|
// used as ambient occlusion in the pits, and albedo mottling.
|
||||||
|
|
||||||
uniform vec3 sand_color : source_color = vec3(0.50, 0.46, 0.34);
|
uniform vec3 sand_color : source_color = vec3(0.50, 0.46, 0.34);
|
||||||
uniform vec3 rock_color : source_color = vec3(0.21, 0.20, 0.18);
|
uniform vec3 rock_color : source_color = vec3(0.21, 0.20, 0.18);
|
||||||
uniform vec3 deep_tint : source_color = vec3(0.04, 0.09, 0.15);
|
uniform vec3 deep_tint : source_color = vec3(0.04, 0.09, 0.15);
|
||||||
@@ -8,8 +15,19 @@ uniform float sand_slope_cosine = 0.9397; // cos(20 deg)
|
|||||||
uniform float rock_slope_cosine = 0.7660; // cos(40 deg)
|
uniform float rock_slope_cosine = 0.7660; // cos(40 deg)
|
||||||
uniform float depth_tint_start_m = 20.0;
|
uniform float depth_tint_start_m = 20.0;
|
||||||
uniform float depth_tint_end_m = 90.0;
|
uniform float depth_tint_end_m = 90.0;
|
||||||
uniform float ripple_wavelength_m = 1.6;
|
|
||||||
uniform float ripple_contrast = 0.12;
|
uniform sampler2D sand_normal_map : hint_normal, repeat_enable, filter_linear_mipmap;
|
||||||
|
uniform sampler2D rock_normal_map : hint_normal, repeat_enable, filter_linear_mipmap;
|
||||||
|
uniform sampler2D rock_grain_map : hint_default_white, repeat_enable, filter_linear_mipmap;
|
||||||
|
uniform float sand_tile_m = 3.2;
|
||||||
|
uniform float rock_tile_m = 7.0;
|
||||||
|
uniform float rock_grain_tile_m = 1.7;
|
||||||
|
uniform float sand_normal_strength = 1.0;
|
||||||
|
uniform float rock_normal_strength = 1.1;
|
||||||
|
uniform float rock_grain_normal_strength = 0.45;
|
||||||
|
uniform float grain_ao_strength = 0.5;
|
||||||
|
uniform float grain_albedo_contrast = 0.45;
|
||||||
|
uniform vec2 ripple_direction = vec2(0.8, 0.6);
|
||||||
|
|
||||||
varying vec3 world_position;
|
varying vec3 world_position;
|
||||||
varying vec3 world_normal;
|
varying vec3 world_normal;
|
||||||
@@ -19,17 +37,75 @@ void vertex() {
|
|||||||
world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vec3 unpack_normal(vec4 sample) {
|
||||||
|
return sample.xyz * 2.0 - 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triplanar normal perturbation (UDN blend): each plane's tangent-space xy is added
|
||||||
|
// onto the world normal along that plane's axes, weighted by how much the surface
|
||||||
|
// faces the plane.
|
||||||
|
vec3 triplanar_normal(sampler2D map, vec3 position, vec3 normal, float strength) {
|
||||||
|
vec3 weights = pow(abs(normal), vec3(4.0));
|
||||||
|
weights /= weights.x + weights.y + weights.z;
|
||||||
|
vec3 tx = unpack_normal(texture(map, position.zy));
|
||||||
|
vec3 ty = unpack_normal(texture(map, position.xz));
|
||||||
|
vec3 tz = unpack_normal(texture(map, position.xy));
|
||||||
|
vec3 perturbation = (
|
||||||
|
vec3(0.0, tx.y, tx.x) * weights.x
|
||||||
|
+ vec3(ty.x, 0.0, ty.y) * weights.y
|
||||||
|
+ vec3(tz.x, tz.y, 0.0) * weights.z
|
||||||
|
) * strength;
|
||||||
|
return normalize(normal + perturbation);
|
||||||
|
}
|
||||||
|
|
||||||
|
float triplanar_grain(sampler2D map, vec3 position, vec3 normal) {
|
||||||
|
vec3 weights = pow(abs(normal), vec3(4.0));
|
||||||
|
weights /= weights.x + weights.y + weights.z;
|
||||||
|
return texture(map, position.zy).r * weights.x
|
||||||
|
+ texture(map, position.xz).r * weights.y
|
||||||
|
+ texture(map, position.xy).r * weights.z;
|
||||||
|
}
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
float up = clamp(world_normal.y, 0.0, 1.0);
|
vec3 n = normalize(world_normal);
|
||||||
|
float up = clamp(n.y, 0.0, 1.0);
|
||||||
float rockness = 1.0 - smoothstep(rock_slope_cosine, sand_slope_cosine, up);
|
float rockness = 1.0 - smoothstep(rock_slope_cosine, sand_slope_cosine, up);
|
||||||
vec3 albedo = mix(sand_color, rock_color, rockness);
|
|
||||||
float phase = (world_position.x * 0.8 + world_position.z * 0.6) * TAU / ripple_wavelength_m;
|
// Sand: ripple normal map rotated to the swell direction, on the xz plane.
|
||||||
float ripple = sin(phase) * ripple_contrast * (1.0 - rockness);
|
vec2 ripple_axis = normalize(ripple_direction);
|
||||||
albedo *= 1.0 + ripple;
|
vec2 sand_uv = vec2(
|
||||||
|
dot(world_position.xz, ripple_axis),
|
||||||
|
dot(world_position.xz, vec2(-ripple_axis.y, ripple_axis.x))
|
||||||
|
) / sand_tile_m;
|
||||||
|
vec3 sand_tangent = unpack_normal(texture(sand_normal_map, sand_uv));
|
||||||
|
vec3 sand_perturbation = (
|
||||||
|
vec3(ripple_axis.x, 0.0, ripple_axis.y) * sand_tangent.x
|
||||||
|
+ vec3(-ripple_axis.y, 0.0, ripple_axis.x) * sand_tangent.y
|
||||||
|
) * sand_normal_strength;
|
||||||
|
vec3 sand_normal = normalize(n + sand_perturbation);
|
||||||
|
|
||||||
|
// Rock: coarse triplanar normal map plus fine grain, both in world space.
|
||||||
|
vec3 rock_normal = triplanar_normal(rock_normal_map, world_position / rock_tile_m, n, rock_normal_strength);
|
||||||
|
rock_normal = triplanar_normal(rock_normal_map, world_position / rock_grain_tile_m, rock_normal, rock_grain_normal_strength);
|
||||||
|
float grain = triplanar_grain(rock_grain_map, world_position / rock_grain_tile_m, n);
|
||||||
|
|
||||||
|
vec3 shading_normal = normalize(mix(sand_normal, rock_normal, rockness));
|
||||||
|
NORMAL = normalize((VIEW_MATRIX * vec4(shading_normal, 0.0)).xyz);
|
||||||
|
|
||||||
|
// Albedo: mottled rock, lightly grained sand, both tinted with depth.
|
||||||
|
vec3 rock_albedo = rock_color * (1.0 + (grain - 0.5) * 2.0 * grain_albedo_contrast);
|
||||||
|
vec3 sand_albedo = sand_color * (1.0 + (grain - 0.5) * 0.3);
|
||||||
|
vec3 albedo = mix(sand_albedo, rock_albedo, rockness);
|
||||||
float depth_m = -world_position.y;
|
float depth_m = -world_position.y;
|
||||||
float tint = smoothstep(depth_tint_start_m, depth_tint_end_m, depth_m);
|
float tint = smoothstep(depth_tint_start_m, depth_tint_end_m, depth_m);
|
||||||
albedo = mix(albedo, deep_tint, tint * 0.6);
|
albedo = mix(albedo, deep_tint, tint * 0.6);
|
||||||
ALBEDO = albedo;
|
ALBEDO = albedo;
|
||||||
ROUGHNESS = mix(0.95, 0.82, rockness);
|
|
||||||
|
// Detail shading: pits of the grain map and ripple troughs read as occlusion.
|
||||||
|
float rock_ao = mix(1.0, 0.45 + 0.55 * grain, grain_ao_strength);
|
||||||
|
float sand_ao = mix(1.0, 0.8 + 0.2 * clamp(sand_tangent.z, 0.0, 1.0), grain_ao_strength * 0.6);
|
||||||
|
AO = mix(sand_ao, rock_ao, rockness);
|
||||||
|
AO_LIGHT_AFFECT = 0.35;
|
||||||
|
ROUGHNESS = mix(0.95, 0.78 + 0.12 * grain, rockness);
|
||||||
METALLIC = 0.0;
|
METALLIC = 0.0;
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ const BathymetricTerrainActor = preload(
|
|||||||
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
||||||
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
||||||
const DiveSite = preload("res://game/world/sites/dive_site.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 SonarCaptureRequest = preload("res://game/application/sonar/sonar_capture_request.gd")
|
||||||
const ReferenceBathymetryAcquisition = preload(
|
const ReferenceBathymetryAcquisition = preload(
|
||||||
"res://game/world/sonar/reference_bathymetry_acquisition.gd"
|
"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 ZONE1_SITE_PATH := "res://content/sites/zone1_vigilant.tres"
|
||||||
const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres"
|
const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres"
|
||||||
const GROOVE_PROBE_OFFSET_M := 25.0
|
const GROOVE_PROBE_OFFSET_M := 25.0
|
||||||
const TEST_COUNT := 36
|
const TEST_COUNT := 40
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
var _failures: Array[String] = []
|
||||||
|
|
||||||
@@ -74,6 +75,30 @@ func _test_actor_matches_field() -> void:
|
|||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
||||||
_expect(actor.tile_count() == 2, "80 x 64 cells split into 2 x 1 tiles of at most 64 cells")
|
_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 first_tile := actor.get_node("Tiles").get_child(0) as MeshInstance3D
|
||||||
var arrays: Array = first_tile.mesh.surface_get_arrays(0)
|
var arrays: Array = first_tile.mesh.surface_get_arrays(0)
|
||||||
var tile_vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
var tile_vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||||
|
|||||||