Sam Coutteau
Sam Coutteau

Reputation: 417

projected cube map shader, white line between edges

enter image description here

I'm trying to write a shader which renders a cube map / cube texture as an equirectangular projection.

The main part of this is done however I get white lines between the faces.

My methodology is:

  1. Starting from UV ([0,1]x[0,1])
  2. Transform to [-1,1]x[-1,1] and than to [-180,180]x[-90,90]
  3. These are now long lat, which can be transformed into 3D (xyz)
  4. Get the face they belong to, as well as their position within this face ([-1,1]x[-1,1])
  5. Transform this face position to a UV within the cube texture

At first I thought the output of step 4 was wrong and that I was sampling from outside the texture, but even after multiplying the face coordinates by 1/2, I still get the white lines.

reference: https://codepen.io/coutteausam/pen/jOKKYYy

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));

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

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

  if (abs(xyz.z) == m) {
    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);

  xy = (xy + 1.) / 2.; // [-1,1] -> [0,1]

  xy.x = clamp(xy.x, 0., 1.);
  xy.y = clamp(xy.y, 0., 1.);

  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.);
  }

  return xy / vec2(4., 3.); // [0,4]x[0,3] -> [0,1]x[0,1]
}

// 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, vec2(clamp(uv.x, 0., 1.), clamp(uv.y, 0., 1.)));
  gl_FragColor = texturePixel;
}

Upvotes: 1

Views: 92

Answers (1)

M -
M -

Reputation: 28482

The math behind your shader looks sound. However, you need to consider how texture sampling behaves when dealing with sub-pixels. Take a look at your source texture:

enter image description here

When you cross the boundary between Top to Right, your sampler will try to squeeze all the texture between magenta and teal into a single pixel. Since there's lots of white in that area, that squeezed pixel will be mostly white. Notice this doesn't happen between Top to Front, because there's no white area between those two faces. (Read: mipmapping to see how textures behave when scaled down)

Solutions:

  1. You might be able to sample the nearest full pixel, instead of trying to blend in between them, by turning off mipmapping. To do so, you can change the texture's minification filter to linear filtering.
const imgTexture = new THREE.TextureLoader().load('https://i.imgur.com/tBzfYG5.png');
imgTexture.minFilter = THREE.LinearFilter;

The only downside is that you might get some hard edges along the boundaries.

enter image description here

  1. If your project allows it, you could simply break up your texture into six images, then use the cubemap method offered by Three.js

Upvotes: 1

Related Questions