Renderer
The custom renderer powers the entire Angular Three and is provided via a dedicated platform entry point.
angular-three/dom
for DOM/Browser platform
import { provideNgtRenderer } from 'angular-three/dom';
bootstrapApplication(AppComponent, { providers: [provideNgtRenderer()]});
Catalogue
Angular Three maintains a single catalogue of THREE.js objects to render. By default, the catalogue is empty.
extend
In order to populate the catalogue, you can call the extend()
function and pass in a Record
of THREE.js objects. Angular Three then maps the catalogue to Custom Element tags with the following naming convention:
import { extend } from 'angular-three';import { Mesh, MeshBasicMaterial, LOD, Object3D } from 'three';
extend({ Mesh, // makes ngt-mesh available MeshBasicMaterial, // makes ngt-mesh-basic-material available LOD, // makes ngt-lOD available Object3D, // makes ngt-object3D MyMesh: Mesh, // makes ngt-my-mesh available});
Overrides
extend()
can be called multiple times and the later invocation will merge the Record
with the existing catalogue. This is useful for when you want to provide a different implementation for a specific Custom Element tag like switching between WebGPURenderer
and WebGLRenderer
import { extend } from 'angular-three';import { DirectionalLight } from 'three';import { DirectionalLight as WebGPUDirectionalLight } from 'three/webgpu';
extend({ DirectionalLight }); // make ngt-directional-light availableextend({ DirectionalLight: WebGPUDirectionalLight }); // after this, ngt-directional-light will now instantiate the WebGPU DirectionalLight instead
CUSTOM_ELEMENTS_SCHEMA
Since Angular Three works with Custom Element tags, you need to have schemas: [CUSTOM_ELEMENTS_SCHEMA]
in your component’s metadata.
Property Bindings
You can bind values to any ngt-*
elements using Property Binding syntax.
<ngt-mesh [position]="[1, 2, 3]"> <ngt-box-geometry /> <ngt-mesh-basic-material color="red" /></ngt-mesh>
Shortcuts
Some THREE.js objects have different methods to update their values due to performance and how WebGL works. For example, THREE.Vector3
has a set
method that accepts a tuple of [x, y, z]
instead. On the other hand, if we pass in a THREE.Vector3
instance, the current THREE.Vector3
will call copy
and pass in the new instance instead.
This characteristic is baked into the custom renderer so it is more intuitive to pass values to these properties.
<ngt-mesh
[position]="[0, 1, 0]"
[scale]="1.5"
[rotation]="myRotation"/>
ColorRepresentation
Any elements that accept a color property can accept a ColorRepresentation
type
<!-- different ways to pass in color -->
<ngt-mesh-basic-material color="hotpink" />
<ngt-mesh-basic-material color="#ff00ff" />
<ngt-mesh-basic-material color="rgb(255, 0, 255)" />
<ngt-mesh-basic-material [color]="myColor" />
<ngt-mesh-basic-material [color]="myHexadecimalColor" />
Coercion
Angular Three custom renderer automatically coerces primitive values.
<ngt-mesh castShadow receiveShadow/>
Pierced Properties
Angular Three supports pierced properties via dot-notation. Imagine you want to turn a plane (THREE.Mesh
with THREE.PlaneGeometry
) on its side to make the plane into a floor, instead of passing [rotation]="[-Math.PI / 2, 0, 0]"
to the <ngt-mesh>
, you can pierce the rotation property with [rotation.x]="-Math.PI / 2"
.
<ngt-mesh [position.y]="-0.5" [rotation.x]="-Math.PI / 2"> <ngt-plane-geometry /></ngt-mesh>
Pierced properties trade-offs
There are trade-offs worth noting for piercing properties.
The pro is it works extremely well for component-based frameworks like Angular where underlying operations use some sort of change detection strategy to determine whether to update the template or not. Since pierced properties allow us to work with more primitive values on the template, it is easier for the change detection mechanism to work with.
However, when it comes to piercing properties of a non-builtin object like an external THREE.Material
that you bind to a <ngt-mesh>
via [material]
, timing of property bindings might get weird and things might not work the way you expect it to. For cases like this, it is recommended to use ngt-value
instead.
Considering the following example
<ngt-mesh [material]="gltf.materials.material" material.color="green" /><!-- vs --><ngt-mesh [material]="gltf.materials.material" [material.color]="'green'">
These 2 snippets behave differently in terms of timing. The first snippet will update the original material first then bind a new material later; while the second snippet will bind correctly but the order matters. It gets trickier when it comes to more complex objects like textures.
Hence, pierce properties are only recommended for built-in properties of a THREE.js object that are less likely to change like position
, rotation
, scale
etc…
Angular Three Properties
Beside the properties of the native THREE.js objects, there are some special properties that are specific to Angular Three.
parameters
All custom elements accept a parameters
property that accepts an object of properties to pass to the underlying object.
<ngt-mesh-basic-material [parameters]="{ color: 'hotpink', side: BackSide, transparent: true }"/>
Shortcuts are still applied with parameters
<ngt-mesh [parameters]="{ position: [1, 2, 3], scale: 1.5, rotation: myRotation }"/>
attach
This property is used to specify a property on the parent that this element should be attached to. Attaching takes into account the life-cycle of the elements and will automatically detach when the elements are destroyed.
Static Attach
If you want to bind a static value to attach
, use Attribute Binding to bind a static string to it
<ngt-mesh> <ngt-box-geometry attach="geometry" /> <ngt-mesh-basic-material attach="material" /></ngt-mesh>
THREE.js equivalent
const mesh = new THREE.Mesh();const material = new THREE.MeshBasicMaterial();const geometry = new THREE.BoxGeometry();
mesh.material = material;mesh.geometry = geometry;
Nested Path Attach
You can attach to a deeply nested property on the parent by using dot-separated path
<ngt-spot-light castShadow> <ngt-vector2 attach="shadow.mapSize" /></ngt-spot-light>
THREE.js equivalent
const spotLight = new THREE.SpotLight();spotLight.castShadow = true;
const vector2 = new THREE.Vector2();// shortcut is still applied automaticallyspotLight.shadow.mapSize.copy(vector2);
Dynamic Attach
You can pass a dynamic value by using Property Binding syntax [attach]
. When this is the case, attach
accepts Array<string | number>
in addition to just string
. Common use-case for this is to attach different materials to different faces of a geometry.
@Component({ template: ` <ngt-mesh> <ngt-box-geometry /> @for (color of colors; track $index) { <ngt-mesh-basic-material [attach]="['material', $index]" [color]="color" /> } </ngt-mesh> `})export class MyCube { protected colors = ['red', 'green', 'blue', 'yellow', 'orange', 'purple']; // cube has 6 faces}
THREE.js equivalent
const mesh = new THREE.Mesh();const geometry = new THREE.BoxGeometry();
mesh.geometry = geometry;mesh.material = [];
const colors = ['red', 'green', 'blue', 'yellow', 'orange', 'purple'];
for (let i = 0; i < colors.length; i++) { const material = new THREE.MeshBasicMaterial(); material.color.set(colors[i]); mesh.material[i] = material;}
NgtAttachFunction
Last but not least, you can pass a NgtAttachFunction
to [attach]
property. When this is the case, you are responsible for attaching and detaching the element yourself.
NgtAttachFunction
can be created using an identity function createAttachFunction
to help with typings.
import { createAttachFunction } from 'angular-three';
@Component({ template: ` <ngt-mesh> <ngt-mesh-basic-material [attach]="attachFn" /> </ngt-mesh> `})export class SceneGraph { protected attachFn = createAttachFunction<THREE.MeshBasicMaterial, THREE.Mesh>(({ parent, child }) => { const oldMaterial = parent.material; parent.material = child;
// return a clean-up function that will be called when `ngt-mesh-basic-material` is destroyed return () => { parent.material = oldMaterial; } })}
Event Bindings
As you’ve learned in Handling Events, you can use Event Binding syntax to listen to various events on the elements. This section will go over some THREE.js native events and some specific Angular Three events that all objects share in addition to Raycast Events.
THREE.js native events
Most THREE.js objects extends EventDispatcher
and can emit arbitrary events. There are some well-known events that Angular Three renderer supports via the Event Binding syntax.
added
: when the object is added to the parentremoved
: when the object is removed from the parentchildadded
: when a child is added to the objectchildremoved
: when a child is removed from the objectchange
: when the object’s properties change. This is mostly applied to controls likeOrbitControls
disposed
: when the object is disposed. The original event name isdispose
but Angular Three usesdisposed
to not have conflict with[dispose]
property
<ngt-group (childadded)="onChildAdded()"> <ngt-mesh (added)="onAdded()"></ngt-mesh></ngt-group>
THREE.js equivalent
const group = new THREE.Group();group.addEventListener('childadded', onChildAdded);
const mesh = new THREE.Mesh();mesh.addEventListener('added', onAdded);
group.add(mesh);
Angular Three events
attached
This event is emitted when the element is attached or added to the parent. This is subtly different than the THREE.js added
event because it also emits when a non-Object3D object (i.e: Material) is attached to the parent.
@Component({ template: ` <ngt-mesh> <ngt-mesh-basic-material (attached)="onAttached($event)" /> </ngt-mesh> `})export class SceneGraph { onAttached(event: NgtAfterAttach<THREE.MeshBasicMaterial, THREE.Mesh>) { const { parent, node } = event; // ^? Mesh ^? MeshBasicMaterial }}
updated
This event is emitted when the element is updated, mostly via Property Binding
@Component({ template: ` <ngt-mesh [position]="[1, 1, 1]" (updated)="onUpdated($event)" ></ngt-mesh> `})export class SceneGraph { onUpdated(event: THREE.Mesh) { }}