Files
IronSteelNon-ferrousMetallu…/node_modules/three/examples/jsm/tsl/display/RetroPassNode.js

264 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 );