Kamui! using visual shader in Godot!

Hello everyone, welcome back.

In today's post we will be replicating Obito Uchiha’s so broken mangekyo ability, Kamui! For this I will be using Godot 4.1 and a visual shader addon Shader-lib.




Effect breakdown!

Let’s break down the effect, first we will create a quad in front of our object. Then we will project what’s behind onto the quad then we will add some texture for dramatic effect then we will distort our quads UV. Finally we will scale our quad and object simultaneously.

Creating kamui quad

Ok now let’s create a quad in our scene so add child, MeshInstance3DSelect new quad mesh, I will call our node kamuiNow let’s create a visual shader, I will call it kamui_shaderLet’s also create a new shader material, I will call it kamui and I like to keep the extension .material.

Let’s assign our shader to our material. In the Flags, make sure to check UnshadedWe don’t want it to interact with lights. and assign our material to our quad.

In our visual shader, first we want to project what is behind, on the quad itself. So create a Texture2D node, select Screen. It will give us the screen texture. Then take it’s color and feed it into the Albedo.

Now we want to distort the UVs so let’s create a Twirl node. It is the node from the addon, which will give us this nice effect. Now if I just set the Strength to 0 and feed the output in our screen texture’s UVs, notice that the quad no longer properly projects what is behind.

The reason is Twirl node uses the mesh UVs by default. So we will simply grab the screen_uv and feed it into Twirl node’s UVs.

Now we will feed the Strength from the inspector so, let’s create a FloatParameter, call it TwrilStrength and give default value of like 200.

Now we want to animate the effect so let’s create another FloatParameter, call it TwirlProgress, set Hint to Range from 0 to 1 and give default value of 0. Then we will multiply TwirlStrength with TwirlProgress and feed it into the Strength.

We need to calculate the center via code so for now let’s create a Vector2ParameterCall it TwirlCenter, give default value of 0.5, 0.5 and feed it into center.



Now our quad will have hard edges when we will distort our UVs. So let’s create a UVFunc node with Function mode Panning. It will give us UVs which we can offset using this offset, set it to -0.5, -0.5. Then take its output and feed it into VectorLen node. It will give us the length of a vector.

Then take its output and feed it into One minus node. One minus as the name suggest subtract whatever value we feed in from 1. In our case it will invert the white color to black and black to white.

Now let’s create a SmoothStep node, set the edge0 to 0.5, edge1 to 0.8, and feed our One minus output in xSmoothStep node will return 0 if the value of x is less than edge0, return 1 if the value of x is greater than edge1 and for values that lies between edge0 and edge1 it will return the result of Hermite interpolation which will be between 0 and 1. Then we will take its output and feed it into alpha.

And we no longer have hard edges. Pretty cool!



Now let’s add the texture on top, for that let’s create UVPolarCoords node. Polar coordinate are circular coordinate where X goes outward and Y goes around. Let’s split its output using Vector2Decompose node.

We want Y values so take its output and feed it into TriangleWave node. It is another nice node from the addon, which will return tringular waves with values between -1 and 1. In UVPolarCoords set the Repeat value to 10, it will give us this nice triangular waves with 10 teeth.

Well actually it is 20, because other 10 are in negative numbers so we can’t see it in preview. We don’t want them so let’s clamp this using Clamp node. We will clamp the values between 0 and 0.7 because I want the texture to be a bit transparent.

Then take its output and feed it into Pow node. Power node will darken values which are less than 1 as we increase the power. Set the power to 10.

Now let’s shear our waves a bit and for that create RadialShear node. Another nice node from the addon which will provide the warping effect on UVs. Take its output and feed it into UVPolarCoords node’s UVs.

Let’s also rotate our waves and for that let’s create Rotate node. Another node from the addon which will rotate the UVs. We will rotate UVs over time so let’s grab TimeAnd feed it into RotationThen take Rotate node’s output and feed it into RadialShear node’s UVs.

Finally let’s distort our waves a bit so create a SimpleNoise node. This node is also from the addon which will give us nice value noise. Alternatively we can use Texture2D with noise texture as well. We will mix Simple noise with our polar coordinates.

So let’s create a Mix node for Vector2Mix node basically interpolates input a and b based on weightFeed Polar coordinates into a, noise into b. Now it doesn’t matter what we feed in x for weight but for y feed 0.5. Take our Mix nodes output and feed it into VectorDecompose node.

Now we will add this distorted waves on top of our screen texture with Add node for Vector3Then take our addition and feed it into AlbedoThat’s it for our Fragment processor, It will look like this.



Now let’s head over to Vertex processor.

Create GetBillboardMatrix node. keep Billboard Type to Enable then check Keep scale. Take its output and feed it into Model View Matrix.



We have made our kamui into bill board so it doesn’t mater from which angle we look at it, it will always face the camera and that’s it for our shader.

Defining the behaviour via GDScripts

Now we want to define the behaviour for our effect, I want to create a quad when I click on any object and then make the object disappear along with the effect and for that we need to create scripts.

