# DX11 - SDF

本篇阐述在 DX11 中使用 SDF 生成距离场软阴影的实现。\

前置知识

  1. Compute Shader

  2. SDF 原理和实现参考

# SDF 步骤分析

SDF 的本质原理非常简单,各个体素存载离自己最近的物体的距离。在着色时,从着色点朝向光源方向进行改进的 RayMarch,每次的步进都是 sdf 值。如果 sdf 值小于阈值就认为是被遮挡。
SDF 的优点是着色器算法简单并且速度很快。但是时间由空间换来,SDF 需要使用三维纹理存储,并且最大的难题就是生成。

# 生成 SDF 纹理

本次的场景为 10x10x10 的区域,我计划将其切分为 64x64x64 的体素纹理。
计算 SDF 有两类思路,一种是使用 CPU 进行光线追踪求交,一种是利用 GPU 的并行能力进行渲染。

# CPU

写过光线追踪的 uu 都知道,对于一条射线和一个给定的三角形,我们可以有直接的公式进行交点的求解,可以快速拿到交点是否在三角形内以及垂直距离。
为了加速光线求交,我们可以构建 BVH 或者 KD-Tree。这里选择使用 BVH。

//bvh 结点类定义
class bvh : public hittable {
  public:
    shared_ptr<hittable> left;
    shared_ptr<hittable> right;
    AABB _aabb;
    bvh(Scene &sce);
    bvh(std::vector<shared_ptr<hittable>> objects, int begin, int end);
    bool hit(const Ray &ray, Interval rayT, InterPoint &inter) const override;
    AABB aabb() const override {
        return _aabb;
    }
    static bool compare(const shared_ptr<hittable> a, const shared_ptr<hittable> b, int axisIndex) {
        auto aAxisInterval = a->aabb().axisInterval(axisIndex);
        auto bAxisInterval = b->aabb().axisInterval(axisIndex);
        return (aAxisInterval.min + aAxisInterval.max) < (bAxisInterval.min + bAxisInterval.max);
    }
    static bool comparex(const shared_ptr<hittable> a, const shared_ptr<hittable> b) {
        return compare(a, b, 0);
    }
    static bool comparey(const shared_ptr<hittable> a, const shared_ptr<hittable> b) {
        return compare(a, b, 1);
    }
    static bool comparez(const shared_ptr<hittable> a, const shared_ptr<hittable> b) {
        return compare(a, b, 2);
    }
};
// 其中 hittable 是物体纯虚基类,提供 hit 方法返回交点。
//bvh 的每一个 aabb 都应该视作一个没有 mesh 的物体,可以正常的求交,而 bvh 的最底层就是实际物体所拥有的三角形。

BVH 的构建和求交都是对于二叉树的递归操作。

//bvh 构建
bvh::bvh(std::vector<shared_ptr<hittable>> objects, int begin, int end) {
    // 递归的进行构造
    // 0. 递归出口
    if (begin == end - 1) {
        left = right = objects[begin];
    } else if (begin == end - 2) {
        left = objects[begin];
        right = objects[end - 1];
    } else {
        // 1. 维度选择 - 随机选择一个维度
        auto axis = random_int(0, 2);
        auto compareFunc = axis == 0 ? comparex : (axis == 1 ? comparey : comparez);
        // 2. 按照此维度进行排序
        std::sort(objects.begin() + begin, objects.begin() + end, compareFunc);
        // 3. 递归构建
        int mid = (begin + end) >> 1;
        left = make_shared<bvh>(objects, begin, mid);
        right = make_shared<bvh>(objects, mid, end);
    }
    // 4. 构建当前节点
    _aabb = AABB(left->aabb(), right->aabb());
}
//bvh 加速求交
bool bvh::hit(const Ray &ray, Interval rayT, InterPoint &inter) const {
    // 如果没打中外部包围盒 , 内部的就更不可能打中
    if (!_aabb.hit(ray, rayT)) {
        return false;
    }
    // 如果打中了 那么就继续判断有没有打中内部的
    auto hitleft = left->hit(ray, rayT, inter);
    auto hitright = right->hit(ray, Interval(rayT.min, hitleft ? inter.t : rayT.max), inter);
    return hitleft || hitright;
}

在 Init 中我们加入对 bvh 的构建。然后我们创建三维纹理数组,遍历 64x64x64 个坐标点,在球面坐标上均匀打出 32x32 条光线进行求交。求交后进行距离计算和正反面判断,然后将最小值写入。

