Add compute texture demo (#938)

This commit is contained in:
Bastiaan Olij
2023-11-17 22:55:09 +11:00
committed by GitHub
parent 40ce32c5da
commit 5eed925b7b
15 changed files with 620 additions and 0 deletions

38
compute/texture/README.md Normal file
View File

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

View File

@@ -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
compute/texture/icon.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 950 B

View File

@@ -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
compute/texture/main.gd Normal file
View File

@@ -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
compute/texture/main.tscn Normal file
View File

@@ -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"]

View File

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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}