技术标签: unity 游戏引擎 Shader尝试入门笔记
此文及专栏系是以Shader入门精要为基础整理的Unity Shader学习笔记,尽量以初学者视角还原(其实半年前我就是初学者),错误还需指正。
本篇是实操部分的第三个Shader,即高光反射Shader,文章选取顶点着色器生成的高光反射Shader作为说明,具体名词可能不再解释。
本篇实现的是高光反射这一渲染中的重要主题,这种效果通常出现在金属、镜面反射等场合。按照图形学中基础的光照模型,高光反射的光强及色彩由以下公式决定:
从左到右的参数分别是:最终结果,即高光反射的光强和色彩;入射光线的颜色和强度;高光反射系数;max函数的运算结果,是对观察视角向量v和反射方向向量r的取正。
是材质的光泽度,这可能是大多数人不曾了解过的名词,事实上我尝试如下理解,请看下面这个光球,如果无视右下角的光泽,那么大亮点就是我们的高光部分。想象一下,亮点的中心那里反射光线正好与观察视角向量重合,也即我们的“眼睛”正好接收到那里的反射光线,v和r单位向量乘积为1;而偏移了那里,反射光线就和我们的视角有夹角了,v和r这两个单位向量的乘积逐渐减小,因为光泽度作为指数存在,这个结果逐渐接近0,也就是光线减弱趋向于消失。
当然,反射方向我们还需要计算,其公式为:
这一效果可以由CG语言的相关函数得到。其中n和I向量分别为法线方向和光源入射方向。
接下来正式撰写我们的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"
}
区别于简单的漫反射,我们的高光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这个式子计算出漫反射结果。
接下来就是高光部分了,根据公式
我们用reflect(-worldLightDir, worldNormal) 计算出反射向量(我们不关心封装好的函数,但可以知道原理,不是吗),这里的worldLightDir加了个负号取反了,因为reflect函数的入射光线要求是光照点指向光源,而unity提供的_WorldSpaceLightPos0是反向的,需要反着来。
我们来看公式,此时,我们已经拿到了(_LightColor0,内置参数)、(_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默认的那种,感觉有点油腻的反射。事实上不必嫌弃它,我们已经实现了基本的高光反射,预设的一些目标也已经完成,比如我们修正的半兰伯特模型也保证了背后不至于全黑。
文章浏览阅读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次。胸有成竹 → 竹柏异心 → 心安理得 → 得薄能鲜 → 鲜为人知 → 知不诈愚 → 愚不可及 → 及宾有鱼 → 鱼帛狐篝 → 篝灯呵冻 → 冻解冰释 → 释车下走 → 走伏无地 → 地北天南 → 南北东西 → 西除东荡 → 荡产倾家 → 家败人亡 → 亡不待夕 → 夕寐宵兴 → 兴不由己 → 己饥己溺 → 溺爱不明 → 明白了当 → 当场出彩 → 彩笔生花 → 花闭月羞 → 羞惭满面 → 面壁九年_车马填门,马中关五
文章浏览阅读227次。特点1、立即执行2、用完释放(function(){})()(function(){}())_js中iief
文章浏览阅读6.8k次。20170214IntelliJ IDEA 2017.1 EAP与异步堆栈跟踪调试器扩展作者钉钉用户名:@Li Zhang封面配图:文章摘要:反应性编程趋势后,我们的代码越来越异步。文章正文:早些时候java8介绍了CompletableFuture(采用Guava’s ListenableFuture),通过Akka, Ratpack, Reactor, RxJava, Vert.x以及其它库实现_ideal 调试 completablefuture
文章浏览阅读4.2k次,点赞151次,收藏153次。带你手撕进制转换_进制转换含小数部分
文章浏览阅读429次。【代码】基于ubuntu22.04进行对linuxqtdeploy进行源码编译,解决编译中遇到的多个错误并最终成功生成 linuxdeployqt可执行程序。_linuxdeployqt ubuntu 22
文章浏览阅读553次。tensorboard是个可视化工具,用于观察神经网络训练过程SummaryWriter:首先需要创建一个SummaryWriter示例其中有些参数不常用,我们可以只在参数里写上一个训练保存的路径即可例:writer=SummaryWriter('logs')我们在SummaryWriter中常用的方法有:1. add_scalar2. add_imageadd_scalaradd_scalar(tag, scalar_value, global_step=None, walltime=_tensorboard pytorch
文章浏览阅读2.8w次,点赞6次,收藏15次。一、前言:首先确保PyCharm是专业版而非社区版的,社区版的菜单栏中没有此功能。二、报错:输入账号、密码测试连接的时候(2020版本)(2018版本)三、错误分析:【我的错误原因】:先说结论吧,我个人的原因是因为选的是SFTP协议,因此改换Linux系统的服务器就可以了,因为本人选的是SFTP协议,这个协议需要SSH协议的支持,而SSH协议是Linux才有的,而Windows没有,准确来说是不自带。【其他可能因粗心导致的原因】:当然,如果你的服务器是Linux系统的,那么看一下_pycharm无法联网
文章浏览阅读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贪心算法
文章浏览阅读3.5k次,点赞3次,收藏12次。Thingsboard的网关通过tb核心服务提供的mqtt以客户端的形式连接到tb上,一个网关的连接同时只能在线一个网关服务。正常情况下,网关下的所有设备上报数据都是通过网关上报,网关下所有设备都是先把数据上报到边缘端的mqtt上,然后网关通过订阅边缘端的mqtt获取设备数据,再统一通过tb的mqtt上传到tb上,上传操作还考虑到了服务掉线的情况,会把数据暂时存起来,待服务连接正常继续上报。Thingsboard官方网关用的python写的,最早官方网关用的java,但是不知道什么原因下架换成pytho_mqtt gateway offline
文章浏览阅读519次。微信表情/emoji表情是个麻烦的东西,即使你能存储,也不一定能完美显示。在iOS以外的平台上,例如PC或者android。如果你需要显示emoji,就得准备一大堆emoji图片并使用第三方前端类库才行。即便如此,还是可能因为emoji图片不够全而出现无法显示的情况,在大多数业务场景下,emoji也不是非要不可的。我们可以适当地考虑干掉它,节约各种成本。```phppublic function ..._mysql去除emj表情