Skip to content

Commit 167c022

Browse files
authored
Addons: Add LensflareNode. (#29715)
* LensflareNode: Experimenting with different approaches. * LensflareNode: Improve example. * Examples: Clean up * Examples: Add comment. * LensflareNode: Updating API. * Examples: Clean up.
1 parent ca20cf9 commit 167c022

File tree

6 files changed

+345
-0
lines changed

6 files changed

+345
-0
lines changed

examples/files.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@
393393
"webgpu_postprocessing_dof",
394394
"webgpu_postprocessing_pixel",
395395
"webgpu_postprocessing_fxaa",
396+
"webgpu_postprocessing_lensflare",
396397
"webgpu_postprocessing_masking",
397398
"webgpu_postprocessing_motion_blur",
398399
"webgpu_postprocessing_outline",
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { RenderTarget, Vector2 } from 'three';
2+
import { convertToTexture, TempNode, nodeObject, Fn, NodeUpdateType, QuadMesh, PostProcessingUtils, NodeMaterial, passTexture, uv, vec2, vec3, vec4, max, float, sub, int, Loop, fract, pow, distance } from 'three/tsl';
3+
4+
const _quadMesh = /*@__PURE__*/ new QuadMesh();
5+
const _size = /*@__PURE__*/ new Vector2();
6+
let _rendererState;
7+
8+
/**
9+
* References:
10+
* https://john-chapman-graphics.blogspot.com/2013/02/pseudo-lens-flare.html
11+
* https://john-chapman.github.io/2017/11/05/pseudo-lens-flare.html
12+
*/
13+
class LensflareNode extends TempNode {
14+
15+
static get type() {
16+
17+
return 'LensflareNode';
18+
19+
}
20+
21+
constructor( textureNode, params = {} ) {
22+
23+
super();
24+
25+
this.textureNode = textureNode;
26+
27+
const {
28+
ghostTint = vec3( 1, 1, 1 ),
29+
threshold = float( 0.5 ),
30+
ghostSamples = float( 4 ),
31+
ghostSpacing = float( 0.25 ),
32+
ghostAttenuationFactor = float( 25 ),
33+
downSampleRatio = 4
34+
} = params;
35+
36+
this.ghostTint = ghostTint;
37+
this.threshold = threshold;
38+
this.ghostSamples = ghostSamples;
39+
this.ghostSpacing = ghostSpacing;
40+
this.ghostAttenuationFactor = ghostAttenuationFactor;
41+
this.downSampleRatio = downSampleRatio;
42+
43+
this.updateBeforeType = NodeUpdateType.FRAME;
44+
45+
// render targets
46+
47+
this._renderTarget = new RenderTarget( 1, 1, { depthBuffer: false } );
48+
this._renderTarget.texture.name = 'LensflareNode';
49+
50+
// materials
51+
52+
this._material = new NodeMaterial();
53+
this._material.name = 'LensflareNode';
54+
55+
//
56+
57+
this._textureNode = passTexture( this, this._renderTarget.texture );
58+
59+
}
60+
61+
getTextureNode() {
62+
63+
return this._textureNode;
64+
65+
}
66+
67+
setSize( width, height ) {
68+
69+
const resx = Math.round( width / this.downSampleRatio );
70+
const resy = Math.round( height / this.downSampleRatio );
71+
72+
this._renderTarget.setSize( resx, resy );
73+
74+
}
75+
76+
updateBefore( frame ) {
77+
78+
const { renderer } = frame;
79+
80+
const size = renderer.getDrawingBufferSize( _size );
81+
this.setSize( size.width, size.height );
82+
83+
_rendererState = PostProcessingUtils.resetRendererState( renderer, _rendererState );
84+
85+
_quadMesh.material = this._material;
86+
87+
// clear
88+
89+
renderer.setMRT( null );
90+
91+
// lensflare
92+
93+
renderer.setRenderTarget( this._renderTarget );
94+
_quadMesh.render( renderer );
95+
96+
// restore
97+
98+
PostProcessingUtils.restoreRendererState( renderer, _rendererState );
99+
100+
}
101+
102+
setup( builder ) {
103+
104+
const lensflare = Fn( () => {
105+
106+
// flip uvs so lens flare pivot around the image center
107+
108+
const texCoord = uv().oneMinus().toVar();
109+
110+
// ghosts are positioned along this vector
111+
112+
const ghostVec = sub( vec2( 0.5 ), texCoord ).mul( this.ghostSpacing ).toVar();
113+
114+
// sample ghosts
115+
116+
const result = vec4().toVar();
117+
118+
Loop( { start: int( 0 ), end: int( this.ghostSamples ), type: 'int', condition: '<' }, ( { i } ) => {
119+
120+
// use fract() to ensure that the texture coordinates wrap around
121+
122+
const sampleUv = fract( texCoord.add( ghostVec.mul( float( i ) ) ) ).toVar();
123+
124+
// reduce contributions from samples at the screen edge
125+
126+
const d = distance( sampleUv, vec2( 0.5 ) );
127+
const weight = pow( d.oneMinus(), this.ghostAttenuationFactor );
128+
129+
// accumulate
130+
131+
let sample = this.textureNode.uv( sampleUv ).rgb;
132+
133+
sample = max( sample.sub( this.threshold ), vec3( 0 ) ).mul( this.ghostTint );
134+
135+
result.addAssign( sample.mul( weight ) );
136+
137+
} );
138+
139+
return result;
140+
141+
} );
142+
143+
this._material.fragmentNode = lensflare().context( builder.getSharedContext() );
144+
this._material.needsUpdate = true;
145+
146+
return this._textureNode;
147+
148+
}
149+
150+
dispose() {
151+
152+
this._renderTarget.dispose();
153+
this._material.dispose();
154+
155+
}
156+
157+
}
158+
159+
export default LensflareNode;
160+
161+
export const lensflare = ( inputNode, params ) => nodeObject( new LensflareNode( convertToTexture( inputNode ), params ) );
2.76 MB
Binary file not shown.
26.6 KB
Loading
134 KB
Loading
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js webgpu - postprocessing lensflares</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link type="text/css" rel="stylesheet" href="main.css">
8+
</head>
9+
<body>
10+
11+
<div id="info">
12+
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - postprocessing lensflares<br />
13+
<a href="https://skfb.ly/6SqUF" target="_blank" rel="noopener">Space Ship Hallway</a> by
14+
<a href="https://sketchfab.com/yeeyeeman" target="_blank" rel="noopener">yeeyeeman</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">Creative Commons Attribution</a>.<br />
15+
<a href="https://www.spacespheremaps.com/planetary-spheremaps/" target="_blank" rel="noopener">Ice Planet Close</a> from <a href="https://www.spacespheremaps.com/" target="_blank" rel="noopener">Space Spheremaps</a>.
16+
</div>
17+
18+
<script type="importmap">
19+
{
20+
"imports": {
21+
"three": "../build/three.webgpu.js",
22+
"three/tsl": "../build/three.webgpu.js",
23+
"three/addons/": "./jsm/"
24+
}
25+
}
26+
</script>
27+
28+
<script type="module">
29+
30+
import * as THREE from 'three';
31+
import { pass, mrt, output, emissive, uniform } from 'three/tsl';
32+
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
33+
import { lensflare } from 'three/addons/tsl/display/LensflareNode.js';
34+
import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
35+
36+
import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
37+
38+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
39+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
40+
41+
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
42+
import Stats from 'three/addons/libs/stats.module.js';
43+
44+
let camera, scene, renderer, controls, stats;
45+
let postProcessing;
46+
47+
init();
48+
49+
async function init() {
50+
51+
const container = document.createElement( 'div' );
52+
document.body.appendChild( container );
53+
54+
//
55+
56+
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
57+
camera.position.set( 0, 0.5, - 0.5 );
58+
59+
scene = new THREE.Scene();
60+
61+
const texture = await new UltraHDRLoader().loadAsync( 'textures/equirectangular/ice_planet_close.jpg' );
62+
63+
texture.mapping = THREE.EquirectangularReflectionMapping;
64+
65+
scene.background = texture;
66+
scene.environment = texture;
67+
68+
scene.backgroundIntensity = 2;
69+
scene.environmentIntensity = 15;
70+
71+
// model
72+
73+
const loader = new GLTFLoader();
74+
const gltf = await loader.loadAsync( 'models/gltf/space_ship_hallway.glb' );
75+
76+
const object = gltf.scene;
77+
78+
const aabb = new THREE.Box3().setFromObject( object );
79+
const center = aabb.getCenter( new THREE.Vector3() );
80+
81+
object.position.x += ( object.position.x - center.x );
82+
object.position.y += ( object.position.y - center.y );
83+
object.position.z += ( object.position.z - center.z );
84+
85+
scene.add( object );
86+
87+
//
88+
89+
renderer = new THREE.WebGPURenderer();
90+
renderer.setPixelRatio( window.devicePixelRatio );
91+
renderer.setSize( window.innerWidth, window.innerHeight );
92+
renderer.setAnimationLoop( render );
93+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
94+
container.appendChild( renderer.domElement );
95+
96+
//
97+
98+
const scenePass = pass( scene, camera );
99+
scenePass.setMRT( mrt( {
100+
output,
101+
emissive
102+
} ) );
103+
104+
const outputPass = scenePass.getTextureNode();
105+
const emissivePass = scenePass.getTextureNode( 'emissive' );
106+
107+
const bloomPass = bloom( emissivePass, 1, 1 );
108+
109+
const threshold = uniform( 0.5 );
110+
const ghostAttenuationFactor = uniform( 25 );
111+
const ghostSpacing = uniform( 0.25 );
112+
113+
const flarePass = lensflare( bloomPass, {
114+
threshold,
115+
ghostAttenuationFactor,
116+
ghostSpacing
117+
} );
118+
119+
const blurPass = gaussianBlur( flarePass, 8 ); // optional (blurring produces better flare quality but also adds some overhead)
120+
121+
postProcessing = new THREE.PostProcessing( renderer );
122+
postProcessing.outputNode = outputPass.add( bloomPass ).add( blurPass );
123+
124+
//
125+
126+
controls = new OrbitControls( camera, renderer.domElement );
127+
controls.enableDamping = true;
128+
controls.enablePan = false;
129+
controls.enableZoom = false;
130+
controls.target.copy( camera.position );
131+
controls.target.z -= 0.01;
132+
controls.update();
133+
134+
window.addEventListener( 'resize', onWindowResize );
135+
136+
//
137+
138+
stats = new Stats();
139+
document.body.appendChild( stats.dom );
140+
141+
//
142+
143+
const gui = new GUI();
144+
145+
const bloomFolder = gui.addFolder( 'bloom' );
146+
bloomFolder.add( bloomPass.strength, 'value', 0.0, 2.0 ).name( 'strength' );
147+
bloomFolder.add( bloomPass.radius, 'value', 0.0, 1.0 ).name( 'radius' );
148+
149+
const lensflareFolder = gui.addFolder( 'lensflare' );
150+
lensflareFolder.add( threshold, 'value', 0.0, 1.0 ).name( 'threshold' );
151+
lensflareFolder.add( ghostAttenuationFactor, 'value', 10.0, 50.0 ).name( 'attenuation' );
152+
lensflareFolder.add( ghostSpacing, 'value', 0.0, 0.3 ).name( 'spacing' );
153+
154+
const toneMappingFolder = gui.addFolder( 'tone mapping' );
155+
toneMappingFolder.add( renderer, 'toneMappingExposure', 0.1, 2 ).name( 'exposure' );
156+
157+
}
158+
159+
function onWindowResize() {
160+
161+
camera.aspect = window.innerWidth / window.innerHeight;
162+
camera.updateProjectionMatrix();
163+
164+
renderer.setSize( window.innerWidth, window.innerHeight );
165+
166+
}
167+
168+
//
169+
170+
function render() {
171+
172+
stats.update();
173+
174+
controls.update();
175+
176+
postProcessing.render();
177+
178+
}
179+
180+
</script>
181+
182+
</body>
183+
</html>

0 commit comments

Comments
 (0)