Unity Shader - Vertex and fragment shader examples 顶点和片段着色器的例子

目录:Unity Shader - 知识点目录(先占位,后续持续更新)
原文:Vertex and fragment shader examples
版本:2019.1

Vertex and fragment shader examples

顶点和片段着色器的例子

这个页面将包含了顶点与片段程序的例子。介绍着色器的一些基础,可查看着色器教程:Part 1Part 2。想用简单的方式来编写常规的材质着色器,可查看Surface Shaders

你可以下载下面已展示的例子 zipped Unity project相关项目。
(如果下载不了,或是下载很慢,可以点击(提取码: vddu)这里,从百度网盘下载)

Setting up the scene

设置好你的场景

如果你对Unity的Scene, Hierarchy, Project, Inspector视图都不熟悉,那么你现在不太适合这些章节内容,你应该先去手册上了解一些Unity Basics(基础)。

第一步创建一些你将用于测试着色器而用的对象。菜单栏Game Object > 3D Object > Capsule。然后定为好相机或是Capsule(胶囊体)的位置,以便于我们能在场景看到他。在Hierarchy视图中双击Capsule对象,可以快速定位对象在Scene视图上,然后在Hierarchy选择Main Camera对象,再到菜单栏中选择Game object > Align with View,可以将Game视图的相机快速与Scene视图的匹配上。
在这里插入图片描述
在Project视图中,右键菜单Create > Material来创建一个Material材质。新建号的材质名为New Material出现在Project视图中。
在这里插入图片描述

Creating a shader

创建一个着色器

现在使用类似的方式创建新的Shader着色器。在Project视图,右键菜单Create > Shader > Unlit Shader。这就创建好了一个基础的shader。
在这里插入图片描述
你也可以从Create > Shader菜单中创建其他类型的shader,如:surface shader

Linking the mesh, material and shader

关联上网格,材质和着色器

在Meterial的Inspector中可以指定材质使用的shader,或是在Project视图中将一个shader资源拖拽到材质资源上。当你使用这个NewUnlitShader时,Material Inspector视图上将显示出一个白色的圆球。
在这里插入图片描述
现在将Project视图的材质资源拖拽到Scene视图或是Hierarchy视图中你想要应用的网格对象。或是你可以在Project或是Scene选择你的网格对象,在MeshRenderer组件的Materials的列表槽位中设置你的材质。
在这里插入图片描述
完成这些步骤后,你将会看到这么一个使用NewUnlitShader的材质显示的效果。

Main parts of the shader

着色器的主要部分

在开始shader的实验前,在Project视图中双击打开shader资源。shader代码将使用的你脚本编辑器打开(MonoDevelop or Visual Studio)。

Shader代码:

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

这么个shader看起来不太简单。但别担心,我们将一步步的讲解。

让我们看看主要部分:

Shader

Shader命令后面包含一个Shader的名称字符串。你可以使用"/"斜杆字符来划分在Material Inspector中的Shader菜单中显示。

Properties

Properties块包含shader的变量(纹理,颜色,等等),这些变量可以显示在Material Inspector中。在我们的Unlit shader模板中,有一个纹理属性定义。

SubShader

一个Shader可以包含一个或多个SubShaders,主要就用于兼容不同的GPU而实现的shader。在这个教程,我们不太涉及太多这点知识,所以我们所有的shader都只有一个SubShader。

Pass

每个SubShader都是有一个passes来组成的,每个Pass代表着一个顶点与片段程序代码渲染时的一个绘制。许多简单的shader都仅仅只有一个pass,但如果想与光照有交互的话,可能需要多个(查看Lighting Pipeline了解更多)。Pass命令通常会设置一些固定函数的状态,如混合模式。

__CGPROGRAM__ .. ENDCG

该关键字将HLSL的vertex and fragment着色器代码都括起来。这是些最有趣的代码部分了。查看vertex and fragment shaders了解更多。

Simple unlit shader

简单的无光照shader

unlit shader模型处理了一个内容,同时需要一张显示用的纹理。该shader也支持雾化,在Material inspector中可以设置纹理,及tiling/offset自定。我们将该shader最简化,并添加一些注释吧:

