catlikecoding:Custom SRP(Directional Shadows)

官网教程:catlikecoding:Custom SRP(Directional Shadows)

有几个坑参考了其他大佬的总结:SRP Directional Shadows 一些坑

1、Rendering Shadows

最常见的方法是生成一个阴影贴图shadow map,该贴图存储的是:存储光源到达物体表面的距离。任何在同一个方向上更远的距离都不能被同一个光源照亮。Unity's RPs使用这种方法。

1.1 Shadow Settings

在开始渲染阴影之前,我们首先要对阴影的质量做出一些定义,特别是要决定要渲染阴影的距离以及阴影贴图的大小。

虽然我们可以渲染相机所能看到的阴影,但这需要大量的绘图和非常大的地图才能充分覆盖该区域,这几乎是不切实际的。因此,我们将引入阴影的最大距离,最小值为零,默认情况下设置为 100 个单位。创建一个新的可序列化类 ShadowSettings以包含此选项。此类纯粹是配置选项的容器,因此我们将为其提供一个公共字段 maxDistance
using UnityEngine;

[System.Serializable]
public class ShadowSettings {

[Min(0f)]
public float maxDistance = 100f;
}
对于贴图大小,我们将在ShadowSettings内嵌套一个TextureSize枚举类型。使用它来定义允许的纹理大小,所有大小均为256-8192(2的幂)。
public enum TextureSize {
_256 = 256, _512 = 512, _1024 = 1024,
_2048 = 2048, _4096 = 4096, _8192 = 8192
}

1.4 Lights with Shadows

shadows.cs中ReserveDirectionalShadows函数的if判断句中的下面的代码的含义:可见光可能最终不会影响任何投射阴影的对象,因为当前灯光没有照射任何物体,或者因为光线只影响超出最大阴影距离的对象,但是依然进行了shadowmap的绘制。

if (··· && cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b))
        {
            ···
        }

1.5 Creating the Shadow Atlas

通过将阴影投射对象绘制到纹理来完成创建阴影贴图。我们将使用_DirectionalShadowAtlas来引用定向阴影图集。从设置中检索整数形式的图集大小,然后以纹理标识符作为参数,在命令缓冲区上调用GetTemporaryRT,再加上其宽度和高度的大小(以像素为单位)。

static int dirShadowAtlasId = Shader.PropertyToID("_DirectionalShadowAtlas");

public void Render () {
    if (ShadowedDirectionalLightCount > 0) {
        RenderDirectionalShadows();
    }
}

void RenderDirectionalShadows () {
    int atlasSize = (int)settings.directional.atlasSize;
    buffer.GetTemporaryRT(dirShadowAtlasId, atlasSize, atlasSize);
}

如果只是用前三个参数的话,就是默认情况下的普通ARGB纹理。

对于一个阴影贴图,通过在调用中添加另外三个参数来指定阴影贴图。首先是深度缓冲区的位数。我们希望它尽可能高,所以让我们使用32。其次是filter 模式,为此我们使用默认的bilinear filtering。第三是渲染纹理类型,必须RenderTextureFormat.ShadowMap,如下图:

1.6 Shadows First

当我们在阴影图集之前设置常规摄影机时,最终会在渲染常规几何图形之前切换到阴影图集,这不是我们想要的。我们应该在调用CameraRenderer.Render中的CameraRenderer.Setup之前渲染阴影,这样常规渲染不会受到影响。

// Lighting实例, 在绘制可见几何体之前用它来设置lighting。
lighting.Setup(context, cullingResults, shadowSettings);

Setup();
        
DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);

通过在设置照明之前开始采样并在清除照明对象之前立即结束采样,可以在帧调试器中将阴影条目嵌套在相机的内部。

buffer.BeginSample(SampleName);
ExecuteBuffer();
lighting.Setup(context, cullingResults, shadowSettings);
buffer.EndSample(SampleName);
Setup();

1.7 Rendering

shadow map的原理是,我们从灯光的角度渲染场景,只存储深度信息。信息是光线在击中某物之前会传播多远。

