Shadertoy 教程 Part 15 - 通道(Channels),紋理( Textures), 以及緩衝( Buffers )

Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been authorized by the author. If reprinted or reposted, please be sure to mark the original link and description in the key position of the article after obtaining the author’s consent as well as the translator's. If the article is helpful to you, click this Donation link to buy the author a cup of coffee.
說明:該系列博文翻譯自Nathan Vaughn着色器語言教程。文章已經獲得作者翻譯授權,如有轉載請務必在取得作者譯者同意之後在文章的重點位置標明原文鏈接以及說明。如果你覺得文章對你有幫助,點擊此打賞鏈接請作者喝一杯咖啡。

朋友們,你們好!歡迎來到Shadertoy系列的第15篇教程。本篇文章將會討論如何在Shadertoy中使用通道(channels)和緩存(buffers),用紋理創建多個通道的着色器。

通道(Channels)

Shadertoy使用了一種叫做通道的概念來獲取不同類型的數據。在頁面的底部,有四個黑色的盒子選項:iChannel0,iChannel1,iChannel2,和iChannel3.

點擊任意一個通道,就會彈出一個彈框。從中我們可以選擇各種各樣的交互元素,(textures)紋理,(Cubemaps)立方體貼圖,(Volumes)音量,(Videos)視頻以及(Music)音樂。

在(其他)“Misc”標籤欄,可以選擇交互的元素有:鍵盤,攝像頭,麥克風,或者從SoundCloud上播放音樂。緩衝(buffers),A,B,C,D 可以創建多通道的着色器。可以將他們看作是着色器管線通道中的額外着色程序。“CubeMap A”是一個特殊的着色器程序,它可以讓你創建自己的天空盒。並將其傳遞給緩衝或者你的image程序中,下一篇教程中我們會重點討論它。

下一個標籤欄,可以找到三頁可用的2D紋理圖。把2D紋理看作是從圖片中抽取的像素。在本文編寫的過程中,還只能用自帶提供的紋理,而非自己導入的。但是仍然有很多方式繞過這道限制。



CubeMaps標籤欄,包含了可供選擇的多個立方體貼圖。在下一篇教程中我們會提到它們。立方體貼圖技術經常被使用在Unity這種3D遊戲的渲染引擎中,讓你感覺你身處在一個被包圍的世界。

音量標籤欄包含了3D的紋理。典型的2D紋理使用UV座標訪問x軸(u)和y軸(v)上的數據。你可以把3D紋理想像成可以從其中抽取像素的立方體,像是從三維空間中拉取數據。

視頻(Videos)標籤欄包含了隨時間變化的2D紋理。這也就是說,可以在Shadertoy畫布上播放視頻。在Shadertoy上使用視頻是爲了讓開發者體驗一些後期的特效或者圖片的特效,這些特效依賴前面一幀返回的數據。Britney Spears 以及 Claude Van Damme視頻是體驗這種特效的很好的方式。

最後,是音樂(Music)標籤欄讓我們可以體驗多種音樂效果。如果你在一個通道中選擇了一首音樂,那麼當有人訪問你的着色器片段的時候,音樂會自動播放出來。

使用紋理

在Shadertoy中使用紋理是非常簡單的一件事情。點擊打開一個新的着色器,將代碼替換如下:

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; // Normalized pixel coordinates (from 0 to 1)

  vec4 col = texture(iChannel0, uv);

  fragColor = vec4(col); // Output to screen
}

然後點擊iChannel0,彈窗出現時,選擇紋理標籤欄。我們選擇“Abstract 1”紋理,先看一下彈出的菜單欄的一些細節。

上圖展示了這片紋理的分辨率是1024*1024像素,意味着它更適合於正方形的畫布。它包含三個通道(紅,綠,藍)使用unit8類型的,無符號整數字節。

我們點擊>“Abstarct1”加載紋理到iChannel0中去。然後,運行着色器程序。你就會看到一幅完整的紋理出現在Shadertoy畫布上。

我們接下來分析一下這段着色器程序:

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; // Normalized pixel coordinates (from 0 to 1)

  vec4 col = texture(iChannel0, uv);

  fragColor = vec4(col); // Output to screen
}

UV座標被設置到了0到1之間,分別是x軸和y軸。記住,起始點爲(0,0),開始於左下角的位置。使用iChannel0和uv座標,紋理函數從紋理中獲取紋素。

