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)