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.

使用 SubViewport 作為紋理

前言

本教學將介紹如何使用 SubViewport 作為可應用於 3D 物件的紋理。為此,我們將帶你一步步製作下方這種程式化行星:

../../_images/planet_example.png

備註

本教學不會介紹如何編寫如圖中星球那樣的動態大氣。

本教學假設你已熟悉如何建立基本場景,包括:一個 Camera3D 、一個 光源、一個帶有 Primitive MeshMeshInstance3D ,並將 StandardMaterial3D 應用於該網格。本篇重點在於如何使用 SubViewport 動態產生可套用於網格的紋理。

在本教學中,我們將介紹以下主題:

  • 如何使用 SubViewport 作為算繪紋理

  • 使用等距矩形投影(equirectangular mapping)將紋理貼到球體

  • 程式化行星的片段著色器技術

  • Viewport 紋理 設定粗糙度貼圖

設定場景

建立新場景並依下方範例新增以下節點。

../../_images/viewport_texture_node_tree.webp

進入 MeshInstance3D,將其網格設為 SphereMesh

設定 SubViewport

點選 SubViewport 節點,並將其尺寸設為 (1024, 512)SubViewport 其實可以是任何尺寸,只要寬度為高度的兩倍即可。寬度需要是高度的兩倍,這樣圖像才能正確對應到球體上,因為我們將使用等距矩形投影(equirectangular projection)。後續會進一步說明。

接著關閉 3D 功能。我們將使用 ColorRect 來繪製表面,因此不需要啟用 3D。

../../_images/planet_new_viewport.webp

選取 ColorRect,於屬性面板(Inspector)將 Anchors 預設值設為 Full Rect,這樣可以確保 ColorRect 會填滿整個 SubViewport

../../_images/planet_new_colorrect.webp

接著,為 ColorRect 新增 Shader Material <class_ShaderMaterial>`(ColorRect > CanvasItem > Material > Material > ``New ShaderMaterial`)。

備註

建議對著色器有基本認識再學習本教學。不過即使你是新手,所有程式碼都會提供,跟著操作應該沒有問題。

點擊 ShaderMaterial 的下拉選單按鈕並選擇 Edit。然後前往 Shader > New Shader,命名後點選「Create」。在屬性面板中點擊該著色器以開啟著色器編輯器,刪除預設程式碼並輸入下方內容:

shader_type canvas_item;

void fragment() {
    COLOR = vec4(UV.x, UV.y, 0.5, 1.0);
}

儲存著色器程式碼後,你會在屬性面板看到上述程式碼呈現出如下圖的漸層。

../../_images/planet_gradient.png

現在,我們已經完成一個可算繪內容的 SubViewport,並且擁有可以套用到球體的專屬圖像。

套用紋理

現在進入 MeshInstance3D ,並新增一個 StandardMaterial3D 給它。不需要特別的 Shader Material (不過若要實作更進階效果,如前述範例的大氣,則可以考慮)。

MeshInstance3D > GeometryInstance > Geometry > Material Override > New StandardMaterial3D

接著點擊 StandardMaterial3D 的下拉選單並選擇「Edit」

到「Resource」區塊勾選 Local to scene ,然後到「Albedo」區塊,點選「Texture」屬性旁的按鈕以新增 Albedo 紋理。這裡請選擇「New ViewportTexture」,將我們剛做的紋理套用上去

../../_images/planet_new_viewport_texture.webp

點選剛建立的 ViewportTexture,然後點擊「Assign」。在跳出的選單中選擇我們前面算繪內容的 SubViewport。

../../_images/planet_pick_viewport_texture.webp

現在,你的球體應該已經套用上我們算繪進 Viewport 的顏色。

../../_images/planet_seam.webp

你有注意到在紋理環繞時產生的醜陋縫隙嗎?這是因為我們根據 UV 座標取色,但 UV 座標本身不會環繞紋理,這是 2D 地圖投影時的經典問題。遊戲開發時經常需要將 2D 紋理投射到球體,而這種投影方式會產生明顯的接縫。這個問題其實有個優雅的解法,我們會在下一節介紹。

製作行星紋理

現在,當我們將內容算繪到 SubViewport 時,它就會神奇地顯示在球體上。不過目前因為紋理座標的關係產生了醜陋的接縫。那要怎麼讓座標在球體上自然環繞呢?一種作法是使用會重複的數學函式,例如 sincos。我們來將這些函式套用到紋理上,看看會發生什麼事。請將著色器中的原有色彩程式碼換成下方內容:

COLOR.xyz = vec3(sin(UV.x * 3.14159 * 4.0) * cos(UV.y * 3.14159 * 4.0) * 0.5 + 0.5);
../../_images/planet_sincos.webp

還不錯。你會發現接縫已經消失,但兩極出現了夾點。這是因為 Godot 的 StandardMaterial3D 將紋理貼到球體時採用了一種稱為等距矩形投影(equirectangular projection)的技術,它會將球面座標轉換成 2D 平面,因此極點會被擠壓。

備註

如果你想了解更多技術細節,我們其實會將球面座標轉換為直角座標。球面座標描述的是球體的經度與緯度,而直角座標則是從球心指向該點的向量。

對於每個像素,我們會計算其在球體上的 3D 位置,然後用 3D 噪聲來決定顏色。透過在 3D 空間計算噪聲,就能解決極點夾點的問題。原因在於:如果噪聲是在球面上計算,你永遠不會撞到邊界,因此不會產生接縫或極點夾點。以下程式碼會將 UV 轉換成直角座標。

float theta = UV.y * 3.14159;
float phi = UV.x * 3.14159 * 2.0;
vec3 unit = vec3(0.0, 0.0, 0.0);

