Open In App

How To Use Compressed Texture Formats in WebGL?

Last Updated : 08 Sep, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

Using compressed texture formats in WebGL can significantly improve performance and reduce the memory footprint of your WebGL applications. Textures, which are essentially images applied to 3D models, can take up a lot of memory and processing power.

Compressed texture formats allow you to use textures in a compressed state, reducing the overall size while maintaining quality, which leads to faster load times and better performance.

What Are Compressed Texture Formats?

Compressed texture formats are specialized image formats optimized for rendering in 3D environments. Unlike standard image formats like JPEG or PNG, compressed texture formats are designed to be directly used by the GPU, which means the textures don’t need to be decompressed by the CPU, saving valuable resources.

There are several compressed texture formats available in WebGL, each with its own set of features and compatibility across different devices:

  • ETC (Ericsson Texture Compression): Widely supported in OpenGL ES devices, suitable for basic RGB compression.
  • S3TC (S3 Texture Compression): Commonly used on desktop devices, providing good compression for both RGB and RGBA textures.
  • PVRTC (PowerVR Texture Compression): Used primarily on iOS devices, offering efficient compression with decent quality.
  • ASTC (Adaptive Scalable Texture Compression): A more recent and flexible format that allows adjustable block sizes, offering better compression ratios and quality.

Why Use Compressed Textures?

  1. Reduced Memory Usage: Compressed textures use significantly less memory, which is important for mobile and web applications where resources are limited.
  2. Faster Load Times: Smaller texture files mean faster download and loading times, improving the overall user experience.
  3. Better Performance: Since compressed textures are stored in a GPU-friendly format, they can be rendered more quickly, enhancing application performance.
  4. Quality Preservation: Compressed formats are optimized to retain visual quality, even at reduced file sizes, making them ideal for real-time applications like games.

Steps To Implement Compressed Textures in WebGL

To use compressed textures in WebGL, you'll typically need to use extensions that provide support for these formats. Here’s a step-by-step guide:

Folder Structure:

dvce
Folder Structure

Step 1: Create the HTML File

Create an index.html file in the root of your project. This file sets up the basic structure and includes the WebGL canvas.

HTML
<!--index.html-->

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGL Compressed Textures</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <canvas id="webgl-canvas"></canvas>
    <script type="module" src="app.js"></script>
</body>

</html>

Step 2: Add Basic Styling

Create a style.css file to ensure the canvas covers the whole screen.

CSS
/* style.css */

body,
html {
    margin: 0;
    padding: 0;
    overflow: hidden;
}

canvas {
    display: block;
    width: 100vw;
    height: 100vh;
}

Step 3: Write the WebGL Application Code

This script will initialize the WebGL context, create shaders, and generate a simple checkerboard texture programmatically.

JavaScript
//app.js

(async function () {
    const canvas = document.getElementById("webgl-canvas");
    const gl = canvas.getContext("webgl");

    // Check for WebGL context
    if (!gl) {
        alert("WebGL not supported on this browser.");
        throw new Error("WebGL not supported");
    }

    // Load shaders using await inside async function
    const vertexShaderSource = await fetch("shaders/vertex.glsl").then((res) =>
        res.text()
    );
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);

    const fragmentShaderSource = await fetch("shaders/fragment.glsl").then(
        (res) => res.text()
    );
    const fragmentShader = createShader(
        gl,
        gl.FRAGMENT_SHADER,
        fragmentShaderSource
    );

    // Create the shader program
    const program = createProgram(gl, vertexShader, fragmentShader);
    gl.useProgram(program);

    // Set up the geometry (a simple square)
    const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

    const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

    // Texture coordinates for the square
    const texCoords = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
    const texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);

    const texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
    gl.enableVertexAttribArray(texCoordLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

    // Generate a checkerboard texture programmatically
    createCheckerboardTexture(gl);

    function createShader(gl, type, source) {
        const shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            console.error(gl.getShaderInfoLog(shader));
            gl.deleteShader(shader);
            return null;
        }
        return shader;
    }

    function createProgram(gl, vertexShader, fragmentShader) {
        const program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            console.error(gl.getProgramInfoLog(program));
            gl.deleteProgram(program);
            return null;
        }
        return program;
    }

    function createCheckerboardTexture(gl) {
        const size = 64;
        const data = new Uint8Array(size * size * 4);

        // Generate checkerboard pattern
        for (let y = 0; y < size; ++y) {
            for (let x = 0; x < size; ++x) {
                const offset = (y * size + x) * 4;
                const color = (x & 8) ^ (y & 8) ? 255 : 0;
                data[offset] = color; // Red
                data[offset + 1] = color; // Green
                data[offset + 2] = color; // Blue
                data[offset + 3] = 255; // Alpha
            }
        }

        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.RGBA,
            size,
            size,
            0,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            data
        );

        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

        // Set the texture uniform
        const textureLocation = gl.getUniformLocation(program, "u_texture");
        gl.uniform1i(textureLocation, 0); // Texture unit 0

        requestAnimationFrame(drawScene);
    }

    function drawScene() {
        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);

        // Draw the square with the texture
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }
})();

Step 4: Write the Shaders

Create the shader files inside the shaders folder.

JavaScript
//vertex.glsl

attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;

void main() {
    gl_Position = a_position;
    v_texCoord = a_texCoord; 
}
JavaScript
// fragment.glsl

precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;

void main() {
    gl_FragColor = texture2D(u_texture, v_texCoord); 
}

Output

frg
Use Compressed Texture Formats in WebGL

Tips for Working with Compressed Textures

  • Pre-compress Textures: Use tools to pre-compress textures before deploying your WebGL application. This reduces the load on the client’s device and speeds up rendering.
  • Check Compatibility: Always verify which compressed texture formats are supported by the user’s device and provide fallbacks when necessary.
  • Optimize Texture Sizes: Use the smallest block sizes that maintain acceptable quality to further reduce memory usage.
  • Use Mipmaps: Consider using mipmaps with your compressed textures for better quality and performance at various levels of detail.

Explore