Shader "Unlit/SimpleUnlitTexturedShader"
{
    Properties
    {
        // 我们将纹理的tiling/offset属性删除,并从material inspector中不显示它们
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            // 使用 "vert" 函数作为顶点着色器
            #pragma vertex vert
            // 使用 "frag" 函数作为像素(片段)着色器
            #pragma fragment frag
            // 顶点着色器的输入
            struct appdata
            {
                float4 vertex : POSITION; // 顶点坐标
                float2 uv : TEXCOORD0; // 纹理坐标
            };
            
            // 顶点着色器的输出("顶点传给片段")
            struct v2f
            {
                float2 uv : TEXCOORD0; // 纹理坐标
                float4 vertex : SV_POSITION; // 裁剪空间位置
            };

            // 顶点着色器
            v2f vert (appdata v)
            {
                v2f o;
                // 将坐标转换到裁剪空间
                // (与model*view*projection 矩阵相乘)
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                // 将纹理坐标传过去
                o.uv = v.uv;
                return o;
            }
            
            // 用于采样的纹理
            sampler2D _MainTex;

            // 像素着色器,返回低精度("fixed4"类型)
            // color ("SV_Target" 语义)
            fixed4 frag (v2f i) : SV_Target
            {
                // 采样纹理并返回数据
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

Vertex Shader(顶点着色器)程序将对模型上每个顶点执行一遍的程序。通常不会处理什么有趣的事。这里我们仅仅将顶点坐标从对象空间转换到所谓的"clip space"裁剪空间,用于GPU对屏幕中对象光栅化处理的。我们也将输入的纹理坐标传过去,将用于片段着色器对纹理采样用的。

Fragment Shader(片段着色器)程序运行于屏幕上每个像素,且通常用于计算每个像素的颜色输出。通常屏幕上有几百万个像素,片段着色器将对每个像素都执行一遍!所以优化像素着色器是整个游戏中非常重要的性能处理。

某些变量或是函数的定义后面跟着Semantic Signifier(语义符号),如 : POSITION或是 : SV_Target。这些语义符号将变量的意义传给了GPU。查看shader semantics页面了解详情。

一个漂亮的模型加上漂亮的纹理,就算用在我们的简单shader中看起来也不错!
在这里插入图片描述

Even simpler single color shader

更简单的颜色着色器

让我们更进一步的简化shader,让shader绘制对象时只用一种颜色。这虽然没啥用,但这里我们测试用而已。

Shader "Unlit/SingleColor"
{
    Properties
    {
        // Material Inspector中的Color属性,默认为白色
        _Color ("Main Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            // 顶点着色器
            // 这次我们不用"appdata"结构体数据,也不返回v2f结构体,我们仅仅输入一个float4 vertex : POSITION,和返回输出一个float4的SV_POSITION裁剪空间坐标
            float4 vert (float4 vertex : POSITION) : SV_POSITION
            {
                return mul(UNITY_MATRIX_MVP, vertex);
            }
            
            // 材质属性中对象的_Color属性
            fixed4 _Color;

            // 像素着色器,不需要输入
            fixed4 frag () : SV_Target
            {
                return _Color; // 仅仅将颜色返回
            }
            ENDCG
        }
    }
}

这次我们不使用输入的appdata结构体和输出的v2f结构体数据,shader函数输入,输出我们都简单的手写一下。之前的方式与现在的方式都没有问题,就看你的编码风格与喜欢。
在这里插入图片描述

Using mesh normals for fun and profit

使用网格法线的乐趣与好处

我们使用shader来显示世界空间下的网格的法线。

Shader "Unlit/WorldSpaceNormals"
{
    // 这次我们两Properties块都不需要
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // include头文件,需要用到UnityObjectToWorldNormal函数
            #include "UnityCG.cginc"

            struct v2f {
                // 输出世界空间下的法线,使用TEXTCOORD0语义的插值器
                half3 worldNormal : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

			// 顶点着色器:将另外带上对象空间下的法线作为输入
            v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                // UnityCG.cginc文件包含的UnityObjectToWorldNormal函数用来将法线从对象空间转换到世界空间
                o.worldNormal = UnityObjectToWorldNormal(normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 c = 0;
                // 法线是个3D向量,有分量:xyz,都再-1~1的范围
                // 作为颜色显示,我们需要将其转换到0~1的范围,并将其输出到RGB颜色分量
                c.rgb = i.worldNormal*0.5+0.5;
                return c;
            }
            ENDCG
        }
    }
}

在这里插入图片描述
这里我们将法线用于显示漂亮的颜色,法线也用于图形各种效果,如:光照,反射,轮廓描边,等。

在上面的shader中,我们使用了内置的shader include files。这里的UnityCG.cginc头文件包含了方便使用的UnityObjectToWorldNormal函数。我们也使用了UnityObjectToClipPos函数,将顶点从对象空间转换到屏幕(裁剪空间)上。这会让代码可读性更高,且在某些情况下效能更高。

我们也看到了从顶点传到片段着色器的数据,我们称之为:“interpolators”(插值器)。在HLSL着色器语言中,通常使用TEXCOORDn语义,每个插值器都是带有4个分量的向量(查看semantics语义页面了解详情)。

我们也学到了如何将可视的法线向量(范围:-1~1)转换到颜色(0~1范围):仅仅将他们乘以0.5再加上0.5即可。了解更多顶点数据可视化例子,查看vertex program inputs页面。

Environment reflection using world-space normals

使用世界空间法线实现环境反射

当使用Skybox(天空盒)用作经常的反射效果源时,本质上默认是Reflection Probe(反射探测器)的创建,包含了天空盒的数据。反射探测器内部是一个Cubemap纹理(立体纹理);我们将使用上面的shader的世界空间下的法线来实现。

现在代码稍微开始有丢丢复杂了。当然,如果你想shader自动处理光效,阴影,反射,还有其他的光效系统的功能,你可以使用surface shaders。这个例子将为了展示如何手动实现一部分光照系统的光照效果。

Shader "Unlit/SkyReflection"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f {
                half3 worldRefl : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                // 计算顶点的世界坐标
                float3 worldPos = mul(_Object2World, vertex).xyz;
                // 计算世界空间下的视角方向
                float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
                // 世界空间下的法线
                float3 worldNormal = UnityObjectToWorldNormal(normal);
                // 世界空间下的反射向量
                o.worldRefl = reflect(-worldViewDir, worldNormal);
                return o;
            }
        
            fixed4 frag (v2f i) : SV_Target
            {
                // 使用反射向量采样默认的反射立体纹理(unity_SpecCube0)
                half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, i.worldRefl);
                // 解码cubemap的数据到具体显示的颜色
                half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
                // 输出颜色
                fixed4 c = 0;
                c.rgb = skyColor;
                return c;
            }
            ENDCG
        }
    }
}

