diff --git a/Game/scenes/ship_camera_rig.tscn b/Game/scenes/ship_camera_rig.tscn index 2ded671f..4f4d401d 100644 --- a/Game/scenes/ship_camera_rig.tscn +++ b/Game/scenes/ship_camera_rig.tscn @@ -1,9 +1,35 @@ -[gd_scene load_steps=2 format=3] +[gd_scene load_steps=5 format=3] [ext_resource type="Script" path="res://scripts/ship_camera.gd" id="1_rig"] +[ext_resource type="Shader" path="res://shaders/post_process.gdshader" id="2_post"] + +[sub_resource type="CameraAttributesPractical" id="CameraAttributes_gameplay"] +dof_blur_far_enabled = true +dof_blur_far_distance = 24.0 +dof_blur_far_transition = 14.0 +dof_blur_near_enabled = true +dof_blur_near_distance = 1.25 +dof_blur_near_transition = 0.75 +dof_blur_amount = 0.08 + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_post"] +shader = ExtResource("2_post") [node name="ShipCameraRig" type="Node3D"] script = ExtResource("1_rig") [node name="Camera3D" type="Camera3D" parent="."] current = true +attributes = SubResource("CameraAttributes_gameplay") + +[node name="PostProcess" type="CanvasLayer" parent="."] +layer = 0 + +[node name="PostFX" type="ColorRect" parent="PostProcess"] +material = SubResource("ShaderMaterial_post") +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 diff --git a/Game/scripts/ship_camera.gd b/Game/scripts/ship_camera.gd index e762615c..09495f15 100644 --- a/Game/scripts/ship_camera.gd +++ b/Game/scripts/ship_camera.gd @@ -36,6 +36,7 @@ var target: Ship: var ball_cam_enabled := true @onready var camera: Camera3D = $Camera3D +@onready var post_material: ShaderMaterial = $PostProcess/PostFX.material as ShaderMaterial var _ball: Node3D # Smoothed horizontal direction from ball to ship; the ball-cam orbits along @@ -43,6 +44,7 @@ var _ball: Node3D # raw position (which whips when ball and ship are close together). var _orbit_dir := Vector3.BACK var _turbo_blend := 0.0 +var _speed_blend := 0.0 var _effective_distance := 8.0 var _shake_strength := 0.0 var _shake_noise := FastNoiseLite.new() @@ -175,11 +177,17 @@ func _update_speed_feel(delta: float) -> void: var feel_t := 1.0 - exp(-feel_smoothing * delta) var turbo_target := 1.0 if target.is_turbo_active() else 0.0 _turbo_blend = lerpf(_turbo_blend, turbo_target, feel_t) - var target_fov := base_fov + target.get_speed_ratio() * speed_fov_add + _turbo_blend * turbo_fov_kick + _speed_blend = lerpf(_speed_blend, target.get_speed_ratio(), feel_t) + var target_fov := base_fov + _speed_blend * speed_fov_add + _turbo_blend * turbo_fov_kick camera.fov = lerpf(camera.fov, target_fov, feel_t) _effective_distance = lerpf( _effective_distance, camera_distance + _turbo_blend * turbo_distance_kick, feel_t ) + post_material.set_shader_parameter( + "motion_blur_amount", _speed_blend * _speed_blend * 0.006 + _turbo_blend * 0.006 + ) + post_material.set_shader_parameter("chromatic_aberration", _turbo_blend * 0.007) + post_material.set_shader_parameter("vignette_strength", 0.22 + _turbo_blend * 0.16) func _on_target_ball_contact(intensity: float, _world_position: Vector3) -> void: @@ -224,7 +232,17 @@ func begin_goal_cut(goal_position: Vector3) -> void: camera.global_position = _goal_cut_position camera.fov = 58.0 camera.look_at(_goal_cut_look_at, Vector3.UP) + # A hard, stationary cinematic camera must not inherit the scoring frame's + # ship-speed smear or turbo chroma. Keep only a deliberate cinematic edge. + post_material.set_shader_parameter("motion_blur_amount", 0.0) + post_material.set_shader_parameter("chromatic_aberration", 0.0) + post_material.set_shader_parameter("vignette_strength", 0.3) func end_goal_cut() -> void: _goal_cut_active = false + _speed_blend = 0.0 + _turbo_blend = 0.0 + post_material.set_shader_parameter("motion_blur_amount", 0.0) + post_material.set_shader_parameter("chromatic_aberration", 0.0) + post_material.set_shader_parameter("vignette_strength", 0.22) diff --git a/Game/shaders/post_process.gdshader b/Game/shaders/post_process.gdshader new file mode 100644 index 00000000..8d19ea6c --- /dev/null +++ b/Game/shaders/post_process.gdshader @@ -0,0 +1,43 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap; +uniform float motion_blur_amount : hint_range(0.0, 0.03) = 0.0; +uniform float chromatic_aberration : hint_range(0.0, 0.02) = 0.0; +uniform float vignette_strength : hint_range(0.0, 1.0) = 0.22; + + +vec4 motion_sample(vec2 uv) { + // A restrained centre-directed velocity smear. The camera script scales it + // with ship speed/turbo; mipmapped screen reads soften the outer samples. + if (motion_blur_amount < 0.0001) { + return textureLod(screen_texture, uv, 0.0); + } + vec2 step_uv = (uv - vec2(0.5)) * motion_blur_amount; + vec4 color = textureLod(screen_texture, uv, 0.0) * 0.36; + color += textureLod(screen_texture, uv - step_uv * 0.35, 0.5) * 0.25; + color += textureLod(screen_texture, uv - step_uv * 0.7, 1.0) * 0.21; + color += textureLod(screen_texture, uv - step_uv, 1.5) * 0.18; + return color; +} + + +void fragment() { + vec2 radial = UV - vec2(0.5); + vec2 chroma = radial * chromatic_aberration; + vec4 center = motion_sample(UV); + vec3 color = center.rgb; + // The branch is driven by one uniform for the whole draw, so non-turbo + // frames avoid eight redundant screen taps without causing warp divergence. + if (chromatic_aberration > 0.0001) { + color.r = motion_sample(UV + chroma).r; + color.b = motion_sample(UV - chroma).b; + } + + // Aspect-correct vignette, always subtle and tightened slightly on turbo. + vec2 aspect_radial = radial; + aspect_radial.x *= SCREEN_PIXEL_SIZE.y / SCREEN_PIXEL_SIZE.x; + float vignette = smoothstep(0.38, 0.78, length(aspect_radial)); + color *= 1.0 - vignette * vignette_strength; + COLOR = vec4(color, 1.0); +} diff --git a/Game/shaders/post_process.gdshader.uid b/Game/shaders/post_process.gdshader.uid new file mode 100644 index 00000000..20824501 --- /dev/null +++ b/Game/shaders/post_process.gdshader.uid @@ -0,0 +1 @@ +uid://cx1lpjodunjhx diff --git a/TODO.md b/TODO.md index 554cc161..46a1dc21 100644 --- a/TODO.md +++ b/TODO.md @@ -23,7 +23,7 @@ The largest gap between this and a AAA-feeling product is presentation, not code - [x] Local lighting / dynamic GI — four local pitch lights plus SDFGI provide bounce and contact lighting for the runtime-generated arena shell; the shader's `hull_fill` stand-in is retired. (The shell is generated in `_ready`, so it cannot participate in an editor LightmapGI bake.) - [x] Dress `arena_03` — its asteroid field now has mining stations, debris clusters and a distant planet. - [x] Custom font + a real `Theme` resource for the HUD. Packaged Orbitron typography, shared label styling and themed procedural-instrument text replace `ThemeDB.fallback_font`. -- [ ] Post-processing beyond glow: DoF, motion blur, vignette, chromatic aberration on turbo. +- [x] Post-processing beyond glow: camera DoF plus speed-scaled motion blur, a cinematic vignette and turbo-driven chromatic aberration. ## Multiplayer (long term)