Browse Source

Improve Shader preprocessor documentation (#6563)

Hugo Locurcio 2 years ago
parent
commit
e2313a8395

+ 307 - 46
tutorials/shaders/shader_reference/shader_preprocessor.rst

@@ -1,56 +1,252 @@
 .. _doc_shader_preprocessor:
 .. _doc_shader_preprocessor:
 
 
-Shader Preprocessor
+Shader preprocessor
 ===================
 ===================
 
 
-The shader preprocessor is an optional step before shader compilation of text shaders in Godot.
-If you don't need it, you may ignore it, but it contains some useful macros which may speed up your productivity.
+Why use a shader preprocessor?
+------------------------------
+
+In programming languages, a *preprocessor* allows changing the code before the
+compiler reads it. Unlike the compiler, the preprocessor does not care about
+whether the syntax of the preprocessed code is valid. The preprocessor always
+performs what the *directives* tell it to do. A directive is a statement
+starting with a hash symbol (``#``). It is not a *keyword* of the shader
+language (such as ``if`` or ``for``), but a special kind of token within the
+language.
+
+From Godot 4.0 onwards, you can use a shader preprocessor within text-based
+shaders. The syntax is similar to what most GLSL shader compilers support
+(which in turn is similar to the C/C++ preprocessor).
+
+.. note::
+
+    The shader preprocessor is not available in :ref:`visual shaders <doc_visual_shaders>`.
+    If you need to introduce preprocessor statements to a visual shader, you can
+    convert it to a text-based shader using the **Convert to Shader** option in
+    the VisualShader inspector resource dropdown. This conversion is a one-way
+    operation; text shaders cannot be converted back to visual shaders.
+
+Directives
+----------
+
+General syntax
+^^^^^^^^^^^^^^
+
+- Preprocessor directives do not use brackets (``{}``), but can use parentheses.
+- Preprocessor directives **never** end with semicolons.
+- Preprocessor directives can span several lines by ending each line with a
+  blackslash (``\``). The first line break *not* featuring a backslash will end
+  the preprocessor statement.
 
 
 #define
 #define
 ^^^^^^^
 ^^^^^^^
-\ **Syntax:** `#define identifier <replacement_code>`.
+\ **Syntax:** ``#define <identifier> [replacement_code]``.
+
+Defines the identifier after that directive as a macro, and replaces all
+successive occurrences of it with the replacement code given in the shader.
+Replacement is performed on a "whole words" basis, which means no replacement is
+performed if the string is part of another string (without any spaces separating
+it).
+
+Defines with replacements may also have one or more *arguments*, which can then
+be passed when referencing the define (similar to a function call).
 
 
-Defines the identifier after that directive as a macro, and replaces all successive occurrence of it with the replacement code given in the shader.
-If the replacement code is not defined, it may only be used within `#ifdef` or `#ifndef` directives.
+If the replacement code is not defined, the identifier may only be used with
+``#ifdef` or ``#ifndef`` directives.
+
+Compared to constants (``const CONSTANT = value;``), ``#define``s can be used
+anywhere within the shader. ``#define``s can also be used to insert arbitrary
+shader code at any location, while constants can't do that.
 
 
 .. code-block:: glsl
 .. code-block:: glsl
 
 
     shader_type spatial;
     shader_type spatial;
 
 
+    // Notice the lack of semicolon at the end of the line, as the replacement text
+    // shouldn't insert a semicolon on its own.
     #define USE_MY_COLOR
     #define USE_MY_COLOR
     #define MY_COLOR vec3(1, 0, 0)
     #define MY_COLOR vec3(1, 0, 0)
 
 
+    // Replacement with arguments.
+    // All arguments are required (no default values can be provided).
+    #define BRIGHTEN_COLOR(r, g, b) vec3(r + 0.5, g + 0.5, b + 0.5)
+
+    // Multiline replacement using backslashes for continuation:
+    #define SAMPLE(param1, param2, param3, param4) long_function_call( \
+            param1, \
+            param2, \
+            param3, \
+            param4 \
+    )
+
     void fragment() {
     void fragment() {
     #ifdef USE_MY_COLOR
     #ifdef USE_MY_COLOR
         ALBEDO = MY_COLOR;
         ALBEDO = MY_COLOR;
     #endif
     #endif
     }
     }
 
 
+
+Defining a ``#define`` for an identifier that is already defined results in an
+error. To prevent this, use ``#undef <identifier>``.
+
+#undef
+^^^^^^
+
+**Syntax:** ``#undef identifier``
+
+The ``#undef`` directive may be used to cancel a previously defined ``#define`` directive:
+
+.. code-block:: glsl
+
+    #define MY_COLOR vec3(1, 0, 0)
+
+    vec3 get_red_color() {
+        return MY_COLOR;
+    }
+
+    #undef MY_COLOR
+    #define MY_COLOR vec3(0, 1, 0)
+
+    vec3 get_green_color() {
+        return MY_COLOR;
+    }
+
+Without ``#undef`` in the above example, there would be a macro redefinition error.
+
 #if
 #if
 ^^^
 ^^^
-\ **Syntax:** `#if condition`.
 
 
-The `#if` directive checks the condition and if it evaluates to a non-zero value, the code block is included, otherwise it is skipped.
-In order to evaluate, the condition must be an expression giving a simple floating-point, integer or boolean result. There may be multiple condition blocks connected by `||` or `&&` operators.
-It may be continued by a `#else` block, but must be ended with the `#endif` directive.
+**Syntax:** ``#if <condition>``
+
+The ``#if`` directive checks whether the ``condition`` passed. If it evaluates
+to a non-zero value, the code block is included, otherwise it is skipped.
+
+To evaluate correctly, the condition must be an expression giving a simple
+floating-point, integer or boolean result. There may be multiple condition
+blocks connected by ``&&`` (AND) or ``||`` (OR) operators. It may be continued
+by a ``#else`` block, but **must** be ended with the ``#endif`` directive.
 
 
 .. code-block:: glsl
 .. code-block:: glsl
 
 
     #define VAR 3
     #define VAR 3
-    #define USE_LIGHT 0 // evaluates to false
-    #define USE_COLOR 1 // evaluates to true
+    #define USE_LIGHT 0 // Evaluates to `false`.
+    #define USE_COLOR 1 // Evaluates to `true`.
 
 
     #if VAR == 3 && (USE_LIGHT || USE_COLOR)
     #if VAR == 3 && (USE_LIGHT || USE_COLOR)
+    // Condition is `true`. Include this portion in the final shader.
+    #endif
+
+Using the ``defined()`` *preprocessor function*, you can check whether the
+passed identifier is defined a by ``#define`` placed above that directive. This
+is useful for creating multiple shader versions in the same file. It may be
+continued by a `#else` block, but must be ended with the ``#endif`` directive.
+
+The ``defined()`` function's result can be negated by using the ``!`` (boolean NOT)
+symbol in front of it. This can be used to check whether a define is *not* set.
+
+.. code-block:: glsl
+
+    #define USE_LIGHT
+    #define USE_COLOR
+
+    // Correct syntax:
+    #if defined(USE_LIGHT) || defined(USE_COLOR) || !defined(USE_REFRACTION)
+    // Condition is `true`. Include this portion in the final shader.
+    #endif
+
+Be careful, as ``defined()`` must only wrap a single identifier within parentheses, never more:
+
+.. code-block:: glsl
+
+    // Incorrect syntax (parentheses are not placed where they should be):
+    #if defined(USE_LIGHT || USE_COLOR || !USE_REFRACTION)
+    // This will cause an error or not behave as expected.
+    #endif
 
 
+.. tip::
 
 
+    In the shader editor, preprocessor branches that evaluate to ``false`` (and
+    are therefore excluded from the final compiled shader) will appear grayed
+    out. This does not apply to run-time ``if`` statements.
+
+**#if preprocessor versus if statement: Performance caveats**
+
+The :ref:`shading language <doc_shading_language>` supports run-time ``if`` statements:
+
+.. code-block:: glsl
+
+    uniform bool USE_LIGHT = true;
+
+    if (USE_LIGHT) {
+        // This part is included in the compiled shader, and always run.
+    } else {
+        // This part is included in the compiled shader, but never run.
+    }
+
+If the uniform is never changed, this behaves identical to the following usage
+of the ``#if`` preprocessor statement:
+
+.. code-block:: glsl
+
+    #define USE_LIGHT
+
+    #if defined(USE_LIGHT)
+    // This part is included in the compiled shader, and always run.
+    #else
+    // This part is *not* included in the compiled shader (and therefore never run).
+    #endif
+
+However, the ``#if`` variant can be faster in certain scenarios. This is because
+all run-time branches in a shader are still compiled and variables within 
+those branches may still take up register space, even if they are never run in
+practice.
+
+`Modern GPUs are quite effective at performing "static" branching <https://medium.com/@jasonbooth_86226/branching-on-a-gpu-18bfc83694f2>`__.
+"Static" branching refers to ``if`` statements where *all* pixels/vertices
+evaluate to the same result in a given shader invocation. However, high amounts
+of :abbr:`VGPR (Vector General-Purpose Register)`s (which can be caused by
+having too many branches) can still slow down shader execution significantly.
+
+#elif
+^^^^^
+
+The ``#elif`` directive stands for "else if" and checks the condition passed if
+the above ``#if`` evaluated to ``false``. ``#elif`` can only be used within an
+``#if`` block. It is possible to use several ``#elif``s in the same ``#if`` statement.
+
+.. code-block:: glsl
+
+    #define VAR 3
+    #define USE_LIGHT 0 // Evaluates to `false`.
+    #define USE_COLOR 1 // Evaluates to `true`.
+
+    #if VAR == 3 && (USE_LIGHT || USE_COLOR)
+    // Condition is `true`. Include this portion in the final shader.
+    #endif
+
+Like with ``#if``, the ``defined()`` preprocessor function can be used:
+
+.. code-block:: glsl
+
+    #define SHADOW_QUALITY_MEDIUM
+
+    #if defined(SHADOW_QUALITY_HIGH)
+    // High shadow quality.
+    #elif defined(SHADOW_QUALITY_MEDIUM)
+    // Medium shadow quality.
+    #else
+    // Low shadow quality.
     #endif
     #endif
 
 
 #ifdef
 #ifdef
 ^^^^^^
 ^^^^^^
-\ **Syntax:** `#ifdef identifier`.
 
 
-Checks whether the passed identifier is defined by `#define` before that directive. Useful for creating multiple shader versions in the same file.
-It may be continued by a `#else` block, but must be ended with the `#endif` directive.
+**Syntax:** ``#ifdef <identifier>``
+
+This is a shorthand for ``#if defined(...)``. Checks whether the passed
+identifier is defined by ``#define`` placed above that directive. This is useful
+for creating multiple shader versions in the same file. It may be continued by a
+``#else`` block, but must be ended with the `#endif` directive.
 
 
 .. code-block:: glsl
 .. code-block:: glsl
 
 
@@ -59,71 +255,130 @@ It may be continued by a `#else` block, but must be ended with the `#endif` dire
 
 
     #endif
     #endif
 
 
+The processor does *not* support ``#elifdef`` as a shortcut for ``#elif defined(...)``.
+Instead, use the following series of ``#ifdef`` and ``#else`` when you need more
+than two branches:
+
+.. code-block:: glsl
+
+    #define SHADOW_QUALITY_MEDIUM
+
+    #ifdef SHADOW_QUALITY_HIGH
+    // High shadow quality.
+    #else
+    #ifdef SHADOW_QUALITY_MEDIUM
+    // Medium shadow quality.
+    #else
+    // Low shadow quality.
+    #endif // This ends `SHADOW_QUALITY_MEDIUM`'s branch.
+    #endif // This ends `SHADOW_QUALITY_HIGH`'s branch.
+
 #ifndef
 #ifndef
 ^^^^^^^
 ^^^^^^^
-\ **Syntax:** `#ifndef identifier`.
 
 
-Similar to `#ifdef` but checks whether the passed identifier is not defined by `#define` before that directive.
+**Syntax:** ``#ifndef <identifier>``
+
+This is a shorthand for ``#if !defined(...)``. Similar to ``#ifdef``, but checks
+whether the passed identifier is **not** defined by `#define` before that
+directive.
+
+This is the exact opposite of ``#ifdef``; it will always match in situations
+where ``#ifdef`` would never match, and vice versa.
+
+.. code-block:: glsl
+
+    #define USE_LIGHT
+
+    #ifndef USE_LIGHT
+    // Evaluates to `false`. This portion won't be included in the final shader.
+    #endif
+
+    #ifndef USE_COLOR
+    // Evaluates to `true`. This portion will be included in the final shader.
+    #endif
 
 
 #else
 #else
 ^^^^^
 ^^^^^
-\ **Syntax:** `#else`.
 
 
-Defines the optional block which is included when the previously defined `#if`, `#ifdef` or `#ifndef` directive evaluates to false.
+**Syntax:** ``#else``
+
+Defines the optional block which is included when the previously defined `#if`,
+``#ifdef` or `#ifndef`` directive evaluates to false.
 
 
 .. code-block:: glsl
 .. code-block:: glsl
 
 
     shader_type spatial;
     shader_type spatial;
 
 
-    #define MY_COLOR vec3(1, 0, 0)
+    #define MY_COLOR vec3(1.0, 0, 0)
 
 
     void fragment() {
     void fragment() {
     #ifndef MY_COLOR
     #ifndef MY_COLOR
         ALBEDO = MY_COLOR;
         ALBEDO = MY_COLOR;
     #else
     #else
-        ALBEDO = vec3(0, 0, 1);
+        ALBEDO = vec3(0, 0, 1.0);
     #endif
     #endif
     }
     }
 
 
 #endif
 #endif
 ^^^^^^
 ^^^^^^
-\ **Syntax:** `#endif`.
 
 
-Used as terminator for the `#if`, `#ifdef`, `#ifndef` or subsequent `#else` directives.
+**Syntax:** ``#endif``
 
 
-#undef
-^^^^^^
-\ **Syntax:** `#undef identifier`.
+Used as terminator for the ``#if``, ``#`ifdef``, ``#ifndef`` or subsequent ``#else`` directives.
 
 
-The `#undef` directive may be used to cancel the previously defined `#define` directive: 
+#include
+^^^^^^^^
+\ **Syntax:** `#include "path"`.
 
 
-.. code-block:: glsl
+The ``#include`` directive includes the *entire* content of a shader include
+file in a shader. ``"path"`` can be an absolute ``res://`` path or relative to
+the current shader file. Relative paths are only allowed in shaders that are
+saved to ``.gdshader`` or ``.gdshaderinc`` files, while absolute paths can be
+used in shaders that are built into a scene/resource file.
 
 
-    #define MY_COLOR vec3(1, 0, 0)
+This directive may be used in any place, but is recommended at
+the beginning of the shader file, after the ``shader_type`` to prevent possible
+errors. The shader include may be created by using a **File > Create Shader
+Include** menu option of the shader editor.
 
 
-    vec3 get_red_color() {
-        return MY_COLOR;
-    }
+``#include`` is useful for creating libraries of helper functions (or macros)
+and reducing code duplication. When using ``#include``, be careful about naming
+collisions, as redefining functions or macros is not allowed.
 
 
-    #undef MY_COLOR
-    #define MY_COLOR vec3(0, 1, 0)
+``#include`` is subject to several restrictions:
 
 
-    vec3 get_green_color() {
-        return MY_COLOR;
-    }
+- Only shader include resources (ending with ``.gdshaderinc``) can be included.
+  ``.gdshader`` files cannot be included by another shader, but a
+  ``.gdshaderinc`` file can include other ``.gdshaderinc`` files.
+- Cyclic dependencies are **not** allowed and will result in an error.
+- To avoid infinite recursion, include depth is limited to 25 steps.
 
 
-Without `#undef` in that case there will be a macro redefinition error.
+Example shader include file:
 
 
-#include
-^^^^^^^^
-\ **Syntax:** `#include "path"`.
+.. code-block:: glsl
+
+    // fancy_color.gdshaderinc
+
+    // While technically allowed, there is usually no `shader_type` declaration in include files.
+
+    vec3 get_fancy_color() {
+        return vec3(0.3, 0.6, 0.9);
+    }
 
 
-The `#include` directive includes the content of shader include to a shader. It may be used in any place, but is recommended at the beginning of the shader file, 
-after the `shader_type` to prevent possible errors. The shader include may be created by using a `File → Create Shader Include` menu option of the shader editor.
+Example base shader (using the include file we created above):
 
 
 .. code-block:: glsl
 .. code-block:: glsl
 
 
-    #include "my_shader_inc.gdshaderinc"
+    // material.gdshader
+
+    shader_type spatial;
+
+    #include "res://fancy_color.gdshaderinc"
+
+    void fragment() {
+        // No error, as we've included a definition for `get_fancy_color()` via the shader include.
+        COLOR = get_fancy_color();
+    }
 
 
 #pragma
 #pragma
 ^^^^^^^
 ^^^^^^^
@@ -131,9 +386,15 @@ after the `shader_type` to prevent possible errors. The shader include may be cr
 
 
 The `#pragma` directive provides additional information to the preprocessor or compiler.
 The `#pragma` directive provides additional information to the preprocessor or compiler.
 
 
-Currently, it may have only one value: `disable_preprocessor`.
-If you don't need the preprocessor, use that directive, and it will speed up the shader compilation by excluding the preprocessor step. 
+Currently, it may have only one value: ``disable_preprocessor``. If you don't need
+the preprocessor, use that directive to speed up shader compilation by excluding
+the preprocessor step.
 
 
 .. code-block:: glsl
 .. code-block:: glsl
 
 
     #pragma disable_preprocessor
     #pragma disable_preprocessor
+
+    #if USE_LIGHT
+    // This causes a shader compilation error, as the `#if USE_LIGHT` and `#endif`
+    // are included as-is in the final shader code.
+    #endif

+ 1 - 1
tutorials/shaders/shader_reference/shading_language.rst

@@ -512,7 +512,7 @@ Godot Shading language supports the most common types of flow control:
 
 
     } while (cond);
     } while (cond);
 
 