在这里插入图片描述
上面例子中使用了built-in shader include files内置着色器文件的一些东西:

  • unity_SpecCube0, unity_SpecCube0_HDRObject2WorldUNITY_MATRIX_MVP,的这些built-in shader variables(内置着色器变量)。
  • UNITY_SAMPLE_TEXCUBEbuilt-in macro(内置的宏)用来采样cubemap的。大多数cubemap是需要被定义的且使用标准的HLSL函数来采样(samplerCUBEtexCUBE),然而反射探测立体贴图在Unity中被定义在一个sampler槽位了。如果你不知道这些信息,别担心,仅需要了解unity_SpecCube0立体贴图用于UNITY_SAMPLE_TEXCUBE宏定义来采样用的。
  • UnityCG.cginc头文件的UnityWorldSpaceViewDir函数,和DecodeHDR函数。后者是用于获取反射探测器数据的颜色,反射探测器数据来自于Unity使用特殊的编码将反射数据存在Reflection Probe Cubemap里头。
  • reflect是HLSL内置的函数,用于计算:给定入射方向,与法线方向,计算出反射向量。

Environment reflection with a normal map

环境反射+法线贴图

通常Normal Maps(法线贴图)用于给对象添加更多的细节,而不需要更多的几何体。让我们看看如何让shader使用法线贴图、环境反射的效果。

