Improve generated river terrain

This commit is contained in:
Alexander Sellite 2026-08-24 20:21:49 -04:00
parent 8456093f8c
commit e6f46dcad9
21 changed files with 586 additions and 18 deletions

View file

@ -261,9 +261,11 @@ copies of the entire customization catalog.
### Texture sampling
NETfishing artwork always uses nearest-neighbor sampling. Do not enable linear,
bilinear, trilinear, or anisotropic texture filtering, and do not generate
mipmaps. Shader samplers must declare `filter_nearest`. The
NETfishing artwork uses nearest-neighbor sampling. Do not enable linear,
bilinear, trilinear, or anisotropic texture filtering. Shader samplers must
declare `filter_nearest`. The three canonical repeating terrain textures are
the sole mipmap exception: their projected shader uses nearest-filtered
mipmaps to prevent distance aliasing without smoothing nearby pixels. The
`TextureSamplingPolicy` autoload applies nearest sampling to 2D canvas items,
Sprite3D presentations, imported 3D materials, and dynamically constructed
mesh materials at runtime.

View file

@ -31,6 +31,11 @@ const FACIAL_FEATURE_IMPORT_PATH: String = (
"art/exported/characters/faces/"
)
const FACIAL_FEATURE_SIZE_LIMIT: int = 512
const PROJECTED_TERRAIN_TEXTURE_IMPORTS: Array[String] = [
"res://world/generation/chunks/assets/chunk_0000_grass_lite.png.import",
"res://world/generation/chunks/assets/chunk_0001_sand.png.import",
"res://world/generation/chunks/assets/chunk_0031_dirt.png.import",
]
var apply_changes: bool = false
var verbose: bool = false
@ -153,6 +158,8 @@ func _handle_import_file(import_path: String) -> void:
var desired: Variant = section_data[section_key]
if section_key == "process/size_limit":
desired = _size_limit_for(import_path)
elif section_key == "mipmaps/generate":
desired = _generates_mipmaps(import_path)
var existing: Variant = null
if cfg.has_section(section_name) and cfg.has_section_key(section_name, section_key):
existing = cfg.get_value(section_name, section_key)
@ -187,6 +194,11 @@ func _size_limit_for(import_path: String) -> int:
return int(DEFAULT_PROFILE["params"]["process/size_limit"])
func _generates_mipmaps(import_path: String) -> bool:
var normalized_path := import_path.replace("\\", "/")
return normalized_path in PROJECTED_TERRAIN_TEXTURE_IMPORTS
func print_summary() -> void:
if apply_changes:
_print(

View file

@ -247,6 +247,7 @@ func _validate_generated_region(
assert(_placement_count(placements, "chunk_0034") == 1)
assert(_placement_count(placements, "chunk_0035") == 1)
assert(_placement_count(placements, "chunk_0036") == 1)
assert(_placement_count(placements, "chunk_0038") == 2)
assert(
_placement_count(placements, "chunk_0014")
== (2 if has_secondary_elevation else 0)
@ -285,10 +286,31 @@ func _validate_generated_region(
var river_edge_count := (
_placement_count(placements, "chunk_0024")
+ _placement_count(placements, "chunk_0025")
+ _placement_count(placements, "chunk_0038")
)
assert(river_coordinate_count >= 12)
assert(generator._river_feature_placements.size() == river_coordinate_count)
assert(river_edge_count == river_coordinate_count - 8)
var stone_bridge_specs: Array[Dictionary] = []
for spec: Dictionary in generator._river_feature_placements:
if spec.get("id", &"") == &"chunk_0038":
stone_bridge_specs.append(spec)
assert(stone_bridge_specs.size() == 2)
var first_bridge_coordinate := stone_bridge_specs[0].get(
"coordinate",
Vector2i(-1, -1),
) as Vector2i
var second_bridge_coordinate := stone_bridge_specs[1].get(
"coordinate",
Vector2i(-1, -1),
) as Vector2i
var bridge_delta := second_bridge_coordinate - first_bridge_coordinate
assert(absi(bridge_delta.x) + absi(bridge_delta.y) == 1)
assert(posmod(
int(stone_bridge_specs[1].get("turns", -1))
- int(stone_bridge_specs[0].get("turns", -1)),
4,
) == 2)
var river_source_coordinates := generator._river_source_coordinates(
generator._river_feature_origin,
generator._river_feature_footprint_size,
@ -446,6 +468,20 @@ func _validate_generated_region(
assert(fresh.surface_size.x <= 10.0 and fresh.surface_size.y <= 10.0)
assert(not fresh.visual_surface_enabled)
assert(not (fresh.get_node("VisualWater") as MeshInstance3D).visible)
var generated_bed := fresh.get_node_or_null(
"GeneratedBed",
) as MeshInstance3D
assert(generated_bed != null and generated_bed.mesh != null)
assert(
is_equal_approx(
generated_bed.global_position.y,
GeneratedWorldRegion.GENERATED_FRESH_WATER_BED_HEIGHT,
)
)
assert(
generated_bed.material_override != null
and generated_bed.material_override.resource_name == "dirt"
)
var fresh_recovery := (
fresh.get_node("RecoveryRegion") as PlayerWaterTrigger
)
@ -486,6 +522,16 @@ func _validate_generated_region(
)
var decorations := region.get_node("Decorations") as Node3D
var grass_prop_surface_triangles := region.get_spawn_surface_triangles(
[&"grass_lite"],
-INF,
0.35,
)
var sand_prop_surface_triangles := region.get_spawn_surface_triangles(
[&"sand"],
-INF,
0.35,
)
var anchors := region.get_node(
"GatherableAnchors/ReachableTreeTrunks"
) as GatherableAnchorSet3D
@ -540,6 +586,8 @@ func _validate_generated_region(
region,
child as Node3D,
definition,
grass_prop_surface_triangles,
sand_prop_surface_triangles,
)
if prop_id == &"prop_palm":
var cluster_id := int(
@ -1063,6 +1111,8 @@ func _validate_decoration_transform(
region: GeneratedWorldRegion,
prop: Node3D,
definition: TerrainPropDefinition,
grass_surface_triangles: Array[PackedVector3Array],
sand_surface_triangles: Array[PackedVector3Array],
) -> void:
assert(prop != null)
assert(prop.scale.is_equal_approx(Vector3.ONE))
@ -1142,6 +1192,43 @@ func _validate_decoration_transform(
],
],
)
var required_surface_triangles: Array[PackedVector3Array] = (
sand_surface_triangles
if definition.procedural_group == &"sand_tree"
else grass_surface_triangles
)
var required_surface_height := _triangle_surface_height_at(
prop.global_position,
required_surface_triangles,
)
assert(
required_surface_height > -INF
and absf(required_surface_height - prop.global_position.y) <= 0.01,
"%s (%s) is not rooted on its required terrain material at %s."
% [prop.name, definition.stable_id, prop.global_position],
)
func _triangle_surface_height_at(
position: Vector3,
triangles: Array[PackedVector3Array],
) -> float:
var highest := -INF
var segment_start := position + Vector3.UP * 100.0
var segment_end := position + Vector3.DOWN * 100.0
for triangle: PackedVector3Array in triangles:
if triangle.size() != 3:
continue
var hit: Variant = Geometry3D.segment_intersects_triangle(
segment_start,
segment_end,
triangle[0],
triangle[1],
triangle[2],
)
if hit is Vector3:
highest = maxf(highest, (hit as Vector3).y)
return highest
func _validate_tree_gatherable_anchors(

View file

@ -42,6 +42,7 @@ const EXPECTED_IDS: Array[String] = [
"chunk_0035",
"chunk_0036",
"chunk_0037",
"chunk_0038",
"chunk_9999",
"chunk_9998",
"chunk_9997",
@ -98,6 +99,7 @@ const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
&"chunk_0035": 4,
&"chunk_0036": 4,
&"chunk_0037": 4,
&"chunk_0038": 4,
&"chunk_9999": 4,
&"chunk_9998": 4,
&"chunk_9997": 4,
@ -163,6 +165,7 @@ func _validate_catalog() -> void:
var lake_narrow_to_regular := CATALOG.definition_for_id(&"chunk_0023")
var river_edge := CATALOG.definition_for_id(&"chunk_0024")
var river_edge_variant := CATALOG.definition_for_id(&"chunk_0025")
var river_stone_bridge := CATALOG.definition_for_id(&"chunk_0038")
var river_source_right := CATALOG.definition_for_id(&"chunk_0029")
var river_source_left := CATALOG.definition_for_id(&"chunk_0030")
var river_outlet_right := CATALOG.definition_for_id(&"chunk_0031")
@ -208,6 +211,7 @@ func _validate_catalog() -> void:
and lake_narrow_to_regular != null
and river_edge != null
and river_edge_variant != null
and river_stone_bridge != null
and river_source_right != null
and river_source_left != null
and river_outlet_right != null
@ -255,6 +259,7 @@ func _validate_catalog() -> void:
and lake_narrow_to_regular != null
and river_edge != null
and river_edge_variant != null
and river_stone_bridge != null
and river_source_right != null
and river_source_left != null
and river_outlet_right != null
@ -509,6 +514,8 @@ func _validate_catalog() -> void:
(
not river_edge.participates_in_base_solver
and not river_edge_variant.participates_in_base_solver
and not river_stone_bridge.participates_in_base_solver
and river_stone_bridge.maximum_placements == 2
and not river_source_right.participates_in_base_solver
and not river_source_left.participates_in_base_solver
and not river_outlet_right.participates_in_base_solver
@ -522,6 +529,8 @@ func _validate_catalog() -> void:
and river_edge.water_outlet_edges == 14
and river_edge_variant.water_inlet_edges == 14
and river_edge_variant.water_outlet_edges == 14
and river_stone_bridge.water_inlet_edges == 14
and river_stone_bridge.water_outlet_edges == 14
and river_source_right.water_inlet_edges == 6
and river_source_right.water_outlet_edges == 6
and river_source_left.water_inlet_edges == 3
@ -544,6 +553,7 @@ func _validate_catalog() -> void:
and river_cap.water_outlet_edges == 6
and river_edge.water_surface_polygon.size() == 13
and river_edge_variant.water_surface_polygon.size() == 13
and river_stone_bridge.water_surface_polygon.size() == 13
and river_source_right.water_surface_polygon.size() == 13
and river_source_left.water_surface_polygon.size() == 13
and river_outlet_right.water_surface_polygon.size() == 19
@ -554,6 +564,8 @@ func _validate_catalog() -> void:
and river_cap.primary_mesh_name == &"chunk_0020"
and "river" in river_edge.tags
and "river" in river_edge_variant.tags
and "river_bridge" in river_stone_bridge.tags
and "stepping_stones" in river_stone_bridge.tags
and "river_source_right" in river_source_right.tags
and "river_source_left" in river_source_left.tags
and "river_outlet_right" in river_outlet_right.tags
@ -1461,6 +1473,10 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
var river_edge_variant_bottom := _find_variant(generator, &"chunk_0025", 2)
var river_edge_vertical_left := _find_variant(generator, &"chunk_0024", 1)
var river_edge_vertical_right := _find_variant(generator, &"chunk_0025", 3)
var river_stones_top := _find_variant(generator, &"chunk_0038", 0)
var river_stones_bottom := _find_variant(generator, &"chunk_0038", 2)
var river_stones_left := _find_variant(generator, &"chunk_0038", 1)
var river_stones_right := _find_variant(generator, &"chunk_0038", 3)
var river_source_right := _find_variant(generator, &"chunk_0029", 0)
var river_source_left := _find_variant(generator, &"chunk_0030", 0)
var river_outlet_right := _find_variant(generator, &"chunk_0031", 0)
@ -1500,6 +1516,10 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and river_edge_variant_bottom != null
and river_edge_vertical_left != null
and river_edge_vertical_right != null
and river_stones_top != null
and river_stones_bottom != null
and river_stones_left != null
and river_stones_right != null
and river_source_right != null
and river_source_left != null
and river_outlet_right != null
@ -1525,6 +1545,10 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and river_edge_variant_bottom != null
and river_edge_vertical_left != null
and river_edge_vertical_right != null
and river_stones_top != null
and river_stones_bottom != null
and river_stones_left != null
and river_stones_right != null
and river_source_right != null
and river_source_left != null
and river_outlet_right != null
@ -1535,6 +1559,30 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
and river_bend_d != null
and beach_east != null
):
_check(
generator._edges_are_compatible(
river_edge_top,
TerrainChunkTopology.Edge.EAST,
river_stones_top,
TerrainChunkTopology.Edge.WEST,
)
and generator._edges_are_compatible(
river_stones_top,
TerrainChunkTopology.Edge.SOUTH,
river_stones_bottom,
TerrainChunkTopology.Edge.NORTH,
)
and generator._edges_are_compatible(
river_stones_left,
TerrainChunkTopology.Edge.EAST,
river_stones_right,
TerrainChunkTopology.Edge.WEST,
),
(
"The stepping-stone banks must replace one paired straight river "
+ "station in either orientation."
),
)
_check(
generator._edges_are_compatible(
river_source_right,
@ -1750,8 +1798,8 @@ func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
func _validate_generation_summary(generator: TerrainChunkGenerator) -> void:
var summary := generator._build_summary()
_check(
int(summary.get("variant_count", 0)) == 170,
"The current catalog must expose all 170 authored rotations.",
int(summary.get("variant_count", 0)) == 174,
"The current catalog must expose all 174 authored rotations.",
)
_check(
int(summary.get("solver_variant_count", 0))

View file

@ -1,6 +1,11 @@
extends SceneTree
const POLICY := preload("res://main/texture_sampling_policy.gd")
const PROJECTED_TERRAIN_TEXTURE_IMPORTS: Array[String] = [
"res://world/generation/chunks/assets/chunk_0000_grass_lite.png.import",
"res://world/generation/chunks/assets/chunk_0001_sand.png.import",
"res://world/generation/chunks/assets/chunk_0031_dirt.png.import",
]
var failures: Array[String] = []
@ -83,9 +88,14 @@ func _validate_repository_files(path: String) -> void:
func _validate_import_file(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
var expects_mipmaps := path in PROJECTED_TERRAIN_TEXTURE_IMPORTS
_check(
"mipmaps/generate=true" not in source,
"Texture import generates mipmaps: %s" % path,
("mipmaps/generate=true" in source) == expects_mipmaps,
(
"Projected terrain mipmap policy mismatch: %s"
if expects_mipmaps
else "Texture import generates unapproved mipmaps: %s"
) % path,
)

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://b3gl4nmbcgb6i"
path="res://.godot/imported/chunk_0038.glb-aab0f64bae856fb9a86c3367af401891.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0038.glb"
dest_files=["res://.godot/imported/chunk_0038.glb-aab0f64bae856fb9a86c3367af401891.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://wmom2kmvf0sb"
path.s3tc="res://.godot/imported/chunk_0038_cliff_wall.png-43b1f2500c61c119fe3d6dd64d103bce.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0038_cliff_wall.png-43b1f2500c61c119fe3d6dd64d103bce.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0038_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0038_cliff_wall.png-43b1f2500c61c119fe3d6dd64d103bce.s3tc.ctex", "res://.godot/imported/chunk_0038_cliff_wall.png-43b1f2500c61c119fe3d6dd64d103bce.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cvffkp5rc3xnq"
path.s3tc="res://.godot/imported/chunk_0038_dirt.png-523fbe5835e3869864352e6e698e49e3.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0038_dirt.png-523fbe5835e3869864352e6e698e49e3.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "07d90b3c46a4c3d0c172bed143dc9436"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0038_dirt.png"
dest_files=["res://.godot/imported/chunk_0038_dirt.png-523fbe5835e3869864352e6e698e49e3.s3tc.ctex", "res://.godot/imported/chunk_0038_dirt.png-523fbe5835e3869864352e6e698e49e3.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b231ivhqjd1n5"
path.s3tc="res://.godot/imported/chunk_0038_grass_lite.png-0784255d8b50ad328314d6b349ab3d43.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0038_grass_lite.png-0784255d8b50ad328314d6b349ab3d43.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0038_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0038_grass_lite.png-0784255d8b50ad328314d6b349ab3d43.s3tc.ctex", "res://.godot/imported/chunk_0038_grass_lite.png-0784255d8b50ad328314d6b349ab3d43.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View file

@ -0,0 +1,23 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0038.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0038"
label = "river stepping-stone edge"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0038"
participates_in_base_solver = false
selection_weight = 0.1
maximum_placements = 2
tags = PackedStringArray("land", "walkable", "grass", "flat", "spawn_safe", "fresh_water", "river", "river_border", "river_bridge", "stepping_stones", "flowing")
allowed_neighbor_tags = PackedStringArray("grass")
north_allowed_neighbor_tags = PackedStringArray("grass")
north_surface_tags = PackedStringArray("grass")
preferred_neighbor_tags = PackedStringArray("grass", "fresh_water", "river")
must_be_interior = true
water_inlet_edges = 14
water_outlet_edges = 14
water_surface_polygon = PackedVector2Array(-5, -4.619923, -3.897078, -4.512341, -2.980631, -4.203938, -2.250611, -4.038979, -0.609729, -4.132217, 0.90103, -4.286418, 1.924084, -4.17525, 2.678182, -4.067667, 3.604412, -4.189594, 4.269883, -4.408344, 5, -4.619923, 5, 5, -5, 5)

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=47 format=3]
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=48 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"]
@ -42,6 +42,7 @@
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0035.tres" id="44_river_bend_a"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0036.tres" id="45_river_bend_d"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0037.tres" id="46_lake_edge_variant"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0038.tres" id="47_river_stone_bridge"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_9999.tres" id="21_cliff_sea_transition_right"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_9998.tres" id="22_cliff_sea_transition_left"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_9997.tres" id="23_cliff_beach_transition_right"]
@ -50,4 +51,4 @@
[resource]
script = ExtResource("1_catalog")
chunk_size = 10.0
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("20_stream_variant"), ExtResource("6_pond"), ExtResource("25_lake_edge"), ExtResource("46_lake_edge_variant"), ExtResource("26_lake_corner"), ExtResource("27_lake_fill"), ExtResource("28_lake_diagonal"), ExtResource("29_lake_narrow"), ExtResource("30_lake_regular_to_narrow"), ExtResource("31_lake_narrow_to_regular"), ExtResource("32_river_edge"), ExtResource("33_river_edge_variant"), ExtResource("38_river_source_right"), ExtResource("39_river_source_left"), ExtResource("40_river_outlet_right"), ExtResource("41_river_outlet_left"), ExtResource("42_river_bend_c"), ExtResource("43_river_bend_b"), ExtResource("44_river_bend_a"), ExtResource("45_river_bend_d"), ExtResource("34_river_cap"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("12_beach_ocean_corner"), ExtResource("17_grass_sand_diagonal"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("35_cliff_third_tier_edge"), ExtResource("36_cliff_third_tier_corner"), ExtResource("37_cliff_third_tier_top"), ExtResource("18_cliff_sea_edge"), ExtResource("19_cliff_sea_corner"), ExtResource("21_cliff_sea_transition_right"), ExtResource("22_cliff_sea_transition_left"), ExtResource("23_cliff_beach_transition_right"), ExtResource("24_cliff_beach_transition_left"), ExtResource("8_spawn")])
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("20_stream_variant"), ExtResource("6_pond"), ExtResource("25_lake_edge"), ExtResource("46_lake_edge_variant"), ExtResource("26_lake_corner"), ExtResource("27_lake_fill"), ExtResource("28_lake_diagonal"), ExtResource("29_lake_narrow"), ExtResource("30_lake_regular_to_narrow"), ExtResource("31_lake_narrow_to_regular"), ExtResource("32_river_edge"), ExtResource("33_river_edge_variant"), ExtResource("47_river_stone_bridge"), ExtResource("38_river_source_right"), ExtResource("39_river_source_left"), ExtResource("40_river_outlet_right"), ExtResource("41_river_outlet_left"), ExtResource("42_river_bend_c"), ExtResource("43_river_bend_b"), ExtResource("44_river_bend_a"), ExtResource("45_river_bend_d"), ExtResource("34_river_cap"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("12_beach_ocean_corner"), ExtResource("17_grass_sand_diagonal"), ExtResource("13_cliff_top"), ExtResource("14_cliff_corner"), ExtResource("15_cliff_edge"), ExtResource("16_cliff_ramp"), ExtResource("35_cliff_third_tier_edge"), ExtResource("36_cliff_third_tier_corner"), ExtResource("37_cliff_third_tier_top"), ExtResource("18_cliff_sea_edge"), ExtResource("19_cliff_sea_corner"), ExtResource("21_cliff_sea_transition_right"), ExtResource("22_cliff_sea_transition_left"), ExtResource("23_cliff_beach_transition_right"), ExtResource("24_cliff_beach_transition_left"), ExtResource("8_spawn")])

View file

@ -19,6 +19,9 @@ const SALT_WATER_MATERIAL: Material = preload(
const FRESH_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water_fresh.tres"
)
const GENERATED_TERRAIN_DIRT_MATERIAL: Material = preload(
"res://world/materials/generated_terrain_dirt.tres"
)
const GENERATED_POND_POOL: FishPool = preload(
"res://fish/pools/generated_pond_pool.tres"
)
@ -32,12 +35,26 @@ const OCEAN_POOL: FishPool = preload(
"res://fish/pools/starter_ocean_pool.tres"
)
const WATER_HEIGHT := -0.25
const GENERATED_FRESH_WATER_BED_HEIGHT := -2.85
const PROP_EDGE_MARGIN := 2.2
const PROP_PLACEMENT_ATTEMPTS := 12
const PROP_CLUSTER_PLACEMENT_ATTEMPTS := 10
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
const PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE := 0.35
const PROP_CHANCE_SCALE := 10000
const PROP_SELECTION_WEIGHT_SCALE := 1000
const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [
Vector2(1.0, 0.0),
Vector2(0.70710678, 0.70710678),
Vector2(0.0, 1.0),
Vector2(-0.70710678, 0.70710678),
Vector2(-1.0, 0.0),
Vector2(-0.70710678, -0.70710678),
Vector2(0.0, -1.0),
Vector2(0.70710678, -0.70710678),
]
const GRASS_PROP_SURFACE_MATERIALS: Array[StringName] = [&"grass_lite"]
const SAND_PROP_SURFACE_MATERIALS: Array[StringName] = [&"sand"]
const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
&"grass_tree",
&"grass_detail",
@ -185,7 +202,16 @@ func _on_generation_completed(summary: Dictionary) -> void:
_placed_group_coordinates.clear()
var random := RandomNumberGenerator.new()
random.seed = _current_seed ^ 0x5EED71
var terrain_triangles := _terrain_surface_triangles()
var grass_surface_triangles_by_coordinate := (
_terrain_surface_triangles_by_coordinate(
GRASS_PROP_SURFACE_MATERIALS,
)
)
var sand_surface_triangles_by_coordinate := (
_terrain_surface_triangles_by_coordinate(
SAND_PROP_SURFACE_MATERIALS,
)
)
var biomes: Array[TerrainBiomeDefinition] = []
if biome_catalog != null:
biomes.assign(biome_catalog.definitions)
@ -250,6 +276,14 @@ func _on_generation_completed(summary: Dictionary) -> void:
"tags",
PackedStringArray(),
)
var terrain_triangles := _surface_triangles_for_coordinate(
(
sand_surface_triangles_by_coordinate
if group == &"sand_tree"
else grass_surface_triangles_by_coordinate
),
coordinate,
)
var definitions := _spawn_distance_eligible_definitions(
_prop_definitions_for(rule, tags),
coordinate,
@ -287,7 +321,8 @@ func _on_generation_completed(summary: Dictionary) -> void:
eligible_records,
group_counts,
random,
terrain_triangles,
grass_surface_triangles_by_coordinate,
sand_surface_triangles_by_coordinate,
)
_place_spawn_amenities(spawn_position)
_configure_fresh_water(records)
@ -406,6 +441,11 @@ func _configure_fresh_water(records: Array[Dictionary]) -> void:
body.surface_polygon = surface_polygon
else:
body.surface_size = surface_size
_add_generated_fresh_water_bed(
body,
surface_polygon,
surface_size,
)
body.visual_surface_enabled = false
body.water_material = _fresh_water_material()
body.water_type = WaterType.Type.FRESH_WATER
@ -418,6 +458,59 @@ func _configure_fresh_water(records: Array[Dictionary]) -> void:
body.selection_priority = 10
func _add_generated_fresh_water_bed(
body: WaterBodyAuthoring,
surface_polygon: PackedVector2Array,
surface_size: Vector2,
) -> void:
var polygon := surface_polygon
if polygon.size() < 3:
var half_size := surface_size * 0.5
polygon = PackedVector2Array([
Vector2(-half_size.x, -half_size.y),
Vector2(half_size.x, -half_size.y),
Vector2(half_size.x, half_size.y),
Vector2(-half_size.x, half_size.y),
])
var triangle_indices := Geometry2D.triangulate_polygon(polygon)
if triangle_indices.size() < 3:
return
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
for offset: int in range(0, triangle_indices.size(), 3):
var triangle := PackedVector3Array()
for point_offset: int in 3:
var point := polygon[triangle_indices[offset + point_offset]]
triangle.append(Vector3(point.x, 0.0, point.y))
if (
(triangle[1] - triangle[0]).cross(
triangle[2] - triangle[0]
).dot(Vector3.UP) < 0.0
):
var swap := triangle[1]
triangle[1] = triangle[2]
triangle[2] = swap
vertices.append_array(triangle)
normals.append_array(PackedVector3Array([
Vector3.UP,
Vector3.UP,
Vector3.UP,
]))
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_NORMAL] = normals
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
var bed := MeshInstance3D.new()
bed.name = "GeneratedBed"
bed.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
bed.position.y = GENERATED_FRESH_WATER_BED_HEIGHT - WATER_HEIGHT
bed.material_override = GENERATED_TERRAIN_DIRT_MATERIAL
bed.mesh = mesh
body.add_child(bed)
func _fresh_water_location_tags(tags: PackedStringArray) -> Array[StringName]:
var result: Array[StringName] = [&"generated_fresh_water"]
if "pond" in tags:
@ -524,6 +617,7 @@ func _add_prop(
)
var primary_scale := visual_scales[0]
var scaled_clearance := definition.clearance_radius * primary_scale
var support_radius := clampf(scaled_clearance * 0.35, 0.25, 0.65)
var placement := Vector3.ZERO
var found_surface := false
for _attempt: int in PROP_PLACEMENT_ATTEMPTS:
@ -541,6 +635,12 @@ func _add_prop(
surface_height > -INF
and surface_height
> WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
and _surface_supports_prop_footprint(
placement,
surface_height,
support_radius,
terrain_triangles,
)
and _has_prop_clearance(
placement,
scaled_clearance,
@ -572,6 +672,11 @@ func _add_prop(
for member_index: int in range(1, cluster_size):
var member_scale := visual_scales[member_index]
var member_clearance := definition.clearance_radius * member_scale
var member_support_radius := clampf(
member_clearance * 0.35,
0.25,
0.65,
)
for _attempt: int in PROP_CLUSTER_PLACEMENT_ATTEMPTS:
var sector_angle := (
cluster_angle
@ -598,6 +703,12 @@ func _add_prop(
)
if (
surface_height <= WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
or not _surface_supports_prop_footprint(
candidate,
surface_height,
member_support_radius,
terrain_triangles,
)
or not _has_prop_clearance(candidate, member_clearance)
):
continue
@ -959,7 +1070,8 @@ func _ensure_minimum_props(
eligible_records: Dictionary[StringName, Array],
group_counts: Dictionary[StringName, int],
random: RandomNumberGenerator,
terrain_triangles: Array[PackedVector3Array],
grass_surface_triangles_by_coordinate: Dictionary[Vector2i, Array],
sand_surface_triangles_by_coordinate: Dictionary[Vector2i, Array],
) -> void:
if biome_catalog == null:
return
@ -993,6 +1105,14 @@ func _ensure_minimum_props(
"coordinate",
Vector2i.ZERO,
)
var terrain_triangles := _surface_triangles_for_coordinate(
(
sand_surface_triangles_by_coordinate
if group == &"sand_tree"
else grass_surface_triangles_by_coordinate
),
coordinate,
)
var definitions := _spawn_distance_eligible_definitions(
_prop_definitions_for(rule, tags),
coordinate,
@ -1101,12 +1221,26 @@ func _apply_material_variant_to_meshes(
)
func _terrain_surface_triangles() -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = []
func _terrain_surface_triangles_by_coordinate(
material_names: Array[StringName],
) -> Dictionary[Vector2i, Array]:
var triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
if material_names.is_empty():
return triangles_by_coordinate
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
if mesh_instance.mesh == null:
continue
var coordinate := _terrain_coordinate_for(mesh_instance)
if coordinate.x < 0 or coordinate.y < 0:
continue
var triangles: Array[PackedVector3Array] = []
for surface_index: int in mesh_instance.mesh.get_surface_count():
var material := mesh_instance.get_active_material(surface_index)
if (
material == null
or StringName(material.resource_name) not in material_names
):
continue
_append_surface_triangles(
triangles,
mesh_instance,
@ -1114,6 +1248,35 @@ func _terrain_surface_triangles() -> Array[PackedVector3Array]:
-INF,
0.35,
)
if triangles.is_empty():
continue
var coordinate_triangles: Array = triangles_by_coordinate.get(
coordinate,
[],
)
coordinate_triangles.append_array(triangles)
triangles_by_coordinate[coordinate] = coordinate_triangles
return triangles_by_coordinate
func _terrain_coordinate_for(mesh_instance: MeshInstance3D) -> Vector2i:
var current: Node = mesh_instance
while current != null and current != _generator:
if current.has_meta(&"terrain_chunk_coordinate"):
return current.get_meta(
&"terrain_chunk_coordinate",
Vector2i(-1, -1),
) as Vector2i
current = current.get_parent()
return Vector2i(-1, -1)
func _surface_triangles_for_coordinate(
triangles_by_coordinate: Dictionary[Vector2i, Array],
coordinate: Vector2i,
) -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = []
triangles.assign(triangles_by_coordinate.get(coordinate, []))
return triangles
@ -1139,6 +1302,28 @@ func _surface_height_at(
return highest
func _surface_supports_prop_footprint(
position: Vector3,
surface_height: float,
radius: float,
triangles: Array[PackedVector3Array],
) -> bool:
for direction: Vector2 in PROP_SURFACE_SAMPLE_DIRECTIONS:
var sample_position := position + Vector3(
direction.x * radius,
0.0,
direction.y * radius,
)
var sample_height := _surface_height_at(sample_position, triangles)
if (
sample_height <= -INF
or absf(sample_height - surface_height)
> PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE
):
return false
return true
func _add_prop_collision(
prop: Node3D,
definition: TerrainPropDefinition,

View file

@ -64,6 +64,7 @@ const PROJECTED_DIRT_MATERIAL: Material = preload(
@export var river_feature_enabled := false
@export var river_edge_chunk_id: StringName = &"chunk_0024"
@export var river_edge_variant_chunk_id: StringName = &"chunk_0025"
@export var river_stone_bridge_chunk_id: StringName = &"chunk_0038"
@export var river_source_right_chunk_id: StringName = &"chunk_0029"
@export var river_source_left_chunk_id: StringName = &"chunk_0030"
@export var river_outlet_right_chunk_id: StringName = &"chunk_0031"
@ -1728,6 +1729,7 @@ func _prepare_river_feature_region(report_no_room_error := true) -> bool:
for stable_id: StringName in [
river_edge_chunk_id,
river_edge_variant_chunk_id,
river_stone_bridge_chunk_id,
river_source_right_chunk_id,
river_source_left_chunk_id,
river_outlet_right_chunk_id,
@ -2190,6 +2192,16 @@ func _build_river_feature_candidate(
+ bend_origin.y,
2,
)
var straight_station_count := bend_distance + outlet_distance - 2
var bridge_station_index := posmod(
generation_seed
+ source_coordinates[0].x * 73856093
+ source_coordinates[0].y * 19349663
+ bend_origin.x * 83492791
+ bend_origin.y * 2971215073
+ bend_turns * 433494437,
straight_station_count,
)
var station_index := 0
for step: int in range(1, bend_distance):
var pair: Array[Vector2i] = []
@ -2200,6 +2212,7 @@ func _build_river_feature_candidate(
pair,
flow_direction,
phase + station_index,
station_index == bridge_station_index,
)
)
station_index += 1
@ -2215,6 +2228,7 @@ func _build_river_feature_candidate(
pair,
outlet_direction,
phase + station_index,
station_index == bridge_station_index,
)
)
station_index += 1
@ -2355,6 +2369,7 @@ func _river_edge_pair_specs(
pair: Array[Vector2i],
flow_direction: Vector2i,
phase: int,
use_stone_bridge := false,
) -> Array[Dictionary]:
var ordered := _river_sorted_pair(pair, flow_direction)
if ordered.size() != 2:
@ -2363,6 +2378,11 @@ func _river_edge_pair_specs(
river_edge_chunk_id,
river_edge_variant_chunk_id,
]
if use_stone_bridge:
edge_ids.assign([
river_stone_bridge_chunk_id,
river_stone_bridge_chunk_id,
])
var first_index := posmod(phase, edge_ids.size())
var first_turns := 0 if flow_direction.x != 0 else 1
var second_turns := 2 if flow_direction.x != 0 else 3

View file

@ -1,7 +1,7 @@
shader_type spatial;
render_mode cull_back;
uniform sampler2D albedo_texture : source_color, repeat_enable, filter_nearest;
uniform sampler2D albedo_texture : source_color, repeat_enable, filter_nearest_mipmap;
uniform vec4 albedo_tint : source_color = vec4(1.0);
uniform float tile_world_size : hint_range(0.1, 20.0, 0.05) = 2.5;
uniform float blend_sharpness : hint_range(1.0, 16.0, 0.25) = 6.0;