Rework island water and shop character

This commit is contained in:
Alexander Sellite 2026-08-08 00:34:06 -04:00
parent 0d50c324e9
commit 352350157b
65 changed files with 1144 additions and 1120 deletions

View file

@ -3,17 +3,25 @@ extends Area3D
signal local_player_range_changed(in_range: bool)
@export_node_path("Node3D") var look_character_path: NodePath = ^"../Shopkeeper"
var _local_player: Player
var _local_player_in_range: bool = false
var _look_character: WorldCharacterDisplay
func _ready() -> void:
_look_character = get_node_or_null(
look_character_path
) as WorldCharacterDisplay
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func setup_local_player(player: Player) -> void:
_local_player = player
if _look_character != null:
_look_character.set_head_look_target(player)
_refresh_overlaps()
@ -29,6 +37,12 @@ func is_avatar_in_range(avatar: Player) -> bool:
)
func get_prompt_anchor_position() -> Vector3:
if _look_character != null:
return _look_character.get_head_anchor_position()
return global_position + Vector3.UP * 1.25
func _refresh_overlaps() -> void:
var is_overlapping: bool = (
_local_player != null
@ -49,6 +63,8 @@ func _on_body_exited(body: Node3D) -> void:
func _set_local_player_in_range(in_range: bool) -> void:
if _look_character != null:
_look_character.set_head_look_active(in_range)
if _local_player_in_range == in_range:
return
_local_player_in_range = in_range

File diff suppressed because one or more lines are too long

View file

@ -1,90 +1,27 @@
[gd_scene load_steps=11 format=3]
[gd_scene load_steps=5 format=3]
[ext_resource type="Script" path="res://world/fishing_shop_interaction.gd" id="1_shop"]
[ext_resource type="Script" path="res://world/world_prop.gd" id="2_prop"]
[sub_resource type="BoxMesh" id="CounterMesh"]
resource_local_to_scene = true
size = Vector3(4.2, 1.25, 1.8)
[sub_resource type="BoxMesh" id="RoofMesh"]
resource_local_to_scene = true
size = Vector3(5.2, 0.35, 3.2)
[sub_resource type="BoxMesh" id="PostMesh"]
resource_local_to_scene = true
size = Vector3(0.28, 3.2, 0.28)
[sub_resource type="BoxMesh" id="BackMesh"]
resource_local_to_scene = true
size = Vector3(5, 2.8, 0.25)
[sub_resource type="StandardMaterial3D" id="PrimaryMaterial"]
resource_local_to_scene = true
albedo_color = Color(0.12, 0.68, 0.62, 1)
[sub_resource type="StandardMaterial3D" id="AccentMaterial"]
resource_local_to_scene = true
albedo_color = Color(1, 0.68, 0.18, 1)
[sub_resource type="StandardMaterial3D" id="BackMaterial"]
resource_local_to_scene = true
albedo_color = Color(0.13, 0.34, 0.38, 1)
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="2_character"]
[ext_resource type="Script" path="res://world/world_character_display.gd" id="3_display"]
[sub_resource type="SphereShape3D" id="InteractionShape"]
resource_local_to_scene = true
radius = 3.0
[sub_resource type="BoxShape3D" id="CounterShape"]
resource_local_to_scene = true
size = Vector3(4.2, 1.25, 1.8)
[node name="FishingShopWorld" type="Node3D"]
script = ExtResource("2_prop")
collision_root_path = NodePath("Collision")
interaction_area_path = NodePath("InteractionArea")
entrance_marker_path = NodePath("EntranceMarker")
[node name="Visual" type="Node3D" parent="."]
[node name="Shopkeeper" parent="." instance=ExtResource("2_character")]
script = ExtResource("3_display")
species_id = "pointy"
fur_color_id = "white"
ears_id = "pointy_wide"
eyes_id = "punk_purple"
nose_id = "triangle_small"
mouth_id = "slanted"
head_look_enabled = true
[node name="Counter" type="MeshInstance3D" parent="Visual"]
position = Vector3(0, 0.625, 0)
mesh = SubResource("CounterMesh")
material_override = SubResource("PrimaryMaterial")
[node name="Back" type="MeshInstance3D" parent="Visual"]
position = Vector3(0, 1.4, 1.25)
mesh = SubResource("BackMesh")
material_override = SubResource("BackMaterial")
[node name="Roof" type="MeshInstance3D" parent="Visual"]
position = Vector3(0, 3.15, 0.35)
mesh = SubResource("RoofMesh")
material_override = SubResource("AccentMaterial")
[node name="WestPost" type="MeshInstance3D" parent="Visual"]
position = Vector3(-2.15, 1.6, -0.8)
mesh = SubResource("PostMesh")
material_override = SubResource("PrimaryMaterial")
[node name="EastPost" type="MeshInstance3D" parent="Visual"]
position = Vector3(2.15, 1.6, -0.8)
mesh = SubResource("PostMesh")
material_override = SubResource("PrimaryMaterial")
[node name="ShopName" type="Label3D" parent="Visual"]
position = Vector3(0, 3.7, 0)
text = "fishing shop"
font_size = 42
outline_size = 9
billboard = 1
no_depth_test = true
[node name="Collision" type="StaticBody3D" parent="."]
[node name="CounterShape" type="CollisionShape3D" parent="Collision"]
position = Vector3(0, 0.625, 0)
shape = SubResource("CounterShape")
[node name="AnimationPlayer" parent="Shopkeeper"]
autoplay = "idle"
[node name="InteractionArea" type="Area3D" parent="."]
unique_name_in_owner = true
@ -95,6 +32,3 @@ script = ExtResource("1_shop")
[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"]
shape = SubResource("InteractionShape")
[node name="EntranceMarker" type="Marker3D" parent="."]
position = Vector3(0, 0, -2)

View file

@ -1,16 +1,16 @@
shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_disabled, unshaded;
uniform vec4 lead_color : source_color = vec4(0.620, 0.875, 0.855, 1.0);
uniform vec4 trail_color : source_color = vec4(0.430, 0.780, 0.770, 1.0);
uniform float ribbon_width : hint_range(0.2, 2.0, 0.01) = 0.85;
uniform float land_inset : hint_range(0.0, 0.4, 0.01) = 0.10;
uniform vec4 lead_color : source_color = vec4(0.780, 0.960, 0.930, 1.0);
uniform vec4 trail_color : source_color = vec4(0.500, 0.840, 0.820, 1.0);
uniform float ribbon_width : hint_range(0.2, 2.0, 0.01) = 1.35;
uniform float land_inset : hint_range(0.0, 0.4, 0.01) = 0.12;
uniform float tide_speed : hint_range(0.1, 3.0, 0.01) = 1.42;
uniform float tide_distance : hint_range(0.0, 0.6, 0.01) = 0.30;
uniform float lead_width : hint_range(0.01, 0.2, 0.005) = 0.065;
uniform float trail_width : hint_range(0.02, 0.4, 0.005) = 0.18;
uniform float lead_strength : hint_range(0.0, 1.0, 0.01) = 0.48;
uniform float trail_strength : hint_range(0.0, 1.0, 0.01) = 0.25;
uniform float tide_distance : hint_range(0.0, 0.6, 0.01) = 0.42;
uniform float lead_width : hint_range(0.01, 0.2, 0.005) = 0.14;
uniform float trail_width : hint_range(0.02, 0.4, 0.005) = 0.32;
uniform float lead_strength : hint_range(0.0, 1.0, 0.01) = 0.88;
uniform float trail_strength : hint_range(0.0, 1.0, 0.01) = 0.52;
uniform float phase_scale : hint_range(0.001, 0.2, 0.001) = 0.035;
uniform float phase_strength : hint_range(0.0, 2.0, 0.01) = 0.55;

View file

@ -5,15 +5,15 @@
[resource]
render_priority = 1
shader = ExtResource("1_shader")
shader_parameter/lead_color = Color(0.619608, 0.87451, 0.854902, 1)
shader_parameter/trail_color = Color(0.431373, 0.780392, 0.768627, 1)
shader_parameter/ribbon_width = 0.85
shader_parameter/land_inset = 0.1
shader_parameter/lead_color = Color(0.78, 0.96, 0.93, 1)
shader_parameter/trail_color = Color(0.5, 0.84, 0.82, 1)
shader_parameter/ribbon_width = 1.35
shader_parameter/land_inset = 0.12
shader_parameter/tide_speed = 1.42
shader_parameter/tide_distance = 0.3
shader_parameter/lead_width = 0.065
shader_parameter/trail_width = 0.18
shader_parameter/lead_strength = 0.48
shader_parameter/trail_strength = 0.25
shader_parameter/tide_distance = 0.42
shader_parameter/lead_width = 0.14
shader_parameter/trail_width = 0.32
shader_parameter/lead_strength = 0.88
shader_parameter/trail_strength = 0.52
shader_parameter/phase_scale = 0.035
shader_parameter/phase_strength = 0.55

View file

@ -1,15 +0,0 @@
[gd_resource type="ShaderMaterial" load_steps=3 format=3]
[ext_resource type="Texture2D" path="res://art/exported/environment/textures/grass_lite.png" id="1_texture"]
[ext_resource type="Shader" path="res://world/materials/terrain_surface_projection.gdshader" id="2_shader"]
[resource]
resource_name = "Starter Island Grass"
shader = ExtResource("2_shader")
shader_parameter/albedo_texture = ExtResource("1_texture")
shader_parameter/albedo_tint = Color(1, 1, 1, 1)
shader_parameter/tile_world_size = 2.5
shader_parameter/blend_sharpness = 6.0
shader_parameter/top_projection_bias = 2.0
shader_parameter/roughness = 1.0
shader_parameter/specular = 0.5

View file

@ -1,15 +0,0 @@
[gd_resource type="ShaderMaterial" load_steps=3 format=3]
[ext_resource type="Texture2D" path="res://art/exported/environment/textures/sand.png" id="1_texture"]
[ext_resource type="Shader" path="res://world/materials/terrain_surface_projection.gdshader" id="2_shader"]
[resource]
resource_name = "Starter Island Sand"
shader = ExtResource("2_shader")
shader_parameter/albedo_texture = ExtResource("1_texture")
shader_parameter/albedo_tint = Color(1, 1, 1, 1)
shader_parameter/tile_world_size = 2.5
shader_parameter/blend_sharpness = 6.0
shader_parameter/top_projection_bias = 2.0
shader_parameter/roughness = 1.0
shader_parameter/specular = 0.5

View file

@ -1,15 +0,0 @@
[gd_resource type="ShaderMaterial" load_steps=3 format=3]
[ext_resource type="Texture2D" path="res://art/exported/environment/textures/sandstone_heavy.png" id="1_texture"]
[ext_resource type="Shader" path="res://world/materials/terrain_surface_projection.gdshader" id="2_shader"]
[resource]
resource_name = "Starter Island Sandstone"
shader = ExtResource("2_shader")
shader_parameter/albedo_texture = ExtResource("1_texture")
shader_parameter/albedo_tint = Color(1, 1, 1, 1)
shader_parameter/tile_world_size = 2.5
shader_parameter/blend_sharpness = 6.0
shader_parameter/top_projection_bias = 2.0
shader_parameter/roughness = 1.0
shader_parameter/specular = 0.0

View file

@ -29,9 +29,29 @@ uniform float contour_corner_variation : hint_range(0.0, 0.15, 0.005) = 0.045;
uniform float contour_smoothing_pixels : hint_range(0.0, 2.5, 0.05) = 1.0;
uniform float contour_smoothing_strength : hint_range(0.0, 1.0, 0.01) = 0.72;
uniform float contour_depth_reject : hint_range(0.05, 2.0, 0.01) = 0.60;
uniform float shoreline_horizontal_reach : hint_range(0.05, 1.5, 0.01) = 0.32;
uniform float contact_fade_depth : hint_range(0.0, 0.1, 0.001) = 0.018;
uniform float open_water_drift_scale : hint_range(0.001, 0.3, 0.001) = 0.018;
uniform vec2 open_water_drift_speed = vec2(0.010, -0.007);
uniform float open_water_drift_strength : hint_range(0.0, 0.08, 0.001) = 0.045;
uniform vec4 surface_mark_color : source_color = vec4(0.580, 0.850, 0.860, 1.0);
uniform vec2 surface_mark_density = vec2(1.30, 0.90);
uniform vec2 surface_mark_drift = vec2(0.045, -0.032);
uniform float surface_mark_strength : hint_range(0.0, 0.3, 0.005) = 0.12;
uniform float surface_mottle_scale : hint_range(0.01, 1.0, 0.01) = 0.11;
uniform vec2 surface_mottle_speed = vec2(-0.018, 0.013);
uniform float surface_mottle_strength : hint_range(0.0, 0.2, 0.005) = 0.07;
uniform vec2 shoreline_mark_density = vec2(4.60, 3.20);
uniform vec2 shoreline_mark_drift = vec2(0.085, -0.060);
uniform float shoreline_mark_depth : hint_range(0.1, 2.0, 0.01) = 0.95;
uniform float shoreline_mark_strength : hint_range(0.0, 1.0, 0.01) = 0.82;
uniform float shoreline_base_fill : hint_range(0.0, 1.0, 0.01) = 0.32;
uniform float shoreline_opacity_boost : hint_range(0.0, 0.5, 0.01) = 0.26;
uniform float surface_highlight_night_floor : hint_range(0.0, 1.0, 0.01) = 0.72;
uniform float shoreline_minimum_reach : hint_range(0.1, 1.0, 0.01) = 0.48;
uniform float shoreline_crest_width : hint_range(0.02, 0.5, 0.01) = 0.13;
uniform float shoreline_trail_spacing : hint_range(0.05, 0.8, 0.01) = 0.28;
uniform float shoreline_trail_strength : hint_range(0.0, 1.0, 0.01) = 0.55;
uniform float distant_alpha_start : hint_range(10.0, 2000.0, 1.0) = 260.0;
uniform float distant_alpha_end : hint_range(20.0, 5000.0, 1.0) = 1400.0;
@ -61,7 +81,63 @@ float value_noise(vec2 point) {
}
float reconstruct_view_depth(
float block_mark(vec2 point, vec2 half_size) {
return (
step(abs(point.x), half_size.x)
* step(abs(point.y), half_size.y)
);
}
float blocky_mark_layer(
vec2 flowing_grid,
float seed,
float occupancy_threshold
) {
float row = floor(flowing_grid.y);
flowing_grid.x += mod(row, 2.0) * 0.43;
vec2 cell = floor(flowing_grid);
vec2 local = fract(flowing_grid) - vec2(0.5);
vec2 jitter = vec2(
hash_21(cell + vec2(3.7 + seed, 9.1)),
hash_21(cell + vec2(7.3, 2.9 + seed))
) - vec2(0.5);
local -= jitter * vec2(0.52, 0.46);
local = floor((local + vec2(0.5)) * 8.0) / 8.0 - vec2(0.5);
float shape_key = hash_21(cell + vec2(13.1 + seed, 5.7));
float size_key = hash_21(cell + vec2(1.9, 17.3 + seed));
vec2 half_size = mix(vec2(0.10, 0.07), vec2(0.18, 0.12), size_key);
float mark = block_mark(local, half_size);
if (shape_key < 0.34) {
mark = max(
mark,
block_mark(
local - vec2(half_size.x * 0.8, -0.11),
vec2(half_size.x * 0.52, 0.045)
)
);
} else if (shape_key < 0.67) {
mark *= 1.0 - block_mark(
local - vec2(half_size.x * 0.7, 0.0),
vec2(0.045)
);
} else {
mark = max(
mark,
block_mark(
local + vec2(half_size.x * 0.75, 0.10),
vec2(0.055, 0.04)
)
);
}
return mark * step(
occupancy_threshold,
hash_21(cell + vec2(19.7 + seed, 23.3))
);
}
vec3 reconstruct_view_position(
vec2 screen_uv,
float raw_depth,
mat4 inverse_projection
@ -72,22 +148,39 @@ float reconstruct_view_depth(
vec3 ndc = vec3(screen_uv * 2.0 - 1.0, raw_depth);
#endif
vec4 view_position = inverse_projection * vec4(ndc, 1.0);
return -view_position.z / max(abs(view_position.w), 0.00001);
return view_position.xyz / max(abs(view_position.w), 0.00001);
}
vec3 reconstruct_world_position(
vec2 screen_uv,
float raw_depth,
mat4 inverse_projection,
mat4 inverse_view
) {
vec3 view_position = reconstruct_view_position(
screen_uv,
raw_depth,
inverse_projection
);
return (inverse_view * vec4(view_position, 1.0)).xyz;
}
float sample_water_depth(
vec2 screen_uv,
float water_surface_depth,
mat4 inverse_projection
float water_surface_height,
mat4 inverse_projection,
mat4 inverse_view
) {
float raw_depth = texture(depth_texture, screen_uv).r;
float scene_depth = reconstruct_view_depth(
vec3 scene_world_position = reconstruct_world_position(
screen_uv,
raw_depth,
inverse_projection
inverse_projection,
inverse_view
);
return scene_depth - water_surface_depth;
return water_surface_height - scene_world_position.y;
}
@ -118,12 +211,13 @@ void vertex() {
void fragment() {
float raw_depth = texture(depth_texture, SCREEN_UV).r;
float scene_view_depth = reconstruct_view_depth(
vec3 scene_world_position = reconstruct_world_position(
SCREEN_UV,
raw_depth,
INV_PROJECTION_MATRIX
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float water_depth = max(scene_view_depth - surface_view_depth, 0.0);
float water_depth = max(world_position.y - scene_world_position.y, 0.0);
water_depth = min(water_depth, deep_depth * 4.0);
float contour_depth = water_depth;
@ -137,23 +231,51 @@ void fragment() {
float contour_weight_sum = 1.0;
float left_depth = sample_water_depth(
SCREEN_UV - vec2(contour_texel.x, 0.0),
surface_view_depth,
INV_PROJECTION_MATRIX
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float right_depth = sample_water_depth(
SCREEN_UV + vec2(contour_texel.x, 0.0),
surface_view_depth,
INV_PROJECTION_MATRIX
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float upper_depth = sample_water_depth(
SCREEN_UV - vec2(0.0, contour_texel.y),
surface_view_depth,
INV_PROJECTION_MATRIX
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float lower_depth = sample_water_depth(
SCREEN_UV + vec2(0.0, contour_texel.y),
surface_view_depth,
INV_PROJECTION_MATRIX
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float upper_left_depth = sample_water_depth(
SCREEN_UV - contour_texel,
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float upper_right_depth = sample_water_depth(
SCREEN_UV + vec2(contour_texel.x, -contour_texel.y),
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float lower_left_depth = sample_water_depth(
SCREEN_UV + vec2(-contour_texel.x, contour_texel.y),
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float lower_right_depth = sample_water_depth(
SCREEN_UV + contour_texel,
world_position.y,
INV_PROJECTION_MATRIX,
INV_VIEW_MATRIX
);
float left_weight = contour_neighbor_weight(
water_depth, left_depth, contour_depth_reject
@ -167,11 +289,37 @@ void fragment() {
float lower_weight = contour_neighbor_weight(
water_depth, lower_depth, contour_depth_reject
);
float diagonal_scale = 0.70710678;
float upper_left_weight = contour_neighbor_weight(
water_depth, upper_left_depth, contour_depth_reject
) * diagonal_scale;
float upper_right_weight = contour_neighbor_weight(
water_depth, upper_right_depth, contour_depth_reject
) * diagonal_scale;
float lower_left_weight = contour_neighbor_weight(
water_depth, lower_left_depth, contour_depth_reject
) * diagonal_scale;
float lower_right_weight = contour_neighbor_weight(
water_depth, lower_right_depth, contour_depth_reject
) * diagonal_scale;
contour_depth_sum += left_depth * left_weight;
contour_depth_sum += right_depth * right_weight;
contour_depth_sum += upper_depth * upper_weight;
contour_depth_sum += lower_depth * lower_weight;
float neighbor_support = left_weight + right_weight + upper_weight + lower_weight;
contour_depth_sum += upper_left_depth * upper_left_weight;
contour_depth_sum += upper_right_depth * upper_right_weight;
contour_depth_sum += lower_left_depth * lower_left_weight;
contour_depth_sum += lower_right_depth * lower_right_weight;
float neighbor_support = (
left_weight
+ right_weight
+ upper_weight
+ lower_weight
+ upper_left_weight
+ upper_right_weight
+ lower_left_weight
+ lower_right_weight
);
contour_weight_sum += neighbor_support;
float smoothed_contour_depth = contour_depth_sum / contour_weight_sum;
contour_depth = mix(
@ -182,6 +330,35 @@ void fragment() {
contour_feature_support = smoothstep(0.75, 1.75, neighbor_support);
}
vec2 scene_world_dx = dFdx(scene_world_position.xz);
vec2 scene_world_dy = dFdy(scene_world_position.xz);
float horizontal_dx = max(length(scene_world_dx), 0.0001);
float horizontal_dy = max(length(scene_world_dy), 0.0001);
float depth_gradient = length(vec2(
dFdx(water_depth) / horizontal_dx,
dFdy(water_depth) / horizontal_dy
));
float estimated_shore_distance = min(
water_depth / max(depth_gradient, 0.001),
shoreline_horizontal_reach * 4.0
);
float proximity_center = shoreline_horizontal_reach * 0.86;
float proximity_filter_width = max(
fwidth(estimated_shore_distance) * 1.5,
shoreline_horizontal_reach * 0.12
);
float shoreline_proximity = 1.0 - smoothstep(
max(proximity_center - proximity_filter_width, 0.0),
proximity_center + proximity_filter_width,
estimated_shore_distance
);
float contact_filter_width = max(fwidth(water_depth) * 1.5, 0.004);
float contact_stability = smoothstep(
max(contact_fade_depth - contact_filter_width, 0.0),
contact_fade_depth + contact_filter_width,
water_depth
);
float shallow_mix = smoothstep(0.0, shallow_depth, water_depth);
float deep_mix = smoothstep(shallow_depth, deep_depth, water_depth);
vec3 water_color = mix(shore_color.rgb, shallow_color.rgb, shallow_mix);
@ -207,6 +384,7 @@ void fragment() {
contour_depth
);
tide_wash *= contour_feature_support;
tide_wash *= shoreline_proximity;
tide_wash *= 1.0 - smoothstep(shallow_depth, deep_depth, water_depth);
water_color = mix(
water_color,
@ -220,6 +398,148 @@ void fragment() {
);
}
vec2 flowing_grid = (
world_position.xz * surface_mark_density
+ TIME * surface_mark_drift
);
float surface_row = floor(flowing_grid.y);
flowing_grid.x += mod(surface_row, 2.0) * 0.43;
vec2 surface_cell = floor(flowing_grid);
vec2 surface_local = fract(flowing_grid) - vec2(0.5);
vec2 surface_jitter = vec2(
hash_21(surface_cell + vec2(3.7, 9.1)),
hash_21(surface_cell + vec2(7.3, 2.9))
) - vec2(0.5);
surface_local -= surface_jitter * vec2(0.52, 0.46);
surface_local = (
floor((surface_local + vec2(0.5)) * 8.0) / 8.0
- vec2(0.5)
);
float surface_shape = hash_21(surface_cell + vec2(13.1, 5.7));
float surface_size = hash_21(surface_cell + vec2(1.9, 17.3));
vec2 surface_half_size = mix(
vec2(0.10, 0.07),
vec2(0.18, 0.12),
surface_size
);
float surface_mark = block_mark(surface_local, surface_half_size);
if (surface_shape < 0.34) {
surface_mark = max(
surface_mark,
block_mark(
surface_local - vec2(surface_half_size.x * 0.8, -0.11),
vec2(surface_half_size.x * 0.52, 0.045)
)
);
} else if (surface_shape < 0.67) {
surface_mark *= 1.0 - block_mark(
surface_local - vec2(surface_half_size.x * 0.7, 0.0),
vec2(0.045)
);
} else {
surface_mark = max(
surface_mark,
block_mark(
surface_local + vec2(surface_half_size.x * 0.75, 0.10),
vec2(0.055, 0.04)
)
);
}
surface_mark *= step(
0.22,
hash_21(surface_cell + vec2(19.7, 23.3))
);
float mark_visibility = mix(1.0, 0.68, deep_mix);
float surface_highlight = (
surface_mark * surface_mark_strength * mark_visibility
);
vec2 shoreline_phase_coordinates = world_position.xz * phase_noise_scale;
float shoreline_phase = (
value_noise(shoreline_phase_coordinates)
+ value_noise(
shoreline_phase_coordinates * 0.47 + vec2(9.7, 3.1)
) * 0.5
- 0.75
) * phase_noise_strength;
float shoreline_cycle = (
sin(TIME * tide_speed + shoreline_phase) * 0.5 + 0.5
);
float animated_shoreline_depth = shoreline_mark_depth * mix(
shoreline_minimum_reach,
1.0,
shoreline_cycle
);
float shoreline_mask = 1.0 - smoothstep(
max(animated_shoreline_depth * 0.08, 0.01),
max(animated_shoreline_depth, 0.02),
contour_depth
);
float shoreline_depth_visibility = 1.0 - smoothstep(
shallow_depth,
deep_depth,
water_depth
);
shoreline_mask *= shoreline_depth_visibility;
float shoreline_lead_depth = animated_shoreline_depth * 0.82;
float shoreline_lead_crest = 1.0 - smoothstep(
shoreline_crest_width * 0.25,
shoreline_crest_width,
abs(contour_depth - shoreline_lead_depth)
);
float shoreline_trail_depth = max(
shoreline_lead_depth - shoreline_trail_spacing,
shoreline_crest_width
);
float shoreline_trail_crest = 1.0 - smoothstep(
shoreline_crest_width * 0.40,
shoreline_crest_width * 1.45,
abs(contour_depth - shoreline_trail_depth)
);
shoreline_lead_crest *= shoreline_depth_visibility;
shoreline_trail_crest *= (
shoreline_depth_visibility * shoreline_trail_strength
);
float shoreline_marks = max(
blocky_mark_layer(
world_position.xz * shoreline_mark_density
+ TIME * shoreline_mark_drift,
17.0,
0.04
),
blocky_mark_layer(
world_position.xz * shoreline_mark_density * 1.37
- TIME * shoreline_mark_drift.yx * 0.73,
41.0,
0.08
)
);
float shoreline_foam = shoreline_mask * mix(
shoreline_base_fill,
1.0,
shoreline_marks
);
float moving_crests = max(
shoreline_lead_crest,
shoreline_trail_crest
) * mix(0.68, 1.0, shoreline_marks);
shoreline_foam = max(shoreline_foam, moving_crests);
shoreline_foam *= shoreline_proximity;
water_alpha = min(
water_alpha + shoreline_foam * shoreline_opacity_boost,
0.98
);
water_alpha *= contact_stability;
float surface_mottle = value_noise(
world_position.xz * surface_mottle_scale
+ TIME * surface_mottle_speed
);
surface_mottle = floor(surface_mottle * 6.0) / 5.0;
water_color *= 1.0 + (
surface_mottle * 2.0 - 1.0
) * surface_mottle_strength;
vec2 drift_coordinates = (
world_position.xz * open_water_drift_scale
+ TIME * open_water_drift_speed
@ -236,6 +556,19 @@ void fragment() {
water_alpha = mix(water_alpha, 0.96, distant_mix);
water_color *= environment_tint.rgb * environment_brightness;
vec3 visible_highlight_color = surface_mark_color.rgb * max(
environment_tint.rgb * environment_brightness,
vec3(surface_highlight_night_floor)
);
float visible_highlight = max(
surface_highlight,
shoreline_foam * shoreline_mark_strength
);
water_color = mix(
water_color,
visible_highlight_color,
clamp(visible_highlight, 0.0, 1.0)
);
ALBEDO = water_color;
ALPHA = clamp(water_alpha, 0.1, 0.96);

View file

@ -1,6 +1,6 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
[gd_resource type="ShaderMaterial" format=3 uid="uid://6lafg0ac2qul"]
[ext_resource type="Shader" path="res://world/materials/stylized_water.gdshader" id="1_water_shader"]
[ext_resource type="Shader" uid="uid://lvo7xyr3c1k3" path="res://world/materials/stylized_water.gdshader" id="1_water_shader"]
[resource]
render_priority = 0
@ -11,25 +11,45 @@ shader_parameter/deep_color = Color(0.109804, 0.345098, 0.435294, 1)
shader_parameter/tide_wash_color = Color(0.360784, 0.721569, 0.733333, 1)
shader_parameter/shallow_depth = 0.7
shader_parameter/deep_depth = 3.6
shader_parameter/shore_depth_width = 0.16
shader_parameter/shore_alpha = 0.44
shader_parameter/shallow_alpha = 0.6
shader_parameter/deep_alpha = 0.82
shader_parameter/shore_depth_width = 0.1
shader_parameter/shore_alpha = 0.68
shader_parameter/shallow_alpha = 0.78
shader_parameter/deep_alpha = 0.9
shader_parameter/tide_effect_enabled = true
shader_parameter/tide_speed = 1.42
shader_parameter/tide_distance = 0.3
shader_parameter/tide_wash_width = 0.38
shader_parameter/tide_distance = 0.11
shader_parameter/tide_wash_width = 0.16
shader_parameter/tide_wash_strength = 0.11
shader_parameter/tide_wash_alpha_shift = 0.045
shader_parameter/phase_noise_scale = 0.055
shader_parameter/phase_noise_strength = 0.68
shader_parameter/contour_corner_variation = 0.045
shader_parameter/contour_smoothing_pixels = 1.0
shader_parameter/contour_smoothing_strength = 0.72
shader_parameter/contour_corner_variation = 0.025
shader_parameter/contour_smoothing_pixels = 1.35
shader_parameter/contour_smoothing_strength = 0.88
shader_parameter/contour_depth_reject = 0.6
shader_parameter/shoreline_horizontal_reach = 0.32
shader_parameter/contact_fade_depth = 0.018
shader_parameter/open_water_drift_scale = 0.018
shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007)
shader_parameter/open_water_drift_strength = 0.045
shader_parameter/surface_mark_color = Color(0.58, 0.85, 0.86, 1)
shader_parameter/surface_mark_density = Vector2(1.5, 1.05)
shader_parameter/surface_mark_drift = Vector2(0.045, -0.032)
shader_parameter/surface_mark_strength = 0.12
shader_parameter/surface_mottle_scale = 0.11
shader_parameter/surface_mottle_speed = Vector2(-0.018, 0.013)
shader_parameter/surface_mottle_strength = 0.07
shader_parameter/shoreline_mark_density = Vector2(4.6, 3.2)
shader_parameter/shoreline_mark_drift = Vector2(0.085, -0.06)
shader_parameter/shoreline_mark_depth = 0.55
shader_parameter/shoreline_mark_strength = 0.95
shader_parameter/shoreline_base_fill = 0.58
shader_parameter/shoreline_opacity_boost = 0.38
shader_parameter/surface_highlight_night_floor = 0.72
shader_parameter/shoreline_minimum_reach = 0.22
shader_parameter/shoreline_crest_width = 0.08
shader_parameter/shoreline_trail_spacing = 0.13
shader_parameter/shoreline_trail_strength = 0.55
shader_parameter/distant_alpha_start = 260.0
shader_parameter/distant_alpha_end = 1400.0
shader_parameter/environment_tint = Color(1, 1, 1, 1)

View file

@ -1,6 +1,6 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
[gd_resource type="ShaderMaterial" format=3 uid="uid://d27er7sx5bcqw"]
[ext_resource type="Shader" path="res://world/materials/stylized_water.gdshader" id="1_water_shader"]
[ext_resource type="Shader" uid="uid://lvo7xyr3c1k3" path="res://world/materials/stylized_water.gdshader" id="1_water_shader"]
[resource]
render_priority = 0
@ -11,14 +11,45 @@ shader_parameter/deep_color = Color(0.109804, 0.345098, 0.435294, 1)
shader_parameter/tide_wash_color = Color(0.360784, 0.721569, 0.733333, 1)
shader_parameter/shallow_depth = 0.7
shader_parameter/deep_depth = 3.6
shader_parameter/shore_depth_width = 0.16
shader_parameter/shore_alpha = 0.44
shader_parameter/shallow_alpha = 0.6
shader_parameter/deep_alpha = 0.82
shader_parameter/shore_depth_width = 0.1
shader_parameter/shore_alpha = 0.68
shader_parameter/shallow_alpha = 0.78
shader_parameter/deep_alpha = 0.9
shader_parameter/tide_effect_enabled = false
shader_parameter/tide_speed = 1.42
shader_parameter/tide_distance = 0.3
shader_parameter/tide_wash_width = 0.38
shader_parameter/tide_wash_strength = 0.11
shader_parameter/tide_wash_alpha_shift = 0.045
shader_parameter/phase_noise_scale = 0.055
shader_parameter/phase_noise_strength = 0.68
shader_parameter/contour_corner_variation = 0.045
shader_parameter/contour_smoothing_pixels = 1.35
shader_parameter/contour_smoothing_strength = 0.88
shader_parameter/contour_depth_reject = 0.6
shader_parameter/shoreline_horizontal_reach = 0.32
shader_parameter/contact_fade_depth = 0.018
shader_parameter/open_water_drift_scale = 0.018
shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007)
shader_parameter/open_water_drift_strength = 0.045
shader_parameter/surface_mark_color = Color(0.58, 0.85, 0.86, 1)
shader_parameter/surface_mark_density = Vector2(1.5, 1.05)
shader_parameter/surface_mark_drift = Vector2(0.045, -0.032)
shader_parameter/surface_mark_strength = 0.12
shader_parameter/surface_mottle_scale = 0.11
shader_parameter/surface_mottle_speed = Vector2(-0.018, 0.013)
shader_parameter/surface_mottle_strength = 0.07
shader_parameter/shoreline_mark_density = Vector2(4.6, 3.2)
shader_parameter/shoreline_mark_drift = Vector2(0.085, -0.06)
shader_parameter/shoreline_mark_depth = 0.55
shader_parameter/shoreline_mark_strength = 0.95
shader_parameter/shoreline_base_fill = 0.58
shader_parameter/shoreline_opacity_boost = 0.38
shader_parameter/surface_highlight_night_floor = 0.72
shader_parameter/shoreline_minimum_reach = 0.22
shader_parameter/shoreline_crest_width = 0.08
shader_parameter/shoreline_trail_spacing = 0.13
shader_parameter/shoreline_trail_strength = 0.55
shader_parameter/distant_alpha_start = 260.0
shader_parameter/distant_alpha_end = 1400.0
shader_parameter/environment_tint = Color(1, 1, 1, 1)

121
world/rain_ambience.gd Normal file
View file

@ -0,0 +1,121 @@
class_name RainAmbience
extends AudioStreamPlayer
const RAIN_STREAM_PATH: String = (
"res://audio/ambience/rain_loop_ontario.ogg"
)
const SILENT_VOLUME_DB: float = -60.0
@export_range(-40.0, 0.0, 0.5) var rain_volume_db: float = -8.0
@export_range(0.0, 10.0, 0.1) var fade_seconds: float = 2.5
var _weather_service: WorldWeatherService
var _fade_tween: Tween
var _is_active: bool = false
func _ready() -> void:
bus = &"SFX"
var rain_stream := load(RAIN_STREAM_PATH) as AudioStreamOggVorbis
var runtime_stream := (
rain_stream.duplicate() as AudioStreamOggVorbis
if rain_stream != null
else null
)
if runtime_stream != null:
runtime_stream.loop = true
stream = runtime_stream
volume_db = SILENT_VOLUME_DB
func configure(weather_service: WorldWeatherService) -> void:
if (
_weather_service != null
and _weather_service.weather_changed.is_connected(
_on_weather_changed
)
):
_weather_service.weather_changed.disconnect(_on_weather_changed)
_weather_service = weather_service
if (
_weather_service != null
and not _weather_service.weather_changed.is_connected(
_on_weather_changed
)
):
_weather_service.weather_changed.connect(_on_weather_changed)
func set_active(active: bool) -> void:
if _is_active == active:
return
_is_active = active
if not active:
_silence_immediately()
return
_transition_to_rain(
_weather_service != null and _weather_service.is_raining()
)
func _on_weather_changed(
weather: WorldWeatherService.Weather,
_seconds_remaining: float,
) -> void:
_transition_to_rain(weather == WorldWeatherService.Weather.RAINY)
func _transition_to_rain(is_raining: bool) -> void:
if not _is_active:
return
_stop_fade()
if is_raining:
if stream == null:
return
if not playing:
volume_db = SILENT_VOLUME_DB
play()
_fade_volume_to(rain_volume_db, false)
return
if playing:
_fade_volume_to(SILENT_VOLUME_DB, true)
func _fade_volume_to(target_volume_db: float, stop_after_fade: bool) -> void:
if fade_seconds <= 0.0:
volume_db = target_volume_db
if stop_after_fade:
_finish_fade_out()
return
_fade_tween = create_tween()
_fade_tween.set_trans(Tween.TRANS_SINE)
_fade_tween.set_ease(Tween.EASE_IN_OUT)
_fade_tween.tween_property(
self,
"volume_db",
target_volume_db,
fade_seconds,
)
if stop_after_fade:
_fade_tween.tween_callback(_finish_fade_out)
func _finish_fade_out() -> void:
if _weather_service != null and _weather_service.is_raining():
return
stop()
volume_db = SILENT_VOLUME_DB
_fade_tween = null
func _silence_immediately() -> void:
_stop_fade()
stop()
volume_db = SILENT_VOLUME_DB
func _stop_fade() -> void:
if _fade_tween == null:
return
_fade_tween.kill()
_fade_tween = null

View file

@ -0,0 +1 @@
uid://dbpt2uqm82kmn

View file

@ -5,9 +5,6 @@ extends WorldRegion
const FishingShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
const GRASS_SURFACE_NAME: String = "grass_lite"
const SAND_SURFACE_NAME: String = "sand"
const SANDSTONE_SURFACE_NAME: String = "sandstone_heavy"
@export_group("Owned Nodes")
@export_node_path("MeshInstance3D")
@ -26,31 +23,12 @@ var player_spawn_path: NodePath = ^"PlayerSpawn"
var fishing_shop_path: NodePath = (
^"Interactables/FishingShopWorld/InteractionArea"
)
@export_node_path("Node3D")
var pelican_landmark_path: NodePath = (
^"Interactables/PelicanCoolerPerch"
)
@export_group("Grass Surface")
@export var apply_surface_overrides: bool = true
@export var grass_material: Material
@export_range(0, 16, 1) var grass_surface_index: int = 0
@export_group("Sand Surface")
@export var sand_material: Material
@export_range(0, 16, 1) var sand_surface_index: int = 4
@export_group("Sandstone Surface")
@export var sandstone_material: Material
@export_range(0, 16, 1) var sandstone_surface_index: int = 5
@export_group("Terrain Collision")
@export var rebuild_terrain_collision_on_ready: bool = false
func _ready() -> void:
if apply_surface_overrides:
_apply_grass_material()
_apply_sand_material()
_apply_sandstone_material()
_build_terrain_collision()
if rebuild_terrain_collision_on_ready or not has_terrain_collision():
_build_terrain_collision()
if Engine.is_editor_hint():
update_configuration_warnings()
@ -64,10 +42,6 @@ func get_fishing_shop() -> FishingShopInteractionType:
return get_node_or_null(fishing_shop_path) as FishingShopInteractionType
func get_pelican_landmark() -> Node3D:
return get_node_or_null(pelican_landmark_path) as Node3D
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
@ -79,81 +53,6 @@ func has_terrain_collision() -> bool:
return collision_shape != null and collision_shape.shape != null
func _apply_grass_material() -> void:
var visual_mesh: MeshInstance3D = (
get_node_or_null(visual_mesh_path) as MeshInstance3D
)
if visual_mesh == null or visual_mesh.mesh == null:
push_error("Starter island visual mesh is unavailable.")
return
if grass_material == null:
push_error("Starter island grass material is unavailable.")
return
if grass_surface_index >= visual_mesh.mesh.get_surface_count():
push_error("Starter island grass surface index is invalid.")
return
if (
visual_mesh.mesh.surface_get_name(grass_surface_index)
!= GRASS_SURFACE_NAME
):
push_error("Starter island grass surface name does not match.")
return
visual_mesh.set_surface_override_material(
grass_surface_index,
grass_material
)
func _apply_sand_material() -> void:
var visual_mesh: MeshInstance3D = (
get_node_or_null(visual_mesh_path) as MeshInstance3D
)
if visual_mesh == null or visual_mesh.mesh == null:
push_error("Starter island visual mesh is unavailable.")
return
if sand_material == null:
push_error("Starter island sand material is unavailable.")
return
if sand_surface_index >= visual_mesh.mesh.get_surface_count():
push_error("Starter island sand surface index is invalid.")
return
if (
visual_mesh.mesh.surface_get_name(sand_surface_index)
!= SAND_SURFACE_NAME
):
push_error("Starter island sand surface name does not match.")
return
visual_mesh.set_surface_override_material(
sand_surface_index,
sand_material
)
func _apply_sandstone_material() -> void:
var visual_mesh: MeshInstance3D = (
get_node_or_null(visual_mesh_path) as MeshInstance3D
)
if visual_mesh == null or visual_mesh.mesh == null:
push_error("Starter island visual mesh is unavailable.")
return
if sandstone_material == null:
push_error("Starter island sandstone material is unavailable.")
return
if sandstone_surface_index >= visual_mesh.mesh.get_surface_count():
push_error("Starter island sandstone surface index is invalid.")
return
if (
visual_mesh.mesh.surface_get_name(sandstone_surface_index)
!= SANDSTONE_SURFACE_NAME
):
push_error("Starter island sandstone surface name does not match.")
return
visual_mesh.set_surface_override_material(
sandstone_surface_index,
sandstone_material
)
func _build_terrain_collision() -> void:
var terrain_root: Node = get_node_or_null(terrain_visual_root_path)
if terrain_root == null:
@ -218,39 +117,6 @@ func _get_configuration_warnings() -> PackedStringArray:
warnings.append("Terrain/Visual must provide the island mesh.")
return warnings
if apply_surface_overrides:
if grass_surface_index >= visual_mesh.mesh.get_surface_count():
warnings.append("Grass surface index is outside the terrain mesh.")
elif (
visual_mesh.mesh.surface_get_name(grass_surface_index)
!= GRASS_SURFACE_NAME
):
warnings.append("Grass surface index must identify the grass surface.")
if grass_material == null:
warnings.append("Assign the starter-island grass material.")
if sand_surface_index >= visual_mesh.mesh.get_surface_count():
warnings.append("Sand surface index is outside the terrain mesh.")
elif (
visual_mesh.mesh.surface_get_name(sand_surface_index)
!= SAND_SURFACE_NAME
):
warnings.append("Sand surface index must identify the sand surface.")
if sand_material == null:
warnings.append("Assign the starter-island sand material.")
if sandstone_surface_index >= visual_mesh.mesh.get_surface_count():
warnings.append(
"Sandstone surface index is outside the terrain mesh."
)
elif (
visual_mesh.mesh.surface_get_name(sandstone_surface_index)
!= SANDSTONE_SURFACE_NAME
):
warnings.append(
"Sandstone surface index must identify the sandstone surface."
)
if sandstone_material == null:
warnings.append("Assign the starter-island sandstone material.")
if get_node_or_null(terrain_collision_shape_path) == null:
warnings.append("Terrain/Collision must provide a collision shape.")
if get_node_or_null(terrain_visual_root_path) == null:
@ -259,6 +125,4 @@ func _get_configuration_warnings() -> PackedStringArray:
warnings.append("PlayerSpawn marker is missing.")
if get_node_or_null(fishing_shop_path) == null:
warnings.append("Fishing Shop placement is missing.")
if get_node_or_null(pelican_landmark_path) == null:
warnings.append("Pelican placement is missing.")
return warnings

File diff suppressed because one or more lines are too long

View file

@ -57,10 +57,6 @@ func get_fishable_water_regions() -> Array[FishableWaterRegion]:
return waters
func get_pelican_convenience_landmark() -> Node3D:
return _starter_island.get_pelican_landmark()
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return _starter_island.get_saltwater_shoreline_mesh()

View file

@ -5,11 +5,11 @@ const WATER_PLANE_EPSILON := 0.001
const ENDPOINT_MERGE_TOLERANCE := 0.03
const LOOP_CLOSURE_TOLERANCE := 0.06
const MINIMUM_FRAGMENT_LENGTH := 2.0
const SIMPLIFICATION_TOLERANCE := 0.20
const SMOOTHING_ITERATIONS := 2
const RESAMPLE_SPACING := 0.22
const RIBBON_WIDTH := 0.85
const LAND_INSET := 0.10
const SIMPLIFICATION_TOLERANCE := 0.025
const SMOOTHING_ITERATIONS := 0
const RESAMPLE_SPACING := 0.10
const RIBBON_WIDTH := 1.35
const LAND_INSET := 0.12
const SURFACE_OFFSET := 0.018
@ -342,6 +342,11 @@ static func _build_mesh(
for path_data: Dictionary in paths:
var points: PackedVector2Array = path_data["points"]
var closed: bool = path_data["closed"]
var closed_water_side := (
_closed_path_water_side(points, water_is_inside)
if closed
else 0.0
)
var base_index := vertices.size()
var path_distance := 0.0
for index: int in points.size():
@ -352,16 +357,39 @@ static func _build_mesh(
if not closed:
previous = points[maxi(index - 1, 0)]
following = points[mini(index + 1, points.size() - 1)]
var tangent := previous.direction_to(following)
var water_normal := Vector2(-tangent.y, tangent.x)
var toward_reference := points[index].direction_to(water_reference)
if (
(water_is_inside and water_normal.dot(toward_reference) < 0.0)
or (not water_is_inside and water_normal.dot(toward_reference) > 0.0)
):
water_normal = -water_normal
var land_point := points[index] - water_normal * LAND_INSET
var water_point := points[index] + water_normal * (RIBBON_WIDTH - LAND_INSET)
var point := points[index]
var incoming := previous.direction_to(point)
var outgoing := point.direction_to(following)
if incoming.is_zero_approx():
incoming = outgoing
if outgoing.is_zero_approx():
outgoing = incoming
var incoming_normal := Vector2(-incoming.y, incoming.x)
var outgoing_normal := Vector2(-outgoing.y, outgoing.x)
var water_normal := incoming_normal + outgoing_normal
if water_normal.is_zero_approx():
water_normal = outgoing_normal
water_normal = water_normal.normalized()
if closed:
water_normal *= closed_water_side
else:
var toward_reference := point.direction_to(water_reference)
if (
(water_is_inside and water_normal.dot(toward_reference) < 0.0)
or (not water_is_inside and water_normal.dot(toward_reference) > 0.0)
):
water_normal = -water_normal
var join_scale := minf(
1.0 / maxf(absf(water_normal.dot(outgoing_normal)), 0.55),
1.65,
)
var land_point := (
point - water_normal * LAND_INSET * join_scale
)
var water_point := (
point
+ water_normal * (RIBBON_WIDTH - LAND_INSET) * join_scale
)
vertices.append(Vector3(land_point.x, water_height + SURFACE_OFFSET, land_point.y))
vertices.append(Vector3(water_point.x, water_height + SURFACE_OFFSET, water_point.y))
normals.append(Vector3.UP)
@ -388,6 +416,19 @@ static func _build_mesh(
return mesh
static func _closed_path_water_side(
points: PackedVector2Array,
water_is_inside: bool,
) -> float:
var signed_area_twice := 0.0
for index: int in points.size():
signed_area_twice += points[index].cross(
points[(index + 1) % points.size()]
)
var interior_side := 1.0 if signed_area_twice >= 0.0 else -1.0
return interior_side if water_is_inside else -interior_side
static func _point_key(point: Vector2) -> Vector2i:
return Vector2i(
roundi(point.x / ENDPOINT_MERGE_TOLERANCE),

View file

@ -0,0 +1,68 @@
class_name WaterSurfaceMotion
extends MeshInstance3D
const DEFAULT_AMPLITUDE: float = 0.055
const DEFAULT_CYCLE_SECONDS: float = 7.0
const DEFAULT_SECONDARY_SWELL: float = 0.18
const DEFAULT_PHASE_OFFSET: float = 0.0
@export_range(0.0, 0.2, 0.005, "suffix:m") var amplitude: float = (
DEFAULT_AMPLITUDE
)
@export_range(1.0, 30.0, 0.1, "suffix:s") var cycle_seconds: float = (
DEFAULT_CYCLE_SECONDS
)
@export_range(0.0, 0.5, 0.01) var secondary_swell: float = (
DEFAULT_SECONDARY_SWELL
)
@export_range(0.0, TAU, 0.01, "radians") var phase_offset: float = 0.0
var _base_height: float
func _ready() -> void:
_base_height = position.y
set_process(amplitude > 0.0)
func _process(_delta: float) -> void:
position.y = _base_height + calculate_height_offset(
_current_time_seconds(),
amplitude,
cycle_seconds,
secondary_swell,
phase_offset,
)
static func get_default_height_offset() -> float:
return calculate_height_offset(
_current_time_seconds(),
DEFAULT_AMPLITUDE,
DEFAULT_CYCLE_SECONDS,
DEFAULT_SECONDARY_SWELL,
DEFAULT_PHASE_OFFSET,
)
static func calculate_height_offset(
time_seconds: float,
motion_amplitude: float,
motion_cycle_seconds: float,
motion_secondary_swell: float,
motion_phase_offset: float,
) -> float:
var safe_cycle := maxf(motion_cycle_seconds, 0.001)
var phase := (
fposmod(time_seconds, safe_cycle) / safe_cycle * TAU
+ motion_phase_offset
)
var swell := (
sin(phase)
+ sin(phase * 2.0 + 1.1) * motion_secondary_swell
) / (1.0 + motion_secondary_swell)
return swell * motion_amplitude
static func _current_time_seconds() -> float:
return float(Time.get_ticks_msec()) * 0.001

View file

@ -0,0 +1 @@
uid://drdjm443o35tc

View file

@ -0,0 +1,127 @@
class_name WorldCharacterDisplay
extends Node3D
const CharacterCustomizationCatalogType = preload(
"res://progression/character_customization_catalog.gd"
)
const PlayerVisualPresenterType = preload(
"res://player/player_visual_presenter.gd"
)
@export_enum("round", "pointy") var species_id: String = "round"
@export_enum(
"white",
"gray",
"charcoal",
"brown",
"orange",
"yellow",
"green",
"teal",
) var fur_color_id: String = "white"
@export_enum(
"none",
"pointy_long",
"pointy_short",
"pointy_wide",
) var ears_id: String = "none"
@export var eyes_id: String = "simple_shine"
@export var nose_id: String = "dog_round"
@export var mouth_id: String = "three"
@export_category("Head Look")
@export var head_look_enabled: bool = false
@export_range(0.5, 10.0, 0.1, "suffix:m") var head_look_distance: float = 4.0
@export_range(0.5, 3.0, 0.05, "suffix:m") var look_target_height: float = 1.25
@export_range(0.0, 80.0, 1.0, "degrees") var head_yaw_limit: float = 50.0
@export_range(0.0, 45.0, 1.0, "degrees") var head_pitch_limit: float = 22.0
@export_range(0.05, 2.0, 0.05, "suffix:s") var head_blend_seconds: float = 0.3
var _head_look: LookAtModifier3D
var _skeleton: Skeleton3D
var _look_target: Node3D
var _look_target_proxy: Node3D
var _head_look_requested: bool = false
func _ready() -> void:
var appearance := CharacterCustomizationCatalogType.default_snapshot()
appearance["species"] = species_id
appearance["fur_pattern"] = fur_color_id
appearance["ears"] = ears_id
appearance["eyes"] = eyes_id
appearance["nose"] = nose_id
appearance["mouth"] = mouth_id
PlayerVisualPresenterType.apply_appearance(self, appearance)
_prepare_head_look()
func _process(delta: float) -> void:
if _head_look == null or _look_target_proxy == null:
return
var target_is_valid := (
_look_target != null and is_instance_valid(_look_target)
)
if target_is_valid:
_look_target_proxy.global_position = (
_look_target.global_position + Vector3.UP * look_target_height
)
var target_influence := (
1.0 if _head_look_requested and target_is_valid else 0.0
)
_head_look.influence = move_toward(
_head_look.influence,
target_influence,
delta / maxf(head_blend_seconds, 0.001),
)
func set_head_look_target(target: Node3D) -> void:
_look_target = target
func set_head_look_active(active: bool) -> void:
_head_look_requested = active
func get_head_anchor_position() -> Vector3:
if _skeleton == null:
return global_position + Vector3.UP * 1.8
var head_index := _skeleton.find_bone("head")
if head_index < 0:
return global_position + Vector3.UP * 1.8
var head_pose := _skeleton.get_bone_global_pose(head_index)
return (
(_skeleton.global_transform * head_pose).origin
+ Vector3.UP * 0.85
)
func _prepare_head_look() -> void:
if not head_look_enabled:
set_process(false)
return
_skeleton = find_child("Skeleton3D", true, false) as Skeleton3D
if _skeleton == null or _skeleton.find_bone("head") < 0:
push_warning("World character head-look requires a head bone.")
set_process(false)
return
_look_target_proxy = Node3D.new()
_look_target_proxy.name = "HeadLookTarget"
add_child(_look_target_proxy)
_head_look = LookAtModifier3D.new()
_head_look.name = "HeadLookModifier"
_head_look.bone_name = "head"
_head_look.use_secondary_rotation = true
_head_look.use_angle_limitation = true
_head_look.symmetry_limitation = true
_head_look.primary_limit_angle = deg_to_rad(head_yaw_limit)
_head_look.secondary_limit_angle = deg_to_rad(head_pitch_limit)
_head_look.primary_damp_threshold = 0.65
_head_look.secondary_damp_threshold = 0.65
_head_look.duration = head_blend_seconds
_head_look.influence = 0.0
_head_look.set("origin_from", 1)
_head_look.set("origin_bone_name", "head")
_skeleton.add_child(_head_look)
_head_look.target_node = _head_look.get_path_to(_look_target_proxy)
set_process(true)

View file

@ -0,0 +1 @@
uid://dqsehnkidygg7