unit.x = sin(phi) * sin(theta);
unit.y = cos(theta) * -1.0;
unit.z = cos(phi) * sin(theta);
unit = normalize(unit);

如果我們將 unit 作為輸出的 COLOR 值,會得到:

../../_images/planet_normals.webp

既然我們能計算球體表面的 3D 位置,就能用 3D 雜訊來製作星球表面。這裡直接套用來自 Shadertoy 的 noise 函式:

vec3 hash(vec3 p) {
    p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
             dot(p, vec3(269.5, 183.3, 246.1)),
             dot(p, vec3(113.5, 271.9, 124.6)));

    return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}

float noise(vec3 p) {
  vec3 i = floor(p);
  vec3 f = fract(p);
  vec3 u = f * f * (3.0 - 2.0 * f);

  return mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
                     dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
                 mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
                     dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
             mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
                     dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
                 mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
                     dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
}

備註

所有功勞歸於作者 Inigo Quilez。這段程式碼以 MIT 授權釋出。

現在要使用 noise,請在 fragment 函式中加入以下內容:

float n = noise(unit * 5.0);
COLOR.xyz = vec3(n * 0.5 + 0.5);
../../_images/planet_noise.webp

備註

為了凸顯紋理效果,我們將材質設為無陰影(unshaded)。

你可以看到噪聲現在已經無縫環繞整個球體,雖然目前看起來還不像行星,所以我們接下來要讓它更有色彩。

星球著色

接下來要為行星上色。雖然有很多種方式,這裡我們先做一個水域與陸地的顏色漸層。

要在 GLSL 裡做漸層,我們使用 mix 函式。mix 需要兩個數值進行插值,第三個參數決定插值比例;本質上,就是 混合 這兩個值。在其他 API 裡這個功能通常稱作 lerp。但 lerp 多用於混合兩個浮點數,而 mix 可以混合浮點數或向量類型。

COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), n * 0.5 + 0.5);

第一個顏色是海洋的藍色,第二個顏色是偏紅的顏色(畢竟外星行星都要紅色地表)。最後我們用 n * 0.5 + 0.5 混合它們。n-11 之間平滑變化,因此我們將它轉換到 0-1 的範圍給 mix 使用。你會發現顏色會在藍色與紅色之間變化。

../../_images/planet_noise_color.webp

這樣的效果比我們想像中還模糊。行星通常陸地與海洋的分界清楚。要達到這樣的效果,我們將最後的參數換成 smoothstep(-0.1, 0.0, n)。這樣整行程式碼就會變成:

COLOR.xyz = mix(vec3(0.05, 0.3, 0.5), vec3(0.9, 0.4, 0.1), smoothstep(-0.1, 0.0, n));

smoothstep 的作用是:當第三個參數比第一個還小時回傳 0,比第二個還大時回傳 1,如果第三個值介於前兩者之間,則在 01 間平滑混合。所以在這一行,當 n 小於 -0.1 時回傳 0,大於 0 時回傳 1

../../_images/planet_noise_smooth.webp

再來讓行星看起來更像行星一點。陸地不應該是圓滑的塊狀,我們要讓邊緣更粗糙。著色器中常用的技巧是用不同頻率的雜訊疊加,一層決定大陸的主體形狀,再用額外幾層破壞邊緣,如此類推。我們會用四行程式碼來計算 n,而不是只有一行,如下所示:

float n = noise(unit * 5.0) * 0.5;
n += noise(unit * 10.0) * 0.25;
n += noise(unit * 20.0) * 0.125;
n += noise(unit * 40.0) * 0.0625;

現在,行星看起來像這樣:

../../_images/planet_noise_fbm.webp

製作海洋

最後一點,讓它更像一顆行星。海洋與陸地反光程度不同,我們希望海洋比陸地更閃亮。可以把第四個值傳給輸出的 COLORalpha 通道,並將它作為粗糙度(Roughness)貼圖使用。

COLOR.a = 0.3 + 0.7 * smoothstep(-0.1, 0.0, n);

這一行對水域回傳 0.3,對陸地回傳 1.0。這代表陸地會非常粗糙,水域會很光滑。

然後在材質的「Metallic」區塊,將 Metallic 設為 0Specular 設為 1。這是因為水域會大量反射光線但不是金屬。這些數值雖不完全符合物理現實,但對這個展示來說已經足夠。

接著,在「Roughness」區塊,將粗糙度貼圖(Roughness Texture)設為指向我們行星紋理的 Viewport Texture,也就是指到我們的 SubViewport。最後將 Texture Channel 設為 Alpha,這樣算繪器就會使用我們輸出 COLORalpha 通道作為 Roughness 的值。

../../_images/planet_ocean.webp

你會發現除了星球不再反射天空外,幾乎沒有其他變化。這是因為預設情況下,具有 alpha 值的物件會以透明方式繪製在背景上,而 SubViewport 的預設背景是不透明的,導致 Viewport Texturealpha 通道都是 1,所以行星紋理顏色會較淡,且 Roughness 值到處都是 1。為了解決這個問題,請切換到 SubViewport,啟用「Transparent Bg」屬性。由於現在是在另一個透明物件上繪製,我們還要啟用 blend_premul_alpha

render_mode blend_premul_alpha;

這會將顏色預先與 alpha 相乘,再正確混合。通常,當你在另一個透明顏色上疊加時,即使背景的 alpha0``(就像這裡),也可能出現奇怪的顏色滲出現象。設定 ``blend_premul_alpha 就能解決這個問題。

現在星球應該只會在海洋區域反射光線,陸地則不會。你可以移動場景中的 OmniLight3D,觀察海洋反射效果。

../../_images/planet_ocean_reflect.webp

就是這樣!你現在已經用 SubViewport 產生了程式化行星。