纹理贴图叠加BlinnPhong光照模型

单张纹理结合BlinnPhong光照模型

在计算时,有以下的3点注意点

  1. 纹理颜色需要和漫反射颜色 进行乘法叠加 作为新的漫反射颜色
  2. 计算兰伯特光照时 要使用 1的结果 作为漫反射颜色
  3. 最终和环境光叠加时,为了避免渲染效果发灰 环境光变量和1中结果(新的漫反射颜色)相乘

片元着色器实现

Shader "Unlit/uvlearn2"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _MainColor("MainColor",COLOR) = (1,1,1,1)
        _HighLightColor("HighLight",COLOR) = (1,1,1,1)
        _HighLightNum("HighNum",Range(0.1,20)) = 5
    }
    SubShader
    {

        Pass
        {
            Tags{ "LightMode"="ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _MainColor;
            fixed4 _HighLightColor;
            float _HighLightNum;

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 normal:NORMAL;
                float4 wpos:TEXCOORD1;
                half2 uv: TEXCOORD0;
            };

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.wpos = mul(unity_ObjectToWorld,v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord.xy,_MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                //uv取色和漫反射颜色相乘
                fixed3 uvColor = tex2D(_MainTex,i.uv).rgb * _MainColor.rgb;
                //归一化法向量
                float3 normal = normalize(i.normal);
                //Lambert
                float3 cameraDir = normalize(_WorldSpaceCameraPos.xyz);
                fixed3 lamBertColor = _LightColor0.rgb * uvColor.rgb * max(0,dot(cameraDir,normal));//叠加uvcolor
                //BlinnPhong
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wpos).xyz);
                float3 halfA = normalize(lightDir + viewDir);
                fixed3 BlinnPhongColor = _LightColor0.rgb * _HighLightColor.rgb * pow(max(0,dot(halfA,normal)),_HighLightNum);
                //颜色叠加
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT * uvColor + lamBertColor + BlinnPhongColor;
                return fixed4(color,1);
            }
            ENDCG
        }
    }
}

黑白效果Shader实现

我们可以通过将颜色值点乘float3(0.299, 0.587, 0.114)将颜色转为黑白

float3(0.299, 0.587, 0.114)这三个系数正好是 人眼感知亮度 的权重(ITU-R BT.601 标准),用来把 RGB 颜色转换为亮度值(luminance / 灰度值)。

而这三个数字,是科学家用光谱数据和视觉实验算出来的,保证在 看起来的亮度 上最符合人眼感知。

Shader实现

Shader "Unlit/gray"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Grayscale("Grayscale", Range(0, 1)) = 0
    }
    SubShader
    {

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Grayscale;
            v2f_img vert (appdata_base v)
            {
                v2f_img o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord.xy,_MainTex);
                return o;
            }

            fixed4 frag (v2f_img i) : SV_Target
            {
                fixed4 color = tex2D(_MainTex,i.uv);
                fixed4 grayColor = dot(color,float3(0.299, 0.587, 0.114));
                color = lerp(color,grayColor,_Grayscale);       
                return color;
            }
            ENDCG
        }
    }
}

Blinn-Phong光照模型

Blinn Phong光照模型的公式

物体表面光照颜色 = 环境光颜色 + 漫反射光颜色 + 高光反射光颜色

其中:

环境光颜色 = UNITY_LIGHTMODEL_AMBIENT(unity_AmbientSky、unity_AmbientEquator、unity_AmbientGround)

漫反射光颜色 = 兰伯特光照模型 计算得到的颜色

高光反射光颜色 = Blinn Phong式高光反射光照模型 计算得到的颜色


顶点着色器实现

Shader "Unlit/Blinn_Phong"
{
    Properties
    {
        _MainColor("MainColor",COLOR) = (1,1,1,1)
        _HighLightColor("HighLightColor",COLOR) = (1,1,1,1)
        _HighLightNum("HighLightNum",Range(0.1,20)) = 10
       
    }
    SubShader
    {

        Pass
        {
            Tags{"LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag


            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : NORMAL;
                fixed3 color : COLOR;
            };


            fixed4 _MainColor;
            fixed4 _HighLightColor;
            float _HighLightNum;

            fixed3 getLembertColor(in float3 wnormal)
            {
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0,dot(wnormal,lightDir));
                return color;
            } 

            fixed3 getBlinnPhongColor(in float3 wpos, in float3 wnormal)
            {
                float3 viewDic = normalize(_WorldSpaceCameraPos.xyz - wpos);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 halfDir = normalize(viewDic + lightDir);
                fixed3 color = _LightColor0.rgb * _HighLightColor.rgb * pow(max(0,dot(wnormal,halfDir)),_HighLightNum);
                return color;
            }

            v2f vert (appdata_base v)
            {
                v2f data;
                data.pos = UnityObjectToClipPos(v.vertex);
                data.normal = UnityObjectToWorldNormal(v.normal);
                //计算兰伯特光照
                float3 wnormal = normalize(data.normal);
                fixed3 lembertColor = getLembertColor(wnormal);
                //计算BlinnPhong高光模型
                float3 wpos = mul(unity_ObjectToWorld,v.vertex);
                fixed3 blinnPhongColor = getBlinnPhongColor(wpos,wnormal);
                //BlinnPhong光照模型
                data.color = UNITY_LIGHTMODEL_AMBIENT.rbg + lembertColor + blinnPhongColor;
                return data;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(i.color,1);
            }
            ENDCG
        }
    }
}

片元着色器实现

