截图展示的是:近处实时阴影,远处烘焙阴影

1 Baking Shadows

阴影让场景更具层次感和真实感,但是实时阴影渲染距离有限,超出阴影距离的世界由于没有阴影显得很“平”.烘焙的阴影不会受限于阴影距离,可以与实时阴影结合解决该问题:

  • 最大阴影距离之内使用实时阴影
  • 最大阴影距离之外用烘焙阴影

1.1 生成烘焙阴影

配置生成 shadow mask

  • Lighting/Mixed Lighting/Lighting Mode: 选择 ShadowMask

  • Project Settings/Quality/Shadows/ShadowMaskMode: 选择 DistanceShadowMask

  • 确保光源的 Mode 是 Mixed,只有该模式才会生成 shadow mask

Lighting/Baked Lightmaps 下的预览,不会显示 shadow mask,但是通过下方的概要信息,可以看到生成的 light map 是带有 shadow mask 的.

通过点击LightingData,在 Asset 窗口定位,可以看到该目录下有对应的 shadow mask 贴图.

Shadow Mask 将 shadow attenuation 存储到 G 通道中,所以看起来是红色的

lightmap

shadow mask map

1.2 判断是否启用 shadow mask

我们需要增加一个 shader keyword 来支持 shadow mask,因此在渲染时,需要知道场景渲染是否需要使用 shadow mask,以此为根据设置 keywords.当场景中存在任意一个光源,渲染了 shadow mask 时,就启用keywords.

  • 在 Shadow.cs 中定义 shader keyword: _SHADOW_MASK_DISTANCE 
  • 并定义 一个 bool 变量,跟踪该状态
  • 如果如果检测到使用了 shadow mask 则设置 keyword
    • 每个光源的 LightMap 信息存储在 LightBakingOutput 中,如果
      • lightMapBakeType = Mixed 并且
      • mixedLightingMode = ShadowMask 则表示要渲染 shadow mask,设置 bool 跟踪状态
// 烘焙阴影的 keywords
private static string[] shadowMaskKeywords = { "_SHADOW_MASK_DISTANCE" };// 跟踪是否使用 shadow mask
private bool useShadowMask = false;public void Setup(ScriptableRenderContext context, CullingResults cullingResults, ShadowSettings settings)
{...// 每次初始化,不使用烘焙阴影useShadowMask = false;
}/// <summary>
/// 缓存下需要绘制阴影的方向光
/// </summary>
/// <param name="light"></param>
/// <param name="visibleLightIndex"></param>
/// <returns></returns>
public Vector3 ReserveDirectionalShadows(Light light, int visibleLightIndex)
{...// 判断是否需要使用 shadow maskif (light.bakingOutput.lightmapBakeType == LightmapBakeType.Mixed &&light.bakingOutput.mixedLightingMode == MixedLightingMode.Shadowmask)useShadowMask = true;return new Vector3(light.shadowStrength,settings.directional.cascadeCount * directionalLightCount++,light.shadowNormalBias);}return Vector3.zero;
}public void Render()
{...// 不论是否渲染实时阴影,都要根据跟踪的状态启用 keyword,因此烘焙阴影不是实时的buffer.BeginSample(bufferName);SetKeywords(shadowMaskKeywords, useShadowMask ? 1 : 0);buffer.EndSample(bufferName);ExecuteBuffer();
}

在 shadow.hlsl 中定义相应的 shader keyword

#pragma multi_compile _ _SHADOW_MASK_DISTANCE
#pragma multi_compile _ LIGHTMAP_ON

1.3 采样 Shadow Mask

shadow mask 数据与 lightmap 一样采样得到,但是它毕竟是 shadow 数据,为了逻辑上的一致性,在 shadows.hlsl 中定义结构体 ShadowMask 来存储数据,并将其添加到 ShadowData 中,并在返回时进行初始化

struct ShadowMask
{bool distance;float4 shadows;
};struct ShadowData
{int cascadeIndex; // 级联索引,0-3float cascadeBlend; // 级联混合,用于在级联之间平滑过渡float strength; // 阴影强度,0表示没有阴影,1表示完全有阴影ShadowMask shadowMask;
};ShadowData GetShadowData(Surface surfaceWS)
{ShadowData shadowData;// 初始化 shadow mask 数据shadowData.shadowMask.distance = false;shadowData.shadowMask.shadows = 1.0;...
}

