第五章unity中的基础光照(代码片段)

xiegaosen xiegaosen     2022-12-08     477

关键词:

1. Unity中的环境光和自发光

在标准光照模型中,环境光和自发光的计算是最简单的。
在Unity中,场景中的环境光可以在Window->Lighting->Ambient Source/Ambient Intensity中控制,如下图所示。在Shader中,我们只需要通过Unity的内置变量UNITY_LIGHTMODEL_AMBIENT就可以得到环境光的颜色和强度信息。
而大多数物体是没有发光特性的,因此在本文中的大部分Shader中都没有计算自发光部分。如果要就算自发光也很简单,我们只需要在片元着色器输出最后的颜色之前,把材质的自发光颜色添加到输出颜色即可。
技术图片

2. 在UnityShader中实现漫反射光照模型

在了解了上述理论后,我们现在来看一下如何在Unity中实现这些基本的光照模型。首先,我们来实现标准光照模型中的漫反射光照部分。
在以前我们给出了基本光照模型中漫反射部分的计算公式:
技术图片
从公式可以看出,要计算漫反射需要知道4个参数:入射光线的颜色和强度Clight,材质的漫反射系数mdiffuse,表面法线n以及光源方向I。
为防止点积的结果出现负值,我们需要使用max操作,而Cg提供了这样的函数。在本例中使用Cg的另一个函数可以达到同样的目的,即saturate函数。
函数:saturate(x)
参数:x:为用于操作的标量或矢量,可以是float、float2、float3等类型。
描述:把x截取在[0,1]的范围内,如果x是一个矢量,那么会对它的每一个分量进行这样的操作。

2.1 实践:逐顶点光照

我们首先来看如何实现一个逐顶点的漫反射光照效果。在学习完本节后,我们会得到类似于下图的效果。
技术图片
(1)首先,为了得到并且控制材质的漫反射颜色,我们首先在Shader语的Properties语义块中声明了一个Color类型的属性,并把它的初始值设置为白色:

Properties
_Diffuse("Diffuse",Color)=(1,1,1,1)

(2)然后,我们在SubShader语义块中定义了一个Pass语义块。这是因为顶点/片元着色器的代码需要写在Pass语义块,而非SubShader语义块中。而且我们在Pass的第一行指明了该Pass的光照模式:

SubShader
        Pass
                    Tags"LightMode"="ForwardBase"

LightMode标签是Pass标签中的一种,它用于定义该Pass在Unity的光照流水线中的角色,在后面我们会更加详细的解释它。在这里我们只需要知道,只有定义了正确的LightMode,我们才能得到一些Unity内置光照变量,例如下面讲到的_LightColor0。
(3)然后,我们使用CGPROGRAM和ENDCG来包围Cg代码片段,以定义最重要的顶点着色器和片元着色器代码。首先,我们使用#pragma指令来告诉Unity,我们定义的顶点着色器和片元着色器叫什么名字。在本例中,它们的名字分别是vert和frag:

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

(4)为了使用Unity内置的一些变量,如后面讲到的_LightColor0,还需要包含进Unity的内置文件Lighting.cginc:

#include "Lighting.cginc"

(5)为了在Shader中使用Properties语义块中声明的属性,我们需要定义一个和该属性类型相匹配的变量:

fixed _Diffuse;

通过这样的方式,我们就可以得到漫反射公式中需要的参数之一——材质的漫反射属性。由于颜色的属性范围在0到1之间,我们可以使用fixed精度的变量来存储它。
(6)然后我们定义了顶点着色器的输入和输出结构体(输出结构体同时也是片元着色器的输入结构体):

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

为了访问顶点的法线,我们需要在a2v中定义一个normal变量,并通过NORMAL语义来告诉Unity要把模型顶点的法线信息存储到normal变量中。为了把在顶点着色器计算得到的光照颜色传递给片元着色器,我们需要在v2f中定义一个color变量,且并不是必须使用COLOR语义,一些资料中会使用TEXCOORD0语义。
(7)接下来是关键的顶点着色器。由于本小节关注如何实现一个逐顶点的漫反射光照,因此漫反射部分的计算都将在顶点着色器中进行:

