diff --git a/src/effects/HalftoneEffect.ts b/src/effects/HalftoneEffect.ts index 6c64d2cac..40d2b1ae3 100644 --- a/src/effects/HalftoneEffect.ts +++ b/src/effects/HalftoneEffect.ts @@ -1,9 +1,9 @@ +import { Uniform, Vector3 } from "three"; import { HalftoneShape } from "../enums/HalftoneShape.js"; +import { LinearDodgeBlendFunction } from "./blending/blend-functions/LinearDodgeBlendFunction.js"; import { Effect } from "./Effect.js"; -import { OverlayBlendFunction } from "./blending/blend-functions/OverlayBlendFunction.js"; import fragmentShader from "./shaders/halftone.frag"; -import { Uniform, Vector3 } from "three"; /** * HalftoneEffect options. @@ -12,13 +12,73 @@ import { Uniform, Vector3 } from "three"; */ export interface HalftoneEffectOptions { + + /** + * The halftone shape. + * + * @defaultValue {@link HalftoneShape.DOT} + */ + shape?: HalftoneShape; + + /** + * The pattern radius. + * + * @defaultValue 6 + */ + radius?: number; + + /** + * The grid rotation for all color channels in radians. + * + * This setting yields better performance compared to individual rotations per channel. + * + * @defaultValue 0 + */ + + rotation?: number; + + /** + * The grid rotation for the red channel in radians. + * + * @defaultValue {@link rotation} + */ + rotationR?: number; + + /** + * The grid rotation for the green channel in radians. + * + * @defaultValue {@link rotationR} + */ + rotationG?: number; + + /** + * The grid rotation for the blue channel in radians. + * + * @defaultValue {@link rotationG} + */ + rotationB?: number; + + /** + * The halftone scatter factor. + * + * @defaultValue 0 + */ + scatterFactor?: number; + + /** + * The sample count. + * + * @defaultValue 8 + */ + samples?: number; + } /** @@ -39,11 +99,12 @@ export class HalftoneEffect extends Effect { */ constructor({ - shape = HalftoneShape.ELLIPSE, + shape = HalftoneShape.DOT, radius = 6, - rotationR = 14, - rotationG = 45, - rotationB = 30, + rotation = 0, + rotationR = rotation, + rotationG = rotationR, + rotationB = rotationG, scatterFactor = 0, samples = 8 }: HalftoneEffectOptions = {}) { @@ -51,20 +112,58 @@ export class HalftoneEffect extends Effect { super("HalftoneEffect"); this.fragmentShader = fragmentShader; - this.samples = samples; - this.shape = shape; - - this.blendMode.blendFunction = new OverlayBlendFunction(); + this.blendMode.blendFunction = new LinearDodgeBlendFunction(); const uniforms = this.input.uniforms; uniforms.set("radius", new Uniform(radius)); uniforms.set("rotationRGB", new Uniform(new Vector3(rotationR, rotationG, rotationB))); uniforms.set("scatterFactor", new Uniform(scatterFactor)); + this.shape = shape; + this.samples = samples; + + } + + /** + * The halftone shape. + */ + + get shape() { + + return this.input.defines.get("SHAPE") as number; + + } + + set shape(value: HalftoneShape) { + + this.input.defines.set("SHAPE", value); + this.setChanged(); + } /** - * The halftone dot radius. + * The amount of samples. + */ + + get samples(): number { + + return this.input.defines.get("SAMPLES") as number; + + } + + set samples(value: number) { + + value = Math.max(value, 1); + + this.input.defines.set("SAMPLES", value); + this.input.defines.set("INV_SAMPLES", (1.0 / value).toFixed(9)); + this.input.defines.set("INV_SAMPLES_PLUS_ONE", (1.0 / (value + 1.0)).toFixed(9)); + this.setChanged(); + + } + + /** + * The pattern radius. */ get radius() { @@ -80,7 +179,7 @@ export class HalftoneEffect extends Effect { } /** - * The halftone dot scatterFactor. + * The halftone scatter factor. */ get scatterFactor() { @@ -96,7 +195,26 @@ export class HalftoneEffect extends Effect { } /** - * The halftone dot grid rotation in the red channel. + * The grid rotation in radians. + */ + + get rotation() { + + const rotationRGB = this.input.uniforms.get("rotationRGB")!.value as Vector3; + return rotationRGB.x; + + } + + set rotation(value: number) { + + const rotationRGB = this.input.uniforms.get("rotationRGB")!.value as Vector3; + rotationRGB.setScalar(value); + this.updateRGBRotation(); + + } + + /** + * The grid rotation for the red channel in radians. */ get rotationR() { @@ -110,12 +228,12 @@ export class HalftoneEffect extends Effect { const rotationRGB = this.input.uniforms.get("rotationRGB")!.value as Vector3; rotationRGB.x = value; - this.input.uniforms.get("rotationRGB")!.value = rotationRGB; + this.updateRGBRotation(); } /** - * The halftone dot grid rotation in the green channel. + * The grid rotation for the green channel in radians. */ get rotationG() { @@ -129,12 +247,12 @@ export class HalftoneEffect extends Effect { const rotationRGB = this.input.uniforms.get("rotationRGB")!.value as Vector3; rotationRGB.y = value; - this.input.uniforms.get("rotationRGB")!.value = rotationRGB; + this.updateRGBRotation(); } /** - * The halftone dot grid rotation in the red channel. + * The grid rotation for the blue channel in radians. */ get rotationB() { @@ -148,42 +266,39 @@ export class HalftoneEffect extends Effect { const rotationRGB = this.input.uniforms.get("rotationRGB")!.value as Vector3; rotationRGB.z = value; - this.input.uniforms.get("rotationRGB")!.value = rotationRGB; + this.updateRGBRotation(); } /** - * The halftone dot shape. + * Enables or disables RGB rotation based on the current rotation settings. */ - get shape() { + private updateRGBRotation() { - return this.input.defines.get("SHAPE") as number; + const currentlyEnabled = this.input.defines.has("RGB_ROTATION"); - } + const shouldBeEnabled = ( + this.rotationR !== this.rotationG || + this.rotationR !== this.rotationB || + this.rotationG !== this.rotationB + ); - set shape(value: HalftoneShape) { + if(shouldBeEnabled) { + this.input.defines.set("RGB_ROTATION", true); - this.input.defines.set("SHAPE", value); - this.setChanged(); - - } - - /** - * The amount of samples. - */ + } else { - get samples(): number { + this.input.defines.delete("RGB_ROTATION"); - return this.input.defines.get("SAMPLES") as number; + } - } + if(currentlyEnabled !== shouldBeEnabled) { - set samples(value: number) { + this.setChanged(); - this.input.defines.set("SAMPLES", value); - this.setChanged(); + } } diff --git a/src/effects/shaders/halftone.frag b/src/effects/shaders/halftone.frag index ad5ba63ab..79eb07aea 100644 --- a/src/effects/shaders/halftone.frag +++ b/src/effects/shaders/halftone.frag @@ -5,197 +5,212 @@ #define SHAPE_LINE 3 #define SHAPE_SQUARE 4 -uniform float radius; uniform vec3 rotationRGB; -uniform float scatter; -uniform int shape; +uniform float radius; +uniform float scatterFactor; -float hypot(float x, float y) { +struct Cell { + vec2 n; + vec2 p1; + vec2 p2; + vec2 p3; + vec2 p4; + float sample1; + float sample2; + float sample3; + float sample4; +}; - // vector magnitude - return sqrt(x * x + y * y); +float getPattern(float cellSample, vec2 coord, vec2 n, vec2 p, float angle, float maxRadius) { -} + float magnetude = length(coord - p); + float r = cellSample; -float distanceToDotRadius(float channel, vec2 coord, vec2 normal, vec2 p, float angle, float radMax) { + #if SHAPE == SHAPE_DOT - // apply shape-specific transforms - float dist = hypot(coord.x - p.x, coord.y - p.y); - float rad = channel; + r = pow(abs(r), 1.125) * maxRadius; + + #elif SHAPE == SHAPE_ELLIPSE - # if SHAPE == SHAPE_DOT + r = pow(abs(r), 1.125) * maxRadius; - rad = pow(abs(rad), 1.125) * radMax; - - # elif SHAPE == SHAPE_ELLIPSE + if(magnetude != 0.0) { + + float dotP = abs(dot((p - coord) / magnetude, n)); - rad = pow(abs(rad), 1.125) * radMax; + magnetude = dot( + vec2(magnetude, magnetude * dotP), + vec2(1.0 - SQRT2_HALF_MINUS_ONE, SQRT2_MINUS_ONE) + ); - if (dist != 0.0) { - float dotP = abs((p.x - coord.x) / dist * normal.x + (p.y - coord.y) / dist * normal.y); - dist = (dist * (1.0 - SQRT2_HALF_MINUS_ONE)) + dotP * dist * SQRT2_MINUS_ONE; } - - # elif SHAPE == SHAPE_LINE - rad = pow(abs(rad), 1.5) * radMax; - float dotP = (p.x - coord.x) * normal.x + (p.y - coord.y) * normal.y; - dist = hypot(normal.x * dotP, normal.y * dotP); + #elif SHAPE == SHAPE_LINE + + r = pow(abs(r), 1.5) * maxRadius; + float dotP = dot(p - coord, n); + magnetude = length(n * dotP); - # elif SHAPE == SHAPE_SQUARE + #elif SHAPE == SHAPE_SQUARE float theta = atan(p.y - coord.y, p.x - coord.x) - angle; float sinT = abs(sin(theta)); float cosT = abs(cos(theta)); - rad = pow(abs(rad), 1.4); - rad = radMax * (rad + ((sinT > cosT) ? rad - sinT * rad : rad - cosT * rad)); + r = pow(abs(r), 1.4); + r += (sinT > cosT) ? r - sinT * r : r - cosT * r; + r *= maxRadius; - # endif + #endif - return rad - dist; + return r - magnetude; } -struct Cell { - - // grid sample positions - vec2 normal; - vec2 p1; - vec2 p2; - vec2 p3; - vec2 p4; - float sample1; - float sample2; - float sample3; - float sample4; - -}; - vec4 getSample(vec2 point) { - // multi-sampled point - vec4 tex = texture(gBuffer.color, point * resolution.zw); - float base = rand(vec2(floor(point.x), floor(point.y))) * PI2; - float step = PI2 / float(SAMPLES); - float dist = radius * 0.66; + vec4 texel = texture(gBuffer.color, point * resolution.zw); + float base = rand(floor(point)) * PI2; + float step = PI2 * INV_SAMPLES; + float magnetude = radius * 0.66; - for (int i = 0; i < SAMPLES; ++i) { + for(int i = 0; i < SAMPLES; ++i) { float r = base + step * float(i); - vec2 coord = point + vec2(cos(r) * dist, sin(r) * dist); - tex += texture(gBuffer.color, coord * resolution.zw); + vec2 coord = point + vec2(cos(r), sin(r)) * magnetude; + texel += texture(gBuffer.color, coord * resolution.zw); } - tex /= float(SAMPLES) + 1.0; - return tex; + texel *= INV_SAMPLES_PLUS_ONE; + return texel; } -float getDotColor(Cell c, vec2 p, int channel, float angle, float aa) { - - // get color for given point - float distC1, distC2, distC3, distC4, res; - - if (channel == 0) { - - c.sample1 = getSample(c.p1).r; - c.sample2 = getSample(c.p2).r; - c.sample3 = getSample(c.p3).r; - c.sample4 = getSample(c.p4).r; +Cell getReferenceCell(vec2 p, vec2 origin, float gridAngle, float step) { - } else if (channel == 1) { + Cell cell; - c.sample1 = getSample(c.p1).g; - c.sample2 = getSample(c.p2).g; - c.sample3 = getSample(c.p3).g; - c.sample4 = getSample(c.p4).g; + vec2 n = vec2(cos(gridAngle), sin(gridAngle)); - } else { + vec2 v = p - origin; + float dotNormal = dot(v, n); + float dotLine = dot(v, vec2(-n.y, n.x)); - c.sample1 = getSample(c.p1).b; - c.sample3 = getSample(c.p3).b; - c.sample2 = getSample(c.p2).b; - c.sample4 = getSample(c.p4).b; + float threshold = step * 0.5; + vec2 offset = n * dotNormal; - } + float offsetNormal = mod(length(offset), step); + float normalDir = (dotNormal < 0.0) ? 1.0 : -1.0; + float normalScale = (offsetNormal < threshold) ? -offsetNormal : step - offsetNormal; + normalScale *= normalDir; - distC1 = distanceToDotRadius(c.sample1, c.p1, c.normal, p, angle, radius); - distC2 = distanceToDotRadius(c.sample2, c.p2, c.normal, p, angle, radius); - distC3 = distanceToDotRadius(c.sample3, c.p3, c.normal, p, angle, radius); - distC4 = distanceToDotRadius(c.sample4, c.p4, c.normal, p, angle, radius); - res = (distC1 > 0.0) ? clamp(distC1 / aa, 0.0, 1.0) : 0.0; - res += (distC2 > 0.0) ? clamp(distC2 / aa, 0.0, 1.0) : 0.0; - res += (distC3 > 0.0) ? clamp(distC3 / aa, 0.0, 1.0) : 0.0; - res += (distC4 > 0.0) ? clamp(distC4 / aa, 0.0, 1.0) : 0.0; - res = clamp(res, 0.0, 1.0); + float offsetLine = mod(length(v - offset), step); + float lineDir = (dotLine < 0.0) ? 1.0 : -1.0; + float lineScale = (offsetLine < threshold) ? -offsetLine : step - offsetLine; + lineScale *= lineDir; - return res; + // Get the closest corner. + cell.n = n; + cell.p1 = p - n * normalScale + vec2(n.y, -n.x) * lineScale; -} + if(scatterFactor != 0.0) { -Cell getReferenceCell(vec2 p, vec2 origin, float gridAngle, float step) { + float offMag = scatterFactor * threshold * 0.5; + float offAngle = rand(floor(cell.p1)) * PI2; + cell.p1 += vec2(cos(offAngle), sin(offAngle)) * offMag; - // get containing cell - Cell c; + } - // calc grid - vec2 n = vec2(cos(gridAngle), sin(gridAngle)); - float threshold = step * 0.5; - float dotNormal = n.x * (p.x - origin.x) + n.y * (p.y - origin.y); - float dotLine = -n.y * (p.x - origin.x) + n.x * (p.y - origin.y); - vec2 offset = vec2(n.x * dotNormal, n.y * dotNormal); - float offsetNormal = mod(hypot(offset.x, offset.y), step); - float normalDir = (dotNormal < 0.0) ? 1.0 : -1.0; - float normalScale = ((offsetNormal < threshold) ? -offsetNormal : step - offsetNormal) * normalDir; - float offsetLine = mod(hypot((p.x - offset.x) - origin.x, (p.y - offset.y) - origin.y), step); - float lineDir = (dotLine < 0.0) ? 1.0 : -1.0; - float lineScale = ((offsetLine < threshold) ? -offsetLine : step - offsetLine) * lineDir; + // Find corners. + float normalStep = normalDir * ((offsetNormal < threshold) ? step : -step); + float lineStep = lineDir * ((offsetLine < threshold) ? step : -step); + cell.p2 = cell.p1 - n.xy * normalStep; + cell.p3 = cell.p1 + vec2(n.y, -n.x) * lineStep; + cell.p4 = cell.p1 - n * normalStep + vec2(n.y, -n.x) * lineStep; - // get closest corner - c.normal = n; - c.p1.x = p.x - n.x * normalScale + n.y * lineScale; - c.p1.y = p.y - n.y * normalScale - n.x * lineScale; + return cell; - // scatter - if (scatter != 0.0) { +} - float offMag = scatter * threshold * 0.5; - float offAngle = rand(vec2(floor(c.p1.x), floor(c.p1.y))) * PI2; - c.p1.x += cos(offAngle) * offMag; - c.p1.y += sin(offAngle) * offMag; +float halftone(Cell cell, vec2 p, float angle, float aa) { - } + float distC1 = getPattern(cell.sample1, cell.p1, cell.n, p, angle, radius); + float distC2 = getPattern(cell.sample2, cell.p2, cell.n, p, angle, radius); + float distC3 = getPattern(cell.sample3, cell.p3, cell.n, p, angle, radius); + float distC4 = getPattern(cell.sample4, cell.p4, cell.n, p, angle, radius); - // find corners - float normalStep = normalDir * ((offsetNormal < threshold) ? step : -step); - float lineStep = lineDir * ((offsetLine < threshold) ? step : -step); - c.p2.x = c.p1.x - n.x * normalStep; - c.p2.y = c.p1.y - n.y * normalStep; - c.p3.x = c.p1.x + n.y * lineStep; - c.p3.y = c.p1.y - n.x * lineStep; - c.p4.x = c.p1.x - n.x * normalStep + n.y * lineStep; - c.p4.y = c.p1.y - n.y * normalStep - n.x * lineStep; + float result = (distC1 > 0.0) ? clamp(distC1 * aa, 0.0, 1.0) : 0.0; + result += (distC2 > 0.0) ? clamp(distC2 * aa, 0.0, 1.0) : 0.0; + result += (distC3 > 0.0) ? clamp(distC3 * aa, 0.0, 1.0) : 0.0; + result += (distC4 > 0.0) ? clamp(distC4 * aa, 0.0, 1.0) : 0.0; + result = clamp(result, 0.0, 1.0); - return c; + return result; } vec4 mainImage(const in vec4 inputColor, const in vec2 uv, const in GData gData) { - // setup vec2 p = uv * resolution.xy; - vec2 origin = vec2(0, 0); - float aa = (radius < 2.5) ? radius * 0.5 : 1.25; - - // get channel samples - Cell cellR = getReferenceCell(p, origin, rotationRGB.r, radius); - Cell cellG = getReferenceCell(p, origin, rotationRGB.g, radius); - Cell cellB = getReferenceCell(p, origin, rotationRGB.b, radius); - float r = getDotColor(cellR, p, 0, rotationRGB.r, aa); - float g = getDotColor(cellG, p, 1, rotationRGB.g, aa); - float b = getDotColor(cellB, p, 2, rotationRGB.b, aa); - - return vec4(r, g, b, 1.0); + vec2 origin = vec2(0.0); + float aa = (radius < 2.5) ? 1.0 / (radius * 0.5) : 0.8; // 1.0 / 1.25 = 0.8 + + #ifdef RGB_ROTATION + + Cell cellR = getReferenceCell(p, origin, rotationRGB.r, radius); + Cell cellG = getReferenceCell(p, origin, rotationRGB.g, radius); + Cell cellB = getReferenceCell(p, origin, rotationRGB.b, radius); + + cellR.sample1 = getSample(cellR.p1).r; + cellR.sample2 = getSample(cellR.p2).r; + cellR.sample3 = getSample(cellR.p3).r; + cellR.sample4 = getSample(cellR.p4).r; + + cellG.sample1 = getSample(cellG.p1).g; + cellG.sample2 = getSample(cellG.p2).g; + cellG.sample3 = getSample(cellG.p3).g; + cellG.sample4 = getSample(cellG.p4).g; + + cellB.sample1 = getSample(cellB.p1).b; + cellB.sample2 = getSample(cellB.p2).b; + cellB.sample3 = getSample(cellB.p3).b; + cellB.sample4 = getSample(cellB.p4).b; + + #else + + Cell cell = getReferenceCell(p, origin, rotationRGB.r, radius); + Cell cellR = cell; + Cell cellG = cell; + Cell cellB = cell; + + vec3 sample1 = getSample(cell.p1).rgb; + vec3 sample2 = getSample(cell.p2).rgb; + vec3 sample3 = getSample(cell.p3).rgb; + vec3 sample4 = getSample(cell.p4).rgb; + + cellR.sample1 = sample1.r; + cellR.sample2 = sample2.r; + cellR.sample3 = sample3.r; + cellR.sample4 = sample4.r; + + cellG.sample1 = sample1.g; + cellG.sample2 = sample2.g; + cellG.sample3 = sample3.g; + cellG.sample4 = sample4.g; + + cellB.sample1 = sample1.b; + cellB.sample2 = sample2.b; + cellB.sample3 = sample3.b; + cellB.sample4 = sample4.b; + + #endif + + vec3 pattern = vec3( + halftone(cellR, p, rotationRGB.r, aa), + halftone(cellG, p, rotationRGB.g, aa), + halftone(cellB, p, rotationRGB.b, aa) + ); + + return vec4(pattern, inputColor.a); }