Unity Shader知识点(三)高光反射Shader_unityobjecttoclippos-程序员宅基地

技术标签: unity  游戏引擎  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默认的那种,感觉有点油腻的反射。事实上不必嫌弃它,我们已经实现了基本的高光反射,预设的一些目标也已经完成,比如我们修正的半兰伯特模型也保证了背后不至于全黑。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_18547605/article/details/123337927

智能推荐

php cdi_异步CDI事件-程序员宅基地

文章浏览阅读104次。php cdi 几天前,在我们的常规代码审查中,我的一位同事提出了一个问题,即如果可能,一次同时调用CDI观察者(这样的方法带有参数@Observes )将发生多次?用于不同的事件实例。 换句话说,在产生少量事件之后,是否有可能同时由多个线程处理以下方法: public void observe(@Observes MyEvent myEvent) { ... } 考虑一下之后,我决定运行一..._.lockcdi

费用流模板-程序员宅基地

文章浏览阅读251次。最小费用最大流:(SPFA求最长路)(下面是一个求二分图最小权重的例题)375. 蚂蚁将白黑点左右分部,连接所有边,这道题等价于求总边权最小。#include<cstdio>#include<algorithm>#include<cmath>#include<cstring>#include<queue>using..._费用流模板

终极成语接龙,谁能继续往下接,哈哈!!!_车马填门,马中关五-程序员宅基地

文章浏览阅读4.3w次。胸有成竹 → 竹柏异心 → 心安理得 → 得薄能鲜 → 鲜为人知 → 知不诈愚 → 愚不可及 → 及宾有鱼 → 鱼帛狐篝 → 篝灯呵冻 → 冻解冰释 → 释车下走 → 走伏无地 → 地北天南 → 南北东西 → 西除东荡 → 荡产倾家 → 家败人亡 → 亡不待夕 → 夕寐宵兴 → 兴不由己 → 己饥己溺 → 溺爱不明 → 明白了当 → 当场出彩 → 彩笔生花 → 花闭月羞 → 羞惭满面 → 面壁九年_车马填门,马中关五

js立即执行函数 (IIFE)_js中iief-程序员宅基地

文章浏览阅读227次。特点1、立即执行2、用完释放(function(){})()(function(){}())_js中iief

IntelliJ IDEA 2017.1 EAP与异步堆栈跟踪调试器扩展_ideal 调试 completablefuture-程序员宅基地

文章浏览阅读6.8k次。20170214IntelliJ IDEA 2017.1 EAP与异步堆栈跟踪调试器扩展作者钉钉用户名:@Li Zhang封面配图:文章摘要:反应性编程趋势后,我们的代码越来越异步。文章正文:早些时候java8介绍了CompletableFuture(采用Guava’s ListenableFuture),通过Akka, Ratpack, Reactor, RxJava, Vert.x以及其它库实现_ideal 调试 completablefuture

随便推点

基于ubuntu22.04对使用 cmake 对linuxdeployqt进行源码编译,解决编译中遇到的多个错误并最终成功生成 linuxdeployqt可执行程序_linuxdeployqt ubuntu 22-程序员宅基地

文章浏览阅读429次。【代码】基于ubuntu22.04进行对linuxqtdeploy进行源码编译,解决编译中遇到的多个错误并最终成功生成 linuxdeployqt可执行程序。_linuxdeployqt ubuntu 22

PyTorch-tensorboard_tensorboard pytorch-程序员宅基地

文章浏览阅读553次。tensorboard是个可视化工具,用于观察神经网络训练过程SummaryWriter:首先需要创建一个SummaryWriter示例其中有些参数不常用,我们可以只在参数里写上一个训练保存的路径即可例:writer=SummaryWriter('logs')我们在SummaryWriter中常用的方法有:1. add_scalar2. add_imageadd_scalaradd_scalar(tag, scalar_value, global_step=None, walltime=_tensorboard pytorch

PyCharm远程连接失败、错误,报错:Can‘t connect...【解决方法与错误分析】_pycharm无法联网-程序员宅基地

文章浏览阅读2.8w次,点赞6次,收藏15次。一、前言:首先确保PyCharm是专业版而非社区版的,社区版的菜单栏中没有此功能。二、报错:输入账号、密码测试连接的时候(2020版本)(2018版本)三、错误分析:【我的错误原因】:先说结论吧,我个人的原因是因为选的是SFTP协议,因此改换Linux系统的服务器就可以了,因为本人选的是SFTP协议,这个协议需要SSH协议的支持,而SSH协议是Linux才有的,而Windows没有,准确来说是不自带。【其他可能因粗心导致的原因】:当然,如果你的服务器是Linux系统的,那么看一下_pycharm无法联网

【数据结构与算法】KMP算法_kmp贪心算法-程序员宅基地

文章浏览阅读154次。KMP算法思想朴素的匹配算法是一对一的匹配,每次匹配失败主串和模式串上的指针都回退到开头,但实际上这种回退是没有必要的而KMP算法通过对模式串的处理进而减少回退次数时间复杂度T=O(n+m)实现typedef int Position;#define NotFound -1void BuildMatch(Position match[],char *pattern,int len){ match[0]=-1; for(Position j=1;j<len;_kmp贪心算法

Thingsboard 源码分析 -- Gateway mqtt网关原理实现_mqtt gateway offline-程序员宅基地

文章浏览阅读3.5k次,点赞3次,收藏12次。Thingsboard的网关通过tb核心服务提供的mqtt以客户端的形式连接到tb上,一个网关的连接同时只能在线一个网关服务。正常情况下,网关下的所有设备上报数据都是通过网关上报,网关下所有设备都是先把数据上报到边缘端的mqtt上,然后网关通过订阅边缘端的mqtt获取设备数据,再统一通过tb的mqtt上传到tb上,上传操作还考虑到了服务掉线的情况,会把数据暂时存起来,待服务连接正常继续上报。Thingsboard官方网关用的python写的,最早官方网关用的java,但是不知道什么原因下架换成pytho_mqtt gateway offline

mysql 过滤emoji表情_过滤微信表情/emoji表情-程序员宅基地

文章浏览阅读519次。微信表情/emoji表情是个麻烦的东西,即使你能存储,也不一定能完美显示。在iOS以外的平台上,例如PC或者android。如果你需要显示emoji,就得准备一大堆emoji图片并使用第三方前端类库才行。即便如此,还是可能因为emoji图片不够全而出现无法显示的情况,在大多数业务场景下,emoji也不是非要不可的。我们可以适当地考虑干掉它,节约各种成本。```phppublic function ..._mysql去除emj表情