【ShaderToy】基礎篇之談談點、線的繪製,shadertoy基礎篇

來源:互聯網
上載者:User

【ShaderToy】基礎篇之談談點、線的繪製,shadertoy基礎篇


寫在前面


寫前面一篇的時候,發現還是不夠基礎。因此打算增加幾篇基礎篇,從點線面開始,希望可以更好理解。


其實用Pixel Shader的過程很像在紙上繪畫的過程。螢幕上的每一個像素對應了紙上的一個方格,如果你願意,你甚至可以一個個判斷像素的位置,從而畫出任何你想畫的映像,也的確有愛好者這麼做過。但往往,我們需要的是一個動態效果,這個效果往往依賴於數學公式的約束。我們可以說是,用數學去繪畫。我們用數學去約束,哪些點應該用什麼顏色去繪製。


這篇,我們從基本的點和線開始,看一下如何在Pixel Shader裡面隨心畫出點和線。



在哪裡畫


在開始之前,有一個最基本的問題我們要計算清楚,就是如何知道當前計算的fragment的像素位置。在之前的開篇中,我們給出了模板。其中v2f結構裡,有一個很重要的變數srcPos,它的計算如下:

        v2f vert(appdata_base v) {          v2f o;        o.pos = mul (UNITY_MATRIX_MVP, v.vertex);            o.srcPos = ComputeScreenPos(o.pos);              o.w = o.pos.w;            return o;            }  

ComputeScreenPos是在UnityCG.cginc中定義的函數,它就作用如名字一樣,計算該頂點轉換到螢幕上的位置。但如果我們想要得到正確的螢幕位置,還需要在frag函數中這樣:

        fixed4 frag(v2f _iParam) : COLOR0 {         vec2 fragCoord = gl_FragCoord;        return main(gl_FragCoord);        }  

其中:

  // 螢幕的尺寸  #define iResolution _ScreenParams  // 螢幕中的座標,以pixel為單位  #define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy) 

難懂的是gl_FragCoord的定義。(_iParam.srcPos.xy/_iParam.srcPos.w)將 得到在螢幕中歸一化後的螢幕位置,即返回分量範圍在(0, 1)的螢幕橫縱座標值。 螢幕的左下角值為(0, 0),右上方值為(1, 1)。然後再乘以螢幕的長款像素值,就得到了該fragment對應的螢幕像素位置。這是我們後面計算的基礎。


根據不同的需求,我們會在shader中對位置有不同的需求:有時我們想要得到如上的像素位置,有時我們想得到相對於螢幕中心的uv座標等等。以下有五種常見的位置需求:


vec4 main(vec2 fragCoord) {vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)vec2 pos = fragCoord.xy / iResolution.xy; // pos.x ~ (0, 1), pos.y ~ (0, 1)vec2 pos = fragCoord / min(iResolution.x, iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (0, 1.xx), pos.y ~ (0, 1)vec2 pos =fragCoord.xy / iResolution.xy * 2. - 1.; // pos.x ~ (-1, 1), pos.y ~ (-1, 1)vec2 pos = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.x,iResolution.y);// If iResolution.x > iResolution.y, pos.x ~ (-1.xx, 1.xx), pos.y ~ (-1, 1)return vec4(1);}

當然需求不同,一開始計算的pos也會不同。



畫第一個點(圓)


在Shader中,一個點其實可以當成一個圓形。那麼問題就變成了如何繪製一個圓:給定圓心在螢幕的位置(圓心像素值占螢幕的百分比),圓的半徑(像素為單位),以及圓的顏色,如何繪製一個圓。


為此,我們先需要在Properties中聲明兩個參數:_Parameters和_Color:

