Skip to content

Commit 30f70ba

Browse files
committed
feat(postprocessing): add outline effect and demo
1 parent e0b575f commit 30f70ba

File tree

8 files changed

+269
-8
lines changed

8 files changed

+269
-8
lines changed

apps/kitchen-sink/src/app/postprocessing/basic/experience.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ import {
66
input,
77
viewChild,
88
} from '@angular/core';
9-
import { NgtArgs, extend, injectBeforeRender } from 'angular-three';
9+
import { NgtArgs, injectBeforeRender } from 'angular-three';
1010
import { NgtpEffectComposer, NgtpGodRays } from 'angular-three-postprocessing';
11-
import * as THREE from 'three';
1211
import { Group, Mesh } from 'three';
1312

14-
extend(THREE);
15-
1613
// NOTE: this is to be used with GodRaysEffect as the effect needs a sun source
1714
@Component({
1815
selector: 'app-sun',
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
2+
import { NgtArgs, NgtHexify, NgtSelect, NgtSelection } from 'angular-three';
3+
import { NgtpEffectComposer, NgtpOutline } from 'angular-three-postprocessing';
4+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
6+
/**
7+
* There are multiple ways to use the Outline effect.
8+
*
9+
* 1. Via NgtSelection and NgtSelect
10+
* This is the recommended way to use the Outline effect.
11+
*
12+
* 1a. We can use NgtSelection as hostDirective (as shown) to enable Selection on the entire scene.
13+
* NgtpOutline will automatically be aware of the NgtSelection context and will use it for the selected objects.
14+
*
15+
* 1b. We can wrap `<ng-container ngtSelection>` around the objects we want to select AS WELL AS the Outline effect.
16+
*
17+
* 2. Via selection input on NgtpOutline
18+
* If we want to control the selection ourselves, we can pass in the selection input an Array of Object3D or ElementRef<Object3D>
19+
* then we control this selection collection based on our own logic.
20+
*
21+
* <ngtp-outline [options]="{ selection: selection(), edgeStrength: 100, pulseSpeed: 0 }" />
22+
*
23+
*/
24+
25+
@Component({
26+
standalone: true,
27+
template: `
28+
<ngt-color attach="background" *args="['black']" />
29+
30+
<ngts-orbit-controls />
31+
32+
<ngt-ambient-light />
33+
<ngt-point-light [position]="[0, -1, -1]" [decay]="0" color="green" />
34+
<ngt-directional-light [position]="[0, 1, 1]" />
35+
36+
<ngt-select [enabled]="hovered()" (pointerenter)="hovered.set(true)" (pointerleave)="hovered.set(false)">
37+
<ngt-mesh>
38+
<ngt-box-geometry />
39+
<ngt-mesh-standard-material color="hotpink" />
40+
</ngt-mesh>
41+
</ngt-select>
42+
43+
<ngtp-effect-composer [options]="{ autoClear: false }">
44+
<ngtp-outline [options]="{ edgeStrength: 100, pulseSpeed: 0 }" />
45+
</ngtp-effect-composer>
46+
`,
47+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
48+
changeDetection: ChangeDetectionStrategy.OnPush,
49+
host: { class: 'postprocessing-sample' },
50+
hostDirectives: [NgtSelection],
51+
imports: [NgtsOrbitControls, NgtSelect, NgtHexify, NgtpEffectComposer, NgtpOutline, NgtArgs],
52+
})
53+
export class Experience {
54+
hovered = signal(false);
55+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { NgtCanvas } from 'angular-three';
3+
import { Experience } from './experience';
4+
5+
@Component({
6+
standalone: true,
7+
template: `
8+
<ngt-canvas [sceneGraph]="sceneGraph" />
9+
`,
10+
changeDetection: ChangeDetectionStrategy.OnPush,
11+
host: { class: 'postprocessing-outline' },
12+
imports: [NgtCanvas],
13+
})
14+
export default class PostprocessingOutline {
15+
sceneGraph = Experience;
16+
}

apps/kitchen-sink/src/app/postprocessing/postprocessing.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ const routes: Routes = [
55
path: 'basic',
66
loadComponent: () => import('./basic/basic'),
77
},
8+
{
9+
path: 'outline',
10+
loadComponent: () => import('./outline/outline'),
11+
},
812
{
913
path: '',
1014
redirectTo: 'basic',

apps/kitchen-sink/src/app/postprocessing/postprocessing.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { ChangeDetectionStrategy, Component } from '@angular/core';
22
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
3+
import { extend } from 'angular-three';
4+
import * as THREE from 'three';
5+
6+
extend(THREE);
37

48
@Component({
59
standalone: true,
@@ -29,5 +33,5 @@ import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
2933
host: { class: 'postprocessing' },
3034
})
3135
export default class Postprocessing {
32-
examples = ['basic'];
36+
examples = ['basic', 'outline'];
3337
}

libs/postprocessing/src/lib/effect-composer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ import {
2626
import { Camera, Group, HalfFloatType, NoToneMapping, Scene, TextureDataType } from 'three';
2727
import { isWebGL2Available } from 'three-stdlib';
2828

29-
extend({ Group });
30-
31-
interface NgtpEffectComposerOptions {
29+
export interface NgtpEffectComposerOptions {
3230
enabled: boolean;
3331
depthBuffer?: boolean;
3432
/** Only used for SSGI currently, leave it disabled for everything else unless it's needed */
@@ -141,6 +139,8 @@ export class NgtpEffectComposer {
141139
});
142140

143141
constructor() {
142+
extend({ Group });
143+
144144
afterNextRender(() => {
145145
this.disableToneMapping();
146146
this.setComposerSize();

libs/postprocessing/src/lib/effects/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './hue-saturation';
1515
export * from './lens-flare';
1616
export * from './lut';
1717
export * from './noise';
18+
export * from './outline';
1819
export * from './pixelation';
1920
export * from './scanline';
2021
export * from './sepia';
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import {
2+
afterNextRender,
3+
ChangeDetectionStrategy,
4+
Component,
5+
computed,
6+
CUSTOM_ELEMENTS_SCHEMA,
7+
ElementRef,
8+
inject,
9+
input,
10+
untracked,
11+
} from '@angular/core';
12+
import { injectStore, NgtArgs, NgtSelection, omit, pick, resolveRef } from 'angular-three';
13+
import { injectAutoEffect } from 'ngxtension/auto-effect';
14+
import { mergeInputs } from 'ngxtension/inject-inputs';
15+
import { OutlineEffect } from 'postprocessing';
16+
import { Object3D } from 'three';
17+
import { NgtpEffectComposer } from '../effect-composer';
18+
19+
export type NgtpOutlineOptions = ConstructorParameters<typeof OutlineEffect>[2] & {
20+
selection?: Array<Object3D | ElementRef<Object3D>>;
21+
selectionLayer: number;
22+
};
23+
24+
const defaultOptions: NgtpOutlineOptions = {
25+
selectionLayer: 10,
26+
};
27+
28+
@Component({
29+
selector: 'ngtp-outline',
30+
standalone: true,
31+
template: `
32+
<ngt-primitive *args="[effect()]" />
33+
`,
34+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
35+
changeDetection: ChangeDetectionStrategy.OnPush,
36+
imports: [NgtArgs],
37+
})
38+
export class NgtpOutline {
39+
options = input(defaultOptions, { transform: mergeInputs(defaultOptions) });
40+
41+
private ngtSelection = inject(NgtSelection, { optional: true });
42+
private effectComposer = inject(NgtpEffectComposer);
43+
private store = injectStore();
44+
private invalidate = this.store.select('invalidate');
45+
46+
private selection = pick(this.options, 'selection');
47+
private selectionLayer = pick(this.options, 'selectionLayer');
48+
49+
private blendFunction = pick(this.options, 'blendFunction');
50+
private patternTexture = pick(this.options, 'patternTexture');
51+
private edgeStrength = pick(this.options, 'edgeStrength');
52+
private pulseSpeed = pick(this.options, 'pulseSpeed');
53+
private visibleEdgeColor = pick(this.options, 'visibleEdgeColor');
54+
private hiddenEdgeColor = pick(this.options, 'hiddenEdgeColor');
55+
private width = pick(this.options, 'width');
56+
private height = pick(this.options, 'height');
57+
private kernelSize = pick(this.options, 'kernelSize');
58+
private blur = pick(this.options, 'blur');
59+
private xRay = pick(this.options, 'xRay');
60+
private restOptions = omit(this.options, [
61+
'blendFunction',
62+
'patternTexture',
63+
'edgeStrength',
64+
'pulseSpeed',
65+
'visibleEdgeColor',
66+
'hiddenEdgeColor',
67+
'width',
68+
'height',
69+
'kernelSize',
70+
'blur',
71+
'xRay',
72+
]);
73+
74+
effect = computed(() => {
75+
const [
76+
scene,
77+
camera,
78+
blendFunction,
79+
patternTexture,
80+
edgeStrength,
81+
pulseSpeed,
82+
visibleEdgeColor,
83+
hiddenEdgeColor,
84+
width,
85+
height,
86+
kernelSize,
87+
blur,
88+
xRay,
89+
restOptions,
90+
] = [
91+
this.effectComposer.scene(),
92+
this.effectComposer.camera(),
93+
this.blendFunction(),
94+
this.patternTexture(),
95+
this.edgeStrength(),
96+
this.pulseSpeed(),
97+
this.visibleEdgeColor(),
98+
this.hiddenEdgeColor(),
99+
this.width(),
100+
this.height(),
101+
this.kernelSize(),
102+
this.blur(),
103+
this.xRay(),
104+
untracked(this.restOptions),
105+
];
106+
107+
return new OutlineEffect(scene, camera, {
108+
blendFunction,
109+
patternTexture,
110+
edgeStrength,
111+
pulseSpeed,
112+
visibleEdgeColor,
113+
hiddenEdgeColor,
114+
width,
115+
height,
116+
kernelSize,
117+
blur,
118+
xRay,
119+
...restOptions,
120+
});
121+
});
122+
123+
constructor() {
124+
const autoEffect = injectAutoEffect();
125+
126+
afterNextRender(() => {
127+
autoEffect(() => {
128+
const effect = this.effect();
129+
return () => effect.dispose();
130+
});
131+
132+
autoEffect(() => {
133+
const [effect, invalidate, selectionLayer] = [this.effect(), this.invalidate(), this.selectionLayer()];
134+
effect.selectionLayer = selectionLayer;
135+
invalidate();
136+
});
137+
138+
autoEffect(() => {
139+
// NOTE: we run this effect if declarative NgtSelection is not enabled
140+
if (!this.ngtSelection) {
141+
// NOTE: if NgtSelection is not used and selection is not provided, we throw
142+
if (this.selection() === undefined) {
143+
throw new Error('[NGT PostProcessing]: ngtp-outline requires selection input or use NgtSelection');
144+
}
145+
146+
return this.handleSelectionChangeEffect(this.selection, this.effect, this.invalidate);
147+
}
148+
149+
// NOTE: we run this effect if declarative NgtSelection is enabled
150+
const selectionEnabled = this.ngtSelection.enabled();
151+
if (!selectionEnabled) return;
152+
return this.handleSelectionChangeEffect(this.ngtSelection.collection, this.effect, this.invalidate);
153+
});
154+
});
155+
}
156+
157+
private handleSelectionChangeEffect(
158+
collection: () => Array<Object3D | ElementRef<Object3D>> | undefined,
159+
_effect: () => OutlineEffect,
160+
_invalidate: () => () => void,
161+
) {
162+
const selection = collection();
163+
if (!selection || selection.length === 0) return;
164+
165+
const [effect, invalidate] = [_effect(), _invalidate()];
166+
167+
const objects: Object3D[] = [];
168+
for (const el of selection) {
169+
const obj = resolveRef(el);
170+
if (!obj) continue;
171+
objects.push(obj);
172+
}
173+
174+
if (objects.length === 0) return;
175+
176+
effect.selection.set(objects);
177+
invalidate();
178+
179+
return () => {
180+
effect.selection.clear();
181+
invalidate();
182+
};
183+
}
184+
}

0 commit comments

Comments
 (0)