现在数学方面的内容会开始复杂了,我们将需要处理这么几步的内容。在上面的shader中,反射方向是在顶点着色器中计算的,而在片段着色器中仅仅是对反射探测的cubemap的反射采样。然而,一旦我们使用了法线贴图,那么表面的法线计算将在逐像素执行了,意味在我们不得不在逐像素计算环境反射向量。

首先,我们重写上面的shader,将一些计算挪到像素着色器中计算:

Shader "Unlit/SkyReflection Per Pixel"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f {
                float3 worldPos : TEXCOORD0;
                half3 worldNormal : TEXCOORD1;
                float4 pos : SV_POSITION;
            };

            v2f vert (float4 vertex : POSITION, float3 normal : NORMAL)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                o.worldPos = mul(_Object2World, vertex).xyz;
                o.worldNormal = UnityObjectToWorldNormal(normal);
                return o;
            }
        
            fixed4 frag (v2f i) : SV_Target
            {
                // 逐像素计算视角方向和反射向量
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 worldRefl = reflect(-worldViewDir, i.worldNormal);
				
				// 与之前的shader一样
                half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
                half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
                fixed4 c = 0;
                c.rgb = skyColor;
                return c;
            }
            ENDCG
        }
    }
}

着色器看起来完全一样,只是现在它运行得更慢了,因为它为屏幕上的每个像素做了更多的计算,而不是仅仅为模型的每个顶点。然而,我们很快就会需要这些计算。更逼真图像通常更复杂的着色器。

现在我们将要学习新玩意了,它们叫"tangent space"(切线空间)。法线贴图纹理通常是表达模型的表面空间下的坐标空间。在我们的shader里,我们将需要了解切线空间的基向量,从法线纹理中读取法线向量,并将它转换到世界空间下,然后想上面的shader一样处理数学计算就可以了。OK,让我们开始吧!

Shader "Unlit/SkyReflection Per Pixel"
{
    Properties {
        // 法线贴图纹理
        _BumpMap("Normal Map", 2D) = "bump" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f {
                float3 worldPos : TEXCOORD0;
                // 这三个向量相当于矩阵
                // 用于将切线空间转换到世界空间的矩阵
                half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
                half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
                half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
                // 法线的uv坐标
                float2 uv : TEXCOORD4;
                float4 pos : SV_POSITION;
            };

            // 顶点着色器现在需要处理切线向量
            // 在Unity切线需要4D向量,w用于表示副切线向量的方向
            // 我们也需要纹理坐标
            v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                o.worldPos = mul(_Object2World, vertex).xyz;
                half3 wNormal = UnityObjectToWorldNormal(normal);
                half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
                // 用法线与切线叉乘计算得副切线
                half tangentSign = tangent.w * unity_WorldTransformParams.w;
                half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
                // 输出切线空间的转换矩阵
                o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
                o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
                o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
                o.uv = uv;
                return o;
            }

            // shader properties里的法线贴图纹理
            sampler2D _BumpMap;
        
            fixed4 frag (v2f i) : SV_Target
            {
                // 采样法线贴图并解码数据
                half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
                // 将法线从切线空间转换到世界空间
                half3 worldNormal;
                worldNormal.x = dot(i.tspace0, tnormal);
                worldNormal.y = dot(i.tspace1, tnormal);
                worldNormal.z = dot(i.tspace2, tnormal);

                // 剩余的和之前的shader一样
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 worldRefl = reflect(-worldViewDir, worldNormal);
                half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
                half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);
                fixed4 c = 0;
                c.rgb = skyColor;
                return c;
            }
            ENDCG
        }
    }
}

Wow,现在真的很复杂了。法线贴图后的反射样子
在这里插入图片描述

Adding more textures

添加更多的纹理

给上面应用了法线映射,天空反射的shader添加更多的纹理。我们添加一个叫"Base texture"的颜色纹理,就和第一个unlit shader例子一样的纹理,然后再添加遮挡贴图(AO贴图)给凹陷的地方调整暗一些。

