You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

投影立方体贴图Shader问题:面片间出现白线

立方体贴图转等矩形投影Shader面片间白线问题解决

面片间白线问题

我正在编写一款将Cube Map/Cube Texture渲染为Equirectangular Projection的Shader,主体功能已实现,但面片间出现了白线问题。

实现步骤:

  • 从UV([0,1]x[0,1])开始
  • 转换至[-1,1]x[-1,1],再转为[-180,180]x[-90,90]的经纬度坐标
  • 将经纬度转换为3D坐标(xyz)
  • 获取该坐标所属的立方体面片,以及在面片内的位置([-1,1]x[-1,1])
  • 将面片内位置转换为立方体贴图对应的UV

起初认为是步骤4的输出错误导致采样超出纹理范围,但即使将面片坐标乘以1/2,白线问题依然存在。


问题根源

白线的核心诱因是GPU浮点精度误差和纹理采样逻辑的边界处理缺陷:

  1. 用abs(xyz.x) == m这类精确相等判断在浮点运算中完全不可靠,精度误差会导致本该归属某一面片的点被错误分配到相邻面片,甚至落入默认分支生成异常UV。
  2. 直接将面片内坐标映射到[0,1]边界时,纹理过滤会采样到面片边缘的空白区域(或跨面片采样),产生白色缝隙。
  3. 中途的clamp操作会强制拉回边缘UV,反而加剧边界处的采样偏差。

修复方案

关键修改点

  1. 添加浮点容差的面片归属判断:用范围比较替代精确相等,避免精度误差导致的面片分类错误。
  2. 收缩UV采样范围:将面片内UV从[0,1]收缩到[margin, 1-margin],避开纹理边缘的过滤盲区。
  3. 优化UV clamp时机:只在最终全局UV计算完成后做必要的clamp,避免中途破坏边界采样逻辑。

修改后的完整代码

float max3(vec3 v) {
  return max(max(v.x, v.y), v.z);
}

vec2 sample_cube_map_1(vec3 xyz, out float faceIndex) {
  xyz /= length(xyz);

  float m = max3(abs(xyz));
  float eps = 1e-6; // 浮点精度容差

  if (abs(xyz.x) >= m - eps) {
    faceIndex = sign(xyz.x);
    return xyz.yz / abs(xyz.x);
  }

  if (abs(xyz.y) >= m - eps) {
    faceIndex = 2. * sign(xyz.y);
    return xyz.xz / abs(xyz.y);
  }

  if (abs(xyz.z) >= m - eps) {
    faceIndex = 3. * sign(xyz.z);
    return xyz.xy / abs(xyz.z);
  }

  faceIndex = 1.0;
  return vec2(0., 0.);
}

vec2 sample_cube_map(vec3 xyz) {
  float face;

  vec2 xy = sample_cube_map_1(xyz, face);

  // 收缩UV范围,避免采样到面片边缘
  float margin = 0.005;
  xy = (xy + 1.) / 2.; // [-1,1] -> [0,1]
  xy = clamp(xy, margin, 1. - margin); // 避开纹理边界

  if (face == 1.) {
    // front
    xy += vec2(1., 1.);
  }
  else if (face == -1.) {
    //back
    xy.x = 1. - xy.x;
    xy += vec2(3., 1.);
  }
  else if (face == 2.) {
    // right
    xy.x = 1. - xy.x;
    xy += vec2(2., 1.);
  }
  else if (face == -2.) {
    // left
    xy += vec2(0., 1.);
  }
  else if (face == 3.) {
    // top
    xy = vec2(xy.y, 1. - xy.x);
    xy += vec2(1., 2.);
  }
  else if (face == -3.) {
    // bottom
    xy = xy.yx;
    xy += vec2(1., 0.);
  }
  else {
    xy += vec2(1., 0.);
  }

  vec2 finalUv = xy / vec2(4., 3.); // [0,4]x[0,3] -> [0,1]x[0,1]
  return clamp(finalUv, 0. + margin/4., 1. - margin/4.); // 全局UV微小收缩
}

// projects
//   uv:([0,1] x [0,1])
// to
//   xy:([ -2, 2 ] x [ -1, 1 ])
vec2 uv_2_xy(vec2 uv) {
  return vec2(uv.x * 4. - 2., uv.y * 2. - 1.);
}

// projects
//   xy:([ -2, 2 ] x [ -1, 1 ])
// to
//   longlat: ([ -pi, pi ] x [-pi/2,pi/2])
vec2 xy_2_longlat(vec2 xy) {
  float pi = 3.1415926535897932384626433832795;
  return xy * pi / 2.;
}

vec3 longlat_2_xyz(vec2 longlat) {
  return vec3(cos(longlat.x) * cos(longlat.y), sin(longlat.x) * cos(longlat.y), sin(longlat.y));
}

vec3 uv_2_xyz(vec2 uv) {
  return longlat_2_xyz(xy_2_longlat(uv_2_xy(uv)));
}

vec3 roty(vec3 xyz, float alpha) {
  return vec3(cos(alpha) * xyz.x + sin(alpha) * xyz.z, xyz.y, cos(alpha) * xyz.z - sin(alpha) * xyz.x);
}

varying vec2 vUv;
uniform sampler2D image;
uniform float time;
void main() {
  vec3 xyz = uv_2_xyz(vUv);

  xyz = roty(xyz, time);

  vec2 uv = sample_cube_map(xyz);

  vec4 texturePixel = texture2D(image, uv);
  gl_FragColor = texturePixel;
}

额外优化建议

  • 将立方体贴图的采样模式设置为Clamp to Edge,避免边缘采样时重复相邻面片内容。
  • 若使用压缩纹理,可尝试用未压缩格式测试,排除压缩算法导致的边缘精度损失。

内容的提问来源于stack exchange,提问作者Sam Coutteau

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 02:35:35