屏幕空间反射(SSR)实战:原理、GLSL实现与调优技巧

发布时间:2026/10/5 13:58:45
屏幕空间反射(SSR)实战:原理、GLSL实现与调优技巧 屏幕空间反射SSRScreen Space Reflections是我做实时渲染时最喜欢的反射方案。它不需要预烘焙、不依赖场景复杂度只从当前帧画面里反复采样就能给水面、金属表面、湿地板这些材质做出足够可信的反射效果。对于做游戏画面、三维可视化、工业预览的朋友来说SSR是一个投入产出比很高的技术——美术想要的效果大部分它都能顶住引擎内置实现能直接用想改造也留有很多余地。这篇文章不推复杂公式只讲我落地SSR时踩过的坑、试出来的参数习惯以及可以直接抄走的GLSL实现逻辑。1. SSR的思路与选型逻辑1.1 先聊聊反射这件事为什么难做实时渲染里的反射是个老难题。真实世界里光打到物体表面再进入摄像机中间经历的是一次完整的“光线弹射”。实时渲染要模拟这种弹射计算量经常指数级上升。之前做项目时美术想要光滑大理石地面倒映出天花板灯带我第一反应不是上SSR而是先算算反射探针够不够用。反射探针Reflection Probe是早期标准方案本质是在场景某个点捕获周围环境到立方体贴图然后当作反射源。优点是速度快、可控性强缺点也很明显——它只能反射出“探针捕捉到的东西”对于动态物体、近距离物体画面很容易对不上。比如一个士兵走过大理石地面探针如果放在房间中央地面反射里的士兵位置和真实位置经常差半米穿帮严重。平面反射Planar Reflection能做得很精准但它会把场景再画一遍开销大而且只适合平面表面遇到曲面就抓瞎。SSR走的是另一条路不额外重构场景而是利用当前帧屏幕里已经渲染好的深度、法线、颜色信息把这些信息当成一个“场景切片”然后从靠近相机的那层像素出发半条反射轨迹在屏幕空间里进行射线步进。说白了它是在2D屏幕上做“伪光线追踪”。因为用的就是当前帧画面所以动态物体、粒子、角色位置可以天然和反射对齐不存在“探针数据过期”的问题。1.2 为什么SSR值得优先考虑使用SSR最大的好处是廉价。它不需要重新绘制几何体也不维护额外的场景加速结构核心计算就是一堆纹理采样和比较运算。在延迟渲染里深度和法线本来就有现成的G-BufferSSR直接捡起来用管线集成非常顺。另一个优势是效果真实。由于它采样的是当前帧的最终画面反射里能带上其他后处理效果比如雾、激光、粒子爆闪这在美术调画面时很有价值。传统探针反射经常“缺少灵魂”因为和最终画面中间隔着各种光照和特效堆叠反射颜色经常偏平。SSR则基本零学习成本所见即所得。但SSR也有硬伤它只能反射屏幕内“能看见”的内容。如果一个物体在画面外、被墙挡住、或者屏幕边缘外那它不可能出现在反射里。所以实际项目中几乎没有纯SSR的方案大家都是“SSR打底探针补漏”最终反射结果用探针颜色做远端插值。这也是我后来做品质优化时最常用的一招。很多学图形学的朋友容易陷入“我要把SSR做到完美”的误区最后发现屏幕空间本身的限制是解决不了的。我的态度是把SSR当成“锦上添花”而不是“唯一真理”。只要它能大幅度提升主要视觉区域边缘那些反射丢失问题就用混合去掩盖这才是工程心态。2. 核心细节与实现要点2.1 从深度纹理重建像素位置SSR第一步是知道当前像素在三维空间里在哪以及视线朝哪个方向。通常延迟渲染里都有深度纹理里面存的是NDC标准化设备坐标深度值需要解算出世界空间或视空间坐标。我习惯在视空间里做步进因为相机在原点反射向量算起来直观步进方向也好控制。重建视空间坐标有标准做法拿到屏幕坐标uv和深度值d先转换成NDC坐标xyz范围在[-1, 1]然后利用投影矩阵的逆矩阵算出视空间坐标。代码看起来是这样vec3 reconstructViewPos(vec2 uv, float depth) { // uv: [0,1] 屏幕坐标, depth: 线性或非线性深度 vec4 ndc vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); vec4 viewPos inverseProjectionMatrix * ndc; return viewPos.xyz / viewPos.w; }很多引擎为了省性能不会真用逆矩阵乘而是用一个近似的“在近裁剪面插值”方法。因为视空间里射线在屏幕上投影是线性的所以可以用屏幕空间四个角的方向向量做插值再乘以深度。这个优化在小规模场景里效果不明显但VR和移动端很吃性能值得做。需要注意的是深度纹理的存储格式。很多引擎把深度变成非线性的存到16位上下。如果直接在步进时拿这种深度去比较会出现近处精度高、远处精度低的偏差导致反射命中位置漂移。我一般会先做一步“线性化”处理把depth从[0,1]变成视空间里的实际z值方便后面做距离判断。线性化公式网上很多本质是用透视投影的近远裁剪面还原。给一个常见版本float linearDepth(float depth, float near, float far) { float z depth * 2.0 - 1.0; return 2.0 * near * far / (far near - z * (far - near)); }这套手续费不了多少性能但对后续步进命中判定的稳定性帮助很大。2.2 步进距离和方向怎么定拿到像素的世界/视空间位置后就要算出反射方向。如果法线纹理里存的是世界空间法线直接用reflect(viewDir, normal)就行如果法线在切线空间需要先变换。在视空间里视线方向是normalize(-viewPos)然后反射向量就是用标准reflect公式。注意这里法线必须和视线在同一空间否则反射方向会有肉眼可见的偏差。确定方向后SSR最常见的实现是“沿着反射向量往前迈步每次迈步后把三维坐标投影到屏幕uv然后采样深度纹理比较当前深度和步进点深度”。如果步进点深度更靠近相机且和实际深度足够接近就认为射线命中了几何表面然后取该uv处的颜色作反射色。伪代码如下vec3 stepDir normalize(reflectDir); vec3 curPos startPos stepDir * minDist; float stepLen 0.05; // 步长需要自适应 for (int i 0; i maxSteps; i) { vec4 clipPos projectionMatrix * vec4(curPos, 1.0); vec2 uv clipPos.xy / clipPos.w * 0.5 0.5; // 超出屏幕范围就放弃 if (uv.x 0.0 || uv.x 1.0 || uv.y 0.0 || uv.y 1.0) break; float sceneDepth texture(depthTexture, uv).r; float sampledZ -curPos.z; // 看坐标系约定 float delta sampledZ - linearDepth(sceneDepth); if (abs(delta) hitThreshold) { // 命中 return texture(colorTexture, uv).rgb; } curPos stepDir * stepLen; }这段代码能跑但实际项目里不能用固定步长。因为屏幕空间里近处物体变化快远处物体变化慢固定步长会导致近处漏检、远处过采样。更合理的做法是根据视空间深度动态调整步长离相机越远步长越大离相机越近步长越小。也可以直接使用“线性视空间步长”然后投影到屏幕空间后检查进度。另一个容易被忽略的点是起始位置。如果射线从像素位置本身出发第一个步进点可能和几何体重合造成自遮挡空洞。建议给起始位置加一个很小的偏移量比如沿法线方向偏移一点点。这个偏移量不能太大否则细缝里的反射会消失太小又会有自交噪点。通常取0.001 * viewDepth左右比较稳妥。2.3 HZB加速和二分命中优化纯线性步进效率很低。maxSteps设少了反射全是断断续续的“斑马纹”设多了性能又扛不住。我推荐的加速方案是层级Z缓冲HZBHierarchical Z-Buffer原理就是把深度纹理做成一整套mipmap链每个层级记录该区域内离相机最近或最远的深度。步进时先从粗层级开始如果步进点深度比该层级的深度记录还远说明还没碰到任何场景直接跳一大步如果碰到就降一层继续精细判断直到命中。这个思路和“先看地图找大方向再缩小范围找具体街道”一样。在2K分辨率下用HZB可以把平均步进次数从100降到20~30画面质量反而更稳定因为算法能自适应地形复杂度。二分命中优化是配合HZB使用的。当你在某一层发现当前深度已经越过场景深度就“回退”到上一层缩小步长继续迭代。通过反复“前进过头再回退一半”可以逐步逼近真实交点。这比纯向前步进能更好处理薄片物体比如水面上的落叶、栅栏。实际编码时HZB的命中判断不能只看单个采样点否则物体边缘会有很重的锯齿。可以用一个类似“最近深度”的mip步进点前后各取若干深度范围比较允许一定余量。这个余量会直接影响视觉观感调小了反射出现虫洞调大了反射泛白模糊。我个人喜欢把HZB计算放在预处理阶段用一次计算着色器生成层级深度。然后在SSR的步进循环里通过textureLod(depthTexture, uv, mipLevel)采到指定层级的深度用这个和步进点的深度做比较。mipLevel并不是固定的而是根据当前uv区域和步进过程动态调整。3. 实操一版可用的SSR实现3.1 输入输出和渲染流程在谈代码前我们先对齐接口。SSR作为后处理或G-Buffer之后的pass一般需要这些输入颜色纹理最终光照后的帧缓冲颜色深度纹理场景深度带mipmap法线纹理世界空间或视空间相机投影矩阵、逆投影矩阵从颜色纹理分辨率映射到SSR渲染分辨率输出是一张“反射颜色图”可带透明度和置信度后面再接一个反射混合pass和屏幕反射探针结果做插值。这样逻辑解耦出了问题方便排查。在延迟渲染里法线纹理可以复用G-Buffer中那张normals省一次采样。如果做前向渲染则需要额外生成法线图和深度图成本稍微高一点。我的建议是SSR尽量配合延迟渲染使用不然投入的成本会让美术皱眉头。3.2 关键代码与逐段说明这里给一段精简但能工作的GLSL片段基于视空间实现。为了阅读方便我拆成几个子函数。uniform sampler2D colorTexture; uniform sampler2D depthTexture; uniform sampler2D normalTexture; uniform mat4 projectionMatrix; uniform mat4 inverseProjectionMatrix; // 由uv和深度重建视空间坐标 vec3 viewPosFromUV(vec2 uv, float depth) { vec4 ndc vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); vec4 viewPos inverseProjectionMatrix * ndc; return viewPos.xyz / viewPos.w; } // 视空间坐标投影到屏幕uv vec2 uvFromViewPos(vec3 viewPos) { vec4 clipPos projectionMatrix * vec4(viewPos, 1.0); return (clipPos.xy / clipPos.w) * 0.5 0.5; } vec3 ssr(vec2 uv, float depth) { vec3 viewPos viewPosFromUV(uv, depth); vec3 normal texture(normalTexture, uv).xyz; // 约定视空间摄像方向朝-zviewDir是normalize(-viewPos) vec3 viewDir normalize(-viewPos); vec3 reflectDir reflect(-viewDir, normal); // 起始偏移一点防止和自身相交 vec3 curPos viewPos normal * 0.002 * abs(viewPos.z); vec3 stepVec reflectDir * 0.02 * abs(viewPos.z); for (int i 0; i 64; i) { curPos stepVec; vec2 sampleUV uvFromViewPos(curPos); if (sampleUV.x 0.0 || sampleUV.x 1.0 || sampleUV.y 0.0 || sampleUV.y 1.0) { break; } float sceneDepth linearizeDepth(texture(depthTexture, sampleUV).r); float sampleDepth -curPos.z; // 视空间z为负 float depthDelta sampleDepth - sceneDepth; if (depthDelta 0.01 depthDelta -0.05) { // 命中 return texture(colorTexture, sampleUV).rgb; } // 如果步进点已经在场景深处说明越过表面需要回退修正 if (depthDelta -0.05) { curPos - stepVec * 0.5; stepVec * 0.5; } } return vec3(0.0); }这个版本每一步都做一次投影变换性能不是最优但胜在逻辑清楚。实际项目可以改成一次“屏幕空间射线步进”算法直接在屏幕空间推UV不反复做向量与矩阵运算性能能提升一个量级。depthDelta的上下阈值很关键。我这里的0.01表示步进点深度略大于场景深度也就是步进点刚好在场景表面的后方一点点这个通常被认为是命中-0.05表示步进点比场景深度更靠近相机如果大得比较明显说明射线已经穿透表面需要回退。具体数值因场景尺寸而异建议做成材质参数方便不同关卡调整。3.3 参数调优与质量档位我做过一组参数组合用来匹配不同目标帧率下的画质。不需要每个项目都一样但可以作为起跳点档位最大步进次数初始步长系数深度命中阈值HZB层级目标平台低160.030.02无HZB固定深度偏移手机低端中320.020.01使用2级mip手机中高端 / 主机高640.010.005使用4级mip桌面超高1280.0050.003全mip 二分细化高端PC步长系数配合视空间深度会变化所以不同的视角和场景规模对参数很敏感。我的经验是先固定步进次数把步长调大直到出现明显断层再往回降低三档然后调阈值到反射边缘出现黑色裂缝前往回放松一点点。整个过程在动态场景里反复晃相机千万别只盯静止画面看。SSR边缘的柔和度也值得调。通常反射命中后不能直接当作最终颜色要根据命中点uv离屏幕边缘的距离做fade否则屏幕边缘会突然“断反射”。另外命中点离起点越远信息越不可信也要做距离衰减。这两个衰减曲线直接影响观感我做项目时常把edgeFade和distanceFade单独放开给美术调。4. 常见问题与排查技巧4.1 反射边缘断裂和噪点反射边缘疯狂闪、断成一条条噪点带是我见过最多的问题尤其是在阳光下的金属表面。原因通常是步进次数不够、步长固定导致步进点跨越了表面却没能采样到。关键在于让步进自适应用HZB或者动态步长让近距离小步走远距离大步跳。加了HZB之后依旧有少量噪点可以在命中处做“邻域遮罩检测”——检查命中点uv和初始uv之间是否被凸起物体遮挡。具体做法是在命中点往回反弹半步再投影采样一次深度看是否一致。如果落差大就把该采样标记为无效用探针颜色兜底。这个技巧在曲面高光反射里很有用。另一个造成噪点的情况是法线贴图高频细节太强。法线波动导致反射方向剧烈变化屏幕空间里步进路径会弯曲即使每个点的深度都对反射方向却对不齐。这种情况我一般会把法线贴图的mip level调高一点或者对法线做一次3x3模糊。效果立竿见影代价是反射细节损失一点。4.2 屏幕边缘的反射消失和物体后方的漏光SSR的信息来源是当前帧画面所以屏幕外物体反射不出来是正常现象。但屏幕上方的动态物体、角色背后的路灯如果反射消失了会特别突兀。我的处理是给SSR增加一个“回退颜色”的混合逻辑在距离衰减和边缘衰减为0的地方不要直接取黑色而是取反射探针颜色做混合。这样就算反射没命中颜色也不会发黑。物体后方漏光指的是反射命中点其实在另一个物体的背面但由于屏幕空间信息没有几何连续性射线穿过去了拿到一个不该看到的颜色。这个问题在薄墙、栏杆、树叶场景尤其严重。解决办法是限制最大反射距离并加“背面剔除”检查在命中点处再采样法线判断反射射线与表面法线方向是否大致相反如果方向不合理就放弃命中。我做过对比只加这一道检查能把“物体后方漏光”的伪影减少90%以上代价是多了两次纹理采样。但要注意有些材质比如玻璃希望在光线穿透后还反射后面的颜色所以这道逻辑需要做成可选。4.3 性能瓶颈与移动端适配SSR的性能大头在步进循环里的纹理采样和矩阵运算。在PC上64步加上HZB完全没压力但到了移动端尤其是OpenGL ES 3.0的设备每一步都可能是压死帧率的最后一根稻草。移动端常用这几种省钱方式降低SSR缓冲区的分辨率比如按半分辨率甚至四分之一分辨率计算再做上采样。反射是一种低频信息用低分辨率计算视觉差异不大。限制最大步进次数。移动端给24次以内比较稳超过32次在部分芯片上可能掉帧。放弃HZB回退改用固定步长加深阈值。移动端GPU纹理采样带宽有限HZB需要来回读不同层的mip反而可能更慢。用“双边滤波”代替“大范围高斯滤波”做反射颜色平滑。双边滤波能在保持边缘的同时消除大部分步进噪声。我之前在骁龙8系上跑过中档效果320x180的SSR缓冲24步开启双边滤波整体开销在1.2ms左右放在后处理管线里完全能接受。如果无脑上2K全分辨率和64步开销直接飙到4ms以上帧率立刻破相。4.4 屏幕空间深度压缩带来的近裁剪面失真最后一个我特别想提醒的坑靠近近裁剪面的物体会产生巨大深度差SSR在屏幕边缘容易出现严重失真。这是因为透视投影把大量精度给了近处深度值变化率极大固定步长根本追不上。如果你发现靠近画面边缘的反射像被拉伸成一坨糊状大概率是近裁剪面太近导致的。解决办法有两个一是调大近裁剪面从0.01调成0.1很多漂浮感会消失二是把深度线性化后用视空间距离比较而不直接用原始深度。我在做第一人称项目时把相机近裁剪面从0.05改成0.5后SSR整体稳定度提升不少代价是贴近物体的细节会有裁剪需要在地形碰撞上做补偿。另外要留意屏幕尺寸和FOV变化。不同宽高比下屏幕边缘衰减的系数要重新计算。用UV坐标算边缘距离时最好乘上一个宽高比修正系数否则超宽屏上左右两侧的反射范围会偏窄。总之SSR是个越用越有心得的技术。有人在网上问“为什么我的SSR反射会有黑条”多半是阈值和步长不匹配有人问“为什么SSR画面很花”通常需要滤波和降分辨率。最终效果好坏其实不是靠某个大招而是把深度线性化、HZB、命中衰减、边缘融合这一串细节都做对了反射自然就扎实了。自己从零实现一遍之后再去理解商业引擎里的SSR参数会觉得整个选项都变得亲切起来了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询