一塊紋素表示的是紋理上某個座標的指定的值。對於2D紋理例如圖片,紋素表示一個像素值。我們對2D紋理進行採樣,讓UV座標沿着圖片定位到0到1之間。同時也可以使用UVmap將紋理映射到整個畫布上去。

對於3D紋理來說,你可以想象紋素就是3D座標中的某個像素值。3D紋理一般被使用到處理噪聲生產的情景,或者光線步進算法。一般情況下它很少見。

你也許會好奇什麼是iChannel0,爲什麼將它作爲一個參數傳遞一個參數傳遞給texture函數。這是因爲Shadertoy在爲你自動處理許多的事情。採樣器就是將紋理單位進行綁定的方式。採樣器的類型會根據你使用的通道中加載的資源類型變化而變化。在這個例子當中,我們在iChannel0中上傳的是2D紋理,因此,iChannel0將會是一個sampler2d的類型。你可以在OpenGl維基百科頁面瞭解這些採樣器的類型。假設你想要創建一個方法,這個方法傳遞一個特殊的通道。你可以通過下面的代碼來實現。

  vec3 get2DTexture( sampler2D sam, vec2 uv ) {
  return texture(sam, uv).rgb;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; // Normalized pixel coordinates (from 0 to 1)

  vec3 col = vec3(0.);
  
  col = get2DTexture(iChannel0, uv);
  col += get2DTexture(iChannel1, uv);

  fragColor = vec4(col,1.0); // Output to screen
}

點擊iChannel1,選擇Abstract3 紋理,運行代碼,你就可以看到兩個圖合併在一起了。

get2DTexture方法創建接收一個sampler2D類型的參數。在通道中使用2d紋理,Shadertoy自動會給你返回sampler2D類的數據。如果你想在Shadertoy的畫布上播放視頻,你可以遵循2D紋理的同樣的規則。只需要爲iChannel0選擇視頻,那麼就可以看到視頻自動播放了。

通道設置

那麼,現在讓我們看看這些通道可以爲我們做些什麼。首先,把下面的代碼複製到你的着色器中:

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; // Normalized pixel coordinates (from 0 to 1)

  vec4 col = texture(iChannel0, uv);

  fragColor = vec4(col); // Output to screen
}

然後,使用一個新的紋理。點擊iChannel0,切換到“紋理”標籤欄,翻到第2頁,可以看見一個“Nyancat”的紋理。

“Nyancat”紋理是一個256*32的圖片,擁有4個顏色通道。點擊這個紋理,那麼它就會在iChannel0中出現。

運行代碼,你就可以看到圖片出現了,不過是有點兒模糊。

要修復這種模糊,點擊右下角的設置按鈕即可。

點擊之後會彈出一個設置菜單:Filter,Wrap,以及VFlip。

Filter選項讓我們調整修改算法類型爲過濾紋理算法。紋理的維度不一定總是和Shadertoy畫布保持一致,所以filter通常被用來對紋理進行採樣。默認,Filter選項被設置爲mipmap。點擊下拉菜單,選擇nearest使用nearest-eighbor 差值算法。這種算法在保持像素化時十分有用。

選擇filter爲nearest,就可以看到一張清晰的紋理圖片了。

紋理看起來有些方正,我們通過給它進行縮放0.25個單位來修復這個問題。

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; // Normalized pixel coordinates (from 0 to 1)
  
  uv.x *= 0.25;

  vec4 col = texture(iChannel0, uv);

  fragColor = vec4(col); // Output to screen
}

運行上面的代碼,我們的紋理圖片看起來就是正常的了。

使用VFlip選項可以將紋理圖片進行上下翻轉。取消複選框的勾選,設置紋理翻轉選項。

返回並且檢查VFlip選項,返回一張普通的圖片。然後我們通過移動uv.x以及使用iTime可以讓紋理旋轉起來。

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; // Normalized pixel coordinates (from 0 to 1)
  
  uv.x *= 0.25;
  
  uv.x -= iTime * 0.05;

  vec4 col = texture(iChannel0, uv);

  fragColor = vec4(col); // Output to screen
}

默認情況下,Wrap模式是重複的。這意味當UV座標超出了0到1的範圍,紋理會重新再0到1之間進行採樣。uv.x越來越小,它最終會爲0,但是樣本會足夠聰明找到一個合適的值。如果你不想要重複的效果,你可以設置wrap模式修改爲clamp。

