Skip to content

Angular Three v2 is here! ❤️

After almost a year of development, we’re thrilled to announce the release of Angular Three v2! 🎉 Through countless examples and tutorials from other THREE.js ecosystems, we’ve identified the shortcomings of the previous version of Angular Three. Since then, we’ve been working tirelessly to enhance the library, making it more stable, performant, and predictable when working with THREE.js scene graphs.

Following over 100 beta releases, we’re confident that Angular Three v2 is ready for production and represents a significant improvement for the Angular THREE.js ecosystem as a whole.

Foreword

Angular Three v2 is a major release with a substantial time gap since the previous version. It aims to address the limitations of the first version, resulting in numerous breaking changes. Some of these changes are subtle and may not be immediately apparent.

Consequently, even though the surface-level APIs of v1 and v2 are similar, as both use a custom renderer, we’re not providing an upgrade path from v1 to v2. Additionally, Angular Three v2 requires a minimum of Angular v18. Therefore, Angular Three v2 is better suited for new projects rather than upgrading existing ones.

What’s new in Angular Three v2

  • Angular Signals 🚦
  • Improved performance and stability 📈
  • Better composability 🧩
  • Better Portal powered components 🪞
  • Improved documentation 📖
  • Testing (Experimental) 🧪

While this list might seem modest at first glance, the core improvements in Angular Three v2 unlock a wealth of potential that has been incorporated into other packages like angular-three-soba, angular-three-cannon, and angular-three-postprocessing.

Angular Signals 🚦

The most significant change in Angular Three v2 is the adoption of Angular Signals. Angular Signals is a powerful and flexible way to manage state in modern Angular applications. Angular Signals also provides an entire set of new tools: Signal Inputs, Signal Outputs, and Signal Queries; all of which are designed to work seamlessly together.

Angular Three v2’s core is built on Angular Signals. This means that most, if not all, of Angular Three v2’s APIs are Signals-based: they can accept Signals or Functions as arguments and return Signals as results.

Let’s examine some examples from across the Angular Three ecosystem.

injectStore

injectStore is a Custom Inject Function (CIF) that allows the consumers to interact with the Angular Three Store. The store contains THREE.js building blocks such as the root Scene, the default Camera, and the Renderer itself etc…

1
@Component({
2
template: `
3
<ngt-mesh>
4
<!-- content -->
5
</ngt-mesh>
6
7
8
<ngt-orbit-controls *args="[camera(), glDomElement()]" />
9
`,
10
imports: [NgtArgs]
11
})
12
export class MyCmp {
13
14
15
private store = injectStore();
16
17
18
protected camera = this.store.select('camera'); // Signal<NgtCamera>
19
protected domElement = this.store.select('gl', 'domElement'); // Signal<HTMLElement>
20
}

injectLoader

injectLoader is a Custom Inject Function (CIF) that allows the consumers to interact with THREE.js loaders.

1
@Component({
2
template: `
3
@if (gltf(); as gltf) {
4
<ngt-primitive *args="[gltf.scene]" [dispose]="null" />
5
}
6
`,
7
schemas: [CUSTOM_ELEMENTS_SCHEMA],
8
imports: [NgtArgs]
9
})
10
export class MyModel {
11
12
13
path = input.required<string>();
14
15
16
gltf = injectLoader(() => GLTFLoader, this.path);
17
}

injectBody (from angular-three-cannon)

injectBody is a Custom Inject Function (CIF) that allows the consumers to interact with Cannon.js bodies.

1
@Component({
2
template: `
3
<ngt-mesh #mesh [castShadow]="true" [receiveShadow]="true">
4
<ngt-box-geometry *args="[4, 4, 4]" />
5
<ngt-mesh-lambert-material color="white" />
6
</ngt-mesh>
7
`,
8
schemas: [CUSTOM_ELEMENTS_SCHEMA],
9
changeDetection: ChangeDetectionStrategy.OnPush,
10
imports: [NgtArgs],
11
})
12
export class Box {
13
14
15
meshRef = viewChild.required<ElementRef<Mesh>>('mesh');
16
17
18
boxApi = injectBox(
19
() => ({ args: [4, 4, 4], mass: 1, type: 'Kinematic' }),
20
this.meshRef
21
);
22
23
constructor() {
24
injectBeforeRender(({ clock }) => {
25
const api = this.boxApi();
26
if (!api) return;
27
const t = clock.getElapsedTime();
28
29
30
api.position.set(Math.sin(t * 2) * 5, Math.cos(t * 2) * 5, 3);
31
api.rotation.set(Math.sin(t * 6), Math.cos(t * 6), 0);
32
});
33
}
34
}

