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, we can use NgTemplateOutlet if we just want to portal objects around with (or without) different context data. In other words, we can
use this technique to reuse templates.
import { NgTemplateOutlet } from '@angular/common';import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, viewChild } from '@angular/core';import { extend, injectBeforeRender, NgtCanvas } from 'angular-three';import { NgtsGrid } from 'angular-three-soba/abstractions';import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';import { NgtsOrbitControls } from 'angular-three-soba/controls';import * as THREE from 'three';import { DEG2RAD } from 'three/src/math/MathUtils.js';
extend(THREE);
@Component({ 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], host: { class: 'template-outlet-experience' },})export class Experience { protected readonly DEG2RAD = DEG2RAD;
private trailRef = viewChild.required<ElementRef<THREE.Object3D>>('trail');
constructor() { injectBeforeRender(() => { const obj = this.trailRef().nativeElement; obj.position.x = Math.sin(Date.now() / 1000) * 4; }); }}
@Component({ template: ` <ngt-canvas [sceneGraph]="scene" /> `, imports: [NgtCanvas], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'template-outlet-docs' },})export default class TemplateOutletScene { scene = Experience;}What we’re seeing here is:
- An
Object3Dthat is being moved back and forth - A
Meshas a child of theObject3D - A
Group - Another
Meshas 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 ngt-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
Signalof 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, we can use NgtPortal component to create an off-screen buffer that can be used to render secondary scenes.
NgtPortal provides a layered NgtSignalStore<NgtState> that its children can inject. This makes sure that children of NgtPortal
access the state of the NgtPortal and not the root NgtSignalStore<NgtState>.
@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, NgtPortalContent],})export class HUD { secondaryScene = new Scene();}The portal can have its own scene, camera, and children.