Creating dynamic digital experiences with WebGL [Part 2]

Andrey Filenko

by Andrey Filenko

WebGL optimization techniques Rolpb5m

This is Part II into our extended look into WebGL. If you haven’t already, go check out Part I in which we provide an intro on WebGL and briefly describe how to render some graphics in the browser using WebGL. Read more →

In this article, we would like to talk more about common optimization approaches that might help you to create smooth interactive graphics in the browser. 

WebGL optimization techniques

There are many approaches to performance optimization in computer graphics. Some of them depend on specifics of the particular task, some are more generic.

The most frequently used optimization techniques in WebGL include the following: 

  • Transfer of calculations from javascript code to shaders.
  • Optimization of rendering of a large number of objects.

Now we would like to demonstrate these optimization techniques using the example of one of Star’s joint projects with the Design Institute for Health at The University of Texas at Austin, namely, the Interactive Health Tool. 

How to perform calculations on GPU

Animating objects is one of the common use-cases in computer graphics. For example, when you want to, move some objects or transform their geometry or color, etc.

Similar tasks are often solved directly in JavaScript due to the simplicity of implementation. However, this can be a weak point in the entire application performance.

We will now show you an example of a simple animation where objects are moving at different speeds along some paths in space.

Calculation of coordinates as a function of time looks quite simple:

const easingValue = (1 + Math.sin(Math.PI * (t - 0.5))) / 2

this.shardsGroup.children.forEach((mesh) => {
    const { initialPosition, explosionVector, rotationSpeed, rotationAxes } = mesh

    mesh.position.set(
        initialPosition.add(explosionVector.multiplyScalar(easingValue))
    )

    mesh.rotation[rotationAxes[0]] += rotationSpeed
    mesh.rotation[rotationAxes[1]] += rotationSpeed
})

However, we should keep in mind that all these calculations will be performed for each frame and they depend on the number of animated objects.

What does the animation look like before optimization? To demonstrate this, let’s increase objects count to about 2500 objects, each consisting of 4 vertices.

As you can see, the frame rate is quite low and the animation doesn’t look smooth. In the browser profiler, you can see that the CPU is almost completely loaded (green chart shows frames per second 0-60 and yellow one is the CPU load 0-100%).

In order to reduce the CPU load, we will transfer the animation to the graphics processing unit (GPU) using vertex attributes and uniform variables.

mat4 rotationMartix = makeRotationMatrixY(uRotation);

mat3 internalRotationMartix = makeInternalRotationMatrix(
    uInternalRotation * uInternalRotationFactor, rotationAxes
);

vec3 explosionDelta = explosionDestination * easeInOutSin(uExplosionElapsedTime);

vec4 vertexPositionTransformed =
    modelViewMatrix * vec4(
        internalRotationMartix * position + explosionDelta + initialPosition, 1.0
    );

gl_Position = projectionMatrix * vertexPositionTransformed;

The transformed object coordinates are calculated according to the following data:

  • explosionDelta – a translation vector used to determine the current position of an object in each moment of time.
  • initialPosition – initial position of the object in the world coordinates. 
  • internalRotationMatrix – a matrix used to rotate an object.

WebGL uses matrix calculus. This is not that difficult but may take some time to adapt. Here is an example of creating the rotation matrices that we used above to transform the coordinates of animated objects:

mat3 makeInternalRotationMatrix(float theta, vec3 axes) {

    float cX = cos(theta * axes.x);
    float cY = cos(theta * axes.y);
    float cZ = cos(theta * axes.z);
    float sX = sin(theta * axes.x);
    float sY = sin(theta * axes.y);
    float sZ = sin(theta * axes.z);

    return mat3(
        cY * cZ, cY * sZ, -sY,
        sX * sY * cZ - cX * sZ, sX * sY * sZ + cX * cZ, sX * cY,
        cX * sY * cZ + sX * sZ, cX * sY * sZ - sX * cZ, cX * cY
    );
}

mat4 makeRotationMatrixY(float theta) {

    float c = cos(theta);
    float s = sin(theta);

    return mat4(
        c, 0., -s, 0.,
        0., 1., 0., 0.,
        s, 0., c, 0.,
        0., 0., 0., 1.
    );
}

On this video you can see the result of our optimization after transferring calculations to the shaders.
Now the animation is much smoother, but the frame rate is still far from 60 FPS.

 
Optimize “Lots of Objects” rendering

Handing a large number of objects is a significant impact on performance. 

Render calls are performed for each object that needs to be drawn and are quite resource intensive. Therefore, it is always advisable to minimize the number of draw calls. This can be achieved through combining similar objects by merging their geometries.

This approach slightly changes the way data is conveyed to shaders, for example, in transforming the coordinates of each part of such an aggregate object. In particular, uniform variables are no longer suitable for this purpose since their values ​​are common to all vertices.

In this case, we use attributes that enable us to set data on each vertex separately. Let us compare the code that sets the coordinates of objects before and after merging geometries. Passing coordinates to shaders using uniform variables:

const setMeshVectorUniform = (mesh, vector, uniformName) => {
    mesh.material.uniforms[uniformName] = {
        value: vector
    }
}

And using attributes:

const setGeometryVectorAttribute = (geometry, attributeName, vector) => {
    const vertexAttributesCount = geometry.attributes.position.array.length

    const attributeValuesArray = new Float32Array(vertexAttributesCount).fill(0)

    for (let i = 0; i < vertexAttributesCount; i = i + 3) {
        attributeValuesArray[i] = vector[0]
        attributeValuesArray[i + 1] = vector[1]
        attributeValuesArray[i + 2] = vector[2]
    }

    geometry.setAttribute(
        attributeName,
        new BufferAttribute(attributeValuesArray, 3),
    )
}

The second method might look a bit complicated, but it provides a more efficient way to work with geometry data.

The result of the optimization described above can be seen in the video. The result is as follows. As you can see, we managed to achieve 60 FPS and the animation is now smooth enough. This also reduced CPU and GPU load drastically.

Conclusion

Using the approaches outlined in this article, you can use WebGL for a number of applications, including: 

  • Visualizing data, for example, cartographic, statistical or data obtained from various scanners used in medicine, robotics, etc.
  • Implementing complex design solutions.
  • Developing games.
  • Creating graphic editors.
  • Image processing.
  • Developing interactive presentations e.g. of manufacturers of automotive equipment or mobile devices.
  • Implementing virtual/augmented reality projects.

WebGL is a quite versatile tool which mainly requires a graphics processor and imagination.

Share

WebGL optimization techniques R2mq5pb5m
Andrey Filenko
Software Engineer at Star

Andrey has over a decade of experience building web applications. As a front-end developer, he works on projects across numerous areas, including robotics logistics, HealthTech, and consumer IoT. He is passionate about functional programming, computer graphics, and application performance optimization and harnesses these talents to create innovative digital experiences.

Harness our Healthcare capabilities

We are passionate about improving healthcare outcomes with digital products that are a pleasure to use

Explore our expertise
Loading...
plus iconminus iconarrow icon pointing rightarrow icon pointing rightarrow icon pointing downarrow icon pointing leftarrow icon pointing toparrow icon pointing top rightPlay iconPause iconarrow pointing right in a circleDownload iconResume iconCross iconActive Badge iconActive Badge iconInactive Badge iconInactive Badge iconFocused Badge iconDropdown Arrow iconQuestion Mark iconFacebook logoTikTok logoLinkedin logoLinkedIn logoFacebook logoTwitter logoInstagram logoClose IconEvo Arrowarrow icon pointing right without lineburgersearch