Quality Gate / Quality Gate (pull_request) Failing after 2m58s
SeabedFloraActor scatters seagrass tufts on flat sand in the lit band and sea fans on rock between 15 and 60 m, deterministically from the site id, one MultiMesh per tile and kind, with procedural alpha-cut meshes and textures (SeabedFloraAssets), vertex sway and wrapped lighting, no collision and no sonar presence. Placement reads the field arrays directly and runs on the worker thread pool. The seabed shader grows turf algae on sunlit rock, coralline crusts in patches and encrusting sponges in the shade, fading out between 40 and 75 m. The water-column optics move to a shared shader include. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
123 lines
4.8 KiB
GDScript
123 lines
4.8 KiB
GDScript
class_name SeabedFloraAssets
|
|
extends RefCounted
|
|
|
|
## Procedural meshes and alpha-cut textures for the seabed flora, built synchronously
|
|
## from a seed so no asset is needed and every build is identical: a seagrass tuft
|
|
## (three crossed unit quads carrying a strap-blade texture) and a sea fan (one unit
|
|
## quad carrying a branching net texture). Meshes are unit sized, y from 0 to 1.
|
|
|
|
const BLADE_TEXTURE_WIDTH := 128
|
|
const BLADE_TEXTURE_HEIGHT := 256
|
|
const BLADE_COUNT := 5
|
|
const FAN_TEXTURE_SIZE := 256
|
|
|
|
var tuft_mesh: ArrayMesh
|
|
var fan_mesh: ArrayMesh
|
|
var blade_texture: ImageTexture
|
|
var fan_texture: ImageTexture
|
|
|
|
|
|
static func build(seed: int) -> SeabedFloraAssets:
|
|
var result := SeabedFloraAssets.new()
|
|
result.blade_texture = _texture_from(_blade_image(seed))
|
|
result.fan_texture = _texture_from(_fan_image(seed + 1))
|
|
result.tuft_mesh = _crossed_quads(3)
|
|
result.fan_mesh = _crossed_quads(1)
|
|
return result
|
|
|
|
|
|
## Vertical unit quads crossed around the y axis, each 1 wide and 1 tall, UV mapped
|
|
## so the texture's bottom row sits on the ground.
|
|
static func _crossed_quads(count: int) -> ArrayMesh:
|
|
var vertices := PackedVector3Array()
|
|
var normals := PackedVector3Array()
|
|
var uvs := PackedVector2Array()
|
|
var indices := PackedInt32Array()
|
|
for quad in count:
|
|
var angle := PI * float(quad) / float(count)
|
|
var right := Vector3(cos(angle), 0.0, sin(angle)) * 0.5
|
|
var normal := Vector3(-sin(angle), 0.0, cos(angle))
|
|
var base := vertices.size()
|
|
vertices.append_array([-right, right, right + Vector3.UP, -right + Vector3.UP])
|
|
for _corner in 4:
|
|
normals.append(normal)
|
|
uvs.append_array([Vector2(0.0, 1.0), Vector2(1.0, 1.0), Vector2(1.0, 0.0), Vector2(0.0, 0.0)])
|
|
indices.append_array([base, base + 2, base + 1, base, base + 3, base + 2])
|
|
var arrays: Array = []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
|
arrays[Mesh.ARRAY_INDEX] = indices
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
return mesh
|
|
|
|
|
|
## A few strap-shaped blades rising from the bottom edge, each with its own curve,
|
|
## width and length, darker towards the edges and the base. Alpha outside the blades.
|
|
static func _blade_image(seed: int) -> Image:
|
|
var generator := RandomNumberGenerator.new()
|
|
generator.seed = seed
|
|
var image := Image.create(BLADE_TEXTURE_WIDTH, BLADE_TEXTURE_HEIGHT, false, Image.FORMAT_RGBA8)
|
|
image.fill(Color(0.0, 0.0, 0.0, 0.0))
|
|
for blade in BLADE_COUNT:
|
|
var root_x := (float(blade) + 0.5) / float(BLADE_COUNT) * float(BLADE_TEXTURE_WIDTH)
|
|
var bend := generator.randf_range(-22.0, 22.0)
|
|
var curve := generator.randf_range(0.6, 1.4)
|
|
var length := generator.randf_range(0.72, 1.0)
|
|
var half_width := generator.randf_range(3.5, 6.0)
|
|
var hue_shift := generator.randf_range(-0.05, 0.05)
|
|
for y in BLADE_TEXTURE_HEIGHT:
|
|
var t := 1.0 - float(y) / float(BLADE_TEXTURE_HEIGHT - 1) # 0 at the base, 1 at the tip
|
|
if t > length:
|
|
continue
|
|
var centre := root_x + bend * pow(t, curve)
|
|
var width := half_width * (1.0 - 0.6 * t * t)
|
|
for x in range(int(floorf(centre - width)), int(ceilf(centre + width)) + 1):
|
|
if x < 0 or x >= BLADE_TEXTURE_WIDTH:
|
|
continue
|
|
var edge := absf(float(x) - centre) / width
|
|
if edge > 1.0:
|
|
continue
|
|
var shade := (0.7 + 0.3 * (1.0 - edge * edge)) * (0.75 + 0.25 * t)
|
|
var color := Color(0.46 + hue_shift, 0.66 + hue_shift * 0.5, 0.3, 1.0) * shade
|
|
color.a = 1.0
|
|
image.set_pixel(x, y, color)
|
|
return image
|
|
|
|
|
|
## A sea fan: branching net inside a half-ellipse rising from a short stem. Ridged
|
|
## noise above a threshold gives the branches, denser near the stem.
|
|
static func _fan_image(seed: int) -> Image:
|
|
var noise := FastNoiseLite.new()
|
|
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
noise.seed = seed
|
|
noise.frequency = 0.045
|
|
noise.fractal_type = FastNoiseLite.FRACTAL_RIDGED
|
|
noise.fractal_octaves = 3
|
|
var image := Image.create(FAN_TEXTURE_SIZE, FAN_TEXTURE_SIZE, false, Image.FORMAT_RGBA8)
|
|
image.fill(Color(0.0, 0.0, 0.0, 0.0))
|
|
var centre_x := float(FAN_TEXTURE_SIZE) * 0.5
|
|
for y in FAN_TEXTURE_SIZE:
|
|
var height := 1.0 - float(y) / float(FAN_TEXTURE_SIZE - 1) # 0 at the base
|
|
for x in FAN_TEXTURE_SIZE:
|
|
var dx := (float(x) - centre_x) / (float(FAN_TEXTURE_SIZE) * 0.48)
|
|
var dy := (height - 0.1) / 0.9
|
|
var inside := dx * dx + dy * dy
|
|
var stem := absf(dx) < 0.03 and height < 0.25
|
|
if inside > 1.0 and not stem:
|
|
continue
|
|
var ridge := noise.get_noise_2d(float(x), float(y)) * 0.5 + 0.5
|
|
var density_threshold := 0.5 + 0.22 * inside
|
|
if not stem and ridge < density_threshold:
|
|
continue
|
|
var shade := 0.7 + 0.3 * ridge
|
|
image.set_pixel(x, y, Color(shade, shade * 0.92, shade * 0.85, 1.0))
|
|
return image
|
|
|
|
|
|
static func _texture_from(image: Image) -> ImageTexture:
|
|
image.generate_mipmaps()
|
|
return ImageTexture.create_from_image(image)
|