@xperimntl/vue-threejs-postprocessing 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,539 @@
1
+ import { defineComponent, shallowRef, watch, provide, onBeforeUnmount, inject } from 'vue';
2
+ import { EffectComposer as EffectComposer$1, RenderPass, EffectPass, BloomEffect, BrightnessContrastEffect, HueSaturationEffect, LUT3DEffect, ToneMappingMode, ToneMappingEffect, DepthOfFieldEffect, BlendFunction, NoiseEffect, VignetteEffect } from 'postprocessing';
3
+ import { useThree, useFrame, defineFiberPlugin, withPluginOptions } from '@xperimntl/vue-threejs';
4
+
5
+ /** Injection key for the EffectComposer context. */
6
+ const COMPOSER_CONTEXT = Symbol('v3f-composer-context');
7
+ /** Injection key for plugin-level defaults. */
8
+ const POSTPROCESSING_DEFAULTS = Symbol('v3f-postprocessing-defaults');
9
+
10
+ const EffectComposer = defineComponent({
11
+ name: 'EffectComposer',
12
+ props: {
13
+ enabled: {
14
+ type: Boolean,
15
+ default: true
16
+ },
17
+ multisampling: {
18
+ type: Number,
19
+ default: 8
20
+ },
21
+ autoClear: {
22
+ type: Boolean,
23
+ default: true
24
+ },
25
+ resolutionScale: {
26
+ type: Number,
27
+ default: 1
28
+ },
29
+ depthBuffer: {
30
+ type: Boolean,
31
+ default: true
32
+ },
33
+ stencilBuffer: {
34
+ type: Boolean,
35
+ default: false
36
+ }
37
+ },
38
+ setup(props, {
39
+ slots
40
+ }) {
41
+ const state = useThree();
42
+ const composerRef = shallowRef(null);
43
+
44
+ // Ordered list of registered effects
45
+ const effects = [];
46
+
47
+ // Track the current EffectPass so we can dispose/replace it
48
+ let currentEffectPass = null;
49
+ let currentRenderPass = null;
50
+
51
+ // Store the original autoClear value so we can restore on unmount
52
+ let originalAutoClear = null;
53
+ function disposeComposer() {
54
+ // Dispose composer
55
+ if (composerRef.value) {
56
+ composerRef.value.dispose();
57
+ composerRef.value = null;
58
+ }
59
+ currentEffectPass = null;
60
+ currentRenderPass = null;
61
+ }
62
+ function createComposer() {
63
+ const {
64
+ gl,
65
+ scene,
66
+ camera
67
+ } = state.value;
68
+ if (!gl || !scene || !camera) return null;
69
+
70
+ // Dispose previous composer if it exists
71
+ disposeComposer();
72
+ const composer = new EffectComposer$1(gl, {
73
+ multisampling: props.multisampling > 0 ? props.multisampling : 0,
74
+ depthBuffer: props.depthBuffer,
75
+ stencilBuffer: props.stencilBuffer
76
+ });
77
+
78
+ // Create render pass
79
+ currentRenderPass = new RenderPass(scene, camera);
80
+ composer.addPass(currentRenderPass);
81
+
82
+ // Create effect pass if we already have effects
83
+ if (effects.length > 0) {
84
+ const sortedEffects = [...effects].sort((a, b) => a.priority - b.priority).map(e => e.effect);
85
+ currentEffectPass = new EffectPass(camera, ...sortedEffects);
86
+ composer.addPass(currentEffectPass);
87
+ }
88
+ composerRef.value = composer;
89
+
90
+ // Disable default renderer autoClear
91
+ if (originalAutoClear === null) {
92
+ originalAutoClear = gl.autoClear;
93
+ }
94
+ gl.autoClear = false;
95
+ return composer;
96
+ }
97
+ function rebuildEffectPass() {
98
+ const composer = composerRef.value;
99
+ if (!composer) return;
100
+ const {
101
+ camera
102
+ } = state.value;
103
+
104
+ // Remove old effect pass
105
+ if (currentEffectPass) {
106
+ composer.removePass(currentEffectPass);
107
+ currentEffectPass.dispose();
108
+ currentEffectPass = null;
109
+ }
110
+
111
+ // Build new effect pass with current effects
112
+ if (effects.length > 0) {
113
+ const sortedEffects = [...effects].sort((a, b) => a.priority - b.priority).map(e => e.effect);
114
+ currentEffectPass = new EffectPass(camera, ...sortedEffects);
115
+ composer.addPass(currentEffectPass);
116
+ }
117
+ }
118
+ watch(() => [state.value.gl, state.value.scene, state.value.camera], ([gl, scene, camera]) => {
119
+ if (!gl || !scene || !camera) {
120
+ disposeComposer();
121
+ return;
122
+ }
123
+ createComposer();
124
+ }, {
125
+ immediate: true
126
+ });
127
+
128
+ // Provide the context so child effect components can register
129
+ const composerContext = {
130
+ addEffect(effect, priority = 0) {
131
+ effects.push({
132
+ effect,
133
+ priority
134
+ });
135
+ rebuildEffectPass();
136
+ return () => composerContext.removeEffect(effect);
137
+ },
138
+ removeEffect(effect) {
139
+ const index = effects.findIndex(e => e.effect === effect);
140
+ if (index !== -1) {
141
+ effects.splice(index, 1);
142
+ rebuildEffectPass();
143
+ }
144
+ },
145
+ composer: composerRef
146
+ };
147
+ provide(COMPOSER_CONTEXT, composerContext);
148
+
149
+ // Watch for camera changes — update render pass and effect pass
150
+ watch(() => state.value.camera, camera => {
151
+ if (currentRenderPass) {
152
+ currentRenderPass.mainCamera = camera;
153
+ }
154
+ if (currentEffectPass) {
155
+ currentEffectPass.mainCamera = camera;
156
+ }
157
+ });
158
+
159
+ // Watch for size changes — update composer size
160
+ watch(() => state.value.size, size => {
161
+ const composer = composerRef.value;
162
+ if (composer) {
163
+ composer.setSize(size.width, size.height, false);
164
+ }
165
+ });
166
+
167
+ // Watch for multisampling changes — recreate composer
168
+ watch(() => props.multisampling, () => {
169
+ if (createComposer()) rebuildEffectPass();
170
+ });
171
+
172
+ // Use useFrame with priority 1 to take over rendering
173
+ useFrame((_frameState, delta) => {
174
+ if (!props.enabled || !composerRef.value) return;
175
+ composerRef.value.render(delta);
176
+ }, 1);
177
+
178
+ // Cleanup on unmount
179
+ onBeforeUnmount(() => {
180
+ const {
181
+ gl
182
+ } = state.value;
183
+
184
+ // Restore original autoClear
185
+ if (originalAutoClear !== null) {
186
+ gl.autoClear = originalAutoClear;
187
+ }
188
+ disposeComposer();
189
+ effects.length = 0;
190
+ });
191
+ return () => slots.default == null ? void 0 : slots.default();
192
+ }
193
+ });
194
+
195
+ const Bloom = defineComponent({
196
+ name: 'Bloom',
197
+ props: {
198
+ intensity: {
199
+ type: Number,
200
+ default: 1
201
+ },
202
+ luminanceThreshold: {
203
+ type: Number,
204
+ default: 0.9
205
+ },
206
+ luminanceSmoothing: {
207
+ type: Number,
208
+ default: 0.025
209
+ },
210
+ mipmapBlur: {
211
+ type: Boolean,
212
+ default: true
213
+ }
214
+ },
215
+ setup(props) {
216
+ const composerCtx = inject(COMPOSER_CONTEXT);
217
+ if (!composerCtx) {
218
+ throw new Error('Bloom must be a child of EffectComposer');
219
+ }
220
+ const effect = new BloomEffect({
221
+ intensity: props.intensity,
222
+ luminanceThreshold: props.luminanceThreshold,
223
+ luminanceSmoothing: props.luminanceSmoothing,
224
+ mipmapBlur: props.mipmapBlur
225
+ });
226
+ const removeEffect = composerCtx.addEffect(effect, 0);
227
+ watch(() => props.intensity, value => {
228
+ effect.intensity = value;
229
+ });
230
+ watch(() => props.luminanceThreshold, value => {
231
+ effect.luminanceMaterial.threshold = value;
232
+ });
233
+ watch(() => props.luminanceSmoothing, value => {
234
+ effect.luminanceMaterial.smoothing = value;
235
+ });
236
+ watch(() => props.mipmapBlur, value => {
237
+ effect.mipmapBlurPass.enabled = value;
238
+ });
239
+ onBeforeUnmount(() => {
240
+ removeEffect();
241
+ effect.dispose();
242
+ });
243
+ return () => null;
244
+ }
245
+ });
246
+
247
+ const BrightnessContrast = defineComponent({
248
+ name: 'BrightnessContrast',
249
+ props: {
250
+ brightness: {
251
+ type: Number,
252
+ default: 0
253
+ },
254
+ contrast: {
255
+ type: Number,
256
+ default: 0
257
+ }
258
+ },
259
+ setup(props) {
260
+ const composerCtx = inject(COMPOSER_CONTEXT);
261
+ if (!composerCtx) {
262
+ throw new Error('BrightnessContrast must be a child of EffectComposer');
263
+ }
264
+ const effect = new BrightnessContrastEffect({
265
+ brightness: props.brightness,
266
+ contrast: props.contrast
267
+ });
268
+ const removeEffect = composerCtx.addEffect(effect, 0);
269
+ watch(() => props.brightness, value => {
270
+ effect.brightness = value;
271
+ });
272
+ watch(() => props.contrast, value => {
273
+ effect.contrast = value;
274
+ });
275
+ onBeforeUnmount(() => {
276
+ removeEffect();
277
+ effect.dispose();
278
+ });
279
+ return () => null;
280
+ }
281
+ });
282
+
283
+ const HueSaturation = defineComponent({
284
+ name: 'HueSaturation',
285
+ props: {
286
+ hue: {
287
+ type: Number,
288
+ default: 0
289
+ },
290
+ saturation: {
291
+ type: Number,
292
+ default: 0
293
+ }
294
+ },
295
+ setup(props) {
296
+ const composerCtx = inject(COMPOSER_CONTEXT);
297
+ if (!composerCtx) {
298
+ throw new Error('HueSaturation must be a child of EffectComposer');
299
+ }
300
+ const effect = new HueSaturationEffect({
301
+ hue: props.hue,
302
+ saturation: props.saturation
303
+ });
304
+ const removeEffect = composerCtx.addEffect(effect, 0);
305
+ watch(() => props.hue, value => {
306
+ effect.hue = value;
307
+ });
308
+ watch(() => props.saturation, value => {
309
+ effect.saturation = value;
310
+ });
311
+ onBeforeUnmount(() => {
312
+ removeEffect();
313
+ effect.dispose();
314
+ });
315
+ return () => null;
316
+ }
317
+ });
318
+
319
+ const LUT = defineComponent({
320
+ name: 'LUT',
321
+ props: {
322
+ lut: {
323
+ type: Object,
324
+ required: true
325
+ },
326
+ tetrahedralInterpolation: {
327
+ type: Boolean,
328
+ default: false
329
+ }
330
+ },
331
+ setup(props) {
332
+ const composerCtx = inject(COMPOSER_CONTEXT);
333
+ if (!composerCtx) {
334
+ throw new Error('LUT must be a child of EffectComposer');
335
+ }
336
+ const effect = new LUT3DEffect(props.lut, {
337
+ tetrahedralInterpolation: props.tetrahedralInterpolation
338
+ });
339
+ const removeEffect = composerCtx.addEffect(effect, 0);
340
+ watch(() => props.lut, value => {
341
+ effect.lut = value;
342
+ });
343
+ watch(() => props.tetrahedralInterpolation, value => {
344
+ effect.tetrahedralInterpolation = value;
345
+ });
346
+ onBeforeUnmount(() => {
347
+ removeEffect();
348
+ effect.dispose();
349
+ });
350
+ return () => null;
351
+ }
352
+ });
353
+
354
+ const ToneMapping = defineComponent({
355
+ name: 'ToneMapping',
356
+ props: {
357
+ mode: {
358
+ type: Number,
359
+ default: ToneMappingMode.AGX
360
+ },
361
+ resolution: {
362
+ type: Number,
363
+ default: 256
364
+ },
365
+ whitePoint: {
366
+ type: Number,
367
+ default: 4
368
+ },
369
+ middleGrey: {
370
+ type: Number,
371
+ default: 0.6
372
+ }
373
+ },
374
+ setup(props) {
375
+ const composerCtx = inject(COMPOSER_CONTEXT);
376
+ if (!composerCtx) {
377
+ throw new Error('ToneMapping must be a child of EffectComposer');
378
+ }
379
+ const effect = new ToneMappingEffect({
380
+ mode: props.mode,
381
+ resolution: props.resolution,
382
+ whitePoint: props.whitePoint,
383
+ middleGrey: props.middleGrey
384
+ });
385
+ const removeEffect = composerCtx.addEffect(effect, 0);
386
+ watch(() => props.mode, value => {
387
+ effect.mode = value;
388
+ });
389
+ watch(() => props.resolution, value => {
390
+ effect.resolution.width = value;
391
+ effect.resolution.height = value;
392
+ });
393
+ watch(() => props.whitePoint, value => {
394
+ effect.whitePoint = value;
395
+ });
396
+ watch(() => props.middleGrey, value => {
397
+ effect.middleGrey = value;
398
+ });
399
+ onBeforeUnmount(() => {
400
+ removeEffect();
401
+ effect.dispose();
402
+ });
403
+ return () => null;
404
+ }
405
+ });
406
+
407
+ const DepthOfField = defineComponent({
408
+ name: 'DepthOfField',
409
+ props: {
410
+ focusDistance: {
411
+ type: Number,
412
+ default: 0
413
+ },
414
+ focalLength: {
415
+ type: Number,
416
+ default: 0.1
417
+ },
418
+ bokehScale: {
419
+ type: Number,
420
+ default: 2
421
+ }
422
+ },
423
+ setup(props) {
424
+ const composerCtx = inject(COMPOSER_CONTEXT);
425
+ if (!composerCtx) {
426
+ throw new Error('DepthOfField must be a child of EffectComposer');
427
+ }
428
+ const state = useThree();
429
+ const {
430
+ camera
431
+ } = state.value;
432
+ const effect = new DepthOfFieldEffect(camera, {
433
+ focusDistance: props.focusDistance,
434
+ focalLength: props.focalLength,
435
+ bokehScale: props.bokehScale
436
+ });
437
+ const removeEffect = composerCtx.addEffect(effect, 0);
438
+ watch(() => props.focusDistance, value => {
439
+ effect.cocMaterial.focusDistance = value;
440
+ });
441
+ watch(() => props.focalLength, value => {
442
+ effect.cocMaterial.focalLength = value;
443
+ });
444
+ watch(() => props.bokehScale, value => {
445
+ effect.bokehScale = value;
446
+ });
447
+ onBeforeUnmount(() => {
448
+ removeEffect();
449
+ effect.dispose();
450
+ });
451
+ return () => null;
452
+ }
453
+ });
454
+
455
+ const Noise = defineComponent({
456
+ name: 'Noise',
457
+ props: {
458
+ premultiply: {
459
+ type: Boolean,
460
+ default: false
461
+ },
462
+ blendFunction: {
463
+ type: Number,
464
+ default: BlendFunction.SCREEN
465
+ }
466
+ },
467
+ setup(props) {
468
+ const composerCtx = inject(COMPOSER_CONTEXT);
469
+ if (!composerCtx) {
470
+ throw new Error('Noise must be a child of EffectComposer');
471
+ }
472
+ const effect = new NoiseEffect({
473
+ premultiply: props.premultiply,
474
+ blendFunction: props.blendFunction
475
+ });
476
+ const removeEffect = composerCtx.addEffect(effect, 0);
477
+ watch(() => props.premultiply, value => {
478
+ effect.premultiply = value;
479
+ });
480
+ watch(() => props.blendFunction, value => {
481
+ effect.blendMode.blendFunction = value;
482
+ });
483
+ onBeforeUnmount(() => {
484
+ removeEffect();
485
+ effect.dispose();
486
+ });
487
+ return () => null;
488
+ }
489
+ });
490
+
491
+ const Vignette = defineComponent({
492
+ name: 'Vignette',
493
+ props: {
494
+ offset: {
495
+ type: Number,
496
+ default: 0.5
497
+ },
498
+ darkness: {
499
+ type: Number,
500
+ default: 0.5
501
+ }
502
+ },
503
+ setup(props) {
504
+ const composerCtx = inject(COMPOSER_CONTEXT);
505
+ if (!composerCtx) {
506
+ throw new Error('Vignette must be a child of EffectComposer');
507
+ }
508
+ const effect = new VignetteEffect({
509
+ offset: props.offset,
510
+ darkness: props.darkness
511
+ });
512
+ const removeEffect = composerCtx.addEffect(effect, 0);
513
+ watch(() => props.offset, value => {
514
+ effect.offset = value;
515
+ });
516
+ watch(() => props.darkness, value => {
517
+ effect.darkness = value;
518
+ });
519
+ onBeforeUnmount(() => {
520
+ removeEffect();
521
+ effect.dispose();
522
+ });
523
+ return () => null;
524
+ }
525
+ });
526
+
527
+ const postprocessingFiberPlugin = defineFiberPlugin({
528
+ name: '@xperimntl/vue-threejs-postprocessing',
529
+ setup(ctx, options) {
530
+ if (options) {
531
+ ctx.provide(POSTPROCESSING_DEFAULTS, options);
532
+ }
533
+ }
534
+ });
535
+ function createPostprocessingPlugin(options) {
536
+ return withPluginOptions(postprocessingFiberPlugin, options);
537
+ }
538
+
539
+ export { Bloom, BrightnessContrast, COMPOSER_CONTEXT, DepthOfField, EffectComposer, HueSaturation, LUT, Noise, POSTPROCESSING_DEFAULTS, ToneMapping, Vignette, createPostprocessingPlugin, postprocessingFiberPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xperimntl/vue-threejs-postprocessing",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Vue-native postprocessing effects for @xperimntl/vue-threejs",
5
5
  "license": "MIT",
6
6
  "repository": {