Shader "shadertoy/Simple Circle" { Properties{_Parameters ("Circle Parameters", Vector) = (0.5, 0.5, 10, 0) // Center: (x, y), Radius: z_Color ("Circle Color", Color) = (1, 1, 1, 1)}

_Parameters的x和y分量表示圓心在螢幕中的uv值(即範圍在(0, 1)),z分量是圓的半徑,單位是像素。


圓在數學裡面的運算式相對簡單,即到圓心距離小於半徑的點就在圓內。那麼事情就變得簡單了:只要在圓內的點就是用設定的顏色繪製,否則用背景色繪製(黑色)。

        vec4 circle(vec2 pos, vec2 center, float radius, float4 color) {        if (length(pos - center) < radius) {        // In the circle        return vec4(1, 1, 1, 1) * color;        } else {        return vec4(0, 0, 0, 1);        }        }

vec4 main(vec2 fragCoord) {vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)//vec2 pos = fragCoord.xy / iResolution.xy; // pos.x ~ (0, 1), pos.y ~ (0, 1)//vec2 pos = fragCoord / min(iResolution.x, iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (0, 1.xx), pos.y ~ (0, 1)//vec2 pos =fragCoord.xy / iResolution.xy * 2. - 1.; // pos.x ~ (-1, 1), pos.y ~ (-1, 1)//vec2 pos = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.x,iResolution.y);// If iResolution.x > iResolution.y, pos.x ~ (-1.xx, 1.xx), pos.y ~ (-1, 1)return circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _Color);}

得到的效果如下:

 


我不要鋸齒!


上面得到的圓在邊緣處有一些小鋸齒,這當然是我們無法忍受的拉!Shader中消除鋸齒的原理大概是這樣:由於原來非A即B的計算會使得A和B的交界處產生鋸齒(例如上面圓的邊界),因此我們只需要在A和B的邊界平緩過渡即可。這往往需要透明度的配合,即使用透明度來混合顏色。


在shader中,一種常見的消除鋸齒(平滑)操作是使用smoothstep函數(當然有人詬病這種方法不直觀,但我覺得挺好用的。。。whatever~)。smoothstep函數在CG文檔裡面是這樣的:

Interpolates smoothly from 0 to 1 based on x compared to a and b.1) Returns 0 if x < a < b or x > a > b1) Returns 1 if x < b < a or x > b > a3) Returns a value in the range [0,1] for the domain [a,b].

也就是說它的傳回值範圍總是在(0, 1),也就是透明度的取值範圍,這也是為何它在消除鋸齒領域如此受歡迎的原因了吧。


這樣,我們就可以改寫原來的circle函數:

        vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {        float d = length(pos - center) - radius;        float t = smoothstep(0, antialias, d);        return vec4(color, 1.0 - t);        }

antialias就是平滑過渡的邊界範圍。為了方便調試,我們可以在shader中利用_Parameters的z分量作為消除鋸齒因子,當然在實際工程中可以設為定值。接下來就是和背景顏色進行混合,我們使用的是lerp函數(在ShaderToy中對應的是mix函數):


vec4 main(vec2 fragCoord) {vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);vec4 layer2 = circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);return mix(layer1, layer2, layer2.a);}

完整代碼如下:

