diff --git a/examples/files.json b/examples/files.json
index ac769186754ed8..0a9bdf2cfa0c72 100644
--- a/examples/files.json
+++ b/examples/files.json
@@ -375,6 +375,7 @@
"webgpu_postprocessing_anamorphic",
"webgpu_postprocessing_ao",
"webgpu_postprocessing_dof",
+ "webgpu_postprocessing_pixel",
"webgpu_postprocessing_fxaa",
"webgpu_postprocessing_sobel",
"webgpu_postprocessing",
diff --git a/examples/screenshots/webgpu_postprocessing_pixel.jpg b/examples/screenshots/webgpu_postprocessing_pixel.jpg
new file mode 100644
index 00000000000000..85dc9edb420f27
Binary files /dev/null and b/examples/screenshots/webgpu_postprocessing_pixel.jpg differ
diff --git a/examples/webgpu_postprocessing_pixel.html b/examples/webgpu_postprocessing_pixel.html
new file mode 100644
index 00000000000000..a44bc4867070b1
--- /dev/null
+++ b/examples/webgpu_postprocessing_pixel.html
@@ -0,0 +1,277 @@
+
+
+
+ three.js webgpu - postprocessing pixel
+
+
+
+
+
+
+
+
three.js - Node based pixelation pass with optional single pixel outlines by
+
Kody King
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/nodes/Nodes.js b/src/nodes/Nodes.js
index caec0e8a5bdb31..f5bd5c7ab08254 100644
--- a/src/nodes/Nodes.js
+++ b/src/nodes/Nodes.js
@@ -137,6 +137,7 @@ export { default as Lut3DNode, lut3D } from './display/Lut3DNode.js';
export { default as GTAONode, ao } from './display/GTAONode.js';
export { default as FXAANode, fxaa } from './display/FXAANode.js';
export { default as RenderOutputNode, renderOutput } from './display/RenderOutputNode.js';
+export { default as PixelationPassNode, pixelationPass } from './display/PixelationPassNode.js';
export { default as PassNode, pass, passTexture, depthPass } from './display/PassNode.js';
diff --git a/src/nodes/display/PixelationPassNode.js b/src/nodes/display/PixelationPassNode.js
new file mode 100644
index 00000000000000..625f1cd38209df
--- /dev/null
+++ b/src/nodes/display/PixelationPassNode.js
@@ -0,0 +1,201 @@
+import TempNode from '../core/TempNode.js';
+import { uv } from '../accessors/UVNode.js';
+import { addNodeElement, tslFn, nodeObject, vec2, vec3, float, If } from '../shadernode/ShaderNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { uniform } from '../core/UniformNode.js';
+import { dot, clamp, smoothstep, sign, step, floor } from '../math/MathNode.js';
+import { Vector4 } from '../../math/Vector4.js';
+import { output, property } from '../core/PropertyNode.js';
+import PassNode from './PassNode.js';
+import { mrt } from '../core/MRTNode.js';
+import { normalView } from '../accessors/NormalNode.js';
+import { NearestFilter } from '../../constants.js';
+
+class PixelationNode extends TempNode {
+
+ constructor( textureNode, depthNode, normalNode, pixelSize, normalEdgeStrength, depthEdgeStrength ) {
+
+ super();
+
+ // Input textures
+
+ this.textureNode = textureNode;
+ this.depthNode = depthNode;
+ this.normalNode = normalNode;
+
+ // Input uniforms
+
+ this.pixelSize = pixelSize;
+ this.normalEdgeStrength = normalEdgeStrength;
+ this.depthEdgeStrength = depthEdgeStrength;
+
+ // Private uniforms
+
+ this._resolution = uniform( new Vector4() );
+
+ this.updateBeforeType = NodeUpdateType.RENDER;
+
+ }
+
+ updateBefore() {
+
+ const map = this.textureNode.value;
+
+ const width = map.image.width;
+ const height = map.image.height;
+
+ this._resolution.value.set( width, height, 1 / width, 1 / height );
+
+ }
+
+ setup() {
+
+ const { textureNode, depthNode, normalNode } = this;
+
+ const uvNodeTexture = textureNode.uvNode || uv();
+ const uvNodeDepth = depthNode.uvNode || uv();
+ const uvNodeNormal = normalNode.uvNode || uv();
+
+ const sampleTexture = () => textureNode.uv( uvNodeTexture );
+
+ const sampleDepth = ( x, y ) => depthNode.uv( uvNodeDepth.add( vec2( x, y ).mul( this._resolution.zw ) ) ).r;
+
+ const sampleNormal = ( x, y ) => normalNode.uv( uvNodeNormal.add( vec2( x, y ).mul( this._resolution.zw ) ) ).rgb.normalize();
+
+ const depthEdgeIndicator = ( depth ) => {
+
+ const diff = property( 'float', 'diff' );
+ diff.addAssign( clamp( sampleDepth( 1, 0 ).sub( depth ) ) );
+ diff.addAssign( clamp( sampleDepth( - 1, 0 ).sub( depth ) ) );
+ diff.addAssign( clamp( sampleDepth( 0, 1 ).sub( depth ) ) );
+ diff.addAssign( clamp( sampleDepth( 0, - 1 ).sub( depth ) ) );
+
+ return floor( smoothstep( 0.01, 0.02, diff ).mul( 2 ) ).div( 2 );
+
+ };
+
+ const neighborNormalEdgeIndicator = ( x, y, depth, normal ) => {
+
+ const depthDiff = sampleDepth( x, y ).sub( depth );
+ const neighborNormal = sampleNormal( x, y );
+
+ // Edge pixels should yield to faces who's normals are closer to the bias normal.
+
+ const normalEdgeBias = vec3( 1, 1, 1 ); // This should probably be a parameter.
+ const normalDiff = dot( normal.sub( neighborNormal ), normalEdgeBias );
+ const normalIndicator = clamp( smoothstep( - 0.01, 0.01, normalDiff ), 0.0, 1.0 );
+
+ // Only the shallower pixel should detect the normal edge.
+
+ const depthIndicator = clamp( sign( depthDiff.mul( .25 ).add( .0025 ) ), 0.0, 1.0 );
+
+ return float( 1.0 ).sub( dot( normal, neighborNormal ) ).mul( depthIndicator ).mul( normalIndicator );
+
+ };
+
+ const normalEdgeIndicator = ( depth, normal ) => {
+
+ const indicator = property( 'float', 'indicator' );
+
+ indicator.addAssign( neighborNormalEdgeIndicator( 0, - 1, depth, normal ) );
+ indicator.addAssign( neighborNormalEdgeIndicator( 0, 1, depth, normal ) );
+ indicator.addAssign( neighborNormalEdgeIndicator( - 1, 0, depth, normal ) );
+ indicator.addAssign( neighborNormalEdgeIndicator( 1, 0, depth, normal ) );
+
+ return step( 0.1, indicator );
+
+ };
+
+ const pixelation = tslFn( () => {
+
+ const texel = sampleTexture();
+
+ const depth = property( 'float', 'depth' );
+ const normal = property( 'vec3', 'normal' );
+
+ If( this.depthEdgeStrength.greaterThan( 0.0 ).or( this.normalEdgeStrength.greaterThan( 0.0 ) ), () => {
+
+ depth.assign( sampleDepth( 0, 0 ) );
+ normal.assign( sampleNormal( 0, 0 ) );
+
+ } );
+
+ const dei = property( 'float', 'dei' );
+
+ If( this.depthEdgeStrength.greaterThan( 0.0 ), () => {
+
+ dei.assign( depthEdgeIndicator( depth ) );
+
+ } );
+
+ const nei = property( 'float', 'nei' );
+
+ If( this.normalEdgeStrength.greaterThan( 0.0 ), () => {
+
+ nei.assign( normalEdgeIndicator( depth, normal ) );
+
+ } );
+
+ const strength = dei.greaterThan( 0 ).cond( float( 1.0 ).sub( dei.mul( this.depthEdgeStrength ) ), nei.mul( this.normalEdgeStrength ).add( 1 ) );
+
+ return texel.mul( strength );
+
+ } );
+
+ const outputNode = pixelation();
+
+ return outputNode;
+
+ }
+
+}
+
+const pixelation = ( node, depthNode, normalNode, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 ) => nodeObject( new PixelationNode( nodeObject( node ).toTexture(), nodeObject( depthNode ).toTexture(), nodeObject( normalNode ).toTexture(), nodeObject( pixelSize ), nodeObject( normalEdgeStrength ), nodeObject( depthEdgeStrength ) ) );
+
+addNodeElement( 'pixelation', pixelation );
+
+class PixelationPassNode extends PassNode {
+
+ constructor( scene, camera, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 ) {
+
+ super( 'color', scene, camera, { minFilter: NearestFilter, magFilter: NearestFilter } );
+
+ this.pixelSize = pixelSize;
+ this.normalEdgeStrength = normalEdgeStrength;
+ this.depthEdgeStrength = depthEdgeStrength;
+
+ this.isPixelationPassNode = true;
+
+ this._mrt = mrt( {
+ output: output,
+ normal: normalView
+ } );
+
+ }
+
+ setSize( width, height ) {
+
+ const pixelSize = this.pixelSize.value ? this.pixelSize.value : this.pixelSize;
+
+ const adjustedWidth = Math.floor( width / pixelSize );
+ const adjustedHeight = Math.floor( height / pixelSize );
+
+ super.setSize( adjustedWidth, adjustedHeight );
+
+ }
+
+ setup() {
+
+ const color = super.getTextureNode( 'output' );
+ const depth = super.getTextureNode( 'depth' );
+ const normal = super.getTextureNode( 'normal' );
+
+ return pixelation( color, depth, normal, this.pixelSize, this.normalEdgeStrength, this.depthEdgeStrength );
+
+ }
+
+}
+
+export const pixelationPass = ( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) => nodeObject( new PixelationPassNode( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) );
+
+export default PixelationPassNode;
diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js
index 90f534583296d9..17e071bb3f3b9e 100644
--- a/test/e2e/puppeteer.js
+++ b/test/e2e/puppeteer.js
@@ -126,6 +126,7 @@ const exceptionList = [
// WebGPURenderer: Unknown problem
'webgpu_postprocessing_afterimage',
'webgpu_postprocessing_3dlut',
+ "webgpu_postprocessing_pixel",
'webgpu_postprocessing_ao',
'webgpu_backdrop_water',
'webgpu_camera_logarithmicdepthbuffer',