Loading

Unity Shader CGPROGRAM下的代码组织

前言

目前主流的着色器语言有HLSL,GLSL,Cg。三者在语法上也有诸多共通之处,选择一种学习即可。而Unity选择Cg作为着色器语言。在Shader编写的过程中,我们会经常穿梭在各个空间中,这里不对3D数学部分的前置知识作介绍,相关知识可从前面章节推荐的书籍学习。

在Shaderlab中,有三种着色器的书写方式。一种是Fixed-Function Shader,固定管线着色器。在这个着色器中,我们只能对渲染进行少量的配置,效果也很有限,在Unity 5.x之后的版本,Unity弃用了这种着色器。第二种是Surface Shader,表面着色器,这是Unity为我们提供的一种便于书写的方式,我们可以通过少量的代码,控制光照阴影等繁复的细节由Unity帮我们处理。新建一个Standard Surface Shader,可以看到里面只有50余行代码,但它包含了所有基础实现。最后一种,是Vertex/Fragment Shader,顶点/片元着色器,这是实现各种天马行空想象的最佳场所,当然,它的代码量以及复杂度也是最高的。而前两种shader也会被编译成对应的Vertex/Fragment Shader。这三种书写方式,都是在.shader文件中进行,组织方式上也是极为相似的。

CGPROGRAM

以之前模板的代码作为例子:

Shader "Blog/Start" {

    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Color ("Color", Color) = (1, 1, 1, 1)
    }

    SubShader {
        CGINCLUDE
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        float4 _MainTex_ST;
        fixed4 _Color;

        struct a2v {
            float4 vertex : POSITION;
            float2 texcoord : TEXCOORD0;
        };

        struct v2f {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
        };

        v2f vert(a2v v) {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            return o;
        }

        fixed4 frag(v2f i) : SV_Target {
            fixed4 color;
            color = tex2D(_MainTex, i.uv);
            color *= _Color;
            return color;
        }
        ENDCG

        Pass {

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }

    FallBack "Diffuse"
}

在这个Shader中,出现了两个不同的代码块。首先第一个是CGINCLUDE代码块,它可以被放置在任何位置,甚至是整个Shader代码块的外部。在这个代码块中,我们可以编写那些需要重用的代码(如顶点着色器或片元着色器)。然后是CGPROGRAM代码块。这个代码块需要放在Pass块内,否则编译器会把这个Shader当成Surface Shader转而去检索surf()函数进而引起报错。这个代码块也是定义Vertex/Fragment Shader的地方。要保证,每个Pass都有且只有一个Vertex Shader和Fragment Shader。这两个Shader通过#pragma编译命令指定。接着是两个结构体:

struct a2v {
	float4 vertex : POSITION;
	float2 texcoord : TEXCOORD0;
};

struct v2f {
	float4 pos : SV_POSITION;
	float2 uv : TEXCOORD0;
};

第一个是顶点着色器的输入结构体,a2v即Application To Vertex(应用阶段到顶点着色),在每一个变量的后面都跟了一个冒号说明,冒号后的是这个变量的Semantic(语义),语义是和GPU通信的桥梁,告诉GPU在这个变量中填充什么数据。float4 vertex : POSITION;告诉GPU,把顶点数据的POSITION(模型空间下的顶点坐标)输入到vertex变量中,float2 texcoord : TEXCOORD0;的意思是,把纹理坐标集0给texcoord变量使用。在着色器之间的数据传递都是依靠语义实现的,使用结构体只是为了代码组织更有条理。

v2f即Vertex To Fragment,这是顶点着色器的输出结构体,也是片元着色器的输入结构体。float4 pos : SV_POSITION;:SV指System Value,带有SV前缀的语义在管线中都有特殊的含义,SV_POSITION的含义是裁剪空间下的坐标。

为什么输出裁剪空间下的顶点坐标?

因为这个坐标接下来用于片元着色器,片元着色器需要的是光栅化后的坐标,也就是裁剪空间的坐标。

然后是顶点着色器部分:

v2f vert(a2v v) {
	v2f o;
	o.pos = UnityObjectToClipPos(v.vertex);
	o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
	return o;
}

这里只是做里简单的空间变换以及纹理映射。既然需要的是裁剪空间的坐标,那直接把输入的顶点坐标变换到裁剪空间即可。UnityObjectToClipPos()是Unity为我们提供的坐标空间转换函数。

// Tranforms position from object to homogenous space
inline float4 UnityObjectToClipPos(in float3 pos)
{
#if defined(STEREO_CUBEMAP_RENDER_ON)
    return UnityObjectToClipPosODS(pos);
#else
    // More efficient than computing M*VP matrix product
    return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
#endif
}
inline float4 UnityObjectToClipPos(float4 pos) // overload for float4; avoids "implicit truncation" warning for existing shaders
{
    return UnityObjectToClipPos(pos.xyz);
}

可以看到,这个函数处理了一些差别并重载了两个版本,但本质上,都是MVP矩阵右乘顶点坐标的列向量形式。然后是TRANSFORM_TEX宏。

// Transforms 2D UV by scale/bias property
#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)

可以看到这个宏计算了顶点对应的纹理采样位置,计算方式也对应了我们之前说到的_ST(Scale & Tiling,纹理缩放和偏移)相关知识点。然后是片元着色器部分:

fixed4 frag(v2f i) : SV_Target {
	fixed4 color;
	color = tex2D(_MainTex, i.uv);
	color *= _Color;
	return color;
}

片元着色器的最终目的是确定片元的像素颜色,即一个RGBA值。首先注意到的是SV_Target,它的语义是:这个着色器只返回一个值,这个值也就是片元的像素颜色值。此外,片元着色器还可以返回多个颜色,这时我们需要用到SV_TargetN语义,在这个情境下,SV_Target0是对应片元的像素颜色。例:

struct frag_output {
    fixed4 color0 : SV_Target0;
    fixed4 color1 : SV_Target1;
    fixed4 color2 : SV_Target2;
}

frag_output frag(v2f i) {
	frag_output output;
	// ...
	return output;
}

回到模板的片元着色器内部,首先是color = tex2D(_MainTex, i.uv);tex2D(sampler2D texture, float2 uv);是Cg为我们提供的一个纹理采样函数,它将按照输入的uv采样输入的纹理texture,最后返回采样颜色。然后是color *= _Color;这里只是简单的把采样颜色和shader外部给定的颜色做乘法处理(叠加)。在场景中新建一个sphere,把这个shader添加到一个新的material,再把这个material挂到sphere上,不出意外没有报错的话,即可得到一个纯白色的球(由于没有任何光照阴影计算,也没有给纹理赋值)。这是我们第一个生效的shader。

Sphere for First Shader

posted @ 2019-05-03 21:55  Vel'Koz  阅读(1836)  评论(1编辑  收藏  举报