Custom Renderer
Throughout this documentation, when we talk about Custom Renderer, we mean the Angular Custom Renderer implementation, not THREE.js Renderer
Custom Element tags
Since NGT is an Angular Custom Renderer, we can control the tags on the template. Thanks to that, we can take a set of Custom Element tags and create corresponding THREE.js entities from those tags.
The convention of these Custom Element tags is: ngt-three-js-class-in-kebab-case
ngt-mesh
->THREE.Mesh
ngt-box-geometry
->THREE.BoxGeometry
ngt-lOD
->THREE.LOD
CUSTOM_ELEMENTS_SCHEMA
At the moment, Angular does not support userland schemas
. Hence, we need to rely on CUSTOM_ELEMENTS_SCHEMA
to compile our
application when using Custom Elements tag
@Component({
template: `...`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class SceneGraph {}
Only the Components that use Custom Element tags in their template needs CUSTOM_ELEMENTS_SCHEMA
. The Angular Compiler will
throw compilation errors if we violate.
Catalogue
In order to map Custom Element tags to THREE.js entities, NGT creates an internal catalogue
to keep a dictionary of THREE.js entities.
This is done so the consumers do not have to include the whole THREE.js namespace in their application all the time.
To add THREE.js entities to this catalogue
, consumers use extend()
function; ideally at the beginning of the SceneGraph
component
import { extend } from 'angular-three';
// call extend here
extend({ Mesh, BoxGeometry });
@Component({
/*...*/
})
export class SceneGraph {} // SceneGraph can be named anything
@Component({
template: '<ngt-canvas [sceneGraph]="SceneGraph" />',
imports: [NgtCanvas],
})
export class SomeFeatureComponent {
readonly SceneGraph = SceneGraph; // import SceneGraph component above
}
- It is ok to call
extend()
multiple times with duplications - It is ok to call
extend(THREE)
once to include the entire THREE.js namespace. However, the bundle will also include the entire THREE.js namespace
THREE.js Inputs
We can pass in any THREE.js entities' properties as Inputs to the Custom Element tags.
<ngt-mesh [position]="[1, 1, 1]" [scale]="1.5" [castShadow]="true">
<ngt-mesh-basic-material color="red" [wireframe]="true" />
</ngt-mesh>
<ngt-ambient-light [intensity]="0.5" />
Due to limitations of Angular schemas, we do not have intellisense support here. The best documentation for these Inputs is THREE.js Documentatation
Short-cuts
set()
All Inputs whose underlying object has a .set()
can accept the same arguments as set()
. For example, THREE.Color#set
can accept
a CSS-like color string. Hence, we can pass color="red"
instead of [color]="color"
(where color = new THREE.Color('red')
in our Component code)
<ngt-mesh-basic-material color="red" />
setScalar()
All Inputs whose underlying object has a .setScalar()
can accept the same arguments as setScalar()
<!-- equivalent to [position]="[10, 10, 10]" -->
<ngt-mesh [position]="10" />
There are other shortcuts like copy()
, fromArray()
etc... The concept is the same but those aren't used as nearly common as set()
and setScalar()
NGT Inputs
In addition to Inputs that are THREE.js entities' properties, there are several Inputs that are unique to NGT Custom Renderer
attach
This Input is used to specify a property on the parent that this object should be attached to. Objects with attach
will be taken off their parent when they're not on the template (eg: under an *ngIf
or some other Structural Directive)
Static value
If the property on the parent is known and static, use attach
as Attribute Binding with string
values.
<ngt-mesh>
<ngt-mesh-basic-material attach="material" />
</ngt-mesh>
This is equivalent to
const mesh = new THREE.Mesh();
const material = new THREE.MeshBasicMaterial();
mesh.material = material;
- All Geometries have
attach="geometry"
by default - All Materials have
attach="material"
by default
<ngt-mesh>
<!-- implicit attach="geometry" -->
<ngt-box-geometry />
<!-- implicit attach="material" -->
<ngt-mesh-basic-material />
</ngt-mesh>
We can also pass a dotted.path
to attach
if the property is nested
<ngt-spot-light [castShadow]="true">
<ngt-vector2 attach="shadow.mapSize" />
</ngt-spot-light>
This is equivalent to
const spotLight = new THREE.SpotLight();
spotLight.castShadow = true;
const vector2 = new THREE.Vector2();
// shortcut is still applied automatically
spotLight.shadow.mapSize.copy(vector2);
Dynamic value
If we need to pass a dynamic value to attach
, use attach
as Property Binding with Array<string | number>
values
<ngt-mesh>
<ngt-box-geometry />
<!-- ngForRepeat is an NGT directive. We'll learn about it in a different section -->
<ngt-mesh-lambert-material *ngFor="let i; repeat 6" [attach]="['material', i]" />
</ngt-mesh>
This is equivalent to:
const mesh = new THREE.Mesh();
const geometry = new THREE.BoxGeometry();
mesh.geometry = geometry;
mesh.material = [];
for (let i = 0; i < 6; i++) {
const material = new THREE.MeshLambertMaterial();
mesh.material[i] = material;
}
NgtAttachFunction
Optionally, we can also pass an NgtAttachFunction
to [attach]
. When this is the case, we are responsible for attaching the child onto the parent
as well as de-attaching (clean-up phase)
// we can import this utility from 'angular-three'
import { createAttachFunction } from 'angular-three':
@Component({
template: `
<ngt-mesh>
<ngt-mesh-basic-material [attach]="attachFn" />
</ngt-mesh>
`,
})
export class SceneGraph {
// "store" is the NgtStore, which has all information about the NgtCanvas
readonly attachFn = createAttachFunction<Mesh, MeshBasicMaterial>(({ parent, child /*, store */ }) => {
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;
};
});
}
priority
See Render Priority
rawValue
See Raw Value
ref
See Ref
Outputs
Object3D Events
All of the following Outputs will trigger Change Detection upon invoked. This is intentional as we usually update Component's state with these Events.
name | description |
---|---|
click | If observed, emits when the object is clicked |
contextmenu | If observed, emits when the object is right-clicked |
dblclick | If observed, emits when the object is double clicked |
pointerup | If observed, emits when the pointer moves up while on the object |
pointerdown | If observed, emits when the pointer moves down while on the object |
pointerover | If observed, emits when the pointer is over the object |
pointerout | If observed, emits when the pointer gets on then out of the object |
pointerenter | If observed, emits when the pointer gets on the object |
pointerleave | If observed, emits when the pointer gets on then out of the object |
pointermove | If observed, emits when the pointer moves while on the object |
pointermissed | If observed, emits when the pointer misses the object |
pointercancel | If observed, emits when the current pointer event gets cancelled |
wheel | If observed, emits when the wheel is acted on when on the object |
The events system in NGT is completely ported from R3F. For more information, please check React Three Fiber Events
beforeRender
To register a callback in the animation loop, we can listen for (beforeRender)
@Component({
template: `<ngt-mesh (beforeRender)="onBeforeRender($any($event))" />`,
})
export class SceneGraph {
onBeforeRender(event: NgtBeforeRenderEvent<Mesh>) {
// call per frame, will not trigger Change Detection
}
}
When the element is destroyed, (beforeRender)
is unregistered automatically.
We use $any($event)
because of Angular limitations on CUSTOM_ELEMENTS_SCHEMA
Render Priority
By default, NGT renders the scene on every frame. If we need to control this process, we can pass priority
as Attribute Binding with number-string values
to any object whose (beforeRender)
is being listened to. When a priority
is set, we are responsible to render our scene now.
@Component({
template: `
<ngt-mesh priority="1" (beforeRender)="onBeforeRender($any($event))" />
<ngt-mesh priority="2" (beforeRender)="onOtherBeforeRender($any($event))" />
`,
})
export class SceneGraph {
onBeforeRender(event: NgtBeforeRenderEvent<Mesh>) {
const { gl, scene, camera } = event.state;
// do something
gl.render(scene, camera);
// do something else
}
onOtherBeforeRender(event: NgtBeforeRenderEvent<Mesh>) {
// this runs after the above beforeRender
}
}
afterAttach
This event emits after the child has been attached to the parent
@Component({
template: `
<ngt-mesh>
<ngt-mesh-basic-material attach="material" (afterAttach)="onAfterAttach($any($event))" />
</ngt-mesh>
`,
})
export class SceneGraph {
onAfterAttach(event: NgtAfterAttach<Mesh, MeshBasicMaterial>) {}
}
afterUpdate
This event emits after an object is updated
@Component({
template: ` <ngt-mesh (afterUpdate)="onAfterUpdate($any($event))" [position]="[0, 1, 2]" /> `,
})
export class SceneGraph {
onAfterUpdate(event: Mesh) {}
}