123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- @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)
|