UnityShaderLab读书笔记 第6章(光照模型)

本文详细解读了3D渲染中的光照模型,包括基础的Lambert模型,解决暗面问题的Half-Lambert,以及模拟光滑表面的Phong和提高效率的Blinn-Phong。讲解了它们的公式和在Unity Shader中的实现,以及逐像素光照的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  • 前5章主要是基础3D数学和Shader基本语法后面抽空再补上

第6章 光照模型

  • 本章主要内容光照模型:Lambert、Half-Lambert、Phong、Blinn-Phong等

Lambert光照模型

  • 物体的颜色是由射进眼睛里的光决定的,有反射光和透视光
  • 漫反射满足Lambert定律:反射光的强度与表面法线和光源方向夹角成正比,公式如下
    Cdiffuse=(Clight⋅Mdiffuse)saturate(n⋅l)C_{diffuse}=(C_{light}·M_{diffuse})saturate(n·l)Cdiffuse=(ClightMdiffuse)saturate(nl)
    ClightC_{light}Clight为入射光线颜色,MdiffuseM_{diffuse}Mdiffuse为材质漫反射颜色,lll为从物体指向光的方向,根据点乘可以知道当nnnlll夹角越小漫反射强度就越大,但是当Cos夹角大于90为负,所以使用saturate函数限定至(0,1)
v2f vert (appdata_base v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    float3 n = UnityObjectToWorldNormal(v.normal);
    n = normalize(n);
    // _WorldSpaceLightPos0和_LightColor0包含在UnityLightingCommon.cginc文件中
    // l处在世界空间中,所以法向量需要转换
    fixed3 l = normalize(_WorldSpaceLightPos0.xyz);

    fixed ndotl = dot(n,l);
    // Lambert公式
    o.diff = _LightColor0 * _MainColor * saturate(ndotl);

    return o;
}

Half-Lambert光照模型

  • 可以法线背对着光源,直接就是黑色的,所以提出了Half-Lambert,直接看下面公式
    Cdiffuse=(Clight⋅Mdiffuse)[0.5(n⋅l)+0.5]C_{diffuse}=(C_{light}·M_{diffuse})[0.5(n·l)+0.5]Cdiffuse=(ClightMdiffuse)[0.5(nl)+0.5]
    这么看(n⋅l)∈[−1,1](n·l)∈[-1,1](nl)[1,1][0.5(n⋅l)+0.5]∈[0,1][0.5(n⋅l)+0.5]∈[0,1][0.5(nl)+0.5][0,1]结果好像没啥区别,因为saturate也是[0,1]区间,但是仔细看一下saturate直接把(n⋅l)(n·l)(nl)小于0的部分全部给抹掉了,这意味小于90度漫反射强度在[0,1],大于90度的全黑,而Half-Lambert公式并不像Lambert这么暴力具体看下面两张图

Phong光照模型

  • Lambert可以很好的模拟粗糙表面,但是现实生活中还有很多光滑的表面如金属、陶瓷等
    CsurfaceColor=CAmbient+CDiffuse+CSpecularC_{surfaceColor}=C_{Ambient}+C_{Diffuse}+C_{Specular}CsurfaceColor=CAmbient+CDiffuse+CSpecular
    CAmbientC_{Ambient}CAmbient为环境光、CSpecularC_{Specular}CSpecular为镜面反射,因为光照射到光滑表面还会反射一部分光
    CSpecular=(Clight⋅Mspecular)saturate(v⋅r)MshininessC_{Specular}=(C_{light}·M_{specular})saturate(v·r)^{M_{shininess}}CSpecular=(ClightMspecular)saturate(vr)Mshininess
    MspecularM_{specular}Mspecular物体材质的镜面反射颜色、vvv是视角方向(顶点指向摄像机)、rrr是光线反射的方向、MshininessM_{shininess}Mshininess材质的光泽度
  • 可以从UnityShaderVariables.cginc得到环境光变量,具体代码如下
Shader "Unlit/Phong"
{
    Properties
    {
        _MainColor("MainColor", Color) = (1,1,1,1)
        // 材质镜面反射颜色
        _SpecularColor("SpecularColor", Color) = (1,1,1,1)
        // 材质光泽度
        _Shininess("Shininess", Range(1,100)) = 1
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed4 color:COLOR0;
            };

            fixed4 _MainColor;
            fixed4 _SpecularColor;
            half _Shininess;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                float3 n = UnityObjectToWorldNormal(v.normal);
                n = normalize(n);
                fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                fixed ndotl = saturate(dot(n,l));
                // 漫反射
                fixed4 diff = _LightColor0 * _MainColor * ndotl;
                // 计算光的反射方向,l计算从顶点到光源的方向
                // 计算反射reflect需要的入射光线所以为-l
                float3 r = reflect(-l, n);
                r = normalize(r);
                // 顶点指向摄像机
                fixed3 view = WorldSpaceViewDir(v.vertex);
                view = normalize(view);

                fixed vdotr = saturate(dot(view, r));
                // 镜面反射公式
                fixed4 spec = _LightColor0 * _SpecularColor * pow(vdotr, _Shininess);
                // phong公式
                o.color = unity_AmbientSky + diff + spec;
                return o;
            }

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

