SSSNode.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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 1.0
  81. */
  82. this.shadowIntensity = uniform( 1.0, '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. * @private
  171. * @type {UniformNode<float>}
  172. */
  173. this._temporalOffset = uniform( 0 );
  174. /**
  175. * The frame ID use when temporal filtering is enabled.
  176. *
  177. * @private
  178. * @type {UniformNode<uint>}
  179. */
  180. this._frameId = uniform( 0 );
  181. /**
  182. * A reference to the scene's main light.
  183. *
  184. * @private
  185. * @type {DirectionalLight}
  186. */
  187. this._mainLight = mainLight;
  188. /**
  189. * The camera the scene is rendered with.
  190. *
  191. * @private
  192. * @type {Camera}
  193. */
  194. this._camera = camera;
  195. /**
  196. * The render target the SSS is rendered into.
  197. *
  198. * @private
  199. * @type {RenderTarget}
  200. */
  201. this._sssRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, format: RedFormat, type: UnsignedByteType } );
  202. this._sssRenderTarget.texture.name = 'SSS';
  203. /**
  204. * The material that is used to render the effect.
  205. *
  206. * @private
  207. * @type {NodeMaterial}
  208. */
  209. this._material = new NodeMaterial();
  210. this._material.name = 'SSS';
  211. /**
  212. * The result of the effect is represented as a separate texture node.
  213. *
  214. * @private
  215. * @type {PassTextureNode}
  216. */
  217. this._textureNode = passTexture( this, this._sssRenderTarget.texture );
  218. }
  219. /**
  220. * Returns the result of the effect as a texture node.
  221. *
  222. * @return {PassTextureNode} A texture node that represents the result of the effect.
  223. */
  224. getTextureNode() {
  225. return this._textureNode;
  226. }
  227. /**
  228. * Sets the size of the effect.
  229. *
  230. * @param {number} width - The width of the effect.
  231. * @param {number} height - The height of the effect.
  232. */
  233. setSize( width, height ) {
  234. width = Math.round( this.resolutionScale * width );
  235. height = Math.round( this.resolutionScale * height );
  236. this._resolution.value.set( width, height );
  237. this._sssRenderTarget.setSize( width, height );
  238. }
  239. /**
  240. * This method is used to render the effect once per frame.
  241. *
  242. * @param {NodeFrame} frame - The current node frame.
  243. */
  244. updateBefore( frame ) {
  245. const { renderer } = frame;
  246. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  247. //
  248. const size = renderer.getDrawingBufferSize( _size );
  249. this.setSize( size.width, size.height );
  250. // update temporal uniforms
  251. if ( this.useTemporalFiltering === true ) {
  252. const frameId = frame.frameId;
  253. this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];
  254. this._frameId = frame.frameId;
  255. } else {
  256. this._temporalOffset.value = 0;
  257. this._frameId = 0;
  258. }
  259. //
  260. _quadMesh.material = this._material;
  261. _quadMesh.name = 'SSS';
  262. // clear
  263. renderer.setClearColor( 0xffffff, 1 );
  264. // sss
  265. renderer.setRenderTarget( this._sssRenderTarget );
  266. _quadMesh.render( renderer );
  267. // restore
  268. RendererUtils.restoreRendererState( renderer, _rendererState );
  269. }
  270. /**
  271. * This method is used to setup the effect's TSL code.
  272. *
  273. * @param {NodeBuilder} builder - The current node builder.
  274. * @return {PassTextureNode}
  275. */
  276. setup( builder ) {
  277. const uvNode = uv();
  278. const getViewZ = Fn( ( [ depth ] ) => {
  279. let viewZNode;
  280. if ( this._camera.isPerspectiveCamera ) {
  281. viewZNode = perspectiveDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  282. } else {
  283. viewZNode = orthographicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  284. }
  285. return viewZNode;
  286. } );
  287. const sampleDepth = ( uv ) => {
  288. const depth = this.depthNode.sample( uv ).r;
  289. if ( builder.renderer.logarithmicDepthBuffer === true ) {
  290. const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  291. return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
  292. }
  293. return depth;
  294. };
  295. const sss = Fn( () => {
  296. const depth = sampleDepth( uvNode ).toVar();
  297. depth.greaterThanEqual( 1.0 ).discard();
  298. // compute ray position and direction (in view-space)
  299. const rayStartPosition = getViewPosition( uvNode, depth, this._cameraProjectionMatrixInverse ).toVar( 'rayStartPosition' );
  300. const rayDirection = this._cameraViewMatrix.transformDirection( lightPosition( this._mainLight ).sub( lightTargetPosition( this._mainLight ) ) ).toConst( 'rayDirection' );
  301. const rayEndPosition = rayStartPosition.add( rayDirection.mul( this.maxDistance ) ).toConst( 'rayEndPosition' );
  302. // d0 and d1 are the start and maximum points of the ray in screen space
  303. const d0 = screenCoordinate.xy.toVar();
  304. const d1 = getScreenPosition( rayEndPosition, this._cameraProjectionMatrix ).mul( this._resolution ).toVar();
  305. // below variables are used to control the raymarching process
  306. // total length of the ray
  307. const totalLen = d1.sub( d0 ).length().toVar();
  308. // offset in x and y direction
  309. const xLen = d1.x.sub( d0.x ).toVar();
  310. const yLen = d1.y.sub( d0.y ).toVar();
  311. // determine the larger delta
  312. // The larger difference will help to determine how much to travel in the X and Y direction each iteration and
  313. // how many iterations are needed to travel the entire ray
  314. const totalStep = int( max( abs( xLen ), abs( yLen ) ).mul( this.quality.clamp() ) ).toConst();
  315. // step sizes in the x and y directions
  316. const xSpan = xLen.div( totalStep ).toVar();
  317. const ySpan = yLen.div( totalStep ).toVar();
  318. // compute noise based ray offset
  319. const noise = interleavedGradientNoise( screenCoordinate );
  320. const offset = fract( noise.add( this._temporalOffset ) ).add( rand( uvNode.add( this._frameId ) ) ).toConst( 'offset' );
  321. const occlusion = float( 0 ).toVar();
  322. Loop( totalStep, ( { i } ) => {
  323. // advance on the ray by computing a new position in screen coordinates
  324. const xy = vec2( d0.x.add( xSpan.mul( float( i ).add( offset ) ) ), d0.y.add( ySpan.mul( float( i ).add( offset ) ) ) ).toVar();
  325. // stop processing if the new position lies outside of the screen
  326. If( xy.x.lessThan( 0 ).or( xy.x.greaterThan( this._resolution.x ) ).or( xy.y.lessThan( 0 ) ).or( xy.y.greaterThan( this._resolution.y ) ), () => {
  327. Break();
  328. } );
  329. // compute new uv, depth and viewZ for the next fragment
  330. const uvNode = xy.div( this._resolution );
  331. const fragmentDepth = sampleDepth( uvNode ).toConst();
  332. const fragmentViewZ = getViewZ( fragmentDepth ).toConst( 'fragmentViewZ' );
  333. const s = xy.sub( d0 ).length().div( totalLen ).toVar();
  334. const rayPosition = mix( rayStartPosition, rayEndPosition, s );
  335. const depthDelta = rayPosition.z.sub( fragmentViewZ ).negate(); // Port note: viewZ values are negative in three
  336. // check if the camera can't "see" the ray (ray depth must be larger than the camera depth, so positive depth_delta)
  337. If( depthDelta.greaterThan( 0 ).and( depthDelta.lessThan( this.thickness ) ), () => {
  338. // mark as occluded
  339. occlusion.assign( this.shadowIntensity );
  340. Break();
  341. } );
  342. } );
  343. return occlusion.oneMinus();
  344. } );
  345. this._material.fragmentNode = sss().context( builder.getSharedContext() );
  346. this._material.needsUpdate = true;
  347. return this._textureNode;
  348. }
  349. /**
  350. * Frees internal resources. This method should be called
  351. * when the effect is no longer required.
  352. */
  353. dispose() {
  354. this._sssRenderTarget.dispose();
  355. this._material.dispose();
  356. }
  357. }
  358. export default SSSNode;
  359. /**
  360. * TSL function for creating a SSS effect.
  361. *
  362. * @tsl
  363. * @function
  364. * @param {TextureNode} depthNode - A texture node that represents the scene's depth.
  365. * @param {Camera} camera - The camera the scene is rendered with.
  366. * @param {DirectionalLight} mainLight - The main directional light of the scene.
  367. * @returns {SSSNode}
  368. */
  369. export const sss = ( depthNode, camera, mainLight ) => nodeObject( new SSSNode( depthNode, camera, mainLight ) );
粤ICP备19079148号