broody
broody

Reputation: 707

Piecing together meshes in ThreeJS causes visible seam

I'm trying to piece together a sphere with individual slices. Basically, I have multiple SphereGeoemtery slices that form a sphere and used to project a panorama. Slices are used for lazy loading very large panoramas.

With the default texture wrapping mode (THREE.ClampToEdgeWrapping) on these slices, from far away the panorama looks fine but if you zoom in it's very clear the edges of the meshes are stretching, causing visible seams. It make sense since it's stretching the last pixel at the edge..

enter image description here

I also tried changing wrapping mode to THREE.RepeatWrapping, however, the seam becomes completely visible:

enter image description here

So my question is, what's the best method here for piecing together meshes? Or is this just unavoidable?

Upvotes: 1

Views: 431

Answers (1)

user128511
user128511

Reputation:

Off the top of my head you'd have to make each texture contain one border row and border column in each direction that's a repeat of the its neighbor, then adjust the UV coordinates appropriately

For example if the big image is 8 pixels wide and 6 pixels tall

ABCDEFGH
IJKLMNOP
QRSTUVWX
YZ123456
789abcde
fghijklm

And you want to divide it into into 4 parts (each 4, 3) then you'd need these 4 parts

ABCDE DEFGH
IJKLM LMNOP
QRSTU TUVWX
YZ123 23456

QRSTU TUVWX
YZ123 23456
789ab abcde
fghij ijklm

Also to make it easy repeat the edges so

AABCDE DEFGHH
AABCDE DEFGHH
IIJKLM LMNOPP
QQRSTU TUVWXX
YYZ123 234566

QQRSTU TUVWXX
YYZ123 234566
7789ab abcdee
ffghij ijklmm
ffghij ijklmm

Repeating the edges is because I'm assuming you're splitting into more than 2x2 so technically if you were going to split something 50 pixels wide into 5 parts you could do parts that are 11, 12, 12, 12, 11 in width. The edges being only 11 pixels instead of 12 would need a different UV adjustment. But, by repeating the edges we can make them all 12, 12, 12, 12, 12 so everything is consistant.

testing, left is normal split showing the seam. Right is the fixed one, no seam.

body {
  margin: 0;
}
#c {
  width: 100vw;
  height: 100vh;
  display: block;
}
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});

  const fov = 75;
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 5;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 1;

  const scene = new THREE.Scene();

  // make our texture using a canvas to test
  const bigImage = document.createElement('canvas');
  {
    const ctx = bigImage.getContext('2d');
    const width = 32;
    const height = 16;
    ctx.canvas.width = width;
    ctx.canvas.height = height;
    const gradient = ctx.createLinearGradient(0, 0, width, height);
    gradient.addColorStop(0, 'red');
    gradient.addColorStop(0.5, 'yellow');
    gradient.addColorStop(1, 'blue');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, width, height);
  }
  
  const forceTextureInitialization = function() {
    const material = new THREE.MeshBasicMaterial();
    const geometry = new THREE.PlaneBufferGeometry();
    const scene = new THREE.Scene();
    scene.add(new THREE.Mesh(geometry, material));
    const camera = new THREE.Camera();

    return function forceTextureInitialization(texture) {
      material.map = texture;
      renderer.render(scene, camera);
    };
  }();  

  // bad
  {
    const ctx = document.createElement('canvas').getContext('2d');
  
    // split the texture into 4 parts across 4 planes
    const across = 2;
    const down = 2;
    const pixelsAcross = bigImage.width / across;
    const pixelsDown = bigImage.height / down;
    ctx.canvas.width = pixelsAcross;
    ctx.canvas.height = pixelsDown;
    
    for (let y = 0; y < down; ++y) {
      for (let x = 0; x < across; ++x) {
        ctx.clearRect(0, 0, pixelsAcross, pixelsDown);
        ctx.drawImage(bigImage, 
                      x * pixelsAcross, (down - 1 - y) * pixelsDown, pixelsAcross, pixelsDown,
                      0, 0, pixelsAcross, pixelsDown);
        const texture = new THREE.CanvasTexture(ctx.canvas);
        // see https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
        forceTextureInitialization(texture);
        const geometry = new THREE.PlaneBufferGeometry(1 / across, 1 / down);
        const material = new THREE.MeshBasicMaterial({map: texture});
        const plane = new THREE.Mesh(geometry, material);
        scene.add(plane);
        plane.position.set(-1 + x / across, y / down - 0.25, 0);
      }
    }
  }
  
  // good
  {
    const ctx = document.createElement('canvas').getContext('2d');
  
    // split the texture into 4 parts across 4 planes
    const across = 2;
    const down = 2;
    const pixelsAcross = bigImage.width / across;
    const pixelsDown = bigImage.height / down;
    ctx.canvas.width = pixelsAcross + 2;
    ctx.canvas.height = pixelsDown + 2;
    
    // just draw the image at all these offsets.
    // it would be more efficient to draw the edges
    // 1 pixel wide but I'm lazy
    const offsets = [
      [ 0,  0],
      [ 1,  0],
      [ 2,  0],
      [ 0,  1],
      [ 2,  1],
      [ 0,  2],
      [ 1,  2],
      [ 2,  2],
      [ 1,  1],
    ];
    
    for (let y = 0; y < down; ++y) {
      for (let x = 0; x < across; ++x) {
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        let srcX = x * pixelsAcross - 1;
        let srcY = (down - 1 - y) * pixelsDown - 1;
        let dstX = 0;
        let dstY = 0;
        let width = pixelsAcross + 2;
        let height = pixelsDown + 2;
        ctx.drawImage(bigImage, 
                      srcX, srcY, width, height,
                      dstX, dstY, width, height);
        // handle edges
        if (srcX < 0) { 
          // repeat left edge
          ctx.drawImage(bigImage,
                        0, srcY, 1, height,
                        0, dstY, 1, height);
        }
        if (srcY < 0) {
          // repeat top edge
          ctx.drawImage(bigImage,
                        srcX, 0, width, 1,
                        dstX, 0, width, 1);
        }
        if (srcX + width > bigImage.width) {
          // repeat right edge
          ctx.drawImage(bigImage,
                        bigImage.width - 1, srcY, 1, height,
                        ctx.canvas.width - 1, dstY, 1, height);
        }
        if (srcY + height > bigImage.height) {
          // repeat bottom edge
          ctx.drawImage(bigImage,
                        srcX, bigImage.height - 1, width, 1,
                        dstX, ctx.canvas.height - 1, width, 1);
        }
        // TODO: handle corners
        const texture = new THREE.CanvasTexture(ctx.canvas);
        texture.minFilter = THREE.LinearFilter;
        // offset UV coords 1 pixel to skip the edge pixel
        texture.offset.set(1 / ctx.canvas.width, 1 / ctx.canvas.height);
        // only textureSize - 2 of the pixels in the texture
        texture.repeat.set(pixelsAcross / ctx.canvas.width, pixelsDown / ctx.canvas.height);
        // see https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
        forceTextureInitialization(texture);
        const geometry = new THREE.PlaneBufferGeometry(1 / across, 1 / down);
        const material = new THREE.MeshBasicMaterial({map: texture});
        const plane = new THREE.Mesh(geometry, material);
        scene.add(plane);
        plane.position.set(1 + x / across - 0.5, y / down - 0.25, 0);
      }
    }
  }

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  function render(time) {
    time *= 0.001;

    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement;
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }

  requestAnimationFrame(render);
}

main();
</script>

Upvotes: 3

Related Questions