但是,假设定向光无限远,因此没有真实位置。因此,我们所做的是找出与光源方向相匹配的视图和投影矩阵,并为我们提供一个剪辑空间立方体,该立方体与摄像机可见的区域重叠,该区域可以包含光源的阴影。

我们可以使用cullingResults的ComputeDirectionalShadowMatricesAndCullingPrimitives方法为我们完成此工作,并为其传递9个参数。

前六个是输入参数。第一个参数是可见光指数。接下来的三个参数是两个整数和一个Vector3,它们控制阴影级联(shadow cascade)。稍后我们将处理级联,因此现在使用零,一和零向量。然后是纹理尺寸,我们需要使用平铺尺寸。第六个参数是靠近平面的阴影,我们现在将其忽略并将其设置为零。

其余三个是输出参数。首先是视图矩阵(view matrix),然后是投影矩阵(projection matrix),最后一个参数是ShadowSplitData结构。

接来下将ShadowSplitData复制到阴影设置中。然后在缓冲区上调用SetViewProjectionMatrices来应用视图和投影矩阵。

最后,通过执行缓冲区,然后在上下文中调用DrawShadows来调度阴影投射程序的绘制,并通过引用将阴影设置传递给它,代码如下:

void RenderDirectionalShadows(int index, int tileSize)
{
    ShadowedDirectionalLight light = ShadowedDirectionalLights[index];
    var shadowSettings = new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);

    cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(light.visibleLightIndex, 0, 1, Vector3.zero, tileSize, 0f, out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,out ShadowSplitData splitData);

    shadowSettings.splitData = splitData;
    buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
    ExecuteBuffer();
    context.DrawShadows(ref shadowSettings);
}

1.8 Shadow Caster Pass

渲染阴影,对应上文代码中context.DrawShaderws(ref shadowSettings);但是此时无任何作用,这是因为DrawShadows仅使用具有ShadowCaster传递的材质来渲染对象。

因此,在Lit着色器中添加第二个Pass块,设置如下:

接下来,创建ShadowCasterPass.hlsl

Create the ShadowCasterPass file by duplicating LitPass and removing everything that isn't necessary for shadow casters. So we only need the clip-space position, plus the base color for clipping. The fragment function has nothing to return so becomes void without semantics. The only thing it does is potentially clip fragments.
#ifndef CUSTOM_SHADOW_CASTER_PASS_INCLUDED
#define CUSTOM_SHADOW_CASTER_PASS_INCLUDED

#include "../ShaderLibrary/Common.hlsl"

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