如果重設置時間到0,然後你就可以看到UV座標從0到1.紋理圖像就消失不見了。

紋理圖片爲我們提供了一個alpha的通道,我們就可以輕易地設置背景的透明度了。請確保時間被設置到了初始狀態,然後運行上面的代碼。

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy; // Normalized pixel coordinates (from 0 to 1)
  
  vec4 col = vec4(0.75);
  
  uv.x *= 0.25;
  uv.x -= iTime * 0.05;

  vec4 texCol = texture(iChannel0, uv);
  
  col = mix(col, texCol, texCol.a);

  fragColor = vec4(col); // Output to screen
}

這樣我們就給紋理設置了一個透明色的背景了。

請注意大多數紋理只有三個顏色通道。有些紋理則只有一個通道,像是Bayer紋理,只有一個紅色的通道用來存儲數據,但其他的三個通道則不會。這導致你只能看到紅顏色。有些紋理被用來使用做噪聲或者用來創建其他類型的圖形。你甚至可以使用紋理來存儲信息從而來改變地形結構。紋理對象的用處真的是很多。

緩衝

Shadertoy 提供了對緩衝的支持。你可以在完全不同的着色器中運行不同的緩衝。每一個着色器將會輸出最終的一個fragColor,這個值可以被用作其他的着色器中去,最後傳遞到mainImage函數中輸出顏色結果。緩衝分四種:BufferA, BufferB, BufferC,BufferD。每一種緩衝都可以保存四個通道值。要訪問緩衝,我們使用其中的一個。現在就來練練手吧。在你的編輯器的頂部,可以看到一個image欄標籤。image標籤就是是我們的主着色器程序。要添加一個buffer,只需要簡單地點擊加號即可。

點擊下拉的菜單,選擇一個common,聲音,以及BufferA, BufferB, BufferC,BufferD和CubeMapA等若干個選項。

Common的選項被用來在不同的着色器(所有的緩衝,包括其他的着色器聲音以及CubemapA)中分享代碼。聲音選項讓我們創建一個聲音着色器。CubeMapA選項讓我們生成一個立方體貼圖。本文中,我們試試每個緩衝着色器,這些着色器都是普通簡單的着色器,返回一個類型爲vec4 的着色器片段(紅,綠,藍,透明);選擇一個BufferA. 你就可以看到下面的模板的代碼了。

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  fragColor = vec4(0.0,0.0,1.0,1.0);
}

看起來代碼只是簡單的返回藍色。我們返回到image標籤欄中去。點擊iChannel0,切換到其他(Misc)標籤欄。選擇BufferA。你現在就可以使用在IChannel0中使用BufferA了。在image着色器中,粘貼下面的代碼:

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = fragCoord/iResolution.xy;

  vec3 col = texture(iChannel0, uv).rgb;
  
  col += vec3(1, 0, 0);

  // Output to screen
  fragColor = vec4(col, 1.0);
}

運行上面的代碼,就可以看到整個畫布變成了紫色。這是因爲我們從BufferA中提取了藍顏色值, 傳遞到了Image着色器中去,然後又給藍色添加一個紅色,結果就是我們在屏幕上看到的紫色。本質上,緩衝爲我們提供了更大的發揮空間。你可以將整個着色器的功能在BufferA中實現,然後將結果傳遞到更多的Buffer裏面去。最後傳遞結果給主着色器作爲最終的輸出。把它們想象爲一個管線通道,顏色可以通過這個管道一直傳遞下去。這就是爲什麼使用了緩衝或者添加了多個着色器的程序也被稱之爲多通道着色器。

使用鍵盤

你可能見過一些着色器可以使用鍵盤來控制場景。我寫過一個着色器,展示如何移動物體,通過鍵盤以及緩衝,存儲每個按鍵的值,看過這個着色器,你就能看到我們是如何使用多個通道的。在BufferA中你可以看到如下的代碼:

  // Numbers are based on JavaScript key codes: https://keycode.info/
const int KEY_LEFT  = 37;
const int KEY_UP    = 38;
const int KEY_RIGHT = 39;
const int KEY_DOWN  = 40;

