diff --git a/terrain/jajce/assets.tres b/terrain/jajce/assets.tres index 13d134b..2af04a4 100644 --- a/terrain/jajce/assets.tres +++ b/terrain/jajce/assets.tres @@ -1,3 +1,33 @@ -[gd_resource type="Terrain3DAssets" format=3] +[gd_resource type="Terrain3DAssets" load_steps=7 format=3] + +[ext_resource type="Texture2D" path="res://terrain/jajce/textures/rock_albedo.png" id="1_rock"] +[ext_resource type="Texture2D" path="res://terrain/jajce/textures/grass_albedo.png" id="2_grass"] +[ext_resource type="Texture2D" path="res://terrain/jajce/textures/soil_albedo.png" id="3_soil"] +[sub_resource type="Terrain3DTextureAsset" id="Texture_rock"] +name = "Limestone" +albedo_texture = ExtResource("1_rock") +normal_texture = ExtResource("1_rock") +normal_depth = 0.0 +uv_scale = 0.18 +detiling_rotation = 0.12 + +[sub_resource type="Terrain3DTextureAsset" id="Texture_grass"] +name = "Valley Grass" +id = 1 +albedo_texture = ExtResource("2_grass") +normal_texture = ExtResource("2_grass") +normal_depth = 0.0 +uv_scale = 0.16 +detiling_rotation = 0.18 + +[sub_resource type="Terrain3DTextureAsset" id="Texture_soil"] +name = "Warm Soil" +id = 2 +albedo_texture = ExtResource("3_soil") +normal_texture = ExtResource("3_soil") +normal_depth = 0.0 +uv_scale = 0.2 +detiling_rotation = 0.1 [resource] +texture_list = Array[Terrain3DTextureAsset]([SubResource("Texture_rock"), SubResource("Texture_grass"), SubResource("Texture_soil")]) diff --git a/terrain/jajce/data/terrain3d-01-01.res b/terrain/jajce/data/terrain3d-01-01.res index 6794324..98007f7 100644 Binary files a/terrain/jajce/data/terrain3d-01-01.res and b/terrain/jajce/data/terrain3d-01-01.res differ diff --git a/terrain/jajce/data/terrain3d-01_00.res b/terrain/jajce/data/terrain3d-01_00.res index d164256..3250cb0 100644 Binary files a/terrain/jajce/data/terrain3d-01_00.res and b/terrain/jajce/data/terrain3d-01_00.res differ diff --git a/terrain/jajce/data/terrain3d_00-01.res b/terrain/jajce/data/terrain3d_00-01.res index 2bd1ee3..1172646 100644 Binary files a/terrain/jajce/data/terrain3d_00-01.res and b/terrain/jajce/data/terrain3d_00-01.res differ diff --git a/terrain/jajce/data/terrain3d_00_00.res b/terrain/jajce/data/terrain3d_00_00.res index 61b12bc..a305acf 100644 Binary files a/terrain/jajce/data/terrain3d_00_00.res and b/terrain/jajce/data/terrain3d_00_00.res differ diff --git a/terrain/jajce/textures/grass_albedo.png b/terrain/jajce/textures/grass_albedo.png new file mode 100644 index 0000000..80e7595 Binary files /dev/null and b/terrain/jajce/textures/grass_albedo.png differ diff --git a/terrain/jajce/textures/grass_albedo.png.import b/terrain/jajce/textures/grass_albedo.png.import new file mode 100644 index 0000000..11e1cd8 --- /dev/null +++ b/terrain/jajce/textures/grass_albedo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d0ihxulegsl2w" +path="res://.godot/imported/grass_albedo.png-d561326727ac4b17c40015cc1a0f2d80.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://terrain/jajce/textures/grass_albedo.png" +dest_files=["res://.godot/imported/grass_albedo.png-d561326727ac4b17c40015cc1a0f2d80.ctex"] + +[params] + +compress/mode=0 +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=true +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=1 diff --git a/terrain/jajce/textures/rock_albedo.png b/terrain/jajce/textures/rock_albedo.png new file mode 100644 index 0000000..ee0ee52 Binary files /dev/null and b/terrain/jajce/textures/rock_albedo.png differ diff --git a/terrain/jajce/textures/rock_albedo.png.import b/terrain/jajce/textures/rock_albedo.png.import new file mode 100644 index 0000000..13b9989 --- /dev/null +++ b/terrain/jajce/textures/rock_albedo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cf16txygbbyqb" +path="res://.godot/imported/rock_albedo.png-79acf4ac9e1b72da4ea4de6be8601de9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://terrain/jajce/textures/rock_albedo.png" +dest_files=["res://.godot/imported/rock_albedo.png-79acf4ac9e1b72da4ea4de6be8601de9.ctex"] + +[params] + +compress/mode=0 +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=true +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=1 diff --git a/terrain/jajce/textures/soil_albedo.png b/terrain/jajce/textures/soil_albedo.png new file mode 100644 index 0000000..ab030b5 Binary files /dev/null and b/terrain/jajce/textures/soil_albedo.png differ diff --git a/terrain/jajce/textures/soil_albedo.png.import b/terrain/jajce/textures/soil_albedo.png.import new file mode 100644 index 0000000..1b16ff9 --- /dev/null +++ b/terrain/jajce/textures/soil_albedo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b75t0ego2sdja" +path="res://.godot/imported/soil_albedo.png-ea48d391fd40c4b02fb09bdb7bd9897d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://terrain/jajce/textures/soil_albedo.png" +dest_files=["res://.godot/imported/soil_albedo.png-ea48d391fd40c4b02fb09bdb7bd9897d.ctex"] + +[params] + +compress/mode=0 +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=true +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=1 diff --git a/tests/jajce_world_scaffold_test.gd b/tests/jajce_world_scaffold_test.gd index 0693e03..57ea20f 100644 --- a/tests/jajce_world_scaffold_test.gd +++ b/tests/jajce_world_scaffold_test.gd @@ -22,9 +22,26 @@ func _run() -> void: terrain.data_directory == "res://terrain/jajce/data", "Terrain3D should use dedicated Jajce data" ) + _check( + terrain.assets.get_texture_count() == 3, + "Jajce terrain should expose its bounded three-material palette (found %s)" + % terrain.assets.get_texture_count() + ) + _check( + absf(terrain.data.get_height(Vector3.ZERO)) < 0.1, + "Central gameplay area should remain level for current navigation" + ) + _check( + terrain.data.get_height(Vector3(-72, 0, 18)) > 10.0, + "Authored terrain should raise the western fortress ridge" + ) _check(world.has_node("WaterRoot"), "JajceWorld should expose WaterRoot") _check(world.has_node("VillageRoot"), "JajceWorld should expose VillageRoot") _check(world.has_node("FoliageRoot"), "JajceWorld should expose FoliageRoot") + _check( + world.get_node("FoliageRoot").get_child_count() >= 10, + "JajceWorld should include restrained authored foliage clusters" + ) _check( world.has_node("GreyboxLandmarks/FortressBlockout"), "Greybox should include the fortress landmark" @@ -33,6 +50,18 @@ func _run() -> void: world.has_node("WaterRoot/WaterfallGreybox"), "Greybox should include the waterfall drop" ) + _check( + world.has_node("WaterRoot/WaterfallFoam") + and world.has_node("WaterRoot/Mist_01"), + "Waterfall should include readable foam and mist layers" + ) + var environment: Environment = ( + world.get_node("WorldEnvironment") as WorldEnvironment + ).environment + _check( + environment.fog_enabled and environment.sky != null, + "Jajce environment should provide sky and restrained valley fog" + ) _check( world.has_node("VillageRoot/MillBlockout") and world.has_node("VillageRoot/BridgeBlockout"), diff --git a/tools/generate_jajce_terrain.gd b/tools/generate_jajce_terrain.gd new file mode 100644 index 0000000..c2675fe --- /dev/null +++ b/tools/generate_jajce_terrain.gd @@ -0,0 +1,132 @@ +extends SceneTree + +const MAP_SIZE := 512 +const HALF_SIZE := MAP_SIZE / 2 +const HEIGHT_SCALE := 40.0 +const DATA_DIRECTORY := "res://terrain/jajce/data" +const TEXTURE_DIRECTORY := "res://terrain/jajce/textures" + +func _initialize() -> void: + call_deferred("_generate") + +func _generate() -> void: + DirAccess.make_dir_recursive_absolute( + ProjectSettings.globalize_path(TEXTURE_DIRECTORY) + ) + _generate_texture("grass_albedo.png", Color("#516f36"), Color("#79924c"), 17) + _generate_texture("soil_albedo.png", Color("#65513b"), Color("#92734d"), 29) + _generate_texture("rock_albedo.png", Color("#55534f"), Color("#89847b"), 43) + + var terrain := Terrain3D.new() + terrain.region_size = 256 + root.add_child(terrain) + await process_frame + + var height_map := Image.create_empty( + MAP_SIZE, + MAP_SIZE, + false, + Image.FORMAT_RF + ) + var noise := FastNoiseLite.new() + noise.seed = 1943 + noise.frequency = 0.018 + noise.fractal_octaves = 4 + noise.fractal_gain = 0.45 + + for image_z in MAP_SIZE: + for image_x in MAP_SIZE: + var world_x := float(image_x - HALF_SIZE) + var world_z := float(image_z - HALF_SIZE) + var distance := Vector2(world_x, world_z).length() + var outside_play_area := smoothstep(20.0, 52.0, distance) + var ridge := 28.0 * _elliptical_peak( + world_x, + world_z, + -72.0, + 18.0, + 72.0, + 48.0 + ) + var eastern_hill := 15.0 * _elliptical_peak( + world_x, + world_z, + 105.0, + 65.0, + 110.0, + 90.0 + ) + var southern_hill := 11.0 * _elliptical_peak( + world_x, + world_z, + -35.0, + -125.0, + 130.0, + 85.0 + ) + var river_trench := -5.0 * exp( + -pow((world_x - 25.0) / 18.0, 2.0) + ) + var broad_slope := maxf(0.0, (-world_x - 30.0) * 0.035) + var detail := noise.get_noise_2d(world_x, world_z) * 2.4 + var height := ( + ridge + + eastern_hill + + southern_hill + + river_trench + + broad_slope + + detail + ) * outside_play_area + height_map.set_pixel( + image_x, + image_z, + Color(height / HEIGHT_SCALE, 0.0, 0.0, 1.0) + ) + + terrain.data.import_images( + [height_map, null, null], + Vector3(-HALF_SIZE, 0.0, -HALF_SIZE), + 0.0, + HEIGHT_SCALE + ) + terrain.data.save_directory(DATA_DIRECTORY) + print("[TerrainGenerator] Generated bounded Jajce terrain and textures") + quit(0) + +func _elliptical_peak( + x: float, + z: float, + center_x: float, + center_z: float, + width: float, + depth: float +) -> float: + return exp( + -( + pow((x - center_x) / width, 2.0) + + pow((z - center_z) / depth, 2.0) + ) + ) + +func _generate_texture( + file_name: String, + dark_color: Color, + light_color: Color, + seed_value: int +) -> void: + var noise := FastNoiseLite.new() + noise.seed = seed_value + noise.frequency = 0.035 + noise.fractal_octaves = 3 + var image := Image.create_empty(256, 256, false, Image.FORMAT_RGBA8) + for y in image.get_height(): + for x in image.get_width(): + var value := noise.get_noise_2d(x, y) * 0.5 + 0.5 + var color := dark_color.lerp(light_color, value) + color.a = value + image.set_pixel(x, y, color) + image.generate_mipmaps() + var path := TEXTURE_DIRECTORY.path_join(file_name) + var error := image.save_png(path) + if error != OK: + push_error("Could not write terrain texture: " + path) diff --git a/tools/generate_jajce_terrain.gd.uid b/tools/generate_jajce_terrain.gd.uid new file mode 100644 index 0000000..0101bbc --- /dev/null +++ b/tools/generate_jajce_terrain.gd.uid @@ -0,0 +1 @@ +uid://cvcgl6q54kq2v diff --git a/world/jajce/JajceWorld.tscn b/world/jajce/JajceWorld.tscn index 7d97a80..9ec32dd 100644 --- a/world/jajce/JajceWorld.tscn +++ b/world/jajce/JajceWorld.tscn @@ -1,10 +1,21 @@ -[gd_scene load_steps=21 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"] [ext_resource type="Script" path="res://world/jajce/jajce_world.gd" id="3_world"] +[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="4_tree"] [sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"] +_shader_parameters = { +"auto_base_texture": 1, +"auto_overlay_texture": 0, +"auto_slope": 0.72, +"blend_sharpness": 0.82, +"enable_macro_variation": true, +"macro_variation1": Color(0.9, 0.94, 0.82, 1), +"macro_variation2": Color(0.76, 0.81, 0.7, 1), +"macro_variation_slope": 0.38 +} world_background = 0 auto_shader = true @@ -22,8 +33,13 @@ albedo_color = Color(0.34, 0.31, 0.27, 1) albedo_color = Color(0.42, 0.5, 0.29, 1) [sub_resource type="StandardMaterial3D" id="Material_water"] -albedo_color = Color(0.16, 0.43, 0.62, 0.78) +albedo_color = Color(0.12, 0.39, 0.55, 0.72) transparency = 1 +metallic = 0.18 +roughness = 0.12 +emission_enabled = true +emission = Color(0.05, 0.19, 0.24, 1) +emission_energy_multiplier = 0.45 [sub_resource type="StandardMaterial3D" id="Material_building"] albedo_color = Color(0.62, 0.48, 0.32, 1) @@ -63,12 +79,51 @@ size = Vector3(8, 0.5, 3) material = SubResource("Material_water") size = Vector3(5, 10, 0.5) +[sub_resource type="StandardMaterial3D" id="Material_foam"] +albedo_color = Color(0.82, 0.94, 0.94, 0.85) +transparency = 1 +roughness = 0.25 +emission_enabled = true +emission = Color(0.36, 0.52, 0.54, 1) +emission_energy_multiplier = 0.35 + +[sub_resource type="BoxMesh" id="Mesh_foam"] +material = SubResource("Material_foam") +size = Vector3(5.5, 0.12, 2.2) + +[sub_resource type="StandardMaterial3D" id="Material_mist"] +albedo_color = Color(0.82, 0.91, 0.92, 0.16) +transparency = 1 +shading_mode = 0 + +[sub_resource type="SphereMesh" id="Mesh_mist"] +material = SubResource("Material_mist") +radius = 2.8 +height = 4.0 + +[sub_resource type="ProceduralSkyMaterial" id="SkyMaterial_jajce"] +sky_top_color = Color(0.23, 0.43, 0.62, 1) +sky_horizon_color = Color(0.82, 0.68, 0.5, 1) +ground_bottom_color = Color(0.12, 0.17, 0.12, 1) +ground_horizon_color = Color(0.62, 0.55, 0.42, 1) +sun_angle_max = 18.0 + +[sub_resource type="Sky" id="Sky_jajce"] +sky_material = SubResource("SkyMaterial_jajce") + [sub_resource type="Environment" id="Environment_greybox"] -background_mode = 1 -background_color = Color(0.55, 0.72, 0.82, 1) -ambient_light_source = 2 -ambient_light_color = Color(1, 0.9, 0.78, 1) -ambient_light_energy = 0.55 +background_mode = 2 +sky = SubResource("Sky_jajce") +ambient_light_source = 3 +ambient_light_color = Color(0.88, 0.83, 0.73, 1) +ambient_light_energy = 0.62 +reflected_light_source = 2 +tonemap_mode = 2 +fog_enabled = true +fog_light_color = Color(0.7, 0.76, 0.74, 1) +fog_light_energy = 0.65 +fog_density = 0.0025 +fog_sky_affect = 0.35 [node name="JajceWorld" type="Node3D"] script = ExtResource("3_world") @@ -89,6 +144,7 @@ top_level = true [node name="GreyboxGround" type="StaticBody3D" parent="NavigationRegion3D"] [node name="Mesh" type="MeshInstance3D" parent="NavigationRegion3D/GreyboxGround"] +visible = false mesh = SubResource("Mesh_ground") [node name="Collision" type="CollisionShape3D" parent="NavigationRegion3D/GreyboxGround"] @@ -115,13 +171,27 @@ mesh = SubResource("Mesh_terrace") [node name="WaterRoot" type="Node3D" parent="."] [node name="RiverGreybox" type="MeshInstance3D" parent="WaterRoot"] -position = Vector3(25, 0.08, 0) +position = Vector3(25, 0.12, 0) mesh = SubResource("Mesh_river") [node name="WaterfallGreybox" type="MeshInstance3D" parent="WaterRoot"] position = Vector3(25, 5, -24) mesh = SubResource("Mesh_waterfall") +[node name="WaterfallFoam" type="MeshInstance3D" parent="WaterRoot"] +position = Vector3(25, 0.22, -20.5) +mesh = SubResource("Mesh_foam") + +[node name="Mist_01" type="MeshInstance3D" parent="WaterRoot"] +position = Vector3(24, 1.7, -20) +scale = Vector3(1.5, 0.55, 1) +mesh = SubResource("Mesh_mist") + +[node name="Mist_02" type="MeshInstance3D" parent="WaterRoot"] +position = Vector3(27, 2.2, -22) +scale = Vector3(1.1, 0.4, 0.8) +mesh = SubResource("Mesh_mist") + [node name="VillageRoot" type="Node3D" parent="."] [node name="House_01" type="MeshInstance3D" parent="VillageRoot"] @@ -146,6 +216,44 @@ mesh = SubResource("Mesh_bridge") [node name="FoliageRoot" type="Node3D" parent="."] +[node name="Tree_West_01" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(-33, 0, -15) +scale = Vector3(1.25, 1.25, 1.25) + +[node name="Tree_West_02" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(-29, 0, -21) +scale = Vector3(0.9, 0.9, 0.9) + +[node name="Tree_West_03" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(-38, 0, 2) +scale = Vector3(1.4, 1.4, 1.4) + +[node name="Tree_North_01" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(-2, 0, 25) + +[node name="Tree_North_02" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(7, 0, 28) +scale = Vector3(1.2, 1.2, 1.2) + +[node name="Tree_North_03" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(15, 0, 24) +scale = Vector3(0.85, 0.85, 0.85) + +[node name="Tree_South_01" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(-16, 0, -27) +scale = Vector3(1.15, 1.15, 1.15) + +[node name="Tree_South_02" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(-4, 0, -30) + +[node name="Tree_River_01" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(19, 0, 18) +scale = Vector3(0.9, 0.9, 0.9) + +[node name="Tree_River_02" parent="FoliageRoot" instance=ExtResource("4_tree")] +position = Vector3(31, 0, 14) +scale = Vector3(1.25, 1.25, 1.25) + [node name="WorldObjects" type="Node3D" parent="."] [node name="ResourceNodes" type="Node3D" parent="WorldObjects"] @@ -192,7 +300,9 @@ position = Vector3(4, 0, 5) [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] rotation_degrees = Vector3(-55, -35, 0) light_color = Color(1, 0.87, 0.72, 1) -light_energy = 1.25 +light_energy = 1.45 +shadow_enabled = true +directional_shadow_max_distance = 180.0 [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment_greybox") diff --git a/world/jajce/StylizedTree.tscn b/world/jajce/StylizedTree.tscn new file mode 100644 index 0000000..fc5f0dd --- /dev/null +++ b/world/jajce/StylizedTree.tscn @@ -0,0 +1,30 @@ +[gd_scene load_steps=5 format=3] + +[sub_resource type="StandardMaterial3D" id="Material_trunk"] +albedo_color = Color(0.27, 0.16, 0.09, 1) +roughness = 0.95 + +[sub_resource type="CylinderMesh" id="Mesh_trunk"] +material = SubResource("Material_trunk") +top_radius = 0.22 +bottom_radius = 0.32 +height = 2.8 + +[sub_resource type="StandardMaterial3D" id="Material_canopy"] +albedo_color = Color(0.19, 0.38, 0.17, 1) +roughness = 0.9 + +[sub_resource type="SphereMesh" id="Mesh_canopy"] +material = SubResource("Material_canopy") +radius = 1.35 +height = 2.4 + +[node name="StylizedTree" type="Node3D"] + +[node name="Trunk" type="MeshInstance3D" parent="."] +position = Vector3(0, 1.4, 0) +mesh = SubResource("Mesh_trunk") + +[node name="Canopy" type="MeshInstance3D" parent="."] +position = Vector3(0, 3.2, 0) +mesh = SubResource("Mesh_canopy") diff --git a/world/jajce/jajce_world.gd b/world/jajce/jajce_world.gd index 80ae15d..db90113 100644 --- a/world/jajce/jajce_world.gd +++ b/world/jajce/jajce_world.gd @@ -3,9 +3,13 @@ extends Node3D @onready var terrain: Terrain3D = $TerrainRoot/Terrain3D func _ready() -> void: - call_deferred("_bind_active_camera") + call_deferred("_initialize_world_presentation") -func _bind_active_camera() -> void: +func _initialize_world_presentation() -> void: var active_camera := get_viewport().get_camera_3d() if active_camera != null: terrain.set_camera(active_camera) + for tree in $FoliageRoot.get_children(): + var terrain_height: float = terrain.data.get_height(tree.global_position) + if not is_nan(terrain_height): + tree.global_position.y = terrain_height