基于SDF光线步进的实时软阴影与环境光遮蔽实现

📅 2026/7/23 11:46:25
基于SDF光线步进的实时软阴影与环境光遮蔽实现
1. 项目概述从硬到软的视觉革命如果你玩过一些早期的3D游戏或者尝试过用传统的光栅化管线比如OpenGL或DirectX的固定管线自己写过一个简单的场景你大概率会对那种“一刀切”的阴影印象深刻——边缘锐利得像用尺子画出来一样物体要么完全在阴影里要么完全在光下缺乏现实世界中那种柔和、渐变的过渡。这种就是典型的“硬阴影”。在追求真实感和沉浸感的现代图形应用中尤其是在独立游戏和实时渲染领域硬阴影已经远远不够看了。玩家和开发者都在追求更自然的光影效果而软阴影和环境光遮蔽Ambient Occlusion, AO正是提升画面质感的两大“廉价”法宝。为什么说“廉价”因为在传统的、基于三角形网格的渲染管线里实现高质量的软阴影和AO通常意味着巨大的性能开销。软阴影需要从多个角度采样光源如PCF, PCSSAO则需要从半球空间采样周围几何信息如SSAO。这些技术要么依赖深度图要么需要大量的后处理采样对GPU带宽和算力都是考验。但对于很多风格化、体素化或者程序化生成的场景我们其实有另一条路可以走符号距离场Signed Distance Field, SDF结合光线步进Ray Marching。这个项目的核心就是用C从头实现一个基于SDF光线步进的渲染器专门用来生成游戏级的软阴影和环境光遮蔽效果。我们完全抛弃了传统的多边形网格和光栅化流程而是用数学函数来定义场景中的每一个物体球体、盒子、平面等。通过光线步进我们可以高效、精确地计算出光线与这些隐式表面的交点。更重要的是在这个框架下计算软阴影和AO变得异常优雅和高效——我们只需要在步进的过程中多问几个关于“遮挡”的问题。想象一下你要判断一个点p是否在阴影中。传统方法可能需要发射一条阴影射线去测试与所有三角形的相交。而在SDF的世界里你只需要从点p向光源方向“步进”同时查询场景的SDF值。如果在你到达光源之前SDF值就变成了负数表示你撞到了东西那这个点就在阴影里。而软阴影的“软”度则可以通过这个遮挡物距离光线有多近来量化从而得到一个平滑的阴影衰减系数。环境光遮蔽的思路也类似不过是向半球空间的多个方向步进统计被遮挡的程度。我将带你从零开始搭建这个纯C的、不依赖任何图形API如OpenGL的SDF渲染器。我们会从最基础的向量数学和SDF定义开始逐步实现光线步进、光照模型最后重点攻克软阴影我们采用一种称为“软阴影”的SDF技巧和屏幕空间环境光遮蔽Screen Space Ambient Occlusion, SSAO的变体——但在SDF空间里它更准确我们可以称之为“SDF AO”。文章末尾会提供完整的、可编译运行的源码链接。无论你是想为你的体素游戏添加动态光影还是想深入理解实时渲染的底层数学亦或是单纯被SDF这种简洁强大的表达方式所吸引这篇实践指南都将为你提供一条清晰的路径。2. 核心原理与数学基石在深入代码之前我们必须夯实理论基础。SDF光线步进渲染器建立在几个关键的数学概念之上理解它们是如何工作的比直接复制代码重要得多。2.1 符号距离场SDF用距离定义形状符号距离场SDF是一个函数对于空间中的任意一点p(x, y, z)它返回该点到某个物体表面最近的距离。这个距离是有符号的正值表示点p在物体外部。零表示点p正好在物体表面上。负值表示点p在物体内部。这个简单的定义带来了巨大的威力。首先它统一了各种几何形状的描述。一个球体、一个盒子、一个圆环都可以用一个简洁的数学函数来表示。其次SDF支持非常容易的几何布尔运算并集、交集、差集和平滑融合这使得构建复杂场景变得像搭积木一样简单。让我们看几个最基本的SDF例子这将是我们的渲染器里最基础的构建块球体SDFsdSphere(p, center, radius) length(p - center) - radius原理点到球心的距离减去半径。在球外为正球上为0球内为负。盒子SDFsdBox(p, center, size)稍微复杂一点需要计算点到盒子六个面的最大距离分量但核心思想一致。平面SDFsdPlane(p, normal, distanceFromOrigin) dot(p, normal) distanceFromOrigin原理利用点积计算点到平面的有符号距离。一个复杂的场景SDF就是这些基础SDF通过min()并集、max()交集和max(a, -b)差集等操作组合而成的。例如min(sdSphere(p), sdBox(p))描述的就是一个球和一个盒子合并后的形状。注意SDF函数的计算精度和性能至关重要。在实现时要尽量避免不必要的sqrt开方运算比如在比较距离大小时可以比较距离的平方。对于复杂SDF可以考虑空间划分如八叉树来加速查询但对我们这个入门项目全局遍历所有基本体已经足够。2.2 光线步进Ray Marching安全地探索未知空间有了描述场景的SDF函数我们如何渲染它这就是光线步进的用武之地。传统的光线追踪是求解光线与显式方程的交点而光线步进是一种迭代逼近的方法。其核心算法Sphere Tracing令人惊叹的简单从相机位置ro(ray origin) 沿某个像素方向rd(ray direction) 发射一条光线。查询当前光线起点p到场景的最近距离d即调用SDF函数。因为d是当前位置到最近表面的安全距离所以我们可以放心地将光线向前推进d而不会错过任何表面。令p p rd * d。重复步骤2和3。终止条件d小于一个极小的阈值如0.001我们认为已经击中表面返回p作为交点。步进次数超过最大限制如100次我们认为光线逃逸到了无穷远返回“未击中”。光线前进的总距离超过最大视距同样返回“未击中”。这个过程就像一个人在浓雾中摸索前进每次只走到能确保安全的最近距离。它的稳定性来自于SDF提供的“安全距离”保证。float rayMarch(vec3 ro, vec3 rd) { float totalDistance 0.0; for (int i 0; i MAX_STEPS; i) { vec3 p ro rd * totalDistance; float distance sceneSDF(p); // 查询场景SDF if (distance EPSILON) { // 命中表面 return totalDistance; } if (totalDistance MAX_DIST) { // 超出最大视距 break; } totalDistance distance; // 向前安全步进 } return -1.0; // 表示未命中 }实操心得EPSILON命中阈值的选择是个平衡艺术。太小如1e-5会增加不必要的步进次数可能导致浮点数精度问题太大如0.01则会使交点的几何精度下降物体边缘出现“浮”在空中的感觉。通常1e-3到1e-4是一个不错的起点。MAX_STEPS直接影响性能和渲染质量复杂场景需要更多步数但通常64-128步对于中等复杂度的SDF场景已经足够。2.3 法线计算与基础光照一旦通过光线步进找到了交点p我们需要计算该点的法线以便进行光照。在SDF中计算法线异常简单优雅——利用中心差分来近似梯度梯度方向就是法线方向。vec3 calcNormal(vec3 p) { const float eps 0.001; // 一个很小的偏移量 float d sceneSDF(p); float dx sceneSDF(p vec3(eps, 0, 0)) - d; float dy sceneSDF(p vec3(0, eps, 0)) - d; float dz sceneSDF(p vec3(0, 0, eps)) - d; return normalize(vec3(dx, dy, dz)); }有了法线n和交点p我们就可以应用经典的光照模型比如兰伯特Lambert漫反射和布林-冯Blinn-Phong高光。假设光源位置是lightPos。vec3 lightDir normalize(lightPos - p); float diff max(dot(n, lightDir), 0.0); // 漫反射系数 vec3 viewDir normalize(ro - p); vec3 reflectDir reflect(-lightDir, n); float spec pow(max(dot(viewDir, reflectDir), 0.0), 32.0); // 高光系数 vec3 color material.albedo * diff * light.color material.specular * spec * light.color;这就是一个带硬阴影的基础渲染器了。但阴影是“硬”的因为上面的diff计算没有考虑遮挡。接下来我们就要解决这个问题。3. 实现游戏级软阴影在SDF渲染中实现阴影的核心思想是从着色点p向光源方向发射一条新的光线并步进检查是否有遮挡。如果这条“阴影射线”在到达光源前击中了其他物体那么点p就在阴影中。硬阴影的判断是二元的是或否而软阴影则需要一个平滑的过渡值。3.1 传统二元硬阴影的实现首先我们实现一个基础的硬阴影函数这有助于理解流程float hardShadow(vec3 p, vec3 lightDir, float maxDist) { float rayDepth 0.01; // 从表面稍微偏移一点开始避免自遮挡 for (int i 0; i MAX_SHADOW_STEPS; i) { vec3 samplePoint p lightDir * rayDepth; float dist sceneSDF(samplePoint); if (dist EPSILON) { // 在到达光源前碰到了物体完全在阴影中 return 0.0; } rayDepth dist; if (rayDepth maxDist) { // 安全到达光源完全在光照中 break; } } return 1.0; // 无遮挡光照强度为1 }在着色时我们将漫反射系数乘以这个阴影值diff * shadow;。这就会产生边缘锐利的硬阴影。问题在于这种非0即1的阴影在现实中很少见看起来非常生硬。3.2 SDF软阴影技巧利用距离估计软阴影的核心思想是遮挡物离阴影射线越近阴影就应该越“硬”越暗遮挡物离得越远阴影就越“软”越淡。幸运的是在光线步进的过程中SDF值dist正好给出了当前采样点到最近表面的最近距离。我们可以利用这个信息来构造一个平滑的阴影系数。一种广泛使用且效果很好的方法常被称为“软阴影”或“百分比渐进软阴影”如下float softShadow(vec3 p, vec3 lightDir, float maxDist, float k) { float rayDepth 0.05; // 起始偏移比硬阴影稍大以减少痤疮 float res 1.0; // 最终阴影系数1.0表示无阴影 for (int i 0; i MAX_SHADOW_STEPS; i) { vec3 samplePoint p lightDir * rayDepth; float dist sceneSDF(samplePoint); if (dist EPSILON) { // 直接击中表面完全遮挡 return 0.0; } // 关键步骤利用当前距离估计来软化阴影 // dist是安全距离也是遮挡物距离射线的最近距离。 // 公式res min(res, k * dist / rayDepth); // k 是控制软硬程度的参数。k越大阴影越硬k越小阴影越软、过渡范围越大。 // dist / rayDepth 这个比值很重要当遮挡物很近时(dist小)比值小阴影变暗当遮挡物很远时比值大阴影影响小。 res min(res, k * dist / rayDepth); rayDepth dist; if (rayDepth maxDist || res 0.001) { // 到达光源或者阴影已经足够暗提前退出循环 break; } } // 将结果限制在[0,1]范围并可能做一个平滑映射如 pow(res, 2.0)让过渡更自然 return clamp(res, 0.0, 1.0); }参数k的物理意义你可以把k理解为“阴影的锐利度”。在物理上它模拟了光源的大小。k值大如32.0模拟一个点光源产生的阴影边缘较锐利k值小如4.0模拟一个面光源阴影的半影区penumbra很宽过渡非常柔和。调整k值可以让你在性能和视觉效果之间取得平衡也是实现不同艺术风格的关键。注意事项与常见问题阴影痤疮Shadow Acne由于浮点数精度问题阴影射线可能从表面下方开始导致错误的自我遮挡产生难看的黑点。解决方案是给rayDepth一个足够大的初始偏移如0.05让射线从表面上方一点开始。性能优化软阴影的步进次数MAX_SHADOW_STEPS通常可以比主光线步进少因为阴影不需要极高的几何精度。16-32步往往就能得到不错的效果。同时利用res 0.001这样的条件提前退出循环可以节省大量计算。距离maxDist这是从着色点到光源的距离。需要准确计算否则阴影可能过早终止或过度延伸。通常用length(lightPos - p)得到。带状瑕疵Banding在非常柔和的阴影k值很小区域可能会因为res值的量化而产生带状瑕疵。可以通过在最后对res应用一个简单的平滑函数如smoothstep或在步进时加入少量随机噪声来缓解。将softShadow函数返回的值乘以漫反射光照你就能立刻得到具有平滑过渡边缘的、视觉效果提升巨大的软阴影。这是SDF渲染中最“物美价廉”的效果之一。4. 实现SDF空间的环境光遮蔽AO环境光遮蔽AO模拟的是场景中缝隙、角落和物体接触处因为周围环境光被遮挡而变暗的现象。它能极大地增强场景的立体感和真实感。在传统栅格化中SSAO屏幕空间环境光遮蔽是主流但它有精度问题只依赖深度图。在SDF中我们可以实现一种更准确、更自然的AO。4.1 原理半球空间采样与距离衰减SDF AO的基本思想是在着色点p的法线n所指向的半球空间内随机发射一系列采样射线。对于每条采样射线我们沿着它步进一小段距离并检查是否被几何体遮挡。遮挡的程度即AO值由两个因素决定遮挡距离采样点是否在很近的距离内碰到了几何体碰到的越早遮挡越严重。衰减权重距离着色点越远的遮挡其贡献应该越小。我们通过累加所有采样射线的遮挡贡献并取平均值来得到最终的AO系数0为完全遮挡1为无遮挡。4.2 实现细节与优化以下是SDF AO的一个经典实现有时被称为“SSAO”的SDF变体或直接叫“AO”float calculateAO(vec3 p, vec3 n) { float occlusion 0.0; float sampleRadius 0.5; // 采样半径控制AO的影响范围 float bias 0.01; // 避免自相交 int aoSamples 8; // 采样数影响质量和性能 for (int i 0; i aoSamples; i) { // 1. 生成半球空间的随机方向 vec3 randomVec randomHemisphereDirection(i, n); // 需要一个随机函数 vec3 sampleDir normalize(n randomVec * 0.5); // 偏向法线方向 // 2. 沿着该方向步进 float rayDepth bias; for (int j 0; j 3; j) { // 通常只需步进很少几次 vec3 samplePoint p sampleDir * rayDepth; float dist sceneSDF(samplePoint); if (dist EPSILON) { // 遇到几何体计算遮挡贡献 // 贡献度 1.0 - (当前步进距离 / 采样半径) // 距离越近贡献越大1.0 - 小值 大值 occlusion 1.0 - (rayDepth / sampleRadius); break; // 遇到遮挡停止这条采样射线 } rayDepth dist; if (rayDepth sampleRadius) { // 超出采样半径认为无遮挡 break; } } } // 3. 平均并反转occlusion是遮挡值我们需要的是能见度 occlusion / float(aoSamples); return clamp(1.0 - occlusion, 0.0, 1.0); }关键参数解析sampleRadius这定义了AO的“感知范围”。半径小只有非常近的缝隙会变暗半径大更大的凹陷区域也会产生AO。通常需要根据场景尺度来调整。aoSamples采样数量直接决定AO的质量和噪点。8-16个采样对于预览可能足够但为了安静的画面可能需要32甚至64个采样。性能警告AO计算非常昂贵因为每个着色点都要进行多次光线步进。务必谨慎选择采样数。bias和阴影一样用于避免从表面内部开始采样导致的自遮挡。随机方向生成randomHemisphereDirection函数需要生成在法线n指向的半球内均匀或余弦加权的随机方向。一个简单的方法是先在一个单位球内生成随机点然后拒绝掉在错误半球dot(point, n) 0的点。为了提高效率也可以使用预计算的低差异序列如Halton序列来替代完全随机这可以用更少的采样获得更均匀的覆盖减少噪点。实操心得性能与质量的权衡SDF AO非常耗费资源因为它为每个像素增加了aoSamples * aoSteps次SDF查询。在实时的SDF渲染中全分辨率计算AO通常是不可行的。有几种优化策略降低采样数这是最直接的方法但会导致明显噪点。降低渲染分辨率先以半分辨率或四分之一分辨率计算AO再上采样。SDF对缩放相对不敏感这很有效。空间复用/缓存利用帧间连贯性复用上一帧的AO计算结果只对变化区域重新计算。仅在需要时计算对于平坦、开阔的区域AO贡献很小可以跳过计算或使用一个默认值。使用更快的近似有一种更廉价但效果尚可的“局部AO”近似它只使用法线方向和SDF在着色点附近的局部曲率信息来估算遮挡完全不需要随机采样。虽然物理不正确但对于风格化渲染可能够用。在最终着色时将AO系数ao乘到最终的漫反射颜色有时也影响高光上color * ao;。你会立刻看到物体的角落、接触面变得更深邃整个场景的层次感和重量感大幅提升。5. 完整项目搭建与核心代码解析现在让我们把所有这些模块组合起来构建一个完整的、交互式的SDF软阴影AO渲染器。我们将使用纯C并借助一个简单的图形库如SDL2 手动操作像素缓冲区或OpenGL的一个着色器来显示结果。为了聚焦于核心算法我选择用伪代码和关键函数片段来说明并在最后提供完整项目的获取方式。5.1 项目结构与依赖一个最小化的项目结构可能如下SDF_Renderer/ ├── src/ │ ├── main.cpp // 程序入口窗口和主循环 │ ├── renderer.h/cpp // 渲染器核心类包含SDF定义、光线步进、着色逻辑 │ ├── math/ // 向量、矩阵等数学工具类 (vec3.h等) │ └── utils/ // 随机数生成器、图像保存等工具 ├── assets/ // (可选) 纹理或其他资源 └── CMakeLists.txt // 构建配置核心依赖C编译器支持C11或以上我们需要chrono,random等。图形窗口库SDL2是一个轻量级、跨平台的选择用于创建窗口和处理输入。数学库强烈建议自己实现一个简单的vec3三维向量类。它需要支持基本的运算加、减、乘、除、点积、叉积、归一化等。这不仅是学习的一部分也能让你对底层计算有完全的控制。5.2 核心渲染循环在main.cpp的主循环中我们大致做以下几件事处理输入鼠标、键盘更新相机位置 (ro) 和朝向 (front,right,up用于构造射线方向rd)。清空像素缓冲区。对屏幕上的每一个像素(x, y) a. 根据相机参数计算从相机出发穿过该像素的射线方向rd。 b. 调用rayMarch(ro, rd)函数得到交点距离t。如果t 0计算交点p ro rd * t。 c. 计算交点处的法线n calcNormal(p)。 d. 计算软阴影系数shadow softShadow(p, lightDir, lightDist, softnessK)。 e. 计算环境光遮蔽系数ao calculateAO(p, n)。 f. 应用光照模型color (diffuse * shadow specular) * ao ambient。 g. 将color写入像素缓冲区对应的位置。将像素缓冲区的内容更新到窗口。循环。5.3 场景SDF函数示例在renderer.cpp中sceneSDF函数定义了我们的整个世界。这里是一个包含地面、几个球体和盒子的简单场景float Renderer::sceneSDF(const vec3 p) { float dist 1e10; // 初始化为一个大数 // 地面 (一个在y-1的平面) float ground sdPlane(p, vec3(0, 1, 0), 1.0); // plane: y -1 // 一个大球体 vec3 spherePos vec3(0, 0, 0); float sphere sdSphere(p, spherePos, 1.0); // 一个盒子 vec3 boxPos vec3(2, 0, 1); vec3 boxSize vec3(0.8, 1.2, 0.8); float box sdBox(p, boxPos, boxSize); // 一个圆环 (torus) vec3 torusPos vec3(-2, 0, 0.5); float torus sdTorus(p - torusPos, vec2(1.0, 0.3)); // 大半径1.0管半径0.3 // 利用min操作符合并所有物体 dist min(dist, ground); dist min(dist, sphere); dist min(dist, box); dist min(dist, torus); // 我们可以创建更复杂的形状比如一个“挖了洞”的盒子 // float boxWithHole sdSubtraction(box, sdSphere(p, boxPos, 0.5)); // dist min(dist, boxWithHole); return dist; }5.4 着色器函数整合在同一个类中我们的着色函数可能如下所示vec3 Renderer::shade(const vec3 ro, const vec3 rd, float t) { if (t 0) { return vec3(0.1, 0.2, 0.3); // 未命中返回背景色天空 } vec3 p ro rd * t; // 交点 vec3 n calcNormal(p); // 法线 // 光照参数 vec3 lightPos vec3(5, 5, 3); vec3 lightColor vec3(1.0, 0.9, 0.8); vec3 albedo vec3(0.8, 0.6, 0.4); // 材质颜色 // 计算光照方向与距离 vec3 lightDir lightPos - p; float lightDist length(lightDir); lightDir lightDir / lightDist; // 归一化 // 基础光照 float diff max(dot(n, lightDir), 0.0); vec3 viewDir normalize(ro - p); vec3 halfVec normalize(lightDir viewDir); float spec pow(max(dot(n, halfVec), 0.0), 32.0); // 添加软阴影 float shadow softShadow(p n * 0.02, lightDir, lightDist, 8.0); // 注意添加法线偏移 // 添加环境光遮蔽 float ao calculateAO(p n * 0.01, n); // 同样添加微小偏移 // 组合最终颜色 vec3 ambient vec3(0.03, 0.04, 0.06); vec3 diffuseTerm albedo * diff * lightColor * shadow; vec3 specularTerm vec3(0.3) * spec * lightColor * shadow; // 假设材质高光色为白色 vec3 color (diffuseTerm specularTerm) * ao ambient; // 可选应用简单的色调映射如Reinhard防止过亮 // color color / (color vec3(1.0)); return color; }5.5 性能优化初步一个朴素的实现每个像素进行上百次SDF查询可能会很慢。除了之前提到的减少AO/阴影采样数还有以下优化方向距离场加速结构对于复杂场景可以构建一个层次化的SDF如八叉树在查询时快速跳过空区域。利用SIMD指令现代CPU支持单指令多数据流可以同时对多个像素的SDF查询进行向量化计算。但这需要重写数学库和核心循环。GPU移植SDF光线步进是“令人尴尬的并行”问题每个像素独立非常适合GPU。将核心算法用GLSL或HLSL写成片段着色器性能将有数百倍的提升。这也是许多实时SDF演示如Shadertoy上的作品的做法。6. 常见问题、调试技巧与效果微调即使按照步骤实现了所有功能你可能还是会遇到各种奇怪的问题。这里汇总了一些常见坑点和调试技巧。6.1 视觉瑕疵与解决方案问题现象可能原因解决方案阴影痤疮物体表面出现散乱的黑点或条纹。阴影射线起点离表面太近浮点精度导致自相交。增加阴影射线起始偏移量 (rayDepth初始值)如从0.01加到0.05。在着色点p处沿法线方向稍微偏移 (p n * bias) 再发射阴影射线。AO噪点AO效果有明显的颗粒状噪点。采样数 (aoSamples) 太少。随机数质量差。增加采样数牺牲性能。使用低差异序列如Halton, Sobol替代纯随机获得更均匀的采样分布。对多帧结果进行临时累积TAA。阴影过软或丢失软阴影完全没有锐利边缘或者该有阴影的地方没有。软阴影函数中的k参数太大或太小。maxDist到光源距离计算错误。调整k值通常在4.0到64.0之间尝试。确保lightDist是着色点到光源的真实距离。检查光源是否在几何体内部。物体边缘闪烁或抖动当相机移动时物体边缘像在“游泳”。光线步进的EPSILON命中阈值设置不当。法线计算中的eps值不合适。微调EPSILON通常在1e-3到1e-4。法线计算的eps应与场景尺度匹配太大法线不准太小对浮点误差敏感0.001是常用值。性能极差帧率非常低。SDF函数过于复杂。AO/阴影采样数过高。主光线步进次数 (MAX_STEPS) 太多。简化场景SDF。大幅降低aoSamples(如到4) 和MAX_SHADOW_STEPS(如到16)。确保MAX_DIST最大视距设置合理不要太大。使用调试工具找出性能瓶颈。奇怪的“云状”或“块状”阴影阴影内部有不均匀的块状或云状图案。软阴影函数中min(res, k * dist / rayDepth)的dist可能为负值如果步进不小心穿入了物体内部。随机数函数有周期性。确保在软阴影步进中如果dist EPSILON应直接返回0.0。检查随机数生成器的种子和范围。6.2 调试工具可视化SDF与步进过程当渲染结果不符合预期时可视化中间数据是强大的调试手段。可视化SDF值修改着色器直接返回sceneSDF(p)的数值作为颜色映射到0-1范围。这可以让你看到距离场的等值面检查SDF函数是否正确。可视化步进次数将rayMarch函数中的步进次数i映射为颜色。这能直观显示哪些区域计算复杂步进次数多有助于性能分析和发现无限循环。可视化法线将计算出的法线(n.x, n.y, n.z)从[-1,1]映射到[0,1]并显示为颜色。不正确的法线会导致光照怪异这是检查SDF函数和法线计算精度的好方法。单独开关效果在代码中提供宏定义或运行时变量可以单独禁用软阴影或AO以隔离问题。6.3 艺术化控制与风格调整掌握了基础之后你可以通过调整参数来创造不同的艺术风格卡通风格将软阴影的过渡进行离散化使用floor或smoothstep进行分段将连续的阴影系数变为几个固定的色阶。水墨风格利用AO来强化轮廓和内部阴影同时使用非常柔和的软阴影极小的k值并可能结合边缘检测通过法线或深度突变。科幻风格使用高对比度的AO和锐利的阴影大的k值并给SDF表面添加一些程序化噪声或发光边缘。控制AO范围通过动态调整sampleRadius可以实现类似于“近视”或“远视”的效果近处细节AO强远处AO弱。实现一个完整的、交互式的SDF渲染器是一项非常有成就感的工程。它迫使你深入理解光线、几何和阴影的本质。虽然本文提供的代码框架是CPU端的但其中每一个概念都可以无缝迁移到GPU着色器中。当你看到自己用数学公式定义的世界被柔和的光影和真实的遮蔽所点亮时那种感觉是无与伦比的。