@new-immersion/ngx-threejs
v2.1.1
Published
Angular services and components for running one application-wide Three.js runtime with WebGPURenderer, TSL, optional Lenis/GSAP integration, resource loading and the native Three.js Inspector.
Readme
@new-immersion/ngx-threejs
Angular services and components for running one application-wide Three.js runtime with WebGPURenderer, TSL, optional Lenis/GSAP integration, resource loading and the native Three.js Inspector.
Version 2 is intended for new projects. It assumes that an application owns one runtime, which may contain several scenes.
Requirements
- Angular 22
- Three.js 0.185
- RxJS 7.8
- GSAP 3.15
- Lenis 1.3
- detect-gpu 5
GSAP and Lenis are peer dependencies because their public types and optional runtime integrations must resolve in consuming applications.
Installation
npm install @new-immersion/ngx-threejs \
three@^0.185.1 @types/three@^0.185.4 \
rxjs@^7.8 gsap@^3.15 lenis@^1.3 detect-gpu@^5Configure the application
Register the library once in the application providers. Every service is provided in root;
provideNgxThreejs() only supplies configuration and Inspector parameter extensions.
import { ApplicationConfig } from '@angular/core';
import { provideNgxThreejs } from '@new-immersion/ngx-threejs';
export const appConfig: ApplicationConfig = {
providers: [
provideNgxThreejs({
renderLoopDriver: 'auto',
loaders: {
dracoDecoderPath: '/draco/',
},
debug: {
enable: false,
localStoragePrefix: 'my-app',
},
}),
],
};Calling provideNgxThreejs() is optional when the defaults are sufficient.
Create and mount the runtime
Call preload() in the component constructor to start resource and GPU work before its view and
canvas are initialized. Its first call stores the options; subsequent calls, including the internal
call made by mount(), reuse the same promise and must omit options. Guard the constructor call in
applications that use SSR because Three.js asset loaders require browser APIs.
import { isPlatformBrowser } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ElementRef,
inject,
PLATFORM_ID,
viewChild,
} from '@angular/core';
import {
POST_PROCESSING_DEFAULTS,
ThreeRuntime,
WorldContext,
} from '@new-immersion/ngx-threejs';
import * as THREE from 'three';
@Component({
selector: 'app-three-scene',
template: '<canvas #canvas></canvas>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ThreeSceneComponent {
private readonly runtime = inject(ThreeRuntime);
private readonly world = inject(WorldContext);
private readonly platformId = inject(PLATFORM_ID);
private readonly canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
constructor() {
if (!isPlatformBrowser(this.platformId)) return;
void this.runtime.preload({
sources: [
{ name: 'noise', type: 'texture', path: '/textures/noise.webp', weight: 1 },
{ name: 'scene', type: 'gltfModel', path: '/models/scene.glb', weight: 4 },
],
detectGPU: true,
});
}
async ngAfterViewInit(): Promise<void> {
if (!isPlatformBrowser(this.platformId)) return;
await this.runtime.mount({
canvas: this.canvas().nativeElement,
camera: { fov: 45, near: 0.1, far: 100, position: { z: 5 } },
postProcessing: {
...POST_PROCESSING_DEFAULTS,
enable: true,
},
antialias: true,
});
this.world.scene.add(new THREE.AmbientLight(0xffffff, 1));
}
}ready$ emits after the camera, renderer, scroll integration and render loop are ready.
Mount options
| Option | Purpose |
| --- | --- |
| canvas | Required canvas owned by the runtime. |
| canvasColor | Clear color. Omit it to keep the canvas transparent. |
| camera | Initial perspective camera values. |
| postProcessing | Initial post-processing state, independent of debug mode. |
| lenis | Enables Lenis and optionally ScrollTrigger integration. |
| antialias | Enables renderer antialiasing. |
| forceWebGL | Forces WebGL through WebGPURenderer. |
| render | Replaces the default main-scene render callback. |
| alertLowFps | Enables the sustained low-FPS warning. |
| fpsAlertComponentOpts | Styles or replaces the built-in FPS warning. |
Scenes, camera and renderer
WorldContext owns the main scene and any additional named scenes:
const overlayScene = world.addScene('overlay');
world.destroyScene('overlay');The runtime renders world.scene with CameraService.camera by default. Use a custom mount render
callback for several scenes, render targets or post-processing:
await runtime.mount({
canvas,
render: () => {
renderer.renderer.render(world.scenes['background'], camera.camera);
renderer.renderer.render(world.scene, camera.camera);
},
});Useful camera APIs include:
enableOrbitControls()anddisableOrbitControls();isInView()for frustum checks;syncCameraToScroll()anddesyncCameraFromScroll();computeElementWorldRect()for DOM-to-world projection.
Render loop, Timer and GSAP
Subscribe to RenderLoop.frame$ for per-frame updates. It emits immediately before rendering.
renderLoop.frame$.subscribe(({ elapsed, delta, frameId, timestamp }) => {
mesh.rotation.y = elapsed * 0.25;
});elapsedanddeltaare expressed in seconds.timestampis the raw animation timestamp in milliseconds.- Three.js
Timeris connected todocument, preventing a large delta after returning to a hidden tab. frameIdincreases once per rendered frame.
The renderLoopDriver configuration accepts:
auto: uses GSAP whenmount({ lenis: { useScrollTrigger: true } })is enabled; otherwise it usesWebGPURenderer.setAnimationLoop();renderer: uses the renderer loop and supports WebXR;gsap: synchronizes Three.js rendering, GSAP and Lenis on the GSAP ticker.
Choose gsap explicitly when the application uses GSAP animations without the library's
ScrollTrigger integration.
Resources and loading progress
Supported resource types are texture, cubeTexture and gltfModel. Resources are cached by
name and can be retrieved with a type-safe expected type:
const texture = resources.getValue('noise', 'texture');
const gltf = resources.getValue('scene', 'gltfModel');weight represents the relative cost of a source when byte progress is unavailable. For example,
a model with weight: 4 contributes four times more to the overall progress than a texture with
weight: 1.
resources.progress$.subscribe(({ status, ratio, sourceName }) => {
const percentage = Math.round(ratio * 100);
});Loading batches are atomic: a failed batch does not add partial results to the resource cache.
Configure loaders.dracoDecoderPath with provideNgxThreejs(), or set it to null to disable DRACO.
Native scrolling, Lenis and ScrollTrigger
Omit lenis from mount() to use native scrolling. In both modes, ScrollService exposes:
scroll$for position, velocity and direction;input$for raw wheel and touch input;scrollTo()for programmatic scrolling;refreshed$after a completed ScrollTrigger refresh.
To enable Lenis, load the supplied global stylesheet in the application's angular.json:
{
"styles": [
"node_modules/@new-immersion/ngx-threejs/styles/_ngx-threejs-with-lenis.scss",
"src/styles.scss"
]
}Create the required layout using the exported IDs:
<div [id]="SCROLL_EVENTS_TARGET">
<div [id]="SCROLL_WRAPPER">
<main [id]="SCROLL_CONTENT">
<router-outlet />
</main>
</div>
</div>readonly SCROLL_EVENTS_TARGET = SCROLL_EVENTS_TARGET;
readonly SCROLL_WRAPPER = SCROLL_WRAPPER;
readonly SCROLL_CONTENT = SCROLL_CONTENT;Then mount with Lenis:
await runtime.mount({
canvas,
lenis: {
options: { lerp: 0.1 },
useScrollTrigger: true,
},
});When ScrollTrigger is enabled, the Lenis wrapper becomes its default scroller. Individual triggers
do not need to repeat the scroller option. ScrollControllerService adds scroll locks, anchor
handling and user-scroll direction detection.
Pointer input and raycasting
PointerService tracks positions relative to the runtime canvas in screen and normalized device
coordinates. All supported pointer events are enabled initially; disable unused events when a
project does not need them. Events can be enabled or disabled independently:
pointer.togglePointerEvent('pointermove', true);
pointer.togglePointerEvent('pointerdown', true);
pointer.togglePointerEvent('pointercancel', false);
pointer.getEvent$('pointerdown').subscribe(() => {
const current = pointer.pointers[0];
if (!current) return;
const hits = pointer.intersectObjects(current, selectableObjects);
});getRay()creates a world-space ray without usingRaycaster.getWorldPosition()projects onto a world XY plane at a selected Z value.intersectPlane()works with anyTHREE.Plane.intersectObjects()uses the reusable raycaster exposed bypointer.raycaster.
Pass reusable targets to the intersection methods when avoiding per-call allocations matters.
TSL DOM images
ThreejsImageComponent synchronizes an HTML image and a Three.js plane. The application owns the
TSL material:
import {
ThreejsImageComponent,
ThreejsImageMaterialFactory,
} from '@new-immersion/ngx-threejs';
import { MeshBasicNodeMaterial } from 'three/webgpu';
readonly materialFactory: ThreejsImageMaterialFactory = ({ texture }) => {
const material = new MeshBasicNodeMaterial();
material.colorNode = texture;
return material;
};<ngx-threejs-image
resourceName="hero"
imageAlt="Product preview"
[materialFactory]="materialFactory"
/>Use syncMode="fast" for stable layouts and syncMode="accurate" for sticky, pinned or otherwise
dynamic layouts. The created output exposes the mesh and generated image element.
Three.js Inspector
Debug mode uses the Inspector distributed with Three.js. Tweakpane is not required.
provideNgxThreejs({
debug: {
enable: true,
localStoragePrefix: 'my-project',
parameterServices: [
CameraParametersService,
CanvasParametersService,
PostProcessingParametersService,
ToneMappingParametersService,
SceneParametersService,
],
},
});CameraParametersService, CanvasParametersService, PostProcessingParametersService and
ToneMappingParametersService are optional built-in groups. Import and register only the ones the
application needs. The post-processing group enables a RenderPipeline with an optional Bloom
effect, Pixelation, chromatic aberration and FXAA. Each effect can be toggled live. Pixelation is
always composed first. Intermediate effects expose an Order priority in their folder and are
composed from the lowest value to the highest; equal values keep their declaration order. FXAA is
always last because it requires color-transformed sRGB input. When the global group is disabled,
rendering falls back to the main scene directly.
The Bloom mask toggle controls the default bloomMask MRT output (0 or 1). Set it
to false for selective Bloom, then opt individual node materials in from the application:
postProcessing.setBloomMask(material);The service removes the material MRT automatically whenever Bloom or the complete post-processing
pipeline is disabled, and restores it when Bloom is enabled again. Call clearBloomMask(material)
when the material should no longer participate.
Application parameter groups extend BaseInspectorParameters:
import { Injectable } from '@angular/core';
import { BaseInspectorParameters } from '@new-immersion/ngx-threejs';
@Injectable()
export class SceneParametersService extends BaseInspectorParameters<
typeof SceneParametersService.DEFAULTS
> {
static readonly DEFAULTS = { speed: 1, bloom: 0.25 };
constructor() {
super(SceneParametersService.DEFAULTS, 'Scene', 'scene');
}
protected createParameters(): void {
this.mainGroup.add(this.values, 'speed', 0, 2, 0.01).name('Speed');
const effects = this.createFolder('effects', 'Effects');
effects.add(this.values, 'bloom', 0, 1, 0.01).name('Bloom');
}
}Built-in groups can be extended without copying their implementation. Subclass the service, call
its implementation first, then add controls to mainGroup or retrieve one of its folders with
getGroup():
import { Injectable, inject } from '@angular/core';
import { CameraParametersService, CameraService } from '@new-immersion/ngx-threejs';
@Injectable()
export class ProjectCameraParametersService extends CameraParametersService {
private readonly camera = inject(CameraService);
private readonly projectValues = { fieldOfView: 50 };
protected override createParameters(): void {
super.createParameters();
this.getGroup('transform')
.add(this.projectValues, 'fieldOfView', 20, 100, 1)
.name('Field of view')
.onChange((value) => {
this.camera.camera.fov = value;
this.camera.camera.updateProjectionMatrix();
});
}
}Register the subclass instead of CameraParametersService. Camera folder IDs are
orbitControls, transform, transform.position and transform.rotation. The canvas and tone
mapping services currently expose only their main group. The post-processing service exposes the
pixelation, bloom, chromaticAberration, chromaticAberration.center and fxaa folders.
Values owned by a BaseInspectorParameters service, folder states and the main Parameters panel
state are persisted in localStorage. Extra controls that bind another service keep that service as
their source of truth. The Inspector keeps native scrolling when Lenis is active.
TransformControlsParametersService can also be registered in parameterServices and populated
with addAvailableTarget().
Low-FPS warning
The warning is enabled by default and can be disabled with alertLowFps: false. It appears after
sustained performance around 30 FPS or below. It ignores hidden tabs, initial startup and isolated
long frames. The message presents energy-saving mode as a possible cause; the browser does not
expose a reliable way to detect that mode directly.
await runtime.mount({
canvas,
alertLowFps: true,
fpsAlertComponentOpts: {
cssBgColor: '#ffffff',
cssColor: '#18181b',
cssMoreBtnColor: '#2563eb',
},
});Use customComponent to replace the default design or localize its content. The custom component
must accept text, textMore and componentRef inputs.
Device capability
Enable GPU detection during preload, then inject DetectGpuService to read gpuTier or the
simplified devicePower value (low, medium or high). Detection failures resolve to null
instead of failing runtime initialization.
Public services
| Service | Responsibility |
| --- | --- |
| ThreeRuntime | Preload and mount orchestration. |
| WorldContext | Main scene, named scenes and disposal helpers. |
| RendererService | WebGPU/WebGL renderer access and rendering. |
| CameraService | Camera, controls, frustum and DOM projection. |
| RenderLoop | Shared Three.js, GSAP and Lenis clock. |
| Viewport | Stabilized canvas size, resolution and resize notifications. |
| ResourcesService | Resource batches, cache and weighted progress. |
| ScrollService | Native or Lenis scrolling and ScrollTrigger integration. |
| ScrollControllerService | Locks, anchors and high-level scroll flows. |
| PointerService | Canvas pointer state, rays and object intersections. |
| DebugService | Native Three.js Inspector lifecycle. |
| PostProcessingService | Main scene render pipeline and post-processing effects. |
| Built-in parameter services | Optional Camera, Canvas, Post-processing and Tone-mapping Inspector groups. |
| DetectGpuService | Optional GPU-tier detection. |
Build the library
From the workspace root:
npm run build -- ngx-threejsThe publishable package is generated in dist/ngx-threejs.
