diff --git a/compute/texture/README.md b/compute/texture/README.md new file mode 100644 index 00000000000..013d220131b --- /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 00000000000..b01b3aa6ec8 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 00000000000..b657fac8a74 --- /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 00000000000..b370ceb7274 --- /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 00000000000..84741ca2600 --- /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 00000000000..633dc352acc --- /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 00000000000..b6e3ee67145 --- /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 00000000000..38e4d9add24 --- /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 00000000000..e69de29bb2d diff --git a/compute/texture/screenshots/compute_texture.webp b/compute/texture/screenshots/compute_texture.webp new file mode 100644 index 00000000000..dfa57369949 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 00000000000..284520ac228 --- /dev/null +++ b/compute/texture/water_plane/water_compute.glsl @@ -0,0 +1,47 @@ +#[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); + + 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 00000000000..8ef651fc6f5 --- /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 00000000000..42a88e80bb7 --- /dev/null +++ b/compute/texture/water_plane/water_plane.gd @@ -0,0 +1,234 @@ +# @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 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 + + # 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(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 + +var current_rd : RID +var current_set : RID +var previous_rd : RID +var previous_set : RID +var next_rd : RID +var next_set : 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(p_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 = p_texture_size.x + tf.height = p_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 + + # We need three textures for this effect + current_rd = rd.texture_create(tf, RDTextureView.new(), []) + previous_rd = rd.texture_create(tf, RDTextureView.new(), []) + next_rd = rd.texture_create(tf, RDTextureView.new(), []) + + # Make sure our textures are cleared + rd.texture_clear(current_rd, Color(0, 0, 0, 0), 0, 1, 0, 1) + rd.texture_clear(previous_rd, Color(0, 0, 0, 0), 0, 1, 0, 1) + rd.texture_clear(next_rd, Color(0, 0, 0, 0), 0, 1, 0, 1) + + # Now create our uniform set so we can use these textures in our shader + current_set = _create_uniform_set(current_rd) + previous_set = _create_uniform_set(previous_rd) + next_set = _create_uniform_set(next_rd) + +func _render_process(p_add_wave_point, p_texture_size, p_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(p_add_wave_point.x) + push_constant.push_back(p_add_wave_point.y) + push_constant.push_back(p_add_wave_point.z) + push_constant.push_back(p_add_wave_point.w) + + push_constant.push_back(p_texture_size.x) + push_constant.push_back(p_texture_size.y) + push_constant.push_back(p_damp) + push_constant.push_back(0.0) + + # 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, p_texture_size.x / 8, p_texture_size.y / 8, 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) + + # Update our texture to show our new result + if texture: + texture.texture_rd_rid = next_rd + + # Cycle buffers, we're just moving RIDs around, + # saves expensive texture copies :) + var swap_rd : RID = previous_rd + var swap_set : RID = previous_set + previous_rd = current_rd + previous_set = current_set + current_rd = next_rd + current_set = next_set + next_rd = swap_rd + next_set = swap_set + +func _free_compute_resources(): + # Note that our sets and pipeline are cleaned up automatically as they are dependencies :P + if current_rd: + rd.free_rid(current_rd) + if previous_rd: + rd.free_rid(previous_rd) + if next_rd: + rd.free_rid(next_rd) + 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 00000000000..d264f2dd11a --- /dev/null +++ b/compute/texture/water_plane/water_plane.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=7 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"] + +[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(0, 0.615686, 0.92549, 1) +shader_parameter/metalic = 0.8 +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) + +[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") diff --git a/compute/texture/water_plane/water_shader.gdshader b/compute/texture/water_plane/water_shader.gdshader new file mode 100644 index 00000000000..0153630bb7d --- /dev/null +++ b/compute/texture/water_plane/water_shader.gdshader @@ -0,0 +1,36 @@ +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=UV; + + 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; + + TANGENT = normalize(vec3(1.0, 0.0, f2 - f1)); + BINORMAL = normalize(vec3(0.0, 1.0, f3 - f1)); + NORMAL = normalize(cross(TANGENT, BINORMAL)); + + ALBEDO = albedo.rgb; + //ALBEDO = vec3(f1); + METALLIC = metalic; + ROUGHNESS = roughness; + SPECULAR = 0.5; +}