NgtsMask
import { ChangeDetectionStrategy, Component } from '@angular/core';import { SobaWrapper } from '@soba/wrapper.ts';import { NgtCanvas, provideNgtRenderer } from 'angular-three/dom';import { SceneGraph } from './scene-graph';
@Component({ selector: 'app-mask', template: ` <ngt-canvas [camera]="{ position: [0, 0, 5], fov: 50 }"> <app-soba-wrapper *canvasContent [grid]="false"> <app-scene-graph /> </app-soba-wrapper> </ngt-canvas> `, changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'mask-demo relative block h-full' }, imports: [NgtCanvas, SobaWrapper, SceneGraph],})export default class Mask { static clientProviders = [provideNgtRenderer()];}import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Component, ElementRef, viewChild } from '@angular/core';import { NgtArgs, beforeRender } from 'angular-three';import { NgtsMask, mask } from 'angular-three-soba/staging';import * as THREE from 'three';
@Component({ selector: 'app-scene-graph', template: ` <!-- Circular mask --> <ngts-mask [id]="1"> <ngt-circle-geometry *args="[1, 64]" /> </ngts-mask>
<!-- Content visible INSIDE the mask --> <ngt-mesh #maskedMesh> <ngt-torus-knot-geometry *args="[0.8, 0.3, 128, 32]" /> <ngt-mesh-standard-material color="#ff6b6b" [stencilWrite]="true" [stencilRef]="insideMaskProps().stencilRef" [stencilFunc]="insideMaskProps().stencilFunc" /> </ngt-mesh>
<!-- Content visible OUTSIDE the mask (inverted) --> <ngt-mesh [position]="[0, 0, -1]"> <ngt-plane-geometry *args="[10, 10]" /> <ngt-mesh-standard-material color="#4ecdc4" [stencilWrite]="true" [stencilRef]="outsideMaskProps().stencilRef" [stencilFunc]="outsideMaskProps().stencilFunc" /> </ngt-mesh> `, schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgtArgs, NgtsMask],})export class SceneGraph { private maskedMeshRef = viewChild<ElementRef<THREE.Mesh>>('maskedMesh');
// Show content only inside the mask insideMaskProps = mask(() => 1);
// Show content only outside the mask (inverted) outsideMaskProps = mask( () => 1, () => true, );
constructor() { beforeRender(({ delta }) => { const mesh = this.maskedMeshRef()?.nativeElement; if (mesh) { mesh.rotation.x += delta * 0.5; mesh.rotation.y += delta * 0.3; } }); }}NgtsMask and mask() are ports of Drei’s Mask which create stencil masks for selective rendering. Objects can be shown or hidden based on mask boundaries.
Components and Functions
NgtsMask
Creates a stencil mask shape. Content geometry is placed as children.
mask() Function
Creates stencil material properties to apply to materials that should respect mask boundaries.
function mask( id: () => number, inverse: () => boolean = () => false,): Signal<{ stencilWrite: boolean; stencilRef: number; stencilFunc: THREE.StencilFunc; stencilFail: THREE.StencilOp; stencilZFail: THREE.StencilOp; stencilZPass: THREE.StencilOp;}>;Usage
Basic Mask Setup
<!-- Define the mask shape --><ngts-mask [id]="1"> <ngt-circle-geometry *args="[0.5, 64]" /></ngts-mask>
<!-- Object that respects the mask --><ngt-mesh [material]="maskedMaterial"> <ngt-box-geometry /></ngt-mesh>import { mask } from 'angular-three-soba/staging';
@Component({...})export class MaskDemo { // Material that only renders inside the mask maskedMaterial = new THREE.MeshStandardMaterial({ color: 'orange', ...mask(() => 1)() });}Inverted Mask (Render Outside)
// Material that only renders OUTSIDE the maskoutsideMaterial = new THREE.MeshStandardMaterial({ color: 'blue', ...mask( () => 1, () => true, )(),});Multiple Masks with Different IDs
<!-- First mask --><ngts-mask [id]="1" [options]="{ position: [-2, 0, 0] }"> <ngt-circle-geometry *args="[1, 32]" /></ngts-mask>
<!-- Second mask --><ngts-mask [id]="2" [options]="{ position: [2, 0, 0] }"> <ngt-ring-geometry *args="[0.5, 1, 32]" /></ngts-mask>
<!-- Object visible only in first mask --><ngt-mesh [material]="mask1Material" [position]="[-2, 0, 0]"> <ngt-box-geometry /></ngt-mesh>
<!-- Object visible only in second mask --><ngt-mesh [material]="mask2Material" [position]="[2, 0, 0]"> <ngt-sphere-geometry /></ngt-mesh>mask1Material = new THREE.MeshStandardMaterial({ color: 'red', ...mask(() => 1)(),});
mask2Material = new THREE.MeshStandardMaterial({ color: 'green', ...mask(() => 2)(),});Dynamic Mask ID with Signals
maskId = signal(1);
dynamicMaterial = computed( () => new THREE.MeshStandardMaterial({ color: 'purple', ...mask(this.maskId)(), }),);Custom Mask Shape
<ngts-mask [id]="1"> <ngt-shape-geometry *args="[heartShape]" /></ngts-mask>Portal Window Effect
Create a “window” that reveals a different scene:
<!-- The window frame (mask) --><ngts-mask [id]="1" [options]="{ position: [0, 1.5, 0] }"> <ngt-plane-geometry *args="[2, 3]" /></ngts-mask>
<!-- Content visible through the window --><ngt-group> <ngt-mesh [material]="portalMaterial" [position]="[0, 1.5, -2]"> <ngt-sphere-geometry /> </ngt-mesh>
<ngts-stars [material]="portalMaterial" /></ngt-group>Notes
- Masks use WebGL stencil buffer for selective rendering
- Each unique mask ID creates an independent masking region
- The
inverseparameter inmask()flips the visibility behavior - Mask shapes can be any geometry (circle, plane, custom shapes, etc.)
- Multiple objects can share the same mask ID
Inputs
| name | type | description | required |
|---|---|---|---|
| id | number | The stencil reference ID used for masking. Default: 1 | no |
Options
options input accepts any properties from THREE.Mesh in addition to the following:
Properties
| name | type | description |
|---|---|---|
| colorWrite | boolean | Whether to write color to the framebuffer. Default: false |
| depthWrite | boolean | Whether to write depth to the depth buffer. Default: false |