|
@@ -0,0 +1,237 @@
|
|
|
+@tool
|
|
|
+extends Area3D
|
|
|
+
|
|
|
+############################################################################
|
|
|
+# Water ripple effect shader - Bastiaan Olij
|
|
|
+#
|
|
|
+# This is an example of how to implement a more complex compute shader
|
|
|
+# in Godot and making use of the new Custom Texture RD API added to
|
|
|
+# the RenderingServer.
|
|
|
+#
|
|
|
+# If thread model is set to Multi-Threaded the code related to compute will
|
|
|
+# run on the render thread. This is needed as we want to add our logic to
|
|
|
+# the normal rendering pipeline for this thread.
|
|
|
+#
|
|
|
+# The effect itself is an implementation of the classic ripple effect
|
|
|
+# that has been around since the 90ies but in a compute shader.
|
|
|
+# If someone knows if the original author ever published a paper I could
|
|
|
+# quote, please let me know :)
|
|
|
+
|
|
|
+@export var rain_size : float = 3.0
|
|
|
+@export var mouse_size : float = 5.0
|
|
|
+@export var texture_size : Vector2i = Vector2i(512, 512)
|
|
|
+@export_range(1.0, 10.0, 0.1) var damp : float = 1.0
|
|
|
+
|
|
|
+var t = 0.0
|
|
|
+var max_t = 0.1
|
|
|
+
|
|
|
+var texture : Texture2DRD
|
|
|
+var next_texture : int = 0
|
|
|
+
|
|
|
+var add_wave_point : Vector4
|
|
|
+var mouse_pos : Vector2
|
|
|
+var mouse_pressed : bool = false
|
|
|
+
|
|
|
+# Called when the node enters the scene tree for the first time.
|
|
|
+func _ready():
|
|
|
+ # In case we're running stuff on the rendering thread
|
|
|
+ # we need to do our initialisation on that thread.
|
|
|
+ RenderingServer.call_on_render_thread(_initialize_compute_code.bind(texture_size))
|
|
|
+
|
|
|
+ # Get our texture from our material so we set our RID.
|
|
|
+ var material : ShaderMaterial = $MeshInstance3D.material_override
|
|
|
+ if material:
|
|
|
+ material.set_shader_parameter("effect_texture_size", texture_size)
|
|
|
+
|
|
|
+ # Get our texture object.
|
|
|
+ texture = material.get_shader_parameter("effect_texture")
|
|
|
+
|
|
|
+
|
|
|
+func _exit_tree():
|
|
|
+ # Make sure we clean up!
|
|
|
+ if texture:
|
|
|
+ texture.texture_rd_rid = RID()
|
|
|
+
|
|
|
+ RenderingServer.call_on_render_thread(_free_compute_resources)
|
|
|
+
|
|
|
+
|
|
|
+func _unhandled_input(event):
|
|
|
+ # If tool enabled, we don't want to handle our input in the editor.
|
|
|
+ if Engine.is_editor_hint():
|
|
|
+ return
|
|
|
+
|
|
|
+ if event is InputEventMouseMotion or event is InputEventMouseButton:
|
|
|
+ mouse_pos = event.global_position
|
|
|
+
|
|
|
+ if event is InputEventMouseButton and event.button_index == MouseButton.MOUSE_BUTTON_LEFT:
|
|
|
+ mouse_pressed = event.pressed
|
|
|
+
|
|
|
+
|
|
|
+func _check_mouse_pos():
|
|
|
+ # This is a mouse event, do a raycast.
|
|
|
+ var camera = get_viewport().get_camera_3d()
|
|
|
+
|
|
|
+ var parameters = PhysicsRayQueryParameters3D.new()
|
|
|
+ parameters.from = camera.project_ray_origin(mouse_pos)
|
|
|
+ parameters.to = parameters.from + camera.project_ray_normal(mouse_pos) * 100.0
|
|
|
+ parameters.collision_mask = 1
|
|
|
+ parameters.collide_with_bodies = false
|
|
|
+ parameters.collide_with_areas = true
|
|
|
+
|
|
|
+ var result = get_world_3d().direct_space_state.intersect_ray(parameters)
|
|
|
+ if result.size() > 0:
|
|
|
+ # Transform our intersection point.
|
|
|
+ var pos = global_transform.affine_inverse() * result.position
|
|
|
+ add_wave_point.x = clamp(pos.x / 5.0, -0.5, 0.5) * texture_size.x + 0.5 * texture_size.x
|
|
|
+ add_wave_point.y = clamp(pos.z / 5.0, -0.5, 0.5) * texture_size.y + 0.5 * texture_size.y
|
|
|
+ add_wave_point.w = 1.0 # We have w left over so we use it to indicate mouse is over our water plane.
|
|
|
+ else:
|
|
|
+ add_wave_point.x = 0.0
|
|
|
+ add_wave_point.y = 0.0
|
|
|
+ add_wave_point.w = 0.0
|
|
|
+
|
|
|
+
|
|
|
+# Called every frame. 'delta' is the elapsed time since the previous frame.
|
|
|
+func _process(delta):
|
|
|
+ # If tool is enabled, ignore mouse input.
|
|
|
+ if Engine.is_editor_hint():
|
|
|
+ add_wave_point.w = 0.0
|
|
|
+ else:
|
|
|
+ # Check where our mouse intersects our area, can change if things move.
|
|
|
+ _check_mouse_pos()
|
|
|
+
|
|
|
+ # If we're not using the mouse, animate water drops, we (ab)used our W for this.
|
|
|
+ if add_wave_point.w == 0.0:
|
|
|
+ t += delta
|
|
|
+ if t > max_t:
|
|
|
+ t = 0
|
|
|
+ add_wave_point.x = randi_range(0, texture_size.x)
|
|
|
+ add_wave_point.y = randi_range(0, texture_size.y)
|
|
|
+ add_wave_point.z = rain_size
|
|
|
+ else:
|
|
|
+ add_wave_point.z = 0.0
|
|
|
+ else:
|
|
|
+ add_wave_point.z = mouse_size if mouse_pressed else 0.0
|
|
|
+
|
|
|
+ # Increase our next texture index.
|
|
|
+ next_texture = (next_texture + 1) % 3
|
|
|
+
|
|
|
+ # Update our texture to show our next result (we are about to create).
|
|
|
+ # Note that `_initialize_compute_code` may not have run yet so the first
|
|
|
+ # frame this my be an empty RID.
|
|
|
+ if texture:
|
|
|
+ texture.texture_rd_rid = texture_rds[next_texture]
|
|
|
+
|
|
|
+ # While our render_process may run on the render thread it will run before our texture
|
|
|
+ # is used and thus our next_rd will be populated with our next result.
|
|
|
+ # It's probably overkill to sent texture_size and damp as parameters as these are static
|
|
|
+ # but we sent add_wave_point as it may be modified while process runs in parallel.
|
|
|
+ RenderingServer.call_on_render_thread(_render_process.bind(next_texture, add_wave_point, texture_size, damp))
|
|
|
+
|
|
|
+###############################################################################
|
|
|
+# Everything after this point is designed to run on our rendering thread.
|
|
|
+
|
|
|
+var rd : RenderingDevice
|
|
|
+
|
|
|
+var shader : RID
|
|
|
+var pipeline : RID
|
|
|
+
|
|
|
+# We use 3 textures:
|
|
|
+# - One to render into
|
|
|
+# - One that contains the last frame rendered
|
|
|
+# - One for the frame before that
|
|
|
+var texture_rds : Array = [ RID(), RID(), RID() ]
|
|
|
+var texture_sets : Array = [ RID(), RID(), RID() ]
|
|
|
+
|
|
|
+func _create_uniform_set(texture_rd : RID) -> RID:
|
|
|
+ var uniform := RDUniform.new()
|
|
|
+ uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
|
|
|
+ uniform.binding = 0
|
|
|
+ uniform.add_id(texture_rd)
|
|
|
+ # Even though we're using 3 sets, they are identical, so we're kinda cheating.
|
|
|
+ return rd.uniform_set_create([uniform], shader, 0)
|
|
|
+
|
|
|
+
|
|
|
+func _initialize_compute_code(init_with_texture_size):
|
|
|
+ # As this becomes part of our normal frame rendering,
|
|
|
+ # we use our main rendering device here.
|
|
|
+ rd = RenderingServer.get_rendering_device()
|
|
|
+
|
|
|
+ # Create our shader.
|
|
|
+ var shader_file = load("res://water_plane/water_compute.glsl")
|
|
|
+ var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
|
|
|
+ shader = rd.shader_create_from_spirv(shader_spirv)
|
|
|
+ pipeline = rd.compute_pipeline_create(shader)
|
|
|
+
|
|
|
+ # Create our textures to manage our wave.
|
|
|
+ var tf : RDTextureFormat = RDTextureFormat.new()
|
|
|
+ tf.format = RenderingDevice.DATA_FORMAT_R32_SFLOAT
|
|
|
+ tf.texture_type = RenderingDevice.TEXTURE_TYPE_2D
|
|
|
+ tf.width = init_with_texture_size.x
|
|
|
+ tf.height = init_with_texture_size.y
|
|
|
+ tf.depth = 1
|
|
|
+ tf.array_layers = 1
|
|
|
+ tf.mipmaps = 1
|
|
|
+ tf.usage_bits = RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT + RenderingDevice.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT + RenderingDevice.TEXTURE_USAGE_STORAGE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_UPDATE_BIT + RenderingDevice.TEXTURE_USAGE_CAN_COPY_TO_BIT
|
|
|
+
|
|
|
+ for i in range(3):
|
|
|
+ # Create our texture.
|
|
|
+ texture_rds[i] = rd.texture_create(tf, RDTextureView.new(), [])
|
|
|
+
|
|
|
+ # Make sure our textures are cleared.
|
|
|
+ rd.texture_clear(texture_rds[i], Color(0, 0, 0, 0), 0, 1, 0, 1)
|
|
|
+
|
|
|
+ # Now create our uniform set so we can use these textures in our shader.
|
|
|
+ texture_sets[i] = _create_uniform_set(texture_rds[i])
|
|
|
+
|
|
|
+
|
|
|
+func _render_process(with_next_texture, wave_point, tex_size, damp):
|
|
|
+ # We don't have structures (yet) so we need to build our push constant
|
|
|
+ # "the hard way"...
|
|
|
+ var push_constant : PackedFloat32Array = PackedFloat32Array()
|
|
|
+ push_constant.push_back(wave_point.x)
|
|
|
+ push_constant.push_back(wave_point.y)
|
|
|
+ push_constant.push_back(wave_point.z)
|
|
|
+ push_constant.push_back(wave_point.w)
|
|
|
+
|
|
|
+ push_constant.push_back(tex_size.x)
|
|
|
+ push_constant.push_back(tex_size.y)
|
|
|
+ push_constant.push_back(damp)
|
|
|
+ push_constant.push_back(0.0)
|
|
|
+
|
|
|
+ # Calculate our dispatch group size.
|
|
|
+ # We do `n - 1 / 8 + 1` in case our texture size is not nicely
|
|
|
+ # divisible by 8.
|
|
|
+ # In combination with a discard check in the shader this ensures
|
|
|
+ # we cover the entire texture.
|
|
|
+ var x_groups = (tex_size.x - 1) / 8 + 1
|
|
|
+ var y_groups = (tex_size.y - 1) / 8 + 1
|
|
|
+
|
|
|
+ var next_set = texture_sets[with_next_texture]
|
|
|
+ var current_set = texture_sets[(with_next_texture - 1) % 3]
|
|
|
+ var previous_set = texture_sets[(with_next_texture - 2) % 3]
|
|
|
+
|
|
|
+ # Run our compute shader.
|
|
|
+ var compute_list := rd.compute_list_begin()
|
|
|
+ rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
|
|
|
+ rd.compute_list_bind_uniform_set(compute_list, current_set, 0)
|
|
|
+ rd.compute_list_bind_uniform_set(compute_list, previous_set, 1)
|
|
|
+ rd.compute_list_bind_uniform_set(compute_list, next_set, 2)
|
|
|
+ rd.compute_list_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4)
|
|
|
+ rd.compute_list_dispatch(compute_list, x_groups, y_groups, 1)
|
|
|
+ rd.compute_list_end()
|
|
|
+
|
|
|
+ # We don't need to sync up here, Godots default barriers will do the trick.
|
|
|
+ # If you want the output of a compute shader to be used as input of
|
|
|
+ # another computer shader you'll need to add a barrier:
|
|
|
+ #rd.barrier(RenderingDevice.BARRIER_MASK_COMPUTE)
|
|
|
+
|
|
|
+
|
|
|
+func _free_compute_resources():
|
|
|
+ # Note that our sets and pipeline are cleaned up automatically as they are dependencies :P
|
|
|
+ for i in range(3):
|
|
|
+ if texture_rds[i]:
|
|
|
+ rd.free_rid(texture_rds[i])
|
|
|
+
|
|
|
+ if shader:
|
|
|
+ rd.free_rid(shader)
|