光源的着色器
我们在《渲染13:延迟渲染》中添加了对延迟渲染路径的支持。我们所要做的只是填补G缓冲区。光源在后面的过程中会在后面渲染。那个教程简要地介绍了Unity如何添加这些光源。 这一次,我们将自己渲染这些光源。
为了测试光源,我将使用一个简单的场景,其环境强度设置为零。它用延迟模式下的高动态光照渲染摄像机进行渲染。
测试场景,有方向光源和没有方向光源的效果对比。
场景中的所有对象都使用我们自己的着色器渲染到G缓冲区。但是,这些光源是用Unity的默认延迟着色器渲染的,它被命名为Hidden / Internal-DefferedShader。你可以通过“编辑/项目设置/图形”转到图形设置并将“延迟”着色器模式切换为“自定义着色器”来进行验证。
默认的延迟光源着色器。
使用一个自定义的着色器
每个延迟光源使用单独的渲染通道进行渲染,修改图像的颜色。实际上,它们是图像效果,就像我们之前教程中的延迟渲染下雾的着色器一样。 我们从一个简单的着色器开始,用黑色覆盖一切。
Shader"Custom/DeferredShading"{
Properties {
}
SubShader {
Pass {
Cull Off
ZTest Always
ZWrite Off
CGPROGRAM
#pragma target 3.0
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
#pragma exclude_renderers nomrt
#include "UnityCG.cginc"
structVertexData {
float4 vertex : POSITION;
};
structInterpolators {
float4 pos : SV_POSITION;
};
Interpolators VertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
returni;
}
float4 FragmentProgram (Interpolators i) : SV_Target {
return0;
}
ENDCG
}
}
}
指示Unity在渲染延迟光源的时候使用此着色器。
使用我们的自定义着色器。
第二个渲染通道
切换到我们的着色器后,Unity抱怨说它没有足够的渲染通道。显然,需要第二个渲染通道。我们只是复制我们已经拥有的渲染通道,看看会发生什么。
Pass {
…
}
Pass {
…
}
Unity现在接受我们的着色器并使用它来渲染方向光源。 结果,一切都变黑了。 唯一的例外是天空。模板缓冲区用作掩码以避免在那里渲染,因为方向光源不影响背景。
自定义着色器,照亮和不照亮的效果对比。
但是第二个渲染通道呢? 请记住,当高动态光照渲染被禁用的时候,光源数据被用对数编码。最后一个渲染通道需要、反转这个编码。这是第二个渲染通道的用处。 因此,如果你禁用了摄像机的高动态光照渲染,我们的着色器的第二个渲染通道还是会被使用,一次。
避开对天空的影响
当以低动态光照渲染模式渲染时,你可能会看到天空变黑。 这可能发生在场景视图或游戏视图中。如果天空变黑,那么执行转换的渲染通道没有正确地使用模板缓冲区作为掩码。要解决此问题,请明确设置第二个渲染通道的模板。当我们处理的是非背景部分的片段的时候,我们只应该进行渲染。 通过_StencilNonBackground提供了适当的模板值。
Pass {
Cull Off
ZTest Always
ZWrite Off
Stencil {
Ref [_StencilNonBackground]
ReadMask [_StencilNonBackground]
CompBack Equal
CompFront Equal
}
…
}
我们可以调试模板缓冲区吗?
不幸的是,帧调试器不会显示有关模板缓冲区的任何信息,也不显示其内容以及通道如何使用。 也许这些信息将在以后的版本中添加。
转换颜色
为了使第二个渲染通道工作,我们必须转换光源缓冲区中的数据。像我们的雾着色器一样,使用UV坐标来绘制全屏的四边形,我们可以使用它来采样缓冲区。
structVertexData {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
structInterpolators {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
Interpolators VertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
i.uv = v.uv;
returni;
}
光源缓冲区本身通过_LightBuffer变量提供给着色器。
sampler2D _LightBuffer;
…
float4 FragmentProgram (Interpolators i) : SV_Target {
returntex2D(_LightBuffer, i.uv);
}
当没有被照亮的时候,原始的低动态光照渲染数据。
使用公式2-C对低动态光照渲染的颜色进行对数编码。 为了解码,我们必须使用公式-log2 C。
1
return-log2(tex2D(_LightBuffer, i.uv));
当没有被照亮的时候的低动态光照渲染的图像。
现在我们知道它可以正常工作,再次启用高动态光照渲染。
项目文件下载地址:unitypackage。
方向光源
第一个渲染通道会负责渲染光源,所以这将是相当复杂的。让我们为它创建一个名为MyDeferredShading.cginc的导入文件。 将渲染通道中的所有代码复制到此文件。
#if !defined(MY_DEFERRED_SHADING)
#define MY_DEFERRED_SHADING
#include "UnityCG.cginc"
…
#endif
然后在第一个渲染通道导入MyDeferredShading。
Pass {
Cull Off
ZTest Always
ZWrite Off
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
#pragma exclude_renderers nomrt
#include "MyDeferredShading.cginc"
ENDCG
}
因为我们应该添加光照到图像,我们必须确保我们不会移除已经渲染的内容。 我们可以通过更改混合模式来组合完整的源颜色和目标颜色。
1
2
3
4Blend One One
Cull Off
ZTest Always
ZWrite Off
我们需要所有可能的光照配置的着色器变体。multi_compile_lightpasscompiler指令创建我们需要的所有关键字组合。唯一的例外是高动态光照渲染模式。 我们必须为此添加一个单独的多编译指令。
#pragma exclude_renderers nomrt
#pragma multi_compile_lightpass
#pragma multi_compile _ UNITY_HDR_ON
虽然这种着色器用于所有这三种类型的光源,但我们首先限制我们使用方向光源。
G缓冲区的UV坐标
我们需要UV坐标从G缓冲区中进行采样。不幸的是,Unity不提供带有纹理坐标的光照渲染通道。相反,我们必须从裁剪空间的位置来派生它们。为此,我们可以使用UnityCG中定义的ComputeScreenPos函数。这个函数会产生齐次坐标,就像裁剪空间的坐标一样,所以我们必须使用一个float4类型的变量来存储它们。
structInterpolators {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
};
Interpolators VertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
i.uv = ComputeScreenPos(i.pos);
returni;
}
在片段程序中,我们可以计算最终的2D坐标。如在《渲染7:阴影》中所解释的那样,这必须在插值之后发生。
float4 FragmentProgram (Interpolators i) : SV_Target {
float2 uv = i.uv.xy / i.uv.w;
return0;
}
世界位置
当我们创建我们的延迟渲染下的雾的图像效果的时候,我们必须弄清楚片段与相机的距离。我们通过从相机发射光线通过每个片段到远平面,然后对片段的深度值进行缩放,从而实现了这一点。 我们可以使用相同的方法来重建片段的世界位置。
在方向光源的情况下,四边形的四个顶点的光线会作为法向矢量提供。所以我们可以把它们传递给顶点程序进行插值。
structVertexData {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
structInterpolators {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 ray : TEXCOORD1;
};
Interpolators VertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
i.uv = ComputeScreenPos(i.pos);
i.ray = v.normal;
returni;
}
我们可以通过对_CameraDepthTexturetexture进行采样并将其线性化,就像我们对雾的效果所做的一样,在片段程序中找到深度值。
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
…
float4 FragmentProgram (Interpolators i) : SV_Target {
float2 uv = i.uv.xy / i.uv.w;
floatdepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
depth = Linear01Depth(depth);
return0;
}
不过,我们提供的光线到达远平面的处理与我们的雾着色器中的光线到达远平面的处理差别很大。在这种情况下,我们被提供的是到达近平面的光线。 我们必须对它们进行放缩,所以我们得到的是到达远平面的光线。 这可以通过缩放光线来完成,使其Z坐标变为1,并将其与远平面的距离相乘。
depth = Linear01Depth(depth);
float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
将此光线缩放深度值来给我们一个位置。所提供的光线在视图空间中定义,这是相机的本地空间。 所以我们最终得到了片段在视图空间中的位置。
1
2float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
float3 viewPos = rayToFarPlane * depth;
从这个空间到世界空间的转换是用ShaderVariables中定义的unity_CameraToWorld矩阵完成的。
float3 viewPos = rayToFarPlane * depth;
float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
读取G缓冲区数据
接下来,我们需要访问G缓冲区来检索表面属性。 缓冲区可通过三个_CameraGBufferTexture变量变得可用。
sampler2D _CameraGBufferTexture0;
sampler2D _CameraGBufferTexture1;
sampler2D _CameraGBufferTexture2;
我们在《渲染13:延迟着色器》教程中填充了相同的缓冲区。现在我们从这个缓冲区那里读取数据出来。 我们需要反照率、镜面高光的色泽、光滑度和法线值。
float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
float3 albedo = tex2D(_CameraGBufferTexture0, uv).rgb;
float3 specularTint = tex2D(_CameraGBufferTexture1, uv).rgb;
float3 smoothness = tex2D(_CameraGBufferTexture1, uv).a;
float3 normal = tex2D(_CameraGBufferTexture2, uv).rgb * 2 - 1;
计算双向反射分布函数
双向反射分布函数函数在UnityPBSLighting中定义,因此我们必须导入该文件。
//#include "UnityCG.cginc"
#include "UnityPBSLighting.cginc"
现在如果我们要在我们的片段程序中调用双向反射分布函数需要三个额外的数据位。首先是视图方向,这是用通常的方法得到的。
float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);
第二个数据是表面反射率。我们可以从镜面色调中推导出来。它只是最强的颜色分量。 我们可以使用SpecularStrength函数来提取它。
float3 albedo = tex2D(_CameraGBufferTexture0, uv).rgb;
float3 specularTint = tex2D(_CameraGBufferTexture1, uv).rgb;
float3 smoothness = tex2D(_CameraGBufferTexture1, uv).a;
float3 normal = tex2D(_CameraGBufferTexture2, uv).rgb * 2 - 1;
floatoneMinusReflectivity = 1 - SpecularStrength(specularTint);
第三,我们需要光源数据。 让我们从虚拟光源开始吧。
floatoneMinusReflectivity = 1 - SpecularStrength(specularTint);
UnityLight light;
light.color = 0;
light.dir = 0;
UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;
最后,我们可以使用双向反射分布函数来计算该片段中光源的贡献。
indirectLight.specular = 0;
float4 color = UNITY_BRDF_PBS(
albedo, specularTint, oneMinusReflectivity, smoothness,
normal, viewDir, light, indirectLight
);
returncolor;
配置光源
间接光源在这里不适用,所以它仍然是黑色的。 但是直接光源必须进行配置,使其与当前正在渲染的光源相匹配。对于方向光源,我们需要一个颜色和方向。这些可以通过_LightColor和_LightDirvariables来获取。
1
float4 _LightColor, _LightDir;
让我们创建一个单独的函数来设置光源。简单地将变量复制到光源结构中并返回。
UnityLight CreateLight () {
UnityLight light;
light.dir = _LightDir;
light.color = _LightColor.rgb;
returnlight;
}
在片段程序中使用这个函数。
UnityLight light = CreateLight();
// light.color = 0;
// light.dir = 0;
来自错误的方向的光源。
我们终于得到了光照,但光线似乎来自错误的方向。这是因为_LightDir设置为光线行进的方向。对于我们的计算,我们需要的是从表面到光源的方向,所以应该相反。
light.dir = -_LightDir;
方向光源,没有阴影的效果。
阴影
在My Lighting中,我们依靠AutoLight的宏来确定由阴影引起的光衰减。 不幸的是,该文件没有写入延迟光源。 所以我们自己来进行阴影采样。阴影贴图可以通过_ShadowMapTexture变量来访问。
sampler2D _ShadowMapTexture;
但是,我们不能随便声明这个变量。 它已经为UnityShadowLibrary中被定义点光源和聚光光源的阴影,我们间接导入了它们。 所以我们不应该自己定义它,除非使用方向光源的阴影。
#if defined (SHADOWS_SCREEN)
sampler2D _ShadowMapTexture;
#endif
要应用方向光源的阴影,我们只需要对阴影纹理进行采样,并使用它来衰减光的眼色。 在CreateLight函数中执行此操作意味着必须将UV坐标作为参数添加到其中。
UnityLight CreateLight (float2 uv) {
UnityLight light;
light.dir = -_LightDir;
floatshadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
light.color = _LightColor.rgb * shadowAttenuation;
returnlight;
}
在片段程序中将UV坐标传递给它。
UnityLight light = CreateLight(uv);
有阴影的方向光源的效果。
当然,这只有在方向光源启用了阴影的时候才有效。 如果方向光源没有启用阴影的话,阴影衰减始终为1。
floatshadowAttenuation = 1;
#if defined(SHADOWS_SCREEN)
shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
#endif
light.color = _LightColor.rgb * shadowAttenuation;
阴影的衰减
阴影贴图是有限的。 它不能覆盖整个世界。它覆盖的面积越大,阴影的分辨率越低。Unity有一个绘制阴影的最大距离。 除此之外,还没有实时阴影。这个距离可以通过“编辑/项目设置/质量”进行调整。
阴影距离质量设置。
当阴影接近这个距离的时候,它们会消失。 至少这就是Unity的着色器所做的事情。 因为我们是手动采样阴影贴图,所以当到达地图边缘的时候,阴影会被截断。得到的结果就是阴影被急剧地切断或者超出了衰减的距离。
阴影距离设置的比较大和比较小的效果对比。
为了让阴影衰减,我们首先要知道阴影应该完全消失的距离。这个距离取决于方向光源的阴影的投影方式。在“稳定拟合”模式中,这个衰减是按照球面进行衰减的,以贴图的中间为中心。在”紧密拟合”模式下,它基于视图深度来进行衰减的。
UnityComputeShadowFadeDistance函数可以为我们找出正确的指标。 它具有世界位置和视图深度作为参数。它将返回距离影子中心的距离或是未修改的视图深度。
UnityLight CreateLight (float2 uv, float3 worldPos,floatviewZ) {
UnityLight light;
light.dir = -_LightDir;
floatshadowAttenuation = 1;
#if defined(SHADOWS_SCREEN)
shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
floatshadowFadeDistance =
UnityComputeShadowFadeDistance(worldPos, viewZ);
#endif
light.color = _LightColor.rgb * shadowAttenuation;
returnlight;
}
阴影应该逐渐消失,因为它们逐渐接近褪色距离,一旦到达这个距离阴影就会完全消失。UnityComputeShadowFade函数会计算适当的渐变因子。
floatshadowFadeDistance =
UnityComputeShadowFadeDistance(worldPos, viewZ);
floatshadowFade = UnityComputeShadowFade(shadowFadeDistance);
这些函数是什么样子的?
它们在UnityShadowLibrary中定义。unity_ShadowFadeCenterAndType变量包含阴影的中心和阴影的类型。_LightShadowData变量的Z和W组件包含了用于衰减的缩放和偏移量。
阴影衰减因子是一个从0到1的值,表示阴影应该衰减多少。实际的衰减可以通过简单地将该值加到阴影衰减中,并将计算出来的值限制到0到1这个范围来完成。
floatshadowFade = UnityComputeShadowFade(shadowFadeDistance);
shadowAttenuation = saturate(shadowAttenuation + shadowFade);
为了做到这一点,在我们的片段程序中提供世界的位置和深度到CreateLight函数。 视图深度是片段在视图空间中的位置的Z分量。
UnityLight light = CreateLight(uv, worldPos, viewPos.z);
发生了衰减的阴影效果。
光源的Cookie
我们必须支持的另一件事情是光源的cookie。通过_LightTexture0可以获得cookie纹理。此外,我们还必须从世界空间转换为光源所在空间,因此我们可以对纹理进行采样。该转换通过unity_WorldToLightmatrix变量提供。
sampler2D _LightTexture0;
float4x4 unity_WorldToLight;
在CreateLight函数中,使用矩阵将世界位置转换为光源空间的坐标。然后使用它们来对cookie纹理进行采样。我们使用单独的衰减变量来跟踪cookie的衰减。
light.dir = -_LightDir;
floatattenuation = 1;
floatshadowAttenuation = 1;
#if defined(DIRECTIONAL_COOKIE)
float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
attenuation *= tex2D(_LightTexture0, uvCookie).w;
#endif
…
light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
带有cookie的方向光源。
结果看起来很好,除非你密切注意几何边缘才会注意到问题。
沿着几何边缘的瑕疵。
当相邻片段的cookie坐标之间存在很大差异的时候,会出现这些瑕疵。在这些情况下,图形处理器会选择对于最接近的表面来说太低的mipmap级别。Aras Pranckevičius给Unity指出了这个问题。Unity使用的解决方案是在采样mip地图的时候应用一个偏差,因此我们也将这样做。
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
带有偏差的cookie采样。
支持低动态光照渲染
现在我们可以正确地渲染方向光源,但只能在高动态光照渲染模式下。低动态光照渲染模式下会出错。
低动态光照渲染模式下颜色不正确。
首先,编码的低动态光照渲染的颜色必须乘以光源缓冲区的颜色,而不是加上光源缓冲区的颜色。我们可以通过将我们的着色器的混合模式更改为Blend DstColor Zero来实现。但是,如果我们这样做的话,那么高动态光照渲染模式下的渲染会出错。相反,我们必须使混合模式变量。Unity为此使用_SrcBlend和_DstBlend。
Blend [_SrcBlend] [_DstBlend]
效果不同,但仍然不正确。
在未定义UNITY_HDR_ON的时候,我们会在片段程序结束的时候还是必须应用2-C这个转换。
float4 color = UNITY_BRDF_PBS(
albedo, specularTint, oneMinusReflectivity, smoothness,
normal, viewDir, light, indirectLight
);
#if !defined(UNITY_HDR_ON)
color = exp2(-color);
#endif
returncolor;
项目文件下载地址:unitypackage。
聚光光源
因为方向光源会影响一切,所以它们被画成全屏四边形。相比之下,聚光光源仅影响位于其锥体内的场景部分。通常不需要为整个图像计算聚光光源的光照。相反,金字塔被渲染成与聚光光源影响区域相匹配的方式。
绘制一个金字塔区域
禁用方向光源,并使用聚光光源来代替。因为我们的着色器只能在方向光源上正常工作,所以结果将是错误的。但是它可以让你看到金字塔区域的哪些部分被渲染。
金字塔区域的一部分。
事实证明,金字塔区域被渲染为一个常规的3D对象。它的背面被剔除,所以我们看到金字塔区域的前方。只有在前面没有任何东西的时候才会被绘制出来。此外,添加了一个渲染通道,设置模板缓冲区以将绘制限制在位于金字塔区域内的片段上。你可以通过帧调试器来验证这些设置。
它是如何绘制的。
这意味着我们的着色器的裁剪和z检验设置被推翻了。所以让我们从着色器中删除它们。
Blend [_SrcBlend] [_DstBlend]
// Cull Off
// ZTest Always
ZWrite Off
当聚光光源照亮的体积离摄像机足够远的时候,此方法有效。但是,当光线太靠近摄像机的时候,会失败。当这种情况发生的时候,摄像机可能会在聚光光源照亮的体积的内部。 甚至可能近平面的一部分都在聚光光源照亮的体积的内部,而其余部分位于聚光光源照亮的体积的外部。在这些情况下,模板缓冲区不能用于限制渲染。
这个技巧用于仍然在金字塔的内表面渲染光,而不是金字塔的外表面渲染光。 这是通过渲染金字塔的背面而不是金字塔的的正面来完成的。此外,这些曲面只有在它们之前的物体已经渲染出来之后才会被渲染。这种方法还涵盖了聚光光源体积内的所有片段。但是它最终会渲染太多的片段,因为金字塔通常隐藏的部分现在也被渲染了。所以只有在必要的时候才这么做。
在靠近摄像机的时候绘制背面
如果你将摄像机或是聚光光源靠近彼此来移动,你会看到Unity会根据需要在这两种渲染方法之间进行切换。一旦我们的着色器在聚光光源下工作正常,两种方法之间就没有视觉上的区别。
支持多种光源类型
目前,CreateLight只适用于方向光源。让我们确保仅在适的时候时才使用特定于方向光源的代码。
UnityLight CreateLight (float2 uv, float3 worldPos,floatviewZ) {
UnityLight light;
// light.dir = -_LightDir;
floatattenuation = 1;
floatshadowAttenuation = 1;
#if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
light.dir = -_LightDir;
#if defined(DIRECTIONAL_COOKIE)
float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
#endif
#if defined(SHADOWS_SCREEN)
shadowed =true;
shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
floatshadowFadeDistance =
UnityComputeShadowFadeDistance(worldPos, viewZ);
floatshadowFade = UnityComputeShadowFade(shadowFadeDistance);
shadowAttenuation = saturate(shadowAttenuation + shadowFade);
#endif
#else
light.dir = 1;
#endif
light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
returnlight;
}
虽然阴影衰减可以在基于方向光源阴影贴图的时候正常工作,那么其他光源类型的阴影也是需要衰减的。这样可以确保所有的阴影都以相同的方式消失,而不是只有一些阴影会消失。 因此,阴影衰落代码适用于所有光源类型,只要这个光源类型会产生阴影。所以我们把这块的代码移到特定光源块之外。
我们可以使用布尔值来控制是否使用阴影衰落代码。 由于布尔值作为常量值,如果该值保持为false,则这段代码不会被执行。
UnityLight CreateLight (float2 uv, float3 worldPos,floatviewZ) {
UnityLight light;
floatattenuation = 1;
floatshadowAttenuation = 1;
boolshadowed =false;
#if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
…
#if defined(SHADOWS_SCREEN)
shadowed =true;
shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
// float shadowFadeDistance =
// UnityComputeShadowFadeDistance(worldPos, viewZ);
// float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
// shadowAttenuation = saturate(shadowAttenuation + shadowFade);
#endif
#else
light.dir = 1;
#endif
if(shadowed) {
floatshadowFadeDistance =
UnityComputeShadowFadeDistance(worldPos, viewZ);
floatshadowFade = UnityComputeShadowFade(shadowFadeDistance);
shadowAttenuation = saturate(shadowAttenuation + shadowFade);
}
light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
returnlight;
}
非方向光源具有一个位置。这个位置通过_LightPos提供。
float4 _LightColor, _LightDir, _LightPos;
现在我们可以确定聚光光源的光矢量和光线方向。
#else
float3 lightVec = _LightPos.xyz - worldPos;
light.dir = normalize(lightVec);
#endif
再次计算世界位置
光线方向似乎不正确,结果为黑色。发生这种情况是因为聚光光源的世界位置计算不正确。当我们在场景中的某个地方渲染一个金字塔的时候,我们没有一个方便的全屏四边形,会在法线通道中存储光线。相反,MyVertexProgram必须从顶点位置导出光线。 这是通过将点转换为视图空间的位置来完成的,我们可以使用UnityObjectToViewPos函数。
i.ray = UnityObjectToViewPos(v.vertex);
然而,这会产生具有错误方向的光线。我们必须对它们的X和Y坐标取负。
i.ray = UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1);
正确的世界空间下的位置。
UnityObjectToViewPos是如何工作的?
这个函数是在UnityCG中定义的。 它首先将点转换为世界空间的位置,然后使用视图矩阵将世界空间的位置转换为相机空间的位置。
#else
float3 lightVec = _LightPos.xyz - worldPos;
light.dir = normalize(lightVec);
#endif
当在场景中渲染光的几何体的时候,这种替代方法起作用。当使用全屏四边形的时候,我们应该使用顶点法线。Unity通过_LightAsQuad变量告诉我们现在是正在处理哪种情况。
float_LightAsQuad;
如果这个变量设置为1,那么我们正在处理一个四边形,可以使用法线。 否则,我们必须使用UnityObjectToViewPos。
i.ray = lerp(
UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1),
v.normal,
_LightAsQuad
);
Cookie的衰减
聚光光源的圆锥衰减是通过Cookie纹理创建的,无论是默认圆圈还是自定义Cookie。 我们可以从复制方向光源的cookie代码开始。
float3 lightVec = _LightPos.xyz - worldPos;
light.dir = normalize(lightVec);
float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
然而,聚光光源的cookie在越远离光源位置的情况下会变得越大。这是通过透视变换完成的。所以矩阵乘法会产生四维的齐次坐标。为了得到常规的二维坐标,我们必须将X分量和Y分量除以W分量。
float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
uvCookie.xy /= uvCookie.w;
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
Cookie的衰减。
这实际上会导致两个光锥,一个光锥向前,一个光锥向后。向后的锥体通常在渲染区域的外部结束,但这不能保证。我们只想要向前的锥体,它与负的W坐标相对应。
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
attenuation *= uvCookie.w < 0;
距离衰减
来自聚光光源的光线也会根据距离进行衰减。该衰减存储在查找纹理中,可通过_LightTextureB0获得。
sampler2D _LightTexture0, _LightTextureB0;
纹理被设计为必须采用平方后光源的距离进行采样,按照光源的范围进行缩放。光源的范围存储在_LightPos的第四个变量中。应该使用哪个纹理通道,因平台而异,由UNITY_ATTEN_CHANNEL宏进行定义。
light.dir = normalize(lightVec);
attenuation *= tex2D(
_LightTextureB0,
(dot(lightVec, lightVec) * _LightPos.w).rr
).UNITY_ATTEN_CHANNEL;
float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
Cookie和距离衰减。
阴影
当聚光光源有阴影的时候,会定义SHADOWS_DEPTH关键字。
float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
uvCookie.xy /= uvCookie.w;
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
#if defined(SHADOWS_DEPTH)
shadowed =true;
#endif
聚光光源和方向光源使用相同的变量来对其阴影贴图进行采样。在聚光光源的情况下,我们可以使用UnitySampleShadowmap来处理采样硬阴影或软阴影的细节。我们必须在阴影空间中提供片段位置。unity_WorldToShadow数组中的第一个矩阵可用于从世界空间转换为阴影空间。
shadowed =true;
shadowAttenuation = UnitySampleShadowmap(
mul(unity_WorldToShadow[0], float4(worldPos, 1))
);
带有阴影的聚光光源。
项目文件下载地址:unitypackage。
点光源
点光源使用与聚光光源相同的光矢量、方向和距离衰减。所以他们可以共享该代码。聚光光源代码的其余部分只能在定义SPOT关键字的时候使用。
#if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
…
#else
float3 lightVec = _LightPos.xyz - worldPos;
light.dir = normalize(lightVec);
attenuation *= tex2D(
_LightTextureB0,
(dot(lightVec, lightVec) * _LightPos.w).rr
).UNITY_ATTEN_CHANNEL;
#if defined(SPOT)
float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
uvCookie.xy /= uvCookie.w;
attenuation *=
tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
attenuation *= uvCookie.w < 0;
#if defined(SHADOWS_DEPTH)
shadowed =true;
shadowAttenuation = UnitySampleShadowmap(
mul(unity_WorldToShadow[0], float4(worldPos, 1))
);
#endif
#endif
#endif
这已经足够让点光源工作了。它们的渲染与聚光光源相同,不同之处在于使用的是一个圈而不是金字塔。
高强度的点光源。
阴影
点光源的阴影存储在立方体贴图之中。UnitySampleShadowmap负责我们的采样。在这种情况下,我们必须提供一个从光源到表面的向量,以便对立方体贴图进行采样。这与光矢量的方向相反。
#if defined(SPOT)
…
#else
#if defined(SHADOWS_CUBE)
shadowed =true;
shadowAttenuation = UnitySampleShadowmap(-lightVec);
#endif
#endif
带有阴影的点光源。
Cookie
点光源的cookie也可以通过_LightTexture0获得。然而,在这种情况下,我们需要一个立方体贴图而不是一个普通的纹理。
//sampler2D _LightTexture0, _LightTextureB0;
#if defined(POINT_COOKIE)
samplerCUBE _LightTexture0;
#else
sampler2D _LightTexture0;
#endif
sampler2D _LightTextureB0;
float4x4 unity_WorldToLight;
要对cookie进行采样,请将片段的世界位置转换为光照空间的位置,并使用该位置对立方体贴图进行采样。
#else
#if defined(POINT_COOKIE)
float3 uvCookie =
mul(unity_WorldToLight, float4(worldPos, 1)).xyz;
attenuation *=
texCUBEbias(_LightTexture0, float4(uvCookie, -8)).w;
#endif
#if defined(SHADOWS_CUBE)
shadowed =true;
shadowAttenuation = UnitySampleShadowmap(-lightVec);
#endif
#endif
带有cookie的点光源。
点光源的cookie纹理不起作用?
如果你最初使用的是较旧的Unity版本来导入Cookie的立方体贴图纹理,那么导入设置可能会出错。这只发生在立方体贴图上。确保其纹理类型为Cookie,映射设置为“自动”,光源类型为”点光源“。
点光源cookie纹理的导入设置。
跳过阴影
我们现在可以使用我们自己的着色器渲染所有动态光源了。在现在这个时间点上,我们不太注意优化,但这里有一个潜在的大型优化值得考虑。
最终超出阴影褪色距离的片段不会被遮挡。但是,我们仍然在对其阴影进行采样,这个操作可能是昂贵的。我们可以通过基于阴影褪色因子做分支来避免这种情况。 如果这个值接近1,那么我们可以完全跳过阴影衰减。
if(shadowed) {
floatshadowFadeDistance =
UnityComputeShadowFadeDistance(worldPos, viewZ);
floatshadowFade = UnityComputeShadowFade(shadowFadeDistance);
shadowAttenuation = saturate(shadowAttenuation + shadowFade);
UNITY_BRANCH
if(shadowFade > 0.99) {
shadowAttenuation = 1;
}
}
然而,分支本身可能是昂贵的。 这只是一个改进,因为这是一个连贯的分支。 除了阴影区域的边缘附近,所有碎片都落入其内部或外部。 但是,如果图形处理器能够利用这一点,这会是重要的。 在这种情况下,HLSLSupport定义了UNITY_FAST_COHERENT_DYNAMIC_BRANCHING宏。
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING)
UNITY_BRANCH
if(shadowFade > 0.99) {
shadowAttenuation = 1;
}
#endif
即使这样,只有当阴影需要多个纹理采样的时候,它才是真正值得的。对于聚光光源和点光源的软阴影来说,这是用SHADOWS_SOFT关键词来表示的。 方向光源的阴影总是需要一个单一的纹理采样,而这便宜。
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
UNITY_BRANCH
if(shadowFade > 0.99) {
shadowAttenuation = 1;
}
#endif
网友评论