Search code examples
openglglslwebgl

Compute mip level selection from texcoord varying


Marking as OpenGL even those the example in the question is WebGL because OpenGL domain experts should be able to answer this question. There's an OpenGL repo here

I'm trying to compute texture mip levels the same way samplers do it based on texture coordinate derivatives.

I see lots of answers here and elsewhere. For example

All 3 of those say the calculation is

   vec2  dx_vtc        = dFdx(texture_coordinate);
   vec2  dy_vtc        = dFdy(texture_coordinate);
   float delta_max_sqr = max(dot(dx_vtc, dx_vtc), dot(dy_vtc, dy_vtc));

   mip_level = 0.5 * log2(delta_max_sqr);

Given that, I wrote a test. In the test I have 2 shaders.

  • The first shader draws a quad using texture coordinates and sampling a texture. The texture has 7 mip levels, each level is a different solid color. As I increase the range of the texture coordinates I should see different mips get selected (using NEAREST_MIPMAP_NEAREST). This works!

  • The second shader draws a quad by computing the mip level using the formula above and then using that mip level to select a color from a table, the colors in the table match the colors in the texture. This does not work. All I ever see is red (the first color).

Here's the second shader

#version 300 es
precision highp float;

in vec2 v_texCoord;

out vec4 outColor;

const vec4 colors[8] = vec4[8](
  vec4(  1,   0,   0, 1), // 0: red
  vec4(  1,   1,   0, 1), // 1: yellow
  vec4(  0,   1,   0, 1), // 2: green
  vec4(  0,   1,   1, 1), // 3: cyan
  vec4(  0,   0,   1, 1), // 4: blue
  vec4(  1,   0,   1, 1), // 5: magenta
  vec4(0.5, 0.5, 0.5, 1), // 6: gray
  vec4(  1,   1,   1, 1));// 7: white

void main() {
  vec2 dx = dFdx(v_texCoord);
  vec2 dy = dFdy(v_texCoord);
  float deltaMaxSq = max(dot(dx, dx), dot(dy, dy));
  float mipLevel = 0.5 * log2(deltaMaxSq);
  
  outColor = colors[int(mipLevel)];
}

Maybe I just have a typo but I've tried various ways of debugging

  • I tried visualizing the texture coordinates in the second shader. They are clearly correct
  • I tried making sure indexing the colors works. That works.
  • I tried visualizing the mip level by writing mipLevel / 7.0 from the shader, so as the mip level goes to 7 the quad should get brighter. This does not work.

What am I doing wrong?

html, body {
  margin: 0;
  font-family: monospace;
  height: 100%;
}
canvas {
  display: block;
  width: 100%;
  height: 100%;
}
<canvas id="c"></canvas>

<script type="module">
import * as twgl from 'https://twgljs.org/dist/5.x/twgl-full.module.js';

const vs = `#version 300 es
uniform mat4 u_worldViewProjection;
uniform mat3 u_texMat;

out vec2 v_texCoord;

const vec2 position[6] = vec2[6](
  vec2(0, 0),
  vec2(1, 0),
  vec2(0, 1),
  vec2(0, 1),
  vec2(1, 0),
  vec2(1, 1));

void main() {
  vec2 p = position[gl_VertexID];
  v_texCoord = (u_texMat * vec3(p, 1)).xy;
  gl_Position = u_worldViewProjection * vec4(p, 0, 1);
}
`;
const fsTex = `#version 300 es
precision highp float;

in vec2 v_texCoord;

uniform sampler2D u_tex;

out vec4 outColor;

void main() {
  outColor = texture(u_tex, v_texCoord);
}
`;
const fsMipLevel = `#version 300 es
precision highp float;

in vec2 v_texCoord;

out vec4 outColor;

const vec4 colors[8] = vec4[8](
  vec4(  1,   0,   0, 1), // 0: red
  vec4(  1,   1,   0, 1), // 1: yellow
  vec4(  0,   1,   0, 1), // 2: green
  vec4(  0,   1,   1, 1), // 3: cyan
  vec4(  0,   0,   1, 1), // 4: blue
  vec4(  1,   0,   1, 1), // 5: magenta
  vec4(0.5, 0.5, 0.5, 1), // 6: gray
  vec4(  1,   1,   1, 1));// 7: white

void main() {
  vec2 dx = dFdx(v_texCoord);
  vec2 dy = dFdy(v_texCoord);
  float deltaMaxSq = max(dot(dx, dx), dot(dy, dy));
  float mipLevel = 0.5 * log2(deltaMaxSq);
  
  // mipLevel = mod(gl_FragCoord.x / 16.0, 8.0);  // comment in to test we can use the colors

  outColor = colors[int(mipLevel)];

  // outColor = vec4(mipLevel / 7.0, 0, 0, 1);  // comment in to visualize another way
  // outColor = vec4(fract(v_texCoord), 0, 1);  // comment in to visualize texcoord
}
`;

