import { NearestFilter, Vector4, TempNode, NodeUpdateType, PassNode } from 'three/webgpu';
import { nodeObject, Fn, float, uv, uniform, convertToTexture, vec2, vec3, clamp, floor, dot, smoothstep, If, sign, step, mrt, output, normalView, property } from 'three/tsl';
/** @module PixelationPassNode **/
/**
* A inner node definition that implements the actual pixelation TSL code.
*
* @inner
* @augments TempNode
*/
class PixelationNode extends TempNode {
static get type() {
return 'PixelationNode';
}
/**
* Constructs a new pixelation node.
*
* @param {TextureNode} textureNode - The texture node that represents the beauty pass.
* @param {TextureNode} depthNode - The texture that represents the beauty's depth.
* @param {TextureNode} normalNode - The texture that represents the beauty's normals.
* @param {Node<float>} pixelSize - The pixel size.
* @param {Node<float>} normalEdgeStrength - The normal edge strength.
* @param {Node<float>} depthEdgeStrength - The depth edge strength.
*/
constructor( textureNode, depthNode, normalNode, pixelSize, normalEdgeStrength, depthEdgeStrength ) {
super( 'vec4' );
/**
* The texture node that represents the beauty pass.
*
* @type {TextureNode}
*/
this.textureNode = textureNode;
/**
* The texture that represents the beauty's depth.
*
* @type {TextureNode}
*/
this.depthNode = depthNode;
/**
* The texture that represents the beauty's normals.
*
* @type {TextureNode}
*/
this.normalNode = normalNode;
/**
* The pixel size.
*
* @type {Node<float>}
*/
this.pixelSize = pixelSize;
/**
* The pixel size.
*
* @type {Node<float>}
*/
this.normalEdgeStrength = normalEdgeStrength;
/**
* The depth edge strength.
*
* @type {Node<float>}
*/
this.depthEdgeStrength = depthEdgeStrength;
/**
* Uniform node that represents the resolution.
*
* @type {Node<vec4>}
*/
this._resolution = uniform( new Vector4() );
/**
* The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates
* its internal uniforms once per frame in `updateBefore()`.
*
* @type {String}
* @default 'frame'
*/
this.updateBeforeType = NodeUpdateType.FRAME;
}
/**
* This method is used to update uniforms once per frame.
*
* @param {NodeFrame} frame - The current node frame.
*/
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 );
}
/**
* This method is used to setup the effect's TSL code.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {ShaderCallNodeInternal}
*/
setup() {
const { textureNode, depthNode, normalNode } = this;
const uvNodeTexture = textureNode.uvNode || uv();
const uvNodeDepth = depthNode.uvNode || uv();
const uvNodeNormal = normalNode.uvNode || uv();
const sampleTexture = () => textureNode.sample( uvNodeTexture );
const sampleDepth = ( x, y ) => depthNode.sample( uvNodeDepth.add( vec2( x, y ).mul( this._resolution.zw ) ) ).r;
const sampleNormal = ( x, y ) => normalNode.sample( 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 = Fn( () => {
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 ).select( 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( convertToTexture( node ), convertToTexture( depthNode ), convertToTexture( normalNode ), nodeObject( pixelSize ), nodeObject( normalEdgeStrength ), nodeObject( depthEdgeStrength ) ) );
/**
* A special render pass node that renders the scene with a pixelation effect.
*
* @augments PassNode
*/
class PixelationPassNode extends PassNode {
static get type() {
return 'PixelationPassNode';
}
/**
* Constructs a new pixelation pass node.
*
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera to render the scene with.
* @param {Node<float> | Number} [pixelSize=6] - The pixel size.
* @param {Node<float> | Number} [normalEdgeStrength=03] - The normal edge strength.
* @param {Node<float> | Number} [depthEdgeStrength=03] - The depth edge strength.
*/
constructor( scene, camera, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 ) {
super( PassNode.COLOR, scene, camera, { minFilter: NearestFilter, magFilter: NearestFilter } );
/**
* The pixel size.
*
* @type {Number}
* @default 6
*/
this.pixelSize = pixelSize;
/**
* The normal edge strength.
*
* @type {Number}
* @default 0.3
*/
this.normalEdgeStrength = normalEdgeStrength;
/**
* The depth edge strength.
*
* @type {Number}
* @default 0.4
*/
this.depthEdgeStrength = depthEdgeStrength;
/**
* This flag can be used for type testing.
*
* @type {Boolean}
* @readonly
* @default true
*/
this.isPixelationPassNode = true;
this._mrt = mrt( {
output: output,
normal: normalView
} );
}
/**
* Sets the size of the pass.
*
* @param {Number} width - The width of the pass.
* @param {Number} height - The height of the pass.
*/
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 );
}
/**
* This method is used to setup the effect's TSL code.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {PixelationNode}
*/
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 );
}
}
/**
* TSL function for creating a pixelation render pass node for post processing.
*
* @function
* @param {Scene} scene - The scene to render.
* @param {Camera} camera - The camera to render the scene with.
* @param {Node<float> | Number} [pixelSize=6] - The pixel size.
* @param {Node<float> | Number} [normalEdgeStrength=03] - The normal edge strength.
* @param {Node<float> | Number} [depthEdgeStrength=03] - The depth edge strength.
* @returns {PixelationPassNode}
*/
export const pixelationPass = ( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) => nodeObject( new PixelationPassNode( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) );
export default PixelationPassNode;