struct Attributes {
    float3 positionOS : POSITION;
    float2 baseUV : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings {
    float4 positionCS : SV_POSITION;
    float2 baseUV : VAR_BASE_UV;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

Varyings ShadowCasterPassVertex (Attributes input) {
    Varyings output;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    float3 positionWS = TransformObjectToWorld(input.positionOS);
    output.positionCS = TransformWorldToHClip(positionWS);

    float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
    output.baseUV = input.baseUV * baseST.xy + baseST.zw;
    return output;
}

void ShadowCasterPassFragment (Varyings input) {
    UNITY_SETUP_INSTANCE_ID(input);
    float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
    float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
    float4 base = baseMap * baseColor;
    #if defined(_CLIPPING)
        clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
    #endif
}

#endif

在场景中放三个不透明物体。

FrameDebugger中shadowmap如下:

可以发现,shadowmap太小了,摄像机其实并没有离得这么远。那么接下来,可以调整我们管线上的Max Distance,使阴影贴图放大。

注:当前阴影投射器使用正交投影进行渲染,因为我们是针对定向光进行渲染。

1.9 Multiple Lights

对于多灯光,将每个灯光的shadowmap,分割开来渲染到同一个纹理中,否则会叠加。

代码如下:

const int maxShadowedDirectionalLightCount = 4;
void RenderDirectionalShadows(int index, int split, int tileSize)
{
    ShadowedDirectionalLight light = ShadowedDirectionalLights[index];
    var shadowSettings = new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);

    cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(light.visibleLightIndex, 0, 1, Vector3.zero, tileSize, 0f, out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,out ShadowSplitData splitData);

    shadowSettings.splitData = splitData;
    SetTileViewport(index, split, tileSize);
    buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
    ExecuteBuffer();
    context.DrawShadows(ref shadowSettings);
}

void SetTileViewport(int index, int split, float tileSize)
{
    Vector2 offset = new Vector2(index % split, index / split);
    buffer.SetViewport(new Rect(offset.x * tileSize, offset.y * tileSize, tileSize, tileSize));
}

2、Sampling Shadows

为了显示阴影,我们需要在CustomLit通道中对阴影贴图进行采样,并判断片元是否有阴影。

2.1 Shadow Matrices

对于下面这行代码,这里的vieMatrix和projectionMatrix不是之前的变换VP矩阵,而是跟灯光相关的视图矩阵和阴影投影矩阵,这样一来,我们就可以创建从世界空间到灯光空间的转换矩阵。

cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(light.visibleLightIndex, 0, 1, Vector3.zero, tileSize, 0f, out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,out ShadowSplitData splitData);
static int dirShadowAtlasId = Shader.PropertyToID("_DirectionalShadowAtlas"),
        dirShadowMatricesId = Shader.PropertyToID("_DirectionalShadowMatrices");
        
static Matrix4x4[] dirShadowMatrices = new Matrix4x4[maxShadowedDirectionalLightCount];

void RenderDirectionalShadows (int index, int split, int tileSize) {
    …
    SetTileViewport(index, split, tileSize);
    dirShadowMatrices[index] = projectionMatrix * viewMatrix;
    buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
    …
}

接下来,在渲染完所有阴影光之后,通过调用缓冲区上的SetGlobalMatrixArray将矩阵发送到GPU。

这里不粘贴全部代码了,只放一下对于多灯光而产生的阴影图集的矩阵变换,其他代码可以直接看教程。

