NgxThree wraps three.js in Angular components. It allows to render 3d Scenes in a declarative way. And you can leverage the angular features and ecosystem your are familiar with.
ngx-three uses code generation to be able to provide as much functionality from three js. This approach makes it possible to follow three.js updates with minimal effort.
ngx-three:
- generates wrappers (> 130) for three.js class categories:
- Object3d,
- Material,
- Geometry,
- Post processing passes,
- Controls
- Textures
- Adds support for simple pointer event handling
- Easy handling of async model loading
- Supports Multi-View / Multi-Scene scenarios
- enables declarative post processing
- ...
The project is inspired by the great react three fiber library. But in contrast to RTF angular components are generated that wrap three.js classes.
Check out some examples
From a performance perspective it's important to know, that ngx-three components do not produce any DOM elements.
In addition the generate classes use OnPush change detection strategy and the scene rendering runs outside the angular zone.
This means there is no overhead because of additional DOM elements and the impact of angular's change detection mechanism should be minimized.
npm install ngx-three
In addition to ngx-three you have to install it's peer dependencies Angular (setup howto), three.js and its typings
npm install three
npm install @types/three
You can use npm to get the exact peer dependency versions for ngx-three
npm info ngx-three peerDependencies
We are going to create a basic example showing a cube, with animation and interaction.
Lets start by creating a simple component with an empty template.
import { Component } from '@angular/core';
@Component({
selector: 'app-example',
template: ` <!-- Step 2 Content --> `,
})
export class ExampleComponent {}
As a second step we start filling the template with a canvas and a scene (the most basic setup).
<th-canvas>
<th-scene>
<!-- Step 3 Content -->
</th-scene>
</th-canvas>
In this step we are adding a mesh with material and geometry.
You can add material and geometry to the mesh by nesting th-*Material
and th-*Geometry
components
inside a th-mesh
componet.
By means of the args
attribute you can pass parameters
to the constructor of the three.js basic material.
<th-mesh>
<th-boxGeometry></th-boxGeometry>
<th-meshBasicMaterial [args]="{color: 'purple'}"> </th-meshBasicMaterial>
</th-mesh>
<!-- Step 4 Content -->
Now lets bring some (ambient)light to the scene.
The perspective camera takes multiple constructor arguments.
These can be passed to the camera constructor by passing an
an array holding the arguments to args
.
The position of the camera is set by means of the position
attribute.
<th-ambientLight> </th-ambientLight>
<th-perspectiveCamera [args]="[75, 2, 0.1, 1000]" [position]="[1,1,5]">
</th-perspectiveCamera>
By adding a boolean member selected
to our class
we can modify the scale of the cube
<th-mesh>
(onClick)="selected = !selected" [scale]="selected ? [2, 2, 2] : [1, 1, 1]"
...
</th-mesh>
By reacting to the canvas' onRender
we can animate the box by setting its rotation.
Template:
<th-canvas (onRender)="this.onBeforeRender()">
...
<th-mesh [rotation]="rotation" ...> ... </th-mesh>
...
</th-canvas>
Component:
...
public rotation: [x: number, y: number, z: number] = [0, 0, 0];
public onBeforeRender() {
this.rotation = [0, this.rotation[2] + 0.01, this.rotation[2] + 0.01];
}
...
The mesh part of the current code can be seperated into its own class Then we can use two instances ot the box component in the app component Take a look at the working example
Box Template:
<th-mesh
[rotation]="rotation"
[position]="position"
(onClick)="selected = !selected"
[scale]="selected ? [2, 2, 2] : [1, 1, 1]"
>
<th-boxGeometry></th-boxGeometry>
<th-meshBasicMaterial [args]="{color: 'purple'}"></th-meshBasicMaterial>
</th-mesh>
Box Component:
@Component({
selector: 'app-box',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Box {
selected = false;
@Input()
public rotation: [x: number, y: number, z: number] = [0, 0, 0];
@Input()
public position: [x: number, y: number, z: number] = [0, 0, 0];
}
App Template:
<th-canvas (onRender)="this.onBeforeRender()">
<th-scene>
<th-box [position]="[-2,0,0]" [rotation]="rotation"> </th-box>
<th-box [position]="[2,0,0]" [rotation]="rotation"> </th-box>
<th-ambientLight> </th-ambientLight>
<th-perspectiveCamera
[args]="[75, 2, 0.1, 1000]"
[position]="[1,1,5]"
></th-perspectiveCamera>
</th-scene>
</th-canvas>
App Component:
@Component({
selector: 'app-example',
template: `...`
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExampleComponent {
public rotation: [x: number, y: number, z: number] = [0, 0, 0];
public onBeforeRender() {
this.rotation = [0, this.rotation[2] + 0.01, this.rotation[2] + 0.01];
}
}
Canvas View and Scene are the main building blocks of ngx-three.
In general the ThCanvas
contains ThView istances, and a
ThView contains a ThScene.
In a standard scenario ThCanvas
provides (or actually 'is') the default view.
So a typical template might look like this
<th-canvas>
<th-scene> ... </th-scene>
</th-canvas>
ThCanvas
creates the canvas dom element that's used
for rendering. Actually ThCanvas is the only ngx-three component that inserts an element into dom!
ThCanvas
provides the ThEngineService.
That means if you have multiple ThCanvas
instances
every one gets its own engine service.
As ThCanvas
is derived from ThView
it also shares its inputs / outputs
In addition ThCanvas
also provides an input rendererParameters
. It takes a configuration object
that allows for setting all the members provided by the
THREE.WebGLRenderer.
One can say that ThView
provides the view port.
The view consists of:
- a scene ThScene
- a camera (
ThCamera
) - and an optional effect composer (
ThEffectComposer
)
This combination makes it possible to render multiple scenarios
- The same scene with multiple camera perspectives (Multi View Example)
- mutliple scenes with the same camera (Multi Scene Example)
- one / multiple scene with multiple effects (Multi Effects Example)
ThScene
is the ngx-three wrapper of THREE.Scene and provides all
of its members as inputs. It is mandatory for rendering.
ngx-thre supports different renderers provided by three.js:
- WebGLRenderer
- CSS3DRenderer
- CSS2DRenderer
And they can be combined too!
You can configure them in two different ways
-
by providing them with the helper functions
provideWebGLRenderer
,provideCSS2dRenderer
andprovideCSS3dRenderer
:@Component({ selector: 'app-example', providers: [provideCSS3dRenderer({...})], ... })
or
provideWebGLRenderer({...})
orprovideCSS2dRenderer({...})
-
or by means of structural directives:
<th-canvas *rendererParameters="{ ...WebGLRendererOptions... }"> ... </th-canvas>
or
*css2dRendererParameters]="{...}"
,*css3dRendererParameters]="{...}"
If you do not provide any renderer the WebGLRenderer is used as the default renderer with its default configuration.
Examples:
The ThAnimationLoop
service runs the render loop.
You can start
and stop
the loop.
In addition you can request a 'one-shotrendering by calling the Service-method
requestAnimationFrame`;
You can react to a 'global' render event by means of listening to the
ThAnimationLoop.beforeRender$
observable
or by using the beforeRender
output of the ThRenderDirective
.
<th-object3D (beforeRender)="doSomething()" ></th-object3D>
In addition you can react to the onRender
outputs of the ThView
(ThCanvas
is derived from it) instances.
<th-canvas (onRender)="doSomething()"></th-canvas>
For common scenarios ngx-three handles resizing automatically by observing the size of the canvas.
But for special scenarios (Multi View Example) you might have to do calculations when the size changes. This can be achieved by:
- using the
onResize
event of theThRenderDirective
.
<th-object3D (onResize)=calculateSomething($event)></th-object3D>
- or by subscribing to the
resize$
of theThEngineService
The ThEngineService
takes care of resizing, does the rendering and organizes the available views (ThView)
Note: The engine service automatically takes into account the device pixel ratio when calculating the renderer dimensions. This works dynamically i.e.: when moving the window from one display to a second one with different device pixel ratio.
In case you do not need an animation (i.e.: static model viewer)
you can disable the animation loop by setting renderOnDemand
to true.
From this time on the render calls only happen in following cases:
- Angular change detection is triggered for one of
ThCanvas
' children - A controller fires an event
This works with OrbitControls
, MapControls
, DragControls
, TransformControls
and ArcBallControls
.
But FlyControls
, TrackbalControls
and FirstPersonControls
need a render loop.
You can play with these controls and on-demand rendering in the On-Demand Example.
Example:
This allows to render only once to show a scene (i.e.: resulting from a loaded GLB file). And while you move the camera by means of the orbit control continuous render calls will be triggered. When you stop moving no render calls will happen.
Let's say you change the background color that is bound in a template. In this case the angular changed detection mechanism triggers and the scene is rendered (once).
<th-canvas [renderOnDemand]="doOnDemandRendering"></th-canvas>
In three.js anything that can be added to a Scene is an Object3D
.
In ngx-three the component ThObject3D
with the tag th-object3D
can be seen as the equivalent.
A mesh (Three.Mesh
) can be represented by th-mesh
in ngx-three.
A mesh can have a material (ThMaterial
) and a Geometry(ThGeometry
).
<th-mesh>
<th-boxGeometry></th-boxGeometry>
<th-meshBasicMaterial></th-meshBasicMaterial>
</th-mesh>
Every ngx-three object has a member called
objRef
this one holds the reference to the
three.js object.
For example ThMesh
has a member objRef: THREE.Mesh
.
There are two ways to reference existing ngx-three object class instances.
You can use ViewChild to reference template objects from within the component code.
@Component({
selector: 'app-myapp',
template: ` <th-mesh></th-mesh> `,
})
export class MyApp {
@ViewChild(ThMesh, { static: true }) mesh: ThMesh;
}
Referencing ngx-three objects (th-object3D
) can be
easiliy referenced from within the template by means
of template variables
<th-mesh>
<th-boxGeometry #theGeo></th-boxGeometry>
</th-mesh>
<th-mesh>
<th-material [objRef]="theGeo.objRef"></th-material>
<th-meshBasicMaterial></th-meshBasicMaterial>
</th-mesh>
If you want to put an existing object into the angular component tree
(maybe it was easier to construct the specific object in an imperative way)
this can be easily achieved by setting the objRef
Attribute
<th-object3D [objRef]="existingObj"></th-object3D>
ngx-three provides an easy way to load models / scenes and apply it
to a th-object3D
element.
It implements a generic loader service, loader directive and loader pipe,
That is then used by the specific loader implementations
All types of loaders can load models by means of:
- a loader service
- provides a Promise of the resulting 'model'
- a progress callback
- a directive
- applies the model to the host
ThObject3d
component ( to itsobjRef
) - provides a progress output
- provides a loaded output
- applies the model to the host
- a pipe
- provides a progress callback
under the hood the directive and the pipe use the service One example is the GLTF-Loader
Loading GLTF / GLB files can be achieved
by using the loadGLTF
directive:
<th-object3D loadGLTF url="assets/helmet.glb"> </th-object3D>
the loadGLTF
pipe:
<th-object3D [objRef]="('assets/helmet.glb' | loadGLTF | async).scene"> </th-object3D>
or by using the GLTFLoaderService directly:
...
constructor(private service: GLTFLoaderService) {
}
async ngOnInit() {
const result: GLTF = await service.load('assets/helmet.glb');
}
the load
method of the service uses the three.js Loader.loadAsync method under the hood
and also provides the same parameters
load(url: string, onProgress?: (event: ProgressEvent) => void): Promise<any>;
You can find an example here
To load draco compressed gltf files you have to specify the path to a folder containing the WASM/JS decoding libraries.
All you have to do is to inject the DRACOLoaderService
and set the decoder path.
constructor(dracoLoader: DRACOLoaderService) {
// specify the draco decoder path used by the gltf loader instances
dracoLoader.setDecoderPath('https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/jsm/libs/draco/gltf/');
}
You may have to specify a different crossOrigin string to implement CORS i.e.:
dracoLoader.setCrossOrigin('no-cors'); // just for testing (default: "anonymous")
By default, a single DRACOLoader is reused when loading consecutive models. To change this behaviour, explicitly disable it:
dracoLoader.setReuseInstance(false); // (default: true)
You can find an example here
In addition to the pre-defined loaders it is actually quite simple to add additional loader Service / Directive / Pipe.
The below example shows how to implement simple 'obj' loading
Creating the service is as simple as deriving from ThAsyncLoaderService<OBJLoader>
and setting the clazz
member to the Loader class provided by three.js.
@Injectable({
providedIn: 'root',
})
export class OBJLoaderService extends ThAsyncLoaderService<OBJLoader> {
public readonly clazz = OBJLoader;
}
Creating your own loader pipe is equaly simple. You just have to derive from
ThAsyncLoaderBasePipe<OBJLoader>
and inject the previously defined service.
@Pipe({
name: 'loadObj',
pure: true,
})
export class ThObjLoaderPipe
extends ThAsyncLoaderBasePipe<OBJLoader>
implements PipeTransform
{
constructor(protected service: OBJLoaderService) {
super();
}
}
The directive can be implemented as follows:
@Directive({
selector: '[loadObj]',
})
export class ThObjLoaderDirective extends ThAsyncLoaderBaseDirective<OBJLoader> {
constructor(
@Host() protected host: ThObject3D,
protected zone: NgZone,
protected service: OBJLoaderService
) {
super(host, zone);
}
protected getRefFromResponse(response: Group) {
return response;
}
}
- you have to inject the previously implemented service
- and implement the method
getRefFromResponse
. The value returned by this method is applied to the host ThObject3D'sobjRef
member.
The FBXLoader can be used like the GLTF loader
by using the loadFBX
directive:
<th-object3D loadFBX url="assets/model.fbx"> </th-object3D>
the loadFBX
pipe:
<th-object3D [objRef]="'assets/model.fbx' | loadFBX"> </th-object3D>
or by using the FBXLoaderService directly.
The PLYLoader is a little bit different than the previous Loaders (i.e.: GLTFLoader). Because the PLYLoader only provides/loads geometry and no material. In contrast the GLTFLoader loads a full scene.
You can use the PLYLoader directive:
<th-mesh>
<!-- PLY file ( only provides geometry! ) -->
<th-bufferGeometry loadPLY [url]="assets/dolphins.ply"> </th-bufferGeometry>
<th-meshStandardMaterial
[args]="{ color: '#0055ff' }"
></th-meshStandardMaterial>
</th-mesh>
or the pipe
<th-mesh>
<!-- PLY file ( only provides geometry! ) -->
<th-bufferGeometry [objRef]="'assets/dolphins.ply' | loadPLY">
</th-bufferGeometry>
<th-meshStandardMaterial
[args]="{ color: '#0055ff' }"
></th-meshStandardMaterial>
</th-mesh>
or you can use the PLYLoader service directly.
You can find an example here
to enable loader caching you can use three.js' built in cache:
THREE.Cache.enabled = true;
ngx-three generates wrappers for
- CanvasTexture
- CompressedTexture
- CubeTexture
- DataTexture
- DataTexture2DArray
- DataTexture3D
- DepthTexture
- Texture
- VideoTexture
- FramebufferTexture
all those wrappers can be placed in an angular template
<th-Texture #myTexture></th-Texture>
and you can reuse it in the template by means of a template reference (i.e.: myTexture
);
To load a Texture you have 3 possibilities (service, pipe, directive)
-
place a loader directive on a wrapper component
<th-Texture loadTexture url="thetexture.jpg"></th-Texture>
-
use the loader pipe
<th-MeshBasicMaterial [map]='"thetexture.jpg" | loadTexture'> </th-MeshBasicMaterial>
-
use the injected service
... constructor(service: TextureLoaderService) { const texture = service.load('thetexture.jpg') }
the loaders provide event emitters / callbacks for 'loaded' and 'progress'
Following texture loaders are available:
- TextureLoaderService, ThTextureLoaderDirective, ThTextureLoaderPipe
- CubeTextureLoaderService, ThCubeTextureLoaderDirective, ThCubeTextureLoaderPipe,
- Data Texture Loaders
- ThDDSLoaderDirective, ThDDSLoaderPipe, DDSLoaderService
- ThKTXLoaderDirective, ThKTXLoaderPipe, KTXLoaderService
- ThKTX2LoaderDirective, ThKTX2LoaderPipe, ThKTX2LoaderService
- ThPVRLoaderDirective, ThPVRLoaderPipe, PVRLoaderService
- Compressed Texture Loaders
- ThEXRLoaderDirective, ThEXRLoaderPipe, EXRLoaderService
- ThRGBELoaderDirective, ThRGBELoaderPipe, RGBELoaderService
- ThRGBMLoaderDirective, ThRGBMLoaderPipe, RGBMLoaderService
- ThTGALoaderDirective, ThTGALoaderPipe, TGALoaderService
the pipe and directive names follow a naming scheme
load*Texture
where*
can be''
,Cube
,DDS
,EXR
...
ngx-three supports the following mouse/pointer events:
- onClick
- onMouseEnter
- onMouseExit
- onPointerDown
- onPointerUp
All of them return a RaycasterEmitEvent
that holds the target component and the intersection data of raycaster.intersectObject (except for onMouseExit
)
Every th-object3D
element emits property changes.
you can listen to it like this:
<th-object3D (onUpdate)="doSomething($event)></th-object3D>
Every wrapper can be used to bind to three js events
within an Angular template.
For this purpose you can pass an object ( key = event name, value = callback) to the threeEvents
input.
You have to make shure that context of the callback functions get preserved.
This can be done
- by means of using the bind pipe
- or by using fat arrow function members instead of methods (see example below)
component:
@Component({
// ...
})
export class TestComponent {
public onOrbitControlChange(evt: Event) {
// ...
}
public onOrbitControlEnd = (evt: Event) => { // <-- preserves this context when used in template
// ...
}
}
template:
<!--
binding to three.js events:
1) change: with bind directive for preserving this scope
2) end: using a fat arrow member function for preserving this scope
-->
<th-orbitControls
[threeEvents]="{
'change': onOrbitControlChange | bind : this,
'end': onOrbitControlEnd
}"></th-orbitControls>
</th-perspectiveCamera>
ngx-three provides some utility pipes that ease input assignments
This directive can be used to react to 'global' rendering loop events.
Listen to the beforeRender
output to to apply changes
before the next rendering run happens.
If you want to do something directly after the rendering pass use afterRender
.
Use this pipe to create a Color from any of it's constructor parameters
<th-ambientLight [color]="'#aabbcc' | color"> </th-ambientLight> ...
the vector2
, vector3
, and vector4
construct the respective vector from an array of numbers
usage:
<th-object3D [position]="[1,2,3] | vector3"> </th-object3D>
calls clone on a clonable three.js instance (or it's ngx-three wrapper) and ensures that the clone call only happens once
<th-hemisphereLight #light ...></th-hemisphereLight>
<th-light [objRef]="light | clone"></th-light>
binds a function to an object by means of Function.prototype.bind()
<th-orbitControls [threeEvents]="{ end: onOrbitEnd | bind: this }"></th-orbitControls>
creates a three.js plane instance from an array of numbers [x,y,z]
( = normal vector ) and an optional
argument ( = constant: the signed distance from the origin to the plane ).
<th-planeHelper [args]="[[0,1,0] | plane: 2"></th-planeHelper>
A utility directive that helps you selecting a specific node of a model.
<th-object3d [loadGltf]="head.glb">
<th-mesh refById="left-eye" >
<th-meshBasicMaterial [args]="{color: 'purple'}"></th-meshBasicMaterial>
</th-mesh>
</th-object3d>
You can find an example here
If you want to display the well known stats panel
you can do that simply by adding the thStats
directive
to the canvas.
<th-canvas [thStats]="true"> ... </th-canvas>
With ngx-three you can leverage the full potential of Angular DevTools without sacrificing performance.
In dev-mode (ng serve
) ngx-three can be debugged with the DevTools:
by inserting the Angular components into the dom:
And in production mode / build not a single additional dom node is inserted for the wrapped three.js objects:
IF you create your own components intended to be rendered inside of the three.js node tree don't forget to insert a <ng-content />
inside of your template.
Then you can see child nodes attached to your component in debug mode!