UNPKG

@tresjs/post-processing

Version:

Post-processing library for TresJS

1,620 lines (1,529 loc) 82.2 kB
/** * name: @tresjs/post-processing * version: v3.4.0 * (c) 2026 * description: Post-processing library for TresJS * author: Alvaro Saburido <hola@alvarosaburido.dev> (https://github.com/alvarosabu/) */ import { computed, defineComponent, inject, nextTick, onUnmounted, provide, renderSlot, shallowRef, toRaw, watch, watchEffect } from "vue"; import { ASCIIEffect, ASCIITexture, BlendFunction, BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, ColorAverageEffect, ColorDepthEffect, DepthDownsamplingPass, DepthOfFieldEffect, DepthPickingPass, DotScreenEffect, Effect, EffectComposer, EffectPass, FXAAEffect, GlitchEffect, GlitchMode, GodRaysEffect, GridEffect, HueSaturationEffect, LensDistortionEffect, NoiseEffect, NormalPass, OutlineEffect, PixelationEffect, RenderPass, SMAAEffect, ScanlineEffect, SepiaEffect, ShockWaveEffect, TextureEffect, TiltShiftEffect, ToneMappingEffect, VignetteEffect } from "postprocessing"; import { normalizeColor, useLoop, useTres, useTresContext } from "@tresjs/core"; import { HalfFloatType, Mesh, MeshBasicMaterial, SphereGeometry, Uniform, Vector2, Vector3 } from "three"; import WEBGL from "three/examples/jsm/capabilities/WebGL.js"; import { useDevicePixelRatio } from "@vueuse/core"; import { EffectComposer as EffectComposer$1 } from "three/examples/jsm/postprocessing/EffectComposer.js"; import { RenderPass as RenderPass$1 } from "three/examples/jsm/postprocessing/RenderPass.js"; import { GlitchPass } from "three/examples/jsm/postprocessing/GlitchPass.js"; import { HalftonePass } from "three/examples/jsm/postprocessing/HalftonePass.js"; import { HalftoneShader } from "three/examples/jsm/shaders/HalftoneShader.js"; import { RenderPixelatedPass } from "three/examples/jsm/postprocessing/RenderPixelatedPass.js"; import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass.js"; import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js"; import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js"; import { LuminosityHighPassShader } from "three/examples/jsm/shaders/LuminosityHighPassShader.js"; //#region src/core/pmndrs/EffectComposerPmndrs.vue?vue&type=script&setup=true&lang.ts const effectComposerInjectionKey$1 = Symbol("effectComposerPmndrs"); var EffectComposerPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "EffectComposerPmndrs", props: { enabled: { type: Boolean, required: false, default: true }, depthBuffer: { type: Boolean, required: false, default: void 0 }, disableNormalPass: { type: Boolean, required: false, default: false }, stencilBuffer: { type: Boolean, required: false, default: void 0 }, resolutionScale: { type: Number, required: false }, autoClear: { type: Boolean, required: false, default: true }, multisampling: { type: Number, required: false, default: 0 }, frameBufferType: { type: Number, required: false, default: HalfFloatType } }, emits: ["render"], setup(__props, { expose: __expose, emit: __emit }) { const props = __props; const emit = __emit; const { scene, camera, renderer, sizes } = useTresContext(); const effectComposer = shallowRef(null); let downSamplingPass = null; let normalPass = null; provide(effectComposerInjectionKey$1, effectComposer); __expose({ composer: effectComposer }); const setNormalPass = () => { if (!effectComposer.value) return; normalPass = new NormalPass(scene.value, camera.activeCamera.value); normalPass.enabled = false; effectComposer.value.addPass(normalPass); if (props.resolutionScale !== void 0 && WEBGL.isWebGL2Available()) { downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale: props.resolutionScale }); downSamplingPass.enabled = false; effectComposer.value.addPass(downSamplingPass); } }; const effectComposerParams = computed(() => { const plainEffectComposer = new EffectComposer(); const params = { depthBuffer: props.depthBuffer !== void 0 ? props.depthBuffer : plainEffectComposer.inputBuffer.depthBuffer, stencilBuffer: props.stencilBuffer !== void 0 ? props.stencilBuffer : plainEffectComposer.inputBuffer.stencilBuffer, multisampling: WEBGL.isWebGL2Available() ? props.multisampling !== void 0 ? props.multisampling : plainEffectComposer.multisampling : 0, frameBufferType: props.frameBufferType !== void 0 ? props.frameBufferType : HalfFloatType }; plainEffectComposer.dispose(); return params; }); const initEffectComposer = () => { if (!renderer.instance && !scene.value && !camera.activeCamera.value) return; effectComposer.value?.dispose(); effectComposer.value = new EffectComposer(renderer.instance, effectComposerParams.value); effectComposer.value.addPass(new RenderPass(scene.value, camera.activeCamera.value)); if (!props.disableNormalPass) setNormalPass(); }; watch([ scene, camera.activeCamera, () => props.disableNormalPass ], () => { if (!sizes.width.value || !sizes.height.value) return; initEffectComposer(); }); watch(() => [sizes.width.value, sizes.height.value], ([width, height]) => { if (!width && !height) return; if (effectComposer.value) effectComposer.value.setSize(width, height); else initEffectComposer(); }, { immediate: true }); renderer.replaceRenderFunction((notifySuccess) => { if (props.enabled && renderer.instance && effectComposer.value && sizes.width.value && sizes.height.value) { const currentAutoClear = renderer.instance.autoClear; renderer.instance.autoClear = props.autoClear; if (props.stencilBuffer && !props.autoClear) renderer.instance.clearStencil(); effectComposer.value.render(); emit("render", effectComposer.value); renderer.instance.autoClear = currentAutoClear; notifySuccess(); } }); onUnmounted(() => { effectComposer.value?.dispose(); }); return (_ctx, _cache) => { return renderSlot(_ctx.$slots, "default"); }; } }); //#endregion //#region src/core/pmndrs/EffectComposerPmndrs.vue var EffectComposerPmndrs_default = EffectComposerPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/composables/useEffectPmndrs.ts /** * @param newEffectFunction - A function that returns a new effect instance. * @param passDependencies - A reactive object that the pass depends on (usually props). Changes to this object will trigger re-rendering. * @param dependencyFieldsTriggeringRecreation - fields in passDependencies that require effect recreation when changed */ const useEffectPmndrs = (newEffectFunction, passDependencies, dependencyFieldsTriggeringRecreation) => { const composer = inject(effectComposerInjectionKey$1); const pass = shallowRef(null); const effect = shallowRef(null); const { scene, camera, invalidate } = useTres(); watch(passDependencies, () => invalidate()); const removePass = () => { if (pass.value) composer?.value?.removePass(pass.value); effect.value?.dispose(); pass.value?.dispose(); }; const createEffect = (index) => { if (!camera.value || !composer?.value || !scene.value) return; effect.value = newEffectFunction(); pass.value = new EffectPass(camera.value, effect.value); composer.value.addPass(pass.value, index); }; if (dependencyFieldsTriggeringRecreation) watch(() => dependencyFieldsTriggeringRecreation.map((field) => passDependencies[field]), () => { if (!composer?.value) return; const index = composer.value?.passes.findIndex((p) => p === pass.value); if (!~index) return; removePass(); createEffect(index); }); watchEffect(() => { if (!camera.value || !effect?.value) return; effect.value.mainCamera = camera.value; }); const unwatch = watchEffect(() => { if (!camera.value || !composer?.value || !scene.value) return; nextTick(() => unwatch()); if (effect.value) return; createEffect(); }); onUnmounted(() => { removePass(); }); return { pass, effect }; }; //#endregion //#region src/util/object.ts const pathRegex = /([^[.\]])+/g; /** * Retrieves the value at a given path within a provided object. * * @template T - The type of value to be returned * * @param {any} obj - The object to extract value from * @param {string | string[]} path - A path or an array of path where the value should be get from * * @returns {T | undefined} - The value at the given path in the object, or undefined if path is not found * * @example * * const obj = { a: { b: { c: 1 } } } * * const result = get(obj, 'a.b.c') * * console.log(result) // 1 */ const get = (obj, path) => { if (!path) return; return (Array.isArray(path) ? path : path.match(pathRegex))?.reduce((prevObj, key) => prevObj && prevObj[key], obj); }; /** * Sets a value at a given path within a provided object. If the path does not exist, nested objects will be created. * * @param {any} obj - The original object to set value in * @param {string | string[]} path - A path or an array of path where the value should be set * @param {any} value - The value to be set at the provided path * * @returns {void} * * @example * const obj = { a: { b: { c: 1 } } } * * set(obj, 'a.b.c', 2) * * console.log(obj) // { a: { b: { c: 2 } } } */ const set = (obj, path, value) => { const pathArray = Array.isArray(path) ? path : path.match(pathRegex); if (pathArray) pathArray.reduce((acc, key, i) => { if (acc[key] === void 0) acc[key] = {}; if (i === pathArray.length - 1) acc[key] = value; return acc[key]; }, obj); }; /** * Omits given properties from a provided object. * * @template T - An object with string keys and any type of values * * @param {T} obj - The original object to omit properties from * @param {(keyof T)[]} properties - An array of property key names to omit from the base object * * @returns {Partial<T>} The new object with omitted properties * * @example * const obj = { a: 1, b: 2, c: 3 } * const propsToOmit = ['b', 'c'] * * const newObj = omit(obj, propsToOmit) * * console.log(newObj) // { a: 1 } */ const omit = (obj, properties) => { const newObj = { ...obj }; properties.forEach((prop) => delete newObj[prop]); return newObj; }; //#endregion //#region src/util/prop.ts /** * Creates a prop watcher function that monitors changes to a property and updates a target object. * * @template T - The type of the property being watched. * @template E - The type of the target object. * @param {() => T} propGetter - A function that retrieves the prop value to be watched. * @param {Ref<E>} target - A Ref representing the target object to be updated. * @param {string} propertyPath - The dot-separated path to the property within the target object. * @param {() => E & { dispose?(): void }} newPlainObjectFunction - A function that creates a new plain object to retrieve the defaults from with an optional "dispose" method for cleanup. * @param {WatchOptions} watchOptions - The options for watch. */ const makePropWatcher = (propGetter, target, propertyPath, newPlainObjectFunction, watchOptions = {}) => watch(propGetter, (newValue) => { if (!target.value) return; if (newValue === void 0) { const plainObject = newPlainObjectFunction(); set(target.value, propertyPath, get(plainObject, propertyPath)); plainObject.dispose?.(); } else set(target.value, propertyPath, propGetter()); }, watchOptions); /** * Creates multiple prop watchers for monitoring changes to multiple properties and updating a target object. * * @template T - The type of the property being watched. * @template E - The type of the target object. * @param {(string | (() => T))[][]} propGettersAndPropertyPaths - An array of arrays containing pairs of prop getters and their corresponding property paths within the target object. * @param {Ref<E>} target - A Ref representing the target object to be updated. * @param {() => E & { dispose?(): void }} newPlainObjectFunction - A function that creates a new plain object to retrieve the defaults from with an optional "dispose" method for cleanup. */ const makePropWatchers = (propGettersAndPropertyPaths, target, newPlainObjectFunction) => propGettersAndPropertyPaths.map(([propGetterFn, path]) => makePropWatcher(propGetterFn, target, path, newPlainObjectFunction)); /** * Creates multiple prop watchers via the props object for monitoring changes to multiple properties and updating a target object. * Use this method in case the prop names match the names of the properties you want to set on your target object. * * @param props - The props object. Usually created via defineProps. * @param {Ref<E>} target - A Ref representing the target object to be updated. * @param {() => E & { dispose?(): void }} newPlainObjectFunction - A function that creates a new plain object to retrieve the defaults from with an optional "dispose" method for cleanup. */ const makePropWatchersUsingAllProps = (props, target, newPlainObjectFunction) => Object.keys(props).map((key) => makePropWatcher(() => props[key], target, key, newPlainObjectFunction)); //#endregion //#region src/core/pmndrs/BloomPmndrs.vue?vue&type=script&setup=true&lang.ts var BloomPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "BloomPmndrs", props: { blendFunction: { type: null, required: false }, intensity: { type: Number, required: false }, kernelSize: { type: null, required: false }, luminanceThreshold: { type: Number, required: false }, luminanceSmoothing: { type: Number, required: false }, mipmapBlur: { type: Boolean, required: false, default: void 0 } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new BloomEffect(props), props, ["mipmapBlur"]); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.intensity, "intensity"], [() => props.kernelSize, "kernelSize"], [() => props.luminanceSmoothing, "luminanceMaterial.smoothing"], [() => props.luminanceThreshold, "luminanceMaterial.threshold"] ], effect, () => new BloomEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/BloomPmndrs.vue var BloomPmndrs_default = BloomPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/DepthOfFieldPmndrs.vue?vue&type=script&setup=true&lang.ts var DepthOfFieldPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "DepthOfFieldPmndrs", props: { blendFunction: { type: null, required: false }, worldFocusDistance: { type: Number, required: false }, worldFocusRange: { type: Number, required: false }, focusDistance: { type: Number, required: false }, focusRange: { type: Number, required: false }, bokehScale: { type: Number, required: false }, resolutionScale: { type: Number, required: false }, resolutionX: { type: Number, required: false }, resolutionY: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { camera } = useTres(); const { pass, effect } = useEffectPmndrs(() => new DepthOfFieldEffect(camera.value, props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.worldFocusDistance, "circleOfConfusionMaterial.worldFocusDistance"], [() => props.focusDistance, "circleOfConfusionMaterial.focusDistance"], [() => props.worldFocusRange, "circleOfConfusionMaterial.worldFocusRange"], [() => props.focusRange, "circleOfConfusionMaterial.focusRange"], [() => props.bokehScale, "bokehScale"], [() => props.resolutionScale, "blurPass.resolution.scale"], [() => props.resolutionX, "resolution.width"], [() => props.resolutionY, "resolution.height"] ], effect, () => new DepthOfFieldEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/DepthOfFieldPmndrs.vue var DepthOfFieldPmndrs_default = DepthOfFieldPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/GlitchPmndrs.vue?vue&type=script&setup=true&lang.ts var GlitchPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "GlitchPmndrs", props: { blendFunction: { type: null, required: false }, delay: { type: Object, required: false }, duration: { type: Object, required: false }, strength: { type: Object, required: false }, mode: { type: null, required: false }, active: { type: Boolean, required: false }, ratio: { type: Number, required: false }, columns: { type: Number, required: false }, chromaticAberrationOffset: { type: Object, required: false }, perturbationMap: { type: Object, required: false }, dtSize: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new GlitchEffect(props), props, ["dtSize"]); __expose({ pass, effect }); const { invalidate } = useTres(); const { onBeforeRender } = useLoop(); onBeforeRender(() => invalidate()); watchEffect(() => { const getMode = () => { if (props.mode !== void 0) return props.active === false ? GlitchMode.DISABLED : props.mode; const plainEffectPass = new GlitchEffect(); const defaultMode = plainEffectPass.mode; plainEffectPass.dispose(); return defaultMode; }; if (effect.value) effect.value.mode = getMode(); }); makePropWatcher(() => props.blendFunction, effect, "blendMode.blendFunction", () => new GlitchEffect()); makePropWatchersUsingAllProps(omit(props, ["active", "blendFunction"]), effect, () => new GlitchEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/GlitchPmndrs.vue var GlitchPmndrs_default = GlitchPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/NoisePmndrs.vue?vue&type=script&setup=true&lang.ts var NoisePmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "NoisePmndrs", props: { premultiply: { type: Boolean, required: false, default: void 0 }, blendFunction: { type: null, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new NoiseEffect(props), props); __expose({ pass, effect }); const { invalidate } = useTres(); const { onBeforeRender } = useLoop(); onBeforeRender(() => invalidate()); makePropWatchers([[() => props.blendFunction, "blendMode.blendFunction"], [() => props.premultiply, "premultiply"]], effect, () => new NoiseEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/NoisePmndrs.vue var NoisePmndrs_default = NoisePmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/OutlinePmndrs.vue?vue&type=script&setup=true&lang.ts var OutlinePmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "OutlinePmndrs", props: { outlinedObjects: { type: Array, required: true }, blur: { type: Boolean, required: false, default: void 0 }, xRay: { type: Boolean, required: false, default: void 0 }, kernelSize: { type: null, required: false }, pulseSpeed: { type: Number, required: false }, resolutionX: { type: Number, required: false }, resolutionY: { type: Number, required: false }, edgeStrength: { type: Number, required: false }, patternScale: { type: Number, required: false }, multisampling: { type: Number, required: false }, blendFunction: { type: null, required: false }, patternTexture: { type: Object, required: false }, resolutionScale: { type: Number, required: false }, hiddenEdgeColor: { type: null, required: false }, visibleEdgeColor: { type: null, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const colorToNumber = (color) => color !== void 0 ? normalizeColor(color).getHex() : void 0; const { camera, scene } = useTres(); const { pass, effect } = useEffectPmndrs(() => new OutlineEffect(scene.value, camera.value, { blur: props.blur, xRay: props.xRay, kernelSize: props.kernelSize, pulseSpeed: props.pulseSpeed, resolutionX: props.resolutionX, resolutionY: props.resolutionY, patternScale: props.patternScale, edgeStrength: props.edgeStrength, blendFunction: props.blendFunction, multisampling: props.multisampling, patternTexture: props.patternTexture, resolutionScale: props.resolutionScale, hiddenEdgeColor: colorToNumber(props.hiddenEdgeColor), visibleEdgeColor: colorToNumber(props.visibleEdgeColor) }), props); __expose({ pass, effect }); watch([() => props.outlinedObjects, effect], () => { effect.value?.selection.set(props.outlinedObjects || []); }, { immediate: true }); const normalizedColors = computed(() => ({ hiddenEdgeColor: props.hiddenEdgeColor ? normalizeColor(props.hiddenEdgeColor) : void 0, visibleEdgeColor: props.visibleEdgeColor ? normalizeColor(props.visibleEdgeColor) : void 0 })); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.blur, "blur"], [() => props.xRay, "xRay"], [() => props.pulseSpeed, "pulseSpeed"], [() => props.kernelSize, "kernelSize"], [() => props.edgeStrength, "edgeStrength"], [() => props.patternScale, "patternScale"], [() => props.multisampling, "multisampling"], [() => props.resolutionX, "resolution.width"], [() => props.resolutionY, "resolution.height"], [() => props.patternTexture, "patternTexture"], [() => props.resolutionScale, "resolution.scale"], [() => normalizedColors.value.hiddenEdgeColor, "hiddenEdgeColor"], [() => normalizedColors.value.visibleEdgeColor, "visibleEdgeColor"] ], effect, () => new OutlineEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/OutlinePmndrs.vue var OutlinePmndrs_default = OutlinePmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/PixelationPmndrs.vue?vue&type=script&setup=true&lang.ts var PixelationPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "PixelationPmndrs", props: { granularity: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new PixelationEffect(props.granularity), props); __expose({ pass, effect }); makePropWatchersUsingAllProps(props, effect, () => new PixelationEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/PixelationPmndrs.vue var PixelationPmndrs_default = PixelationPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/VignettePmndrs.vue?vue&type=script&setup=true&lang.ts var VignettePmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "VignettePmndrs", props: { technique: { type: null, required: false }, blendFunction: { type: null, required: false }, offset: { type: Number, required: false }, darkness: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new VignetteEffect(props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.offset, "offset"], [() => props.darkness, "darkness"], [() => props.technique, "technique"] ], effect, () => new VignetteEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/VignettePmndrs.vue var VignettePmndrs_default = VignettePmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/custom/barrel-blur/index.ts /** * BarrelBlurEffect - A custom effect for applying a barrel distortion * with chromatic aberration blur. */ var BarrelBlurEffect = class extends Effect { /** * Creates a new BarrelBlurEffect instance. * * @param {object} [options] - Configuration options for the effect. * @param {BlendFunction} [options.blendFunction] - Blend mode. * @param {number} [options.amount] - Intensity of the barrel distortion (0 to 1). * @param {Vector2} [options.offset] - Offset of the barrel distortion center (0 to 1 for both x and y). This allows you to change the position of the distortion effect. * */ constructor({ blendFunction = BlendFunction.NORMAL, amount = .15, offset = new Vector2(.5, .5) } = {}) { super("BarrelBlurEffect", ` uniform float amount; uniform vec2 offset; #define NUM_ITER 16 #define RECIP_NUM_ITER 0.0625 #define GAMMA 1.0 vec3 spectrum_offset(float t) { float lo = step(t, 0.5); float hi = 1.0 - lo; float w = 1.0 - abs(2.0 * t - 1.0); return pow(vec3(lo, 1.0, hi) * vec3(1.0 - w, w, 1.0 - w), vec3(1.0 / GAMMA)); } vec2 barrelDistortion(vec2 p, float amt) { p = p - offset; float theta = atan(p.y, p.x); float radius = pow(length(p), 1.0 + 3.0 * amt); return vec2(cos(theta), sin(theta)) * radius + offset; } void mainUv(inout vec2 uv) { uv = barrelDistortion(uv, amount * 0.5); } void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { vec3 sumcol = vec3(0.0); vec3 sumw = vec3(0.0); for (int i = 0; i < NUM_ITER; ++i) { float t = float(i) * RECIP_NUM_ITER; vec3 w = spectrum_offset(t); vec2 distortedUV = barrelDistortion(uv, amount * t); sumcol += w * texture(inputBuffer, distortedUV).rgb; sumw += w; } vec3 outcol = pow(sumcol / sumw, vec3(1.0 / GAMMA)); outcol = clamp(outcol, 0.0, 1.0); // Ensures normalized color values outputColor = vec4(outcol, inputColor.a); // Preserves original alpha } `, { blendFunction, uniforms: new Map([["amount", new Uniform(amount)], ["offset", new Uniform(offset)]]) }); } /** * The amount. * * @type {number} */ get amount() { return this.uniforms.get("amount")?.value; } set amount(value) { this.uniforms.get("amount").value = value; } /** * The offset. * * @type {Vector2} */ get offset() { return this.uniforms.get("offset")?.value; } set offset(value) { this.uniforms.get("offset").value = value; } }; //#endregion //#region src/core/pmndrs/BarrelBlurPmndrs.vue?vue&type=script&setup=true&lang.ts var BarrelBlurPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "BarrelBlurPmndrs", props: { blendFunction: { type: null, required: false }, amount: { type: Number, required: false }, offset: { type: [Object, Array], required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new BarrelBlurEffect({ ...props, offset: Array.isArray(props.offset) ? new Vector2(...props.offset) : props.offset }), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.amount, "amount"], [() => props.offset, "offset"] ], effect, () => new BarrelBlurEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/BarrelBlurPmndrs.vue var BarrelBlurPmndrs_default = BarrelBlurPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/ToneMappingPmndrs.vue?vue&type=script&setup=true&lang.ts var ToneMappingPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "ToneMappingPmndrs", props: { mode: { type: null, required: false }, blendFunction: { type: null, required: false }, resolution: { type: Number, required: false }, averageLuminance: { type: Number, required: false }, middleGrey: { type: Number, required: false }, minLuminance: { type: Number, required: false }, whitePoint: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new ToneMappingEffect(props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.mode, "mode"], [() => props.blendFunction, "blendMode.blendFunction"], [() => props.resolution, "resolution"], [() => props.averageLuminance, "averageLuminance"], [() => props.middleGrey, "middleGrey"], [() => props.minLuminance, "adaptiveLuminanceMaterial.minLuminance"], [() => props.whitePoint, "whitePoint"] ], effect, () => new ToneMappingEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/ToneMappingPmndrs.vue var ToneMappingPmndrs_default = ToneMappingPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/ChromaticAberrationPmndrs.vue?vue&type=script&setup=true&lang.ts var ChromaticAberrationPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "ChromaticAberrationPmndrs", props: { blendFunction: { type: null, required: false }, offset: { type: Object, required: false }, radialModulation: { type: Boolean, required: false, default: void 0 }, modulationOffset: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const plainEffect = new ChromaticAberrationEffect(); const { pass, effect } = useEffectPmndrs(() => new ChromaticAberrationEffect({ ...props, radialModulation: props.radialModulation ?? plainEffect.radialModulation, modulationOffset: props.modulationOffset ?? plainEffect.modulationOffset }), props); plainEffect.dispose(); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.offset, "offset"], [() => props.radialModulation, "radialModulation"], [() => props.modulationOffset, "modulationOffset"] ], effect, () => new ChromaticAberrationEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/ChromaticAberrationPmndrs.vue var ChromaticAberrationPmndrs_default = ChromaticAberrationPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/HueSaturationPmndrs.vue?vue&type=script&setup=true&lang.ts var HueSaturationPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "HueSaturationPmndrs", props: { saturation: { type: Number, required: false }, hue: { type: Number, required: false }, blendFunction: { type: null, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new HueSaturationEffect(props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.hue, "hue"], [() => props.saturation, "saturation"] ], effect, () => new HueSaturationEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/HueSaturationPmndrs.vue var HueSaturationPmndrs_default = HueSaturationPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/ScanlinePmndrs.vue?vue&type=script&setup=true&lang.ts var ScanlinePmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "ScanlinePmndrs", props: { blendFunction: { type: null, required: false }, density: { type: Number, required: false }, scrollSpeed: { type: Number, required: false }, opacity: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new ScanlineEffect(props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.density, "density"], [() => props.scrollSpeed, "scrollSpeed"] ], effect, () => new ScanlineEffect()); watch([() => props.opacity], () => { if (props.opacity !== void 0) effect.value?.blendMode.setOpacity(props.opacity); else { const plainEffect = new ScanlineEffect(); effect.value?.blendMode.setOpacity(plainEffect.blendMode.getOpacity()); plainEffect.dispose(); } }, { immediate: true }); return () => {}; } }); //#endregion //#region src/core/pmndrs/ScanlinePmndrs.vue var ScanlinePmndrs_default = ScanlinePmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/custom/kuwahara/index.ts /** * The `MAX_SECTOR_COUNT` is set to 8 to balance between performance and quality. * Increasing the number of sectors beyond 8 would significantly increase the computational cost without providing * a proportional improvement in the visual quality of the effect. Therefore, 8 is chosen as a practical upper limit * to ensure the effect remains performant while still delivering high-quality results. */ const fragmentShader = ` uniform float radius; uniform int sectorCount; const int MAX_SECTOR_COUNT = 8; float polynomialWeight(float x, float y, float eta, float lambda) { float polyValue = (x + eta) - lambda * (y * y); return max(0.0, polyValue * polyValue); } void getSectorVarianceAndAverageColor(mat2 anisotropyMat, float angle, float radius, out vec3 avgColor, out float variance) { vec3 weightedColorSum = vec3(0.0); vec3 weightedSquaredColorSum = vec3(0.0); float totalWeight = 0.0; float eta = 0.1; float lambda = 0.5; float angleStep = 0.196349; // Precompute angle step float halfAngleRange = 0.392699; // Precompute half angle range float cosAngle = cos(angle); float sinAngle = sin(angle); for (float r = 1.0; r <= radius; r += 1.0) { float rCosAngle = r * cosAngle; float rSinAngle = r * sinAngle; for (float a = -halfAngleRange; a <= halfAngleRange; a += angleStep) { float cosA = cos(a); float sinA = sin(a); vec2 sampleOffset = vec2(rCosAngle * cosA - rSinAngle * sinA, rCosAngle * sinA + rSinAngle * cosA) / resolution; sampleOffset *= anisotropyMat; vec3 color = texture2D(inputBuffer, vUv + sampleOffset).rgb; float weight = polynomialWeight(sampleOffset.x, sampleOffset.y, eta, lambda); weightedColorSum += color * weight; weightedSquaredColorSum += color * color * weight; totalWeight += weight; } } // Calculate average color and variance avgColor = weightedColorSum / totalWeight; vec3 varianceRes = (weightedSquaredColorSum / totalWeight) - (avgColor * avgColor); variance = dot(varianceRes, vec3(0.299, 0.587, 0.114)); // Convert to luminance } vec4 getDominantOrientation(vec4 structureTensor) { float Jxx = structureTensor.r; float Jyy = structureTensor.g; float Jxy = structureTensor.b; float trace = Jxx + Jyy; float det = Jxx * Jyy - Jxy * Jxy; float lambda1 = 0.5 * (trace + sqrt(trace * trace - 4.0 * det)); float lambda2 = 0.5 * (trace - sqrt(trace * trace - 4.0 * det)); float dominantOrientation = atan(2.0 * Jxy, Jxx - Jyy) / 2.0; return vec4(dominantOrientation, lambda1, lambda2, 0.0); } void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { vec4 structureTensor = texture2D(inputBuffer, uv); vec3 sectorAvgColors[MAX_SECTOR_COUNT]; float sectorVariances[MAX_SECTOR_COUNT]; vec4 orientationAndAnisotropy = getDominantOrientation(structureTensor); vec2 orientation = orientationAndAnisotropy.xy; float anisotropy = (orientationAndAnisotropy.z - orientationAndAnisotropy.w) / (orientationAndAnisotropy.z + orientationAndAnisotropy.w + 1e-7); float alpha = 25.0; float scaleX = alpha / (anisotropy + alpha); float scaleY = (anisotropy + alpha) / alpha; mat2 anisotropyMat = mat2(orientation.x, -orientation.y, orientation.y, orientation.x) * mat2(scaleX, 0.0, 0.0, scaleY); for (int i = 0; i < sectorCount; i++) { float angle = float(i) * 6.28318 / float(sectorCount); // 2π / sectorCount getSectorVarianceAndAverageColor(anisotropyMat, angle, float(radius), sectorAvgColors[i], sectorVariances[i]); } float minVariance = sectorVariances[0]; vec3 finalColor = sectorAvgColors[0]; for (int i = 1; i < sectorCount; i++) { if (sectorVariances[i] < minVariance) { minVariance = sectorVariances[i]; finalColor = sectorAvgColors[i]; } } outputColor = vec4(finalColor, inputColor.a); } `; var KuwaharaEffect = class extends Effect { /** * Creates a new KuwaharaEffect instance. * * @param {object} [options] - Configuration options for the effect. * @param {BlendFunction} [options.blendFunction] - Blend mode. * @param {number} [options.radius] - Intensity of the effect. * @param {number} [options.sectorCount] - Number of sectors. * */ constructor({ blendFunction = BlendFunction.NORMAL, radius = 1, sectorCount = 4 } = {}) { super("KuwaharaEffect", fragmentShader, { blendFunction, uniforms: new Map([["radius", new Uniform(radius)], ["sectorCount", new Uniform(sectorCount)]]) }); } /** * The radius. * * @type {number} */ get radius() { return this.uniforms.get("radius")?.value; } set radius(value) { this.uniforms.get("radius").value = value; } /** * The sector count. * * @type {number} */ get sectorCount() { return this.uniforms.get("sectorCount")?.value; } set sectorCount(value) { this.uniforms.get("sectorCount").value = value; } }; //#endregion //#region src/core/pmndrs/KuwaharaPmndrs.vue?vue&type=script&setup=true&lang.ts var KuwaharaPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "KuwaharaPmndrs", props: { blendFunction: { type: null, required: false }, radius: { type: Number, required: false }, sectorCount: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new KuwaharaEffect(props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.radius, "radius"], [() => props.sectorCount, "sectorCount"] ], effect, () => new KuwaharaEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/KuwaharaPmndrs.vue var KuwaharaPmndrs_default = KuwaharaPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/ColorAveragePmndrs.vue?vue&type=script&setup=true&lang.ts var ColorAveragePmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "ColorAveragePmndrs", props: { blendFunction: { type: null, required: false }, opacity: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new ColorAverageEffect(props.blendFunction), props); __expose({ pass, effect }); makePropWatcher(() => props.blendFunction, effect, "blendMode.blendFunction", () => new ColorAverageEffect()); watch([effect, () => props.opacity], () => { if (!effect.value) return; if (props.opacity !== void 0) effect.value?.blendMode.setOpacity(props.opacity); else { const plainEffect = new ColorAverageEffect(); effect.value?.blendMode.setOpacity(plainEffect.blendMode.getOpacity()); plainEffect.dispose(); } }); return () => {}; } }); //#endregion //#region src/core/pmndrs/ColorAveragePmndrs.vue var ColorAveragePmndrs_default = ColorAveragePmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/LensDistortionPmndrs.vue?vue&type=script&setup=true&lang.ts var LensDistortionPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "LensDistortionPmndrs", props: { distortion: { type: [Object, Array], required: false }, principalPoint: { type: [Object, Array], required: false }, focalLength: { type: [Object, Array], required: false }, skew: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new LensDistortionEffect({ ...props, distortion: props.distortion ? Array.isArray(props.distortion) ? new Vector2(...props.distortion) : props.distortion : new Vector2(), principalPoint: props.principalPoint ? Array.isArray(props.principalPoint) ? new Vector2(...props.principalPoint) : props.principalPoint : new Vector2(), focalLength: props.focalLength ? Array.isArray(props.focalLength) ? new Vector2(...props.focalLength) : props.focalLength : new Vector2() }), props); __expose({ pass, effect }); makePropWatchersUsingAllProps(props, effect, () => new LensDistortionEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/LensDistortionPmndrs.vue var LensDistortionPmndrs_default = LensDistortionPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/ShockWavePmndrs.vue?vue&type=script&setup=true&lang.ts var ShockWavePmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "ShockWavePmndrs", props: { position: { type: [Object, Array], required: false }, amplitude: { type: Number, required: false }, speed: { type: Number, required: false }, maxRadius: { type: Number, required: false }, waveSize: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { camera } = useTres(); const { pass, effect } = useEffectPmndrs(() => new ShockWaveEffect(camera.value, Array.isArray(props.position) ? new Vector3(...props.position) : props.position, props), props); __expose({ pass, effect }); watch(() => props.position, (newPosition) => { if (!effect.value) return; if (Array.isArray(newPosition)) effect.value.position.set(...newPosition); else if (newPosition instanceof Vector3) effect.value.position.copy(newPosition); }, { immediate: true }); makePropWatchers([ [() => props.amplitude, "amplitude"], [() => props.waveSize, "waveSize"], [() => props.maxRadius, "maxRadius"], [() => props.speed, "speed"] ], effect, () => new ShockWaveEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/ShockWavePmndrs.vue var ShockWavePmndrs_default = ShockWavePmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/TiltShiftPmndrs.vue?vue&type=script&setup=true&lang.ts var TiltShiftPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "TiltShiftPmndrs", props: { blendFunction: { type: null, required: false }, offset: { type: Number, required: false }, rotation: { type: Number, required: false }, focusArea: { type: Number, required: false }, feather: { type: Number, required: false }, kernelSize: { type: null, required: false }, resolutionScale: { type: Number, required: false }, resolutionX: { type: Number, required: false }, resolutionY: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new TiltShiftEffect(props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.offset, "offset"], [() => props.rotation, "rotation"], [() => props.focusArea, "focusArea"], [() => props.feather, "feather"], [() => props.kernelSize, "kernelSize"], [() => props.resolutionScale, "resolution.scale"], [() => props.resolutionX, "resolution.width"], [() => props.resolutionY, "resolution.height"] ], effect, () => new TiltShiftEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/TiltShiftPmndrs.vue var TiltShiftPmndrs_default = TiltShiftPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/DotScreenPmndrs.vue?vue&type=script&setup=true&lang.ts var DotScreenPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "DotScreenPmndrs", props: { angle: { type: Number, required: false }, scale: { type: Number, required: false }, blendFunction: { type: null, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new DotScreenEffect(props), props); __expose({ pass, effect }); makePropWatchers([ [() => props.blendFunction, "blendMode.blendFunction"], [() => props.angle, "angle"], [() => props.scale, "scale"] ], effect, () => new DotScreenEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/DotScreenPmndrs.vue var DotScreenPmndrs_default = DotScreenPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/SepiaPmndrs.vue?vue&type=script&setup=true&lang.ts var SepiaPmndrs_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "SepiaPmndrs", props: { blendFunction: { type: null, required: false }, intensity: { type: Number, required: false } }, setup(__props, { expose: __expose }) { const props = __props; const { pass, effect } = useEffectPmndrs(() => new SepiaEffect(props), props); __expose({ pass, effect }); makePropWatchers([[() => props.blendFunction, "blendMode.blendFunction"], [() => props.intensity, "intensity"]], effect, () => new SepiaEffect()); return () => {}; } }); //#endregion //#region src/core/pmndrs/SepiaPmndrs.vue var SepiaPmndrs_default = SepiaPmndrs_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/core/pmndrs/custom/linocut/index.ts /** * LinocutEffect - A custom effect for applying a linocut shader effect. */ var LinocutEffect = class extends Effect { /** * Creates a new LinocutEffect instance. * * @param {LinocutPmndrsProps} [options] - Configuration options for the effect. * */ constructor({ blendFunction = BlendFunction.NORMAL, scale = .85, noiseScale = 0, center = [.5, .5], rotation = 0 } = {}) { const centerVec = Array.isArray(center) ? new Vector2().fromArray(center) : center; super("LinocutEffect", ` uniform float scale; uniform float noiseScale; uniform vec2 center; uniform float rotation; float luma(vec3 color) { return dot(color, vec3(0.299, 0.587, 0.114)); } float luma(vec4 color) { return dot(color.rgb, vec3(0.299, 0.587, 0.114)); } // Simple pseudo-random noise function float noise(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123); } void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { // Calculate the center based on center vec2 fragCoord = uv * resolution.xy; // Apply rotation to the coordinates vec2 d = fragCoord - center * resolution.xy; mat2 rotMat = mat2(cos(rotation), -sin(rotation), sin(rotation), cos(rotation)); vec2 rotatedD = d * rotMat; // Calculate radial distance and angle float r = length(rotatedD) / (1000.0 / max(scale, 0.01)); // Normalization to avoid artifacts float a = atan(rotatedD.y, rotatedD.x) + scale * (0.5 - r) / 0.5; // Calculate transformed coordinates vec2 uvt = center * resolution.xy + r * vec2(cos(a), sin(a)); // Normalize UV coordinates vec2 uv2 = fragCoord / resolution.xy; // Generate sinusoidal line patterns float c = (0.75 + 0.25 * sin(uvt.x * 1000.0 * max(scale, 0.01))); // Prevent excessive distortions // Load the texture and convert to grayscale vec4 color = texture(inputBuffer, uv2); color.rgb = color.rgb * color.rgb; // Convert from sRGB to linear float l = luma(color); // Add noise based on noiseScale float n = noise(uv2 * 10.0); // Generate noise l += noiseScale * (n - 0.5); // Apply noise as a perturbation // Apply smoothing to achieve the linocut effect float f = smoothstep(0.5 * c, c, l); f = smoothstep(0.0, 0.5, f); // Convert the final value back to sRGB f = sqrt(f); // Output the final color in black and white outputColor = vec4(vec3(f), 1.0); } `, { blendFunction, uniforms: new Map([ ["scale", new Uniform(scale)], ["noiseScale", new Uniform(noiseScale)], ["center", new Uniform(centerVec)], ["rotation", new Uniform(rotation)] ]) }); } get scale() { return this.uniforms.get("scale")?.value; } set scale(value) { this.uniforms.get("scale").value = value; } get noiseScale() { return this.uniforms.get("noiseScale")?.value; } set noiseScale(value) { this.uniforms.get("noiseScale").value = value; } get center() { return this.uniforms.get("center")?.value; } set center(value) { this.uniforms.get("center").value = Array.isArray(value) ? new Vecto