diff --git a/compute/texture/README.md b/compute/texture/README.md new file mode 100644 index 00000000..013d2201 --- /dev/null +++ b/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 +and are licensed under CC0 1.0 Universal. diff --git a/compute/texture/assets/polyhaven/industrial_sunset_puresky_2k.hdr b/compute/texture/assets/polyhaven/industrial_sunset_puresky_2k.hdr new file mode 100644 index 00000000..b01b3aa6 Binary files /dev/null and b/compute/texture/assets/polyhaven/industrial_sunset_puresky_2k.hdr differ diff --git a/compute/texture/assets/polyhaven/industrial_sunset_puresky_2k.hdr.import b/compute/texture/assets/polyhaven/industrial_sunset_puresky_2k.hdr.import new file mode 100644 index 00000000..b657fac8 --- /dev/null +++ b/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 diff --git a/compute/texture/icon.svg b/compute/texture/icon.svg new file mode 100644 index 00000000..b370ceb7 --- /dev/null +++ b/compute/texture/icon.svg @@ -0,0 +1 @@ + diff --git a/compute/texture/icon.svg.import b/compute/texture/icon.svg.import new file mode 100644 index 00000000..84741ca2 --- /dev/null +++ b/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 diff --git a/compute/texture/main.gd b/compute/texture/main.gd new file mode 100644 index 00000000..62a18057 --- /dev/null +++ b/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 diff --git a/compute/texture/main.tscn b/compute/texture/main.tscn new file mode 100644 index 00000000..b6e3ee67 --- /dev/null +++ b/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"] diff --git a/compute/texture/project.godot b/compute/texture/project.godot new file mode 100644 index 00000000..38e4d9ad --- /dev/null +++ b/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 diff --git a/compute/texture/screenshots/.gdignore b/compute/texture/screenshots/.gdignore new file mode 100644 index 00000000..e69de29b diff --git a/compute/texture/screenshots/compute_texture.webp b/compute/texture/screenshots/compute_texture.webp new file mode 100644 index 00000000..dfa57369 Binary files /dev/null and b/compute/texture/screenshots/compute_texture.webp differ diff --git a/compute/texture/water_plane/water_compute.glsl b/compute/texture/water_plane/water_compute.glsl new file mode 100644 index 00000000..fbed31c7 --- /dev/null +++ b/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); +} diff --git a/compute/texture/water_plane/water_compute.glsl.import b/compute/texture/water_plane/water_compute.glsl.import new file mode 100644 index 00000000..8ef651fc --- /dev/null +++ b/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] + diff --git a/compute/texture/water_plane/water_plane.gd b/compute/texture/water_plane/water_plane.gd new file mode 100644 index 00000000..cbab26e8 --- /dev/null +++ b/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) diff --git a/compute/texture/water_plane/water_plane.tscn b/compute/texture/water_plane/water_plane.tscn new file mode 100644 index 00000000..b520d4aa --- /dev/null +++ b/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") diff --git a/compute/texture/water_plane/water_shader.gdshader b/compute/texture/water_plane/water_shader.gdshader new file mode 100644 index 00000000..481fb6cc --- /dev/null +++ b/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; +}