Shader "shadertoy/Simple Circle" { Properties{_Parameters ("Circle Parameters", Vector) = (0.5, 0.5, 10, 1) // Center: (x, y), Radius: z_CircleColor ("Circle Color", Color) = (1, 1, 1, 1)_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)}  CGINCLUDE     #include "UnityCG.cginc"     #pragma target 3.0        #define vec2 float2  #define vec3 float3  #define vec4 float4  #define mat2 float2x2  #define iGlobalTime _Time.y  #define mod fmod  #define mix lerp  #define atan atan2  #define fract frac   #define texture2D tex2D  // 螢幕的尺寸  #define iResolution _ScreenParams  // 螢幕中的座標,以pixel為單位  #define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy)     #define PI2 6.28318530718  #define pi 3.14159265358979  #define halfpi (pi * 0.5)  #define oneoverpi (1.0 / pi)    float4 _Parameters;  float4 _CircleColor;  float4 _BackgroundColor;          struct v2f {                float4 pos : SV_POSITION;                float4 srcPos : TEXCOORD0;          };                             //   precision highp float;        v2f vert(appdata_base v) {          v2f o;        o.pos = mul (UNITY_MATRIX_MVP, v.vertex);            o.srcPos = ComputeScreenPos(o.pos);              return o;            }                  vec4 main(vec2 fragCoord);                fixed4 frag(v2f _iParam) : COLOR0 {         vec2 fragCoord = gl_FragCoord;        return main(gl_FragCoord);        }                  vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {        float d = length(pos - center) - radius;        float t = smoothstep(0, antialias, d);        return vec4(color, 1.0 - t);        }        vec4 main(vec2 fragCoord) {vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);vec4 layer2 = circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);return mix(layer1, layer2, layer2.a);}    ENDCG          SubShader {         Pass {            CGPROGRAM                  #pragma vertex vert                #pragma fragment frag                #pragma fragmentoption ARB_precision_hint_fastest                   ENDCG            }        }         FallBack Off    }

消除鋸齒效果如下:

 

 



畫兩個點


我們現在來看看如何畫出更多的點。之前的circle函數已經可以畫出任何一個大小、圓心的圓了,現在的問題僅僅是如何將這些元素都添加到畫布上。一種基本的思想就是圖層疊加,這很像我們在Photoshop中做的事情:背景在最後一層,我們只需要增加新的圖層,並確保它們按層級順序一層層向上排列即可。所以,我們可以這樣做:

vec4 main(vec2 fragCoord) {vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);vec2 point1 = vec2(0.3, 0.8);vec2 point2 = vec2(0.8, 0.2);vec4 layer2 =  circle(pos, point1 * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);vec4 layer3 =  circle(pos, point2 * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);vec4 fragColor = mix(layer1, layer2, layer2.a);fragColor = mix(fragColor, layer3, layer3.a);return fragColor;}

上面的代碼中,我們繪製了兩個圓,一個圓心位置在(0.3, 0.8)處,一個在(0.8, 0.2)處。layer1仍舊是背景層,layer2和layer1分別表示兩個圓所在圖層。我們按照層級順序依次調用lerp函數(也就是代碼中的mix函數)即可以混合這些元素。結果如下:

 



這種思想可以延伸到任意層數的元素疊加~



畫一條直線


現在,我們已經知道了直線上的兩個點,那麼我們來看看如何畫出過這兩個點的直線。首先,我們可以聲明一個空的畫直線的函數,並且添加一個新的圖層給它,後續再填充這個函數:

        vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {        return vec4(0);        }                vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {        float d = length(pos - center) - radius;        float t = smoothstep(0, antialias, d);        return vec4(color, 1.0 - t);        }        vec4 main(vec2 fragCoord) {vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)vec2 point1 = vec2(0.3, 0.8) * iResolution.xy;vec2 point2 = vec2(0.8, 0.2) * iResolution.xy;vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);vec4 layer2 = line(pos, point1, point2, _LineWidth, _LineColor.rgb, _Antialias);vec4 layer3 =  circle(pos, point1, _CircleRadius, _CircleColor.rgb, _Antialias);vec4 layer4 =  circle(pos, point2, _CircleRadius, _CircleColor.rgb, _Antialias);vec4 fragColor = mix(layer1, layer2, layer2.a);fragColor = mix(fragColor, layer3, layer3.a);fragColor = mix(fragColor, layer4, layer4.a);return fragColor;}

為了方便,上面新定義了幾個參數。後面會給出完整代碼,但我相信這裡也很好懂。對於圖層的順序我也進行了調增,即第二層為直線,後面是點圖層,這是因為我們希望點的顏色可以覆蓋直線顏色。儲存後返回查看結果,是沒有任何變化的,因為此時直線圖層返回的顏色的透明度為0。


注意了!!!現在又到了數學魅力展現的時刻了!!!繪製直線的思想其實和圓很類似,我們只需要判斷像素位置是否在直線內(因為這裡的直線是有寬度的)就可以了:判斷像素點到直線的距離是否小於直線寬度的一半。那麼,我們只需要回想起當年的點到直線距離公式。你是不是忘記了!!!這裡直接給出答案了,公式也很簡單:

        vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {        float k = (point1.y - point2.y)/(point1.x - point2.x);    float b = point1.y - k * point1.x;        float d = abs(k * pos.x - pos.y + b) / sqrt(k * k + 1);    float t = smoothstep(width/2.0, width/2.0 + antialias, d);        return vec4(color, 1.0 - t);        }