vec2 handleKeyboard(vec2 offset) {
    float velocity = 1. / 100.; // This will cause offset to change by 0.01 each time an arrow key is pressed
    
    // texelFetch(iChannel1, ivec2(KEY, 0), 0).x will return a value of one if key is pressed, zero if not pressed
    vec2 left = texelFetch(iChannel1, ivec2(KEY_LEFT, 0), 0).x * vec2(-1, 0);
    vec2 up = texelFetch(iChannel1, ivec2(KEY_UP,0), 0).x * vec2(0, 1);
    vec2 right = texelFetch(iChannel1, ivec2(KEY_RIGHT, 0), 0).x * vec2(1, 0);
    vec2 down = texelFetch(iChannel1, ivec2(KEY_DOWN, 0), 0).x * vec2(0, -1);
    
    offset += (left + up + right + down) * velocity;

    return offset;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Return the offset value from the last frame (zero if it's first frame)
    vec2 offset = texelFetch( iChannel0, ivec2(0, 0), 0).xy;
    
    // Pass in the offset of the last frame and return a new offset based on keyboard input
    offset = handleKeyboard(offset);

    // Store offset in the XY values of every pixel value and pass this data to the "Image" shader and the next frame of Buffer A
    fragColor = vec4(offset, 0, 0);
}

在Image着色器中,你可以看到如下的代碼:

  float sdfCircle(vec2 uv, float r, vec2 offset) {
    float x = uv.x - offset.x;
    float y = uv.y - offset.y;
    
    float d = length(vec2(x, y)) - r;
    
    return step(0., -d);
}

vec3 drawScene(vec2 uv) {
    vec3 col = vec3(0);
    
    // Fetch the offset from the XY part of the pixel values returned by Buffer A
    vec2 offset = texelFetch( iChannel0, ivec2(0,0), 0 ).xy;
    
    float blueCircle = sdfCircle(uv, 0.1, offset);
    
    col = mix(col, vec3(0, 0, 1), blueCircle);
    
    return col;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord/iResolution.xy; // <0, 1>
    uv -= 0.5; // <-0.5,0.5>
    uv.x *= iResolution.x/iResolution.y; // fix aspect ratio

    vec3 col = drawScene(uv);

    // Output to screen
    fragColor = vec4(col,1.0);
}
繪製一個圓,使用鍵盤移動它。其實就是我們通過按鍵的值,應用到圓的偏移值上去。
![](https://img2020.cnblogs.com/blog/612959/202112/612959-20211228105633122-440919328.png)

如果你仔細觀察BufferA,會發現我在iChannel0中調用了自身。這是怎麼回事呢?當你在BufferA着色器使用自身時,會獲取fragColor的前一幀的值。這裏並非遞歸,GLSL代碼中不允許使用遞歸,而只能使用迭代。但這不能妨礙我們逐幀地使用幀緩衝。
texelFeth函數的作用是在紋理中查找單個紋素。但鍵盤並不算一個真正的紋理。所以它是如何工作的呢?Shadertoy本質上把所有的信息以紋理的形式進行存儲,這樣我們就有權限通過訪問紋理從而獲取任何的信息了。texelFeth檢查一個鍵盤是否被按下,決定是回退0或者前進1,再乘以一個veclocity的值來調整位移座標。位移值將會被傳遞到下一幀裏面去。最後它會被傳遞到主着色器中去。如果場景運行的速率是60幀,即一幀的切換速度是六十分之一秒,那麼在多個着色器之間,取最後一個幀的BufferA值,然後將像素繪製到畫布上面去。這個循環將會每秒60次地循環執行下去。其他的交互元素,例如麥克風也是通過這樣的方式被訪問的。你可以去閱讀Inigo Quilez創建的示例,這些例子都是關於如何在Shadertoy中使用各種交互元素。

總結

紋理在計算機圖形學以及遊戲開發中是一個很重要的概念。GLSL以及其他的一些語言爲訪問紋理數據提供了特定的函數。Shadertoy也爲我們提供了很多便捷的方法,讓我們快速地訪問可以交互的元素。你可以利用紋理儲存顏色值,或者其他完全不同的數據類型例如高度,位移,深度或者其他的任何你想要的東西。查看下面的資源學習使用更多的可交互元素。

引用資源

Khronos: Data Type (GLSL)
Khronos: texture
Khronos: texelFetch
Khronos: Sampler (GLSL)
2D Movement with Keyboard
Input - Keyboard
Input - Microphone
Input - Mouse
Input - Sound
Input - SoundCloud
Input - Time
Input - TimeDelta
Input - 3D Texture
Example - mainCubemap
Cheap Cubemap

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章