逐像素光照

  • 在上面Phong的材质转动视角会发现一个问题,镜面反射光并不光滑会出现棱角,如下图

    这是因为上面代码是逐顶点光照而不是逐像素光照,逐顶点光照就是对每个顶点计算颜色,然后根据顶点在多边形占据的面积进行线性插值,这在三角面多的模型中没什么大问题,在三角面不是很多的模型上就会出现棱角,就像上面Unity默认球体模型
  • 那什么又是逐像素呢?就是在片元着色器中计算,颜色的计算不再基于顶点,而是基于像素,最终屏幕越大计算量越大
    具体可以看下图,上面是逐片元,下面是逐顶点可以很明显看出不同,代码和逐顶点其实没有什么太大不同
下面是逐像素光照
Shader "Unlit/FragPhong"
{
    Properties
    {
        _MainColor("MainColor", Color) = (1,1,1,1)
        // 材质镜面反射颜色
        _SpecularColor("SpecularColor", Color) = (1,1,1,1)
        // 材质光泽度
        _Shininess("Shininess", Range(1,100)) = 1
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 normal:TEXCOORD0;
                // 用来计算view所以需要将模型空间顶点坐标传给片元着色器
                float4 vertex:TEXCOORD1;
            };

            fixed4 _MainColor;
            fixed4 _SpecularColor;
            half _Shininess;

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

            fixed4 frag (v2f i) : SV_Target
            {
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);
                fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                fixed ndotl = saturate(dot(n,l));
                // 漫反射
                fixed4 diff = _LightColor0 * _MainColor * ndotl;
                // 计算光的反射方向,l计算从顶点到光源的方向
                // 计算反射reflect需要的入射光线所以为-l
                float3 r = reflect(-l, n);
                r = normalize(r);
                // 顶点指向摄像机
                fixed3 view = WorldSpaceViewDir(i.vertex);
                view = normalize(view);

                fixed vdotr = saturate(dot(view, r));
                // 镜面反射公式
                fixed4 spec = _LightColor0 * _SpecularColor * pow(vdotr, _Shininess);
                // phong公式
                return unity_AmbientSky + diff + spec;
            }
            ENDCG
        }
    }
}

Blinn-Phong光照模式

CSpecular=(Clight⋅Mspecular)saturate(n⋅h)MshininessC_{Specular}=(C_{light}·M_{specular})saturate(n·h)^{M_{shininess}}CSpecular=(ClightMspecular)saturate(nh)Mshininess
Blinn-Phong不再依靠光反射向量rrr,而是依靠视角方向和灯光方向的角平分向量hhh计算,然后与法向量点乘,性能方面当观察者和灯光离物体很远时,Blinn可将h视为常量,而普通Phong需要根据表面曲率逐一去计算反射向量,Blinn计算效率远高于普通Phong代码片段如下

fixed4 frag (v2f i) : SV_Target
{
    float3 n = UnityObjectToWorldNormal(i.normal);
    n = normalize(n);
    fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
    fixed ndotl = saturate(dot(n,l));
    // 漫反射
    fixed4 diff = _LightColor0 * _MainColor * ndotl;

    // 顶点指向摄像机
    fixed3 view = WorldSpaceViewDir(i.vertex);
    view = normalize(view);

    // Blinn通过计算视角方向和光方向的角平分线向量
    float3 h = normalize(l + view);

    fixed ndoth = saturate(dot(n, h));
    // 镜面反射公式
    fixed4 spec = _LightColor0 * _SpecularColor * pow(ndoth, _Shininess);
    // phong公式
    return unity_AmbientSky + diff + spec;
}

