From d8c60fbf412dbeef9b4d83b080adc44ef4fa0418 Mon Sep 17 00:00:00 2001 From: karl Date: Mon, 28 Jun 2021 20:00:10 +0200 Subject: [PATCH] Add water in a slightly hacky way --- World.gd | 20 ++++++ World.tscn | 9 ++- .../PersistentShaderTexture.tscn | 21 ++++++ addons/interactive_water/Water.gd | 71 +++++++++++++++++++ addons/interactive_water/Water.shader | 18 +++++ addons/interactive_water/Water.tscn | 38 ++++++++++ addons/interactive_water/WaterHeights.gd | 30 ++++++++ addons/interactive_water/WaterUpdate.shader | 63 ++++++++++++++++ .../WaterUpdateMaterial.tres | 10 +++ 9 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 World.gd create mode 100644 addons/interactive_water/PersistentShaderTexture.tscn create mode 100644 addons/interactive_water/Water.gd create mode 100644 addons/interactive_water/Water.shader create mode 100644 addons/interactive_water/Water.tscn create mode 100644 addons/interactive_water/WaterHeights.gd create mode 100644 addons/interactive_water/WaterUpdate.shader create mode 100644 addons/interactive_water/WaterUpdateMaterial.tres diff --git a/World.gd b/World.gd new file mode 100644 index 0000000..3c2d668 --- /dev/null +++ b/World.gd @@ -0,0 +1,20 @@ +extends Spatial + + +const ray_length = 2000 + + +func _physics_process(delta): + if Input.is_mouse_button_pressed(1): + # If the left mouse button was pressed, cast a ray towards the mouse position + var mouse_pos = get_viewport().get_mouse_position() + + var camera = $LerpedFollow/LerpedCamera + var from = camera.project_ray_origin(mouse_pos) + var to = from + camera.project_ray_normal(mouse_pos) * ray_length + + var result = get_world().direct_space_state.intersect_ray(from, to) + + if not result.empty(): + # If there was a collision, set the depth of the water at that position + $Water.set_depth_at_position(result.position, 0.0) diff --git a/World.tscn b/World.tscn index 78075d6..6567a60 100644 --- a/World.tscn +++ b/World.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=23 format=2] +[gd_scene load_steps=25 format=2] [ext_resource path="res://Player.tscn" type="PackedScene" id=1] [ext_resource path="res://Planets.gd" type="Script" id=2] @@ -11,6 +11,8 @@ [ext_resource path="res://Resources/gas_planet_flowmap.png" type="Texture" id=9] [ext_resource path="res://Resources/gas_planet_base.png" type="Texture" id=10] [ext_resource path="res://LerpedFollow.gd" type="Script" id=11] +[ext_resource path="res://addons/interactive_water/Water.tscn" type="PackedScene" id=12] +[ext_resource path="res://World.gd" type="Script" id=13] [sub_resource type="CubeMesh" id=1] size = Vector3( 10, 0.5, 10 ) @@ -101,6 +103,7 @@ glow_strength = 1.1 glow_bloom = 0.1 [node name="World" type="Spatial"] +script = ExtResource( 13 ) [node name="MovingPlatformPivot" type="Position3D" parent="."] transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, -125, 37, 60 ) @@ -204,3 +207,7 @@ target_paths = [ NodePath("../EnemyPos1"), NodePath("../EnemyPos2") ] [node name="Player" parent="." instance=ExtResource( 1 )] transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, 0 ) + +[node name="Water" parent="." instance=ExtResource( 12 )] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 2, -66, -50.5288 ) +plane_size = 200.0 diff --git a/addons/interactive_water/PersistentShaderTexture.tscn b/addons/interactive_water/PersistentShaderTexture.tscn new file mode 100644 index 0000000..0915231 --- /dev/null +++ b/addons/interactive_water/PersistentShaderTexture.tscn @@ -0,0 +1,21 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/interactive_water/WaterHeights.gd" type="Script" id=1] + +[node name="PersistentShaderTexture" type="Node"] +script = ExtResource( 1 ) + +[node name="Viewport" type="Viewport" parent="."] +size = Vector2( 1, 1 ) +transparent_bg = true +handle_input_locally = false +hdr = false +disable_3d = true +usage = 0 +render_target_v_flip = true +render_target_update_mode = 3 + +[node name="Texture" type="ColorRect" parent="Viewport"] +margin_right = 128.0 +margin_bottom = 128.0 +rect_min_size = Vector2( 128, 128 ) diff --git a/addons/interactive_water/Water.gd b/addons/interactive_water/Water.gd new file mode 100644 index 0000000..baa94cb --- /dev/null +++ b/addons/interactive_water/Water.gd @@ -0,0 +1,71 @@ +extends Spatial + + +# Saves every frame as a PNG in the project directory. Use for debugging (with caution) +export var output_debug_textures := false +export var first_output_frame := 0 + +export var plane_size := 2.0 + +var _frame_number := 0 +var _positions_to_set = [] + + +func set_depth_at_position(pos: Vector3, depth: float): + # Transform the global position into 2D points on the water surface with values between 0 and 1 + pos -= translation + + var position_2d = Vector2(pos.x, pos.z) + + position_2d += Vector2(plane_size, plane_size) / 2.0 + position_2d /= plane_size + + if position_2d.x < 1.0 and position_2d.y < 1.0 \ + and position_2d.x >= 0.0 and position_2d.y >= 0.0: + _positions_to_set.append([position_2d, depth]) + + +func _ready(): + $WaterMesh.mesh.size = Vector2(plane_size, plane_size) + + # Apply the scale and positioning to the collider + $WaterMesh/StaticBody.scale.x = plane_size / 2.0 + $WaterMesh/StaticBody.scale.y = plane_size / 2.0 + $WaterMesh/StaticBody.scale.z = plane_size / 2.0 + + $WaterMesh/StaticBody.translation.y = 0.6 * 40.0 + + +func _physics_process(delta): + # Get result of previous frame + var result = $WaterHeights.get_texture() + + # Set it as the data of the water mesh + $WaterMesh.material_override.set_shader_param("water_heights", result) + + # Calculate a new frame. First, get the data of the last frame and modify it accordingly + var image_data = result.get_data() + + # Set outstanding pixels + for position_and_depth in _positions_to_set: + var pos = position_and_depth[0] + var depth = position_and_depth[1] + + image_data.lock() + image_data.set_pixel(floor(pos.x * 512), floor(pos.y * 512), Color(depth, 0.0, 0.0, 0.0)) + image_data.unlock() + + _positions_to_set.clear() + + # Create an ImageTexture for this new frame + var previous_frame = ImageTexture.new() + previous_frame.create_from_image(image_data) + + # Set the previous texture in the shader so that a new texture will be available next frame + $WaterHeights.set_previous_texture(previous_frame) + + # Debug output if needed + if output_debug_textures and _frame_number > first_output_frame: + image_data.save_png("res://debugframes/frame%s.png" % [_frame_number]) + + _frame_number += 1 diff --git a/addons/interactive_water/Water.shader b/addons/interactive_water/Water.shader new file mode 100644 index 0000000..8113b8a --- /dev/null +++ b/addons/interactive_water/Water.shader @@ -0,0 +1,18 @@ +shader_type spatial; + +uniform sampler2D water_heights; + + +float read_height(sampler2D tex, vec2 uv) { + return texture(tex, uv).r + texture(tex, uv).g / 255.0; +} + +void fragment() { + ALBEDO = vec3(0.5, 0.7, 1.0) * (read_height(water_heights, UV) - 0.6) * 3.0; +} + +void vertex() { + float vertex_distance = length(VERTEX) * 0.06; + + VERTEX += NORMAL * (read_height(water_heights, UV) * 40.0 - pow(2, vertex_distance)); +} \ No newline at end of file diff --git a/addons/interactive_water/Water.tscn b/addons/interactive_water/Water.tscn new file mode 100644 index 0000000..e79615d --- /dev/null +++ b/addons/interactive_water/Water.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=9 format=2] + +[ext_resource path="res://addons/interactive_water/Water.shader" type="Shader" id=1] +[ext_resource path="res://addons/interactive_water/Water.gd" type="Script" id=3] +[ext_resource path="res://addons/interactive_water/WaterUpdateMaterial.tres" type="Material" id=4] +[ext_resource path="res://addons/interactive_water/PersistentShaderTexture.tscn" type="PackedScene" id=5] + +[sub_resource type="ViewportTexture" id=1] +viewport_path = NodePath("WaterHeightViewport") + +[sub_resource type="ShaderMaterial" id=2] +resource_local_to_scene = true +shader = ExtResource( 1 ) +shader_param/water_heights = SubResource( 1 ) + +[sub_resource type="PlaneMesh" id=3] +subdivide_width = 512 +subdivide_depth = 512 + +[sub_resource type="PlaneShape" id=4] + +[node name="Water" type="Spatial"] +script = ExtResource( 3 ) + +[node name="WaterMesh" type="MeshInstance" parent="."] +material_override = SubResource( 2 ) +mesh = SubResource( 3 ) +material/0 = null + +[node name="StaticBody" type="StaticBody" parent="WaterMesh"] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0 ) + +[node name="CollisionShape" type="CollisionShape" parent="WaterMesh/StaticBody"] +shape = SubResource( 4 ) + +[node name="WaterHeights" parent="." instance=ExtResource( 5 )] +size = Vector2( 512, 512 ) +shader_material = ExtResource( 4 ) diff --git a/addons/interactive_water/WaterHeights.gd b/addons/interactive_water/WaterHeights.gd new file mode 100644 index 0000000..95d893f --- /dev/null +++ b/addons/interactive_water/WaterHeights.gd @@ -0,0 +1,30 @@ +extends Node + +# A texture which is continuously (statefully) updated by a shader. +# +# Typical usage: Retrieve the current result with `get_texture` and, at the end of the frame, +# re-insert that texture (updated if needed) with `set_previous_texture`. +# +# Note that the given shader needs to accept a `previous_frame` sampler2D. This represents the +# result of the last frame which is used for generating the new result. + + +export var size := Vector2(64, 64) +export var shader_material: ShaderMaterial + + +func _ready(): + $Viewport/Texture.rect_min_size = size + $Viewport/Texture.rect_size = size + $Viewport.size = size + + assert(shader_material) + $Viewport/Texture.material = shader_material + + +func get_texture(): + return $Viewport.get_texture() + + +func set_previous_texture(texture): + $Viewport/Texture.material.set_shader_param("previous_frame", texture) diff --git a/addons/interactive_water/WaterUpdate.shader b/addons/interactive_water/WaterUpdate.shader new file mode 100644 index 0000000..450609d --- /dev/null +++ b/addons/interactive_water/WaterUpdate.shader @@ -0,0 +1,63 @@ +shader_type canvas_item; +render_mode unshaded, blend_disabled; + +uniform sampler2D previous_frame; + +uniform float water_height = 0.6; + +uniform float height_damping = 0.13; +uniform float velocity_damping = 0.8; +uniform float spread = 0.94; + +// Height and Velocity are encoded in two components each, so RG is height and BA is velocity. +// This is needed to get a workable accuracy. +float read_height(sampler2D tex, vec2 uv) { + return texture(tex, uv).r + texture(tex, uv).g / 255.0; +} +float read_velocity(sampler2D tex, vec2 uv) { + return texture(tex, uv).b + texture(tex, uv).a / 255.0; +} +float get_encoded_remainder(float num) { + return fract(num * 255.0); +} + +void fragment() { + // Read values here + float height_here = read_height(previous_frame, UV); + float velocity_here = read_velocity(previous_frame, UV); + + // Apply force towards the base height + // This follows from the damped harmonic oscillator equation F = -kx-bv + float force = -height_damping * (height_here - water_height) - velocity_here * velocity_damping; + float acceleration_here = force; + + // In addition to each individual height behaving like a spring, neighbouring heights are + // "connected by springs" as well: + + // Read more samples + float uv_mod = 1.0 / float(textureSize(previous_frame, 0).x); + + float height_up = read_height(previous_frame, UV + vec2(0.0, uv_mod)); + float height_down = read_height(previous_frame, UV + vec2(0.0, -uv_mod)); + float height_left = read_height(previous_frame, UV + vec2(-uv_mod, 0.0)); + float height_right = read_height(previous_frame, UV + vec2(uv_mod, 0.0)); + + // Calculate differences + float up_delta = spread * (height_up - height_here); + float down_delta = spread * (height_down - height_here); + float left_delta = spread * (height_left - height_here); + float right_delta = spread * (height_right - height_here); + + // Use the biggest delta to apply to this height + float sum_delta = max(max(left_delta, right_delta), max(up_delta, down_delta)); + + // Apply velocity and height + velocity_here += sum_delta + acceleration_here; + height_here += velocity_here; + + // Write to the texture + COLOR = vec4( + height_here, get_encoded_remainder(height_here), + velocity_here, get_encoded_remainder(velocity_here) + ); +} \ No newline at end of file diff --git a/addons/interactive_water/WaterUpdateMaterial.tres b/addons/interactive_water/WaterUpdateMaterial.tres new file mode 100644 index 0000000..8a4e86c --- /dev/null +++ b/addons/interactive_water/WaterUpdateMaterial.tres @@ -0,0 +1,10 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=2] + +[ext_resource path="res://addons/interactive_water/WaterUpdate.shader" type="Shader" id=1] + +[resource] +shader = ExtResource( 1 ) +shader_param/water_height = 0.6 +shader_param/height_damping = 0.13 +shader_param/velocity_damping = 0.8 +shader_param/spread = 0.94