Improve tree gathering and rain occlusion

This commit is contained in:
Alexander Sellite 2026-08-24 14:34:58 -04:00
parent a7c71213cc
commit 3608be41ab
8 changed files with 165 additions and 2 deletions

View file

@ -12,6 +12,9 @@ const GeneratedLakePool: FishPool = preload(
const GeneratedRiverPool: FishPool = preload( const GeneratedRiverPool: FishPool = preload(
"res://fish/pools/generated_river_pool.tres" "res://fish/pools/generated_river_pool.tres"
) )
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const FIRST_SEED := 13001 const FIRST_SEED := 13001
const SECOND_SEED := 13002 const SECOND_SEED := 13002
const RIVER_FALLBACK_SEED := 13012 const RIVER_FALLBACK_SEED := 13012
@ -502,6 +505,12 @@ func _validate_generated_region(
assert(PROCEDURAL_PROP_IDS.has(prop_id)) assert(PROCEDURAL_PROP_IDS.has(prop_id))
var definition := region.get_prop_catalog().definition_for_id(prop_id) var definition := region.get_prop_catalog().definition_for_id(prop_id)
assert(definition != null) assert(definition != null)
if definition.procedural_group in [&"grass_tree", &"sand_tree"]:
assert(
_has_precipitation_occlusion_layer(child),
"Precipitation occlusion layer missing from %s (%s)."
% [child.name, prop_id],
)
var biome_id := StringName( var biome_id := StringName(
child.get_meta(&"terrain_biome_id", &"") child.get_meta(&"terrain_biome_id", &"")
) )
@ -967,10 +976,12 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
assert(pine != null) assert(pine != null)
assert(pine.minimum_visual_scale < pine.maximum_visual_scale) assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
assert(pine.material_variants.is_empty()) assert(pine.material_variants.is_empty())
assert(not pine.has_gatherable_surface())
var large_pine := catalog.definition_for_id(&"prop_pine_large") var large_pine := catalog.definition_for_id(&"prop_pine_large")
assert(large_pine != null) assert(large_pine != null)
assert(large_pine.minimum_visual_scale < large_pine.maximum_visual_scale) assert(large_pine.minimum_visual_scale < large_pine.maximum_visual_scale)
assert(large_pine.material_variants.is_empty()) assert(large_pine.material_variants.is_empty())
assert(not large_pine.has_gatherable_surface())
var palm := catalog.definition_for_id(&"prop_palm") var palm := catalog.definition_for_id(&"prop_palm")
assert(palm != null) assert(palm != null)
assert(is_equal_approx(palm.minimum_visual_scale, 0.5)) assert(is_equal_approx(palm.minimum_visual_scale, 0.5))
@ -1180,6 +1191,21 @@ func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
return null return null
func _has_precipitation_occlusion_layer(root_node: Node) -> bool:
var mesh_instance := root_node as MeshInstance3D
if (
mesh_instance != null
and mesh_instance.get_layer_mask_value(
PrecipitationOcclusionType.RENDER_LAYER_NUMBER
)
):
return true
for child: Node in root_node.get_children():
if _has_precipitation_occlusion_layer(child):
return true
return false
func _has_material_variant_override( func _has_material_variant_override(
root_node: Node, root_node: Node,
variants: Array[Material], variants: Array[Material],

View file

@ -10,6 +10,9 @@ const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldTimeVisualControllerType = preload( const WorldTimeVisualControllerType = preload(
"res://world/world_time_visual_controller.gd" "res://world/world_time_visual_controller.gd"
) )
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const FishingContextType = preload("res://fishing/fishing_context.gd") const FishingContextType = preload("res://fishing/fishing_context.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd") const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const FishAvailabilityType = preload("res://fish/fish_availability.gd") const FishAvailabilityType = preload("res://fish/fish_availability.gd")
@ -526,6 +529,14 @@ func _validate_weather_presentation() -> void:
)) ))
var rain_material := rain.process_material as ParticleProcessMaterial var rain_material := rain.process_material as ParticleProcessMaterial
assert(rain_material != null) assert(rain_material != null)
assert(
rain_material.collision_mode
== ParticleProcessMaterial.COLLISION_HIDE_ON_CONTACT
)
assert(is_equal_approx(
rain.collision_base_size,
WorldTimeVisualControllerType.RAIN_COLLISION_BASE_SIZE,
))
assert(rain_material.emission_box_extents.is_equal_approx( assert(rain_material.emission_box_extents.is_equal_approx(
WorldTimeVisualControllerType.RAIN_EMISSION_EXTENTS WorldTimeVisualControllerType.RAIN_EMISSION_EXTENTS
)) ))
@ -542,6 +553,26 @@ func _validate_weather_presentation() -> void:
assert(rain_mesh.size.is_equal_approx( assert(rain_mesh.size.is_equal_approx(
WorldTimeVisualControllerType.RAIN_DROP_SIZE WorldTimeVisualControllerType.RAIN_DROP_SIZE
)) ))
var rain_collision := visuals.get_node(
"LocalRainCanopyCollision"
) as GPUParticlesCollisionHeightField3D
assert(rain_collision != null)
assert(rain_collision.follow_camera_enabled)
assert(
rain_collision.resolution
== GPUParticlesCollisionHeightField3D.RESOLUTION_256
)
assert(
rain_collision.update_mode
== GPUParticlesCollisionHeightField3D.UPDATE_MODE_WHEN_MOVED
)
assert(rain_collision.size.is_equal_approx(
WorldTimeVisualControllerType.RAIN_COLLISION_SIZE
))
assert(
rain_collision.heightfield_mask
== PrecipitationOcclusionType.RENDER_LAYER_MASK
)
visuals.call( visuals.call(
"_on_weather_changed", "_on_weather_changed",
WorldWeatherServiceType.Weather.SUNNY, WorldWeatherServiceType.Weather.SUNNY,

View file

@ -0,0 +1,56 @@
class_name PrecipitationOcclusion
extends RefCounted
## A secondary visual layer used only when building the local rain height field.
## Canopy meshes remain on layer 1 for ordinary cameras.
const RENDER_LAYER_NUMBER: int = 20
const RENDER_LAYER_MASK: int = 1 << (RENDER_LAYER_NUMBER - 1)
const CANOPY_MATERIAL_NAMES: Array[String] = [
"leaf",
"leaf_light",
"leaf_mid",
"leaf_dark",
"pine",
"tree",
]
static func mark_canopy_meshes(root_node: Node) -> int:
if root_node == null:
return 0
var marked_count: int = 0
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null and _has_canopy_material(mesh_instance):
mesh_instance.layers |= RENDER_LAYER_MASK
marked_count += 1
for child: Node in root_node.get_children():
marked_count += mark_canopy_meshes(child)
return marked_count
## Generated tree definitions are already explicit terrain metadata, so every
## mesh in their visual scene can safely participate. This also supports older
## combined tree meshes whose imported material name does not identify leaves.
static func mark_tree_meshes(root_node: Node) -> int:
if root_node == null:
return 0
var marked_count: int = 0
var mesh_instance := root_node as MeshInstance3D
if mesh_instance != null:
mesh_instance.layers |= RENDER_LAYER_MASK
marked_count += 1
for child: Node in root_node.get_children():
marked_count += mark_tree_meshes(child)
return marked_count
static func _has_canopy_material(mesh_instance: MeshInstance3D) -> bool:
if mesh_instance == null or mesh_instance.mesh == null:
return false
for surface_index: int in mesh_instance.mesh.get_surface_count():
var material := mesh_instance.mesh.surface_get_material(surface_index)
if material == null:
continue
if material.resource_name.to_lower() in CANOPY_MATERIAL_NAMES:
return true
return false

View file

@ -12,6 +12,9 @@ const PlayerStorageInteractionType = preload(
const MeshSurfaceAnchorSamplerType = preload( const MeshSurfaceAnchorSamplerType = preload(
"res://world/generation/mesh_surface_anchor_sampler.gd" "res://world/generation/mesh_surface_anchor_sampler.gd"
) )
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn") const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
const SALT_WATER_MATERIAL: Material = preload( const SALT_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water.tres" "res://world/materials/stylized_water.tres"
@ -292,9 +295,21 @@ func _on_generation_completed(summary: Dictionary) -> void:
_place_spawn_amenities(spawn_position) _place_spawn_amenities(spawn_position)
_configure_fresh_water(records) _configure_fresh_water(records)
_configure_diggable_area() _configure_diggable_area()
# Imported prop instances may finish applying their scene state while the
# generated world is assembled. Mark the final decoration tree once every
# prop and material variant is in place so rain canopy occlusion persists.
_mark_precipitation_occluders()
world_generated.emit(_current_seed, summary) world_generated.emit(_current_seed, summary)
func _mark_precipitation_occluders() -> void:
for prop: Node in _decorations.get_children():
var prop_group := prop.get_meta(&"terrain_prop_group", &"") as StringName
if prop_group not in [&"grass_tree", &"sand_tree"]:
continue
PrecipitationOcclusionType.mark_tree_meshes(prop)
func _assign_biomes( func _assign_biomes(
records: Array[Dictionary], records: Array[Dictionary],
summary: Dictionary, summary: Dictionary,

View file

@ -17,4 +17,3 @@ minimum_visual_scale = 0.65
maximum_visual_scale = 1.2 maximum_visual_scale = 1.2
collision_radius = 0.5 collision_radius = 0.5
collision_height = 4.0 collision_height = 4.0
gatherable_surface_material_names = PackedStringArray("wood")

View file

@ -17,4 +17,3 @@ minimum_visual_scale = 0.75
maximum_visual_scale = 1.2 maximum_visual_scale = 1.2
collision_radius = 0.65 collision_radius = 0.65
collision_height = 9.5 collision_height = 9.5
gatherable_surface_material_names = PackedStringArray("wood")

View file

@ -11,6 +11,9 @@ const PlayerStorageInteractionType = preload(
const FOLIAGE_WIND_SHADER: Shader = preload( const FOLIAGE_WIND_SHADER: Shader = preload(
"res://world/materials/foliage_wind.gdshader" "res://world/materials/foliage_wind.gdshader"
) )
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const FOLIAGE_MATERIAL_NAMES: Array[StringName] = [ const FOLIAGE_MATERIAL_NAMES: Array[StringName] = [
&"leaf", &"leaf",
&"leaf_light", &"leaf_light",
@ -60,6 +63,9 @@ var _foliage_wind_enabled: bool = false
func _ready() -> void: func _ready() -> void:
if rebuild_terrain_collision_on_ready or not has_terrain_collision(): if rebuild_terrain_collision_on_ready or not has_terrain_collision():
_build_terrain_collision() _build_terrain_collision()
PrecipitationOcclusionType.mark_canopy_meshes(
get_node_or_null(terrain_visual_root_path)
)
_set_foliage_wind_enabled(true) _set_foliage_wind_enabled(true)
if Engine.is_editor_hint(): if Engine.is_editor_hint():
update_configuration_warnings() update_configuration_warnings()

View file

@ -17,6 +17,8 @@ const LIGHT_RAIN_FIXED_FPS: int = 8
const RAIN_VELOCITY_MIN: float = 16.0 const RAIN_VELOCITY_MIN: float = 16.0
const RAIN_VELOCITY_MAX: float = 20.0 const RAIN_VELOCITY_MAX: float = 20.0
const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014) const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)
const RAIN_COLLISION_BASE_SIZE: float = 0.08
const RAIN_COLLISION_SIZE := Vector3(28.0, 24.0, 28.0)
const FOG_DAYLIGHT_SCENE_BRIGHTNESS: float = 0.42 const FOG_DAYLIGHT_SCENE_BRIGHTNESS: float = 0.42
const FOG_DAYLIGHT_FOG_LIGHT_BRIGHTNESS: float = 0.42 const FOG_DAYLIGHT_FOG_LIGHT_BRIGHTNESS: float = 0.42
const FOG_DAYLIGHT_WATER_BRIGHTNESS: float = 0.42 const FOG_DAYLIGHT_WATER_BRIGHTNESS: float = 0.42
@ -37,6 +39,9 @@ const FRESH_WATER_MATERIAL: ShaderMaterial = preload(
const LocalStormCloudLayerType = preload( const LocalStormCloudLayerType = preload(
"res://world/environment/local_storm_cloud_layer.gd" "res://world/environment/local_storm_cloud_layer.gd"
) )
const PrecipitationOcclusionType = preload(
"res://world/environment/precipitation_occlusion.gd"
)
const DAY_SKY_TOP := Color(0.204, 0.498, 0.643) const DAY_SKY_TOP := Color(0.204, 0.498, 0.643)
const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847) const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847)
@ -88,6 +93,7 @@ var _rain_camera_provider: Callable
var _environment: Environment var _environment: Environment
var _sky_material: ShaderMaterial var _sky_material: ShaderMaterial
var _rain: GPUParticles3D var _rain: GPUParticles3D
var _rain_collision: GPUParticlesCollisionHeightField3D
var _storm_clouds: LocalStormCloudLayer var _storm_clouds: LocalStormCloudLayer
var _elapsed: float = 0.0 var _elapsed: float = 0.0
var _weather_from: WorldWeatherService.Weather = ( var _weather_from: WorldWeatherService.Weather = (
@ -211,6 +217,7 @@ func _prepare_rain() -> void:
LIGHT_RAIN_FIXED_FPS if _light_performance_profile else 30 LIGHT_RAIN_FIXED_FPS if _light_performance_profile else 30
) )
_rain.local_coords = false _rain.local_coords = false
_rain.collision_base_size = RAIN_COLLISION_BASE_SIZE
_rain.visibility_aabb = RAIN_VISIBILITY_AABB _rain.visibility_aabb = RAIN_VISIBILITY_AABB
var process_material := ParticleProcessMaterial.new() var process_material := ParticleProcessMaterial.new()
process_material.emission_shape = ( process_material.emission_shape = (
@ -222,6 +229,9 @@ func _prepare_rain() -> void:
process_material.initial_velocity_min = RAIN_VELOCITY_MIN process_material.initial_velocity_min = RAIN_VELOCITY_MIN
process_material.initial_velocity_max = RAIN_VELOCITY_MAX process_material.initial_velocity_max = RAIN_VELOCITY_MAX
process_material.gravity = Vector3(0.0, -2.0, 0.0) process_material.gravity = Vector3(0.0, -2.0, 0.0)
process_material.collision_mode = (
ParticleProcessMaterial.COLLISION_HIDE_ON_CONTACT
)
_rain.process_material = process_material _rain.process_material = process_material
var drop_mesh := BoxMesh.new() var drop_mesh := BoxMesh.new()
drop_mesh.size = RAIN_DROP_SIZE drop_mesh.size = RAIN_DROP_SIZE
@ -232,9 +242,30 @@ func _prepare_rain() -> void:
drop_mesh.material = drop_material drop_mesh.material = drop_material
_rain.draw_pass_1 = drop_mesh _rain.draw_pass_1 = drop_mesh
add_child(_rain) add_child(_rain)
_prepare_rain_collision()
_update_rain_position() _update_rain_position()
func _prepare_rain_collision() -> void:
_rain_collision = GPUParticlesCollisionHeightField3D.new()
_rain_collision.name = "LocalRainCanopyCollision"
_rain_collision.size = RAIN_COLLISION_SIZE
_rain_collision.resolution = (
GPUParticlesCollisionHeightField3D.RESOLUTION_256
)
_rain_collision.update_mode = (
GPUParticlesCollisionHeightField3D.UPDATE_MODE_WHEN_MOVED
)
_rain_collision.follow_camera_enabled = true
_rain_collision.heightfield_mask = (
PrecipitationOcclusionType.RENDER_LAYER_MASK
)
# Local rain remains on its normal layer; unrelated particle systems do not
# need to query this weather-specific collision field.
_rain_collision.cull_mask = 1
add_child(_rain_collision)
func _prepare_storm_clouds() -> void: func _prepare_storm_clouds() -> void:
_storm_clouds = LocalStormCloudLayerType.new() _storm_clouds = LocalStormCloudLayerType.new()
_storm_clouds.name = "LocalStormClouds" _storm_clouds.name = "LocalStormClouds"