 Matrix4x4 ConvertToAtlasMatrix(Matrix4x4 m, Vector2 offset, int split)
    {
        // 反向Z buffer,此时1代表近平面,0代表远平面。OpenGL就是这样做的。
        if (SystemInfo.usesReversedZBuffer)
        {
            m.m20 = -m.m20;
            m.m21 = -m.m21;
            m.m22 = -m.m22;
            m.m23 = -m.m23;
        }

        // clip space is defined inside a cube with with coordinates going from −1 to 1,
        // But, textures coordinates and depth go from zero to one.
        // Also, apply the tile offset and scale.
        float scale = 1f / split;
        m.m00 = (0.5f * (m.m00 + m.m30) + offset.x * m.m30) * scale;
        m.m01 = (0.5f * (m.m01 + m.m31) + offset.x * m.m31) * scale;
        m.m02 = (0.5f * (m.m02 + m.m32) + offset.x * m.m32) * scale;
        m.m03 = (0.5f * (m.m03 + m.m33) + offset.x * m.m33) * scale;
        m.m10 = (0.5f * (m.m10 + m.m30) + offset.y * m.m30) * scale;
        m.m11 = (0.5f * (m.m11 + m.m31) + offset.y * m.m31) * scale;
        m.m12 = (0.5f * (m.m12 + m.m32) + offset.y * m.m32) * scale;
        m.m13 = (0.5f * (m.m13 + m.m33) + offset.y * m.m33) * scale;

        m.m20 = 0.5f * (m.m20 + m.m30);
        m.m21 = 0.5f * (m.m21 + m.m31);
        m.m22 = 0.5f * (m.m22 + m.m32);
        m.m23 = 0.5f * (m.m23 + m.m33);

        return m;
    }

2.2 Storing Shadow Data Per Light

要对光源的阴影进行采样,我们需要知道阴影图集中其图块的索引(如果有的话)。这是每个灯必须存储的东西,所以让我们返回所需的数据。我们将提供两个值:阴影强度和阴影图块偏移量,如果光线没有阴影,则结果为零矢量。

2.3 Shadows HLSL File

我们还将创建一个专用的Shadows HLSL文件以进行阴影采样,定义相同的最大阴影定向光计数、_DirectionalShadowAtlas纹理以及纹理采样器sampler_DirectionalShadowAtlas,还有在_CustomShadows buffer中定义一个_DirectionalShadowMatrices 数组。

并使用专门用于阴影的纹理定义关键字和纹理采样器的关键字:TEXTURE2D_SHADOW和SAMPLER_CMP。

实际上,只有一种合适的方法可以对阴影贴图进行采样,因此我们可以定义一个明确的采样器状态,而不是依赖Unity推导的渲染纹理状态。可以内联定义采样器状态,方法是在其名称中创建一个带有特定单词的状态。我们可以使用sampler_linear_clamp_compare。并为其定义一个简写的SHADOW_SAMPLER宏。

#ifndef CUSTOM_SHADOWS_INCLUDED
#define CUSTOM_SHADOWS_INCLUDED

#define MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT 4

TEXTURE2D_SHADOW(_DirectionalShadowAtlas);
#define SHADOW_SAMPLER sampler_linear_clamp_compare
SAMPLER_CMP(SHADOW_SAMPLER);

CBUFFER_START(_CustomShadows)
    float4x4 _DirectionalShadowMatrices[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
CBUFFER_END

#endif

2.4 Sampling Shadows

为了对阴影进行采样,需要了解每个光的阴影数据,因此让我们在Shadows中为它定义一个结构,特别是定向光。它包含strength 和tile offset。

1、向“Shadows”添加SampleDirectionalShadowAtlas函数,该函数通过SAMPLE_TEXTURE2D_SHADOW宏对阴影图集进行采样,并向其传递图集,阴影采样器以及阴影纹理空间中的位置(这是一个对应的参数)。
2、添加GetDirectionalShadowAttenuation函数,该函数在给定方向阴影数据和表面的情况下返回阴影衰减,该阴影应该在世界空间中定义。它使用tile offset 来检索正确的矩阵,将表面位置转换为阴影图块空间(WorldSpace To ShadowSpace),然后对图集进行采样。
The result of sampling the shadow atlas is a factor that determines how much of the light reaches the surface, taking only shadows into account. It's a value in the 0–1 range that's know as an attenuation factor. If the fragment is fully shadowed then we get zero and when it's not shadowed at all then we get one. Values in between indicate that the fragment is partially shadowed.
Besides that, a light's shadow strength can be reduced, either for artistic reasons or to represent shadows of semitransparent surfaces. When the strength is reduced to zero then attenuation isn't affected by shadows at all and should be one. So the final attenuation is found via linear interpolation between one and the sampled attenuation, based on strength.
return lerp(1.0, shadow, data.strength);

剩下的代码较多,直接看教程。需要提一点的是,shadow在着色器中的一系列计算之后,就相当于光的衰减attention。所以最终在shader中入射光乘以的是光的衰减attention,而不是shadow。

3、Cascaded Shadow Maps

阴影投射器被渲染了不止一次,因此每个光在图集中会得到多个图块,称为级联(Cascaded )。第一个级联仅覆盖靠近相机的一小部分区域,而连续的级联会缩小以覆盖越来越大的具有相同像素数量的区域。然后,着色器对每个片段可用的最佳级联进行采样。类似纹理的mipmap

跟着教程完成第三章的代码后,会得到上图的结果,看起来也太那啥了。。。

原因:不应被阴影化的表面最终会被形成像素化带的阴影伪影所覆盖。这些是由于阴影贴图的有限分辨率导致的自我阴影化。使用不同的分辨率会更改伪影模式,但不会消除它们。这些表面最终会部分遮盖自身。

4.3 Normal Bias

这部分参考了:Normal Bias

深度bias就是一种退化版的Normal bias。

一般情况下,不做任何处理的shadowmap,由于精度问题,会出现上图的阴影锯齿(俗称Shadow acne)

最简单的优化手段是在深度上增加一个偏移,形象点就是把物体向远裁剪面推了一段距离,这样原本因为精度导致的锯齿被拉开了,这个偏移就是经常看到的depth bias,可以解决大部分问题。

存在一种情况,当表面与灯光远近裁剪面呈垂直或者将近垂直的情况下,增加了偏移的深度(把表面往远裁剪面推),会和让其与后面的表面产生精度问题,也就是说bias只能解决面朝灯光方向的阴影锯齿。

这时候就需要normal bias,对于面向灯光方向,作用和bias基本一致,对于其他方向则是往物体内部压,这样就可以解决多个方向的问题。

对于两侧空出来的区域深度会显著加大,这也就让原有的两侧在渲染时深度明显小于SM上的深度,从而处于光照之下。

4.5 Shadow Pancking

unity官网对应下图 Shadow Near Plane的解释:

4.6 PCF Filtering

以上使用级联模式Cascaded Shadow Mapping(简称CSM),这也是unity内置的方向光实时阴影技术。我们仅对每个片段采样一次阴影贴图,且使用了硬阴影。阴影比较采样器使用特殊形式的双线性插值,在插值之前执行深度比较。这被称为百分比紧密过滤(percentage closer filtering 简称PCF),因为其中包含四个纹理像素,所以一般指是2×2 PCF过滤器。

但这不是我们过滤阴影贴图的唯一方法。我们也可以使用更大的滤镜,使阴影更柔和,更不易混叠,尽管准确性也较低。让我们添加对2×2、3×3、5×5和7×7过滤的支持。我们不会使用现有的柔和阴影模式来控制每个灯光。相反,我们将使所有定向光源使用相同的滤镜。为此,向ShadowSettings添加一个FilterMode枚举,并将创建的Directional结构体 的过滤器选项默认设置为2×2。

剩下的内容实在太多了。。。直接看教程吧,比较详细。

注:跟教程会发现,当对Lit材质的物体,使用transparent或者fade的渲染队列,会发现物体都消失了,这是因为教程里的代码,有一行漏掉了,找了半天才找到= =,下面给出教程中缺少代码的位置,和正确的代码。

这是第三章,Directional Lights的教程中的代码块,之前教程中说的是先渲染opaque物体,然后渲染天空盒,最后渲染透明物体。

看这里教程中添加的代码的位置是opaque模块下,SetShaderPassName这行代码的意思是让我们前面Lit.shader的中第一个(第一个参数1,代表index)Pass,且Pass名称(Tags)为litShaderTagId生效。

所以我们的Lit材质就可以正常渲染opaque的渲染队列的物体。

当然,下面透明物体的渲染,也应该加上SetShaderPassName这行代码,加上之后,结果就正确了,代码如下:

void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing)
    {
        var sortingSettings = new SortingSettings(camera) { criteria = SortingCriteria.CommonOpaque };
        var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
        {
            enableDynamicBatching = useDynamicBatching,
            enableInstancing = useGPUInstancing
        };

        // 要渲染使用这个pass的对象,我们必须把它添加进CameraRenderer中,利用Tags标识符。
        drawingSettings.SetShaderPassName(1, litShaderTagId);
        var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);


        context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
        
        context.DrawSkybox(camera);

        sortingSettings = new SortingSettings(camera) { criteria = SortingCriteria.CommonTransparent };
        drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
        // 这个如果不加的话,渲染队列设置成transparent,物体不会显示,上面opaque同理。
        drawingSettings.SetShaderPassName(1, litShaderTagId);
        filteringSettings = new FilteringSettings(RenderQueueRange.transparent);

        context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
    }

效果图(有点丑= =,但能看出来效果):

本文有好多地方没有写(后面抽空再补上= =),可以去官网查看详细流程。

OKOK,第三章完结撒花 ~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值