真正采样 shadow mask 上 GI 来完成的,因此在 GI.hlsl 中的 GI 结构体中,也增加 ShadowMask 成员.

unity 定义了 unity_ShadowMask 来提供采样贴图,我们只需要声明贴图及对应的采样器,然后定义一个采样函数来完成采样.

TEXTURE2D(unity_ShadowMask);
SAMPLER(samplerunity_ShadowMask);struct GI
{float3 diffuse;ShadowMask shadowMask;
};// 采样 shadow mask
float3 SampleBakedShadow(float2 uv)
{// 只有定义了 lightmap, unity_ShadowMask 才是可用的
#if defined(LIGHTMAP_ON)return SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, uv);// 否则直接返回 1
#elsereturn 1.0;
#endif
}GI GetGI (float2 lightMapUV, Surface surfaceWS) 
{GI gi;gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);// 启用了 shadow mask distance 才采样
#if defined(_SHADOW_MASK_DISTANCE)gi.shadowMask.distance = true;gi.shadowMask.shadows = SampleBakedShadow(lightMapUV);
#elsegi.shadowMask.shadows = 1.0;gi.shadowMask.distance = false;
#endifreturn gi;
}

在 Lighting.hlsl 中,将 GI 中的 shadow mask 数据传递给 shadow data,以供后续的光照计算使用.

这里我们先直接返回 shadow mask 的颜色,在场景里看数据

float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{ShadowData shadowData = GetShadowData(surfaceWS);shadowData.shadowMask = gi.shadowMask;// 临时返回以查看数据return gi.shadowMask.shadows.rgb;
}

同 lightmap, light probe 等 GI 数据一样,需要在 DrawingSettings 中配置,通知 unity 准备并向 GPU 提交这些数据.在 CameraRenderer.cs 中:

