feat: bake jajce terrain navigation

This commit is contained in:
2026-07-08 15:53:39 +02:00
parent cc0d6ef548
commit 324084f233
8 changed files with 157 additions and 34 deletions
+5 -5
View File
@@ -626,14 +626,14 @@ Completed:
8. Base Terrain3D collision/navigation guardrails: runtime tests now keep
Terrain3D collision enabled, keep the old greybox ground out of physics, and
verify required navigation paths track the authored height field.
9. Terrain3D-derived playable-loop navigation: `JajceWorld` now uses an
external baked navigation resource generated from Terrain3D source geometry,
and runtime tests reject partial paths that do not reach their targets.
Next:
1. Continue replacing the temporary greybox navigation source with baked
Terrain3D-derived navigation for the sculpted playable loop.
2. Capture and visually review “Jajce Lookdev 01.”
3. Capture “Simulation Garden 01.”
1. Capture and visually review “Jajce Lookdev 01.”
2. Capture “Simulation Garden 01.”
Do not start with GIS data, a full city, a large asset pack, or more NPC
mechanics. The next proof is a beautiful stage for the systems that already
+4
View File
@@ -674,6 +674,10 @@ Recently completed:
now assert Terrain3D collision remains enabled, the hidden greybox ground no
longer provides physics collision, and tested navigation routes stay close to
the authored Terrain3D height field.
- The temporary greybox navigation source was replaced by a project-owned
Terrain3D-derived navigation mesh resource generated by
`tools/bake_jajce_navigation.gd`; route tests now also reject partial paths
that do not reach their requested targets.
This order strengthens the simulation while regularly producing visible
progress suitable for public development updates.
+10 -8
View File
@@ -193,8 +193,8 @@ study, and patrol target typed activity sites. The migration is documented in
- **Main scene:** `res://main.tscn`
- **Terrain:** Terrain3D 1.0.2 is installed and enabled
- **Jajce runtime:** reusable 512 m Terrain3D seed, greybox landmarks, stable
ResourceNode placement, lookdev camera, tested runtime navigation, and base
Terrain3D collision/navigation guardrails
ResourceNode placement, lookdev camera, and Terrain3D-derived runtime
navigation with collision guardrails
- **Terrain compatibility floor:** Godot 4.4 according to the bundled extension
- **Version control:** Git
- **Primary branch:** `main`
@@ -328,10 +328,11 @@ distance with resource `safety_risk`, `comfort_distance`, and
- warm sky, fog, shadows, and wind-reactive foliage;
- eight ResourceNodes preserving stable food/wood discovery IDs;
- typed village pantry storage and typed guard, study, and rest activity sites;
- a temporary baked navigation loop with 24 tested routes;
- a Terrain3D-derived baked navigation mesh covering the current playable loop;
- base collision/navigation guardrails that keep Terrain3D collision enabled,
keep the hidden greybox ground out of runtime physics, and compare required
navigation paths against the Terrain3D height field;
keep the old greybox ground out of runtime physics, require paths to reach
their targets, and compare required navigation paths against the Terrain3D
height field;
- a look-development scene and camera.
This is a runtime integration proof, not the final terrain composition.
@@ -538,9 +539,10 @@ These are expected prototype constraints, not necessarily isolated bugs:
mutation APIs, but selection, execution, target resolution, and active-world
queries now have focused collaborators.
- Path failure and interruption emit a `navigation_failed` signal and send the NPC to wander; this is functional but not yet polished.
- The old greybox navigation source remains temporary; current tests now guard
against collision/nav regressions, but the next terrain base phase is a
Terrain3D-derived bake for the sculpted playable loop.
- The old greybox navigation source has been replaced by a project-owned
Terrain3D-derived navigation resource. The current bake is still a first
playable-loop pass, so future terrain sculpting should rerun the bake tool and
recheck reachability rather than hand-editing navigation polygons.
- Terrain3D and the bounded Jajce beauty baseline now run in the main game
scene; final visual review and character/profession readability remain.
- Combat, companions, factions, politics, trade, rumours, quests, persistence,
+25 -3
View File
@@ -11,6 +11,7 @@ func _run() -> void:
var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene)
await process_frame
for _frame in 10:
await physics_frame
var simulation_manager: Node = main_scene.get_node("SimulationManager")
@@ -27,10 +28,9 @@ func _run() -> void:
"Runtime Terrain3D should bind to the gameplay camera"
)
_check(terrain.collision_mode != 0, "Runtime Terrain3D collision should be enabled")
var greybox_ground := main_scene.get_node("JajceWorld/NavigationRegion3D/GreyboxGround") as StaticBody3D
_check(
greybox_ground.collision_layer == 0 and greybox_ground.collision_mask == 0,
"Runtime GreyboxGround should not provide physics collision"
not main_scene.has_node("JajceWorld/NavigationRegion3D/GreyboxGround"),
"Runtime should not keep the temporary greybox navigation ground"
)
_check(
(
@@ -49,6 +49,11 @@ func _run() -> void:
)
var navigation_map: RID = main_scene.get_world_3d().navigation_map
var navigation_region := main_scene.get_node("JajceWorld/NavigationRegion3D") as NavigationRegion3D
_check(
navigation_region.navigation_mesh.resource_path == "res://world/jajce/JajceNavigationMesh.tres",
"Runtime navigation should use the Terrain3D-derived baked mesh resource"
)
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
@@ -82,6 +87,9 @@ func _run() -> void:
_check_path_tracks_terrain(
terrain, path, "Runtime navigation path should track Terrain3D height"
)
_check_path_reaches_destination(
path, destination, "Runtime navigation path should reach its requested target"
)
var village := SimVillage.new()
var worker := SimNPC.new(100, "BaselineWorker", SimulationIds.PROFESSION_SCHOLAR, 5.0, 5.0)
@@ -152,3 +160,17 @@ func _check_path_tracks_terrain(terrain: Terrain3D, path: PackedVector3Array, me
% [message, point, terrain_height]
)
return
func _check_path_reaches_destination(path: PackedVector3Array, destination: Vector3, message: String) -> void:
if path.is_empty():
return
var final_point := path[path.size() - 1]
var final_xz := Vector2(final_point.x, final_point.z)
var destination_xz := Vector2(destination.x, destination.z)
if final_xz.distance_to(destination_xz) > 1.5:
failures.append(
"%s: final path point %s is too far from %s"
% [message, final_point, destination]
)
+24 -4
View File
@@ -17,7 +17,7 @@ func _run() -> void:
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
root.add_child(world)
await process_frame
for frame in 10:
for _frame in 10:
await physics_frame
var terrain = world.get_node("TerrainRoot/Terrain3D")
@@ -27,10 +27,9 @@ func _run() -> void:
"Terrain3D should use dedicated Jajce data"
)
_check(terrain.collision_mode != 0, "Terrain3D collision should be enabled for gameplay")
var greybox_ground := world.get_node("NavigationRegion3D/GreyboxGround") as StaticBody3D
_check(
greybox_ground.collision_layer == 0 and greybox_ground.collision_mask == 0,
"GreyboxGround should not provide runtime physics collision"
not world.has_node("NavigationRegion3D/GreyboxGround"),
"JajceWorld should not keep the temporary greybox navigation ground"
)
_check(
configured_texture_count == 6,
@@ -203,6 +202,10 @@ func _run() -> void:
navigation_region.navigation_mesh.get_polygon_count() > 0,
"Jajce navigation mesh should contain baked polygons"
)
_check(
navigation_region.navigation_mesh.resource_path == "res://world/jajce/JajceNavigationMesh.tres",
"Jajce navigation should use the Terrain3D-derived baked mesh resource"
)
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
@@ -224,6 +227,9 @@ func _run() -> void:
_check_path_tracks_terrain(
terrain, path, "Jajce navigation path should track Terrain3D height"
)
_check_path_reaches_destination(
path, destination, "Jajce navigation path should reach its requested target"
)
var adapter := ActiveWorldAdapter.new()
adapter.pantry_storage = pantry
@@ -330,3 +336,17 @@ func _check_path_tracks_terrain(terrain: Terrain3D, path: PackedVector3Array, me
% [message, point, terrain_height]
)
return
func _check_path_reaches_destination(path: PackedVector3Array, destination: Vector3, message: String) -> void:
if path.is_empty():
return
var final_point := path[path.size() - 1]
var final_xz := Vector2(final_point.x, final_point.z)
var destination_xz := Vector2(destination.x, destination.z)
if final_xz.distance_to(destination_xz) > 1.5:
failures.append(
"%s: final path point %s is too far from %s"
% [message, final_point, destination]
)
+74
View File
@@ -0,0 +1,74 @@
extends SceneTree
const OUTPUT_PATH := "res://world/jajce/JajceNavigationMesh.tres"
const WORLD_SCENE_PATH := "res://world/jajce/JajceWorld.tscn"
const TERRAIN_PATH := "TerrainRoot/Terrain3D"
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var world_scene := load(WORLD_SCENE_PATH) as PackedScene
if world_scene == null:
push_error("Could not load %s" % WORLD_SCENE_PATH)
quit(1)
return
var world := world_scene.instantiate() as Node3D
root.add_child(world)
await process_frame
for _frame in 10:
await physics_frame
var terrain := world.get_node(TERRAIN_PATH) as Terrain3D
if terrain == null or terrain.data == null:
push_error("Jajce Terrain3D data is not available")
quit(1)
return
var nav_mesh := _create_navigation_mesh_template()
var bake_aabb: AABB = nav_mesh.filter_baking_aabb
bake_aabb.position += nav_mesh.filter_baking_aabb_offset
var terrain_faces: PackedVector3Array = terrain.generate_nav_mesh_source_geometry(bake_aabb, false)
if terrain_faces.is_empty():
push_error("Terrain3D produced no navigation source faces")
quit(1)
return
var source_geometry := NavigationMeshSourceGeometryData3D.new()
source_geometry.add_faces(terrain_faces, Transform3D.IDENTITY)
NavigationServer3D.bake_from_source_geometry_data(nav_mesh, source_geometry)
if nav_mesh.get_polygon_count() == 0:
push_error("Baked Jajce navigation mesh contains no polygons")
quit(1)
return
var error := ResourceSaver.save(nav_mesh, OUTPUT_PATH)
if error != OK:
push_error("Could not save %s: %s" % [OUTPUT_PATH, error_string(error)])
quit(1)
return
print(
"[TOOL] Saved %s with %d vertices and %d polygons"
% [OUTPUT_PATH, nav_mesh.get_vertices().size(), nav_mesh.get_polygon_count()]
)
quit(0)
func _create_navigation_mesh_template() -> NavigationMesh:
var nav_mesh := NavigationMesh.new()
nav_mesh.agent_height = 1.7
nav_mesh.agent_radius = 0.4
nav_mesh.agent_max_climb = 0.55
nav_mesh.agent_max_slope = 35.0
nav_mesh.cell_size = 0.25
nav_mesh.cell_height = 0.1
nav_mesh.filter_baking_aabb = AABB(
Vector3(-36.0, -8.0, -36.0), Vector3(72.0, 32.0, 72.0)
)
return nav_mesh
File diff suppressed because one or more lines are too long
+3 -13
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=26 format=3]
[gd_scene load_steps=27 format=3]
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
@@ -15,6 +15,7 @@
[ext_resource type="Script" path="res://world/jajce/day_night_cycle.gd" id="13_daynight"]
[ext_resource type="PackedScene" path="res://world/storage/StorageNode.tscn" id="14_storage"]
[ext_resource type="Script" path="res://world/activity/ActivitySite.gd" id="15_activity"]
[ext_resource type="NavigationMesh" path="res://world/jajce/JajceNavigationMesh.tres" id="16_nav"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = {
@@ -118,18 +119,7 @@ size = Vector3(2.8, 0.3, 0.9)
script = ExtResource("3_world")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
navigation_mesh = SubResource("NavigationMesh_greybox")
[node name="GreyboxGround" type="StaticBody3D" parent="NavigationRegion3D"]
collision_layer = 0
collision_mask = 0
[node name="Mesh" type="MeshInstance3D" parent="NavigationRegion3D/GreyboxGround"]
visible = false
mesh = SubResource("Mesh_ground")
[node name="Collision" type="CollisionShape3D" parent="NavigationRegion3D/GreyboxGround"]
shape = SubResource("Shape_ground")
navigation_mesh = ExtResource("16_nav")
[node name="TerrainRoot" type="Node3D" parent="."]