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
32 changes: 16 additions & 16 deletions examples/jsm/tsl/display/AfterImageNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ class AfterImageNode extends TempNode {
this.textureNodeOld = texture();

/**
* The damping intensity as a uniform node.
* How quickly the after-image fades. A higher value means the after-image
* persists longer, while a lower value means it fades faster. Should be in
* the range `[0, 1]`.
*
* @type {UniformNode<float>}
*/
Expand Down Expand Up @@ -174,27 +176,25 @@ class AfterImageNode extends TempNode {

//

const uvNode = textureNode.uvNode || uv();
textureNodeOld.uvNode = textureNode.uvNode || uv();

textureNodeOld.uvNode = uvNode;

const sampleTexture = ( uv ) => textureNode.sample( uv );

const when_gt = Fn( ( [ x_immutable, y_immutable ] ) => {

const y = float( y_immutable ).toVar();
const x = vec4( x_immutable ).toVar();
const afterImg = Fn( () => {

return max( sign( x.sub( y ) ), 0.0 );
const texelOld = textureNodeOld.sample().toVar();
const texelNew = textureNode.sample().toVar();

} );
const threshold = float( 0.1 ).toConst();

const afterImg = Fn( () => {
// m acts as a mask. It's 1 if the previous pixel was "bright enough" (above the threshold) and 0 if it wasn't.
const m = max( sign( texelOld.sub( threshold ) ), 0.0 );

const texelOld = vec4( textureNodeOld );
const texelNew = vec4( sampleTexture( uvNode ) );
// This is where the after-image fades:
//
// - If m is 0, texelOld is multiplied by 0, effectively clearing the after-image for that pixel.
// - If m is 1, texelOld is multiplied by "damp". Since "damp" is between 0 and 1, this reduces the color value of
// texelOld, making it darker and causing it to fade.
texelOld.mulAssign( this.damp.mul( m ) );

texelOld.mulAssign( this.damp.mul( when_gt( texelOld, 0.1 ) ) );
return max( texelNew, texelOld );

} );
Expand Down
Binary file modified examples/screenshots/webgpu_postprocessing_afterimage.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
143 changes: 113 additions & 30 deletions examples/webgpu_postprocessing_afterimage.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - postprocessing - after image<br />
Based on <a href="https://web.archive.org/web/20220629101314/http://oos.moxiecode.com/js_webgl/spiral/" target="_blank" rel="noopener">Particle Spiral</a>
by <a href="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/oosmoxiecode" target="_blank" rel="noopener">oosmoxiecode</a>
</div>

<script type="importmap">
{
"imports": {
Expand All @@ -21,62 +27,145 @@
<script type="module">

import * as THREE from 'three';
import { pass } from 'three/tsl';
import { instancedBufferAttribute, mod, pass, texture, float, time, vec2, vec3, vec4, sin, cos } from 'three/tsl';
import { afterImage } from 'three/addons/tsl/display/AfterImageNode.js';

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import Stats from 'three/addons/libs/stats.module.js';

let camera, scene, renderer;
let mesh, postProcessing, combinedPass;
let camera, scene, renderer, particles, stats;
let postProcessing, afterImagePass, scenePass;

const params = {

damp: 0.96
damp: 0.8,
enabled: true

};

init();
createGUI();

function init() {

renderer = new THREE.WebGPURenderer( { antialias: true } );
renderer = new THREE.WebGPURenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );

camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.z = 400;
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.z = 1000;

scene = new THREE.Scene();
scene.fog = new THREE.Fog( 0x000000, 1, 1000 );

const geometry = new THREE.TorusKnotGeometry( 100, 30, 100, 16 );
const material = new THREE.MeshNormalMaterial();
mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
const sprite = new THREE.TextureLoader().load( 'textures/sprites/circle.png' );

// geometry

const radius = 600;
const count = 50000;

const vertex = new THREE.Vector3();
const color = new THREE.Color();

const colors = [];
const vertices = [];
const timeOffsets = [];

for ( var i = 0; i < count; i ++ ) {

getRandomPointOnSphere( radius, vertex );
vertices.push( vertex.x, vertex.y, vertex.z );

color.setHSL( i / count, 0.7, 0.7, THREE.SRGBColorSpace );
colors.push( color.r, color.g, color.b );

timeOffsets.push( i / count );

}

const positionAttribute = new THREE.InstancedBufferAttribute( new Float32Array( vertices ), 3 );
const colorAttribute = new THREE.InstancedBufferAttribute( new Float32Array( colors ), 3 );
const timeAttribute = new THREE.InstancedBufferAttribute( new Float32Array( timeOffsets ), 1 );

// material and TSL

const material = new THREE.SpriteNodeMaterial( { blending: THREE.AdditiveBlending, depthWrite: false, transparent: true } );
Copy link
Collaborator Author

@Mugen87 Mugen87 Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunag I had to set transparent to true since the default value of SpriteNodeMaterial is false.

However, SpriteMaterial.transparent is true by default. Is the new default value of SpriteNodeMaterial as expected?

If not, it seems the copy of default values in NodeMaterial.setDefaultValues() does not work in all cases.

setDefaultValues( material ) {
// This approach is to reuse the native refreshUniforms*
// and turn available the use of features like transmission and environment in core
for ( const property in material ) {
const value = material[ property ];
if ( this[ property ] === undefined ) {
this[ property ] = value;
if ( value && value.clone ) this[ property ] = value.clone();
}
}
const descriptors = Object.getOwnPropertyDescriptors( material.constructor.prototype );
for ( const key in descriptors ) {
if ( Object.getOwnPropertyDescriptor( this.constructor.prototype, key ) === undefined &&
descriptors[ key ].get !== undefined ) {
Object.defineProperty( this.constructor.prototype, key, descriptors[ key ] );
}
}
}

Copy link
Collaborator

@sunag sunag Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sprites should be transparent by default.

If not, it seems the copy of default values in NodeMaterial.setDefaultValues() does not work in all cases.

It would just define values ​​that do not exist in the class in order to save some computation when cloning the classes, of course this could be solved by testing, but I think this approach is a hack and should be discontinued at some point and replaced by something like #28328


const localTime = instancedBufferAttribute( timeAttribute ).add( time.mul( 0.1 ) );
const modTime = mod( localTime, 1.0 );
const accTime = modTime.mul( modTime );

const angle = accTime.mul( 40.0 );
const pulse = vec2( sin( angle ).mul( 20.0 ), cos( angle ).mul( 20.0 ) );
const pos = instancedBufferAttribute( positionAttribute );

const animated = vec3( pos.x.mul( accTime ).add( pulse.x ), pos.y.mul( accTime ).add( pulse.y ), pos.z.mul( accTime ).mul( 1.75 ) );
const fAlpha = modTime.oneMinus().mul( 2.0 );

material.colorNode = texture( sprite ).mul( vec4( instancedBufferAttribute( colorAttribute ), fAlpha ) );
material.positionNode = animated;
material.scaleNode = float( 2 );

particles = new THREE.Sprite( material );
particles.count = count;
scene.add( particles );

// postprocessing

postProcessing = new THREE.PostProcessing( renderer );

const scenePass = pass( scene, camera );
const scenePassColor = scenePass.getTextureNode();
scenePass = pass( scene, camera );

afterImagePass = afterImage( scenePass, params.damp );

postProcessing.outputNode = afterImagePass;

combinedPass = scenePassColor;
combinedPass = afterImage( combinedPass, params.damp );
//

postProcessing.outputNode = combinedPass;
const gui = new GUI( { title: 'Damp setting' } );
gui.add( afterImagePass.damp, 'value', 0.25, 1 );
gui.add( params, 'enabled' ).onChange( updatePassChain );

//

stats = new Stats();
document.body.appendChild( stats.dom );

window.addEventListener( 'resize', onWindowResize );

}

function createGUI() {
function updatePassChain() {

const gui = new GUI( { title: 'Damp setting' } );
gui.add( combinedPass.damp, 'value', 0, 1 ).step( 0.001 );
if ( params.enabled === true ) {

postProcessing.outputNode = afterImagePass;

} else {

postProcessing.outputNode = scenePass;

}

postProcessing.needsUpdate = true;


}


function getRandomPointOnSphere( r, v ) {

const angle = Math.random() * Math.PI * 2;
const u = Math.random() * 2 - 1;

v.set(
Math.cos( angle ) * Math.sqrt( 1 - Math.pow( u, 2 ) ) * r,
Math.sin( angle ) * Math.sqrt( 1 - Math.pow( u, 2 ) ) * r,
u * r
);

return v;

}

Expand All @@ -89,19 +178,13 @@

}

function render() {

mesh.rotation.x += 0.0075;
mesh.rotation.y += 0.015;
function animate( time ) {

particles.rotation.z = time * 0.001;

postProcessing.render();

}

function animate() {

render();
stats.update();

}

Expand Down
1 change: 1 addition & 0 deletions test/e2e/puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const exceptionList = [
'webgpu_materials_envmaps_bpcem',
'webgpu_postprocessing_sobel',
'webgpu_postprocessing_3dlut',
'webgpu_postprocessing_afterimage',

// WebGPU idleTime and parseTime too low
'webgpu_compute_particles',
Expand Down