@tresjs/post-processing
Version:
Post-processing library for TresJS
1,620 lines (1,529 loc) • 82.2 kB
JavaScript
/**
* 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