twgl.js/test/tests/texture-tests.js
Gregg Tavares 2b0e1daeb1 Support uploading multiple mip levels
This only works for passing arrays. It assumes you pass
however much data is needed for mips up to the level you
want uploaded. In other words. Say your texture is 10x7 RGBA.
Then src would be a TypedArray with 10x7 + 5x3 + 2x1 + 1x1
4 byte texels or exactly 352 bytes.

If you pass (10x7 + 5x3) * 4, which is 340 bytes, then it would
only fill out the first 2 mip levels. That texture would be
unrenderable unless you (1) set filtering to not use mips or
(2) set `maxLevel` to 1 (TEXTURE_MAX_LEVEL).
2025-07-15 23:15:58 -07:00

343 lines
9.8 KiB
JavaScript

import {
// assertArrayEqual,
// assertTruthy,
// assertFalsy,
assertEqual,
} from '../assert.js';
import {describe} from '../mocha-support.js';
import {
assertNoWebGLError,
createContext,
createContext2,
itWebGL,
itWebGL2,
checkColor,
setCanvasAndViewportSizeTo1x1
} from '../webgl.js';
function assertPixelFromTexture(gl, texture, expected) {
twgl.createFramebufferInfo(gl, [ { attachment: texture } ], 1, 2);
const actual = new Uint8Array(4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, actual);
assertEqual(actual, expected);
}
function createRedGreenURL() {
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 1;
ctx.canvas.height = 2;
ctx.fillStyle = '#F00';
ctx.fillRect(0, 0, 1, 1);
ctx.fillStyle = '#0F0';
ctx.fillRect(0, 1, 1, 1);
return ctx.canvas.toDataURL();
}
function create1PixelTextureRenderingProgram(gl) {
const vs = `#version 300 es
void main() {
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 1.0;
}
`;
const fs = `#version 300 es
precision highp float;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
fragColor = texture(u_texture, vec2(0.5));
}
`;
return twgl.createProgram(gl, [vs, fs]);
}
function create1PixelLodSelectingRenderingProgram(gl) {
const vs = `#version 300 es
void main() {
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 1.0;
}
`;
const fs = `#version 300 es
precision highp float;
uniform sampler2D u_texture;
uniform float u_lod;
out vec4 fragColor;
void main() {
fragColor = textureLod(u_texture, vec2(0.5), u_lod);
}
`;
const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
return (gl, lod) => {
gl.useProgram(prgInfo.program);
twgl.setUniforms(prgInfo, { u_lod: lod });
gl.drawArrays(gl.POINTS, 0, 1);
return prgInfo;
};
}
describe('texture tests', () => {
itWebGL(`test y flips correctly`, async() => {
const {gl} = createContext();
const red = [255, 0, 0, 255];
const green = [0, 255, 0, 255];
const src = [...red, ...green];
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
const t1gr = twgl.createTexture(gl, { src, width: 1 });
const t2rg = twgl.createTexture(gl, { src, width: 1, flipY: false });
const t3gr = twgl.createTexture(gl, { src, width: 1 });
assertPixelFromTexture(gl, t1gr, green);
assertPixelFromTexture(gl, t2rg, red);
assertPixelFromTexture(gl, t3gr, green);
// Test that the state is saved when async.
const p = twgl.createTextureAsync(gl, { src: createRedGreenURL() });
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
const { texture: t4gr } = await p;
assertPixelFromTexture(gl, t4gr, green);
assertNoWebGLError(gl);
});
itWebGL(`test pre-multiplies alpha correctly`, () => {
const {gl} = createContext();
const src = [255, 0, 0, 128];
const premultiplied = [128, 0, 0, 128];
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
const t1gr = twgl.createTexture(gl, { src });
const t2rg = twgl.createTexture(gl, { src, premultiplyAlpha: false });
const t3gr = twgl.createTexture(gl, { src });
assertPixelFromTexture(gl, t1gr, premultiplied);
assertPixelFromTexture(gl, t2rg, src);
assertPixelFromTexture(gl, t3gr, premultiplied);
assertNoWebGLError(gl);
});
itWebGL2('test uploads multiple mip levels', () => {
const {gl} = createContext2();
setCanvasAndViewportSizeTo1x1(gl);
const r = [255, 0, 0, 255];
const y = [255, 255, 0, 255];
const g = [ 0, 255, 0, 255];
const b = [ 0, 0, 255, 255];
const data = new Uint8Array([
y, y, y, y, y, y, y, y, y, y,
y, y, y, y, y, y, y, y, y, y,
y, y, y, y, y, y, y, y, y, y,
y, y, y, y, y, y, y, y, y, y,
y, y, y, y, y, y, y, y, y, y,
y, y, y, y, y, y, y, y, y, y,
y, y, y, y, y, y, y, y, y, y,
r, r, r, r, r,
r, r, r, r, r,
r, r, r, r, r,
g, g,
b,
].flat());
const texture = twgl.createTexture(gl, { width: 10, height: 7, src: data });
assertNoWebGLError(gl);
const drawFn = create1PixelLodSelectingRenderingProgram(gl);
const tests = [
{ lod: 0, expected: y },
{ lod: 1, expected: r },
{ lod: 2, expected: g },
{ lod: 3, expected: b },
];
tests.forEach(({lod, expected}, i) => {
drawFn(gl, lod);
checkColor(gl, expected, `mipLevel: ${i}`);
});
assertNoWebGLError(gl);
gl.deleteTexture(texture);
});
itWebGL2(`test compressed texture format EXT_texture_compression_bptc`, ['EXT_texture_compression_bptc'], async() => {
const {gl} = createContext2();
twgl.addExtensionsToContext(gl);
setCanvasAndViewportSizeTo1x1(gl);
const green = [2, 255, 2, 255];
const internalFormat = gl.COMPRESSED_RGBA_BPTC_UNORM;
const green_4x4 = new Uint8Array([32, 128, 193, 255, 15, 24, 252, 255, 175, 170, 170, 170, 0, 0, 0, 0]);
const width = 4;
const height = 4;
// eslint-disable-next-line no-unused-vars
const texture = twgl.createTexture(gl, { src: green_4x4, width, height, internalFormat });
assertNoWebGLError(gl);
const prg = create1PixelTextureRenderingProgram(gl);
gl.useProgram(prg);
gl.drawArrays(gl.POINTS, 0, 1);
checkColor(gl, green);
gl.deleteTexture(texture);
});
itWebGL2(`test compressed texture format WEBGL_compressed_texture_s3tc`, ['WEBGL_compressed_texture_s3tc'], async() => {
const {gl} = createContext2();
twgl.addExtensionsToContext(gl);
setCanvasAndViewportSizeTo1x1(gl);
const red = [255, 0, 0, 255];
const internalFormat = gl.COMPRESSED_RGB_S3TC_DXT1_EXT;
const red_4x4 = new Uint16Array([
0b11111_000000_00000,
0b11111_000000_00000,
0, 0,
]);
const width = 4;
const height = 4;
const texture = twgl.createTexture(gl, { src: red_4x4, width, height, internalFormat });
assertNoWebGLError(gl);
const prg = create1PixelTextureRenderingProgram(gl);
gl.useProgram(prg);
gl.drawArrays(gl.POINTS, 0, 1);
checkColor(gl, red);
assertNoWebGLError(gl);
gl.deleteTexture(texture);
});
itWebGL2(`test compressed texture format WEBGL_compressed_texture_s3tc with mips`, ['WEBGL_compressed_texture_s3tc'], async() => {
const {gl} = createContext2();
twgl.addExtensionsToContext(gl);
setCanvasAndViewportSizeTo1x1(gl);
const internalFormat = gl.COMPRESSED_RGB_S3TC_DXT1_EXT;
const red_4x4 = [
0b11111_000000_00000,
0b11111_000000_00000,
0, 0,
];
const yellow_4x4 = [
0b11111_111111_00000,
0b11111_111111_00000,
0, 0,
];
const green_4x4 = [
0b00000_111111_00000,
0b00000_111111_00000,
0, 0,
];
const blue_4x4 = [
0b00000_000000_11111,
0b00000_000000_11111,
0, 0,
];
const data = new Uint16Array([
...red_4x4, ...red_4x4, ...red_4x4, // 12x8
...red_4x4, ...red_4x4, ...red_4x4,
...green_4x4, ...green_4x4, // 6x4
...blue_4x4, // 3x2
...yellow_4x4, // 1x1
]);
const width = 12;
const height = 8;
const r = [255, 0, 0, 255];
const y = [255, 255, 0, 255];
const g = [ 0, 255, 0, 255];
const b = [ 0, 0, 255, 255];
const texture = twgl.createTexture(gl, { src: data, width, height, internalFormat });
assertNoWebGLError(gl);
const drawFn = create1PixelLodSelectingRenderingProgram(gl);
const tests = [
{ lod: 0, expected: r },
{ lod: 1, expected: g },
{ lod: 2, expected: b },
{ lod: 3, expected: y },
];
tests.forEach(({lod, expected}, i) => {
drawFn(gl, lod);
checkColor(gl, expected, `mipLevel: ${i}`);
});
assertNoWebGLError(gl);
gl.deleteTexture(texture);
});
itWebGL2(`test compressed texture format WEBGL_compressed_texture_s3tc cubemap`, ['WEBGL_compressed_texture_s3tc'], async() => {
const {gl} = createContext2();
twgl.addExtensionsToContext(gl);
setCanvasAndViewportSizeTo1x1(gl);
const internalFormat = gl.COMPRESSED_RGB_S3TC_DXT1_EXT;
const red565 = 0b11111_000000_00000;
const yellow565 = 0b11111_111111_00000;
const green565 = 0b0000_111111_00000;
const cyan565 = 0b0000_111111_11111;
const blue565 = 0b00000_000000_11111;
const magenta565 = 0b11111_000000_11111;
const cubeMapData = new Uint16Array([
...[red565, red565, 0, 0],
...[yellow565, yellow565, 0, 0],
...[green565, green565, 0, 0],
...[cyan565, cyan565, 0, 0],
...[blue565, blue565, 0, 0],
...[magenta565, magenta565, 0, 0],
]);
const width = 4;
const height = 4;
const texture = twgl.createTexture(gl, { target: gl.TEXTURE_CUBE_MAP, src: cubeMapData, width, height, internalFormat });
assertNoWebGLError(gl);
const vs = `#version 300 es
void main() {
gl_Position = vec4(0, 0, 0, 1);
gl_PointSize = 1.0;
}
`;
const fs = `#version 300 es
precision highp float;
uniform samplerCube u_texture;
uniform vec3 u_dir;
out vec4 fragColor;
void main() {
fragColor = texture(u_texture, u_dir);
}
`;
const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
gl.useProgram(prgInfo.program);
const tests = [
{ u_dir: [ 1, 0, 0], expected: [255, 0, 0, 255] },
{ u_dir: [-1, 0, 0], expected: [255, 255, 0, 255] },
{ u_dir: [ 0, 1, 0], expected: [ 0, 255, 0, 255] },
{ u_dir: [ 0, -1, 0], expected: [ 0, 255, 255, 255] },
{ u_dir: [ 0, 0, 1], expected: [ 0, 0, 255, 255] },
{ u_dir: [ 0, 0, -1], expected: [255, 0, 255, 255] },
];
for (const {u_dir, expected} of tests) {
twgl.setUniforms(prgInfo, { u_dir });
gl.drawArrays(gl.POINTS, 0, 1);
checkColor(gl, expected);
}
assertNoWebGLError(gl);
gl.deleteTexture(texture);
});
});