Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 18 additions & 67 deletions build/three.webgpu.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/three.webgpu.min.js

Large diffs are not rendered by default.

85 changes: 18 additions & 67 deletions build/three.webgpu.nodes.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/three.webgpu.nodes.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@
"webgpu_postprocessing_outline",
"webgpu_postprocessing_pixel",
"webgpu_postprocessing_radial_blur",
"webgpu_postprocessing_retro",
"webgpu_postprocessing_smaa",
"webgpu_postprocessing_sobel",
"webgpu_postprocessing_ssaa",
Expand Down
150 changes: 150 additions & 0 deletions examples/jsm/tsl/display/CRT.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Fn, float, vec2, vec3, sin, screenUV, mix, clamp, dot, convertToTexture, time, uv, select } from 'three/tsl';
import { circle } from './Shape.js';

/**
* Creates barrel-distorted UV coordinates.
* The center of the screen appears to bulge outward (convex distortion).
*
* @tsl
* @function
* @param {Node<float>} [curvature=0.1] - The amount of curvature (0 = flat, 0.5 = very curved).
* @param {Node<vec2>} [coord=uv()] - The input UV coordinates.
* @return {Node<vec2>} The distorted UV coordinates.
*/
export const barrelUV = Fn( ( [ curvature = float( 0.1 ), coord = uv() ] ) => {

// Center UV coordinates (-1 to 1)
const centered = coord.sub( 0.5 ).mul( 2.0 );

// Calculate squared distance from center
const r2 = dot( centered, centered );

// Barrel distortion: push center outward (bulge effect)
const distortion = float( 1.0 ).sub( r2.mul( curvature ) );

// Calculate scale to compensate for edge expansion
// At corners r² = 2, so we scale by the inverse of corner distortion
const cornerDistortion = float( 1.0 ).sub( curvature.mul( 2.0 ) );

// Apply distortion and compensate scale to keep edges aligned
const distorted = centered.div( distortion ).mul( cornerDistortion ).mul( 0.5 ).add( 0.5 );

return distorted;

} );

/**
* Checks if UV coordinates are inside the valid 0-1 range.
* Useful for masking areas inside the distorted screen.
*
* @tsl
* @function
* @param {Node<vec2>} coord - The UV coordinates to check.
* @return {Node<float>} 1.0 if inside bounds, 0.0 if outside.
*/
export const barrelMask = Fn( ( [ coord ] ) => {

const outOfBounds = coord.x.lessThan( 0.0 )
.or( coord.x.greaterThan( 1.0 ) )
.or( coord.y.lessThan( 0.0 ) )
.or( coord.y.greaterThan( 1.0 ) );

return select( outOfBounds, float( 0.0 ), float( 1.0 ) );

} );

/**
* Applies color bleeding effect to simulate horizontal color smearing.
* Simulates the analog signal bleeding in CRT displays where colors
* "leak" into adjacent pixels horizontally.
*
* @tsl
* @function
* @param {Node} color - The input texture node.
* @param {Node<float>} [amount=0.002] - The amount of color bleeding (0-0.01).
* @return {Node<vec3>} The color with bleeding effect applied.
*/
export const colorBleeding = Fn( ( [ color, amount = float( 0.002 ) ] ) => {

const inputTexture = convertToTexture( color );

// Get the original color
const original = inputTexture.sample( screenUV ).rgb;

// Sample colors from the left (simulating signal trailing)
const left1 = inputTexture.sample( screenUV.sub( vec2( amount, 0.0 ) ) ).rgb;
const left2 = inputTexture.sample( screenUV.sub( vec2( amount.mul( 2.0 ), 0.0 ) ) ).rgb;
const left3 = inputTexture.sample( screenUV.sub( vec2( amount.mul( 3.0 ), 0.0 ) ) ).rgb;

// Red bleeds more (travels further in analog signal)
const bleedR = original.r
.add( left1.r.mul( 0.4 ) )
.add( left2.r.mul( 0.2 ) )
.add( left3.r.mul( 0.1 ) );

// Green bleeds medium
const bleedG = original.g
.add( left1.g.mul( 0.25 ) )
.add( left2.g.mul( 0.1 ) );

// Blue bleeds least
const bleedB = original.b
.add( left1.b.mul( 0.15 ) );

// Normalize and clamp
const r = clamp( bleedR.div( 1.7 ), 0.0, 1.0 );
const g = clamp( bleedG.div( 1.35 ), 0.0, 1.0 );
const b = clamp( bleedB.div( 1.15 ), 0.0, 1.0 );

return vec3( r, g, b );

} );

