Categories
Angular Mobile Development Web Development

Introduction to WebGL Using Angular- How to Set Up a Scene (Part 1)

Overview

WebGL has to be one of the most under-used JavaScript APIs within modern web browsers.

It offers rendering interactive 2D and 3D graphics and is fully integrated with other web standards, allowing GPU-accelerated usage of physics and image processing effects as part of the web page canvas (Wikipedia, 2020).

In this article, we’re going to setup WebGL within a typical Angular app by utilising the HTML5 canvas element.

Prerequisites

Before starting, its worthwhile to ensure your system is setup with the following:

  • Nodejs is installed
  • You have setup a new or existing Angular app
  • You are using a modern web browser (Chrome 56+, Firefox 51+, Opera 43+, Edge 10240+)

WebGL Fundamentals

There’s honestly a lot to take in regards to the fundamentals of WebGL (and more specifically OpenGL). It does mean that you’ll need to have some basic understanding of linear algebra and 2d/3d rendering in general. WebGL Fundamentals does a great job at providing an introduction to WebGL fundamentals and I’ll be referencing their documentation as we step through setting up our Angular app to use WebGL.

Before going any further, its important that you understand the following at a minimum.

WebGL is not a 3D API. You can’t just use it to instantly render objects and models and get them to do some awesome magic.

WebGL is just a rasterization engine. It draws points, lines and triangles based on the code you supply.

If you want WebGL to do anything else, its up to you to write code that uses points, lines and triangles to accomplish the task you want.

WebGL runs on the GPU and requires that you provide code that runs on the GPU.
The code that we need to provide is in the form of pairs of functions.

They are known as:

  • a vertex shader
    • responsible for computing vertex positions – based on the positions, WebGL can then rasterize primitives including points, lines, or triangles.
  • a fragment shader
    • when primitives are being rasterized, WebGL calls the fragment shader to compute a colour for each pixel of the primitive that’s currently being drawn.

Each shader is written in GLSL which is a strictly typed C/C++ like language.
When a vertex and fragment shader are combined, they’re collectively known as a program.

Nearly all of the entire WebGL API is about setting up state for these pairs of functions to run. For each thing you want to draw, you setup a bunch of state then execute a pair of functions by calling gl.drawArrays or gl.drawElements which executes your shaders on the GPU.

Any data you want those functions to have access to, must be provided to the GPU. There are 4 ways a shader can receive data.

  • Attributes and buffers
    • Buffers are arrays of binary data you upload to the GPU. Usually buffers contain things like positions, normals, texture coordinates, vertex colours, etc although you’re free to put anything you want in them.
    • Attributes are used to specify how to pull data out of your buffers and provide them to your vertex shader. For example you might put positions in a buffer as three 32bit floats per position. You would tell a particular attribute which buffer to pull the positions out of, what type of data it should pull out (3 component 32 bit floating point numbers), what offset in the buffer the positions start, and how many bytes to get from one position to the next.
    • Buffers are not random access. Instead a vertex shader is executed a specified number of times. Each time it’s executed the next value from each specified buffer is pulled out and assigned to an attribute.
  • Uniforms
    • Uniforms are effectively global variables you set before you execute your shader program.
  • Textures
    • Textures are arrays of data you can randomly access in your shader program. The most common thing to put in a texture is image data but textures are just data and can just as easily contain something other than colours.
  • Varyings
    • Varyings are a way for a vertex shader to pass data to a fragment shader. Depending on what is being rendered, points, lines, or triangles, the values set on a varying by a vertex shader will be interpolated while executing the fragment shader.

(WebGL Fundamentals, 2015).

I’m glossing over a lot of technical detail here, but if you really want to know more, head over to WebGL Fundamentals lessons for more info.

Setting up a playground

Let’s set up a playground so we have something that we can use in order to continue setting up WebGL.

First, create a component. You can create one by executing the following command within your Angular root (src) directory. I’ve gone ahead and named mine scene.

E.g. ng generate component scene

PS X:\...\toucan-webgl> ng generate component scene
CREATE src/app/scene/scene.component.html (20 bytes)
CREATE src/app/scene/scene.component.spec.ts (619 bytes)
CREATE src/app/scene/scene.component.ts (272 bytes)
CREATE src/app/scene/scene.component.scss (0 bytes)
PS X:\...\toucan-webgl>

Let’s also create a service with the component and call it WebGL too.

E.g. ng generate service scene/services/webGL

PS X:\...\toucan-webgl> ng generate service scene/services/webGL
CREATE src/app/scene/services/web-gl.service.spec.ts (352 bytes)
CREATE src/app/scene/services/web-gl.service.ts (134 bytes)
PS X:\...\toucan-webgl>

If you’re using a new Angular app, hopefully you’ve already configured it to use App Routing. If you haven’t, follow the next couple of steps.

ng generate module app-routing --flat --module=app

You’ll now have an app-routing.module.ts file, if you haven’t got one already.

Update the contents of the file with the following:

import { Routes, RouterModule } from "@angular/router";
import { SceneComponent } from "./scene/scene.component";
const routes: Routes = [{ path: "", component: SceneComponent }];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

This will ensure that on app load, it’ll display the SceneComponent first.

Next, add the WebGLService to the SceneComponent‘s constructor like so:

import { Component, OnInit } from "@angular/core";
import { WebGLService } from "./services/web-gl.service";
@Component({
  selector: "app-scene",
  templateUrl: "./scene.component.html",
  styleUrls: ["./scene.component.scss"],
})
export class SceneComponent implements OnInit {
  // *** Update constructor here ***
  constructor(private webglService: WebGLService) {}
  ngOnInit(): void {}
}

