1
0

RetroPassNode.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { MeshBasicNodeMaterial, PassNode, UnsignedByteType, NearestFilter, CubeMapNode, MeshPhongNodeMaterial } from 'three/webgpu';
  2. import { float, vec2, vec4, Fn, uv, varying, cameraProjectionMatrix, cameraViewMatrix, positionWorld, screenSize, materialColor, uint, texture, uniform, context, reflectVector } from 'three/tsl';
  3. const _affineUv = varying( vec2() );
  4. const _w = varying( float() );
  5. const _clipSpaceRetro = Fn( () => {
  6. const defaultPosition = cameraProjectionMatrix
  7. .mul( cameraViewMatrix )
  8. .mul( positionWorld );
  9. const roundedPosition = defaultPosition.xy
  10. .div( defaultPosition.w.mul( 2 ) )
  11. .mul( screenSize.xy )
  12. .round()
  13. .div( screenSize.xy )
  14. .mul( defaultPosition.w.mul( 2 ) );
  15. _affineUv.assign( uv().mul( defaultPosition.w ) );
  16. _w.assign( defaultPosition.w );
  17. return vec4( roundedPosition.xy, defaultPosition.zw );
  18. } )();
  19. /**
  20. * A post-processing pass that applies a retro PS1-style effect to the scene.
  21. *
  22. * This node renders the scene with classic PlayStation 1 visual characteristics:
  23. * - **Vertex snapping**: Vertices are snapped to screen pixels, creating the iconic "wobbly" geometry
  24. * - **Affine texture mapping**: Textures are sampled without perspective correction, resulting in distortion effects
  25. * - **Low resolution**: Default 0.25 scale (typical 320x240 equivalent)
  26. * - **Nearest-neighbor filtering**: Sharp pixelated textures without smoothing
  27. *
  28. * @augments PassNode
  29. */
  30. class RetroPassNode extends PassNode {
  31. /**
  32. * Creates a new RetroPassNode instance.
  33. *
  34. * @param {Scene} scene - The scene to render.
  35. * @param {Camera} camera - The camera to render from.
  36. * @param {Object} [options={}] - Additional options for the retro pass.
  37. * @param {Node} [options.affineDistortion=null] - An optional node to apply affine distortion to UVs.
  38. */
  39. constructor( scene, camera, options = {} ) {
  40. super( PassNode.COLOR, scene, camera );
  41. const {
  42. affineDistortion = null,
  43. filterTextures = false
  44. } = options;
  45. this.setResolutionScale( .25 );
  46. this.renderTarget.texture.type = UnsignedByteType;
  47. this.renderTarget.texture.magFilter = NearestFilter;
  48. this.renderTarget.texture.minFilter = NearestFilter;
  49. this.affineDistortionNode = affineDistortion;
  50. this.filterTextures = filterTextures;
  51. this._materialCache = new Map();
  52. }
  53. /**
  54. * Updates the retro pass before rendering.
  55. *
  56. * @override
  57. * @param {Frame} frame - The current frame information.
  58. * @returns {void}
  59. */
  60. updateBefore( frame ) {
  61. const renderer = frame.renderer;
  62. const currentRenderObjectFunction = renderer.getRenderObjectFunction();
  63. renderer.setRenderObjectFunction( ( object, scene, camera, geometry, material, ...params ) => {
  64. const retroMaterialData = this._materialCache.get( material );
  65. let retroMaterial;
  66. if ( retroMaterialData === undefined || retroMaterialData.version !== material.version ) {
  67. if ( retroMaterialData !== undefined ) {
  68. retroMaterialData.material.dispose();
  69. }
  70. if ( material.isMeshBasicMaterial || material.isMeshBasicNodeMaterial ) {
  71. retroMaterial = new MeshBasicNodeMaterial();
  72. } else {
  73. retroMaterial = new MeshPhongNodeMaterial();
  74. }
  75. retroMaterial.colorNode = material.colorNode || null;
  76. retroMaterial.opacityNode = material.opacityNode || null;
  77. retroMaterial.positionNode = material.positionNode || null;
  78. retroMaterial.vertexNode = material.vertexNode || _clipSpaceRetro;
  79. let colorNode = material.colorNode || materialColor;
  80. if ( material.isMeshStandardNodeMaterial || material.isMeshStandardMaterial ) {
  81. const envMap = material.envMap || scene.environment;
  82. if ( envMap ) {
  83. const reflection = new CubeMapNode( texture( envMap ) );
  84. let metalness;
  85. if ( material.metalnessNode ) {
  86. metalness = material.metalnessNode;
  87. } else {
  88. metalness = uniform( material.metalness ).onRenderUpdate( ( { material } ) => material.metalness );
  89. if ( material.metalnessMap ) {
  90. const textureUniform = texture( material.metalnessMap ).onRenderUpdate( ( { material } ) => material.metalnessMap );
  91. metalness = metalness.mul( textureUniform.b );
  92. }
  93. }
  94. colorNode = metalness.mix( colorNode, reflection );
  95. }
  96. }
  97. retroMaterial.colorNode = colorNode;
  98. //
  99. const contextData = {};
  100. if ( this.affineDistortionNode ) {
  101. contextData.getUV = ( texture ) => {
  102. let finalUV;
  103. if ( texture.isCubeTextureNode ) {
  104. finalUV = reflectVector;
  105. } else {
  106. finalUV = this.affineDistortionNode.mix( uv(), _affineUv.div( _w ) );
  107. }
  108. return finalUV;
  109. };
  110. }
  111. if ( this.filterTextures !== true ) {
  112. contextData.getTextureLevel = () => uint( 0 );
  113. }
  114. retroMaterial.contextNode = context( contextData );
  115. //
  116. this._materialCache.set( material, {
  117. material: retroMaterial,
  118. version: material.version
  119. } );
  120. } else {
  121. retroMaterial = retroMaterialData.material;
  122. }
  123. for ( const property in material ) {
  124. if ( retroMaterial[ property ] === undefined ) continue;
  125. retroMaterial[ property ] = material[ property ];
  126. }
  127. renderer.renderObject( object, scene, camera, geometry, retroMaterial, ...params );
  128. } );
  129. super.updateBefore( frame );
  130. renderer.setRenderObjectFunction( currentRenderObjectFunction );
  131. }
  132. /**
  133. * Disposes the retro pass and its internal resources.
  134. *
  135. * @override
  136. * @returns {void}
  137. */
  138. dispose() {
  139. super.dispose();
  140. this._materialCache.forEach( ( data ) => {
  141. data.material.dispose();
  142. } );
  143. this._materialCache.clear();
  144. }
  145. }
  146. export default RetroPassNode;
  147. /**
  148. * Creates a new RetroPassNode instance for PS1-style rendering.
  149. *
  150. * The retro pass applies vertex snapping, affine texture mapping, and low-resolution
  151. * rendering to achieve an authentic PlayStation 1 aesthetic. Combine with other
  152. * post-processing effects like dithering, posterization, and scanlines for full retro look.
  153. *
  154. * ```js
  155. * // Combined with other effects
  156. * let pipeline = retroPass( scene, camera );
  157. * pipeline = bayerDither( pipeline, 32 );
  158. * pipeline = posterize( pipeline, 32 );
  159. * renderPipeline.outputNode = pipeline;
  160. * ```
  161. *
  162. * @tsl
  163. * @function
  164. * @param {Scene} scene - The scene to render.
  165. * @param {Camera} camera - The camera to render from.
  166. * @param {Object} [options={}] - Additional options for the retro pass.
  167. * @param {Node} [options.affineDistortion=null] - An optional node to apply affine distortion to UVs.
  168. * @return {RetroPassNode} A new RetroPassNode instance.
  169. */
  170. export const retroPass = ( scene, camera, options = {} ) => new RetroPassNode( scene, camera, options );
粤ICP备19079148号