feat: carve Jajce river into terrain

This commit is contained in:
Rijad Zuzo
2026-07-17 01:05:09 +02:00
parent 5356c34fd7
commit 145b90eb97
25 changed files with 603 additions and 74 deletions
File diff suppressed because one or more lines are too long
+49
View File
@@ -0,0 +1,49 @@
class_name JajceWatercourse
extends RefCounted
const WATERFALL_CENTER := Vector2(25.0, -24.0)
const SHELF_TOP_HEIGHT := 12.4
const UPPER_STREAM_START_Z := -58.0
const UPPER_STREAM_END_Z := -25.0
const PLUNGE_POOL_CENTER := Vector2(25.0, -19.5)
const PLUNGE_POOL_BED_HEIGHT := -0.65
const DOWNSTREAM_START_Z := -20.0
const DOWNSTREAM_END_Z := 42.0
const WATER_SURFACE_OFFSET := 0.32
static func downstream_progress(world_z: float) -> float:
return clampf(inverse_lerp(DOWNSTREAM_START_Z, DOWNSTREAM_END_Z, world_z), 0.0, 1.0)
static func downstream_center_x(world_z: float) -> float:
var progress := downstream_progress(world_z)
var downstream_curve := smoothstep(0.58, 1.0, progress) * 12.0
return WATERFALL_CENTER.x + sin(progress * TAU) * 1.4 + downstream_curve
static func downstream_half_width(world_z: float) -> float:
var progress := downstream_progress(world_z)
var base_width := lerpf(4.8, 3.8, smoothstep(0.0, 0.62, progress))
return base_width + smoothstep(0.78, 1.0, progress) * 0.8
static func downstream_bed_height(world_z: float) -> float:
return lerpf(-0.55, -1.3, downstream_progress(world_z))
static func upper_stream_progress(world_z: float) -> float:
return clampf(inverse_lerp(UPPER_STREAM_START_Z, UPPER_STREAM_END_Z, world_z), 0.0, 1.0)
static func upper_stream_center_x(world_z: float) -> float:
var progress := upper_stream_progress(world_z)
return WATERFALL_CENTER.x + sin(progress * PI * 1.6) * 0.9
static func upper_stream_half_width(world_z: float) -> float:
return lerpf(2.8, 4.4, smoothstep(0.55, 1.0, upper_stream_progress(world_z)))
static func upper_stream_bed_height(world_z: float) -> float:
return lerpf(11.55, 11.15, upper_stream_progress(world_z))
+1
View File
@@ -0,0 +1 @@
uid://dl3523bi00bto
+50 -20
View File
@@ -1,15 +1,16 @@
extends Node3D
const Watercourse := preload("res://world/jajce/JajceWatercourse.gd")
@export var terrain_path := NodePath("../../TerrainRoot/Terrain3D")
@export var river_material: Material
@export_range(6.0, 20.0, 0.5) var river_width := 8.0
@export_range(2, 12, 1) var width_segments := 6
@export_range(8, 96, 1) var length_segments := 36
@export var local_start_z := -25.0
@export var local_end_z := 40.0
@export_range(0.05, 0.5, 0.01) var water_offset := 0.18
@export_range(8, 64, 1) var upper_length_segments := 18
@export_range(0.05, 0.5, 0.01) var water_offset := Watercourse.WATER_SURFACE_OFFSET
@onready var river_ribbon: MeshInstance3D = $RiverRibbon
@onready var upper_river_ribbon: MeshInstance3D = $UpperRiverRibbon
func _ready() -> void:
@@ -22,22 +23,48 @@ func _build_terrain_ribbon() -> void:
push_warning("TerrainRiverRibbon: terrain or material is unavailable")
return
river_ribbon.mesh = _build_ribbon(
terrain,
Watercourse.DOWNSTREAM_START_Z,
Watercourse.DOWNSTREAM_END_Z,
length_segments,
false
)
upper_river_ribbon.mesh = _build_ribbon(
terrain,
Watercourse.UPPER_STREAM_START_Z,
Watercourse.UPPER_STREAM_END_Z,
upper_length_segments,
true
)
_align_pool_to_terrain(terrain)
func _build_ribbon(
terrain: Terrain3D, start_z: float, end_z: float, segment_count: int, is_upper: bool
) -> ArrayMesh:
var vertices := PackedVector3Array()
var normals := PackedVector3Array()
var uvs := PackedVector2Array()
var indices := PackedInt32Array()
var row_size := width_segments + 1
for z_index in length_segments + 1:
var progress := float(z_index) / float(length_segments)
var local_z := lerpf(local_start_z, local_end_z, progress)
var downstream_curve := smoothstep(0.58, 1.0, progress) * 12.0
var center_offset := sin(progress * TAU) * 1.4 + downstream_curve
var downstream_taper := lerpf(1.0, 0.45, smoothstep(0.65, 1.0, progress))
var half_width := river_width * lerpf(0.52, 0.45, progress) * downstream_taper
for z_index in segment_count + 1:
var progress := float(z_index) / float(segment_count)
var world_z := lerpf(start_z, end_z, progress)
var center_x := (
Watercourse.upper_stream_center_x(world_z)
if is_upper
else Watercourse.downstream_center_x(world_z)
)
var half_width := (
Watercourse.upper_stream_half_width(world_z)
if is_upper
else Watercourse.downstream_half_width(world_z)
)
for x_index in width_segments + 1:
var across := float(x_index) / float(width_segments)
var local_x := center_offset + lerpf(-half_width, half_width, across)
var world_sample := to_global(Vector3(local_x, 0.0, local_z))
var world_x := center_x + lerpf(-half_width, half_width, 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
@@ -47,7 +74,7 @@ func _build_terrain_ribbon() -> void:
normals.append(Vector3.UP)
uvs.append(Vector2(across, progress))
for z_index in length_segments:
for z_index in segment_count:
for x_index in width_segments:
var top_left := z_index * row_size + x_index
var top_right := top_left + 1
@@ -66,17 +93,20 @@ func _build_terrain_ribbon() -> void:
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh.surface_set_material(0, river_material)
river_ribbon.mesh = mesh
_align_pool_to_terrain(terrain)
return mesh
func _align_pool_to_terrain(terrain: Terrain3D) -> void:
var pool := $PlungePool as MeshInstance3D
var pool_sample := to_global(Vector3(pool.position.x, 0.0, pool.position.z))
var pool_sample := Vector3(
Watercourse.PLUNGE_POOL_CENTER.x, 0.0, Watercourse.PLUNGE_POOL_CENTER.y
)
var pool_height := terrain.data.get_height(pool_sample)
if is_nan(pool_height):
return
var local_surface := to_local(Vector3(pool_sample.x, pool_height + water_offset, pool_sample.z))
for node_name in [&"PlungePool", &"FoamRing", &"FoamAtFall"]:
var surface := get_node(NodePath(node_name)) as MeshInstance3D
surface.position.y = local_surface.y
pool.position = local_surface
var foam_ring := $FoamRing as MeshInstance3D
foam_ring.position = local_surface + Vector3(0.0, 0.1, 0.0)
var foam_at_fall := $FoamAtFall as MeshInstance3D
foam_at_fall.position = local_surface + Vector3(0.0, 0.13, -2.7)
+8 -8
View File
@@ -17,22 +17,22 @@ rings = 2
[sub_resource type="StandardMaterial3D" id="Material_foam"]
transparency = 1
shading_mode = 0
albedo_color = Color(0.8, 0.93, 0.86, 0.46)
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.18
emission_energy_multiplier = 0.06
roughness = 0.74
[sub_resource type="TorusMesh" id="Mesh_foam_ring"]
material = SubResource("Material_foam")
inner_radius = 4.8
outer_radius = 5.65
inner_radius = 5.15
outer_radius = 5.5
rings = 28
ring_segments = 10
[sub_resource type="PlaneMesh" id="Mesh_foam_patch"]
material = SubResource("Material_foam")
size = Vector2(10.5, 2.2)
size = Vector2(6, 1.5)
subdivide_width = 8
subdivide_depth = 2
@@ -44,18 +44,18 @@ river_material = SubResource("ShaderMaterial_water")
[node name="RiverRibbon" type="MeshInstance3D" parent="."]
cast_shadow = 0
[node name="UpperRiverRibbon" type="MeshInstance3D" parent="."]
cast_shadow = 0
[node name="PlungePool" type="MeshInstance3D" parent="."]
position = Vector3(0, -0.02, -25)
scale = Vector3(1.25, 1, 0.82)
mesh = SubResource("Mesh_plunge_pool")
[node name="FoamRing" type="MeshInstance3D" parent="."]
position = Vector3(0, 0.09, -25)
scale = Vector3(1.25, 0.12, 0.82)
cast_shadow = 0
mesh = SubResource("Mesh_foam_ring")
[node name="FoamAtFall" type="MeshInstance3D" parent="."]
position = Vector3(0, 0.12, -25.8)
cast_shadow = 0
mesh = SubResource("Mesh_foam_patch")
+38 -14
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=10 format=3]
[gd_scene load_steps=12 format=3]
[ext_resource type="Shader" path="res://world/jajce/materials/waterfall.gdshader" id="1_waterfall_shader"]
@@ -7,7 +7,7 @@ shader = ExtResource("1_waterfall_shader")
[sub_resource type="QuadMesh" id="Mesh_waterfall_body"]
material = SubResource("ShaderMaterial_waterfall")
size = Vector2(10.8, 14)
size = Vector2(9.6, 11.4)
subdivide_width = 18
subdivide_depth = 28
@@ -32,50 +32,74 @@ size = Vector3(4.8, 5.5, 3.4)
[sub_resource type="BoxMesh" id="Mesh_lip"]
material = SubResource("Material_rock_moss")
size = Vector3(11.6, 0.7, 3.2)
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="PlaneMesh" id="Mesh_lip_foam"]
material = SubResource("Material_lip_foam")
size = Vector2(9.2, 1.45)
subdivide_width = 8
subdivide_depth = 2
[node name="WaterfallBody" type="Node3D"]
position = Vector3(25, 8.9, -24)
position = Vector3(25, 5.8, -24)
[node name="RockFrame" type="Node3D" parent="."]
[node name="WestCliff" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(-7.2, -0.45, -1)
position = Vector3(-6.1, -0.15, -0.9)
rotation_degrees = Vector3(0, 8, -3)
scale = Vector3(0.72, 0.86, 0.8)
mesh = SubResource("Mesh_cliff_side")
[node name="EastCliff" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(7.2, -0.25, -1.2)
position = Vector3(6.2, 0.05, -1.1)
rotation_degrees = Vector3(0, -7, 4)
scale = Vector3(0.68, 0.88, 0.76)
mesh = SubResource("Mesh_cliff_side")
[node name="WestCrownRock" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(-5.0, 7.45, -1.25)
position = Vector3(-4.9, 5.8, -1.2)
rotation_degrees = Vector3(8, -14, -11)
scale = Vector3(0.85, 0.62, 0.9)
scale = Vector3(0.82, 0.58, 0.88)
mesh = SubResource("Mesh_rock_lobe")
[node name="EastCrownRock" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(5.1, 7.55, -1.4)
position = Vector3(4.8, 5.9, -1.35)
rotation_degrees = Vector3(-6, 12, 9)
scale = Vector3(0.82, 0.6, 0.86)
scale = Vector3(0.78, 0.58, 0.84)
mesh = SubResource("Mesh_rock_lobe")
[node name="WestLowerRock" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(-5.9, -4.8, 0.1)
position = Vector3(-5.35, -5.0, 0.15)
rotation_degrees = Vector3(7, 14, 12)
scale = Vector3(0.88, 0.72, 0.9)
mesh = SubResource("Mesh_rock_lobe")
[node name="EastLowerRock" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(6.0, -4.4, 0)
position = Vector3(5.45, -4.75, 0)
rotation_degrees = Vector3(-4, -12, -9)
scale = Vector3(0.9, 0.74, 0.88)
mesh = SubResource("Mesh_rock_lobe")
[node name="RiverLip" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(0, 7.05, 0.2)
position = Vector3(0, 5.42, -0.2)
mesh = SubResource("Mesh_lip")
[node name="LipFoam" type="MeshInstance3D" parent="RockFrame"]
position = Vector3(0, 5.74, -0.85)
cast_shadow = 0
mesh = SubResource("Mesh_lip_foam")
[node name="FallingWater" type="MeshInstance3D" parent="."]
position = Vector3(0, 0, 1.05)
position = Vector3(0, 0, 1.1)
cast_shadow = 0
mesh = SubResource("Mesh_waterfall_body")
+10 -7
View File
@@ -1,4 +1,6 @@
[gd_scene load_steps=4 format=3]
[gd_scene load_steps=5 format=3]
[ext_resource type="Texture2D" path="res://world/jajce/materials/mist_soft.svg" id="1_mist_texture"]
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_mist"]
lifetime_randomness = 0.35
@@ -7,26 +9,27 @@ gravity = Vector3(0, -0.35, 0)
initial_velocity_min = 0.25
initial_velocity_max = 1.2
scale_min = 0.08
scale_max = 2.0
color = Color(0.82, 0.94, 0.96, 0.35)
scale_max = 1.35
color = Color(0.82, 0.94, 0.96, 0.22)
emission_shape = 1
emission_box_extents = Vector3(5.8, 1.5, 1.35)
[sub_resource type="StandardMaterial3D" id="Material_mist"]
albedo_color = Color(0.82, 0.94, 0.96, 0.28)
albedo_color = Color(0.82, 0.94, 0.96, 0.3)
transparency = 1
shading_mode = 0
albedo_texture = ExtResource("1_mist_texture")
billboard_mode = 1
cull_mode = 0
[sub_resource type="QuadMesh" id="Mesh_mist"]
material = SubResource("Material_mist")
size = Vector2(1.8, 1.8)
size = Vector2(1.45, 1.45)
[node name="WaterfallMist" type="GPUParticles3D"]
position = Vector3(25, 2.05, -24)
position = Vector3(25, 0.65, -21.5)
emitting = true
amount = 96
amount = 56
lifetime = 3.0
one_shot = false
preprocess = 1.5
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<radialGradient id="mist" cx="50%" cy="48%" r="50%">
<stop offset="0" stop-color="#dff4ef" stop-opacity="0.82"/>
<stop offset="0.42" stop-color="#c8e7e3" stop-opacity="0.48"/>
<stop offset="0.76" stop-color="#b8d9d7" stop-opacity="0.14"/>
<stop offset="1" stop-color="#b8d9d7" stop-opacity="0"/>
</radialGradient>
</defs>
<circle cx="32" cy="32" r="31" fill="url(#mist)"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c0ie27xnkmfd8"
path="res://.godot/imported/mist_soft.svg-8d0454048e96ea4cac3f1157714c34e4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://world/jajce/materials/mist_soft.svg"
dest_files=["res://.godot/imported/mist_soft.svg-8d0454048e96ea4cac3f1157714c34e4.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=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false