Skip to content
47 changes: 47 additions & 0 deletions libs/soba/performances/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ npm install three-mesh-bvh
- [NgtsAdaptiveEvents](#ngtsadaptiveevents)
- [NgtsBVH](#ngtsbvh)
- [NgtsDetailed](#ngtsdetailed)
- [NgtsLOD](#ngtslod)
- [NgtsInstances](#ngtsinstances)
- [NgtsSegments](#ngtssegments)
- [NgtsPoints](#ngtspoints)
Expand Down Expand Up @@ -94,6 +95,52 @@ Implements Level of Detail (LOD) rendering. Automatically switches between diffe
</ngts-detailed>
```

## NgtsLOD

Implements Level of Detail (LOD) rendering. Automatically switches between different detail levels of child objects based on camera distance.

Unlike `NgtsDetailed`, this is an implementation based on Angular and angular-three APIs rather than Three's LOD class.
The component adds and remove objects from the scene graph rather than hiding them with `visible = false`.
This solves a number of issues such as avoid raycasting over hidden objects.

Usage:

```html
<ngt-group lod [maxDistance]="10000">
<ngt-mesh *lodLevel />
<ngt-mesh *lodLevel="{distance: 100, hysteresis: 0.1}" />
<ngt-mesh *lodLevel="{distance: 1000}" />
</ngt-group>
```

The `[lod]` directive (`NgtsLODImpl`) supports the following optional input:

| Property | Description | Default Value |
| ------------- | ------------------------------------------------------------------------------ | ------------- |
| `maxDistance` | Distance beyond which nothing is displayed (equivalent to a last empty level) | `undefined` |

The `[lodLevel]` directive (`NgtsLODLevel`) supports the following object inputs:

| Property | Description | Default Value |
| ------------ | ----------------------------------------------------- | ------------- |
| `distance` | Distance threshold above which to display the object | `0` |
| `hysteresis` | Prevents rapid switching near distance thresholds | `0` |

This directive may also be used with the following shorthand syntax:

```html
<ngt-group lod>
<ng-template lodLevel>
<ngt-mesh />
<ngt-mesh />
</ng-template>
<ng-template [lodLevel]="{distance: 100}">
<ngt-mesh />
<ngt-mesh />
</ng-template>
</ngt-group>
```

## NgtsInstances

Efficiently renders many instances of the same geometry and material using a single draw call via `THREE.InstancedMesh`.
Expand Down
1 change: 1 addition & 0 deletions libs/soba/performances/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './lib/adaptive-dpr';
export * from './lib/adaptive-events';
export * from './lib/bvh';
export * from './lib/detailed';
export * from './lib/lod';
export * from './lib/instances/instances';
export * from './lib/points/points';
export * from './lib/segments/segments';
116 changes: 116 additions & 0 deletions libs/soba/performances/src/lib/lod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Component, contentChildren, Directive, ElementRef, inject, input, signal, TemplateRef } from "@angular/core";
import { NgTemplateOutlet } from "@angular/common";
import { beforeRender, injectStore } from "angular-three";
import { mergeInputs } from 'ngxtension/inject-inputs';
import { Object3D, Vector3 } from "three";

export type NgtsLODLevelOptions = {
distance: number;
hysteresis: number;
}

const defaultLodLevelOptions: NgtsLODLevelOptions = {
distance: 0,
hysteresis: 0,
};

const _v1 = new Vector3();
const _v2 = new Vector3();

/**
* Helper directive to capture a template to attach to
* an NgtsLOD component.
*/
@Directive({
selector: 'ng-template[lodLevel]'
})
export class NgtsLODLevel {
lodLevel = input(defaultLodLevelOptions, { transform: mergeInputs(defaultLodLevelOptions) });
template = inject(TemplateRef);
}

/**
* Angular-native port of THREE.LOD
*
* Allows to display an object with several levels of details.
*
* The main difference with THREE.LOD is that we use angular-three
* to add/remove the right object from the scene graph, rather than
* setting the visible flag on one of the object, but keeping them
* all in the graph.
*
* Usage:
*
* ```html
* <ngt-group lod [maxDistance]="10000">
* <ngt-mesh *lodLevel />
* <ngt-mesh *lodLevel="{distance: 100, hysteresis: 0.1}" />
* <ngt-mesh *lodLevel="{distance: 1000}" />
* </ngt-group>
* ```
*/
@Component({
selector: '[lod]',
template: `
<ng-container [ngTemplateOutlet]="level()?.template" />
`,
imports: [NgTemplateOutlet],
})
export class NgtsLODImpl {
maxDistance = input<number>();

private store = injectStore();
private container = inject(ElementRef);

readonly levels = contentChildren(NgtsLODLevel);
readonly level = signal<NgtsLODLevel|undefined>(undefined);

constructor() {
beforeRender(() => {

const levels = this.levels();
const currentLevel = this.level();
const maxDistance = this.maxDistance();

let level: NgtsLODLevel|undefined = levels[0];

if(level && (levels.length > 1 || maxDistance)) {

const container = this.container.nativeElement as Object3D;
const {matrixWorld, zoom} = this.store.snapshot.camera;

_v1.setFromMatrixPosition( matrixWorld );
_v2.setFromMatrixPosition( container.matrixWorld );

const distance = _v1.distanceTo( _v2 ) / zoom;

if(maxDistance && distance > maxDistance) {
level = undefined;
}
else {
for (let i = 1, l = levels.length; i < l; i ++ ) {
const _level = levels[i];
let {distance: levelDistance, hysteresis} = _level.lodLevel();

if (hysteresis && currentLevel === _level) {
levelDistance -= levelDistance * hysteresis;
}

if (distance >= levelDistance) {
level = _level;
}
else {
break;
}
}
}
}

if(level !== currentLevel) {
this.level.set(level);
}
});
}
}

export const NgtsLOD = [NgtsLODImpl, NgtsLODLevel] as const;
49 changes: 49 additions & 0 deletions libs/soba/src/performances/lod.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
import { Meta } from '@storybook/angular';
import { NgtArgs } from 'angular-three';
import { NgtsOrbitControls } from 'angular-three-soba/controls';
import { NgtsLODImpl, NgtsLODLevel } from 'angular-three-soba/performances';
import { storyDecorators, storyFunction } from '../setup-canvas';

@Component({
template: `
<ngt-group lod [maxDistance]="200">
<ngt-mesh *lodLevel>
<ngt-icosahedron-geometry *args="[10, 3]" />
<ngt-mesh-basic-material color="hotpink" wireframe />
</ngt-mesh>

<ngt-mesh *lodLevel="{distance: 50}" (click)="toggleColor()">
<ngt-icosahedron-geometry *args="[10, 2]" />
<ngt-mesh-basic-material [color]="color()" wireframe />
</ngt-mesh>

<ngt-mesh *lodLevel="{distance: 150, hysteresis:0.1}">
<ngt-icosahedron-geometry *args="[10, 1]" />
<ngt-mesh-basic-material color="lightblue" wireframe />
</ngt-mesh>
</ngt-group>

<ngts-orbit-controls [options]="{ enablePan: false, enableRotate: false, zoomSpeed: 0.5 }" />
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgtsLODImpl, NgtsLODLevel, NgtArgs, NgtsOrbitControls],
})
class DefaultLODStory {
protected color = signal('#ff0000');

toggleColor() {
this.color.update(c => c === '#ff0000'? '#00ff00' : '#ff0000' );
}
}

export default {
title: 'Performances/LOD',
decorators: storyDecorators(),
} as Meta;

export const Default = storyFunction(DefaultLODStory, {
camera: { position: [0, 0, 100] },
controls: false,
});