Basic 3D representation for Digital Twin using Angular, Three.js, WebGL

Gibin Francis
8 min readJan 23, 2023

--

Recently we got a chance to work with the azure digital twin, so I thought to write some article around representing the same in a 3D visualization as its really boring to view a tabular way of representation of our data.

This will be a very basic representation of a digital twin, this can be enhanced with complex 3D models based on the availability and expertise on the same. Am writing this for a very beginner level programmer with UI experience.

Lets start, in this use case we want to keep some twin information about the vessel and its partitioned areas. This can be used to represent a smart building or any use case witch need very less 3D skills. If we work with a real world example, the 3D models of a vessel will be very complex and need more processing and experience on 3D to accomplish the same, but lets start with a small step, here we will consider small Lego blocks of cubes to represent a vessel and represent the same.

You can follow the links in the reference section to get more insights about WebGL and 3D representations. Here I will be covering only the relevant parts for our need.

Lets start with a new angular application with necessary packages and components


npm install -g @angular/cli

ng new VesselDigialTwinUI

cd VesselDigialTwinUI

npm install — save three

npm install — save @types/three

ng generate component vessel

Now we have our vessel component ready to develop the same.

Lets head over to the routing and add the vesselComponenet as a default route for your application


const routes: Routes = [
{
path: '',
component: VesselComponent,
}
];

and also remove unwanted pre-generated content from your app.componenet.html and make it only contains the router outlet tag as below

<router-outlet></router-outlet>

now lets head over to the vessel.componenet.html and add out html component, we will be using the canvas component in html to render our 3D model in it

<canvas #canvas id="canvas" style="height: 100vh; width: 100vw;" ></canvas>

lets add below two image files to the assets folder, we will discuss them later

texture_blue.png
texture_red.png

Now lets head over to the vessel.componenet.ts file to write our main code

Now lets add the necessary packages

import {
Component,
OnInit,
AfterViewInit,
Input,
ViewChild,
ElementRef,
} from '@angular/core';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

Now lets add the canvas component for our reference to access it from TS file

export class VesselComponent implements OnInit, AfterViewInit {
// Canvas element
@ViewChild('canvas')
private canvasRef!: ElementRef;
}

now add the texture for our block

// Cube Properties
@Input() public texture_red: string = '/assets/texture_red.png';
@Input() public texture_blue: string = '/assets/texture_blue.png';

now lets add the stage details

//* Stage Properties
@Input() public cameraZ: number = 100;
@Input() public fieldOfView: number = 1;
@Input('nearClipping') public nearClippingPane: number = 0.1;
@Input('farClipping') public farClippingPane: number = 1000;

now lets add some scene information, here we are also creating our basic building block “BoxGeometry”.

// Scene properties
private camera!: THREE.PerspectiveCamera
private controls!: OrbitControls;
private ambientLight!: THREE.AmbientLight;
private loader = new THREE.TextureLoader();
private renderer!: THREE.WebGLRenderer;
private scene!: THREE.Scene;

private geometry = new THREE.BoxGeometry(0.25, 0.25, 0.25);

now lets add some supporting functions

private get canvas(): HTMLCanvasElement {
return this.canvasRef.nativeElement;
}

private material_red = new THREE.MeshBasicMaterial({
map: this.loader.load(this.texture_red),
});

private material_blue = new THREE.MeshBasicMaterial({
map: this.loader.load(this.texture_blue),
});

private createControls = () => {
const renderer = new CSS2DRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0px';
document.body.appendChild(renderer.domElement);
this.controls = new OrbitControls(this.camera, renderer.domElement);
this.controls.autoRotate = true;
this.controls.enableZoom = true;
this.controls.enablePan = false;
this.controls.update();
};

private getAspectRatio() {
return this.canvas.clientWidth / this.canvas.clientHeight;
}

Now lets create some cubes to the scene, this will create a 3 dimensional cube array, then we can further modify it for our need.

private cubes: any;
//this will be the multiplier for the cubes in the scene
private boxLength: number = 30;
private boxBreadth: number = 5;
private boxHeight: number = 5;

now lets create some 3 dimensional array of cubes, here we will be placing each cube with 0.35 doffernce as our cube are scaled at 0.25 and thus we will be a 0.10 differnce in between, you can adjust accordingly.

/*Create Cubes */
private createCubes = () => {
//initilize our 3 dimentioanl array
this.cubes = new Array(this.boxHeight);
for (let i = 0; i < this.cubes.length; i++) {
this.cubes[i] = new Array(this.boxBreadth);
for (let j = 0; j < this.cubes[i].length; j++) {
this.cubes[i][j] = new Array(this.boxLength);
}
}

for (var i = 0; i < this.boxHeight; i++) {
for (var j = 0; j < this.boxBreadth; j++) {
for (var k = 0; k < this.boxLength; k++) {
var item;
item = new THREE.Mesh(this.geometry, this.material_blue);
item.position.x = i * 0.35;
item.position.y = j * 0.35;
item.position.z = k * 0.35;
this.cubes[i][j][k] = item;
}
}
}
};

Now lets render it by creating scene with the cubes in it

/* Create the scene */
private createScene() {
//* Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xd4d4d8);
this.createCubes();

for (var i = 0; i < this.boxHeight; i++) {
for (var j = 0; j < this.boxBreadth; j++) {
for (var k = 0; k < this.boxLength; k++) {
this.scene.add(this.cubes[i][j][k]);
}
}
}

//*Camera
let aspectRatio = this.getAspectRatio();
this.camera = new THREE.PerspectiveCamera(
this.fieldOfView,
aspectRatio,
this.nearClippingPane,
this.farClippingPane
);
this.camera.position.x = 100;
this.camera.position.y = 100;
this.camera.position.z = 100;
this.ambientLight = new THREE.AmbientLight(0x00000, 80);
this.scene.add(this.ambientLight);
}

