PrismarineJS/prismarine-viewer

Terrain mipmapping #172

Karang posted onGitHub

Add mipmapping to remove moiré patterns, especially visible in VR but also when moving the camera.

This is not a trivial issue. Setting the minFilter of the texture to THREE.NearestMipmapNearestFilter solve the moiré issue but handle very badly transparent blocks and cause issue with the alpha testing.

A correct solution would be to generate the mipmaps manually and use a better algorithm to interpolate the alpha (max neighbor?). I tried to do that but it wasn't very concluant, may revisit this solution in the future.

An other solution could be to generate a binary alphaMap that doesnt have the mipmap and use the mipmap on the texture.


(Buggy?) code to generate the mipmaps manually:

function mipmap(img, scale = false) {
  const imageCanvas = document.createElement( "canvas" )
  const context = imageCanvas.getContext( "2d" )
  if (!scale) {
    imageCanvas.width = imageCanvas.height = img.width
    context.drawImage(img, 0, 0)
  } else {
    imageCanvas.width = imageCanvas.height = img.width / 2
    const inData = img.getContext('2d').getImageData(0, 0, img.width, img.height).data
    const out = context.getImageData(0, 0, imageCanvas.width, imageCanvas.height)
    for (let i=0 ; i<imageCanvas.height; i++) {
      for (let j=0 ; j<imageCanvas.width; j++) {
        let in0 = (i*2 * imageCanvas.width + j*2) * 4
        let in1 = in0 + 4
        let in2 = in0 + img.width
        let in3 = in0 + img.width + 4

        let a0 = inData[in0+3]
        let a1 = inData[in1+3]
        let a2 = inData[in2+3]
        let a3 = inData[in3+3]

        let r = (inData[in0] * a0 + inData[in1] * a1 + inData[in2] * a2 + inData[in3] * a3) / 1020
        let g = (inData[in0+1] * a0 + inData[in1+1] * a1 + inData[in2+1] * a2 + inData[in3+1] * a3) / 1020
        let b = (inData[in0+2] * a0 + inData[in1+2] * a1 + inData[in2+2] * a2 + inData[in3+2] * a3) / 1020
        let a = Math.max(Math.max(a0, a1), Math.max(a2, a3))
        let outIdx = (i * imageCanvas.width + j) * 4
        out.data[outIdx++] = r
        out.data[outIdx++] = g
        out.data[outIdx++] = b
        out.data[outIdx++] = a
      }
    }
    context.putImageData(out, 0, 0)
  }
  return imageCanvas
}

function loadTextureMipmap(path, cb) {
  let img = new Image()
  img.onload = () => {
    const canvas = mipmap(img)
    const texture = new THREE.CanvasTexture(canvas)
    texture.mipmaps[0] = canvas
    let i=0
    while (texture.mipmaps[i].width != 1) {
      i++
      texture.mipmaps[i] = mipmap(texture.mipmaps[i-1], true)
    }

    texture.wrapS = THREE.ClampToEdgeWrapping
    texture.wrapT = THREE.ClampToEdgeWrapping
    texture.magFilter = THREE.NearestFilter
    texture.minFilter = THREE.NearestMipmapNearestFilter
    texture.flipY = false

    cb(texture)
  }
  img.src = path
}
posted by Karang about 4 years ago

Debugged mipmap generator:

function mipmap(img, scale = false) {
  const imageCanvas = document.createElement( "canvas" )
  const context = imageCanvas.getContext( "2d" )
  if (!scale) {
    imageCanvas.width = imageCanvas.height = img.width
    context.drawImage(img, 0, 0)
  } else {
    imageCanvas.width = imageCanvas.height = img.width / 2
    const inData = img.getContext('2d').getImageData(0, 0, img.width, img.height).data
    const out = context.getImageData(0, 0, imageCanvas.width, imageCanvas.height)
    for (let i=0 ; i<imageCanvas.height; i++) {
      for (let j=0 ; j<imageCanvas.width; j++) {
        let in0 = (i*2 * img.width + j*2) * 4
        let in1 = in0 + 4
        let in2 = in0 + img.width * 4
        let in3 = in0 + img.width * 4 + 4

        let a0 = inData[in0 + 3]
        let a1 = inData[in1 + 3]
        let a2 = inData[in2 + 3]
        let a3 = inData[in3 + 3]

        let r = g = b = a = 0
        let div = 0

        if (a0 > 0) {
          r += inData[in0]
          g += inData[in0 + 1]
          b += inData[in0 + 2]
          div++
        }

        if (a1 > 0) {
          r += inData[in1]
          g += inData[in1 + 1]
          b += inData[in1 + 2]
          div++
        }

        if (a2 > 0) {
          r += inData[in2]
          g += inData[in2 + 1]
          b += inData[in2 + 2]
          div++
        }

        if (a3 > 0) {
          r += inData[in3]
          g += inData[in3 + 1]
          b += inData[in3 + 2]
          div++
        }

        if (div > 0) {
          r /= div
          g /= div
          b /= div
        }
        a = Math.max(Math.max(a0, a1), Math.max(a2, a3))

        let outIdx = (i * imageCanvas.width + j) * 4
        out.data[outIdx++] = r
        out.data[outIdx++] = g
        out.data[outIdx++] = b
        out.data[outIdx++] = a
      }
    }
    context.putImageData(out, 0, 0)
  }
  return imageCanvas
}

function loadTextureMipmap(path, cb) {
  let img = new Image()
  img.onload = () => {
    const canvas = mipmap(img)
    const texture = new THREE.CanvasTexture(canvas)
    texture.mipmaps[0] = canvas
    let i=0
    while (texture.mipmaps[i].width != 1) {
      i++
      texture.mipmaps[i] = mipmap(texture.mipmaps[i-1], true)
    }

    texture.wrapS = THREE.ClampToEdgeWrapping
    texture.wrapT = THREE.ClampToEdgeWrapping
    texture.magFilter = THREE.NearestFilter
    texture.minFilter = THREE.NearestMipmapNearestFilter
    texture.flipY = false

    cb(texture)
  }
  img.src = path
}
posted by Karang about 4 years ago

Fund this Issue

$0.00
Funded
Only logged in users can fund an issue

Pull requests