| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- import { MeshBasicNodeMaterial, PassNode, UnsignedByteType, NearestFilter, CubeMapNode, MeshPhongNodeMaterial } from 'three/webgpu';
- import { float, vec2, vec4, Fn, uv, varying, cameraProjectionMatrix, cameraViewMatrix, positionWorld, screenSize, materialColor, uint, texture, uniform, context, reflectVector } from 'three/tsl';
- const _affineUv = varying( vec2() );
- const _w = varying( float() );
- const _clipSpaceRetro = Fn( () => {
- const defaultPosition = cameraProjectionMatrix
- .mul( cameraViewMatrix )
- .mul( positionWorld );
- const roundedPosition = defaultPosition.xy
- .div( defaultPosition.w.mul( 2 ) )
- .mul( screenSize.xy )
- .round()
- .div( screenSize.xy )
- .mul( defaultPosition.w.mul( 2 ) );
- _affineUv.assign( uv().mul( defaultPosition.w ) );
- _w.assign( defaultPosition.w );
- return vec4( roundedPosition.xy, defaultPosition.zw );
- } )();
- /**
- * A post-processing pass that applies a retro PS1-style effect to the scene.
- *
- * This node renders the scene with classic PlayStation 1 visual characteristics:
- * - **Vertex snapping**: Vertices are snapped to screen pixels, creating the iconic "wobbly" geometry
- * - **Affine texture mapping**: Textures are sampled without perspective correction, resulting in distortion effects
- * - **Low resolution**: Default 0.25 scale (typical 320x240 equivalent)
- * - **Nearest-neighbor filtering**: Sharp pixelated textures without smoothing
- *
- * @augments PassNode
- */
- class RetroPassNode extends PassNode {
- /**
- * Creates a new RetroPassNode instance.
- *
- * @param {Scene} scene - The scene to render.
- * @param {Camera} camera - The camera to render from.
- * @param {Object} [options={}] - Additional options for the retro pass.
- * @param {Node} [options.affineDistortion=null] - An optional node to apply affine distortion to UVs.
- */
- constructor( scene, camera, options = {} ) {
- super( PassNode.COLOR, scene, camera );
- const {
- affineDistortion = null,
- filterTextures = false
- } = options;
- this.setResolutionScale( .25 );
- this.renderTarget.texture.type = UnsignedByteType;
- this.renderTarget.texture.magFilter = NearestFilter;
- this.renderTarget.texture.minFilter = NearestFilter;
- this.affineDistortionNode = affineDistortion;
- this.filterTextures = filterTextures;
- this._materialCache = new Map();
- }
- /**
- * Updates the retro pass before rendering.
- *
- * @override
- * @param {Frame} frame - The current frame information.
- * @returns {void}
- */
- updateBefore( frame ) {
- const renderer = frame.renderer;
- const currentRenderObjectFunction = renderer.getRenderObjectFunction();
- renderer.setRenderObjectFunction( ( object, scene, camera, geometry, material, ...params ) => {
- const retroMaterialData = this._materialCache.get( material );
- let retroMaterial;
- if ( retroMaterialData === undefined || retroMaterialData.version !== material.version ) {
- if ( retroMaterialData !== undefined ) {
- retroMaterialData.material.dispose();
- }
- if ( material.isMeshBasicMaterial || material.isMeshBasicNodeMaterial ) {
- retroMaterial = new MeshBasicNodeMaterial();
- } else {
- retroMaterial = new MeshPhongNodeMaterial();
- }
- retroMaterial.colorNode = material.colorNode || null;
- retroMaterial.opacityNode = material.opacityNode || null;
- retroMaterial.positionNode = material.positionNode || null;
- retroMaterial.vertexNode = material.vertexNode || _clipSpaceRetro;
- let colorNode = material.colorNode || materialColor;
- if ( material.isMeshStandardNodeMaterial || material.isMeshStandardMaterial ) {
- const envMap = material.envMap || scene.environment;
- if ( envMap ) {
- const reflection = new CubeMapNode( texture( envMap ) );
- let metalness;
- if ( material.metalnessNode ) {
- metalness = material.metalnessNode;
- } else {
- metalness = uniform( material.metalness ).onRenderUpdate( ( { material } ) => material.metalness );
- if ( material.metalnessMap ) {
- const textureUniform = texture( material.metalnessMap ).onRenderUpdate( ( { material } ) => material.metalnessMap );
- metalness = metalness.mul( textureUniform.b );
- }
- }
- colorNode = metalness.mix( colorNode, reflection );
- }
- }
- retroMaterial.colorNode = colorNode;
- //
- const contextData = {};
- if ( this.affineDistortionNode ) {
- contextData.getUV = ( texture ) => {
- let finalUV;
- if ( texture.isCubeTextureNode ) {
- finalUV = reflectVector;
- } else {
- finalUV = this.affineDistortionNode.mix( uv(), _affineUv.div( _w ) );
- }
- return finalUV;
- };
- }
- if ( this.filterTextures !== true ) {
- contextData.getTextureLevel = () => uint( 0 );
- }
- retroMaterial.contextNode = context( contextData );
- //
- this._materialCache.set( material, {
- material: retroMaterial,
- version: material.version
- } );
- } else {
- retroMaterial = retroMaterialData.material;
- }
- for ( const property in material ) {
- if ( retroMaterial[ property ] === undefined ) continue;
- retroMaterial[ property ] = material[ property ];
- }
- renderer.renderObject( object, scene, camera, geometry, retroMaterial, ...params );
- } );
- super.updateBefore( frame );
- renderer.setRenderObjectFunction( currentRenderObjectFunction );
- }
- /**
- * Disposes the retro pass and its internal resources.
- *
- * @override
- * @returns {void}
- */
- dispose() {
- super.dispose();
- this._materialCache.forEach( ( data ) => {
- data.material.dispose();
- } );
- this._materialCache.clear();
- }
- }
- export default RetroPassNode;
- /**
- * Creates a new RetroPassNode instance for PS1-style rendering.
- *
- * The retro pass applies vertex snapping, affine texture mapping, and low-resolution
- * rendering to achieve an authentic PlayStation 1 aesthetic. Combine with other
- * post-processing effects like dithering, posterization, and scanlines for full retro look.
- *
- * ```js
- * // Combined with other effects
- * let pipeline = retroPass( scene, camera );
- * pipeline = bayerDither( pipeline, 32 );
- * pipeline = posterize( pipeline, 32 );
- * renderPipeline.outputNode = pipeline;
- * ```
- *
- * @tsl
- * @function
- * @param {Scene} scene - The scene to render.
- * @param {Camera} camera - The camera to render from.
- * @param {Object} [options={}] - Additional options for the retro pass.
- * @param {Node} [options.affineDistortion=null] - An optional node to apply affine distortion to UVs.
- * @return {RetroPassNode} A new RetroPassNode instance.
- */
- export const retroPass = ( scene, camera, options = {} ) => new RetroPassNode( scene, camera, options );
|