Rewrote the custom post-processing shader tutorial

The original method of applying shaders to viewports did not work in practice. It was also too complicated. This new way of doing it with CanvasLayers works well and is easy to implement.
I reorganised the information to improve the information flow and readability, and to reduce redundancy.
I replaced the original cube example with a sheep sprite. While the original tutorial claimed to be in 2D, the example cubes were in 3D, which was confusing. Also, sheep are more engaging than cubes.
I replaced the the Sobel filter example with an hex pixelization example, because in practice applying a Sobel filter to a frame often makes it appear pure black. People trying out this filter may think it does not work. Removing the Sobel filter example also removes a note about the filter's implementation, saving space. In contrast, a pixelization filter works on nearly all images and makes for a more engaging and clear example.
This commit is contained in:
Roland Marchand
2022-07-20 22:39:00 -04:00
parent a52f5eb499
commit 245975f4aa
12 changed files with 99 additions and 124 deletions
+99 -124
View File
@@ -6,167 +6,142 @@ Custom post-processing
Introduction
------------
Godot provides many post-processing effects out of the box, including Bloom, DOF, and SSAO. Sometimes you
want to write your own custom effect. Here's how you can do so.
Godot provides many post-processing effects out of the box, including Bloom,
DOF, and SSAO. However, advanced use cases may require custom effects. This
article explains how to write your own custom effects.
Post-processing effects are shaders applied to a frame after Godot rendered it. You first want to render
your scene into a :ref:`Viewport <class_Viewport>`, then render the ``Viewport``
inside a :ref:`ViewportTexture <class_ViewportTexture>` and show it on the screen.
The easiest way to implement a custom post-processing shader is to use Godot's built-in ability to read from
the screen texture. If you're not familiar with this, you should read the :ref:`Screen Reading Shaders
Tutorial <doc_screen-reading_shaders>` first.
.. note::
As of the time of writing, Godot does not support rendering to multiple buffers at the same time. Your
post-processing shader will not have access to normals or other render passes. You only have
access to the rendered frame.
The easiest way to implement a custom post-processing shader is to use Godot's
built-in ability to read from the screen texture. If you're not familiar with
this, you should read the
:ref:`Screen Reading Shaders Tutorial <doc_screen-reading_shaders>` first.
Single pass post-processing
---------------------------
You will need a ``Viewport`` to render your scene to, and a scene to render your
``Viewport`` on the screen. You can use a :ref:`ViewportContainer
<class_ViewportContainer>` to display your ``Viewport`` on the entire screen or inside
another :ref:`Control <class_Control>` node.
Post-processing effects are shaders applied to a frame after Godot has rendered
it. To apply a shader to a frame, create a :ref:`CanvasLayer
<class_CanvasLayer>`, and give it a :ref:`ColorRect <class_ColorRect>`. Assign a
new :ref:`ShaderMaterial <class_ShaderMaterial>` to the newly created
``ColorRect``, and set the ``ColorRect``'s layout to "Full Rect".
Your scene tree will look something like this:
.. image:: img/post_tree1.png
.. note::
Rendering using a ``Viewport`` gives you control over
how the scene render, including the framerate, and you can use the
``ViewportContainer`` to render 3D objects in a 2D scene.
For this demo, we will use a :ref:`Node2D <class_Node2D>` with a ``ViewportContainer`` and finally a
``Viewport``. Your **Scene** tab should look like this:
.. image:: img/post_hierarchy1.png
Inside the ``Viewport``, you can have whatever you want. This will contain
your main scene. For this tutorial, we will use a field of random boxes:
.. image:: img/post_boxes.png
Add a new :ref:`ShaderMaterial <class_ShaderMaterial>` to the ``ViewportContainer``, and assign a new
shader resource to it. You can access your rendered ``Viewport`` with the built-in ``TEXTURE`` uniform.
Another more efficient method is to use a :ref:`BackBufferCopy
<class_BackBufferCopy>` to copy a region of the screen to a buffer and to
access it in a shader script through ``texture(SCREEN_TEXTURE, ...)``.
.. note::
You can choose not to use a ``ViewportContainer``, but if you do so, you will
need to create your own uniform in the shader and pass the ``Viewport`` texture in
manually, like so:
As of the time of writing, Godot does not support rendering to multiple
buffers at the same time. Your post-processing shader will not have access
to normals or other render passes. You only have access to the rendered
frame.
.. code-block:: glsl
For this demo, we will use this :ref:`Sprite <class_Sprite2D>` of a sheep.
// Inside the Shader.
uniform sampler2D ViewportTexture;
.. image:: img/post_example1.png
And you can pass the texture into the shader from GDScript like so:
Assign a new :ref:`Shader <class_Shader>` to the ``ColorRect``'s
``ShaderMaterial``. You can access the frame's texture and UV with the built in
``SCREEN_TEXTURE`` and ``SCREEN_UV`` uniforms.
::
# In GDScript.
func _ready():
$Sprite.material.set_shader_param("ViewportTexture", $Viewport.get_texture())
Copy the following code to your shader. The above code is a single pass edge detection filter, a
`Sobel filter <https://en.wikipedia.org/wiki/Sobel_operator>`_.
Copy the following code to your shader. The code below is a hex pixelization
shader by `arlez80 <https://bitbucket.org/arlez80/hex-mosaic/src/master/>`_,
.. code-block:: glsl
shader_type canvas_item;
shader_type canvas_item;
void fragment() {
vec3 col = -8.0 * texture(TEXTURE, UV).xyz;
col += texture(TEXTURE, UV + vec2(0.0, SCREEN_PIXEL_SIZE.y)).xyz;
col += texture(TEXTURE, UV + vec2(0.0, -SCREEN_PIXEL_SIZE.y)).xyz;
col += texture(TEXTURE, UV + vec2(SCREEN_PIXEL_SIZE.x, 0.0)).xyz;
col += texture(TEXTURE, UV + vec2(-SCREEN_PIXEL_SIZE.x, 0.0)).xyz;
col += texture(TEXTURE, UV + SCREEN_PIXEL_SIZE.xy).xyz;
col += texture(TEXTURE, UV - SCREEN_PIXEL_SIZE.xy).xyz;
col += texture(TEXTURE, UV + vec2(-SCREEN_PIXEL_SIZE.x, SCREEN_PIXEL_SIZE.y)).xyz;
col += texture(TEXTURE, UV + vec2(SCREEN_PIXEL_SIZE.x, -SCREEN_PIXEL_SIZE.y)).xyz;
COLOR.xyz = col;
}
uniform vec2 size = vec2(32.0, 28.0);
.. note::
void fragment() {
vec2 norm_size = size * SCREEN_PIXEL_SIZE;
bool half = mod(SCREEN_UV.y / 2.0, norm_size.y) / norm_size.y < 0.5;
vec2 uv = SCREEN_UV + vec2(norm_size.x * 0.5 * float(half), 0.0);
vec2 center_uv = floor(uv / norm_size) * norm_size;
vec2 norm_uv = mod(uv, norm_size) / norm_size;
center_uv += mix(vec2(0.0, 0.0),
mix(mix(vec2(norm_size.x, -norm_size.y),
vec2(0.0, -norm_size.y),
float(norm_uv.x < 0.5)),
mix(vec2(0.0, -norm_size.y),
vec2(-norm_size.x, -norm_size.y),
float(norm_uv.x < 0.5)),
float(half)),
float(norm_uv.y < 0.3333333) * float(norm_uv.y / 0.3333333 < (abs(norm_uv.x - 0.5) * 2.0)));
The Sobel filter reads pixels in a 9x9 grid around the current pixel and adds them together, using weight.
What makes it interesting is that it assigns weights to each pixel; +1 for each of the eight around the
center and -8 for the center pixel. The choice of weights is called a "kernel". You can use different
kernels to create edge detection filters, outlines, and all sorts of effects.
COLOR = textureLod(SCREEN_TEXTURE, center_uv, 0.0);
}
.. image:: img/post_outline.png
The sheep will look something like this:
.. image:: img/post_example2.png
Multi-pass post-processing
--------------------------
Some post-processing effects like blur are resource intensive. If you break them down in multiple passes
however, you can make them run a lot faster. In a multipass material, each pass takes the result from the
previous pass as an input and processes it.
Some post-processing effects like blurs are resource intensive. You can make
them run a lot faster if you break them down in multiple passes. In a multipass
material, each pass takes the result from the previous pass as an input and
processes it.
To make a multi-pass post-processing shader, you stack ``Viewport`` nodes. In the example above, you
rendered the content of one ``Viewport`` object into the root ``Viewport``, through a ``ViewportContainer``
node. You can do the same thing for a multi-pass shader by rendering the content of one ``Viewport`` into
another and then rendering the last ``Viewport`` into the root ``Viewport``.
To produce a multi-pass post-processing shader, you stack ``CanvasLayer`` and
``ColorRect`` nodes. In the example above, you use a ``CanvasLayer`` object to
render a shader using the frame on the layer below. Apart from the node
structure, the steps are the same as with the single-pass post-processing
shader.
Your scene hierarchy will look something like this:
Your scene tree will look something like this:
.. image:: img/post_hierarchy2.png
.. image:: img/post_tree2.png
Godot will render the bottom ``Viewport`` node first. So if the order of the passes matters for your
shaders, make sure that you assign the shader you want to apply first to the lowest ``ViewportContainer`` in
the tree.
.. note::
You can also render your Viewports separately without nesting them like this. You just
need to use two Viewports and to render them one after the other.
Apart from the node structure, the steps are the same as with the single-pass post-processing shader.
As an example, you could write a full screen Gaussian blur effect by attaching the following pieces of code
to each of the :ref:`ViewportContainers <class_ViewportContainer>`. The order in which you apply the shaders
does not matter:
As an example, you could write a full screen Gaussian blur effect by attaching
the following pieces of code to each of the ``ColorRect`` nodes. The order in
which you apply the shaders depends on the position of the ``CanvasLayer`` in
the scene tree, higher means sooner. For this blur shader, the order does not
matter.
.. code-block:: glsl
shader_type canvas_item;
shader_type canvas_item;
// Blurs the screen in the X-direction.
void fragment() {
vec3 col = texture(TEXTURE, UV).xyz * 0.16;
col += texture(TEXTURE, UV + vec2(SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15;
col += texture(TEXTURE, UV + vec2(-SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15;
col += texture(TEXTURE, UV + vec2(2.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12;
col += texture(TEXTURE, UV + vec2(2.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12;
col += texture(TEXTURE, UV + vec2(3.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09;
col += texture(TEXTURE, UV + vec2(3.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09;
col += texture(TEXTURE, UV + vec2(4.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05;
col += texture(TEXTURE, UV + vec2(4.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05;
COLOR.xyz = col;
}
// Blurs the screen in the X-direction.
void fragment() {
vec3 col = texture(SCREEN_TEXTURE, SCREEN_UV).xyz * 0.16;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(-SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(2.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(2.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(3.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(3.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(4.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(4.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05;
COLOR.xyz = col;
}
.. code-block:: glsl
shader_type canvas_item;
shader_type canvas_item;
// Blurs the screen in the Y-direction.
void fragment() {
vec3 col = texture(TEXTURE, UV).xyz * 0.16;
col += texture(TEXTURE, UV + vec2(0.0, SCREEN_PIXEL_SIZE.y)).xyz * 0.15;
col += texture(TEXTURE, UV + vec2(0.0, -SCREEN_PIXEL_SIZE.y)).xyz * 0.15;
col += texture(TEXTURE, UV + vec2(0.0, 2.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.12;
col += texture(TEXTURE, UV + vec2(0.0, 2.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.12;
col += texture(TEXTURE, UV + vec2(0.0, 3.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.09;
col += texture(TEXTURE, UV + vec2(0.0, 3.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.09;
col += texture(TEXTURE, UV + vec2(0.0, 4.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.05;
col += texture(TEXTURE, UV + vec2(0.0, 4.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.05;
COLOR.xyz = col;
}
// Blurs the screen in the Y-direction.
void fragment() {
vec3 col = texture(SCREEN_TEXTURE, SCREEN_UV).xyz * 0.16;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, SCREEN_PIXEL_SIZE.y)).xyz * 0.15;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, -SCREEN_PIXEL_SIZE.y)).xyz * 0.15;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, 2.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.12;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, 2.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.12;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, 3.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.09;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, 3.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.09;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, 4.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.05;
col += texture(SCREEN_TEXTURE, SCREEN_UV + vec2(0.0, 4.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.05;
COLOR.xyz = col;
}
Using the above code, you should end up with a full screen blur effect like below.
Using the above code, you should end up with a full screen blur effect like
below.
.. image:: img/post_blur.png
For more information on how ``Viewport`` nodes work, see the :ref:`Viewports Tutorial <doc_viewports>`.
.. image:: img/post_example3.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB