Skip to content

Your First Scene

Initial application structure

First step is to open up the app.component.ts file and import NgtCanvas component then render it on the template.

app.component.ts
import { Component } from "@angular/core";
import { NgtCanvas } from "angular-three/dom";
@Component({
selector: "app-root",
template: `
<ngt-canvas></ngt-canvas>
`,
imports: [NgtCanvas]
})
export class AppComponent {}

NgtCanvas is the entry-point to Angular Three. It creates a WebGLRenderer, a default Scene, and a default PerspectiveCamera; the 3 main building blocks for a THREE.js application.

We’ll also need to adjust the NgtCanvas dimensions by adjusting the parent’s dimensions.

html,
body {
height: 100%;
width: 100%;
margin: 0;
}

NgtCanvas also provides a store that the Angular Three application will act upon. To improve accessing this store via Dependency Injection, it is recommended to create a separate component scene-graph.ts and render it as the NgtCanvas content.

import { Component } from "@angular/core";
@Component({
selector: "app-scene-graph",
template: ``
})
export class SceneGraph {}

Provide the Renderer

In order to actually render our THREE.js scene graph to the canvas, we need to provide the custom renderer from angular-three

app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideNgtRenderer } from "angular-three/dom";
export const appConfig: ApplicationConfig = {
providers: [
// other providers
provideNgtRenderer(),
]
}

Create objects

In scene-graph.ts, we’ll create a simple THREE.js cube which is comprised of a THREE.Mesh with a THREE.BoxGeometry and a THREE.MeshBasicMaterial.

But before we can do so, we need to tell Angular Three about these objects by extending the catalogue. By default, this catalogue is empty. Angular Three maps the catalogue to Custom Elements tags with the following naming convention:

<ngt-{entityName-in-kebab-case} />

Call extend and pass in a Record of entities.

scene-graph.ts
import { Component } from "@angular/core";
import { extend } from "angular-three";
import { Mesh, MeshBasicMaterial, BoxGeometry } from "three";
extend({
Mesh, // makes ngt-mesh available
MeshBasicMaterial, // makes ngt-mesh-basic-material available
BoxGeometry, // makes ngt-box-geometry available
});
scene-graph.ts
import { Component } from "@angular/core";
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { extend } from "angular-three";
import * as THREE from "three";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-mesh>
<ngt-box-geometry />
<ngt-mesh-basic-material />
</ngt-mesh>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {}
THREE.js equivalent
import * as THREE from 'three';
// create a mesh, geometry, and material
const mesh = new THREE.Mesh();
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial();
// "attach" geometry and material to the mesh
mesh.geometry = geometry;
mesh.material = material;

Modify objects

We can modify any THREE.js entities by leveraging Angular Property Binding on any properties that the THREE.js exposes for the class that we’re using.

For example, we can modify the Mesh’s position, and the MeshBasicMaterial’s color

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { extend } from "angular-three";
import * as THREE from "three";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-mesh
[position]="[0, 1, 0]"
>
<ngt-box-geometry />
<ngt-mesh-basic-material
color="mediumpurple"
/>
</ngt-mesh>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {}
THREE.js equivalent
import * as THREE from 'three';
const mesh = new THREE.Mesh();
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial();
mesh.geometry = geometry;
mesh.material = material;
// set the position of the mesh
mesh.position.fromArray([0, 1, 0]);
// set the color of the material
material.color.set('mediumpurple');

As for entities like THREE.BoxGeometry whose Constructor Arguments changes will require the entity to be reconstructed, Angular Three provides a structural directive called NgtArgs. Let’s see how we can use it to modify the THREE.BoxGeometry

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { extend } from "angular-three";
import { extend, NgtArgs } from "angular-three";
import * as THREE from "three";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-mesh [position]="[0, 1, 0]">
<ngt-box-geometry
*args="[1, 2, 1]"
/>
<ngt-mesh-basic-material color="mediumpurple" />
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {}
THREE.js equivalent
import * as THREE from 'three';
const mesh = new THREE.Mesh();
// set the dimensions of the geometry
const geometry = new THREE.BoxGeometry(1, 2, 1);
const material = new THREE.MeshBasicMaterial();
mesh.geometry = geometry;
mesh.material = material;
mesh.position.fromArray([0, 1, 0]);
material.color.set('mediumpurple');

Modify the Default Camera

Up to this point, we’ve only been looking at the side of the cube. This is because the camera’s default position set by Angular Three. To modify this default camera, we can use the [camera] and [lookAt] inputs on the NgtCanvas component.

app.component.ts
import { Component } from "@angular/core";
import { NgtCanvas } from "angular-three/dom";
import { SceneGraph } from "./scene-graph";
@Component({
selector: "app-root",
template: `
<ngt-canvas
[camera]="{ position: [5, 5, 5] }"
[lookAt]="[0, 1, 0]"
>
<app-scene-graph *canvasContent />
</ngt-canvas>
`,
imports: [NgtCanvas, SceneGraph],
styles: `
:host {
display: block;
height: 100dvh;
}
`
})
export class AppComponent {}

Animate the Cube

Let’s make our cube less boring by adding some motion to it. The best way to do this in Angular Three is to participate in Angular Three’s unified frame loop via injectBeforeRender() function.

First, we’ll need to get a reference to the undering THREE.Mesh element from <ngt-mesh>. We can use viewChild to do this.

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { Component, CUSTOM_ELEMENTS_SCHEMA, viewChild, ElementRef } from "@angular/core";
import { extend, NgtArgs } from "angular-three";
import * as THREE from "three";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-mesh
#mesh
[position]="[0, 1, 0]"
>
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-basic-material color="mediumpurple" />
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
}

Next, we’ll use meshRef in injectBeforeRender and update its property frame by frame before the canvas is rendered.

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { Component, CUSTOM_ELEMENTS_SCHEMA, viewChild, ElementRef } from "@angular/core";
import { extend, NgtArgs } from "angular-three";
import { extend, NgtArgs, injectBeforeRender } from "angular-three";
import * as THREE from "three";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-mesh #mesh [position]="[0, 1, 0]">
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-basic-material color="mediumpurple" />
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
constructor() {
injectBeforeRender(({ delta }) => {
this.meshRef().nativeElement.rotation.y += delta;
});
}
}

Make a component

Using Angular means we can componentize our cube so we can reuse it. This is what it means to “scale at ease”. Let’s create a new component called cube.ts

