第五章 Unity中的基础光照(2)
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);
}
在上面代码中,我们使用了半兰伯特模型替代了原有的兰伯特模型。下图给出了逐顶点漫反射光照、逐像素漫反射光照和半兰伯特光照的对比效果。
第五章 Unity中的基础光照(2)的更多相关文章
- 第五章 Unity中的基础光照(3)
目录 1. 在Unity Shader中实现高光反射光照模型 1.1 实践:逐顶点光照 1.2 逐像素光照 1.3 Blinn-Phong光照模型 2. 召唤神龙:使用Unity内置的函数 @ 1. ...
- 第五章 Unity中的基础光照(1)
[TOC] 渲染总是围绕着一个基础问题:我们如何决定一个像素的颜色?从宏观上来说,渲染包括了两大部分:决定一个像素的可见性,决定这个像素上的光照计算.而光照模型用于决定在一个像素上进行怎样的光照计算. ...
- Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照
转自冯乐乐的<Unity Shader入门精要> 通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象. 首先,光线从光源中被发射出来. 然后,光线和场景中的一些物体相交 ...
- Unity中的基础光照
渲染包含了两大部分:决定一个像素的可见性,决定这个像素上的光照计算. 光照模型就是用于决定在一个像素上进行怎样的光照计算. 一.光源 在实时渲染中我们通常把光源当做一个没有体积的点. 1.1 辐照度 ...
- 【Unity】第6章 Unity脚本开发基础
分类:Unity.C#.VS2015 创建日期:2016-04-16 一.简介 游戏吸引人的地方在于它的可交互性.如果游戏没有交互,场景做得再美观和精致,也难以称其为游戏. 在Unity中,游戏交互通 ...
- Unity Shader入门精要学习笔记 - 第16章 Unity中的渲染优化技术
转自冯乐乐的 <Unity Shader 入门精要> 移动平台的特点 为了尽可能一处那些隐藏的表面,减少overdraw(即一个像素被绘制多次),PowerVR芯片(通常用于ios设备和某 ...
- 第五章 Java中锁
Lock接口 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁).在Lock接口出现之前,Java程序 ...
- 【Unity Shader】---基础光照
一.[标准光照模型]1.自发光emissve:描述一个表面本身会发散多少光.在没有使用全局光照时,这些自发光是不会真正照亮周围物体. 自发光就是直接由发光体发射进入摄像机,不经过任何反射,在标准光照模 ...
- java并发学习第五章--线程中的锁
一.公平锁与非公平锁 线程所谓的公平,就是指的是线程是否按照锁的申请顺序来获取锁,如果是遵守顺序来获取,这就是个公平锁,反之为非公平锁. 非公平锁的优点在于吞吐量大,但是由于其不是遵循申请锁的顺序来获 ...
随机推荐
- CSPS_104
又被爆踩. 正解写挂. 暴力不会. 只会改题. 还要加油. T1 $nlog^2$是显然的 那么考虑只二分一次,就$nlog$了! 有什么能通过一次二分$O(1)$得到呢? 二分a的位置,$O(1)$ ...
- 使用Typescript重构axios(二十四)——防御XSRF攻击
0. 系列文章 1.使用Typescript重构axios(一)--写在最前面 2.使用Typescript重构axios(二)--项目起手,跑通流程 3.使用Typescript重构axios(三) ...
- Python3安装目录介绍
目录组织方式 关于如何组织一个较好的Python工程目录结构,已经有一些得到了共识的目录结构. 假设你的项目名为foo, 我比较建议的最方便快捷目录结构这样就足够了: Foo/ |-- bin/ | ...
- 在ubuntu中安装minicom时出现device /dev/tty8 is locked解决办法
未正常关闭minicom yesaidu@ywf-ubuntu: ~$ ls /var/lock LCK..ttyS0 subsys yesaidu@ywf-ubuntu: ~$ kill 0 ye ...
- Eclipse添加自定义注释
首先介绍几个常用的注解: @author 作者名 @date 日期 @version 版本标识 @parameter 参数及其意义 @return 返回值 @throws 异常类及抛出条件 @depr ...
- Spring注解之@RestControllerAdvice
前言 前段时间部门搭建新系统,需要出异常后统一接口的返回格式,于是用到了Spring的注解@RestControllerAdvice.现在把此注解的用法总结一下. 用法 首先定义返回对象Respons ...
- 南开大学校徽及手写字的Tikz源码
话不多说,直接上内容. % ---------------------------------- % !TeX enginee = pdfLaTeX/XeLaTeX % !TeX encoding = ...
- 如何对 React 函数式组件进行优化
文章首发个人博客 前言 目的 本文只介绍函数式组件特有的性能优化方式,类组件和函数式组件都有的不介绍,比如 key 的使用.另外本文不详细的介绍 API 的使用,后面也许会写,其实想用好 hooks ...
- Zabbix-(五)监控Docker容器与自定义jvm监控项
Zabbix-(五)监控Docker容器与自定义jvm监控项 一.前言 前文中讲述了Zabbix对服务器硬件方面的监控功能,本文将讲述利用Zabbix监控Docker容器中的Java Web服务,并通 ...
- 力扣(LeetCode)验证回文串 个人题解(C++)
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写. 说明:本题中,我们将空字符串定义为有效的回文串. 示例 1: 输入: "A man, a plan, a c ...