|
|
@@ -0,0 +1,650 @@
|
|
|
+import { RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, MathUtils } from 'three/webgpu';
|
|
|
+import { clamp, normalize, reference, nodeObject, Fn, NodeUpdateType, uniform, vec4, passTexture, uv, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getViewPosition, screenCoordinate, float, sub, fract, dot, vec2, rand, vec3, Loop, mul, PI, cos, sin, uint, cross, acos, sign, pow, luminance, If, max, abs, Break, sqrt, HALF_PI, div, ceil, shiftRight, uvec2, convertToTexture, bool, getNormalFromDepth } from 'three/tsl';
|
|
|
+
|
|
|
+const _quadMesh = /*@__PURE__*/ new QuadMesh();
|
|
|
+const _size = /*@__PURE__*/ new Vector2();
|
|
|
+
|
|
|
+// From Activision GTAO paper: https://www.activision.com/cdn/research/s2016_pbs_activision_occlusion.pptx
|
|
|
+const _temporalRotations = [ 60, 300, 180, 240, 120, 0 ];
|
|
|
+const _spatialOffsets = [ 0, 0.5, 0.25, 0.75 ];
|
|
|
+
|
|
|
+let _rendererState;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Post processing node for applying Screen Space Global Illumination (SSGI) to a scene.
|
|
|
+ *
|
|
|
+ * References:
|
|
|
+ * - {@link https://github.com/cdrinmatane/SSRT3}.
|
|
|
+ * - {@link https://cdrinmatane.github.io/posts/ssaovb-code/}.
|
|
|
+ * - {@link https://cdrinmatane.github.io/cgspotlight-slides/ssilvb_slides.pdf}.
|
|
|
+ *
|
|
|
+ * The quality and performance of the effect mainly depend on `sliceCount` and `stepCount`.
|
|
|
+ * The total number of samples taken per pixel is `sliceCount` * `stepCount` * `2`. Here are some
|
|
|
+ * recommened presets depending on whether temporal filtering is used or not.
|
|
|
+ *
|
|
|
+ * With temporal filtering (recommended):
|
|
|
+ *
|
|
|
+ * - Low: `sliceCount` of `1`, `stepCount` of `12`.
|
|
|
+ * - Medium: `sliceCount` of `2`, `stepCount` of `8`.
|
|
|
+ * - High: `sliceCount` of `3`, `stepCount` of `16`.
|
|
|
+ *
|
|
|
+ * Use for a higher slice count if you notice temporal instabilties like flickering. Reduce the sample
|
|
|
+ * count then to mitigate the performance lost.
|
|
|
+ *
|
|
|
+ * Without temporal filtering:
|
|
|
+ *
|
|
|
+ * - Low: `sliceCount` of `2`, `stepCount` of `6`.
|
|
|
+ * - Medium: `sliceCount` of `3`, `stepCount` of `8`.
|
|
|
+ * - High: `sliceCount` of `4`, `stepCount` of `12`.
|
|
|
+ *
|
|
|
+ * @augments TempNode
|
|
|
+ * @three_import import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
|
|
|
+ */
|
|
|
+class SSGINode extends TempNode {
|
|
|
+
|
|
|
+ static get type() {
|
|
|
+
|
|
|
+ return 'SSGINode';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constructs a new SSGI node.
|
|
|
+ *
|
|
|
+ * @param {TextureNode} beautyNode - The texture node that represents the input of the effect.
|
|
|
+ * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
|
|
|
+ * @param {TextureNode} normalNode - A texture node that represents the scene's normals.
|
|
|
+ * @param {PerspectiveCamera} camera - The camera the scene is rendered with.
|
|
|
+ */
|
|
|
+ constructor( beautyNode, depthNode, normalNode, camera ) {
|
|
|
+
|
|
|
+ super( 'vec4' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A node that represents the scene's depth.
|
|
|
+ *
|
|
|
+ * @type {Node<float>}
|
|
|
+ */
|
|
|
+ this.beautyNode = beautyNode;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A node that represents the scene's depth.
|
|
|
+ *
|
|
|
+ * @type {TextureNode}
|
|
|
+ */
|
|
|
+ this.depthNode = depthNode;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A node that represents the scene's normals. If no normals are passed to the
|
|
|
+ * constructor (because MRT is not available), normals can be automatically
|
|
|
+ * reconstructed from depth values in the shader.
|
|
|
+ *
|
|
|
+ * @type {TextureNode}
|
|
|
+ */
|
|
|
+ this.normalNode = normalNode;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Number of per-pixel hemisphere slices. This has a big performance cost and should be kept as low as possible.
|
|
|
+ * Should be in the range `[1, 4]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<int>}
|
|
|
+ * @default 1
|
|
|
+ */
|
|
|
+ this.sliceCount = uniform( 1, 'uint' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Number of samples taken along one side of a given hemisphere slice. This has a big performance cost and should
|
|
|
+ * be kept as low as possible. Should be in the range `[1, 32]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<int>}
|
|
|
+ * @default 12
|
|
|
+ */
|
|
|
+ this.stepCount = uniform( 12, 'uint' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Power function applied to AO to make it appear darker/lighter. Should be in the range `[0, 4]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<float>}
|
|
|
+ * @default 1
|
|
|
+ */
|
|
|
+ this.aoIntensity = uniform( 1, 'float' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Intensity of the indirect diffuse light. Should be in the range `[0, 100]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<float>}
|
|
|
+ * @default 10
|
|
|
+ */
|
|
|
+ this.giIntensity = uniform( 10, 'float' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Effective sampling radius in world space. AO and GI can only have influence within that radius.
|
|
|
+ * Should be in the range `[1, 25]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<float>}
|
|
|
+ * @default 12
|
|
|
+ */
|
|
|
+ this.radius = uniform( 12, 'float' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Makes the sample distance in screen space instead of world-space (helps having more detail up close).
|
|
|
+ *
|
|
|
+ * @type {UniformNode<bool>}
|
|
|
+ * @default false
|
|
|
+ */
|
|
|
+ this.useScreenSpaceSampling = uniform( true, 'bool' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Controls samples distribution. It's an exponent applied at each step get increasing step size over the distance.
|
|
|
+ * Should be in the range `[1, 3]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<float>}
|
|
|
+ * @default 2
|
|
|
+ */
|
|
|
+ this.expFactor = uniform( 2, 'float' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constant thickness value of objects on the screen in world units. Allows light to pass behind surfaces past that thickness value.
|
|
|
+ * Should be in the range `[0.01, 10]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<float>}
|
|
|
+ * @default 1
|
|
|
+ */
|
|
|
+ this.thickness = uniform( 1, 'float' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Whether to increase thickness linearly over distance or not (avoid losing detail over the distance).
|
|
|
+ *
|
|
|
+ * @type {UniformNode<bool>}
|
|
|
+ * @default false
|
|
|
+ */
|
|
|
+ this.useLinearThickness = uniform( false, 'bool' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * How much light backface surfaces emit.
|
|
|
+ * Should be in the range `[0, 1]`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<float>}
|
|
|
+ * @default 0
|
|
|
+ */
|
|
|
+ this.backfaceLighting = uniform( 0, 'float' );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Whether to use temporal filtering or not. Setting this property to
|
|
|
+ * `true` requires the usage of `TRAANode`. This will help to reduce noice
|
|
|
+ * although it introduces typical TAA artifacts like ghosting and temporal
|
|
|
+ * instabilities.
|
|
|
+ *
|
|
|
+ * If setting this property to `false`, a manual denoise via `DenoiseNode`
|
|
|
+ * is required.
|
|
|
+ *
|
|
|
+ * @type {boolean}
|
|
|
+ * @default true
|
|
|
+ */
|
|
|
+ this.useTemporalFiltering = true;
|
|
|
+
|
|
|
+ // private uniforms
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The resolution of the effect.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<vec2>}
|
|
|
+ */
|
|
|
+ this._resolution = uniform( new Vector2() );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Used to compute the effective step radius when viewSpaceSampling is `false`.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<vec2>}
|
|
|
+ */
|
|
|
+ this._halfProjScale = uniform( 1 );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Temporal direction that influences the rotation angle for each slice.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<float>}
|
|
|
+ */
|
|
|
+ this._temporalDirection = uniform( 0 );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Temporal offset added to the initial ray step.
|
|
|
+ *
|
|
|
+ * @type {UniformNode<vec2>}
|
|
|
+ */
|
|
|
+ this._temporalOffset = uniform( 0 );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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 );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A reference to the scene's camera.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {PerspectiveCamera}
|
|
|
+ */
|
|
|
+ this._camera = camera;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The render target the GI is rendered into.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {RenderTarget}
|
|
|
+ */
|
|
|
+ this._ssgiRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false } );
|
|
|
+ this._ssgiRenderTarget.texture.name = 'SSGI';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The material that is used to render the effect.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {NodeMaterial}
|
|
|
+ */
|
|
|
+ this._material = new NodeMaterial();
|
|
|
+ this._material.name = 'SSGI';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The result of the effect is represented as a separate texture node.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {PassTextureNode}
|
|
|
+ */
|
|
|
+ this._textureNode = passTexture( this, this._ssgiRenderTarget.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 ) {
|
|
|
+
|
|
|
+ this._resolution.value.set( width, height );
|
|
|
+ this._ssgiRenderTarget.setSize( width, height );
|
|
|
+
|
|
|
+ this._halfProjScale.value = height / ( Math.tan( this._camera.fov * MathUtils.DEG2RAD * 0.5 ) * 2 ) * 0.5;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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 );
|
|
|
+ this.setSize( size.width, size.height );
|
|
|
+
|
|
|
+ // update temporal uniforms
|
|
|
+
|
|
|
+ if ( this.useTemporalFiltering === true ) {
|
|
|
+
|
|
|
+ const frameId = frame.frameId;
|
|
|
+
|
|
|
+ this._temporalDirection.value = _temporalRotations[ frameId % 6 ] / 360;
|
|
|
+ this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ this._temporalDirection.value = 1;
|
|
|
+ this._temporalOffset.value = 1;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ //
|
|
|
+
|
|
|
+ _quadMesh.material = this._material;
|
|
|
+
|
|
|
+ // clear
|
|
|
+
|
|
|
+ renderer.setClearColor( 0xffffff, 1 );
|
|
|
+
|
|
|
+ // gi
|
|
|
+
|
|
|
+ renderer.setRenderTarget( this._ssgiRenderTarget );
|
|
|
+ _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 MAX_RAY = uint( 32 );
|
|
|
+
|
|
|
+ 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 sampleNormal = ( uv ) => ( this.normalNode !== null ) ? this.normalNode.sample( uv ).rgb.normalize() : getNormalFromDepth( uv, this.depthNode.value, this._cameraProjectionMatrixInverse );
|
|
|
+ const sampleBeauty = ( uv ) => this.beautyNode.sample( uv );
|
|
|
+
|
|
|
+ // From Activision GTAO paper: https://www.activision.com/cdn/research/s2016_pbs_activision_occlusion.pptx
|
|
|
+
|
|
|
+ const spatialOffsets = Fn( ( [ position ] ) => {
|
|
|
+
|
|
|
+ return float( 0.25 ).mul( sub( position.y, position.x ).bitAnd( 3 ) );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ // Interleaved gradient function from Jimenez 2014 http://goo.gl/eomGso
|
|
|
+
|
|
|
+ const gradientNoise = Fn( ( [ position ] ) => {
|
|
|
+
|
|
|
+ return fract( float( 52.9829189 ).mul( fract( dot( position, vec2( 0.06711056, 0.00583715 ) ) ) ) );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ const GTAOFastAcos = Fn( ( [ value ] ) => {
|
|
|
+
|
|
|
+ const outVal = abs( value ).mul( float( - 0.156583 ) ).add( HALF_PI );
|
|
|
+ outVal.mulAssign( sqrt( abs( value ).oneMinus() ) );
|
|
|
+
|
|
|
+ const x = value.x.greaterThanEqual( 0 ).select( outVal.x, PI.sub( outVal.x ) );
|
|
|
+ const y = value.y.greaterThanEqual( 0 ).select( outVal.y, PI.sub( outVal.y ) );
|
|
|
+
|
|
|
+ return vec2( x, y );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ const bitCount = Fn( ( [ value_immutable ] ) => {
|
|
|
+
|
|
|
+ const value = value_immutable;
|
|
|
+ value.assign( value.sub( value.shiftRight( uint( 1 ) ).bitAnd( uint( 0x55555555 ) ) ) );
|
|
|
+ value.assign( value.bitAnd( uint( 0x33333333 ) ).add( value.shiftRight( uint( 2 ) ).bitAnd( uint( 0x33333333 ) ) ) );
|
|
|
+
|
|
|
+ return value.add( value.shiftRight( uint( 4 ) ) ).bitAnd( uint( 0xF0F0F0F ) ).mul( uint( 0x1010101 ) ).shiftRight( uint( 24 ) );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ const computeOccludedBitfield = Fn( ( [ minHorizon, maxHorizon, globalOccludedBitfield ] ) => {
|
|
|
+
|
|
|
+ const startHorizonInt = uint( minHorizon.mul( float( MAX_RAY ) ) ).toConst();
|
|
|
+ const angleHorizonInt = uint( ceil( maxHorizon.sub( minHorizon ).mul( float( MAX_RAY ) ) ) ).toConst();
|
|
|
+ const angleHorizonBitfield = angleHorizonInt.greaterThan( uint( 0 ) ).select( uint( shiftRight( uint( 0xFFFFFFFF ), uint( 32 ).sub( MAX_RAY ).add( MAX_RAY.sub( angleHorizonInt ) ) ) ), uint( 0 ) ).toConst();
|
|
|
+ let currentOccludedBitfield = angleHorizonBitfield.shiftLeft( startHorizonInt );
|
|
|
+ currentOccludedBitfield = currentOccludedBitfield.bitAnd( globalOccludedBitfield.bitNot() );
|
|
|
+
|
|
|
+ const result = uvec2();
|
|
|
+ result.x = globalOccludedBitfield.bitOr( currentOccludedBitfield );
|
|
|
+ result.y = bitCount( currentOccludedBitfield );
|
|
|
+
|
|
|
+ return result;
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ const horizonSampling = Fn( ( [ directionIsRight, RADIUS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n, globalOccludedBitfield ] ) => {
|
|
|
+
|
|
|
+ const STEP_COUNT = this.stepCount.toConst();
|
|
|
+ const EXP_FACTOR = this.expFactor.toConst();
|
|
|
+ const THICKNESS = this.thickness.toConst();
|
|
|
+ const BACKFACE_LIGHTING = this.backfaceLighting.toConst();
|
|
|
+
|
|
|
+ const stepRadius = float( 0 );
|
|
|
+
|
|
|
+ If( this.useScreenSpaceSampling.equal( true ), () => {
|
|
|
+
|
|
|
+ stepRadius.assign( RADIUS.mul( this._resolution.x.div( 2 ) ).div( float( STEP_COUNT ) ) );
|
|
|
+
|
|
|
+ } ).Else( () => {
|
|
|
+
|
|
|
+ stepRadius.assign( max( RADIUS.mul( this._halfProjScale ).div( viewPosition.z.negate() ), float( STEP_COUNT ) ) ); // Port note: viewZ is negative so a negate is requried
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ stepRadius.divAssign( float( STEP_COUNT ).add( 1 ) );
|
|
|
+ const radiusVS = max( 1, float( STEP_COUNT.sub( 1 ) ) ).mul( stepRadius );
|
|
|
+ const samplingDirection = directionIsRight.equal( true ).select( vec2( 1, - 1 ), vec2( - 1, 1 ) ); // Port note: Because of different uv conventions, uv-y has a different sign
|
|
|
+
|
|
|
+ const color = vec3( 0 );
|
|
|
+ const occludedBitfield = uint( globalOccludedBitfield ).toVar();
|
|
|
+
|
|
|
+ const lastSampleViewPosition = vec3( viewPosition ).toVar();
|
|
|
+
|
|
|
+ Loop( { start: uint( 0 ), end: STEP_COUNT, type: 'uint', condition: '<' }, ( { i } ) => {
|
|
|
+
|
|
|
+ const offset = pow( abs( mul( stepRadius, float( i ).add( initialRayStep ) ).div( radiusVS ) ), EXP_FACTOR ).mul( radiusVS ).toConst();
|
|
|
+ const uvOffset = slideDirTexelSize.mul( max( offset, float( i ).add( 1 ) ) ).toConst();
|
|
|
+ const sampleUV = uvNode.add( uvOffset.mul( samplingDirection ) ).toConst();
|
|
|
+
|
|
|
+ If( sampleUV.x.lessThanEqual( 0 ).or( sampleUV.y.lessThanEqual( 0 ) ).or( sampleUV.x.greaterThanEqual( 1 ) ).or( sampleUV.y.greaterThanEqual( 1 ) ), () => {
|
|
|
+
|
|
|
+ Break();
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ const sampleViewPosition = getViewPosition( sampleUV, sampleDepth( sampleUV ), this._cameraProjectionMatrixInverse ).toConst();
|
|
|
+ const pixelToSample = sampleViewPosition.sub( viewPosition ).normalize().toConst();
|
|
|
+ const linearThicknessMultiplier = this.useLinearThickness.equal( true ).select( sampleViewPosition.z.div( this._cameraFar ).clamp().mul( 100 ), float( 1 ) );
|
|
|
+ const pixelToSampleBackface = normalize( sampleViewPosition.sub( linearThicknessMultiplier.mul( viewDir ).mul( THICKNESS ) ).sub( viewPosition ) );
|
|
|
+
|
|
|
+ let frontBackHorizon = vec2( dot( pixelToSample, viewDir ), dot( pixelToSampleBackface, viewDir ) );
|
|
|
+ frontBackHorizon = GTAOFastAcos( clamp( frontBackHorizon, - 1, 1 ) );
|
|
|
+ frontBackHorizon = clamp( div( mul( samplingDirection, vec2( frontBackHorizon.x.negate(), frontBackHorizon.y ) ).sub( n.sub( HALF_PI ) ), PI ) ); // Port note: This line also required an update because of different uv conventions
|
|
|
+ frontBackHorizon = directionIsRight.equal( true ).select( frontBackHorizon.yx, frontBackHorizon.xy ); // Front/Back get inverted depending on angle
|
|
|
+
|
|
|
+ const result = computeOccludedBitfield( frontBackHorizon.x, frontBackHorizon.y, occludedBitfield );
|
|
|
+ occludedBitfield.assign( result.x );
|
|
|
+ const numOccludedZones = result.y;
|
|
|
+
|
|
|
+ If( numOccludedZones.greaterThan( 0 ), () => { // If a ray hit the sample, that sample is visible from shading point
|
|
|
+
|
|
|
+ const lightColor = sampleBeauty( sampleUV );
|
|
|
+
|
|
|
+ If( luminance( lightColor ).greaterThan( 0.001 ), () => { // Continue if there is light at that location (intensity > 0)
|
|
|
+
|
|
|
+ const lightDirectionVS = normalize( pixelToSample );
|
|
|
+ const normalDotLightDirection = clamp( dot( viewNormal, lightDirectionVS ) );
|
|
|
+
|
|
|
+ If( normalDotLightDirection.greaterThan( 0.001 ), () => { // Continue if light is facing surface normal
|
|
|
+
|
|
|
+ const lightNormalVS = sampleNormal( sampleUV );
|
|
|
+
|
|
|
+ // Intensity of outgoing light in the direction of the shading point
|
|
|
+
|
|
|
+ let lightNormalDotLightDirection = dot( lightNormalVS, lightDirectionVS.negate() );
|
|
|
+
|
|
|
+ const d = sign( lightNormalDotLightDirection ).lessThan( 0 ).select( abs( lightNormalDotLightDirection ).mul( BACKFACE_LIGHTING ), abs( lightNormalDotLightDirection ) );
|
|
|
+ lightNormalDotLightDirection = BACKFACE_LIGHTING.greaterThan( 0 ).and( dot( lightNormalVS, viewDir ).greaterThan( 0 ) ).select( d, clamp( lightNormalDotLightDirection ) );
|
|
|
+
|
|
|
+ color.rgb.addAssign( float( numOccludedZones ).div( float( MAX_RAY ) ).mul( lightColor ).mul( normalDotLightDirection ).mul( lightNormalDotLightDirection ) );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ lastSampleViewPosition.assign( sampleViewPosition );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ return vec4( color, occludedBitfield );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ const gi = Fn( () => {
|
|
|
+
|
|
|
+ const depth = sampleDepth( uvNode ).toVar();
|
|
|
+
|
|
|
+ depth.greaterThanEqual( 1.0 ).discard();
|
|
|
+
|
|
|
+ const viewPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toVar();
|
|
|
+ const viewNormal = sampleNormal( uvNode ).toVar();
|
|
|
+ const viewDir = normalize( viewPosition.xyz.negate() ).toVar();
|
|
|
+
|
|
|
+ //
|
|
|
+
|
|
|
+ const noiseOffset = spatialOffsets( screenCoordinate );
|
|
|
+ const noiseDirection = gradientNoise( screenCoordinate );
|
|
|
+ const initialRayStep = fract( noiseOffset.add( this._temporalOffset ) ).add( rand( uvNode ).mul( 2 ).sub( 1 ) );
|
|
|
+
|
|
|
+ const ao = float( 0 );
|
|
|
+ const color = vec3( 0 );
|
|
|
+
|
|
|
+ const ROTATION_COUNT = this.sliceCount.toConst();
|
|
|
+ const AO_INTENSITY = this.aoIntensity.toConst();
|
|
|
+ const GI_INTENSITY = this.giIntensity.toConst();
|
|
|
+ const RADIUS = this.radius.toConst();
|
|
|
+
|
|
|
+ Loop( { start: uint( 0 ), end: ROTATION_COUNT, type: 'uint', condition: '<' }, ( { i } ) => {
|
|
|
+
|
|
|
+ const rotationAngle = mul( float( i ).add( noiseDirection ).add( this._temporalDirection ), PI.div( float( ROTATION_COUNT ) ) ).toConst();
|
|
|
+ const sliceDir = vec3( vec2( cos( rotationAngle ), sin( rotationAngle ) ), 0 ).toConst();
|
|
|
+ const slideDirTexelSize = sliceDir.xy.mul( float( 1 ).div( this._resolution ) ).toConst();
|
|
|
+
|
|
|
+ const planeNormal = normalize( cross( sliceDir, viewDir ) ).toConst();
|
|
|
+ const tangent = cross( viewDir, planeNormal ).toConst();
|
|
|
+ const projectedNormal = viewNormal.sub( planeNormal.mul( dot( viewNormal, planeNormal ) ) ).toConst();
|
|
|
+ const projectedNormalNormalized = normalize( projectedNormal ).toConst();
|
|
|
+
|
|
|
+ const cos_n = clamp( dot( projectedNormalNormalized, viewDir ), - 1, 1 ).toConst();
|
|
|
+ const n = sign( dot( projectedNormal, tangent ) ).negate().mul( acos( cos_n ) ).toConst();
|
|
|
+
|
|
|
+ const globalOccludedBitfield = uint( 0 );
|
|
|
+
|
|
|
+ const resultRight = horizonSampling( bool( true ), RADIUS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n, globalOccludedBitfield );
|
|
|
+ color.addAssign( resultRight.xyz );
|
|
|
+ globalOccludedBitfield.assign( resultRight.a );
|
|
|
+
|
|
|
+ const resultLeft = horizonSampling( bool( false ), RADIUS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n, globalOccludedBitfield );
|
|
|
+ color.addAssign( resultLeft.xyz );
|
|
|
+ globalOccludedBitfield.assign( resultLeft.a );
|
|
|
+
|
|
|
+ ao.addAssign( float( bitCount( globalOccludedBitfield ) ).div( float( MAX_RAY ) ) );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ ao.divAssign( float( ROTATION_COUNT ) );
|
|
|
+ ao.assign( pow( ao.clamp().oneMinus(), AO_INTENSITY ).clamp() );
|
|
|
+
|
|
|
+ color.divAssign( float( ROTATION_COUNT ) );
|
|
|
+ color.mulAssign( GI_INTENSITY );
|
|
|
+
|
|
|
+ // scale color based on luminance
|
|
|
+
|
|
|
+ const maxLuminance = float( 7 ).toConst(); // 7 represent a HDR luminance value
|
|
|
+ const currentLuminance = luminance( color );
|
|
|
+
|
|
|
+ const scale = currentLuminance.greaterThan( maxLuminance ).select( maxLuminance.div( currentLuminance ), float( 1 ) );
|
|
|
+ color.mulAssign( scale );
|
|
|
+
|
|
|
+ return vec4( color, ao );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ this._material.fragmentNode = gi().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._ssgiRenderTarget.dispose();
|
|
|
+
|
|
|
+ this._material.dispose();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+export default SSGINode;
|
|
|
+
|
|
|
+/**
|
|
|
+ * TSL function for creating a SSGI effect.
|
|
|
+ *
|
|
|
+ * @tsl
|
|
|
+ * @function
|
|
|
+ * @param {TextureNode} beautyNode - The texture node that represents the input of the effect.
|
|
|
+ * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
|
|
|
+ * @param {TextureNode} normalNode - A texture node that represents the scene's normals.
|
|
|
+ * @param {Camera} camera - The camera the scene is rendered with.
|
|
|
+ * @returns {SSGINode}
|
|
|
+ */
|
|
|
+export const ssgi = ( beautyNode, depthNode, normalNode, camera ) => nodeObject( new SSGINode( convertToTexture( beautyNode ), depthNode, normalNode, camera ) );
|