如何修复OpenGL片段着色器生成网格的闪烁(scintillation)伪影?
解决方案:基于屏幕像素的网格线渲染
你的问题核心在于网格线宽是基于纹理空间的固定比例定义的,没有考虑相机旋转、缩放时,纹理空间到屏幕像素空间的映射会发生变化——比如45度旋转时,纹理空间的单位长度在屏幕上会被拉伸,导致同一线宽在屏幕上的像素数忽多忽少,平移时还会因为采样对齐问题产生闪烁。
要解决这个问题,必须基于屏幕像素来定义网格线宽,让线宽始终保持固定的像素数量,不受相机变换影响。下面是修改后的代码和关键思路:
修改后的片段着色器代码
if (uDrawGrid) { vec2 uv = fTexCoordinate; // 设定网格线的目标像素宽度(比如2像素,可根据需求调整) float targetLinePixelWidth = 2.0; // 设定网格间距(纹理空间单位,对应原代码的千分位精度) float gridUVSpacing = 0.001; // 获取当前纹理坐标的屏幕像素导数:计算每个屏幕像素对应纹理空间的变化量 vec2 dUVdx = dFdx(uv); vec2 dUVdy = dFdy(uv); // 计算纹理空间中,对应1个屏幕像素的长度(综合x、y方向的变换) vec2 uvPerPixel = sqrt(dUVdx * dUVdx + dUVdy * dUVdy); // 转换为纹理空间的线宽阈值:目标像素宽度 × 每个像素对应的纹理长度 vec2 lineUVThreshold = targetLinePixelWidth * uvPerPixel; // 计算当前片段到最近网格线的距离 vec2 gridFract = fract(uv / gridUVSpacing); vec2 gridDist = min(gridFract, 1.0 - gridFract) * gridUVSpacing; // 用平滑过渡判断是否属于网格线区域,避免硬边缘的闪烁 float lineXAlpha = smoothstep(lineUVThreshold.x, lineUVThreshold.x * 0.9, gridDist.x); float lineYAlpha = smoothstep(lineUVThreshold.y, lineUVThreshold.y * 0.9, gridDist.y); float lineAlpha = clamp(lineXAlpha + lineYAlpha, 0.0, 1.0); if (lineAlpha > 0.0) { // 混合网格颜色与底层颜色(与原代码混合逻辑一致) vec4 gridColor = vec4(0.25 + topColor.r * 0.5, 0.25 + topColor.g * 0.5, 0.25 + topColor.b * 0.5, 1.0); color = mix(color, gridColor, lineAlpha); } }
关键改进点说明
基于屏幕像素定义线宽
使用GLSL内置函数dFdx/dFdy获取纹理坐标在屏幕像素上的导数,这能精确计算出每个屏幕像素对应纹理空间的长度,从而将目标像素线宽转换为纹理空间的阈值,保证无论相机如何旋转、缩放,网格线在屏幕上的宽度始终一致。替换round为fract计算距离
fract函数直接计算当前坐标到最近网格线的距离,比round更高效,同时避免了千分位截断带来的精度误差,减少平移时的闪烁。平滑过渡替代硬判断
用smoothstep代替原代码的<=硬判断,让网格线边缘有一个小的过渡区间,避免像素边界处的突然跳变,彻底解决闪烁问题。
额外注意事项
- 如果网格是基于世界空间而不是纹理空间的,只需将代码中的
fTexCoordinate替换为世界空间坐标,再用同样的导数方法计算即可。 - 保持
precision highp float;开启,避免导数计算的精度损失。 - 可根据需求调整
targetLinePixelWidth(线宽像素数)和gridUVSpacing(网格间距)参数。
内容的提问来源于stack exchange,提问作者Callum Rodgers
相关产品推荐
相关产品推荐

