import { DataTexture, RenderTarget, RepeatWrapping, Vector2, Vector3, PostProcessingUtils } from 'three'; import { getNormalFromDepth, getScreenPosition, getViewPosition, QuadMesh, TempNode, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, vec4, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, mul, cross, div, mix, sqrt, sub, acos, clamp, NodeMaterial } from 'three/tsl'; const _quadMesh = /*@__PURE__*/ new QuadMesh(); const _size = /*@__PURE__*/ new Vector2(); let _rendererState; class GTAONode extends TempNode { static get type() { return 'GTAONode'; } constructor( depthNode, normalNode, camera ) { super(); this.depthNode = depthNode; this.normalNode = normalNode; this.radius = uniform( 0.25 ); this.resolution = uniform( new Vector2() ); this.thickness = uniform( 1 ); this.distanceExponent = uniform( 1 ); this.distanceFallOff = uniform( 1 ); this.scale = uniform( 1 ); this.noiseNode = texture( generateMagicSquareNoise() ); this.cameraProjectionMatrix = uniform( camera.projectionMatrix ); this.cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse ); this.SAMPLES = uniform( 16 ); this._aoRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false } ); this._aoRenderTarget.texture.name = 'GTAONode.AO'; this._material = null; this._textureNode = passTexture( this, this._aoRenderTarget.texture ); this.updateBeforeType = NodeUpdateType.FRAME; } getTextureNode() { return this._textureNode; } setSize( width, height ) { this.resolution.value.set( width, height ); this._aoRenderTarget.setSize( width, height ); } updateBefore( frame ) { const { renderer } = frame; _rendererState = PostProcessingUtils.resetRendererState( renderer, _rendererState ); // const size = renderer.getDrawingBufferSize( _size ); this.setSize( size.width, size.height ); _quadMesh.material = this._material; // clear renderer.setClearColor( 0xffffff, 1 ); // ao renderer.setRenderTarget( this._aoRenderTarget ); _quadMesh.render( renderer ); // restore PostProcessingUtils.restoreRendererState( renderer, _rendererState ); } setup( builder ) { const uvNode = uv(); const sampleDepth = ( uv ) => this.depthNode.uv( uv ).x; const sampleNoise = ( uv ) => this.noiseNode.uv( uv ); const sampleNormal = ( uv ) => ( this.normalNode !== null ) ? this.normalNode.uv( uv ).rgb.normalize() : getNormalFromDepth( uv, this.depthNode.value, this.cameraProjectionMatrixInverse ); const ao = 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 radiusToUse = this.radius; const noiseResolution = textureSize( this.noiseNode, 0 ); let noiseUv = vec2( uvNode.x, uvNode.y.oneMinus() ); noiseUv = noiseUv.mul( this.resolution.div( noiseResolution ) ); const noiseTexel = sampleNoise( noiseUv ); const randomVec = noiseTexel.xyz.mul( 2.0 ).sub( 1.0 ); const tangent = vec3( randomVec.xy, 0.0 ).normalize(); const bitangent = vec3( tangent.y.mul( - 1.0 ), tangent.x, 0.0 ); const kernelMatrix = mat3( tangent, bitangent, vec3( 0.0, 0.0, 1.0 ) ); const DIRECTIONS = this.SAMPLES.lessThan( 30 ).select( 3, 5 ).toVar(); const STEPS = add( this.SAMPLES, DIRECTIONS.sub( 1 ) ).div( DIRECTIONS ).toVar(); const ao = float( 0 ).toVar(); Loop( { start: int( 0 ), end: DIRECTIONS, type: 'int', condition: '<' }, ( { i } ) => { const angle = float( i ).div( float( DIRECTIONS ) ).mul( PI ).toVar(); const sampleDir = vec4( cos( angle ), sin( angle ), 0., add( 0.5, mul( 0.5, noiseTexel.w ) ) ); sampleDir.xyz = normalize( kernelMatrix.mul( sampleDir.xyz ) ); const viewDir = normalize( viewPosition.xyz.negate() ).toVar(); const sliceBitangent = normalize( cross( sampleDir.xyz, viewDir ) ).toVar(); const sliceTangent = cross( sliceBitangent, viewDir ); const normalInSlice = normalize( viewNormal.sub( sliceBitangent.mul( dot( viewNormal, sliceBitangent ) ) ) ); const tangentToNormalInSlice = cross( normalInSlice, sliceBitangent ).toVar(); const cosHorizons = vec2( dot( viewDir, tangentToNormalInSlice ), dot( viewDir, tangentToNormalInSlice.negate() ) ).toVar(); Loop( { end: STEPS, type: 'int', name: 'j', condition: '<' }, ( { j } ) => { const sampleViewOffset = sampleDir.xyz.mul( radiusToUse ).mul( sampleDir.w ).mul( pow( div( float( j ).add( 1.0 ), float( STEPS ) ), this.distanceExponent ) ); // x const sampleScreenPositionX = getScreenPosition( viewPosition.add( sampleViewOffset ), this.cameraProjectionMatrix ).toVar(); const sampleDepthX = sampleDepth( sampleScreenPositionX ).toVar(); const sampleSceneViewPositionX = getViewPosition( sampleScreenPositionX, sampleDepthX, this.cameraProjectionMatrixInverse ).toVar(); const viewDeltaX = sampleSceneViewPositionX.sub( viewPosition ).toVar(); If( abs( viewDeltaX.z ).lessThan( this.thickness ), () => { const sampleCosHorizon = dot( viewDir, normalize( viewDeltaX ) ); cosHorizons.x.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.x ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) ); } ); // y const sampleScreenPositionY = getScreenPosition( viewPosition.sub( sampleViewOffset ), this.cameraProjectionMatrix ).toVar(); const sampleDepthY = sampleDepth( sampleScreenPositionY ).toVar(); const sampleSceneViewPositionY = getViewPosition( sampleScreenPositionY, sampleDepthY, this.cameraProjectionMatrixInverse ).toVar(); const viewDeltaY = sampleSceneViewPositionY.sub( viewPosition ).toVar(); If( abs( viewDeltaY.z ).lessThan( this.thickness ), () => { const sampleCosHorizon = dot( viewDir, normalize( viewDeltaY ) ); cosHorizons.y.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.y ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) ); } ); } ); const sinHorizons = sqrt( sub( 1.0, cosHorizons.mul( cosHorizons ) ) ).toVar(); const nx = dot( normalInSlice, sliceTangent ); const ny = dot( normalInSlice, viewDir ); const nxb = mul( 0.5, acos( cosHorizons.y ).sub( acos( cosHorizons.x ) ).add( sinHorizons.x.mul( cosHorizons.x ).sub( sinHorizons.y.mul( cosHorizons.y ) ) ) ); const nyb = mul( 0.5, sub( 2.0, cosHorizons.x.mul( cosHorizons.x ) ).sub( cosHorizons.y.mul( cosHorizons.y ) ) ); const occlusion = nx.mul( nxb ).add( ny.mul( nyb ) ); ao.addAssign( occlusion ); } ); ao.assign( clamp( ao.div( DIRECTIONS ), 0, 1 ) ); ao.assign( pow( ao, this.scale ) ); return vec4( vec3( ao ), 1.0 ); } ); const material = this._material || ( this._material = new NodeMaterial() ); material.fragmentNode = ao().context( builder.getSharedContext() ); material.name = 'GTAO'; material.needsUpdate = true; // return this._textureNode; } dispose() { this._aoRenderTarget.dispose(); } } export default GTAONode; function generateMagicSquareNoise( size = 5 ) { const noiseSize = Math.floor( size ) % 2 === 0 ? Math.floor( size ) + 1 : Math.floor( size ); const magicSquare = generateMagicSquare( noiseSize ); const noiseSquareSize = magicSquare.length; const data = new Uint8Array( noiseSquareSize * 4 ); for ( let inx = 0; inx < noiseSquareSize; ++ inx ) { const iAng = magicSquare[ inx ]; const angle = ( 2 * Math.PI * iAng ) / noiseSquareSize; const randomVec = new Vector3( Math.cos( angle ), Math.sin( angle ), 0 ).normalize(); data[ inx * 4 ] = ( randomVec.x * 0.5 + 0.5 ) * 255; data[ inx * 4 + 1 ] = ( randomVec.y * 0.5 + 0.5 ) * 255; data[ inx * 4 + 2 ] = 127; data[ inx * 4 + 3 ] = 255; } const noiseTexture = new DataTexture( data, noiseSize, noiseSize ); noiseTexture.wrapS = RepeatWrapping; noiseTexture.wrapT = RepeatWrapping; noiseTexture.needsUpdate = true; return noiseTexture; } function generateMagicSquare( size ) { const noiseSize = Math.floor( size ) % 2 === 0 ? Math.floor( size ) + 1 : Math.floor( size ); const noiseSquareSize = noiseSize * noiseSize; const magicSquare = Array( noiseSquareSize ).fill( 0 ); let i = Math.floor( noiseSize / 2 ); let j = noiseSize - 1; for ( let num = 1; num <= noiseSquareSize; ) { if ( i === - 1 && j === noiseSize ) { j = noiseSize - 2; i = 0; } else { if ( j === noiseSize ) { j = 0; } if ( i < 0 ) { i = noiseSize - 1; } } if ( magicSquare[ i * noiseSize + j ] !== 0 ) { j -= 2; i ++; continue; } else { magicSquare[ i * noiseSize + j ] = num ++; } j ++; i --; } return magicSquare; } export const ao = ( depthNode, normalNode, camera ) => nodeObject( new GTAONode( nodeObject( depthNode ), nodeObject( normalNode ), camera ) );