feat(environment): add painterly skies and living meadows

This commit is contained in:
Rijad Zuzo
2026-09-05 20:27:09 +02:00
parent 7899d36f03
commit ec16e2f2ca
71 changed files with 2280 additions and 213 deletions
+132 -14
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=68 format=3]
[gd_scene load_steps=71 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"]
@@ -21,8 +21,8 @@
[ext_resource type="PackedScene" path="res://addons/terrain_3d/extras/particle_example/Terrain3DParticles.tscn" id="19_terrain_particles"]
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_material.tres" id="20_grass_material"]
[ext_resource type="Script" path="res://world/jajce/GrassInteractionController.gd" id="21_grass_controller"]
[ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="22_conifer"]
[ext_resource type="PackedScene" path="res://assets/foliage/tree-high.glb" id="23_conifer_high"]
[ext_resource type="PackedScene" path="res://world/jajce/PainterlyConifer.tscn" id="22_conifer"]
[ext_resource type="PackedScene" path="res://world/jajce/PainterlyConifer.tscn" id="23_conifer_high"]
[ext_resource type="PackedScene" path="res://world/jajce/CozyButterfly.tscn" id="24_butterfly"]
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
[ext_resource type="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
@@ -33,8 +33,12 @@
[ext_resource type="ArrayMesh" path="res://assets/storybook/meadow_grass.res" id="31_meadow"]
[ext_resource type="Shader" path="res://world/jajce/materials/storybook_sky.gdshader" id="32_sky"]
[ext_resource type="Texture2D" path="res://assets/painterly/painted_sky.png" id="36_painted_sky"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="33_berry"]
[ext_resource type="Script" path="res://world/jajce/PainterlyMeadow.gd" id="34_meadow"]
[ext_resource type="PackedScene" path="res://world/jajce/HarvestableTreePresentation.tscn" id="35_harvest_tree"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = {
"auto_base_texture": 0,
@@ -42,8 +46,8 @@ _shader_parameters = {
"auto_slope": 0.62,
"blend_sharpness": 0.88,
"enable_macro_variation": true,
"macro_variation1": Color(0.84, 0.95, 0.73, 1),
"macro_variation2": Color(0.60, 0.79, 0.61, 1),
"macro_variation1": Color(0.91, 0.98, 0.80, 1),
"macro_variation2": Color(0.76, 0.89, 0.76, 1),
"macro_variation_slope": 0.48
}
world_background = 0
@@ -179,6 +183,7 @@ size = Vector3(2.9, 0.08, 1.4)
[sub_resource type="ShaderMaterial" id="SkyMaterial_jajce"]
resource_local_to_scene = true
shader = ExtResource("32_sky")
shader_parameter/painted_panorama = ExtResource("36_painted_sky")
[sub_resource type="Sky" id="Sky_jajce"]
resource_local_to_scene = true
@@ -188,17 +193,17 @@ sky_material = SubResource("SkyMaterial_jajce")
resource_local_to_scene = true
background_mode = 2
sky = SubResource("Sky_jajce")
background_energy_multiplier = 0.9
ambient_light_source = 3
ambient_light_color = Color(0.95, 0.9, 0.76, 1)
ambient_light_energy = 0.75
background_energy_multiplier = 1.0
ambient_light_source = 2
ambient_light_color = Color(0.65, 0.8, 0.87, 1)
ambient_light_energy = 0.65
reflected_light_source = 2
tonemap_mode = 2
glow_enabled = true
glow_normalized = true
glow_intensity = 0.18
glow_intensity = 0.12
glow_strength = 0.9
glow_bloom = 0.03
glow_bloom = 0.015
fog_enabled = true
fog_light_color = Color(0.88, 0.84, 0.7, 1)
fog_light_energy = 0.78
@@ -216,8 +221,8 @@ volumetric_fog_length = 96.0
volumetric_fog_detail_spread = 1.6
adjustment_enabled = true
adjustment_brightness = 1.0
adjustment_contrast = 1.03
adjustment_saturation = 1.04
adjustment_contrast = 1.015
adjustment_saturation = 1.07
[sub_resource type="StandardMaterial3D" id="Material_guard"]
albedo_color = Color(0.55, 0.45, 0.35, 1)
@@ -384,6 +389,9 @@ scale = Vector3(1.1, 1, 9)
cast_shadow = 0
mesh = SubResource("Mesh_path_readability")
[node name="PainterlyMeadow" type="Node3D" parent="."]
script = ExtResource("34_meadow")
[node name="FoliageRoot" type="Node3D" parent="."]
[node name="Tree_West_01" parent="FoliageRoot" instance=ExtResource("4_tree")]
@@ -448,6 +456,91 @@ position = Vector3(8, 0, -31)
rotation_degrees = Vector3(0, -20, 0)
scale = Vector3(2.65, 2.65, 2.65)
[node name="Woodland_Edge_01" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-34, 0, -29)
rotation_degrees = Vector3(0, 0, 0)
scale = Vector3(1.3, 1.3, 1.3)
[node name="Woodland_Edge_02" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-39, 0, -25)
rotation_degrees = Vector3(0, 137, 0)
scale = Vector3(1.6, 1.6, 1.6)
[node name="Woodland_Edge_03" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-44, 0, -18)
rotation_degrees = Vector3(0, 274, 0)
scale = Vector3(1.5, 1.5, 1.5)
[node name="Woodland_Edge_04" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-45, 0, -8)
rotation_degrees = Vector3(0, 51, 0)
scale = Vector3(1.8, 1.8, 1.8)
[node name="Woodland_Edge_05" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-43, 0, 4)
rotation_degrees = Vector3(0, 188, 0)
scale = Vector3(1.15, 1.15, 1.15)
[node name="Woodland_Edge_06" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-41, 0, 16)
rotation_degrees = Vector3(0, 325, 0)
scale = Vector3(1.3, 1.3, 1.3)
[node name="Woodland_Edge_07" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-36, 0, 28)
rotation_degrees = Vector3(0, 102, 0)
scale = Vector3(1.2, 1.2, 1.2)
[node name="Woodland_Edge_08" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-26, 0, -35)
rotation_degrees = Vector3(0, 239, 0)
scale = Vector3(1.2, 1.2, 1.2)
[node name="Woodland_Edge_09" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-19, 0, -37)
rotation_degrees = Vector3(0, 16, 0)
scale = Vector3(1.55, 1.55, 1.55)
[node name="Woodland_Edge_10" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-11, 0, -38)
rotation_degrees = Vector3(0, 153, 0)
scale = Vector3(1.35, 1.35, 1.35)
[node name="Woodland_Edge_11" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(-3, 0, -36)
rotation_degrees = Vector3(0, 290, 0)
scale = Vector3(1.4, 1.4, 1.4)
[node name="Woodland_Edge_12" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(5, 0, -39)
rotation_degrees = Vector3(0, 67, 0)
scale = Vector3(1.25, 1.25, 1.25)
[node name="Woodland_Edge_13" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(13, 0, -35)
rotation_degrees = Vector3(0, 204, 0)
scale = Vector3(1.3, 1.3, 1.3)
[node name="Woodland_Edge_14" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(43, 0, -14)
rotation_degrees = Vector3(0, 341, 0)
scale = Vector3(1.5, 1.5, 1.5)
[node name="Woodland_Edge_15" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(46, 0, -5)
rotation_degrees = Vector3(0, 118, 0)
scale = Vector3(1.5, 1.5, 1.5)
[node name="Woodland_Edge_16" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(43, 0, 4)
rotation_degrees = Vector3(0, 255, 0)
scale = Vector3(1.15, 1.15, 1.15)
[node name="Woodland_Edge_17" parent="FoliageRoot" instance=ExtResource("4_tree")]
position = Vector3(43, 0, 14)
rotation_degrees = Vector3(0, 32, 0)
scale = Vector3(1.3, 1.3, 1.3)
[node name="WildlifeRoot" type="Node3D" parent="."]
[node name="Butterfly_Meadow_01" parent="WildlifeRoot" instance=ExtResource("24_butterfly")]
@@ -579,6 +672,12 @@ yield_per_action = 3.0
safety_risk = 0.12
comfort_distance = 24.0
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_01/Visual"]
visible = false
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_01/Visual" instance=ExtResource("35_harvest_tree")]
scale = Vector3(0.82, 0.82, 0.82)
[node name="Tree_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(10, 0, -4)
node_id = &"tree_02"
@@ -589,6 +688,12 @@ yield_per_action = 3.0
safety_risk = 0.16
comfort_distance = 26.0
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_02/Visual"]
visible = false
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_02/Visual" instance=ExtResource("35_harvest_tree")]
scale = Vector3(0.82, 0.82, 0.82)
[node name="Tree_North_Outskirts_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(6, 0, 12)
node_id = &"tree_north_outskirts_01"
@@ -600,6 +705,12 @@ safety_risk = 0.2
comfort_distance = 28.0
discovery_priority = 0.25
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_North_Outskirts_01/Visual"]
visible = false
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_North_Outskirts_01/Visual" instance=ExtResource("35_harvest_tree")]
scale = Vector3(0.82, 0.82, 0.82)
[node name="Tree_South_Edge_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(-12, 0, -12)
node_id = &"tree_south_edge_01"
@@ -610,6 +721,12 @@ yield_per_action = 2.0
safety_risk = 0.24
comfort_distance = 26.0
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_South_Edge_01/Visual"]
visible = false
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_South_Edge_01/Visual" instance=ExtResource("35_harvest_tree")]
scale = Vector3(0.82, 0.82, 0.82)
[node name="WoodPile_Village_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(5, 0, -13)
node_id = &"wood_pile_village_01"
@@ -914,7 +1031,8 @@ text = "Rest Bench"
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
rotation_degrees = Vector3(-55, -35, 0)
light_color = Color(1, 0.87, 0.72, 1)
light_energy = 1.45
light_energy = 1.35
light_angular_distance = 0.6
shadow_enabled = true
directional_shadow_max_distance = 180.0
+24
View File
@@ -0,0 +1,24 @@
extends Node3D
## Shared opaque boughs; decorative presentation without simulation state.
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
func _ready() -> void:
var canopy := $Geometry/ConiferCanopy as MeshInstance3D
var material := ShaderMaterial.new()
material.shader = WIND_SHADER
material.set_shader_parameter("albedo", Color(0.28, 0.47, 0.33))
material.set_shader_parameter("painted_vertex_weight", 1.0)
material.set_shader_parameter("wind_strength", 0.055)
material.set_shader_parameter("wind_speed", 0.55)
material.set_shader_parameter("wind_phase", float(posmod(hash(global_position), 6283)) / 1000.0)
material.set_shader_parameter("wind_base_height", 0.45)
material.set_shader_parameter("wind_full_height", 3.1)
canopy.material_override = material
var trunk := $Geometry/ConiferTrunk as MeshInstance3D
var bark := StandardMaterial3D.new()
bark.vertex_color_use_as_albedo = true
bark.roughness = 0.95
bark.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
trunk.material_override = bark
+1
View File
@@ -0,0 +1 @@
uid://bk3cxfe3kph2x
+9
View File
@@ -0,0 +1,9 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://world/jajce/PainterlyConifer.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://assets/painterly/painterly_conifer.glb" id="2_geometry"]
[node name="PainterlyConifer" type="Node3D"]
script = ExtResource("1_script")
[node name="Geometry" parent="." instance=ExtResource("2_geometry")]
+217
View File
@@ -0,0 +1,217 @@
class_name PainterlyMeadow
extends Node3D
## Decorative flower drifts. Local seeded placement never touches simulation RNG.
const FLOWER_SCENES: Array[PackedScene] = [
preload("res://assets/meadow/cream_daisy.glb"),
preload("res://assets/meadow/pink_cosmos.glb"),
preload("res://assets/meadow/yellow_buttercup.glb"),
preload("res://assets/meadow/lavender_spire.glb"),
]
const FLOWER_SHADER := preload("res://world/jajce/materials/meadow_flowers.gdshader")
const PROFILE_DENSITY := [1.0, 0.65, 0.32]
const PROFILE_DISTANCE := [56.0, 46.0, 34.0]
@export var terrain_path := NodePath("../TerrainRoot/Terrain3D")
@export_range(0, 450, 1) var clumps_per_species := 420
@export var placement_seed := 57091
## Local x/z center, then ellipse x/z radii. Each drift favors one flower color.
@export var drifts: Array[Vector4] = [
Vector4(7.0, 20.0, 7.0, 3.5),
Vector4(15.0, 13.0, 2.4, 4.0),
Vector4(-5.0, 20.0, 5.0, 3.0),
Vector4(-18.0, -21.0, 4.5, 3.2),
Vector4(14.0, -10.0, 3.8, 4.5),
Vector4(-25.0, -17.0, 3.5, 4.0),
Vector4(8.0, -20.0, 5.0, 2.5),
Vector4(-2.0, -24.0, 6.0, 2.2),
Vector4(17.0, 24.0, 4.0, 3.0),
Vector4(-9.0, 21.0, 3.6, 2.5),
Vector4(9.0, 7.2, 3.2, 1.8),
Vector4(-16.0, 19.0, 3.0, 2.4),
]
var _terrain: Terrain3D
var _batches: Array[MultiMeshInstance3D] = []
var _material: ShaderMaterial
var _quality := 0
var _clearings: Array[Vector3] = []
var _path_strips: Array[MeshInstance3D] = []
func _ready() -> void:
call_deferred("_build_meadow")
func apply_presentation_quality(quality: int) -> void:
_quality = clampi(quality, 0, PROFILE_DENSITY.size() - 1)
for batch in _batches:
batch.multimesh.visible_instance_count = floori(
batch.multimesh.instance_count * PROFILE_DENSITY[_quality]
)
if _material != null:
_material.set_shader_parameter("fade_begin", PROFILE_DISTANCE[_quality] - 12.0)
_material.set_shader_parameter("fade_end", PROFILE_DISTANCE[_quality])
func get_presentation_stats() -> Dictionary:
var total := 0
var visible_count := 0
for batch in _batches:
total += batch.multimesh.instance_count
visible_count += batch.multimesh.visible_instance_count
return {
"flower_clump_count": total,
"visible_flower_clump_count": visible_count,
"multimesh_batch_count": _batches.size(),
}
func _build_meadow() -> void:
if not _batches.is_empty():
return
_terrain = get_node_or_null(terrain_path) as Terrain3D
if _terrain == null or _terrain.data == null or drifts.is_empty():
return
_collect_clearings()
_material = ShaderMaterial.new()
_material.shader = FLOWER_SHADER
for species in FLOWER_SCENES.size():
var source := FLOWER_SCENES[species].instantiate()
var mesh := _find_mesh(source)
if mesh != null:
_add_batch(mesh, species)
source.free()
apply_presentation_quality(_quality)
func _find_mesh(node: Node) -> Mesh:
if node is MeshInstance3D:
return (node as MeshInstance3D).mesh
for child in node.get_children():
var mesh := _find_mesh(child)
if mesh != null:
return mesh
return null
func _add_batch(mesh: Mesh, species: int) -> void:
var rng := RandomNumberGenerator.new()
rng.seed = placement_seed + species * 173
var transforms: Array[Transform3D] = []
var variations: Array[Color] = []
for _attempt in clumps_per_species * 18:
if transforms.size() >= clumps_per_species:
break
var drift_index := rng.randi_range(0, drifts.size() - 1)
# Broad color masses with a few companion flowers at the edges.
if drift_index % FLOWER_SCENES.size() != species and rng.randf() > 0.12:
continue
var drift := drifts[drift_index]
var angle := rng.randf_range(0.0, TAU)
var radius := pow(rng.randf(), 0.7) * (0.9 + 0.12 * sin(angle * 3.0 + drift_index))
var offset := Vector3(
drift.x + cos(angle) * radius * drift.z, 0.0, drift.y + sin(angle) * radius * drift.w
)
var world_sample := to_global(offset)
if _is_cleared(world_sample):
continue
var height := _terrain.data.get_height(world_sample)
if is_nan(height):
continue
var nearby := _terrain.data.get_height(world_sample + Vector3(0.35, 0.0, 0.35))
if is_nan(nearby) or absf(height - nearby) > 0.32:
continue
var local_position := to_local(Vector3(world_sample.x, height - 0.018, world_sample.z))
var scale_variation := rng.randf_range(0.80, 1.28)
var basis := Basis(Vector3.UP, rng.randf_range(0.0, TAU)).scaled(
Vector3.ONE * scale_variation
)
transforms.append(Transform3D(basis, local_position))
variations.append(Color(rng.randf(), 0.0, 0.0, 1.0))
var multimesh := MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
# Compatibility needs an explicit neutral color alongside the custom buffer
# to preserve the GLB's painted vertex colors.
multimesh.use_colors = true
multimesh.use_custom_data = true
multimesh.mesh = mesh
multimesh.instance_count = transforms.size()
for index in transforms.size():
multimesh.set_instance_transform(index, transforms[index])
multimesh.set_instance_color(index, Color.WHITE)
multimesh.set_instance_custom_data(index, variations[index])
var batch := MultiMeshInstance3D.new()
batch.name = "FlowerDrifts_%d" % species
batch.multimesh = multimesh
batch.material_override = _material
batch.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
batch.extra_cull_margin = 0.4
add_child(batch)
_batches.append(batch)
func _collect_clearings() -> void:
var village := get_node_or_null("../VillageRoot")
if village != null:
for child in village.get_children():
if child is MeshInstance3D and str(child.name).begins_with("Path_"):
_path_strips.append(child)
elif child is Node3D:
var radius := 4.3 if str(child.name).begins_with("House_") else 4.5
_add_clearing(child, radius)
var fortress := get_node_or_null("../FortressBlockout") as Node3D
if fortress != null:
_add_clearing(fortress, 11.0)
for root_path in [
"../WorldObjects/ResourceNodes",
"../WorldObjects/StorageSites",
"../WorldObjects/ActivitySites",
"../WorldObjects/AnimalHabitats",
]:
var root := get_node_or_null(root_path)
if root != null:
for child in root.get_children():
if child is Node3D:
_add_worksite_clearing(child)
var clusters := get_node_or_null("../WorldObjects/ResourceClusters")
if clusters != null:
for cluster in clusters.get_children():
var anchors := cluster.get_node_or_null("ResourceAnchors")
if anchors == null:
continue
for anchor in anchors.get_children():
if anchor is Node3D:
_add_worksite_clearing(anchor)
func _add_worksite_clearing(node: Node3D) -> void:
_add_clearing(node, 2.0)
var interaction := node.get_node_or_null("InteractionPoint") as Node3D
if interaction != null:
_add_clearing(interaction, 1.2)
func _add_clearing(node: Node3D, radius: float) -> void:
_clearings.append(Vector3(node.global_position.x, node.global_position.z, radius))
func _is_cleared(point: Vector3) -> bool:
var flat := Vector2(point.x, point.z)
for clearing in _clearings:
if flat.distance_squared_to(Vector2(clearing.x, clearing.y)) < clearing.z * clearing.z:
return true
for strip in _path_strips:
var box := strip.mesh as BoxMesh
if box == null:
continue
var local := strip.to_local(point)
var half_width := box.size.x * 0.5 + 0.35 / strip.global_basis.x.length()
if absf(local.x) < half_width and absf(local.z) < box.size.z * 0.5 + 0.08:
return true
var river_distance := absf(point.x - JajceWatercourse.downstream_center_x(point.z))
return (
point.z >= JajceWatercourse.DOWNSTREAM_START_Z - 3.0
and point.z <= JajceWatercourse.DOWNSTREAM_END_Z
and river_distance < JajceWatercourse.downstream_half_width(point.z) + 1.7
)
+1
View File
@@ -0,0 +1 @@
uid://cacjl430m8mcw
+1 -1
View File
@@ -4,7 +4,7 @@
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="3_tree"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="4_berry"]
[ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="5_conifer"]
[ext_resource type="PackedScene" path="res://world/jajce/PainterlyConifer.tscn" id="5_conifer"]
[ext_resource type="PackedScene" path="res://world/jajce/HarvestableTreePresentation.tscn" id="6_harvestable_tree"]
[node name="RiverbankResourceCluster" type="Node3D"]
+33 -51
View File
@@ -7,15 +7,20 @@ enum CanopyVariant {
}
const COLORS := {
CanopyVariant.GREEN: Color(0.32, 0.51, 0.29),
CanopyVariant.DARK_GREEN: Color(0.22, 0.41, 0.30),
CanopyVariant.YELLOW_GREEN: Color(0.49, 0.60, 0.28),
CanopyVariant.GREEN: Color(0.40, 0.62, 0.28),
CanopyVariant.DARK_GREEN: Color(0.26, 0.51, 0.34),
CanopyVariant.YELLOW_GREEN: Color(0.55, 0.67, 0.29),
}
const TREE_MODEL := preload("res://assets/painterly/painterly_tree.glb")
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
const WIND_STRENGTH := 0.07
const WIND_SPEED := 0.55
static var _canopy_mesh: Mesh
static var _trunk_mesh: Mesh
static var _bark_material: StandardMaterial3D
func _ready() -> void:
for child in get_children():
@@ -28,76 +33,56 @@ func _ready() -> void:
var canopy_radius := 1.25 + rng.randf() * 0.6
var wind_phase := rng.randf_range(0.0, TAU)
var trunk_mesh := CylinderMesh.new()
trunk_mesh.top_radius = 0.22
trunk_mesh.bottom_radius = 0.32
trunk_mesh.height = 3.4
trunk_mesh.radial_segments = 9
var trunk_mat := StandardMaterial3D.new()
trunk_mat.albedo_color = Color(0.27, 0.16, 0.09)
trunk_mat.roughness = 0.95
trunk_mesh.material = trunk_mat
_ensure_shared_meshes()
var trunk := MeshInstance3D.new()
trunk.name = "Trunk"
trunk.mesh = trunk_mesh
trunk.position = Vector3(0, 1.7, 0)
trunk.mesh = _trunk_mesh
trunk.material_override = _bark_material
add_child(trunk)
_add_branch(Vector3(-0.18, 2.45, 0.0), Vector3(0.0, 0.0, 42.0), 1.45, trunk_mat)
_add_branch(Vector3(0.2, 2.6, 0.08), Vector3(24.0, 0.0, -38.0), 1.25, trunk_mat)
var canopy_color: Color = COLORS[canopy_variant]
var crown_height := 3.7 + (canopy_radius - 1.2) * 0.45
_add_canopy(
"Canopy",
Vector3(0.0, crown_height, 0.0),
Vector3(canopy_radius * 1.12, canopy_radius, canopy_radius),
Vector3(canopy_radius * 1.18, canopy_radius * 0.96, canopy_radius * 1.05),
canopy_color,
wind_phase
)
_add_canopy(
"CanopyLeft",
Vector3(-0.82, crown_height - 0.15, 0.1),
Vector3(canopy_radius * 0.78, canopy_radius * 0.72, canopy_radius * 0.76),
Vector3(-0.94, crown_height - 0.27, 0.14),
Vector3(canopy_radius * 0.84, canopy_radius * 0.79, canopy_radius * 0.84),
canopy_color.lightened(0.05),
wind_phase + 0.12
)
_add_canopy(
"CanopyRight",
Vector3(0.78, crown_height - 0.05, -0.12),
Vector3(canopy_radius * 0.74, canopy_radius * 0.68, canopy_radius * 0.72),
Vector3(0.91, crown_height - 0.08, -0.2),
Vector3(canopy_radius * 0.82, canopy_radius * 0.73, canopy_radius * 0.79),
canopy_color.darkened(0.04),
wind_phase - 0.1
)
_add_canopy(
"CanopyTop",
Vector3(0.08, crown_height + 0.72, 0.04),
Vector3(canopy_radius * 0.72, canopy_radius * 0.66, canopy_radius * 0.7),
Vector3(0.13, crown_height + 0.68, -0.08),
Vector3(canopy_radius * 0.78, canopy_radius * 0.74, canopy_radius * 0.75),
canopy_color.lightened(0.08),
wind_phase + 0.05
)
func _add_branch(
branch_position: Vector3,
branch_rotation_degrees: Vector3,
branch_height: float,
material: StandardMaterial3D
) -> void:
var mesh := CylinderMesh.new()
mesh.top_radius = 0.08
mesh.bottom_radius = 0.14
mesh.height = branch_height
mesh.radial_segments = 7
mesh.material = material
var branch := MeshInstance3D.new()
branch.name = "Branch"
branch.mesh = mesh
branch.position = branch_position
branch.rotation_degrees = branch_rotation_degrees
add_child(branch)
static func _ensure_shared_meshes() -> void:
if _canopy_mesh != null:
return
var source := TREE_MODEL.instantiate()
_canopy_mesh = (source.find_child("Canopy", true, false) as MeshInstance3D).mesh
_trunk_mesh = (source.find_child("Trunk", true, false) as MeshInstance3D).mesh
_bark_material = StandardMaterial3D.new()
_bark_material.vertex_color_use_as_albedo = true
_bark_material.roughness = 0.95
_bark_material.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
source.free()
func _add_canopy(
@@ -107,11 +92,6 @@ func _add_canopy(
canopy_color: Color,
wind_phase: float
) -> void:
var mesh := SphereMesh.new()
mesh.radius = 1.0
mesh.height = 2.0
mesh.radial_segments = 12
mesh.rings = 6
var material := ShaderMaterial.new()
material.shader = WIND_SHADER
material.set_shader_parameter("albedo", canopy_color)
@@ -119,10 +99,12 @@ func _add_canopy(
material.set_shader_parameter("wind_strength", WIND_STRENGTH)
material.set_shader_parameter("wind_speed", WIND_SPEED)
material.set_shader_parameter("wind_phase", wind_phase)
mesh.material = material
material.set_shader_parameter("painted_vertex_weight", 1.0)
var canopy := MeshInstance3D.new()
canopy.name = canopy_name
canopy.mesh = mesh
canopy.mesh = _canopy_mesh
canopy.material_override = material
canopy.position = canopy_position
canopy.scale = canopy_scale
canopy.rotation.y = wind_phase * 0.43 + canopy_position.x * 0.31
add_child(canopy)
+53 -7
View File
@@ -48,6 +48,9 @@ func _build_ribbon(
var uvs := PackedVector2Array()
var indices := PackedInt32Array()
var row_size := width_segments + 1
var pool := $PlungePool as MeshInstance3D
var pool_mesh := pool.mesh as PlaneMesh
var pool_radii := pool_mesh.size * Vector2(pool.scale.x, pool.scale.z) * 0.5
for z_index in segment_count + 1:
var progress := float(z_index) / float(segment_count)
var world_z := lerpf(start_z, end_z, progress)
@@ -61,16 +64,30 @@ func _build_ribbon(
if is_upper
else Watercourse.downstream_half_width(world_z)
)
if not is_upper:
# The outlet continues the existing pool silhouette without drawing a
# second, coplanar sheet on top of the pool's transparent material.
var pool_progress := (world_z - Watercourse.PLUNGE_POOL_CENTER.y) / pool_radii.y
if absf(pool_progress) < 1.0:
var pool_width := pool_radii.x * sqrt(1.0 - pool_progress * pool_progress)
half_width = maxf(half_width, pool_width)
# A stream has one surface across its width. Sampling each bank separately
# wraps the water mesh up the terrain and makes it read as a solid ramp.
var terrain_height := terrain.data.get_height(Vector3(center_x, 0.0, world_z))
if is_nan(terrain_height):
terrain_height = global_position.y
var surface_height := terrain_height + water_offset
var left_x := _shoreline_x(
terrain, center_x, center_x - half_width, world_z, surface_height
)
var right_x := _shoreline_x(
terrain, center_x, center_x + half_width, world_z, surface_height
)
for x_index in width_segments + 1:
var across := float(x_index) / float(width_segments)
var world_x := center_x + lerpf(-half_width, half_width, across)
var world_x := lerpf(left_x, right_x, across)
var world_sample := Vector3(world_x, 0.0, world_z)
var terrain_height := terrain.data.get_height(world_sample)
if is_nan(terrain_height):
terrain_height = global_position.y
vertices.append(
to_local(Vector3(world_sample.x, terrain_height + water_offset, world_sample.z))
)
vertices.append(to_local(Vector3(world_sample.x, surface_height, world_sample.z)))
normals.append(Vector3.UP)
uvs.append(Vector2(across, progress))
@@ -96,8 +113,37 @@ func _build_ribbon(
return mesh
func _shoreline_x(
terrain: Terrain3D, center_x: float, edge_x: float, world_z: float, surface_height: float
) -> float:
var edge_height := terrain.data.get_height(Vector3(edge_x, 0.0, world_z))
if is_nan(edge_height) or edge_height <= surface_height:
return edge_x
# The carved basin is wider on one side. Stop its visible surface at the
# shore instead of extending the pool's ideal ellipse metres under a hill.
var wet_x := center_x
var dry_x := edge_x
for step in range(1, 17):
var sample_x := lerpf(center_x, edge_x, float(step) / 16.0)
var sample_height := terrain.data.get_height(Vector3(sample_x, 0.0, world_z))
if is_nan(sample_height) or sample_height > surface_height:
dry_x = sample_x
break
wet_x = sample_x
for _step in 10:
var sample_x := (wet_x + dry_x) * 0.5
var sample_height := terrain.data.get_height(Vector3(sample_x, 0.0, world_z))
if is_nan(sample_height) or sample_height > surface_height:
dry_x = sample_x
else:
wet_x = sample_x
return wet_x
func _align_pool_to_terrain(terrain: Terrain3D) -> void:
var pool := $PlungePool as MeshInstance3D
var pool_material := pool.mesh.surface_get_material(0) as ShaderMaterial
pool_material.set_shader_parameter("pool_outlet_z", Watercourse.DOWNSTREAM_START_Z)
var pool_sample := Vector3(
Watercourse.PLUNGE_POOL_CENTER.x, 0.0, Watercourse.PLUNGE_POOL_CENTER.y
)
+22 -23
View File
@@ -1,34 +1,32 @@
[gd_scene load_steps=8 format=3]
[gd_scene load_steps=11 format=3]
[ext_resource type="Shader" path="res://world/jajce/materials/water.gdshader" id="1_water_shader"]
[ext_resource type="Script" path="res://world/jajce/TerrainRiverRibbon.gd" id="2_river_script"]
[ext_resource type="Shader" path="res://world/jajce/materials/water_foam.gdshader" id="3_foam_shader"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_water"]
shader = ExtResource("1_water_shader")
[sub_resource type="CylinderMesh" id="Mesh_plunge_pool"]
material = SubResource("ShaderMaterial_water")
top_radius = 7.6
bottom_radius = 7.6
height = 0.08
radial_segments = 32
rings = 2
[sub_resource type="ShaderMaterial" id="ShaderMaterial_pool"]
shader = ExtResource("1_water_shader")
shader_parameter/pool_surface = true
[sub_resource type="StandardMaterial3D" id="Material_foam"]
transparency = 1
shading_mode = 0
albedo_color = Color(0.8, 0.93, 0.86, 0.22)
emission_enabled = true
emission = Color(0.34, 0.56, 0.48, 1)
emission_energy_multiplier = 0.06
roughness = 0.74
[sub_resource type="PlaneMesh" id="Mesh_plunge_pool"]
material = SubResource("ShaderMaterial_pool")
size = Vector2(15.2, 15.2)
subdivide_width = 20
subdivide_depth = 20
[sub_resource type="TorusMesh" id="Mesh_foam_ring"]
material = SubResource("Material_foam")
inner_radius = 5.15
outer_radius = 5.5
rings = 28
ring_segments = 10
[sub_resource type="ShaderMaterial" id="Material_foam"]
shader = ExtResource("3_foam_shader")
[sub_resource type="ShaderMaterial" id="Material_pool_foam"]
shader = ExtResource("3_foam_shader")
shader_parameter/circular_wash = true
[sub_resource type="PlaneMesh" id="Mesh_foam_ring"]
material = SubResource("Material_pool_foam")
size = Vector2(11, 11)
[sub_resource type="PlaneMesh" id="Mesh_foam_patch"]
material = SubResource("Material_foam")
@@ -49,10 +47,11 @@ cast_shadow = 0
[node name="PlungePool" type="MeshInstance3D" parent="."]
scale = Vector3(1.25, 1, 0.82)
cast_shadow = 0
mesh = SubResource("Mesh_plunge_pool")
[node name="FoamRing" type="MeshInstance3D" parent="."]
scale = Vector3(1.25, 0.12, 0.82)
scale = Vector3(1.25, 1, 0.82)
cast_shadow = 0
mesh = SubResource("Mesh_foam_ring")
+4 -9
View File
@@ -1,6 +1,7 @@
[gd_scene load_steps=12 format=3]
[gd_scene load_steps=13 format=3]
[ext_resource type="Shader" path="res://world/jajce/materials/waterfall.gdshader" id="1_waterfall_shader"]
[ext_resource type="Shader" path="res://world/jajce/materials/water_foam.gdshader" id="2_foam_shader"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_waterfall"]
shader = ExtResource("1_waterfall_shader")
@@ -34,14 +35,8 @@ size = Vector3(4.8, 5.5, 3.4)
material = SubResource("Material_rock_moss")
size = Vector3(10.4, 0.65, 3.0)
[sub_resource type="StandardMaterial3D" id="Material_lip_foam"]
transparency = 1
shading_mode = 0
albedo_color = Color(0.82, 0.95, 0.87, 0.72)
emission_enabled = true
emission = Color(0.46, 0.72, 0.62, 1)
emission_energy_multiplier = 0.22
roughness = 0.62
[sub_resource type="ShaderMaterial" id="Material_lip_foam"]
shader = ExtResource("2_foam_shader")
[sub_resource type="PlaneMesh" id="Mesh_lip_foam"]
material = SubResource("Material_lip_foam")
+20 -15
View File
@@ -7,17 +7,17 @@ extends Node
const TO_RAD := PI / 180.0
const DAY_SKY_TOP := Color(0.27, 0.57, 0.74, 1)
const DAY_SKY_HORIZON := Color(0.79, 0.88, 0.82, 1)
const DAY_SKY_TOP := Color(0.12, 0.49, 0.69, 1)
const DAY_SKY_HORIZON := Color(0.65, 0.84, 0.84, 1)
const DAY_GROUND_BOTTOM := Color(0.12, 0.17, 0.12, 1)
const DAY_GROUND_HORIZON := Color(0.56, 0.69, 0.57, 1)
const DAY_AMBIENT := Color(0.72, 0.83, 0.88, 1)
const DAY_AMBIENT_ENERGY := 0.55
const DAY_FOG_LIGHT := Color(0.70, 0.83, 0.81, 1)
const DAY_GROUND_HORIZON := DAY_SKY_HORIZON
const DAY_AMBIENT := Color(0.65, 0.80, 0.87, 1)
const DAY_AMBIENT_ENERGY := 0.65
const DAY_FOG_LIGHT := Color(0.63, 0.80, 0.82, 1)
const DAY_FOG_LIGHT_ENERGY := 0.8
const DAY_FOG_DENSITY := 0.0015
const DAY_FOG_DENSITY := 0.0018
const DAY_LIGHT_COLOR := Color(1, 0.94, 0.79, 1)
const DAY_LIGHT_ENERGY := 1.2
const DAY_LIGHT_ENERGY := 1.35
const NIGHT_SKY_TOP := Color(0.04, 0.04, 0.12, 1)
const NIGHT_SKY_HORIZON := Color(0.08, 0.06, 0.14, 1)
@@ -76,8 +76,13 @@ func _process(delta: float) -> void:
func _apply_light_rotation(cycle: float) -> void:
if directional_light == null:
return
var angle_rad: float = cycle * 2.0 * PI
directional_light.rotation = Vector3(-PI / 4.0 + sin(angle_rad) * PI / 4.0, angle_rad, 0)
# The bright interval and the sun's arc share dawn/noon/dusk. The old
# quarter-cycle offset put the brightest morning sun on the horizon.
var solar_progress := clampf((cycle - 0.15) / 0.7, 0.0, 1.0)
var elevation := 8.0 + sin(solar_progress * PI) * 52.0
if cycle < 0.15 or cycle > 0.85:
elevation = 32.0 # One cool moon fill during the night interval.
directional_light.rotation = Vector3(-elevation * TO_RAD, cycle * TAU, 0)
func _apply_environment(cycle: float) -> void:
@@ -85,7 +90,6 @@ func _apply_environment(cycle: float) -> void:
return
var day_factor := _day_factor(cycle)
var night_factor := 1.0 - day_factor
directional_light.light_color = _lerp_color(NIGHT_LIGHT_COLOR, DAY_LIGHT_COLOR, day_factor)
directional_light.light_energy = lerpf(NIGHT_LIGHT_ENERGY, DAY_LIGHT_ENERGY, day_factor)
@@ -136,6 +140,7 @@ func _apply_environment(cycle: float) -> void:
"ground_horizon_color", NIGHT_GROUND_HORIZON.lerp(DAY_GROUND_HORIZON, day_factor)
)
mat.set_shader_parameter("daylight", day_factor)
mat.set_shader_parameter("golden_hour", transition_peak)
func _find_simulation_node() -> void:
@@ -155,10 +160,10 @@ func _day_factor(cycle: float) -> float:
func _sunrise_sunset_weight(cycle: float) -> float:
if cycle < 0.15:
return smoothstep(0.05, 0.15, cycle) * (1.0 - smoothstep(0.1, 0.15, cycle))
if cycle > 0.85:
return smoothstep(0.85, 0.95, cycle) * (1.0 - smoothstep(0.9, 0.95, cycle))
if cycle < 0.3:
return smoothstep(0.15, 0.21, cycle) * (1.0 - smoothstep(0.23, 0.3, cycle))
if cycle > 0.7:
return smoothstep(0.7, 0.77, cycle) * (1.0 - smoothstep(0.79, 0.85, cycle))
return 0.0
+36 -10
View File
@@ -32,6 +32,7 @@ static var _viewport_scale_states: Dictionary = {}
var _active_presentation_quality := -1
var _authored_presentation_captured := false
var _authored_render_scale := 1.0
var _authored_msaa: int = Viewport.MSAA_DISABLED
var _authored_glow_enabled := false
var _authored_volumetric_fog_enabled := HIGH_VOLUMETRIC_FOG_ENABLED
var _authored_adjustment_enabled := false
@@ -45,6 +46,7 @@ var _authored_grass_interactors := 1
var _authored_grass_update_interval := 0.1
var _authored_grass_controller_enabled := true
var _last_applied_render_scale := -1.0
var _last_applied_msaa := -1
var _viewport_instance_id := 0
@@ -78,7 +80,8 @@ func apply_presentation_quality(quality: int) -> bool:
_authored_volumetric_fog_enabled,
_authored_adjustment_enabled,
_authored_shadow_distance,
_authored_shadow_mode
_authored_shadow_mode,
_authored_msaa
)
_apply_grass_quality(
_authored_grass_visible,
@@ -96,7 +99,8 @@ func apply_presentation_quality(quality: int) -> bool:
false,
_authored_adjustment_enabled,
minf(_authored_shadow_distance, BALANCED_SHADOW_DISTANCE),
DirectionalLight3D.SHADOW_PARALLEL_2_SPLITS
DirectionalLight3D.SHADOW_PARALLEL_2_SPLITS,
mini(_authored_msaa, Viewport.MSAA_2X)
)
_apply_grass_quality(
_authored_grass_visible,
@@ -114,7 +118,8 @@ func apply_presentation_quality(quality: int) -> bool:
false,
false,
minf(_authored_shadow_distance, LOW_SHADOW_DISTANCE),
DirectionalLight3D.SHADOW_ORTHOGONAL
DirectionalLight3D.SHADOW_ORTHOGONAL,
Viewport.MSAA_DISABLED
)
_apply_grass_quality(
false,
@@ -126,6 +131,9 @@ func apply_presentation_quality(quality: int) -> bool:
false
)
var meadow := get_node_or_null("PainterlyMeadow")
if meadow != null:
meadow.call("apply_presentation_quality", quality)
_active_presentation_quality = quality
return true
@@ -223,11 +231,14 @@ func _apply_environment_quality(
volumetric_fog_enabled: bool,
adjustment_enabled: bool,
shadow_distance: float,
shadow_mode: int
shadow_mode: int,
msaa: int
) -> void:
_claim_viewport_scale_owner()
get_viewport().scaling_3d_scale = render_scale
get_viewport().msaa_3d = msaa
_last_applied_render_scale = render_scale
_last_applied_msaa = msaa
var environment := _environment()
if environment != null:
environment.glow_enabled = glow_enabled
@@ -285,8 +296,13 @@ func _register_viewport_scale_owner() -> void:
_viewport_instance_id = viewport.get_instance_id()
var state: Dictionary = _viewport_scale_states.get(_viewport_instance_id, {})
if state.is_empty():
state = {"base_scale": viewport.scaling_3d_scale, "owners": []}
state = {
"base_scale": viewport.scaling_3d_scale,
"base_msaa": viewport.msaa_3d,
"owners": [],
}
_authored_render_scale = float(state["base_scale"])
_authored_msaa = int(state["base_msaa"])
_viewport_scale_states[_viewport_instance_id] = state
_claim_viewport_scale_owner()
@@ -324,13 +340,20 @@ func _release_viewport_scale_owner() -> void:
if not owners.is_empty():
state["owners"] = owners
_viewport_scale_states[_viewport_instance_id] = state
if (
was_active_owner
and is_equal_approx(viewport.scaling_3d_scale, _last_applied_render_scale)
):
if was_active_owner:
var previous_owner := (owners[-1] as WeakRef).get_ref() as JajceWorld
if previous_owner != null and previous_owner._last_applied_render_scale >= 0.0:
if (
previous_owner != null
and previous_owner._last_applied_render_scale >= 0.0
and is_equal_approx(viewport.scaling_3d_scale, _last_applied_render_scale)
):
viewport.scaling_3d_scale = previous_owner._last_applied_render_scale
if (
previous_owner != null
and previous_owner._last_applied_msaa >= 0
and viewport.msaa_3d == _last_applied_msaa
):
viewport.msaa_3d = previous_owner._last_applied_msaa
else:
_viewport_scale_states.erase(_viewport_instance_id)
if (
@@ -338,8 +361,11 @@ func _release_viewport_scale_owner() -> void:
and is_equal_approx(viewport.scaling_3d_scale, _last_applied_render_scale)
):
viewport.scaling_3d_scale = float(state["base_scale"])
if was_active_owner and viewport.msaa_3d == _last_applied_msaa:
viewport.msaa_3d = int(state["base_msaa"])
_viewport_instance_id = 0
_last_applied_render_scale = -1.0
_last_applied_msaa = -1
func _initialize_world_presentation() -> void:
@@ -0,0 +1,36 @@
// Original opaque petal geometry: soft wrap lighting without alpha overdraw.
shader_type spatial;
render_mode skip_vertex_transform, cull_disabled, specular_disabled;
uniform vec2 wind_direction = vec2(1.0, 0.7);
uniform float wind_amount : hint_range(0.0, 1.0) = 0.115;
uniform float fade_begin = 42.0;
uniform float fade_end = 56.0;
varying float palette_variation;
void vertex() {
vec3 anchor = MODEL_MATRIX[3].xyz;
float distance_to_camera = distance(anchor, CAMERA_POSITION_WORLD);
float presence = 1.0 - smoothstep(fade_begin, fade_end, distance_to_camera);
float height = max(VERTEX.y, 0.0);
float phase = dot(anchor.xz, vec2(0.38, 0.29));
float gust = sin(TIME * 1.15 + phase) * 0.65 + sin(TIME * 0.58 + phase * 0.42) * 0.35;
VERTEX *= presence;
vec3 world_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
world_vertex.xz += normalize(wind_direction) * wind_amount * gust * height * height * presence;
VERTEX = (VIEW_MATRIX * vec4(world_vertex, 1.0)).xyz;
vec3 local_normal = normalize(NORMAL + vec3(0.0, 1.6, 0.0));
NORMAL = normalize((MODELVIEW_MATRIX * vec4(local_normal, 0.0)).xyz);
palette_variation = mix(0.92, 1.06, INSTANCE_CUSTOM.r);
}
void fragment() {
ALBEDO = COLOR.rgb * palette_variation;
ROUGHNESS = 1.0;
}
void light() {
float sunlight = smoothstep(-0.35, 0.55, dot(NORMAL, LIGHT));
vec3 shade = mix(vec3(0.43, 0.58, 0.55), vec3(1.0, 0.98, 0.90), sunlight);
DIFFUSE_LIGHT += shade * LIGHT_COLOR * ATTENUATION / PI;
}
@@ -0,0 +1 @@
uid://b1r81tjhi1o0y
+18 -19
View File
@@ -1,27 +1,26 @@
shader_type sky;
uniform vec3 sky_top_color : source_color = vec3(0.27, 0.57, 0.74);
uniform vec3 sky_horizon_color : source_color = vec3(0.79, 0.88, 0.82);
// Original painted panorama; no volume or per-frame sky pass.
uniform sampler2D painted_panorama : source_color, filter_linear_mipmap, repeat_enable;
uniform vec3 sky_top_color : source_color = vec3(0.12, 0.49, 0.69);
uniform vec3 sky_horizon_color : source_color = vec3(0.65, 0.84, 0.84);
uniform vec3 ground_bottom_color : source_color = vec3(0.12, 0.17, 0.12);
uniform vec3 ground_horizon_color : source_color = vec3(0.56, 0.69, 0.57);
uniform vec3 ground_horizon_color : source_color = vec3(0.65, 0.84, 0.84);
uniform float daylight : hint_range(0.0, 1.0) = 1.0;
float hash21(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash21(i), hash21(i + vec2(1.0, 0.0)), f.x), mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0)), f.x), f.y);
}
uniform float golden_hour : hint_range(0.0, 1.0) = 0.0;
void sky() {
float height = max(EYEDIR.y, 0.0);
vec3 sky_color = mix(sky_horizon_color, sky_top_color, pow(height, 0.48));
vec2 p = EYEDIR.xz / max(0.17, height) * 1.15;
float cloud = noise(p) * 0.65 + noise(p * 2.1 + 7.3) * 0.25 + noise(p * 4.3) * 0.10;
float mask = smoothstep(0.55, 0.64, cloud) * smoothstep(0.02, 0.20, height);
vec3 cloud_shadow = mix(vec3(0.08, 0.10, 0.19), vec3(0.70, 0.80, 0.80), daylight);
vec3 cloud_light = mix(vec3(0.15, 0.18, 0.29), vec3(1.0, 0.98, 0.87), daylight);
vec3 cloud_color = mix(cloud_shadow, cloud_light, smoothstep(0.61, 0.77, cloud));
sky_color = mix(sky_color, cloud_color, mask);
vec3 sky_gradient = mix(sky_horizon_color, sky_top_color, pow(height, 0.45));
// Art horizon is at 69% of the sheet. Remap only the upper hemisphere;
// the ground bounce uses the day/night palette and never paints terrain.
vec2 panorama_uv = vec2(fract(SKY_COORDS.x * 2.0 + 0.40), clamp((SKY_COORDS.y - 0.26) * 2.875, 0.0, 0.69));
vec3 painted = texture(painted_panorama, panorama_uv).rgb;
vec3 seam_color = (texture(painted_panorama, vec2(0.001, panorama_uv.y)).rgb + texture(painted_panorama, vec2(0.999, panorama_uv.y)).rgb) * 0.5;
painted = mix(seam_color, painted, smoothstep(0.0, 0.025, min(panorama_uv.x, 1.0 - panorama_uv.x)));
painted = mix(painted, painted * vec3(1.18, 0.79, 0.66), golden_hour * 0.50);
vec3 night_paint = painted * vec3(0.045, 0.07, 0.14) + sky_gradient * 0.12;
vec3 sky_color = mix(night_paint, painted, daylight);
// Dissolve the panorama edge into the haze at the actual horizon.
sky_color = mix(sky_gradient, sky_color, smoothstep(0.0, 0.075, height));
COLOR = EYEDIR.y >= 0.0 ? sky_color : mix(ground_horizon_color, ground_bottom_color, pow(-EYEDIR.y, 0.4));
}
+66 -27
View File
@@ -1,42 +1,81 @@
shader_type spatial;
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley, specular_schlick_ggx;
uniform float wave_strength : hint_range(0.0, 0.3) = 0.055;
uniform float wave_strength : hint_range(0.0, 0.3) = 0.025;
uniform float wave_speed : hint_range(0.0, 3.0) = 0.72;
uniform float flow_speed : hint_range(0.0, 2.0) = 0.46;
uniform vec3 deep_color : source_color = vec3(0.03, 0.39, 0.45);
uniform vec3 shallow_color : source_color = vec3(0.07, 0.67, 0.64);
uniform vec3 sunlit_color : source_color = vec3(0.55, 0.95, 0.78);
uniform vec3 foam_color : source_color = vec3(0.82, 0.94, 0.84);
uniform float flow_speed : hint_range(0.0, 2.0) = 0.28;
uniform bool pool_surface = false;
uniform float pool_outlet_z = -20.0;
uniform vec3 deep_color : source_color = vec3(0.035, 0.34, 0.54);
uniform vec3 shallow_color : source_color = vec3(0.12, 0.65, 0.69);
uniform vec3 sunlit_color : source_color = vec3(0.57, 0.83, 0.88);
uniform vec3 foam_color : source_color = vec3(0.9, 0.96, 0.91);
varying vec3 water_position;
float painted_noise(vec2 p) {
vec2 cell = floor(p);
vec2 blend = fract(p);
blend = blend * blend * (3.0 - 2.0 * blend);
vec4 corners = fract(sin(vec4(
dot(cell, vec2(127.1, 311.7)),
dot(cell + vec2(1.0, 0.0), vec2(127.1, 311.7)),
dot(cell + vec2(0.0, 1.0), vec2(127.1, 311.7)),
dot(cell + vec2(1.0), vec2(127.1, 311.7))
)) * 43758.5453);
return mix(mix(corners.x, corners.y, blend.x), mix(corners.z, corners.w, blend.x), blend.y);
}
void vertex() {
float broad_wave = sin(VERTEX.x * 0.42 + VERTEX.z * 0.18 + TIME * wave_speed);
float crossing_wave = cos(VERTEX.x * 0.7 - VERTEX.z * 0.31 + TIME * wave_speed * 0.73);
// World coordinates keep the pool and both ribbons at the same ripple scale.
water_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
float broad_wave = sin(water_position.x * 0.42 + water_position.z * 0.18 + TIME * wave_speed);
float crossing_wave = cos(water_position.x * 0.7 - water_position.z * 0.31 + TIME * wave_speed * 0.73);
VERTEX.y += (broad_wave + crossing_wave * 0.45) * wave_strength;
}
void fragment() {
vec2 flowing_uv = UV * vec2(7.0, 10.0);
flowing_uv.y -= TIME * flow_speed;
float ribbon_a = sin(flowing_uv.y + sin(flowing_uv.x * 0.72) * 1.25) * 0.5 + 0.5;
float ribbon_b = sin(flowing_uv.y * 1.7 - flowing_uv.x * 0.55 + TIME * 0.23) * 0.5 + 0.5;
float quiet_ripple = smoothstep(0.72, 0.98, ribbon_a * 0.62 + ribbon_b * 0.38);
float fresnel = pow(1.0 - clamp(dot(normalize(NORMAL), normalize(VIEW)), 0.0, 1.0), 2.4);
float bank_distance = min(UV.x, 1.0 - UV.x);
float bank_foam = 1.0 - smoothstep(0.0, 0.07, bank_distance);
bank_foam *= 0.72 + sin(UV.y * 52.0 + TIME * 0.35) * 0.12;
vec3 water_color = mix(deep_color, shallow_color, ribbon_a * 0.2 + 0.48);
water_color = mix(water_color, sunlit_color, fresnel * 0.42);
water_color = mix(water_color, foam_color, quiet_ripple * 0.05);
water_color = mix(water_color, foam_color, bank_foam * 0.3);
// The ribbon owns the outlet so two transparent surfaces never overlap.
if (pool_surface && water_position.z >= pool_outlet_z) {
discard;
}
vec2 flow = water_position.xz - vec2(0.0, TIME * flow_speed);
float broad_patch = painted_noise(flow * vec2(0.18, 0.3));
float small_patch = painted_noise(flow * vec2(0.64, 0.85));
float ripple_phase = flow.y * 3.2 + sin(flow.x * 0.83) * 1.2 + small_patch * 0.8;
float ribbon = sin(ripple_phase);
float stroke_width = max(fwidth(ribbon) * 0.7, 0.012);
float strokes = smoothstep(0.975 - stroke_width, 0.975 + stroke_width, ribbon);
strokes *= smoothstep(0.58, 0.8, painted_noise(flow * vec2(1.1, 1.8))) * 0.16;
strokes *= 1.0 - smoothstep(0.28, 0.8, fwidth(ripple_phase));
vec3 ripple_normal = normalize(vec3(
cos(flow.x * 0.83 + flow.y * 0.37 + broad_patch * 6.0) * 0.006,
1.0,
cos(ripple_phase + small_patch * 5.0) * 0.008
));
NORMAL = normalize((VIEW_MATRIX * vec4(ripple_normal, 0.0)).xyz);
float fresnel = pow(1.0 - clamp(dot(NORMAL, normalize(VIEW)), 0.0, 1.0), 2.6);
// Painted sky patches need no screen/depth texture and work in Compatibility.
vec3 world_view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz);
vec3 reflection = reflect(-world_view, ripple_normal);
vec2 sky_uv = reflection.xz / max(reflection.y + 0.35, 0.25);
float clouds = painted_noise(sky_uv * 2.0 + vec2(0.7, 2.4));
clouds = smoothstep(0.49, 0.77, clouds + painted_noise(sky_uv * 4.5) * 0.18);
float bank_distance = min(UV.x, 1.0 - UV.x) * 2.0;
if (pool_surface) {
bank_distance = 1.0 - length(UV * 2.0 - 1.0);
}
float bank_foam = (1.0 - smoothstep(0.01, 0.045, bank_distance));
bank_foam *= smoothstep(0.4, 0.75, small_patch) * 0.48;
vec3 water_color = mix(deep_color, shallow_color, 0.19 + broad_patch * 0.2);
water_color = mix(water_color, sunlit_color, clouds * (0.13 + fresnel * 0.35));
water_color = mix(water_color, foam_color, strokes + bank_foam);
ALBEDO = water_color;
ROUGHNESS = mix(0.34, 0.5, ribbon_b);
SPECULAR = 0.38;
EMISSION = water_color * 0.12 + sunlit_color * (quiet_ripple * 0.05 + fresnel * 0.03);
EMISSION += foam_color * bank_foam * 0.025;
ALPHA = mix(0.94, 0.99, fresnel);
ROUGHNESS = 0.38 + broad_patch * 0.08;
SPECULAR = 0.14;
ALPHA = smoothstep(0.0, 0.018, bank_distance) * 0.97;
}
+25
View File
@@ -0,0 +1,25 @@
shader_type spatial;
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley;
uniform bool circular_wash = false;
uniform vec3 foam_color : source_color = vec3(0.83, 0.94, 0.93);
void fragment() {
vec2 p = UV * 2.0 - 1.0;
float wash = sin(p.x * 18.0 + sin(p.y * 12.0 + TIME * 0.8) * 1.5);
wash *= sin(p.y * 23.0 - TIME * 1.4 + p.x * 3.0);
float mask = (1.0 - smoothstep(0.45, 1.0, abs(p.x)));
mask *= 1.0 - smoothstep(0.2, 1.0, abs(p.y));
if (circular_wash) {
float radius = length(p);
float angle = atan(p.y, p.x);
float broken_arcs = sin(radius * 32.0 - TIME * 0.75 + sin(angle * 5.0) * 0.6);
mask = smoothstep(0.65, 0.94, broken_arcs);
mask *= smoothstep(0.1, 0.35, radius) * (1.0 - smoothstep(0.65, 0.98, radius));
mask *= smoothstep(-0.3, 0.8, sin(angle * 3.0 + radius * 9.0));
}
ALBEDO = foam_color;
ROUGHNESS = 0.65;
ALPHA = mask * smoothstep(-0.45, 0.6, wash) * 0.48;
}
@@ -0,0 +1 @@
uid://bcw0dj01f0m3h
+7 -8
View File
@@ -2,9 +2,9 @@ shader_type spatial;
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley, specular_schlick_ggx;
uniform float scroll_speed : hint_range(0.0, 4.0) = 1.35;
uniform vec3 shadow_color : source_color = vec3(0.075, 0.33, 0.34);
uniform vec3 water_color : source_color = vec3(0.31, 0.68, 0.64);
uniform vec3 light_color : source_color = vec3(0.78, 0.94, 0.83);
uniform vec3 shadow_color : source_color = vec3(0.11, 0.39, 0.54);
uniform vec3 water_color : source_color = vec3(0.39, 0.73, 0.82);
uniform vec3 light_color : source_color = vec3(0.85, 0.96, 0.94);
uniform vec3 foam_color : source_color = vec3(0.91, 0.97, 0.9);
@@ -22,17 +22,16 @@ void fragment() {
float soft_thread = sin(flow_uv.x * 13.0 - flow_uv.y * 7.0) * 0.5 + 0.5;
float falling_detail = sin(flow_uv.y * 27.0 + flow_uv.x * 7.0) * 0.5 + 0.5;
float thread = smoothstep(0.36, 0.9, long_thread * 0.6 + soft_thread * 0.4);
float broken_foam = smoothstep(0.72, 0.98, falling_detail * thread);
float broken_foam = smoothstep(0.61, 0.94, falling_detail * thread);
float base_foam = smoothstep(0.58, 1.0, UV.y);
float edge_fade = smoothstep(0.0, 0.08, UV.x) * smoothstep(1.0, 0.92, UV.x);
float edge_fade = smoothstep(0.0, 0.08, UV.x) * (1.0 - smoothstep(0.92, 1.0, UV.x));
vec3 color = mix(shadow_color, water_color, 0.5 + thread * 0.35);
color = mix(color, light_color, broken_foam * 0.5);
color = mix(color, light_color, thread * 0.35 + broken_foam * 0.35);
color = mix(color, foam_color, base_foam * (0.38 + broken_foam * 0.42));
ALBEDO = color;
ROUGHNESS = mix(0.16, 0.32, broken_foam);
SPECULAR = 0.65;
EMISSION = light_color * (broken_foam * 0.07 + base_foam * 0.04);
ALPHA = edge_fade * mix(0.62, 0.9, thread * 0.56 + base_foam * 0.3);
ALPHA = edge_fade * mix(0.76, 0.95, thread * 0.56 + base_foam * 0.3);
}
+12 -5
View File
@@ -10,8 +10,11 @@ uniform float gust_speed : hint_range(0.0, 1.0) = 0.12;
uniform float gust_strength : hint_range(0.0, 0.8) = 0.35;
uniform float flutter_strength : hint_range(0.0, 0.05) = 0.012;
uniform float wind_phase : hint_range(0.0, 6.2832) = 0.0;
uniform float wind_base_height = -1.2;
uniform float wind_full_height = 1.2;
uniform vec3 albedo : source_color = vec3(0.19, 0.38, 0.17);
uniform float roughness : hint_range(0.0, 1.0) = 0.9;
uniform float painted_vertex_weight : hint_range(0.0, 1.0) = 0.0;
uniform vec3 contact_root = vec3(0.0);
uniform float contact_strength = 0.0;
uniform int interactor_count = 0;
@@ -20,8 +23,7 @@ varying vec3 paint_position;
void vertex() {
paint_position = VERTEX;
float half_height = 1.2;
float height_factor = clamp((VERTEX.y + half_height) / (half_height * 2.0), 0.0, 1.0);
float height_factor = clamp((VERTEX.y - wind_base_height) / max(wind_full_height - wind_base_height, 0.01), 0.0, 1.0);
height_factor = height_factor * height_factor;
vec2 direction = normalize(wind_direction);
@@ -32,8 +34,10 @@ void vertex() {
float bend = sin(TIME * wind_speed + world_phase) * wind_strength * gust;
float flutter = sin(TIME * 1.7 + world_phase * 1.9 + VERTEX.y * 2.0) * flutter_strength;
VERTEX.xz += direction * bend * height_factor;
VERTEX.xz += crosswind * flutter * height_factor;
// Crown orientation varies, but all foliage follows the same world breeze.
vec3 world_bend = vec3(direction.x, 0.0, direction.y) * bend;
world_bend += vec3(crosswind.x, 0.0, crosswind.y) * flutter;
VERTEX += (inverse(MODEL_MATRIX) * vec4(world_bend, 0.0)).xyz * height_factor;
if (foliage_trails_enabled && contact_strength > 0.0) {
// One bend for the whole bush, including fruit; its base remains planted.
vec3 contact = sample_foliage_trail(contact_root);
@@ -69,7 +73,10 @@ void vertex() {
void fragment() {
float patches = sin(paint_position.x * 6.0 + sin(paint_position.z * 7.0)) * sin(paint_position.y * 5.0);
float top = smoothstep(-0.65, 0.8, paint_position.y);
ALBEDO = albedo * mix(0.85, 1.17, top) * (1.0 + smoothstep(0.2, 0.4, patches) * 0.10);
vec3 pigment = mix(vec3(1.0), COLOR.rgb, painted_vertex_weight);
vec3 shade_tint = mix(vec3(0.77, 0.95, 1.01), vec3(1.10, 1.06, 0.87), top);
ALBEDO = albedo * pigment * shade_tint * mix(0.91, 1.13, top);
ALBEDO *= 1.0 + smoothstep(0.18, 0.65, patches) * 0.07;
ROUGHNESS = roughness;
SPECULAR = 0.0;
}