Now lets add rendering loop to the same


private startRenderingLoop() {
//* Renderer
// Use canvas element in template
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
antialias: true,
});
this.renderer.setPixelRatio(devicePixelRatio);
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight);
let component: VesselComponent = this;
(function render() {
component.renderer.render(component.scene, component.camera);
requestAnimationFrame(render);
})();
}

Now lets call the same from the componet to get it in action

constructor() {}

ngOnInit(): void {}

ngAfterViewInit() {
this.createScene();
this.startRenderingLoop();
this.createControls();
}

This code will create a 3 dimensional array of cubes in a 3D view like this

Hooha, we finihsed the half way of the journey. Now lets make it more functional. lets make some exclusion list to exclude some cubes for the above, so that it will look more identical to a ruff vessel

for that we will create an exclusion list, dont be bothered about the same, you can use your logic to remove your specified blocks to be removed from the same

var ExclusionList = [
'0,0,0','0,1,0','0,2,0','0,3,0','0,4,0',
'1,0,0','1,1,0','1,2,0','1,3,0','1,4,0',
'2,0,0','2,1,0','2,2,0','2,3,0',
'3,0,0','3,1,0','3,2,0','3,3,0','3,4,0',
'4,0,0','4,1,0','4,2,0','4,3,0','4,4,0',
'0,0,1','0,1,1','0,2,1','0,3,1','0,4,1',
'4,0,1','4,1,1','4,2,1','4,3,1','4,4,1',
'1,0,1','1,1,1','1,2,1',
'2,0,1','2,1,1','2,2,1',
'3,0,1','3,1,1','3,2,1',
'0,0,2','0,1,2','0,2,2',
'0,3,2','0,4,2',
'4,0,2','4,1,2','4,2,2','4,3,2','4,4,2',
'1,0,2','1,1,2',
'2,0,2','2,1,2',
'3,0,2','3,1,2',
'0,0,3','0,1,3','0,2,3','1,0,3',
'2,0,3',
'3,0,3',
'4,0,3','4,1,3','4,2,3',
'0,0,4','0,1,4','0,2,4',
'1,0,4',
'2,0,4',
'3,0,4',
'4,0,4','4,1,4','4,2,4',
];

and make some change in the function to exclude the list mentioned below.

please find the additional code we added to createCubes method

if (
ExclusionList.includes(
i.toString() + ',' + j.toString() + ',' + k.toString(),
0
) == true
)
continue;

So the updated code block will look like below

/*Create Cubes */
private createCubes = () => {
//initilize our 3 dimentioanl array
this.cubes = new Array(this.boxHeight);
for (let i = 0; i < this.cubes.length; i++) {
this.cubes[i] = new Array(this.boxBreadth);
for (let j = 0; j < this.cubes[i].length; j++) {
this.cubes[i][j] = new Array(this.boxLength);
}
}

for (var i = 0; i < this.boxHeight; i++) {
for (var j = 0; j < this.boxBreadth; j++) {
for (var k = 0; k < this.boxLength; k++) {
//index is in exclusion list
if (ExclusionList.includes(
i.toString() + ',' + j.toString() + ',' + k.toString(),
0
) == true)
continue;

var item;
item = new THREE.Mesh(this.geometry, this.material_blue);
item.position.x = i * 0.35;
item.position.y = j * 0.35;
item.position.z = k * 0.35;
this.cubes[i][j][k] = item;
}
}
}
};

now we have a vessel looking (not so beautiful, but functional.. :) 3D array of blocks as below.

top view
side view
3 Dimensional View

Now we can use the same to represent our digital twin metrices to the 3D image and make use of the same, lets do some sample as below, I want to highlight some cubes or area that need attention. So lest’s create a list of block that need some color change

let HighlightList = ['3,4,15', '4,4,29', '2,3,19', '4,2,12'];

lets do some colour change for the specified blocks in createCubes method

if (HighlightList.includes(
i.toString() + ',' + j.toString() + ',' + k.toString(),
0
) == true)
item = new THREE.Mesh(this.geometry, this.material_red);
else item = new THREE.Mesh(this.geometry, this.material_blue)

Now lets see how the code block will change after the same

/*Create Cubes */
private createCubes = () => {
//initilize our 3 dimentioanl array
this.cubes = new Array(this.boxHeight);
for (let i = 0; i < this.cubes.length; i++) {
this.cubes[i] = new Array(this.boxBreadth);
for (let j = 0; j < this.cubes[i].length; j++) {
this.cubes[i][j] = new Array(this.boxLength);
}
}

for (var i = 0; i < this.boxHeight; i++) {
for (var j = 0; j < this.boxBreadth; j++) {
for (var k = 0; k < this.boxLength; k++) {
//index is in exclusion list
if (ExclusionList.includes(
i.toString() + ',' + j.toString() + ',' + k.toString(),
0
) == true)
continue;

var item;
if (HighlightList.includes(
i.toString() + ',' + j.toString() + ',' + k.toString(),
0
) == true)
item = new THREE.Mesh(this.geometry, this.material_red);
else item = new THREE.Mesh(this.geometry, this.material_blue);
item.position.x = i * 0.35;
item.position.y = j * 0.35;
item.position.z = k * 0.35;
this.cubes[i][j][k] = item;
}
}
}
};

Now the same will look like below

Tada.. Now we created a very basic intuitive 3D representation with very minimal 3D representation, tat is far more easy compared to a tabular view of the same.

Lets explore more and create new levels based on your need.

Happy coding..

Reference

--

--

Gibin Francis
Gibin Francis

Written by Gibin Francis

Technical guy interested in MIcrosoft Technologies, IoT, Azure, Docker, UI framework, and more

Responses (1)