PRO EDITOR

Lens Flare Effect

In this article you will learn how to add a lens flare effect to your scene using PRO Editor.

First of all, you will need to open the PRO Editor. You can do this by creating a new project or open an existing one from the dashboard and clicking on the PRO Editor button in the top bar.
In the PRO Editor window click on your scene in the layers field on the right panel.
Click on New button on the script panel.
Set up the name of your script and click Edit to open the code editor.
Paste the following code into the editor:
const {
	AdditiveBlending,
	Box2,
	BufferGeometry,
	ClampToEdgeWrapping,
	Color,
	DataTexture,
	InterleavedBuffer,
	InterleavedBufferAttribute,
	Mesh,
	MeshBasicMaterial,
	NearestFilter,
	RGBFormat,
	RawShaderMaterial,
	Vector2,
	Vector3,
	Vector4
} = THREE;

class Lensflare extends Mesh {
	constructor() {
		super( Lensflare.Geometry, new MeshBasicMaterial( { opacity: 0, transparent: true } ) );
		this.type = 'Lensflare';
		this.frustumCulled = false;
		this.renderOrder = Infinity;
		//
		const positionScreen = new Vector3();
		const positionView = new Vector3();
		// textures
		const tempMap = new DataTexture( new Uint8Array( 16 * 16 * 3 ), 16, 16, RGBFormat );
		tempMap.minFilter = NearestFilter;
		tempMap.magFilter = NearestFilter;
		tempMap.wrapS = ClampToEdgeWrapping;
		tempMap.wrapT = ClampToEdgeWrapping;
		const occlusionMap = new DataTexture( new Uint8Array( 16 * 16 * 3 ), 16, 16, RGBFormat );
		occlusionMap.minFilter = NearestFilter;
		occlusionMap.magFilter = NearestFilter;
		occlusionMap.wrapS = ClampToEdgeWrapping;
		occlusionMap.wrapT = ClampToEdgeWrapping;
		// material
		const geometry = Lensflare.Geometry;
		const material1a = new RawShaderMaterial( {
			uniforms: {
				'scale': { value: null },
				'screenPosition': { value: null }
			},
			vertexShader: /* glsl */`
				precision highp float;
				uniform vec3 screenPosition;
				uniform vec2 scale;
				attribute vec3 position;
				void main() {
					gl_Position = vec4( position.xy * scale + screenPosition.xy, screenPosition.z, 1.0 );
				}`,
			fragmentShader: /* glsl */`
				precision highp float;
				void main() {
					gl_FragColor = vec4( 1.0, 0.0, 1.0, 1.0 );
				}`,
			depthTest: true,
			depthWrite: false,
			transparent: false
		} );
		const material1b = new RawShaderMaterial( {
			uniforms: {
				'map': { value: tempMap },
				'scale': { value: null },
				'screenPosition': { value: null }
			},
			vertexShader: /* glsl */`
				precision highp float;
				uniform vec3 screenPosition;
				uniform vec2 scale;
				attribute vec3 position;
				attribute vec2 uv;
				varying vec2 vUV;
				void main() {
					vUV = uv;
					gl_Position = vec4( position.xy * scale + screenPosition.xy, screenPosition.z, 1.0 );
				}`,
			fragmentShader: /* glsl */`
				precision highp float;
				uniform sampler2D map;
				varying vec2 vUV;
				void main() {
					gl_FragColor = texture2D( map, vUV );
				}`,
			depthTest: false,
			depthWrite: false,
			transparent: false
		} );
		// the following object is used for occlusionMap generation
		const mesh1 = new Mesh( geometry, material1a );
		//
		const elements = [];
		const shader = LensflareElement.Shader;
		const material2 = new RawShaderMaterial( {
			uniforms: {
				'map': { value: null },
				'occlusionMap': { value: occlusionMap },
				'color': { value: new Color( 0xffffff ) },
				'scale': { value: new Vector2() },
				'screenPosition': { value: new Vector3() }
			},
			vertexShader: shader.vertexShader,
			fragmentShader: shader.fragmentShader,
			blending: AdditiveBlending,
			transparent: true,
			depthWrite: false
		} );
		const mesh2 = new Mesh( geometry, material2 );
		this.addElement = function ( element ) {
			elements.push( element );
		};
		//
		const scale = new Vector2();
		const screenPositionPixels = new Vector2();
		const validArea = new Box2();
		const viewport = new Vector4();
		this.onBeforeRender = function ( renderer, scene, camera ) {
			renderer.getCurrentViewport( viewport );
			const invAspect = viewport.w / viewport.z;
			const halfViewportWidth = viewport.z / 2.0;
			const halfViewportHeight = viewport.w / 2.0;
			let size = 16 / viewport.w;
			scale.set( size * invAspect, size );
			validArea.min.set( viewport.x, viewport.y );
			validArea.max.set( viewport.x + ( viewport.z - 16 ), viewport.y + ( viewport.w - 16 ) );
			// calculate position in screen space
			positionView.setFromMatrixPosition( this.matrixWorld );
			positionView.applyMatrix4( camera.matrixWorldInverse );
			if ( positionView.z > 0 ) return; // lensflare is behind the camera
			positionScreen.copy( positionView ).applyMatrix4( camera.projectionMatrix );
			// horizontal and vertical coordinate of the lower left corner of the pixels to copy
			screenPositionPixels.x = viewport.x + ( positionScreen.x * halfViewportWidth ) + halfViewportWidth - 8;
			screenPositionPixels.y = viewport.y + ( positionScreen.y * halfViewportHeight ) + halfViewportHeight - 8;
			// screen cull
			if ( validArea.containsPoint( screenPositionPixels ) ) {
				// save current RGB to temp texture
				renderer.copyFramebufferToTexture( screenPositionPixels, tempMap );
				// render pink quad
				let uniforms = material1a.uniforms;
				uniforms[ 'scale' ].value = scale;
				uniforms[ 'screenPosition' ].value = positionScreen;
				renderer.renderBufferDirect( camera, null, geometry, material1a, mesh1, null );
				// copy result to occlusionMap
				renderer.copyFramebufferToTexture( screenPositionPixels, occlusionMap );
				// restore graphics
				uniforms = material1b.uniforms;
				uniforms[ 'scale' ].value = scale;
				uniforms[ 'screenPosition' ].value = positionScreen;
				renderer.renderBufferDirect( camera, null, geometry, material1b, mesh1, null );
				// render elements
				const vecX = - positionScreen.x * 2;
				const vecY = - positionScreen.y * 2;
				for ( let i = 0, l = elements.length; i < l; i ++ ) {
					const element = elements[ i ];
					const uniforms = material2.uniforms;
					uniforms[ 'color' ].value.copy( element.color );
					uniforms[ 'map' ].value = element.texture;
					uniforms[ 'screenPosition' ].value.x = positionScreen.x + vecX * element.distance;
					uniforms[ 'screenPosition' ].value.y = positionScreen.y + vecY * element.distance;
					size = element.size / viewport.w;
					const invAspect = viewport.w / viewport.z;
					uniforms[ 'scale' ].value.set( size * invAspect, size );
					material2.uniformsNeedUpdate = true;
					renderer.renderBufferDirect( camera, null, geometry, material2, mesh2, null );
				}
			}
		};
		this.dispose = function () {
			material1a.dispose();
			material1b.dispose();
			material2.dispose();
			tempMap.dispose();
			occlusionMap.dispose();
			for ( let i = 0, l = elements.length; i < l; i ++ ) {
				elements[ i ].texture.dispose();
			}
		};
	}
}
Lensflare.prototype.isLensflare = true;
//
class LensflareElement {
	constructor( texture, size = 1, distance = 0, color = new Color( 0xffffff ) ) {
		this.texture = texture;
		this.size = size;
		this.distance = distance;
		this.color = color;
	}
}
LensflareElement.Shader = {
	uniforms: {
		'map': { value: null },
		'occlusionMap': { value: null },
		'color': { value: null },
		'scale': { value: null },
		'screenPosition': { value: null }
	},
	vertexShader: /* glsl */`
		precision highp float;
		uniform vec3 screenPosition;
		uniform vec2 scale;
		uniform sampler2D occlusionMap;
		attribute vec3 position;
		attribute vec2 uv;
		varying vec2 vUV;
		varying float vVisibility;
		void main() {
			vUV = uv;
			vec2 pos = position.xy;
			vec4 visibility = texture2D( occlusionMap, vec2( 0.1, 0.1 ) );
			visibility += texture2D( occlusionMap, vec2( 0.5, 0.1 ) );
			visibility += texture2D( occlusionMap, vec2( 0.9, 0.1 ) );
			visibility += texture2D( occlusionMap, vec2( 0.9, 0.5 ) );
			visibility += texture2D( occlusionMap, vec2( 0.9, 0.9 ) );
			visibility += texture2D( occlusionMap, vec2( 0.5, 0.9 ) );
			visibility += texture2D( occlusionMap, vec2( 0.1, 0.9 ) );
			visibility += texture2D( occlusionMap, vec2( 0.1, 0.5 ) );
			visibility += texture2D( occlusionMap, vec2( 0.5, 0.5 ) );
			vVisibility =        visibility.r / 9.0;
			vVisibility *= 1.0 - visibility.g / 9.0;
			vVisibility *=       visibility.b / 9.0;
			gl_Position = vec4( ( pos * scale + screenPosition.xy ).xy, screenPosition.z, 1.0 );
		}`,
	fragmentShader: /* glsl */`
		precision highp float;
		uniform sampler2D map;
		uniform vec3 color;
		varying vec2 vUV;
		varying float vVisibility;
		void main() {
			vec4 texture = texture2D( map, vUV );
			texture.a *= vVisibility;
			gl_FragColor = texture;
			gl_FragColor.rgb *= color;
		}`
};
Lensflare.Geometry = ( function () {
	const geometry = new BufferGeometry();
	const float32Array = new Float32Array( [
		- 1, - 1, 0, 0, 0,
		1, - 1, 0, 1, 0,
		1, 1, 0, 1, 1,
		- 1, 1, 0, 0, 1
	] );
	const interleavedBuffer = new InterleavedBuffer( float32Array, 5 );
	geometry.setIndex( [ 0, 1, 2,	0, 2, 3 ] );
	geometry.setAttribute( 'position', new InterleavedBufferAttribute( interleavedBuffer, 3, 0, false ) );
	geometry.setAttribute( 'uv', new InterleavedBufferAttribute( interleavedBuffer, 2, 3, false ) );
	return geometry;
} )();