cube.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from "@angular/core";
import { NgtArgs, injectBeforeRender } from "angular-three";
import * as THREE from "three";
@Component({
selector: "app-cube",
template: `
<ngt-mesh #mesh [position]="[0, 1, 0]">
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-basic-material color="mediumpurple" />
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class Cube {
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
constructor() {
injectBeforeRender(({ delta }) => {
this.meshRef().nativeElement.rotation.y += delta;
});
}
}

Then, we’ll use this Cube component in our SceneGraph

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA, viewChild, ElementRef } from "@angular/core";
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { extend, NgtArgs, injectBeforeRender } from "angular-three";
import { extend } from "angular-three";
import * as THREE from "three";
import { Cube } from "./cube";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-mesh #mesh [position]="[0, 1, 0]">
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-basic-material color="mediumpurple" />
</ngt-mesh>
<app-cube />
`,
imports: [NgtArgs],
imports: [Cube],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
constructor() {
injectBeforeRender(({ delta }) => {
this.meshRef().nativeElement.rotation.y += delta;
});
}
}

Add state to the Cube

Everything is the same as before, except we now have a Cube component that can have its own state and logic. We’ll add 2 states hovered and clicked to the Cube component:

  • When the cube is hovered, it will change its color.
  • When the cube is clicked, it will change its scale.
cube.ts
import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
viewChild,
signal
} from "@angular/core";
import { NgtArgs, injectBeforeRender } from "angular-three";
import * as THREE from "three";
@Component({
selector: "app-cube",
template: `
<ngt-mesh #mesh
[position]="[0, 1, 0]"
[scale]="clicked() ? 1.5 : 1"
(pointerover)="hovered.set(true)"
(pointerout)="hovered.set(false)"
(click)="clicked.set(!clicked())"
>
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-basic-material
color="mediumpurple"
[color]="hovered() ? 'hotpink' : 'mediumpurple'"
/>
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class Cube {
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
protected hovered = signal(false);
protected clicked = signal(false);
constructor() {
injectBeforeRender(({ delta }) => {
this.meshRef().nativeElement.rotation.y += delta;
});
}
}

Render another cube

Having our Cube component means that we can render as many cubes as we want. Let’s render another cube on the canvas. However, we need to add a position input to the Cube component so that the SceneGraph can put the cubes in the correct positions for them to show up.

cube.ts
import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
viewChild,
signal,
input
} from "@angular/core";
import { NgtArgs, injectBeforeRender } from "angular-three";
import * as THREE from "three";
@Component({
selector: "app-cube",
template: `
<ngt-mesh #mesh
[position]="[0, 1, 0]"
[position]="[positionX(), 1, 0]"
[scale]="clicked() ? 1.5 : 1"
(pointerover)="hovered.set(true)"
(pointerout)="hovered.set(false)"
(click)="clicked.set(!clicked())"
>
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-basic-material [color]="hovered() ? 'hotpink' : 'mediumpurple'" />
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class Cube {
positionX = input(0);
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
protected hovered = signal(false);
protected clicked = signal(false);
constructor() {
injectBeforeRender(({ delta }) => {
this.meshRef().nativeElement.rotation.y += delta;
});
}
}

Next, we’ll update the SceneGraph component.

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { extend } from "angular-three";
import * as THREE from "three";
import { Cube } from "./cube";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<app-cube />
<app-cube [positionX]="-2" />
<app-cube [positionX]="2" />
`,
imports: [Cube],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {}

Now, we have two cubes that have their own states, and react to events independently.

Adjust the Lighting

Let’s add some lights to our scene to make the cubes look more dynamic as they look bland at the moment.

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { extend } from "angular-three";
import * as THREE from "three";
import { Cube } from "./cube";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-ambient-light [intensity]="0.5" />
<ngt-spot-light
[position]="[5, 10, -10]"
[intensity]="0.5 * Math.PI"
[angle]="0.5"
[penumbra]="1"
[decay]="0"
/>
<ngt-point-light [position]="-10" [intensity]="0.5 * Math.PI" [decay]="0" />
<app-cube [positionX]="-2" />
<app-cube [positionX]="2" />
`,
imports: [Cube],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {
protected readonly Math = Math;
}

Next, we will want to change the material of the cube to THREE.MeshStandardMaterial so that it can be lit by the lights.

cube.ts
import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
viewChild,
signal,
input
} from "@angular/core";
import { NgtArgs, injectBeforeRender } from "angular-three";
import * as THREE from "three";
@Component({
selector: "app-cube",
template: `
<ngt-mesh #mesh
[position]="[positionX(), 1, 0]"
[scale]="clicked() ? 1.5 : 1"
(pointerover)="hovered.set(true)"
(pointerout)="hovered.set(false)"
(click)="clicked.set(!clicked())"
>
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-basic-material [color]="hovered() ? 'hotpink' : 'mediumpurple'" />
<ngt-mesh-standard-material [color]="hovered() ? 'hotpink' : 'mediumpurple'" />
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class Cube {
positionX = input(0);
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
protected hovered = signal(false);
protected clicked = signal(false);
constructor() {
injectBeforeRender(({ delta }) => {
this.meshRef().nativeElement.rotation.y += delta;
});
}
}

Cast shadows

We’re almost there! Now that we have some lightings, let’s add some shadows so our scene is even more interesting.

First, we need to turn on shadows support explicitly since rendering shadows is an expensive operation.

app.component.ts
import { Component } from "@angular/core";
import { NgtCanvas } from "angular-three/dom";
import { SceneGraph } from "./scene-graph";
@Component({
selector: "app-root",
template: `
<ngt-canvas
shadows
[camera]="{ position: [5, 5, 5] }"
[lookAt]="[0, 1, 0]"
>
<app-scene-graph *canvasContent />
</ngt-canvas>
`,
imports: [NgtCanvas, SceneGraph],
styles: `
:host {
display: block;
height: 100dvh;
}
`
})
export class AppComponent {}

Next, we’ll adjust our THREE.SpotLight and our objects to cast and receive shadows.

scene-graph.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { extend } from "angular-three";
import * as THREE from "three";
import { Cube } from "./cube";
extend(THREE);
@Component({
selector: "app-scene-graph",
template: `
<ngt-ambient-light [intensity]="0.5" />
<ngt-spot-light
[position]="[5, 10, -10]"
[intensity]="0.5 * Math.PI"
[angle]="0.5"
[penumbra]="1"
[decay]="0"
castShadow
/>
<ngt-point-light [position]="-10" [intensity]="0.5 * Math.PI" [decay]="0" />
<ngt-mesh [rotation]="[-Math.PI / 2, 0, 0]" receiveShadow>
<ngt-circle-geometry *args="[4, 40]" />
<ngt-mesh-standard-material />
</ngt-mesh>
<app-cube [positionX]="-2" />
<app-cube [positionX]="2" />
`,
imports: [Cube],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class SceneGraph {
protected readonly Math = Math;
}
cube.ts
import {
Component,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
viewChild,
signal,
input
} from "@angular/core";
import { NgtArgs, injectBeforeRender } from "angular-three";
import * as THREE from "three";
@Component({
selector: "app-cube",
template: `
<ngt-mesh #mesh
[position]="[positionX(), 1, 0]"
[scale]="clicked() ? 1.5 : 1"
(pointerover)="hovered.set(true)"
(pointerout)="hovered.set(false)"
(click)="clicked.set(!clicked())"
castShadow
>
<ngt-box-geometry *args="[1, 2, 1]" />
<ngt-mesh-standard-material [color]="hovered() ? 'hotpink' : 'mediumpurple'" />
</ngt-mesh>
`,
imports: [NgtArgs],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class Cube {
positionX = input(0);
private meshRef = viewChild.required<ElementRef<THREE.Mesh>>('mesh');
protected hovered = signal(false);
protected clicked = signal(false);
constructor() {
injectBeforeRender(({ delta }) => {
this.meshRef().nativeElement.rotation.y += delta;
});
}
}

Conclusion

Congratulations. We have learned how to create a basic scene, add some lights, and make our cubes interactive. This guide, while short, includes important THREE.js and Angular Three concepts that should give you a good starting point for your own projects.

Here are several things you can play around to understand more how THREE.js works:

  • Can you change the cube to a different geometry?
  • Can you change how the objects are animated?
  • Can you change the lights’ positions and intensities?
  • Can you integrate OrbitControls to take control of the camera? (hint: use extend)