292 lines
8.8 KiB
JavaScript
292 lines
8.8 KiB
JavaScript
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 ) );
|