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 );
+
+ }
+
+}