void CalculateSDF(){
    for(int i=0;i<64;i++){
        for(int j=0;j<64;j++){
            for(int k=0;k<64;k++){
                XMFLOAT3 pos = GetPos(i,j,k);
                for(int x=0;x<32;x++){
                    for(int y=0;y<32;y++){
                        auto light = CastLight(x,y);
                        Interval rayT;
                        InterPoint inter;
                        
                        //bvh 求交
                        bool isHit = bvhHead->hit(light,rayT,inter);
                        
                        // 如果击中
                        if(isHit){
                            float dis = CalDis(pos,inter.pos);
                            float sdf = dis * (inter.isFront ? 1 : -1 );
                            SDFtex[i][j][k] = min(SDFtex[i][j][k],sdf);
                        }
                    }
                }
            }
        }
    }
}

至此 SDFtex 可以被计算出来。但是用于是 CPU 并且是暴力求解,速度会较慢,计算如此小场景(约 4・5 个物体,一万面)也需要 5~10 分钟。如果要使用推荐学习 UE 的做法进行异步计算。

# GPU

由于 sdf 就是最小距离,也就是最小深度信息,不难想到模拟渲染管线,利用 GPU 的并行能力进行计算。这里就需要我们的 ComputeShader 大放异彩。
我的思路是这样的,对于每一个体素纹理,在每一个位置假设六个摄像机,分别对六个面进行非常低分辨率渲染,不过不能直接使用深度图,因为深度图不能保存正反信息。之后使用 ComputeShader 对每个体素的六个面进行并行处理计算出最小值。

RWTexture3D<float> g_Output : register(u0);
Texture2DArray g_texs : register(t0);
SamplerState g_Sampler : register(s0);
cbuffer ConstantBuffer : register(b0)
{
    int g_Size;
    float3 g_pad;
};
[numthreads(16, 16, 1)]
void CS(uint3 DTid : SV_DispatchThreadID)
{
    // 采样 Texture2DArray 获取 SDF 值
    float2 texCoord = float2(pos.x / g_Size.x, pos.y / g_Size.y);
    int arrayIndex = int(pos.z / g_Size.z * 4.0f);
    float4 texSample = g_texs.Sample(g_Sampler, float3(texCoord, arrayIndex));
    int i;
    int j;
    int k;
    float sdf = 1000.0fh;
    [unroll]
    for(i=0;i<6;i++){
        for(j=0;j<g_Size;j++){
            for(k=0;k<g_Size;k++){
                float2 texCoord = float2(j / g_Size, k / g_Size);
                int arrayIndex = i;
                float curDepth = g_texs.Sample(g_Sampler, float3(texCoord, arrayIndex)).r;
                sdf = min(sdf, curDepth);
            }
        }   
    }
    // 将 SDF 和纹理采样结果写入 RWTexture3D
    g_Output[DTid] = sdf;
}

我们使用包含 16x16x1 线程的线程组,那么就需要 4x4x64 个线程组。写出的 g_Output 纹理是 UAV,我们生成一个对应的 SRV 绑定到最终的 PS 上即可采样。

# 计算 SDF 硬阴影

float SdfTrace(float3 ro, float3 rd, float maxd){
    float t = 0.01f;
    for(int i=0;i<100;i++){
        float d = GetSDF(ro + rd * t);
        t += d;
        [flatten]
        if(d<0.0f)
            return 0.0f;
       
        if(t > maxd){
            break;
        }
    }
    return 1.0f;
}

# 计算 SDF 软阴影

SDF 软阴影的计算非常简单。这里采用 Games 中阐述的方法,对于每次步进,计算 “安全角度” = k * sdf / (p - o) ,并记录安全角度的最小值作为阴影软硬的因子。

float SdfTrace(float3 ro, float3 rd, float maxd){
    float t = 0.01f;
    float minD = 10.0f;
    for(int i=0;i<100;i++){
        float d = GetSDF(ro + rd * t);
        float theta = k * abs(d) / t;
        minD = min(minD,theta);
        t += d;
        [flatten]
        if(d<0.0f)
            return 0.0f;
       
        if(t > maxd){
            break;
        }
    }
    return saturate(minD);
}

# 最终效果

硬阴影

软阴影

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Yian Zhao 微信支付

微信支付

Yian Zhao 支付宝

支付宝

Yian Zhao 贝宝

贝宝