1010 lines
30 KiB
JavaScript
1010 lines
30 KiB
JavaScript
/*
|
|
* Copyright 2015-2020 MDN Contributors
|
|
* Copyright 2024 Mark Callow
|
|
* SPDX-License-Identifier: CC0-1.0
|
|
*/
|
|
|
|
/*
|
|
This code originated from sample 7 in the MDN WebGL examples
|
|
at https://github.com/mdn/webgl-examples which is licensed
|
|
under Creative Commons Zero v1.0 Universal. It has been
|
|
extensively modified to create a test for the JS wrapper
|
|
for libktx. Modifications are also licensed under CC0v1.
|
|
*/
|
|
|
|
var cubeRotation = 0.0;
|
|
var gl;
|
|
|
|
var astcSupported = false;
|
|
var etcSupported = false;
|
|
var dxtSupported = false;
|
|
var pvrtcSupported = false;
|
|
|
|
//
|
|
// Start here
|
|
//
|
|
const canvas = document.querySelector('#glcanvas');
|
|
gl = canvas.getContext('webgl2');
|
|
|
|
// If we don't have a GL context, give up now
|
|
if (!gl) {
|
|
alert('Unable to initialize WebGL. Your browser or machine may not support it.');
|
|
} else {
|
|
createKtxModule({preinitializedWebGLContext: gl}).then(instance => {
|
|
window.ktx = instance;
|
|
// Make existing WebGL context current for Emscripten OpenGL.
|
|
ktx.GL.makeContextCurrent(
|
|
ktx.GL.createContext(document.getElementById("glcanvas"),
|
|
{ majorVersion: 2.0 })
|
|
);
|
|
main()
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
|
|
elem('runtests').disabled = false;
|
|
|
|
astcSupported = !!gl.getExtension('WEBGL_compressed_texture_astc');
|
|
etcSupported = !!gl.getExtension('WEBGL_compressed_texture_etc1');
|
|
dxtSupported = !!gl.getExtension('WEBGL_compressed_texture_s3tc');
|
|
pvrtcSupported = !!(gl.getExtension('WEBGL_compressed_texture_pvrtc')) || !!(gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'));
|
|
|
|
// Vertex shader program
|
|
|
|
const vsSource = `
|
|
attribute vec4 aVertexPosition;
|
|
attribute vec3 aVertexNormal;
|
|
attribute vec3 aTextureCoord;
|
|
|
|
uniform mat4 uNormalMatrix;
|
|
uniform mat4 uModelViewMatrix;
|
|
uniform mat4 uProjectionMatrix;
|
|
uniform mat3 uUVMatrix;
|
|
|
|
varying highp vec2 vTextureCoord;
|
|
varying highp vec3 vLighting;
|
|
|
|
void main(void) {
|
|
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
|
|
//vTextureCoord.x = aTextureCoord.x;
|
|
// Invert Y coordinate to account for PNG top-left origin.
|
|
//vTextureCoord.y = aTextureCoord.y * -1.0 + 1.0;
|
|
vTextureCoord = vec2(uUVMatrix * aTextureCoord);
|
|
|
|
// Apply lighting effect
|
|
|
|
highp vec3 ambientLight = vec3(0.3, 0.3, 0.3);
|
|
highp vec3 directionalLightColor = vec3(1, 1, 1);
|
|
highp vec3 directionalVector = normalize(vec3(0.85, 0.8, 0.75));
|
|
|
|
highp vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0);
|
|
|
|
highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
|
|
vLighting = ambientLight + (directionalLightColor * directional);
|
|
}
|
|
`;
|
|
|
|
// Fragment shader program
|
|
|
|
const fsSource = `
|
|
varying highp vec2 vTextureCoord;
|
|
varying highp vec3 vLighting;
|
|
|
|
uniform sampler2D uSampler;
|
|
|
|
highp vec3 srgb_encode(highp vec3 color) {
|
|
highp float r = color.r < 0.0031308 ? 12.92 * color.r : 1.055 * pow(color.r, 1.0/2.4) - 0.055;
|
|
highp float g = color.g < 0.0031308 ? 12.92 * color.g : 1.055 * pow(color.g, 1.0/2.4) - 0.055;
|
|
highp float b = color.b < 0.0031308 ? 12.92 * color.b : 1.055 * pow(color.b, 1.0/2.4) - 0.055;
|
|
return vec3(r, g, b);
|
|
}
|
|
|
|
void main(void) {
|
|
highp vec3 vertexColor = vec3(0.9, 0.9, 0.9);
|
|
highp vec4 texelColor = texture2D(uSampler, vTextureCoord);
|
|
highp vec4 fragcolor;
|
|
// DECAL
|
|
fragcolor.rgb = vertexColor.rgb * (1.0 - texelColor.a) + texelColor.rgb * texelColor.a;
|
|
fragcolor.a = texelColor.a;
|
|
fragcolor.rgb *= vLighting;
|
|
fragcolor.rgb = srgb_encode(fragcolor.rgb);
|
|
gl_FragColor = fragcolor;
|
|
}
|
|
`;
|
|
|
|
// Initialize a shader program; this is where all the lighting
|
|
// for the vertices and so forth is established.
|
|
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
|
|
|
|
// Collect all the info needed to use the shader program.
|
|
// Look up which attributes our shader program is using
|
|
// for aVertexPosition, aVertexNormal, aTextureCoord,
|
|
// and look up uniform locations.
|
|
const programInfo = {
|
|
program: shaderProgram,
|
|
attribLocations: {
|
|
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
|
|
vertexNormal: gl.getAttribLocation(shaderProgram, 'aVertexNormal'),
|
|
textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
|
|
},
|
|
uniformLocations: {
|
|
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
|
|
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
|
|
normalMatrix: gl.getUniformLocation(shaderProgram, 'uNormalMatrix'),
|
|
uvMatrix: gl.getUniformLocation(shaderProgram, 'uUVMatrix'),
|
|
uSampler: gl.getUniformLocation(shaderProgram, 'uSampler'),
|
|
},
|
|
};
|
|
|
|
// Here's where we call the routine that builds all the
|
|
// objects we'll be drawing.
|
|
const buffers = initBuffers(gl);
|
|
|
|
var then = 0;
|
|
|
|
function resizeCanvasToDisplaySize(canvas) {
|
|
// Lookup the size the browser is displaying the canvas in CSS pixels.
|
|
const displayWidth = canvas.clientWidth;
|
|
const displayHeight = canvas.clientHeight;
|
|
|
|
// Check if the canvas is not the same size.
|
|
const needResize = canvas.width !== displayWidth ||
|
|
canvas.height !== displayHeight;
|
|
|
|
if (needResize) {
|
|
// Make the canvas the same size
|
|
canvas.width = displayWidth;
|
|
canvas.height = displayHeight;
|
|
}
|
|
|
|
return needResize;
|
|
}
|
|
|
|
// Draw the scene repeatedly
|
|
function render(now) {
|
|
now *= 0.001; // convert to seconds
|
|
const deltaTime = now - then;
|
|
then = now;
|
|
|
|
gl.enable(gl.CULL_FACE);
|
|
gl.enable(gl.DEPTH_TEST);
|
|
gl.enable(gl.SCISSOR_TEST);
|
|
// In case the source image has translucent parts ...
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
|
|
resizeCanvasToDisplaySize(gl.canvas);
|
|
|
|
for (const [index, value] of items.entries()) {
|
|
if (index == 0) continue;
|
|
|
|
const {element, color, texture} = value;
|
|
const rect = element.getBoundingClientRect(); // Includes padding and border.
|
|
var style = window.getComputedStyle(element, null);
|
|
|
|
const borderLeft = parseFloat(style.getPropertyValue("border-left-width"));
|
|
const borderRight = parseFloat(style.getPropertyValue("border-right-width"));
|
|
const borderTop = parseFloat(style.getPropertyValue("border-top-width"));
|
|
const borderBottom = parseFloat(style.getPropertyValue("border-bottom-width"));
|
|
// TODO: Handle padding as well.
|
|
|
|
// CSS has 0 padding, 0 border so no need for the above style heroics.
|
|
const cvrect = gl.canvas.getBoundingClientRect();
|
|
|
|
const width = rect.right - borderRight - rect.left - borderLeft;
|
|
const height = rect.bottom - borderBottom - rect.top - borderTop;
|
|
const left = rect.left + borderLeft - cvrect.left;
|
|
const bottom = cvrect.bottom - rect.bottom + borderBottom;
|
|
|
|
if (bottom < 0 || bottom + height > gl.canvas.clientHeight ||
|
|
left + width < 0 || left > gl.canvas.clientWidth) {
|
|
continue; // it's off screen
|
|
}
|
|
|
|
gl.viewport(left, bottom, width, height);
|
|
// To limit clearing to the viewport.
|
|
gl.scissor(left, bottom, width, height);
|
|
gl.clearColor(...color);
|
|
// Tell WebGL we want to affect texture unit 0
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
// Bind the texture to texture unit 0
|
|
gl.bindTexture(texture.target, texture.object);
|
|
|
|
// Create a perspective projection matrix.
|
|
// Our field of view is 45 degrees, with a width/height
|
|
// ratio that matches the size of the current item, and
|
|
// we only want to see objects between 0.1 units and
|
|
// 100 units away from the camera.
|
|
|
|
const fieldOfView = 45 * Math.PI / 180; // in radians
|
|
const aspect = width / height;
|
|
const zNear = 0.1;
|
|
const zFar = 100.0;
|
|
const projectionMatrix = mat4.create();
|
|
|
|
// note: glmatrix.js always has the first argument
|
|
// as the destination to receive the result.
|
|
mat4.perspective(projectionMatrix,
|
|
fieldOfView,
|
|
aspect,
|
|
zNear,
|
|
zFar);
|
|
|
|
drawScene(gl, projectionMatrix, texture.uvMatrix, programInfo, buffers, texture, deltaTime);
|
|
}
|
|
requestAnimationFrame(render);
|
|
}
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
//
|
|
// initBuffers
|
|
//
|
|
// Initialize the buffers we'll need. For this demo, we just
|
|
// have one object -- a simple three-dimensional cube.
|
|
//
|
|
function initBuffers(gl) {
|
|
|
|
// Create a buffer for the cube's vertex positions.
|
|
|
|
const positionBuffer = gl.createBuffer();
|
|
|
|
// Select the positionBuffer as the one to apply buffer
|
|
// operations to from here out.
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
|
|
// Now create an array of positions for the cube.
|
|
|
|
const positions = [
|
|
// Front face
|
|
-1.0, -1.0, 1.0,
|
|
1.0, -1.0, 1.0,
|
|
1.0, 1.0, 1.0,
|
|
-1.0, 1.0, 1.0,
|
|
|
|
// Back face
|
|
-1.0, -1.0, -1.0,
|
|
-1.0, 1.0, -1.0,
|
|
1.0, 1.0, -1.0,
|
|
1.0, -1.0, -1.0,
|
|
|
|
// Top face
|
|
-1.0, 1.0, -1.0,
|
|
-1.0, 1.0, 1.0,
|
|
1.0, 1.0, 1.0,
|
|
1.0, 1.0, -1.0,
|
|
|
|
// Bottom face
|
|
-1.0, -1.0, -1.0,
|
|
1.0, -1.0, -1.0,
|
|
1.0, -1.0, 1.0,
|
|
-1.0, -1.0, 1.0,
|
|
|
|
// Right face
|
|
1.0, -1.0, -1.0,
|
|
1.0, 1.0, -1.0,
|
|
1.0, 1.0, 1.0,
|
|
1.0, -1.0, 1.0,
|
|
|
|
// Left face
|
|
-1.0, -1.0, -1.0,
|
|
-1.0, -1.0, 1.0,
|
|
-1.0, 1.0, 1.0,
|
|
-1.0, 1.0, -1.0,
|
|
];
|
|
|
|
// Now pass the list of positions into WebGL to build the
|
|
// shape. We do this by creating a Float32Array from the
|
|
// JavaScript array, then use it to fill the current buffer.
|
|
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
|
|
|
// Set up the normals for the vertices, so that we can compute lighting.
|
|
|
|
const normalBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
|
|
|
const vertexNormals = [
|
|
// Front
|
|
0.0, 0.0, 1.0,
|
|
0.0, 0.0, 1.0,
|
|
0.0, 0.0, 1.0,
|
|
0.0, 0.0, 1.0,
|
|
|
|
// Back
|
|
0.0, 0.0, -1.0,
|
|
0.0, 0.0, -1.0,
|
|
0.0, 0.0, -1.0,
|
|
0.0, 0.0, -1.0,
|
|
|
|
// Top
|
|
0.0, 1.0, 0.0,
|
|
0.0, 1.0, 0.0,
|
|
0.0, 1.0, 0.0,
|
|
0.0, 1.0, 0.0,
|
|
|
|
// Bottom
|
|
0.0, -1.0, 0.0,
|
|
0.0, -1.0, 0.0,
|
|
0.0, -1.0, 0.0,
|
|
0.0, -1.0, 0.0,
|
|
|
|
// Right
|
|
1.0, 0.0, 0.0,
|
|
1.0, 0.0, 0.0,
|
|
1.0, 0.0, 0.0,
|
|
1.0, 0.0, 0.0,
|
|
|
|
// Left
|
|
-1.0, 0.0, 0.0,
|
|
-1.0, 0.0, 0.0,
|
|
-1.0, 0.0, 0.0,
|
|
-1.0, 0.0, 0.0
|
|
];
|
|
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals),
|
|
gl.STATIC_DRAW);
|
|
|
|
// Now set up the texture coordinates for the faces.
|
|
|
|
const textureCoordBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
|
|
|
|
const textureCoordinates = [
|
|
// Front
|
|
0.0, 0.0,
|
|
1.0, 0.0,
|
|
1.0, 1.0,
|
|
0.0, 1.0,
|
|
// Back
|
|
0.0, 0.0,
|
|
1.0, 0.0,
|
|
1.0, 1.0,
|
|
0.0, 1.0,
|
|
// Top
|
|
0.0, 0.0,
|
|
1.0, 0.0,
|
|
1.0, 1.0,
|
|
0.0, 1.0,
|
|
// Bottom
|
|
0.0, 0.0,
|
|
1.0, 0.0,
|
|
1.0, 1.0,
|
|
0.0, 1.0,
|
|
// Right
|
|
0.0, 0.0,
|
|
1.0, 0.0,
|
|
1.0, 1.0,
|
|
0.0, 1.0,
|
|
// Left
|
|
0.0, 0.0,
|
|
1.0, 0.0,
|
|
1.0, 1.0,
|
|
0.0, 1.0,
|
|
];
|
|
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates),
|
|
gl.STATIC_DRAW);
|
|
|
|
// Build the element array buffer; this specifies the indices
|
|
// into the vertex arrays for each face's vertices.
|
|
|
|
const indexBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
|
|
// This array defines each face as two triangles, using the
|
|
// indices into the vertex array to specify each triangle's
|
|
// position.
|
|
|
|
const indices = [
|
|
0, 1, 2, 0, 2, 3, // front
|
|
4, 5, 6, 4, 6, 7, // back
|
|
8, 9, 10, 8, 10, 11, // top
|
|
12, 13, 14, 12, 14, 15, // bottom
|
|
16, 17, 18, 16, 18, 19, // right
|
|
20, 21, 22, 20, 22, 23, // left
|
|
];
|
|
|
|
// Now send the element array to GL
|
|
|
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
|
|
new Uint16Array(indices), gl.STATIC_DRAW);
|
|
|
|
return {
|
|
position: positionBuffer,
|
|
normal: normalBuffer,
|
|
textureCoord: textureCoordBuffer,
|
|
indices: indexBuffer,
|
|
};
|
|
}
|
|
|
|
function elem(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
// Upload content of a ktxTexture to WebGL.
|
|
//
|
|
// Returns the created WebGL texture object and texture target.
|
|
function uploadTextureToGl(gl, ktexture) {
|
|
const { transcode_fmt } = ktx;
|
|
var formatString;
|
|
|
|
if (ktexture.needsTranscoding) {
|
|
var format;
|
|
if (astcSupported) {
|
|
formatString = 'ASTC';
|
|
format = transcode_fmt.ASTC_4x4_RGBA;
|
|
} else if (dxtSupported) {
|
|
formatString = ktexture.numComponents == 4 ? 'BC3' : 'BC1';
|
|
format = transcode_fmt.BC1_OR_3;
|
|
} else if (pvrtcSupported) {
|
|
formatString = 'PVRTC1';
|
|
format = transcode_fmt.PVRTC1_4_RGBA;
|
|
} else if (etcSupported) {
|
|
formatString = 'ETC';
|
|
format = transcode_fmt.ETC;
|
|
} else {
|
|
formatString = 'RGBA4444';
|
|
format = transcode_fmt.RGBA4444;
|
|
}
|
|
if (ktexture.transcodeBasis(format, 0) != ktx.error_code.SUCCESS) {
|
|
alert('Texture transcode failed. See console for details.');
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
const result = ktexture.glUpload();
|
|
if (result.error != gl.NO_ERROR) {
|
|
alert('WebGL error when uploading texture, code = '
|
|
+ result.error.toString(16));
|
|
return undefined;
|
|
}
|
|
if (result.object === undefined) {
|
|
alert('Texture upload failed. See console for details.');
|
|
return undefined;
|
|
}
|
|
if (result.target != gl.TEXTURE_2D) {
|
|
alert('Loaded texture is not a TEXTURE2D.');
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
target: result.target,
|
|
object: result.object,
|
|
format: formatString,
|
|
uvMatrix: null
|
|
}
|
|
}
|
|
|
|
function createPlaceholderTexture(gl, color)
|
|
{
|
|
// // Must create texture via Emscripten so it knows of it.
|
|
// var texName;
|
|
// ktx.GL._glGenTextures(1, texName);
|
|
// texture = ktx.GL.textures[texName];
|
|
// Since it doesn't seem possible to get the above to work
|
|
// use a placeholder WebGLTexture object to hold the temporary
|
|
// image.
|
|
const placeholder = gl.createTexture();
|
|
gl.bindTexture(gl.TEXTURE_2D, placeholder);
|
|
|
|
const level = 0;
|
|
const internalFormat = gl.RGBA;
|
|
const width = 1;
|
|
const height = 1;
|
|
const border = 0;
|
|
const srcFormat = gl.RGBA;
|
|
const srcType = gl.UNSIGNED_BYTE;
|
|
const pixel = new Uint8Array(color);
|
|
|
|
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
|
|
width, height, border, srcFormat, srcType,
|
|
pixel);
|
|
return {
|
|
target: gl.TEXTURE_2D,
|
|
object: placeholder,
|
|
format: "",
|
|
uvMatrix: mat3.create()
|
|
};
|
|
}
|
|
|
|
async function setUVMatrix(texture, inMatrix, ktexture) {
|
|
texture.uvMatrix = inMatrix;
|
|
if (ktexture.orientation.x == ktx.OrientationX.LEFT) {
|
|
mat3.translate(texture.uvMatrix, texture.uvMatrix, [1.0, 0.0]);
|
|
mat3.scale(texture.uvMatrix, texture.uvMatrix, [-1.0, 1.0]);
|
|
}
|
|
if (ktexture.orientation.y == ktx.OrientationY.DOWN) {
|
|
mat3.translate(texture.uvMatrix, texture.uvMatrix, [0.0, 1.0]);
|
|
mat3.scale(texture.uvMatrix, texture.uvMatrix, [1.0, -1.0]);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Binds a texture and sets suitable texture parameters.
|
|
//
|
|
// The WebGL texture object is expected to have been created from the
|
|
// content of the ktxTexture object.
|
|
//
|
|
function setTexParameters(texture, ktexture) {
|
|
gl.bindTexture(texture.target, texture.object);
|
|
|
|
if (ktexture.numLevels > 1 || ktexture.generateMipmaps) {
|
|
// Enable bilinear mipmapping.
|
|
gl.texParameteri(texture.target,
|
|
gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
|
|
} else {
|
|
gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
}
|
|
gl.texParameteri(texture.target, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
|
|
gl.bindTexture(texture.target, null);
|
|
}
|
|
|
|
function isPowerOf2(value) {
|
|
return (value & (value - 1)) == 0;
|
|
}
|
|
|
|
//
|
|
// Draw the scene.
|
|
//
|
|
function drawScene(gl, projectionMatrix, uvMatrix, programInfo, buffers, texture, deltaTime) {
|
|
gl.clearDepth(1.0); // Clear everything
|
|
gl.depthFunc(gl.LEQUAL); // Near things obscure far things
|
|
|
|
// Clear the canvas before we start drawing on it.
|
|
|
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
|
|
// Set the drawing position to the "identity" point, which is
|
|
// the center of the scene.
|
|
const modelViewMatrix = mat4.create();
|
|
|
|
// Now move the drawing position a bit to where we want to
|
|
// start drawing the square.
|
|
|
|
mat4.translate(modelViewMatrix, // destination matrix
|
|
modelViewMatrix, // matrix to translate
|
|
[-0.0, 0.0, -6.0]); // amount to translate
|
|
mat4.rotate(modelViewMatrix, // destination matrix
|
|
modelViewMatrix, // matrix to rotate
|
|
cubeRotation, // amount to rotate in radians
|
|
[0, 0, 1]); // axis to rotate around (Z)
|
|
mat4.rotate(modelViewMatrix, // destination matrix
|
|
modelViewMatrix, // matrix to rotate
|
|
cubeRotation * .7,// amount to rotate in radians
|
|
[0, 1, 0]); // axis to rotate around (X)
|
|
|
|
const normalMatrix = mat4.create();
|
|
mat4.invert(normalMatrix, modelViewMatrix);
|
|
mat4.transpose(normalMatrix, normalMatrix);
|
|
|
|
// Tell WebGL how to pull out the positions from the position
|
|
// buffer into the vertexPosition attribute
|
|
{
|
|
const numComponents = 3;
|
|
const type = gl.FLOAT;
|
|
const normalize = false;
|
|
const stride = 0;
|
|
const offset = 0;
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
|
|
gl.vertexAttribPointer(
|
|
programInfo.attribLocations.vertexPosition,
|
|
numComponents,
|
|
type,
|
|
normalize,
|
|
stride,
|
|
offset);
|
|
gl.enableVertexAttribArray(
|
|
programInfo.attribLocations.vertexPosition);
|
|
}
|
|
|
|
// Tell WebGL how to pull out the texture coordinates from
|
|
// the texture coordinate buffer into the textureCoord attribute.
|
|
{
|
|
const numComponents = 2;
|
|
const type = gl.FLOAT;
|
|
const normalize = false;
|
|
const stride = 0;
|
|
const offset = 0;
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
|
|
gl.vertexAttribPointer(
|
|
programInfo.attribLocations.textureCoord,
|
|
numComponents,
|
|
type,
|
|
normalize,
|
|
stride,
|
|
offset);
|
|
gl.enableVertexAttribArray(
|
|
programInfo.attribLocations.textureCoord);
|
|
}
|
|
|
|
// Tell WebGL how to pull out the normals from
|
|
// the normal buffer into the vertexNormal attribute.
|
|
{
|
|
const numComponents = 3;
|
|
const type = gl.FLOAT;
|
|
const normalize = false;
|
|
const stride = 0;
|
|
const offset = 0;
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
|
|
gl.vertexAttribPointer(
|
|
programInfo.attribLocations.vertexNormal,
|
|
numComponents,
|
|
type,
|
|
normalize,
|
|
stride,
|
|
offset);
|
|
gl.enableVertexAttribArray(
|
|
programInfo.attribLocations.vertexNormal);
|
|
}
|
|
|
|
// Tell WebGL which indices to use to index the vertices
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
|
|
|
|
// Tell WebGL to use our program when drawing
|
|
|
|
gl.useProgram(programInfo.program);
|
|
|
|
// Set the shader uniforms
|
|
|
|
gl.uniformMatrix4fv(
|
|
programInfo.uniformLocations.projectionMatrix,
|
|
false,
|
|
projectionMatrix);
|
|
gl.uniformMatrix4fv(
|
|
programInfo.uniformLocations.modelViewMatrix,
|
|
false,
|
|
modelViewMatrix);
|
|
gl.uniformMatrix4fv(
|
|
programInfo.uniformLocations.normalMatrix,
|
|
false,
|
|
normalMatrix);
|
|
gl.uniformMatrix3fv(
|
|
programInfo.uniformLocations.uvMatrix,
|
|
false,
|
|
uvMatrix);
|
|
|
|
// Tell the shader we bound the texture to texture unit 0
|
|
gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
|
|
|
|
{
|
|
const vertexCount = 36;
|
|
const type = gl.UNSIGNED_SHORT;
|
|
const offset = 0;
|
|
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
|
|
}
|
|
|
|
// Update the rotation for the next draw
|
|
|
|
cubeRotation += deltaTime;
|
|
}
|
|
|
|
//
|
|
// Initialize a shader program, so WebGL knows how to draw our data
|
|
//
|
|
function initShaderProgram(gl, vsSource, fsSource) {
|
|
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
|
|
// Create the shader program
|
|
|
|
const shaderProgram = gl.createProgram();
|
|
gl.attachShader(shaderProgram, vertexShader);
|
|
gl.attachShader(shaderProgram, fragmentShader);
|
|
gl.linkProgram(shaderProgram);
|
|
|
|
// If creating the shader program failed, alert
|
|
|
|
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
|
alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
|
|
return null;
|
|
}
|
|
|
|
return shaderProgram;
|
|
}
|
|
|
|
//
|
|
// creates a shader of the given type, uploads the source and
|
|
// compiles it.
|
|
//
|
|
function loadShader(gl, type, source) {
|
|
const shader = gl.createShader(type);
|
|
|
|
// Send the source to the shader object
|
|
|
|
gl.shaderSource(shader, source);
|
|
|
|
// Compile the shader program
|
|
|
|
gl.compileShader(shader);
|
|
|
|
// See if it compiled successfully
|
|
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
|
|
gl.deleteShader(shader);
|
|
return null;
|
|
}
|
|
|
|
return shader;
|
|
}
|
|
|
|
async function updateItem(item, texture) {
|
|
gl.deleteTexture(item.texture.object);
|
|
item.texture = texture;
|
|
}
|
|
|
|
function arraysEqual(a, b) {
|
|
if (a.length != b.length)
|
|
return false;
|
|
for (let i = 0; i < a.length; i++)
|
|
if (a[i] != b[i]) return false;
|
|
return true;
|
|
}
|
|
|
|
function showTestResult(id, pass) {
|
|
element =elem(id);
|
|
if (pass) {
|
|
element.innerText = "PASSED";
|
|
element.className = "pass";
|
|
} else {
|
|
element.innerText = "FAILED.";
|
|
element.className = "fail";
|
|
}
|
|
}
|
|
|
|
// Test creation of a ktxTexture2 from the provided imageData.
|
|
async function testCreate(imageData) {
|
|
const createInfo = new ktx.textureCreateInfo();
|
|
const colorSpace = imageData.colorSpace;
|
|
|
|
createInfo.baseWidth = imageData.width;
|
|
createInfo.baseHeight = imageData.height;
|
|
createInfo.baseDepth = 1;
|
|
createInfo.numDimensions = 2;
|
|
createInfo.numLevels = 1;
|
|
createInfo.numLayers = 1;
|
|
createInfo.numFaces = 1;
|
|
createInfo.isArray = false;
|
|
createInfo.generateMipmaps = false;
|
|
|
|
var displayP3;
|
|
// Image data from 2d canvases is always 8-bit RGBA.
|
|
// The only possible ImageData colorSpace choices are undefined, "srgb"
|
|
// and "displayp3." All use the sRGB transfer function.
|
|
createInfo.vkFormat = ktx.VkFormat.R8G8B8A8_SRGB;
|
|
if ( imageData.colorSpace == "display-p3") {
|
|
displayP3 = true;
|
|
}
|
|
|
|
const ktexture = new ktx.texture(createInfo,
|
|
ktx.TextureCreateStorageEnum.ALLOC_STORAGE);
|
|
showTestResult('create_result', ktexture != null);
|
|
if (ktexture != null) {
|
|
if (displayP3) {
|
|
ktexture.primaries = ktx.khr_df_primaries.DISPLAYP3;
|
|
}
|
|
// Check DFD settings. Oetf should be SRGB due to SRGB vkFormat.
|
|
// Primaries default to BT709 unless set to P3 above.
|
|
const expectedPrimaries = displayP3 ? ktx.khr_df_primaries.DISPLAYP3
|
|
: ktx.khr_df_primaries.BT709;
|
|
const oetf = ktexture.oetf;
|
|
const primaries = ktexture.primaries;
|
|
// Do not know how to compare without using .value as all these
|
|
// enumerators are objects.
|
|
showTestResult('colorspace_set_result',
|
|
( oetf.value == ktx.khr_df_transfer.SRGB.value
|
|
&& primaries.value == expectedPrimaries.value));
|
|
|
|
result = ktexture.setImageFromMemory(0, 0, 0, imageData.data);
|
|
showTestResult('copy_image_result', result == ktx.error_code.SUCCESS);
|
|
}
|
|
return ktexture;
|
|
}
|
|
|
|
function testWriteReadMetadata(ktexture) {
|
|
const writer = "libktx-js-test";
|
|
const orientation = "rd";
|
|
ktexture.addKVPairString(ktx.ORIENTATION_KEY, orientation);
|
|
ktexture.addKVPairString(ktx.WRITER_KEY, writer);
|
|
|
|
var textDecoder = new TextDecoder();
|
|
var value = ktexture.findKeyValue(ktx.WRITER_KEY);
|
|
// subarray to remove the terminating null we know is there.
|
|
var string = textDecoder.decode(value.subarray(0,value.byteLength-1));
|
|
var passed = true;
|
|
//console.log(string);
|
|
if (!writer.localeCompare(string)) {
|
|
value = ktexture.findKeyValue(ktx.ORIENTATION_KEY);
|
|
string = textDecoder.decode(value.subarray(0,value.byteLength-1));
|
|
//console.log(string);
|
|
if (orientation.localeCompare(string)) {
|
|
passed = false;
|
|
}
|
|
} else {
|
|
passed = false;
|
|
}
|
|
|
|
if (passed) {
|
|
// Test passing of array-valued metadata between JS & C++.
|
|
var animData = new Uint32Array(3);
|
|
animData[0] = 20; // duration
|
|
animData[1] = 1000; // timescale
|
|
animData[2] = 10; // loopCount
|
|
// In real use the data should be endian-converted when on a
|
|
// big-endian machine as the KTX v2 spec. requires these values
|
|
// be little-endian. For the purpose of this test, it is not
|
|
// necessary as the data is not used.
|
|
ktexture.addKVPairByte(ktx.ANIMDATA_KEY, animData);
|
|
value = ktexture.findKeyValue(ktx.ANIMDATA_KEY);
|
|
if (value != null) {
|
|
// In real use, data needs to be endian-converted when on a
|
|
// big-endian machine.
|
|
const retData = new Uint32Array(value.buffer, value.byteOffset,
|
|
value.length / 4);
|
|
if (retData[0] != 20 || retData[1] != 1000 || retData[2] != 10)
|
|
passed = false;
|
|
} else {
|
|
passed = false;
|
|
}
|
|
// Since ktexture is not an array texture presence of AnimData
|
|
// will cause the a load to fail So delete the AnimData.
|
|
ktexture.deleteKVPair(ktx.ANIMDATA_KEY);
|
|
}
|
|
showTestResult('metadata_result', passed);
|
|
}
|
|
|
|
async function testGetImage(ktexture, imageData) {
|
|
var passed = true;
|
|
|
|
result = ktexture.getImage(0, 0, 0);
|
|
if (result != null) {
|
|
passed = arraysEqual(result, imageData.data);
|
|
} else {
|
|
passed = false;
|
|
}
|
|
showTestResult('get_image_result', passed);
|
|
}
|
|
|
|
async function testEncodeBasis(ktexture) {
|
|
const basisu_options = new ktx.basisParams();
|
|
|
|
basisu_options.uastc = false;
|
|
basisu_options.noSSE = true;
|
|
basisu_options.verbose = false;
|
|
basisu_options.qualityLevel = 200;
|
|
basisu_options.compressionLevel = ktx.ETC1S_DEFAULT_COMPRESSION_LEVEL;
|
|
|
|
var result = ktexture.compressBasis(basisu_options);
|
|
|
|
showTestResult('compress_basis_result', result == ktx.error_code.SUCCESS);
|
|
}
|
|
|
|
async function testEncodeAstc(ktexture) {
|
|
const params = new ktx.astcParams();
|
|
|
|
params.blockDimension = ktx.pack_astc_block_dimension.D8x8;
|
|
params.mode = ktx.pack_astc_encoder_mode.DEFAULT;
|
|
params.qualityLevel = ktx.pack_astc_quality_levels.FAST;
|
|
params.normalMap = false;
|
|
|
|
// Before we compress, test inputSwizzle setting
|
|
params.inputSwizzle = 'rrrg';
|
|
showTestResult('swizzle_set_result',
|
|
params.inputSwizzle.localeCompare('rrrg') == 0);
|
|
params.inputSwizzle = ''; // Reset to default.
|
|
|
|
var result = ktexture.compressAstc(params);
|
|
showTestResult('compress_astc_result', result == ktx.error_code.SUCCESS);
|
|
}
|
|
|
|
async function testCreateCopy(ktexture) {
|
|
const copy = ktexture.createCopy();
|
|
showTestResult('create_copy_result', copy != null);
|
|
return copy;
|
|
}
|
|
|
|
async function testWriteToMemoryAndRead(ktexture) {
|
|
// result is a KTX file in memory with the compressed image.
|
|
result = ktexture.writeToMemory();
|
|
// TODO: Check the first bytes are a KTX header.
|
|
showTestResult('write_to_memory_result', result != null);
|
|
|
|
readKTexture = new ktx.texture(result);
|
|
showTestResult('read_from_memory_result', result != null);
|
|
const readTexture = await uploadTextureToGl(gl, readKTexture);
|
|
setUVMatrix(readTexture, mat3.create(), readKTexture);
|
|
setTexParameters(readTexture, readKTexture);
|
|
updateItem(items[writeReadTextureItem], readTexture);
|
|
}
|
|
|
|
async function loadImageData (img, flip = false) {
|
|
const canvas = document.createElement("canvas");
|
|
const context = canvas.getContext("2d");
|
|
// These values draw the full image which gives a better starting
|
|
// point for compression.
|
|
//const width = img.naturalWidth;
|
|
//const height = img.naturalHeight;
|
|
// These values draw the image at the size displayed on the web
|
|
// page which, per CSS, is currently 256px giving a faster result
|
|
// which saves time for testing.
|
|
const width = img.width;
|
|
const height = img.height;
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
if (flip) {
|
|
context.translate(0, height);
|
|
context.scale(1, -1);
|
|
}
|
|
context.drawImage(img, 0, 0, width, height);
|
|
|
|
const imageData = context.getImageData(0, 0, width, height);
|
|
return imageData;
|
|
};
|
|
|
|
async function loadImage(src){
|
|
return new Promise((resolve, reject) => {
|
|
let img = new Image();
|
|
div = items[origImageItem].element;
|
|
img.onload = () => { div.appendChild(img); resolve(img); }
|
|
img.onerror = reject;
|
|
img.src = src;
|
|
})
|
|
}
|
|
|
|
async function runTests(filename) {
|
|
const img = await loadImage(filename);
|
|
const imageData = await loadImageData(img);
|
|
console.log(img);
|
|
console.log(imageData);
|
|
|
|
const ktexture = await testCreate(imageData);
|
|
if (ktexture == null)
|
|
return;
|
|
|
|
testWriteReadMetadata(ktexture);
|
|
|
|
const texture = await uploadTextureToGl(gl, ktexture);
|
|
setUVMatrix(texture, mat3.create(), ktexture);
|
|
setTexParameters(texture, ktexture);
|
|
updateItem(items[uncompTextureItem], texture);
|
|
|
|
await testGetImage(ktexture, imageData);
|
|
|
|
const ktextureCopy = await testCreateCopy(ktexture);
|
|
const textureCopy = uploadTextureToGl(gl, ktextureCopy);
|
|
setUVMatrix(textureCopy, mat3.create(), ktextureCopy);
|
|
updateItem(items[copyTextureItem], textureCopy);
|
|
|
|
await testEncodeBasis(ktexture);
|
|
textureComp = uploadTextureToGl(gl, ktexture);
|
|
// upload transcodes the texture so ktexture is uncompresssed again.
|
|
setUVMatrix(textureComp, mat3.create(), ktexture);
|
|
setTexParameters(texture, ktexture);
|
|
updateItem(items[basisCompTextureItem], textureComp);
|
|
items[basisCompTextureItem].label.textContent +=
|
|
", transcoded to " + textureComp.format;
|
|
|
|
await testWriteToMemoryAndRead(ktexture)
|
|
|
|
await testEncodeAstc(ktextureCopy);
|
|
if (!astcSupported) {
|
|
var result = ktextureCopy.decodeAstc();
|
|
showTestResult('compress_astc_result', result == ktx.error_code.SUCCESS);
|
|
items[astcCompTextureItem].label.textContent +=
|
|
". (Software decoded. This device does not support WEBGL_compressed_texture_astc)"
|
|
}
|
|
textureAstc = uploadTextureToGl(gl, ktextureCopy);
|
|
setUVMatrix(textureAstc, mat3.create(), ktextureCopy);
|
|
setTexParameters(texture, ktexture);
|
|
updateItem(items[astcCompTextureItem], textureAstc);
|
|
|
|
elem('runtests').disabled = true;
|
|
|
|
}
|
|
|