const colors = [
  '#F00',
  '#FF0',
  '#0F0',
  '#0FF',
  '#00F',
  '#F0F',
  '#888',
  '#FFF',
];


function createMips(colors) {
  const ctx = document.createElement('canvas').getContext('2d');
  const numMips = colors.length;
  return colors.map((color, i) => {
    const size = 2 ** (numMips - i - 1);
    ctx.canvas.width = size;
    ctx.canvas.height = size;
    ctx.fillStyle = color;
    ctx.fillRect(0, 0, size, size);
    return ctx.getImageData(0, 0, size, size);
  });
}

function main() {
  const m4 = twgl.m4;
  const gl = document.getElementById("c").getContext("webgl2");
  if (!gl) {
    alert("Sorry, this example requires WebGL 2.0");  // eslint-disable-line
    return;
  }

  const texProgramInfo = twgl.createProgramInfo(gl, [vs, fsTex]);
  const mipProgramInfo = twgl.createProgramInfo(gl, [vs, fsMipLevel]);

  const texImage = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texImage);
  const data = createMips(colors);
  data.forEach(({width, height, data}, level) => {
    gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
  });
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);

  const lerp = (a, b, t) => a + (b - a) * t;

  function render(time) {
    time *= 0.001;
    twgl.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clearColor(0.3, 0.3, 0.3, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const uniforms = {};

    const s = lerp(1, 128, Math.sin(time) * 0.5 + 0.5);
    uniforms.u_texMat = [
      s, 0, 0,
      0, s, 0,
      0, 0, 1,
    ];
    uniforms.u_worldViewProjection = m4.translation([-1.01, -0.5, 0]);

    gl.useProgram(texProgramInfo.program);
    twgl.setUniforms(texProgramInfo, uniforms);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    uniforms.u_worldViewProjection = m4.translation([0.01, -0.5, 0]);

    gl.useProgram(mipProgramInfo.program);
    twgl.setUniforms(mipProgramInfo, uniforms);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}

main();
</script>


Solution

  • The mipmap level also depends on the size of the texture. The mipmap level is a measure of how many (square)texels should be squeezed into one fragment. In the answer to the linked question (How to access automatic mipmap level in GLSL fragment shader texture?), the mipmap level is calculated from the texture coordinate multiplied by the size of the texture:

    mip_map_level(textureCoord * textureSize(myTexture, 0));  
    

    You missed that part in your code. So the formula should be:

    vec2 size = vec2(128.0, 128.0); // size of the texture in your example
    vec2 dx = dFdx(v_texCoord * size);
    vec2 dy = dFdy(v_texCoord * size);
    float deltaMaxSq = max(dot(dx, dx), dot(dy, dy));
    float mipLevel = 0.5 * log2(deltaMaxSq);
    

    Also, you need to round the mipmap level when you look up the color:

    outColor = colors[int(mipLevel)];

    outColor = colors[int(mipLevel + 0.5)];
    

    html, body {
      margin: 0;
      font-family: monospace;
      height: 100%;
    }
    canvas {
      display: block;
      width: 100%;
      height: 100%;
    }
    <canvas id="c"></canvas>
    
    <script type="module">
    import * as twgl from 'https://twgljs.org/dist/5.x/twgl-full.module.js';
    
    const vs = `#version 300 es
    uniform mat4 u_worldViewProjection;
    uniform mat3 u_texMat;
    
    out vec2 v_texCoord;
    
    const vec2 position[6] = vec2[6](
      vec2(0, 0),
      vec2(1, 0),
      vec2(0, 1),
      vec2(0, 1),
      vec2(1, 0),
      vec2(1, 1));
    
    void main() {
      vec2 p = position[gl_VertexID];
      v_texCoord = (u_texMat * vec3(p, 1)).xy;
      gl_Position = u_worldViewProjection * vec4(p, 0, 1);
    }
    `;
    const fsTex = `#version 300 es
    precision highp float;
    
    in vec2 v_texCoord;
    
    uniform sampler2D u_tex;
    
    out vec4 outColor;
    
    void main() {
      outColor = texture(u_tex, v_texCoord);
    }
    `;
    const fsMipLevel = `#version 300 es
    precision highp float;
    
    in vec2 v_texCoord;
    
    out vec4 outColor;
    
    const vec4 colors[8] = vec4[8](
      vec4(  1,   0,   0, 1), // 0: red
      vec4(  1,   1,   0, 1), // 1: yellow
      vec4(  0,   1,   0, 1), // 2: green
      vec4(  0,   1,   1, 1), // 3: cyan
      vec4(  0,   0,   1, 1), // 4: blue
      vec4(  1,   0,   1, 1), // 5: magenta
      vec4(0.5, 0.5, 0.5, 1), // 6: gray
      vec4(  1,   1,   1, 1));// 7: white
    
    void main() {
      vec2 size = vec2(128.0, 128.0); // size of the texture in your example
      vec2 dx = dFdx(v_texCoord * size);
      vec2 dy = dFdy(v_texCoord * size);
      float deltaMaxSq = max(dot(dx, dx), dot(dy, dy));
      float mipLevel = 0.5 * log2(deltaMaxSq);
      
      // mipLevel = mod(gl_FragCoord.x / 16.0, 8.0);  // comment in to test we can use the colors
    
      outColor = colors[int(mipLevel + 0.5)];
    
      // outColor = vec4(mipLevel / 7.0, 0, 0, 1);  // comment in to visualize another way
      // outColor = vec4(fract(v_texCoord), 0, 1);  // comment in to visualize texcoord
    }
    `;
    
    const colors = [
      '#F00',
      '#FF0',
      '#0F0',
      '#0FF',
      '#00F',
      '#F0F',
      '#888',
      '#FFF',
    ];
    
    
    function createMips(colors) {
      const ctx = document.createElement('canvas').getContext('2d');
      const numMips = colors.length;
      return colors.map((color, i) => {
        const size = 2 ** (numMips - i - 1);
        ctx.canvas.width = size;
        ctx.canvas.height = size;
        ctx.fillStyle = color;
        ctx.fillRect(0, 0, size, size);
        return ctx.getImageData(0, 0, size, size);
      });
    }
    
    function main() {
      const m4 = twgl.m4;
      const gl = document.getElementById("c").getContext("webgl2");
      if (!gl) {
        alert("Sorry, this example requires WebGL 2.0");  // eslint-disable-line
        return;
      }
    
      const texProgramInfo = twgl.createProgramInfo(gl, [vs, fsTex]);
      const mipProgramInfo = twgl.createProgramInfo(gl, [vs, fsMipLevel]);
    
      const texImage = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, texImage);
      const data = createMips(colors);
      data.forEach(({width, height, data}, level) => {
        gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA8, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
      });
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
    
      const lerp = (a, b, t) => a + (b - a) * t;
    
      function render(time) {
        time *= 0.001;
        twgl.resizeCanvasToDisplaySize(gl.canvas);
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
        gl.clearColor(0.3, 0.3, 0.3, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
    
        const uniforms = {};
    
        const s = lerp(1, 128, Math.sin(time) * 0.5 + 0.5);
        uniforms.u_texMat = [
          s, 0, 0,
          0, s, 0,
          0, 0, 1,
        ];
        uniforms.u_worldViewProjection = m4.translation([-1.01, -0.5, 0]);
    
        gl.useProgram(texProgramInfo.program);
        twgl.setUniforms(texProgramInfo, uniforms);
        gl.drawArrays(gl.TRIANGLES, 0, 6);
    
        uniforms.u_worldViewProjection = m4.translation([0.01, -0.5, 0]);
    
        gl.useProgram(mipProgramInfo.program);
        twgl.setUniforms(mipProgramInfo, uniforms);
        gl.drawArrays(gl.TRIANGLES, 0, 6);
    
        requestAnimationFrame(render);
      }
      requestAnimationFrame(render);
    }
    
    main();
    </script>