# DX11 - SDF
本篇阐述在 DX11 中使用 SDF 生成距离场软阴影的实现。\
前置知识
Compute Shader
XJun的DX11教学DX11 Compute Shader
SDF 原理和实现参考
SDF原理
Unity 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); | |
} |
# 最终效果
硬阴影
软阴影