WebGL2: Drawing a textured quad causes artifacts at boundary when using mip-mapping

213 Views Asked by At

I'm drawing a quad with texture coordinates ranging from 0-1 onto a canvas using WebGL2. This quad is used to draw an image to the canvas which can be zoomed and moved by the user. I use nearest-filtering on the texture in order to see sharp pixels. When no mip-mapping is enabled everything works as expected, but as soon as I turn on mip-mapping I get artifacts at the boundary (even when zoomed in a lot in which case only the most detailed mip-map should be at work). These artifacts are usually only a single pixel wide, no matter how to much I zoom in.

Here's an example (the white line on the right of the quad): enter image description here

Somebody knows what's causing this and how to get rid of it ?

1

There are 1 best solutions below

5
LJᛃ On

The mip level is derived by the screen-space derivative of the given coordinates, so the more the coordinates change from one pixel to another the higher the miplevel(=lower-res mipmap) that is being sampled from. Now when you shade a fragment that is on the edge of the surface so that the edge runs through the pixel and you have your texture wrapping set to REPEAT there's a good chance the derivative will be huge which will result in a lower resolution mipmap being sampled from:

UV derivative over pixel footprint

The fix is easy, don't use REPEAT.

const gl = canvas.getContext('webgl2');
const 
  vs = gl.createShader(gl.VERTEX_SHADER),
  fs = gl.createShader(gl.FRAGMENT_SHADER),
  program = gl.createProgram(),
  texture = gl.createTexture()
;
gl.shaderSource(vs, `#version 300 es
uniform vec3 transform;
out vec2 uv;
void main(){
  gl_Position = vec4(gl_VertexID<2,gl_VertexID%2,.5,1)*2.-1.;
  uv = gl_Position.xy*.5+.5;
  gl_Position.xy+=transform.xy;
  gl_Position.xy*=transform.z;
}`);
gl.shaderSource(fs, `#version 300 es
precision highp float;
uniform sampler2D tex;
in vec2 uv;
out vec4 OUT;
void main(){
  OUT=texture(tex,uv);
}`);
gl.compileShader(vs);gl.attachShader(program, vs);
gl.compileShader(fs);gl.attachShader(program, fs);
gl.linkProgram(program);
const log = gl.getProgramInfoLog(program);
if (log) console.warn(log);
gl.useProgram(program);

const transformLocation = gl.getUniformLocation(program, 'transform');
const fixLocation = gl.getUniformLocation(program, 'fix');
gl.uniform1i(gl.getUniformLocation(program, 'tex'),0);

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([
  255,0,0,255, 255,255,0,255,
  0,255,0,255, 0,0,255,255
]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.generateMipmap(gl.TEXTURE_2D);

let drag=false,x=.54321,y=-.54321,z=1;
canvas.addEventListener('pointerdown',()=>drag=true);
canvas.addEventListener('pointerup',()=>drag=false);
canvas.addEventListener('pointermove', (e)=>{
  if(!drag)return;
  x+=e.movementX*.001;
  y-=e.movementY*.001;
});
canvas.addEventListener('wheel', (e)=>z=Math.min(Math.max(z+e.deltaY*.01,.3),5.));

filter.addEventListener('change', ()=>{
const val = parseInt(filter.value,10);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, val);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, val);
});

(function render(){
gl.uniform3f(transformLocation,x,y,z);
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
requestAnimationFrame(render);
})();
canvas{outline: 1px solid red;}
label{display:block;}
<label>Filter<select id="filter"><option value="10497">REPEAT</option><option value="33071">CLAMP_TO_EDGE</option><option value="33648">MIRRORED_REPEAT</option></select></label><canvas id="canvas" width="400" height="400"></canvas>

This is how it looks for me, you can see the red/blue mipmap fringing around the green square. Mipmap fringing