Shader "Unlit/Blinn_PhongF"
{
    Properties
    {
        _MainColor("MainColor",COLOR) = (1,1,1,1)
        _HighLightColor("HighLightColor",COLOR) = (1,1,1,1)
        _HighLightNum("HighLightNum",Range(0.1,20)) = 10
    }
    SubShader
    {

        Pass
        {
            Tags{"LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"


            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal : NORMAL;
                fixed3 color : COLOR;
                float4 wpos : TEXCOORD;
            };

            fixed4 _MainColor;
            fixed4 _HighLightColor;
            float _HighLightNum;

            fixed3 getLembertColor(in float3 wnormal)
            {
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0,dot(wnormal,lightDir));
                return color;
            } 

            fixed3 getBlinnPhongColor(in float3 wpos, in float3 wnormal)
            {
                float3 viewDic = normalize(_WorldSpaceCameraPos.xyz - wpos);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 halfDir = normalize(viewDic + lightDir);
                fixed3 color = _LightColor0.rgb * _HighLightColor.rgb * pow(max(0,dot(wnormal,halfDir)),_HighLightNum);
                return color;
            }

            v2f vert (appdata_base v)
            {
                v2f data;
                data.pos = UnityObjectToClipPos(v.vertex);
                data.normal = UnityObjectToWorldNormal(v.normal);
                data.wpos = mul(unity_ObjectToWorld,v.vertex);
                return data;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 wnormal = normalize(i.normal);
                fixed3 lambertColor = getLembertColor(wnormal);
                fixed3 blinnPhongColor = getBlinnPhongColor(i.wpos,wnormal);
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT + lambertColor + blinnPhongColor;
                return fixed4(color,1);
            }
            ENDCG
        }
    }
}

Blinn-Phong高光反射光照模型

Blinn-Phong式高光反射光照模型的来历和原理

Blinn-Phong式高光反射光照模型 是 Blinn-Phong光照模型的一部分

Blinn-Phone光照模型的提出者:

吉姆·布林(Jim Blinn,美国计算机科学家)

原理:

Blinn-Phong式高光反射光照模型的理论是

它是对Phong式高光反射光照模型的改进

它不再使用反射向量计算镜面反射,而是使用半角向量来进行计算

半角向量为视角方向和灯光方向的角平分线方向

认为高光反射的颜色和 顶点法线向量以及半角向量夹角的余弦成正比

并且通过对余弦值取n次幂来表示光泽度(或反光度)


Blinn-Phong式高光反射光照模型的公式

高光反射光照颜色 = 光源的颜色 * 材质高光反射颜色 * max(0, 标准化后顶点法线方向向量 · 标准化后半角向量方向向量)幂

  1. 标准化后顶点法线方向向量 · 标准化后半角向量方向向量 得到的结果就是 cosθ
  2. 半角向量方向向量 = 视角单位向量 + 入射光单位向量
  3. 幂 代表的是光泽度 余弦值取n次幂

Phong和Blinn-Phong的区别

由于两个光照模型中高光反射光照模型的计算方式不一样

从而会带来一些表现上的不同

高光散射

Blinn-Phong模型的高光通常会产生相对均匀的高光散射,这会使物体看起来光滑而均匀。

Phong模型的高光可能会呈现更为锐利的高光散射,因为它基于观察者和光源之间的夹角。

这可能导致一些区域看起来特别亮,而另一些区域则非常暗。

高光锐度

Blinn-Phong模型的高光通常具有较广的散射角,因此看起来不那么锐利。

Phong模型的高光可能会更加锐利,特别是在观察者和光源夹角较小时,可能表现为小而亮的点。

光滑度和表面纹理

Blinn-Phong模型通常更适合表现光滑的表面,因为它考虑了表面微观凹凸之间的相互作用,使得光照在表面上更加均匀分布。

Phong模型有时更适合表现具有粗糙表面纹理的物体,因为它的高光散射可能会使纹理和细节更加突出。

镜面高光大小

Blinn-Phong模型通常产生的镜面高光相对较大,但均匀分布。

Phong模型可能会产生较小且锐利的镜面高光。


顶点着色器实现

Shader "Unlit/BlinnPhong"
{
    Properties
    {
        _HighLightColor ("HighLightColor", COLOR) = (1,1,1,1)
        _HighLightNum("HighLightNum", Range(0,20)) = 5
    }
    SubShader
    {

        Pass
        {
            Tags{ "LightMode" = "ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
                fixed3 color : COLOR;
            };

            float4 _HighLightColor;
            float _HighLightNum;

            v2f vert (appdata_base v)
            {
                v2f data;
                data.vertex = UnityObjectToClipPos(v.vertex);
                data.normal = UnityObjectToWorldNormal(v.normal);
                float3 wpos = mul(unity_ObjectToWorld,v.vertex).xyz;
                float3 viewDir = normalize(_WorldSpaceCameraPos - wpos);
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 halfDir = normalize(viewDir + lightDir);
                float3 wnormal = UnityObjectToWorldNormal(v.normal);
                data.color = _LightColor0 * _HighLightColor * pow(max(0,dot(wnormal,halfDir)),_HighLightNum);
                return data;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(i.color,1);
            }
            ENDCG
        }
    }
}

片元着色器实现

Shader "Unlit/BlinnPhongF"
{
    Properties
    {
        _HighLightColor ("HighLightColor", COLOR) = (1,1,1,1)
        _HighLightNum("HighLightNum", Range(0,20)) = 5
    }
    SubShader
    {

        Pass
        {
            Tags{ "LightMode" = "ForwardBase" }
 
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag


            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
                fixed3 color : COLOR;
                float4 wpos : TEXCOORD0;
            };

            float4 _HighLightColor;
            float _HighLightNum;

            v2f vert (appdata_base v)
            {
                v2f data;
                data.vertex = UnityObjectToClipPos(v.vertex);
                data.normal = UnityObjectToWorldNormal(v.normal);
                data.wpos = mul(unity_ObjectToWorld,v.vertex);
                return data;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 normal = normalize(i.normal);
                float3 viewPos = normalize (_WorldSpaceCameraPos - i.wpos).xyz;
                float3 lightPos = normalize(_WorldSpaceLightPos0).xyz;
                float3 halfDir = normalize(viewPos + lightPos);
                fixed3 color = _LightColor0 * _HighLightColor * pow(max(0 , dot(normal,halfDir)) ,_HighLightNum);
                return fixed4(color,1);
            }
            ENDCG
        }
    }
}

Phong光照模型

两个颜色相加

我们在编写兰伯特光照模型时了解了 两个颜色相乘

他的作用就是让两个颜色叠加在一起

也就是让两个颜色的RGBA值各自相乘 得到一个新的颜色

两个颜色相加 同样是让两个颜色叠加在一起

区别是:

相乘