Shader "Unlit/More Textures"
{
    Properties {
        // 材质中使用了三张纹理
        _MainTex("Base texture", 2D) = "white" {}
        _OcclusionMap("Occlusion", 2D) = "white" {}
        _BumpMap("Normal Map", 2D) = "bump" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            // 和之前的shader一样
            struct v2f {
                float3 worldPos : TEXCOORD0;
                half3 tspace0 : TEXCOORD1;
                half3 tspace1 : TEXCOORD2;
                half3 tspace2 : TEXCOORD3;
                float2 uv : TEXCOORD4;
                float4 pos : SV_POSITION;
            };
            v2f vert (float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT, float2 uv : TEXCOORD0)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(vertex);
                o.worldPos = mul(_Object2World, vertex).xyz;
                half3 wNormal = UnityObjectToWorldNormal(normal);
                half3 wTangent = UnityObjectToWorldDir(tangent.xyz);
                half tangentSign = tangent.w * unity_WorldTransformParams.w;
                half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
                o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
                o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
                o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
                o.uv = uv;
                return o;
            }

            // shader properties中的纹理
            sampler2D _MainTex;
            sampler2D _OcclusionMap;
            sampler2D _BumpMap;
        
            fixed4 frag (v2f i) : SV_Target
            {
                // 和之前的shader一样
                half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.uv));
                half3 worldNormal;
                worldNormal.x = dot(i.tspace0, tnormal);
                worldNormal.y = dot(i.tspace1, tnormal);
                worldNormal.z = dot(i.tspace2, tnormal);
                half3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                half3 worldRefl = reflect(-worldViewDir, worldNormal);
                half4 skyData = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, worldRefl);
                half3 skyColor = DecodeHDR (skyData, unity_SpecCube0_HDR);                
                fixed4 c = 0;
                c.rgb = skyColor;

				// 使用Base Texture纹理和遮挡贴图来调整颜色
                fixed3 baseColor = tex2D(_MainTex, i.uv).rgb;
                fixed occlusion = tex2D(_OcclusionMap, i.uv).r;
                c.rgb *= baseColor;
                c.rgb *= occlusion;

                return c;
            }
            ENDCG
        }
    }
}

这气球猫看起来很不错了!
在这里插入图片描述

Texturing shader examples

其他条纹应用的shader例子

Procedural checkerboard pattern

程序化的棋盘图案

在这shader基于顶点的纹理坐标输出一个棋盘团:

Shader "Unlit/Checkerboard"
{
    Properties
    {
        _Density ("Density", Range(2,50)) = 30
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

            float _Density;

            v2f vert (float4 pos : POSITION, float2 uv : TEXCOORD0)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(pos);
                o.uv = uv * _Density;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                float2 c = i.uv;
                c = floor(c) / 2;
                float checker = frac(c.x + c.y) * 2;
                return checker;
            }
            ENDCG
        }
    }
}

Properties块中的density(密度)的滑动条属性是控制棋盘的密集度的。在顶点着色器中,网格的uv乘以density数值让原来的uv(0-1)转换到uv(0-30),再传到像素着色器中采样用。

像素着色器的代码中,使用了HLSL内置的floor函数取了输入的uv坐标的整数部分,然后除以2。回想一下我们之前输入的坐标是0-30的值,现在让它们"quantized"(可量化,因为之前的是连续不不间断的float数值)处理,0, 0.5, 1, 1.5, 2, 2.5,等等。这样uv的坐标输入就完成了。

接下来,我们将uv.x,y相加(x,y分量只会在0,0.5,1.5这样的数值上),然后再用HLSL内置函数frac取小数部分。这是结果只会有0.0或是0.5。最后我们再乘以2,那就是0或是1了,然后作为颜色的输出值(这就对应着黑色或是白色了)。
在这里插入图片描述

