SSGINode.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. import { HalfFloatType, RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, MathUtils, WebGPUCoordinateSystem } from 'three/webgpu';
  2. import { clamp, normalize, reference, Fn, NodeUpdateType, uniform, vec4, passTexture, uv, logarithmicDepthToViewZ, viewZToPerspectiveDepth, 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, convertToTexture, getNormalFromDepth, countOneBits, interleavedGradientNoise } from 'three/tsl';
  3. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  4. const _size = /*@__PURE__*/ new Vector2();
  5. // From Activision GTAO paper: https://www.activision.com/cdn/research/s2016_pbs_activision_occlusion.pptx
  6. const _temporalRotations = [ 60, 300, 180, 240, 120, 0 ];
  7. const _spatialOffsets = [ 0, 0.5, 0.25, 0.75 ];
  8. let _rendererState;
  9. /**
  10. * Post processing node for applying Screen Space Global Illumination (SSGI) to a scene.
  11. *
  12. * References:
  13. * - {@link https://github.com/cdrinmatane/SSRT3}.
  14. * - {@link https://cdrinmatane.github.io/posts/ssaovb-code/}.
  15. * - {@link https://cdrinmatane.github.io/cgspotlight-slides/ssilvb_slides.pdf}.
  16. *
  17. * The quality and performance of the effect mainly depend on `sliceCount` and `stepCount`.
  18. * The total number of samples taken per pixel is `sliceCount` * `stepCount` * `2`. Here are some
  19. * recommended presets depending on whether temporal filtering is used or not.
  20. *
  21. * With temporal filtering (recommended):
  22. *
  23. * - Low: `sliceCount` of `1`, `stepCount` of `12`.
  24. * - Medium: `sliceCount` of `2`, `stepCount` of `8`.
  25. * - High: `sliceCount` of `3`, `stepCount` of `16`.
  26. *
  27. * Use for a higher slice count if you notice temporal instabilities like flickering. Reduce the sample
  28. * count then to mitigate the performance lost.
  29. *
  30. * Without temporal filtering:
  31. *
  32. * - Low: `sliceCount` of `2`, `stepCount` of `6`.
  33. * - Medium: `sliceCount` of `3`, `stepCount` of `8`.
  34. * - High: `sliceCount` of `4`, `stepCount` of `12`.
  35. *
  36. * @augments TempNode
  37. * @three_import import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
  38. */
  39. class SSGINode extends TempNode {
  40. static get type() {
  41. return 'SSGINode';
  42. }
  43. /**
  44. * Constructs a new SSGI node.
  45. *
  46. * @param {TextureNode} beautyNode - A texture node that represents the beauty or scene pass.
  47. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  48. * @param {TextureNode} normalNode - A texture node that represents the scene's normals.
  49. * @param {PerspectiveCamera} camera - The camera the scene is rendered with.
  50. */
  51. constructor( beautyNode, depthNode, normalNode, camera ) {
  52. super( 'vec4' );
  53. /**
  54. * A texture node that represents the beauty or scene pass.
  55. *
  56. * @type {TextureNode}
  57. */
  58. this.beautyNode = beautyNode;
  59. /**
  60. * A node that represents the scene's depth.
  61. *
  62. * @type {TextureNode}
  63. */
  64. this.depthNode = depthNode;
  65. /**
  66. * A node that represents the scene's normals. If no normals are passed to the
  67. * constructor (because MRT is not available), normals can be automatically
  68. * reconstructed from depth values in the shader.
  69. *
  70. * @type {TextureNode}
  71. */
  72. this.normalNode = normalNode;
  73. /**
  74. * An optional node that supplies the per-pixel sampling jitter as a `vec2`: `x`
  75. * drives the slice rotation and `y` the initial ray step, both in `[0, 1)`. The
  76. * node is expected to vary over time when temporal filtering is used — an animated
  77. * blue-noise node converges faster and with less visible noise than the default.
  78. * Keep the source static when `useTemporalFiltering` is disabled (e.g. via
  79. * `BlueNoiseNode.animated = false`).
  80. * When `null`, interleaved gradient noise with the GTAO spatial/temporal offset
  81. * tables is used instead.
  82. *
  83. * @type {?Node}
  84. * @default null
  85. */
  86. this.noiseNode = null;
  87. /**
  88. * The resolution scale. By default the effect is rendered in full resolution
  89. * for best quality but a value of `0.5` is an effective way of improving
  90. * performance, especially when the result is denoised and upsampled anyway.
  91. *
  92. * @type {number}
  93. * @default 1
  94. */
  95. this.resolutionScale = 1;
  96. /**
  97. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
  98. * its effect once per frame in `updateBefore()`.
  99. *
  100. * @type {string}
  101. * @default 'frame'
  102. */
  103. this.updateBeforeType = NodeUpdateType.FRAME;
  104. /**
  105. * Number of per-pixel hemisphere slices. This has a big performance cost and should be kept as low as possible.
  106. * Should be in the range `[1, 4]`.
  107. *
  108. * @type {UniformNode<uint>}
  109. * @default 1
  110. */
  111. this.sliceCount = uniform( 1, 'uint' );
  112. /**
  113. * Number of samples taken along one side of a given hemisphere slice. This has a big performance cost and should
  114. * be kept as low as possible. Should be in the range `[1, 32]`.
  115. *
  116. * @type {UniformNode<uint>}
  117. * @default 12
  118. */
  119. this.stepCount = uniform( 12, 'uint' );
  120. /**
  121. * Power function applied to AO to make it appear darker/lighter. Should be in the range `[0, 4]`.
  122. *
  123. * @type {UniformNode<float>}
  124. * @default 1
  125. */
  126. this.aoIntensity = uniform( 1, 'float' );
  127. /**
  128. * Intensity of the indirect diffuse light. Should be in the range `[0, 100]`.
  129. *
  130. * @type {UniformNode<float>}
  131. * @default 10
  132. */
  133. this.giIntensity = uniform( 10, 'float' );
  134. /**
  135. * Effective sampling radius in world space. AO and GI can only have influence within that radius.
  136. * Should be in the range `[1, 25]`.
  137. *
  138. * @type {UniformNode<float>}
  139. * @default 12
  140. */
  141. this.radius = uniform( 12, 'float' );
  142. /**
  143. * Makes the sample distance in screen space instead of world-space (helps having more detail up close).
  144. *
  145. * @type {UniformNode<bool>}
  146. * @default true
  147. */
  148. this.useScreenSpaceSampling = uniform( true, 'bool' );
  149. /**
  150. * Controls samples distribution. It's an exponent applied at each step get increasing step size over the distance.
  151. * Should be in the range `[1, 3]`.
  152. *
  153. * @type {UniformNode<float>}
  154. * @default 2
  155. */
  156. this.expFactor = uniform( 2, 'float' );
  157. /**
  158. * Constant thickness value of objects on the screen in world units. Allows light to pass behind surfaces past that thickness value.
  159. * Should be in the range `[0.01, 10]`.
  160. *
  161. * @type {UniformNode<float>}
  162. * @default 1
  163. */
  164. this.thickness = uniform( 1, 'float' );
  165. /**
  166. * Whether to increase thickness linearly over distance or not (avoid losing detail over the distance).
  167. *
  168. * @type {UniformNode<bool>}
  169. * @default false
  170. */
  171. this.useLinearThickness = uniform( false, 'bool' );
  172. /**
  173. * How much light backface surfaces emit.
  174. * Should be in the range `[0, 1]`.
  175. *
  176. * @type {UniformNode<float>}
  177. * @default 0
  178. */
  179. this.backfaceLighting = uniform( 0, 'float' );
  180. /**
  181. * Whether to use temporal filtering or not. Setting this property to
  182. * `true` requires a temporal resolve like `TRAANode` or `SVGFNode` in the
  183. * pipeline, which converges the varying sampling pattern to a stable result.
  184. *
  185. * If setting this property to `false`, the sampling pattern is static and
  186. * a spatial denoise via `DenoiseNode` is required instead.
  187. *
  188. * @type {boolean}
  189. * @default true
  190. */
  191. this.useTemporalFiltering = true;
  192. // private uniforms
  193. /**
  194. * The resolution of the effect.
  195. *
  196. * @private
  197. * @type {UniformNode<vec2>}
  198. */
  199. this._resolution = uniform( new Vector2() );
  200. /**
  201. * Used to compute the effective step radius when viewSpaceSampling is `false`.
  202. *
  203. * @private
  204. * @type {UniformNode<float>}
  205. */
  206. this._halfProjScale = uniform( 1 );
  207. /**
  208. * Temporal direction that influences the rotation angle for each slice.
  209. *
  210. * @private
  211. * @type {UniformNode<float>}
  212. */
  213. this._temporalDirection = uniform( 0 );
  214. /**
  215. * Temporal offset added to the initial ray step.
  216. *
  217. * @private
  218. * @type {UniformNode<float>}
  219. */
  220. this._temporalOffset = uniform( 0 );
  221. /**
  222. * Represents the projection matrix of the scene's camera.
  223. *
  224. * @private
  225. * @type {UniformNode<mat4>}
  226. */
  227. this._cameraProjectionMatrix = uniform( camera.projectionMatrix );
  228. /**
  229. * Represents the inverse projection matrix of the scene's camera.
  230. *
  231. * @private
  232. * @type {UniformNode<mat4>}
  233. */
  234. this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
  235. /**
  236. * Represents the near value of the scene's camera.
  237. *
  238. * @private
  239. * @type {ReferenceNode<float>}
  240. */
  241. this._cameraNear = reference( 'near', 'float', camera );
  242. /**
  243. * Represents the far value of the scene's camera.
  244. *
  245. * @private
  246. * @type {ReferenceNode<float>}
  247. */
  248. this._cameraFar = reference( 'far', 'float', camera );
  249. /**
  250. * A reference to the scene's camera.
  251. *
  252. * @private
  253. * @type {PerspectiveCamera}
  254. */
  255. this._camera = camera;
  256. /**
  257. * The render target the GI is rendered into.
  258. *
  259. * @private
  260. * @type {RenderTarget}
  261. */
  262. this._ssgiRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  263. this._ssgiRenderTarget.texture.name = 'SSGI';
  264. /**
  265. * The material that is used to render the effect.
  266. *
  267. * @private
  268. * @type {NodeMaterial}
  269. */
  270. this._material = new NodeMaterial();
  271. this._material.name = 'SSGI';
  272. /**
  273. * The result of the effect is represented as a separate texture node.
  274. *
  275. * @private
  276. * @type {PassTextureNode}
  277. */
  278. this._textureNode = passTexture( this, this._ssgiRenderTarget.texture );
  279. }
  280. /**
  281. * Returns the result of the effect as a texture node.
  282. *
  283. * @return {PassTextureNode} A texture node that represents the result of the effect.
  284. */
  285. getTextureNode() {
  286. return this._textureNode;
  287. }
  288. /**
  289. * Sets the size of the effect.
  290. *
  291. * @param {number} width - The width of the effect.
  292. * @param {number} height - The height of the effect.
  293. */
  294. setSize( width, height ) {
  295. width = Math.round( width * this.resolutionScale );
  296. height = Math.round( height * this.resolutionScale );
  297. this._resolution.value.set( width, height );
  298. this._ssgiRenderTarget.setSize( width, height );
  299. this._halfProjScale.value = height / ( Math.tan( this._camera.fov * MathUtils.DEG2RAD * 0.5 ) * 2 ) * 0.5;
  300. }
  301. /**
  302. * This method is used to render the effect once per frame.
  303. *
  304. * @param {NodeFrame} frame - The current node frame.
  305. */
  306. updateBefore( frame ) {
  307. const { renderer } = frame;
  308. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  309. //
  310. const size = renderer.getDrawingBufferSize( _size );
  311. this.setSize( size.width, size.height );
  312. // update temporal uniforms
  313. if ( this.useTemporalFiltering === true ) {
  314. const frameId = frame.frameId;
  315. this._temporalDirection.value = _temporalRotations[ frameId % 6 ] / 360;
  316. this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];
  317. } else {
  318. this._temporalDirection.value = 1;
  319. this._temporalOffset.value = 1;
  320. }
  321. //
  322. _quadMesh.material = this._material;
  323. _quadMesh.name = 'SSGI';
  324. // clear
  325. renderer.setClearColor( 0x000000, 1 );
  326. // gi
  327. renderer.setRenderTarget( this._ssgiRenderTarget );
  328. _quadMesh.render( renderer );
  329. // restore
  330. RendererUtils.restoreRendererState( renderer, _rendererState );
  331. }
  332. /**
  333. * This method is used to setup the effect's TSL code.
  334. *
  335. * @param {NodeBuilder} builder - The current node builder.
  336. * @return {PassTextureNode}
  337. */
  338. setup( builder ) {
  339. const uvNode = uv();
  340. const MAX_RAY = uint( 32 );
  341. const globalOccludedBitfield = uint( 0 );
  342. const sampleDepth = ( uv ) => {
  343. const depth = this.depthNode.sample( uv ).r;
  344. if ( builder.renderer.logarithmicDepthBuffer === true ) {
  345. const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  346. return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
  347. }
  348. return depth;
  349. };
  350. const sampleNormal = ( uv ) => ( this.normalNode !== null ) ? this.normalNode.sample( uv ).rgb.normalize() : getNormalFromDepth( uv, this.depthNode.value, this._cameraProjectionMatrixInverse );
  351. const sampleBeauty = ( uv ) => this.beautyNode.sample( uv );
  352. // optimized version of getViewPosition() that exploits the sparsity of the inverse projection matrix
  353. const getViewPosition = ( screenPosition, depth ) => {
  354. let ndc;
  355. if ( builder.renderer.coordinateSystem === WebGPUCoordinateSystem ) {
  356. ndc = vec3( vec2( screenPosition.x, screenPosition.y.oneMinus() ).mul( 2.0 ).sub( 1.0 ), depth ).toConst();
  357. } else {
  358. ndc = vec3( screenPosition.x, screenPosition.y.oneMinus(), depth ).mul( 2.0 ).sub( 1.0 ).toConst();
  359. }
  360. const c0 = this._cameraProjectionMatrixInverse.element( 0 );
  361. const c1 = this._cameraProjectionMatrixInverse.element( 1 );
  362. const c2 = this._cameraProjectionMatrixInverse.element( 2 );
  363. const c3 = this._cameraProjectionMatrixInverse.element( 3 );
  364. const viewSpacePosition = vec3(
  365. ndc.x.mul( c0.x ).add( c3.x ),
  366. ndc.y.mul( c1.y ).add( c3.y ),
  367. ndc.z.mul( c2.z ).add( c3.z )
  368. );
  369. return viewSpacePosition.div( ndc.z.mul( c2.w ).add( c3.w ) );
  370. };
  371. // From Activision GTAO paper: https://www.activision.com/cdn/research/s2016_pbs_activision_occlusion.pptx
  372. const spatialOffsets = Fn( ( [ position ] ) => {
  373. return float( 0.25 ).mul( sub( position.y, position.x ).bitAnd( 3 ) );
  374. } ).setLayout( {
  375. name: 'spatialOffsets',
  376. type: 'float',
  377. inputs: [
  378. { name: 'position', type: 'vec2' }
  379. ]
  380. } );
  381. const GTAOFastAcos = Fn( ( [ value ] ) => {
  382. const outVal = abs( value ).mul( float( - 0.156583 ) ).add( HALF_PI );
  383. outVal.mulAssign( sqrt( abs( value ).oneMinus() ) );
  384. const x = value.x.greaterThanEqual( 0 ).select( outVal.x, PI.sub( outVal.x ) );
  385. const y = value.y.greaterThanEqual( 0 ).select( outVal.y, PI.sub( outVal.y ) );
  386. return vec2( x, y );
  387. } ).setLayout( {
  388. name: 'GTAOFastAcos',
  389. type: 'vec2',
  390. inputs: [
  391. { name: 'value', type: 'vec2' }
  392. ]
  393. } );
  394. const horizonSampling = Fn( ( [ directionIsRight, stepRadius, radiusVS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n ] ) => {
  395. const STEP_COUNT = this.stepCount.toConst();
  396. const EXP_FACTOR = this.expFactor.toConst();
  397. const THICKNESS = this.thickness.toConst();
  398. const BACKFACE_LIGHTING = this.backfaceLighting.toConst();
  399. const uvDirection = directionIsRight.select( vec2( 1, - 1 ), vec2( - 1, 1 ) ); // Port note: Because of different uv conventions, uv-y has a different sign
  400. const samplingDirection = directionIsRight.select( 1, - 1 );
  401. const color = vec3( 0 );
  402. Loop( { start: uint( 0 ), end: STEP_COUNT, type: 'uint', condition: '<' }, ( { i } ) => {
  403. const offset = pow( abs( mul( stepRadius, float( i ).add( initialRayStep ) ).div( radiusVS ) ), EXP_FACTOR ).mul( radiusVS ).toConst();
  404. const uvOffset = slideDirTexelSize.mul( max( offset, float( i ).add( 1 ) ) ).toConst();
  405. const sampleUV = uvNode.add( uvOffset.mul( uvDirection ) ).toConst();
  406. If( sampleUV.x.lessThanEqual( 0 ).or( sampleUV.y.lessThanEqual( 0 ) ).or( sampleUV.x.greaterThanEqual( 1 ) ).or( sampleUV.y.greaterThanEqual( 1 ) ), () => {
  407. Break();
  408. } );
  409. const sampleViewPosition = getViewPosition( sampleUV, sampleDepth( sampleUV ) ).toConst();
  410. const pixelToSample = sampleViewPosition.sub( viewPosition ).normalize().toConst();
  411. const linearThicknessMultiplier = this.useLinearThickness.select( sampleViewPosition.z.negate().div( this._cameraFar ).clamp().mul( 100 ), float( 1 ) );
  412. const pixelToSampleBackface = normalize( sampleViewPosition.sub( linearThicknessMultiplier.mul( viewDir ).mul( THICKNESS ) ).sub( viewPosition ) );
  413. let frontBackHorizon = vec2( dot( pixelToSample, viewDir ), dot( pixelToSampleBackface, viewDir ) );
  414. frontBackHorizon = GTAOFastAcos( clamp( frontBackHorizon, - 1, 1 ) );
  415. frontBackHorizon = clamp( div( mul( samplingDirection, frontBackHorizon.negate() ).sub( n.sub( HALF_PI ) ), PI ) ); // Port note: subtract half pi instead of adding it
  416. frontBackHorizon = directionIsRight.select( frontBackHorizon.yx, frontBackHorizon.xy ); // Front/Back get inverted depending on angle
  417. // inline ComputeOccludedBitfield() for easier debugging
  418. const minHorizon = frontBackHorizon.x.toConst();
  419. const maxHorizon = frontBackHorizon.y.toConst();
  420. const startHorizonInt = uint( frontBackHorizon.mul( float( MAX_RAY ) ) ).toConst();
  421. const angleHorizonInt = uint( ceil( maxHorizon.sub( minHorizon ).mul( float( MAX_RAY ) ) ) ).toConst();
  422. const angleHorizonBitfield = angleHorizonInt.greaterThan( uint( 0 ) ).select( uint( shiftRight( uint( 0xFFFFFFFF ), uint( 32 ).sub( MAX_RAY ).add( MAX_RAY.sub( angleHorizonInt ) ) ) ), uint( 0 ) ).toConst();
  423. let currentOccludedBitfield = angleHorizonBitfield.shiftLeft( startHorizonInt );
  424. currentOccludedBitfield = currentOccludedBitfield.bitAnd( globalOccludedBitfield.bitNot() );
  425. globalOccludedBitfield.assign( globalOccludedBitfield.bitOr( currentOccludedBitfield ) );
  426. const numOccludedZones = countOneBits( currentOccludedBitfield );
  427. //
  428. If( numOccludedZones.greaterThan( 0 ), () => { // If a ray hit the sample, that sample is visible from shading point
  429. const lightColor = sampleBeauty( sampleUV );
  430. If( luminance( lightColor ).greaterThan( 0.001 ), () => { // Continue if there is light at that location (intensity > 0)
  431. const lightDirectionVS = normalize( pixelToSample );
  432. const normalDotLightDirection = clamp( dot( viewNormal, lightDirectionVS ) );
  433. If( normalDotLightDirection.greaterThan( 0.001 ), () => { // Continue if light is facing surface normal
  434. const lightNormalVS = sampleNormal( sampleUV );
  435. // Intensity of outgoing light in the direction of the shading point
  436. let lightNormalDotLightDirection = dot( lightNormalVS, lightDirectionVS.negate() );
  437. const d = sign( lightNormalDotLightDirection ).lessThan( 0 ).select( abs( lightNormalDotLightDirection ).mul( BACKFACE_LIGHTING ), abs( lightNormalDotLightDirection ) );
  438. lightNormalDotLightDirection = BACKFACE_LIGHTING.greaterThan( 0 ).and( dot( lightNormalVS, viewDir ).greaterThan( 0 ) ).select( d, clamp( lightNormalDotLightDirection ) );
  439. color.rgb.addAssign( float( numOccludedZones ).div( float( MAX_RAY ) ).mul( lightColor ).mul( normalDotLightDirection ).mul( lightNormalDotLightDirection ) );
  440. } );
  441. } );
  442. } );
  443. } );
  444. return color;
  445. } );
  446. const gi = Fn( () => {
  447. const depth = sampleDepth( uvNode ).toVar();
  448. depth.greaterThanEqual( 1.0 ).discard();
  449. const viewPosition = getViewPosition( uvNode, depth ).toVar();
  450. const viewNormal = sampleNormal( uvNode ).toVar();
  451. const viewDir = normalize( viewPosition.xyz.negate() ).toVar();
  452. //
  453. // Per-pixel sampling jitter: the slice rotation ( noiseDirection ) and the initial ray step.
  454. let noiseDirection, initialRayStep;
  455. if ( this.noiseNode !== null ) {
  456. const noise = vec2( this.noiseNode );
  457. noiseDirection = noise.x;
  458. initialRayStep = noise.y;
  459. } else {
  460. const noiseOffset = spatialOffsets( screenCoordinate );
  461. const noiseJitterIdx = this._temporalDirection.mul( 0.02 ); // Port: Add noiseJitterIdx here for slightly better noise convergence with TRAA (see #31890 for more details)
  462. noiseDirection = interleavedGradientNoise( screenCoordinate ).add( this._temporalDirection );
  463. initialRayStep = fract( noiseOffset.add( this._temporalOffset ) ).add( rand( uvNode.add( noiseJitterIdx ).mul( 2 ).sub( 1 ) ) );
  464. }
  465. const ao = float( 0 );
  466. const color = vec3( 0 );
  467. const ROTATION_COUNT = this.sliceCount.toConst();
  468. const STEP_COUNT = this.stepCount.toConst();
  469. const AO_INTENSITY = this.aoIntensity.toConst();
  470. const GI_INTENSITY = this.giIntensity.toConst();
  471. const RADIUS = this.radius.toConst();
  472. // the step radius only depends on per-pixel values so it can be computed once for all slice directions
  473. const stepRadius = float( 0 );
  474. If( this.useScreenSpaceSampling, () => {
  475. stepRadius.assign( RADIUS.mul( this._resolution.x.div( 2 ) ).div( float( 16 ) ) ); // SSRT3 has a bug where stepRadius is divided by STEP_COUNT twice; fix here
  476. } ).Else( () => {
  477. stepRadius.assign( max( RADIUS.mul( this._halfProjScale ).div( viewPosition.z.negate() ), float( STEP_COUNT ) ) ); // Port note: viewZ is negative so a negate is required
  478. } );
  479. stepRadius.divAssign( float( STEP_COUNT ).add( 1 ) );
  480. const radiusVS = max( 1, float( STEP_COUNT.sub( 1 ) ) ).mul( stepRadius ).toConst();
  481. Loop( { start: uint( 0 ), end: ROTATION_COUNT, type: 'uint', condition: '<' }, ( { i } ) => {
  482. const rotationAngle = mul( float( i ).add( noiseDirection ), PI.div( float( ROTATION_COUNT ) ) ).toConst();
  483. const sliceDir = vec3( vec2( cos( rotationAngle ), sin( rotationAngle ) ), 0 ).toConst();
  484. const slideDirTexelSize = sliceDir.xy.mul( float( 1 ).div( this._resolution ) ).toConst();
  485. const planeNormal = normalize( cross( sliceDir, viewDir ) ).toConst();
  486. const tangent = cross( viewDir, planeNormal ).toConst();
  487. const projectedNormal = viewNormal.sub( planeNormal.mul( dot( viewNormal, planeNormal ) ) ).toConst();
  488. const projectedNormalNormalized = normalize( projectedNormal ).toConst();
  489. const cos_n = clamp( dot( projectedNormalNormalized, viewDir ), - 1, 1 ).toConst();
  490. const n = sign( dot( projectedNormal, tangent ) ).negate().mul( acos( cos_n ) ).toConst();
  491. globalOccludedBitfield.assign( 0 );
  492. Loop( { start: uint( 0 ), end: uint( 2 ), type: 'uint', condition: '<', name: 'side' }, ( { side } ) => {
  493. const directionIsRight = side.equal( uint( 0 ) );
  494. color.addAssign( horizonSampling( directionIsRight, stepRadius, radiusVS, viewPosition, slideDirTexelSize, initialRayStep, uvNode, viewDir, viewNormal, n ) );
  495. } );
  496. ao.addAssign( float( countOneBits( globalOccludedBitfield ) ).div( float( MAX_RAY ) ) );
  497. } );
  498. ao.divAssign( float( ROTATION_COUNT ) );
  499. ao.assign( pow( ao.clamp().oneMinus(), AO_INTENSITY ).clamp() );
  500. color.divAssign( float( ROTATION_COUNT ) );
  501. color.mulAssign( GI_INTENSITY );
  502. // scale color based on luminance
  503. const maxLuminance = float( 7 ).toConst(); // 7 represent a HDR luminance value
  504. const currentLuminance = luminance( color );
  505. const scale = currentLuminance.greaterThan( maxLuminance ).select( maxLuminance.div( currentLuminance ), float( 1 ) );
  506. color.mulAssign( scale );
  507. return vec4( color, ao );
  508. } );
  509. this._material.fragmentNode = gi().context( builder.getSharedContext() );
  510. this._material.needsUpdate = true;
  511. //
  512. return this._textureNode;
  513. }
  514. /**
  515. * Frees internal resources. This method should be called
  516. * when the effect is no longer required.
  517. */
  518. dispose() {
  519. this._ssgiRenderTarget.dispose();
  520. this._material.dispose();
  521. }
  522. }
  523. export default SSGINode;
  524. /**
  525. * TSL function for creating a SSGI effect.
  526. *
  527. * @tsl
  528. * @function
  529. * @param {TextureNode} beautyNode - The texture node that represents the input of the effect.
  530. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  531. * @param {TextureNode} normalNode - A texture node that represents the scene's normals.
  532. * @param {Camera} camera - The camera the scene is rendered with.
  533. * @returns {SSGINode}
  534. */
  535. export const ssgi = ( beautyNode, depthNode, normalNode, camera ) => new SSGINode( convertToTexture( beautyNode ), depthNode, normalNode, camera );
粤ICP备19079148号