颜色相乘时,最终颜色会往黑色靠拢

计算两个颜色混合时一般用颜色相乘,因为真实世界中多个颜色混在一起最终会变成黑色。

相加

颜色相加时,最终颜色会往白色靠拢

计算光照反射时一般用颜色相加,因为向白色靠拢能带来 更亮的感觉,复合光的表现


Unity Shader中的环境光

我们在编写兰伯特光照模型时

在计算完漫反射光照后,加上了一个环境光变量 UNITY_LIGHTMODEL_AMBIENT

这个环境光变量其实是可以在Unity中进行设置的

Window——>Rendering——>Lighting

Environment(环境)页签中的Environment Lighting(环境光)这里可以设置环境光来源

当是Skybox和Color时,我们可以通过UNITY_LIGHTMODEL_AMBIENT 获取到对应环境光颜色

当是Gradient(渐变)时,通过以下3个成员可以得到对应的环境光

unity_AmbientSky(周围的天空环境光)

unity_AmbientEquator(周围的赤道环境光)

unity_AmbientGround(周围的地面环境光)

注意:

这些内置变量都包含在 UnityShaderVariables.cginc

在编译时,会自动包含该文件,可以不用手动包含


Phong光照模型的来历和原理

它是由裴祥风(Bui-Tuong Phong,越南裔美国计算机学家)在1975年时,提出的一种局部光照经验模型

裴祥风认为物体表面反射光线是由三部分组成的

环境光 + 漫反射光 + 镜面反射光(高光反射光)

是一种经验模型


Phong光照模型的公式

物体表面光照颜色 = 环境光颜色 + 漫反射光颜色 + 高光反射光颜色

其中:

环境光颜色 = UNITY_LIGHTMODEL_AMBIENT(unity_AmbientSky、unity_AmbientEquator、unity_AmbientGround)

漫反射光颜色 = 兰伯特光照模型 计算得到的颜色

高光反射光颜色 = Phong式高光反射光照模型 计算得到的颜色


逐顶点实现

Shader "Unlit/Phong"
{
    Properties
    {
        //漫反射颜色
       _MainColor("MainColor", COLOR) = (1,1,1,1)
       //高光颜色
       _HighLightColor("HighLightColor",COLOR) = (1,1,1,1)
       //光泽度
       _HighLightNum("HighLightNum",Range(0,20)) = 0.5
    }
    SubShader
    {
        Pass
        {
            //光照模式 向前渲染
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos:SV_POSITION;
                fixed3 color:COLOR;
            };

            fixed4 _MainColor; //漫反射颜色
            fixed4 _HighLightColor; //高光颜色
            float _HighLightNum; //光泽度

            
            fixed3 LambertHandle(in float3 normal)//世界坐标系下的归一化法向量
            {
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(normal, lightDir));
                //fixed3 color = _LightColor0.rgb * _MainColor.rgb * (0.5 * dot(normal, lightDir) + 0.5);//半兰伯特
                return color;
            }

            fixed3 PhongHandle(in float3 normal, in float3 vertex)//世界坐标系下的归一化法向量和归一化顶点坐标
            {
                //反射向量
                float3 reflectDir = reflect(-normalize(_WorldSpaceLightPos0.xyz),normal);
                //视角向量  归一化(世界坐标系下摄像机位置-世界坐标系下顶点坐标)
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - vertex);
                fixed3 color = _LightColor0.rgb * _HighLightColor * pow(max(0,dot(viewDir,reflectDir)),_HighLightNum);
                return color;
            }

            //顶点着色器
            v2f vert (appdata_base v)
            {
                v2f data;
                data.pos = UnityObjectToClipPos(v.vertex); //将 模型坐标系下的顶点坐标 转换到其次裁剪坐标系下

                //将法向量 从模型空间转到世界空间 并归一化
                float3 worldNormalPos = normalize(UnityObjectToWorldNormal(v.normal).xyz);

                //计算兰伯特光照
                fixed3 lamberColor = LambertHandle(worldNormalPos);
                
                //计算Phong高光反射
                float3 worldVertexPos = normalize(mul(UNITY_MATRIX_M, v.vertex).xyz); //世界坐标系下的顶点坐标(模型先顶点坐标× MVP矩阵中的M)
                fixed3 phongColor = PhongHandle(worldNormalPos,worldVertexPos);
                
                //将颜色相加
                data.color = UNITY_LIGHTMODEL_AMBIENT.rgb + lamberColor + phongColor;
                return data;
            }

            //片元着色器
            fixed4 frag (v2f i) : SV_Target
            {
                return fixed4(i.color,1);
            }
            ENDCG
        }
    }
}

逐片元实现

Shader "Unlit/PhongF"
{
    Properties
    {
        //漫反射颜色
       _MainColor("MainColor", COLOR) = (1,1,1,1)
       //高光颜色
       _HighLightColor("HighLightColor",COLOR) = (1,1,1,1)
       //光泽度
       _HighLightNum("HighLightNum",Range(0,20)) = 0.5
    }
    SubShader
    {

        Pass
        {
            //光照模式 向前渲染
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 wNormal:NORMAL;
                fixed3 color:COLOR;
                float4 wPos:TEXCOORD0;
            };

            fixed4 _MainColor; //漫反射颜色
            fixed4 _HighLightColor; //高光颜色
            float _HighLightNum; //光泽度

            
            fixed3 LambertHandle(in float3 normal)//世界坐标系下的归一化法向量
            {
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                fixed3 color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(normal, lightDir));
                return color;
            }

            fixed3 PhongHandle(in float3 normal, in float3 vertex)//世界坐标系下的归一化法向量和归一化顶点坐标
            {
                //反射向量
                float3 reflectDir = reflect(-normalize(_WorldSpaceLightPos0.xyz),normal);
                //视角向量  归一化(世界坐标系下摄像机位置-世界坐标系下顶点坐标)
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - vertex);
                fixed3 color = _LightColor0.rgb * _HighLightColor * pow(max(0,dot(viewDir,reflectDir)),_HighLightNum);
                return color;
            }

            v2f vert (appdata_base v)
            {
                v2f data;
                data.pos = UnityObjectToClipPos(v.vertex);
                data.wNormal = UnityObjectToWorldNormal(v.normal);
                data.wPos = mul(unity_ObjectToWorld,v.vertex);
                return data;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 normal = normalize(i.wNormal).xyz;
                float3 vertex = normalize(i.wPos).xyz;
                fixed3 lambertColor = LambertHandle(normal);
                fixed3 phongColor = PhongHandle(normal,vertex);
                fixed3 color = UNITY_LIGHTMODEL_AMBIENT + lambertColor + phongColor;
                return fixed4(color,1);
            }
            ENDCG
        }
    }
}

