diff --git a/example/viewer.html b/example/viewer.html
new file mode 100644
index 00000000..7220a304
--- /dev/null
+++ b/example/viewer.html
@@ -0,0 +1,48 @@
+
+
+	
+		Drag and Drop GLTF Example
+		
+
+		
+
+	
+	
+		Drop GLTF/GLB file here
+		
+	
+
diff --git a/example/viewer.js b/example/viewer.js
new file mode 100644
index 00000000..5ac4321e
--- /dev/null
+++ b/example/viewer.js
@@ -0,0 +1,222 @@
+import {
+	ACESFilmicToneMapping,
+	Scene,
+	EquirectangularReflectionMapping,
+	WebGLRenderer,
+	PerspectiveCamera,
+	Box3,
+	Vector3,
+	Group,
+	LoadingManager,
+} from 'three';
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
+import { getScaledSettings } from './utils/getScaledSettings.js';
+import { LoaderElement } from './utils/LoaderElement.js';
+import { WebGLPathTracer } from '..';
+
+const ENV_URL = 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/master/hdri/chinese_garden_1k.hdr';
+
+let pathTracer, renderer, controls;
+let camera, scene;
+let loader, modelContainer;
+let isModelLoaded = false;
+
+const dropZone = document.getElementById( 'drop-zone' );
+
+init();
+
+async function init() {
+
+	const { tiles, renderScale } = getScaledSettings();
+
+	loader = new LoaderElement();
+	loader.attach( document.body );
+
+	// renderer
+	renderer = new WebGLRenderer( { antialias: true } );
+	renderer.toneMapping = ACESFilmicToneMapping;
+	renderer.toneMappingExposure = 0.5;
+	document.body.appendChild( renderer.domElement );
+
+	// path tracer
+	pathTracer = new WebGLPathTracer( renderer );
+	pathTracer.filterGlossyFactor = 0.5;
+	pathTracer.renderScale = renderScale;
+	pathTracer.tiles.set( tiles, tiles );
+
+	// camera
+	camera = new PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.025, 500 );
+	camera.position.set( 0, 0, 4 );
+
+	// scene
+	scene = new Scene();
+	scene.backgroundBlurriness = 0.05;
+	scene.environmentIntensity = 3;
+
+	modelContainer = new Group();
+	scene.add( modelContainer );
+
+	// controls
+	controls = new OrbitControls( camera, renderer.domElement );
+	controls.addEventListener( 'change', () => pathTracer.updateCamera() );
+	controls.update();
+
+	// environment
+	const envTexture = await new RGBELoader().loadAsync( ENV_URL ).then( tex => {
+
+		tex.mapping = EquirectangularReflectionMapping;
+		return tex;
+
+	} );
+
+	scene.background = envTexture;
+	scene.environment = envTexture;
+
+	// initialize the path tracer
+	pathTracer.setScene( scene, camera );
+	loader.setPercentage( 1 );
+
+	// listeners
+	window.addEventListener( 'resize', onResize );
+
+	window.addEventListener( 'dragover', e => {
+
+		e.preventDefault();
+		if ( ! isModelLoaded ) {
+
+			dropZone.classList.add( 'drag-over' );
+
+		}
+
+	} );
+
+	window.addEventListener( 'dragleave', e => {
+
+		if ( e.relatedTarget === null || e.relatedTarget === document.documentElement ) {
+
+			dropZone.classList.remove( 'drag-over' );
+
+		}
+
+	} );
+
+	window.addEventListener( 'drop', e => {
+
+		e.preventDefault();
+		dropZone.classList.remove( 'drag-over' );
+
+		const files = e.dataTransfer.files;
+		if ( files.length > 0 ) {
+
+			dropZone.innerText = 'Loading...';
+			dropZone.classList.remove( 'hidden' );
+
+			const fileMap = new Map();
+			let rootUrl = null;
+
+			for ( const file of files ) {
+
+				const url = URL.createObjectURL( file );
+				fileMap.set( file.name, url );
+
+				if ( file.name.match( /\.gltf$/i ) ) {
+
+					rootUrl = url;
+
+				}
+
+			}
+
+			const loadingManager = new LoadingManager();
+			loadingManager.setURLModifier( url => fileMap.get( url.split( '/' ).pop() ) || url );
+
+			const loader = new GLTFLoader( loadingManager );
+			const onLoad = gltf => {
+
+				modelContainer.clear();
+				modelContainer.add( gltf.scene );
+
+				const box = new Box3().setFromObject( gltf.scene );
+				const center = box.getCenter( new Vector3() );
+				const size = box.getSize( new Vector3() );
+
+				gltf.scene.position.sub( center );
+
+				const maxDim = Math.max( size.x, size.y, size.z );
+				const fov = camera.fov * ( Math.PI / 180 );
+				camera.position.z = maxDim / ( 2 * Math.tan( fov / 2 ) );
+				camera.position.z *= 1.5;
+
+				camera.near = maxDim / 100;
+				camera.far = maxDim * 10;
+				camera.updateProjectionMatrix();
+
+				controls.target.set( 0, 0, 0 );
+				controls.update();
+
+				pathTracer.setScene( scene, camera );
+
+				dropZone.innerText = 'Drop GLTF/GLB file here';
+				dropZone.classList.add( 'hidden' );
+				isModelLoaded = true;
+
+				fileMap.forEach( url => URL.revokeObjectURL( url ) );
+
+			};
+
+			if ( rootUrl ) {
+
+				loader.load( rootUrl, onLoad );
+
+			} else {
+
+				const file = files[ 0 ];
+				const reader = new FileReader();
+				reader.onload = e => {
+
+					loader.parse( e.target.result, '', onLoad );
+
+				};
+
+				reader.readAsArrayBuffer( file );
+
+			}
+
+		}
+
+	} );
+
+	onResize();
+	animate();
+
+}
+
+function onResize() {
+
+	// update resolution
+	renderer.setSize( window.innerWidth, window.innerHeight );
+	renderer.setPixelRatio( window.devicePixelRatio );
+
+	camera.aspect = window.innerWidth / window.innerHeight;
+	camera.updateProjectionMatrix();
+
+	// update camera
+	pathTracer.updateCamera();
+
+}
+
+function animate() {
+
+	requestAnimationFrame( animate );
+
+	if ( isModelLoaded ) {
+
+		pathTracer.renderSample();
+
+		loader.setSamples( pathTracer.samples, pathTracer.isCompiling );
+
+	}
+
+}