(如果看不懂unity的文档说明,可以看我下面总结的frac(floor(c)/2)*2 棋盘条纹图解

frac(floor(c)/2)*2 棋盘条纹图解

比较稀少的数据看不出来(点击图片放大查看)
在这里插入图片描述

密集一些的数据模拟多片段采样,就可以看出条纹比较均匀(点击图片放大查看)
在这里插入图片描述
上面两个图解我使用的是excel来分析的,文件可点击这里下载Procedural Checkerboard Pattern excel文件(提取码: w35z)

Tri-planar texturing

三个面向的贴上纹理图案

对于复杂或程序化的网格,有时候使用三个主要的方向来投影纹理到对象上面是很有用的,而不是使用常规的UV纹理坐标。这个叫做"tri-planar"(三面)贴图。主要想法是使用表面的法线偏向于三个纹理方向。

Shader "Unlit/Triplanar"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Tiling ("Tiling", Float) = 1.0
        _OcclusionMap("Occlusion", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct v2f
            {
                half3 objNormal : TEXCOORD0;
                float3 coords : TEXCOORD1;
                float2 uv : TEXCOORD2;
                float4 pos : SV_POSITION;
            };

            float _Tiling;

            v2f vert (float4 pos : POSITION, float3 normal : NORMAL, float2 uv : TEXCOORD0)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(pos);
                o.coords = pos.xyz * _Tiling;
                o.objNormal = normal;
                o.uv = uv;
                return o;
            }

            sampler2D _MainTex;
            sampler2D _OcclusionMap;
            
            fixed4 frag (v2f i) : SV_Target
            {
                // 使用法线的绝对值作为纹理的权值
                half3 blend = abs(i.objNormal);
                // 确保权值的和为1(相当于除以:blend.x+blend.y+blend.z)
                // 相当于:blend = blend / (blend.x+blend.y+blend.z);
                blend /= dot(blend,1.0);
                // 读取三个纹理的x,y,z轴向的投影
                fixed4 cx = tex2D(_MainTex, i.coords.yz);
                fixed4 cy = tex2D(_MainTex, i.coords.xz);
                fixed4 cz = tex2D(_MainTex, i.coords.xy);
                // 基于权值混合纹理
                fixed4 c = cx * blend.x + cy * blend.y + cz * blend.z;
                // 根据遮挡贴图来调整亮度
                c *= tex2D(_OcclusionMap, i.uv);
                return c;
            }
            ENDCG
        }
    }
}

在这里插入图片描述

Calculating lighting

计算光照

通常,当你想你的shader能与Unity的光照系统一起工作,你将可能写个surface shader。surface shader为你处理了大量的处理,你只需要在shader代码定义一些表面属性就可以了。

然而,在某些情况,你想绕过standard surface shader,或是你出于性能原因,只想要这些光照管线中的部分子功能,又或者是你想自定义一些东西而不像standard的光照效果的。那么接下来的例子将向你展示如何手写顶点和片段着色器来获取这些光照数据。查看surface shaders生成的代码(通过shader inspector中的compile and show code)也一种不错的学习方式。

Simple diffuse lighting

简单的漫反射光照

首先我们需要处理的是,在shader中传入我们需要的光照信息。Unity的rendering pipeline(渲染管线)支持不同的渲染方式;这里我们将使用默认的forward rendering(正向渲染)的方式。

我们将从支持一个方向光开始。在Unity正向渲染中,是主要以directional light(方向光),ambient(环境光),lightmap(静态烘焙光照映射图)和reflections(环境反射)都在一个pass处理,称之为ForwardBase(正向渲染的基础Pass)。在shader中,给一个pass添加 pass tagTags{“LightMode”=“ForwardBase”} 来表示该pass就是"ForwardBase"的pass。这将会让方向光数据通过built-in variables(内置变量)传入到shader中。

下面的shader将计算简单的逐顶点漫反射光照,和使用一个主要的纹理:

Shader "Lit/Simple Diffuse"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            // 表示我们的pass是一个"base" 正向渲染管线的pass基础。
            // 它将可以获取环境光和主要的方向光数据(这些数据都是外部app对shader的设置)
            // 光源方向存于_WorldSpaceLightPos0和颜色存于_LightColor0
            Tags {"LightMode"="ForwardBase"}
        
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc" // 为了使用UnityObjectToWorldNormal函数
            #include "UnityLightingCommon.cginc" // 为了使用_LightColor0变量

            struct v2f
            {
                float2 uv : TEXCOORD0;
                fixed4 diff : COLOR0; // 漫反射光照颜色
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata_base v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                // 获取世界空间下的顶点法线
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                // 标准的漫反射光照(Lambert)量因子是从法线与光源方向的dot product(点乘)求得
                half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
                // 作为光照颜色的因子
                o.diff = nl * _LightColor0;
                return o;
            }
            
            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                // 采样纹理
                fixed4 col = tex2D(_MainTex, i.uv);
                // 乘以光照
                col *= i.diff;
                return col;
            }
            ENDCG
        }
    }
}