Phong高光反射模型

Phone式高光反射模型的来历和原理

高光反射模型 没有单一的发明者 因为有多种计算的方式

和半兰伯特光照模型一样,是经过很多从业者的研究和发展演化而来

其中比较关键的贡献者为:

Phong光照模型的提出者:

裴祥风(Bui-Tuong Phong,越南裔美国计算机学家)

Blinn-Phone光照模型的提出者:

吉姆·布林(Jim Blinn,美国计算机科学家)

原理:

基于光的反射行为和观察者的位置决定高光反射的表现效果

并且通过对余弦值取n次幂表示光泽度(或反光度)


Phong式高光反射光照模型的公式

公式:

高光反射光照颜色 = 光源颜色 * 材质高光反射颜色 * max(0,标准化后观察方向向量 · 标准化后的反射方向)

  • 标准化后观察方向向量· 标准化后的反射方向 得到的结果就是 cosθ
  • 幂 代表的是光泽度 余弦值取n次幂

在Shader中获取公式中的关键信息

信息语法
观察者的位置(摄像机的位置)_WorldSpaceCameraPos
相对于法向量的反射向量 方法reflect(入射向量,顶点法向量) 返回反射向量
指数幂 方法pow(底数,指数) 返回计算结果

Phong式高光反射模型Shader实现

顶点着色器

Shader "Unlit/Phong"
{
    Properties
    {
        //高光颜色
        _SpeColor("_SpecColor",color) = (1,1,1,1)
        //光泽度
        _SpecNum("_SpecNum",Range(0,20)) = 0.5
    }
    SubShader
    {
        Tags{"LightMode"="ForwardBase"}

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                //裁剪空间下的顶点坐标
                float4 pos:SV_POSITION;
                //颜色信息
                fixed3 color:COLOR;
            };
            fixed4 _SpeColor;
            float _SpecNum;
            v2f vert (appdata_base v)
            {
                v2f data;
                data.pos = UnityObjectToClipPos(v.vertex);
                //世界坐标系下的顶点坐标
                float4 wordPos = mul(UNITY_MATRIX_M , v.vertex);
                //视角向量
                float3 viewDir = normalize(_WorldSpaceCameraPos - wordPos.xyz);
                //反射向量
                float3 worldNormal = UnityObjectToWorldNormal(v.normal);//世界坐标系下的法向量
                float normalizeLightPos = normalize(_WorldSpaceLightPos0.xyz);//世界坐标系下的灯光向量
                float3 specDir = reflect(-normalizeLightPos , worldNormal);
                data.color = _LightColor0.rgb * _SpeColor.rgb * pow(max(0,dot(viewDir,specDir)), _SpecNum);
                return data;
            }

            fixed4 frag (v2f i) : SV_Target
            {
               return fixed4(i.color,1);
            }
            ENDCG
        }
    }
}

片元着色器

Shader "Unlit/PhongFarg"
{
    Properties
    {
        //高光颜色
        _SpeColor("_SpecColor",color) = (1,1,1,1)
        //光泽度
        _SpecNum("_SpecNum",Range(0,20)) = 0.5

    }
    SubShader
    {
        Tags {"LightMode"="ForwardBase"}
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 normal:NORMAL;
                float3 wpos:TEXCOORD0; //存世界坐标下的顶点坐标;
            };

            v2f vert (appdata_base v)
            {
                v2f data;
                data.pos = UnityObjectToClipPos(v.vertex);
                data.normal = UnityObjectToWorldNormal(v.normal);
                //data.wpos = mul(UNITY_MATRIX_M,v.vertex);
                data.wpos = mul(unity_ObjectToWorld,v.vertex);
                return data;
            }

            fixed4 _SpeColor;
            float _SpecNum;
            fixed4 frag (v2f i) : SV_Target
            {
                //计算视角向量
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.wpos);
                //计算反射向量
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                float3 reflectDir = reflect(-lightDir, i.normal);
                //计算高光反射颜色
                fixed3 specColor = _LightColor0.xyz * _SpeColor.xyz * pow(max(0, dot(viewDir, reflectDir)), _SpecNum);
                return fixed4(specColor, 1);
            }
            ENDCG
        }
    }
}

Lambert光照

两个颜色相乘

两个颜色变量相乘,通常用于计算光照、材质混合、纹理混合等等需求

它是一种用于混合颜色的操作

计算结果可以理解为两种颜色叠加在一起的效果

在颜色相乘中,通常是使用颜色通道的值来执行逐通道的乘法

颜色A * 颜色B = A、B颜色叠加


漫反射基本概念

漫反射(Diffuse Reflection)是光线撞击一个物体表面后以各个方向均匀地反射出去的过程

在漫反射下,光线以无规律的方式散射,而不像镜面反射那样按照特定的角度反射。

这种散射导致了物体表面看起来均匀而不闪烁的效果。


兰伯特光照模型的来历和原理

兰伯特(Lambert)光照模型,也称为朗伯反射模型

是由瑞士数学家约翰·海因里希·朗伯(Johann Heinrich Lambert)于1760年左右首次提出

朗伯是18世纪的一个杰出科学家,他在光学和数学领域作出了众多贡献。

兰伯特光照模型描述了漫反射表面对光线的反射行为,它成为计算机图形学和渲染中重要的基础模型之一

兰伯特光照模型的理论是 认为漫反射光的强度仅与入射光的方向和反射点处表面法线的夹角的余弦成正比


兰伯特光照模型的公式

公式:

漫反射光照颜色 = 光源的颜色 * 材质的漫反射颜色 * max(0, 标准化后物体表面法线向量· 标准化后光源方向向量)

其中:

  1. 标准化后物体表面法线向量· 标准化后光源方向向量 得到的结果就是 cosθ
  2. max(0,cosθ)的目的是避免负数,对于模型背面的部分,认为找不到光,直接乘以0,变为黑色

兰伯特光照模型实现

逐顶点光照

Shader "Unlit/Lambert"
{
    Properties
    {
         _MainColor("MainColor", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags {"LightMode"="ForwardBase" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            fixed4 _MainColor;
                
            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 color:COLOR;
            };

            v2f vert(appdata_base v)
            {
                v2f v2fData;
                //将模型坐标系下的顶点坐标转换为裁剪坐标系下的顶点坐标
                v2fData.pos = UnityObjectToClipPos(v.vertex);
                //获得世界坐标系下的法线信息
                float3 normal = UnityObjectToWorldNormal(v.normal);
                //归一化世界坐标下的光源向量
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
                //计算兰伯特光照
                v2fData.color = _LightColor0.rgb * _MainColor.rgb * max(0, dot(normal, lightDir));
                return v2fData;
            }

            fixed4 frag(v2f i):SV_Target
            {
                return fixed4(i.color.rgb,1);
            }
            ENDCG
        }
    }
}

逐片元光照

Shader "Unlit/Lambert"
{
    Properties
    {
         _MainColor("MainColor", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags {"LightMode"="ForwardBase" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            fixed4 _MainColor;
                
            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 normal:NORMAL;
            };

            v2f vert(appdata_base v)
            {      
                v2f v2fData;
                v2fData.pos = UnityObjectToClipPos(v.vertex);
                v2fData.normal = UnityObjectToWorldNormal(v.normal);
                return v2fData;
            }

            fixed4 frag(v2f i):SV_Target
            {       
                fixed3 lightDir = normalize(_WorldSpaceLightPos0);
                fixed3 color = _LightColor0 * _MainColor * max(0,dot(i.normal,lightDir));
                return fixed4(color.rgb,1);
            }
            ENDCG
        }
    }
}

半兰伯特光照

根据数学公式我们知道

逐顶点光照和逐片元光照

什么是光照模型

光照模型指的是用于模拟光照效果的一组数学公式和算法

用于确定在3D场景中的模型表面应该如何对光进行反射和散射

从而实现视觉上逼真的照明效果

光照模型就是用来计算光照效果的数学公式


逐顶点光照

逐顶点光照会在每个物体的顶点(顶点着色器)上进行光照计算

这意味着光照计算只在物体的顶点位置上执行

而在顶点之间的内部区域使用插值来获得颜色信息

优点:

逐顶点光照的计算量较小,通常在移动设备上性能较好,适用于移动游戏等要求性能的场景

缺点:

照明效果可能不够精细,特别是在物体表面上的细节区域,因为颜色插值可能不足以捕捉到细微的照明变化

适用场景:

逐顶点光照适用于需要在有限资源下获得较好性能的场景,例如移动游戏


关于逐顶点光照的插值运算

我们不需要自己去处理这个插值运算

插值运算是由图形硬件(GPU)来执行的

GPU负责处理3D图形的渲染,包括顶点插值和像素插值等操作

这个过程在图形硬件中被高度优化过,因此在实时渲染中能够快速而高效地执行

假设:三角面片的三个顶点A、B、C

该三角面片中的任何像素P,首先会计算出它相对于3个顶点的位置权重

然后使用这个权重参与到P点的颜色计算中

PixelColorP = WeightA * ColorA + WeightB * ColorB + WeightC * ColorC


逐片元光照

逐片元光照会在每个像素(片元着色器)上进行光照计算

每个像素都会根据其位置、法线、材质等信息独立地进行光照计算

优点:

逐片元光照提供了更高的精细度,可以捕捉到物体表面上的细微照明变化,提供更逼真的效果

缺点:

计算量较大,对于像素密集的场景需要更多的计算资源

适用场景:

逐片元光照通常用于需要高质量照明效果的PC和主机游戏,以及要求视觉逼真度较高的场景

CG语法

CG语句写在哪里?

对于顶点\片元着色器来说

CG语句需要写在Pass渲染通道语句块中

我们需要在Pass语句块中加入指令CGPROGRAM

指定着色器函数

我们需要先使用 #pragma 声明编译指令

用来指定 顶点、片元着色器使用的函数

#pragma vertex name(实现顶点着色器的函数名)
#pragma fragment name(实现片元着色器的函数名)

之后我们只需要在这两个函数中书写Shader逻辑即可


CG基础数据类型

类型说明
uint32位无符号整型
int32位整型
float32位浮点数 符号:f
half16位浮点数 符号:h
fixed12位浮点数
bool布尔
string字符串
smapler纹理对象句柄(处理纹理(Texture)数据的数据类型)
sampler1D用于一维纹理,通常用于对一维纹理进行采样,例如从左到右的渐变色
sampler2D用于二维纹理,最常见的纹理类型之一。它用于处理二维图像纹理,例如贴图
sampler3D用于三维纹理,通常用于体积纹理,例如体积渲染
samplerCUBE用于立方体纹理,通常用于处理环境映射等需要立方体贴图的情况
samplerRECT用于处理矩形纹理,通常用于一些非标准的纹理映射需求

声明

float a = 1.4;

基础复合数据类型

数组

int a[4] = {1,2,3,4};

二维数组

int b[2][3] = {{1,2,3},{4,5,6}};

结构体

struct v2f{
    int a[4] = {1,2,3,4};
    int b[2][3] = {{1,2,3},{4,5,6}};
}

特殊数据类型

向量

向量类型属于CG语言的内置数据类型

GrabPass命令

我们可以利用GrabPass命令把即将绘制对象时的屏幕内容抓取到纹理中

在后续通道中即可使用此纹理,从而执行基于图像的高级效果。

举例:

将绘制该对象之前的屏幕抓取到 _BackgroundTexture 中

GrabPass
{
    "_BackgroundTexture"
}

注意:

该命令一般写在某个Pass前,在之后的Pass代码中可以利用_BackgroundTexture变量进行处理

ShaderLab-设置渲染状态

渲染状态的语法结构

渲染状态 状态类型

渲染状态是通过 渲染状态关键词+空格+状态类型 决定的

可以存在多个渲染状态 通过空格或空行隔开

ZWrite off

剔除方式

设置多边形的剔除方式,有背面剔除、正面剔除、不剔除

剔除,就是不渲染,背面剔除就是背面不渲染,正面剔除就是正面不渲染,不剔除就是都渲染

剔除方式说明
Cull Back背面剔除(默认)
Cull Front正面剔除
Cull Off不剔除

一般情况下,我们需要两面渲染时,会设置为不剔除


深度缓冲

是否写入深度缓冲

深度缓冲(Depth Buffer):

深度缓冲是一个与屏幕像素对应的缓冲区,用于存储每个像素的深度值(距离相机的距离)。

在渲染场景之前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。

最后留在深度缓冲中的信息会被渲染

是否写入深度缓冲说明
ZWrite On写入深度缓冲(默认)
ZWrite Off不写入深度缓冲

深度测试

设置深度测试的对比方式

深度测试的主要目的是确保在渲染时,像素按照正确的深度(距离相机的距离)顺序进行绘制,

从而创建正确的遮挡关系和透视效果

在渲染场景之前,深度缓冲被初始化为最大深度值,表示所有像素都在相机之外。

在渲染过程中,对于每个像素,深度测试会将当前像素的深度值与深度缓冲中对应位置的值进行比较。

一般情况下

  1. 如果当前像素的深度值小于深度缓冲中的值,说明当前像素在其他物体之前,它会被绘制,并更新深度缓冲。
  2. 如果当前像素的深度值大于等于深度缓冲中的值,说明当前像素在其他物体之后,它会被丢弃,不会被绘制,并保持深度缓冲不变
深度测试方式说明
ZTest Less小于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest Greater大于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest LEqual小于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中(默认)
ZTest GEqual大于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest Equal等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest NotEqual不等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest Always始终通过深度测试写入深度缓冲中

一般情况下,我们只有在实现一些特殊效果时才会区修改深度测试方式,比如透明物体渲染会修改为Less,描边效果会修改为Greater等

ShaderLab-渲染标签Tags

SubShader中的标签

Pass渲染通道语句块中也可以声明

下叙标签仅可以在SubShader语句块中声明


渲染标签的语法结构

Tags{ "标签名1" = "标签值1" "标签名2" = "标签值2" "标签名2" = "标签值2" .......}

我们可以通过标签来确定什么时候以及如何对物体进行渲染

渲染标签是通过键值对的形式进行声明的

渲染标签没有数量限制


渲染队列Queue

渲染队列用来确定物体的渲染顺序

渲染队列队列名称队列号说明例子
Background背景1000最早被渲染的物体的队列,一般用来渲染天空盒或者背景Tags{ “Queue” = “Background” }
Geometry几何2000不透明的几何体通常使用该队列,当没有声明渲染队列时,Unity会默认使用这个队列Tags{ “Queue” = “Geometry” }
AlphaTest透明测试2450有透明通道的,需要进行Alpha测试的几何体会使用该队列,当所有Geometry队列实体绘制完后再绘制AlphaTest队列,效率更高Tags{ “Queue” = “AlphaTest” }
Transparent透明3000该队列中几何体按照由远到近的顺序进行绘制,半透明物体的渲染队列,所有进行透明混合的几何体都应该使用该队列比如:玻璃材质,粒子特效等Tags{ “Queue” = “Transparent” }
Overlay覆盖4000用是放在最后渲染的队列,于叠加渲染的效果,比如镜头光晕等Tags{ “Queue” = “Overlay” }

还有一种自定义队列

ShaderLab

什么是ShaderLab

学习和编写着色器的过程 一直是一个学习曲线很陡峭的过程,通常情况下为了自定义渲染效果 往往需要和很多文件和设置打交道。而且一些细节问题往往也需要开发者花费很多时间去解决。

Unity为了解决上述问题,提供了一层抽象——Unity Shader,而我们和这层抽象打交道的途径 就是使用Unity提供的一种专门为Unity shader服务的语言——Shader Lab

/usr/uploads/2025/05/2626368639.png

在Unity中所有的Unity Shader都是使用ShaderLab编写的。ShaderLab是Unity提供的编写Unity Shader的一种说明性语言。

它使用了一些嵌套在花括号内部的语义 来描述一个Unity shader文件的结构。

这些结构包含了许多渲染所需的数据

例如Properties语句块中定义了着色器所需的各种属性,这些属性会显示在材质面板上。

一个Unity Shader的结构如下所示

Shader "ShaderName"{
	Properties{
	//属性
	}
	SubShader{
	//显卡A使用的子着色器
	}
	SubShader{
	//显卡B使用的子着色器
	}
	Fallback "VertexLit"
}

Unity在背后会根据使用的平台 把这些结构编译成真正的代码和Shader文件,而开发者只需要和Unity shader打交道即可。


Unity shader的结构

给Shader起个名字

每个Unity Shader文件的第一行都要通过Shader语义来指定该Shader的名字,例如"MyShader"

当为材质选择使用的Unity Shader时 这些名字就会出现在材质面板的下拉列表里

通过在字符串中添加斜杠 可以控制Unity Shader在材质面板中出现的位置 例如:

UnityShader概述

材质和UnityShader

总体来说,在Unity中我们需要配合使用材质(Material)和Unity Shader才能达到需要的效果

常见的流程是

  • 创建一个材质
  • 创建一个Unity Shader,并把它赋给上一步中创建的材质
  • 把材质赋给要渲染的对象
  • 在材质面板中调整Unity Shader的属性

/usr/uploads/2025/05/2581152968.png

Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(使用那些纹理等)和指令(渲染和标签设置等)。

而材质允许我么调节这些属性,并将其最终赋给相应的模型


Unity中的材质

Unity中的材质需要结合一个GameObject的Mesh 或者Particle Systems组件来工作,

他决定了我们的游戏对象看起来是什么样子的。

我们可以在Project视图中右键点击->Create->Material来创建

默认情况下,一个新建的啊辞职将使用Unity内置的Standard Shader。

对于美术人员来说,材质是他们非常熟悉的一种事物,Unity的材质和许多建模软件(Maya、C4D等)中提供的材质功能类似。

他们都提供了一个面板来调整各个参数。

/usr/uploads/2025/05/3322760765.png

/usr/uploads/2025/05/3478383046.png


Unity中的Shader

一个单独的Unity Shader是无法发挥任何作用的,他必须和材质集合起来。

为此我们需要在材质面板的最上方选择我们想要使用的Unity Shader。

选择完毕后,材质面板会出现该Unity Shader可用的各种属性,颜色 纹理等。

UnityShader 本质上就是一个文本文件,和Unity中的很多外部文件类似,Unity Shader也有导入设置面板。

在该面板上,我们可以在Default Maps中指定该Unity Shader使用的默认纹理。

当任何材质第一次使用该Unity Shader时,这些纹理里就会自动被赋到相应的属性上。

在下方面板中,Unity会显示出该Unity Shader相关的信息,例如他是否是一个表面着色器,是否是一个固定函数着色器。

还有一些信息是和我们在Unity Shader中的标签设置有关,例如是否会投射阴影,使用的渲染队列,LOD值等。

什么是HLSL、GLSL、CG?

在可编程管线出现前,为了编写着色器代码,开发者需要学习汇编语言。

为了给开发者打开方便的大门,就出现了更高级的着色语言。

着色语言是专门用来编写着色器的。

常见的着色语言有

  • DirectX的HLSL(Height Lvevl Shading Language)
  • OpenGL的GLSL(OpenGL Shading Language)
  • Nvdia的CG(C for Graphic)

这种高级是相对汇编来说的,而不是像C#相对于C的高级那样。

这些语言会被编译成和机器无关的汇编语言,也被称为中间语言(Intermediate Language,IL)。

这些中间语言再交给显卡驱动来翻译成真正的机器语言。


GLSL的优点在于他的跨平台性,它可以在Windows,Linux,Mac甚至移动平台等多种平台上工作,但这种跨平台性是由于OpenGL没有提供着色器编译器,而是由显卡驱动来完成着色器的编译工作。

也就是说,只要显卡驱动支持对GLSL的编译就可以运行。

好处在于,由于供应商完全了解自己的硬件构造,他们知道怎么样做可以发挥出最大的作用。

换句话说GLSL是依赖硬件,而非系统层级的,但也意味着GLSL的编译结果取决于硬件供应商,他们对GLSL的编译实现不尽相同,这可能会造成编译结果不一致的情况。

对于HLSL,是由微软控制着色器的编译,就算使用不同的硬件,同一个着色器的编译结果也是一样的(前提是版本相同)。但也因此支持HLSL的平台相对比较有限,几乎全是微软自己的产品Windows,Xbox等。因为在其他平台上没有可以编译HLSL的编译器。

Cg则是真正意义上的跨平台,他会根据平台的不同,编译成相应的中间语言,Cg语言的跨平台性很大原因取决于和微软的合作,这也导致Cg语言的语法和HLSL非常像,Cg语言可以无缝移植成HLSL代码。但缺点是无法完全发挥出OpenGL的最新特性。

对于Unity平台,我们同样可以选择使用哪种语言。在Unity Shader中,我们可以使用“Cg/HLSL”或者"GLSL"。但Unity里的这些着色器语言 并不是真正意义上的对应的着色器语言,尽管他们的语法几乎一样。


来源作者:《UnityShader入门精要》冯乐乐

HDRP、URP是什么?

为了让用户可以根据项目需求定义适合自己的渲染流水线(Render Pipeline),使其拥有更高的扩展性,Unity与2018.1版本中推出了可编程渲染管线,并提供了两个可以直接使用的SRP模板,分别是高清渲染管线(HDRP)和轻量级渲染管线(LWRP)。

HDRP是给配备高性能显卡的设备打造的渲染管线,模板中提供了更多的材质效果设置和更高质量的光影效果,对于渲染效果有非常大的提升,因此对硬件需求比较高。

LWRP是专门为了移动端打造的渲染管线,当然也适用于其他平台,相比与HDRP,在其性能和效果之间做了权衡,在渲染效果方面适当的做出了妥协,从而使大多数移动设备都能流畅运行。

Unity在2019.3版本中将LWRP进行了升级,并将名称改为URP。

Draw Call

什么是Draw Call?

Draw Call本身的含义很简单,就是CPU调用图像编程接口

OpenGL中的glDrawElements命令 或者 DirectX中的DrawIndexedPrimitive命令

以命令GPU进行渲染的操作。


CPU和GPU是如何并行工作的?

如果没有流水线化,CPU需要等GPU完成上一个渲染任务才能再次发送渲染命令,这种渲染方法效率比较低。

因此我们需要让CPU和GPU可以并行工作,解决方法就是命令缓冲区(Command Buffer)

命令缓冲区包含了一个命令队列,由CPU向其添加命令,由GPU读取命令。

添加和读取的过程相互独立,

当CPU需要渲染一些对象时,它可以向命令缓冲区中添加命令

当GPU完成上一次渲染任务后,他就可以从命令队列中再取出一个命令执行

命令缓冲区中的命令有很多种,Draw Call是其中的一种,其他命令还有改变渲染状态,着色器,使用不同的纹理等。

/usr/uploads/2025/05/1082672713.png


为什么Draw Call多了会影响帧率?

一个常见的误区是,DrawCall中造成问题的元凶是GPU,认为GPU上的状态切换是耗时的,

其实不是的,真正“拖后腿”的是CPU

我们来做个实验:

创建10000个小文件,每个文件大小为1KB,然后从一个文件夹复制到另一个文件夹。

会发现尽管文件大小总和不超过10MB,但需要花费很长时间。

现在创建单独的文件 他的大小是10MB,然后从一个文件夹复制到另一个文件夹。

但这次事件却少很多。

因为每一个复制动作都需要很多额外操作,例如分配内存,创建元数据等。

这些操作会造成额外的开销,如果复制了很多小文件 这些开销会很大。

渲染的过程也是类似的

CPU在每次调用Draw Call前,都需要向GPU发送很多内容,包括数据、状态、命令等。

这一阶段 CPU需要完成很多工作 例如检查渲染状态。

一旦CPU完成了这些准备工作 GPU就可以开始本次渲染。

GPU的渲染能力是很强的 渲染200个还是2000个三角网络 通常没什么区别。

因此渲染速度往往快于提交命令(DrawCall)的速度。

如果DrawCall数量太多,CPU就会把大量时间花费在提交Draw Call上。

/usr/uploads/2025/05/1926298104.png


如何减少Draw Call?

可以使用批处理的方式,把很多小的Draw Call合并成一个大的DrawCall。

需要注意的是,由于我们需要在CPU的内存中合并网络,而合并的过程是需要消耗时间的。

批处理技术更加适合一些静态的物体,大地石头等。

当然我们也可以对动态的物体进行批处理,但是由于这些物体是不断运动的,因此每一帧都需要重新进行合并再发给GPU,这对时间和空间都会造成一些影响。同时Unity对动态合批也有一定的限制。

GPU流水线——几何、光栅化阶段

GPU流水线

当GPU从CPU得到渲染命令后,就会进行一系列的流水线操作,最终把图元渲染到屏幕上。

对于概念阶段的最后两个阶段即几何和光栅阶段,开发者无法拥有绝对的控制权,其实现的载体是GPU。

虽然我们无法完全控制这两个阶段,但GPU向开发者开放了很多控制权。

几何阶段和光栅化阶段可以分成若干个更小的流水线阶段,每个阶段提供了不同的可配置性和可编程性。

/usr/uploads/2025/05/3281744393.png

绿色表示该流水线阶段是完全可编程控制的

黄色标识该流水线阶段可以配置 但是不可以编程

蓝色表示该流水线是由GPU固定实现的 开发者没有控制权

实线表示该shader必须由开发者编程实现,虚线则表示可选的


从图中可以看出GPU的渲染流水线接收顶点数据作为输入,这些顶点数据是由应用阶段加载到显存中,再由draw call指定的,这些数据随后被传递给顶点着色器

几何阶段:

  • **顶点着色器(Vertex Shader)**是完全可编程的,通常用于实现顶点的空间变换,顶点着色等功能。

  • **曲面细分着色器(Tessellation Shader)**可选着色器,用于图元细分。

  • **几何着色器(Geometry Shader)**可选着色器,用于执行逐图元的着色操作,或被用于生产产生图元。

  • **裁剪(Clipping)**这个阶段是可配置的,将不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元片面。

    我们使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面。

  • **屏幕映射(Screen Mapping)**这一阶段是不可配置和编程的,他负责把每个图元的坐标转换到屏幕坐标系中。

光栅化阶段

  • **三角形设置(Triangle Setup)、三角形遍历(TriangleTraversal)**阶段都是固定函数 不可编程和配置。
  • **片元着色器(Fragment Shader)**是完全可编程的,它用于实现逐片元(Pre-Fragment )的着色操作。
  • **逐片元操作(Pre-Fragment)**它是不可编程的,但具有很高的可配置性。负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等。

几何阶段

顶点着色器

顶点着色器是流水线的第一个阶段,他的输入来自CPU,顶点着色器的处理单位是顶点。

也就是说,输入进来的每个顶点 都会调用一次顶点着色器,顶点着色器本身不可以创建或销毁任何顶点,也无法得到顶点和顶点之间的关系。

例如,我们无法得知两个顶点是否属于一个三角网络。

但正因为这样的相互独立性,GPU可以利用本身的特性进行优化处理每一个顶点,这意味着这一阶段的处理速度会很快。

顶点着色器需要完成的工作有:坐标变换和逐顶点光照。

除了这两个主要任务外 顶点着色器还可以输出后续阶段所需的数据。

/usr/uploads/2025/05/942983214.png

坐标变换

故名思意,就是对顶点的坐标 进行某种变换。顶点着色器可以在这一步中改变顶点的位置,这在顶点动画中是非常有用的。

例如,我们可以通过改变顶点位置来模拟水面,布料等。

但要注意的是,无论我们在顶点着色器中怎样改变顶点的位置,一个最基本的顶点着色器必须完成的工作是

把顶点坐标从模型空间转换到齐次裁剪空间。

o.pos = mul(UNITY_MVP,v.position);

类似上面这句代码的功能,就是把顶点坐标转换到齐次裁剪坐标系下。接着再由硬件做透视除法后,最终得到归一化设备坐标(Normalized Device Coordinates,NDC)。

CPU和GPU之间的通信——应用阶段

CPU和GPU之间的通信

渲染流水线的起点是CPU,即应用阶段。

应用阶段大概可以分为下面3个阶段:

  1. 把数据加载到显存中
  2. 设置渲染状态
  3. 调用Draw Call

把数据加载到显存中

所有渲染所需的数据都要从硬盘加载到内存中。

然后网络和纹理等数据 又被加载到显存中。

这是因为显卡对于显存的访问速度更快,且大多数显卡对于内存 没有直接的访问权限。

把内存加载到显存后,内存中的数据就可以移除了,

但对于一些数据来说,CPU仍然需要访问他们(例如我们希望CPU可以访问网络数据进行碰撞检测)那么我们就不希望这些是数据被移除(因为硬盘加载到内存的过程是十分耗时的)。

在这之后,开发者还需要通过CPU来设置渲染状态,从而“执导”GPU如何进行渲染工作


设置渲染状态

渲染状态通俗的解释就是,这些状态定义了场景中的网络是如何被渲染的。

例如,使用哪个顶点着色器/片元着色器、光源属性、材质等。

如果没有更改渲染状态,那么所有的网络都将使用同一种渲染状态。

在准备好上述工作后,CPU就会调用一个渲染命令告诉GPU:”嘿!bro,数据都准备好了 可以开始渲染了“

而这个命令就是Draw Call


调用Draw Call

Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。

这个命令仅会只想一个需要被渲染的图元列表,不会包含任何材质信息。

当给定一个Draw Call时 GPU就会根据渲染状态(材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出到屏幕上。


来源作者:《UnityShader入门精要》冯乐乐