Declarative scene graphs
Angular Three allows users to use every feature of THREE.js in a declarative way via Angular template syntax. Scale your 3D experiences with ease by leveraging Angular’s batteries-included APIs like Signal, and more
import { NgtCanvas } from 'angular-three/dom';import { SceneGraph } from './scene-graph';
@Component({ template: ` <ngt-canvas> <app-scene-graph *canvasContent /> </ngt-canvas> `, imports: [NgtCanvas, SceneGraph]})export class SimpleScene {}
import { extend, injectBeforeRender } from 'angular-three';import { Mesh, BoxGeometry, MeshNormalMaterial } from 'three';
@Component({ selector: 'app-scene-graph', template: ` <ngt-mesh #mesh [scale]="scale()" (pointerover)="scale.set(1.5)" (pointerout)="scale.set(1)" > <ngt-box-geometry /> <ngt-mesh-normal-material /> </ngt-mesh> `, schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class SceneGraph { private meshRef = viewChild.required<ElementRef<Mesh>>('mesh');
protected scale = signal(1);
constructor() { extend({ Mesh, BoxGeometry, MeshNormalMaterial });
injectBeforeRender(() => { const mesh = this.meshRef().nativeElement; mesh.rotation.x += 0.01; mesh.rotation.y += 0.01; }); }}
import { ChangeDetectionStrategy, Component } from '@angular/core';import { NgtCanvas } from 'angular-three/dom';import { SceneGraph } from '../hud/scene-graph';
@Component({ selector: 'rapier-demo', template: ` <ngt-canvas [camera]="{ position: [-1, 5, 5], fov: 45 }" shadows> <app-scene-graph *canvasContent /> </ngt-canvas> `, imports: [NgtCanvas, SceneGraph], changeDetection: ChangeDetectionStrategy.OnPush,})export class RapierDemo {}
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, input,} from "@angular/core";import { extend, NgtArgs, type NgtVector3 } from "angular-three";import { NgtrCuboidCollider, NgtrPhysics, NgtrRigidBody } from "angular-three-rapier";import * as THREE from "three";
@Component({ selector: "app-floor", template: ` <ngt-object3D rigidBody="fixed" [options]="{ colliders: false }" [position]="[0, -1, 0]"> <ngt-mesh receiveShadow [rotation]="[-Math.PI / 2, 0, 0]"> <ngt-plane-geometry *args="[50, 50]" /> <ngt-shadow-material [opacity]="0.5" /> </ngt-mesh>
<ngt-object3D cuboidCollider [args]="[1000, 0, 1000]" /> </ngt-object3D> `, imports: [NgtrRigidBody, NgtrCuboidCollider, NgtArgs], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class Floor { protected readonly Math = Math;}
@Component({ selector: "app-box", template: ` <ngt-object3D rigidBody> <ngt-mesh #mesh castShadow receiveShadow [position]="position()" [rotation]="[0.4, 0.2, 0.5]"> <ngt-box-geometry /> <ngt-mesh-standard-material [roughness]="0.5" color="#E3B6ED" /> </ngt-mesh> </ngt-object3D> `, imports: [NgtrRigidBody, NgtrCuboidCollider], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class Box { position = input<NgtVector3>([0, 5, 0]);}
@Component({ selector: "app-scene-graph", template: ` <ngt-color attach="background" *args="['lightblue']" /> <ngt-ambient-light /> <ngt-directional-light [position]="10" castShadow> <ngt-vector2 *args="[2048, 2048]" attach="shadow.mapSize" /> </ngt-directional-light>
<ngtr-physics [options]="{ debug: true }"> <ng-template> <app-floor /> @for (position of positions; track $index) { <app-box [position]="position" /> } </ng-template> </ngtr-physics> `, imports: [NgtArgs, NgtrPhysics, Floor, Box], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class SceneGraph { positions: NgtVector3[] = [ [0.1, 5, 0], [0, 10, -1], [0, 20, -2], ];
constructor() { extend(THREE); }}
Powerful utilities
Angular Three comes with integrations for physics engines like Rapier and
Cannon; as well as postprocessing library like Postprocessing. On top
of that, angular-three-soba
provides a collection of utilities to help you focus on
building your ideas.
Apply familiar workflow
Angular Three, as a custom Angular renderer, allows you to apply your Angular knowledge to THREE.js elements. Extend Angular Three with Components or provide THREE.js elements custom behaviors with custom Directives. Everything Angular provides is at your fingertips.
import { ChangeDetectionStrategy, Component } from "@angular/core";import { NgtCanvas } from "angular-three/dom";import { SceneGraph } from "./scene-graph";
@Component({ template: ` <ngt-canvas> <app-scene-graph *canvasContent /> </ngt-canvas>
<span class="font-mono absolute bottom-0 right-0 text-sm"> * click/hover the cube </span> `, host: { class: "relative flex h-full", }, imports: [NgtCanvas, SceneGraph], changeDetection: ChangeDetectionStrategy.OnPush,})export class PointerDemo {}
import { DOCUMENT } from "@angular/common";import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, Directive, ElementRef, inject, signal, viewChild,} from "@angular/core";import { extend, injectBeforeRender, injectObjectEvents } from "angular-three";import { NgtsEnvironment } from "angular-three-soba/staging";import * as THREE from "three";
@Directive({ selector: "ngt-mesh[cursor]" })export class Cursor { constructor() { const document = inject(DOCUMENT); const elementRef = inject<ElementRef<THREE.Mesh>>(ElementRef); const nativeElement = elementRef.nativeElement;
if (nativeElement.isMesh) { injectObjectEvents(() => nativeElement, { pointerover: () => { document.body.style.cursor = "pointer"; }, pointerout: () => { document.body.style.cursor = "default"; }, }); } }}
@Component({ selector: "app-scene-graph", template: ` <ngt-mesh #mesh cursor [scale]="scale()" (pointerover)="hovered.set(true)" (pointerout)="hovered.set(false)" (click)="scale.set(scale() === 2 ? 3 : 2)" > <ngt-box-geometry /> <ngt-mesh-standard-material [color]="hovered() ? 'mediumpurple' : 'maroon'" [roughness]="0.5" [metalness]="0.5" /> </ngt-mesh>
<ngts-environment [options]="{ preset: 'warehouse' }" /> `, imports: [Cursor, NgtsEnvironment], changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA],})export class SceneGraph { private meshRef = viewChild.required<ElementRef<THREE.Mesh>>("mesh");
protected hovered = signal(false); protected scale = signal(2);
constructor() { extend(THREE);
injectBeforeRender(({ delta }) => { const mesh = this.meshRef().nativeElement; mesh.rotation.x += delta; mesh.rotation.y += delta; }); }}