瀏覽代碼

Add compute texture demo (#938)

Bastiaan Olij 1 年之前
父節點
當前提交
5eed925b7b

+ 38 - 0
compute/texture/README.md

@@ -0,0 +1,38 @@
+# Compute texture
+
+This demo shows how to use compute shaders to populate a texture that is used as an input for a material shader.
+
+When the mouse cursor isn't hovering above the plane random "drops" of water are added that drive the ripple effect.
+When the mouse cursor is above the plane you can "draw" on the plane to drive the ripple effect.
+
+Language: GDScript
+
+Renderer: Forward Plus
+
+> Note: this demo requires Godot 4.2 or later
+
+## Screenshots
+
+![Screenshot](screenshots/compute_texture.webp)
+
+## Technical description
+
+The texture populated by the compute shader contains height data that is used in the material shader to create a rain drops/water ripple effect. It's a well known technique that has been around since the mid 90ies, adapted to a compute shader.
+
+Three textures are created directly on the rendering device:
+- One texture is used to write the heightmap to and used in the material shader.
+- One texture is read from and contains the previous frames data.
+- One texture is read from and contains data from the frame before that.
+
+Instead of copying data from texture to texture to create this history, we simply cycle the RIDs.
+
+Note that in this demo we are using the main rendering device to ensure we execute our compute shader before our normal rendering.
+
+To use the texture with the latest height data we use a `Texture2DRD` resource, this is a special texture resource node that is able to use a texture directly created on the rendering device and expose it to material shaders.
+
+The material shader uses a standard gradient approach by sampling the height map and calculating tangent and bi-normal vectors and adjust the normal accordingly.
+
+## Licenses
+
+Files in the `polyhaven/` folder are downloaded from <https://polyhaven.com/a/industrial_sunset_puresky>
+and are licensed under CC0 1.0 Universal.

二進制
compute/texture/assets/polyhaven/industrial_sunset_puresky_2k.hdr


+ 34 - 0
compute/texture/assets/polyhaven/industrial_sunset_puresky_2k.hdr.import

@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d051ugdf65it1"
+path="res://.godot/imported/industrial_sunset_puresky_2k.hdr-2273dddf6859dd4da64c4a85b4589512.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/polyhaven/industrial_sunset_puresky_2k.hdr"
+dest_files=["res://.godot/imported/industrial_sunset_puresky_2k.hdr-2273dddf6859dd4da64c4a85b4589512.ctex"]
+
+[params]
+
+compress/mode=3
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0

+ 1 - 0
compute/texture/icon.svg

@@ -0,0 +1 @@
+<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

+ 37 - 0
compute/texture/icon.svg.import

@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bonkdv3wikslq"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false

+ 27 - 0
compute/texture/main.gd

@@ -0,0 +1,27 @@
+extends Node3D
+
+# Note, the code here just adds some control to our effects.
+# Check res://water_plane/water_plane.gd for the real implementation.
+
+var y = 0.0
+
+@onready var water_plane = $WaterPlane
+
+func _ready():
+	$Container/RainSize/HSlider.value = $WaterPlane.rain_size
+	$Container/MouseSize/HSlider.value = $WaterPlane.mouse_size
+
+
+# Called every frame. 'delta' is the elapsed time since the previous frame.
+func _process(delta):
+	if $Container/Rotate.button_pressed:
+		y += delta
+		water_plane.basis = Basis(Vector3.UP, y)
+
+
+func _on_rain_size_changed(value):
+	$WaterPlane.rain_size = value
+
+
+func _on_mouse_size_changed(value):
+	$WaterPlane.mouse_size = value

+ 76 - 0
compute/texture/main.tscn

@@ -0,0 +1,76 @@
+[gd_scene load_steps=7 format=3 uid="uid://c7nfvt1chslyh"]
+
+[ext_resource type="Script" path="res://main.gd" id="1_yvrvl"]
+[ext_resource type="Texture2D" uid="uid://d051ugdf65it1" path="res://assets/polyhaven/industrial_sunset_puresky_2k.hdr" id="2_g2q6b"]
+[ext_resource type="PackedScene" uid="uid://b2a5bjsxw63wr" path="res://water_plane/water_plane.tscn" id="2_k1nfp"]
+
+[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_obhcg"]
+panorama = ExtResource("2_g2q6b")
+
+[sub_resource type="Sky" id="Sky_s1sgk"]
+sky_material = SubResource("PanoramaSkyMaterial_obhcg")
+
+[sub_resource type="Environment" id="Environment_5dv8s"]
+background_mode = 2
+sky = SubResource("Sky_s1sgk")
+tonemap_mode = 2
+tonemap_white = 4.56
+
+[node name="Main" type="Node3D"]
+script = ExtResource("1_yvrvl")
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(0.5, -0.75, 0.433013, 2.78059e-08, 0.5, 0.866026, -0.866025, -0.433013, 0.25, 0, 1, 0)
+shadow_enabled = true
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_5dv8s")
+
+[node name="WaterPlane" parent="." instance=ExtResource("2_k1nfp")]
+
+[node name="Camera3D" type="Camera3D" parent="."]
+transform = Transform3D(0.900266, -0.142464, 0.41137, -0.113954, 0.834877, 0.538512, -0.420162, -0.531681, 0.735377, 1.55343, 1.1434, 2.431)
+
+[node name="Container" type="VBoxContainer" parent="."]
+offset_right = 40.0
+offset_bottom = 40.0
+
+[node name="Rotate" type="CheckBox" parent="Container"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "Rotate"
+
+[node name="RainSize" type="HBoxContainer" parent="Container"]
+layout_mode = 2
+
+[node name="HSlider" type="HSlider" parent="Container/RainSize"]
+custom_minimum_size = Vector2(250, 0)
+layout_mode = 2
+min_value = 1.0
+max_value = 10.0
+step = 0.1
+value = 1.0
+
+[node name="Label" type="Label" parent="Container/RainSize"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "Rain size"
+
+[node name="MouseSize" type="HBoxContainer" parent="Container"]
+layout_mode = 2
+
+[node name="HSlider" type="HSlider" parent="Container/MouseSize"]
+custom_minimum_size = Vector2(250, 0)
+layout_mode = 2
+min_value = 1.0
+max_value = 10.0
+step = 0.1
+value = 1.1
+
+[node name="Label" type="Label" parent="Container/MouseSize"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0, 0, 0, 1)
+text = "Mouse size"
+
+[connection signal="value_changed" from="Container/RainSize/HSlider" to="." method="_on_rain_size_changed"]
+[connection signal="value_changed" from="Container/MouseSize/HSlider" to="." method="_on_mouse_size_changed"]

+ 20 - 0
compute/texture/project.godot

@@ -0,0 +1,20 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="TestCustomTextures"
+run/main_scene="res://main.tscn"
+config/features=PackedStringArray("4.2", "Forward Plus")
+config/icon="res://icon.svg"
+
+[rendering]
+
+driver/threads/thread_model=2

+ 0 - 0
compute/texture/screenshots/.gdignore


二進制
compute/texture/screenshots/compute_texture.webp


+ 52 - 0
compute/texture/water_plane/water_compute.glsl

@@ -0,0 +1,52 @@
+#[compute]
+#version 450
+
+// Invocations in the (x, y, z) dimension.
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+// Our textures.
+layout(r32f, set = 0, binding = 0) uniform restrict readonly image2D current_image;
+layout(r32f, set = 1, binding = 0) uniform restrict readonly image2D previous_image;
+layout(r32f, set = 2, binding = 0) uniform restrict writeonly image2D output_image;
+
+// Our push PushConstant.
+layout(push_constant, std430) uniform Params {
+	vec4 add_wave_point;
+	vec2 texture_size;
+	float damp;
+	float res2;
+} params;
+
+// The code we want to execute in each invocation.
+void main() {
+	ivec2 tl = ivec2(0, 0);
+	ivec2 size = ivec2(params.texture_size.x - 1, params.texture_size.y - 1);
+
+	ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
+
+	// Just in case the texture size is not divisable by 8.
+	if ((uv.x > size.x) || (uv.y > size.y)) {
+		return;
+	}
+
+	float current_v = imageLoad(current_image, uv).r;
+	float up_v = imageLoad(current_image, clamp(uv - ivec2(0, 1), tl, size)).r;
+	float down_v = imageLoad(current_image, clamp(uv + ivec2(0, 1), tl, size)).r;
+	float left_v = imageLoad(current_image, clamp(uv - ivec2(1, 0), tl, size)).r;
+	float right_v = imageLoad(current_image, clamp(uv + ivec2(1, 0), tl, size)).r;
+	float previous_v = imageLoad(previous_image, uv).r;
+
+	float new_v = 2.0 * current_v - previous_v + 0.25 * (up_v + down_v + left_v + right_v - 4.0 * current_v);
+	new_v = new_v - (params.damp * new_v * 0.001);
+
+	if (params.add_wave_point.z > 0.0 && uv.x == floor(params.add_wave_point.x) && uv.y == floor(params.add_wave_point.y)) {
+		new_v = params.add_wave_point.z;
+	}
+
+	if (new_v < 0.0) {
+		new_v = 0.0;
+	}
+	vec4 result = vec4(new_v, new_v, new_v, 1.0);
+
+	imageStore(output_image, uv, result);
+}

+ 14 - 0
compute/texture/water_plane/water_compute.glsl.import

@@ -0,0 +1,14 @@
+[remap]
+
+importer="glsl"
+type="RDShaderFile"
+uid="uid://b6pdquh2n2jvn"
+path="res://.godot/imported/water_compute.glsl-c7fe8f11197ba28412c4cdf6f7a9a21b.res"
+
+[deps]
+
+source_file="res://water_plane/water_compute.glsl"
+dest_files=["res://.godot/imported/water_compute.glsl-c7fe8f11197ba28412c4cdf6f7a9a21b.res"]
+
+[params]
+

+ 237 - 0
compute/texture/water_plane/water_plane.gd

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

+ 51 - 0
compute/texture/water_plane/water_plane.tscn

@@ -0,0 +1,51 @@
+[gd_scene load_steps=11 format=3 uid="uid://b2a5bjsxw63wr"]
+
+[ext_resource type="Script" path="res://water_plane/water_plane.gd" id="1_ltm8k"]
+[ext_resource type="Shader" path="res://water_plane/water_shader.gdshader" id="1_rujqj"]
+[ext_resource type="Texture2D" uid="uid://d051ugdf65it1" path="res://assets/polyhaven/industrial_sunset_puresky_2k.hdr" id="3_fdqn0"]
+
+[sub_resource type="Texture2DRD" id="Texture2DRD_gbeoi"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_qy6ln"]
+resource_local_to_scene = true
+render_priority = 0
+shader = ExtResource("1_rujqj")
+shader_parameter/albedo = Color(5.19812e-06, 0.748295, 0.942472, 1)
+shader_parameter/metalic = 1.0
+shader_parameter/roughness = 0.0
+shader_parameter/effect_texture_size = null
+shader_parameter/effect_texture = SubResource("Texture2DRD_gbeoi")
+
+[sub_resource type="PlaneMesh" id="PlaneMesh_wl5mm"]
+size = Vector2(5, 5)
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_gvcbg"]
+size = Vector3(5, 0.01, 5)
+
+[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_xm1lt"]
+panorama = ExtResource("3_fdqn0")
+
+[sub_resource type="Sky" id="Sky_ng08w"]
+sky_material = SubResource("PanoramaSkyMaterial_xm1lt")
+
+[sub_resource type="Environment" id="Environment_iw7ig"]
+background_mode = 2
+sky = SubResource("Sky_ng08w")
+
+[node name="WaterPlane" type="Area3D"]
+script = ExtResource("1_ltm8k")
+damp = 2.0
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
+material_override = SubResource("ShaderMaterial_qy6ln")
+mesh = SubResource("PlaneMesh_wl5mm")
+skeleton = NodePath("../..")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = SubResource("BoxShape3D_gvcbg")
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+transform = Transform3D(0.983217, 5.59419e-08, 0.182442, -0.178298, 0.211922, 0.960885, -0.0386633, -0.977287, 0.208365, 0, 1.12002, 0)
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+environment = SubResource("Environment_iw7ig")

+ 33 - 0
compute/texture/water_plane/water_shader.gdshader

@@ -0,0 +1,33 @@
+shader_type spatial;
+render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
+
+uniform vec3 albedo : source_color;
+uniform float metalic : hint_range(0.0, 1.0, 0.1) = 0.8;
+uniform float roughness : hint_range(0.0, 1.0, 0.1) = 0.2;
+uniform sampler2D effect_texture;
+uniform vec2 effect_texture_size;
+
+varying vec2 uv_tangent;
+varying vec2 uv_binormal;
+
+void vertex() {
+	vec2 pixel_size = vec2(1.0, 1.0) / effect_texture_size;
+
+	uv_tangent = UV + vec2(pixel_size.x, 0.0);
+	uv_binormal = UV + vec2(0.0, pixel_size.y);
+}
+
+void fragment() {
+	float f1 = texture(effect_texture, UV).r;
+	float f2 = texture(effect_texture, uv_tangent).r;
+	float f3 = texture(effect_texture, uv_binormal).r;
+
+	vec3 tangent = normalize(vec3(1.0, 0.0, f2 - f1));
+	vec3 binormal = normalize(vec3(0.0, 1.0, f3 - f1));
+	NORMAL_MAP = normalize(cross(binormal, tangent)) * 0.5 + 0.5;
+
+	ALBEDO = albedo.rgb;
+	METALLIC = metalic;
+	ROUGHNESS = roughness;
+	SPECULAR = 0.5;
+}