中级 9-11
一轮整理:2021年6月18日17:46:02
二轮整理:2021年11月2日11:37:08
作者:聪头
Ch9.更复杂的光照
LightMode标签 !!!
9.1 前向渲染路径
原理
3种处理光照的方式
- 逐顶点处理
- 逐像素处理
- 球谐函数(Spherical Harmonics,SH)
判断规则
两个Pass
- 说明详见P183
内置光照变量和函数!!!
- 仅前向渲染可访问
延迟渲染路径(了解)
介绍
两个Pass
补充:只有通过深度测试的片元,即可见的片元,才写入G-buffer中
![img](中级 9-11.assets/_CopyPix_1619368989_8.png)
内变量
- 位于UnityDeferredLibrary.cginc
![img](中级 9-11.assets/_CopyPix_1619369106_9.png)
9.2 光源类型
Unity支持4种光源类型:平行光、点光源、聚光灯和面光源(烘焙时有效,不列入本书讨论范围)
光源5个重要属性:位置、方向、颜色、强度及衰减
BasePass中:通常计算一次逐像素平行光、一次自发光、一次环境光及其他多个逐顶点和SH光源
- 不影响ProjectSetting中逐像素光源数量,即排除此逐像素光源
AdditionalPass中:一个逐像素光源(除BasePass的逐像素平行光)计算一次
9.3 光照衰减
法1.使用光照衰减纹理
- Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减(注意,如果对光源使用了cookie,那么衰减查找纹理是
_LightTextureB0
,此情况这里不作讨论)。我们通常只关心_LightTexture0
对角线上的纹理颜色值。这些值表明了在光源空间中不同位置的点的衰减值- (0,0)表示与光源位置重合点的衰减值,(1,1)表示在光源空间中距离最远的点的衰减值
核心代码
1 | float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;//世界->光源 |
法2.数学公式计算
- 公式太easy,不推荐
1 | float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz); |
9.4 阴影
阴影原理详见P197
- LightMode标记为ShadowCaster的Pass会被阴影映射纹理识别,就可以投射阴影了
- 接收其他物体的阴影,必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果
ShadowCaster的Pass实现原理详见P199
阴影三剑客
包含新的内置文件:AutoLight.cginc,因为下面计算阴影时所用的宏都是在这个文件中声明的
- SHADOW_COORDS
- v2f内
- 声明一个对阴影纹理采样的坐标(名为_ShadowCoord),这个宏的参数是下一个可用的插值寄存器的索引值
- TRANSFER_SHADOW
- 顶点着色器内
- 这个宏用于在顶点着色中计算上一步中声明的阴影纹理坐标
- 若屏幕空间的阴影映射技术可用(通过UNITY_NO_SCREENSPACE_SHADOWS来得到),则调用ComputeScreenPos函数来计算。否则使用传统阴影映射技术,把顶点坐标从模型空间变换到光源空间后存储到_ShadowCoord中
- SHADOW_ATTENUATION
- 片元着色器内
- 使用_ShadowCoord对相关的纹理进行采样,得到阴影信息
前提:a2v的顶点坐标输入必须命名vertex,v2f顶点位置变量必须为pos
统一光照和衰减
UNITY_LIGHT_ATTENUATION
- 片元着色器内,将原来的SHADOW_ATTENUATION替换成该宏即可
- 计算阴影和衰减
- Unity针对不同光源类型,是否启用cookie等不同情况声明了多个版本的UNITY_LIGHT_ATTENUATION
1
2
3
4//参数①:UNITY_LIGHT_ATTENUATION会帮我们声明atten。用于存储光照衰减和阴影值相乘后的结果
//参数②:结构体v2f。这个参数会传递给SHADOW_ATTENUATION来计算阴影值
//参数③:世界空间下的坐标。用于计算光源空间下的坐标,再对光照衰减纹理采样得到光照衰减值 同P196
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
实现思路
阴影补充
Additional Pass中添加阴影效果
- 使用
#pragma multi_compile_fwdadd_fullshadows
来代替#pragma multi_compile_fwdadd
指令
透明度物体的阴影
透明度测试:使用
Fallback Transparent/Cutout/VertexLit
- 必须使用名为**_Cutoff**的属性来进行透明度测试才能得到正确的结果
透明度混合:目前无法很好实现
- 不实现阴影
- 可以把Fallback设为
VertexLit
等强制产生阴影
透明度混合不易实现阴影的原因:需要关闭深度写入,由此带来的问题也影响了阴影的生成。想要为这些半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会使阴影处理变得非常复杂,影响性能
实战9-1:标准光照着色器
- 全部转到世界空间下计算
- 基于Blinn-Phong
1 | Shader "Unity Shaders Book/Chapter 9/CT_Bumped Specular" |
Ch10.高级纹理
10.1 立方体纹理
- 立方体纹理(Cubemap)是环境映射的一种实现方式。环境映射可以模拟物体周围的环境,而使用了环境映射的物体看起来像镀了层金属一样反射出周围的环境
- 和之前使用二维坐标采样不同,对立方体纹理采样我们需要提供一个三维的纹理坐标,这个三维纹理坐标表示了我们在世界空间下的一个3D方向
- 应用
- 天空盒子:在Unity中,天空盒子是在所有不透明物体之后渲染的
- 环境映射:可以模拟出金属质感的材质,常用于反射和折射。创建环境映射纹理的方法有以下三种:
- 直接由一些特殊布局的纹理创建,例如,类似立方体展开图的交叉布局、全景布局等。然后把该纹理的Texture Type设置为Cubemap即可。在基于物理的渲染中,通常会使用HDR图像生成高质量Cubemap。同时可以对纹理数据进行压缩,而且支持边缘修正、光滑反射等,推荐
- Unity创建一个Cubemap。把6张纹理拖拽到它的面板中
- 利用脚本创建,立方体纹理更具个性化。这是通过Camera.RenderToCubemap函数来实现的,该函数可以把任意位置观察到的场景图像存储到6张图像中,从而创建出该位置上对应的立方体纹理
本节使用方法3:
- 确保项目中存在RenderCubemapWizard.cs脚本
- 创建一个立方体(带有Transform信息的物体都可)
- 创建一个Cubemap(Create->Legacy->Cubemap),并勾选Readable选项
- 菜单栏中选择GameObject->Render into Cubemap,依次赋值,点击Render!即可
反射
- 我们只需要通过入射光线和表面法线来计算反射方向,再利用反射方向对立方体纹理采样即可
反射关键代码
- 顶点着色器
1 | o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); |
- 片元着色器
1 | fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb; |
反射向量没有归一化,是因为texCUBE内部会默认归一化
折射
斯涅耳定律
- 当光从介质1沿着和表面法线夹角为θ
1的方向斜射入介质2时,我们可以使用如下公式来计算折射光线与法线的夹角θ2
$$
\eta_1sin\theta_1 = \eta_2sin\theta_2
$$
其中$\eta_1$和$\eta_2$分别是两个介质的折射率(index of refraction)
- 真空折射率:1
- 玻璃:1.5
折射关键代码
- 顶点着色器:计算折射方向
1 | //参数①:入射光线方向,必须归一化 |
- 片元着色器:利用折射方向对立方体纹理采样
1 | fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb; |
折射向量没有归一化,因为对立方体纹理的采样只需提供方向即可
菲涅尔反射
基于视角方向控制反射程度
菲涅尔反射描述了一种光学现象,即当光线照射到物体表面上时,一部分发生反射,一部分进入物体内部,发生折射或散射。被反射的光和入射光之间存在一定的比率关系,这个比率关系可以通过菲涅尔等式计算
菲涅尔反射关键代码
- 片元着色器
- 使用Schlick菲涅尔近似等式
- 一些实现也会直接把fresnel和反射光照相乘后叠加到漫反射光照上,模拟边缘光照效果
1 | //使用Schlick菲涅尔近似等式计算 |
10.2 渲染纹理
- 渲染目标纹理(Render Target Texture, RTT):现代GPU允许我们把整个三维场景渲染到一个中间缓冲中
- 多重渲染目标(Multiple Render Target,MRT):这种技术指的是GPU允许我们把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标单独渲染完整的场景
- 渲染纹理(Render Texture):Unity为渲染目标纹理定义了一种专门的纹理类型
- 创建法1:在Project目录下创建,然后把某个摄像机的渲染目标设置成该渲染纹理
- 创建法2:在屏幕后处理时使用GrabPass命令或OnRenderImage函数来获取当前屏幕图像。Unity会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中
10.3 程序纹理
两种程序纹理的生成方式:
- 通过脚本生成程序纹理
- 使用程序材质(以.sbsar为后缀)。它使用的程序纹理是通过Substance Designer在Unity外部生成的
- 好处:多变性,我们可以通过调整程序纹理的属性来控制纹理的外观
实战10-1:玻璃效果
- 在使用GrabPass的时候,我们往往需要把物体的渲染队列设置成透明队列(即”Queue”=”Transparent”)。这样可以确保渲染该物体时,所有不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像
- 原理:首先使用一张法线纹理来修改模型的法线信息,然后使用之前介绍的反射方法,通过一个Cubemap来模拟玻璃的反射,而在模拟折射时,则使用了GrabPass获取玻璃后面的屏幕图像,并使用切线空间下的法线对屏幕纹理坐标偏移后,再对屏幕图像进行采样来模拟近似的折射效果
- 本质,就是通过法线贴图偏扰uv,采样GrabPass抓取的屏幕纹理
折射:使用切线空间的法线,偏扰屏幕uv坐标,进而采样屏幕纹理即可
反射:使用反射向量采样立方体贴图
1 | Shader "Unity Shaders Book/Chapter 10/CT_GlassRefreaction" |
Ch11.让画面动起来
11.1 内置时间变量!!!
实战11-1:序列帧动画
- 序列帧图像通常是透明纹理,关闭深度写入
1 | Shader "Unity Shaders Book/Chapter 11/CT_ImageSequenceAnimation" |
实战11-2:滚动的背景
关键代码
- 顶点着色器
1 | o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); |
完整代码
1 | Shader "Unity Shaders Book/Chapter 11/CT_ImageSequenceAnimation" |
![GIF 2021-11-2 11-28-03](中级 9-11.assets/GIF%202021-11-2%2011-28-03.gif)
实战11-3:河流
关键代码
DisableBatching:设为True,可以取消对该Shader的批处理工作。因为该Shader包含了模型空间的顶点动画,批处理会合并所有相关模型,而这些模型各自的模型空间就会丢失,所以们要关闭批处理
顶点着色器
1 | offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude; //修改的波动方程 |
模型类似一个x轴向下的面片
完整代码
- 需要加入阴影详见书P240
1 | Shader "Unity Shaders Book/Chapter 11/CT_Water" |
![GIF 2021-11-2 11-27-17](中级 9-11.assets/GIF%202021-11-2%2011-27-17.gif)
实战11-4:广告牌
思路:设视角方向为新法线方向,通过与向上方向叉积得向右方向,再通过向右方向和新法线方向叉积得新向上方向
1 | Shader "Unity Shaders Book/Chapter 11/CT_Billboard" |
![image-20210619175124963](中级 9-11.assets/image-20210619175124963.png)