Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

著色器風格指南

本風格指南列出了撰寫優雅著色器的慣例。其目的是鼓勵撰寫乾淨、易讀的程式碼,並促進專案、討論與教學的一致性。同時也期望能促進自動格式化工具的發展。

由於 Godot 的著色器語言與 C 風格語言及 GLSL 相當接近,本指南是參考 Godot 自身的 GLSL 格式而來。你可以在 Godot 原始碼中`這裡 <https://github.com/godotengine/godot/blob/master/drivers/gles3/shaders/>`__找到 GLSL 範例。

風格指南並不是一種強制規範。有時候可能會無法符合下列的某些準則,這是則需要自行判斷最佳方法,或是詢問其他開發者的意見。

一般來說,在專案中或是在團隊成員間保持程式碼風格的一致性比嚴格遵守本指南還要重要。

備註

Godot 的內建著色器編輯器預設就用了許多這些慣例,善加利用它來輔助你吧。

以下是基於這些規範的完整著色器範例:

shader_type canvas_item;
// Screen-space shader to adjust a 2D scene's brightness, contrast
// and saturation. Taken from
// https://github.com/godotengine/godot-demo-projects/blob/master/2d/screen_space_shaders/shaders/BCS.gdshader

uniform sampler2D screen_texture : hint_screen_texture, filter_linear_mipmap;
uniform float brightness = 0.8;
uniform float contrast = 1.5;
uniform float saturation = 1.8;

void fragment() {
    vec3 c = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;

    c.rgb = mix(vec3(0.0), c.rgb, brightness);
    c.rgb = mix(vec3(0.5), c.rgb, contrast);
    c.rgb = mix(vec3(dot(vec3(1.0), c.rgb) * 0.33333), c.rgb, saturation);

    COLOR.rgb = c;
}

格式