Finally, run ng serve and check to see if the Angular app is running and displaying the SceneComponent.
It should look like this:

Now, lets move onto adding a WebGL context.

Setting up the WebGL context

Setting up the WebGL context is a little bit involved but once we get the foundation going we can then proceed to start getting something on the screen.

Let’s start by opening up scene.component.html and add a HTML5 canvas element.

<div class="scene">
  <canvas #sceneCanvas>
    Your browser doesn't appear to support the
    <code><canvas></code> element.
  </canvas>
</div>

Open up scene.component.scss (or equivalent) and add in the following styles:

.scene {
  height: 100%;
  width: 100%;
}
.scene canvas {
  height: 100%;
  width: 100%;
  border-style: solid;
  border-width: 1px;
  border-color: black;
}

The following css should just make sure the canvas element extends to the size of the browser window. I just added some border styling so you can explicitly see it for yourself.

TIP: If you want, you can also update the global styles.scss so all content expands to the height of the window respectively.

styles.scss

/* You can add global styles to this file, and also import other style files */
html,
body {
  height: 99%;
}

We’ll now embark on doing the following:

  1. Resolving the canvas element in typescript via the #canvas id
  2. Binding the canvas element to a WebGL rendering context
  3. Initialize the WebGL rendering canvas

Resolving the canvas element

Open scene.component.ts and add the following property:

@ViewChild('sceneCanvas') private canvas: HTMLCanvasElement;

Update your the SceneComponent class to implement AfterViewInit, we’ll need to hook into this lifecycle hook to continue setting up the WebGL canvas.

Add in the following guard to the ngAfterViewInit method to ensure that we actually have the canvas element before attempting to bind it:

if (!this.canvas) {
  alert("canvas not supplied! cannot bind WebGL context!");
  return;
}

NOTE: If the alert is hit, it’s due to the fact that the ElementRef ID you’re using does match the one defined in HTML and the TS class. You need to ensure they match.

Your component implementation should now look like this:

import { AfterViewInit, Component, OnInit, ViewChild } from "@angular/core";
import { WebGLService } from "./services/web-gl.service";
@Component({
  selector: "app-scene",
  templateUrl: "./scene.component.html",
  styleUrls: ["./scene.component.scss"],
})
export class SceneComponent implements OnInit, AfterViewInit {
  @ViewChild("sceneCanvas") private canvas: HTMLCanvasElement;
  constructor(private webglService: WebGLService) {}
  ngAfterViewInit(): void {
    if (!this.canvas) {
      alert("canvas not supplied! cannot bind WebGL context!");
      return;
    }
  }
  ngOnInit(): void {}
}

Binding the canvas element to a WebGL rendering context

Open up the web-gl.service.ts file.

Create a method called initialiseWebGLContext with a parameter canvas: HTMLCanvasElement.

initialiseWebGLContext(canvas: HTMLCanvasElement) {
}

Go back to scene.component.ts and add in the following line after the guard check in ngAfterViewInit.

ngAfterViewInit(): void {
  if (!this.canvas) {
      alert('canvas not supplied! cannot bind WebGL context!');
      return;
  }
  this.webglService.initialiseWebGLContext(this.canvas.nativeElement);
}

Now, back in web-gl.service.ts, lets retrieve a WebGL context from the canvas’s native element and reference it to a property that we’ll call gl.

private _renderingContext: RenderingContext;
private get gl(): WebGLRenderingContext {
  return this._renderingContext as WebGLRenderingContext;
}
constructor() {}
initialiseWebGLContext(canvas: HTMLCanvasElement) {
  // Try to grab the standard context. If it fails, fallback to experimental.
  this._renderingContext = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  // If we don't have a GL context, give up now... only continue if WebGL is available and working...
  if (!this.gl) {
      alert('Unable to initialize WebGL. Your browser may not support it.');
      return;
  }
}

Once we’ve retrieved the WebGLRenderingContext, we can then set the WebGL canvas’s height and width, and then finally proceed to initialise the WebGL canvas.

Lets add two methods which do that I described above:

setWebGLCanvasDimensions(canvas: HTMLCanvasElement) {
  // set width and height based on canvas width and height - good practice to use clientWidth and clientHeight
  this.gl.canvas.width = canvas.clientWidth;
  this.gl.canvas.height = canvas.clientHeight;
}
initialiseWebGLCanvas() {
  // Set clear colour to black, fully opaque
  this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
  // Enable depth testing
  this.gl.enable(this.gl.DEPTH_TEST);
  // Near things obscure far things
  this.gl.depthFunc(this.gl.LEQUAL);
  // Clear the colour as well as the depth buffer.
  this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}

Now finally call them at the end of the initialiseWebGLContext method.

initialiseWebGLContext(canvas: HTMLCanvasElement) {
  // Try to grab the standard context. If it fails, fallback to experimental.
  this._renderingContext =
    canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  // If we don't have a GL context, give up now... only continue if WebGL is available and working...
  if (!this.gl) {
    alert('Unable to initialize WebGL. Your browser may not support it.');
    return;
  }
  // *** set width, height and initialise the webgl canvas ***
  this.setWebGLCanvasDimensions(canvas);
  this.initialiseWebGLCanvas();
}

Run the app again, you should now see that the canvas is entirely black.

This shows that we’ve successfully initialised the WebGL context.

Thats it for part 1!

Next: Introduction to WebGL using Angular – Part 2 – Setting up shaders and a triangle

In part 2, we’ll proceed to add in shaders and start setting up content to render on screen!

Stay tuned!

The source code for this tutorial is available at https://gitlab.com/MikeHewett/intro-webgl-part-1.git

References

5 1 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments