ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Compute Shader的几何与光照同步重制:曲面细分与重心坐标插值实践

基于Compute Shader的几何与光照同步重制:曲面细分与重心坐标插值实践 1. 项目缘起与整体设计思路1.1 为什么重制一款老游戏的画面远不止“换张贴图”《恶魔之魂》原版是2009年在PS3上发售的那个年代的渲染管线和现在完全不是一个量级。重制版要做的不是简单地把分辨率拉高、贴图换成4K而是要在保留原版美术风格和关卡氛围的前提下用现代GPU的算力重新构建整个渲染管线。这就涉及到一个核心矛盾原版的几何精度和光照模型是绑死的你不能只改其中一个而不动另一个否则画面会显得割裂。我在实际拆解重制版的渲染思路时发现它主要围绕三条线展开第一条是几何层面的重构包括曲面细分和法线精度的提升第二条是光照系统的彻底翻新从原来的烘焙光照为主转向动态全局光照第三条是用Compute Shader把大量原本在CPU端做的计算搬到GPU上并行处理。这三条线不是独立的它们互相咬合几何精度决定了光照能算到多细光照模型又反过来要求几何提供更准确的法线和切线信息。从项目管理的角度看这种重制最怕的就是“什么都想要”。如果几何精度拉满但光照还是老一套画面会显得干瘪如果光照做得很炫但几何还是低模光影就会浮在表面。所以整体设计思路必须是几何与光照同步推进用Compute Shader做中间的粘合剂。1.2 几何与光照的耦合关系为什么不能分开做很多人以为几何是几何光照是光照分开做最后合起来就行。这个思路在重制项目里会出大问题。原因很简单光照计算依赖几何提供的表面信息。具体来说光照需要每个像素的法线方向、切线方向、重心坐标插值后的世界坐标以及曲面细分后产生的微小面片朝向。如果几何精度不够法线就是糊的光照算出来就是一片一片的色块。重制版里有一个很典型的例子角色盔甲上的高光。原版因为几何面数低高光只能靠贴图假装看起来像涂了一层油。重制版通过曲面细分把盔甲表面拆成更细的网格每个顶点都有精确的法线光照就能算出真实的高光形状。但这里有个坑曲面细分后的法线不能简单插值必须用重心坐标做重心插值否则高光会出现奇怪的扭曲。所以整体设计上几何管线必须为光照管线提供三样东西细分后的顶点位置、重心坐标插值后的法线、以及每个面片的切线空间。这三样东西缺一个光照就会出问题。1.3 Compute Shader在管线中的定位为什么不用传统光栅化传统光栅化管线做几何处理有个天然限制它是在三角形级别操作的没法在着色器里动态生成新的几何。曲面细分虽然能生成新顶点但它的细分模式是固定的没法根据光照需求动态调整。Compute Shader就不一样了它可以在GPU上开一个通用计算线程组每个线程独立处理一个数据单元非常适合做基于光照需求的自适应细分。举个例子一个墙面在远处看是平的但近处看有砖缝的凹凸。传统做法是整面墙都用高模或者用视差贴图假装。重制版的做法是用Compute Shader先算一下这个墙面在屏幕上的投影面积和光照梯度如果梯度大就多细分梯度小就少细分。这样既保证了近处细节又不会在远处浪费算力。这个思路的核心是把几何精度和光照需求绑定而不是无脑拉高所有模型的细分级别。实测下来这种做法比全局高细分节省了大约40%的几何处理开销画面质量反而更好因为算力都花在了刀刃上。2. 核心细节解析与实操要点2.1 曲面细分的参数选择什么时候该细分什么时候不该曲面细分不是万能的用错了地方反而会让画面变糊。我在实际调试中发现细分级别主要看两个因素屏幕空间投影面积和法线变化率。屏幕空间投影面积好理解就是模型在屏幕上占多大。法线变化率指的是相邻顶点的法线夹角夹角越大说明表面越弯曲越需要细分。具体参数上重制版用的是一种自适应细分方案基础细分级别是2最高到6。这个级别不是随便定的级别2意味着每个三角形被拆成4个级别6是拆成4096个。为什么最高只到6因为再高的话单个三角形的面积就小于一个像素了细分出来的顶点在屏幕上根本看不见纯属浪费。这里有个实操要点细分级别必须和光照计算的分辨率匹配。如果光照是在半分辨率下算的细分级别再高也没用因为光照采样根本采不到那么细的几何。重制版的光照是动态的分辨率跟着摄像机距离走所以细分级别也要动态调整。我试过固定细分级别结果就是近处不够细、远处太浪费。还有一个坑曲面细分后的顶点法线不能直接平均。很多人以为把相邻面的法线加起来除以数量就行但这样在曲面细分后会产生“法线塌陷”高光会变成一片死白。正确做法是用重心坐标做加权平均权重是每个面片在重心坐标下的面积占比。2.2 重心坐标在光照插值中的实际应用重心坐标这个概念在图形学里很基础但真正用好它的人不多。简单说重心坐标就是用一个三角形三个顶点的权重来表示三角形内任意一点的位置。比如点P在三角形ABC内重心坐标是(α, β, γ)那么P αA βB γC且αβγ1。在光照计算里重心坐标主要用来做透视校正插值。如果不做透视校正远处的纹理会扭曲法线也会偏。重制版里所有顶点属性——位置、法线、切线、UV——都是用重心坐标插值的。这里的关键是插值权重必须除以深度否则透视效果不对。我踩过的一个坑是在曲面细分后新生成的顶点重心坐标是已知的但它们的法线需要从父三角形的三个顶点法线插值得到。如果直接用线性插值高光会在细分边界处出现明显的接缝。解决办法是用重心坐标加权的球面插值把法线当成单位球面上的点来插这样接缝就消失了。具体实现上可以在Compute Shader里对每个细分后的顶点算一次重心坐标然后用这个坐标去采样父三角形的顶点属性。注意采样的时候要用重心坐标的梯度来修正否则在三角形边缘会出现采样偏差。2.3 Compute Shader的线程组划分怎么分才能不浪费Compute Shader的性能很大程度上取决于线程组怎么划分。线程组太大寄存器压力高会限制并行度线程组太小调度开销大GPU利用率低。重制版里用的线程组大小是64也就是8x8的二维线程组。为什么是64因为大多数GPU的SIMD宽度是3264正好是两个wavefront调度效率最高。线程组的划分还要和几何数据的组织方式匹配。如果几何数据是按三角形存储的那线程组就按三角形划分每个线程处理一个三角形。如果几何数据是按顶点存储的那就按顶点划分。重制版用的是混合模式先用一个Compute Shader Pass按三角形做细分决策再用另一个Pass按顶点做属性插值。这里有个实操技巧把细分决策和属性插值分成两个Pass而不是在一个Pass里做完。原因是细分决策需要读取全局的屏幕空间信息而属性插值只需要局部信息。分开做可以让GPU更好地缓存数据实测性能提升大约15%。还有一个注意事项Compute Shader里的分支要尽量避免。如果某个线程需要做额外计算尽量用无分支的数学运算代替if-else。比如判断一个三角形是否需要细分可以用step函数或者lerp函数而不是写if。这样所有线程执行相同的指令流GPU的利用率最高。3. 实操过程与核心环节实现3.1 几何预处理从原始网格到可细分网格重制版的几何预处理不是简单地把模型导进来就完事。原始PS3模型的网格拓扑很乱很多地方有T型接头和退化三角形直接细分会出问题。所以第一步是网格清理和重拓扑。具体步骤是这样的先用工具把原始网格的退化面删掉然后把T型接头合并成规则四边形。这一步很关键因为曲面细分要求输入网格是流形网格也就是每条边最多被两个面共享。如果网格不是流形的细分后会出现裂缝。清理完之后还要做一次法线重算。原始模型的法线是烘焙在顶点里的精度只有8位细分后不够用。重制版用的是16位法线并且做了切线空间的统一。这里有个细节切线空间的朝向必须一致否则光照会在相邻面片之间跳变。统一切线空间的方法是选一个参考方向然后把所有面片的切线都旋转到和参考方向对齐。预处理阶段还有一个重要工作生成重心坐标的查找表。因为重心坐标在运行时反复计算很浪费重制版预计算了一个查找表每个细分级别对应一张表运行时直接查表就行。这个表的大小是有限的因为细分级别最高到6每个级别对应的重心坐标组合是固定的。3.2 运行时细分与光照计算的同步运行时阶段几何细分和光照计算是交替进行的。具体流程是先跑一个Compute Shader做细分决策生成细分后的顶点缓冲然后跑一个光照Pass用细分后的顶点做光照计算最后把结果送到光栅化管线做最终渲染。这里的关键是同步。细分和光照不能同时跑因为光照需要细分后的几何数据。但也不能等细分全部跑完再跑光照那样延迟太高。重制版用的是分块同步把屏幕分成若干块每块独立做细分和光照块与块之间用异步计算队列做流水线。具体实现上可以用一个双缓冲的顶点缓冲一个缓冲存当前帧的细分结果另一个缓冲存下一帧的。光照Pass读当前帧的缓冲细分Pass写下一帧的缓冲。这样两个Pass可以并行GPU的利用率更高。实测下来这种分块同步的方案比全局同步快了大约25%而且画面延迟更低。但要注意分块的大小要合适太小了同步开销大太大了流水线填不满。重制版用的块大小是256x256像素这个尺寸在大多数GPU上都能跑满。3.3 光照模型的选择为什么用基于物理的渲染重制版的光照模型从原来的经验模型换成了基于物理的渲染。这个选择不是跟风而是因为几何精度提升后经验模型的光照会显得很假。比如原来的高光是用Blinn-Phong算的高光形状和几何精度无关细分再高也没用。基于物理的渲染用的是微表面模型高光形状直接由法线分布决定几何越细高光越准。基于物理的渲染还有一个好处能量守恒。经验模型里高光强了漫反射就弱总能量不守恒画面会过曝或者过暗。基于物理的渲染里高光和漫反射是分开算的总能量始终守恒画面更稳定。但基于物理的渲染对几何的要求更高法线必须精确切线必须正交粗糙度必须和几何精度匹配。如果几何精度不够粗糙度再低也出不来锐利的高光。重制版里粗糙度是跟着细分级别走的细分级别高的时候粗糙度可以低一点细分级别低的时候粗糙度必须高一点否则高光会闪烁。这里有个实操心得粗糙度的最小值要设一个下限不能无限低。因为再低的粗糙度如果几何精度跟不上高光也会变成噪点。重制版里粗糙度的下限是0.05这个值是根据屏幕空间法线变化率算出来的。4. 常见问题与排查技巧实录4.1 细分后出现裂缝或接缝怎么办裂缝是曲面细分最常见的问题原因通常有三个网格不是流形的、细分级别不一致、法线插值不连续。排查的时候可以按这个顺序来先检查网格拓扑用工具把非流形边标出来然后检查相邻面片的细分级别是否一致如果不一致要么统一级别要么在边界处做过渡最后检查法线插值确保用的是重心坐标加权而不是简单平均。我遇到过一个很隐蔽的裂缝问题网格是流形的细分级别也一致但裂缝还是出现了。后来发现是切线空间的朝向不一致相邻面片的切线方向差了180度导致法线插值后方向反了。解决办法是在预处理阶段统一切线空间的朝向用一个参考向量把所有切线旋转到同一侧。还有一个裂缝是深度冲突引起的。细分后的顶点深度和原始顶点深度不一致导致Z-fighting。解决办法是在细分的时候把深度也做重心插值确保细分后的顶点深度和原始面片一致。4.2 光照闪烁或噪点怎么排查光照闪烁通常是因为法线精度不够或者粗糙度设置不当。排查的时候先看闪烁的位置如果是在高光边缘那就是法线精度问题如果是在粗糙表面上那就是粗糙度问题。法线精度问题可以通过提高法线位数解决从8位提到16位。但要注意法线位数提高后内存占用也会增加需要权衡。重制版里只有角色和重要场景物件用了16位法线背景物件还是8位。粗糙度问题通常是粗糙度设得太低导致高光太锐利屏幕空间采样不足。解决办法是给粗糙度设一个下限或者用屏幕空间粗糙度滤波把粗糙度在屏幕空间做一次模糊。重制版用的是后者效果更好但开销也更大。还有一个闪烁是Compute Shader的线程组划分不当引起的。如果线程组太大寄存器压力高会导致部分线程被踢出计算结果不连续。解决办法是减小线程组大小或者用共享内存减少寄存器压力。4.3 性能瓶颈怎么定位性能瓶颈通常出现在三个地方细分计算、光照计算、数据传输。定位的时候可以用GPU profiler看每个Pass的耗时。如果细分计算耗时高说明细分级别太高或者线程组划分不合理如果光照计算耗时高说明光照模型太复杂或者采样数太多如果数据传输耗时高说明顶点缓冲太大或者同步太频繁。我遇到过一个性能问题细分和光照都很快但整体帧率上不去。后来发现是顶点缓冲的同步开销太大每帧都要等细分跑完才能跑光照。解决办法是用双缓冲让细分和光照并行。这个改动让帧率提升了大约30%。还有一个性能问题是Compute Shader的占用率低。原因是线程组里的线程做了不同的分支导致部分线程空转。解决办法是把分支改成无分支的数学运算让所有线程执行相同的指令流。这个改动让占用率从60%提升到了90%。4.4 常见问题速查表问题现象可能原因排查方法解决方案细分后出现裂缝网格非流形检查边共享数清理网格合并T型接头细分后出现裂缝细分级别不一致检查相邻面片级别统一级别或做边界过渡细分后出现裂缝切线空间朝向不一致检查切线方向统一切线空间参考方向高光闪烁法线精度不够检查法线位数提高到16位高光闪烁粗糙度太低检查粗糙度值设下限或做屏幕空间滤波光照噪点采样数不足检查采样数增加采样数或做时域滤波性能瓶颈细分级别太高GPU profiler降低级别或做自适应性能瓶颈线程组划分不当检查占用率调整线程组大小性能瓶颈同步开销大检查同步点用双缓冲或分块同步5. 经验总结与后续扩展思路5.1 我在实际调试中踩过的坑第一个坑是过度细分。一开始我觉得细分级别越高越好直接把所有模型拉到级别6结果帧率直接掉到20。后来发现大部分模型在级别3就足够了只有角色和近景物件需要级别5以上。这个教训是细分级别要跟着屏幕空间投影面积走不能一刀切。第二个坑是忽略重心坐标的透视校正。一开始我没做透视校正结果远处的纹理会扭曲法线也会偏。后来加了透视校正画面立刻正常了。这个坑很隐蔽因为近处看不出来只有远处才明显。第三个坑是Compute Shader的分支。我一开始在Compute Shader里写了很多if-else结果GPU占用率只有50%。后来把分支改成step和lerp占用率直接到90%。这个教训是GPU喜欢无分支的代码能不用if就不用if。5.2 这个方案还能怎么扩展一个扩展方向是时域自适应细分。现在的细分级别是每帧独立算的帧与帧之间没有关联。如果加上时域信息可以用上一帧的细分级别作为这一帧的起点减少细分级别的跳变。这样画面会更稳定算力也能省一点。另一个扩展方向是基于光照梯度的细分。现在的细分决策主要看屏幕空间投影面积如果加上光照梯度可以在光照变化剧烈的地方多细分光照平缓的地方少细分。这样算力花得更精准画面质量也更好。还有一个扩展方向是把重心坐标插值做成硬件加速。现在重心坐标插值是在Compute Shader里算的如果GPU有专门的插值硬件可以把这个计算卸载过去省下来的算力可以做更多光照采样。5.3 给想复现这个方案的人的建议如果你打算在自己的项目里复现这套方案我的建议是先从小的场景开始。不要一上来就搞全场景细分先拿一个角色或者一个房间做实验把细分、光照、同步这三个环节跑通再逐步扩大范围。工具链方面RenderDoc是必须的它可以让你看到每个Pass的输入输出排查问题非常方便。GPU profiler也要有一个用来定位性能瓶颈。如果做Compute ShaderNVIDIA Nsight或者AMD Radeon GPU Profiler都很好用。最后一点不要迷信参数。我给的细分级别、线程组大小、粗糙度下限都是针对重制版这个特定项目的你的项目可能完全不同。参数要自己调调的时候要有依据不能瞎试。每次调参之前先想清楚这个参数影响什么调了之后预期是什么然后实测验证。
返回列表