《Unity Shader入门精要》自学笔记(七)第九章 更复杂的光照 ——光照衰减

帧调试器FrameDebugger

又是一个好玩的小玩具好用的小工具呢!
可以通过Window → Analysis → Frame Debugger打开
然后点击左上角的Enable,就可以查看了
在这里插入图片描述
点击1打开或关闭Frame Debugger
滑动2一步一步查看渲染过程
点击3切换显示渲染效果和shader中的属性值(对于纹理,可以ctr + 鼠标左键查看大图)
点击4查看每个步骤的状态

目前只要知道这些就可以了,Frame Debugger的更多玩法详情见Unity User Manual (2019.4 LTS)Graphics Optimizing graphics performanceFrame Debugger

Unity中的光源

Unity中光源的属性

Light组件中的3个属性

  1. 颜色(Color)
  2. 强度(Intensity)
  3. 衰减(Attenuation):Unity光源的Inspector面板中,使用该光源可以照亮的半径Range来表示

以及Transform组件中的两个属性

  1. 位置(Position):与衰减、强度参数一起计算到达顶点的光照强度
  2. 方向(Direction):与到达顶点的光照强度、光照颜色等属性计算顶点接受的光照

上面的5个属性用一个可以调节亮度的手电筒来理解就再简单不过了

  1. 颜色:手电筒的灯泡可以是白色,也可以是浅蓝色,还可以是骚气的绿色,它们打在物体上(比如白色的墙面)会有不同的效果
  2. 强度:手电筒可以调节不同的亮度档次,光打在物体上的效果也会不一样
  3. 衰减&位置:拿着手电筒靠近或远离物体,光打在物体上的效果也会不一样
  4. 方向:向左或右迈一大步,但手电筒还是打在之前的位置上,相当于改变了光照的方向,效果也会不一样

Unity中的4种光源类型

  1. 平行光(Directional Light)
    最简单粗暴的光源:对所有点的方向、强度都一样,不受位置、衰减参数的影响
    举例:太阳
    影响属性:颜色、强度、角度
  2. 点光源(Point Light)
    由空间球体定义,由一个点发出的、向所有方向延伸的光,不受角度属性影响
    举例:蜡烛、星星点点的装饰灯
    影响属性:颜色、强度、衰减(Inspector中,使用该光源可以照亮的区域的半径Range来表示)、位置
  3. 聚光灯(Spot Light)
    在点光源的基础上,限制了光线发出的方向范围为一个锥形区域,需要额外计算来判断一个点是否在锥形区域内(是否被照亮)
    举例:手电筒、舞台聚光灯
    影响属性:颜色、强度、衰减、位置、方向、锥形区域打开的角度(Spot Angle)
  4. 面光源(Area Light)
    烘焙光源:只在烘焙时有效

在Unity的前向渲染中获得光源的属性

准备工作

SubShader{
    
    
	Tags{
    
    "RenderType" = "Opaque"}

	Pass{
    
    
		Tags{
    
    "LightMode" = "ForwardBase"} // ForwardBase Pass

		CGPROGRAM
		#pragma multi_compile_fwdbase // 正确赋值一些内置变量			
		#pragma vertex vert
		#pragma fragment frag
		#include "Lighting.cginc"
		// ...
		ENDCG
	}

	Pass{
    
    
		Tags{
    
    "LightMode" = "ForwardAdd"} // ForwardAdd Pass
		Blend One One // 注意开启混合,否则之前的渲染结果会被覆盖
		
		CGPROGRAM
		#pragma multi_compile_fwdadd // 正确赋值一些内置变量
		#pragma vertex vert
		#pragma fragment frag
		#include "Lighting.cginc"
		#include "AutoLight.cginc"
		// ...
		ENDCG
	}
	FallBack "Specular"
}

接下来看之前最常用到的平行光,在ForwardBase Pass中片元着色器的代码

