Unity Shader:几何着色器

在我翻译过的OpenGL和实时渲染相关文章中,简要介绍过几何着色器,它的执行顺序位于细分曲面着色器、光栅化与片元着色器之间,有时不会使用细分曲面着色器,且常不表示固定阶段,所以简要来说,顶点着色器的输出到几何着色器,接着进行某些增减基本体的操作,然后进入片元着色器进行光照计算操作。

几何着色器本质上最常用的是增加基本体,可以用于生成粒子,毛发等。

基本的几何着色器

Unity中,和顶点与片元着色器一样,使用一个预编译指令来标明函数,且额外定义一个结构体用于变量的输入输出,供着色器之间通信。这里给出一个简单的几何着色器例子:

Shader "Unlit/SimpleGeometryShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2g
            {
                float vertex: SV_POSITION;
                float uv : TEXCOORD0;
            };

            struct g2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2g vert (appdata v)
            {
                v2g o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            [maxvertexcount(3)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
            {
                g2f o;
                for(int i = 0;i < 3; i++)
                {
                    o.vertex = UnityObjectToClipPos(IN[i].vertex);
                    UNITY_TRANSFER_FOG(o, o.vertex);
                    o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
                    triStream.Append(o);
                }
                triStream.RestartStrip();
            }

            fixed4 frag (g2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

注意三个结构体:

  • appdata,将物体的属性传入顶点着色器。
  • v2g,将数据从顶点着色器传入几何着色器。
  • g2f,将数据从几何着色器传入片元着色器。

对于将顶点转换到裁剪空间,以及变换UV的操作,我们在几何着色器中执行了,实际上也可以在顶点着色器中执行,只是这么做的话,更灵活。

几何着色器讲解

对于几何着色器函数,这里复制一下:

            [maxvertexcount(3)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
            {
                g2f o;
                for(int i = 0;i < 3; i++)
                {
                    o.vertex = UnityObjectToClipPos(IN[i].vertex);
                    UNITY_TRANSFER_FOG(o, o.vertex);
                    o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
                    triStream.Append(o);
                }
                triStream.RestartStrip();
            }

maxvertexcount代表几何着色器会增加的顶点的最大数量。由于我们在物体上实际添加一个三角形,所以这里设置为3.

接下来讲解几何着色器函数的参数:

  • triangle v2g IN[3]:这是有3个v2g元素的数组,每个元素与我们所编辑的三角形的一个顶点绑定。triangle标签表明几何着色器会将一个三角形作为输入。也可以用line(数组大小为2)或point(数组大小为1)。
  • inout TriangleStream<g2f> triStream:我们可以看到,该函数的返回值为空,所以实际上我们没有返回一个物体。几何着色器实际上将每个三角形添加到一个TriangleStream列表中,类型为g2f。如果想要输出线或点的话,可以用inout LineStream<g2f> lineStreaminout PointStream<g2f> pointStream

接着讲解函数体。首先我们定义一个g2f的结构体对象,我们会对其进行操作然后加入列表中。

然后,我们进行一个简单的循环,将每个输入的顶点添加到流中,创建三角形。因为数据要传入片元着色器,因此我们将顶点坐标转换到裁剪空间,并且按系数偏移UV。

最后,我们使用triStream.Append(o)将一个修改过的g2f结构体添加到三角形流中。在结束循环后,使用RestartStrip函数,这可以让流明白一个独立的三角形要在之后添加。这里并没有额外生成顶点等,一切保持原状。

挤出金字塔

现在我们扩展这个简单的几何着色器,我们从每个三角形上挤出一个金字塔的形状,即在三角形中心添加一个顶点,然后沿法线方向挤出面。


Shader "Unlit/SimpleGeometryShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ExtrusionFactor("Extrusion factor", float)=0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2g
            {
                float4 vertex: SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct g2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float4 color : COLOR;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _ExtrusionFactor;

            v2g vert (appdata v)
            {
                v2g o;
                o.vertex = v.vertex;
                o.uv = v.uv;
                o.normal = v.normal;
                return o;
            }

            [maxvertexcount(12)]
            void geom(triangle v2g IN[3], inout TriangleStream<g2f> triStream)
            {
                g2f o;
                // 重心
                float4 barycenter = (IN[0].vertex + IN[1].vertex + IN[2].vertex)/3;
                float3 normal = (IN[0].normal + IN[1].normal + IN[2].normal)/3;
                 // 构成金字塔的三个三角形
                for(int i = 0;i < 3; i++)
                {
                    // i=0:1;i=1:2;i=2:1;
                    // 即计算相邻顶点索引
                    int next = (i+1)%3;

                    o.vertex = UnityObjectToClipPos(IN[i].vertex);
                    UNITY_TRANSFER_FOG(o, o.vertex);
                    o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
                    o.color = fixed4(0.0,0.0,0.0,1.0);
                    triStream.Append(o);

                    // 金字塔三角形顶尖顶点
                    o.vertex = UnityObjectToClipPos(barycenter + float4(normal, 0.0)*_ExtrusionFactor);
                    UNITY_TRANSFER_FOG(o, o.vertex);
                    o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
                    o.color = fixed4(1.0,1.0,1.0,1.0);
                    triStream.Append(o);

                    o.vertex = UnityObjectToClipPos(IN[next].vertex);
                    UNITY_TRANSFER_FOG(o, o.vertex);
                    o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
                    o.color = fixed4(0.0,0.0,0.0,1.0);
                    triStream.Append(o);
                }
                triStream.RestartStrip();

                // 组装最基本的三角形
                for(int i = 0;i < 3;i++)
                {
                    o.vertex = UnityObjectToClipPos(IN[i].vertex);
                    UNITY_TRANSFER_FOG(o, o.vertex);
                    o.uv = TRANSFORM_TEX(IN[i].uv, _MainTex);
                    o.color = fixed4(0.0,0.0,0.0,1.0);
                    triStream.Append(o);
                }
                triStream.RestartStrip();
            }

            fixed4 frag (g2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

在最开始,我们添加了_ExtrusionFactor属性,用于控制挤出的高度。接着开启Cull Off,关闭面消隐。

在几何着色器函数中,我们首先设置最大输出顶点数量,相当于生成4个三角形,即在每个三角形的基础上生成3个三角形来组成锥体。

对于挤出操作,我们需要每个三角形的中心作为法线,因此,我们通过对三角形各个顶点的坐标做平均计算来得到重心,然后同样通过平均计算来得到三角形的法线。

然后我们生成锥体,算法过程为:

对于每个三角形的点
    得到下一个点的索引
    在当前点的位置处增加一个顶点
    在三角形重心处添加一个顶点,然后沿沿面的法线挤出,乘等于`_ExtrusionFactor`
    在下一个点处添加一个顶点

这就是第一个循环所做的事情。第二个循环中,我们将最开始的三角形组装起来,然后就可以得到想要的三角锥了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343