渲染路径

  • 当显卡无法使用选定的渲染路径时,Unity将会选择较低精度的渲染路径,例如选择延迟渲染无法执行,将会选择前向渲染

  • 延迟渲染路径:
    – 灯光Pass基于G-Buffer(屏幕空间缓存)和深度计算光照,与场景复杂度无关,可以避免计算因未通过深度测试而被丢弃的片段,所有灯光都可以使用阴影和Cookie
    – 延迟渲染不支持真正的抗锯齿、处理半透明物体,使用延迟渲染的模型不支持接收阴影选项、Culling Mask只能再限定条件下使用,并且只能有4个
    – 延迟渲染只能再MRT(多重渲染目标)、Shader Model3或以上并支持深度渲染贴图的显卡运行,移动端可以在OpenGL ES3.0或以上设备运行,并且延迟渲染不支持正交投影模式摄像机

  • 前向渲染路径:
    – 前向渲染支持Unity所有图形功能,一部分最亮的灯光以完全逐像素渲染,4个点光源以逐顶点方式渲染,其余灯光使用更加快速的SH(球谐)光照渲染
    灯光的渲染方式具体以下决定
    –Not Important总是以逐顶点或SH
    – Important总是以逐像素
    – 最亮的平行光总是以逐像素
    – 如果逐像素灯光少于项目质量设置中Pixel Light数量,其余比较亮的灯光以逐像素渲染

  • Unity官方例子,如下图

  • 基础Pass包含一个逐像素的平行光和所有逐顶点或SH灯光,并且包含所有来自Shader光照贴图、环境光、自发光,平行光能投射阴影,但灯光贴图不能接受SH灯光照明

  • 其他逐像素灯光,每个灯光会产生额外的Pass,前线渲染只支持一个投射阴影的平行光,额外Pass中灯光默认不会投射阴影, 需要支持的话需要添加变体编译

实现阴影效果

Shader "Unlit/ShadowLambert"
{
    Properties
    {
        _MainColor("MainColor", Color) = (1,1,1,1)
    }
    SubShader
    {
        // 基础Pass 平行光产生阴影
        Pass
        {
            Tags{"LightMode" = "ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // 在当前pass渲染的每个灯光编译出不同Shader变体
            #pragma multi_compile_fwdbase
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 normal:TEXCOORD0;
                float4 vertex:TEXCOORD1;
                //使用预定义宏保存阴影坐标 2代表TEXCOORD2 这个宏在AutoLight.cginc
                SHADOW_COORDS(2)
            };

            fixed4 _MainColor;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                o.vertex = v.vertex;
                // 这个宏也定义在AutoLight.cginc,变换阴影贴图的纹理坐标存入结构体o中
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);
                // 原来这样计算光源方向fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                // 使用_WorldSpaceLightPos0时,会导致点光源和聚光灯位置对光照效果不起作用
                // i.vertex是float4
                float3 l = WorldSpaceLightDir(i.vertex);
                l = normalize(l);
                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // lambert
                fixed ndotl = saturate(dot(n, l));
                fixed4 c = _LightColor0 * _MainColor * ndotl;

                // 加4个点光源光照
                c.rgb += Shade4PointLights(
                     //4个点光源坐标、颜色、衰减、世界坐标、世界法线
                    unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
                    unity_LightColor[0].rgb,unity_LightColor[1].rgb,
                    unity_LightColor[2].rgb,unity_LightColor[3].rgb,
                    unity_4LightAtten0, worldPos.rgb, n) * _MainColor;

                // 环境光
                c += unity_AmbientSky;

                // 计算阴影系数
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb);

                c.rgb += shadowmask;

                return c;
            }
            ENDCG
        }

        // 为除主光外其他逐像素灯光产生投影
        Pass
        {
            Tags{"LightMode" = "ForwardAdd"}

            // 使绘制的图像与上面的Pass完全混合
            Blend One One

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdadd_fullshadows
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            struct v2f{
                float4 pos :SV_POSITION;
                float3 normal :TEXCOORD0;
                float4 vertex : TEXCOORD1;
                SHADOW_COORDS(2)
            };

            fixed4 _MainColor;

            v2f vert (appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                o.vertex = v.vertex;
                // 这个宏也定义在AutoLight.cginc,变换阴影贴图的纹理坐标存入结构体o中
                TRANSFER_SHADOW(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float3 n = UnityObjectToWorldNormal(i.normal);
                n = normalize(n);
                // 原来这样计算光源方向fixed3 l = normalize(_WorldSpaceLightPos0.xyz);
                // 使用_WorldSpaceLightPos0时,会导致点光源和聚光灯位置对光照效果不起作用
                // i.vertex是float4
                float3 l = WorldSpaceLightDir(i.vertex);
                l = normalize(l);
                float4 worldPos = mul(unity_ObjectToWorld, i.vertex);

                // lambert
                fixed ndotl = saturate(dot(n, l));
                fixed4 c = _LightColor0 * _MainColor * ndotl;

                // 加4个点光源光照
                c.rgb += Shade4PointLights(
                     //4个点光源坐标、颜色、衰减、世界坐标、世界法线
                    unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
                    unity_LightColor[0].rgb,unity_LightColor[1].rgb,
                    unity_LightColor[2].rgb,unity_LightColor[3].rgb,
                    unity_4LightAtten0, worldPos.rgb, n) * _MainColor;

                // 环境光
                c += unity_AmbientSky;

                // 计算阴影系数
                UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb);

                c.rgb += shadowmask;

                return c;
            }


            ENDCG
        }
    }
    // 阴影投射Pass
    FallBack "Diffuse"
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值