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]})exportclassSimpleScene {}import { extend, beforeRender } 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],})exportclassSceneGraph {private meshRef = viewChild.required<ElementRef<Mesh>>('mesh');protected scale =signal(1);constructor() {extend({ Mesh, BoxGeometry, MeshNormalMaterial });beforeRender(()=> {constmesh = 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,})exportclassRapierDemo {}import {ChangeDetectionStrategy,Component,CUSTOM_ELEMENTS_SCHEMA,input,} from"@angular/core";import { 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-object3DrigidBody="fixed"[options]="{ colliders: false }"[position]="[0, -1, 0]"[rotation]="[-Math.PI / 2, 0, 0]"><ngt-meshreceiveShadow><ngt-plane-geometry*args="[50, 50]" /><ngt-shadow-material[opacity]="0.5" /></ngt-mesh><ngt-object3D[cuboidCollider]="[1000, 1000, 0]" /></ngt-object3D>`,imports: [NgtrRigidBody, NgtrCuboidCollider, NgtArgs],changeDetection: ChangeDetectionStrategy.OnPush,schemas: [CUSTOM_ELEMENTS_SCHEMA],})exportclassFloor {protectedreadonly Math = Math;}@Component({selector: "app-box",template: `<ngt-object3DrigidBody[position]="position()"[rotation]="[0.4, 0.2, 0.5]"><ngt-meshcastShadowreceiveShadow><ngt-box-geometry /><ngt-mesh-standard-material[roughness]="0.5"color="#E3B6ED" /></ngt-mesh></ngt-object3D>`,imports: [NgtrRigidBody],changeDetection: ChangeDetectionStrategy.OnPush,schemas: [CUSTOM_ELEMENTS_SCHEMA],})exportclassBox {position =input<NgtVector3>([0, 5, 0]);}@Component({selector: "app-scene-graph",template: `<ngt-point-light[position]="[-10, -10, 30]"[intensity]="0.25 * Math.PI"[decay]="0" /><ngt-spot-light[intensity]="0.3 * Math.PI"[position]="[30, 30, 50]"[angle]="0.2"[penumbra]="1"[decay]="0"castShadow /><ngtr-physics[options]="{ debug: true }"><ng-template><app-floor />@for (positionof positions; track $index) {<app-box[position]="position" />}</ng-template></ngtr-physics>`,imports: [NgtArgs, NgtrPhysics, Floor, Box],changeDetection: ChangeDetectionStrategy.OnPush,schemas: [CUSTOM_ELEMENTS_SCHEMA],})exportclassSceneGraph {protectedreadonly Math = Math;protected positions:NgtVector3[] = [[0.1, 5, 0],[0, 10, -1],[0, 20, -2],];}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 provi 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><spanclass="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,})exportclassPointerDemo {}import { DOCUMENT } from"@angular/common";import {ChangeDetectionStrategy,Component,CUSTOM_ELEMENTS_SCHEMA,Directive,ElementRef,inject,signal,viewChild,} from"@angular/core";import { extend, beforeRender, objectEvents } from"angular-three";import { NgtsEnvironment } from"angular-three-soba/staging";import*as THREE from"three";@Directive({ selector: "ngt-mesh[cursor]" })exportclassCursor {constructor() {constdocument = inject(DOCUMENT);constelementRef = inject<ElementRef<THREE.Mesh>>(ElementRef);constnativeElement = elementRef.nativeElement;if (nativeElement.isMesh) {objectEvents(()=> nativeElement, {pointerover: ()=> {document.body.style.cursor="pointer";},pointerout: ()=> {document.body.style.cursor="default";},});}}}@Component({selector: "app-scene-graph",template: `<ngt-mesh#meshcursor[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],})exportclassSceneGraph {private meshRef = viewChild.required<ElementRef<THREE.Mesh>>("mesh");protected hovered =signal(false);protected scale =signal(2);constructor() {extend(THREE);beforeRender(({ delta })=> {constmesh = this.meshRef().nativeElement;mesh.rotation.x+= delta;mesh.rotation.y+= delta;});}}import { ChangeDetectionStrategy, Component } from'@angular/core';import { NgtCanvas } from'angular-three/dom';import { SceneGraph } from'./scene-graph';@Component({selector: 'app-gltf-demo-home',template: `<ngt-canvas[camera]="{ fov: 75, position: [0, 2, 3] }"><app-scene-graph*canvasContent /></ngt-canvas>`,imports: [NgtCanvas, SceneGraph],changeDetection: ChangeDetectionStrategy.OnPush,})exportdefaultclassGLTFDemoHome {}import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from'@angular/core';import { NgtArgs } from'angular-three';import { NgtsOrbitControls } from'angular-three-soba/controls';import { Bot } from'./bot';@Component({selector: 'app-scene-graph',template: `<ngt-color*args="['#303030']"attach="background" /><ngt-grid-helper*args="[10, 20]" /><app-bot[positionX]="0.75"[rotationY]="-Math.PI / 2"[bodyTexture]="1" /><app-bot[positionX]="-0.75"[rotationY]="Math.PI / 2"[bodyTexture]="2" /><ngts-orbit-controls[options]="{ enableZoom: false, enablePan: false }" />`,imports: [NgtArgs, Bot, NgtsOrbitControls],schemas: [CUSTOM_ELEMENTS_SCHEMA],changeDetection: ChangeDetectionStrategy.OnPush,})exportclassSceneGraph {protectedreadonly Math = Math;}import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, effect, input } from'@angular/core';import { NgtArgs } from'angular-three';import { gltfResource } from'angular-three-soba/loaders';import { animations, type NgtsAnimationClips } from'angular-three-soba/misc';import { matcapTextureResource } from'angular-three-soba/staging';import*as THREE from'three';import { SkeletonUtils, type GLTF } from'three-stdlib';import botGLB from'@common-assets/ybot.glb'with { loader: 'file' };type BotGLTF =GLTF& {animations:NgtsAnimationClips<'Dance'>[];};const bodies = {1: '312D20_80675C_8B8C8B_85848C',2: '5B4CBC_B59AF2_9B84EB_8F78E4',};@Component({selector: 'app-bot',template: `<ngt-primitive*args="[scene()]"[position.x]="positionX()"[rotation.y]="rotationY()" />`,imports: [NgtArgs],schemas: [CUSTOM_ELEMENTS_SCHEMA],changeDetection: ChangeDetectionStrategy.OnPush,})exportclassBot {protectedreadonly Math = Math;positionX =input(0);rotationY =input(0);bodyTexture = input.required<1|2>();private gltf =gltfResource<BotGLTF>(()=> botGLB);private matcapBody =matcapTextureResource(()=> bodies[this.bodyTexture()], {onLoad: (texture)=> {texture.colorSpace=THREE.SRGBColorSpace;},});private matcapJoints =matcapTextureResource(()=>'394641_B1A67E_75BEBE_7D7256', {onLoad: (texture)=> {texture.colorSpace=THREE.SRGBColorSpace;},});protected scene =computed(()=> {constgltf = this.gltf.value();if (!gltf) returnnull;const [matcapBody, matcapJoints] = [this.matcapBody.resource.value(), this.matcapJoints.resource.value()];if (!matcapBody ||!matcapJoints) returnnull;constscene = SkeletonUtils.clone(gltf.scene);constbody = scene.getObjectByName('YB_Body') as THREE.SkinnedMesh;constjoints = scene.getObjectByName('YB_Joints') as THREE.SkinnedMesh;if (!body ||!joints) return scene;body.material=newTHREE.MeshMatcapMaterial({ matcap: matcapBody });joints.material=newTHREE.MeshMatcapMaterial({ matcap: matcapJoints });return scene;});private animations =animations(this.gltf.value, this.scene);constructor() {effect(()=> {if (!this.animations.isReady) return;this.animations.actions.Dance.reset().fadeIn(0.5).play();});}}Seamless integrations of 3D assets
Angular Three makes working with external 3D assets effortless. Import, load, animate, and clone/reuse GLTF models with just a few lines of code.