Spaces:
Running
Running
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; | |