These examples demonstrate just a few of the Angular Signals integrations. By leveraging Angular Signals, Angular Three v2 has eliminated much of its internal artificial timing and complexity in coordinating different 3D entities. This, in turn, makes Angular Three v2 more performant, stable, and predictable.

Improved performance and stability 📈

While Angular Three has always been performant, thanks to Angular, the introduction of Angular Signals has allowed Angular Three v2 to significantly improve its stability story, with performance benefiting as well.

Elimination of custom NgtRef

Before Angular Signals, Angular Three used a custom NgtRef to track the life-cycle of THREE.js entities on the template via the [ref] custom property binding.

1
@Component({
2
template: `
3
<ngt-mesh [ref]="meshRef"></ngt-mesh>
4
`
5
})
6
export class MyCmp {
7
meshRef = injectNgtRef<Mesh>();
8
}

The primary purpose of NgtRef was to provide reactivity to ElementRef. With Signal Queries, this is no longer necessary. Instead, Angular Three v2 embraces Angular’s approach using its query APIs like viewChild, viewChildren, contentChild, and contentChildren.

1
@Component({
2
template: `
3
<ngt-mesh [ref]="meshRef"></ngt-mesh>
4
<ngt-mesh #mesh></ngt-mesh>
5
`
6
})
7
export class MyCmp {
8
meshRef = injectNgtRef<Mesh>();
9
meshRef = viewChild.required<ElementRef<Mesh>>('mesh'); // Signal<ElementRef<Mesh>>
10
}

This change reduces the amount of Angular Three internals needed to handle the custom NgtRef. The timing of when Signal Queries are resolved is controlled by Angular, and it should be more streamlined and predictable to end users.

Change Detection and Events

Previously, Angular Three relied on ChangeDetectorRef to trigger change detections on events like pointerover, click, etc. This was necessary because Angular Three has always run outside Angular Zone, and to provide a better experience for end users, we had to make the event system work as seamlessly as possible. Passing around the ChangeDetectorRef and ensuring that the correct Component instance was a significant challenge and was never entirely reliable.

With Angular v18, Signals and OnPush change detection strategy have been designed to work well together. This means that end users can use Signal to drive the state of their components, and Angular Three events will be automatically handled by Angular internal change detection mechanism.

1
@Component({
2
template: `
3
<ngt-mesh
4
(pointerover)="hovered.set(true)"
5
(pointerout)="hovered.set(false)"
6
(click)="active.set(!active())"
7
[scale]="scale()"
8
>
9
<ngt-box-geometry />
10
<ngt-mesh-basic-material [color]="color()" />
11
</ngt-mesh>
12
`,
13
changeDetection: ChangeDetectionStrategy.OnPush,
14
})
15
export class MyCmp {
16
protected hovered = signal(false);
17
protected active = signal(false);
18
19
protected scale = computed(() => this.active() ? 1.5 : 1);
20
protected color = computed(() => (this.hovered() ? 'hotpink' : 'orange'));
21
}

These examples are just a few of the many stability improvements that Angular Three v2 has introduced. There are numerous additional “under the hood” enhancements that unlock many more features and possibilities, which we’ll explore in the following sections.

Better composability 🧩

Angular Three v2 overhauls the NgtRenderer to make it more composable. NgtRenderer now embraces Angular’s default content projection mechanism to provide a more flexible, predictable, and performant way to render content. In turns, this makes composing different THREE.js entities much easier and more straightforward.

This improvement was inspired by a new addition to Angular Content Projection: Default content. This new feature allows Angular Three to provide default contents for some abstractions like NgtsLightformer while still allowing consumers to override it.

Consider the following example:

1
@Component({
2
template: `
3
<ngt-mesh>
4
<ngt-box-geometry />
5
<ng-content select="[data-box-material]">
6
<ngt-mesh-normal-material />
7
</ng-content>
8
<ng-content />
9
</ngt-mesh>
10
`
11
})
12
export class Box {}

