Unity Shader知识点(三)高光反射Shader

前言

此文及专栏系是以Shader入门精要为基础整理的Unity Shader学习笔记,尽量以初学者视角还原(其实半年前我就是初学者),错误还需指正。

本篇是实操部分的第三个Shader,即高光反射Shader,文章选取顶点着色器生成的高光反射Shader作为说明,具体名词可能不再解释。

高光反射模型

本篇实现的是高光反射这一渲染中的重要主题,这种效果通常出现在金属、镜面反射等场合。按照图形学中基础的光照模型,高光反射的光强及色彩由以下公式决定:

c_{specular}=(c_{light}*m_{specular})max(0,\hat{v}*r)^{m_{glass}}

从左到右的参数分别是:最终结果,即高光反射的光强和色彩;入射光线的颜色和强度;高光反射系数;max函数的运算结果,是对观察视角向量v和反射方向向量r的取正。

m_{glass}材质的光泽度,这可能是大多数人不曾了解过的名词,事实上我尝试如下理解,请看下面这个光球,如果无视右下角的光泽,那么大亮点就是我们的高光部分。想象一下,亮点的中心那里反射光线正好与观察视角向量重合,也即我们的“眼睛”正好接收到那里的反射光线,v和r单位向量乘积为1;而偏移了那里,反射光线就和我们的视角有夹角了,v和r这两个单位向量的乘积逐渐减小,因为光泽度作为指数存在,这个结果逐渐接近0,也就是光线减弱趋向于消失。

当然,反射方向我们还需要计算,其公式为:

r = 2(\hat{n}*\hat{I})\hat{n}-\hat{I}

这一效果可以由CG语言的相关函数得到。其中n和I向量分别为法线方向和光源入射方向。

高光反射Shader

接下来正式撰写我们的Shader,仍然基于2020版本的Unity,实测可以渲染成功,环境配置不再赘述,全部代码如下:


Shader "Unlit/HighLightShader"
{
    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 { "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

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

            v2f vert (a2v v)
            {
                //这是反射部分计算,参考上一篇
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				fixed halfLambert = dot(worldNormal, worldLightDir)*0.8+0.2;
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;

                //接下来正式计算高光部分
                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)),_Gloss);
                o.color = ambient + diffuse +specular;

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 片元着色器,简单传参
                return fixed4(i.color, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Specular"
}

Properties块参数引用

区别于简单的漫反射,我们的高光Shader不能再使用基础的Color属性参数了,这里提供给材质方面的自定义接口还包括反射颜色和光泽度。

    Properties {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
        _Specular ("Specular", Color) = (1, 1, 1, 1) //这是反射色彩纹理
        _Gloss("Gloss", Range(8.0, 256)) = 20    //这是光泽度
    }

我们传入一个Color属性参数,用于反射色彩(为什么要专门新建一个变量,区别于漫反射色彩?我的理解是这玩意和漫反射还是不一样的,比如玻璃和一些镀膜金属就有和表面不同的反射色彩,而且这也方便了游戏美术的运用),至于光泽度Gloss采用float类型,我们这里用Range函数定义,这个函数一看就懂,表示数值范围是8到256(我们前面对光泽度有解释,这个参数放在公式指数位置,因此必然是一个远大于1的值)

格式语言

    SubShader {
        Pass {
            Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Lighting.cginc"
            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;
            struct a2v
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };
            struct v2f
            {
                float4 pos : SV_POSITION;
                fixed3 color : COLOR;
            };

这里不再多解释了,tags表示Pass块在渲染管线中的位置,#pragma用法指定着色器函数,#include包含内置库以使用参数,然后再把properties中的参数重新声明;a2v和v2f分别是我们顶点着色器的输入和输出结构体,区别于上一篇漫反射Shader中计算大部分在片元着色器,我们这里计算主要在顶点着色器完成,所以需要把色彩信息color也传出。

顶点着色器

v2f vert (a2v v)
            {
                //这是反射部分计算,参考上一篇
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
				fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
				fixed halfLambert = dot(worldNormal, worldLightDir)*0.8+0.2;
				fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;

                //接下来正式计算高光部分
                fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)),_Gloss);
                o.color = ambient + diffuse +specular;

                return o;
            }

顶点着色器的内容分两部分,反射部分和高光部分。

反射部分参考上一篇文章,其原理相近。首先拿到Unity计算好的输入结构体,使用UnityObjectToClipPos裁剪vertex(实际上是POSITION顶点位置)(裁剪可以理解为过滤掉不会被渲染的点),然后通过内置的变换矩阵unity_WorldToObject将这一坐标转换成世界坐标系,最后用normalize将其单位化

UNITY_LIGHTMODEL_AMBIENT是内置变量,代表系统接收到的环境光部分(封装相当好,不必关心实现)。_LightColor0和_WorldSpaceLightPos0分别是光照的光量(色彩和光强)和方向向量信息,我们将这些参数引用,并对光照方向向量进行单位化。(xyz自然就是坐标啦)

接下来使用了修正过的半兰伯特光照模型,我们上次提到,纯粹按照漫反射模型计算,因为没有考虑到反射,物体的阴影部分是纯黑的。半兰伯特模型本来是对光照和物体法线两个向量的运算结果,做一个线性变换,也就是dot(worldNormal, worldLightDir)*0.5+0.5,保证不会出现纯黑,但是这样会导致我们最后渲染出的高光不明显,不利于使用,因此修正为dot(worldNormal, worldLightDir)*0.8+0.2。最后_LightColor0.rgb * _Diffuse.rgb * halfLambert这个式子计算出漫反射结果

接下来就是高光部分了,根据公式

r = 2(\hat{n}*\hat{I})\hat{n}-\hat{I}

我们用reflect(-worldLightDir, worldNormal) 计算出反射向量(我们不关心封装好的函数,但可以知道原理,不是吗),这里的worldLightDir加了个负号取反了,因为reflect函数的入射光线要求是光照点指向光源,而unity提供的_WorldSpaceLightPos0是反向的,需要反着来。

c_{specular}=(c_{light}*m_{specular})max(0,\hat{v}*r)^{m_{glass}}

我们来看公式,此时,我们已经拿到了c_{light}(_LightColor0,内置参数)、m_{specular}(_Specular.rgb,properties引入的参数)、反射向量和观察视角向量,_LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)),_Gloss)这个式子计算出最后的高光反射结果(pow是指数运算函数),然后与漫反射结果相加,输出即可。

片元着色器和结束

            fixed4 frag (v2f i) : SV_Target
            {
                // 片元着色器,简单传参
                return fixed4(i.color, 1.0);
            }
            ENDCG
        }
    }
    Fallback "Specular"

 前面提过,这个Shader没用片元着色器进行计算,因此frag函数的内容非常简单,加一个默认值1.0的分量即可。最后Fallback里面用Specular,因为我们这是高光反射Shader嘛,应该拿unity的反射Shader作为备份。

总结

最后的渲染效果是右边这样,左边则是上一篇的漫反射。你看,是不是很像unity默认的那种,感觉有点油腻的反射。事实上不必嫌弃它,我们已经实现了基本的高光反射,预设的一些目标也已经完成,比如我们修正的半兰伯特模型也保证了背后不至于全黑。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值