v2f vert(a2v v)
v2f o;
//Transform the vertex from object space to projection space
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//Transform the normal fram object space to worldspace
fixed3 worldNormal = normalize(mul(v.normal,(float3×3)_World2Object);
//Get the light direction in world space
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
//compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));
o.color = ambient + diffuse;
return o;

在第一行我们首先定义了返回值o。我们已经重复过很多次,顶点着色器最基本的任务就是把顶点位置从模型空间转换到裁剪空间。因此我们需要使用Unity内置的模型×世界×投影矩阵UNITY_MATRIX_MVP来完成这的坐标变换。接下来我们通过Unity的内置变量UNITY_LIGHTMODEL_AMBIENT得到了环境光部分。
然后,就是真正计算漫反射光照的部分。回忆一下,为了计算漫反射光照我们需要知道四个参数。在前面的步骤中,我们已经知道了材质的漫反射颜色_Diffuse以及顶点法线v.normal。我们还需要知道光源的颜色和强度信息以及光源方向。Unity为我们提供了一个内置变量_LightColor0来访问该Pass处理的光源的颜色和强度信息(注意,想要得到正确的值需要定义合适的LightModel标签),而光源方向可以由_WorldSpaceLightPos0来得到。需要注意的是,这里对光源方向的计算并不具有通用性。在本节中,我们假设场景只有一个光源且该光源的类型是平行光。但如果场景中有多个光源并且类型可能是点光源等其它类型,直接使用_WorldSpaceLightPos0就不能得到正确的结果,我们将在后面学习如何使用内置函数来处理更复杂的光源类型。
在计算法线和光源方向之间的点积时,我们需要选择它们所在的坐标系,只有两者处于同一坐标空间下,它们的点积才有意义。在这里,我们选择了世界坐标空间。而由a2v得到的顶点法线是位于模型空间下的,因此我们首先需要把法线转换到世界空间中。在以前,我们已经知道可以使用顶点变换矩阵的逆转置矩阵对法线进行相同的变换,因此我们首先得到模型空间到世界空间的变换矩阵的逆矩阵_World2Object,然后通过调换它在mul函数中的位置,得到和转置矩阵相同的矩阵乘法。由于法线是一个三维矢量,因此我们只需要截取_World2Object的前三行前三列即可。
在得到了世界空间中的法线和光源方向后,我们需要对它们进行归一化操作。在得到它们的点积结果后,我们要防止这个结果为负值。为此,我们使用了saturate函数。saturate函数是Cg提供的一种函数,它的作用是可以把函数截取到[0,1]的范围。最后再与光源的颜色和强度以及材质的漫反射颜色相乘即可得到最终的漫反射光照部分。
最后,我们对环境光和漫反射光部分相加,得到最终的光照结果。
(8)由于所有的计算在顶点着色器中都已经完成了,因此片元着色器的代码很简单,我们只需要把顶点颜色输出即可:

fixed frag(v2f i):SV_Target
return fixed(i.color,1.0);

(9)最后,我们需要把这个Unity Shader的回调Shader设置为内置的Diffuse:
Fallback "Diffuse"
至此,我们已经详细解释了逐顶点的漫反射光照的实现。对于细分程度较高的模型,逐顶点光照已经可以得到比较好的光照效果了。但对于一些细分程度较低的模型,逐顶点光照就会出现一些细节问题,就如上面的图片我们看到胶囊体的背光面与向光面交界处有一些锯齿。为了解决这些问题,我们可以使用逐像素的漫反射光照。

2.2 实践:逐像素光照

我们只需要对Shader进行一些更改就可以实现逐像素的漫反射效果,如下图所示:
技术图片
对以前的代码修改如下:
(1)修改顶点着色器的输出结构体v2f:

struct v2f
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;

(2)顶点着色器不需要计算光照模型,只需要把世界空间下的法线传递给片元着色器即可。

v2f vert(a2v v)
v2f o;
//Transform the vertex from object space  to projection space
o.pos = mul(UNITY_MATRIX_MVP,v.vertex)
//transform the normal fram object space to world space
o.worldNormal=mul(v.normal,(float3×3)_World2Object);
return o;

(3)片元着色器需要计算漫反射光照模型:

fixed4 frag(v2f i):SV_Target
//Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//Get the normal in world space
fixed3 worldNormal = normalize(i.worldNormal);
//Get the light direction in world space
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//Compute diffuse term
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
fixed3 color = ambient + diffuse;
return fixed4(color,1.0);

逐像素光照可以得到更加平滑的光照效果。但是即便使用了逐像素,漫反射光照,有一个问题仍然存在。在光照无法到达的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型背光区域看起来就像一个平面一样,失去了模型细节表现。实际上我们可以通过添加环境光来得到非全黑的效果,但即便这样让然无法解决背光面明暗一样的缺点。为此,有一种改善技术被提了出来,这就是半兰伯特(Half Lambert)光照模型

2.3 半兰伯特模型

在2.1小结中,我们使用的漫反射光照模型也被称为兰伯特光照模型,因为它符合兰伯特定律——在平面某点漫反射光的光强与该反射点的法向量和入射光角度的余弦值成正比。为了改变上小结中提出的问题,Valve公司在开发游戏《半条命》时提出了一种技术,由于该技术是在原兰伯特光照模型的基础上进行了一个简单的修改,因此被称为半兰伯特光照模型。
广义的半兰伯特光照模型的公式如下:
技术图片
可以看出,与原兰伯特模型相比,半兰伯特光照模型没有使用max操作来防止n和l的点积为负值,而是对其结果进行了一个α倍的缩放再加上一个β大小的偏移。绝大多数情况下,α和β的值均为0.5,即公式为
技术图片
通过这样的方式,我们可以把n·l的结果范围从[-1,1]映射到[0,1]的范围内。也就是说,对于模型的背光面,在原版兰伯特光照模型中点积结果将映射到同一个值,即0值处;而在半兰伯特模型中,背光面也可以有明暗变化,不同的点积结果会映射到不同的值上。
需要注意的是,半兰伯特是没有任何物理依据的,它仅仅是一个视觉加强技术。
我们只需把代码进行一点修改就可以得到半兰伯特模型:

fixed frag(v2f  i):SV_Target
......
//Compute diffuse term
fixed halfLambert = dot(worldNormal,worldLightDir)*0.5+0.5;
fixed3 diffuse = _LightColor0.rgb*_Diffuse.rgb*halfLambert;
fixed3 color = ambient+diffuse;
return fixed(color,1.0);

在上面代码中,我们使用了半兰伯特模型替代了原有的兰伯特模型。下图给出了逐顶点漫反射光照、逐像素漫反射光照和半兰伯特光照的对比效果。
技术图片

第五章小结(代码片段)

第五章学习了二叉树:每个结点至多只有两颗子树,且子树有左右之分。二叉树的遍历:几乎所有操作建立在遍历的基础上,利用递归完成二叉树前(根)序,中(根)序,后(根)序遍历。 voidPreOrderTraverse(BiTreeT)If(T)//若二... 查看详情

《java并发编程的艺术》读后笔记-第五章java中的锁(代码片段)

文章目录《Java并发编程的艺术》读后笔记-第五章Java中的锁第五章Java中的锁1.Lock接口1-1定义1-2Lock的使用1-3Lock与synchronized区别1-4Lock的API2.队列同步器2-1定义2-2队列同步器的接口和示例2-3队列同步器的实现分析1)同步队列2ÿ... 查看详情