In this snippet, we have a Box component that renders a Mesh with BoxGeometry and a default MeshNormalMaterial. The Box component can be used as follows:

1
<app-box />
2
3
<app-box>
4
<!-- targeting [data-box-material] content -->
5
<ngt-mesh-basic-material data-box-material color="hotpink" />
6
</app-box>
7
8
<app-box>
9
<!-- targeting regular content -->
10
<app-box />
11
</app-box>

Object Inputs

Another win for composability is unlocked by Signal Inputs. With Signal Inputs, Angular Three v2 makes uses of Object Inputs to provide a more composable way to pass inputs into custom components via [parameters] custom property binding.

Let’s revisit the Box component and allow consumers to pass in everything they can pass into Box to control the Mesh and the geometry.

1
import { NgtMesh } from 'angular-three';
2
3
@Component({
4
template: `
5
<ngt-mesh [parameters]="options()">
6
<ngt-box-geometry *args="boxArgs()" />
7
<ng-content select="[data-box-material]">
8
<ngt-mesh-normal-material />
9
</ng-content>
10
<ng-content />
11
</ngt-mesh>
12
`,
13
imports: [NgtArgs]
14
})
15
export class Box {
16
boxArgs = input<ConstructorParameters<BoxGeometry>>([1, 1, 1]);
17
options = input<Partial<NgtMesh>>({});
18
}

Now the consumers can use Box component as follows:

1
<app-box [boxArgs]="[2, 2, 2]" />
2
3
<app-box [options]="{ position: [1.5, 0, 0] }" />
4
<app-box [options]="{ position: [-1.5, 0, 0] }">
5
<app-box [options]="{ position: [0, 0.5, 0], scale: 0.5 }" />
6
</app-box>

All angular-three-soba components are built on top of Object Inputs concept, which allows for a much better composability story without the need to implement custom ngtCompound construct in the renderer internals.

Thanks to this new improvement, abstractions in angular-three-soba are significantly easier to use and reason about. Here’s another example of Text3D component with Center and Float.

1
<!-- centering a floating text 3d -->
2
3
4
<ngts-center>
5
6
7
<ngts-float>
8
9
10
<ngts-text-3d
11
text="hello world"
12
font="helvetiker_regular.typeface.json"
13
[options]="{ curveSegments: 32, bevelEnabled: true, bevelSize: 0.04, bevelThickness: 0.1, height: 0.5, lineHeight: 0.5, letterSpacing: -0.06, size: 1.5 }"
14
>
15
16
17
<ngt-mesh-normal-material />
18
</ngts-text-3d>
19
</ngts-float>
20
</ngts-center>

You just use them, nest them, and compose them. The possibilities are endless.

Better Portal powered components 🪞

NgtPortal has also received a major upgrade. It is now truly a portal with a separate layered NgtStore on top of the default root NgtStore. This means that the content of NgtPortal is rendered into an off-screen buffer, with access to the state of both the root and the layered NgtStore. This allows consumers to have better predictability and control over the components rendered inside an ngt-portal component.

Heads-up display example

For instance, we can use NgtPortal and NgtsRenderTexture (which also relies on NgtPortal) to create a heads-up display (HUD) sample.

The main scene contains the torus (donut). The view cube (HUD) is rendered in a portal with its own PerspectiveCamera. Then each face of the view cube is yet rendered into a separate portal with its own OrthographicCamera and a Text component to render the face name.

You can check out the code here: HudScene

Environment with Lightformers

NgtsEnvironment with content projection has never worked correctly. Now with v2 NgtPortal, we can finally have proper NgtsEnvironment content with NgtsLightformer to control the lighting of the environment.

Improved documentation 📖

Angular Three v2 documentation is powered by Starlight and AnalogJS to provide an enhanced experience for Angular users.

This very release blog post is powered by the same stack, making it a delight to work with. With this, we aim to provide a superior documentation experience for Angular Three users.

Testing (Experimental) 🧪

Angular Three v2 introduces an experimental testing module available through the angular-three/testing entry point. This module provides utilities to help you write unit tests for your Angular Three components and scene graphs.