void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing)
{// 渲染不透明物体var sortingSettings = new SortingSettings(camera){ criteria = SortingCriteria.CommonOpaque };var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings){ enableDynamicBatching = useDynamicBatching, enableInstancing = useGPUInstancing};// 索引是 1,因为索引为 0 的通过构造函数将 unlitShaderTagId 设置了drawingSettings.SetShaderPassName(1, litShaderTagId);drawingSettings.perObjectData = PerObjectData.Lightmaps | PerObjectData.LightProbe| PerObjectData.LightProbeProxyVolume| PerObjectData.ShadowMask;...

可以看到效果:

1.4 处理 Occlusion probes

unity 中动态物体 ShadowMask 需要用到 Light Probe.通过变量 unity_ProbesOcclusion 提供

  • 在 UnityInput.hlsl 中定义常量
    real4 unity_WorldTransformParams;
    float4 unity_ProbesOcclusion;   // 动态物体 shadow mask 数据
  • 如果对象没有用 lightmap,则直接返回 unity_ProbesOcclusion. 该数据在 shadow mask 中,其中红色通道中是 shadow,其它通道是 occlusion probe 数据
    // 采样 shadow mask
    float3 SampleBakedShadow(float2 uv)
    {// 只有定义了 lightmap, unity_ShadowMask 才是可用的
    #if defined(LIGHTMAP_ON)return SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, uv);// 否则返回 occlusion probe 数据
    #elsereturn unity_ProbesOcclusion;
    #endif
    }
  • 注意,unity_ProbesOcclusion 会导致 UnityInstancing 被打断,为了支持 instance,需要在 common.hlsl 中定义 SHADOWS_SHADOWMASK.需要在包含 UnityInstancing.hlsl 之前
    #if defined(_SHADOW_MASK_DISTANCE)#define SHADOWS_SHADOWMASK
    #endif#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
  • cs 代码中,DrawingSettings.PerObjectData 配置 OcclusionProbes
    drawingSettings.perObjectData = PerObjectData.Lightmaps | PerObjectData.LightProbe| PerObjectData.LightProbeProxyVolume| PerObjectData.ShadowMask| PerObjectData.OcclusionProbe;

    效果如图:阴影内的动态物体是青色,光照到的物体时白色

1.5 LPPVs

LPPV 也支持 shadow mask

  • DrawingSettings.PerObjectData 配置 OcclusionProbeProxyVolume
    drawingSettings.perObjectData = PerObjectData.Lightmaps | PerObjectData.LightProbe| PerObjectData.LightProbeProxyVolume| PerObjectData.ShadowMask| PerObjectData.OcclusionProbe| PerObjectData.OcclusionProbeProxyVolume;;
  • 跟lightmap 一样,当 unity_ProbeVolumeParams.x != 0 时,表示需要采样 shadow mask,用接口 SampleProbeOcclusion 进行采样,参数与 SampleProbeVolumeSH4 一样,只是不需要法线.
// 采样 shadow mask
float3 SampleBakedShadow(float2 uv, Surface surfaceWS)
{// 只有定义了 lightmap, unity_ShadowMask 才是可用的
#if defined(LIGHTMAP_ON)return SAMPLE_TEXTURE2D(unity_ShadowMask, samplerunity_ShadowMask, uv);
#else// unity_ProbeVolumeParams.x !=0 表示用 LPPVif(unity_ProbeVolumeParams.x)return SampleProbeOcclusion(TEXTURE3D_ARGS(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH),surfaceWS.position, unity_ProbeVolumeWorldToObject,unity_ProbeVolumeParams.y, unity_ProbeVolumeParams.z,unity_ProbeVolumeMin.xyz, unity_ProbeVolumeSizeInv.xyz);// 否则返回 occlusion probe 数据else return unity_ProbesOcclusion;
#endif
}
  • 修改调用 GetGI 的代码,传入 surface 参数
#if defined(_SHADOW_MASK_DISTANCE)gi.shadowMask.distance = true;gi.shadowMask.shadows = float4(SampleBakedShadow(lightMapUV, surfaceWS), 1.0);
#elsegi.shadowMask.shadows = 1.0;gi.shadowMask.distance = false;
#endif

如下图,车库中的长条对象,在阴影中的部分渲染成了青色

最后,移除 lighting.hlsl 中 GetLighting 函数中返回 gi shadow 的调试代码.

1.6 Mesh Balls

DrawInstanced ,在CPU中计算每个对象的 Occlusion Probe 数据,并拷贝到 MaterialPropertyBlock 完成提交.

// 生成 interpolated light probes
var lightProbes = new SphericalHarmonicsL2[1023];
var occlusionProbes = new Vector4[1023];
// 参数1: 位置熟知
// 参数2: 返回 SphericalHarmonicsL2 数据
// 参数3: 返回 Occlusion Probe 数据
LightProbes.CalculateInterpolatedLightAndOcclusionProbes(positions, lightProbes, occlusionProbes);
// 将数据拷贝到 material property block
matPropBlock.CopySHCoefficientArraysFrom(lightProbes); 
matPropBlock.CopyProbeOcclusionArrayFrom(occlusionProbes);

2 Mixing Shadows

这一步,我们要混合 realtime 和 baked shadow,超出实时阴影距离的,用烘焙阴影.

2.1 混合

在 shadow.hlsl 中,首先将计算级联阴影的代码从 GetDirShadowAttenuation 中分离出来,形式单独的 GetCascadedShadow 函数

然后定义获取烘焙阴影的函数,如果启用了 distance shandow,返回 ShadowMask.shadows.r

定义MixBakedAndRealtimeShadow函数混合阴影


// 获取级联阴影
float GetCascadedShadow(DirShadowData shadowData, ShadowData global, Surface surfaceWS)
{// 根据像素法线和图素对角线长度,计算偏移float3 normalBias = surfaceWS.normal * shadowData.normalBias * _CascadeData[global.cascadeIndex].y;// 计算采样阴影图的 UVfloat3 positionSTS = mul(_DirShadowMatrices[shadowData.tileIndex], float4(surfaceWS.position+normalBias,1.0)).xyz;// 采样阴影图float shadow = FilterDirectionalShadow(positionSTS);// 如果有级联混合,则需要跟下一级级联进行混合if (global.cascadeBlend < 1.0f){normalBias = surfaceWS.normal * shadowData.normalBias * _CascadeData[global.cascadeIndex + 1].y;positionSTS = mul(_DirShadowMatrices[shadowData.tileIndex + 1], float4(surfaceWS.position + normalBias, 1.0)).xyz;float nextShadow = FilterDirectionalShadow(positionSTS);shadow = lerp(shadow, nextShadow, global.cascadeBlend);}return shadow;
}// 获取 shadow mask 阴影衰减
float GetBakedShadow(ShadowMask shadowMask)
{float shadow = 1.0f;// 启用了 distance shadow mask,返回 r 通道中的阴影if(shadowMask.distance)return shadowMask.shadows.r;return shadow;
}// 混合烘焙和实时阴影
// global 全局阴影参数
// shadow 实时阴影
// strength 光源的阴影强度
float MixBakedAndRealtimeShadow(ShadowData global, float shadow, float strength)
{float baked = GetBakedShadow(global.shadowMask);// shadow mask 有效if(global.shadowMask.distance){// 混合实时和烘焙阴影shadow = lerp(baked, shadow, global.strength);// 应用光源阴影强度进行插值return lerp(1.0f, shadow, strength);}// 直接基于实时阴影和光源阴影强度进行插值else{return lerp(1.0f, shadow, strength*global.strength);}
}// 计算方向光阴影衰减
// dirShadowData 方向光阴影参数
// global 全局阴影参数
// surfaceWS 世界空间表面/像素
float GetDirShadowAttenuation(DirShadowData dirShadowData, ShadowData global, Surface surfaceWS)
{// 材质不接收阴影
#if !defined(_RECEIVE_SHADOWS)return 1.0f;
#endif// 方向光阴影强度为 0if(dirShadowData.strength <= 0.0f)return 1.0f;  else{// 计算采样实时阴影float shadow = GetCascadedShadow(dirShadowData, global, surfaceWS);// 混合实时阴影和烘焙阴影shadow = MixBakedAndRealtimeShadow(global, shadow, dirShadowData.strength);return shadow; }
}

之前在 Light.hlsl 中的 GetDirectionalLightShadowData 函数中,已经应用了最大阴影距离强度来进行淡出,现在是在 MixBakedAndRealtimeShadow 中完成的淡出,因此之前的要删掉

data.strength =_DirectionalLightShadowData[lightIndex].x; // * shadowData.strength;

2.2 仅烘焙阴影

现在,如果将摄像机拉的足够远,会发现实时阴影和烘焙阴影都消失了,这是因为在管线中,收集投影的光源时,如果光源的 cascade 没有裁剪到任何对象,光源就被裁掉了.

要解决该问题,需要修改光源裁减算法,改为只要该光源渲染了 shadow mask,那么即使光源阴影投射范围内没有对象,也收集该光源

同时需要在 shader 中区别出该情况,并仅使用烘焙阴影

在 shadow.hlsl 中

// 当光源完全没有投射(渲染) shadow map 时,仅使用 shadow mask 阴影
float GetBakedShadow(ShadowMask shadowMask, float strength)
{// distance shadow mask 有效if(shadowMask.distance)return lerp(1.0f, shadowMask.shadows.r, strength);return 1.0f;
}// 计算方向光阴影衰减
// dirShadowData 方向光阴影参数
// global 全局阴影参数
// surfaceWS 世界空间表面/像素
float GetDirShadowAttenuation(DirShadowData dirShadowData, ShadowData global, Surface surfaceWS)
{// 材质不接收阴影
#if !defined(_RECEIVE_SHADOWS)return 1.0f;
#endif// 方向光阴影强度为 0,不投射阴影,返回1// 管线中,收集投影方向光时,已经判断了如果强度为0则忽略,因此这里的判断不是很有必要if(dirShadowData.strength == 0.0f){return 1.0f;  }// 我们在管线中,如果方向光没有实时阴影,仅有烘焙阴影,则设置强度为负数// 这里识别并用绝对值获得烘焙阴影,不在采样级联阴影else if(dirShadowData.strength * global.strength <= 0.0f){return GetBakedShadow(global.shadowMask, abs(dirShadowData.strength));}else{// 计算采样实时阴影float shadow = GetCascadedShadow(dirShadowData, global, surfaceWS);// 混合实时阴影和烘焙阴影shadow = MixBakedAndRealtimeShadow(global, shadow, dirShadowData.strength);return shadow; }
}

在 shadow.cs 中

public Vector3 ReserveDirectionalShadows(Light light, int visibleLightIndex)
{if (directionalLightCount < maxdirectionalLightCount &&light.shadows != LightShadows.None &&light.shadowStrength > 0f){directionalLights[directionalLightCount] = new ShadowedDirectionalLight(){visibleLightIndex = visibleLightIndex,slopeScaleBias = light.shadowBias,nearPlaneOffset = light.shadowNearPlane};// 判断是否需要使用 shadow maskif (light.bakingOutput.lightmapBakeType == LightmapBakeType.Mixed &&light.bakingOutput.mixedLightingMode == MixedLightingMode.Shadowmask)useShadowMask = true;// 如果该光源投影范围内没有任何对象,则仅使用烘焙阴影.// 我们在返回值的 x 分量用负数的阴影强度,shader 中识别后,则仅使用烘焙阴影if (!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)){return new Vector3(-light.shadowStrength, 0f, 0f);}return new Vector3(light.shadowStrength,settings.directional.cascadeCount * directionalLightCount++,light.shadowNormalBias);}return Vector3.zero;
}

2.3 总是使用 shadow mask

shadow mask 是离线烘焙的,运行时不需要再进行阴影的渲染,因此性能提升明显.所以有些情况下,我们希望对于静态对象,完全抛弃实时阴影,仅使用 shadow mask.

  • 在 ProjectSettings/Quality/Shadows/Shadowmask Mode 配置,改为 Shadowmask 启用该特性
  • 需要加入新的 shader keyword :_SHADOW_MASK_ALWAYS,
// shadow.cs 中
private static string[] shadowMaskKeywords = { "_SHADOW_MASK_ALWAYS", "_SHADOW_MASK_DISTANCE" };// Lit.shader 中
#pragma multi_compile _ _SHADOW_MASK_ALWAYS _SHADOW_MASK_DISTANCE// common.hlsl 中,增加 always 模式对 instancing 的支持
// 只有定义了 SHADOWS_SHADOWMASK unity 才会对 shadow mask 材质兼容 instancing
#if defined(_SHADOW_MASK_DISTANCE) || defined(_SHADOW_MASK_ALWAYS)#define SHADOWS_SHADOWMASK
#endif
  • 渲染管线中,根据是否启用 shadow mask 以及 QualitySetting.shadowmaskMode 来设置 keyword
public void Render()
{...// 不论是否渲染实时阴影,都要根据跟踪的状态启用 keyword,因此烘焙阴影不是实时的buffer.BeginSample(bufferName);// 确定 shadow mask keywordint shadowMaskKeywordIndex = 0;if (useShadowMask){// 如果配置了 shadow maskif (QualitySettings.shadowmaskMode == ShadowmaskMode.Shadowmask)shadowMaskKeywordIndex = 1;// 否则就是 distance shadow maskelseshadowMaskKeywordIndex = 2;}SetKeywords(shadowMaskKeywords, shadowMaskKeywordIndex);buffer.EndSample(bufferName);ExecuteBuffer();
}
  • shadow.hlsl 中,为ShadowMask 数据增加 always 标记,在 IG.hlsl 中的 GetGI 函数中,根据 keyword 为其赋值
GI GetGI (float2 lightMapUV, Surface surfaceWS) 
{GI gi;gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);// 启用了 shadow mask distance 才采样
#if defined(_SHADOW_MASK_DISTANCE)gi.shadowMask.distance = true;gi.shadowMask.always = false;gi.shadowMask.shadows = float4(SampleBakedShadow(lightMapUV, surfaceWS), 1.0);
#else ifdefined(_SHADOW_MASK_ALWAYS)gi.shadowMask.distance = false;gi.shadowMask.always = true;gi.shadowMask.shadows = float4(SampleBakedShadow(lightMapUV, surfaceWS), 1.0);
#elsegi.shadowMask.shadows = 1.0;gi.shadowMask.distance = false;gi.shadowMask.always = false;
#endifreturn gi;
}
  • 混合实时/烘焙阴影时,处理 always 的情况
// 获取 shadow mask 阴影衰减
float GetBakedShadow(ShadowMask shadowMask)
{float shadow = 1.0f;// 启用了 distance shadow mask,返回 r 通道中的阴影if(shadowMask.distance || shadowMask.always)return shadowMask.shadows.r;return shadow;
}// 当光源完全没有投射(渲染) shadow map 时,仅使用 shadow mask 阴影
float GetBakedShadow(ShadowMask shadowMask, float strength)
{// distance shadow mask 有效if(shadowMask.distance || shadowMask.always)return lerp(1.0f, shadowMask.shadows.r, strength);return 1.0f;
}// 混合烘焙和实时阴影
// global 全局阴影参数
// shadow 实时阴影
// strength 光源的阴影强度
float MixBakedAndRealtimeShadow(ShadowData global, float shadow, float strength)
{float baked = GetBakedShadow(global.shadowMask);// distance shadow mask 有效if(global.shadowMask.distance){// 混合实时和烘焙阴影shadow = lerp(baked, shadow, global.strength);// 应用光源阴影强度进行插值return lerp(1.0f, shadow, strength);}// always shadow maskelse if(global.shadowMask.always){// 根据最大阴影距离衰减阴影shadow = lerp(1.0f, shadow, global.strength);// 使用实时阴影和烘焙阴影中较小的值shadow = min(shadow, baked);// 应用光源阴影强度进行插值return lerp(1.0f, shadow, strength);}// 直接基于实时阴影和光源阴影强度进行插值else{return lerp(1.0f, shadow, strength*global.strength);}
}

3 支持多个光源

shadow mask map 有4个通道,因此最多支持4个 mixed lights.Unity 会根据“重要程度”对场景中的光源进行排序,并选取前4个分别烘焙到 shadow mask 的 r g b a 通道中.超过4个光源,会被当作 full baked 模式来进行处理.目前我们仅支持方向光.

增加一个方向光,并调整其强度比另一个方向光小一点,Unity 会为我们正确烘焙多光源 shadow mask.红色表示首第一个光源影响,绿色表示受第二个光源影响,黄色表示同时受到两个光源的影响.

前面的代码中值使用了 r 通道,第二个光源要使用 g 通道,这要求我们提供光源在 shadow mask map 中的索引,基于该索引选择通道.我们将利用方向光阴影数据的 w 通道来向 shader 提交数据

  • 在Light.cs 中,获取通道索引
public Vector4 ReserveDirectionalShadows(Light light, int visibleLightIndex)
{if (directionalLightCount < maxdirectionalLightCount &&light.shadows != LightShadows.None &&light.shadowStrength > 0f){directionalLights[directionalLightCount] = new ShadowedDirectionalLight(){visibleLightIndex = visibleLightIndex,slopeScaleBias = light.shadowBias,nearPlaneOffset = light.shadowNearPlane};float shadowMaskChannel = -1.0f;// 判断是否需要使用 shadow maskif (light.bakingOutput.lightmapBakeType == LightmapBakeType.Mixed &&light.bakingOutput.mixedLightingMode == MixedLightingMode.Shadowmask){useShadowMask = true;shadowMaskChannel = light.bakingOutput.occlusionMaskChannel;}// 如果该光源投影范围内没有任何对象,则仅使用烘焙阴影.// 我们在返回值的 x 分量用负数的阴影强度,shader 中识别后,则仅使用烘焙阴影if (!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)){return new Vector4(-light.shadowStrength, 0f, 0f, shadowMaskChannel);}return new Vector4(light.shadowStrength,settings.directional.cascadeCount * directionalLightCount++,light.shadowNormalBias, shadowMaskChannel);}return Vector4.zero;
}
  • 在shadow.hlsl 中,为方向光阴影数据增加 通道索引成员
struct DirShadowData
{float strength;       // 该光源的阴影强度int tileIndex;        // 在 cascaded shadow map 中的初始 tile 的索引float normalBias;      // 法线偏移,用于阴影偏移int shadowMaskChannel; // 在 shadow mask map 中的通道索引
}; 
  • 在 Light.hlsl 中,填充该数据
DirShadowData GetDirectionalLightShadowData(int index, ShadowData shadowData)
{DirShadowData dirShadowData;// 应用级联阴影强度默认为1,像素超出级联范围时强度为0dirShadowData.strength = _DirLightShadowData[index].x;// y 是该光源的第一个 tile 的索引, + shadowData.cascadeIndex 表示该光源的第几个级联阴影 tiledirShadowData.tileIndex = _DirLightShadowData[index].y + shadowData.cascadeIndex;dirShadowData.normalBias = _DirLightShadowData[index].z; // 法线偏移dirShadowData.shadowMaskChannel = s_DirLightShadowData[index].w;return dirShadowData;
}
  • shadow.hlsl 中,获取 shadow mask 时,根据索引返回对应通道数据
// 获取 shadow mask 阴影衰减
float GetBakedShadow(ShadowMask shadowMask, int channel)
{float shadow = 1.0f;// 启用了 distance shadow mask,返回 r 通道中的阴影if(shadowMask.distance || shadowMask.always){if(channel >= 0 && channel < 4)return shadowMask.shadows[channel];}return shadow;
}// 当光源完全没有投射(渲染) shadow map 时,仅使用 shadow mask 阴影
float GetBakedShadow(ShadowMask shadowMask, float strength, int channel)
{// distance shadow mask 有效if(shadowMask.distance || shadowMask.always){if(channel >= 0 && channel < 4)return lerp(1.0f, shadowMask.shadows[channel], strength);}return 1.0f;
}
  • 最后,正确调用 GetBakedShadow

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/94801.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/94801.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/94801.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python爬虫实战:研究spidermonkey库,构建电商网站数据采集和分析系统

1 引言 1.1 研究背景 互联网数据已成为商业决策、学术研究的核心资源,网络爬虫作为数据获取的主要工具,在静态网页时代发挥了重要作用。然而,随着 AJAX、React、Vue 等技术的广泛应用,超过 70% 的主流网站采用 JavaScript 动态生成内容(如商品列表滚动加载、评论分页加载…

智能驾驶规划技术总结

前言 本文主要对智能驾驶规划技术相关知识进行初步探究和总结&#xff0c;以加深理解&#xff0c;及方便后续学习过程中查漏补缺。 分层规划策略 寻径 A*算法 概念 节点&#xff1a;网格化后的每一个最小单元父节点&#xff1a;路径规划中用于回溯的节点列表&#xff1a;需要不…

05 网络信息内容安全--对抗攻击技术

1 课程内容 网络信息内容获取技术网络信息内容预处理技术网络信息内容过滤技术社会网络分析技术异常流量检测技术对抗攻击技术 2 对抗攻击概述 2.1 对抗攻击到底是啥&#xff1f; 咱们先举个生活例子&#xff1a; 你平时看苹果能认出来 —— 红颜色、圆溜溜、带个小揪揪。但如果…

【FPGA】VGA显示-贪吃蛇

这个项目实现了一个完整的贪吃蛇游戏&#xff0c;使用Verilog HDL在FPGA上构建。项目包含了VGA显示控制、按键消抖处理、游戏逻辑和图形渲染等多个模块&#xff0c;展示了数字逻辑设计的综合应用。 项目概述 该设计使用硬件描述语言实现了经典贪吃蛇游戏的所有核心功能&#…

从PostgreSQL到人大金仓(KingBase)数据库迁移实战:Spring Boot项目完整迁移指南

&#x1f4d6; 前言 在国产化浪潮的推动下&#xff0c;越来越多的企业开始将数据库从国外产品迁移到国产数据库。本文将以一个真实的Spring Boot项目为例&#xff0c;详细介绍从PostgreSQL迁移到人大金仓&#xff08;KingBase&#xff09;数据库的完整过程&#xff0c;包括遇到…

Docker 入门指南:从基础概念到常见命令及高级工具详解

Docker 入门指南&#xff1a;从基础概念到常见命令及高级工具详解 大家好&#xff01;今天我们来聊聊 Docker 这个强大的容器化工具。如果你是一个开发者、运维工程师&#xff0c;或者只是对云计算和容器技术感兴趣的人&#xff0c;Docker 绝对值得你深入了解。它可以帮助你轻松…

Redis数据持久化——RDB快照和Aof日志追加

Redis数据持久化数据持久化&#xff1a;将内存中的数据保存到磁盘中。作用&#xff1a;让Redis服务重启后可以恢复之前的数据。一、Redis数据持久化的方式&#xff1a;RDB&#xff08;快照&#xff09;&#xff1a;将内存中Redis缓存的所有数据&#xff0c;都以二进制字符串的方…

浅聊达梦数据库物理热备的概念及原理

达梦数据库&#xff08;DM Database&#xff09;的物理热备份&#xff0c;核心是在数据库不中断业务&#xff08;联机&#xff09; 的前提下&#xff0c;通过对数据库物理文件&#xff08;如数据文件、控制文件、日志文件等&#xff09;的增量或全量复制&#xff0c;实现数据备…

C++ 中 ::(作用域解析运算符)的用途

C 中 ::&#xff08;作用域解析运算符&#xff09;的应用场景详解 在 C 中&#xff0c;:: 被称为 作用域解析运算符&#xff08;Scope Resolution Operator&#xff09;&#xff0c;用于明确指定某个名字&#xff08;变量、函数、类型等&#xff09;所属的命名空间或类作用域&a…

鸿蒙中CPU活动分析:CPU分析

1 CPU分析的核心概念与重要性 CPU活动分析&#xff08;CPU Profiling&#xff09;是性能优化的核心手段&#xff0c;它通过测量代码执行时间&#xff0c;帮助开发者定位性能瓶颈。应用的响应速度直接影响用户体验&#xff0c;过长的加载时间或卡顿会导致用户流失 1.1 为什么C…

十大经典 Java 算法解析与应用

在 Java 开发的世界里&#xff0c;算法就如同构建大厦的基石&#xff0c;它们支撑着各种复杂应用的高效运行。无论是处理海量数据的排序&#xff0c;还是在庞大结构中精准查找信息&#xff0c;合适的算法都能大幅提升程序的性能。接下来&#xff0c;我们将深入解析十大经典的 J…

从感知机到大模型:神经网络的全景解析与实践指南

从感知机到大模型&#xff1a;神经网络的全景解析与实践指南在当今 AI 时代&#xff0c;我们身边的每一个智能应用 —— 从手机里的人脸识别、语音助手&#xff0c;到聊天机器人 ChatGPT、图像生成工具 MidJourney&#xff0c;再到自动驾驶的环境感知系统 —— 背后都离不开一个…

核心篇(下):Transformer 架构详解(程序员视角・实战版)

在上一篇 NLP 预处理文章中&#xff0c;你已经掌握了 “文本→向量” 的转化流程&#xff0c;解决了 DashScope Tokenizer 的调用问题。但此时你可能会问&#xff1a;“这些向量输入模型后&#xff0c;大模型是如何理解长文本语义的&#xff1f;比如‘小明告诉小红&#xff0c;…

FreeRTOS学习笔记(四):任务执行与切换

第一部分&#xff1a;FreeRTOS 任务是如何执行的&#xff1f; FreeRTOS 是一个抢占式的实时操作系统内核。其任务执行遵循一个核心原则&#xff1a;调度器&#xff08;Scheduler&#xff09;总是选择当前处于“就绪态”&#xff08;Ready&#xff09;的最高优先级任务来运行。 …

区块链技术探索与应用:从密码学奇迹到产业变革引擎

&#x1f31f; Hello&#xff0c;我是蒋星熠Jaxonic&#xff01; &#x1f308; 在浩瀚无垠的技术宇宙中&#xff0c;我是一名执着的星际旅人&#xff0c;用代码绘制探索的轨迹。 &#x1f680; 每一个算法都是我点燃的推进器&#xff0c;每一行代码都是我航行的星图。 &#x…

如何监控和调优JVM的内存使用情况?

监控和调优 JVM 内存使用是保障 Java 应用稳定性和性能的核心手段&#xff0c;需要结合监控工具、关键指标分析和针对性调优策略。以下是具体的实施方法&#xff1a;一、JVM 内存监控&#xff1a;工具与核心指标监控的目标是掌握内存使用趋势、GC 行为、线程状态等&#xff0c;…

把用户输进来的明文密码做一层 MD5 哈希

这一行干的就是&#xff1a;把用户输进来的明文密码先做一层 MD5 哈希&#xff0c;再把得到的 32 位十六进制字符串存到变量 password 里。 逐段拆开&#xff1a;password.getBytes() 把字符串转成字节数组&#xff0c;MD5 算法只能对字节/字节数组做运算。DigestUtils.md5Dige…

jeecg-boot3.7.0对接钉钉登录(OAuth2.0)

当前的jeecg-boot 是3.7.0前端问题&#xff1a;1.前端的路由vue-router的版本需要固定死。要不然会报page_not_found router the same.这种奇奇怪怪的问题。 就是把package.json的“^”&#xff0c;这个符号&#xff0c;删掉。&#xff08;或者全局搜索&#xff0c;这个page no…

【C#】获取不重复的编码(递增,非GUID)

获取不重复的编码&#xff1a;从原始实现到高效优化本文针对软件开发中“为新对象分配唯一编码”的常见需求&#xff0c;以C#通信设备管理场景为例&#xff0c;从原始代码分析入手&#xff0c;逐步讲解基于LINQ和哈希集合的优化方案&#xff0c;帮助开发者理解不同场景下的最佳…

腾讯云人脸库技术架构深度解析

腾讯云人脸库技术架构深度解析人脸库是现代人脸识别系统的核心组件&#xff0c;负责海量人脸特征的高效存储、检索和管理。腾讯云在人脸库设计上采用了多项创新技术&#xff0c;本文将深入探讨其技术实现细节。一、人脸库核心架构腾讯云人脸库采用分层架构设计&#xff1a;应用…