https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/
1 聚光阴影
1.1 阴影混合
修改Shadows.hlsl
中的GetOtherShadowAttenuation
,让其和GetDirectionalShadowAttenuation
类似:
float GetOtherShadow (
OtherShadowData other, ShadowData global, Surface surfaceWS
)
{
return 1.0;
}
float GetOtherShadowAttenuation (
OtherShadowData other, ShadowData global, Surface surfaceWS
)
{
#if !defined(_RECEIVE_SHADOWS)
return 1.0;
#endif
float shadow;
if (other.strength * global.strength <= 0.0)
{
shadow = GetBakedShadow(
global.shadowMask, other.shadowMaskChannel, abs(other.strength)
);
}
else
{
shadow = GetOtherShadow(other, global, surfaceWS);
shadow = MixBakedAndRealtimeShadows(
global, shadow, other.shadowMaskChannel, other.strength
);
}
return shadow;
}
global.strength
用于决定我们是否跳过采样实时阴影这一步骤。级联只应用于平行阴影,而其它光源有固定位置,它们的阴影贴图不能随观察移动。最好的方法是用相同的方式渐变阴影,因此使用相同的global.strength
。
我们将设置级联数和距离渐变的代码移至Shadows.Render
:
public void Render ()
{
…
buffer.SetGlobalInt(
cascadeCountId,
shadowedDirLightCount > 0 ? settings.directional.cascadeCount : 0
);
float f = 1f - settings.directional.cascadeFade;
buffer.SetGlobalVector(
shadowDistanceFadeId, new Vector4(
1f / settings.maxDistance, 1f / settings.distanceFade,
1f / (1f - f * f)
)
);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
void RenderDirectionalShadows ()
{
…
//buffer.SetGlobalInt(cascadeCountId, settings.directional.cascadeCount);
buffer.SetGlobalVectorArray(
cascadeCullingSpheresId, cascadeCullingSpheres
);
buffer.SetGlobalVectorArray(cascadeDataId, cascadeData);
buffer.SetGlobalMatrixArray(dirShadowMatricesId, dirShadowMatrices);
//float f = 1f - settings.directional.cascadeFade;
//buffer.SetGlobalVector(
// shadowDistanceFadeId, new Vector4(
// 1f / settings.maxDistance, 1f / settings.distanceFade,
// 1f / (1f - f * f)
// )
//);
…
}
在GetShadowData
中确保全局强度不会设置为0:
if (i == _CascadeCount && _CascadeCount > 0)
{
data.strength = 0.0;
}
1.2 其它光实时阴影
我们定义最大可产生实时阴影的其它类型光的数量:
const int maxShadowedDirLightCount = 4, maxShadowedOtherLightCount = 16;
const int maxCascades = 4;
…
int shadowedDirLightCount, shadowedOtherLightCount;
…
public void Setup (…)
{
…
shadowedDirLightCount = shadowedOtherLightCount = 0;
useShadowMask = false;
}
灯光是否可以产生阴影区别于其在可见光列表中的位置,次序低的就不保存阴影数据,但若是有烘培阴影也可以保留。我们可以在ReserveOtherShadow
的一开始针对那些没有阴影的灯光返回默认值:
public Vector4 ReserveOtherShadows (Light light, int visibleLightIndex)
{
if (light.shadows == LightShadows.None || light.shadowStrength <= 0f)
{
return new Vector4(0f, 0f, 0f, -1f);
}
float maskChannel = -1f;
//if (light.shadows != LightShadows.None && light.shadowStrength > 0f) {
LightBakingOutput lightBaking = light.bakingOutput;
if (
lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
) {
useShadowMask = true;
maskChannel = lightBaking.occlusionMaskChannel;
}
return new Vector4(
light.shadowStrength, 0f, 0f,
maskChannel
);
//}
//}
//return new Vector4(0f, 0f, 0f, -1f);
}
在最后的返回值前,判断灯光数量是否达到最大值,或者对于该光是否还有阴影可渲染。如果是的话,返回负数阴影强度,这样就可以恰当地使用烘培阴影:
if (
shadowedOtherLightCount >= maxShadowedOtherLightCount ||
!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)
)
{
return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
}
return new Vector4(
light.shadowStrength, shadowedOtherLightCount++, 0f,
maskChannel
);
1.3 两个图集
在ShadowSettings
中,我们为其它光源的阴影创建新的图集:
[System.Serializable]
public struct Other
{
public MapSize atlasSize;
public FilterMode filter;
}
public Other other = new Other
{
atlasSize = MapSize._1024,
filter = FilterMode.PCF2x2
};
添加对应的multi_compile
指令:
#pragma multi_compile _ _OTHER_PCF3 _OTHER_PCF5 _OTHER_PCF7
向Shadows.cs
添加关键字:
static string[] otherFilterKeywords =
{
"_OTHER_PCF3",
"_OTHER_PCF5",
"_OTHER_PCF7",
};
定义阴影图集和矩阵:
static int
dirShadowAtlasId = Shader.PropertyToID("_DirectionalShadowAtlas"),
dirShadowMatricesId = Shader.PropertyToID("_DirectionalShadowMatrices"),
otherShadowAtlasId = Shader.PropertyToID("_OtherShadowAtlas"),
otherShadowMatricesId = Shader.PropertyToID("_OtherShadowMatrices"),
…;
…
static Matrix4x4[]
dirShadowMatrices = new Matrix4x4[maxShadowedDirLightCount * maxCascades],
otherShadowMatrices = new Matrix4x4[maxShadowedOtherLightCount];
我们定义一个4维向量,xy组件存储平行光阴影图集尺寸,zw组件存储其它光阴影尺寸:
Vector4 atlasSizes;
…
public void Render ()
{
…
buffer.SetGlobalVector(shadowAtlasSizeId, atlasSizes);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
void RenderDirectionalShadows ()
{
int atlasSize = (int)settings.directional.atlasSize;
atlasSizes.x = atlasSize;
atlasSizes.y = 1f / atlasSize;
…
//buffer.SetGlobalVector(
// shadowAtlasSizeId, new Vector4(atlasSize, 1f / atlasSize)
//);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
添加新的RenderOtherShadow
方法:
void RenderOtherShadows ()
{
int atlasSize = (int)settings.other.atlasSize;
atlasSizes.z = atlasSize;
atlasSizes.w = 1f / atlasSize;
buffer.GetTemporaryRT(
otherShadowAtlasId, atlasSize, atlasSize,
32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap
);
buffer.SetRenderTarget(
otherShadowAtlasId,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store
);
buffer.ClearRenderTarget(true, false, Color.clear);
buffer.BeginSample(bufferName);
ExecuteBuffer();
int tiles = shadowedOtherLightCount;
int split = tiles <= 1 ? 1 : tiles <= 4 ? 2 : 4;
int tileSize = atlasSize / split;
for (int i = 0; i < shadowedOtherLightCount; i++)
{
}
buffer.SetGlobalMatrixArray(otherShadowMatricesId, otherShadowMatrices);
SetKeywords(
otherFilterKeywords, (int)settings.other.filter - 1
);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
接着在Render
中渲染两种阴影:
public void Render () {
if (shadowedDirLightCount > 0)
{
RenderDirectionalShadows();
}
else
{
buffer.GetTemporaryRT(
dirShadowAtlasId, 1, 1,
32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap
);
}
if (shadowedOtherLightCount > 0)
{
RenderOtherShadows();
}
else
{
buffer.SetGlobalTexture(otherShadowAtlasId, dirShadowAtlasId);
}
…
}
在Cleanup
中移除其它阴影数据:
public void Cleanup ()
{
buffer.ReleaseTemporaryRT(dirShadowAtlasId);
if (shadowedOtherLightCount > 0)
{
buffer.ReleaseTemporaryRT(otherShadowAtlasId);
}
ExecuteBuffer();
}
1.4 渲染聚光阴影
为渲染聚光阴影,我们需要知道可见光索引,梯度缩放偏移量和法线偏移量。我们创建一个ShadowedOtherLight
结构体:
struct ShadowedOtherLight {
public int visibleLightIndex;
public float slopeScaleBias;
public float normalBias;
}
ShadowedOtherLight[] shadowedOtherLights =
new ShadowedOtherLight[maxShadowedOtherLightCount];
在ReserveOtherShadows
中填充:
public Vector4 ReserveOtherShadows (Light light, int visibleLightIndex)
{
…
shadowedOtherLights[shadowedOtherLightCount] = new ShadowedOtherLight {
visibleLightIndex = visibleLightIndex,
slopeScaleBias = light.shadowBias,
normalBias = light.shadowNormalBias
};
return new Vector4(
light.shadowStrength, shadowedOtherLightCount++, 0f,
maskChannel
);
}
我们需要确保我们使用的是恰当的可见光索引,因此修改所有的初始化方法:
void SetupDirectionalLight (
int index, int visibleIndex, ref VisibleLight visibleLight
)
{
…
dirLightShadowData[index] =
shadows.ReserveDirectionalShadows(visibleLight.light, visibleIndex);
}
void SetupPointLight (
int index, int visibleIndex, ref VisibleLight visibleLight
)
{
…
otherLightShadowData[index] =
shadows.ReserveOtherShadows(light, visibleIndex);
}
void SetupSpotLight (
int index, int visibleIndex, ref VisibleLight visibleLight
)
{
…
otherLightShadowData[index] =
shadows.ReserveOtherShadows(light, visibleIndex);
}
修改SetupLights
让其传入正确的可见光索引:
switch (visibleLight.lightType)
{
case LightType.Directional:
if (dirLightCount < maxDirLightCount)
{
SetupDirectionalLight(
dirLightCount++, i, ref visibleLight
);
}
break;
case LightType.Point:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupPointLight(otherLightCount++, i, ref visibleLight);
}
break;
case LightType.Spot:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupSpotLight(otherLightCount++, i, ref visibleLight);
}
break;
}
Shadows.cs
中创建一个RenderSpotShadows
方法,我们使用CullingResults.ComputeSpotShadowMatricesAndCullingPrimitives
来获得view和projection矩阵以及splitData:
void RenderSpotShadows (int index, int split, int tileSize)
{
ShadowedOtherLight light = shadowedOtherLights[index];
var shadowSettings =
new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);
cullingResults.ComputeSpotShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, out Matrix4x4 viewMatrix,
out Matrix4x4 projectionMatrix, out ShadowSplitData splitData
);
shadowSettings.splitData = splitData;
otherShadowMatrices[index] = ConvertToAtlasMatrix(
projectionMatrix * viewMatrix,
SetTileViewport(index, split, tileSize), split
);
buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
ExecuteBuffer();
context.DrawShadows(ref shadowSettings);
buffer.SetGlobalDepthBias(0f, 0f);
}
在RenderOtherShadows
的循环中调用:
for (int i = 0; i < shadowedOtherLightCount; i++)
{
RenderSpotShadows(i, split, tileSize);
}
1.5 无平坠
注意,阴影平坠只对正交阴影投影有效,也就是针对平行光而言,对于聚光灯这种拥有具体位置的光源来说,其后方的阴影投射物就无用了。由于使用透视投影,将顶点紧靠近平面会让阴影严重变形,因此需要在恰当的时候关闭阴影平坠。
使用一个属性:
static int
…
shadowDistanceFadeId = Shader.PropertyToID("_ShadowDistanceFade"),
shadowPancakingId = Shader.PropertyToID("_ShadowPancaking");
RenderDirectionalShadows
中设为1:
buffer.ClearRenderTarget(true, false, Color.clear);
buffer.SetGlobalFloat(shadowPancakingId, 1f);
buffer.BeginSample(bufferName);
RenderOtherShadows
中设为0:
buffer.ClearRenderTarget(true, false, Color.clear);
buffer.SetGlobalFloat(shadowPancakingId, 0f);
buffer.BeginSample(bufferName);
在ShadowCaster
pass中有选择地使用平坠:
bool _ShadowPancaking;
Varyings ShadowCasterPassVertex (Attributes input)
{
…
if (_ShadowPancaking)
{
#if UNITY_REVERSED_Z
output.positionCS.z = min(
output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE
);
#else
output.positionCS.z = max(
output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE
);
#endif
}
output.baseUV = TransformBaseUV(input.baseUV);
return output;
}
1.6 采样聚光阴影
定义宏以及声明变量:
#if defined(_OTHER_PCF3)
#define OTHER_FILTER_SAMPLES 4
#define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_3x3
#elif defined(_OTHER_PCF5)
#define OTHER_FILTER_SAMPLES 9
#define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_5x5
#elif defined(_OTHER_PCF7)
#define OTHER_FILTER_SAMPLES 16
#define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_7x7
#endif
#define MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT 4
#define MAX_SHADOWED_OTHER_LIGHT_COUNT 16
#define MAX_CASCADE_COUNT 4
TEXTURE2D_SHADOW(_DirectionalShadowAtlas);
TEXTURE2D_SHADOW(_OtherShadowAtlas);
#define SHADOW_SAMPLER sampler_linear_clamp_compare
SAMPLER_CMP(SHADOW_SAMPLER);
CBUFFER_START(_CustomShadows)
…
float4x4 _DirectionalShadowMatrices
[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT * MAX_CASCADE_COUNT];
float4x4 _OtherShadowMatrices[MAX_SHADOWED_OTHER_LIGHT_COUNT];
…
CBUFFER_END
定义SampleOtherShadowAtlas
和FilterOtherShadow
方法:
float SampleOtherShadowAtlas (float3 positionSTS)
{
return SAMPLE_TEXTURE2D_SHADOW(
_OtherShadowAtlas, SHADOW_SAMPLER, positionSTS
);
}
float FilterOtherShadow (float3 positionSTS)
{
#if defined(OTHER_FILTER_SETUP)
real weights[OTHER_FILTER_SAMPLES];
real2 positions[OTHER_FILTER_SAMPLES];
float4 size = _ShadowAtlasSize.wwzz;
OTHER_FILTER_SETUP(size, positionSTS.xy, weights, positions);
float shadow = 0;
for (int i = 0; i < OTHER_FILTER_SAMPLES; i++)
{
shadow += weights[i] * SampleOtherShadowAtlas(
float3(positions[i].xy, positionSTS.z)
);
}
return shadow;
#else
return SampleOtherShadowAtlas(positionSTS);
#endif
}
在OtherShadowData
中添加拼贴索引系数:
struct OtherShadowData
{
float strength;
int tileIndex;
int shadowMaskChannel;
};
在GetOtherShadowData
中设置:
OtherShadowData GetOtherShadowData (int lightIndex)
{
OtherShadowData data;
data.strength = _OtherLightShadowData[lightIndex].x;
data.tileIndex = _OtherLightShadowData[lightIndex].y;
data.shadowMaskChannel = _OtherLightShadowData[lightIndex].w;
return data;
}
在GetOtherShadow
中我们获取阴影。由于是透视投影,我们在传入FilterOtherShadow
前将对应坐标进行透视除法:
float GetOtherShadow (
OtherShadowData other, ShadowData global, Surface surfaceWS
)
{
float3 normalBias = surfaceWS.interpolatedNormal * 0.0;
float4 positionSTS = mul(
_OtherShadowMatrices[other.tileIndex],
float4(surfaceWS.position + normalBias, 1.0)
);
return FilterOtherShadow(positionSTS.xyz / positionSTS.w);
}
1.7 法线偏移
聚光阴影也有痤疮效果,但由于使用透视投影,纹素尺寸不是连续的,痤疮也不是连续的,离灯光越远越大。
纹素尺寸从灯光所在平面开始随距离线性增长,该平面将空间分为在灯光前和在灯光后两部分,我们可以计算在距离为1处的纹理尺寸和法线偏移,以此来缩放到合适的尺寸。
由上得阴影拼贴的尺寸为,其中为外角的一半,那么可以得到世界空间的纹素尺寸在距离1处等于2除以投影缩放系数,该系数可由投影矩阵的左上值得到:
float texelSize = 2f / (tileSize * projectionMatrix.m00);
float filterSize = texelSize * ((float)settings.other.filter + 1f);
float bias = light.normalBias * filterSize * 1.4142136f;
otherShadowMatrices[index] = ConvertToAtlasMatrix(
projectionMatrix * viewMatrix,
SetTileViewport(index, split, tileSize), tileScale
);
我们需要将偏移传至GPU,设置拼贴数属性:
static int
…
otherShadowMatricesId = Shader.PropertyToID("_OtherShadowMatrices"),
otherShadowTilesId = Shader.PropertyToID("_OtherShadowTiles"),
…;
static Vector4[]
cascadeCullingSpheres = new Vector4[maxCascades],
cascadeData = new Vector4[maxCascades],
otherShadowTiles = new Vector4[maxShadowedOtherLightCount];
…
void RenderOtherShadows ()
{
…
buffer.SetGlobalMatrixArray(otherShadowMatricesId, otherShadowMatrices);
buffer.SetGlobalVectorArray(otherShadowTilesId, otherShadowTiles);
…
}
创建SetOtherTileData
方法:
void SetOtherTileData (int index, float bias)
{
Vector4 data = Vector4.zero;
data.w = bias;
otherShadowTiles[index] = data;
}
在RenderSpotShadows
中调用:
float bias = light.normalBias * filterSize * 1.4142136f;
SetOtherTileData(index, bias);
shader层面,加入拼贴数据:
CBUFFER_START(_CustomShadows)
…
float4x4 _OtherShadowMatrices[MAX_SHADOWED_OTHER_LIGHT_COUNT];
float4 _OtherShadowTiles[MAX_SHADOWED_OTHER_LIGHT_COUNT];
float4 _ShadowAtlasSize;
float4 _ShadowDistanceFade;
CBUFFER_END
…
float GetOtherShadow (
OtherShadowData other, ShadowData global, Surface surfaceWS
)
{
float4 tileData = _OtherShadowTiles[other.tileIndex];
float3 normalBias = surfaceWS.interpolatedNormal * tileData.w;
…
}
为缩放法线偏移,我们需要世界空间灯光位置和聚光方向:
struct OtherShadowData
{
float strength;
int tileIndex;
int shadowMaskChannel;
float3 lightPositionWS;
float3 spotDirectionWS;
};
在GetOtherLight
获取:
OtherShadowData GetOtherShadowData (int lightIndex)
{
…
data.lightPositionWS = 0.0;
data.spotDirectionWS = 0.0;
return data;
}
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
Light light;
light.color = _OtherLightColors[index].rgb;
float3 position = _OtherLightPositions[index].xyz;
float3 ray = position - surfaceWS.position;
…
float3 spotDirection = _OtherLightDirections[index].xyz;
float spotAttenuation = Square(
saturate(dot(spotDirection, light.direction) *
spotAngles.x + spotAngles.y)
);
OtherShadowData otherShadowData = GetOtherShadowData(index);
otherShadowData.lightPositionWS = position;
otherShadowData.spotDirectionWS = spotDirection;
…
}
计算到灯光平面的距离:
float4 tileData = _OtherShadowTiles[other.tileIndex];
float3 surfaceToLight = other.lightPositionWS - surfaceWS.position;
float distanceToLightPlane = dot(surfaceToLight, other.spotDirectionWS);
float3 normalBias =
surfaceWS.interpolatedNormal * (distanceToLightPlane * tileData.w);
1.8 限制采样
使用法线偏移的话会在拼贴范围外采样。我们可以手动将采样限制在拼贴边界内,不过可能会拉伸边缘的阴影。
修改SetOtherTileData
方法,让其计算和存储拼贴边界,我们存储拼贴最小的纹理坐标作为偏移的基准,并存储缩放。注意将边界缩小一点,确保采样不会超出边界:
void SetOtherTileData (int index, Vector2 offset, float scale, float bias)
{
float border = atlasSizes.w * 0.5f;
Vector4 data;
data.x = offset.x * scale + border;
data.y = offset.y * scale + border;
data.z = scale - border - border;
data.w = bias;
otherShadowTiles[index] = data;
}
在RenderSpotShadows
中调用:
Vector2 offset = SetTileViewport(index, split, tileSize);
SetOtherTileData(index, offset, 1f / split, bias);
otherShadowMatrices[index] = ConvertToAtlasMatrix(
projectionMatrix * viewMatrix, offset, split
);
shader中,在SampleOtherShadowAtlas
中,我们使用边界限制范围:
float SampleOtherShadowAtlas (float3 positionSTS, float3 bounds)
{
positionSTS.xy = clamp(positionSTS.xy, bounds.xy, bounds.xy + bounds.z);
return SAMPLE_TEXTURE2D_SHADOW(
_OtherShadowAtlas, SHADOW_SAMPLER, positionSTS
);
}
2 点光阴影
2.1 6个拼贴
首先,我们要确保是否是渲染点光阴影:
struct ShadowedOtherLight
{
…
public bool isPoint;
}
在ReserveOtherShadows
中,判断我们是否有点光,如果是,数量加6:
public Vector4 ReserveOtherShadows (Light light, int visibleLightIndex)
{
…
bool isPoint = light.type == LightType.Point;
int newLightCount = shadowedOtherLightCount + (isPoint ? 6 : 1);
if (
newLightCount > maxShadowedOtherLightCount ||
!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)
)
{
return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
}
shadowedOtherLights[shadowedOtherLightCount] = new ShadowedOtherLight {
visibleLightIndex = visibleLightIndex,
slopeScaleBias = light.shadowBias,
normalBias = light.shadowNormalBias,
isPoint = isPoint
};
Vector4 data = new Vector4(
light.shadowStrength, shadowedOtherLightCount,
isPoint ? 1f : 0f, maskChannel
);
shadowedOtherLightCount = newLightCount;
return data;
}
2.2 渲染点光阴影
修改RenderOtherShadows
,循环中先调用新的RenderPointShadows
:
for (int i = 0; i < shadowedOtherLightCount;)
{
if (shadowedOtherLights[i].isPoint) {
RenderPointShadows(i, split, tileSize);
i += 6;
}
else {
RenderSpotShadows(i, split, tileSize);
i += 1;
}
}
建立RenderPointShadows
方法,每个灯光要渲染六次。CubemapFace
包含面索引:
void RenderPointShadows (int index, int split, int tileSize)
{
ShadowedOtherLight light = shadowedOtherLights[index];
var shadowSettings =
new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);
for (int i = 0; i < 6; i++)
{
cullingResults.ComputePointShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, (CubemapFace)i, 0f,
out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,
out ShadowSplitData splitData
);
shadowSettings.splitData = splitData;
int tileIndex = index + i;
float texelSize = 2f / (tileSize * projectionMatrix.m00);
float filterSize = texelSize * ((float)settings.other.filter + 1f);
float bias = light.normalBias * filterSize * 1.4142136f;
Vector2 offset = SetTileViewport(tileIndex, split, tileSize);
float tileScale = 1f / split;
SetOtherTileData(tileIndex, offset, tileScale, bias);
otherShadowMatrices[tileIndex] = ConvertToAtlasMatrix(
projectionMatrix * viewMatrix, offset, tileScale
);
buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
ExecuteBuffer();
context.DrawShadows(ref shadowSettings);
buffer.SetGlobalDepthBias(0f, 0f);
}
}
一个立方体面的视角永远是90°,那么在距离为1处的世界空间拼贴大小就永远是2,也就只用计算依次:
float texelSize = 2f / tileSize;
float filterSize = texelSize * ((float)settings.other.filter + 1f);
float bias = light.normalBias * filterSize * 1.4142136f;
float tileScale = 1f / split;
for (int i = 0; i < 6; i++)
{
…
//float texelSize = 2f / (tileSize * projectionMatrix.m00);
//float filterSize = texelSize * ((float)settings.other.filter + 1f);
//float bias = light.normalBias * filterSize * 1.4142136f;
Vector2 offset = SetTileViewport(tileIndex, split, tileSize);
//float tileScale = 1f / split;
…
}
2.3 采样点阴影
采样立方体纹理,我们需要一个光到面的方向:
struct OtherShadowData
{
float strength;
int tileIndex;
bool isPoint;
int shadowMaskChannel;
float3 lightPositionWS;
float3 lightDirectionWS;
float3 spotDirectionWS;
};
在GetOtherShadowData
和GetOtherLight
中设置:
OtherShadowData GetOtherShadowData (int lightIndex)
{
…
data.isPoint = _OtherLightShadowData[lightIndex].z == 1.0;
data.lightPositionWS = 0.0;
data.lightDirectionWS = 0.0;
data.spotDirectionWS = 0.0;
return data;
}
Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
…
otherShadowData.lightPositionWS = position;
otherShadowData.lightDirectionWS = light.direction;
otherShadowData.spotDirectionWS = spotDirection;
…
}
在GetOtherShadow
中,若是点光,使用CubeMapFaceID
来获得面偏移:
float GetOtherShadow (
OtherShadowData other, ShadowData global, Surface surfaceWS
)
{
float tileIndex = other.tileIndex;
float3 lightPlane = other.spotDirectionWS;
if (other.isPoint) {
float faceOffset = CubeMapFaceID(-other.lightDirectionWS);
tileIndex += faceOffset;
}
…
}
我们需要一个匹配面朝向的灯光平面,创建一个对应的朝向数组:
static const float3 pointShadowPlanes[6] =
{
float3(-1.0, 0.0, 0.0),
float3(1.0, 0.0, 0.0),
float3(0.0, -1.0, 0.0),
float3(0.0, 1.0, 0.0),
float3(0.0, 0.0, -1.0),
float3(0.0, 0.0, 1.0)
};
float GetOtherShadow (
OtherShadowData other, ShadowData global, Surface surfaceWS
)
{
float tileIndex = other.tileIndex;
float3 plane = other.spotDirectionWS;
if (other.isPoint)
{
float faceOffset = CubeMapFaceID(-other.lightDirectionWS);
tileIndex += faceOffset;
lightPlane = pointShadowPlanes[faceOffset];
}
…
}
2.4 绘制正确的面
此时会发现点阴影绘制不正确,这是因为Unity针对点阴影,会颠倒顺序绘制,即反转三角形的顶点绘制顺序,即从点光观察的反面会被绘制,这可以有效避免阴影痤疮问题,但会造成物体和阴影间的空白问题。我们可以将view矩阵的第二行反转来还原:
cullingResults.ComputePointShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, (CubemapFace)i, fovBias*0,
out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,
out ShadowSplitData splitData
);
viewMatrix.m11 = -viewMatrix.m11;
viewMatrix.m12 = -viewMatrix.m12;
viewMatrix.m13 = -viewMatrix.m13;
2.5 视野偏移
立方体纹理的面之间的连接往往不是连续的,点阴影会因此出现问题。我们可以提升FOV来改善这一问题,即不采样超出拼贴边的范围
float fovBias =
Mathf.Atan(1f + bias + filterSize) * Mathf.Rad2Deg * 2f - 90f;
for (int i = 0; i < 6; i++)
{
cullingResults.ComputePointShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, (CubemapFace)i, fovBias,
out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,
out ShadowSplitData splitData
);
…
}
该方法并不完美,因为也会提升纹素大小,因此滤波范围和发现偏移也必须提升,但问题又会回去了。