Skip to content

Commit 5ed6f7f

Browse files
ericleibnartc
andauthored
feat(soba): Angular-based Level of Detail (LOD) implementation (#82)
* Create lod.ts * Add lod to index.ts * Update README.md * Create lod.stories.ts * Fix LOD story * Improved API * Update LOD syntax and add click event to a mesh * Update README.md * Fix build * Update libs/soba/performances/src/lib/lod.ts Co-authored-by: Chau Tran <25516557+nartc@users.noreply.github.com> * Revert to object input for [lodLevel], using mergeInputs for defaults * Update README.md * Update lod.stories.ts * switch directive & component to fix storybook --------- Co-authored-by: Chau Tran <25516557+nartc@users.noreply.github.com>
1 parent 7683d93 commit 5ed6f7f

4 files changed

Lines changed: 213 additions & 0 deletions

File tree

libs/soba/performances/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ npm install three-mesh-bvh
1616
- [NgtsAdaptiveEvents](#ngtsadaptiveevents)
1717
- [NgtsBVH](#ngtsbvh)
1818
- [NgtsDetailed](#ngtsdetailed)
19+
- [NgtsLOD](#ngtslod)
1920
- [NgtsInstances](#ngtsinstances)
2021
- [NgtsSegments](#ngtssegments)
2122
- [NgtsPoints](#ngtspoints)
@@ -94,6 +95,52 @@ Implements Level of Detail (LOD) rendering. Automatically switches between diffe
9495
</ngts-detailed>
9596
```
9697

98+
## NgtsLOD
99+
100+
Implements Level of Detail (LOD) rendering. Automatically switches between different detail levels of child objects based on camera distance.
101+
102+
Unlike `NgtsDetailed`, this is an implementation based on Angular and angular-three APIs rather than Three's LOD class.
103+
The component adds and remove objects from the scene graph rather than hiding them with `visible = false`.
104+
This solves a number of issues such as avoid raycasting over hidden objects.
105+
106+
Usage:
107+
108+
```html
109+
<ngt-group lod [maxDistance]="10000">
110+
<ngt-mesh *lodLevel />
111+
<ngt-mesh *lodLevel="{distance: 100, hysteresis: 0.1}" />
112+
<ngt-mesh *lodLevel="{distance: 1000}" />
113+
</ngt-group>
114+
```
115+
116+
The `[lod]` directive (`NgtsLODImpl`) supports the following optional input:
117+
118+
| Property | Description | Default Value |
119+
| ------------- | ------------------------------------------------------------------------------ | ------------- |
120+
| `maxDistance` | Distance beyond which nothing is displayed (equivalent to a last empty level) | `undefined` |
121+
122+
The `[lodLevel]` directive (`NgtsLODLevel`) supports the following object inputs:
123+
124+
| Property | Description | Default Value |
125+
| ------------ | ----------------------------------------------------- | ------------- |
126+
| `distance` | Distance threshold above which to display the object | `0` |
127+
| `hysteresis` | Prevents rapid switching near distance thresholds | `0` |
128+
129+
This directive may also be used with the following shorthand syntax:
130+
131+
```html
132+
<ngt-group lod>
133+
<ng-template lodLevel>
134+
<ngt-mesh />
135+
<ngt-mesh />
136+
</ng-template>
137+
<ng-template [lodLevel]="{distance: 100}">
138+
<ngt-mesh />
139+
<ngt-mesh />
140+
</ng-template>
141+
</ngt-group>
142+
```
143+
97144
## NgtsInstances
98145

99146
Efficiently renders many instances of the same geometry and material using a single draw call via `THREE.InstancedMesh`.

libs/soba/performances/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './lib/adaptive-dpr';
22
export * from './lib/adaptive-events';
33
export * from './lib/bvh';
44
export * from './lib/detailed';
5+
export * from './lib/lod';
56
export * from './lib/instances/instances';
67
export * from './lib/points/points';
78
export * from './lib/segments/segments';
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Component, contentChildren, Directive, ElementRef, inject, input, signal, TemplateRef } from "@angular/core";
2+
import { NgTemplateOutlet } from "@angular/common";
3+
import { beforeRender, injectStore } from "angular-three";
4+
import { mergeInputs } from 'ngxtension/inject-inputs';
5+
import { Object3D, Vector3 } from "three";
6+
7+
export type NgtsLODLevelOptions = {
8+
distance: number;
9+
hysteresis: number;
10+
}
11+
12+
const defaultLodLevelOptions: NgtsLODLevelOptions = {
13+
distance: 0,
14+
hysteresis: 0,
15+
};
16+
17+
const _v1 = new Vector3();
18+
const _v2 = new Vector3();
19+
20+
/**
21+
* Helper directive to capture a template to attach to
22+
* an NgtsLOD component.
23+
*/
24+
@Directive({
25+
selector: 'ng-template[lodLevel]'
26+
})
27+
export class NgtsLODLevel {
28+
lodLevel = input(defaultLodLevelOptions, { transform: mergeInputs(defaultLodLevelOptions) });
29+
template = inject(TemplateRef);
30+
}
31+
32+
/**
33+
* Angular-native port of THREE.LOD
34+
*
35+
* Allows to display an object with several levels of details.
36+
*
37+
* The main difference with THREE.LOD is that we use angular-three
38+
* to add/remove the right object from the scene graph, rather than
39+
* setting the visible flag on one of the object, but keeping them
40+
* all in the graph.
41+
*
42+
* Usage:
43+
*
44+
* ```html
45+
* <ngt-group lod [maxDistance]="10000">
46+
* <ngt-mesh *lodLevel />
47+
* <ngt-mesh *lodLevel="{distance: 100, hysteresis: 0.1}" />
48+
* <ngt-mesh *lodLevel="{distance: 1000}" />
49+
* </ngt-group>
50+
* ```
51+
*/
52+
@Component({
53+
selector: '[lod]',
54+
template: `
55+
<ng-container [ngTemplateOutlet]="level()?.template" />
56+
`,
57+
imports: [NgTemplateOutlet],
58+
})
59+
export class NgtsLODImpl {
60+
maxDistance = input<number>();
61+
62+
private store = injectStore();
63+
private container = inject(ElementRef);
64+
65+
readonly levels = contentChildren(NgtsLODLevel);
66+
readonly level = signal<NgtsLODLevel|undefined>(undefined);
67+
68+
constructor() {
69+
beforeRender(() => {
70+
71+
const levels = this.levels();
72+
const currentLevel = this.level();
73+
const maxDistance = this.maxDistance();
74+
75+
let level: NgtsLODLevel|undefined = levels[0];
76+
77+
if(level && (levels.length > 1 || maxDistance)) {
78+
79+
const container = this.container.nativeElement as Object3D;
80+
const {matrixWorld, zoom} = this.store.snapshot.camera;
81+
82+
_v1.setFromMatrixPosition( matrixWorld );
83+
_v2.setFromMatrixPosition( container.matrixWorld );
84+
85+
const distance = _v1.distanceTo( _v2 ) / zoom;
86+
87+
if(maxDistance && distance > maxDistance) {
88+
level = undefined;
89+
}
90+
else {
91+
for (let i = 1, l = levels.length; i < l; i ++ ) {
92+
const _level = levels[i];
93+
let {distance: levelDistance, hysteresis} = _level.lodLevel();
94+
95+
if (hysteresis && currentLevel === _level) {
96+
levelDistance -= levelDistance * hysteresis;
97+
}
98+
99+
if (distance >= levelDistance) {
100+
level = _level;
101+
}
102+
else {
103+
break;
104+
}
105+
}
106+
}
107+
}
108+
109+
if(level !== currentLevel) {
110+
this.level.set(level);
111+
}
112+
});
113+
}
114+
}
115+
116+
export const NgtsLOD = [NgtsLODImpl, NgtsLODLevel] as const;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
2+
import { Meta } from '@storybook/angular';
3+
import { NgtArgs } from 'angular-three';
4+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
import { NgtsLODImpl, NgtsLODLevel } from 'angular-three-soba/performances';
6+
import { storyDecorators, storyFunction } from '../setup-canvas';
7+
8+
@Component({
9+
template: `
10+
<ngt-group lod [maxDistance]="200">
11+
<ngt-mesh *lodLevel>
12+
<ngt-icosahedron-geometry *args="[10, 3]" />
13+
<ngt-mesh-basic-material color="hotpink" wireframe />
14+
</ngt-mesh>
15+
16+
<ngt-mesh *lodLevel="{distance: 50}" (click)="toggleColor()">
17+
<ngt-icosahedron-geometry *args="[10, 2]" />
18+
<ngt-mesh-basic-material [color]="color()" wireframe />
19+
</ngt-mesh>
20+
21+
<ngt-mesh *lodLevel="{distance: 150, hysteresis:0.1}">
22+
<ngt-icosahedron-geometry *args="[10, 1]" />
23+
<ngt-mesh-basic-material color="lightblue" wireframe />
24+
</ngt-mesh>
25+
</ngt-group>
26+
27+
<ngts-orbit-controls [options]="{ enablePan: false, enableRotate: false, zoomSpeed: 0.5 }" />
28+
`,
29+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
30+
changeDetection: ChangeDetectionStrategy.OnPush,
31+
imports: [NgtsLODImpl, NgtsLODLevel, NgtArgs, NgtsOrbitControls],
32+
})
33+
class DefaultLODStory {
34+
protected color = signal('#ff0000');
35+
36+
toggleColor() {
37+
this.color.update(c => c === '#ff0000'? '#00ff00' : '#ff0000' );
38+
}
39+
}
40+
41+
export default {
42+
title: 'Performances/LOD',
43+
decorators: storyDecorators(),
44+
} as Meta;
45+
46+
export const Default = storyFunction(DefaultLODStory, {
47+
camera: { position: [0, 0, 100] },
48+
controls: false,
49+
});

0 commit comments

Comments
 (0)