diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 9fbd5b9..09db779 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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. diff --git a/scripts/normalize_texture_imports.gd b/scripts/normalize_texture_imports.gd index 5b78ec1..e7b2406 100644 --- a/scripts/normalize_texture_imports.gd +++ b/scripts/normalize_texture_imports.gd @@ -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( diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index ef933bf..0143322 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -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( diff --git a/tests/terrain_chunk_generator_validation.gd b/tests/terrain_chunk_generator_validation.gd index b8d24cf..a46d1d7 100644 --- a/tests/terrain_chunk_generator_validation.gd +++ b/tests/terrain_chunk_generator_validation.gd @@ -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)) diff --git a/tests/texture_sampling_validation.gd b/tests/texture_sampling_validation.gd index deeabce..dcccc38 100644 --- a/tests/texture_sampling_validation.gd +++ b/tests/texture_sampling_validation.gd @@ -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, ) diff --git a/world/generation/chunks/assets/chunk_0000_grass_lite.png.import b/world/generation/chunks/assets/chunk_0000_grass_lite.png.import index ff2855e..bff65f3 100644 --- a/world/generation/chunks/assets/chunk_0000_grass_lite.png.import +++ b/world/generation/chunks/assets/chunk_0000_grass_lite.png.import @@ -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="" diff --git a/world/generation/chunks/assets/chunk_0001_sand.png.import b/world/generation/chunks/assets/chunk_0001_sand.png.import index 3e91993..290b8f9 100644 --- a/world/generation/chunks/assets/chunk_0001_sand.png.import +++ b/world/generation/chunks/assets/chunk_0001_sand.png.import @@ -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="" diff --git a/world/generation/chunks/assets/chunk_0031_dirt.png.import b/world/generation/chunks/assets/chunk_0031_dirt.png.import index 3d13618..8e60684 100644 --- a/world/generation/chunks/assets/chunk_0031_dirt.png.import +++ b/world/generation/chunks/assets/chunk_0031_dirt.png.import @@ -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="" diff --git a/world/generation/chunks/assets/chunk_0038.glb b/world/generation/chunks/assets/chunk_0038.glb new file mode 100644 index 0000000..5fa83e3 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0038.glb differ diff --git a/world/generation/chunks/assets/chunk_0038.glb.import b/world/generation/chunks/assets/chunk_0038.glb.import new file mode 100644 index 0000000..b28ae56 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0038.glb.import @@ -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 diff --git a/world/generation/chunks/assets/chunk_0038_cliff_wall.png b/world/generation/chunks/assets/chunk_0038_cliff_wall.png new file mode 100644 index 0000000..3fe157f Binary files /dev/null and b/world/generation/chunks/assets/chunk_0038_cliff_wall.png differ diff --git a/world/generation/chunks/assets/chunk_0038_cliff_wall.png.import b/world/generation/chunks/assets/chunk_0038_cliff_wall.png.import new file mode 100644 index 0000000..1a371ec --- /dev/null +++ b/world/generation/chunks/assets/chunk_0038_cliff_wall.png.import @@ -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 diff --git a/world/generation/chunks/assets/chunk_0038_dirt.png b/world/generation/chunks/assets/chunk_0038_dirt.png new file mode 100644 index 0000000..588c8c6 Binary files /dev/null and b/world/generation/chunks/assets/chunk_0038_dirt.png differ diff --git a/world/generation/chunks/assets/chunk_0038_dirt.png.import b/world/generation/chunks/assets/chunk_0038_dirt.png.import new file mode 100644 index 0000000..3a73467 --- /dev/null +++ b/world/generation/chunks/assets/chunk_0038_dirt.png.import @@ -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 diff --git a/world/generation/chunks/assets/chunk_0038_grass_lite.png b/world/generation/chunks/assets/chunk_0038_grass_lite.png new file mode 100644 index 0000000..52c7ccb Binary files /dev/null and b/world/generation/chunks/assets/chunk_0038_grass_lite.png differ diff --git a/world/generation/chunks/assets/chunk_0038_grass_lite.png.import b/world/generation/chunks/assets/chunk_0038_grass_lite.png.import new file mode 100644 index 0000000..b8f99dd --- /dev/null +++ b/world/generation/chunks/assets/chunk_0038_grass_lite.png.import @@ -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 diff --git a/world/generation/chunks/definitions/chunk_0038.tres b/world/generation/chunks/definitions/chunk_0038.tres new file mode 100644 index 0000000..fa1828a --- /dev/null +++ b/world/generation/chunks/definitions/chunk_0038.tres @@ -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) diff --git a/world/generation/chunks/terrain_chunk_catalog.tres b/world/generation/chunks/terrain_chunk_catalog.tres index 992e847..4bf38e7 100644 --- a/world/generation/chunks/terrain_chunk_catalog.tres +++ b/world/generation/chunks/terrain_chunk_catalog.tres @@ -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")]) diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index b090844..78188e0 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -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, diff --git a/world/generation/terrain_chunk_generator.gd b/world/generation/terrain_chunk_generator.gd index 0d1703c..49bf693 100644 --- a/world/generation/terrain_chunk_generator.gd +++ b/world/generation/terrain_chunk_generator.gd @@ -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 diff --git a/world/materials/terrain_surface_projection.gdshader b/world/materials/terrain_surface_projection.gdshader index b879c63..a669a14 100644 --- a/world/materials/terrain_surface_projection.gdshader +++ b/world/materials/terrain_surface_projection.gdshader @@ -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;