/**
* Applies scanline effect to simulate CRT monitor horizontal lines with animation.
*
* @tsl
* @function
* @param {Node<vec3>} color - The input color.
* @param {Node<float>} [intensity=0.3] - The intensity of the scanlines (0-1).
* @param {Node<float>} [count=240] - The number of scanlines (typically matches vertical resolution).
* @param {Node<float>} [speed=0.0] - The scroll speed of scanlines (0 = static, 1 = normal CRT roll).
* @param {Node<vec2>} [coord=uv()] - The UV coordinates to use for scanlines.
* @return {Node<vec3>} The color with scanlines applied.
*/
export const scanlines = Fn( ( [ color, intensity = float( 0.3 ), count = float( 240.0 ), speed = float( 0.0 ), coord = uv() ] ) => {

// Animate scanlines scrolling down (like CRT vertical sync roll)
const animatedY = coord.y.sub( time.mul( speed ) );

// Create scanline pattern
const scanline = sin( animatedY.mul( count ) );
const scanlineIntensity = scanline.mul( 0.5 ).add( 0.5 ).mul( intensity );

// Darken alternate lines
return color.mul( float( 1.0 ).sub( scanlineIntensity ) );

} );

/**
* Applies vignette effect to darken the edges of the screen.
*
* @tsl
* @function
* @param {Node<vec3>} color - The input color.
* @param {Node<float>} [intensity=0.4] - The intensity of the vignette (0-1).
* @param {Node<float>} [smoothness=0.5] - The smoothness of the vignette falloff.
* @param {Node<vec2>} [coord=uv()] - The UV coordinates to use for vignette calculation.
* @return {Node<vec3>} The color with vignette applied.
*/
export const vignette = Fn( ( [ color, intensity = float( 0.4 ), smoothness = float( 0.5 ), coord = uv() ] ) => {

// Use circle for radial gradient (1.42 ≈ √2 covers full diagonal)
const mask = circle( float( 1.42 ), smoothness, coord );

// Apply vignette: center = 1, edges = (1 - intensity)
const vignetteAmount = mix( float( 1.0 ).sub( intensity ), float( 1.0 ), mask );

return color.mul( vignetteAmount );

} );
166 changes: 166 additions & 0 deletions examples/jsm/tsl/display/RetroPassNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { MeshBasicNodeMaterial, PassNode, UnsignedByteType, NearestFilter } from 'three/webgpu';
import { float, vec2, vec4, Fn, uv, varying, cameraProjectionMatrix, cameraViewMatrix, positionWorld, screenSize, materialColor, replaceDefaultUV } 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
} = options;

this.setResolutionScale( .25 );

this.renderTarget.texture.type = UnsignedByteType;
this.renderTarget.texture.magFilter = NearestFilter;
this.renderTarget.texture.minFilter = NearestFilter;

this.affineDistortionNode = affineDistortion;

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 ) => {

let retroMaterial = this._materialCache.get( material );

if ( retroMaterial === undefined ) {

retroMaterial = new MeshBasicNodeMaterial();

retroMaterial.colorNode = material.colorNode || null;
retroMaterial.opacityNode = material.opacityNode || null;
retroMaterial.positionNode = material.positionNode || null;
retroMaterial.vertexNode = material.vertexNode || _clipSpaceRetro;

if ( this.affineDistortionNode ) {

retroMaterial.colorNode = replaceDefaultUV( () => {

return this.affineDistortionNode.mix( uv(), _affineUv.div( _w ) );

}, retroMaterial.colorNode || materialColor );

}

this._materialCache.set( material, retroMaterial );

}

retroMaterial.map = material.map;
retroMaterial.color = material.color;
retroMaterial.opacity = material.opacity;
retroMaterial.transparent = material.transparent;
retroMaterial.side = material.side;

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( material => 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 );
29 changes: 29 additions & 0 deletions examples/jsm/tsl/display/Shape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Fn, float, length, smoothstep, uv } from 'three/tsl';

/**
* Returns a radial gradient from center (white) to edges (black).
* Useful for masking effects based on distance from center.
*
* @tsl
* @function
* @param {Node<float>} [scale=1.0] - Controls the size of the gradient (0 = all black, 1 = full circle).
* @param {Node<float>} [softness=0.5] - Controls the edge softness (0 = hard edge, 1 = soft gradient).
* @param {Node<vec2>} [coord=uv()] - The input UV coordinates.
* @return {Node<float>} 1.0 at center, 0.0 at edges.
*/
export const circle = Fn( ( [ scale = float( 1.0 ), softness = float( 0.5 ), coord = uv() ] ) => {

// Center UV coordinates (-0.5 to 0.5)
const centered = coord.sub( 0.5 );

// Calculate distance from center (0 at center, ~0.707 at corners)
const dist = length( centered ).mul( 2.0 );

// Calculate inner and outer edges based on scale and softness
const outer = scale;
const inner = scale.sub( softness.mul( scale ) );

// Smoothstep for soft/hard transition
return smoothstep( outer, inner, dist );

} );
Loading
Loading