First let’s create a script called Kamui which extends from Node3DThen let’s assign it to our Kamui node. Shocker!

Kamui.gd

  1. class_name Kamui extends Node3D
  2.  
  3. var material: ShaderMaterial
  4. var kamui_caller: Kamuiable
  5. var effect_scale: Vector3
  6. var kamui_caller_scale: Vector3
  7.  
  8. @export var scale_in_duration: float = 2.0
  9. @export var effect_duration: float = 3.0
  10.  
  11. func _ready() -> void:
  12.     material = self.material_override
  13.  
  14. func scale_in() -> void:
  15.     var scale_tween: Tween = create_tween()
  16.     scale_tween.tween_method(update_scale, 0.0, 1.0, scale_in_duration).set_trans(Tween.TRANS_QUART)
  17.     scale_tween.finished.connect(animate_progress)
  18.  
  19. func update_scale(new_scale_value: float) -> void:
  20.     var new_scale: Vector3 = effect_scale * new_scale_value
  21.     scale = new_scale
  22.  
  23. func animate_progress() -> void:
  24.     var progress_tween: Tween = create_tween()
  25.     progress_tween.tween_method(update_progress, 0.0, 1.0, effect_duration).set_trans(Tween.TRANS_LINEAR)
  26.     progress_tween.finished.connect(on_animation_done)
  27.  
  28. func update_progress(value: float) -> void:
  29.     material.set_shader_parameter("TwirlProgress", value)
  30.  
  31.     var scale_value: float = 1.0 - value
  32.     update_scale(scale_value)
  33.  
  34.     var new_kamuiable_scale: Vector3 = kamui_caller_scale * scale_value
  35.     kamui_caller.scale = new_kamuiable_scale
  36.  
  37. func on_animation_done() -> void:
  38.     queue_free()
  39.     kamui_caller.queue_free()
  40.  
  41. func set_center(center: Vector2) -> void:
  42.     material.set_shader_parameter("TwirlCenter", center)
  43.  
  44. func set_kamui_caller(effect_caller: Kamuiable) -> void:
  45.     kamui_caller = effect_caller
  46.     kamui_caller_scale = kamui_caller.scale

Then let’s save our Kamui node as an instance scene or let’s say as a prefab. So Right click > Save branch as scene, and I will save it as Kamui.tscn.

Now let’s say we want to Kamui Sphere when we click on it. So let’s create an Area3D node. I will call it KamuiableSphereThen add CollisionShape3D as a child and give new BoxShape3D. This is so that we can click on object.

Then for visuals let’s add MeshInstance3D as Child of our KamuiableSphere. Set the Mesh as Sphere. Let’s create another script, I will call it Kamuiable which extends from Node3D.

Assign it to our KamuiableSphere.

Kamuiable.gd

  1. class_name Kamuiable extends Node3D
  2.  
  3. var is_pointer_on_node: bool = false
  4.  
  5. @export var effect_distance: float = 0.5
  6. @export var effect_scale: Vector3 = Vector3.ONE
  7. @export var camera: Camera3D
  8. @export var kamui_effect: PackedScene
  9.  
  10. func _process(_delta: float) -> void:
  11.     if is_pointer_on_node && Input.is_action_just_pressed("mouse_left"):
  12.         create_kamui()
  13.  
  14. func create_kamui() -> void:
  15.     var camera_direction: Vector3 = (camera.global_position - global_position).normalized()
  16.     var new_position: Vector3 = (effect_distance * camera_direction) + global_position
  17.  
  18.     var kamui: Node3D = kamui_effect.instantiate()
  19.     get_tree().root.get_node("Main").add_child(kamui)
  20.  
  21.     kamui.set_kamui_caller(self)
  22.     kamui.effect_scale = effect_scale
  23.     kamui.set_center(calculate_center())
  24.     kamui.global_position = new_position
  25.     kamui.scale_in()
  26.  
  27. func calculate_center() -> Vector2:
  28.     var screen_pos: Vector2 = camera.unproject_position(global_position)
  29.     var screen_size: Vector2 = get_viewport().size
  30.     var center: Vector2 = Vector2(screen_pos.x / screen_size.x, screen_pos.y / screen_size.y)
  31.     return center
  32.  
  33. func on_mouse_entered() -> void:
  34.     is_pointer_on_node = true
  35.  
  36. func on_mouse_exited() -> void:
  37.     is_pointer_on_node = false
  38.  
  39.  

In Project settings go to Input Map, let’s add new action, as following.



In the KamuiableSphere let’s assign the values and we have our effect.

One final thing. If we setup multiply kamuiables and create multiple effect at the same time, everything goes haywire. The reason is right now our code is updating the same material for all kamui quads. To fix this we will just go to our material and check this Local to scene.



It will create a new instance of our material for all quads which we will load to our scene and now everything will work.

Trouble following along? Check out this video!



Thank you so much for reading.

Comments