fixed4 frag(v2f i) : SV_Target {
    
    
	// 方向(平行光位置参数无效,_WorldSpaceLightPos0.w = 0,_WorldSpaceLightPos0.xyz中存储rotation)
	fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
	fixed3 worldNormal = normalize(i.worldNormal);
	
	fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
	
	// 颜色 & 强度: _LightColor0已经是 颜色*强度 的结果
 	fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

 	fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
 	fixed3 halfDir = normalize(worldLightDir + viewDir);
 	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

	// 衰减:平行光可以认为无衰减
	fixed atten = 1.0;
	
	// 最终结果计算:记得带上衰减的影响
	return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

然后是ForwardAdd Pass中的点光源和聚光灯:
PS:这段代码信息量有点大,代码注释中基本只解释了每行代码的作用,更具体的作用和意思会单独拿出来说

fixed4 frag(v2f i) : SV_Target {
    
    
	// 方向
	#ifdef USING_DIRECTIONAL_LIGHT // 如果该光源是平行光
		// 直接使用平行光的方向即可
		fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
	#else // 如果是点光源或聚光灯
		// 光照方向需要使用 光源位置 - 点坐标 进行计算
		fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
	#endif
	
	// 颜色 & 强度: _LightColor0已经是 颜色*强度 的结果
	fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
	
	fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
	fixed3 halfDir = normalize(worldLightDir + viewDir);
	fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
	
	// 衰减
	#ifdef USING_DIRECTIONAL_LIGHT // 如果是平行光
		fixed atten = 1.0;		   // 认为无衰减
	#else
		#if defined (POINT) // 点光源
			// 计算该点在光源空间的坐标
	        float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
	        // 纹理采样获得该点的光源衰减
	        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
	    
	    #elif defined (SPOT) // 聚光灯
	    	// 计算该点在光源空间的坐标
	        float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
	        fixed atten = // 式子有点长,下面分行写
	        // 物体在聚光灯z轴正方向时会受到光照
	        (lightCoord.z > 0)
	        // _LightTexture0,cookie纹理,聚光灯的投影形态
	        * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w 
	        // _LightTextureB0,衰减纹理
	        * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
	    #else
	        fixed atten = 1.0;
	    #endif
	#endif

	// 最终结果计算:记得带上衰减的影响
	return fixed4((diffuse + specular) * atten, 1.0);
}

迷惑点1:mul(unity_WorldToLight, float4(i.worldPos, 1)),为什么要float4(i.worldPos, 1)的“1”?
之前进行的空间变换都是直接与矩阵相乘,这次为什么要多一个1呢?
因为这次是对点进行变换,之前是对向量(w为0,且位置对向量无意义)

迷惑点2:dot(lightCoord, lightCoord).rr是啥东西?

fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

首先认识一下“最熟悉的陌生人”:.,它是Cg语言中常见的swizzle操作符,可以将一个向量的成员取出组成一个新的向量
比如,float4 a = (0.1, 0.2, 0.3, 1),那a.rgb = a.xyz = (0.1, 0.2, 0.3),a.aa = a.ww = (1, 1)

然后再看,lightCoord是刚刚计算的点在光源空间的坐标
那dot(lightCoord, lightCoord)就是点距离光源中心的距离的平方
那dot(lightCoord, lightCoord).rr就是用这个距离的平方构造了一个xy相等的二维坐标
那tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr就是对_LightTexture0对角线上的纹理进行采样

补充
dot(lightCoord, lightCoord) 是距离的平方,作为采样的纹理坐标,其值域应该是 [0,1],应该怎么理解呢?
 
(目前没有找到更多关于光源空间坐标的信息,以下仅为个人理解)
虽然uv的范围是(0,1),但是纹理也分 clamp 和 repeat 两种
很明显 _LightTextureB0 这里是 clamp 类型,那么距离的平方这里也就很好理解了,当它的范围超过 1 时,会按照(1,1)采样,也就是距离超过了光源可以照亮的最大范围,衰减为 0 了
 
那是不是所有到光源距离超过 1 的点都会衰减到 0 ?
其实不是的,因为准确的说是 “ 光源空间中,到光源距离超过 1 的点的光照都会衰减到 0 ”
也就是说,点的光照是不是会衰减为 0,取决于点转换到光源空间后,它到光源的距离是不是大于 1
 
根据上面说过的两点,其实可以推测出,修改光源的光照范围,其实就是修改光源空间 xyz 三个坐标轴在世界空间的向量表示,导致世界空间和光源空间的转换矩阵被改变,进而改变了点转换到光源空间后到光源的距离,最终改变了这个点的光照衰减值

迷惑点3:纹理采样怎么得到衰减?衰减纹理长什么样?
这里的采样的纹理其实就是一张查找表(Lookup Table,LUT),因为使用数学公式计算衰减往往涉及开根号、除法等计算量较大的操作

根据代码来看,查找表获得的颜色值的UNITY_ATTEN_CHANNEL通道就是衰减值,那么它一定是rgb中的一个颜色,或者a透明度

在场景里只开启一个点光源,放到可以离物体较近且可以较好的照亮物体的地方,打开帧调试器,一步一步找到这个_LightTexture0纹理,它被Unity标记成了“UnityAttenuation”
在这里插入图片描述
从小图标看,大致能猜到这个UNITY_ATTEN_CHANNEL通道就是r通道了
不过这个纹理的尺寸是1024 * 1的,无法正常通过ctr + 鼠标左键打开查看:
在这里插入图片描述
于是从知乎上( UnityShader入门精要笔记12:高级光照B)找到了一个获取衰减纹理的shader,换上这个shader后再打开帧调试器,就能看到这张纹理了:
在这里插入图片描述
可以看到这个纹理的名字是“TempBuffer1713”,尺寸是328 * 322,刚好是当前Game窗口的大小
这是因为现在看到的这张纹理并不是真正的_LightTexture0,而是渲染到Game窗口的最终帧图像
在这里插入图片描述
图中红色框住的是名为TempBuffer1713的普通MainTex(Game窗口尺寸)
蓝色框住的是名为UnityAttenuation的衰减纹理_LightTexture0(还是1024 * 1,查看无果),看阶段的名字也可以明白原因

…扯远了,平面上的纹理对角线的r通道颜色值,就是光照强度随顶点到光源衰减后的值
在这里插入图片描述
要注意把xy轴的方向放对,否则平面的纹理会出现翻转(红边不在左侧)的情况
现在这样就符合从(0,0)到(1,1)光照强度越来越小了

迷惑点4:(lightCoord.z > 0)怎么也可以放到式子里计算?有什么用?
把它改成(lightCoord.z > 0.5),在场景里看一下效果:
在这里插入图片描述
注意观察右侧Transform组件的position的y值,和场景中的光照效果
可以发现,大概是在5的位置,光照效果会在突然出现或消失

代码中写的是0.5,中间差了10倍
找一找就会发现,刚好Range参数的设置是10
在这里插入图片描述
把y值改为4.999,发现光照消失
在这里插入图片描述
再把Range改为20,y值调整为10和9.999进行验证,符合刚刚的结论
在这里插入图片描述
在这里插入图片描述
所以这个(lightCoord.z > 0.5)其实是和Range一起计算,来判断某一点是否会被光照到
它其实也等价于 lightCoord.z > 0 ? 1 : 0;或者lightCoord.z > 0.5 ? 1 : 0;这些

迷惑点5:tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w是什么?
在计算点光源的代码中,_LightTexture0是衰减纹理
与计算聚光灯的代码对比发现,聚光灯的衰减纹理计算使用的纹理是_LightTextureB0,那它的_LightTexture0又是什么?

先分析一下这个代码:

  1. 前面知道lightCoord是点的光照空间坐标
  2. 在线性代数的基础章节可以知道,lightCoord.xy / lightCoord.w的操作其实是将点的齐次坐标转换成了三维空间的坐标(把w分量的值还原为1),再 + 0.5
  3. 接下来再取其xy分量作为纹理坐标,对_LightTexture0进行采样,然后取w通道的值
  4. 最后再用w通道的值乘以衰减纹理(_LightTextureB0)的采样值

对于2中的 + 0.5,之前像素的章节学习过,像素的坐标是它中心点的坐标,所以要 + 0.5

// 物体在聚光灯z轴正方向时会受到光照
(lightCoord.z > 0)
// 
* tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w 
// _LightTextureB0,衰减纹理
* tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

回忆之前学习过的遮罩纹理和渐变纹理,可以很轻松猜到对_LightTexture0采样的w通道应该就是聚光灯的遮罩通道

再打开帧调试器进行确认:
在这里插入图片描述
_LightTextureB0是一个128*128的纹理,可以打开查看
中间白四角黑,边界有较为平滑的灰色过度,很容易让人联想到遮罩
再联想一下,就会想到聚光灯投射到物体上的光点
在这里插入图片描述

补充:
Spot面板上有一个可以放纹理的地方:Cookie
在这里插入图片描述
随便选一个上去
在这里插入图片描述
聚光灯的光照区域变成了刚刚选择的纹理的灰度图
并且给出了警告:聚光灯的Cookie纹理要设置为Clamp模式,避免出现走样

同时打开帧调试器,发现原先的白色圆圈已经变成了刚刚的蓝色法线纹理,也证明了这个LightTexture0就是刚刚的Cookie纹理
在这里插入图片描述
至此这三行代码终于都明白了

// 物体在聚光灯z轴正方向时会受到光照
(lightCoord.z > 0)
// _LightTexture0,cookie纹理,聚光灯的投影形态
* tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w 
// _LightTextureB0,衰减纹理
* tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

总结
这一部分感受最深的是,帧调试器真好用!玩多了甚至有点上瘾
之前复习过的比较基础的线代知识也真的很有用,果然基础很重要

另外,乐乐大佬的这本书编排顺序真的很适合小白学习啊,还好当时没有跳着看

参考资料
UnityShader入门精要笔记12:高级光照B这篇文章帮我解决了很多问题,也提供了很多思路,表示感谢~

猜你喜欢

转载自blog.csdn.net/weixin_44045614/article/details/110732577