solidity学习记录——第五章(代码片段)

...功能第三章编写DAPP所需的基础理论第四章完善僵尸功能第五章ERC721标准和加密资产文章目录Solidity学习记录前言一、本章主要目的二、学习过程1.本节课程知识点2.最终代码总结前言这应该是Solidity学习记录的最后一章,这五... 查看详情

unity中的基础光照(代码片段)

第6章Unity中的基础光照6.1我们是如何看到这个世界的通常来说我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象:首先,光线从光源(lightsource)中被发射出来然后,光线和场景中的一些物... 查看详情

第五章学习小结(代码片段)

---恢复内容开始---第五章主要学习的是树与二叉树,有利用数组进行存储的顺序二叉树,也有利用链表进行存储的链式二叉树,在这个基础上又展开了二叉树的遍历。二叉树的遍历分为前序遍历,中序遍历以及后序遍历,主要区... 查看详情

unity关闭shader中的光照模型以及如何自定义光照模型(代码片段)

//UpgradeNOTE:replaced‘_World2Object‘with‘unity_WorldToObject‘//UpgradeNOTE:replaced‘_World2Object‘with‘unity_WorldToObject‘Shader"Custom/RadarWave"Properties_Color("Color",Color)=(1,1,1,1)_MainTex( 查看详情

