123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- import { NearestFilter, RenderTarget, Vector2, RendererUtils, QuadMesh, TempNode, NodeMaterial, NodeUpdateType } from 'three/webgpu';
- import { reference, viewZToPerspectiveDepth, logarithmicDepthToViewZ, getScreenPosition, getViewPosition, sqrt, mul, div, cross, float, Continue, Break, Loop, int, max, abs, sub, If, dot, reflect, normalize, screenCoordinate, nodeObject, Fn, passTexture, uv, uniform, perspectiveDepthToViewZ, orthographicDepthToViewZ, vec2, vec3, vec4 } from 'three/tsl';
- const _quadMesh = /*@__PURE__*/ new QuadMesh();
- const _size = /*@__PURE__*/ new Vector2();
- let _rendererState;
- /**
- * Post processing node for computing screen space reflections (SSR).
- *
- * Reference: {@link https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html}
- *
- * @augments TempNode
- * @three_import import { ssr } from 'three/addons/tsl/display/SSRNode.js';
- */
- class SSRNode extends TempNode {
- static get type() {
- return 'SSRNode';
- }
- /**
- * Constructs a new SSR node.
- *
- * @param {Node<vec4>} colorNode - The node that represents the beauty pass.
- * @param {Node<float>} depthNode - A node that represents the beauty pass's depth.
- * @param {Node<vec3>} normalNode - A node that represents the beauty pass's normals.
- * @param {Node<float>} metalnessNode - A node that represents the beauty pass's metalness.
- * @param {Camera} camera - The camera the scene is rendered with.
- */
- constructor( colorNode, depthNode, normalNode, metalnessNode, camera ) {
- super( 'vec4' );
- /**
- * The node that represents the beauty pass.
- *
- * @type {Node<vec4>}
- */
- this.colorNode = colorNode;
- /**
- * A node that represents the beauty pass's depth.
- *
- * @type {Node<float>}
- */
- this.depthNode = depthNode;
- /**
- * A node that represents the beauty pass's normals.
- *
- * @type {Node<vec3>}
- */
- this.normalNode = normalNode;
- /**
- * A node that represents the beauty pass's metalness.
- *
- * @type {Node<float>}
- */
- this.metalnessNode = metalnessNode;
- /**
- * The camera the scene is rendered with.
- *
- * @type {Camera}
- */
- this.camera = camera;
- /**
- * The resolution scale. By default SSR reflections
- * are computed in half resolutions. Setting the value
- * to `1` improves quality but also results in more
- * computational overhead.
- *
- * @type {number}
- * @default 0.5
- */
- this.resolutionScale = 0.5;
- /**
- * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
- * its effect once per frame in `updateBefore()`.
- *
- * @type {string}
- * @default 'frame'
- */
- this.updateBeforeType = NodeUpdateType.FRAME;
- /**
- * The render target the SSR is rendered into.
- *
- * @private
- * @type {RenderTarget}
- */
- this._ssrRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, minFilter: NearestFilter, magFilter: NearestFilter } );
- this._ssrRenderTarget.texture.name = 'SSRNode.SSR';
- /**
- * Controls how far a fragment can reflect.
- *
- *
- * @type {UniformNode<float>}
- */
- this.maxDistance = uniform( 1 );
- /**
- * Controls the cutoff between what counts as a possible reflection hit and what does not.
- *
- * @type {UniformNode<float>}
- */
- this.thickness = uniform( 0.1 );
- /**
- * Controls the transparency of the reflected colors.
- *
- * @type {UniformNode<float>}
- */
- this.opacity = uniform( 1 );
- /**
- * Represents the projection matrix of the scene's camera.
- *
- * @private
- * @type {UniformNode<mat4>}
- */
- this._cameraProjectionMatrix = uniform( camera.projectionMatrix );
- /**
- * Represents the inverse projection matrix of the scene's camera.
- *
- * @private
- * @type {UniformNode<mat4>}
- */
- this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
- /**
- * Represents the near value of the scene's camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._cameraNear = reference( 'near', 'float', camera );
- /**
- * Represents the far value of the scene's camera.
- *
- * @private
- * @type {ReferenceNode<float>}
- */
- this._cameraFar = reference( 'far', 'float', camera );
- /**
- * Whether the scene's camera is perspective or orthographic.
- *
- * @private
- * @type {UniformNode<bool>}
- */
- this._isPerspectiveCamera = uniform( camera.isPerspectiveCamera ? 1 : 0 );
- /**
- * The resolution of the pass.
- *
- * @private
- * @type {UniformNode<vec2>}
- */
- this._resolution = uniform( new Vector2() );
- /**
- * This value is derived from the resolution and restricts
- * the maximum raymarching steps in the fragment shader.
- *
- * @private
- * @type {UniformNode<float>}
- */
- this._maxStep = uniform( 0 );
- /**
- * The material that is used to render the effect.
- *
- * @private
- * @type {NodeMaterial}
- */
- this._material = new NodeMaterial();
- this._material.name = 'SSRNode.SSR';
- /**
- * The result of the effect is represented as a separate texture node.
- *
- * @private
- * @type {PassTextureNode}
- */
- this._textureNode = passTexture( this, this._ssrRenderTarget.texture );
- }
- /**
- * Returns the result of the effect as a texture node.
- *
- * @return {PassTextureNode} A texture node that represents the result of the effect.
- */
- getTextureNode() {
- return this._textureNode;
- }
- /**
- * Sets the size of the effect.
- *
- * @param {number} width - The width of the effect.
- * @param {number} height - The height of the effect.
- */
- setSize( width, height ) {
- width = Math.round( this.resolutionScale * width );
- height = Math.round( this.resolutionScale * height );
- this._resolution.value.set( width, height );
- this._maxStep.value = Math.round( Math.sqrt( width * width + height * height ) );
- this._ssrRenderTarget.setSize( width, height );
- }
- /**
- * This method is used to render the effect once per frame.
- *
- * @param {NodeFrame} frame - The current node frame.
- */
- updateBefore( frame ) {
- const { renderer } = frame;
- _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
- const size = renderer.getDrawingBufferSize( _size );
- _quadMesh.material = this._material;
- this.setSize( size.width, size.height );
- // clear
- renderer.setMRT( null );
- renderer.setClearColor( 0x000000, 0 );
- // ssr
- renderer.setRenderTarget( this._ssrRenderTarget );
- _quadMesh.render( renderer );
- // restore
- RendererUtils.restoreRendererState( renderer, _rendererState );
- }
- /**
- * This method is used to setup the effect's TSL code.
- *
- * @param {NodeBuilder} builder - The current node builder.
- * @return {PassTextureNode}
- */
- setup( builder ) {
- const uvNode = uv();
- const pointToLineDistance = Fn( ( [ point, linePointA, linePointB ] )=> {
- // https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html
- return cross( point.sub( linePointA ), point.sub( linePointB ) ).length().div( linePointB.sub( linePointA ).length() );
- } );
- const pointPlaneDistance = Fn( ( [ point, planePoint, planeNormal ] )=> {
- // https://mathworld.wolfram.com/Point-PlaneDistance.html
- // https://en.wikipedia.org/wiki/Plane_(geometry)
- // http://paulbourke.net/geometry/pointlineplane/
- const d = mul( planeNormal.x, planePoint.x ).add( mul( planeNormal.y, planePoint.y ) ).add( mul( planeNormal.z, planePoint.z ) ).negate().toVar();
- const denominator = sqrt( mul( planeNormal.x, planeNormal.x, ).add( mul( planeNormal.y, planeNormal.y ) ).add( mul( planeNormal.z, planeNormal.z ) ) ).toVar();
- const distance = div( mul( planeNormal.x, point.x ).add( mul( planeNormal.y, point.y ) ).add( mul( planeNormal.z, point.z ) ).add( d ), denominator );
- return distance;
- } );
- const getViewZ = Fn( ( [ depth ] ) => {
- let viewZNode;
- if ( this.camera.isPerspectiveCamera ) {
- viewZNode = perspectiveDepthToViewZ( depth, this._cameraNear, this._cameraFar );
- } else {
- viewZNode = orthographicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
- }
- return viewZNode;
- } );
- const sampleDepth = ( uv ) => {
- const depth = this.depthNode.sample( uv ).r;
- if ( builder.renderer.logarithmicDepthBuffer === true ) {
- const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
- return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
- }
- return depth;
- };
- const ssr = Fn( () => {
- const metalness = this.metalnessNode.sample( uvNode ).r;
- // fragments with no metalness do not reflect their environment
- metalness.equal( 0.0 ).discard();
- // compute some standard FX entities
- const depth = sampleDepth( uvNode ).toVar();
- const viewPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toVar();
- const viewNormal = this.normalNode.rgb.normalize().toVar();
- // compute the direction from the position in view space to the camera
- const viewIncidentDir = ( ( this.camera.isPerspectiveCamera ) ? normalize( viewPosition ) : vec3( 0, 0, - 1 ) ).toVar();
- // compute the direction in which the light is reflected on the surface
- const viewReflectDir = reflect( viewIncidentDir, viewNormal ).toVar();
- // adapt maximum distance to the local geometry (see https://www.mathsisfun.com/algebra/vectors-dot-product.html)
- const maxReflectRayLen = this.maxDistance.div( dot( viewIncidentDir.negate(), viewNormal ) ).toVar();
- // compute the maximum point of the reflection ray in view space
- const d1viewPosition = viewPosition.add( viewReflectDir.mul( maxReflectRayLen ) ).toVar();
- // check if d1viewPosition lies behind the camera near plane
- If( this._isPerspectiveCamera.equal( float( 1 ) ).and( d1viewPosition.z.greaterThan( this._cameraNear.negate() ) ), () => {
- // if so, ensure d1viewPosition is clamped on the near plane.
- // this prevents artifacts during the ray marching process
- const t = sub( this._cameraNear.negate(), viewPosition.z ).div( viewReflectDir.z );
- d1viewPosition.assign( viewPosition.add( viewReflectDir.mul( t ) ) );
- } );
- // d0 and d1 are the start and maximum points of the reflection ray in screen space
- const d0 = screenCoordinate.xy.toVar();
- const d1 = getScreenPosition( d1viewPosition, this._cameraProjectionMatrix ).mul( this._resolution ).toVar();
- // below variables are used to control the raymarching process
- // total length of the ray
- const totalLen = d1.sub( d0 ).length().toVar();
- // offset in x and y direction
- const xLen = d1.x.sub( d0.x ).toVar();
- const yLen = d1.y.sub( d0.y ).toVar();
- // determine the larger delta
- // The larger difference will help to determine how much to travel in the X and Y direction each iteration and
- // how many iterations are needed to travel the entire ray
- const totalStep = max( abs( xLen ), abs( yLen ) ).toVar();
- // step sizes in the x and y directions
- const xSpan = xLen.div( totalStep ).toVar();
- const ySpan = yLen.div( totalStep ).toVar();
- const output = vec4( 0 ).toVar();
- // the actual ray marching loop
- // starting from d0, the code gradually travels along the ray and looks for an intersection with the geometry.
- // it does not exceed d1 (the maximum ray extend)
- Loop( { start: int( 0 ), end: int( this._maxStep ), type: 'int', condition: '<' }, ( { i } ) => {
- // TODO: Remove this when Chrome is fixed, see https://issues.chromium.org/issues/372714384#comment14
- If( metalness.equal( 0 ), () => {
- Break();
- } );
- // stop if the maximum number of steps is reached for this specific ray
- If( float( i ).greaterThanEqual( totalStep ), () => {
- Break();
- } );
- // advance on the ray by computing a new position in screen space
- const xy = vec2( d0.x.add( xSpan.mul( float( i ) ) ), d0.y.add( ySpan.mul( float( i ) ) ) ).toVar();
- // stop processing if the new position lies outside of the screen
- If( xy.x.lessThan( 0 ).or( xy.x.greaterThan( this._resolution.x ) ).or( xy.y.lessThan( 0 ) ).or( xy.y.greaterThan( this._resolution.y ) ), () => {
- Break();
- } );
- // compute new uv, depth, viewZ and viewPosition for the new location on the ray
- const uvNode = xy.div( this._resolution );
- const d = sampleDepth( uvNode ).toVar();
- const vZ = getViewZ( d ).toVar();
- const vP = getViewPosition( uvNode, d, this._cameraProjectionMatrixInverse ).toVar();
- const viewReflectRayZ = float( 0 ).toVar();
- // normalized distance between the current position xy and the starting point d0
- const s = xy.sub( d0 ).length().div( totalLen );
- // depending on the camera type, we now compute the z-coordinate of the reflected ray at the current step in view space
- If( this._isPerspectiveCamera.equal( float( 1 ) ), () => {
- const recipVPZ = float( 1 ).div( viewPosition.z ).toVar();
- viewReflectRayZ.assign( float( 1 ).div( recipVPZ.add( s.mul( float( 1 ).div( d1viewPosition.z ).sub( recipVPZ ) ) ) ) );
- } ).Else( () => {
- viewReflectRayZ.assign( viewPosition.z.add( s.mul( d1viewPosition.z.sub( viewPosition.z ) ) ) );
- } );
- // if viewReflectRayZ is less or equal than the real z-coordinate at this place, it potentially intersects the geometry
- If( viewReflectRayZ.lessThanEqual( vZ ), () => {
- // compute the distance of the new location to the ray in view space
- // to clarify vP is the fragment's view position which is not an exact point on the ray
- const away = pointToLineDistance( vP, viewPosition, d1viewPosition ).toVar();
- // compute the minimum thickness between the current fragment and its neighbor in the x-direction.
- const xyNeighbor = vec2( xy.x.add( 1 ), xy.y ).toVar(); // move one pixel
- const uvNeighbor = xyNeighbor.div( this._resolution );
- const vPNeighbor = getViewPosition( uvNeighbor, d, this._cameraProjectionMatrixInverse ).toVar();
- const minThickness = vPNeighbor.x.sub( vP.x ).toVar();
- minThickness.mulAssign( 3 ); // expand a bit to avoid errors
- const tk = max( minThickness, this.thickness ).toVar();
- If( away.lessThanEqual( tk ), () => { // hit
- const vN = this.normalNode.sample( uvNode ).rgb.normalize().toVar();
- If( dot( viewReflectDir, vN ).greaterThanEqual( 0 ), () => {
- // the reflected ray is pointing towards the same side as the fragment's normal (current ray position),
- // which means it wouldn't reflect off the surface. The loop continues to the next step for the next ray sample.
- Continue();
- } );
- // this distance represents the depth of the intersection point between the reflected ray and the scene.
- const distance = pointPlaneDistance( vP, viewPosition, viewNormal ).toVar();
- If( distance.greaterThan( this.maxDistance ), () => {
- // Distance exceeding limit: The reflection is potentially too far away and
- // might not contribute significantly to the final color
- Break();
- } );
- const op = this.opacity.mul( metalness ).toVar();
- // distance attenuation (the reflection should fade out the farther it is away from the surface)
- const ratio = float( 1 ).sub( distance.div( this.maxDistance ) ).toVar();
- const attenuation = ratio.mul( ratio );
- op.mulAssign( attenuation );
- // fresnel (reflect more light on surfaces that are viewed at grazing angles)
- const fresnelCoe = div( dot( viewIncidentDir, viewReflectDir ).add( 1 ), 2 );
- op.mulAssign( fresnelCoe );
- // output
- const reflectColor = this.colorNode.sample( uvNode );
- output.assign( vec4( reflectColor.rgb, op ) );
- Break();
- } );
- } );
- } );
- return output;
- } );
- this._material.fragmentNode = ssr().context( builder.getSharedContext() );
- this._material.needsUpdate = true;
- //
- return this._textureNode;
- }
- /**
- * Frees internal resources. This method should be called
- * when the effect is no longer required.
- */
- dispose() {
- this._ssrRenderTarget.dispose();
- this._material.dispose();
- }
- }
- export default SSRNode;
- /**
- * TSL function for creating screen space reflections (SSR).
- *
- * @tsl
- * @function
- * @param {Node<vec4>} colorNode - The node that represents the beauty pass.
- * @param {Node<float>} depthNode - A node that represents the beauty pass's depth.
- * @param {Node<vec3>} normalNode - A node that represents the beauty pass's normals.
- * @param {Node<float>} metalnessNode - A node that represents the beauty pass's metalness.
- * @param {Camera} camera - The camera the scene is rendered with.
- * @returns {SSRNode}
- */
- export const ssr = ( colorNode, depthNode, normalNode, metalnessNode, camera ) => nodeObject( new SSRNode( nodeObject( colorNode ), nodeObject( depthNode ), nodeObject( normalNode ), nodeObject( metalnessNode ), camera ) );
|