Portals
Portals might have different meaning depending on the use-cases. In general, it means that we want to render something as children of something else without following the hierarchy of the template. Pseudo-code looks something like this:
<ngt-group> <!-- render ngt-mesh here --></ngt-group>
<!-- outside of the hierarchy --><ngt-mesh></ngt-mesh>
NgTemplateOutlet
For many cases, you can use NgTemplateOutlet
if you just want to portal objects around with (or without) different context data. In other words, you can
use this technique to reuse templates.
@Component({ selector: "app-scene-graph", template: ` <ngts-perspective-camera [options]="{ makeDefault: true, position: [10, 10, 10], fov: 30 }" /> <ngts-orbit-controls [options]="{ enableZoom: false, maxPolarAngle: 85 * DEG2RAD, minPolarAngle: 20 * DEG2RAD, maxAzimuthAngle: 45 * DEG2RAD, minAzimuthAngle: -45 * DEG2RAD }" /> <ngts-grid [options]="{ planeArgs: [10.5, 10.5], position: [0, -0.01, 0], cellSize: 0.6, cellThickness: 1, cellColor: '#6f6f6f', sectionSize: 3.3, sectionThickness: 1.5, sectionColor: '#9d4b4b', fadeDistance: 25, fadeStrength: 1, followCamera: false, infiniteGrid: true }" />
<ngt-directional-light [position]="[5, 10, 3]" />
<ngt-object3D #trail [position]="[0, 0.5, 0]"> <ng-container [ngTemplateOutlet]="forTrail" /> </ngt-object3D>
<ng-template #forTrail> <ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ color: '#fe3d00' }" /> <ngt-group [position]="[0, 1, 0]"> <ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ color: '#2f7dc6' }" /> </ngt-group> </ng-template>
<ng-template #mesh let-color="color"> <ngt-mesh> <ngt-box-geometry /> <ngt-mesh-standard-material [color]="color" /> </ngt-mesh> </ng-template> `, schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ NgtsOrbitControls, NgtsPerspectiveCamera, NgtsGrid, NgTemplateOutlet, ],})export class SceneGraph { protected readonly DEG2RAD = THREE.MathUtils.DEG2RAD;
private trailRef = viewChild.required<ElementRef<THREE.Object3D>>("trail");
constructor() { extend(THREE);
injectBeforeRender(() => { const obj = this.trailRef().nativeElement; obj.position.x = Math.sin(Date.now() / 1000) * 4; }); }}
Credits: Threlte’s PortalTarget Demo
What you’re seeing here is:
- An
Object3D
that is being moved back and forth - A
Mesh
as a child of theObject3D
- A
Group
- Another
Mesh
as a child of theGroup
The main takeaway here is that this Mesh
is being reused and has different color based on where it’s rendered.
<ngt-object3D> <ng-container [ngTemplateOutlet]="forObject3D" /></ngt-object3D>
<ng-template #forObject3D> <ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ color: '#fe3d00' }" /> <ngt-group [position]="[0, 1, 0]"> <ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ color: '#2f7dc6' }" /> </ngt-group></ng-template>
<ng-template #mesh let-color="color"> <ngt-mesh> <ngt-box-geometry /> <ngt-mesh-standard-material [color]="color" /> </ngt-mesh></ng-template>
NgtParent
This technique is useful for when you cannot control the template for, well, ng-template.
For example, routed components via router-outlet
NgtParent
is a structural directive and it takes an input parent
. parent
accepts
- A
string
: which will be used to look up the object withgetObjectByName()
- An
Object3D
- An
ElementRef<Object3D>
- or a
Signal
of all of these above
Attaching *parent
on an element will portal that element as a child to the parent
input.
NgtPortal
In THREE.js, there is a construct called WebGLRenderTarget
. It is used to render the scene into a texture and then
render the texture into the canvas. This is useful for things like post-processing effects, or HUD-like visuals.
In Angular Three, you can use NgtPortal
component to create an off-screen buffer that can be used to render secondary scenes.
NgtPortal
provides a layered store that its children can inject. This makes sure that children of NgtPortal
access the state of the NgtPortal
and not the root store.
@Component({ template: ` <ngt-mesh> <ngt-torus-geometry /> </ngt-mesh>
<ngt-portal [container]="secondaryScene"> <ng-template portalContent> <ngts-perspective-camera [options]="{ makeDefault: true }" /> <ngt-mesh> <ngt-box-geometry /> </ngt-mesh> </ng-template> </ngt-portal> `, imports: [NgtPortal, NgtsPerspectiveCamera],})export class HUD { secondaryScene = new Scene();}
The portal can have its own scene, camera, and children.
@Component({ selector: "app-scene-graph", template: ` <ngt-ambient-light [intensity]="0.5 * Math.PI" />
<app-torus [scale]="1.75" /> <app-view-cube />
<ngts-orbit-controls /> <ngts-environment [options]="{ preset: 'city' }" /> `, imports: [NgtsOrbitControls, NgtsEnvironment, Torus, ViewCube], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush,})export class SceneGraph { protected readonly Math = Math;}
@Component({ selector: "app-view-cube", template: ` <ngt-portal [container]="scene()" autoRender> <ng-template portalContent> <ngt-ambient-light [intensity]="Math.PI / 2" /> <ngt-spot-light [position]="[10, 10, 10]" [angle]="0.15" [penumbra]="0" [decay]="0" [intensity]="Math.PI" /> <ngt-point-light [position]="[-10, -10, -10]" [decay]="0" [intensity]="Math.PI" /> <ngts-perspective-camera [options]="{ makeDefault: true, position: [0, 0, 10] }" /> <app-box [position]="boxPosition()" /> <ngt-ambient-light [intensity]="1" /> <ngt-point-light [position]="[200, 200, 100]" [intensity]="0.5" /> </ng-template> </ngt-portal> `, imports: [Box, NgtPortal, NgtsPerspectiveCamera, NgtPortalAutoRender], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush,})export class ViewCube { protected readonly Math = Math;
private box = viewChild(Box);
private store = injectStore();
protected boxPosition = computed(() => [this.store.viewport.width() / 2 - 1, this.store.viewport.height() / 2 - 1, 0]);
protected scene = computed(() => { const scene = new THREE.Scene(); scene.name = "hud-view-cube-virtual-scene"; return scene; });
constructor() { const matrix = new THREE.Matrix4(); injectBeforeRender(() => { const box = this.box()?.mesh().nativeElement; if (box) { matrix.copy(this.store.snapshot.camera.matrix).invert(); box.quaternion.setFromRotationMatrix(matrix); } }); }}
@Component({ selector: "app-box", template: ` <ngt-mesh #mesh [position]="position()" [scale]="scale()" (click)="clicked.set(!clicked())" (pointermove)="$event.stopPropagation(); hovered.set($event.face.materialIndex)" (pointerout)="hovered.set(-1)" > <ngt-box-geometry /> @for (face of faces; track face) { <app-face-material [index]="$index" [text]="face" /> } </ngt-mesh> `, imports: [FaceMaterial], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush,})export class Box { position = input([0, 0, 0]);
mesh = viewChild.required<ElementRef<THREE.Mesh>>("mesh");
protected hovered = signal(-1); isHovered = this.hovered.asReadonly();
protected clicked = signal(false); protected scale = computed(() => (this.clicked() ? 1.5 : 1));
protected faces = ["front", "back", "top", "bottom", "left", "right"];}
@Component({ selector: "app-face-material", template: ` <ngt-mesh-standard-material [attach]="['material', index()]" [color]="color()"> <ngts-render-texture [options]="{ frames: 6, anisotropy: 16 }"> <ng-template renderTextureContent> <ngt-color *args="['white']" attach="background" /> <ngts-orthographic-camera [options]="{ makeDefault: true, left: -1, right: 1, top: 1, bottom: -1, position: [0, 0, 10], zoom: 0.5 }" /> <ngts-text [text]="text()" [options]="{ color: 'black', font: 'https://fonts.gstatic.com/s/raleway/v14/1Ptrg8zYS_SKggPNwK4vaqI.woff' }" /> </ng-template> </ngts-render-texture> </ngt-mesh-standard-material> `, imports: [NgtsText, NgtsRenderTexture, NgtsOrthographicCamera, NgtArgs, NgtsRenderTextureContent], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush,})export class FaceMaterial { index = input.required<number>(); text = input.required<string>();
private box = inject(Box); protected color = computed(() => this.box.isHovered() === this.index() ? "mediumpurple" : "orange");}
@Component({ selector: "app-torus", template: ` <ngt-mesh [scale]="scale()" (pointerover)="hovered.set(true)" (pointerout)="hovered.set(false)"> <ngt-torus-geometry *args="[1, 0.25, 32, 100]" /> <ngt-mesh-standard-material [color]="color()" /> </ngt-mesh> `, imports: [NgtArgs], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush,})export class Torus { scale = input(1);
protected hovered = signal(false); protected color = computed(() => this.hovered() ? "mediumpurple" : "orange");}