第五章eureka服务注册与发现(尚硅谷springcloud)(代码片段)

文章目录一、Eureka基础知识二、单机Eureka构建步骤三、集群Eurake构建步骤四、actuator微服务信息完善五、服务发现Discovery六、eureka自我保护一、Eureka基础知识什么是服务治理1.springcloud封装了Netflix公司开发的Eureka模块来实现服务... 查看详情

第五章学习小结(代码片段)

这一章学习了树的定义、二叉树的性质和存储结构、树和森林的转化、遍历等,以及哈夫曼树的概念和构造算法首先是树的一些定义和性质,刚开始接触时还是比较懵的,对一些术语不太熟悉,对性质也不太理解,后面在几次计... 查看详情

第五章orm(代码片段)

5.1概念对象关系映射(ObjectRelationalMapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数... 查看详情

r语言基础题及答案——r语言与统计分析第五章课后习题(汤银才)(代码片段)

R语言与统计分析第五章课后习题(汤银才)题-1设总体XXX是用无线电测距仪测量距离的误差,它服从(α,β)(α,β)(α,β)上的均匀分布,在200次测量中,误差为XiX_iXi​的次数有nin_ini​次:XiX_iXi​3579111315171921nin_ini​21161526221421221825求(α,β)(α,... 查看详情

r语言基础题及答案——r语言与统计分析第五章课后习题(汤银才)(代码片段)

R语言与统计分析第五章课后习题(汤银才)题-1设总体XXX是用无线电测距仪测量距离的误差,它服从(α,β)(α,β)(α,β)上的均匀分布,在200次测量中,误差为XiX_iXi​的次数有nin_ini​次:XiX_iXi​3579111315171921nin_ini​21161526221421221825求(α,β)(α,... 查看详情

云计算第五章(代码片段)

Cloud-EnablingTechnology云使能技术BroadbandNetworksandInternetArchitecture宽带和Internet架构-Allcloudsmustbeconnectedtoanetwork(InternetorLAN)Thepotentialofcloudplatformsthereforegenerallygrowsinparallelwithadv 查看详情

jquery系列第五章jquery框架动画特效(代码片段)

第五章jQuery框架动画特效5.1jQuery动画特效说明jQuery框架中为我们封装了众多的动画和特效方法,只需要调用对应的动画方法传递合适的参数,就能够方便的实现一些炫酷的效果,而且jQuery框架还支持自定义各种动画效果。jQuery中... 查看详情

第五章:变量(代码片段)

变量:int表示整数double表示带小数点的char表示单个字符string表示在存储字符串的变量bool表示判断真假usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;namespaceAnt1///<summary>// 查看详情

汇编语言——第五章课后总结(代码片段)

1.[BX]movax,[bx]功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。即:(ax)=((ds)*16+(bx))。mov[bx],ax功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将ax中的数据送入内存SA:EA... 查看详情

第五章:数组/array/tuple/yield(代码片段)

数组如果需要使用同一类型的多个对象,就可以考虑使用集合和数组.如果需要使用不同类型的多个对象,可以考虑使用Tuple(元组)数组的声明在声明数组时,应先定义数组元素中的类型,其后是一对空方括号和变量名int[]myAyyay; 数... 查看详情

第五章,面向对象基础篇

5.3封装性   ? 为了解决属性必须封装且又必须访问的矛盾,只要是被封装的属性,必须通过setter和getter方法设置和取得。   ? 为前面类的私有属性加上getter和setter方法PublicvoidsetAge(inta){  Age=a;}PublicintgetAge(... 查看详情

第五章(代码片段)

 认识模块什么是模块模块的导入和使用常用模块一collections模块时间模块random模块os模块sys模块序列化模块re模块  常见的场景:一个模块就是一个包含了python定义和声明的文件,文件名就是模块名字加上.py的后缀。&nb... 查看详情