const light = new THREE.PointLight( 0xffffff, 1.5, 2000 );
const textureLoader = new THREE.TextureLoader();
const textureFlare0 = textureLoader.load( "https://threejs.org/examples/textures/lensflare/lensflare0.png" );
const textureFlare1 = textureLoader.load( "https://threejs.org/examples/textures/lensflare/lensflare2.png" );
const textureFlare2 = textureLoader.load( "https://threejs.org/examples/textures/lensflare/lensflare3.png" );
const lensflare = new Lensflare();
lensflare.addElement( new LensflareElement( textureFlare0, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare1, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare2, 60, 0.6 ) );
light.add( lensflare );
this.add(light);

const clock = new THREE.Clock();
function update( ) {
	if (5 < clock.getElapsedTime()) {
		light.visible = false;
	}
}
You can adjust how long the flare will stay on the scene before disappearing by changing the value in this part of code.
function update( ) {
	if (5 < clock.getElapsedTime()) {
		light.visible = false;
	}
}
In our case the flare will disappear in 5 seconds.

You can adjust the size of your lens flare effect by changing these values:
const lensflare = new Lensflare();
lensflare.addElement( new LensflareElement( textureFlare0, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare1, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare2, 60, 0.6 ) );
Close the code editor and click File -> Publish to export your scene into JSON file.
Go back to the standard editor, click on the JSON MAE object type on the left panel and upload the file you exported from the PRO Editor.
You will see the flare effect on the scene. Now you can preview it using in the editor or try it out on a mobile device.
Related Topics: