Unity-Shader详解-其一

今天我们来介绍Unity的一大核心组件:shader。

Shader

Shader就是我们的着色器,用于控制图形的渲染的计算和生成。

对于不同的引擎,具体实现渲染的方法也不一样,也就是我们俗称的不同的图形引擎API,比如OpenGL,DirectX,Vulkan,而我们的Unity的图形引擎则是封装了多个API来组成的。

我们在Unity项目里创建->着色器->无光着色器,就可以得到下述代码:

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
        }
    }
}

当然这里我们针对所谓的无光照着色器还是得先说明一下,我们可以使用的Unity着色器分为:

无光照着色器(Unlit Shader)​、图像特效着色器(Image Effect Shader)、表面着色器(Surface Shader)、计算着色器(Compute Shader)和光线追踪着色器(Ray Tracing Shader)等。

后续我们可能会一个个介绍,但是在最开始,我们把目光放在最基础的无光照着色器上。

既然都叫无光照着色器了,显然我们不考虑在这个着色器上计算光照。我们会直接把颜色或者纹理在着色器渲染的物体上生成,没有复杂的光照计算。

​回到代码本身,我们可以看到着色器总的由两个大块组成:Properties和SubShader。

Properties

Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }

_MainTex,懂英语的应该都知道这是主纹理的意思,参数里有一个"Texture",代表纹理类型。

Properties就是属性的意思,比如我们定义了一个纹理是白色的,他的固定句式是:

针对数字或者滑动条:

