SSSNode.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import { RedFormat, RenderTarget, Vector2, RendererUtils, QuadMesh, TempNode, NodeMaterial, NodeUpdateType, UnsignedByteType } from 'three/webgpu';
  2. import { reference, viewZToPerspectiveDepth, logarithmicDepthToViewZ, getScreenPosition, getViewPosition, float, Break, Loop, int, max, abs, If, interleavedGradientNoise, screenCoordinate, nodeObject, Fn, passTexture, uv, uniform, perspectiveDepthToViewZ, orthographicDepthToViewZ, vec2, lightPosition, lightTargetPosition, fract, rand, mix } from 'three/tsl';
  3. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  4. const _size = /*@__PURE__*/ new Vector2();
  5. const _spatialOffsets = [ 0, 0.5, 0.25, 0.75 ];
  6. let _rendererState;
  7. /**
  8. * Post processing node for applying Screen-Space Shadows (SSS) to a scene.
  9. *
  10. * Screen-Space Shadows (also known as Contact Shadows) should ideally be used to complement
  11. * traditional shadow maps. They are best suited for rendering detailed shadows of smaller
  12. * objects at a closer scale like intricate shadowing on highly detailed models. In other words:
  13. * Use Shadow Maps for the foundation and Screen-Space Shadows for the details.
  14. *
  15. * The shadows produced by this implementation might have too hard edges for certain use cases.
  16. * Use a box, gaussian or hash blur to soften the edges before doing the composite with the
  17. * beauty pass. Code example:
  18. *
  19. * ```js
  20. * const sssPass = sss( scenePassDepth, camera, mainLight );
  21. *
  22. * const sssBlur = boxBlur( sssPass.r, { size: 2, separation: 1 } ); // optional blur
  23. * ```
  24. *
  25. * Limitations:
  26. *
  27. * - Ideally the maximum shadow length should not exceed `1` meter. Otherwise the effect gets
  28. * computationally very expensive since more samples during the ray marching process are evaluated.
  29. * You can mitigate this issue by reducing the `quality` paramter.
  30. * - The effect can only be used with a single directional light, the main light of your scene.
  31. * This main light usually represents the sun or daylight.
  32. * - Like other Screen-Space techniques SSS can only honor objects in the shadowing computation that
  33. * are currently visible within the camera's view.
  34. *
  35. * References:
  36. * - {@link https://panoskarabelas.com/posts/screen_space_shadows/}.
  37. * - {@link https://www.bendstudio.com/blog/inside-bend-screen-space-shadows/}.
  38. *
  39. * @augments TempNode
  40. * @three_import import { sss } from 'three/addons/tsl/display/SSSNode.js';
  41. */
  42. class SSSNode extends TempNode {
  43. static get type() {
  44. return 'SSSNode';
  45. }
  46. /**
  47. * Constructs a new SSS node.
  48. *
  49. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  50. * @param {Camera} camera - The camera the scene is rendered with.
  51. * @param {DirectionalLight} mainLight - The main directional light of the scene.
  52. */
  53. constructor( depthNode, camera, mainLight ) {
  54. super( 'float' );
  55. /**
  56. * A node that represents the beauty pass's depth.
  57. *
  58. * @type {TextureNode}
  59. */
  60. this.depthNode = depthNode;
  61. /**
  62. * Maximum shadow length in world units. Longer shadows result in more computational
  63. * overhead.
  64. *
  65. * @type {UniformNode<float>}
  66. * @default 0.1
  67. */
  68. this.maxDistance = uniform( 0.1, 'float' );
  69. /**
  70. * Depth testing thickness.
  71. *
  72. * @type {UniformNode<float>}
  73. * @default 0.01
  74. */
  75. this.thickness = uniform( 0.01, 'float' );
  76. /**
  77. * Shadow intensity. Must be in the range `[0, 1]`.
  78. *
  79. * @type {UniformNode<float>}
  80. * @default 0.5
  81. */
  82. this.shadowIntensity = uniform( 0.5, 'float' );
  83. /**
  84. * This parameter controls how detailed the raymarching process works.
  85. * The value ranges is `[0,1]` where `1` means best quality (the maximum number
  86. * of raymarching iterations/samples) and `0` means no samples at all.
  87. *
  88. * A quality of `0.5` is usually sufficient for most use cases. Try to keep
  89. * this parameter as low as possible. Larger values result in noticeable more
  90. * overhead.
  91. *
  92. * @type {UniformNode<float>}
  93. * @default 0.5
  94. */
  95. this.quality = uniform( 0.5 );
  96. /**
  97. * The resolution scale. Valid values are in the range
  98. * `[0,1]`. `1` means best quality but also results in
  99. * more computational overhead. Setting to `0.5` means
  100. * the effect is computed in half-resolution.
  101. *
  102. * @type {number}
  103. * @default 1
  104. */
  105. this.resolutionScale = 1;
  106. /**
  107. * Whether to use temporal filtering or not. Setting this property to
  108. * `true` requires the usage of `TRAANode`. This will help to reduce noice
  109. * although it introduces typical TAA artifacts like ghosting and temporal
  110. * instabilities.
  111. *
  112. * @type {boolean}
  113. * @default false
  114. */
  115. this.useTemporalFiltering = false;
  116. /**
  117. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
  118. * its effect once per frame in `updateBefore()`.
  119. *
  120. * @type {string}
  121. * @default 'frame'
  122. */
  123. this.updateBeforeType = NodeUpdateType.FRAME;
  124. // private uniforms
  125. /**
  126. * Represents the view matrix of the scene's camera.
  127. *
  128. * @private
  129. * @type {UniformNode<mat4>}
  130. */
  131. this._cameraViewMatrix = uniform( camera.matrixWorldInverse );
  132. /**
  133. * Represents the projection matrix of the scene's camera.
  134. *
  135. * @private
  136. * @type {UniformNode<mat4>}
  137. */
  138. this._cameraProjectionMatrix = uniform( camera.projectionMatrix );
  139. /**
  140. * Represents the inverse projection matrix of the scene's camera.
  141. *
  142. * @private
  143. * @type {UniformNode<mat4>}
  144. */
  145. this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
  146. /**
  147. * Represents the near value of the scene's camera.
  148. *
  149. * @private
  150. * @type {ReferenceNode<float>}
  151. */
  152. this._cameraNear = reference( 'near', 'float', camera );
  153. /**
  154. * Represents the far value of the scene's camera.
  155. *
  156. * @private
  157. * @type {ReferenceNode<float>}
  158. */
  159. this._cameraFar = reference( 'far', 'float', camera );
  160. /**
  161. * The resolution of the pass.
  162. *
  163. * @private
  164. * @type {UniformNode<vec2>}
  165. */
  166. this._resolution = uniform( new Vector2() );
  167. /**
  168. * Temporal offset added to the initial ray step.
  169. *
  170. * @type {UniformNode<float>}
  171. */
  172. this._temporalOffset = uniform( 0 );
  173. /**
  174. * The frame ID use when temporal filtering is enabled.
  175. *
  176. * @type {UniformNode<uint>}
  177. */
  178. this._frameId = uniform( 0 );
  179. /**
  180. * A reference to the scene's main light.
  181. *
  182. * @private
  183. * @type {DirectionalLight}
  184. */
  185. this._mainLight = mainLight;
  186. /**
  187. * The camera the scene is rendered with.
  188. *
  189. * @private
  190. * @type {Camera}
  191. */
  192. this._camera = camera;
  193. /**
  194. * The render target the SSS is rendered into.
  195. *
  196. * @private
  197. * @type {RenderTarget}
  198. */
  199. this._sssRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, format: RedFormat, type: UnsignedByteType } );
  200. this._sssRenderTarget.texture.name = 'SSS';
  201. /**
  202. * The material that is used to render the effect.
  203. *
  204. * @private
  205. * @type {NodeMaterial}
  206. */
  207. this._material = new NodeMaterial();
  208. this._material.name = 'SSS';
  209. /**
  210. * The result of the effect is represented as a separate texture node.
  211. *
  212. * @private
  213. * @type {PassTextureNode}
  214. */
  215. this._textureNode = passTexture( this, this._sssRenderTarget.texture );
  216. }
  217. /**
  218. * Returns the result of the effect as a texture node.
  219. *
  220. * @return {PassTextureNode} A texture node that represents the result of the effect.
  221. */
  222. getTextureNode() {
  223. return this._textureNode;
  224. }
  225. /**
  226. * Sets the size of the effect.
  227. *
  228. * @param {number} width - The width of the effect.
  229. * @param {number} height - The height of the effect.
  230. */
  231. setSize( width, height ) {
  232. width = Math.round( this.resolutionScale * width );
  233. height = Math.round( this.resolutionScale * height );
  234. this._resolution.value.set( width, height );
  235. this._sssRenderTarget.setSize( width, height );
  236. }
  237. /**
  238. * This method is used to render the effect once per frame.
  239. *
  240. * @param {NodeFrame} frame - The current node frame.
  241. */
  242. updateBefore( frame ) {
  243. const { renderer } = frame;
  244. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  245. //
  246. const size = renderer.getDrawingBufferSize( _size );
  247. this.setSize( size.width, size.height );
  248. // update temporal uniforms
  249. if ( this.useTemporalFiltering === true ) {
  250. const frameId = frame.frameId;
  251. this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];
  252. this._frameId = frame.frameId;
  253. } else {
  254. this._temporalOffset.value = 0;
  255. this._frameId = 0;
  256. }
  257. //
  258. _quadMesh.material = this._material;
  259. _quadMesh.name = 'SSS';
  260. // clear
  261. renderer.setClearColor( 0xffffff, 1 );
  262. // sss
  263. renderer.setRenderTarget( this._sssRenderTarget );
  264. _quadMesh.render( renderer );
  265. // restore
  266. RendererUtils.restoreRendererState( renderer, _rendererState );
  267. }
  268. /**
  269. * This method is used to setup the effect's TSL code.
  270. *
  271. * @param {NodeBuilder} builder - The current node builder.
  272. * @return {PassTextureNode}
  273. */
  274. setup( builder ) {
  275. const uvNode = uv();
  276. const getViewZ = Fn( ( [ depth ] ) => {
  277. let viewZNode;
  278. if ( this._camera.isPerspectiveCamera ) {
  279. viewZNode = perspectiveDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  280. } else {
  281. viewZNode = orthographicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  282. }
  283. return viewZNode;
  284. } );
  285. const sampleDepth = ( uv ) => {
  286. const depth = this.depthNode.sample( uv ).r;
  287. if ( builder.renderer.logarithmicDepthBuffer === true ) {
  288. const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  289. return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
  290. }
  291. return depth;
  292. };
  293. const sss = Fn( () => {
  294. const depth = sampleDepth( uvNode ).toVar();
  295. depth.greaterThanEqual( 1.0 ).discard();
  296. // compute ray position and direction (in view-space)
  297. const rayStartPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toVar( 'rayStartPosition' );
  298. const rayDirection = this._cameraViewMatrix.transformDirection( lightPosition( this._mainLight ).sub( lightTargetPosition( this._mainLight ) ) ).toConst( 'rayDirection' );
  299. const rayEndPosition = rayStartPosition.add( rayDirection.mul( this.maxDistance ) ).toConst( 'rayEndPosition' );
  300. // d0 and d1 are the start and maximum points of the ray in screen space
  301. const d0 = screenCoordinate.xy.toVar();
  302. const d1 = getScreenPosition( rayEndPosition, this._cameraProjectionMatrix ).mul( this._resolution ).toVar();
  303. // below variables are used to control the raymarching process
  304. // total length of the ray
  305. const totalLen = d1.sub( d0 ).length().toVar();
  306. // offset in x and y direction
  307. const xLen = d1.x.sub( d0.x ).toVar();
  308. const yLen = d1.y.sub( d0.y ).toVar();
  309. // determine the larger delta
  310. // The larger difference will help to determine how much to travel in the X and Y direction each iteration and
  311. // how many iterations are needed to travel the entire ray
  312. const totalStep = int( max( abs( xLen ), abs( yLen ) ).mul( this.quality.clamp() ) ).toConst();
  313. // step sizes in the x and y directions
  314. const xSpan = xLen.div( totalStep ).toVar();
  315. const ySpan = yLen.div( totalStep ).toVar();
  316. // compute noise based ray offset
  317. const noise = interleavedGradientNoise( screenCoordinate );
  318. const offset = fract( noise.add( this._temporalOffset ) ).add( rand( uvNode.add( this._frameId ) ) ).toConst( 'offset' );
  319. const occlusion = float( 0 ).toVar();
  320. Loop( totalStep, ( { i } ) => {
  321. // advance on the ray by computing a new position in screen coordinates
  322. const xy = vec2( d0.x.add( xSpan.mul( float( i ).add( offset ) ) ), d0.y.add( ySpan.mul( float( i ).add( offset ) ) ) ).toVar();
  323. // stop processing if the new position lies outside of the screen
  324. If( xy.x.lessThan( 0 ).or( xy.x.greaterThan( this._resolution.x ) ).or( xy.y.lessThan( 0 ) ).or( xy.y.greaterThan( this._resolution.y ) ), () => {
  325. Break();
  326. } );
  327. // compute new uv, depth and viewZ for the next fragment
  328. const uvNode = xy.div( this._resolution );
  329. const fragmentDepth = sampleDepth( uvNode ).toConst();
  330. const fragmentViewZ = getViewZ( fragmentDepth ).toConst( 'fragmentViewZ' );
  331. const s = xy.sub( d0 ).length().div( totalLen ).toVar();
  332. const rayPosition = mix( rayStartPosition, rayEndPosition, s );
  333. const depthDelta = rayPosition.z.sub( fragmentViewZ ).negate(); // Port note: viewZ values are negative in three
  334. // check if the camera can't "see" the ray (ray depth must be larger than the camera depth, so positive depth_delta)
  335. If( depthDelta.greaterThan( 0 ).and( depthDelta.lessThan( this.thickness ) ), () => {
  336. // mark as occluded
  337. occlusion.assign( this.shadowIntensity );
  338. Break();
  339. } );
  340. } );
  341. return occlusion.oneMinus();
  342. } );
  343. this._material.fragmentNode = sss().context( builder.getSharedContext() );
  344. this._material.needsUpdate = true;
  345. return this._textureNode;
  346. }
  347. /**
  348. * Frees internal resources. This method should be called
  349. * when the effect is no longer required.
  350. */
  351. dispose() {
  352. this._sssRenderTarget.dispose();
  353. this._material.dispose();
  354. }
  355. }
  356. export default SSSNode;
  357. /**
  358. * TSL function for creating a SSS effect.
  359. *
  360. * @tsl
  361. * @function
  362. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  363. * @param {Camera} camera - The camera the scene is rendered with.
  364. * @param {DirectionalLight} mainLight - The main directional light of the scene.
  365. * @returns {SSSNode}
  366. */
  367. export const sss = ( depthNode, camera, mainLight ) => nodeObject( new SSSNode( depthNode, camera, mainLight ) );
粤ICP备19079148号