SSRNode.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353
  1. import { Break, Continue, Fn, If, Loop, abs, bool, cross, distance, div, dot, float, getScreenPosition, getViewPosition, int, logarithmicDepthToViewZ, luminance, max, min, mix, mul, nodeObject, normalize, orthographicDepthToViewZ, passTexture, perspectiveDepthToViewZ, reference, reflect, sub, texture, trunc, uniform, uv, vec2, vec3, vec4, viewZToPerspectiveDepth } from 'three/tsl';
  2. import { HalfFloatType, LinearFilter, LinearMipmapLinearFilter, Matrix4, NodeMaterial, NodeUpdateType, QuadMesh, RenderTarget, RendererUtils, TempNode, Vector2, Vector3 } from 'three/webgpu';
  3. import { bindAnalyticNoise } from '../utils/RNoise.js';
  4. import { ENV_RAY_LENGTH, getSpecularDominantFactor, ggxReflectionSample } from '../utils/SpecularHelpers.js';
  5. import { boxBlur } from './boxBlur.js';
  6. import ImportanceSampledEnvironment from './ImportanceSampledEnvironment.js';
  7. const _quadMesh = /*@__PURE__*/ new QuadMesh();
  8. const _size = /*@__PURE__*/ new Vector2();
  9. let _rendererState;
  10. // Maximum ray-march step count; `quality` (0..1) scales it to a fixed per-ray count.
  11. const MAX_STEPS = 64;
  12. /**
  13. * @typedef {Object} SSRNodeOptions
  14. * @property {boolean} [stochastic=false] - When `false`, traces a single mirror reflection and softens roughness with a blur pass (first-generation SSR). When `true`, varies the reflection direction per pixel with stochastic GGX rays (second-generation SSR); higher quality on rough/glossy surfaces but noisier, so it expects a temporal/spatial denoiser downstream.
  15. * @property {Node<float>} [metalnessNode=null] - Per-pixel metalness. Drives GGX reflection sampling and, with `reflectNonMetals=false`, the non-metal early-out.
  16. * @property {Node<float>} [roughnessNode=null] - Per-pixel roughness. Drives GGX sampling and the blur mip selection.
  17. * @property {boolean} [reflectNonMetals=false] - Only used when `stochastic=false`. When `false`, non-metallic surfaces are discarded for a noticeable performance gain; set `true` to also reflect dielectrics (e.g. marble, polished wood, plastic).
  18. * @property {Texture} [environmentNode=null] - Equirectangular HDR environment map with CPU-side `image.data` (e.g. from RGBELoader). Not compatible with PMREM / `scene.environment` cubemaps.
  19. * @property {boolean} [envImportanceSampling=false] - When `true`, precomputes env-luminance CDF tables and uses MIS for environment misses. Build-time only.
  20. * @property {Node} [diffuseNode=null] - Scene diffuse / base color. Defaults to `vec3(1)` in the shader when omitted.
  21. * @property {boolean} [binaryRefine=false] - Sub-step binary-search refinement of detected hits. Compile-time constant (baked into the shader at construction).
  22. * @property {Camera} [camera=null] - Camera the scene is rendered with. Inferred from the color pass when omitted.
  23. */
  24. /**
  25. * Post processing node for computing screen space reflections (SSR).
  26. *
  27. * Reference: {@link https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html}
  28. *
  29. * @augments TempNode
  30. * @three_import import { ssr } from 'three/addons/tsl/display/SSRNode.js';
  31. */
  32. class SSRNode extends TempNode {
  33. static get type() {
  34. return 'SSRNode';
  35. }
  36. /**
  37. * Constructs a new SSR node.
  38. *
  39. * @param {Node<vec4>} colorNode - The node that represents the beauty pass.
  40. * @param {Node<float>} depthNode - A node that represents the beauty pass's depth.
  41. * @param {Node<vec3>} normalNode - A node that represents the beauty pass's normals.
  42. * @param {SSRNodeOptions} [options] - Optional inputs for material and environment data.
  43. */
  44. constructor( colorNode, depthNode, normalNode, options = {} ) {
  45. super( 'vec4' );
  46. const {
  47. stochastic = false,
  48. metalnessNode = null,
  49. roughnessNode = null,
  50. reflectNonMetals = false,
  51. environmentNode = null,
  52. envImportanceSampling = false,
  53. diffuseNode = null,
  54. binaryRefine = false
  55. } = options;
  56. let camera = options.camera ?? null;
  57. /**
  58. * When `true`, the reflection direction is varied per pixel with stochastic GGX rays
  59. * (second-generation SSR). When `false`, a single mirror reflection is traced and
  60. * roughness is softened with a blur pass (first-generation SSR).
  61. *
  62. * @type {boolean}
  63. */
  64. this.stochastic = stochastic;
  65. /**
  66. * When `true`, env-luminance CDF tables are built and MIS is used for environment misses.
  67. * Fixed at construction time.
  68. *
  69. * @type {boolean}
  70. */
  71. this.envImportanceSampling = envImportanceSampling;
  72. /**
  73. * The node that represents the beauty pass.
  74. *
  75. * @type {Node<vec4>}
  76. */
  77. this.colorNode = colorNode;
  78. /**
  79. * A node that represents the scene's diffuse color (typically the MRT `diffuseColor` attachment).
  80. * When `null`, the shader uses `vec3(1)`.
  81. *
  82. * @type {?Node<vec4>}
  83. */
  84. this.diffuseNode = diffuseNode !== null ? nodeObject( diffuseNode ) : null;
  85. /**
  86. * A node that represents the beauty pass's depth.
  87. *
  88. * @type {Node<float>}
  89. */
  90. this.depthNode = depthNode;
  91. /**
  92. * A node that represents the beauty pass's normals.
  93. *
  94. * @type {Node<vec3>}
  95. */
  96. this.normalNode = normalNode;
  97. /**
  98. * Per-pixel metalness, used to drive the GGX reflection sampling and the non-metal
  99. * early-out. When `null`, the shader treats surfaces as non-metallic.
  100. *
  101. * @type {?Node<float>}
  102. */
  103. this.metalnessNode = metalnessNode;
  104. /**
  105. * Per-pixel roughness, used to drive the GGX reflection sampling and the blur mip
  106. * selection. When `null`, the shader treats surfaces as fully smooth.
  107. *
  108. * @type {?Node<float>}
  109. */
  110. this.roughnessNode = roughnessNode;
  111. /**
  112. * Only used when {@link SSRNode#stochastic} is `false`. When `false`, non-metallic
  113. * surfaces are discarded for a noticeable performance gain; set `true` to also
  114. * reflect dielectrics. Baked into the shader as a compile-time constant; assigning a
  115. * new value recompiles the SSR material.
  116. *
  117. * @type {boolean}
  118. * @default false
  119. */
  120. this._reflectNonMetals = reflectNonMetals;
  121. /**
  122. * The resolution scale. Valid values are in the range
  123. * `[0,1]`. `1` means best quality but also results in
  124. * more computational overhead. Setting to `0.5` means
  125. * the effect is computed in half-resolution.
  126. *
  127. * @type {number}
  128. * @default 1
  129. */
  130. this.resolutionScale = 1;
  131. /**
  132. * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
  133. * its effect once per frame in `updateBefore()`.
  134. *
  135. * @type {string}
  136. * @default 'frame'
  137. */
  138. this.updateBeforeType = NodeUpdateType.FRAME;
  139. /**
  140. * Controls how far a fragment can reflect. Increasing this value result in more
  141. * computational overhead but also increases the reflection distance.
  142. *
  143. * @type {UniformNode<float>}
  144. */
  145. this.maxDistance = uniform( 1 );
  146. /**
  147. * Controls the cutoff between what counts as a possible reflection hit and what does not.
  148. *
  149. * @type {UniformNode<float>}
  150. */
  151. this.thickness = uniform( 0.1 );
  152. /**
  153. * A multiplier for the overall reflection intensity. `1` leaves the
  154. * reflections unchanged, lower values dim them and higher values boost them.
  155. *
  156. * @type {UniformNode<float>}
  157. * @default 1
  158. */
  159. this.intensity = uniform( 1 );
  160. /**
  161. * Screen-edge fade width, in UV units. As a screen-space hit approaches a screen
  162. * border, the reflection is faded over this distance — either toward the environment
  163. * reflection ({@link SSRNode#screenEdgeFadeBlack} `false`) or to zero intensity
  164. * (`true`). `0` disables it.
  165. *
  166. * @type {UniformNode<float>}
  167. * @default 0.2
  168. */
  169. this.screenEdgeFade = uniform( 0.2 );
  170. /**
  171. * When `true`, SSR fades to zero near screen borders instead of blending toward
  172. * the environment map. Hits are faded by the reflection sample UV; misses are
  173. * faded by the surface pixel UV.
  174. *
  175. * Baked into the shader as a compile-time constant so the unused fade branch is
  176. * eliminated; assigning a new value recompiles the SSR material.
  177. *
  178. * @type {boolean}
  179. * @default false
  180. */
  181. this._screenEdgeFadeBlack = false;
  182. /**
  183. * Absolute env luminance cap. HDR env samples above this are scaled down (hue preserved).
  184. *
  185. * @type {UniformNode<float>}
  186. * @default 10
  187. */
  188. this.maxLuminance = uniform( 10 );
  189. /**
  190. * This parameter controls how detailed the raymarching process works.
  191. * The value ranges is `[0,1]` where `1` means best quality (the maximum number
  192. * of raymarching iterations/samples) and `0` means no samples at all.
  193. *
  194. * A quality of `0.5` is usually sufficient for most use cases. Try to keep
  195. * this parameter as low as possible. Larger values result in noticeable more
  196. * overhead.
  197. *
  198. * @type {UniformNode<float>}
  199. */
  200. this.quality = uniform( 0.5 );
  201. /**
  202. * Mirror bias for the stochastic GGX sampling. Concentrates the reflected rays toward
  203. * the lobe's narrow (near-mirror) core, trading a small amount of bias for less noise.
  204. * `0` samples the full VNDF lobe; values toward `1` tighten the cone. Range `[0,1]`.
  205. *
  206. * @type {UniformNode<float>}
  207. * @default 0.5
  208. */
  209. this.mirrorBias = uniform( 0.5 );
  210. /**
  211. * The quality of the blur. Must be an integer in the range `[1,3]`.
  212. *
  213. * Baked into the blur shader as a compile-time constant so the `(size*2+1)²`
  214. * sample loop unrolls; assigning a new value recompiles the blur material.
  215. *
  216. * @type {number}
  217. * @default 2
  218. */
  219. this._blurQuality = 2;
  220. /**
  221. * Enables sub-step binary-search refinement of a detected hit. When on, a coarse
  222. * crossing is bisected toward the exact intersection (sharper hits, less step
  223. * aliasing) at the cost of extra depth samples. Baked into the shader as a
  224. * compile-time constant; assigning a new value rebuilds the SSR material.
  225. *
  226. * @type {boolean}
  227. * @default false
  228. */
  229. this._binaryRefine = binaryRefine;
  230. /**
  231. * Non-linear step distribution exponent. `1` = uniform steps; `> 1` concentrates
  232. * samples near the ray origin — where most short-range reflections are missed — and
  233. * spaces them out toward maxDistance, as `s = (i / steps) ^ stepExponent`.
  234. *
  235. * Baked into the shader as a compile-time constant so `pow()` folds to a few
  236. * multiplies; assigning a new value recompiles the SSR material. Only used by the
  237. * stochastic reflection path.
  238. *
  239. * @type {number}
  240. * @default 2
  241. */
  242. this._stepExponent = 2;
  243. /**
  244. * HDR environment map for screen-space misses.
  245. *
  246. * @type {?Texture}
  247. */
  248. this.environmentNode = environmentNode;
  249. /**
  250. * A node that represents the history texture for multi-bounce reflections.
  251. *
  252. * @type {?Texture}
  253. */
  254. this.historyTexture = null;
  255. /**
  256. * A node that represents the velocity texture for reprojection.
  257. *
  258. * @type {?Node<vec2>}
  259. */
  260. this.velocityTexture = null;
  261. //
  262. if ( camera === null ) {
  263. if ( this.colorNode.passNode && this.colorNode.passNode.isPassNode === true ) {
  264. camera = this.colorNode.passNode.camera;
  265. } else {
  266. throw new Error( 'THREE.SSRNode: No camera found. ssr() requires a camera.' );
  267. }
  268. }
  269. /**
  270. * The camera the scene is rendered with.
  271. *
  272. * @type {Camera}
  273. */
  274. this.camera = camera;
  275. /**
  276. * The spread of the blur. Automatically set when generating mips.
  277. *
  278. * @private
  279. * @type {UniformNode<int>}
  280. */
  281. this._blurSpread = uniform( 1 );
  282. /**
  283. * Represents the projection matrix of the scene's camera.
  284. *
  285. * @private
  286. * @type {UniformNode<mat4>}
  287. */
  288. this._cameraProjectionMatrix = uniform( camera.projectionMatrix );
  289. /**
  290. * Represents the inverse projection matrix of the scene's camera.
  291. *
  292. * @private
  293. * @type {UniformNode<mat4>}
  294. */
  295. this._cameraProjectionMatrixInverse = uniform( camera.projectionMatrixInverse );
  296. /**
  297. * Represents the near value of the scene's camera.
  298. *
  299. * @private
  300. * @type {ReferenceNode<float>}
  301. */
  302. this._cameraNear = reference( 'near', 'float', camera );
  303. /**
  304. * Represents the far value of the scene's camera.
  305. *
  306. * @private
  307. * @type {ReferenceNode<float>}
  308. */
  309. this._cameraFar = reference( 'far', 'float', camera );
  310. this._cameraWorldMatrix = uniform( new Matrix4().copy( camera.matrixWorld ) );
  311. this._cameraWorldPosition = uniform( new Vector3().copy( camera.position ) );
  312. this._cameraViewMatrix = uniform( new Matrix4().copy( camera.matrixWorld ) );
  313. this._cameraViewMatrixInverse = uniform( new Matrix4().copy( camera.matrixWorldInverse ) );
  314. /**
  315. * The resolution of the pass.
  316. *
  317. * @private
  318. * @type {UniformNode<vec2>}
  319. */
  320. this._resolution = uniform( new Vector2() );
  321. this._noiseIndex = uniform( 0 );
  322. /**
  323. * CDF-backed environment sampler. Created when {@link setEnvMap} is called.
  324. *
  325. * @private
  326. * @type {?ImportanceSampledEnvironment}
  327. */
  328. this._importanceEnvironment = null;
  329. /**
  330. * Intensity multiplier applied to environment-map reflections on screen-space
  331. * misses and at screen edges. Defaults to π to match the former hardcoded multiplier.
  332. *
  333. * @type {UniformNode<float>}
  334. * @default Math.PI
  335. */
  336. this.environmentIntensity = uniform( Math.PI );
  337. /**
  338. * The render target the SSR is rendered into.
  339. *
  340. * @private
  341. * @type {RenderTarget}
  342. */
  343. this._ssrRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
  344. this._ssrRenderTarget.texture.name = 'SSRNode.SSR';
  345. /**
  346. * The render target for the blurred SSR reflections.
  347. *
  348. * @private
  349. * @type {RenderTarget}
  350. */
  351. this._blurRenderTarget = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType, minFilter: LinearMipmapLinearFilter, magFilter: LinearFilter } );
  352. this._blurRenderTarget.texture.name = 'SSRNode.Blur';
  353. this._blurRenderTarget.texture.mipmaps.push( {}, {}, {}, {}, {} );
  354. /**
  355. * The material that is used to render the effect.
  356. *
  357. * @private
  358. * @type {NodeMaterial}
  359. */
  360. this._ssrMaterial = new NodeMaterial();
  361. this._ssrMaterial.name = 'SSRNode.SSR';
  362. /**
  363. * The SSR fragment `Fn` and its shared context, captured in {@link SSRNode#setup}.
  364. * Re-invoking the `Fn` produces a fresh node graph, which is how the baked
  365. * compile-time constants are re-applied when they change (see {@link SSRNode#_buildSSRMaterial}).
  366. *
  367. * @private
  368. */
  369. this._ssrFn = null;
  370. this._sharedContext = null;
  371. /**
  372. * The blur material.
  373. *
  374. * @private
  375. * @type {NodeMaterial}
  376. */
  377. this._blurMaterial = new NodeMaterial();
  378. this._blurMaterial.name = 'SSRNode.Blur';
  379. /**
  380. * The copy material.
  381. *
  382. * @private
  383. * @type {NodeMaterial}
  384. */
  385. this._copyMaterial = new NodeMaterial();
  386. this._copyMaterial.name = 'SSRNode.Copy';
  387. /**
  388. * The result of the effect is represented as a separate texture node.
  389. *
  390. * @private
  391. * @type {PassTextureNode}
  392. */
  393. this._textureNode = passTexture( this, this._ssrRenderTarget.texture );
  394. let blurredTextureNode = null;
  395. if ( this.stochastic === false && this.roughnessNode !== null ) {
  396. const mips = this._blurRenderTarget.texture.mipmaps.length - 1;
  397. const r = this.roughnessNode;
  398. const lod = r.mul( r ).mul( mips ).clamp( 0, mips );
  399. blurredTextureNode = passTexture( this, this._blurRenderTarget.texture ).level( lod );
  400. }
  401. /**
  402. * Holds the blurred SSR reflections.
  403. *
  404. * @private
  405. * @type {?PassTextureNode}
  406. */
  407. this._blurredTextureNode = blurredTextureNode;
  408. if ( environmentNode !== null && environmentNode.isTexture === true ) {
  409. this.setEnvMap( environmentNode );
  410. }
  411. }
  412. /**
  413. * Non-linear step distribution exponent (compile-time constant). See the backing
  414. * field for details. Assigning a new value recompiles the SSR material.
  415. *
  416. * @type {number}
  417. */
  418. get stepExponent() {
  419. return this._stepExponent;
  420. }
  421. set stepExponent( value ) {
  422. if ( value !== this._stepExponent ) {
  423. this._stepExponent = value;
  424. this._buildSSRMaterial();
  425. }
  426. }
  427. /**
  428. * Blur kernel size (compile-time constant). Assigning a new value recompiles the
  429. * blur material.
  430. *
  431. * @type {number}
  432. */
  433. get blurQuality() {
  434. return this._blurQuality;
  435. }
  436. set blurQuality( value ) {
  437. if ( value !== this._blurQuality ) {
  438. this._blurQuality = value;
  439. // The size is baked into the boxBlur node tree, so rebuild it (recompiles the material).
  440. if ( this.stochastic === false ) this._buildBlurMaterial();
  441. }
  442. }
  443. /**
  444. * Builds (or rebuilds) the blur material's node graph, baking the current
  445. * {@link SSRNode#blurQuality} as the kernel size so the sample loop unrolls.
  446. *
  447. * @private
  448. */
  449. _buildBlurMaterial() {
  450. this._blurMaterial.fragmentNode = boxBlur( texture( this._ssrRenderTarget.texture ), { size: this._blurQuality, separation: this._blurSpread } );
  451. this._blurMaterial.needsUpdate = true;
  452. }
  453. /**
  454. * Whether SSR fades to black near screen borders (compile-time constant). Assigning
  455. * a new value recompiles the SSR material.
  456. *
  457. * @type {boolean}
  458. */
  459. get screenEdgeFadeBlack() {
  460. return this._screenEdgeFadeBlack;
  461. }
  462. set screenEdgeFadeBlack( value ) {
  463. if ( value !== this._screenEdgeFadeBlack ) {
  464. this._screenEdgeFadeBlack = value;
  465. this._buildSSRMaterial();
  466. }
  467. }
  468. /**
  469. * Whether sub-step binary-search hit refinement is enabled (compile-time constant).
  470. * Assigning a new value rebuilds the SSR material.
  471. *
  472. * @type {boolean}
  473. */
  474. get binaryRefine() {
  475. return this._binaryRefine;
  476. }
  477. set binaryRefine( value ) {
  478. if ( value !== this._binaryRefine ) {
  479. this._binaryRefine = value;
  480. this._buildSSRMaterial();
  481. }
  482. }
  483. /**
  484. * Whether dielectrics are reflected in the non-stochastic path (compile-time constant).
  485. * Assigning a new value rebuilds the SSR material.
  486. *
  487. * @type {boolean}
  488. */
  489. get reflectNonMetals() {
  490. return this._reflectNonMetals;
  491. }
  492. set reflectNonMetals( value ) {
  493. if ( value !== this._reflectNonMetals ) {
  494. this._reflectNonMetals = value;
  495. this._buildSSRMaterial();
  496. }
  497. }
  498. /**
  499. * Rebuilds the SSR material's node graph by re-invoking the fragment `Fn`, which
  500. * re-bakes the compile-time constants ({@link SSRNode#binaryRefine},
  501. * {@link SSRNode#stepExponent}, {@link SSRNode#screenEdgeFadeBlack}) at their current
  502. * values. A no-op until {@link SSRNode#setup} has captured the `Fn`.
  503. *
  504. * @private
  505. */
  506. _buildSSRMaterial() {
  507. if ( this._ssrFn === null ) return;
  508. this._ssrMaterial.fragmentNode = this._ssrFn().context( this._sharedContext );
  509. this._ssrMaterial.needsUpdate = true;
  510. }
  511. /**
  512. * Returns the result of the effect as a texture node.
  513. *
  514. * @return {PassTextureNode} A texture node that represents the result of the effect.
  515. */
  516. getTextureNode() {
  517. return ( this.stochastic === false && this.roughnessNode !== null ) ? this._blurredTextureNode : this._textureNode;
  518. }
  519. /**
  520. * Sets the size of the effect.
  521. *
  522. * @param {number} width - The width of the effect.
  523. * @param {number} height - The height of the effect.
  524. */
  525. setSize( width, height ) {
  526. width = Math.round( this.resolutionScale * width );
  527. height = Math.round( this.resolutionScale * height );
  528. this._resolution.value.set( width, height );
  529. this._ssrRenderTarget.setSize( width, height );
  530. this._blurRenderTarget.setSize( width, height );
  531. }
  532. /**
  533. * Wires the feedback inputs for multi-bounce reflections: the previous frame's
  534. * denoised result (`history`) and the velocity buffer used to reproject it
  535. * (`velocity`). `history` accepts the producing node (e.g. a
  536. * {@link RecurrentDenoiseNode}) — its output render target is used — or a raw
  537. * texture. Pass `null` for both to disable multi-bounce.
  538. *
  539. * @param {Texture} history
  540. * @param {Node<vec2>} velocity
  541. */
  542. setHistory( history, velocity ) {
  543. this.historyTexture = ( history && typeof history.getRenderTarget === 'function' )
  544. ? history.getRenderTarget().texture
  545. : history;
  546. this.velocityTexture = velocity;
  547. }
  548. /**
  549. * Sets the environment map for importance-sampled env lighting when
  550. * screen-space rays miss. Call this whenever the scene's env map changes.
  551. *
  552. * Uses {@link ImportanceSampledEnvironment} (CDF + MIS adapted from
  553. * [three-gpu-pathtracer](https://github.com/gkjohnson/three-gpu-pathtracer)).
  554. *
  555. * @param {Texture|null} hdr - The equirectangular HDR environment map, or null to disable.
  556. * @see {@link https://github.com/gkjohnson/three-gpu-pathtracer}
  557. */
  558. setEnvMap( hdr ) {
  559. if ( hdr === null ) {
  560. if ( this._importanceEnvironment !== null ) {
  561. this._importanceEnvironment.clear();
  562. this._importanceEnvironment = null;
  563. }
  564. this._buildSSRMaterial();
  565. return;
  566. }
  567. if ( hdr.image === undefined || hdr.image.data === undefined ) {
  568. console.warn( 'SSRNode: `environmentNode` / `setEnvMap()` expects an equirectangular HDR texture with CPU-side image data (e.g. RGBELoader). PMREM cubemaps and `scene.environment` are not supported.' );
  569. return;
  570. }
  571. if ( this._importanceEnvironment === null ) {
  572. this._importanceEnvironment = new ImportanceSampledEnvironment( this.envImportanceSampling );
  573. }
  574. this._importanceEnvironment.updateFrom( hdr );
  575. this._buildSSRMaterial();
  576. }
  577. /**
  578. * Intensity multiplier for the importance-sampled env contribution.
  579. * Only available after {@link setEnvMap} has been called.
  580. *
  581. * @type {?UniformNode<float>}
  582. */
  583. get envMapIntensity() {
  584. return this._importanceEnvironment !== null ? this._importanceEnvironment.intensity : null;
  585. }
  586. /**
  587. * This method is used to render the effect once per frame.
  588. *
  589. * @param {NodeFrame} frame - The current node frame.
  590. */
  591. updateBefore( frame ) {
  592. const { renderer } = frame;
  593. this._cameraWorldMatrix.value.copy( this.camera.matrixWorld );
  594. this._cameraWorldPosition.value.copy( this.camera.position );
  595. _rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
  596. const ssrRenderTarget = this._ssrRenderTarget;
  597. const blurRenderTarget = this._blurRenderTarget;
  598. const size = renderer.getDrawingBufferSize( _size );
  599. _quadMesh.material = this._ssrMaterial;
  600. this.setSize( size.width, size.height );
  601. // Advance the noise index once per frame (matches SSGI / Denoise).
  602. this._noiseIndex.value = ( this._noiseIndex.value + 1 ) % 0x7fffffff;
  603. // clear
  604. renderer.setMRT( null );
  605. renderer.setClearColor( 0x000000, 0 );
  606. // ssr
  607. renderer.setRenderTarget( ssrRenderTarget );
  608. _quadMesh.name = 'SSR [ Reflections ]';
  609. _quadMesh.render( renderer );
  610. // blur (optional)
  611. if ( this.stochastic === false && this.roughnessNode !== null ) {
  612. // blur mips but leave the base mip unblurred
  613. for ( let i = 0; i < blurRenderTarget.texture.mipmaps.length; i ++ ) {
  614. _quadMesh.material = ( i === 0 ) ? this._copyMaterial : this._blurMaterial;
  615. this._blurSpread.value = i;
  616. renderer.setRenderTarget( blurRenderTarget, 0, i );
  617. _quadMesh.name = 'SSR [ Blur Level ' + i + ' ]';
  618. _quadMesh.render( renderer );
  619. }
  620. }
  621. // restore
  622. RendererUtils.restoreRendererState( renderer, _rendererState );
  623. }
  624. /**
  625. * This method is used to setup the effect's TSL code.
  626. *
  627. * @param {NodeBuilder} builder - The current node builder.
  628. * @return {PassTextureNode}
  629. */
  630. setup( builder ) {
  631. const uvNode = uv();
  632. const pointToLineDistance = Fn( ( [ point, linePointA, linePointB ] ) => {
  633. // https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html
  634. return cross( point.sub( linePointA ), point.sub( linePointB ) ).length().div( linePointB.sub( linePointA ).length() );
  635. } );
  636. const pointPlaneDistance = Fn( ( [ point, planePoint, planeNormal ] ) => {
  637. // https://mathworld.wolfram.com/Point-PlaneDistance.html
  638. // https://en.wikipedia.org/wiki/Plane_(geometry)
  639. // http://paulbourke.net/geometry/pointlineplane/
  640. // planeNormal is already normalized, so the denominator is 1.
  641. const d = mul( planeNormal.x, planePoint.x ).add( mul( planeNormal.y, planePoint.y ) ).add( mul( planeNormal.z, planePoint.z ) ).negate().toVar();
  642. const distance = mul( planeNormal.x, point.x ).add( mul( planeNormal.y, point.y ) ).add( mul( planeNormal.z, point.z ) ).add( d );
  643. return distance;
  644. } );
  645. const getViewZ = Fn( ( [ depth ] ) => {
  646. let viewZNode;
  647. if ( this.camera.isPerspectiveCamera ) {
  648. viewZNode = perspectiveDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  649. } else {
  650. viewZNode = orthographicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  651. }
  652. return viewZNode;
  653. } );
  654. const sampleDepth = ( uv ) => {
  655. const depth = this.depthNode.sample( uv ).r;
  656. if ( builder.renderer.logarithmicDepthBuffer === true ) {
  657. const viewZ = logarithmicDepthToViewZ( depth, this._cameraNear, this._cameraFar );
  658. return viewZToPerspectiveDepth( viewZ, this._cameraNear, this._cameraFar );
  659. }
  660. return depth;
  661. };
  662. const sampleMarchNoise = this.stochastic === true ? bindAnalyticNoise( this._resolution, 47 ) : null;
  663. const computeScreenBorderFactor = Fn( ( [ uvCoord, borderWidth ] ) => {
  664. const border = borderWidth.max( 1e-4 );
  665. // Distance to the nearest screen edge — uniform falloff at corners.
  666. const edgeDist = min(
  667. min( uvCoord.x, float( 1 ).sub( uvCoord.x ) ),
  668. min( uvCoord.y, float( 1 ).sub( uvCoord.y ) )
  669. );
  670. // Two smoothsteps for a softer ease-in-out than a single ramp.
  671. const t = edgeDist.smoothstep( 0, border );
  672. return t.smoothstep( 0, 1 ).pow( 0.125 );
  673. } ).setLayout( {
  674. name: 'computeScreenBorderFactor',
  675. type: 'float',
  676. inputs: [
  677. { name: 'uvCoord', type: 'vec2' },
  678. { name: 'borderWidth', type: 'float' }
  679. ]
  680. } );
  681. const ssr = Fn( () => {
  682. const noise = this.stochastic === true ? sampleMarchNoise( uvNode, this._noiseIndex ) : null;
  683. const uvPos = uvNode.toVar();
  684. const depth = sampleDepth( uvPos ).toVar();
  685. // Skip background pixels (cleared far-plane depth); the target is cleared each frame.
  686. depth.greaterThanEqual( 1.0 ).discard();
  687. const viewPosition = getViewPosition( uvPos, depth, this._cameraProjectionMatrixInverse ).toVar();
  688. const worldPosition = this._cameraWorldMatrix.mul( vec4( viewPosition, 1.0 ) ).xyz.toVar();
  689. const viewNormal = this.normalNode.rgb.normalize().toVar();
  690. const viewIncidentDir = ( ( this.camera.isPerspectiveCamera ) ? normalize( viewPosition ) : vec3( 0, 0, - 1 ) ).toVar();
  691. // The node system samples the metalness/roughness textures at the current uv,
  692. // so no explicit sample() is needed here.
  693. const metalness = float( this.metalnessNode );
  694. if ( this.stochastic === false && this._reflectNonMetals === false ) {
  695. metalness.lessThanEqual( 0.0 ).discard();
  696. }
  697. const roughness = float( this.roughnessNode );
  698. const glossiness = min( roughness.div( 0.25 ), 1 ).oneMinus();
  699. // Only the fade-to-black miss path reads this, and that path is baked out otherwise.
  700. const surfaceBorderFactor = this.screenEdgeFadeBlack ? computeScreenBorderFactor( uvPos, this.screenEdgeFade ) : null;
  701. const hitBorderWidth = this.screenEdgeFade.mul( glossiness );
  702. const V = viewIncidentDir.negate().normalize().toVar();
  703. let viewReflectDir, finalSampleWeight, specDominantFactor;
  704. const albedo = vec3( 1 ).toVar();
  705. let sampleEnvReflection = null;
  706. if ( this.stochastic === false ) {
  707. viewReflectDir = reflect( viewIncidentDir, viewNormal ).normalize().toVar();
  708. finalSampleWeight = vec3( metalness );
  709. specDominantFactor = float( 1 );
  710. } else {
  711. const Xi = noise.toVar();
  712. // Mirror-bias: pull `Xi.y` toward the cap top to tighten the GGX lobe and cut mid-roughness
  713. // noise. Unbiased — bounded VNDF keeps brdf·cos/pdf ~constant (EA, "Stochastic SSR").
  714. Xi.y.assign( mix( Xi.y, 0.0, this.mirrorBias.mul( Xi.w.sqrt() ) ) );
  715. albedo.assign( ( this.diffuseNode !== null ? this.diffuseNode.sample( uvPos ).rgb : vec3( 1 ) ) );
  716. const ggxSample = ggxReflectionSample( viewNormal, V, roughness, metalness, albedo, Xi ).toVar();
  717. // Sometimes the GGX sample is facing away from the surface, so we need to re-sample.
  718. If( ggxSample.get( 'reflectDir' ).dot( viewNormal ).lessThan( 0 ), () => {
  719. ggxSample.assign( ggxReflectionSample( viewNormal, V, roughness, metalness, albedo, Xi.add( Xi.mul( 7 ) ).fract() ) );
  720. } );
  721. viewReflectDir = ggxSample.get( 'reflectDir' ).toVar();
  722. finalSampleWeight = ggxSample.get( 'sampleWeight' ).toVar();
  723. specDominantFactor = getSpecularDominantFactor( ggxSample.get( 'NdotV' ), roughness ).toVar();
  724. sampleEnvReflection = () => {
  725. const envColor = vec3( 0 ).toVar();
  726. if ( this.envImportanceSampling ) {
  727. const Xi2 = bindAnalyticNoise( this._resolution, 59 )( uvPos, this._noiseIndex );
  728. envColor.assign( this._importanceEnvironment.sampleEnvironmentMIS( {
  729. cameraWorldMatrix: this._cameraWorldMatrix,
  730. viewReflectDir,
  731. N: viewNormal,
  732. V,
  733. alpha: ggxSample.get( 'alpha' ),
  734. f0: ggxSample.get( 'f0' ),
  735. Xi2
  736. } ) );
  737. } else {
  738. envColor.assign( this._importanceEnvironment.sampleEnvironmentBRDF( {
  739. cameraWorldMatrix: this._cameraWorldMatrix,
  740. viewReflectDir,
  741. N: viewNormal,
  742. V,
  743. alpha: ggxSample.get( 'alpha' ),
  744. f0: ggxSample.get( 'f0' )
  745. } ) );
  746. }
  747. return envColor;
  748. };
  749. }
  750. // Multi-bounce: fold in the previous frame's reflection at the hit point, reprojected by its
  751. // own motion. The (1 - history.a) decay damps the feedback. No-op until both textures are set.
  752. const reprojectHitPointHistory = ( uvHit, color ) => {
  753. if ( ! ( this.historyTexture && this.velocityTexture ) ) return color;
  754. const velocity = this.velocityTexture.sample( uvHit ).xy;
  755. const historyUV = uvHit.sub( velocity );
  756. const historyBounce = texture( this.historyTexture, historyUV ).toVar();
  757. const sampleDecay = historyBounce.a.oneMinus();
  758. return color.add( historyBounce.rgb.mul( sampleDecay ) );
  759. };
  760. // Fades a screen-space hit near the screen borders, using the hit sample UV (where the
  761. // screen-space data was read). `screenEdgeFadeBlack` is baked, so the two modes branch in
  762. // JS: fade the reflection to black, or blend it toward the environment reflection.
  763. const applyHitEdgeFade = ( reflectColor, uvS, hitBorderWidth ) => {
  764. if ( this.screenEdgeFadeBlack ) {
  765. const hitBorderFactor = computeScreenBorderFactor( uvS, this.screenEdgeFade );
  766. reflectColor.rgb.mulAssign( hitBorderFactor );
  767. } else {
  768. const hitBorderFactor = computeScreenBorderFactor( uvS, hitBorderWidth );
  769. If( hitBorderFactor.lessThan( 1 ), () => {
  770. reflectColor.rgb.assign( mix( sampleEnvReflection().mul( this.environmentIntensity ), reflectColor.rgb, hitBorderFactor ) );
  771. } );
  772. }
  773. };
  774. const maxReflectRayLen = this.maxDistance.div( dot( viewIncidentDir.negate(), viewNormal ) ).toVar();
  775. const d1viewPosition = viewPosition.add( viewReflectDir.mul( maxReflectRayLen ) ).toVar();
  776. // Camera type is fixed at build time, so guard the near-plane clamp with a JS branch
  777. // rather than a runtime uniform (the orthographic case compiles it out entirely).
  778. if ( this.camera.isPerspectiveCamera ) {
  779. If( d1viewPosition.z.greaterThan( this._cameraNear.negate() ), () => {
  780. const t = sub( this._cameraNear.negate(), viewPosition.z ).div( viewReflectDir.z );
  781. d1viewPosition.assign( viewPosition.add( viewReflectDir.mul( t ) ) );
  782. } );
  783. }
  784. const d0 = uvPos.mul( this._resolution ).xy.toVar();
  785. const d1 = getScreenPosition( d1viewPosition, this._cameraProjectionMatrix ).mul( this._resolution ).toVar();
  786. const xLen = d1.x.sub( d0.x ).toVar();
  787. const yLen = d1.y.sub( d0.y ).toVar();
  788. // dominant-axis ray length in texels (used for the per-step floor below)
  789. const rayLen = max( xLen.abs(), yLen.abs() ).max( 1 ).toVar();
  790. // Blur traces a single mirror ray, so spend steps in proportion to the ray's screen-space
  791. // length (cheap for the short rays that dominate). Scatter needs a fixed, bounded count for
  792. // coherent stochastic sampling; each step then spans the whole ray as rayVec / totalStep.
  793. const totalStep = int( this.stochastic === false
  794. ? trunc( max( abs( xLen ), abs( yLen ) ).mul( this.quality.clamp() ) ).max( int( 1 ) ).toConst()
  795. : this.quality.clamp().mul( MAX_STEPS ).max( float( 1 ) ) ).toConst();
  796. const xSpan = xLen.div( totalStep ).toVar();
  797. const ySpan = yLen.div( totalStep ).toVar();
  798. const stepVec = vec2( xSpan, ySpan ).toVar();
  799. const invResolution = vec2( float( 1 ), float( 1 ) ).div( this._resolution ).toVar();
  800. const uvPixelStepX = vec2( invResolution.x, float( 0 ) ).toVar();
  801. const output = vec4( 0 ).toVar();
  802. const hit = float( 0 ).toVar();
  803. // Reflected-ray view-space Z at ray parameter s ∈ [0,1] (linear in 1/z for perspective),
  804. // hoisted so the march and refinement evaluate it identically.
  805. const recipVPZ = float( 1 ).div( viewPosition.z ).toConst();
  806. const recipD1VPZ = float( 1 ).div( d1viewPosition.z ).toConst();
  807. // Camera type is known at build time, so branch at compile time rather than via a runtime select.
  808. const reflectRayZAt = this.camera.isPerspectiveCamera
  809. ? ( sVal ) => float( 1 ).div( recipVPZ.add( sVal.mul( recipD1VPZ.sub( recipVPZ ) ) ) )
  810. : ( sVal ) => viewPosition.z.add( sVal.mul( d1viewPosition.z.sub( viewPosition.z ) ) );
  811. // Screen-space position along the ray for a given s ∈ [0,1].
  812. const screenPosAt = ( sVal ) => d0.add( stepVec.mul( sVal.mul( totalStep ) ) );
  813. // Ray parameter s ∈ [0,1] for step `idx`. Blur marches uniformly (matching the original loop:
  814. // one ~texel step per iteration). Scatter uses an exponential remap `(idx/steps)^stepExponent`
  815. // that concentrates samples near the origin, floored to ≥1 texel/step; `jitter` dissolves banding.
  816. const sampleFraction = this.stochastic === false
  817. ? ( idx ) => idx.div( totalStep )
  818. : ( idx ) => max(
  819. idx.add( noise.z.sub( 0.5 ) ).div( totalStep ).pow( this.stepExponent ),
  820. idx.div( rayLen )
  821. );
  822. // Carry the hit out of the loop so refinement runs after the march, not nested inside it (a
  823. // loop-inside-a-loop tripped shader-compiler bugs on some drivers). hitSLo/hitSHi bracket s.
  824. const foundHit = bool( false ).toVar();
  825. const hitSLo = float( 0 ).toVar();
  826. const hitSHi = float( 0 ).toVar();
  827. // Carry the coarse hit's UV/depth to skip a redundant fetch when refinement is off.
  828. const hitUvS = vec2( 0 ).toVar();
  829. const hitD = float( 0 ).toVar();
  830. // March from d0 toward d1, looking for an intersection with the depth buffer.
  831. Loop( { start: int( 1 ), end: totalStep }, ( { i } ) => {
  832. // Exponentially-distributed ray parameter, shared by the sample position and ray depth.
  833. const s = sampleFraction( float( i ) ).toVar();
  834. const xy = screenPosAt( s ).toVar();
  835. If( xy.x.lessThan( 0 ).or( xy.x.greaterThan( this._resolution.x ) ).or( xy.y.lessThan( 0 ) ).or( xy.y.greaterThan( this._resolution.y ) ), () => {
  836. Break();
  837. } );
  838. const uvS = xy.mul( invResolution ).toVar();
  839. const d = sampleDepth( uvS ).toVar();
  840. const vZ = getViewZ( d ).toVar();
  841. const viewReflectRayZ = reflectRayZAt( s ).toVar();
  842. If( viewReflectRayZ.lessThanEqual( vZ ), () => {
  843. // Depth crossing: ray went behind the depth buffer. Gate by thickness before stopping
  844. // so an occluder gap doesn't end the march prematurely.
  845. const vP = getViewPosition( uvS, d, this._cameraProjectionMatrixInverse ).toVar();
  846. const away = pointToLineDistance( vP, viewPosition, d1viewPosition ).toVar();
  847. const uvNeighbor = uvS.add( uvPixelStepX ).toVar();
  848. const vPNeighbor = getViewPosition( uvNeighbor, d, this._cameraProjectionMatrixInverse ).toVar();
  849. const minThickness = vPNeighbor.x.sub( vP.x ).mul( 3 ).toVar();
  850. const tk = max( minThickness, this.thickness ).toVar();
  851. If( away.lessThanEqual( tk ), () => {
  852. const vN = this.normalNode.sample( uvS ).rgb.normalize().toVar();
  853. // the reflected ray is pointing towards the same side as the fragment's normal (current ray position),
  854. // which means it wouldn't reflect off the surface. The loop continues to the next step for the next ray sample.
  855. if ( this.stochastic === false ) {
  856. If( dot( viewReflectDir, vN ).greaterThanEqual( 0 ), () => {
  857. Continue();
  858. } );
  859. // this distance represents the depth of the intersection point between the reflected ray and the scene.
  860. const distance = pointPlaneDistance( vP, viewPosition, viewNormal ).toVar();
  861. // Distance exceeding limit: The reflection is potentially too far away and
  862. // might not contribute significantly to the final color
  863. If( distance.greaterThan( this.maxDistance ), () => {
  864. Break();
  865. } );
  866. }
  867. foundHit.assign( true );
  868. hitUvS.assign( uvS );
  869. hitD.assign( d );
  870. if ( this.binaryRefine ) {
  871. hitSLo.assign( sampleFraction( float( i ).sub( 1 ) ) );
  872. hitSHi.assign( s );
  873. }
  874. Break();
  875. } );
  876. } );
  877. } );
  878. If( foundHit, () => {
  879. // Bisect the bracketed crossing toward the exact intersection. Run after the march, not
  880. // nested (a loop-inside-a-loop tripped shader-compiler bugs on some drivers).
  881. if ( this.binaryRefine ) {
  882. Loop( { start: int( 0 ), end: int( 8 ), type: 'int', condition: '<' }, () => {
  883. const sMid = hitSLo.add( hitSHi ).mul( 0.5 ).toVar();
  884. const sceneZMid = getViewZ( sampleDepth( screenPosAt( sMid ).mul( invResolution ) ) );
  885. If( reflectRayZAt( sMid ).lessThanEqual( sceneZMid ), () => {
  886. hitSHi.assign( sMid );
  887. } ).Else( () => {
  888. hitSLo.assign( sMid );
  889. } );
  890. } );
  891. // Refinement moved the crossing, so re-fetch UV/depth at the refined `s`.
  892. hitUvS.assign( screenPosAt( hitSHi ).mul( invResolution ) );
  893. hitD.assign( sampleDepth( hitUvS ) );
  894. }
  895. // Shade the hit, reusing the depth fetched during the march (or refinement).
  896. const uvS = hitUvS;
  897. const vP = getViewPosition( uvS, hitD, this._cameraProjectionMatrixInverse ).toVar();
  898. // In blur mode the ratio² falloff re-grows past maxDistance, so over-range hits fall back
  899. // to env. The scatter path bounds reach via ray length, so every hit shades.
  900. const distancePointPlane = this.stochastic === false ? pointPlaneDistance( vP, viewPosition, viewNormal ).toVar() : float( 0 );
  901. const withinRange = distancePointPlane.lessThanEqual( this.maxDistance );
  902. If( withinRange, () => {
  903. const hitWorldPosition = this._cameraWorldMatrix.mul( vec4( vP, 1.0 ) ).xyz.toVar();
  904. const worldDistance = distance( worldPosition, hitWorldPosition ).mul( specDominantFactor ).toVar();
  905. const reflectColor = this.colorNode.sample( uvS ).toVar();
  906. // Multi-bounce: add the reprojected previous-frame reflection at the hit point.
  907. reflectColor.rgb.assign( reprojectHitPointHistory( uvS, reflectColor.rgb ) );
  908. if ( this.stochastic === true ) applyHitEdgeFade( reflectColor, uvS, hitBorderWidth );
  909. // The scatter (GGX) path bakes distance/grazing response into finalSampleWeight.
  910. // The mirror/blur path is a plain reflection, so reapply upstream's squared
  911. // distance attenuation and grazing Fresnel here to match its falloff.
  912. let weightedColor = reflectColor.rgb.mul( finalSampleWeight );
  913. if ( this.stochastic === false ) {
  914. const ratio = float( 1 ).sub( distancePointPlane.div( this.maxDistance ) ).toVar();
  915. const attenuation = ratio.mul( ratio ).toVar();
  916. const fresnelCoe = div( dot( viewIncidentDir, viewReflectDir ).add( 1 ), 2 ).toVar();
  917. weightedColor = weightedColor.mul( attenuation.mul( fresnelCoe ) );
  918. }
  919. hit.assign( 1 );
  920. output.assign( vec4( weightedColor, worldDistance ) );
  921. } );
  922. } );
  923. // Screen-space ray missed: environment fallback (MIS when CDF env is set up).
  924. If( hit.equal( 0 ), () => {
  925. if ( this.stochastic === true ) {
  926. output.assign( vec4( sampleEnvReflection().mul( this.environmentIntensity ), float( ENV_RAY_LENGTH ) ) );
  927. // Misses fade by the surface pixel UV (where the reflection is being shaded).
  928. if ( this.screenEdgeFadeBlack ) {
  929. output.rgb.mulAssign( surfaceBorderFactor );
  930. }
  931. }
  932. } );
  933. const lum = luminance( output.rgb ).max( 1e-4 ).toVar();
  934. output.rgb.mulAssign( this.maxLuminance.div( lum ).min( 1 ) );
  935. // scale the reflection color by the user-controlled intensity
  936. output.rgb.mulAssign( this.intensity );
  937. return output.max( 0 );
  938. } );
  939. this._ssrFn = ssr;
  940. this._sharedContext = builder.getSharedContext();
  941. this._buildSSRMaterial();
  942. const reflectionBuffer = texture( this._ssrRenderTarget.texture );
  943. if ( this.stochastic === false ) {
  944. this._buildBlurMaterial();
  945. }
  946. this._copyMaterial.fragmentNode = reflectionBuffer;
  947. this._copyMaterial.needsUpdate = true;
  948. //
  949. return this.getTextureNode();
  950. }
  951. getRenderTarget() {
  952. return this._ssrRenderTarget;
  953. }
  954. /**
  955. * Frees internal resources. This method should be called
  956. * when the effect is no longer required.
  957. */
  958. dispose() {
  959. this._ssrRenderTarget.dispose();
  960. this._blurRenderTarget.dispose();
  961. this._ssrMaterial.dispose();
  962. this._blurMaterial.dispose();
  963. this._copyMaterial.dispose();
  964. if ( this._importanceEnvironment !== null ) {
  965. this._importanceEnvironment.dispose();
  966. this._importanceEnvironment = null;
  967. }
  968. }
  969. }
  970. export default SSRNode;
  971. /**
  972. * TSL function for creating screen space reflections (SSR).
  973. *
  974. * @tsl
  975. * @function
  976. * @param {Node<vec4>} colorNode - The node that represents the beauty pass.
  977. * @param {Node<float>} depthNode - A node that represents the beauty pass's depth.
  978. * @param {Node<vec3>} normalNode - A node that represents the beauty pass's normals.
  979. * @param {SSRNodeOptions} [options] - Optional inputs for material and environment data.
  980. * @returns {SSRNode}
  981. */
  982. export const ssr = ( colorNode, depthNode, normalNode, options = {} ) => nodeObject( new SSRNode(
  983. nodeObject( colorNode ),
  984. nodeObject( depthNode ),
  985. nodeObject( normalNode ),
  986. options
  987. ) );
粤ICP备19079148号