luma.gl/modules/webgl/test/classes/texture.spec.js

659 lines
19 KiB
JavaScript

/* eslint-disable max-len */
import test from 'tape-promise/tape';
import GL from '@luma.gl/constants';
import {isWebGL2} from '@luma.gl/webgl';
import {Buffer, Texture2D, getKey, readPixelsToArray} from '@luma.gl/webgl';
import {TEXTURE_FORMATS} from '@luma.gl/webgl/classes/texture-formats';
import {
testSamplerParameters,
SAMPLER_PARAMETERS,
SAMPLER_PARAMETERS_WEBGL2
} from './sampler.utils';
import {fixture} from 'test/setup';
test('WebGL#Texture2D construct/delete', (t) => {
const {gl} = fixture;
t.throws(
// @ts-expect-error
() => new Texture2D(),
/.*WebGLRenderingContext.*/,
'Texture2D throws on missing gl context'
);
const texture = new Texture2D(gl);
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
t.comment(JSON.stringify(texture.getParameters({keys: true})));
texture.delete();
t.ok(texture instanceof Texture2D, 'Texture2D delete successful');
texture.delete();
t.ok(texture instanceof Texture2D, 'Texture2D repeated delete successful');
t.end();
});
function isFormatSupported(format, glContext) {
format = Number(format);
const opts = Object.assign({format}, TEXTURE_FORMATS[format]);
if (!Texture2D.isSupported(glContext, {format}) || (!isWebGL2(glContext) && opts.compressed)) {
return false;
}
return true;
}
test('WebGL#Texture2D check formats', (t) => {
const {gl, gl2} = fixture;
const WEBGL1_FORMATS = [GL.RGB, GL.RGBA, GL.LUMINANCE_ALPHA, GL.LUMINANCE, GL.ALPHA];
const WEBGL2_FORMATS = [GL.R32F, GL.RG32F, GL.RGB32F, GL.RGBA32F];
let unSupportedFormats = [];
WEBGL1_FORMATS.forEach((format) => {
if (!isFormatSupported(format, gl)) {
unSupportedFormats.push(format);
}
});
t.ok(unSupportedFormats.length === 0, 'All WebGL1 formats are supported');
if (gl2) {
const gl2Formats = WEBGL1_FORMATS.concat(WEBGL2_FORMATS);
unSupportedFormats = [];
gl2Formats.forEach((format) => {
if (!isFormatSupported(format, gl2)) {
unSupportedFormats.push(format);
}
});
t.ok(unSupportedFormats.length === 0, 'All WebGL2 formats are supported');
} else {
t.comment('WebGL2 not available, skipping tests');
}
t.end();
});
const DEFAULT_TEXTURE_DATA = new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
const DATA = [1, 0.5, 0.25, 0.125];
const UINT8_DATA = new Uint8Array(DATA);
const UINT16_DATA = new Uint16Array(DATA);
const FLOAT_DATA = new Float32Array(DATA);
const TEXTURE_DATA = {
[GL.UNSIGNED_BYTE]: UINT8_DATA, // RGB_TO[GL.UNSIGNED_BYTE](DATA)),
[GL.UNSIGNED_SHORT_5_6_5]: UINT16_DATA, // RGB_TO[GL.UNSIGNED_SHORT_5_6_5](DATA))
[GL.UNSIGNED_SHORT_4_4_4_4]: UINT16_DATA, // RGB_TO[GL.UNSIGNED_SHORT_5_6_5](DATA))
[GL.UNSIGNED_SHORT_5_5_5_1]: UINT16_DATA, // RGB_TO[GL.UNSIGNED_SHORT_5_6_5](DATA))
[GL.FLOAT]: FLOAT_DATA
};
// const RGB_TO = {
// [GL.UNSIGNED_BYTE]: (r, g, b) => [r * 256, g * 256, b * 256],
// [GL.UNSIGNED_SHORT_5_6_5]: (r, g, b) => r * 32 << 11 + g * 64 << 6 + b * 32
// };
// const RGB_FROM = {
// [GL.UNSIGNED_BYTE]: v => [v[0] / 256, v[1] / 256, v[2] / 256],
// [GL.UNSIGNED_SHORT_5_6_5]: v => [v >> 11 / 32, v >> 6 % 64 / 64, v % 32 * 32]
// };
function testFormatCreation(t, glContext, withData = false) {
for (const formatName in TEXTURE_FORMATS) {
const formatInfo = TEXTURE_FORMATS[formatName];
for (let type of formatInfo.types) {
const format = Number(formatName);
type = Number(type);
const data = withData ? TEXTURE_DATA[type] || DEFAULT_TEXTURE_DATA : null;
const options = Object.assign({}, formatInfo, {
data,
format,
type,
mipmaps: format !== GL.RGB32F, // TODO: for some reason mipmap generation failing for RGB32F format
width: 1,
height: 1
});
if (Texture2D.isSupported(glContext, {format})) {
const texture = new Texture2D(glContext, options);
t.equals(
texture.format,
format,
`Texture2D({format: ${getKey(GL, format)}, type: ${getKey(
GL,
type
)}, dataFormat: ${getKey(GL, options.dataFormat)}) created`
);
texture.delete();
}
}
}
}
function testFormatDeduction(t, glContext) {
for (const format in TEXTURE_FORMATS) {
const formatInfo = TEXTURE_FORMATS[format];
const expectedType = formatInfo.types[0];
const expectedDataFormat = formatInfo.dataFormat;
const options = {
format: Number(format),
height: 1,
width: 1
};
if (Texture2D.isSupported(glContext, {format})) {
const texture = new Texture2D(glContext, options);
const msg = `Texture2D({format: ${getKey(GL, format)}}) created`;
t.equals(texture.format, Number(format), msg);
t.equals(texture.type, expectedType, msg);
t.equals(texture.dataFormat, expectedDataFormat, msg);
texture.delete();
}
}
}
test('WebGL#Texture2D format deduction', (t) => {
const {gl, gl2} = fixture;
testFormatDeduction(t, gl);
if (gl2) {
testFormatDeduction(t, gl2);
} else {
t.comment('WebGL2 not available, skipping tests');
}
t.end();
});
test('WebGL#Texture2D format creation', (t) => {
const {gl, gl2} = fixture;
testFormatCreation(t, gl);
if (gl2) {
testFormatCreation(t, gl2);
} else {
t.comment('WebGL2 not available, skipping tests');
}
t.end();
});
test('WebGL#Texture2D format creation with data', (t) => {
const {gl, gl2} = fixture;
testFormatCreation(t, gl, true);
if (gl2) {
testFormatCreation(t, gl2, true);
} else {
t.comment('WebGL2 not available, skipping tests');
}
t.end();
});
/*
test('WebGL#Texture2D WebGL1 extension format creation', t => {
const {gl} = fixture;
for (const format of TEXTURE_FORMATS) {
}
let texture = new Texture2D(gl, {});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
texture = texture.delete();
t.ok(texture instanceof Texture2D, 'Texture2D delete successful');
t.end();
});
test('WebGL#Texture2D WebGL2 format creation', t => {
const {gl} = fixture;
for (const format in TEXTURE_FORMATS) {
if (!WEBGL1_FORMATS.indexOf(format)) {
}
}
let texture = new Texture2D(gl, {});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
texture = texture.delete();
t.ok(texture instanceof Texture2D, 'Texture2D delete successful');
t.end();
});
*/
test('WebGL#Texture2D setParameters', (t) => {
const {gl} = fixture;
let texture = new Texture2D(gl, {});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
testSamplerParameters({t, texture, parameters: SAMPLER_PARAMETERS});
/*
// Bad tests
const parameter = GL.TEXTURE_MAG_FILTER;
const value = GL.LINEAR_MIPMAP_LINEAR;
texture.setParameters({
[parameter]: value
});
const newValue = texture.getParameter(GL.TEXTURE_MAG_FILTER);
t.equals(newValue, value,
`Texture2D.setParameters({[${getKey(GL, parameter)}]: ${getKey(GL, value)}})`);
*/
texture = texture.delete();
t.ok(texture instanceof Texture2D, 'Texture2D delete successful');
t.end();
});
test('WebGL2#Texture2D setParameters', (t) => {
const {gl2} = fixture;
if (!gl2) {
t.comment('WebGL2 not available, skipping tests');
t.end();
return;
}
let texture = new Texture2D(gl2, {});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
testSamplerParameters({t, texture, parameters: SAMPLER_PARAMETERS_WEBGL2});
texture = texture.delete();
t.ok(texture instanceof Texture2D, 'Texture2D delete successful');
t.end();
});
test('WebGL#Texture2D NPOT Workaround: texture creation', (t) => {
const {gl} = fixture;
// Create NPOT texture with no parameters
let texture = new Texture2D(gl, {data: null, width: 500, height: 512});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
// Default parameters should be changed to supported NPOT parameters.
let minFilter = texture.getParameter(GL.TEXTURE_MIN_FILTER);
t.equals(minFilter, GL.LINEAR, 'NPOT texture min filter is set to LINEAR');
let wrapS = texture.getParameter(GL.TEXTURE_WRAP_S);
t.equals(wrapS, GL.CLAMP_TO_EDGE, 'NPOT texture wrap_s is set to CLAMP_TO_EDGE');
let wrapT = texture.getParameter(GL.TEXTURE_WRAP_T);
t.equals(wrapT, GL.CLAMP_TO_EDGE, 'NPOT texture wrap_t is set to CLAMP_TO_EDGE');
const parameters = {
[GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
[GL.TEXTURE_WRAP_S]: GL.REPEAT,
[GL.TEXTURE_WRAP_T]: GL.MIRRORED_REPEAT
};
// Create NPOT texture with parameters
texture = new Texture2D(gl, {
data: null,
width: 512,
height: 600,
parameters
});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
// Above parameters should be changed to supported NPOT parameters.
minFilter = texture.getParameter(GL.TEXTURE_MIN_FILTER);
t.equals(minFilter, GL.NEAREST, 'NPOT texture min filter is set to NEAREST');
wrapS = texture.getParameter(GL.TEXTURE_WRAP_S);
t.equals(wrapS, GL.CLAMP_TO_EDGE, 'NPOT texture wrap_s is set to CLAMP_TO_EDGE');
wrapT = texture.getParameter(GL.TEXTURE_WRAP_T);
t.equals(wrapT, GL.CLAMP_TO_EDGE, 'NPOT texture wrap_t is set to CLAMP_TO_EDGE');
t.end();
});
test('WebGL#Texture2D NPOT Workaround: setParameters', (t) => {
const {gl} = fixture;
// Create NPOT texture
const texture = new Texture2D(gl, {data: null, width: 100, height: 100});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
const invalidNPOTParameters = {
[GL.TEXTURE_MIN_FILTER]: GL.LINEAR_MIPMAP_NEAREST,
[GL.TEXTURE_WRAP_S]: GL.MIRRORED_REPEAT,
[GL.TEXTURE_WRAP_T]: GL.REPEAT
};
texture.setParameters(invalidNPOTParameters);
// Above parameters should be changed to supported NPOT parameters.
const minFilter = texture.getParameter(GL.TEXTURE_MIN_FILTER);
t.equals(minFilter, GL.LINEAR, 'NPOT texture min filter is set to LINEAR');
const wrapS = texture.getParameter(GL.TEXTURE_WRAP_S);
t.equals(wrapS, GL.CLAMP_TO_EDGE, 'NPOT texture wrap_s is set to CLAMP_TO_EDGE');
const wrapT = texture.getParameter(GL.TEXTURE_WRAP_T);
t.equals(wrapT, GL.CLAMP_TO_EDGE, 'NPOT texture wrap_t is set to CLAMP_TO_EDGE');
t.end();
});
test('WebGL2#Texture2D NPOT Workaround: texture creation', (t) => {
// WebGL2 supports NPOT texture hence, texture parameters should not be changed.
const {gl2} = fixture;
if (!gl2) {
t.comment('WebGL2 not available, skipping tests');
t.end();
return;
}
// Create NPOT texture with no parameters
let texture = new Texture2D(gl2, {data: null, width: 500, height: 512});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
// Default values are un-changed.
let minFilter = texture.getParameter(GL.TEXTURE_MIN_FILTER);
t.equals(
minFilter,
GL.NEAREST_MIPMAP_LINEAR,
'NPOT texture min filter is set to NEAREST_MIPMAP_LINEAR'
);
let wrapS = texture.getParameter(GL.TEXTURE_WRAP_S);
t.equals(wrapS, GL.REPEAT, 'NPOT texture wrap_s is set to REPEAT');
let wrapT = texture.getParameter(GL.TEXTURE_WRAP_T);
t.equals(wrapT, GL.REPEAT, 'NPOT texture wrap_t is set to REPEAT');
const parameters = {
[GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
[GL.TEXTURE_WRAP_S]: GL.REPEAT,
[GL.TEXTURE_WRAP_T]: GL.MIRRORED_REPEAT
};
// Create NPOT texture with parameters
texture = new Texture2D(gl2, {
data: null,
width: 512,
height: 600,
parameters
});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
minFilter = texture.getParameter(GL.TEXTURE_MIN_FILTER);
t.equals(minFilter, GL.NEAREST, 'NPOT texture min filter is set to NEAREST');
wrapS = texture.getParameter(GL.TEXTURE_WRAP_S);
t.equals(wrapS, GL.REPEAT, 'NPOT texture wrap_s is set to REPEAT');
wrapT = texture.getParameter(GL.TEXTURE_WRAP_T);
t.equals(wrapT, GL.MIRRORED_REPEAT, 'NPOT texture wrap_t is set to MIRRORED_REPEAT');
t.end();
});
test('WebGL2#Texture2D NPOT Workaround: setParameters', (t) => {
const {gl2} = fixture;
if (!gl2) {
t.comment('WebGL2 not available, skipping tests');
t.end();
return;
}
// Create NPOT texture
const texture = new Texture2D(gl2, {data: null, width: 100, height: 100});
t.ok(texture instanceof Texture2D, 'Texture2D construction successful');
const invalidNPOTParameters = {
[GL.TEXTURE_MIN_FILTER]: GL.LINEAR_MIPMAP_NEAREST,
[GL.TEXTURE_WRAP_S]: GL.MIRRORED_REPEAT,
[GL.TEXTURE_WRAP_T]: GL.REPEAT
};
texture.setParameters(invalidNPOTParameters);
// Above parameters are not changed for NPOT texture when using WebGL2 context.
const minFilter = texture.getParameter(GL.TEXTURE_MIN_FILTER);
t.equals(
minFilter,
GL.LINEAR_MIPMAP_NEAREST,
'NPOT texture min filter is set to LINEAR_MIPMAP_NEAREST'
);
const wrapS = texture.getParameter(GL.TEXTURE_WRAP_S);
t.equals(wrapS, GL.MIRRORED_REPEAT, 'NPOT texture wrap_s is set to MIRRORED_REPEAT');
const wrapT = texture.getParameter(GL.TEXTURE_WRAP_T);
t.equals(wrapT, GL.REPEAT, 'NPOT texture wrap_t is set to REPEAT');
t.end();
});
test('WebGL1#Texture2D setImageData', (t) => {
const {gl} = fixture;
// data: null
const texture = new Texture2D(gl, {data: null, width: 2, height: 1, mipmaps: false});
t.deepEquals(readPixelsToArray(texture), new Float32Array(8), 'Pixels are empty');
// data: typed array
const data = new Uint8Array([0, 1, 2, 3, 128, 201, 255, 255]);
texture.setImageData({data});
t.deepEquals(readPixelsToArray(texture), data, 'Pixels are set correctly');
// data: canvas
if (typeof document !== 'undefined') {
const canvas = document.createElement('canvas');
canvas.width = 2;
canvas.height = 1;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, 2, 1);
imageData.data[2] = 128;
imageData.data[3] = 255;
imageData.data[7] = 1;
ctx.putImageData(imageData, 0, 0);
texture.setImageData({data: canvas});
t.deepEquals(
readPixelsToArray(texture),
new Uint8Array([0, 0, 128, 255, 0, 0, 0, 1]),
'Pixels are set correctly'
);
}
t.end();
});
test('WebGL2#Texture2D setImageData', (t) => {
const {gl2} = fixture;
if (!gl2) {
t.comment('WebGL2 not available, skipping tests');
t.end();
return;
}
let data;
// data: null
const texture = new Texture2D(gl2, {
data: null,
width: 2,
height: 1,
format: GL.RGBA32F,
type: GL.FLOAT,
mipmaps: false
});
t.deepEquals(readPixelsToArray(texture), new Float32Array(8), 'Pixels are empty');
// data: typed array
data = new Float32Array([0.1, 0.2, -3, -2, 0, 0.5, 128, 255]);
texture.setImageData({data});
t.deepEquals(readPixelsToArray(texture), data, 'Pixels are set correctly');
// data: buffer
data = new Float32Array([21, 0.82, 0, 1, 0, 255, 128, 3.333]);
const buffer = new Buffer(gl2, {data, accessor: {size: 4, type: GL.FLOAT}});
texture.setImageData({data: buffer});
t.deepEquals(readPixelsToArray(texture), data, 'Pixels are set correctly');
// data: canvas
if (typeof document !== 'undefined') {
const canvas = document.createElement('canvas');
canvas.width = 2;
canvas.height = 1;
const ctx = canvas.getContext('2d');
ctx.fillRect(0, 0, 2, 1);
texture.setImageData({data: canvas});
t.deepEquals(
readPixelsToArray(texture),
new Float32Array([0, 0, 0, 1, 0, 0, 0, 1]),
'Pixels are set correctly'
);
}
t.end();
});
test('WebGL1#Texture2D setSubImageData', (t) => {
const {gl} = fixture;
// data: null
const texture = new Texture2D(gl, {data: null, width: 2, height: 1, mipmaps: false});
t.deepEquals(readPixelsToArray(texture), new Uint8Array(8), 'Pixels are empty');
// data: typed array
const data = new Uint8Array([1, 2, 3, 4]);
texture.setSubImageData({data, x: 0, y: 0, width: 1, height: 1});
t.deepEquals(
readPixelsToArray(texture),
new Uint8Array([1, 2, 3, 4, 0, 0, 0, 0]),
'Pixels are set correctly'
);
// data: canvas
if (typeof document !== 'undefined') {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
ctx.fillRect(0, 0, 1, 1);
texture.setSubImageData({data: canvas, x: 1, y: 0, width: 1, height: 1});
t.deepEquals(
readPixelsToArray(texture),
new Uint8Array([1, 2, 3, 4, 0, 0, 0, 255]),
'Pixels are set correctly'
);
}
t.end();
});
test('WebGL2#Texture2D setSubImageData', (t) => {
const {gl2} = fixture;
if (!gl2) {
t.comment('WebGL2 not available, skipping tests');
t.end();
return;
}
let data;
// data: null
const texture = new Texture2D(gl2, {
data: null,
width: 2,
height: 1,
format: GL.RGBA32F,
type: GL.FLOAT,
mipmaps: false
});
t.deepEquals(readPixelsToArray(texture), new Float32Array(8), 'Pixels are empty');
// data: typed array
data = new Float32Array([0.1, 0.2, -3, -2]);
texture.setSubImageData({data, x: 0, y: 0, width: 1, height: 1});
t.deepEquals(
readPixelsToArray(texture),
new Float32Array([0.1, 0.2, -3, -2, 0, 0, 0, 0]),
'Pixels are set correctly'
);
// data: buffer
data = new Float32Array([-3, 255, 128, 3.333]);
const buffer = new Buffer(gl2, {data, accessor: {size: 4, type: GL.FLOAT}});
texture.setSubImageData({data: buffer, x: 1, y: 0, width: 1, height: 1});
t.deepEquals(
readPixelsToArray(texture),
new Float32Array([0.1, 0.2, -3, -2, -3, 255, 128, 3.333]),
'Pixels are set correctly'
);
// data: canvas
if (typeof document !== 'undefined') {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
ctx.fillRect(0, 0, 1, 1);
texture.setSubImageData({data: canvas, x: 1, y: 0, width: 1, height: 1});
t.deepEquals(
readPixelsToArray(texture),
new Float32Array([0.1, 0.2, -3, -2, 0, 0, 0, 1]),
'Pixels are set correctly'
);
}
t.end();
});
test('WebGL2#Texture2D resize', (t) => {
const {gl} = fixture;
let texture = new Texture2D(gl, {
data: null,
width: 2,
height: 2,
mipmaps: true
});
texture.resize({
width: 4,
height: 4,
mipmaps: true
});
t.ok(texture.mipmaps, 'mipmaps should set to true for POT.');
texture.resize({
width: 3,
height: 3,
mipmaps: true
});
t.notOk(texture.mipmaps, 'mipmaps should set to false when resizing to NPOT.');
texture = new Texture2D(gl, {
data: null,
width: 2,
height: 2,
mipmaps: true
});
texture.resize({
width: 4,
height: 4
});
t.notOk(texture.mipmaps, 'mipmaps should set to false when resizing.');
t.end();
});
test('WebGL2#Texture2D generateMipmap', (t) => {
const {gl} = fixture;
let texture = new Texture2D(gl, {
data: null,
width: 3,
height: 3,
mipmaps: false
});
texture.generateMipmap();
t.notOk(texture.mipmaps, 'Should not turn on mipmaps for NPOT.');
texture = new Texture2D(gl, {
data: null,
width: 2,
height: 2,
mipmaps: false
});
texture.generateMipmap();
t.ok(texture.mipmaps, 'Should turn on mipmaps for POT.');
t.end();
});