text: import {
  	ACESFilmicToneMapping,
  	NoToneMapping,
  	Box3,
  	LoadingManager,
  	Sphere,
  	Color,
  	DoubleSide,
  	Mesh,
  	MeshStandardMaterial,
  	PlaneBufferGeometry,
  	Group,
  	MeshPhysicalMaterial,
  	WebGLRenderer,
  	Scene,
  	PerspectiveCamera,
  	MeshBasicMaterial,
  	sRGBEncoding,
  } from 'three';
  import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
  import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
  import { LDrawLoader } from 'three/examples/jsm/loaders/LDrawLoader.js';
  import { LDrawUtils } from 'three/examples/jsm/utils/LDrawUtils.js';
  import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
  import Stats from 'three/examples/jsm/libs/stats.module.js';
  import { generateRadialFloorTexture } from './utils/generateRadialFloorTexture.js';
  import { PathTracingSceneWorker } from '../src/workers/PathTracingSceneWorker.js';
  import { PhysicalPathTracingMaterial, PathTracingRenderer, MaterialReducer, BlurredEnvMapGenerator } from '../src/index.js';
  import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
  
  const envMaps = {
  	'Royal Esplanade': 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/equirectangular/royal_esplanade_1k.hdr',
  	'Moonless Golf': 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/equirectangular/moonless_golf_1k.hdr',
  	'Overpass': 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/equirectangular/pedestrian_overpass_1k.hdr',
  	'Venice Sunset': 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/equirectangular/venice_sunset_1k.hdr',
  };
  
  const models = window.MODEL_LIST || {};
  
  let initialModel = Object.keys( models )[ 0 ];
  if ( window.location.hash ) {
  
  	const modelName = window.location.hash.substring( 1 ).replaceAll( '%20', ' ' );
  	if ( modelName in models ) {
  
  		initialModel = modelName;
  
  	}
  
  }
  
  const params = {
  
  	multipleImportanceSampling: true,
  	acesToneMapping: true,
  	resolutionScale: 1 / window.devicePixelRatio,
  	tilesX: 2,
  	tilesY: 2,
  	samplesPerFrame: 1,
  
  	model: initialModel,
  
  	envMap: envMaps[ 'Royal Esplanade' ],
  
  	gradientTop: '#bfd8ff',
  	gradientBottom: '#ffffff',
  
  	environmentIntensity: 3.0,
  	environmentBlur: 0.0,
  
  	backgroundType: 'Gradient',
  	bgGradientTop: '#111111',
  	bgGradientBottom: '#000000',
  
  	enable: true,
  	bounces: 3,
  	pause: false,
  
  	floorColor: '#080808',
  	floorEnabled: true,
  	floorRoughness: 0.1,
  	floorMetalness: 0.0
  
  };
  
  let creditEl, loadingEl, samplesEl;
  let floorPlane, gui, stats, sceneInfo;
  let renderer, camera, ptRenderer, fsQuad, controls, scene;
  let envMap, envMapGenerator;
  let loadingModel = false;
  let delaySamples = 0;
  
  init();
  
  async function init() {
  
  	creditEl = document.getElementById( 'credits' );
  	loadingEl = document.getElementById( 'loading' );
  	samplesEl = document.getElementById( 'samples' );
  
  	renderer = new WebGLRenderer( { antialias: true } );
  	renderer.outputEncoding = sRGBEncoding;
  	renderer.toneMapping = ACESFilmicToneMapping;
  	document.body.appendChild( renderer.domElement );
  
  	scene = new Scene();
  
  	camera = new PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.025, 500 );
  	camera.position.set( - 1, 0.25, 1 );
  
  	ptRenderer = new PathTracingRenderer( renderer );
  	ptRenderer.camera = camera;
  	ptRenderer.material = new PhysicalPathTracingMaterial();
  	ptRenderer.tiles.set( params.tiles, params.tiles );
  	ptRenderer.material.setDefine( 'GRADIENT_BG', 1 );
  	ptRenderer.material.setDefine( 'USE_MIS', Number( params.multipleImportanceSampling ) );
  	ptRenderer.material.bgGradientTop.set( params.bgGradientTop );
  	ptRenderer.material.bgGradientBottom.set( params.bgGradientBottom );
  
  	fsQuad = new FullScreenQuad( new MeshBasicMaterial( {
  		map: ptRenderer.target.texture,
  		transparent: true,
  	} ) );
  
  	controls = new OrbitControls( camera, renderer.domElement );
  	controls.update();
  	controls.addEventListener( 'change', () => {
  
  		if ( params.tilesX * params.tilesY !== 1.0 ) {
  
  			delaySamples = 1;
  
  		}
  
  		ptRenderer.reset();
  
  	} );
  
  	envMapGenerator = new BlurredEnvMapGenerator( renderer );
  
  	const floorTex = generateRadialFloorTexture( 2048 );
  	floorPlane = new Mesh(
  		new PlaneBufferGeometry(),
  		new MeshStandardMaterial( {
  			map: floorTex,
  			transparent: true,
  			color: 0x080808,
  			roughness: 0.1,
  			metalness: 0.0
  		} )
  	);
  	floorPlane.scale.setScalar( 3 );
  	floorPlane.rotation.x = - Math.PI / 2;
  
  	stats = new Stats();
  	document.body.appendChild( stats.dom );
  	renderer.physicallyCorrectLights = true;
  	renderer.toneMapping = ACESFilmicToneMapping;
  	ptRenderer.material.setDefine( 'GRADIENT_BG', 1 );
  	scene.background = new Color( 0x060606 );
  	ptRenderer.tiles.set( params.tilesX, params.tilesY );
  
  	updateModel();
  	updateEnvMap();
  	onResize();
  
  	animate();
  
  	window.addEventListener( 'resize', onResize );
  
  }
  
  function animate() {
  
  	requestAnimationFrame( animate );
  
  	stats.update();
  
  	if ( loadingModel ) {
  
  		return;
  
  	}
  
  	if ( ptRenderer.samples < 1.0 || ! params.enable ) {
  
  		renderer.render( scene, camera );
  
  	}
  
  	if ( params.enable && delaySamples === 0 ) {
  
  		const samples = Math.floor( ptRenderer.samples );
  		samplesEl.innerText = `samples: ${ samples }`;
  
  		ptRenderer.material.materials.updateFrom( sceneInfo.materials, sceneInfo.textures );
  		ptRenderer.material.filterGlossyFactor = 0.5;
  		ptRenderer.material.environmentIntensity = params.environmentIntensity;
  		ptRenderer.material.bounces = params.bounces;
  		ptRenderer.material.physicalCamera.updateFrom( camera );
  
  		camera.updateMatrixWorld();
  
  		if ( ! params.pause || ptRenderer.samples < 1 ) {
  
  			for ( let i = 0, l = params.samplesPerFrame; i < l; i ++ ) {
  
  				ptRenderer.update();
  
  			}
  
  		}
  
  		renderer.autoClear = false;
  		fsQuad.render( renderer );
  		renderer.autoClear = true;
  
  	} else if ( delaySamples > 0 ) {
  
  		delaySamples --;
  
  	}
  
  	samplesEl.innerText = `Samples: ${ Math.floor( ptRenderer.samples ) }`;
  
  }
  
  function onResize() {
  
  	const w = window.innerWidth;
  	const h = window.innerHeight;
  	const scale = params.resolutionScale;
  	const dpr = window.devicePixelRatio;
  
  	ptRenderer.target.setSize( w * scale * dpr, h * scale * dpr );
  	ptRenderer.reset();
  
  	renderer.setSize( w, h );
  	renderer.setPixelRatio( window.devicePixelRatio * scale );
  	camera.aspect = w / h;
  	camera.updateProjectionMatrix();
  
  }
  
  function buildGui() {
  
  	if ( gui ) {
  
  		gui.destroy();
  
  	}
  
  	gui = new GUI();
  
  	gui.add( params, 'model', Object.keys( models ) ).onChange( updateModel );
  
  	const pathTracingFolder = gui.addFolder( 'path tracing' );
  	pathTracingFolder.add( params, 'enable' );
  	pathTracingFolder.add( params, 'pause' );
  	pathTracingFolder.add( params, 'multipleImportanceSampling' ).onChange( v => {
  
  		ptRenderer.material.setDefine( 'USE_MIS', Number( v ) );
  		ptRenderer.reset();
  
  	} );
  	pathTracingFolder.add( params, 'acesToneMapping' ).onChange( v => {
  
  		renderer.toneMapping = v ? ACESFilmicToneMapping : NoToneMapping;
  
  	} );
  	pathTracingFolder.add( params, 'bounces', 1, 20, 1 ).onChange( () => {
  
  		ptRenderer.reset();
  
  	} );
  
  	const resolutionFolder = gui.addFolder( 'resolution' );
  	resolutionFolder.add( params, 'resolutionScale', 0.1, 1.0, 0.01 ).onChange( () => {
  
  		onResize();
  
  	} );
  	resolutionFolder.add( params, 'samplesPerFrame', 1, 10, 1 );
  	resolutionFolder.add( params, 'tilesX', 1, 10, 1 ).onChange( v => {
  
  		ptRenderer.tiles.x = v;
  
  	} );
  	resolutionFolder.add( params, 'tilesY', 1, 10, 1 ).onChange( v => {
  
  		ptRenderer.tiles.y = v;
  
  	} );
  	resolutionFolder.open();
  
  	const environmentFolder = gui.addFolder( 'environment' );
  	environmentFolder.add( params, 'envMap', envMaps ).name( 'map' ).onChange( updateEnvMap );
  	environmentFolder.add( params, 'environmentBlur', 0.0, 1.0 ).onChange( () => {
  
  		updateEnvBlur();
  		ptRenderer.reset();
  
  	} ).name( 'env map blur' );
  	environmentFolder.add( params, 'environmentIntensity', 0.0, 10.0 ).onChange( () => {
  
  		ptRenderer.reset();
  
  	} ).name( 'intensity' );
  	environmentFolder.open();
  
  	const backgroundFolder = gui.addFolder( 'background' );
  	backgroundFolder.add( params, 'backgroundType', [ 'Environment', 'Gradient' ] ).onChange( v => {
  
  		ptRenderer.material.setDefine( 'GRADIENT_BG', Number( v === 'Gradient' ) );
  		if ( v === 'Gradient' ) {
  
  			scene.background = new Color( 0x060606 );
  
  		} else {
  
  			scene.background = scene.environment;
  
  		}
  
  		ptRenderer.reset();
  
  	} );
  	backgroundFolder.addColor( params, 'bgGradientTop' ).onChange( v => {
  
  		ptRenderer.material.uniforms.bgGradientTop.value.set( v );
  		ptRenderer.reset();
  
  	} );
  	backgroundFolder.addColor( params, 'bgGradientBottom' ).onChange( v => {
  
  		ptRenderer.material.uniforms.bgGradientBottom.value.set( v );
  		ptRenderer.reset();
  
  	} );
  	backgroundFolder.open();
  
  	const floorFolder = gui.addFolder( 'floor' );
  	floorFolder.add( params, 'floorEnabled' ).onChange( v => {
  
  		floorPlane.material.opacity = v ? 1 : 0;
  		ptRenderer.reset();
  
  	} );
  	floorFolder.addColor( params, 'floorColor' ).onChange( v => {
  
  		floorPlane.material.color.set( v );
  		ptRenderer.reset();
  
  	} );
  	floorFolder.add( params, 'floorRoughness', 0, 1 ).onChange( v => {
  
  		floorPlane.material.roughness = v;
  		ptRenderer.reset();
  
  	} );
  	floorFolder.add( params, 'floorMetalness', 0, 1 ).onChange( v => {
  
  		floorPlane.material.metalness = v;
  		ptRenderer.reset();
  
  	} );
  
  }
  
  function updateEnvMap() {
  
  	new RGBELoader()
  		.load( params.envMap, texture => {
  
  			if ( scene.environmentMap ) {
  
  				scene.environment.dispose();
  				envMap.dispose();
  
  			}
  
  			envMap = texture;
  			updateEnvBlur();
  			ptRenderer.reset();
  
  		} );
  
  }
  
  function updateEnvBlur() {
  
  	const blurredEnvMap = envMapGenerator.generate( envMap, params.environmentBlur );
  	ptRenderer.material.envMapInfo.updateFrom( blurredEnvMap );
  
  	scene.environment = blurredEnvMap;
  	if ( params.backgroundType !== 'Gradient' ) {
  
  		scene.background = blurredEnvMap;
  
  	}
  
  }
  
  function convertOpacityToTransmission( model ) {
  
  	model.traverse( c => {
  
  		if ( c.material ) {
  
  			const material = c.material;
  			if ( material.opacity < 0.65 && material.opacity > 0.2 ) {
  
  				const newMaterial = new MeshPhysicalMaterial();
  				for ( const key in material ) {
  
  					if ( key in material ) {
  
  						if ( material[ key ] === null ) {
  
  							continue;
  
  						}
  
  						if ( material[ key ].isTexture ) {
  
  							newMaterial[ key ] = material[ key ];
  
  						} else if ( material[ key ].copy && material[ key ].constructor === newMaterial[ key ].constructor ) {
  
  							newMaterial[ key ].copy( material[ key ] );
  
  						} else if ( ( typeof material[ key ] ) === 'number' ) {
  
  							newMaterial[ key ] = material[ key ];
  
  						}
  
  					}
  
  				}
  
  				newMaterial.opacity = 1.0;
  				newMaterial.transmission = 1.0;
  				c.material = newMaterial;
  
  			}
  
  		}
  
  	} );
  
  }
  
  async function updateModel() {
  
  	if ( gui ) {
  
  		gui.destroy();
  		gui = null;
  
  	}
  
  	let model;
  	const manager = new LoadingManager();
  	const modelInfo = models[ params.model ];
  
  	loadingModel = true;
  	renderer.domElement.style.visibility = 'hidden';
  	samplesEl.innerText = '--';
  	creditEl.innerText = '--';
  	loadingEl.innerText = 'Loading';
  	loadingEl.style.visibility = 'visible';
  
  	scene.traverse( c => {
  
  		if ( c.material ) {
  
  			const material = c.material;
  			for ( const key in material ) {
  
  				if ( material[ key ] && material[ key ].isTexture ) {
  
  					material[ key ].dispose();
  
  				}
  
  			}
  
  		}
  
  	} );
  
  	if ( sceneInfo ) {
  
  		scene.remove( sceneInfo.scene );
  
  	}
  
  
  	const onFinish = async () => {
  
  		if ( modelInfo.removeEmission ) {
  
  			model.traverse( c => {
  
  				if ( c.material ) {
  
  					c.material.emissiveMap = null;
  					c.material.emissiveIntensity = 0;
  
  				}
  
  			} );
  
  		}
  
  		if ( modelInfo.opacityToTransmission ) {
  
  			convertOpacityToTransmission( model );
  
  		}
  
  		model.traverse( c => {
  
  			if ( c.material ) {
  
  				c.material.side = DoubleSide;
  
  			}
  
  		} );
  
  		if ( modelInfo.postProcess ) {
  
  			modelInfo.postProcess( model );
  
  		}
  
  		// center the model
  		const box = new Box3();
  		box.setFromObject( model );
  		model.position
  			.addScaledVector( box.min, - 0.5 )
  			.addScaledVector( box.max, - 0.5 );
  
  		const sphere = new Sphere();
  		box.getBoundingSphere( sphere );
  
  		model.scale.setScalar( 1 / sphere.radius );
  		model.position.multiplyScalar( 1 / sphere.radius );
  
  		box.setFromObject( model );
  
  		// rotate model after so it doesn't affect the bounding sphere scale
  		if ( modelInfo.rotation ) {
  
  			model.rotation.set( ...modelInfo.rotation );
  
  		}
  
  		model.updateMatrixWorld();
  
  		const group = new Group();
  		floorPlane.position.y = box.min.y;
  		group.add( model, floorPlane );
  
  		const reducer = new MaterialReducer();
  		reducer.process( group );
  
  		const generator = new PathTracingSceneWorker();
  		const result = await generator.generate( group, { onProgress: v => {
  
  			const percent = Math.floor( 100 * v );
  			loadingEl.innerText = `Building BVH : ${ percent }%`;
  
  		} } );
  
  		sceneInfo = result;
  		scene.add( sceneInfo.scene );
  
  		const { bvh, textures, materials } = result;
  		const geometry = bvh.geometry;
  		const material = ptRenderer.material;
  
  		material.bvh.updateFrom( bvh );
  		material.normalAttribute.updateFrom( geometry.attributes.normal );
  		material.tangentAttribute.updateFrom( geometry.attributes.tangent );
  		material.uvAttribute.updateFrom( geometry.attributes.uv );
  		material.materialIndexAttribute.updateFrom( geometry.attributes.materialIndex );
  		material.textures.setTextures( renderer, 2048, 2048, textures );
  		material.materials.updateFrom( materials, textures );
  
  		generator.dispose();
  
  		loadingEl.style.visibility = 'hidden';
  
  		creditEl.innerHTML = modelInfo.credit || '';
  		creditEl.style.visibility = modelInfo.credit ? 'visible' : 'hidden';
  		params.bounces = modelInfo.bounces || 3;
  		buildGui();
  
  		loadingModel = false;
  		renderer.domElement.style.visibility = 'visible';
  
  		ptRenderer.reset();
  
  	};
  
  	const url = modelInfo.url;
  	if ( /(gltf|glb)$/i.test( url ) ) {
  
  		manager.onLoad = onFinish;
  		new GLTFLoader( manager )
  			.setMeshoptDecoder( MeshoptDecoder )
  			.load(
  				url,
  				gltf => {
  
  					model = gltf.scene;
  
  				},
  				progress => {
  
  					if ( progress.total !== 0 && progress.total >= progress.loaded ) {
  
  						const percent = Math.floor( 100 * progress.loaded / progress.total );
  						loadingEl.innerText = `Loading : ${ percent }%`;
  
  					}
  
  				},
  			);
  
  	} else if ( /mpd$/i.test( url ) ) {
  
  		manager.onProgress = ( url, loaded, total ) => {
  
  			const percent = Math.floor( 100 * loaded / total );
  			loadingEl.innerText = `Loading : ${ percent }%`;
  
  		};
  
  		const loader = new LDrawLoader( manager );
  		await loader.preloadMaterials( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/colors/ldcfgalt.ldr' );
  		loader
  			.setPartsLibraryPath( 'https://raw.githubusercontent.com/gkjohnson/ldraw-parts-library/master/complete/ldraw/' )
  			.load(
  				url,
  				result => {
  
  					model = LDrawUtils.mergeObject( result );
  					model.rotation.set( Math.PI, 0, 0 );
  					model.traverse( c => {
  
  						if ( c.isLineSegments ) {
  
  							c.visible = false;
  
  						}
  
  						if ( c.isMesh ) {
  
  							c.material.roughness *= 0.01;
  
  						}
  
  					} );
  					onFinish();
  
  				},
  			);
  
  	}
  
  }
  