I'm merging many 2D textures into an Array Texture in WebGL2 (three.js) for rendering models with multi draw. These individual textures change frequently and are using mipmaps. Because it's not possible to generate mipmaps for just an individual layer in an Array Texture I would like to generate mipmaps for a 2d target and then copy the entire mipmap chain into a specific layer for the given texture.
However, looking through the WebGL API it seems like it's not possible to bind a specific mipmap level from a texture created with texStorage2D
and copy it into a specific mipmap level for a layer in the 3d texture. This is happening in three.js but here are the rough copy commands being done:
_gl.pixelStorei( _gl.UNPACK_ROW_LENGTH, image.width );
_gl.pixelStorei( _gl.UNPACK_IMAGE_HEIGHT, image.height );
_gl.pixelStorei( _gl.UNPACK_SKIP_PIXELS, minX );
_gl.pixelStorei( _gl.UNPACK_SKIP_ROWS, minY );
gl.bindTexture( _gl.TEXTURE_2D, srcTextureHandle, _gl.TEXTURE0 );
gl.copyTexSubImage3D( _gl.TEXTURE_2D_ARRAY, level, dstX, dstY, dstZ, minX, minY, width, height );
I see it's possible to set the target mip level in the arguments of copyTexSubImage3D
but I don't see a way to specify the mip level to copy for the bound texture.
I see there's a glCopyImageSubData function in the OpenGLES3.2 spec that allows for specifying source and target mip levels but there's no equivalent available in browsers. Is this possible in WebGL2?
it's not possible to generate mipmaps for just an individual layer in an Array Texture
It is possible. You just have to do it yourself rather than call gl.generateMipmap
canvas { border: 1px solid black; }
<canvas id="c"></canvas>
<script type="module">
import 'https://greggman.github.io/webgl-lint/webgl-lint.js';
import * as twgl from 'https://twgljs.org/dist/5.x/twgl-full.module.js';
const lerp = (a, b, t) => a + (b - a) * t;
const vsGenMipmap = `#version 300 es
out vec2 v_uv;
const vec2 pos[3] = vec2[3](
vec2(-1, -1),
vec2(-1, 3),
vec2( 3, -1)
);
void main() {
vec2 xy = pos[gl_VertexID];
gl_Position = vec4(xy, 0.0, 1.0);
v_uv = xy * vec2(0.5, -0.5) + vec2(0.5);
}
`;
const fsGenMipmap = `#version 300 es
precision highp float;
uniform highp sampler2DArray tex;
uniform uint layer;
in vec2 v_uv;
out vec4 fragColor;
void main() {
fragColor = texture(tex, vec3(v_uv, layer));
}
`;
const vsView = `#version 300 es
uniform vec2 pos;
void main() {
gl_PointSize = 32.0;
gl_Position = vec4(pos, 0, 1);
}
`;
const fsView = `#version 300 es
precision highp float;
uniform highp sampler2DArray tex;
uniform uint layer;
uniform uint mipLevel;
out vec4 fragColor;
void main() {
fragColor = textureLod(tex, vec3(gl_PointCoord.xy, layer), float(mipLevel));
}
`;
/** @type HTMLCanvasElement */
const el = document.getElementById('c');
const gl = el.getContext('webgl2');
const genMipMapProgramInfo = twgl.createProgramInfo(gl, [vsGenMipmap, fsGenMipmap]);
const viewProgramInfo = twgl.createProgramInfo(gl, [vsView, fsView]);
const r = [255, 0, 0, 255];
const g = [0, 255, 0, 255];
const b = [0, 0, 255, 255];
const y = [255, 255, 0, 255];
const c = [0, 255, 255, 255];
const m = [255, 0, 255, 255];
const _ = [0, 0, 0, 255];
const w = [255, 255, 255, 255];
const layers = [
new Uint8Array([
r, r, g, g, r, r, b, b,
r, r, g, g, r, r, b, b,
g, g, r, g, b, b, r, r,
g, g, r, g, b, b, r, r,
r, r, g, g, r, r, b, b,
r, r, g, g, r, r, b, b,
g, g, r, g, b, b, r, r,
g, g, r, g, b, b, r, r,
].flat()),
new Uint8Array([
y, y, c, c, y, y, m, m,
y, y, c, c, y, y, m, m,
c, c, y, c, m, m, y, y,
c, c, y, c, m, m, y, y,
y, y, c, c, y, y, m, m,
y, y, c, c, y, y, m, m,
c, c, y, c, m, m, y, y,
c, c, y, c, m, m, y, y,
].flat()),
new Uint8Array([
w, w, _, _, w, w, _, _,
w, w, _, _, w, w, _, _,
_, _, w, _, _, _, w, w,
_, _, w, _, _, _, w, w,
w, w, _, _, w, w, _, _,
w, w, _, _, w, w, _, _,
_, _, w, _, _, _, w, w,
_, _, w, _, _, _, w, w,
].flat()),
];
const width = 8;
const height = 8;
const numMipLevels = 4;
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex);
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, numMipLevels, gl.RGBA8, width, height, layers.length);
layers.forEach((data, layer) => {
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, width, height, 1, gl.RGBA, gl.UNSIGNED_BYTE, data);
});
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
{
gl.useProgram(genMipMapProgramInfo.program);
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
for (let mipLevel = 1; mipLevel < numMipLevels; ++mipLevel) {
const mipWidth = Math.max(1, width >> mipLevel);
const mipHeight = Math.max(1, height >> mipLevel);
gl.viewport(0, 0, mipWidth, mipHeight);
for (let layer = 0; layer < layers.length; ++layer) {
gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex, mipLevel, layer);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, mipLevel - 1);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_BASE_LEVEL, mipLevel - 1);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LOD, mipLevel - 1);
twgl.setUniforms(genMipMapProgramInfo, {
layer,
});
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
// reset texture back to default settings
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, 1000);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_BASE_LEVEL, 0);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LOD, 1000);
// show the mip levels
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(viewProgramInfo.program);
for (let layer = 0; layer < layers.length; ++layer) {
const l = layer / (layers.length - 1);
for (let mipLevel = 0; mipLevel < numMipLevels; ++mipLevel) {
const m = mipLevel / (numMipLevels - 1);
const pos = [
lerp(-0.6, 0.6, l),
lerp(-0.75, 0.75, m),
];
twgl.setUniforms(viewProgramInfo, {
pos,
layer,
mipLevel,
});
gl.drawArrays(gl.POINTS, 0, 1);
}
}
</script>
canvas { border: 1px solid black; }
<canvas id="c"></canvas>
<script type="module">
import 'https://greggman.github.io/webgl-lint/webgl-lint.js';
import * as twgl from 'https://twgljs.org/dist/5.x/twgl-full.module.js';
const lerp = (a, b, t) => a + (b - a) * t;
const getMipSize = (size, mipLevel) => size.map(s => Math.max(1, s >> mipLevel));
const vsGenMipmap = `#version 300 es
out vec2 v_uv;
const vec2 pos[3] = vec2[3](
vec2(-1, -1),
vec2(-1, 3),
vec2( 3, -1)
);
void main() {
vec2 xy = pos[gl_VertexID];
gl_Position = vec4(xy, 0.0, 1.0);
v_uv = xy * vec2(0.5, -0.5) + vec2(0.5);
}
`;
const fsGenMipmap = `#version 300 es
precision highp float;
uniform highp sampler2DArray tex;
uniform uint layer;
in vec2 v_uv;
out vec4 fragColor;
void main() {
fragColor = texture(tex, vec3(v_uv, layer));
}
`;
const vsView = `#version 300 es
uniform vec2 pos;
void main() {
gl_PointSize = 32.0;
gl_Position = vec4(pos, 0, 1);
}
`;
const fsView = `#version 300 es
precision highp float;
uniform highp sampler2DArray tex;
uniform uint layer;
uniform uint mipLevel;
out vec4 fragColor;
void main() {
fragColor = textureLod(tex, vec3(gl_PointCoord.xy, layer), float(mipLevel));
}
`;
/** @type HTMLCanvasElement */
const el = document.getElementById('c');
const gl = el.getContext('webgl2');
const genMipMapProgramInfo = twgl.createProgramInfo(gl, [vsGenMipmap, fsGenMipmap]);
const viewProgramInfo = twgl.createProgramInfo(gl, [vsView, fsView]);
const r = [255, 0, 0, 255];
const g = [0, 255, 0, 255];
const b = [0, 0, 255, 255];
const y = [255, 255, 0, 255];
const c = [0, 255, 255, 255];
const m = [255, 0, 255, 255];
const _ = [0, 0, 0, 255];
const w = [255, 255, 255, 255];
const layers = [
new Uint8Array([
r, r, g, g, r, r, b, b,
r, r, g, g, r, r, b, b,
g, g, r, g, b, b, r, r,
g, g, r, g, b, b, r, r,
r, r, g, g, r, r, b, b,
r, r, g, g, r, r, b, b,
g, g, r, g, b, b, r, r,
g, g, r, g, b, b, r, r,
].flat()),
new Uint8Array([
y, y, c, c, y, y, m, m,
y, y, c, c, y, y, m, m,
c, c, y, c, m, m, y, y,
c, c, y, c, m, m, y, y,
y, y, c, c, y, y, m, m,
y, y, c, c, y, y, m, m,
c, c, y, c, m, m, y, y,
c, c, y, c, m, m, y, y,
].flat()),
new Uint8Array([
w, w, _, _, w, w, _, _,
w, w, _, _, w, w, _, _,
_, _, w, _, _, _, w, w,
_, _, w, _, _, _, w, w,
w, w, _, _, w, w, _, _,
w, w, _, _, w, w, _, _,
_, _, w, _, _, _, w, w,
_, _, w, _, _, _, w, w,
].flat()),
];
const width = 8;
const height = 8;
const numMipLevels = 4;
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex);
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, numMipLevels, gl.RGBA8, width, height, layers.length);
layers.forEach((data, layer) => {
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, width, height, 1, gl.RGBA, gl.UNSIGNED_BYTE, data);
});
{
gl.useProgram(genMipMapProgramInfo.program);
const readFB = gl.createFramebuffer();
const drawFB = gl.createFramebuffer();
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, readFB);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawFB);
for (let mipLevel = 1; mipLevel < numMipLevels; ++mipLevel) {
const [readWidth, readHeight] = getMipSize([width, height], mipLevel - 1);
const [drawWidth, drawHeight] = getMipSize([width, height], mipLevel);
for (let layer = 0; layer < layers.length; ++layer) {
gl.framebufferTextureLayer(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex, mipLevel, layer);
gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex, mipLevel - 1, layer);
gl.blitFramebuffer(
0, 0, readWidth, readHeight,
0, 0, drawWidth, drawHeight,
gl.COLOR_BUFFER_BIT,
gl.LINEAR);
}
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
// reset texture back to default settings
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, 1000);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_BASE_LEVEL, 0);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LOD, 1000);
// show the mip levels
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(viewProgramInfo.program);
for (let layer = 0; layer < layers.length; ++layer) {
const l = layer / (layers.length - 1);
for (let mipLevel = 0; mipLevel < numMipLevels; ++mipLevel) {
const m = mipLevel / (numMipLevels - 1);
const pos = [
lerp(-0.6, 0.6, l),
lerp(-0.75, 0.75, m),
];
twgl.setUniforms(viewProgramInfo, {
pos,
layer,
mipLevel,
});
gl.drawArrays(gl.POINTS, 0, 1);
}
}
</script>
Those should produce the same results. The advantage to the draw method is you can customize your filtering algoritm.
Those should also suggest how to copy from one layer to another. Set the mipmap/layer in the DRAW_FRAMEBUFFER
via framebufferTextureLayer
then blit or draw. Just make sure if it's the same texture are you're reading from that your texture parameter settings are such that you don't get a feedback loop. In the code above this is done by setting TEXTURE_BASE_LEVEL
and TEXTURE_MAX_LEVEL
so that the shader is only reading from a single mip level