編碼與特殊字元

  • 使用 LF 換行字元來換行,而非 CRLF 或 CR。(編輯器預設

  • 每個檔案結尾都應有一個 LF 換行字元。(編輯器預設

  • 使用不含 位元組順序標記UTF-8 編碼。(編輯器預設

  • 縮排請使用 Tab 字元,勿使用空白字元。(編輯器預設

縮排

每個縮排層級應比其所屬區塊多一個 Tab(定位字元)。

正確例 :

void fragment() {
    COLOR = vec3(1.0, 1.0, 1.0);
}

錯誤例 :

void fragment() {
        COLOR = vec3(1.0, 1.0, 1.0);
}

使用兩層縮排來區分連續行與一般程式碼區塊。

正確例 :

vec2 st = vec2(
        atan(NORMAL.x, NORMAL.z),
        acos(NORMAL.y));

錯誤例 :

vec2 st = vec2(
    atan(NORMAL.x, NORMAL.z),
    acos(NORMAL.y));

換行與空白行

一般的縮排慣例請遵循「1TBS 樣式」,建議將控制敘述的括號置於同一行。所有控制敘述都應加上大括號,即使僅有一行。這樣方便後續重構,也能避免在像 if 這類敘述新增行時出現錯誤。

正確例 :

void fragment() {
    if (true) {
        // ...
    }
}

錯誤例 :

void fragment()
{
    if (true)
        // ...
}

空白行

函式定義應前後各加一行(且僅一行)空白行:

void do_something() {
    // ...
}

void fragment() {
    // ...
}

在函式內用一行(且僅一行)空白行區隔邏輯段落。

單行長度

每行程式碼請控制在 100 字元內。

若有可能,盡量將單行長度維持在 80 字元以內。這有助於在螢幕較小時閱讀,或在外部文字編輯器中並排比較兩份著色器時,例如查看差異時更方便。

一行一個敘述式

不要將多個敘述寫在同一行。

正確例 :

void fragment() {
    ALBEDO = vec3(1.0);
    EMISSION = vec3(1.0);
}

錯誤例 :

void fragment() {
    ALBEDO = vec3(1.0); EMISSION = vec3(1.0);
}

唯一的例外是三元運算子:

void fragment() {
     bool should_be_white = true;
     ALBEDO = should_be_white ? vec3(1.0) : vec3(0.0);
 }

註解空白

一般註解開頭應有一個空格,但註解掉的程式碼則不用。這有助於區分純文字註解與被停用的程式碼。

正確例 :

// This is a comment.
//return;

錯誤例 :

//This is a comment.
// return;

若註解能寫在單行,請勿用多行註解語法:

/* This is another comment. */

備註

在著色器編輯器中,將選取程式碼註解(或取消註解)的方法為:按下 Ctrl + K。這功能會在所選行開頭加上或移除 //

說明文件註解

在 uniform 上方撰寫說明註解時,請使用下列格式:開頭用 兩個 星號(/**),每行前都要有星號(*):

/**
 * This is a documentation comment.
 * These lines will appear in the inspector when hovering the shader parameter
 * named "Something".
 * You can use [b]BBCode[/b] [i]formatting[/i] in the comment.
 */
uniform int something = 1;

這些註解會在檢查器中將滑鼠游標移到屬性上時顯示。若不希望註解在檢查器顯示,請改用一般註解語法(// .../* ... */,僅一個星號)。

空白字元

運算子前後及逗號後都應有一個空格。函式呼叫時避免多餘空格。

正確例 :

COLOR.r = 5.0;
COLOR.r = COLOR.g + 0.1;
COLOR.b = some_function(1.0, 2.0);

錯誤例 :

COLOR.r=5.0;
COLOR.r = COLOR.g+0.1;
COLOR.b = some_function (1.0,2.0);

請勿用空格來讓運算式垂直對齊:

ALBEDO.r   = 1.0;
EMISSION.r = 1.0;

浮點數

整數與小數部分都至少寫一位數,這有助於明確區分浮點數與整數,也方便分辨大於 1 和小於 1 的數值。

正確例 :

void fragment() {
    ALBEDO.rgb = vec3(5.0, 0.1, 0.2);
}

錯誤例 :

void fragment() {
    ALBEDO.rgb = vec3(5., .1, .2);
}

存取向量成員

若向量內容為顏色,存取成員時請用 rgba。若為其他用途,則用 xyzw。這樣閱讀程式碼時更容易理解資料的意義。

正確例 :

COLOR.rgb = vec3(5.0, 0.1, 0.2);

錯誤例 :

COLOR.xyz = vec3(5.0, 0.1, 0.2);

命名慣例

這些命名慣例遵循 Godot Engine 風格。違反這些守則會讓你的程式碼與內建命名方式衝突,導致風格不一致。

函式與變數

函式與變數名稱請使用 snake_case:

void some_function() {
     float some_variable = 0.5;
}

常數

常數請使用 CONSTANT_CASE,也就是全部大寫且用底線(_)分隔單字:

const float GOLDEN_RATIO = 1.618;

前置處理器指令

著色器預處理器 指令請用 CONSTANT__CASE 命名。即使在函式內巢狀,也不能有任何前置縮排。

為了讓著色器錯誤訊息在主控台輸出時保有正確縮排,#if#ifdef#ifndef 區塊內請勿額外增加縮排:

正確例 :

#define HEIGHTMAP_ENABLED

void fragment() {
    vec2 position = vec2(1.0, 2.0);

#ifdef HEIGHTMAP_ENABLED
    sample_heightmap(position);
#endif
}

錯誤例 :

#define heightmap_enabled

void fragment() {
    vec2 position = vec2(1.0, 2.0);

    #ifdef heightmap_enabled
        sample_heightmap(position);
    #endif
}

Applying formatting automatically

To automatically format shader files, you can use clang-format on one or several .gdshader files, as the syntax is close enough to a C-style language.

However, the default style in clang-format doesn't follow this style guide, so you need to save this file as .clang-format in your project's root folder:

BasedOnStyle: LLVM
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlignTrailingComments:
  Kind: Never
  OverEmptyLines: 0
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortFunctionsOnASingleLine: Inline
BreakConstructorInitializers: AfterColon
ColumnLimit: 0
ContinuationIndentWidth: 8
IndentCaseLabels: true
IndentWidth: 4
InsertBraces: true
KeepEmptyLinesAtTheStartOfBlocks: false
RemoveSemicolon: true
SpacesInLineCommentPrefix:
  Minimum: 0 # We want a minimum of 1 for comments, but allow 0 for disabled code.
  Maximum: -1
TabWidth: 4
UseTab: Always

While in the project root, you can then call clang-format -i path/to/shader.gdshader in a terminal to format a single shader file, or clang-format -i path/to/folder/*.gdshader to format all shaders in a folder.

程式碼順序

建議以以下順序組織你的著色器程式碼:

01. shader type declaration
02. render mode declaration
03. // docstring

04. uniforms
05. constants
06. varyings

07. other functions
08. vertex() function
09. fragment() function
10. light() function

我們最佳化了順序,來讓程式碼從上到下閱讀時比較容易,也幫助首次閱讀程式碼的開發者能瞭解程式如何運作的,並避免因變數宣告順序導致的錯誤。

這個撰寫順序遵循兩個原則:

  1. 先寫中繼資料及屬性,之後才是方法。

  2. 「公開」應排在「私有」之前。在著色器語言中,「公開」指的是可由使用者輕易調整的(如 uniform)。

區域變數

區域變數請盡量在首次使用的附近宣告。這樣能讓程式碼更易閱讀,也不用頻繁捲動去尋找變數的宣告位置。