我們先求出了直線公式的參數k和b(y = k * x + b)。這裡沒有進行高中時期的臨界判斷,但shader也沒有報錯呦~然後,我們計算像素點到直線的距離d。在計算透明度因子t的時候,我們考慮了直線的寬度,這意味著,當距離d小於width/2.0的時候將返回0,也就是說該點絕對在直線內;再通過反鋸齒因子進行消除鋸齒計算。


很簡單有木有!!!效果如下:

 

 



完整的代碼如下:

Shader "shadertoy/Simple Line" { Properties{_CircleRadius ("Circle Radius", float) = 5_CircleColor ("Circle Color", Color) = (1, 1, 1, 1)_LineWidth ("Line Width", float) = 5_LineColor ("Line Color", Color) = (1, 1, 1, 1)_Antialias ("Antialias Factor", float) = 3_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)}  CGINCLUDE     #include "UnityCG.cginc"     #pragma target 3.0        #define vec2 float2  #define vec3 float3  #define vec4 float4  #define mat2 float2x2  #define iGlobalTime _Time.y  #define mod fmod  #define mix lerp  #define atan atan2  #define fract frac   #define texture2D tex2D  // 螢幕的尺寸  #define iResolution _ScreenParams  // 螢幕中的座標,以pixel為單位  #define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy)     #define PI2 6.28318530718  #define pi 3.14159265358979  #define halfpi (pi * 0.5)  #define oneoverpi (1.0 / pi)    float _CircleRadius;  float4 _CircleColor;  float _LineWidth;  float4 _LineColor;  float _Antialias;  float4 _BackgroundColor;          struct v2f {                float4 pos : SV_POSITION;                float4 srcPos : TEXCOORD0;          };                             //   precision highp float;        v2f vert(appdata_base v) {          v2f o;        o.pos = mul (UNITY_MATRIX_MVP, v.vertex);            o.srcPos = ComputeScreenPos(o.pos);              return o;            }                  vec4 main(vec2 fragCoord);                fixed4 frag(v2f _iParam) : COLOR0 {         vec2 fragCoord = gl_FragCoord;        return main(gl_FragCoord);        }                  vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {        float k = (point1.y - point2.y)/(point1.x - point2.x);    float b = point1.y - k * point1.x;        float d = abs(k * pos.x - pos.y + b) / sqrt(k * k + 1);    float t = smoothstep(width/2.0, width/2.0 + antialias, d);        return vec4(color, 1.0 - t);        }                vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {        float d = length(pos - center) - radius;        float t = smoothstep(0, antialias, d);        return vec4(color, 1.0 - t);        }        vec4 main(vec2 fragCoord) {vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)vec2 point1 = vec2(0.4, 0.1) * iResolution.xy;vec2 point2 = vec2(0.7, 0.8) * iResolution.xy;vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);vec4 layer2 = line(pos, point1, point2, _LineWidth, _LineColor.rgb, _Antialias);vec4 layer3 =  circle(pos, point1, _CircleRadius, _CircleColor.rgb, _Antialias);vec4 layer4 =  circle(pos, point2, _CircleRadius, _CircleColor.rgb, _Antialias);vec4 fragColor = mix(layer1, layer2, layer2.a);fragColor = mix(fragColor, layer3, layer3.a);fragColor = mix(fragColor, layer4, layer4.a);return fragColor;}    ENDCG          SubShader {         Pass {            CGPROGRAM                  #pragma vertex vert                #pragma fragment frag                #pragma fragmentoption ARB_precision_hint_fastest                   ENDCG            }        }         FallBack Off    }



寫在最後


這篇寫的很基礎,但有些知識是值得真實項目裡借鑒的。有些人說ShaderToy裡的只是玩具,沒有價值,我個人是不這麼覺得啦~遊戲裡很多細膩的動畫效果是無法僅僅靠貼圖來完成的,瞭解些基本或者稍微複雜一點的shader計算還是很有好處滴~


最後,關於ShaderToy的用處,還有一點就是我們可以自己改進成非Pixel Shader的版本,例如利用模型的uv座標去代替螢幕座標等等。更多的用處等待自己去發現啦!





聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.