Key Features

  1. NgtTestBed: A utility that extends Angular’s TestBed, specifically tailored for Angular Three components.
  2. Mocked Rendering: Tests run without actual 3D rendering, focusing on scene graph state assertions.
  3. Event Simulation: Ability to simulate Three.js-specific events like click, pointerover, etc.
  4. Animation Frame Control: Methods to advance animation frames for testing time-based behaviors.

Basic Usage

Here’s a simple example of how you might use the testing utilities:

1
import { NgtTestBed } from 'angular-three/testing';
2
3
@Component({
4
standalone: true,
5
template: `
6
<ngt-mesh #mesh (click)="clicked.set(!clicked())">
7
<ngt-box-geometry />
8
<ngt-mesh-standard-material [color]="color()" />
9
</ngt-mesh>
10
`,
11
schemas: [CUSTOM_ELEMENTS_SCHEMA],
12
changeDetection: ChangeDetectionStrategy.OnPush,
13
})
14
class MyThreeComponent {
15
clicked = signal(false);
16
color = computed(() => (this.clicked() ? 'hotpink' : 'orange'));
17
18
meshRef = viewChild.required<ElementRef<Mesh>>('mesh');
19
20
constructor() {
21
injectBeforeRender(({ delta }) => {
22
const mesh = this.meshRef().nativeElement;
23
mesh.rotation.x += delta;
24
mesh.rotation.y += delta;
25
});
26
}
27
}
28
29
describe('MyThreeComponent', () => {
30
it('should render a mesh', async () => {
31
const { scene, fireEvent, advance } = NgtTestBed.create(MyThreeComponent);
32
33
expect(scene.children.length).toBe(1);
34
const mesh = scene.children[0] as THREE.Mesh;
35
expect(mesh.isMesh).toBe(true);
36
37
await fireEvent(mesh, 'click');
38
// Assert changes after click
39
40
await advance(1); // Advance one frame
41
// Assert changes after animation
42
});
43
});

Getting Started

To get started with Angular Three v2, check out the documentation.

Github: https://github.com/angular-threejs/angular-three

There is also a template repository that you can use to start a new project with Angular Three v2.

Roadmap

As we celebrate the release of Angular Three v2, our focus now shifts to promoting its adoption and ensuring its stability. Here’s a glimpse of our immediate plans:

  • Promotion and Education: We’re committed to creating a wealth of resources to help developers get the most out of Angular Three v2. This includes:

    • Writing in-depth articles and blog posts about various features and use cases
    • Developing comprehensive tutorials covering both basic and advanced topics
    • Creating video content to demonstrate real-world applications of Angular Three v2
  • Enhancing Test Coverage: To maintain the reliability and stability of Angular Three, we’re prioritizing the expansion of our unit test suite. This will help us catch potential issues early and ensure a smooth experience for all users.

  • Community Engagement: We plan to actively engage with the community through workshops, webinars, and conference talks to showcase the power and flexibility of Angular Three v2.

We’re excited about these next steps and look forward to seeing what amazing projects our community will create with Angular Three v2!

Acknowledgements

The journey to Angular Three v2 has been a collaborative effort, and we’d like to express our heartfelt gratitude to several key contributors:

  • The PMNDRS Ecosystem: We owe a great deal to the PMNDRS (Poimandres) community and their various @pmndrs packages. Their innovative work in the 3D web space has been a constant source of inspiration and has significantly influenced the direction of Angular Three.

  • The Angular Team: We extend our sincere thanks to the Angular team for their continuous improvements to the framework. Many of the enhancements in Angular Three v2, particularly those leveraging Signals, were made possible by the Angular team’s forward-thinking approach to reactive programming.

  • The Wider Angular Community: Last but not least, we’re grateful to the entire Angular community for your support, enthusiasm, and patience throughout this development process. Your passion for pushing the boundaries of what’s possible with Angular continues to drive us forward.

We’re proud to be part of such a vibrant and supportive ecosystem, and we look forward to continuing this journey with all of you as we explore the exciting possibilities that Angular Three v2 brings to the world of 3D web development.

Conclusion

The development of Angular Three v2 has been a long journey, but we’re excited to see what you can create with it. We hope you enjoy the improvements and new features that Angular Three v2 brings to the table. If you have any feedback or suggestions, please don’t hesitate to reach out to us on GitHub.

Thank you for reading this blog post. We hope you found it informative and learned something new. Happy coding!