WEBGL Special Topic-3D Special Effects-Drawing Wave Points

In this WebGL topic, we will learn how to use WebGL to create a 3D wavy point effect. We will use vertex and fragment shaders along with WebGL buffer objects and textures to create this effect.

First, we need to define a triangle mesh and calculate the value of a wave function at each vertex. We will use a vertex shader to calculate these values and store them in a texture for use in the fragment shader.

Here is the code for the vertex shader:

<script id="vertex-shader" type="x-shader/x-vertex">
    attribute vec3 aPosition;
    attribute vec2 aTexCoord;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;

    varying vec2 vTexCoord;
    varying vec3 vPosition;

    void main() {
        vTexCoord = aTexCoord;
        vPosition = aPosition;

        gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
    }
</script>

First, we declare the vertex’s position and texture coordinates as input properties. We also declare the uniform variables that need to be passed to the shader for the model view matrix and projection matrix. Finally, we pass the texture coordinates and vertex positions to the fragment shader as output varying variables.

Next, we need to calculate the values of the wave function in the fragment shader and use these values to shade the triangle. We will use a light source located inside the triangle and calculate the light intensity based on the normal vector and the view direction. Here is the code for the fragment shader:

<script id="fragment-shader" type="x-shader/x-fragment">
    precision mediump float;

    uniform sampler2D uWaveTexture;

    varying vec2 vTexCoord;
    varying vec3 vPosition;

    const vec3 lightPosition = vec3(0.0, 0.0, 1.0);
    const vec3 lightColor = vec3(1.0, 1.0, 1.0);
    const vec3 ambientColor = vec3(0.2, 0.2, 0.2);

    float waveFunction(float x, float z, float t) {
        float d = sqrt(x * x + z * z);
        float a = 0.03;
        float b = 0.08;
        float c = 1.0;
        float f = 4.0;
        return a * sin(b * d - c * t) * exp(-d * f);
    }

    void main() {
        float x = vPosition.x;
        float z = vPosition.z;
        float t = 0.001 * mod(gl_FragCoord.y, 1000.0);

        float height = texture2D(uWaveTexture, vec2(x, z)).r;
        vec3 normal = vec3(
            waveFunction(x - 0.01, z, t) - waveFunction(x + 0.01, z, t),
            2.0,
            waveFunction(x, z - 0.01, t) - waveFunction(x, z + 0.01, t)
        );
        normal = normalize(normal);

        vec3 lightDirection = normalize(lightPosition - vPosition);
        float diffuse = max(dot(normal, lightDirection), 0.0);
        vec3 diffuseColor = diffuse * lightColor;

        vec3 ambient = ambientColor * height;
        vec3 color = (diffuseColor + ambient) * vec3(0.7, 0.8, 1.0);

        gl_FragColor = vec4(color, 1.0);
    }
</script>

First, we declare the varying variables for the texture coordinates and vertex positions that need to be passed from the vertex shader. We also declare a wave function that takes x and z coordinates and time t as input and returns a height value.

Next, we calculated the height value of the fragment and used that value to calculate the normal vector of the fragment. We also declare a light source located inside the triangle and calculate the diffuse component of the fragment. Finally, we add the ambient and diffuse intensity and color correct it with a blue tint. The final color is stored in gl_FragColor.

Finally, we need to create the WebGL context in JavaScript and bind the vertex and texture data to the buffer object. We also need to define the rendering function and read the color data from the framebuffer object to update the texture. Here is the complete JavaScript code:

var gl;
var program;
var waveTexture;
var waveFramebuffer;

function init() {<!-- -->
    var canvas = document.getElementById('canvas');
    gl = canvas.getContext('webgl');

    program = createProgramFromScripts(gl, 'vertex-shader', 'fragment-shader');
    gl.useProgram(program);

    var vertices = [];
    var texCoords = [];

    for (var i = -50; i <= 50; i + = 2) {<!-- -->
        for (var j = -50; j <= 50; j + = 2) {<!-- -->
            vertices.push(i, 0, j);
            texCoords.push((i + 50) / 100, (j + 50) / 100);
        }
    }

    var indices = [];

    for (var i = 0; i < 99; i + + ) {<!-- -->
        for (var j = 0; j < 99; j + + ) {<!-- -->
            var index = i * 100 + j;

            indices.push(index, index + 1, index + 100);
            indices.push(index + 1, index + 101, index + 100);
        }
    }

    var positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

    var positionLocation = gl.getAttribLocation(program, 'aPosition');
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

    var texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);

    var texCoordLocation = gl.getAttribLocation(program, 'aTexCoord');
    gl.enableVertexAttribArray(texCoordLocation);
    gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

    var indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

    waveTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, waveTexture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.R32F, 100, 100, 0, gl.RED, gl.FLOAT, null);

    waveFramebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, waveFramebuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, waveTexture, 0);
    gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
}

function updateWaveTexture() {<!-- -->
    gl.bindFramebuffer(gl.FRAMEBUFFER, waveFramebuffer);
    gl.viewport(0, 0, 100, 100);
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.useProgram(program);
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

    var modelViewMatrixLocation = gl.getUniformLocation(program, 'uModelViewMatrix');
    var projectionMatrixLocation = gl.getUniformLocation(program, 'uProjectionMatrix');

    var modelViewMatrix = mat4.create();
    var projectionMatrix = mat4.create();

    mat4.translate(modelViewMatrix, modelViewMatrix, vec3.fromValues(0, -5, -30));
    mat4.perspective(projectionMatrix, glMatrix.toRadian(45), 1, 0.1, 100);

    gl.uniformMatrix4fv(modelViewMatrixLocation, false, modelViewMatrix);
    gl.uniformMatrix4fv(projectionMatrixLocation, false, projectionMatrix);

    var waveTextureLocation = gl