反射不真实通常不是光照模型错误,而是反射数据来源与场景几何不匹配。传统环境贴图把周围环境视为无限远,采样方向固定,物体移动后反射内容不会随之变化;SSR只利用屏幕已有像素,能捕捉近处动态,却会丢失屏幕外和背面的信息。理解两者的数据生成方式,是解决反射问题的基础。

一、反射不真实的核心原因:数据来源与坐标空间
在PBR管线中,反射向量决定采样方向。环境贴图多基于世界空间或局部空间预生成,假设反射环境处于无穷远;实际场景中,桌面、地面、角色等物体距离反射表面很近,如果不加修正,反射内容会与实际位置产生偏差。例如一个金属球放在地板上,使用球谐或立方体贴图时,球底反射的永远是天空盒下缘,而不是地板的颜色,造成明显的漂浮感。
屏幕空间反射则把反射来源改为当前帧渲染结果。它是在屏幕空间进行光线步进,逐像素沿反射方向检测深度缓冲,命中后读取颜色缓冲。这样做的好处是反射内容天然包含场景中的动态物体、角色和近处表面。但它也引入新的限制:反射方向指向屏幕外时无法获取颜色,背向相机或遮挡区域的信息也会缺失。因此,实际项目中往往将两者组合,用SSR补充近距离反射,环境贴图兜底远处与屏幕外部分。
从实现角度看,两者最大的区别是空间域不同:环境贴图在纹理域采样,SSR在屏幕空间做逐像素搜索。前者可以通过预滤波的radiance图实现稳定性能,后者受场景复杂度和分辨率影响较大。
二、环境贴图实现与局部化修正
环境贴图通常使用立方体贴图(cube map),加载HDR或LDR图像,通过反射向量R采样。GLSL中核心代码类似:
vec3 reflectDir = reflect(-viewDir, normal); vec3 envColor = texture(cubemap, reflectDir).rgb;
上面代码中viewDir是从片元指向摄像机的方向,normal为世界空间法线。对于PBR材质,还需使用预卷积的mip级别采样来模拟粗糙度,但直接采样就能得到基础反射。如果场景中存在局部光源或动态物体,纯环境贴图会忽略它们。
一种改进是使用局部反射探针(local reflection probe),将其放置在场景关键位置,利用盒投影(box projection)修正反射方向。盒投影把反射射线与探针包围盒求交,将采样方向从无限远约束到盒体内,从而让反射内容看起来来自场景中的真实区域。着色器代码示例:
vec3 boxProjectReflect(vec3 reflDir, vec3 pos, vec3 boxMin, vec3 boxMax) {
vec3 firstPlane = (boxMax - pos) / reflDir;
vec3 secondPlane = (boxMin - pos) / reflDir;
vec3 furthestPlane = max(firstPlane, secondPlane);
float dist = min(min(furthestPlane.x, furthestPlane.y), furthestPlane.z);
vec3 intersectPos = pos + reflDir * dist;
return intersectPos - probePos;
}
这段代码通过射线与AABB求交,返回指向交点的向量,再作为立方体贴图的采样方向。这样能缓解反射物体与地面脱离的问题,但盒投影不能反映动态遮挡,角色走过金属地板时反射中依然缺少角色。
此外,环境贴图的分辨率与预滤波质量也会影响真实感。低分辨率立方体贴图会使反射边缘模糊,HDR高光强度不足会导致金属表面发灰。工程上常使用多级mip的radiance贴图配合BRDF LUT,但这些都是静态反射数据,无法解决动态场景的根本问题。
三、屏幕空间反射SSR原理与Shader实现
SSR在延迟渲染或前向渲染的后期阶段执行,核心思想是:对每个需要反射的像素,从片元位置沿反射方向在屏幕空间进行步进,比较当前射线深度与场景深度,找到相交点后读取颜色。最简单的实现是固定步长线性光线步进。
vec2 ssrTrace(vec3 pos, vec3 reflDir, float stepSize, int maxSteps) {
vec3 rayPos = pos;
for (int i = 0; i < maxSteps; i++) {
rayPos += reflDir * stepSize;
vec4 rayScreen = projection * vec4(rayPos, 1.0);
rayScreen.xy /= rayScreen.w;
rayScreen.xy = rayScreen.xy * 0.5 + 0.5;
float sceneDepth = texture(depthTex, rayScreen.xy).r;
float rayDepth = rayScreen.z / rayScreen.w;
if (rayDepth > sceneDepth + 0.0001) {
return rayScreen.xy;
}
}
return vec2(-1.0);
}
固定步长的问题很明显:步长太小性能差,步长太大容易错过细小的深度变化,产生反射断裂。工程中更常用分层步进与二分细化结合。先把场景深度生成mip链,在较粗mip上快速找到可能相交区间,再在细mip上二分逼近真实交点,这样能在有限步数内获得较精确结果。
SSR命中后需要处理边缘和置信度。命中点越靠近屏幕边缘,越容易因屏幕外信息缺失而出现拉伸。可以用命中像素的UV偏离屏幕中心程度或与边缘距离来淡出SSR,过渡到环境贴图。颜色采样时还应加入粗糙度模拟:粗糙表面反射模糊,可对命中点周围进行多次抖动采样或使用预滤波深度。
Shader示例中,命中后读取颜色并进行淡出:
vec2 hitUV = ssrTrace(worldPos, reflectDir, 0.15, 64);
float edgeFade = smoothstep(0.15, 0.0, min(hitUV.x, 1.0 - hitUV.x)) *
smoothstep(0.15, 0.0, min(hitUV.y, 1.0 - hitUV.y));
vec3 ssrColor = texture(colorTex, hitUV).rgb * edgeFade;
实际项目中还会根据材质金属度、粗糙度和菲涅尔项决定SSR贡献权重。粗糙度越高,反射越模糊,用高mip的环境贴图更合适;金属度越高,反射颜色越接近反射物体本身,而不是漫反射颜色。这些权重需要与环境贴图结果做统一混合。
四、环境贴图与SSR混合策略及优化
混合两者的目标是在视觉上无缝过渡:SSR覆盖近处、动态、屏幕内的反射;环境贴图补充远处、屏幕外和背面内容。混合公式可以写为:
vec3 finalReflection = mix(envReflection, ssrReflection, ssrVisibility * ssrConfidence);
其中ssrVisibility是边缘淡出和命中成功的乘积,ssrConfidence可以基于命中距离、步进次数或命中点粗糙度。当SSR未命中或置信度低时,自动回退到环境贴图,避免出现黑色反射区域。
优化方面,SSR通常以半分辨率或四分之一分辨率渲染,然后通过双边滤波上采样,减少逐像素光线步进开销。深度缓冲可以预生成mip链,场景层次包围盒可裁剪部分对象。环境贴图方面,使用局部探针和盒投影提高近处准确度,并在反射探针更新时生成预滤波radiance。还可以使用平面反射作为特定场景的精确补充,例如地板、水面使用平面反射可以得到最准确的倒影,再用SSR覆盖不规则表面,环境贴图兜底。
常见误区是认为SSR可以直接替代环境贴图,或者环境贴图已经足够。两者记录的信息空间不同,SSR无法反射背向相机物体,环境贴图无法包含当前帧动态,因此实际项目中应当组合使用,并根据材质粗糙度、移动平台性能预算调整步进次数与分辨率。