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);
 
 }