name ("display name", Range (min, max)) = number
name ("display name", Float) = number
name ("display name", Int) = number

    针对颜色或者矢量(颜色本质上是一个三维(四维)矢量):

    name ("display name", Color) = (number,number,number,number)
    name ("display name", Vector) = (number,number,number,number)

    针对纹理:

    name ("display name", 2D) = "defaulttexture" {}
    name ("display name", Cube) = "defaulttexture" {}
    name ("display name", 3D) = "defaulttexture" {}

    在着色器的代码中,我们用name处的名称使用变量,但是在材质(Material)处,看到的是display name。

    Subshader

    然后是我们的SubShader:

    Tags { "RenderType"="Opaque" }
            LOD 100

    对于每一个Unity的着色器来说,其中都必须包含子着色器,我们真正调用的也是子着色器。

    Tags,翻译为标签。在着色器中每一个Tags就是对应的一系列子着色器的一些标注,比如这里是规定渲染类型为不透明物体,LOD(Level of Details)细节层级为100,当计算机性能不够时可能会自动降低LOD。

    一些其他常见的Tags如下:

    Pass,则是一个具体定义渲染流程的部分,在这里我们会展开具体的子着色器写法。

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

    CGPROGRAM一般和ENDCG一起使用,中间的部分表明是可编程的渲染部分,有图形学学习经验的小伙伴应该都知道,整个渲染流程我们真正可以用代码控制的部分其实主要就是顶点着色器和片元着色器这两部分,一般这两个着色器的代码我们也放在CGPROGRAM和ENDCG之间(CG是Unity书写着色器的语言) 。

    #pragma则是一个编译命令,主要作用就是声明顶点着色器和片元着色器的函数:

    #include 就不介绍了,如果不认识include建议是重修一下大一程序语言课。

                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

    这里就是具体的着色器的内容了,首先是两个结构体:appdata和v2f,我们看到顶点的定义后面有一个:POSITION,这是什么意思呢?

    说白了就是一个程序告诉计算机我们的变量具体存储的是模型空间里的什么内容的语法糖。

    appdata结构体定义了我们具体着色器要操作的对象:顶点坐标和纹理坐标。

    v2f结构体则是告诉我们顶点着色器的输出来作为片元着色器的输入的对象:依然是顶点坐标和纹理坐标,以及一个奇怪的东西:

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

    没有如下,简单地说这就是一个宏,作用是帮你生成一个烟雾特效,对应的参数是对应的作为纹理坐标的索引(我们定义了第一个纹理坐标索引为0,这里相当于新增了一个Unity自带的烟雾效果的纹理坐标索引为1)。

    还有我们的顶点坐标似乎多了一个SV的前缀,这是什么意思呢?其实就是不同空间坐标的顶点坐标,SV对应的是裁剪空间。

                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;
                }

    这里就是具体的顶点着色器和片元着色器的定义了,顶点着色器把appdata类型变量作为输入而把v2f类型变量作为返回值,代码中声明一个v2f实例o,o的顶点坐标和纹理坐标分别用了两个不同的函数来得到:UnityObjectToClipPos和TRANSFORM_TEX。

    不展开细聊的话,我们知道一般从模型空间得到裁剪空间的顶点坐标,然后允许动态调整纹理坐标就是通过这两个函数得到的即可。

    UNITY_TRANSFER_FOG(o,o.vertex)则又是一个内置宏,含义就是给o的顶点坐标施加之前声明的FOG特效。

    片元着色器这边:以v2f作为输入而输出fixed4类型的变量,这里后续跟的:SV_Target是:

    我们从i的UV坐标上采样纹理的颜色值,然后与Unity自带的烟雾颜色插值混合,最后返回。

    概念补充:

    虽然很短的几十行代码,其背后蕴含的原理非常的多,我来稍微做一些补充和介绍。

    语义:

    系统值语义往往代表特殊的关键数据,比如SV_POSITION就是裁剪空间的坐标,SV_TARGET则是片元着色器的最终输出颜色。

    不同的空间坐标:

    Unity总的来说有多种空间坐标系,在我们的顶点着色器运作的时候,大致的流程和变化情况是这样的:

    模型→世界→视图→裁剪

    模型坐标指的就是以模型自我为中心的局部坐标系,在最开始时,场景中没有物体,我们要现根据一个个模型空间的坐标创建出模型之后才加入场景;世界坐标则是针对场景来说,此时所有场景中的模型会共享一个同样的世界坐标;视图坐标则是针对摄像机的坐标系,以摄像机为原点,分别计算不同物体相对于摄像机的位置;最后的裁剪空间就是根据摄像机的参数设置(视锥体大小,远近平面)从视图空间中裁剪出来的空间,最后经过透视除法和视口变化后就得到了我们摄像机中最后可以看到的场景。

    关于颜色插值:

    插值往往作为渐变或者平滑过渡的手段,尤其在颜色、纹理中有用。

    基础光照

    兰伯特

    简单地说,兰伯特光照模型就是理想漫反射光照模型,我们只用考虑光源方向和法线的夹角来计算反射光线是否存在即可。

    逐顶点和逐像素?

    根本差别就在于我们的光照的计算是在顶点着色器还是在片元着色器中完成。

    让我们分别实现这样的光照模型。

    Shader "Chapter3/chapter3_1"
    {
        // Shader 属性(Properties)部分:定义了外部可以调整的参数。
        Properties
        {
            // _MainTex:纹理贴图,默认值为白色纹理
            _MainTex ("Texture", 2D) = "white" {}
        }
        SubShader
        {
            // SubShader 标签(Tags):定义了渲染模式与渲染顺序
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
            LOD 100 // 设置最低的细节等级(LOD)
    
            // Pass 说明渲染过程的一个阶段。每个 Pass 都会执行顶点着色器和片段着色器。
            Pass
            {
                CGPROGRAM
                // 声明使用的着色器语言
                #pragma vertex vert
                #pragma fragment frag
    
                // 包含 Unity 的常用着色器代码库
                #include "UnityCG.cginc"
                // 包含 Unity 的光照模型代码库
                #include "Lighting.cginc"
    
                // 定义输入结构体,传入顶点着色器的数据
                struct appdata
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:顶点位置
                    float4 vertex : POSITION;
                    // normal:表面法线
                    float3 normal : NORMAL;
                };
    
                // 定义输出结构体,传递数据到片段着色器
                struct v2f
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:变换后的顶点位置(裁剪空间位置)
                    float4 vertex : SV_POSITION;
                    // color:计算得到的漫反射光照颜色
                    fixed3 color : COLOR;
                };
    
                // 定义 uniform 变量,用于存储外部传入的数据
                uniform sampler2D _MainTex; // 纹理采样器
                uniform float4 _MainTex_ST; // 纹理的平移和缩放参数
    
                // 顶点着色器:负责处理顶点数据并进行必要的变换
                v2f vert(appdata v)
                {
                    v2f o;
                    // 将物体空间的顶点转换为裁剪空间的顶点
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    // 变换纹理坐标
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    
                    // 计算世界空间的法线:将物体空间法线转换为世界空间
                    fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
                    // 计算世界空间的光照方向:获取光源方向并标准化
                    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
    
                    // 计算漫反射光照强度(Dot product):计算法线与光源方向的点积并应用光源的颜色
                    fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));
    
                    // 将计算的漫反射颜色传递给输出
                    o.color = diffuse;
    
                    // 返回处理后的数据
                    return o;
                }
    
                // 片段着色器:负责计算每个像素的颜色
                fixed4 frag(v2f i) : SV_Target
                {
                    // 采样纹理颜色
                    fixed4 color = tex2D(_MainTex, i.uv);
                    // 将纹理颜色与漫反射光照强度相乘,得到最终的颜色
                    color.rgb = color.rgb * i.color;
                    // 返回最终颜色
                    return color;
                }
    
                ENDCG
            }
        }
    }

     首先依然是定义好基础的纹理。

    定义渲染物体为非透明物体,同时光照模式为前向渲染。

    作为顶点着色器输入的数据有顶点坐标,纹理坐标和法线向量,顶点着色器传入片元着色器的数据有顶点坐标,纹理坐标和经由顶点着色器计算得到的光照颜色值。

                // 顶点着色器:负责处理顶点数据并进行必要的变换
                v2f vert(appdata v)
                {
                    v2f o;
                    // 将物体空间的顶点转换为裁剪空间的顶点
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    // 变换纹理坐标
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    
                    // 计算世界空间的法线:将物体空间法线转换为世界空间
                    fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
                    // 计算世界空间的光照方向:获取光源方向并标准化
                    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
    
                    // 计算漫反射光照强度(Dot product):计算法线与光源方向的点积并应用光源的颜色
                    fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));
    
                    // 将计算的漫反射颜色传递给输出
                    o.color = diffuse;
    
                    // 返回处理后的数据
                    return o;
                }

    顶点着色器中把顶点转换成裁剪空间的顶点传入,纹理坐标设置为可变换,然后计算世界空间中的法线方向和光照方向 ,最后计算总的漫反射光照强度。

    最后计算的代码中:saturate代表[0,1],最后得到一个[0,1]的颜色值。

    效果如图:

    逐像素的话:

    Shader "Chapter3/chapter3_1_frag"
    {
        Properties
        {
            // _MainTex: 纹理贴图,默认值为白色纹理
            _MainTex ("Texture", 2D) = "white" {}
        }
        SubShader
        {
            // 设置渲染类型为“不透明”,并指定光照模式为“ForwardBase”。
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
            LOD 100 // 设置最低的细节等级(LOD)
    
            // Pass阶段定义了渲染的一次完整过程。每个Pass包含顶点着色器和片段着色器的执行。
            Pass
            {
                CGPROGRAM
                // 声明顶点着色器和片段着色器
                #pragma vertex vert
                #pragma fragment frag
    
                // 引入Unity的常用着色器代码库
                #include "UnityCG.cginc"
                // 引入Unity的光照模型代码库
                #include "Lighting.cginc"
    
                // 顶点输入结构体,包含从模型传入的顶点数据
                struct appdata
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:物体空间中的顶点位置
                    float4 vertex : POSITION;
                    // normal:物体空间中的法线
                    float3 normal : NORMAL;
                };
    
                // 顶点输出结构体,将数据传递到片段着色器
                struct v2f
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:裁剪空间中的顶点位置
                    float4 vertex : SV_POSITION;
                    // worldNormal:转换到世界空间的法线
                    float3 worldNormal : TEXCOORD1;
                };
    
                // uniform变量:用于从外部传入的数据
                uniform sampler2D _MainTex; // 纹理采样器
                uniform float4 _MainTex_ST; // 纹理的平移和缩放参数
    
                // 顶点着色器:将顶点从物体空间转换到裁剪空间,并计算法线的世界空间表示
                v2f vert(appdata v)
                {
                    v2f o;
                    // 将物体空间的顶点转换为裁剪空间
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    // 变换纹理坐标
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    // 将法线从物体空间转换到世界空间
                    o.worldNormal = UnityObjectToWorldNormal(v.normal);
                    return o;
                }
    
                // 片段着色器:根据纹理和光照计算每个像素的最终颜色
                fixed4 frag(v2f i) : SV_Target
                {
                    // 从纹理中采样颜色
                    fixed4 color = tex2D(_MainTex, i.uv);
    
                    // 规范化法线(确保长度为1)
                    fixed3 worldNormal = normalize(i.worldNormal);
                    // 获取世界空间的光源方向并规范化
                    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
                    // 计算漫反射光照强度:点积计算法线与光源方向的夹角
                    fixed3 diffuse = _LightColor0.rgb * saturate(dot(worldNormal, worldLight));
    
                    // 将纹理颜色与漫反射光照强度相乘,得到最终颜色
                    color.rgb *= diffuse;
    
                    // 返回最终颜色
                    return color;
                }
                ENDCG
            }
        }
    }

    本质区别只是把原来放在顶点着色器中的光照计算流程放置在了片元着色器中。

    额,看起来好像没啥区别,不过大家知道是怎么回事就可以。

    半兰伯特

    可以看到对于兰伯特光照模型而言,计算公式中对于背光面就是简单粗暴的抹零,这显然不太符合现实规律,我们在此基础上可以加些东西。

    我们直接来看代码怎么写,同样分逐顶点和逐像素:

    逐顶点:

    Shader "Chapter3/chapter3_2_vertex_half"
    {
        Properties
        {
            // _MainTex:纹理属性,默认值为白色纹理
            _MainTex ("Texture", 2D) = "white" {}
        }
        SubShader
        {
            // Tags:设置渲染类型为“Opaque”(不透明),光照模式为“ForwardBase”(前向渲染基础光照)
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
            LOD 100 // 设置最低的细节等级(LOD)
    
            // Pass阶段定义了渲染的一次完整过程。每个Pass包含顶点着色器和片段着色器的执行
            Pass
            {
                CGPROGRAM
                // 声明顶点着色器和片段着色器
                #pragma vertex vert
                #pragma fragment frag
    
                // 引入Unity常用的着色器代码库
                #include "UnityCG.cginc"
                // 引入Unity光照模型代码库
                #include "Lighting.cginc"
    
                // 顶点输入结构体:传入从模型中来的数据
                struct appdata
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:物体空间中的顶点位置
                    float4 vertex : POSITION;
                    // normal:物体空间中的法线
                    float3 normal : NORMAL;
                };
    
                // 顶点输出结构体:传递数据给片段着色器
                struct v2f
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:裁剪空间中的顶点位置
                    float4 vertex : SV_POSITION;
                    // color:在顶点着色器中计算的颜色,传递给片段着色器
                    fixed3 color : COLOR;
                };
    
                // uniform变量:用于从外部传入的数据
                uniform sampler2D _MainTex; // 纹理采样器
                uniform float4 _MainTex_ST; // 纹理的平移和缩放参数
    
                // 顶点着色器:将顶点从物体空间转换为裁剪空间,并计算颜色
                v2f vert(appdata v)
                {
                    v2f o;
                    // 将物体空间中的顶点转换为裁剪空间中的顶点
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    // 将纹理坐标进行变换
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    // 将法线从物体空间转换到世界空间,并规范化(使长度为1)
                    fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
                    // 获取光源的世界空间位置并规范化
                    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
                    // 计算漫反射光照:将法线与光源的方向计算点积,并进行平滑处理
                    // (dot(worldNormal, worldLight) * 0.5 + 0.5) 用于让点积的范围变为 [0, 1],以适应颜色计算
                    fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);
                    // 将计算出的颜色传递给输出结构体
                    o.color = diffuse;
                    return o;
                }
    
                // 片段着色器:根据纹理和计算的光照颜色来生成最终像素的颜色
                fixed4 frag(v2f i) : SV_Target
                {
                    // 从纹理中采样颜色
                    fixed4 color = tex2D(_MainTex, i.uv);
                    // 将采样到的颜色与漫反射光照强度相乘,得到最终的颜色
                    color.rgb = color.rgb * i.color;
                    // 返回最终颜色
                    return color;
                }
                ENDCG
            }
        }
    }

     其中的核心变化:

    fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);

     效果如图:

    逐像素:

    Shader "Chapter3/chapter3_2_frag_half"
    {
        Properties
        {
            // _MainTex:纹理属性,默认值为白色纹理
            _MainTex ("Texture", 2D) = "white" {}
        }
        SubShader
        {
            // Tags:渲染类型为“不透明”,光照模式为“ForwardBase”(前向渲染基础光照)
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
            LOD 100 // 设置着色器的最低细节等级(LOD)
    
            // Pass阶段定义了渲染的具体过程,包括顶点着色器和片段着色器
            Pass
            {
                CGPROGRAM
                // 声明顶点着色器(vert)和片段着色器(frag)
                #pragma vertex vert
                #pragma fragment frag
    
                // 引入Unity常用的着色器代码库
                #include "UnityCG.cginc"
                // 引入Unity的光照模型代码库
                #include "Lighting.cginc"
    
                // 顶点输入结构体:传入从模型中来的数据
                struct appdata
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:物体空间中的顶点位置
                    float4 vertex : POSITION;
                    // normal:物体空间中的法线
                    float3 normal : NORMAL;
                };
    
                // 顶点输出结构体:传递数据给片段着色器
                struct v2f
                {
                    // uv:纹理坐标
                    float2 uv : TEXCOORD0;
                    // vertex:裁剪空间中的顶点位置
                    float4 vertex : SV_POSITION;
                    // worldNormal:世界空间中的法线
                    float3 worldNormal : TEXCOORD1;
                };
    
                // uniform变量:用于从外部传入的数据
                uniform sampler2D _MainTex; // 纹理采样器
                uniform float4 _MainTex_ST; // 纹理的平移和缩放参数
    
                // 顶点着色器:将顶点从物体空间转换为裁剪空间,并计算世界空间中的法线
                v2f vert(appdata v)
                {
                    v2f o;
                    // 将物体空间中的顶点转换为裁剪空间中的顶点
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    // 将纹理坐标进行变换
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    // 将法线从物体空间转换到世界空间,并规范化(确保法线长度为1)
                    o.worldNormal = UnityObjectToWorldNormal(v.normal);
                    return o;
                }
    
                // 片段着色器:根据纹理和计算的光照颜色来生成最终像素的颜色
                fixed4 frag(v2f i) : SV_Target
                {
                    // 从纹理中采样颜色
                    fixed4 color = tex2D(_MainTex, i.uv);
    
                    // 将世界空间中的法线规范化
                    fixed3 worldNormal = normalize(i.worldNormal);
                    // 获取光源的世界空间位置,并规范化
                    fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
                    // 计算漫反射光照,计算法线与光源方向的点积并进行平滑处理
                    // (dot(worldNormal, worldLight) * 0.5 + 0.5) 用于让点积的范围变为 [0, 1]
                    fixed3 diffuse = _LightColor0.rgb * (dot(worldNormal, worldLight) * 0.5 + 0.5);
                    // 将纹理的颜色与漫反射光照颜色相乘,得到最终的颜色
                    color.rgb *= diffuse;
                    return color;
                }
                ENDCG
            }
        }
    }

     效果如图:

    冯模型

    兰伯特模型是只考虑漫反射的光照模型,但是显然真正的光照模型不仅仅只有这么一个考量。

    我们在漫反射的基础上加入一个作为环境光的常量和一个反射光组成我们的冯光照模型。

    显然这众多参数之中,N,I,V向量都是可以直接拿到的,但是R(反射方向)需要我们去进行计算。

    于是我们得到了所有计算的方法:

    Shader "Chapter3/chapter3_3_phong"
    {
        Properties
        {
            // 漫反射颜色
            _Diffuse("Diffuse", Color) = (1,1,1,1)
            
            // 镜面反射颜色
            _Specular("Specular", Color) = (1, 1, 1, 1)
            
            // 光泽度(控制镜面高光的锐利程度)
            _Gloss("Gloss", Range(8.0, 256)) = 20
        }
        SubShader
        {
            Pass
            {
                Tags
                {
                    // 设置光照模式为ForwardBase,适用于前向渲染
                    "LightMode" = "ForwardBase"
                }
    
                CGPROGRAM
                // 声明顶点着色器和片段着色器的函数
                #pragma vertex vert
                #pragma fragment frag
                
                // 引入Unity的光照计算库
                #include "Lighting.cginc"
    
                // 声明传递给着色器的属性
                fixed4 _Diffuse;  // 漫反射颜色
                fixed4 _Specular; // 镜面反射颜色
                float _Gloss;     // 光泽度
    
                // 顶点数据结构
                struct a2v
                {
                    float4 vertex : POSITION;  // 顶点位置
                    float3 normal : NORMAL;    // 顶点法线
                };
    
                // 传递给片段着色器的数据结构
                struct v2f
                {
                    float4 pos : SV_POSITION;  // 顶点位置,最终转换为裁剪空间坐标
                    float3 worldNormal : TEXCOORD0; // 世界空间中的法线
                    float3 worldPos : TEXCOORD1;    // 世界空间中的位置
                };
    
                // 顶点着色器
                v2f vert(a2v v)
                {
                    v2f o;
                    // 将物体空间的顶点坐标转换为裁剪空间坐标
                    o.pos = UnityObjectToClipPos(v.vertex);
                    
                    // 将物体空间中的法线转换为世界空间法线
                    o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                    
                    // 获取物体在世界空间中的位置
                    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    
                    return o;
                }
    
                // 片段着色器
                fixed4 frag(v2f i) : SV_Target
                {
                    // 归一化世界空间中的法线
                    fixed3 worldNormal = normalize(i.worldNormal);
                    // 归一化光源方向
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                    // 计算反射方向
                    fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
                    // 计算观察方向
                    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
    
                    // 环境光(可以在Lighting.cginc中找到计算)
                    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    
                    // 漫反射光照(根据法线和光源方向的点积计算)
                    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
    
                    // 镜面反射光照(反射方向与视线方向的点积,经过光泽度指数计算)
                    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
    
                    // 返回最终的颜色,包含环境光、漫反射光和镜面反射光
                    return fixed4(ambient + diffuse + specular, 1.0);
                }
                ENDCG
            }
        }
        FallBack "Diffuse" // 如果无法使用此着色器,则使用内建的Diffuse着色器作为回退
    }

    效果如图:

    布林-冯模型

    布林-冯模型可以看作是冯模型的优化版本:

    代码如下:

    Shader "Chapter3/chapter3_3_blinn_phong"
    {
        Properties
        {
            // 漫反射颜色
            _Diffuse("Diffuse", Color) = (1,1,1,1)
            
            // 镜面反射颜色
            _Specular("Specular", Color) = (1, 1, 1, 1)
            
            // 光泽度(控制镜面高光的锐利程度)
            _Gloss("Gloss", Range(8.0, 256)) = 20
        }
    
        SubShader
        {
            Pass
            {
                Tags
                {
                    // 设置光照模式为ForwardBase,适用于前向渲染
                    "LightMode" = "ForwardBase"
                }
    
                CGPROGRAM
                // 声明顶点着色器和片段着色器的函数
                #pragma vertex vert
                #pragma fragment frag
                
                // 引入Unity的光照计算库
                #include "Lighting.cginc"
    
                // 声明传递给着色器的属性
                fixed4 _Diffuse;  // 漫反射颜色
                fixed4 _Specular; // 镜面反射颜色
                float _Gloss;     // 光泽度
    
                // 顶点数据结构
                struct a2v
                {
                    float4 vertex : POSITION;  // 顶点位置
                    float3 normal : NORMAL;    // 顶点法线
                };
    
                // 传递给片段着色器的数据结构
                struct v2f
                {
                    float4 pos : SV_POSITION;  // 顶点位置,最终转换为裁剪空间坐标
                    float3 worldNormal : TEXCOORD0; // 世界空间中的法线
                    float3 worldPos : TEXCOORD1;    // 世界空间中的位置
                };
    
                // 顶点着色器
                v2f vert(a2v v)
                {
                    v2f o;
                    // 将物体空间的顶点坐标转换为裁剪空间坐标
                    o.pos = UnityObjectToClipPos(v.vertex);
                    
                    // 将物体空间中的法线转换为世界空间法线
                    o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
                    
                    // 获取物体在世界空间中的位置
                    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    
                    return o;
                }
    
                // 片段着色器
                fixed4 frag(v2f i) : SV_Target
                {
                    // 归一化世界空间中的法线
                    fixed3 worldNormal = normalize(i.worldNormal);
                    // 归一化光源方向
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                    // 计算观察方向(从世界空间中的物体位置到摄像机位置)
                    fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                    // 计算半程向量(Blinn-Phong模型的关键)
                    fixed3 halfDir = normalize(worldLightDir + viewDir);
    
                    // 环境光(从Lighting.cginc中获取)
                    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
    
                    // 漫反射光照(根据法线和光源方向的点积计算)
                    fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
    
                    // 镜面反射光照(Blinn-Phong模型,使用半程向量和法线的点积,经过光泽度指数计算)
                    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
    
                    // 返回最终的颜色,包含环境光、漫反射光和镜面反射光
                    return fixed4(ambient + diffuse + specular, 1.0);
                }
                ENDCG
            }
        }
        FallBack "Diffuse" // 如果无法使用此着色器,则使用内建的Diffuse着色器作为回退
    }

    和布林-冯模型最大的区别:

    // 计算半程向量(Blinn-Phong模型的关键)
    fixed3 halfDir = normalize(worldLightDir + viewDir);
    ...
    // 镜面反射光照(Blinn-Phong模型,使用半程向量和法线的点积,经过光泽度指数计算)
    fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);

     多了一个半程向量的计算并更改了高光反射计算的方法。

    效果如下:

    可以看到高光的范围更大了,且这样减少了计算量。

    智慧消防安全与应急管理是现代城市安全管理的重要组成部分,随着城市化进程的加速,传统消防安全管理面临着诸多挑战,如消防安全责任制度落实不到位、消防设施日常管理不足、消防警力不足等。这些问题不仅制约了消防安全管理水平的提升,也给城市的安全运行带来了潜在风险。然而,物联网和智慧城市技术的快速发展为解决这些问题提供了新的思路和方法。智慧消防作为物联网和智慧城市技术结合的创新产物,正在成为社会消防安全管理的新趋势。 智慧消防的核心在于通过技术创新实现消防安全管理的智能化和自动化。其主要应用包括物联网消防安全监管平台、城市消防远程监控系统、智慧消防平台等,这些系统利用先进的技术手段,如GPS、GSM、GIS等,实现了对消防设施的实时监控、智能巡检和精准定位。例如,单兵定位方案通过信标点定位和微惯导加蓝牙辅助定位技术,能够精确掌握消防人员的位置信息,从而提高救援效率和安全性。智慧消防不仅提升了消防设施的管理质量,还优化了社会消防安全管理资源的配置,降低了管理成本。此外,智慧消防的应用还弥补了传统消防安全管理中数据处理方式落后、值班制度执行不彻底等问题,赋予了建筑消防设施智能化、自动化的能力。 尽管智慧消防技术在社会消防安全管理工作中的应用已经展现出巨大的潜力和优势,但目前仍处于实践探索阶段。相关职能部门和研究企业需要加大研究开发力度,进一步完善系统的功能与实效性。智慧消防的发展既面临风险,也充满机遇。当前,社会消防安全管理工作中仍存在制度执行不彻底、消防设施日常维护不到位等问题,而智慧消防理念与技术的应用可以有效弥补这些弊端,提高消防安全管理的自动化与智能化水平。随着智慧城市理念的不断发展和实践,智慧消防将成为推动社会消防安全管理工作与城市化进程同步发展的关键力量。
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值