-Keep in mind that, in modern GPUs, an infinite loop can exist and can freeze
+Keep in mind that in modern GPUs, an infinite loop can exist and can freeze
 your application (including editor). Godot can't protect you from this, so be
 your application (including editor). Godot can't protect you from this, so be
 careful not to make this mistake!
 careful not to make this mistake!
 
 

+ 39 - 0
tutorials/shaders/shaders_style_guide.rst

@@ -317,6 +317,45 @@ underscore (\_) to separate words:
 
 
     const float GOLDEN_RATIO = 1.618;
     const float GOLDEN_RATIO = 1.618;
 
 
+Preprocessor directives
+~~~~~~~~~~~~~~~~~~~~~~~
+
+:ref:`doc_shader_preprocessor` directives should be written in CONSTANT__CASE.
+Directives should be written without any indentation before them, even if
+nested within a function.
+
+To preserve the natural flow of indentation when shader errors are printed to
+the console, extra indentation should **not** be added within ``#if``,
+``#ifdef`` or ``#ifndef`` blocks:
+
+**Good**:
+
+.. code-block:: glsl
+
+    #define HEIGHTMAP_ENABLED
+
+    void fragment() {
+        vec2 position = vec2(1.0, 2.0);
+
+    #ifdef HEIGHTMAP_ENABLED
+        sample_heightmap(position);
+    #endif
+    }
+
+**Bad**:
+
+.. code-block:: glsl
+
+    #define heightmap_enabled
+
+    void fragment() {
+        vec2 position = vec2(1.0, 2.0);
+
+        #ifdef heightmap_enabled
+            sample_heightmap(position);
+        #endif
+    }
+
 Code order
 Code order
 ----------
 ----------