three
Version:
JavaScript 3D library
264 lines (170 loc) • 6.7 kB
JavaScript
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 );