这将让对象可以与光照方向有反应了,根据方向光光源的方向,部分朝向的面是有光照的,部分是没有的。
在这里插入图片描述

Diffuse lighting with ambient

漫反射光照与环境光

上面的例子中没有处理环境光或是光探测器的光照量。让我们添加一下这些功能吧。实际上我们仅需要添加一行代码就可以了。环境光和light probe(光探测器)的数据都传入了shader中以Spherical Harmonics(球谐函数)形式,且使用 ShadeSH9 函数来对球谐函数才采样,ShadeSH9 函数来自UnityCG.cginc include file(头文件)处理了所有的计算,只要传入一个世界空间的法线即可。

Shader "Lit/Diffuse With Ambient"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            Tags {"LightMode"="ForwardBase"}
        
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "UnityLightingCommon.cginc"

            struct v2f
            {
                float2 uv : TEXCOORD0;
                fixed4 diff : COLOR0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata_base v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
                o.diff = nl * _LightColor0;

                // 与之前shader唯一不同的是:
                // 除了主要光源的漫反射光照,还加上了使用UnityCG.gcinc中ShadeSH9函数的采样环境光或是光探测器数据
                // ShadeSH9使用世界空间的法线参数
                o.diff.rgb += ShadeSH9(half4(worldNormal,1));
                return o;
            }
            
            sampler2D _MainTex;

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

这个shader开始看上去和内置的Legacy Diffuse shader的效果很像了。
在这里插入图片描述

Implementing shadow casting

实现阴影投射

我们的shader目前既不能接收阴影也不能投射阴影。现在让我们实现阴影投射先。

为了实现投射阴影,shader必须有一个pass type(pass类型)为 ShadowCaster 的pass在任何一个subshaders中或是fallback shader(保底shader)中。ShadowCaster的pass用于将对象渲染到shadowmap(阴影映射贴图)中,通常这很简单,且在片段着色器基本上不用做什么。shahdowmap仅仅是 depth buffer (深度缓存),所以就算片段着色器输出颜色也无所谓。

这意味着大多数的shader的阴影投射器的pass都基本相同的(除非有自定义的顶点着色器的变形处理,或是片段着色器需要alpha cutout(alpha剔除)或是半透明)。也可以通过使用UsePass 的shader命令来简单的引用其他shader中的Pass:

Pass
{
    // regular lighting pass
    // 常规的光照pass
}
// 从内置的shader:VertexLit shader中获取shadow caster的pass来使用
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"

然而,我们在这手写编写学习用的shadow caster的简单写法。为了代码精简化,我们替换了光照处理pass(“ForwardBase”)的代码,仅使用无纹理环境映射采样。像下面的代码,只要有一个pass是"ShadowCaster"的,就可以让该对象支持阴影投射。

