Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7c5e03e

Browse files
committed
feat: add effect composer
1 parent 90aea1e commit 7c5e03e

File tree

6 files changed

+440
-31
lines changed

6 files changed

+440
-31
lines changed

.prettierrc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
2-
"singleQuote": true,
3-
"tabWidth": 4,
4-
"printWidth": 120,
5-
"htmlWhitespaceSensitivity": "ignore"
2+
"singleQuote": true,
3+
"tabWidth": 4,
4+
"printWidth": 120,
5+
"htmlWhitespaceSensitivity": "ignore",
6+
"plugins": ["prettier-plugin-organize-imports"]
67
}

libs/angular-three-postprocessing/.eslintrc.json

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,16 @@
55
{
66
"files": ["*.ts"],
77
"rules": {
8-
"@angular-eslint/directive-selector": [
9-
"error",
10-
{
11-
"type": "attribute",
12-
"prefix": "ngtp",
13-
"style": "camelCase"
14-
}
15-
],
16-
"@angular-eslint/component-selector": [
17-
"error",
18-
{
19-
"type": "element",
20-
"prefix": "ngtp",
21-
"style": "kebab-case"
22-
}
23-
]
8+
"@angular-eslint/directive-selector": "off",
9+
"@angular-eslint/directive-class-suffix": "off",
10+
"@angular-eslint/component-selector": "off",
11+
"@angular-eslint/component-class-suffix": "off",
12+
"@typescript-eslint/no-empty-function": "off",
13+
"@typescript-eslint/ban-ts-comment": "off",
14+
"@typescript-eslint/no-explicit-any": "off",
15+
"@typescript-eslint/ban-types": "off",
16+
"@typescript-eslint/member-ordering": "off",
17+
"@typescript-eslint/no-non-null-assertion": "off"
2418
},
2519
"extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"]
2620
},
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { Component, inject, Input, OnInit } from '@angular/core';
2+
import { RxActionFactory } from '@rx-angular/state/actions';
3+
import { extend, getLocalState, injectNgtRef, NgtRxStore, NgtStore, startWithUndefined } from 'angular-three';
4+
import { DepthDownsamplingPass, EffectComposer, EffectPass, NormalPass, RenderPass } from 'postprocessing';
5+
import { combineLatest, map } from 'rxjs';
6+
import * as THREE from 'three';
7+
import { Group } from 'three';
8+
import { isWebGL2Available } from 'three-stdlib';
9+
10+
extend({ Group });
11+
12+
@Component({
13+
selector: 'ngtp-effect-composer',
14+
standalone: true,
15+
template: `
16+
<ngt-group [ref]="composerRef">
17+
<ng-content />
18+
</ngt-group>
19+
`,
20+
providers: [RxActionFactory],
21+
})
22+
export class NgtpEffectComposer extends NgtRxStore implements OnInit {
23+
@Input() composerRef = injectNgtRef<Group>();
24+
25+
@Input() set enabled(enabled: boolean) {
26+
this.set({ enabled });
27+
}
28+
29+
@Input() set depthBuffer(depthBuffer: boolean) {
30+
this.set({ depthBuffer });
31+
}
32+
33+
@Input() set disableNormalPass(disableNormalPass: boolean) {
34+
this.set({ disableNormalPass });
35+
}
36+
37+
@Input() set stencilBuffer(stencilBuffer: boolean) {
38+
this.set({ stencilBuffer });
39+
}
40+
41+
@Input() set autoClear(autoClear: boolean) {
42+
this.set({ autoClear });
43+
}
44+
45+
@Input() set resolutionScale(resolutionScale: number) {
46+
this.set({ resolutionScale });
47+
}
48+
49+
@Input() set multisampling(multisampling: number) {
50+
this.set({ multisampling });
51+
}
52+
53+
@Input() set frameBufferType(frameBufferType: THREE.TextureDataType) {
54+
this.set({ frameBufferType });
55+
}
56+
57+
@Input() set renderPriority(renderPriority: number) {
58+
this.set({ renderPriority });
59+
}
60+
61+
@Input() set camera(camera: THREE.Camera) {
62+
this.set({ camera });
63+
}
64+
65+
@Input() set scene(scene: THREE.Scene) {
66+
this.set({ scene });
67+
}
68+
69+
override initialize(): void {
70+
super.initialize();
71+
this.set({
72+
enabled: true,
73+
renderPriority: 1,
74+
autoClear: true,
75+
multisampling: 0,
76+
frameBufferType: THREE.HalfFloatType,
77+
});
78+
}
79+
80+
private readonly store = inject(NgtStore);
81+
private readonly actions = inject(RxActionFactory<{ setBeforeRender: void }>).create();
82+
83+
ngOnInit() {
84+
this.connect(
85+
'activeScene',
86+
combineLatest([this.store.select('scene'), this.select('scene').pipe(startWithUndefined())]).pipe(
87+
map(([defaultScene, scene]) => scene || defaultScene)
88+
)
89+
);
90+
91+
this.connect(
92+
'activeCamera',
93+
combineLatest([this.store.select('camera'), this.select('camera').pipe(startWithUndefined())]).pipe(
94+
map(([defaultCamera, camera]) => camera || defaultCamera)
95+
)
96+
);
97+
98+
this.connect(
99+
'entities',
100+
combineLatest([
101+
this.store.select('gl'),
102+
this.select('activeScene'),
103+
this.select('activeCamera'),
104+
this.select('multisampling'),
105+
this.select('frameBufferType'),
106+
this.select('depthBuffer').pipe(startWithUndefined()),
107+
this.select('stencilBuffer').pipe(startWithUndefined()),
108+
this.select('disableNormalPass').pipe(startWithUndefined()),
109+
this.select('resolutionScale').pipe(startWithUndefined()),
110+
]).pipe(
111+
map(
112+
([
113+
gl,
114+
scene,
115+
camera,
116+
multisampling,
117+
frameBufferType,
118+
depthBuffer,
119+
stencilBuffer,
120+
disableNormalPass,
121+
resolutionScale,
122+
]) => {
123+
const webGL2Available = isWebGL2Available();
124+
// Initialize composer
125+
const effectComposer = new EffectComposer(gl, {
126+
depthBuffer,
127+
stencilBuffer,
128+
multisampling: multisampling > 0 && webGL2Available ? multisampling : 0,
129+
frameBufferType,
130+
});
131+
132+
// Add render pass
133+
effectComposer.addPass(new RenderPass(scene, camera));
134+
135+
// Create normal pass
136+
let downSamplingPass = null;
137+
let normalPass = null;
138+
if (!disableNormalPass) {
139+
normalPass = new NormalPass(scene, camera);
140+
normalPass.enabled = false;
141+
effectComposer.addPass(normalPass);
142+
if (resolutionScale !== undefined && webGL2Available) {
143+
downSamplingPass = new DepthDownsamplingPass({
144+
normalBuffer: normalPass.texture,
145+
resolutionScale,
146+
});
147+
downSamplingPass.enabled = false;
148+
effectComposer.addPass(downSamplingPass);
149+
}
150+
}
151+
152+
return [effectComposer, normalPass, downSamplingPass];
153+
}
154+
)
155+
)
156+
);
157+
158+
this.setComposerSize();
159+
this.setEffectPassed();
160+
this.setBeforeRender();
161+
}
162+
163+
private setComposerSize() {
164+
this.hold(
165+
combineLatest([this.select('entities'), this.store.select('size')]),
166+
([[composer], size]) => void (composer as EffectComposer).setSize(size.width, size.height)
167+
);
168+
}
169+
170+
private setEffectPassed() {
171+
this.effect(
172+
combineLatest([
173+
this.select('entities'),
174+
this.select('activeCamera'),
175+
this.composerRef.children$('nonObjects'),
176+
]),
177+
([[composer, normalPass, downSamplingPass], camera, effects]) => {
178+
let effectPass: EffectPass;
179+
if (
180+
this.composerRef.nativeElement &&
181+
Object.keys(getLocalState(this.composerRef.nativeElement)).length &&
182+
composer
183+
) {
184+
effectPass = new EffectPass(camera, ...effects);
185+
effectPass.renderToScreen = true;
186+
if (normalPass) normalPass.enabled = true;
187+
if (downSamplingPass) downSamplingPass.enabled = true;
188+
}
189+
190+
return () => {
191+
if (effectPass) composer?.removePass(effectPass);
192+
if (normalPass) normalPass.enabled = false;
193+
if (downSamplingPass) downSamplingPass.enabled = false;
194+
};
195+
}
196+
);
197+
}
198+
199+
private setBeforeRender() {
200+
const renderPriority = this.get('renderPriority');
201+
const enabled = this.get('enabled');
202+
this.effect(this.actions.setBeforeRender$, () =>
203+
this.store.get('internal').subscribe(
204+
({ delta }) => {
205+
const [composer] = this.get('entities') || [];
206+
const enabled = this.get('enabled');
207+
const autoClear = this.get('autoClear');
208+
const gl = this.store.get('gl');
209+
if (composer && enabled) {
210+
gl.autoClear = autoClear;
211+
composer.render(delta);
212+
}
213+
},
214+
enabled ? renderPriority : 0
215+
)
216+
);
217+
this.actions.setBeforeRender();
218+
}
219+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Directive, inject, Input, OnChanges, OnInit, reflectComponentType, SimpleChanges, Type } from '@angular/core';
2+
import { NgtAnyRecord, NgtRxStore, NgtStore, startWithUndefined } from 'angular-three';
3+
import { BlendFunction, Effect } from 'postprocessing';
4+
import { combineLatest, Observable } from 'rxjs';
5+
6+
@Directive()
7+
export abstract class NgtpEffect<T extends Effect> extends NgtRxStore implements OnInit, OnChanges {
8+
@Input() set blendFunction(blendFunction: BlendFunction) {
9+
this.set({ blendFunction });
10+
}
11+
12+
@Input() set opacity(opacity: number) {
13+
this.set({ opacity });
14+
}
15+
16+
abstract get effectConstructor(): new (...args: any[]) => T;
17+
18+
protected defaultBlendMode = BlendFunction.NORMAL;
19+
protected readonly store = inject(NgtStore);
20+
21+
ngOnChanges(changes: SimpleChanges) {
22+
if (changes['opacity']) delete changes['opacity'];
23+
if (changes['blendFunction']) delete changes['blendFunction'];
24+
25+
this.set((s) => ({
26+
...s,
27+
...Object.entries(changes).reduce((props, [key, change]) => {
28+
props[key] = change.currentValue;
29+
return props;
30+
}, {} as NgtAnyRecord),
31+
}));
32+
}
33+
34+
ngOnInit() {
35+
const inputs = reflectComponentType(this.constructor as Type<any>)?.inputs.map((input) => input.propName) || [];
36+
this.connect(
37+
'effect',
38+
combineLatest(
39+
inputs.reduce((combined, input) => {
40+
let input$ = this.select(input);
41+
if (this.get(input) !== undefined) {
42+
input$ = input$.pipe(startWithUndefined());
43+
}
44+
combined[input] = input$;
45+
return combined;
46+
}, {} as Record<string, Observable<any>>)
47+
),
48+
(props) => {
49+
delete props['__ngt_dummy__'];
50+
delete props['effect'];
51+
return new this.effectConstructor(props);
52+
}
53+
);
54+
this.configureBlendMode();
55+
}
56+
57+
private configureBlendMode() {
58+
this.hold(
59+
combineLatest([
60+
this.select('effect'),
61+
this.select('blendFunction').pipe(startWithUndefined()),
62+
this.select('opacity').pipe(startWithUndefined()),
63+
]),
64+
([effect, blendFunction, opacity]) => {
65+
const invalidate = this.store.get('invalidate');
66+
effect.blendMode.blendFunction =
67+
!blendFunction && blendFunction !== 0 ? this.defaultBlendMode : blendFunction;
68+
if (opacity !== undefined) effect.blendMode.opacity.value = opacity;
69+
invalidate();
70+
}
71+
);
72+
}
73+
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@nrwl/workspace": "15.6.3",
2020
"@types/jest": "28.1.1",
2121
"@types/node": "18.11.18",
22+
"@types/three": "^0.149.0",
2223
"@typescript-eslint/eslint-plugin": "^5.49.0",
2324
"@typescript-eslint/parser": "^5.49.0",
2425
"eslint": "~8.33.0",
@@ -51,6 +52,8 @@
5152
"angular-three": "^1.2.2",
5253
"postprocessing": "^6.29.3",
5354
"rxjs": "~7.8.0",
55+
"three": "^0.149.0",
56+
"three-stdlib": "^2.21.6",
5457
"tslib": "^2.5.0",
5558
"zone.js": "~0.12.0"
5659
}

0 commit comments

Comments
 (0)