/* * 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. Modifications made here are also licensed under CC0v1. */ var cubeRotation = 0.0; var gl; var texture; 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 { createKtxReadModule({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() { texture = loadTexture(gl, 'ktx_app_basis.ktx2'); 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; // Draw the scene repeatedly function render(now) { now *= 0.001; // convert to seconds const deltaTime = now - then; then = now; drawScene(gl, 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 matching texture target. // // needs Emscripten's OpenGL ES emulation. 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 for a 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() }; } // // Sets the uvMatrix for a texture. // // The WebGL texture object is expected to have been created from the // content of the ktxTexture object. The matrix is adjusted according // to the orientation in the ktxTexture object. // 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 loadTexture(gl, url) { // Because images have to be downloaded over the internet // they might take a moment until they are ready. Until // then temporarily fill the texture with a single pixel image // so we can use it immediately. When the image has finished // downloading we'll update texture to the new contents const placeholder = createPlaceholderTexture(gl, [0, 0, 255, 255]); gl.bindTexture(placeholder.target, placeholder.object); var xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = "arraybuffer"; xhr.onload = function(){ var ktxdata = new Uint8Array(this.response); ktexture = new ktx.texture(ktxdata); const tex = uploadTextureToGl(gl, ktexture); setUVMatrix(tex, mat3.create(), ktexture); setTexParameters(tex, ktexture); gl.bindTexture(tex.target, tex.object); gl.deleteTexture(texture.object); texture = tex; elem('format').innerText = tex.format; ktexture.delete(); }; //xhr.onprogress = runProgress; //xhr.onloadstart = openProgress; xhr.send(); return placeholder; } function isPowerOf2(value) { return (value & (value - 1)) == 0; } // // Draw the scene. // function drawScene(gl, programInfo, buffers, texture, deltaTime) { gl.enable(gl.CULL_FACE); gl.enable(gl.DEPTH_TEST); gl.enable(gl.DEPTH_TEST); // Enable depth testing gl.depthFunc(gl.LEQUAL); // Near things obscure far things // In case the source image has translucent parts ... gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.clearColor(0.0, 0.0, 0.0, 1.0); // Clear to black, fully opaque gl.clearDepth(1.0); // Clear everything // Clear the canvas before we start drawing on it. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Create a perspective matrix, a special matrix that is // used to simulate the distortion of perspective in a camera. // Our field of view is 45 degrees, with a width/height // ratio that matches the display size of the canvas // 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 = gl.canvas.clientWidth / gl.canvas.clientHeight; 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); // 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, texture.uvMatrix); // Specify the texture to map onto the faces. // 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); // 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; }