Shader "Lit/Shadow Casting"
{
    SubShader
    {
        // 非常简单的光照处理pass,仅处理无纹理环境映射采样
        Pass
        {
            Tags {"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f
            {
                fixed4 diff : COLOR0;
                float4 vertex : SV_POSITION;
            };
            v2f vert (appdata_base v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                // 对采样环境信息
                o.diff.rgb = ShadeSH9(half4(worldNormal,1));
                o.diff.a = 1;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                return i.diff;
            }
            ENDCG
        }

        // 该pass使用UnityCG.cginc的宏来实现阴影投射渲染
        Pass
        {
            Tags {"LightMode"="ShadowCaster"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "UnityCG.cginc"

            struct v2f { 
                V2F_SHADOW_CASTER;
            };

            v2f vert(appdata_base v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
    }
}

下面的平面使用的是内置的shader: Diffuse shader,所以我们可以看到正常阴影(阴影的接收与投射)的显示了(要知道,我们当前的shader并不支持阴影的接收处理)。
在这里插入图片描述
我们使用 #pragma multi_compile_shadowcaster 指令。这情况下shader将使用每个不同的预处理宏定义来编译出多个变体(查看multiple shader variants)。当将对象渲染到shadowmap里,使用点光源和使用其他光源类型的shader代码会稍微有所不同,这就是为啥需要使用这些编译指令。

Receiving shadows

接收阴影

实现支持接收阴影功能,将需要编译基础光照的pass为多个变体,来处理方向光"有"阴影,还是"无"阴影。使用 #pragma multi_compile_fwdbase 指令来处理(查看 multiple shader variants了解详情)。实际上需要更多的处理了:需要为不同的lightmap类型,实时GI的开或关,等等来编译出各个不同的变体。现在我们不需要处理这些,所以我们都直接先跳过这些变体处理。

为了得到精准的阴影计算,我们将使用 #include “AutoLight.cginc” shader头文件的宏:SHADOW_COORDS,TRANSFER_SHADOW,SHADOW_ATTENUATION。

shader代码如下:

Shader "Lit/Diffuse With Shadows"
{
    Properties
    {
        [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            Tags {"LightMode"="ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            // 编译shader为多个变体(这里我们不关心lightmaps,所以我们跳过这些编译(禁用一些设置的宏,如: nolightmap, nodirlightmap, nodynlightmap))
            #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
            // include阴影辅助的函数或宏需要的头文件
            #include "AutoLight.cginc"

            struct v2f
            {
                float2 uv : TEXCOORD0;
                SHADOW_COORDS(1) // 将阴影数据指定存到TEXCOORD1
                fixed3 diff : COLOR0;
                fixed3 ambient : COLOR1;
                float4 pos : SV_POSITION;
            };
            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
                o.diff = nl * _LightColor0.rgb;
                o.ambient = ShadeSH9(half4(worldNormal,1));
                // 计算阴影数据
                TRANSFER_SHADOW(o)
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                // 计算阴影衰减(1.0 = 完整光照不带衰减,即:不在阴影中,0.0 = 完全在阴影在)
                fixed shadow = SHADOW_ATTENUATION(i);
                // 阴影使光照变暗了,但仍然需要保持环境光
                fixed3 lighting = i.diff * shadow + i.ambient;
                col.rgb *= lighting;
                return col;
            }
            ENDCG
        }

        // 阴影投射支持
        UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"
    }
}

看,现在可以接受阴影了!
在这里插入图片描述

Other shader examples

其他的shader例子

Fog

雾化

Shader "Custom/TextureCoordinates/Fog" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            // 雾化效果需要的编译指令
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct vertexInput {
                float4 vertex : POSITION;
                float4 texcoord0 : TEXCOORD0;
            };

            struct fragmentInput{
                float4 position : SV_POSITION;
                float4 texcoord0 : TEXCOORD0;
                
                // 用于pass的雾化量的一个数值,应该占用一个可用的TEXCOORD[N]。这里是TEXCOORD1
                UNITY_FOG_COORDS(1)
            };

            fragmentInput vert(vertexInput i){
                fragmentInput o;
                o.position = UnityObjectToClipPos(i.vertex);
                o.texcoord0 = i.texcoord0;
                
                // 根据裁剪空间的坐标来计算雾化量
                UNITY_TRANSFER_FOG(o,o.position);
                return o;
            }

            fixed4 frag(fragmentInput i) : SV_Target {
                fixed4 color = fixed4(i.texcoord0.xy,0,0);
                
                // 应用雾化(additive pass会自动处理)
                UNITY_APPLY_FOG(i.fogCoord, color); 
                
                // 如果需要处理自定义雾化颜色,需要添加下面的处理
                //#ifdef UNITY_PASS_FORWARDADD
                //  UNITY_APPLY_FOG_COLOR(i.fogCoord, color, float4(0,0,0,0));
                //#else
                //  fixed4 myCustomColor = fixed4(0,0,1,0);
                //  UNITY_APPLY_FOG_COLOR(i.fogCoord, color, myCustomColor);
                //#endif
                
                return color;
            }
            ENDCG
        }
    }
}

你可以下载上面已展示的例子 zipped Unity project相关项目。
(如果下载不了,或是下载很慢,可以点击(提取码: vddu)这里,从百度网盘下载)

Further reading

延伸阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值