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