import { BufferAttribute, BufferGeometry, Matrix4, Vector3, Vector4, Matrix3, MeshBasicMaterial, Mesh, ShaderMaterial, NoBlending, Vector2, WebGLRenderTarget, FloatType, RGBAFormat, NearestFilter, PerspectiveCamera, DataUtils, HalfFloatType, Source, DataTexture, LinearFilter, RepeatWrapping, RedFormat, ClampToEdgeWrapping, Quaternion, DataArrayTexture, DoubleSide, BackSide, FrontSide, Color, WebGLArrayRenderTarget, UnsignedByteType, NoToneMapping, RGFormat, NormalBlending, Spherical, EquirectangularReflectionMapping, LinearMipMapLinearFilter, Clock, Scene, AdditiveBlending, Camera, SpotLight, RectAreaLight, PMREMGenerator, MeshStandardMaterial } from 'three';
import { SAH, MeshBVH, FloatVertexAttributeTexture, MeshBVHUniformStruct, UIntVertexAttributeTexture, BVHShaderGLSL } from 'three-mesh-bvh';
import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';

// target offset is the number of elements in the target buffer stride to skip before copying the
// attributes contents in to.
function copyAttributeContents (attr, target, targetOffset = 0) {

	if (attr.isInterleavedBufferAttribute) {

		const itemSize = attr.itemSize;
		for (let i = 0, l = attr.count; i < l; i++) {

			const io = i + targetOffset;
			target.setX(io, attr.getX(i));
			if (itemSize >= 2) target.setY(io, attr.getY(i));
			if (itemSize >= 3) target.setZ(io, attr.getZ(i));
			if (itemSize >= 4) target.setW(io, attr.getW(i));

		}

	} else {

		const array = target.array;
		const cons = array.constructor;
		const byteOffset = array.BYTES_PER_ELEMENT * attr.itemSize * targetOffset;
		const temp = new cons(array.buffer, byteOffset, attr.array.length);
		temp.set(attr.array);

	}

}

// Clones the given attribute with a new compatible buffer attribute but no data
function createAttributeClone (attr, countOverride = null) {

	const cons = attr.array.constructor;
	const normalized = attr.normalized;
	const itemSize = attr.itemSize;
	const count = countOverride === null ? attr.count : countOverride;

	return new BufferAttribute(new cons(itemSize * count), itemSize, normalized);

}

// Confirms that the two provided attributes are compatible. Returns false if they are not.
function validateAttributes (attr1, attr2) {

	if (!attr1 && !attr2) {

		return true;

	}

	if (Boolean(attr1) !== Boolean(attr2)) {

		return false;

	}

	const sameCount = attr1.count === attr2.count;
	const sameNormalized = attr1.normalized === attr2.normalized;
	const sameType = attr1.array.constructor === attr2.array.constructor;
	const sameItemSize = attr1.itemSize === attr2.itemSize;

	if (!sameCount || !sameNormalized || !sameType || !sameItemSize) {

		return false;

	}

	return true;

}

function validateMergeability (geometries) {

	const isIndexed = geometries[0].index !== null;
	const attributesUsed = new Set(Object.keys(geometries[0].attributes));
	if (!geometries[0].getAttribute('position')) {

		throw new Error('StaticGeometryGenerator: position attribute is required.');

	}

	for (let i = 0; i < geometries.length; ++i) {

		const geometry = geometries[i];
		let attributesCount = 0;

		// ensure that all geometries are indexed, or none
		if (isIndexed !== (geometry.index !== null)) {

			throw new Error('StaticGeometryGenerator: All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.');

		}

		// gather attributes, exit early if they're different
		for (const name in geometry.attributes) {

			if (!attributesUsed.has(name)) {

				throw new Error('StaticGeometryGenerator: All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.');

			}

			attributesCount++;

		}

		// ensure geometries have the same number of attributes
		if (attributesCount !== attributesUsed.size) {

			throw new Error('StaticGeometryGenerator: All geometries must have the same number of attributes.');

		}

	}

}

function getTotalIndexCount (geometries) {

	let result = 0;
	for (let i = 0, l = geometries.length; i < l; i++) {

		result += geometries[i].getIndex().count;

	}

	return result;

}

function getTotalAttributeCount (geometries) {

	let result = 0;
	for (let i = 0, l = geometries.length; i < l; i++) {

		result += geometries[i].getAttribute('position').count;

	}

	return result;

}

function trimMismatchedAttributes (target, indexCount, attrCount) {

	if (target.index && target.index.count !== indexCount) {

		target.setIndex(null);

	}

	const attributes = target.attributes;
	for (const key in attributes) {

		const attr = attributes[key];
		if (attr.count !== attrCount) {

			target.deleteAttribute(key);

		}

	}

}

// Modified version of BufferGeometryUtils.mergeBufferGeometries that ignores morph targets and updates a attributes in place
function mergeGeometries (geometries, options = {}, targetGeometry = new BufferGeometry()) {

	const {
		useGroups = false,
		forceUpdate = false,
		skipAssigningAttributes = [],
		overwriteIndex = true,
	} = options;

	// check if we can merge these geometries
	validateMergeability(geometries);

	const isIndexed = geometries[0].index !== null;
	const totalIndexCount = isIndexed ? getTotalIndexCount(geometries) : - 1;
	const totalAttributeCount = getTotalAttributeCount(geometries);
	trimMismatchedAttributes(targetGeometry, totalIndexCount, totalAttributeCount);

	// set up groups
	if (useGroups) {

		let offset = 0;
		for (let i = 0, l = geometries.length; i < l; i++) {

			const geometry = geometries[i];

			let primitiveCount;
			if (isIndexed) {

				primitiveCount = geometry.getIndex().count;

			} else {

				primitiveCount = geometry.getAttribute('position').count;

			}

			targetGeometry.addGroup(offset, primitiveCount, i);
			offset += primitiveCount;

		}

	}

	// generate the final geometry
	// skip the assigning any attributes for items in the above array
	if (isIndexed) {

		// set up the index if it doesn't exist
		let forceUpdateIndex = false;
		if (!targetGeometry.index) {

			targetGeometry.setIndex(new BufferAttribute(new Uint32Array(totalIndexCount), 1, false));
			forceUpdateIndex = true;

		}

		if (forceUpdateIndex || overwriteIndex) {

			// copy the index data to the target geometry
			let targetOffset = 0;
			let indexOffset = 0;
			const targetIndex = targetGeometry.getIndex();
			for (let i = 0, l = geometries.length; i < l; i++) {

				const geometry = geometries[i];
				const index = geometry.getIndex();
				const skip = !forceUpdate && !forceUpdateIndex && skipAssigningAttributes[i];
				if (!skip) {

					for (let j = 0; j < index.count; ++j) {

						targetIndex.setX(targetOffset + j, index.getX(j) + indexOffset);

					}

				}

				targetOffset += index.count;
				indexOffset += geometry.getAttribute('position').count;

			}

		}

	}

	// copy all the attribute data over
	const attributes = Object.keys(geometries[0].attributes);
	for (let i = 0, l = attributes.length; i < l; i++) {

		let forceUpdateAttr = false;
		const key = attributes[i];
		if (!targetGeometry.getAttribute(key)) {

			const firstAttr = geometries[0].getAttribute(key);
			targetGeometry.setAttribute(key, createAttributeClone(firstAttr, totalAttributeCount));
			forceUpdateAttr = true;

		}

		let offset = 0;
		const targetAttribute = targetGeometry.getAttribute(key);
		for (let g = 0, l = geometries.length; g < l; g++) {

			const geometry = geometries[g];
			const skip = !forceUpdate && !forceUpdateAttr && skipAssigningAttributes[g];
			const attr = geometry.getAttribute(key);
			if (!skip) {

				copyAttributeContents(attr, targetAttribute, offset);

			}

			offset += attr.count;

		}

	}

}

function updateMaterialIndexAttribute (geometry, materials, allMaterials) {

	const indexAttr = geometry.index;
	const posAttr = geometry.attributes.position;
	const vertCount = posAttr.count;
	const totalCount = indexAttr ? indexAttr.count : vertCount;
	let groups = geometry.groups;
	if (groups.length === 0) {

		groups = [{ count: totalCount, start: 0, materialIndex: 0 }];

	}

	let materialIndexAttribute = geometry.getAttribute('materialIndex');
	if (!materialIndexAttribute || materialIndexAttribute.count !== vertCount) {

		// use an array with the minimum precision required to store all material id references.
		let array;
		if (allMaterials.length <= 255) {

			array = new Uint8Array(vertCount);

		} else {

			array = new Uint16Array(vertCount);

		}

		materialIndexAttribute = new BufferAttribute(array, 1, false);
		geometry.deleteAttribute('materialIndex');
		geometry.setAttribute('materialIndex', materialIndexAttribute);

	}

	const materialArray = materialIndexAttribute.array;
	for (let i = 0; i < groups.length; i++) {

		const group = groups[i];
		const start = group.start;
		const count = group.count;
		const endCount = Math.min(count, totalCount - start);

		const mat = Array.isArray(materials) ? materials[group.materialIndex] : materials;
		const materialIndex = allMaterials.indexOf(mat);

		for (let j = 0; j < endCount; j++) {

			let index = start + j;
			if (indexAttr) {

				index = indexAttr.getX(index);

			}

			materialArray[index] = materialIndex;

		}

	}

}

function setCommonAttributes (geometry, attributes) {

	if (!geometry.index) {

		// TODO: compute a typed array
		const indexCount = geometry.attributes.position.count;
		const array = new Array(indexCount);
		for (let i = 0; i < indexCount; i++) {

			array[i] = i;

		}

		geometry.setIndex(array);

	}

	if (!geometry.attributes.normal && (attributes && attributes.includes('normal'))) {

		geometry.computeVertexNormals();

	}

	if (!geometry.attributes.uv && (attributes && attributes.includes('uv'))) {

		const vertCount = geometry.attributes.position.count;
		geometry.setAttribute('uv', new BufferAttribute(new Float32Array(vertCount * 2), 2, false));

	}

	if (!geometry.attributes.uv2 && (attributes && attributes.includes('uv2'))) {

		const vertCount = geometry.attributes.position.count;
		geometry.setAttribute('uv2', new BufferAttribute(new Float32Array(vertCount * 2), 2, false));

	}

	if (!geometry.attributes.tangent && (attributes && attributes.includes('tangent'))) {

		// compute tangents requires a uv and normal buffer
		if (geometry.attributes.uv && geometry.attributes.normal) {

			geometry.computeTangents();

		} else {

			const vertCount = geometry.attributes.position.count;
			geometry.setAttribute('tangent', new BufferAttribute(new Float32Array(vertCount * 4), 4, false));

		}

	}

	if (!geometry.attributes.color && (attributes && attributes.includes('color'))) {

		const vertCount = geometry.attributes.position.count;
		const array = new Float32Array(vertCount * 4);
		array.fill(1.0);
		geometry.setAttribute('color', new BufferAttribute(array, 4));

	}

}

// https://www.geeksforgeeks.org/how-to-create-hash-from-string-in-javascript/
// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
function bufferToHash (buffer) {

	let hash = 0;

	if (buffer.byteLength !== 0) {

		const uintArray = new Uint8Array(buffer);
		for (let i = 0; i < buffer.byteLength; i++) {

			const byte = uintArray[i];
			hash = ((hash << 5) - hash) + byte;
			hash |= 0;

		}

	}

	return hash;

}

function getGeometryHash (geometry) {

	let hash = geometry.uuid;
	const attributes = Object.values(geometry.attributes);
	if (geometry.index) {

		attributes.push(geometry.index);
		hash += `index|${geometry.index.version}`;

	}

	const keys = Object.keys(attributes).sort();
	for (const key of keys) {

		const attr = attributes[key];
		hash += `${key}_${attr.version}|`;

	}

	return hash;

}

function getSkeletonHash (mesh) {

	const skeleton = mesh.skeleton;
	if (skeleton) {

		if (!skeleton.boneTexture) {

			skeleton.computeBoneTexture();

		}

		// we can't use the texture version here because it will change even
		// when the bones haven't
		const dataHash = bufferToHash(skeleton.boneTexture.image.data.buffer);
		return `${dataHash}_${skeleton.boneTexture.uuid}`;

	} else {

		return null;

	}

}

// Checks whether the geometry changed between this and last evaluation
class MeshDiff {

	constructor(mesh = null) {

		this.matrixWorld = new Matrix4();
		this.geometryHash = null;
		this.skeletonHash = null;
		this.primitiveCount = - 1;

		if (mesh !== null) {

			this.updateFrom(mesh);

		}

	}

	updateFrom (mesh) {

		const geometry = mesh.geometry;
		const primitiveCount = (geometry.index ? geometry.index.count : geometry.attributes.position.count) / 3;
		this.matrixWorld.copy(mesh.matrixWorld);
		this.geometryHash = getGeometryHash(geometry);
		this.primitiveCount = primitiveCount;
		this.skeletonHash = getSkeletonHash(mesh);

	}

	didChange (mesh) {

		const geometry = mesh.geometry;
		const primitiveCount = (geometry.index ? geometry.index.count : geometry.attributes.position.count) / 3;

		const identical =
			this.matrixWorld.equals(mesh.matrixWorld) &&
			this.geometryHash === getGeometryHash(geometry) &&
			this.skeletonHash === getSkeletonHash(mesh) &&
			this.primitiveCount === primitiveCount;

		return !identical;

	}

}

const _positionVector = /*@__PURE__*/ new Vector3();
const _normalVector = /*@__PURE__*/ new Vector3();
const _tangentVector = /*@__PURE__*/ new Vector3();
const _tangentVector4 = /*@__PURE__*/ new Vector4();

const _morphVector = /*@__PURE__*/ new Vector3();
const _temp = /*@__PURE__*/ new Vector3();

const _skinIndex = /*@__PURE__*/ new Vector4();
const _skinWeight = /*@__PURE__*/ new Vector4();
const _matrix = /*@__PURE__*/ new Matrix4();
const _boneMatrix = /*@__PURE__*/ new Matrix4();

// A version of "SkinnedMesh.boneTransform" for normals
function boneNormalTransform (mesh, index, target) {

	const skeleton = mesh.skeleton;
	const geometry = mesh.geometry;
	const bones = skeleton.bones;
	const boneInverses = skeleton.boneInverses;

	_skinIndex.fromBufferAttribute(geometry.attributes.skinIndex, index);
	_skinWeight.fromBufferAttribute(geometry.attributes.skinWeight, index);

	_matrix.elements.fill(0);

	for (let i = 0; i < 4; i++) {

		const weight = _skinWeight.getComponent(i);

		if (weight !== 0) {

			const boneIndex = _skinIndex.getComponent(i);
			_boneMatrix.multiplyMatrices(bones[boneIndex].matrixWorld, boneInverses[boneIndex]);

			addScaledMatrix(_matrix, _boneMatrix, weight);

		}

	}

	_matrix.multiply(mesh.bindMatrix).premultiply(mesh.bindMatrixInverse);
	target.transformDirection(_matrix);

	return target;

}

// Applies the morph target data to the target vector
function applyMorphTarget (morphData, morphInfluences, morphTargetsRelative, i, target) {

	_morphVector.set(0, 0, 0);
	for (let j = 0, jl = morphData.length; j < jl; j++) {

		const influence = morphInfluences[j];
		const morphAttribute = morphData[j];

		if (influence === 0) continue;

		_temp.fromBufferAttribute(morphAttribute, i);

		if (morphTargetsRelative) {

			_morphVector.addScaledVector(_temp, influence);

		} else {

			_morphVector.addScaledVector(_temp.sub(target), influence);

		}

	}

	target.add(_morphVector);

}

// Adds the "matrix" multiplied by "scale" to "target"
function addScaledMatrix (target, matrix, scale) {

	const targetArray = target.elements;
	const matrixArray = matrix.elements;
	for (let i = 0, l = matrixArray.length; i < l; i++) {

		targetArray[i] += matrixArray[i] * scale;

	}

}

// inverts the geometry in place
function invertGeometry (geometry) {

	const { index, attributes } = geometry;
	if (index) {

		for (let i = 0, l = index.count; i < l; i += 3) {

			const v0 = index.getX(i);
			const v2 = index.getX(i + 2);
			index.setX(i, v2);
			index.setX(i + 2, v0);

		}

	} else {

		for (const key in attributes) {

			const attr = attributes[key];
			const itemSize = attr.itemSize;
			for (let i = 0, l = attr.count; i < l; i += 3) {

				for (let j = 0; j < itemSize; j++) {

					const v0 = attr.getComponent(i, j);
					const v2 = attr.getComponent(i + 2, j);
					attr.setComponent(i, j, v2);
					attr.setComponent(i + 2, j, v0);

				}

			}

		}

	}

	return geometry;

}

function convertToStaticGeometry (mesh, options = {}, targetGeometry = new BufferGeometry()) {

	options = {
		applyWorldTransforms: true,
		attributes: [],
		...options
	};

	const geometry = mesh.geometry;
	const applyWorldTransforms = options.applyWorldTransforms;
	const includeNormal = options.attributes.includes('normal');
	const includeTangent = options.attributes.includes('tangent');
	const attributes = geometry.attributes;
	const targetAttributes = targetGeometry.attributes;

	// strip any unused and unneeded attributes
	for (const key in targetGeometry.attributes) {

		if (!options.attributes.includes(key) || !(key in geometry.attributes)) {

			targetGeometry.deleteAttribute(key);

		}

	}

	// initialize the attributes if they don't exist
	if (!targetGeometry.index && geometry.index) {

		targetGeometry.index = geometry.index.clone();

	}

	if (!targetAttributes.position) {

		targetGeometry.setAttribute('position', createAttributeClone(attributes.position));

	}

	if (includeNormal && !targetAttributes.normal && attributes.normal) {

		targetGeometry.setAttribute('normal', createAttributeClone(attributes.normal));

	}

	if (includeTangent && !targetAttributes.tangent && attributes.tangent) {

		targetGeometry.setAttribute('tangent', createAttributeClone(attributes.tangent));

	}

	// ensure the attributes are consistent
	validateAttributes(geometry.index, targetGeometry.index);
	validateAttributes(attributes.position, targetAttributes.position);

	if (includeNormal) {

		validateAttributes(attributes.normal, targetAttributes.normal);

	}

	if (includeTangent) {

		validateAttributes(attributes.tangent, targetAttributes.tangent);

	}

	// generate transformed vertex attribute data
	const position = attributes.position;
	const normal = includeNormal ? attributes.normal : null;
	const tangent = includeTangent ? attributes.tangent : null;
	const morphPosition = geometry.morphAttributes.position;
	const morphNormal = geometry.morphAttributes.normal;
	const morphTangent = geometry.morphAttributes.tangent;
	const morphTargetsRelative = geometry.morphTargetsRelative;
	const morphInfluences = mesh.morphTargetInfluences;
	const normalMatrix = new Matrix3();
	normalMatrix.getNormalMatrix(mesh.matrixWorld);

	// copy the index
	if (geometry.index) {

		targetGeometry.index.array.set(geometry.index.array);

	}

	// copy and apply other attributes
	for (let i = 0, l = attributes.position.count; i < l; i++) {

		_positionVector.fromBufferAttribute(position, i);
		if (normal) {

			_normalVector.fromBufferAttribute(normal, i);

		}

		if (tangent) {

			_tangentVector4.fromBufferAttribute(tangent, i);
			_tangentVector.fromBufferAttribute(tangent, i);

		}

		// apply morph target transform
		if (morphInfluences) {

			if (morphPosition) {

				applyMorphTarget(morphPosition, morphInfluences, morphTargetsRelative, i, _positionVector);

			}

			if (morphNormal) {

				applyMorphTarget(morphNormal, morphInfluences, morphTargetsRelative, i, _normalVector);

			}

			if (morphTangent) {

				applyMorphTarget(morphTangent, morphInfluences, morphTargetsRelative, i, _tangentVector);

			}

		}

		// apply bone transform
		if (mesh.isSkinnedMesh) {

			mesh.applyBoneTransform(i, _positionVector);
			if (normal) {

				boneNormalTransform(mesh, i, _normalVector);

			}

			if (tangent) {

				boneNormalTransform(mesh, i, _tangentVector);

			}

		}

		// update the vectors of the attributes
		if (applyWorldTransforms) {

			_positionVector.applyMatrix4(mesh.matrixWorld);

		}

		targetAttributes.position.setXYZ(i, _positionVector.x, _positionVector.y, _positionVector.z);

		if (normal) {

			if (applyWorldTransforms) {

				_normalVector.applyNormalMatrix(normalMatrix);

			}

			targetAttributes.normal.setXYZ(i, _normalVector.x, _normalVector.y, _normalVector.z);

		}

		if (tangent) {

			if (applyWorldTransforms) {

				_tangentVector.transformDirection(mesh.matrixWorld);

			}

			targetAttributes.tangent.setXYZW(i, _tangentVector.x, _tangentVector.y, _tangentVector.z, _tangentVector4.w);

		}

	}

	// copy other attributes over
	for (const i in options.attributes) {

		const key = options.attributes[i];
		if (key === 'position' || key === 'tangent' || key === 'normal' || !(key in attributes)) {

			continue;

		}

		if (!targetAttributes[key]) {

			targetGeometry.setAttribute(key, createAttributeClone(attributes[key]));

		}

		validateAttributes(attributes[key], targetAttributes[key]);
		copyAttributeContents(attributes[key], targetAttributes[key]);

	}

	if (mesh.matrixWorld.determinant() < 0) {

		invertGeometry(targetGeometry);

	}

	return targetGeometry;

}

class BakedGeometry extends BufferGeometry {

	constructor() {

		super();
		this.version = 0;
		this.hash = null;
		this._diff = new MeshDiff();

	}

	// returns whether the passed mesh is compatible with this baked geometry
	// such that it can be updated without resizing attributes
	isCompatible (mesh, attributes) {

		const geometry = mesh.geometry;
		for (let i = 0; i < attributes.length; i++) {

			const key = attributes[i];
			const attr1 = geometry.attributes[key];
			const attr2 = this.attributes[key];
			if (attr1 && !validateAttributes(attr1, attr2)) {

				return false;

			}

		}

		return true;

	}

	updateFrom (mesh, options) {

		const diff = this._diff;
		if (diff.didChange(mesh)) {

			convertToStaticGeometry(mesh, options, this);
			diff.updateFrom(mesh);
			this.version++;
			this.hash = `${this.uuid}_${this.version}`;
			return true;

		} else {

			return false;

		}

	}

}

const NO_CHANGE = 0;
const GEOMETRY_ADJUSTED = 1;
const GEOMETRY_REBUILT = 2;

// iterate over only the meshes in the provided objects
function flatTraverseMeshes (objects, cb) {

	for (let i = 0, l = objects.length; i < l; i++) {

		const object = objects[i];
		object.traverseVisible(o => {

			if (o.isMesh) {

				cb(o);

			}

		});

	}

}

// return the set of materials used by the provided meshes
function getMaterials (meshes) {

	const materials = [];
	for (let i = 0, l = meshes.length; i < l; i++) {

		const mesh = meshes[i];
		if (Array.isArray(mesh.material)) {

			materials.push(...mesh.material);

		} else {

			materials.push(mesh.material);

		}

	}

	return materials;

}

function mergeGeometryList (geometries, target, options) {

	// If we have no geometry to merge then provide an empty geometry.
	if (geometries.length === 0) {

		// if there are no geometries then just create a fake empty geometry to provide
		target.setIndex(null);

		// remove all geometry
		const attrs = target.attributes;
		for (const key in attrs) {

			target.deleteAttribute(key);

		}

		// create dummy attributes
		for (const key in options.attributes) {

			target.setAttribute(options.attributes[key], new BufferAttribute(new Float32Array(0), 4, false));

		}

	} else {

		mergeGeometries(geometries, options, target);

	}

	// Mark all attributes as needing an update
	for (const key in target.attributes) {

		target.attributes[key].needsUpdate = true;

	}

}


class StaticGeometryGenerator {

	constructor(objects) {

		this.objects = null;
		this.useGroups = true;
		this.applyWorldTransforms = true;
		this.generateMissingAttributes = true;
		this.overwriteIndex = true;
		this.attributes = ['position', 'normal', 'color', 'tangent', 'uv', 'uv2'];
		this._intermediateGeometry = new Map();
		this._geometryMergeSets = new WeakMap();
		this._mergeOrder = [];
		this._dummyMesh = null;

		this.setObjects(objects || []);

	}

	_getDummyMesh () {

		// return a consistent dummy mesh
		if (!this._dummyMesh) {

			const dummyMaterial = new MeshBasicMaterial();
			const emptyGeometry = new BufferGeometry();
			emptyGeometry.setAttribute('position', new BufferAttribute(new Float32Array(9), 3));
			this._dummyMesh = new Mesh(emptyGeometry, dummyMaterial);

		}

		return this._dummyMesh;

	}

	_getMeshes () {

		// iterate over only the meshes in the provided objects
		const meshes = [];
		flatTraverseMeshes(this.objects, mesh => {

			meshes.push(mesh);

		});

		// Sort the geometry so it's in a reliable order
		meshes.sort((a, b) => {

			if (a.uuid > b.uuid) return 1;
			if (a.uuid < b.uuid) return - 1;
			return 0;

		});

		if (meshes.length === 0) {

			meshes.push(this._getDummyMesh());

		}

		return meshes;

	}

	_updateIntermediateGeometries () {

		const { _intermediateGeometry } = this;

		const meshes = this._getMeshes();
		const unusedMeshKeys = new Set(_intermediateGeometry.keys());
		const convertOptions = {
			attributes: this.attributes,
			applyWorldTransforms: this.applyWorldTransforms,
		};

		for (let i = 0, l = meshes.length; i < l; i++) {

			const mesh = meshes[i];
			const meshKey = mesh.uuid;
			unusedMeshKeys.delete(meshKey);

			// initialize the intermediate geometry
			// if the mesh and source geometry have changed in such a way that they are no longer
			// compatible then regenerate the baked geometry from scratch
			let geom = _intermediateGeometry.get(meshKey);
			if (!geom || !geom.isCompatible(mesh, this.attributes)) {

				if (geom) {

					geom.dispose();

				}

				geom = new BakedGeometry();
				_intermediateGeometry.set(meshKey, geom);

			}

			// transform the geometry into the intermediate buffer geometry, saving whether
			// or not it changed.
			if (geom.updateFrom(mesh, convertOptions)) {

				// TODO: provide option for only generating the set of attributes that are present
				// and are in the attributes array
				if (this.generateMissingAttributes) {

					setCommonAttributes(geom, this.attributes);

				}

			}

		}

		unusedMeshKeys.forEach(key => {

			_intermediateGeometry.delete(key);

		});

	}

	setObjects (objects) {

		if (Array.isArray(objects)) {

			this.objects = [...objects];

		} else {

			this.objects = [objects];

		}

	}

	generate (targetGeometry = new BufferGeometry()) {

		// track which attributes have been updated and which to skip to avoid unnecessary attribute copies
		const { useGroups, overwriteIndex, _intermediateGeometry, _geometryMergeSets } = this;

		const meshes = this._getMeshes();
		const skipAssigningAttributes = [];
		const mergeGeometry = [];
		const previousMergeInfo = _geometryMergeSets.get(targetGeometry) || [];

		// update all the intermediate static geometry representations
		this._updateIntermediateGeometries();

		// get the list of geometries to merge
		let forceUpdate = false;
		if (meshes.length !== previousMergeInfo.length) {

			forceUpdate = true;

		}

		for (let i = 0, l = meshes.length; i < l; i++) {

			const mesh = meshes[i];
			const geom = _intermediateGeometry.get(mesh.uuid);
			mergeGeometry.push(geom);

			const info = previousMergeInfo[i];
			if (!info || info.uuid !== geom.uuid) {

				skipAssigningAttributes.push(false);
				forceUpdate = true;

			} else if (info.version !== geom.version) {

				skipAssigningAttributes.push(false);

			} else {

				skipAssigningAttributes.push(true);

			}

		}

		// If we have no geometry to merge then provide an empty geometry.
		mergeGeometryList(mergeGeometry, targetGeometry, { useGroups, forceUpdate, skipAssigningAttributes, overwriteIndex });

		// force update means the attribute buffer lengths have changed
		if (forceUpdate) {

			targetGeometry.dispose();

		}

		_geometryMergeSets.set(targetGeometry, mergeGeometry.map(g => ({
			version: g.version,
			uuid: g.uuid,
		})));

		let changeType = NO_CHANGE;
		if (forceUpdate) changeType = GEOMETRY_REBUILT;
		else if (skipAssigningAttributes.includes(false)) changeType = GEOMETRY_ADJUSTED;

		return {
			changeType,
			materials: getMaterials(meshes),
			geometry: targetGeometry,
		};

	}

}

// collect the textures from the materials
function getTextures$1 (materials) {

	const textureSet = new Set();
	for (let i = 0, l = materials.length; i < l; i++) {

		const material = materials[i];
		for (const key in material) {

			const value = material[key];
			if (value && value.isTexture) {

				textureSet.add(value);

			}

		}

	}

	return Array.from(textureSet);

}

// collect the lights in the scene
function getLights$1 (objects) {

	const lights = [];
	const iesSet = new Set();
	for (let i = 0, l = objects.length; i < l; i++) {

		objects[i].traverse(c => {

			if (c.visible) {

				if (
					c.isRectAreaLight ||
					c.isSpotLight ||
					c.isPointLight ||
					c.isDirectionalLight
				) {

					lights.push(c);

					if (c.iesMap) {

						iesSet.add(c.iesMap);

					}

				}

			}

		});

	}

	const iesTextures = Array.from(iesSet).sort((a, b) => {

		if (a.uuid < b.uuid) return 1;
		if (a.uuid > b.uuid) return - 1;
		return 0;

	});

	return { lights, iesTextures };

}

class PathTracingSceneGenerator {

	get initialized () {

		return Boolean(this.bvh);

	}

	constructor(objects) {

		// options
		this.bvhOptions = {};
		this.attributes = ['position', 'normal', 'tangent', 'color', 'uv', 'uv2'];
		this.generateBVH = true;

		// state
		this.bvh = null;
		this.geometry = new BufferGeometry();
		this.staticGeometryGenerator = new StaticGeometryGenerator(objects);
		this._bvhWorker = null;
		this._pendingGenerate = null;
		this._buildAsync = false;

	}

	setObjects (objects) {

		this.staticGeometryGenerator.setObjects(objects);

	}

	setBVHWorker (bvhWorker) {

		this._bvhWorker = bvhWorker;

	}

	async generateAsync (onProgress = null) {

		if (!this._bvhWorker) {

			throw new Error('PathTracingSceneGenerator: "setBVHWorker" must be called before "generateAsync" can be called.');

		}

		if (this.bvh instanceof Promise) {

			// if a bvh is already being generated we can wait for that to finish
			// and build another with the latest data while sharing the results.
			if (!this._pendingGenerate) {

				this._pendingGenerate = new Promise(async () => {

					await this.bvh;
					this._pendingGenerate = null;

					// TODO: support multiple callbacks queued?
					return this.generateAsync(onProgress);

				});

			}

			return this._pendingGenerate;

		} else {

			this._buildAsync = true;
			const result = this.generate(onProgress);
			this._buildAsync = false;

			result.bvh = this.bvh = await result.bvh;
			return result;

		}

	}

	generate (onProgress = null) {

		const { staticGeometryGenerator, geometry, attributes } = this;
		const objects = staticGeometryGenerator.objects;
		staticGeometryGenerator.attributes = attributes;

		// update the skeleton animations in case WebGLRenderer is not running
		// to update it.
		objects.forEach(o => {

			o.traverse(c => {

				if (c.isSkinnedMesh && c.skeleton) {

					c.skeleton.update();

				}

			});

		});

		// generate the geometry
		const result = staticGeometryGenerator.generate(geometry);
		const materials = result.materials;
		const textures = getTextures$1(materials);
		const { lights, iesTextures } = getLights$1(objects);

		if (result.changeType !== NO_CHANGE) {

			updateMaterialIndexAttribute(geometry, materials, materials);

		}

		// only generate a new bvh if the objects used have changed
		if (this.generateBVH) {

			if (this.bvh instanceof Promise) {

				throw new Error('PathTracingSceneGenerator: BVH is already building asynchronously.');

			}

			if (result.changeType === GEOMETRY_REBUILT) {

				const bvhOptions = {
					strategy: SAH,
					maxLeafTris: 1,
					indirect: true,
					onProgress,
					...this.bvhOptions,
				};

				if (this._buildAsync) {

					this.bvh = this._bvhWorker.generate(geometry, bvhOptions);

				} else {

					this.bvh = new MeshBVH(geometry, bvhOptions);

				}

			} else if (result.changeType === GEOMETRY_ADJUSTED) {

				this.bvh.refit();

			}

		}

		return {
			bvhChanged: result.changeType !== NO_CHANGE,
			bvh: this.bvh,
			lights,
			iesTextures,
			geometry,
			materials,
			textures,
			objects,
		};

	}

}

class DynamicPathTracingSceneGenerator extends PathTracingSceneGenerator {

	constructor(...args) {

		super(...args);
		console.warn('DynamicPathTracingSceneGenerator has been deprecated and renamed to "PathTracingSceneGenerator".');

	}

}

class PathTracingSceneWorker extends PathTracingSceneGenerator {

	constructor(...args) {

		super(...args);
		console.warn('PathTracingSceneWorker has been deprecated and renamed to "PathTracingSceneGenerator".');

	}

}

class MaterialBase extends ShaderMaterial {

	set needsUpdate (v) {

		super.needsUpdate = true;
		this.dispatchEvent({

			type: 'recompilation',

		});

	}

	constructor(shader) {

		super(shader);

		for (const key in this.uniforms) {

			Object.defineProperty(this, key, {

				get () {

					return this.uniforms[key].value;

				},

				set (v) {

					this.uniforms[key].value = v;

				}

			});

		}

	}

	// sets the given named define value and sets "needsUpdate" to true if it's different
	setDefine (name, value = undefined) {

		if (value === undefined || value === null) {

			if (name in this.defines) {

				delete this.defines[name];
				this.needsUpdate = true;
				return true;

			}

		} else {

			if (this.defines[name] !== value) {

				this.defines[name] = value;
				this.needsUpdate = true;
				return true;

			}

		}

		return false;

	}

}

class BlendMaterial extends MaterialBase {

	constructor(parameters) {

		super({

			blending: NoBlending,

			uniforms: {

				target1: { value: null },
				target2: { value: null },
				opacity: { value: 1.0 },

			},

			vertexShader: /* glsl */`

				varying vec2 vUv;

				void main() {

					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

				}`,

			fragmentShader: /* glsl */`

				uniform float opacity;

				uniform sampler2D target1;
				uniform sampler2D target2;

				varying vec2 vUv;

				void main() {

					vec4 color1 = texture2D( target1, vUv );
					vec4 color2 = texture2D( target2, vUv );

					float invOpacity = 1.0 - opacity;
					float totalAlpha = color1.a * invOpacity + color2.a * opacity;

					if ( color1.a != 0.0 || color2.a != 0.0 ) {

						gl_FragColor.rgb = color1.rgb * ( invOpacity * color1.a / totalAlpha ) + color2.rgb * ( opacity * color2.a / totalAlpha );
						gl_FragColor.a = totalAlpha;

					} else {

						gl_FragColor = vec4( 0.0 );

					}

				}`

		});

		this.setValues(parameters);

	}

}

// References
// - https://jcgt.org/published/0009/04/01/
// - Code from https://www.shadertoy.com/view/WtGyDm

// functions to generate multi-dimensions variables of the same functions
// to support 1, 2, 3, and 4 dimensional sobol sampling.
function generateSobolFunctionVariants (dim = 1) {

	let type = 'uint';
	if (dim > 1) {

		type = 'uvec' + dim;

	}

	return /* glsl */`
		${type} sobolReverseBits( ${type} x ) {

			x = ( ( ( x & 0xaaaaaaaau ) >> 1 ) | ( ( x & 0x55555555u ) << 1 ) );
			x = ( ( ( x & 0xccccccccu ) >> 2 ) | ( ( x & 0x33333333u ) << 2 ) );
			x = ( ( ( x & 0xf0f0f0f0u ) >> 4 ) | ( ( x & 0x0f0f0f0fu ) << 4 ) );
			x = ( ( ( x & 0xff00ff00u ) >> 8 ) | ( ( x & 0x00ff00ffu ) << 8 ) );
			return ( ( x >> 16 ) | ( x << 16 ) );

		}

		${type} sobolHashCombine( uint seed, ${type} v ) {

			return seed ^ ( v + ${type}( ( seed << 6 ) + ( seed >> 2 ) ) );

		}

		${type} sobolLaineKarrasPermutation( ${type} x, ${type} seed ) {

			x += seed;
			x ^= x * 0x6c50b47cu;
			x ^= x * 0xb82f1e52u;
			x ^= x * 0xc7afe638u;
			x ^= x * 0x8d22f6e6u;
			return x;

		}

		${type} nestedUniformScrambleBase2( ${type} x, ${type} seed ) {

			x = sobolLaineKarrasPermutation( x, seed );
			x = sobolReverseBits( x );
			return x;

		}
	`;

}

function generateSobolSampleFunctions (dim = 1) {

	let utype = 'uint';
	let vtype = 'float';
	let num = '';
	let components = '.r';
	let combineValues = '1u';
	if (dim > 1) {

		utype = 'uvec' + dim;
		vtype = 'vec' + dim;
		num = dim + '';
		if (dim === 2) {

			components = '.rg';
			combineValues = 'uvec2( 1u, 2u )';

		} else if (dim === 3) {

			components = '.rgb';
			combineValues = 'uvec3( 1u, 2u, 3u )';

		} else {

			components = '';
			combineValues = 'uvec4( 1u, 2u, 3u, 4u )';

		}

	}

	return /* glsl */`

		${vtype} sobol${num}( int effect ) {

			uint seed = sobolGetSeed( sobolBounceIndex, uint( effect ) );
			uint index = sobolPathIndex;

			uint shuffle_seed = sobolHashCombine( seed, 0u );
			uint shuffled_index = nestedUniformScrambleBase2( sobolReverseBits( index ), shuffle_seed );
			${vtype} sobol_pt = sobolGetTexturePoint( shuffled_index )${components};
			${utype} result = ${utype}( sobol_pt * 16777216.0 );

			${utype} seed2 = sobolHashCombine( seed, ${combineValues} );
			result = nestedUniformScrambleBase2( result, seed2 );

			return SOBOL_FACTOR * ${vtype}( result >> 8 );

		}
	`;

}

const sobol_common = /* glsl */`

	// Utils
	const float SOBOL_FACTOR = 1.0 / 16777216.0;
	const uint SOBOL_MAX_POINTS = 256u * 256u;

	${generateSobolFunctionVariants(1)}
	${generateSobolFunctionVariants(2)}
	${generateSobolFunctionVariants(3)}
	${generateSobolFunctionVariants(4)}

	uint sobolHash( uint x ) {

		// finalizer from murmurhash3
		x ^= x >> 16;
		x *= 0x85ebca6bu;
		x ^= x >> 13;
		x *= 0xc2b2ae35u;
		x ^= x >> 16;
		return x;

	}

`;

const sobol_point_generation = /* glsl */`

	const uint SOBOL_DIRECTIONS_1[ 32 ] = uint[ 32 ](
		0x80000000u, 0xc0000000u, 0xa0000000u, 0xf0000000u,
		0x88000000u, 0xcc000000u, 0xaa000000u, 0xff000000u,
		0x80800000u, 0xc0c00000u, 0xa0a00000u, 0xf0f00000u,
		0x88880000u, 0xcccc0000u, 0xaaaa0000u, 0xffff0000u,
		0x80008000u, 0xc000c000u, 0xa000a000u, 0xf000f000u,
		0x88008800u, 0xcc00cc00u, 0xaa00aa00u, 0xff00ff00u,
		0x80808080u, 0xc0c0c0c0u, 0xa0a0a0a0u, 0xf0f0f0f0u,
		0x88888888u, 0xccccccccu, 0xaaaaaaaau, 0xffffffffu
	);

	const uint SOBOL_DIRECTIONS_2[ 32 ] = uint[ 32 ](
		0x80000000u, 0xc0000000u, 0x60000000u, 0x90000000u,
		0xe8000000u, 0x5c000000u, 0x8e000000u, 0xc5000000u,
		0x68800000u, 0x9cc00000u, 0xee600000u, 0x55900000u,
		0x80680000u, 0xc09c0000u, 0x60ee0000u, 0x90550000u,
		0xe8808000u, 0x5cc0c000u, 0x8e606000u, 0xc5909000u,
		0x6868e800u, 0x9c9c5c00u, 0xeeee8e00u, 0x5555c500u,
		0x8000e880u, 0xc0005cc0u, 0x60008e60u, 0x9000c590u,
		0xe8006868u, 0x5c009c9cu, 0x8e00eeeeu, 0xc5005555u
	);

	const uint SOBOL_DIRECTIONS_3[ 32 ] = uint[ 32 ](
		0x80000000u, 0xc0000000u, 0x20000000u, 0x50000000u,
		0xf8000000u, 0x74000000u, 0xa2000000u, 0x93000000u,
		0xd8800000u, 0x25400000u, 0x59e00000u, 0xe6d00000u,
		0x78080000u, 0xb40c0000u, 0x82020000u, 0xc3050000u,
		0x208f8000u, 0x51474000u, 0xfbea2000u, 0x75d93000u,
		0xa0858800u, 0x914e5400u, 0xdbe79e00u, 0x25db6d00u,
		0x58800080u, 0xe54000c0u, 0x79e00020u, 0xb6d00050u,
		0x800800f8u, 0xc00c0074u, 0x200200a2u, 0x50050093u
	);

	const uint SOBOL_DIRECTIONS_4[ 32 ] = uint[ 32 ](
		0x80000000u, 0x40000000u, 0x20000000u, 0xb0000000u,
		0xf8000000u, 0xdc000000u, 0x7a000000u, 0x9d000000u,
		0x5a800000u, 0x2fc00000u, 0xa1600000u, 0xf0b00000u,
		0xda880000u, 0x6fc40000u, 0x81620000u, 0x40bb0000u,
		0x22878000u, 0xb3c9c000u, 0xfb65a000u, 0xddb2d000u,
		0x78022800u, 0x9c0b3c00u, 0x5a0fb600u, 0x2d0ddb00u,
		0xa2878080u, 0xf3c9c040u, 0xdb65a020u, 0x6db2d0b0u,
		0x800228f8u, 0x400b3cdcu, 0x200fb67au, 0xb00ddb9du
	);

	uint getMaskedSobol( uint index, uint directions[ 32 ] ) {

		uint X = 0u;
		for ( int bit = 0; bit < 32; bit ++ ) {

			uint mask = ( index >> bit ) & 1u;
			X ^= mask * directions[ bit ];

		}
		return X;

	}

	vec4 generateSobolPoint( uint index ) {

		if ( index >= SOBOL_MAX_POINTS ) {

			return vec4( 0.0 );

		}

		// NOTE: this sobol "direction" is also available but we can't write out 5 components
		// uint x = index & 0x00ffffffu;
		uint x = sobolReverseBits( getMaskedSobol( index, SOBOL_DIRECTIONS_1 ) ) & 0x00ffffffu;
		uint y = sobolReverseBits( getMaskedSobol( index, SOBOL_DIRECTIONS_2 ) ) & 0x00ffffffu;
		uint z = sobolReverseBits( getMaskedSobol( index, SOBOL_DIRECTIONS_3 ) ) & 0x00ffffffu;
		uint w = sobolReverseBits( getMaskedSobol( index, SOBOL_DIRECTIONS_4 ) ) & 0x00ffffffu;

		return vec4( x, y, z, w ) * SOBOL_FACTOR;

	}

`;

const sobol_functions = /* glsl */`

	// Seeds
	uniform sampler2D sobolTexture;
	uint sobolPixelIndex = 0u;
	uint sobolPathIndex = 0u;
	uint sobolBounceIndex = 0u;

	uint sobolGetSeed( uint bounce, uint effect ) {

		return sobolHash(
			sobolHashCombine(
				sobolHashCombine(
					sobolHash( bounce ),
					sobolPixelIndex
				),
				effect
			)
		);

	}

	vec4 sobolGetTexturePoint( uint index ) {

		if ( index >= SOBOL_MAX_POINTS ) {

			index = index % SOBOL_MAX_POINTS;

		}

		uvec2 dim = uvec2( textureSize( sobolTexture, 0 ).xy );
		uint y = index / dim.x;
		uint x = index - y * dim.x;
		vec2 uv = vec2( x, y ) / vec2( dim );
		return texture( sobolTexture, uv );

	}

	${generateSobolSampleFunctions(1)}
	${generateSobolSampleFunctions(2)}
	${generateSobolSampleFunctions(3)}
	${generateSobolSampleFunctions(4)}

`;

class SobolNumbersMaterial extends MaterialBase {

	constructor() {

		super({

			blending: NoBlending,

			uniforms: {

				resolution: { value: new Vector2() },

			},

			vertexShader: /* glsl */`

				varying vec2 vUv;
				void main() {

					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

				}
			`,

			fragmentShader: /* glsl */`

				${sobol_common}
				${sobol_point_generation}

				varying vec2 vUv;
				uniform vec2 resolution;
				void main() {

					uint index = uint( gl_FragCoord.y ) * uint( resolution.x ) + uint( gl_FragCoord.x );
					gl_FragColor = generateSobolPoint( index );

				}
			`,

		});

	}

}

class SobolNumberMapGenerator {

	generate (renderer, dimensions = 256) {

		const target = new WebGLRenderTarget(dimensions, dimensions, {

			type: FloatType,
			format: RGBAFormat,
			minFilter: NearestFilter,
			magFilter: NearestFilter,
			generateMipmaps: false,

		});

		const ogTarget = renderer.getRenderTarget();
		renderer.setRenderTarget(target);

		const quad = new FullScreenQuad(new SobolNumbersMaterial());
		quad.material.resolution.set(dimensions, dimensions);
		quad.render(renderer);

		renderer.setRenderTarget(ogTarget);
		quad.dispose();

		return target;

	}

}

class PhysicalCamera extends PerspectiveCamera {

	set bokehSize (size) {

		this.fStop = this.getFocalLength() / size;

	}

	get bokehSize () {

		return this.getFocalLength() / this.fStop;

	}

	constructor(...args) {

		super(...args);
		this.fStop = 1.4;
		this.apertureBlades = 0;
		this.apertureRotation = 0;
		this.focusDistance = 25;
		this.anamorphicRatio = 1;

	}

	copy (source, recursive) {

		super.copy(source, recursive);

		this.fStop = source.fStop;
		this.apertureBlades = source.apertureBlades;
		this.apertureRotation = source.apertureRotation;
		this.focusDistance = source.focusDistance;
		this.anamorphicRatio = source.anamorphicRatio;

		return this;

	}

}

class PhysicalCameraUniform {

	constructor() {

		this.bokehSize = 0;
		this.apertureBlades = 0;
		this.apertureRotation = 0;
		this.focusDistance = 10;
		this.anamorphicRatio = 1;

	}

	updateFrom (camera) {

		if (camera instanceof PhysicalCamera) {

			this.bokehSize = camera.bokehSize;
			this.apertureBlades = camera.apertureBlades;
			this.apertureRotation = camera.apertureRotation;
			this.focusDistance = camera.focusDistance;
			this.anamorphicRatio = camera.anamorphicRatio;

		} else {

			this.bokehSize = 0;
			this.apertureRotation = 0;
			this.apertureBlades = 0;
			this.focusDistance = 10;
			this.anamorphicRatio = 1;

		}

	}

}

function toHalfFloatArray (f32Array) {

	const f16Array = new Uint16Array(f32Array.length);
	for (let i = 0, n = f32Array.length; i < n; ++i) {

		f16Array[i] = DataUtils.toHalfFloat(f32Array[i]);

	}

	return f16Array;

}

function binarySearchFindClosestIndexOf (array, targetValue, offset = 0, count = array.length) {

	let lower = offset;
	let upper = offset + count - 1;

	while (lower < upper) {

		// calculate the midpoint for this iteration using a bitwise shift right operator to save 1 floating point multiplication
		// and 1 truncation from the double tilde operator to improve performance
		// this results in much better performance over using standard "~ ~ ( (lower + upper) ) / 2" to calculate the midpoint
		const mid = (lower + upper) >> 1;

		// check if the middle array value is above or below the target and shift
		// which half of the array we're looking at
		if (array[mid] < targetValue) {

			lower = mid + 1;

		} else {

			upper = mid;

		}

	}

	return lower - offset;

}

function colorToLuminance (r, g, b) {

	// https://en.wikipedia.org/wiki/Relative_luminance
	return 0.2126 * r + 0.7152 * g + 0.0722 * b;

}

// ensures the data is all floating point values and flipY is false
function preprocessEnvMap (envMap, targetType = HalfFloatType) {

	const map = envMap.clone();
	map.source = new Source({ ...map.image });
	const { width, height, data } = map.image;

	// TODO: is there a simple way to avoid cloning and adjusting the env map data here?
	// convert the data from half float uint 16 arrays to float arrays for cdf computation
	let newData = data;
	if (map.type !== targetType) {

		if (targetType === HalfFloatType) {

			newData = new Uint16Array(data.length);

		} else {

			newData = new Float32Array(data.length);

		}

		let maxIntValue;
		if (data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array) {

			maxIntValue = 2 ** (8 * data.BYTES_PER_ELEMENT - 1) - 1;

		} else {

			maxIntValue = 2 ** (8 * data.BYTES_PER_ELEMENT) - 1;

		}

		for (let i = 0, l = data.length; i < l; i++) {

			let v = data[i];
			if (map.type === HalfFloatType) {

				v = DataUtils.fromHalfFloat(data[i]);

			}

			if (map.type !== FloatType && map.type !== HalfFloatType) {

				v /= maxIntValue;

			}

			if (targetType === HalfFloatType) {

				newData[i] = DataUtils.toHalfFloat(v);

			}

		}

		map.image.data = newData;
		map.type = targetType;

	}

	// remove any y flipping for cdf computation
	if (map.flipY) {

		const ogData = newData;
		newData = newData.slice();
		for (let y = 0; y < height; y++) {

			for (let x = 0; x < width; x++) {

				const newY = height - y - 1;
				const ogIndex = 4 * (y * width + x);
				const newIndex = 4 * (newY * width + x);

				newData[newIndex + 0] = ogData[ogIndex + 0];
				newData[newIndex + 1] = ogData[ogIndex + 1];
				newData[newIndex + 2] = ogData[ogIndex + 2];
				newData[newIndex + 3] = ogData[ogIndex + 3];

			}

		}

		map.flipY = false;
		map.image.data = newData;

	}

	return map;

}

class EquirectHdrInfoUniform {

	constructor() {

		// Default to a white texture and associated weights so we don't
		// just render black initially.
		const blackTex = new DataTexture(toHalfFloatArray(new Float32Array([0, 0, 0, 0])), 1, 1);
		blackTex.type = HalfFloatType;
		blackTex.format = RGBAFormat;
		blackTex.minFilter = LinearFilter;
		blackTex.magFilter = LinearFilter;
		blackTex.wrapS = RepeatWrapping;
		blackTex.wrapT = RepeatWrapping;
		blackTex.generateMipmaps = false;
		blackTex.needsUpdate = true;

		// Stores a map of [0, 1] value -> cumulative importance row & pdf
		// used to sampling a random value to a relevant row to sample from
		const marginalWeights = new DataTexture(toHalfFloatArray(new Float32Array([0, 1])), 1, 2);
		marginalWeights.type = HalfFloatType;
		marginalWeights.format = RedFormat;
		marginalWeights.minFilter = LinearFilter;
		marginalWeights.magFilter = LinearFilter;
		marginalWeights.generateMipmaps = false;
		marginalWeights.needsUpdate = true;

		// Stores a map of [0, 1] value -> cumulative importance column & pdf
		// used to sampling a random value to a relevant pixel to sample from
		const conditionalWeights = new DataTexture(toHalfFloatArray(new Float32Array([0, 0, 1, 1])), 2, 2);
		conditionalWeights.type = HalfFloatType;
		conditionalWeights.format = RedFormat;
		conditionalWeights.minFilter = LinearFilter;
		conditionalWeights.magFilter = LinearFilter;
		conditionalWeights.generateMipmaps = false;
		conditionalWeights.needsUpdate = true;

		this.map = blackTex;
		this.marginalWeights = marginalWeights;
		this.conditionalWeights = conditionalWeights;
		this.totalSum = 0;

		// TODO: Add support for float or half float types here. We need to pass this into
		// the preprocess function and ensure our CDF and MDF textures are appropriately sized
		// Ideally we wouldn't upscale a bit depth if we didn't need to.
		// this.type = HalfFloatType;

	}

	dispose () {

		this.marginalWeights.dispose();
		this.conditionalWeights.dispose();
		this.map.dispose();

	}

	updateFrom (hdr) {

		// https://github.com/knightcrawler25/GLSL-PathTracer/blob/3c6fd9b6b3da47cd50c527eeb45845eef06c55c3/src/loaders/hdrloader.cpp
		// https://pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Sampling_Light_Sources#InfiniteAreaLights
		const map = preprocessEnvMap(hdr);
		map.wrapS = RepeatWrapping;
		map.wrapT = ClampToEdgeWrapping;

		const { width, height, data } = map.image;

		// "conditional" = "pixel relative to row pixels sum"
		// "marginal" = "row relative to row sum"

		// track the importance of any given pixel in the image by tracking its weight relative to other pixels in the image
		const pdfConditional = new Float32Array(width * height);
		const cdfConditional = new Float32Array(width * height);

		const pdfMarginal = new Float32Array(height);
		const cdfMarginal = new Float32Array(height);

		let totalSumValue = 0.0;
		let cumulativeWeightMarginal = 0.0;
		for (let y = 0; y < height; y++) {

			let cumulativeRowWeight = 0.0;
			for (let x = 0; x < width; x++) {

				const i = y * width + x;
				const r = DataUtils.fromHalfFloat(data[4 * i + 0]);
				const g = DataUtils.fromHalfFloat(data[4 * i + 1]);
				const b = DataUtils.fromHalfFloat(data[4 * i + 2]);

				// the probability of the pixel being selected in this row is the
				// scale of the luminance relative to the rest of the pixels.
				// TODO: this should also account for the solid angle of the pixel when sampling
				const weight = colorToLuminance(r, g, b);
				cumulativeRowWeight += weight;
				totalSumValue += weight;

				pdfConditional[i] = weight;
				cdfConditional[i] = cumulativeRowWeight;

			}

			// can happen if the row is all black
			if (cumulativeRowWeight !== 0) {

				// scale the pdf and cdf to [0.0, 1.0]
				for (let i = y * width, l = y * width + width; i < l; i++) {

					pdfConditional[i] /= cumulativeRowWeight;
					cdfConditional[i] /= cumulativeRowWeight;

				}

			}

			cumulativeWeightMarginal += cumulativeRowWeight;

			// compute the marginal pdf and cdf along the height of the map.
			pdfMarginal[y] = cumulativeRowWeight;
			cdfMarginal[y] = cumulativeWeightMarginal;

		}

		// can happen if the texture is all black
		if (cumulativeWeightMarginal !== 0) {

			// scale the marginal pdf and cdf to [0.0, 1.0]
			for (let i = 0, l = pdfMarginal.length; i < l; i++) {

				pdfMarginal[i] /= cumulativeWeightMarginal;
				cdfMarginal[i] /= cumulativeWeightMarginal;

			}

		}

		// compute a sorted index of distributions and the probabilities along them for both
		// the marginal and conditional data. These will be used to sample with a random number
		// to retrieve a uv value to sample in the environment map.
		// These values continually increase so it's okay to interpolate between them.
		const marginalDataArray = new Uint16Array(height);
		const conditionalDataArray = new Uint16Array(width * height);

		// we add a half texel offset so we're sampling the center of the pixel
		for (let i = 0; i < height; i++) {

			const dist = (i + 1) / height;
			const row = binarySearchFindClosestIndexOf(cdfMarginal, dist);

			marginalDataArray[i] = DataUtils.toHalfFloat((row + 0.5) / height);

		}

		for (let y = 0; y < height; y++) {

			for (let x = 0; x < width; x++) {

				const i = y * width + x;
				const dist = (x + 1) / width;
				const col = binarySearchFindClosestIndexOf(cdfConditional, dist, y * width, width);

				conditionalDataArray[i] = DataUtils.toHalfFloat((col + 0.5) / width);

			}

		}

		this.dispose();

		const { marginalWeights, conditionalWeights } = this;
		marginalWeights.image = { width: height, height: 1, data: marginalDataArray };
		marginalWeights.needsUpdate = true;

		conditionalWeights.image = { width, height, data: conditionalDataArray };
		conditionalWeights.needsUpdate = true;

		this.totalSum = totalSumValue;
		this.map = map;

	}

}

const LIGHT_PIXELS = 6;
const RECT_AREA_LIGHT = 0;
const CIRC_AREA_LIGHT = 1;
const SPOT_LIGHT = 2;
const DIR_LIGHT = 3;
const POINT_LIGHT = 4;

const u = new Vector3();
const v = new Vector3();
const m = new Matrix4();
const worldQuaternion = new Quaternion();
const eye = new Vector3();
const target = new Vector3();
const up = new Vector3(0, 1, 0);
class LightsInfoUniformStruct {

	constructor() {

		const tex = new DataTexture(new Float32Array(4), 1, 1);
		tex.format = RGBAFormat;
		tex.type = FloatType;
		tex.wrapS = ClampToEdgeWrapping;
		tex.wrapT = ClampToEdgeWrapping;
		tex.generateMipmaps = false;
		tex.minFilter = NearestFilter;
		tex.magFilter = NearestFilter;

		this.tex = tex;
		this.count = 0;

	}

	updateFrom (lights, iesTextures = []) {

		const tex = this.tex;
		const pixelCount = Math.max(lights.length * LIGHT_PIXELS, 1);
		const dimension = Math.ceil(Math.sqrt(pixelCount));

		if (tex.image.width !== dimension) {

			tex.dispose();

			tex.image.data = new Float32Array(dimension * dimension * 4);
			tex.image.width = dimension;
			tex.image.height = dimension;

		}

		const floatArray = tex.image.data;

		for (let i = 0, l = lights.length; i < l; i++) {

			const l = lights[i];

			const baseIndex = i * LIGHT_PIXELS * 4;
			let index = 0;

			// initialize to 0
			for (let p = 0; p < LIGHT_PIXELS * 4; p++) {

				floatArray[baseIndex + p] = 0;

			}

			// sample 1
			// position
			l.getWorldPosition(v);
			floatArray[baseIndex + (index++)] = v.x;
			floatArray[baseIndex + (index++)] = v.y;
			floatArray[baseIndex + (index++)] = v.z;

			// type
			let type = RECT_AREA_LIGHT;
			if (l.isRectAreaLight && l.isCircular) {

				type = CIRC_AREA_LIGHT;

			} else if (l.isSpotLight) {

				type = SPOT_LIGHT;

			} else if (l.isDirectionalLight) {

				type = DIR_LIGHT;

			} else if (l.isPointLight) {

				type = POINT_LIGHT;

			}

			floatArray[baseIndex + (index++)] = type;

			// sample 2
			// color
			floatArray[baseIndex + (index++)] = l.color.r;
			floatArray[baseIndex + (index++)] = l.color.g;
			floatArray[baseIndex + (index++)] = l.color.b;

			// intensity
			floatArray[baseIndex + (index++)] = l.intensity;

			l.getWorldQuaternion(worldQuaternion);

			if (l.isRectAreaLight) {

				// sample 3
				// u vector
				u.set(l.width, 0, 0).applyQuaternion(worldQuaternion);

				floatArray[baseIndex + (index++)] = u.x;
				floatArray[baseIndex + (index++)] = u.y;
				floatArray[baseIndex + (index++)] = u.z;
				index++;

				// sample 4
				// v vector
				v.set(0, l.height, 0).applyQuaternion(worldQuaternion);

				floatArray[baseIndex + (index++)] = v.x;
				floatArray[baseIndex + (index++)] = v.y;
				floatArray[baseIndex + (index++)] = v.z;

				// area
				floatArray[baseIndex + (index++)] = u.cross(v).length() * (l.isCircular ? (Math.PI / 4.0) : 1.0);

			} else if (l.isSpotLight) {

				const radius = l.radius || 0;
				eye.setFromMatrixPosition(l.matrixWorld);
				target.setFromMatrixPosition(l.target.matrixWorld);
				m.lookAt(eye, target, up);
				worldQuaternion.setFromRotationMatrix(m);

				// sample 3
				// u vector
				u.set(1, 0, 0).applyQuaternion(worldQuaternion);

				floatArray[baseIndex + (index++)] = u.x;
				floatArray[baseIndex + (index++)] = u.y;
				floatArray[baseIndex + (index++)] = u.z;
				index++;

				// sample 4
				// v vector
				v.set(0, 1, 0).applyQuaternion(worldQuaternion);

				floatArray[baseIndex + (index++)] = v.x;
				floatArray[baseIndex + (index++)] = v.y;
				floatArray[baseIndex + (index++)] = v.z;

				// area
				floatArray[baseIndex + (index++)] = Math.PI * radius * radius;

				// sample 5
				// radius
				floatArray[baseIndex + (index++)] = radius;

				// decay
				floatArray[baseIndex + (index++)] = l.decay;

				// distance
				floatArray[baseIndex + (index++)] = l.distance;

				// coneCos
				floatArray[baseIndex + (index++)] = Math.cos(l.angle);

				// sample 6
				// penumbraCos
				floatArray[baseIndex + (index++)] = Math.cos(l.angle * (1 - l.penumbra));

				// iesProfile
				floatArray[baseIndex + (index++)] = l.iesMap ? iesTextures.indexOf(l.iesMap) : - 1;

			} else if (l.isPointLight) {

				const worldPosition = u.setFromMatrixPosition(l.matrixWorld);

				// sample 3
				// u vector
				floatArray[baseIndex + (index++)] = worldPosition.x;
				floatArray[baseIndex + (index++)] = worldPosition.y;
				floatArray[baseIndex + (index++)] = worldPosition.z;
				index++;

				// sample 4
				index += 4;

				// sample 5
				index += 1;

				floatArray[baseIndex + (index++)] = l.decay;
				floatArray[baseIndex + (index++)] = l.distance;

			} else if (l.isDirectionalLight) {

				const worldPosition = u.setFromMatrixPosition(l.matrixWorld);
				const targetPosition = v.setFromMatrixPosition(l.target.matrixWorld);
				target.subVectors(worldPosition, targetPosition).normalize();

				// sample 3
				// u vector
				floatArray[baseIndex + (index++)] = target.x;
				floatArray[baseIndex + (index++)] = target.y;
				floatArray[baseIndex + (index++)] = target.z;

			}

		}

		this.count = lights.length;

		const hash = bufferToHash(floatArray.buffer);
		if (this.hash !== hash) {

			this.hash = hash;
			tex.needsUpdate = true;
			return true;

		}

		return false;

	}

}

function copyArrayToArray (fromArray, fromStride, toArray, toStride, offset) {

	if (fromStride > toStride) {

		throw new Error();

	}

	// scale non-float values to their normalized range
	const count = fromArray.length / fromStride;
	const bpe = fromArray.constructor.BYTES_PER_ELEMENT * 8;
	let maxValue = 1.0;
	switch (fromArray.constructor) {

		case Uint8Array:
		case Uint16Array:
		case Uint32Array:
			maxValue = 2 ** bpe - 1;
			break;

		case Int8Array:
		case Int16Array:
		case Int32Array:
			maxValue = 2 ** (bpe - 1) - 1;
			break;

	}

	for (let i = 0; i < count; i++) {

		const i4 = 4 * i;
		const is = fromStride * i;
		for (let j = 0; j < toStride; j++) {

			toArray[offset + i4 + j] = fromStride >= j + 1 ? fromArray[is + j] / maxValue : 0;

		}

	}

}

class FloatAttributeTextureArray extends DataArrayTexture {

	constructor() {

		super();
		this._textures = [];
		this.type = FloatType;
		this.format = RGBAFormat;
		this.internalFormat = 'RGBA32F';

	}

	updateAttribute (index, attr) {

		// update the texture
		const tex = this._textures[index];
		tex.updateFrom(attr);

		// ensure compatibility
		const baseImage = tex.image;
		const image = this.image;
		if (baseImage.width !== image.width || baseImage.height !== image.height) {

			throw new Error('FloatAttributeTextureArray: Attribute must be the same dimensions when updating single layer.');

		}

		// update the image
		const { width, height, data } = image;
		const length = width * height * 4;
		const offset = length * index;
		let itemSize = attr.itemSize;
		if (itemSize === 3) {

			itemSize = 4;

		}

		// copy the data
		copyArrayToArray(tex.image.data, itemSize, data, 4, offset);

		this.dispose();
		this.needsUpdate = true;

	}

	setAttributes (attrs) {

		// ensure the attribute count
		const itemCount = attrs[0].count;
		const attrsLength = attrs.length;
		for (let i = 0, l = attrsLength; i < l; i++) {

			if (attrs[i].count !== itemCount) {

				throw new Error('FloatAttributeTextureArray: All attributes must have the same item count.');

			}

		}

		// initialize all textures
		const textures = this._textures;
		while (textures.length < attrsLength) {

			const tex = new FloatVertexAttributeTexture();
			textures.push(tex);

		}

		while (textures.length > attrsLength) {

			textures.pop();

		}

		// update all textures
		for (let i = 0, l = attrsLength; i < l; i++) {

			textures[i].updateFrom(attrs[i]);

		}

		// determine if we need to create a new array
		const baseTexture = textures[0];
		const baseImage = baseTexture.image;
		const image = this.image;

		if (baseImage.width !== image.width || baseImage.height !== image.height || baseImage.depth !== attrsLength) {

			image.width = baseImage.width;
			image.height = baseImage.height;
			image.depth = attrsLength;
			image.data = new Float32Array(image.width * image.height * image.depth * 4);

		}

		// copy the other texture data into the data array texture
		const { data, width, height } = image;
		for (let i = 0, l = attrsLength; i < l; i++) {

			const tex = textures[i];
			const length = width * height * 4;
			const offset = length * i;

			let itemSize = attrs[i].itemSize;
			if (itemSize === 3) {

				itemSize = 4;

			}

			copyArrayToArray(tex.image.data, itemSize, data, 4, offset);

		}

		// reset the texture
		this.dispose();
		this.needsUpdate = true;

	}


}

class AttributesTextureArray extends FloatAttributeTextureArray {

	updateNormalAttribute (attr) {

		this.updateAttribute(0, attr);

	}

	updateTangentAttribute (attr) {

		this.updateAttribute(1, attr);

	}

	updateUvAttribute (attr) {

		this.updateAttribute(2, attr);

	}

	updateColorAttribute (attr) {

		this.updateAttribute(3, attr);

	}

	updateFrom (normal, tangent, uv, color) {

		this.setAttributes([normal, tangent, uv, color]);

	}

}

function uuidSort (a, b) {

	if (a.uuid < b.uuid) return 1;
	if (a.uuid > b.uuid) return - 1;
	return 0;

}

// we must hash the texture to determine uniqueness using the encoding, as well, because the
// when rendering each texture to the texture array they must have a consistent color space.
function getTextureHash$1 (t) {

	return `${t.source.uuid}:${t.colorSpace}`;

}

// reduce the set of textures to just those with a unique source while retaining
// the order of the textures.
function reduceTexturesToUniqueSources (textures) {

	const sourceSet = new Set();
	const result = [];
	for (let i = 0, l = textures.length; i < l; i++) {

		const tex = textures[i];
		const hash = getTextureHash$1(tex);
		if (!sourceSet.has(hash)) {

			sourceSet.add(hash);
			result.push(tex);

		}

	}

	return result;

}

function getIesTextures (lights) {

	const textures = lights.map(l => l.iesMap || null).filter(t => t);
	const textureSet = new Set(textures);
	return Array.from(textureSet).sort(uuidSort);

}

function getTextures (materials) {

	const textureSet = new Set();
	for (let i = 0, l = materials.length; i < l; i++) {

		const material = materials[i];
		for (const key in material) {

			const value = material[key];
			if (value && value.isTexture) {

				textureSet.add(value);

			}

		}

	}

	const textureArray = Array.from(textureSet);
	return reduceTexturesToUniqueSources(textureArray).sort(uuidSort);

}

function getLights (scene) {

	const lights = [];
	scene.traverse(c => {

		if (c.visible) {

			if (
				c.isRectAreaLight ||
				c.isSpotLight ||
				c.isPointLight ||
				c.isDirectionalLight
			) {

				lights.push(c);

			}

		}

	});

	return lights.sort(uuidSort);

}

const MATERIAL_PIXELS = 45;
const MATERIAL_STRIDE = MATERIAL_PIXELS * 4;

class MaterialFeatures {

	constructor() {

		this._features = {};

	}

	isUsed (feature) {

		return feature in this._features;

	}

	setUsed (feature, used = true) {

		if (used === false) {

			delete this._features[feature];

		} else {

			this._features[feature] = true;

		}

	}

	reset () {

		this._features = {};

	}

}

class MaterialsTexture extends DataTexture {

	constructor() {

		super(new Float32Array(4), 1, 1);

		this.format = RGBAFormat;
		this.type = FloatType;
		this.wrapS = ClampToEdgeWrapping;
		this.wrapT = ClampToEdgeWrapping;
		this.minFilter = NearestFilter;
		this.magFilter = NearestFilter;
		this.generateMipmaps = false;
		this.features = new MaterialFeatures();

	}

	updateFrom (materials, textures) {

		function getTexture (material, key, def = - 1) {

			if (key in material && material[key]) {

				const hash = getTextureHash$1(material[key]);
				return textureLookUp[hash];

			} else {

				return def;

			}

		}

		function getField (material, key, def) {

			return key in material ? material[key] : def;

		}

		function writeTextureMatrixToArray (material, textureKey, array, offset) {

			const texture = material[textureKey] && material[textureKey].isTexture ? material[textureKey] : null;

			// check if texture exists
			if (texture) {

				if (texture.matrixAutoUpdate) {

					texture.updateMatrix();

				}

				const elements = texture.matrix.elements;

				let i = 0;

				// first row
				array[offset + i++] = elements[0];
				array[offset + i++] = elements[3];
				array[offset + i++] = elements[6];
				i++;

				// second row
				array[offset + i++] = elements[1];
				array[offset + i++] = elements[4];
				array[offset + i++] = elements[7];
				i++;

			}

			return 8;

		}

		let index = 0;
		const pixelCount = materials.length * MATERIAL_PIXELS;
		const dimension = Math.ceil(Math.sqrt(pixelCount)) || 1;
		const { image, features } = this;

		// index the list of textures based on shareable source
		const textureLookUp = {};
		for (let i = 0, l = textures.length; i < l; i++) {

			textureLookUp[getTextureHash$1(textures[i])] = i;

		}

		if (image.width !== dimension) {

			this.dispose();

			image.data = new Float32Array(dimension * dimension * 4);
			image.width = dimension;
			image.height = dimension;

		}

		const floatArray = image.data;

		// on some devices (Google Pixel 6) the "floatBitsToInt" function does not work correctly so we
		// can't encode texture ids that way.
		// const intArray = new Int32Array( floatArray.buffer );

		features.reset();
		for (let i = 0, l = materials.length; i < l; i++) {

			const m = materials[i];

			if (m.isFogVolumeMaterial) {

				features.setUsed('FOG');

				for (let j = 0; j < MATERIAL_STRIDE; j++) {

					floatArray[index + j] = 0;

				}

				// sample 0 .rgb
				floatArray[index + 0 * 4 + 0] = m.color.r;
				floatArray[index + 0 * 4 + 1] = m.color.g;
				floatArray[index + 0 * 4 + 2] = m.color.b;

				// sample 2 .a
				floatArray[index + 2 * 4 + 3] = getField(m, 'emissiveIntensity', 0.0);

				// sample 3 .rgb
				floatArray[index + 3 * 4 + 0] = m.emissive.r;
				floatArray[index + 3 * 4 + 1] = m.emissive.g;
				floatArray[index + 3 * 4 + 2] = m.emissive.b;

				// sample 13 .g
				// reusing opacity field
				floatArray[index + 13 * 4 + 1] = m.density;

				// side
				floatArray[index + 13 * 4 + 3] = 0.0;

				// sample 14 .b
				floatArray[index + 14 * 4 + 2] = 1 << 2;

				index += MATERIAL_STRIDE;
				continue;

			}

			// sample 0
			// color
			floatArray[index++] = m.color.r;
			floatArray[index++] = m.color.g;
			floatArray[index++] = m.color.b;
			floatArray[index++] = getTexture(m, 'map');

			// sample 1
			// metalness & roughness
			floatArray[index++] = getField(m, 'metalness', 0.0);
			floatArray[index++] = getTexture(m, 'metalnessMap');
			floatArray[index++] = getField(m, 'roughness', 0.0);
			floatArray[index++] = getTexture(m, 'roughnessMap');

			// sample 2
			// transmission & emissiveIntensity
			// three.js assumes a default f0 of 0.04 if no ior is provided which equates to an ior of 1.5
			floatArray[index++] = getField(m, 'ior', 1.5);
			floatArray[index++] = getField(m, 'transmission', 0.0);
			floatArray[index++] = getTexture(m, 'transmissionMap');
			floatArray[index++] = getField(m, 'emissiveIntensity', 0.0);

			// sample 3
			// emission
			if ('emissive' in m) {

				floatArray[index++] = m.emissive.r;
				floatArray[index++] = m.emissive.g;
				floatArray[index++] = m.emissive.b;

			} else {

				floatArray[index++] = 0.0;
				floatArray[index++] = 0.0;
				floatArray[index++] = 0.0;

			}

			floatArray[index++] = getTexture(m, 'emissiveMap');

			// sample 4
			// normals
			floatArray[index++] = getTexture(m, 'normalMap');
			if ('normalScale' in m) {

				floatArray[index++] = m.normalScale.x;
				floatArray[index++] = m.normalScale.y;

			} else {

				floatArray[index++] = 1;
				floatArray[index++] = 1;

			}

			// clearcoat
			floatArray[index++] = getField(m, 'clearcoat', 0.0);
			floatArray[index++] = getTexture(m, 'clearcoatMap'); // sample 5

			floatArray[index++] = getField(m, 'clearcoatRoughness', 0.0);
			floatArray[index++] = getTexture(m, 'clearcoatRoughnessMap');

			floatArray[index++] = getTexture(m, 'clearcoatNormalMap');

			// sample 6
			if ('clearcoatNormalScale' in m) {

				floatArray[index++] = m.clearcoatNormalScale.x;
				floatArray[index++] = m.clearcoatNormalScale.y;

			} else {

				floatArray[index++] = 1;
				floatArray[index++] = 1;

			}

			index++;
			floatArray[index++] = getField(m, 'sheen', 0.0);

			// sample 7
			// sheen
			if ('sheenColor' in m) {

				floatArray[index++] = m.sheenColor.r;
				floatArray[index++] = m.sheenColor.g;
				floatArray[index++] = m.sheenColor.b;

			} else {

				floatArray[index++] = 0.0;
				floatArray[index++] = 0.0;
				floatArray[index++] = 0.0;

			}

			floatArray[index++] = getTexture(m, 'sheenColorMap');

			// sample 8
			floatArray[index++] = getField(m, 'sheenRoughness', 0.0);
			floatArray[index++] = getTexture(m, 'sheenRoughnessMap');

			// iridescence
			floatArray[index++] = getTexture(m, 'iridescenceMap');
			floatArray[index++] = getTexture(m, 'iridescenceThicknessMap');

			// sample 9
			floatArray[index++] = getField(m, 'iridescence', 0.0);
			floatArray[index++] = getField(m, 'iridescenceIOR', 1.3);

			const iridescenceThicknessRange = getField(m, 'iridescenceThicknessRange', [100, 400]);
			floatArray[index++] = iridescenceThicknessRange[0];
			floatArray[index++] = iridescenceThicknessRange[1];

			// sample 10
			// specular color
			if ('specularColor' in m) {

				floatArray[index++] = m.specularColor.r;
				floatArray[index++] = m.specularColor.g;
				floatArray[index++] = m.specularColor.b;

			} else {

				floatArray[index++] = 1.0;
				floatArray[index++] = 1.0;
				floatArray[index++] = 1.0;

			}

			floatArray[index++] = getTexture(m, 'specularColorMap');

			// sample 11
			// specular intensity
			floatArray[index++] = getField(m, 'specularIntensity', 1.0);
			floatArray[index++] = getTexture(m, 'specularIntensityMap');

			// isThinFilm
			const isThinFilm = getField(m, 'thickness', 0.0) === 0.0 && getField(m, 'attenuationDistance', Infinity) === Infinity;
			floatArray[index++] = Number(isThinFilm);
			index++;

			// sample 12
			if ('attenuationColor' in m) {

				floatArray[index++] = m.attenuationColor.r;
				floatArray[index++] = m.attenuationColor.g;
				floatArray[index++] = m.attenuationColor.b;

			} else {

				floatArray[index++] = 1.0;
				floatArray[index++] = 1.0;
				floatArray[index++] = 1.0;

			}

			floatArray[index++] = getField(m, 'attenuationDistance', Infinity);

			// sample 13
			// alphaMap
			floatArray[index++] = getTexture(m, 'alphaMap');

			// side & matte
			floatArray[index++] = m.opacity;
			floatArray[index++] = m.alphaTest;
			if (!isThinFilm && m.transmission > 0.0) {

				floatArray[index++] = 0;

			} else {

				switch (m.side) {

					case FrontSide:
						floatArray[index++] = 1;
						break;
					case BackSide:
						floatArray[index++] = - 1;
						break;
					case DoubleSide:
						floatArray[index++] = 0;
						break;

				}

			}

			// sample 14
			floatArray[index++] = Number(getField(m, 'matte', false)); // matte
			floatArray[index++] = Number(getField(m, 'castShadow', true)); // shadow
			floatArray[index++] = Number(m.vertexColors) | (Number(m.flatShading) << 1); // vertexColors & flatShading
			floatArray[index++] = Number(m.transparent); // transparent

			// map transform 15
			index += writeTextureMatrixToArray(m, 'map', floatArray, index);

			// metalnessMap transform 17
			index += writeTextureMatrixToArray(m, 'metalnessMap', floatArray, index);

			// roughnessMap transform 19
			index += writeTextureMatrixToArray(m, 'roughnessMap', floatArray, index);

			// transmissionMap transform 21
			index += writeTextureMatrixToArray(m, 'transmissionMap', floatArray, index);

			// emissiveMap transform 22
			index += writeTextureMatrixToArray(m, 'emissiveMap', floatArray, index);

			// normalMap transform 25
			index += writeTextureMatrixToArray(m, 'normalMap', floatArray, index);

			// clearcoatMap transform 27
			index += writeTextureMatrixToArray(m, 'clearcoatMap', floatArray, index);

			// clearcoatNormalMap transform 29
			index += writeTextureMatrixToArray(m, 'clearcoatNormalMap', floatArray, index);

			// clearcoatRoughnessMap transform 31
			index += writeTextureMatrixToArray(m, 'clearcoatRoughnessMap', floatArray, index);

			// sheenColorMap transform 33
			index += writeTextureMatrixToArray(m, 'sheenColorMap', floatArray, index);

			// sheenRoughnessMap transform 35
			index += writeTextureMatrixToArray(m, 'sheenRoughnessMap', floatArray, index);

			// iridescenceMap transform 37
			index += writeTextureMatrixToArray(m, 'iridescenceMap', floatArray, index);

			// iridescenceThicknessMap transform 39
			index += writeTextureMatrixToArray(m, 'iridescenceThicknessMap', floatArray, index);

			// specularColorMap transform 41
			index += writeTextureMatrixToArray(m, 'specularColorMap', floatArray, index);

			// specularIntensityMap transform 43
			index += writeTextureMatrixToArray(m, 'specularIntensityMap', floatArray, index);

		}

		// check if the contents have changed
		const hash = bufferToHash(floatArray.buffer);
		if (this.hash !== hash) {

			this.hash = hash;
			this.needsUpdate = true;
			return true;

		}

		return false;

	}

}

const prevColor = new Color();
function getTextureHash (texture) {

	return texture ? `${texture.uuid}:${texture.version}` : null;

}

function assignOptions (target, options) {

	for (const key in options) {

		if (key in target) {

			target[key] = options[key];

		}

	}

}

class RenderTarget2DArray extends WebGLArrayRenderTarget {

	constructor(width, height, options) {

		const textureOptions = {
			format: RGBAFormat,
			type: UnsignedByteType,
			minFilter: LinearFilter,
			magFilter: LinearFilter,
			wrapS: RepeatWrapping,
			wrapT: RepeatWrapping,
			generateMipmaps: false,
			...options,
		};

		super(width, height, 1, textureOptions);

		// manually assign the options because passing options into the
		// constructor does not work
		assignOptions(this.texture, textureOptions);

		this.texture.setTextures = (...args) => {

			this.setTextures(...args);

		};

		this.hashes = [null];

		const fsQuad = new FullScreenQuad(new CopyMaterial());
		this.fsQuad = fsQuad;

	}

	setTextures (renderer, textures, width = this.width, height = this.height) {

		// save previous renderer state
		const prevRenderTarget = renderer.getRenderTarget();
		const prevToneMapping = renderer.toneMapping;
		const prevAlpha = renderer.getClearAlpha();
		renderer.getClearColor(prevColor);

		// resize the render target and ensure we don't have an empty texture
		// render target depth must be >= 1 to avoid unbound texture error on android devices
		const depth = textures.length || 1;
		if (width !== this.width || height !== this.height || this.depth !== depth) {

			this.setSize(width, height, depth);
			this.hashes = new Array(depth).fill(null);

		}

		renderer.setClearColor(0, 0);
		renderer.toneMapping = NoToneMapping;

		// render each texture into each layer of the target
		const fsQuad = this.fsQuad;
		const hashes = this.hashes;
		let updated = false;
		for (let i = 0, l = depth; i < l; i++) {

			const texture = textures[i];
			const hash = getTextureHash(texture);
			if (texture && (hashes[i] !== hash || texture.isWebGLRenderTarget)) {

				// revert to default texture transform before rendering
				texture.matrixAutoUpdate = false;
				texture.matrix.identity();

				fsQuad.material.map = texture;

				renderer.setRenderTarget(this, i);
				fsQuad.render(renderer);

				// restore custom texture transform
				texture.updateMatrix();
				texture.matrixAutoUpdate = true;

				// ensure textures are not updated unnecessarily
				hashes[i] = hash;
				updated = true;

			}

		}

		// reset the renderer
		fsQuad.material.map = null;
		renderer.setClearColor(prevColor, prevAlpha);
		renderer.setRenderTarget(prevRenderTarget);
		renderer.toneMapping = prevToneMapping;

		return updated;

	}

	dispose () {

		super.dispose();
		this.fsQuad.dispose();

	}

}

class CopyMaterial extends ShaderMaterial {

	get map () {

		return this.uniforms.map.value;

	}
	set map (v) {

		this.uniforms.map.value = v;

	}

	constructor() {

		super({
			uniforms: {

				map: { value: null },

			},

			vertexShader: /* glsl */`
				varying vec2 vUv;
				void main() {

					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

				}
			`,

			fragmentShader: /* glsl */`
				uniform sampler2D map;
				varying vec2 vUv;
				void main() {

					gl_FragColor = texture2D( map, vUv );

				}
			`
		});

	}

}

// Stratified Sampling based on implementation from hoverinc pathtracer
// - https://github.com/hoverinc/ray-tracing-renderer
// - http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html

function shuffle (arr, random = Math.random()) {

	for (let i = arr.length - 1; i > 0; i--) {

		const j = Math.floor(random() * (i + 1));
		const x = arr[i];
		arr[i] = arr[j];
		arr[j] = x;

	}

	return arr;

}

// strataCount : The number of bins per dimension
// dimensions  : The number of dimensions to generate stratified values for
class StratifiedSampler {

	constructor(strataCount, dimensions, random = Math.random) {

		const l = strataCount ** dimensions;
		const strata = new Uint16Array(l);
		let index = l;

		// each integer represents a statum bin
		for (let i = 0; i < l; i++) {

			strata[i] = i;

		}

		this.samples = new Float32Array(dimensions);

		this.strataCount = strataCount;

		this.reset = function () {

			for (let i = 0; i < l; i++) {

				strata[i] = i;

			}

			index = 0;

		};

		this.reshuffle = function () {

			index = 0;

		};

		this.next = function () {

			const { samples } = this;

			if (index >= strata.length) {

				shuffle(strata, random);
				this.reshuffle();

			}

			let stratum = strata[index++];

			for (let i = 0; i < dimensions; i++) {

				samples[i] = (stratum % strataCount + random()) / strataCount;
				stratum = Math.floor(stratum / strataCount);

			}

			return samples;

		};

	}

}

// Stratified Sampling based on implementation from hoverinc pathtracer

// Stratified set of data with each tuple stratified separately and combined
class StratifiedSamplerCombined {

	constructor(strataCount, listOfDimensions, random = Math.random) {

		let totalDim = 0;
		for (const dim of listOfDimensions) {

			totalDim += dim;

		}

		const combined = new Float32Array(totalDim);
		const strataObjs = [];
		let offset = 0;
		for (const dim of listOfDimensions) {

			const sampler = new StratifiedSampler(strataCount, dim, random);
			sampler.samples = new Float32Array(combined.buffer, offset, sampler.samples.length);
			offset += sampler.samples.length * 4;
			strataObjs.push(sampler);

		}

		this.samples = combined;

		this.strataCount = strataCount;

		this.next = function () {

			for (const strata of strataObjs) {

				strata.next();

			}

			return combined;

		};

		this.reshuffle = function () {

			for (const strata of strataObjs) {

				strata.reshuffle();

			}

		};

		this.reset = function () {

			for (const strata of strataObjs) {

				strata.reset();

			}

		};

	}

}

// https://stackoverflow.com/questions/424292/seedable-javascript-random-number-generator
class RandomGenerator {

	constructor(seed = 0) {

		// LCG using GCC's constants
		this.m = 0x80000000; // 2**31;
		this.a = 1103515245;
		this.c = 12345;

		this.seed = seed;

	}

	nextInt () {

		this.seed = (this.a * this.seed + this.c) % this.m;
		return this.seed;

	}

	nextFloat () {

		// returns in range [0,1]
		return this.nextInt() / (this.m - 1);

	}

}

class StratifiedSamplesTexture extends DataTexture {

	constructor(count = 1, depth = 1, strata = 8) {

		super(new Float32Array(1), 1, 1, RGBAFormat, FloatType);
		this.minFilter = NearestFilter;
		this.magFilter = NearestFilter;

		this.strata = strata;
		this.sampler = null;
		this.generator = new RandomGenerator();
		this.stableNoise = false;
		this.random = () => {

			if (this.stableNoise) {

				return this.generator.nextFloat();

			} else {

				return Math.random();

			}

		};

		this.init(count, depth, strata);

	}

	init (count = this.image.height, depth = this.image.width, strata = this.strata) {

		const { image } = this;
		if (image.width === depth && image.height === count && this.sampler !== null) {

			return;

		}

		const dimensions = new Array(count * depth).fill(4);
		const sampler = new StratifiedSamplerCombined(strata, dimensions, this.random);

		image.width = depth;
		image.height = count;
		image.data = sampler.samples;

		this.sampler = sampler;

		this.dispose();
		this.next();

	}

	next () {

		this.sampler.next();
		this.needsUpdate = true;

	}

	reset () {

		this.sampler.reset();
		this.generator.seed = 0;

	}

}

function shuffleArray (array, random = Math.random) {

	for (let i = array.length - 1; i > 0; i--) {

		const replaceIndex = ~ ~((random() - 1e-6) * i);
		const tmp = array[i];
		array[i] = array[replaceIndex];
		array[replaceIndex] = tmp;

	}

}

function fillWithOnes (array, count) {

	array.fill(0);

	for (let i = 0; i < count; i++) {

		array[i] = 1;

	}

}

class BlueNoiseSamples {

	constructor(size) {

		this.count = 0;
		this.size = - 1;
		this.sigma = - 1;
		this.radius = - 1;
		this.lookupTable = null;
		this.score = null;
		this.binaryPattern = null;

		this.resize(size);
		this.setSigma(1.5);

	}

	findVoid () {

		const { score, binaryPattern } = this;

		let currValue = Infinity;
		let currIndex = - 1;
		for (let i = 0, l = binaryPattern.length; i < l; i++) {

			if (binaryPattern[i] !== 0) {

				continue;

			}

			const pScore = score[i];
			if (pScore < currValue) {

				currValue = pScore;
				currIndex = i;

			}

		}

		return currIndex;

	}

	findCluster () {

		const { score, binaryPattern } = this;

		let currValue = - Infinity;
		let currIndex = - 1;
		for (let i = 0, l = binaryPattern.length; i < l; i++) {

			if (binaryPattern[i] !== 1) {

				continue;

			}

			const pScore = score[i];
			if (pScore > currValue) {

				currValue = pScore;
				currIndex = i;

			}

		}

		return currIndex;

	}

	setSigma (sigma) {

		if (sigma === this.sigma) {

			return;

		}

		// generate a radius in which the score will be updated under the
		// assumption that e^-10 is insignificant enough to be the border at
		// which we drop off.
		const radius = ~ ~(Math.sqrt(10 * 2 * (sigma ** 2)) + 1);
		const lookupWidth = 2 * radius + 1;
		const lookupTable = new Float32Array(lookupWidth * lookupWidth);
		const sigma2 = sigma * sigma;
		for (let x = - radius; x <= radius; x++) {

			for (let y = - radius; y <= radius; y++) {

				const index = (radius + y) * lookupWidth + x + radius;
				const dist2 = x * x + y * y;
				lookupTable[index] = Math.E ** (- dist2 / (2 * sigma2));

			}

		}

		this.lookupTable = lookupTable;
		this.sigma = sigma;
		this.radius = radius;

	}

	resize (size) {

		if (this.size !== size) {

			this.size = size;
			this.score = new Float32Array(size * size);
			this.binaryPattern = new Uint8Array(size * size);

		}


	}

	invert () {

		const { binaryPattern, score, size } = this;

		score.fill(0);

		for (let i = 0, l = binaryPattern.length; i < l; i++) {

			if (binaryPattern[i] === 0) {

				const y = ~ ~(i / size);
				const x = i - y * size;
				this.updateScore(x, y, 1);
				binaryPattern[i] = 1;

			} else {

				binaryPattern[i] = 0;

			}

		}

	}

	updateScore (x, y, multiplier) {

		// TODO: Is there a way to keep track of the highest and lowest scores here to avoid have to search over
		// everything in the buffer?
		const { size, score, lookupTable } = this;

		// const sigma2 = sigma * sigma;
		// const radius = Math.floor( size / 2 );
		const radius = this.radius;
		const lookupWidth = 2 * radius + 1;
		for (let px = - radius; px <= radius; px++) {

			for (let py = - radius; py <= radius; py++) {

				// const dist2 = px * px + py * py;
				// const value = Math.E ** ( - dist2 / ( 2 * sigma2 ) );

				const lookupIndex = (radius + py) * lookupWidth + px + radius;
				const value = lookupTable[lookupIndex];

				let sx = (x + px);
				sx = sx < 0 ? size + sx : sx % size;

				let sy = (y + py);
				sy = sy < 0 ? size + sy : sy % size;

				const sindex = sy * size + sx;
				score[sindex] += multiplier * value;

			}

		}

	}

	addPointIndex (index) {

		this.binaryPattern[index] = 1;

		const size = this.size;
		const y = ~ ~(index / size);
		const x = index - y * size;
		this.updateScore(x, y, 1);
		this.count++;

	}

	removePointIndex (index) {

		this.binaryPattern[index] = 0;

		const size = this.size;
		const y = ~ ~(index / size);
		const x = index - y * size;
		this.updateScore(x, y, - 1);
		this.count--;

	}

	copy (source) {

		this.resize(source.size);
		this.score.set(source.score);
		this.binaryPattern.set(source.binaryPattern);
		this.setSigma(source.sigma);
		this.count = source.count;

	}

}

class BlueNoiseGenerator {

	constructor() {

		this.random = Math.random;
		this.sigma = 1.5;
		this.size = 64;
		this.majorityPointsRatio = 0.1;

		this.samples = new BlueNoiseSamples(1);
		this.savedSamples = new BlueNoiseSamples(1);

	}

	generate () {

		// http://cv.ulichney.com/papers/1993-void-cluster.pdf

		const {
			samples,
			savedSamples,
			sigma,
			majorityPointsRatio,
			size,
		} = this;

		samples.resize(size);
		samples.setSigma(sigma);

		// 1. Randomly place the minority points.
		const pointCount = Math.floor(size * size * majorityPointsRatio);
		const initialSamples = samples.binaryPattern;

		fillWithOnes(initialSamples, pointCount);
		shuffleArray(initialSamples, this.random);

		for (let i = 0, l = initialSamples.length; i < l; i++) {

			if (initialSamples[i] === 1) {

				samples.addPointIndex(i);

			}

		}

		// 2. Remove minority point that is in densest cluster and place it in the largest void.
		while (true) {

			const clusterIndex = samples.findCluster();
			samples.removePointIndex(clusterIndex);

			const voidIndex = samples.findVoid();
			if (clusterIndex === voidIndex) {

				samples.addPointIndex(clusterIndex);
				break;

			}

			samples.addPointIndex(voidIndex);

		}

		// 3. PHASE I: Assign a rank to each progressively less dense cluster point and put it
		// in the dither array.
		const ditherArray = new Uint32Array(size * size);
		savedSamples.copy(samples);

		let rank;
		rank = samples.count - 1;
		while (rank >= 0) {

			const clusterIndex = samples.findCluster();
			samples.removePointIndex(clusterIndex);

			ditherArray[clusterIndex] = rank;
			rank--;

		}

		// 4. PHASE II: Do the same thing for the largest voids up to half of the total pixels using
		// the initial binary pattern.
		const totalSize = size * size;
		rank = savedSamples.count;
		while (rank < totalSize / 2) {

			const voidIndex = savedSamples.findVoid();
			savedSamples.addPointIndex(voidIndex);
			ditherArray[voidIndex] = rank;
			rank++;

		}

		// 5. PHASE III: Invert the pattern and finish out by assigning a rank to the remaining
		// and iteratively removing them.
		savedSamples.invert();

		while (rank < totalSize) {

			const clusterIndex = savedSamples.findCluster();
			savedSamples.removePointIndex(clusterIndex);
			ditherArray[clusterIndex] = rank;
			rank++;

		}

		return { data: ditherArray, maxValue: totalSize };

	}

}

function getStride (channels) {

	if (channels >= 3) {

		return 4;

	} else {

		return channels;

	}

}

function getFormat (channels) {

	switch (channels) {

		case 1:
			return RedFormat;
		case 2:
			return RGFormat;
		default:
			return RGBAFormat;

	}

}

class BlueNoiseTexture extends DataTexture {

	constructor(size = 64, channels = 1) {

		super(new Float32Array(4), 1, 1, RGBAFormat, FloatType);
		this.minFilter = NearestFilter;
		this.magFilter = NearestFilter;

		this.size = size;
		this.channels = channels;
		this.update();

	}

	update () {

		const channels = this.channels;
		const size = this.size;
		const generator = new BlueNoiseGenerator();
		generator.channels = channels;
		generator.size = size;

		const stride = getStride(channels);
		const format = getFormat(stride);
		if (this.image.width !== size || format !== this.format) {

			this.image.width = size;
			this.image.height = size;
			this.image.data = new Float32Array((size ** 2) * stride);
			this.format = format;
			this.dispose();

		}

		const data = this.image.data;
		for (let i = 0, l = channels; i < l; i++) {

			const result = generator.generate();
			const bin = result.data;
			const maxValue = result.maxValue;

			for (let j = 0, l2 = bin.length; j < l2; j++) {

				const value = bin[j] / maxValue;
				data[j * stride + i] = value;

			}

		}

		this.needsUpdate = true;

	}

}

const camera_struct = /* glsl */`

	struct PhysicalCamera {

		float focusDistance;
		float anamorphicRatio;
		float bokehSize;
		int apertureBlades;
		float apertureRotation;

	};

`;

const equirect_struct = /* glsl */`

	struct EquirectHdrInfo {

		sampler2D marginalWeights;
		sampler2D conditionalWeights;
		sampler2D map;

		float totalSum;

	};

`;

const lights_struct = /* glsl */`

	#define RECT_AREA_LIGHT_TYPE 0
	#define CIRC_AREA_LIGHT_TYPE 1
	#define SPOT_LIGHT_TYPE 2
	#define DIR_LIGHT_TYPE 3
	#define POINT_LIGHT_TYPE 4

	struct LightsInfo {

		sampler2D tex;
		uint count;

	};

	struct Light {

		vec3 position;
		int type;

		vec3 color;
		float intensity;

		vec3 u;
		vec3 v;
		float area;

		// spot light fields
		float radius;
		float near;
		float decay;
		float distance;
		float coneCos;
		float penumbraCos;
		int iesProfile;

	};

	Light readLightInfo( sampler2D tex, uint index ) {

		uint i = index * 6u;

		vec4 s0 = texelFetch1D( tex, i + 0u );
		vec4 s1 = texelFetch1D( tex, i + 1u );
		vec4 s2 = texelFetch1D( tex, i + 2u );
		vec4 s3 = texelFetch1D( tex, i + 3u );

		Light l;
		l.position = s0.rgb;
		l.type = int( round( s0.a ) );

		l.color = s1.rgb;
		l.intensity = s1.a;

		l.u = s2.rgb;
		l.v = s3.rgb;
		l.area = s3.a;

		if ( l.type == SPOT_LIGHT_TYPE || l.type == POINT_LIGHT_TYPE ) {

			vec4 s4 = texelFetch1D( tex, i + 4u );
			vec4 s5 = texelFetch1D( tex, i + 5u );
			l.radius = s4.r;
			l.decay = s4.g;
			l.distance = s4.b;
			l.coneCos = s4.a;

			l.penumbraCos = s5.r;
			l.iesProfile = int( round( s5.g ) );

		} else {

			l.radius = 0.0;
			l.decay = 0.0;
			l.distance = 0.0;

			l.coneCos = 0.0;
			l.penumbraCos = 0.0;
			l.iesProfile = - 1;

		}

		return l;

	}

`;

const material_struct = /* glsl */ `

	struct Material {

		vec3 color;
		int map;

		float metalness;
		int metalnessMap;

		float roughness;
		int roughnessMap;

		float ior;
		float transmission;
		int transmissionMap;

		float emissiveIntensity;
		vec3 emissive;
		int emissiveMap;

		int normalMap;
		vec2 normalScale;

		float clearcoat;
		int clearcoatMap;
		int clearcoatNormalMap;
		vec2 clearcoatNormalScale;
		float clearcoatRoughness;
		int clearcoatRoughnessMap;

		int iridescenceMap;
		int iridescenceThicknessMap;
		float iridescence;
		float iridescenceIor;
		float iridescenceThicknessMinimum;
		float iridescenceThicknessMaximum;

		vec3 specularColor;
		int specularColorMap;

		float specularIntensity;
		int specularIntensityMap;
		bool thinFilm;

		vec3 attenuationColor;
		float attenuationDistance;

		int alphaMap;

		bool castShadow;
		float opacity;
		float alphaTest;

		float side;
		bool matte;

		float sheen;
		vec3 sheenColor;
		int sheenColorMap;
		float sheenRoughness;
		int sheenRoughnessMap;

		bool vertexColors;
		bool flatShading;
		bool transparent;
		bool fogVolume;

		mat3 mapTransform;
		mat3 metalnessMapTransform;
		mat3 roughnessMapTransform;
		mat3 transmissionMapTransform;
		mat3 emissiveMapTransform;
		mat3 normalMapTransform;
		mat3 clearcoatMapTransform;
		mat3 clearcoatNormalMapTransform;
		mat3 clearcoatRoughnessMapTransform;
		mat3 sheenColorMapTransform;
		mat3 sheenRoughnessMapTransform;
		mat3 iridescenceMapTransform;
		mat3 iridescenceThicknessMapTransform;
		mat3 specularColorMapTransform;
		mat3 specularIntensityMapTransform;

	};

	mat3 readTextureTransform( sampler2D tex, uint index ) {

		mat3 textureTransform;

		vec4 row1 = texelFetch1D( tex, index );
		vec4 row2 = texelFetch1D( tex, index + 1u );

		textureTransform[0] = vec3(row1.r, row2.r, 0.0);
		textureTransform[1] = vec3(row1.g, row2.g, 0.0);
		textureTransform[2] = vec3(row1.b, row2.b, 1.0);

		return textureTransform;

	}

	Material readMaterialInfo( sampler2D tex, uint index ) {

		uint i = index * 45u;

		vec4 s0 = texelFetch1D( tex, i + 0u );
		vec4 s1 = texelFetch1D( tex, i + 1u );
		vec4 s2 = texelFetch1D( tex, i + 2u );
		vec4 s3 = texelFetch1D( tex, i + 3u );
		vec4 s4 = texelFetch1D( tex, i + 4u );
		vec4 s5 = texelFetch1D( tex, i + 5u );
		vec4 s6 = texelFetch1D( tex, i + 6u );
		vec4 s7 = texelFetch1D( tex, i + 7u );
		vec4 s8 = texelFetch1D( tex, i + 8u );
		vec4 s9 = texelFetch1D( tex, i + 9u );
		vec4 s10 = texelFetch1D( tex, i + 10u );
		vec4 s11 = texelFetch1D( tex, i + 11u );
		vec4 s12 = texelFetch1D( tex, i + 12u );
		vec4 s13 = texelFetch1D( tex, i + 13u );
		vec4 s14 = texelFetch1D( tex, i + 14u );

		Material m;
		m.color = s0.rgb;
		m.map = int( round( s0.a ) );

		m.metalness = s1.r;
		m.metalnessMap = int( round( s1.g ) );
		m.roughness = s1.b;
		m.roughnessMap = int( round( s1.a ) );

		m.ior = s2.r;
		m.transmission = s2.g;
		m.transmissionMap = int( round( s2.b ) );
		m.emissiveIntensity = s2.a;

		m.emissive = s3.rgb;
		m.emissiveMap = int( round( s3.a ) );

		m.normalMap = int( round( s4.r ) );
		m.normalScale = s4.gb;

		m.clearcoat = s4.a;
		m.clearcoatMap = int( round( s5.r ) );
		m.clearcoatRoughness = s5.g;
		m.clearcoatRoughnessMap = int( round( s5.b ) );
		m.clearcoatNormalMap = int( round( s5.a ) );
		m.clearcoatNormalScale = s6.rg;

		m.sheen = s6.a;
		m.sheenColor = s7.rgb;
		m.sheenColorMap = int( round( s7.a ) );
		m.sheenRoughness = s8.r;
		m.sheenRoughnessMap = int( round( s8.g ) );

		m.iridescenceMap = int( round( s8.b ) );
		m.iridescenceThicknessMap = int( round( s8.a ) );
		m.iridescence = s9.r;
		m.iridescenceIor = s9.g;
		m.iridescenceThicknessMinimum = s9.b;
		m.iridescenceThicknessMaximum = s9.a;

		m.specularColor = s10.rgb;
		m.specularColorMap = int( round( s10.a ) );

		m.specularIntensity = s11.r;
		m.specularIntensityMap = int( round( s11.g ) );
		m.thinFilm = bool( s11.b );

		m.attenuationColor = s12.rgb;
		m.attenuationDistance = s12.a;

		m.alphaMap = int( round( s13.r ) );

		m.opacity = s13.g;
		m.alphaTest = s13.b;
		m.side = s13.a;

		m.matte = bool( s14.r );
		m.castShadow = bool( s14.g );
		m.vertexColors = bool( int( s14.b ) & 1 );
		m.flatShading = bool( int( s14.b ) & 2 );
		m.fogVolume = bool( int( s14.b ) & 4 );
		m.transparent = bool( s14.a );

		uint firstTextureTransformIdx = i + 15u;

		// mat3( 1.0 ) is an identity matrix
		m.mapTransform = m.map == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx );
		m.metalnessMapTransform = m.metalnessMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 2u );
		m.roughnessMapTransform = m.roughnessMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 4u );
		m.transmissionMapTransform = m.transmissionMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 6u );
		m.emissiveMapTransform = m.emissiveMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 8u );
		m.normalMapTransform = m.normalMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 10u );
		m.clearcoatMapTransform = m.clearcoatMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 12u );
		m.clearcoatNormalMapTransform = m.clearcoatNormalMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 14u );
		m.clearcoatRoughnessMapTransform = m.clearcoatRoughnessMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 16u );
		m.sheenColorMapTransform = m.sheenColorMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 18u );
		m.sheenRoughnessMapTransform = m.sheenRoughnessMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 20u );
		m.iridescenceMapTransform = m.iridescenceMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 22u );
		m.iridescenceThicknessMapTransform = m.iridescenceThicknessMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 24u );
		m.specularColorMapTransform = m.specularColorMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 26u );
		m.specularIntensityMapTransform = m.specularIntensityMap == - 1 ? mat3( 1.0 ) : readTextureTransform( tex, firstTextureTransformIdx + 28u );

		return m;

	}

`;

const surface_record_struct = /* glsl */`

	struct SurfaceRecord {

		// surface type
		bool volumeParticle;

		// geometry
		vec3 faceNormal;
		bool frontFace;
		vec3 normal;
		mat3 normalBasis;
		mat3 normalInvBasis;

		// cached properties
		float eta;
		float f0;

		// material
		float roughness;
		float filteredRoughness;
		float metalness;
		vec3 color;
		vec3 emission;

		// transmission
		float ior;
		float transmission;
		bool thinFilm;
		vec3 attenuationColor;
		float attenuationDistance;

		// clearcoat
		vec3 clearcoatNormal;
		mat3 clearcoatBasis;
		mat3 clearcoatInvBasis;
		float clearcoat;
		float clearcoatRoughness;
		float filteredClearcoatRoughness;

		// sheen
		float sheen;
		vec3 sheenColor;
		float sheenRoughness;

		// iridescence
		float iridescence;
		float iridescenceIor;
		float iridescenceThickness;

		// specular
		vec3 specularColor;
		float specularIntensity;
	};

	struct ScatterRecord {
		float specularPdf;
		float pdf;
		vec3 direction;
		vec3 color;
	};

`;

const equirect_functions = /* glsl */`

	// samples the the given environment map in the given direction
	vec3 sampleEquirectColor( sampler2D envMap, vec3 direction ) {

		return texture2D( envMap, equirectDirectionToUv( direction ) ).rgb;

	}

	// gets the pdf of the given direction to sample
	float equirectDirectionPdf( vec3 direction ) {

		vec2 uv = equirectDirectionToUv( direction );
		float theta = uv.y * PI;
		float sinTheta = sin( theta );
		if ( sinTheta == 0.0 ) {

			return 0.0;

		}

		return 1.0 / ( 2.0 * PI * PI * sinTheta );

	}

	// samples the color given env map with CDF and returns the pdf of the direction
	float sampleEquirect( vec3 direction, inout vec3 color ) {

		float totalSum = envMapInfo.totalSum;
		if ( totalSum == 0.0 ) {

			color = vec3( 0.0 );
			return 1.0;

		}

		vec2 uv = equirectDirectionToUv( direction );
		color = texture2D( envMapInfo.map, uv ).rgb;

		float lum = luminance( color );
		ivec2 resolution = textureSize( envMapInfo.map, 0 );
		float pdf = lum / totalSum;

		return float( resolution.x * resolution.y ) * pdf * equirectDirectionPdf( direction );

	}

	// samples a direction of the envmap with color and retrieves pdf
	float sampleEquirectProbability( vec2 r, inout vec3 color, inout vec3 direction ) {

		// sample env map cdf
		float v = texture2D( envMapInfo.marginalWeights, vec2( r.x, 0.0 ) ).x;
		float u = texture2D( envMapInfo.conditionalWeights, vec2( r.y, v ) ).x;
		vec2 uv = vec2( u, v );

		vec3 derivedDirection = equirectUvToDirection( uv );
		direction = derivedDirection;
		color = texture2D( envMapInfo.map, uv ).rgb;

		float totalSum = envMapInfo.totalSum;
		float lum = luminance( color );
		ivec2 resolution = textureSize( envMapInfo.map, 0 );
		float pdf = lum / totalSum;

		return float( resolution.x * resolution.y ) * pdf * equirectDirectionPdf( direction );

	}
`;

const light_sampling_functions = /* glsl */`

	float getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) {

		return smoothstep( coneCosine, penumbraCosine, angleCosine );

	}

	float getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {

		// based upon Frostbite 3 Moving to Physically-based Rendering
		// page 32, equation 26: E[window1]
		// https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
		float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), EPSILON );

		if ( cutoffDistance > 0.0 ) {

			distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );

		}

		return distanceFalloff;

	}

	float getPhotometricAttenuation( sampler2DArray iesProfiles, int iesProfile, vec3 posToLight, vec3 lightDir, vec3 u, vec3 v ) {

		float cosTheta = dot( posToLight, lightDir );
		float angle = acos( cosTheta ) / PI;

		return texture2D( iesProfiles, vec3( angle, 0.0, iesProfile ) ).r;

	}

	struct LightRecord {

		float dist;
		vec3 direction;
		float pdf;
		vec3 emission;
		int type;

	};

	bool intersectLightAtIndex( sampler2D lights, vec3 rayOrigin, vec3 rayDirection, uint l, inout LightRecord lightRec ) {

		bool didHit = false;
		Light light = readLightInfo( lights, l );

		vec3 u = light.u;
		vec3 v = light.v;

		// check for backface
		vec3 normal = normalize( cross( u, v ) );
		if ( dot( normal, rayDirection ) > 0.0 ) {

			u *= 1.0 / dot( u, u );
			v *= 1.0 / dot( v, v );

			float dist;

			// MIS / light intersection is not supported for punctual lights.
			if(
				( light.type == RECT_AREA_LIGHT_TYPE && intersectsRectangle( light.position, normal, u, v, rayOrigin, rayDirection, dist ) ) ||
				( light.type == CIRC_AREA_LIGHT_TYPE && intersectsCircle( light.position, normal, u, v, rayOrigin, rayDirection, dist ) )
			) {

				float cosTheta = dot( rayDirection, normal );
				didHit = true;
				lightRec.dist = dist;
				lightRec.pdf = ( dist * dist ) / ( light.area * cosTheta );
				lightRec.emission = light.color * light.intensity;
				lightRec.direction = rayDirection;
				lightRec.type = light.type;

			}

		}

		return didHit;

	}

	LightRecord randomAreaLightSample( Light light, vec3 rayOrigin, vec2 ruv ) {

		vec3 randomPos;
		if( light.type == RECT_AREA_LIGHT_TYPE ) {

			// rectangular area light
			randomPos = light.position + light.u * ( ruv.x - 0.5 ) + light.v * ( ruv.y - 0.5 );

		} else if( light.type == CIRC_AREA_LIGHT_TYPE ) {

			// circular area light
			float r = 0.5 * sqrt( ruv.x );
			float theta = ruv.y * 2.0 * PI;
			float x = r * cos( theta );
			float y = r * sin( theta );

			randomPos = light.position + light.u * x + light.v * y;

		}

		vec3 toLight = randomPos - rayOrigin;
		float lightDistSq = dot( toLight, toLight );
		float dist = sqrt( lightDistSq );
		vec3 direction = toLight / dist;
		vec3 lightNormal = normalize( cross( light.u, light.v ) );

		LightRecord lightRec;
		lightRec.type = light.type;
		lightRec.emission = light.color * light.intensity;
		lightRec.dist = dist;
		lightRec.direction = direction;

		// TODO: the denominator is potentially zero
		lightRec.pdf = lightDistSq / ( light.area * dot( direction, lightNormal ) );

		return lightRec;

	}

	LightRecord randomSpotLightSample( Light light, sampler2DArray iesProfiles, vec3 rayOrigin, vec2 ruv ) {

		float radius = light.radius * sqrt( ruv.x );
		float theta = ruv.y * 2.0 * PI;
		float x = radius * cos( theta );
		float y = radius * sin( theta );

		vec3 u = light.u;
		vec3 v = light.v;
		vec3 normal = normalize( cross( u, v ) );

		float angle = acos( light.coneCos );
		float angleTan = tan( angle );
		float startDistance = light.radius / max( angleTan, EPSILON );

		vec3 randomPos = light.position - normal * startDistance + u * x + v * y;
		vec3 toLight = randomPos - rayOrigin;
		float lightDistSq = dot( toLight, toLight );
		float dist = sqrt( lightDistSq );

		vec3 direction = toLight / max( dist, EPSILON );
		float cosTheta = dot( direction, normal );

		float spotAttenuation = light.iesProfile != - 1 ?
			getPhotometricAttenuation( iesProfiles, light.iesProfile, direction, normal, u, v ) :
			getSpotAttenuation( light.coneCos, light.penumbraCos, cosTheta );

		float distanceAttenuation = getDistanceAttenuation( dist, light.distance, light.decay );
		LightRecord lightRec;
		lightRec.type = light.type;
		lightRec.dist = dist;
		lightRec.direction = direction;
		lightRec.emission = light.color * light.intensity * distanceAttenuation * spotAttenuation;
		lightRec.pdf = 1.0;

		return lightRec;

	}

	LightRecord randomLightSample( sampler2D lights, sampler2DArray iesProfiles, uint lightCount, vec3 rayOrigin, vec3 ruv ) {

		LightRecord result;

		// pick a random light
		uint l = uint( ruv.x * float( lightCount ) );
		Light light = readLightInfo( lights, l );

		if ( light.type == SPOT_LIGHT_TYPE ) {

			result = randomSpotLightSample( light, iesProfiles, rayOrigin, ruv.yz );

		} else if ( light.type == POINT_LIGHT_TYPE ) {

			vec3 lightRay = light.u - rayOrigin;
			float lightDist = length( lightRay );
			float cutoffDistance = light.distance;
			float distanceFalloff = 1.0 / max( pow( lightDist, light.decay ), 0.01 );
			if ( cutoffDistance > 0.0 ) {

				distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDist / cutoffDistance ) ) );

			}

			LightRecord rec;
			rec.direction = normalize( lightRay );
			rec.dist = length( lightRay );
			rec.pdf = 1.0;
			rec.emission = light.color * light.intensity * distanceFalloff;
			rec.type = light.type;
			result = rec;

		} else if ( light.type == DIR_LIGHT_TYPE ) {

			LightRecord rec;
			rec.dist = 1e10;
			rec.direction = light.u;
			rec.pdf = 1.0;
			rec.emission = light.color * light.intensity;
			rec.type = light.type;

			result = rec;

		} else {

			// sample the light
			result = randomAreaLightSample( light, rayOrigin, ruv.yz );

		}

		return result;

	}

`;

const shape_sampling_functions = /* glsl */`

	vec3 sampleHemisphere( vec3 n, vec2 uv ) {

		// https://www.rorydriscoll.com/2009/01/07/better-sampling/
		// https://graphics.pixar.com/library/OrthonormalB/paper.pdf
		float sign = n.z == 0.0 ? 1.0 : sign( n.z );
		float a = - 1.0 / ( sign + n.z );
		float b = n.x * n.y * a;
		vec3 b1 = vec3( 1.0 + sign * n.x * n.x * a, sign * b, - sign * n.x );
		vec3 b2 = vec3( b, sign + n.y * n.y * a, - n.y );

		float r = sqrt( uv.x );
		float theta = 2.0 * PI * uv.y;
		float x = r * cos( theta );
		float y = r * sin( theta );
		return x * b1 + y * b2 + sqrt( 1.0 - uv.x ) * n;

	}

	vec2 sampleTriangle( vec2 a, vec2 b, vec2 c, vec2 r ) {

		// get the edges of the triangle and the diagonal across the
		// center of the parallelogram
		vec2 e1 = a - b;
		vec2 e2 = c - b;
		vec2 diag = normalize( e1 + e2 );

		// pick the point in the parallelogram
		if ( r.x + r.y > 1.0 ) {

			r = vec2( 1.0 ) - r;

		}

		return e1 * r.x + e2 * r.y;

	}

	vec2 sampleCircle( vec2 uv ) {

		float angle = 2.0 * PI * uv.x;
		float radius = sqrt( uv.y );
		return vec2( cos( angle ), sin( angle ) ) * radius;

	}

	vec3 sampleSphere( vec2 uv ) {

		float u = ( uv.x - 0.5 ) * 2.0;
		float t = uv.y * PI * 2.0;
		float f = sqrt( 1.0 - u * u );

		return vec3( f * cos( t ), f * sin( t ), u );

	}

	vec2 sampleRegularPolygon( int sides, vec3 uvw ) {

		sides = max( sides, 3 );

		vec3 r = uvw;
		float anglePerSegment = 2.0 * PI / float( sides );
		float segment = floor( float( sides ) * r.x );

		float angle1 = anglePerSegment * segment;
		float angle2 = angle1 + anglePerSegment;
		vec2 a = vec2( sin( angle1 ), cos( angle1 ) );
		vec2 b = vec2( 0.0, 0.0 );
		vec2 c = vec2( sin( angle2 ), cos( angle2 ) );

		return sampleTriangle( a, b, c, r.yz );

	}

	// samples an aperture shape with the given number of sides. 0 means circle
	vec2 sampleAperture( int blades, vec3 uvw ) {

		return blades == 0 ?
			sampleCircle( uvw.xy ) :
			sampleRegularPolygon( blades, uvw );

	}


`;

const fresnel_functions = /* glsl */`

	bool totalInternalReflection( float cosTheta, float eta ) {

		float sinTheta = sqrt( 1.0 - cosTheta * cosTheta );
		return eta * sinTheta > 1.0;

	}

	// https://google.github.io/filament/Filament.md.html#materialsystem/diffusebrdf
	float schlickFresnel( float cosine, float f0 ) {

		return f0 + ( 1.0 - f0 ) * pow( 1.0 - cosine, 5.0 );

	}

	vec3 schlickFresnel( float cosine, vec3 f0 ) {

		return f0 + ( 1.0 - f0 ) * pow( 1.0 - cosine, 5.0 );

	}

	vec3 schlickFresnel( float cosine, vec3 f0, vec3 f90 ) {

		return f0 + ( f90 - f0 ) * pow( 1.0 - cosine, 5.0 );

	}

	float dielectricFresnel( float cosThetaI, float eta ) {

		// https://schuttejoe.github.io/post/disneybsdf/
		float ni = eta;
		float nt = 1.0;

		// Check for total internal reflection
		float sinThetaISq = 1.0f - cosThetaI * cosThetaI;
		float sinThetaTSq = eta * eta * sinThetaISq;
		if( sinThetaTSq >= 1.0 ) {

			return 1.0;

		}

		float sinThetaT = sqrt( sinThetaTSq );

		float cosThetaT = sqrt( max( 0.0, 1.0f - sinThetaT * sinThetaT ) );
		float rParallel = ( ( nt * cosThetaI ) - ( ni * cosThetaT ) ) / ( ( nt * cosThetaI ) + ( ni * cosThetaT ) );
		float rPerpendicular = ( ( ni * cosThetaI ) - ( nt * cosThetaT ) ) / ( ( ni * cosThetaI ) + ( nt * cosThetaT ) );
		return ( rParallel * rParallel + rPerpendicular * rPerpendicular ) / 2.0;

	}

	// https://raytracing.github.io/books/RayTracingInOneWeekend.html#dielectrics/schlickapproximation
	float iorRatioToF0( float eta ) {

		return pow( ( 1.0 - eta ) / ( 1.0 + eta ), 2.0 );

	}

	vec3 evaluateFresnel( float cosTheta, float eta, vec3 f0, vec3 f90 ) {

		if ( totalInternalReflection( cosTheta, eta ) ) {

			return f90;

		}

		return schlickFresnel( cosTheta, f0, f90 );

	}

	// TODO: disney fresnel was removed and replaced with this fresnel function to better align with
	// the glTF but is causing blown out pixels. Should be revisited
	// float evaluateFresnelWeight( float cosTheta, float eta, float f0 ) {

	// 	if ( totalInternalReflection( cosTheta, eta ) ) {

	// 		return 1.0;

	// 	}

	// 	return schlickFresnel( cosTheta, f0 );

	// }

	// https://schuttejoe.github.io/post/disneybsdf/
	float disneyFresnel( vec3 wo, vec3 wi, vec3 wh, float f0, float eta, float metalness ) {

		float dotHV = dot( wo, wh );
		if ( totalInternalReflection( dotHV, eta ) ) {

			return 1.0;

		}

		float dotHL = dot( wi, wh );
		float dielectricFresnel = dielectricFresnel( abs( dotHV ), eta );
		float metallicFresnel = schlickFresnel( dotHL, f0 );

		return mix( dielectricFresnel, metallicFresnel, metalness );

	}

`;

const math_functions = /* glsl */`

	// Fast arccos approximation used to remove banding artifacts caused by numerical errors in acos.
	// This is a cubic Lagrange interpolating polynomial for x = [-1, -1/2, 0, 1/2, 1].
	// For more information see: https://github.com/gkjohnson/three-gpu-pathtracer/pull/171#issuecomment-1152275248
	float acosApprox( float x ) {

		x = clamp( x, -1.0, 1.0 );
		return ( - 0.69813170079773212 * x * x - 0.87266462599716477 ) * x + 1.5707963267948966;

	}

	// An acos with input values bound to the range [-1, 1].
	float acosSafe( float x ) {

		return acos( clamp( x, -1.0, 1.0 ) );

	}

	float saturateCos( float val ) {

		return clamp( val, 0.001, 1.0 );

	}

	float square( float t ) {

		return t * t;

	}

	vec2 square( vec2 t ) {

		return t * t;

	}

	vec3 square( vec3 t ) {

		return t * t;

	}

	vec4 square( vec4 t ) {

		return t * t;

	}

	vec2 rotateVector( vec2 v, float t ) {

		float ac = cos( t );
		float as = sin( t );
		return vec2(
			v.x * ac - v.y * as,
			v.x * as + v.y * ac
		);

	}

	// forms a basis with the normal vector as Z
	mat3 getBasisFromNormal( vec3 normal ) {

		vec3 other;
		if ( abs( normal.x ) > 0.5 ) {

			other = vec3( 0.0, 1.0, 0.0 );

		} else {

			other = vec3( 1.0, 0.0, 0.0 );

		}

		vec3 ortho = normalize( cross( normal, other ) );
		vec3 ortho2 = normalize( cross( normal, ortho ) );
		return mat3( ortho2, ortho, normal );

	}

`;

const shape_intersection_functions = /* glsl */`

	// Finds the point where the ray intersects the plane defined by u and v and checks if this point
	// falls in the bounds of the rectangle on that same plane.
	// Plane intersection: https://lousodrome.net/blog/light/2020/07/03/intersection-of-a-ray-and-a-plane/
	bool intersectsRectangle( vec3 center, vec3 normal, vec3 u, vec3 v, vec3 rayOrigin, vec3 rayDirection, inout float dist ) {

		float t = dot( center - rayOrigin, normal ) / dot( rayDirection, normal );

		if ( t > EPSILON ) {

			vec3 p = rayOrigin + rayDirection * t;
			vec3 vi = p - center;

			// check if p falls inside the rectangle
			float a1 = dot( u, vi );
			if ( abs( a1 ) <= 0.5 ) {

				float a2 = dot( v, vi );
				if ( abs( a2 ) <= 0.5 ) {

					dist = t;
					return true;

				}

			}

		}

		return false;

	}

	// Finds the point where the ray intersects the plane defined by u and v and checks if this point
	// falls in the bounds of the circle on that same plane. See above URL for a description of the plane intersection algorithm.
	bool intersectsCircle( vec3 position, vec3 normal, vec3 u, vec3 v, vec3 rayOrigin, vec3 rayDirection, inout float dist ) {

		float t = dot( position - rayOrigin, normal ) / dot( rayDirection, normal );

		if ( t > EPSILON ) {

			vec3 hit = rayOrigin + rayDirection * t;
			vec3 vi = hit - position;

			float a1 = dot( u, vi );
			float a2 = dot( v, vi );

			if( length( vec2( a1, a2 ) ) <= 0.5 ) {

				dist = t;
				return true;

			}

		}

		return false;

	}

`;

const texture_sample_functions = /*glsl */`

	// add texel fetch functions for texture arrays
	vec4 texelFetch1D( sampler2DArray tex, int layer, uint index ) {

		uint width = uint( textureSize( tex, 0 ).x );
		uvec2 uv;
		uv.x = index % width;
		uv.y = index / width;

		return texelFetch( tex, ivec3( uv, layer ), 0 );

	}

	vec4 textureSampleBarycoord( sampler2DArray tex, int layer, vec3 barycoord, uvec3 faceIndices ) {

		return
			barycoord.x * texelFetch1D( tex, layer, faceIndices.x ) +
			barycoord.y * texelFetch1D( tex, layer, faceIndices.y ) +
			barycoord.z * texelFetch1D( tex, layer, faceIndices.z );

	}

`;

const util_functions = /* glsl */`

	// TODO: possibly this should be renamed something related to material or path tracing logic

	#ifndef RAY_OFFSET
	#define RAY_OFFSET 1e-4
	#endif

	// adjust the hit point by the surface normal by a factor of some offset and the
	// maximum component-wise value of the current point to accommodate floating point
	// error as values increase.
	vec3 stepRayOrigin( vec3 rayOrigin, vec3 rayDirection, vec3 offset, float dist ) {

		vec3 point = rayOrigin + rayDirection * dist;
		vec3 absPoint = abs( point );
		float maxPoint = max( absPoint.x, max( absPoint.y, absPoint.z ) );
		return point + offset * ( maxPoint + 1.0 ) * RAY_OFFSET;

	}

	// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_volume/README.md#attenuation
	vec3 transmissionAttenuation( float dist, vec3 attColor, float attDist ) {

		vec3 ot = - log( attColor ) / attDist;
		return exp( - ot * dist );

	}

	vec3 getHalfVector( vec3 wi, vec3 wo, float eta ) {

		// get the half vector - assuming if the light incident vector is on the other side
		// of the that it's transmissive.
		vec3 h;
		if ( wi.z > 0.0 ) {

			h = normalize( wi + wo );

		} else {

			// Scale by the ior ratio to retrieve the appropriate half vector
			// From Section 2.2 on computing the transmission half vector:
			// https://blog.selfshadow.com/publications/s2015-shading-course/burley/s2015_pbs_disney_bsdf_notes.pdf
			h = normalize( wi + wo * eta );

		}

		h *= sign( h.z );
		return h;

	}

	vec3 getHalfVector( vec3 a, vec3 b ) {

		return normalize( a + b );

	}

	// The discrepancy between interpolated surface normal and geometry normal can cause issues when a ray
	// is cast that is on the top side of the geometry normal plane but below the surface normal plane. If
	// we find a ray like that we ignore it to avoid artifacts.
	// This function returns if the direction is on the same side of both planes.
	bool isDirectionValid( vec3 direction, vec3 surfaceNormal, vec3 geometryNormal ) {

		bool aboveSurfaceNormal = dot( direction, surfaceNormal ) > 0.0;
		bool aboveGeometryNormal = dot( direction, geometryNormal ) > 0.0;
		return aboveSurfaceNormal == aboveGeometryNormal;

	}

	// ray sampling x and z are swapped to align with expected background view
	vec2 equirectDirectionToUv( vec3 direction ) {

		// from Spherical.setFromCartesianCoords
		vec2 uv = vec2( atan( direction.z, direction.x ), acos( direction.y ) );
		uv /= vec2( 2.0 * PI, PI );

		// apply adjustments to get values in range [0, 1] and y right side up
		uv.x += 0.5;
		uv.y = 1.0 - uv.y;
		return uv;

	}

	vec3 equirectUvToDirection( vec2 uv ) {

		// undo above adjustments
		uv.x -= 0.5;
		uv.y = 1.0 - uv.y;

		// from Vector3.setFromSphericalCoords
		float theta = uv.x * 2.0 * PI;
		float phi = uv.y * PI;

		float sinPhi = sin( phi );

		return vec3( sinPhi * cos( theta ), cos( phi ), sinPhi * sin( theta ) );

	}

	// power heuristic for multiple importance sampling
	float misHeuristic( float a, float b ) {

		float aa = a * a;
		float bb = b * b;
		return aa / ( aa + bb );

	}

	// tentFilter from Peter Shirley's 'Realistic Ray Tracing (2nd Edition)' book, pg. 60
	// erichlof/THREE.js-PathTracing-Renderer/
	float tentFilter( float x ) {

		return x < 0.5 ? sqrt( 2.0 * x ) - 1.0 : 1.0 - sqrt( 2.0 - ( 2.0 * x ) );

	}
`;

const pcg_functions = /* glsl */`

	// https://www.shadertoy.com/view/wltcRS
	uvec4 WHITE_NOISE_SEED;

	void rng_initialize( vec2 p, int frame ) {

		// white noise seed
		WHITE_NOISE_SEED = uvec4( p, uint( frame ), uint( p.x ) + uint( p.y ) );

	}

	// https://www.pcg-random.org/
	void pcg4d( inout uvec4 v ) {

		v = v * 1664525u + 1013904223u;
		v.x += v.y * v.w;
		v.y += v.z * v.x;
		v.z += v.x * v.y;
		v.w += v.y * v.z;
		v = v ^ ( v >> 16u );
		v.x += v.y*v.w;
		v.y += v.z*v.x;
		v.z += v.x*v.y;
		v.w += v.y*v.z;

	}

	// returns [ 0, 1 ]
	float pcgRand() {

		pcg4d( WHITE_NOISE_SEED );
		return float( WHITE_NOISE_SEED.x ) / float( 0xffffffffu );

	}

	vec2 pcgRand2() {

		pcg4d( WHITE_NOISE_SEED );
		return vec2( WHITE_NOISE_SEED.xy ) / float(0xffffffffu);

	}

	vec3 pcgRand3() {

		pcg4d( WHITE_NOISE_SEED );
		return vec3( WHITE_NOISE_SEED.xyz ) / float( 0xffffffffu );

	}

	vec4 pcgRand4() {

		pcg4d( WHITE_NOISE_SEED );
		return vec4( WHITE_NOISE_SEED ) / float( 0xffffffffu );

	}
`;

const stratified_functions = /* glsl */`

	uniform sampler2D stratifiedTexture;
	uniform sampler2D stratifiedOffsetTexture;

	uint sobolPixelIndex = 0u;
	uint sobolPathIndex = 0u;
	uint sobolBounceIndex = 0u;
	vec4 pixelSeed = vec4( 0 );

	vec4 rand4( int v ) {

		ivec2 uv = ivec2( v, sobolBounceIndex );
		vec4 stratifiedSample = texelFetch( stratifiedTexture, uv, 0 );
		return fract( stratifiedSample + pixelSeed.r ); // blue noise + stratified samples

	}

	vec3 rand3( int v ) {

		return rand4( v ).xyz;

	}

	vec2 rand2( int v ) {

		return rand4( v ).xy;

	}

	float rand( int v ) {

		return rand4( v ).x;

	}

	void rng_initialize( vec2 screenCoord, int frame ) {

		// tile the small noise texture across the entire screen
		ivec2 noiseSize = ivec2( textureSize( stratifiedOffsetTexture, 0 ) );
		ivec2 pixel = ivec2( screenCoord.xy ) % noiseSize;
		vec2 pixelWidth = 1.0 / vec2( noiseSize );
		vec2 uv = vec2( pixel ) * pixelWidth + pixelWidth * 0.5;

		// note that using "texelFetch" here seems to break Android for some reason
		pixelSeed = texture( stratifiedOffsetTexture, uv );

	}

`;

/*
wi     : incident vector or light vector (pointing toward the light)
wo     : outgoing vector or view vector (pointing towards the camera)
wh     : computed half vector from wo and wi
Eval   : Get the color and pdf for a direction
Sample : Get the direction, color, and pdf for a sample
eta    : Greek character used to denote the "ratio of ior"
f0     : Amount of light reflected when looking at a surface head on - "fresnel 0"
f90    : Amount of light reflected at grazing angles
*/

const bsdf_functions = /* glsl */`

	// diffuse
	float diffuseEval( vec3 wo, vec3 wi, vec3 wh, SurfaceRecord surf, inout vec3 color ) {

		// https://schuttejoe.github.io/post/disneybsdf/
		float fl = schlickFresnel( wi.z, 0.0 );
		float fv = schlickFresnel( wo.z, 0.0 );

		float metalFactor = ( 1.0 - surf.metalness );
		float transFactor = ( 1.0 - surf.transmission );
		float rr = 0.5 + 2.0 * surf.roughness * fl * fl;
		float retro = rr * ( fl + fv + fl * fv * ( rr - 1.0f ) );
		float lambert = ( 1.0f - 0.5f * fl ) * ( 1.0f - 0.5f * fv );

		// TODO: subsurface approx?

		// float F = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
		float F = disneyFresnel( wo, wi, wh, surf.f0, surf.eta, surf.metalness );
		color = ( 1.0 - F ) * transFactor * metalFactor * wi.z * surf.color * ( retro + lambert ) / PI;

		return wi.z / PI;

	}

	vec3 diffuseDirection( vec3 wo, SurfaceRecord surf ) {

		vec3 lightDirection = sampleSphere( rand2( 11 ) );
		lightDirection.z += 1.0;
		lightDirection = normalize( lightDirection );

		return lightDirection;

	}

	// specular
	float specularEval( vec3 wo, vec3 wi, vec3 wh, SurfaceRecord surf, inout vec3 color ) {

		// if roughness is set to 0 then D === NaN which results in black pixels
		float metalness = surf.metalness;
		float roughness = surf.filteredRoughness;

		float eta = surf.eta;
		float f0 = surf.f0;

		vec3 f0Color = mix( f0 * surf.specularColor * surf.specularIntensity, surf.color, surf.metalness );
		vec3 f90Color = vec3( mix( surf.specularIntensity, 1.0, surf.metalness ) );
		vec3 F = evaluateFresnel( dot( wo, wh ), eta, f0Color, f90Color );

		vec3 iridescenceF = evalIridescence( 1.0, surf.iridescenceIor, dot( wi, wh ), surf.iridescenceThickness, f0Color );
		F = mix( F, iridescenceF,  surf.iridescence );

		// PDF
		// See 14.1.1 Microfacet BxDFs in https://www.pbr-book.org/
		float incidentTheta = acos( wo.z );
		float G = ggxShadowMaskG2( wi, wo, roughness );
		float D = ggxDistribution( wh, roughness );
		float G1 = ggxShadowMaskG1( incidentTheta, roughness );
		float ggxPdf = D * G1 * max( 0.0, abs( dot( wo, wh ) ) ) / abs ( wo.z );

		color = wi.z * F * G * D / ( 4.0 * abs( wi.z * wo.z ) );
		return ggxPdf / ( 4.0 * dot( wo, wh ) );

	}

	vec3 specularDirection( vec3 wo, SurfaceRecord surf ) {

		// sample ggx vndf distribution which gives a new normal
		float roughness = surf.filteredRoughness;
		vec3 halfVector = ggxDirection(
			wo,
			vec2( roughness ),
			rand2( 12 )
		);

		// apply to new ray by reflecting off the new normal
		return - reflect( wo, halfVector );

	}


	// transmission
	/*
	float transmissionEval( vec3 wo, vec3 wi, vec3 wh, SurfaceRecord surf, inout vec3 color ) {

		// See section 4.2 in https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf

		float filteredRoughness = surf.filteredRoughness;
		float eta = surf.eta;
		bool frontFace = surf.frontFace;
		bool thinFilm = surf.thinFilm;

		color = surf.transmission * surf.color;

		float denom = pow( eta * dot( wi, wh ) + dot( wo, wh ), 2.0 );
		return ggxPDF( wo, wh, filteredRoughness ) / denom;

	}

	vec3 transmissionDirection( vec3 wo, SurfaceRecord surf ) {

		float filteredRoughness = surf.filteredRoughness;
		float eta = surf.eta;
		bool frontFace = surf.frontFace;

		// sample ggx vndf distribution which gives a new normal
		vec3 halfVector = ggxDirection(
			wo,
			vec2( filteredRoughness ),
			rand2( 13 )
		);

		vec3 lightDirection = refract( normalize( - wo ), halfVector, eta );
		if ( surf.thinFilm ) {

			lightDirection = - refract( normalize( - lightDirection ), - vec3( 0.0, 0.0, 1.0 ), 1.0 / eta );

		}

		return normalize( lightDirection );

	}
	*/

	// TODO: This is just using a basic cosine-weighted specular distribution with an
	// incorrect PDF value at the moment. Update it to correctly use a GGX distribution
	float transmissionEval( vec3 wo, vec3 wi, vec3 wh, SurfaceRecord surf, inout vec3 color ) {

		color = surf.transmission * surf.color;

		// PDF
		// float F = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
		// float F = disneyFresnel( wo, wi, wh, surf.f0, surf.eta, surf.metalness );
		// if ( F >= 1.0 ) {

		// 	return 0.0;

		// }

		// return 1.0 / ( 1.0 - F );

		// reverted to previous to transmission. The above was causing black pixels
		float eta = surf.eta;
		float f0 = surf.f0;
		float cosTheta = min( wo.z, 1.0 );
		float sinTheta = sqrt( 1.0 - cosTheta * cosTheta );
		float reflectance = schlickFresnel( cosTheta, f0 );
		bool cannotRefract = eta * sinTheta > 1.0;
		if ( cannotRefract ) {

			return 0.0;

		}

		return 1.0 / ( 1.0 - reflectance );

	}

	vec3 transmissionDirection( vec3 wo, SurfaceRecord surf ) {

		float roughness = surf.filteredRoughness;
		float eta = surf.eta;
		vec3 halfVector = normalize( vec3( 0.0, 0.0, 1.0 ) + sampleSphere( rand2( 13 ) ) * roughness );
		vec3 lightDirection = refract( normalize( - wo ), halfVector, eta );

		if ( surf.thinFilm ) {

			lightDirection = - refract( normalize( - lightDirection ), - vec3( 0.0, 0.0, 1.0 ), 1.0 / eta );

		}
		return normalize( lightDirection );

	}

	// clearcoat
	float clearcoatEval( vec3 wo, vec3 wi, vec3 wh, SurfaceRecord surf, inout vec3 color ) {

		float ior = 1.5;
		float f0 = iorRatioToF0( ior );
		bool frontFace = surf.frontFace;
		float roughness = surf.filteredClearcoatRoughness;

		float eta = frontFace ? 1.0 / ior : ior;
		float G = ggxShadowMaskG2( wi, wo, roughness );
		float D = ggxDistribution( wh, roughness );
		float F = schlickFresnel( dot( wi, wh ), f0 );

		float fClearcoat = F * D * G / ( 4.0 * abs( wi.z * wo.z ) );
		color = color * ( 1.0 - surf.clearcoat * F ) + fClearcoat * surf.clearcoat * wi.z;

		// PDF
		// See equation (27) in http://jcgt.org/published/0003/02/03/
		return ggxPDF( wo, wh, roughness ) / ( 4.0 * dot( wi, wh ) );

	}

	vec3 clearcoatDirection( vec3 wo, SurfaceRecord surf ) {

		// sample ggx vndf distribution which gives a new normal
		float roughness = surf.filteredClearcoatRoughness;
		vec3 halfVector = ggxDirection(
			wo,
			vec2( roughness ),
			rand2( 14 )
		);

		// apply to new ray by reflecting off the new normal
		return - reflect( wo, halfVector );

	}

	// sheen
	vec3 sheenColor( vec3 wo, vec3 wi, vec3 wh, SurfaceRecord surf ) {

		float cosThetaO = saturateCos( wo.z );
		float cosThetaI = saturateCos( wi.z );
		float cosThetaH = wh.z;

		float D = velvetD( cosThetaH, surf.sheenRoughness );
		float G = velvetG( cosThetaO, cosThetaI, surf.sheenRoughness );

		// See equation (1) in http://www.aconty.com/pdf/s2017_pbs_imageworks_sheen.pdf
		vec3 color = surf.sheenColor;
		color *= D * G / ( 4.0 * abs( cosThetaO * cosThetaI ) );
		color *= wi.z;

		return color;

	}

	// bsdf
	void getLobeWeights(
		vec3 wo, vec3 wi, vec3 wh, vec3 clearcoatWo, SurfaceRecord surf,
		inout float diffuseWeight, inout float specularWeight, inout float transmissionWeight, inout float clearcoatWeight
	) {

		float metalness = surf.metalness;
		float transmission = surf.transmission;
		// float fEstimate = evaluateFresnelWeight( dot( wo, wh ), surf.eta, surf.f0 );
		float fEstimate = disneyFresnel( wo, wi, wh, surf.f0, surf.eta, surf.metalness );

		float transSpecularProb = mix( max( 0.25, fEstimate ), 1.0, metalness );
		float diffSpecularProb = 0.5 + 0.5 * metalness;

		diffuseWeight = ( 1.0 - transmission ) * ( 1.0 - diffSpecularProb );
		specularWeight = transmission * transSpecularProb + ( 1.0 - transmission ) * diffSpecularProb;
		transmissionWeight = transmission * ( 1.0 - transSpecularProb );
		clearcoatWeight = surf.clearcoat * schlickFresnel( clearcoatWo.z, 0.04 );

		float totalWeight = diffuseWeight + specularWeight + transmissionWeight + clearcoatWeight;
		diffuseWeight /= totalWeight;
		specularWeight /= totalWeight;
		transmissionWeight /= totalWeight;
		clearcoatWeight /= totalWeight;
	}

	float bsdfEval(
		vec3 wo, vec3 clearcoatWo, vec3 wi, vec3 clearcoatWi, SurfaceRecord surf,
		float diffuseWeight, float specularWeight, float transmissionWeight, float clearcoatWeight, inout float specularPdf, inout vec3 color
	) {

		float metalness = surf.metalness;
		float transmission = surf.transmission;

		float spdf = 0.0;
		float dpdf = 0.0;
		float tpdf = 0.0;
		float cpdf = 0.0;
		color = vec3( 0.0 );

		vec3 halfVector = getHalfVector( wi, wo, surf.eta );

		// diffuse
		if ( diffuseWeight > 0.0 && wi.z > 0.0 ) {

			dpdf = diffuseEval( wo, wi, halfVector, surf, color );
			color *= 1.0 - surf.transmission;

		}

		// ggx specular
		if ( specularWeight > 0.0 && wi.z > 0.0 ) {

			vec3 outColor;
			spdf = specularEval( wo, wi, getHalfVector( wi, wo ), surf, outColor );
			color += outColor;

		}

		// transmission
		if ( transmissionWeight > 0.0 && wi.z < 0.0 ) {

			tpdf = transmissionEval( wo, wi, halfVector, surf, color );

		}

		// sheen
		color *= mix( 1.0, sheenAlbedoScaling( wo, wi, surf ), surf.sheen );
		color += sheenColor( wo, wi, halfVector, surf ) * surf.sheen;

		// clearcoat
		if ( clearcoatWi.z >= 0.0 && clearcoatWeight > 0.0 ) {

			vec3 clearcoatHalfVector = getHalfVector( clearcoatWo, clearcoatWi );
			cpdf = clearcoatEval( clearcoatWo, clearcoatWi, clearcoatHalfVector, surf, color );

		}

		float pdf =
			dpdf * diffuseWeight
			+ spdf * specularWeight
			+ tpdf * transmissionWeight
			+ cpdf * clearcoatWeight;

		// retrieve specular rays for the shadows flag
		specularPdf = spdf * specularWeight + cpdf * clearcoatWeight;

		return pdf;

	}

	float bsdfResult( vec3 worldWo, vec3 worldWi, SurfaceRecord surf, inout vec3 color ) {

		if ( surf.volumeParticle ) {

			color = surf.color / ( 4.0 * PI );
			return 1.0 / ( 4.0 * PI );

		}

		vec3 wo = normalize( surf.normalInvBasis * worldWo );
		vec3 wi = normalize( surf.normalInvBasis * worldWi );

		vec3 clearcoatWo = normalize( surf.clearcoatInvBasis * worldWo );
		vec3 clearcoatWi = normalize( surf.clearcoatInvBasis * worldWi );

		vec3 wh = getHalfVector( wo, wi, surf.eta );
		float diffuseWeight;
		float specularWeight;
		float transmissionWeight;
		float clearcoatWeight;
		getLobeWeights( wo, wi, wh, clearcoatWo, surf, diffuseWeight, specularWeight, transmissionWeight, clearcoatWeight );

		float specularPdf;
		return bsdfEval( wo, clearcoatWo, wi, clearcoatWi, surf, diffuseWeight, specularWeight, transmissionWeight, clearcoatWeight, specularPdf, color );

	}

	ScatterRecord bsdfSample( vec3 worldWo, SurfaceRecord surf ) {

		if ( surf.volumeParticle ) {

			ScatterRecord sampleRec;
			sampleRec.specularPdf = 0.0;
			sampleRec.pdf = 1.0 / ( 4.0 * PI );
			sampleRec.direction = sampleSphere( rand2( 16 ) );
			sampleRec.color = surf.color / ( 4.0 * PI );
			return sampleRec;

		}

		vec3 wo = normalize( surf.normalInvBasis * worldWo );
		vec3 clearcoatWo = normalize( surf.clearcoatInvBasis * worldWo );
		mat3 normalBasis = surf.normalBasis;
		mat3 invBasis = surf.normalInvBasis;
		mat3 clearcoatNormalBasis = surf.clearcoatBasis;
		mat3 clearcoatInvBasis = surf.clearcoatInvBasis;

		float diffuseWeight;
		float specularWeight;
		float transmissionWeight;
		float clearcoatWeight;
		// using normal and basically-reflected ray since we don't have proper half vector here
		getLobeWeights( wo, wo, vec3( 0, 0, 1 ), clearcoatWo, surf, diffuseWeight, specularWeight, transmissionWeight, clearcoatWeight );

		float pdf[4];
		pdf[0] = diffuseWeight;
		pdf[1] = specularWeight;
		pdf[2] = transmissionWeight;
		pdf[3] = clearcoatWeight;

		float cdf[4];
		cdf[0] = pdf[0];
		cdf[1] = pdf[1] + cdf[0];
		cdf[2] = pdf[2] + cdf[1];
		cdf[3] = pdf[3] + cdf[2];

		if( cdf[3] != 0.0 ) {

			float invMaxCdf = 1.0 / cdf[3];
			cdf[0] *= invMaxCdf;
			cdf[1] *= invMaxCdf;
			cdf[2] *= invMaxCdf;
			cdf[3] *= invMaxCdf;

		} else {

			cdf[0] = 1.0;
			cdf[1] = 0.0;
			cdf[2] = 0.0;
			cdf[3] = 0.0;

		}

		vec3 wi;
		vec3 clearcoatWi;

		float r = rand( 15 );
		if ( r <= cdf[0] ) { // diffuse

			wi = diffuseDirection( wo, surf );
			clearcoatWi = normalize( clearcoatInvBasis * normalize( normalBasis * wi ) );

		} else if ( r <= cdf[1] ) { // specular

			wi = specularDirection( wo, surf );
			clearcoatWi = normalize( clearcoatInvBasis * normalize( normalBasis * wi ) );

		} else if ( r <= cdf[2] ) { // transmission / refraction

			wi = transmissionDirection( wo, surf );
			clearcoatWi = normalize( clearcoatInvBasis * normalize( normalBasis * wi ) );

		} else if ( r <= cdf[3] ) { // clearcoat

			clearcoatWi = clearcoatDirection( clearcoatWo, surf );
			wi = normalize( invBasis * normalize( clearcoatNormalBasis * clearcoatWi ) );

		}

		ScatterRecord result;
		result.pdf = bsdfEval( wo, clearcoatWo, wi, clearcoatWi, surf, diffuseWeight, specularWeight, transmissionWeight, clearcoatWeight, result.specularPdf, result.color );
		result.direction = normalize( surf.normalBasis * wi );

		return result;

	}

`;

const fog_functions = /* glsl */`

	// returns the hit distance given the material density
	float intersectFogVolume( Material material, float u ) {

		// https://raytracing.github.io/books/RayTracingTheNextWeek.html#volumes/constantdensitymediums
		return material.opacity == 0.0 ? INFINITY : ( - 1.0 / material.opacity ) * log( u );

	}

	ScatterRecord sampleFogVolume( SurfaceRecord surf, vec2 uv ) {

		ScatterRecord sampleRec;
		sampleRec.specularPdf = 0.0;
		sampleRec.pdf = 1.0 / ( 2.0 * PI );
		sampleRec.direction = sampleSphere( uv );
		sampleRec.color = surf.color;
		return sampleRec;

	}

`;

const ggx_functions = /* glsl */`

	// The GGX functions provide sampling and distribution information for normals as output so
	// in order to get probability of scatter direction the half vector must be computed and provided.
	// [0] https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf
	// [1] https://hal.archives-ouvertes.fr/hal-01509746/document
	// [2] http://jcgt.org/published/0007/04/01/
	// [4] http://jcgt.org/published/0003/02/03/

	// trowbridge-reitz === GGX === GTR

	vec3 ggxDirection( vec3 incidentDir, vec2 roughness, vec2 uv ) {

		// TODO: try GGXVNDF implementation from reference [2], here. Needs to update ggxDistribution
		// function below, as well

		// Implementation from reference [1]
		// stretch view
		vec3 V = normalize( vec3( roughness * incidentDir.xy, incidentDir.z ) );

		// orthonormal basis
		vec3 T1 = ( V.z < 0.9999 ) ? normalize( cross( V, vec3( 0.0, 0.0, 1.0 ) ) ) : vec3( 1.0, 0.0, 0.0 );
		vec3 T2 = cross( T1, V );

		// sample point with polar coordinates (r, phi)
		float a = 1.0 / ( 1.0 + V.z );
		float r = sqrt( uv.x );
		float phi = ( uv.y < a ) ? uv.y / a * PI : PI + ( uv.y - a ) / ( 1.0 - a ) * PI;
		float P1 = r * cos( phi );
		float P2 = r * sin( phi ) * ( ( uv.y < a ) ? 1.0 : V.z );

		// compute normal
		vec3 N = P1 * T1 + P2 * T2 + V * sqrt( max( 0.0, 1.0 - P1 * P1 - P2 * P2 ) );

		// unstretch
		N = normalize( vec3( roughness * N.xy, max( 0.0, N.z ) ) );

		return N;

	}

	// Below are PDF and related functions for use in a Monte Carlo path tracer
	// as specified in Appendix B of the following paper
	// See equation (34) from reference [0]
	float ggxLamda( float theta, float roughness ) {

		float tanTheta = tan( theta );
		float tanTheta2 = tanTheta * tanTheta;
		float alpha2 = roughness * roughness;

		float numerator = - 1.0 + sqrt( 1.0 + alpha2 * tanTheta2 );
		return numerator / 2.0;

	}

	// See equation (34) from reference [0]
	float ggxShadowMaskG1( float theta, float roughness ) {

		return 1.0 / ( 1.0 + ggxLamda( theta, roughness ) );

	}

	// See equation (125) from reference [4]
	float ggxShadowMaskG2( vec3 wi, vec3 wo, float roughness ) {

		float incidentTheta = acos( wi.z );
		float scatterTheta = acos( wo.z );
		return 1.0 / ( 1.0 + ggxLamda( incidentTheta, roughness ) + ggxLamda( scatterTheta, roughness ) );

	}

	// See equation (33) from reference [0]
	float ggxDistribution( vec3 halfVector, float roughness ) {

		float a2 = roughness * roughness;
		a2 = max( EPSILON, a2 );
		float cosTheta = halfVector.z;
		float cosTheta4 = pow( cosTheta, 4.0 );

		if ( cosTheta == 0.0 ) return 0.0;

		float theta = acosSafe( halfVector.z );
		float tanTheta = tan( theta );
		float tanTheta2 = pow( tanTheta, 2.0 );

		float denom = PI * cosTheta4 * pow( a2 + tanTheta2, 2.0 );
		return ( a2 / denom );

	}

	// See equation (3) from reference [2]
	float ggxPDF( vec3 wi, vec3 halfVector, float roughness ) {

		float incidentTheta = acos( wi.z );
		float D = ggxDistribution( halfVector, roughness );
		float G1 = ggxShadowMaskG1( incidentTheta, roughness );

		return D * G1 * max( 0.0, dot( wi, halfVector ) ) / wi.z;

	}

`;

const iridescence_functions = /* glsl */`

	// XYZ to sRGB color space
	const mat3 XYZ_TO_REC709 = mat3(
		3.2404542, -0.9692660,  0.0556434,
		-1.5371385,  1.8760108, -0.2040259,
		-0.4985314,  0.0415560,  1.0572252
	);

	vec3 fresnel0ToIor( vec3 fresnel0 ) {

		vec3 sqrtF0 = sqrt( fresnel0 );
		return ( vec3( 1.0 ) + sqrtF0 ) / ( vec3( 1.0 ) - sqrtF0 );

	}

	// Conversion FO/IOR
	vec3 iorToFresnel0( vec3 transmittedIor, float incidentIor ) {

		return square( ( transmittedIor - vec3( incidentIor ) ) / ( transmittedIor + vec3( incidentIor ) ) );

	}

	// ior is a value between 1.0 and 3.0. 1.0 is air interface
	float iorToFresnel0( float transmittedIor, float incidentIor ) {

		return square( ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ) );

	}

	// Fresnel equations for dielectric/dielectric interfaces. See https://belcour.github.io/blog/research/2017/05/01/brdf-thin-film.html
	vec3 evalSensitivity( float OPD, vec3 shift ) {

		float phase = 2.0 * PI * OPD * 1.0e-9;

		vec3 val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );
		vec3 pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );
		vec3 var = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );

		vec3 xyz = val * sqrt( 2.0 * PI * var ) * cos( pos * phase + shift ) * exp( - square( phase ) * var );
		xyz.x += 9.7470e-14 * sqrt( 2.0 * PI * 4.5282e+09 ) * cos( 2.2399e+06 * phase + shift[ 0 ] ) * exp( - 4.5282e+09 * square( phase ) );
		xyz /= 1.0685e-7;

		vec3 srgb = XYZ_TO_REC709 * xyz;
		return srgb;

	}

	// See Section 4. Analytic Spectral Integration, A Practical Extension to Microfacet Theory for the Modeling of Varying Iridescence, https://hal.archives-ouvertes.fr/hal-01518344/document
	vec3 evalIridescence( float outsideIOR, float eta2, float cosTheta1, float thinFilmThickness, vec3 baseF0 ) {

		vec3 I;

		// Force iridescenceIor -> outsideIOR when thinFilmThickness -> 0.0
		float iridescenceIor = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );

		// Evaluate the cosTheta on the base layer (Snell law)
		float sinTheta2Sq = square( outsideIOR / iridescenceIor ) * ( 1.0 - square( cosTheta1 ) );

		// Handle TIR:
		float cosTheta2Sq = 1.0 - sinTheta2Sq;
		if ( cosTheta2Sq < 0.0 ) {

			return vec3( 1.0 );

		}

		float cosTheta2 = sqrt( cosTheta2Sq );

		// First interface
		float R0 = iorToFresnel0( iridescenceIor, outsideIOR );
		float R12 = schlickFresnel( cosTheta1, R0 );
		float R21 = R12;
		float T121 = 1.0 - R12;
		float phi12 = 0.0;
		if ( iridescenceIor < outsideIOR ) {

			phi12 = PI;

		}

		float phi21 = PI - phi12;

		// Second interface
		vec3 baseIOR = fresnel0ToIor( clamp( baseF0, 0.0, 0.9999 ) ); // guard against 1.0
		vec3 R1 = iorToFresnel0( baseIOR, iridescenceIor );
		vec3 R23 = schlickFresnel( cosTheta2, R1 );
		vec3 phi23 = vec3( 0.0 );
		if ( baseIOR[0] < iridescenceIor ) {

			phi23[ 0 ] = PI;

		}

		if ( baseIOR[1] < iridescenceIor ) {

			phi23[ 1 ] = PI;

		}

		if ( baseIOR[2] < iridescenceIor ) {

			phi23[ 2 ] = PI;

		}

		// Phase shift
		float OPD = 2.0 * iridescenceIor * thinFilmThickness * cosTheta2;
		vec3 phi = vec3( phi21 ) + phi23;

		// Compound terms
		vec3 R123 = clamp( R12 * R23, 1e-5, 0.9999 );
		vec3 r123 = sqrt( R123 );
		vec3 Rs = square( T121 ) * R23 / ( vec3( 1.0 ) - R123 );

		// Reflectance term for m = 0 (DC term amplitude)
		vec3 C0 = R12 + Rs;
		I = C0;

		// Reflectance term for m > 0 (pairs of diracs)
		vec3 Cm = Rs - T121;
		for ( int m = 1; m <= 2; ++ m ) {

			Cm *= r123;
			vec3 Sm = 2.0 * evalSensitivity( float( m ) * OPD, float( m ) * phi );
			I += Cm * Sm;

		}

		// Since out of gamut colors might be produced, negative color values are clamped to 0.
		return max( I, vec3( 0.0 ) );

	}

`;

const sheen_functions = /* glsl */`

	// See equation (2) in http://www.aconty.com/pdf/s2017_pbs_imageworks_sheen.pdf
	float velvetD( float cosThetaH, float roughness ) {

		float alpha = max( roughness, 0.07 );
		alpha = alpha * alpha;

		float invAlpha = 1.0 / alpha;

		float sqrCosThetaH = cosThetaH * cosThetaH;
		float sinThetaH = max( 1.0 - sqrCosThetaH, 0.001 );

		return ( 2.0 + invAlpha ) * pow( sinThetaH, 0.5 * invAlpha ) / ( 2.0 * PI );

	}

	float velvetParamsInterpolate( int i, float oneMinusAlphaSquared ) {

		const float p0[5] = float[5]( 25.3245, 3.32435, 0.16801, -1.27393, -4.85967 );
		const float p1[5] = float[5]( 21.5473, 3.82987, 0.19823, -1.97760, -4.32054 );

		return mix( p1[i], p0[i], oneMinusAlphaSquared );

	}

	float velvetL( float x, float alpha ) {

		float oneMinusAlpha = 1.0 - alpha;
		float oneMinusAlphaSquared = oneMinusAlpha * oneMinusAlpha;

		float a = velvetParamsInterpolate( 0, oneMinusAlphaSquared );
		float b = velvetParamsInterpolate( 1, oneMinusAlphaSquared );
		float c = velvetParamsInterpolate( 2, oneMinusAlphaSquared );
		float d = velvetParamsInterpolate( 3, oneMinusAlphaSquared );
		float e = velvetParamsInterpolate( 4, oneMinusAlphaSquared );

		return a / ( 1.0 + b * pow( abs( x ), c ) ) + d * x + e;

	}

	// See equation (3) in http://www.aconty.com/pdf/s2017_pbs_imageworks_sheen.pdf
	float velvetLambda( float cosTheta, float alpha ) {

		return abs( cosTheta ) < 0.5 ? exp( velvetL( cosTheta, alpha ) ) : exp( 2.0 * velvetL( 0.5, alpha ) - velvetL( 1.0 - cosTheta, alpha ) );

	}

	// See Section 3, Shadowing Term, in http://www.aconty.com/pdf/s2017_pbs_imageworks_sheen.pdf
	float velvetG( float cosThetaO, float cosThetaI, float roughness ) {

		float alpha = max( roughness, 0.07 );
		alpha = alpha * alpha;

		return 1.0 / ( 1.0 + velvetLambda( cosThetaO, alpha ) + velvetLambda( cosThetaI, alpha ) );

	}

	float directionalAlbedoSheen( float cosTheta, float alpha ) {

		cosTheta = saturate( cosTheta );

		float c = 1.0 - cosTheta;
		float c3 = c * c * c;

		return 0.65584461 * c3 + 1.0 / ( 4.16526551 + exp( -7.97291361 * sqrt( alpha ) + 6.33516894 ) );

	}

	float sheenAlbedoScaling( vec3 wo, vec3 wi, SurfaceRecord surf ) {

		float alpha = max( surf.sheenRoughness, 0.07 );
		alpha = alpha * alpha;

		float maxSheenColor = max( max( surf.sheenColor.r, surf.sheenColor.g ), surf.sheenColor.b );

		float eWo = directionalAlbedoSheen( saturateCos( wo.z ), alpha );
		float eWi = directionalAlbedoSheen( saturateCos( wi.z ), alpha );

		return min( 1.0 - maxSheenColor * eWo, 1.0 - maxSheenColor * eWi );

	}

	// See Section 5, Layering, in http://www.aconty.com/pdf/s2017_pbs_imageworks_sheen.pdf
	float sheenAlbedoScaling( vec3 wo, SurfaceRecord surf ) {

		float alpha = max( surf.sheenRoughness, 0.07 );
		alpha = alpha * alpha;

		float maxSheenColor = max( max( surf.sheenColor.r, surf.sheenColor.g ), surf.sheenColor.b );

		float eWo = directionalAlbedoSheen( saturateCos( wo.z ), alpha );

		return 1.0 - maxSheenColor * eWo;

	}

`;

const inside_fog_volume_function = /* glsl */`

#ifndef FOG_CHECK_ITERATIONS
#define FOG_CHECK_ITERATIONS 30
#endif

// returns whether the given material is a fog material or not
bool isMaterialFogVolume( sampler2D materials, uint materialIndex ) {

	uint i = materialIndex * 45u;
	vec4 s14 = texelFetch1D( materials, i + 14u );
	return bool( int( s14.b ) & 4 );

}

// returns true if we're within the first fog volume we hit
bool bvhIntersectFogVolumeHit(
	vec3 rayOrigin, vec3 rayDirection,
	usampler2D materialIndexAttribute, sampler2D materials,
	inout Material material
) {

	material.fogVolume = false;

	for ( int i = 0; i < FOG_CHECK_ITERATIONS; i ++ ) {

		// find nearest hit
		uvec4 faceIndices = uvec4( 0u );
		vec3 faceNormal = vec3( 0.0, 0.0, 1.0 );
		vec3 barycoord = vec3( 0.0 );
		float side = 1.0;
		float dist = 0.0;
		bool hit = bvhIntersectFirstHit( bvh, rayOrigin, rayDirection, faceIndices, faceNormal, barycoord, side, dist );
		if ( hit ) {

			// if it's a fog volume return whether we hit the front or back face
			uint materialIndex = uTexelFetch1D( materialIndexAttribute, faceIndices.x ).r;
			if ( isMaterialFogVolume( materials, materialIndex ) ) {

				material = readMaterialInfo( materials, materialIndex );
				return side == - 1.0;

			} else {

				// move the ray forward
				rayOrigin = stepRayOrigin( rayOrigin, rayDirection, - faceNormal, dist );

			}

		} else {

			return false;

		}

	}

	return false;

}

`;

const ray_any_hit_function = /* glsl */`

	bool bvhIntersectAnyHit(
		vec3 rayOrigin, vec3 rayDirection,

		// output variables
		inout float side, inout float dist
	) {

		uvec4 faceIndices;
		vec3 faceNormal;
		vec3 barycoord;

		// stack needs to be twice as long as the deepest tree we expect because
		// we push both the left and right child onto the stack every traversal
		int ptr = 0;
		uint stack[ 60 ];
		stack[ 0 ] = 0u;

		float triangleDistance = 1e20;
		while ( ptr > - 1 && ptr < 60 ) {

			uint currNodeIndex = stack[ ptr ];
			ptr --;

			// check if we intersect the current bounds
			float boundsHitDistance = intersectsBVHNodeBounds( rayOrigin, rayDirection, bvh, currNodeIndex );
			if ( boundsHitDistance == INFINITY ) {

				continue;

			}

			uvec2 boundsInfo = uTexelFetch1D( bvh.bvhContents, currNodeIndex ).xy;
			bool isLeaf = bool( boundsInfo.x & 0xffff0000u );

			if ( isLeaf ) {

				uint count = boundsInfo.x & 0x0000ffffu;
				uint offset = boundsInfo.y;

				bool found = intersectTriangles(
					bvh, rayOrigin, rayDirection, offset, count, triangleDistance,
					faceIndices, faceNormal, barycoord, side, dist
				);

				if ( found ) {

					return true;

				}

			} else {

				uint leftIndex = currNodeIndex + 1u;
				uint splitAxis = boundsInfo.x & 0x0000ffffu;
				uint rightIndex = boundsInfo.y;

				// set c2 in the stack so we traverse it later. We need to keep track of a pointer in
				// the stack while we traverse. The second pointer added is the one that will be
				// traversed first
				ptr ++;
				stack[ ptr ] = leftIndex;

				ptr ++;
				stack[ ptr ] = rightIndex;

			}

		}

		return false;

	}

`;

const attenuate_hit_function = /* glsl */`

	// step through multiple surface hits and accumulate color attenuation based on transmissive surfaces
	// returns true if a solid surface was hit
	bool attenuateHit(
		RenderState state,
		Ray ray, float rayDist,
		out vec3 color
	) {

		// store the original bounce index so we can reset it after
		uint originalBounceIndex = sobolBounceIndex;

		int traversals = state.traversals;
		int transmissiveTraversals = state.transmissiveTraversals;
		bool isShadowRay = state.isShadowRay;
		Material fogMaterial = state.fogMaterial;

		vec3 startPoint = ray.origin;

		// hit results
		SurfaceHit surfaceHit;

		color = vec3( 1.0 );

		bool result = true;
		for ( int i = 0; i < traversals; i ++ ) {

			sobolBounceIndex ++;

			int hitType = traceScene( ray, fogMaterial, surfaceHit );

			if ( hitType == FOG_HIT ) {

				result = true;
				break;

			} else if ( hitType == SURFACE_HIT ) {

				float totalDist = distance( startPoint, ray.origin + ray.direction * surfaceHit.dist );
				if ( totalDist > rayDist ) {

					result = false;
					break;

				}

				// TODO: attenuate the contribution based on the PDF of the resulting ray including refraction values
				// Should be able to work using the material BSDF functions which will take into account specularity, etc.
				// TODO: should we account for emissive surfaces here?

				uint materialIndex = uTexelFetch1D( materialIndexAttribute, surfaceHit.faceIndices.x ).r;
				Material material = readMaterialInfo( materials, materialIndex );

				// adjust the ray to the new surface
				bool isEntering = surfaceHit.side == 1.0;
				ray.origin = stepRayOrigin( ray.origin, ray.direction, - surfaceHit.faceNormal, surfaceHit.dist );

				#if FEATURE_FOG

				if ( material.fogVolume ) {

					fogMaterial = material;
					fogMaterial.fogVolume = surfaceHit.side == 1.0;
					i -= sign( transmissiveTraversals );
					transmissiveTraversals --;
					continue;

				}

				#endif

				if ( ! material.castShadow && isShadowRay ) {

					continue;

				}

				vec2 uv = textureSampleBarycoord( attributesArray, ATTR_UV, surfaceHit.barycoord, surfaceHit.faceIndices.xyz ).xy;
				vec4 vertexColor = textureSampleBarycoord( attributesArray, ATTR_COLOR, surfaceHit.barycoord, surfaceHit.faceIndices.xyz );

				// albedo
				vec4 albedo = vec4( material.color, material.opacity );
				if ( material.map != - 1 ) {

					vec3 uvPrime = material.mapTransform * vec3( uv, 1 );
					albedo *= texture2D( textures, vec3( uvPrime.xy, material.map ) );

				}

				if ( material.vertexColors ) {

					albedo *= vertexColor;

				}

				// alphaMap
				if ( material.alphaMap != - 1 ) {

					albedo.a *= texture2D( textures, vec3( uv, material.alphaMap ) ).x;

				}

				// transmission
				float transmission = material.transmission;
				if ( material.transmissionMap != - 1 ) {

					vec3 uvPrime = material.transmissionMapTransform * vec3( uv, 1 );
					transmission *= texture2D( textures, vec3( uvPrime.xy, material.transmissionMap ) ).r;

				}

				// metalness
				float metalness = material.metalness;
				if ( material.metalnessMap != - 1 ) {

					vec3 uvPrime = material.metalnessMapTransform * vec3( uv, 1 );
					metalness *= texture2D( textures, vec3( uvPrime.xy, material.metalnessMap ) ).b;

				}

				float alphaTest = material.alphaTest;
				bool useAlphaTest = alphaTest != 0.0;
				float transmissionFactor = ( 1.0 - metalness ) * transmission;
				if (
					transmissionFactor < rand( 9 ) && ! (
						// material sidedness
						material.side != 0.0 && surfaceHit.side == material.side

						// alpha test
						|| useAlphaTest && albedo.a < alphaTest

						// opacity
						|| material.transparent && ! useAlphaTest && albedo.a < rand( 10 )
					)
				) {

					result = true;
					break;

				}

				if ( surfaceHit.side == 1.0 && isEntering ) {

					// only attenuate by surface color on the way in
					color *= mix( vec3( 1.0 ), albedo.rgb, transmissionFactor );

				} else if ( surfaceHit.side == - 1.0 ) {

					// attenuate by medium once we hit the opposite side of the model
					color *= transmissionAttenuation( surfaceHit.dist, material.attenuationColor, material.attenuationDistance );

				}

				bool isTransmissiveRay = dot( ray.direction, surfaceHit.faceNormal * surfaceHit.side ) < 0.0;
				if ( ( isTransmissiveRay || isEntering ) && transmissiveTraversals > 0 ) {

					i -= sign( transmissiveTraversals );
					transmissiveTraversals --;

				}

			} else {

				result = false;
				break;

			}

		}

		// reset the bounce index
		sobolBounceIndex = originalBounceIndex;
		return result;

	}

`;

const camera_util_functions = /* glsl */`

	vec3 ndcToRayOrigin( vec2 coord ) {

		vec4 rayOrigin4 = cameraWorldMatrix * invProjectionMatrix * vec4( coord, - 1.0, 1.0 );
		return rayOrigin4.xyz / rayOrigin4.w;
	}

	Ray getCameraRay() {

		vec2 ssd = vec2( 1.0 ) / resolution;

		// Jitter the camera ray by finding a uv coordinate at a random sample
		// around this pixel's UV coordinate for AA
		vec2 ruv = rand2( 0 );
		vec2 jitteredUv = vUv + vec2( tentFilter( ruv.x ) * ssd.x, tentFilter( ruv.y ) * ssd.y );
		Ray ray;

		#if CAMERA_TYPE == 2

			// Equirectangular projection
			vec4 rayDirection4 = vec4( equirectUvToDirection( jitteredUv ), 0.0 );
			vec4 rayOrigin4 = vec4( 0.0, 0.0, 0.0, 1.0 );

			rayDirection4 = cameraWorldMatrix * rayDirection4;
			rayOrigin4 = cameraWorldMatrix * rayOrigin4;

			ray.direction = normalize( rayDirection4.xyz );
			ray.origin = rayOrigin4.xyz / rayOrigin4.w;

		#else

			// get [- 1, 1] normalized device coordinates
			vec2 ndc = 2.0 * jitteredUv - vec2( 1.0 );
			ray.origin = ndcToRayOrigin( ndc );

			#if CAMERA_TYPE == 1

				// Orthographic projection
				ray.direction = ( cameraWorldMatrix * vec4( 0.0, 0.0, - 1.0, 0.0 ) ).xyz;
				ray.direction = normalize( ray.direction );

			#else

				// Perspective projection
				ray.direction = normalize( mat3( cameraWorldMatrix ) * ( invProjectionMatrix * vec4( ndc, 0.0, 1.0 ) ).xyz );

			#endif

		#endif

		#if FEATURE_DOF
		{

			// depth of field
			vec3 focalPoint = ray.origin + normalize( ray.direction ) * physicalCamera.focusDistance;

			// get the aperture sample
			// if blades === 0 then we assume a circle
			vec3 shapeUVW= rand3( 1 );
			int blades = physicalCamera.apertureBlades;
			float anamorphicRatio = physicalCamera.anamorphicRatio;
			vec2 apertureSample = blades == 0 ? sampleCircle( shapeUVW.xy ) : sampleRegularPolygon( blades, shapeUVW );
			apertureSample *= physicalCamera.bokehSize * 0.5 * 1e-3;

			// rotate the aperture shape
			apertureSample =
				rotateVector( apertureSample, physicalCamera.apertureRotation ) *
				saturate( vec2( anamorphicRatio, 1.0 / anamorphicRatio ) );

			// create the new ray
			ray.origin += ( cameraWorldMatrix * vec4( apertureSample, 0.0, 0.0 ) ).xyz;
			ray.direction = focalPoint - ray.origin;

		}
		#endif

		ray.direction = normalize( ray.direction );

		return ray;

	}

`;

const direct_light_contribution_function = /*glsl*/`

	vec3 directLightContribution( vec3 worldWo, SurfaceRecord surf, RenderState state, vec3 rayOrigin ) {

		vec3 result = vec3( 0.0 );

		// uniformly pick a light or environment map
		if( lightsDenom != 0.0 && rand( 5 ) < float( lights.count ) / lightsDenom ) {

			// sample a light or environment
			LightRecord lightRec = randomLightSample( lights.tex, iesProfiles, lights.count, rayOrigin, rand3( 6 ) );

			bool isSampleBelowSurface = ! surf.volumeParticle && dot( surf.faceNormal, lightRec.direction ) < 0.0;
			if ( isSampleBelowSurface ) {

				lightRec.pdf = 0.0;

			}

			// check if a ray could even reach the light area
			Ray lightRay;
			lightRay.origin = rayOrigin;
			lightRay.direction = lightRec.direction;
			vec3 attenuatedColor;
			if (
				lightRec.pdf > 0.0 &&
				isDirectionValid( lightRec.direction, surf.normal, surf.faceNormal ) &&
				! attenuateHit( state, lightRay, lightRec.dist, attenuatedColor )
			) {

				// get the material pdf
				vec3 sampleColor;
				float lightMaterialPdf = bsdfResult( worldWo, lightRec.direction, surf, sampleColor );
				bool isValidSampleColor = all( greaterThanEqual( sampleColor, vec3( 0.0 ) ) );
				if ( lightMaterialPdf > 0.0 && isValidSampleColor ) {

					// weight the direct light contribution
					float lightPdf = lightRec.pdf / lightsDenom;
					float misWeight = lightRec.type == SPOT_LIGHT_TYPE || lightRec.type == DIR_LIGHT_TYPE || lightRec.type == POINT_LIGHT_TYPE ? 1.0 : misHeuristic( lightPdf, lightMaterialPdf );
					result = attenuatedColor * lightRec.emission * state.throughputColor * sampleColor * misWeight / lightPdf;

				}

			}

		} else if ( envMapInfo.totalSum != 0.0 && environmentIntensity != 0.0 ) {

			// find a sample in the environment map to include in the contribution
			vec3 envColor, envDirection;
			float envPdf = sampleEquirectProbability( rand2( 7 ), envColor, envDirection );
			envDirection = invEnvRotation3x3 * envDirection;

			// this env sampling is not set up for transmissive sampling and yields overly bright
			// results so we ignore the sample in this case.
			// TODO: this should be improved but how? The env samples could traverse a few layers?
			bool isSampleBelowSurface = ! surf.volumeParticle && dot( surf.faceNormal, envDirection ) < 0.0;
			if ( isSampleBelowSurface ) {

				envPdf = 0.0;

			}

			// check if a ray could even reach the surface
			Ray envRay;
			envRay.origin = rayOrigin;
			envRay.direction = envDirection;
			vec3 attenuatedColor;
			if (
				envPdf > 0.0 &&
				isDirectionValid( envDirection, surf.normal, surf.faceNormal ) &&
				! attenuateHit( state, envRay, INFINITY, attenuatedColor )
			) {

				// get the material pdf
				vec3 sampleColor;
				float envMaterialPdf = bsdfResult( worldWo, envDirection, surf, sampleColor );
				bool isValidSampleColor = all( greaterThanEqual( sampleColor, vec3( 0.0 ) ) );
				if ( envMaterialPdf > 0.0 && isValidSampleColor ) {

					// weight the direct light contribution
					envPdf /= lightsDenom;
					float misWeight = misHeuristic( envPdf, envMaterialPdf );
					result = attenuatedColor * environmentIntensity * envColor * state.throughputColor * sampleColor * misWeight / envPdf;

				}

			}

		}

		// Function changed to have a single return statement to potentially help with crashes on Mac OS.
		// See issue #470
		return result;

	}

`;

const get_surface_record_function = /* glsl */`

	#define SKIP_SURFACE 0
	#define HIT_SURFACE 1
	int getSurfaceRecord(
		Material material, SurfaceHit surfaceHit, sampler2DArray attributesArray,
		float accumulatedRoughness,
		inout SurfaceRecord surf
	) {

		if ( material.fogVolume ) {

			vec3 normal = vec3( 0, 0, 1 );

			SurfaceRecord fogSurface;
			fogSurface.volumeParticle = true;
			fogSurface.color = material.color;
			fogSurface.emission = material.emissiveIntensity * material.emissive;
			fogSurface.normal = normal;
			fogSurface.faceNormal = normal;
			fogSurface.clearcoatNormal = normal;

			surf = fogSurface;
			return HIT_SURFACE;

		}

		// uv coord for textures
		vec2 uv = textureSampleBarycoord( attributesArray, ATTR_UV, surfaceHit.barycoord, surfaceHit.faceIndices.xyz ).xy;
		vec4 vertexColor = textureSampleBarycoord( attributesArray, ATTR_COLOR, surfaceHit.barycoord, surfaceHit.faceIndices.xyz );

		// albedo
		vec4 albedo = vec4( material.color, material.opacity );
		if ( material.map != - 1 ) {

			vec3 uvPrime = material.mapTransform * vec3( uv, 1 );
			albedo *= texture2D( textures, vec3( uvPrime.xy, material.map ) );

		}

		if ( material.vertexColors ) {

			albedo *= vertexColor;

		}

		// alphaMap
		if ( material.alphaMap != - 1 ) {

			albedo.a *= texture2D( textures, vec3( uv, material.alphaMap ) ).x;

		}

		// possibly skip this sample if it's transparent, alpha test is enabled, or we hit the wrong material side
		// and it's single sided.
		// - alpha test is disabled when it === 0
		// - the material sidedness test is complicated because we want light to pass through the back side but still
		// be able to see the front side. This boolean checks if the side we hit is the front side on the first ray
		// and we're rendering the other then we skip it. Do the opposite on subsequent bounces to get incoming light.
		float alphaTest = material.alphaTest;
		bool useAlphaTest = alphaTest != 0.0;
		if (
			// material sidedness
			material.side != 0.0 && surfaceHit.side != material.side

			// alpha test
			|| useAlphaTest && albedo.a < alphaTest

			// opacity
			|| material.transparent && ! useAlphaTest && albedo.a < rand( 3 )
		) {

			return SKIP_SURFACE;

		}

		// fetch the interpolated smooth normal
		vec3 normal = normalize( textureSampleBarycoord(
			attributesArray,
			ATTR_NORMAL,
			surfaceHit.barycoord,
			surfaceHit.faceIndices.xyz
		).xyz );

		// roughness
		float roughness = material.roughness;
		if ( material.roughnessMap != - 1 ) {

			vec3 uvPrime = material.roughnessMapTransform * vec3( uv, 1 );
			roughness *= texture2D( textures, vec3( uvPrime.xy, material.roughnessMap ) ).g;

		}

		// metalness
		float metalness = material.metalness;
		if ( material.metalnessMap != - 1 ) {

			vec3 uvPrime = material.metalnessMapTransform * vec3( uv, 1 );
			metalness *= texture2D( textures, vec3( uvPrime.xy, material.metalnessMap ) ).b;

		}

		// emission
		vec3 emission = material.emissiveIntensity * material.emissive;
		if ( material.emissiveMap != - 1 ) {

			vec3 uvPrime = material.emissiveMapTransform * vec3( uv, 1 );
			emission *= texture2D( textures, vec3( uvPrime.xy, material.emissiveMap ) ).xyz;

		}

		// transmission
		float transmission = material.transmission;
		if ( material.transmissionMap != - 1 ) {

			vec3 uvPrime = material.transmissionMapTransform * vec3( uv, 1 );
			transmission *= texture2D( textures, vec3( uvPrime.xy, material.transmissionMap ) ).r;

		}

		// normal
		if ( material.flatShading ) {

			// if we're rendering a flat shaded object then use the face normals - the face normal
			// is provided based on the side the ray hits the mesh so flip it to align with the
			// interpolated vertex normals.
			normal = surfaceHit.faceNormal * surfaceHit.side;

		}

		vec3 baseNormal = normal;
		if ( material.normalMap != - 1 ) {

			vec4 tangentSample = textureSampleBarycoord(
				attributesArray,
				ATTR_TANGENT,
				surfaceHit.barycoord,
				surfaceHit.faceIndices.xyz
			);

			// some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
			// resulting in NaNs and slow path tracing.
			if ( length( tangentSample.xyz ) > 0.0 ) {

				vec3 tangent = normalize( tangentSample.xyz );
				vec3 bitangent = normalize( cross( normal, tangent ) * tangentSample.w );
				mat3 vTBN = mat3( tangent, bitangent, normal );

				vec3 uvPrime = material.normalMapTransform * vec3( uv, 1 );
				vec3 texNormal = texture2D( textures, vec3( uvPrime.xy, material.normalMap ) ).xyz * 2.0 - 1.0;
				texNormal.xy *= material.normalScale;
				normal = vTBN * texNormal;

			}

		}

		normal *= surfaceHit.side;

		// clearcoat
		float clearcoat = material.clearcoat;
		if ( material.clearcoatMap != - 1 ) {

			vec3 uvPrime = material.clearcoatMapTransform * vec3( uv, 1 );
			clearcoat *= texture2D( textures, vec3( uvPrime.xy, material.clearcoatMap ) ).r;

		}

		// clearcoatRoughness
		float clearcoatRoughness = material.clearcoatRoughness;
		if ( material.clearcoatRoughnessMap != - 1 ) {

			vec3 uvPrime = material.clearcoatRoughnessMapTransform * vec3( uv, 1 );
			clearcoatRoughness *= texture2D( textures, vec3( uvPrime.xy, material.clearcoatRoughnessMap ) ).g;

		}

		// clearcoatNormal
		vec3 clearcoatNormal = baseNormal;
		if ( material.clearcoatNormalMap != - 1 ) {

			vec4 tangentSample = textureSampleBarycoord(
				attributesArray,
				ATTR_TANGENT,
				surfaceHit.barycoord,
				surfaceHit.faceIndices.xyz
			);

			// some provided tangents can be malformed (0, 0, 0) causing the normal to be degenerate
			// resulting in NaNs and slow path tracing.
			if ( length( tangentSample.xyz ) > 0.0 ) {

				vec3 tangent = normalize( tangentSample.xyz );
				vec3 bitangent = normalize( cross( clearcoatNormal, tangent ) * tangentSample.w );
				mat3 vTBN = mat3( tangent, bitangent, clearcoatNormal );

				vec3 uvPrime = material.clearcoatNormalMapTransform * vec3( uv, 1 );
				vec3 texNormal = texture2D( textures, vec3( uvPrime.xy, material.clearcoatNormalMap ) ).xyz * 2.0 - 1.0;
				texNormal.xy *= material.clearcoatNormalScale;
				clearcoatNormal = vTBN * texNormal;

			}

		}

		clearcoatNormal *= surfaceHit.side;

		// sheenColor
		vec3 sheenColor = material.sheenColor;
		if ( material.sheenColorMap != - 1 ) {

			vec3 uvPrime = material.sheenColorMapTransform * vec3( uv, 1 );
			sheenColor *= texture2D( textures, vec3( uvPrime.xy, material.sheenColorMap ) ).rgb;

		}

		// sheenRoughness
		float sheenRoughness = material.sheenRoughness;
		if ( material.sheenRoughnessMap != - 1 ) {

			vec3 uvPrime = material.sheenRoughnessMapTransform * vec3( uv, 1 );
			sheenRoughness *= texture2D( textures, vec3( uvPrime.xy, material.sheenRoughnessMap ) ).a;

		}

		// iridescence
		float iridescence = material.iridescence;
		if ( material.iridescenceMap != - 1 ) {

			vec3 uvPrime = material.iridescenceMapTransform * vec3( uv, 1 );
			iridescence *= texture2D( textures, vec3( uvPrime.xy, material.iridescenceMap ) ).r;

		}

		// iridescence thickness
		float iridescenceThickness = material.iridescenceThicknessMaximum;
		if ( material.iridescenceThicknessMap != - 1 ) {

			vec3 uvPrime = material.iridescenceThicknessMapTransform * vec3( uv, 1 );
			float iridescenceThicknessSampled = texture2D( textures, vec3( uvPrime.xy, material.iridescenceThicknessMap ) ).g;
			iridescenceThickness = mix( material.iridescenceThicknessMinimum, material.iridescenceThicknessMaximum, iridescenceThicknessSampled );

		}

		iridescence = iridescenceThickness == 0.0 ? 0.0 : iridescence;

		// specular color
		vec3 specularColor = material.specularColor;
		if ( material.specularColorMap != - 1 ) {

			vec3 uvPrime = material.specularColorMapTransform * vec3( uv, 1 );
			specularColor *= texture2D( textures, vec3( uvPrime.xy, material.specularColorMap ) ).rgb;

		}

		// specular intensity
		float specularIntensity = material.specularIntensity;
		if ( material.specularIntensityMap != - 1 ) {

			vec3 uvPrime = material.specularIntensityMapTransform * vec3( uv, 1 );
			specularIntensity *= texture2D( textures, vec3( uvPrime.xy, material.specularIntensityMap ) ).a;

		}

		surf.volumeParticle = false;

		surf.faceNormal = surfaceHit.faceNormal;
		surf.normal = normal;

		surf.metalness = metalness;
		surf.color = albedo.rgb;
		surf.emission = emission;

		surf.ior = material.ior;
		surf.transmission = transmission;
		surf.thinFilm = material.thinFilm;
		surf.attenuationColor = material.attenuationColor;
		surf.attenuationDistance = material.attenuationDistance;

		surf.clearcoatNormal = clearcoatNormal;
		surf.clearcoat = clearcoat;

		surf.sheen = material.sheen;
		surf.sheenColor = sheenColor;

		surf.iridescence = iridescence;
		surf.iridescenceIor = material.iridescenceIor;
		surf.iridescenceThickness = iridescenceThickness;

		surf.specularColor = specularColor;
		surf.specularIntensity = specularIntensity;

		// apply perceptual roughness factor from gltf. sheen perceptual roughness is
		// applied by its brdf function
		// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#microfacet-surfaces
		surf.roughness = roughness * roughness;
		surf.clearcoatRoughness = clearcoatRoughness * clearcoatRoughness;
		surf.sheenRoughness = sheenRoughness;

		// frontFace is used to determine transmissive properties and PDF. If no transmission is used
		// then we can just always assume this is a front face.
		surf.frontFace = surfaceHit.side == 1.0 || transmission == 0.0;
		surf.eta = material.thinFilm || surf.frontFace ? 1.0 / material.ior : material.ior;
		surf.f0 = iorRatioToF0( surf.eta );

		// Compute the filtered roughness value to use during specular reflection computations.
		// The accumulated roughness value is scaled by a user setting and a "magic value" of 5.0.
		// If we're exiting something transmissive then scale the factor down significantly so we can retain
		// sharp internal reflections
		surf.filteredRoughness = applyFilteredGlossy( surf.roughness, accumulatedRoughness );
		surf.filteredClearcoatRoughness = applyFilteredGlossy( surf.clearcoatRoughness, accumulatedRoughness );

		// get the normal frames
		surf.normalBasis = getBasisFromNormal( surf.normal );
		surf.normalInvBasis = inverse( surf.normalBasis );

		surf.clearcoatBasis = getBasisFromNormal( surf.clearcoatNormal );
		surf.clearcoatInvBasis = inverse( surf.clearcoatBasis );

		return HIT_SURFACE;

	}
`;

const render_structs = /* glsl */`

	struct Ray {

		vec3 origin;
		vec3 direction;

	};

	struct SurfaceHit {

		uvec4 faceIndices;
		vec3 barycoord;
		vec3 faceNormal;
		float side;
		float dist;

	};

	struct RenderState {

		bool firstRay;
		bool transmissiveRay;
		bool isShadowRay;
		float accumulatedRoughness;
		int transmissiveTraversals;
		int traversals;
		uint depth;
		vec3 throughputColor;
		Material fogMaterial;

	};

	RenderState initRenderState() {

		RenderState result;
		result.firstRay = true;
		result.transmissiveRay = true;
		result.isShadowRay = false;
		result.accumulatedRoughness = 0.0;
		result.transmissiveTraversals = 0;
		result.traversals = 0;
		result.throughputColor = vec3( 1.0 );
		result.depth = 0u;
		result.fogMaterial.fogVolume = false;
		return result;

	}

`;

const trace_scene_function = /* glsl */`

	#define NO_HIT 0
	#define SURFACE_HIT 1
	#define LIGHT_HIT 2
	#define FOG_HIT 3

	// Passing the global variable 'lights' into this function caused shader program errors.
	// So global variables like 'lights' and 'bvh' were moved out of the function parameters.
	// For more information, refer to: https://github.com/gkjohnson/three-gpu-pathtracer/pull/457
	int traceScene(
		Ray ray, Material fogMaterial, inout SurfaceHit surfaceHit
	) {

		int result = NO_HIT;
		bool hit = bvhIntersectFirstHit( bvh, ray.origin, ray.direction, surfaceHit.faceIndices, surfaceHit.faceNormal, surfaceHit.barycoord, surfaceHit.side, surfaceHit.dist );

		#if FEATURE_FOG

		if ( fogMaterial.fogVolume ) {

			// offset the distance so we don't run into issues with particles on the same surface
			// as other objects
			float particleDist = intersectFogVolume( fogMaterial, rand( 1 ) );
			if ( particleDist + RAY_OFFSET < surfaceHit.dist ) {

				surfaceHit.side = 1.0;
				surfaceHit.faceNormal = normalize( - ray.direction );
				surfaceHit.dist = particleDist;
				return FOG_HIT;

			}

		}

		#endif

		if ( hit ) {

			result = SURFACE_HIT;

		}

		return result;

	}

`;

class PhysicalPathTracingMaterial extends MaterialBase {

	onBeforeRender () {

		this.setDefine('FEATURE_DOF', this.physicalCamera.bokehSize === 0 ? 0 : 1);
		this.setDefine('FEATURE_BACKGROUND_MAP', this.backgroundMap ? 1 : 0);
		this.setDefine('FEATURE_FOG', this.materials.features.isUsed('FOG') ? 1 : 0);

	}

	constructor(parameters) {

		super({

			transparent: true,
			depthWrite: false,

			defines: {
				FEATURE_MIS: 1,
				FEATURE_RUSSIAN_ROULETTE: 1,
				FEATURE_DOF: 1,
				FEATURE_BACKGROUND_MAP: 0,
				FEATURE_FOG: 1,

				// 0 = PCG
				// 1 = Sobol
				// 2 = Stratified List
				RANDOM_TYPE: 2,

				// 0 = Perspective
				// 1 = Orthographic
				// 2 = Equirectangular
				CAMERA_TYPE: 0,

				DEBUG_MODE: 0,

				ATTR_NORMAL: 0,
				ATTR_TANGENT: 1,
				ATTR_UV: 2,
				ATTR_COLOR: 3,
			},

			uniforms: {

				// path trace uniforms
				resolution: { value: new Vector2() },
				opacity: { value: 1 },
				bounces: { value: 10 },
				transmissiveBounces: { value: 10 },
				filterGlossyFactor: { value: 0 },

				// camera uniforms
				physicalCamera: { value: new PhysicalCameraUniform() },
				cameraWorldMatrix: { value: new Matrix4() },
				invProjectionMatrix: { value: new Matrix4() },

				// scene uniforms
				bvh: { value: new MeshBVHUniformStruct() },
				attributesArray: { value: new AttributesTextureArray() },
				materialIndexAttribute: { value: new UIntVertexAttributeTexture() },
				materials: { value: new MaterialsTexture() },
				textures: { value: new RenderTarget2DArray().texture },

				// light uniforms
				lights: { value: new LightsInfoUniformStruct() },
				iesProfiles: {
					value: new RenderTarget2DArray(360, 180, {
						type: HalfFloatType,
						wrapS: ClampToEdgeWrapping,
						wrapT: ClampToEdgeWrapping,
					}).texture
				},
				environmentIntensity: { value: 1.0 },
				environmentRotation: { value: new Matrix4() },
				envMapInfo: { value: new EquirectHdrInfoUniform() },

				// background uniforms
				backgroundBlur: { value: 0.0 },
				backgroundMap: { value: null },
				backgroundAlpha: { value: 1.0 },
				backgroundIntensity: { value: 1.0 },
				backgroundRotation: { value: new Matrix4() },

				// randomness uniforms
				seed: { value: 0 },
				sobolTexture: { value: null },
				stratifiedTexture: { value: new StratifiedSamplesTexture() },
				stratifiedOffsetTexture: { value: new BlueNoiseTexture(64, 1) },
			},

			vertexShader: /* glsl */`

				varying vec2 vUv;
				void main() {

					vec4 mvPosition = vec4( position, 1.0 );
					mvPosition = modelViewMatrix * mvPosition;
					gl_Position = projectionMatrix * mvPosition;

					vUv = uv;

				}

			`,

			fragmentShader: /* glsl */`
				#define RAY_OFFSET 1e-4
				#define INFINITY 1e20

				precision highp isampler2D;
				precision highp usampler2D;
				precision highp sampler2DArray;
				vec4 envMapTexelToLinear( vec4 a ) { return a; }
				#include <common>

				// bvh intersection
				${BVHShaderGLSL.common_functions}
				${BVHShaderGLSL.bvh_struct_definitions}
				${BVHShaderGLSL.bvh_ray_functions}

				// uniform structs
				${camera_struct}
				${lights_struct}
				${equirect_struct}
				${material_struct}
				${surface_record_struct}

				// random
				#if RANDOM_TYPE == 2 	// Stratified List

					${stratified_functions}

				#elif RANDOM_TYPE == 1 	// Sobol

					${pcg_functions}
					${sobol_common}
					${sobol_functions}

					#define rand(v) sobol(v)
					#define rand2(v) sobol2(v)
					#define rand3(v) sobol3(v)
					#define rand4(v) sobol4(v)

				#else 					// PCG

				${pcg_functions}

					// Using the sobol functions seems to break the the compiler on MacOS
					// - specifically the "sobolReverseBits" function.
					uint sobolPixelIndex = 0u;
					uint sobolPathIndex = 0u;
					uint sobolBounceIndex = 0u;

					#define rand(v) pcgRand()
					#define rand2(v) pcgRand2()
					#define rand3(v) pcgRand3()
					#define rand4(v) pcgRand4()

				#endif

				// common
				${texture_sample_functions}
				${fresnel_functions}
				${util_functions}
				${math_functions}
				${shape_intersection_functions}

				// environment
				uniform EquirectHdrInfo envMapInfo;
				uniform mat4 environmentRotation;
				uniform float environmentIntensity;

				// lighting
				uniform sampler2DArray iesProfiles;
				uniform LightsInfo lights;

				// background
				uniform float backgroundBlur;
				uniform float backgroundAlpha;
				#if FEATURE_BACKGROUND_MAP

				uniform sampler2D backgroundMap;
				uniform mat4 backgroundRotation;
				uniform float backgroundIntensity;

				#endif

				// camera
				uniform mat4 cameraWorldMatrix;
				uniform mat4 invProjectionMatrix;
				#if FEATURE_DOF

				uniform PhysicalCamera physicalCamera;

				#endif

				// geometry
				uniform sampler2DArray attributesArray;
				uniform usampler2D materialIndexAttribute;
				uniform sampler2D materials;
				uniform sampler2DArray textures;
				uniform BVH bvh;

				// path tracer
				uniform int bounces;
				uniform int transmissiveBounces;
				uniform float filterGlossyFactor;
				uniform int seed;

				// image
				uniform vec2 resolution;
				uniform float opacity;

				varying vec2 vUv;

				// globals
				mat3 envRotation3x3;
				mat3 invEnvRotation3x3;
				float lightsDenom;

				// sampling
				${shape_sampling_functions}
				${equirect_functions}
				${light_sampling_functions}

				${inside_fog_volume_function}
				${ggx_functions}
				${sheen_functions}
				${iridescence_functions}
				${fog_functions}
				${bsdf_functions}

				float applyFilteredGlossy( float roughness, float accumulatedRoughness ) {

					return clamp(
						max(
							roughness,
							accumulatedRoughness * filterGlossyFactor * 5.0 ),
						0.0,
						1.0
					);

				}

				vec3 sampleBackground( vec3 direction, vec2 uv ) {

					vec3 sampleDir = sampleHemisphere( direction, uv ) * 0.5 * backgroundBlur;

					#if FEATURE_BACKGROUND_MAP

					sampleDir = normalize( mat3( backgroundRotation ) * direction + sampleDir );
					return backgroundIntensity * sampleEquirectColor( backgroundMap, sampleDir );

					#else

					sampleDir = normalize( envRotation3x3 * direction + sampleDir );
					return environmentIntensity * sampleEquirectColor( envMapInfo.map, sampleDir );

					#endif

				}

				${render_structs}
				${camera_util_functions}
				${trace_scene_function}
				${attenuate_hit_function}
				${direct_light_contribution_function}
				${get_surface_record_function}

				void main() {

					// init
					rng_initialize( gl_FragCoord.xy, seed );
					sobolPixelIndex = ( uint( gl_FragCoord.x ) << 16 ) | uint( gl_FragCoord.y );
					sobolPathIndex = uint( seed );

					// get camera ray
					Ray ray = getCameraRay();

					// inverse environment rotation
					envRotation3x3 = mat3( environmentRotation );
					invEnvRotation3x3 = inverse( envRotation3x3 );
					lightsDenom =
						( environmentIntensity == 0.0 || envMapInfo.totalSum == 0.0 ) && lights.count != 0u ?
							float( lights.count ) :
							float( lights.count + 1u );

					// final color
					gl_FragColor = vec4( 0, 0, 0, 1 );

					// surface results
					SurfaceHit surfaceHit;
					ScatterRecord scatterRec;

					// path tracing state
					RenderState state = initRenderState();
					state.transmissiveTraversals = transmissiveBounces;
					#if FEATURE_FOG

					state.fogMaterial.fogVolume = bvhIntersectFogVolumeHit(
						ray.origin, - ray.direction,
						materialIndexAttribute, materials,
						state.fogMaterial
					);

					#endif

					for ( int i = 0; i < bounces; i ++ ) {

						sobolBounceIndex ++;

						state.depth ++;
						state.traversals = bounces - i;
						state.firstRay = i == 0 && state.transmissiveTraversals == transmissiveBounces;

						int hitType = traceScene( ray, state.fogMaterial, surfaceHit );

						// check if we intersect any lights and accumulate the light contribution
						// TODO: we can add support for light surface rendering in the else condition if we
						// add the ability to toggle visibility of the the light
						if ( ! state.firstRay && ! state.transmissiveRay ) {

							LightRecord lightRec;
							float lightDist = hitType == NO_HIT ? INFINITY : surfaceHit.dist;
							for ( uint i = 0u; i < lights.count; i ++ ) {

								if (
									intersectLightAtIndex( lights.tex, ray.origin, ray.direction, i, lightRec ) &&
									lightRec.dist < lightDist
								) {

									#if FEATURE_MIS

									// weight the contribution
									// NOTE: Only area lights are supported for forward sampling and can be hit
									float misWeight = misHeuristic( scatterRec.pdf, lightRec.pdf / lightsDenom );
									gl_FragColor.rgb += lightRec.emission * state.throughputColor * misWeight;

									#else

									gl_FragColor.rgb += lightRec.emission * state.throughputColor;

									#endif

								}

							}

						}

						if ( hitType == NO_HIT ) {

							if ( state.firstRay || state.transmissiveRay ) {

								gl_FragColor.rgb += sampleBackground( ray.direction, rand2( 2 ) ) * state.throughputColor;
								gl_FragColor.a = backgroundAlpha;

							} else {

								#if FEATURE_MIS

								// get the PDF of the hit envmap point
								vec3 envColor;
								float envPdf = sampleEquirect( envRotation3x3 * ray.direction, envColor );
								envPdf /= lightsDenom;

								// and weight the contribution
								float misWeight = misHeuristic( scatterRec.pdf, envPdf );
								gl_FragColor.rgb += environmentIntensity * envColor * state.throughputColor * misWeight;

								#else

								gl_FragColor.rgb +=
									environmentIntensity *
									sampleEquirectColor( envMapInfo.map, envRotation3x3 * ray.direction ) *
									state.throughputColor;

								#endif

							}
							break;

						}

						uint materialIndex = uTexelFetch1D( materialIndexAttribute, surfaceHit.faceIndices.x ).r;
						Material material = readMaterialInfo( materials, materialIndex );

						#if FEATURE_FOG

						if ( hitType == FOG_HIT ) {

							material = state.fogMaterial;
							state.accumulatedRoughness += 0.2;

						} else if ( material.fogVolume ) {

							state.fogMaterial = material;
							state.fogMaterial.fogVolume = surfaceHit.side == 1.0;

							ray.origin = stepRayOrigin( ray.origin, ray.direction, - surfaceHit.faceNormal, surfaceHit.dist );

							i -= sign( state.transmissiveTraversals );
							state.transmissiveTraversals -= sign( state.transmissiveTraversals );
							continue;

						}

						#endif

						// early out if this is a matte material
						if ( material.matte && state.firstRay ) {

							gl_FragColor = vec4( 0.0 );
							break;

						}

						// if we've determined that this is a shadow ray and we've hit an item with no shadow casting
						// then skip it
						if ( ! material.castShadow && state.isShadowRay ) {

							ray.origin = stepRayOrigin( ray.origin, ray.direction, - surfaceHit.faceNormal, surfaceHit.dist );
							continue;

						}

						SurfaceRecord surf;
						if (
							getSurfaceRecord(
								material, surfaceHit, attributesArray, state.accumulatedRoughness,
								surf
							) == SKIP_SURFACE
						) {

							// only allow a limited number of transparency discards otherwise we could
							// crash the context with too long a loop.
							i -= sign( state.transmissiveTraversals );
							state.transmissiveTraversals -= sign( state.transmissiveTraversals );

							ray.origin = stepRayOrigin( ray.origin, ray.direction, - surfaceHit.faceNormal, surfaceHit.dist );
							continue;

						}

						scatterRec = bsdfSample( - ray.direction, surf );
						state.isShadowRay = scatterRec.specularPdf < rand( 4 );

						bool isBelowSurface = ! surf.volumeParticle && dot( scatterRec.direction, surf.faceNormal ) < 0.0;
						vec3 hitPoint = stepRayOrigin( ray.origin, ray.direction, isBelowSurface ? - surf.faceNormal : surf.faceNormal, surfaceHit.dist );

						// next event estimation
						#if FEATURE_MIS

						gl_FragColor.rgb += directLightContribution( - ray.direction, surf, state, hitPoint );

						#endif

						// accumulate a roughness value to offset diffuse, specular, diffuse rays that have high contribution
						// to a single pixel resulting in fireflies
						// TODO: handle transmissive surfaces
						if ( ! surf.volumeParticle && ! isBelowSurface ) {

							// determine if this is a rough normal or not by checking how far off straight up it is
							vec3 halfVector = normalize( - ray.direction + scatterRec.direction );
							state.accumulatedRoughness += max(
								sin( acosApprox( dot( halfVector, surf.normal ) ) ),
								sin( acosApprox( dot( halfVector, surf.clearcoatNormal ) ) )
							);

							state.transmissiveRay = false;

						}

						// accumulate emissive color
						gl_FragColor.rgb += ( surf.emission * state.throughputColor );

						// skip the sample if our PDF or ray is impossible
						if ( scatterRec.pdf <= 0.0 || ! isDirectionValid( scatterRec.direction, surf.normal, surf.faceNormal ) ) {

							break;

						}

						// if we're bouncing around the inside a transmissive material then decrement
						// perform this separate from a bounce
						bool isTransmissiveRay = ! surf.volumeParticle && dot( scatterRec.direction, surf.faceNormal * surfaceHit.side ) < 0.0;
						if ( ( isTransmissiveRay || isBelowSurface ) && state.transmissiveTraversals > 0 ) {

							state.transmissiveTraversals --;
							i --;

						}

						//

						// handle throughput color transformation
						// attenuate the throughput color by the medium color
						if ( ! surf.frontFace ) {

							state.throughputColor *= transmissionAttenuation( surfaceHit.dist, surf.attenuationColor, surf.attenuationDistance );

						}

						#if FEATURE_RUSSIAN_ROULETTE

						// russian roulette path termination
						// https://www.arnoldrenderer.com/research/physically_based_shader_design_in_arnold.pdf
						uint minBounces = 3u;
						float depthProb = float( state.depth < minBounces );

						float rrProb = luminance( state.throughputColor * scatterRec.color / scatterRec.pdf );
						rrProb /= luminance( state.throughputColor );
						rrProb = sqrt( rrProb );
						rrProb = max( rrProb, depthProb );
						rrProb = min( rrProb, 1.0 );
						if ( rand( 8 ) > rrProb ) {

							break;

						}

						// perform sample clamping here to avoid bright pixels
						state.throughputColor *= min( 1.0 / rrProb, 20.0 );

						#endif

						// adjust the throughput and discard and exit if we find discard the sample if there are any NaNs
						state.throughputColor *= scatterRec.color / scatterRec.pdf;
						if ( any( isnan( state.throughputColor ) ) || any( isinf( state.throughputColor ) ) ) {

							break;

						}

						//

						// prepare for next ray
						ray.direction = scatterRec.direction;
						ray.origin = hitPoint;

					}

					gl_FragColor.a *= opacity;

					#if DEBUG_MODE == 1

					// output the number of rays checked in the path and number of
					// transmissive rays encountered.
					gl_FragColor.rgb = vec3(
						float( state.depth ),
						transmissiveBounces - state.transmissiveTraversals,
						0.0
					);
					gl_FragColor.a = 1.0;

					#endif

				}

			`

		});

		this.setValues(parameters);

	}

}

function* renderTask () {

	const {
		_renderer,
		_fsQuad,
		_blendQuad,
		_primaryTarget,
		_blendTargets,
		_sobolTarget,
		_subframe,
		alpha,
		material,
	} = this;
	const _ogScissor = new Vector4();
	const _ogViewport = new Vector4();

	const blendMaterial = _blendQuad.material;
	let [blendTarget1, blendTarget2] = _blendTargets;

	while (true) {

		if (alpha) {

			blendMaterial.opacity = this._opacityFactor / (this.samples + 1);
			material.blending = NoBlending;
			material.opacity = 1;

		} else {

			material.opacity = this._opacityFactor / (this.samples + 1);
			material.blending = NormalBlending;

		}

		const [subX, subY, subW, subH] = _subframe;

		const w = _primaryTarget.width;
		const h = _primaryTarget.height;
		material.resolution.set(w * subW, h * subH);
		material.sobolTexture = _sobolTarget.texture;
		material.stratifiedTexture.init(20, material.bounces + material.transmissiveBounces + 5);
		material.stratifiedTexture.next();
		material.seed++;

		const tilesX = this.tiles.x || 1;
		const tilesY = this.tiles.y || 1;
		const totalTiles = tilesX * tilesY;

		const pxSubW = Math.ceil(w * subW);
		const pxSubH = Math.ceil(h * subH);
		const pxSubX = Math.floor(subX * w);
		const pxSubY = Math.floor(subY * h);

		const pxTileW = Math.ceil(pxSubW / tilesX);
		const pxTileH = Math.ceil(pxSubH / tilesY);

		for (let y = 0; y < tilesY; y++) {

			for (let x = 0; x < tilesX; x++) {

				// store og state
				const ogRenderTarget = _renderer.getRenderTarget();
				const ogAutoClear = _renderer.autoClear;
				const ogScissorTest = _renderer.getScissorTest();
				_renderer.getScissor(_ogScissor);
				_renderer.getViewport(_ogViewport);

				let tx = x;
				let ty = y;
				if (!this.stableTiles) {

					const tileIndex = (this._currentTile) % (tilesX * tilesY);
					tx = tileIndex % tilesX;
					ty = ~ ~(tileIndex / tilesX);

					this._currentTile = tileIndex + 1;

				}

				// set the scissor and the viewport on the render target
				// note that when using the webgl renderer set viewport the device pixel ratio
				// is multiplied into the field causing some pixels to not be rendered
				const reverseTy = tilesY - ty - 1;
				_primaryTarget.scissor.set(
					pxSubX + tx * pxTileW,
					pxSubY + reverseTy * pxTileH,
					Math.min(pxTileW, pxSubW - tx * pxTileW),
					Math.min(pxTileH, pxSubH - reverseTy * pxTileH),
				);

				_primaryTarget.viewport.set(
					pxSubX,
					pxSubY,
					pxSubW,
					pxSubH,
				);

				// three.js renderer takes values relative to the current pixel ratio
				_renderer.setRenderTarget(_primaryTarget);
				_renderer.setScissorTest(true);

				_renderer.autoClear = false;
				_fsQuad.render(_renderer);

				// reset original renderer state
				_renderer.setViewport(_ogViewport);
				_renderer.setScissor(_ogScissor);
				_renderer.setScissorTest(ogScissorTest);
				_renderer.setRenderTarget(ogRenderTarget);
				_renderer.autoClear = ogAutoClear;

				// swap and blend alpha targets
				if (alpha) {

					blendMaterial.target1 = blendTarget1.texture;
					blendMaterial.target2 = _primaryTarget.texture;

					_renderer.setRenderTarget(blendTarget2);
					_blendQuad.render(_renderer);
					_renderer.setRenderTarget(ogRenderTarget);

				}

				this.samples += (1 / totalTiles);

				// round the samples value if we've finished the tiles
				if (x === tilesX - 1 && y === tilesY - 1) {

					this.samples = Math.round(this.samples);

				}

				yield;

			}

		}

		[blendTarget1, blendTarget2] = [blendTarget2, blendTarget1];

	}

}

const ogClearColor = new Color();
class PathTracingRenderer {

	get material () {

		return this._fsQuad.material;

	}

	set material (v) {

		this._fsQuad.material.removeEventListener('recompilation', this._compileFunction);
		v.addEventListener('recompilation', this._compileFunction);

		this._fsQuad.material = v;

	}

	get target () {

		return this._alpha ? this._blendTargets[1] : this._primaryTarget;

	}

	set alpha (v) {

		if (this._alpha === v) {

			return;

		}

		if (!v) {

			this._blendTargets[0].dispose();
			this._blendTargets[1].dispose();

		}

		this._alpha = v;
		this.reset();

	}

	get alpha () {

		return this._alpha;

	}

	get isCompiling () {

		return Boolean(this._compilePromise);

	}

	constructor(renderer) {

		this.camera = null;
		this.tiles = new Vector2(3, 3);

		this.stableNoise = false;
		this.stableTiles = true;

		this.samples = 0;
		this._subframe = new Vector4(0, 0, 1, 1);
		this._opacityFactor = 1.0;
		this._renderer = renderer;
		this._alpha = false;
		this._fsQuad = new FullScreenQuad(new PhysicalPathTracingMaterial());
		this._blendQuad = new FullScreenQuad(new BlendMaterial());
		this._task = null;
		this._currentTile = 0;
		this._compilePromise = null;

		this._sobolTarget = new SobolNumberMapGenerator().generate(renderer);

		this._primaryTarget = new WebGLRenderTarget(1, 1, {
			format: RGBAFormat,
			type: FloatType,
			magFilter: NearestFilter,
			minFilter: NearestFilter,
		});
		this._blendTargets = [
			new WebGLRenderTarget(1, 1, {
				format: RGBAFormat,
				type: FloatType,
				magFilter: NearestFilter,
				minFilter: NearestFilter,
			}),
			new WebGLRenderTarget(1, 1, {
				format: RGBAFormat,
				type: FloatType,
				magFilter: NearestFilter,
				minFilter: NearestFilter,
			}),
		];

		// function for listening to for triggered compilation so we can wait for compilation to finish
		// before starting to render
		this._compileFunction = () => {

			const promise = this.compileMaterial(this._fsQuad._mesh);
			promise.then(() => {

				if (this._compilePromise === promise) {

					this._compilePromise = null;

				}

			});

			this._compilePromise = promise;

		};

		this.material.addEventListener('recompilation', this._compileFunction);

	}

	compileMaterial () {

		return this._renderer.compileAsync(this._fsQuad._mesh);

	}

	setCamera (camera) {

		const { material } = this;
		material.cameraWorldMatrix.copy(camera.matrixWorld);
		material.invProjectionMatrix.copy(camera.projectionMatrixInverse);
		material.physicalCamera.updateFrom(camera);

		// Perspective camera (default)
		let cameraType = 0;

		// An orthographic projection matrix will always have the bottom right element == 1
		// And a perspective projection matrix will always have the bottom right element == 0
		if (camera.projectionMatrix.elements[15] > 0) {

			// Orthographic
			cameraType = 1;

		}

		if (camera.isEquirectCamera) {

			// Equirectangular
			cameraType = 2;

		}

		material.setDefine('CAMERA_TYPE', cameraType);

		this.camera = camera;

	}

	setSize (w, h) {

		w = Math.ceil(w);
		h = Math.ceil(h);

		if (this._primaryTarget.width === w && this._primaryTarget.height === h) {

			return;

		}

		this._primaryTarget.setSize(w, h);
		this._blendTargets[0].setSize(w, h);
		this._blendTargets[1].setSize(w, h);
		this.reset();

	}

	getSize (target) {

		target.x = this._primaryTarget.width;
		target.y = this._primaryTarget.height;

	}

	dispose () {

		this._primaryTarget.dispose();
		this._blendTargets[0].dispose();
		this._blendTargets[1].dispose();
		this._sobolTarget.dispose();

		this._fsQuad.dispose();
		this._blendQuad.dispose();
		this._task = null;

	}

	reset () {

		const { _renderer, _primaryTarget, _blendTargets } = this;
		const ogRenderTarget = _renderer.getRenderTarget();
		const ogClearAlpha = _renderer.getClearAlpha();
		_renderer.getClearColor(ogClearColor);

		_renderer.setRenderTarget(_primaryTarget);
		_renderer.setClearColor(0, 0);
		_renderer.clearColor();

		_renderer.setRenderTarget(_blendTargets[0]);
		_renderer.setClearColor(0, 0);
		_renderer.clearColor();

		_renderer.setRenderTarget(_blendTargets[1]);
		_renderer.setClearColor(0, 0);
		_renderer.clearColor();

		_renderer.setClearColor(ogClearColor, ogClearAlpha);
		_renderer.setRenderTarget(ogRenderTarget);

		this.samples = 0;
		this._task = null;

		this.material.stratifiedTexture.stableNoise = this.stableNoise;
		if (this.stableNoise) {

			this.material.seed = 0;
			this.material.stratifiedTexture.reset();

		}

	}

	update () {

		// ensure we've updated our defines before rendering so we can ensure we
		// can wait for compilation to finish
		this.material.onBeforeRender();
		if (this.isCompiling) {

			return;

		}

		if (!this._task) {

			this._task = renderTask.call(this);

		}

		this._task.next();

	}

}

const _uv = new Vector2();
const _coord = new Vector2();
const _polar = new Spherical();
const _color = new Color();
class ProceduralEquirectTexture extends DataTexture {

	constructor(width = 512, height = 512) {

		super(
			new Float32Array(width * height * 4),
			width, height, RGBAFormat, FloatType, EquirectangularReflectionMapping,
			RepeatWrapping, ClampToEdgeWrapping, LinearFilter, LinearFilter,
		);

		this.generationCallback = null;

	}

	update () {

		this.dispose();
		this.needsUpdate = true;

		const { data, width, height } = this.image;
		for (let x = 0; x < width; x++) {

			for (let y = 0; y < height; y++) {

				_coord.set(width, height);

				_uv.set(x / width, y / height);
				_uv.x -= 0.5;
				_uv.y = 1.0 - _uv.y;

				_polar.theta = _uv.x * 2.0 * Math.PI;
				_polar.phi = _uv.y * Math.PI;
				_polar.radius = 1.0;

				this.generationCallback(_polar, _uv, _coord, _color);

				const i = y * width + x;
				const i4 = 4 * i;
				data[i4 + 0] = (_color.r);
				data[i4 + 1] = (_color.g);
				data[i4 + 2] = (_color.b);
				data[i4 + 3] = (1.0);

			}

		}

	}

	copy (other) {

		super.copy(other);
		this.generationCallback = other.generationCallback;
		return this;

	}

}

const _direction = new Vector3();
class GradientEquirectTexture extends ProceduralEquirectTexture {

	constructor(resolution = 512) {

		super(resolution, resolution);

		this.topColor = new Color().set(0xffffff);
		this.bottomColor = new Color().set(0x000000);
		this.exponent = 2;
		this.generationCallback = (polar, uv, coord, color) => {

			_direction.setFromSpherical(polar);

			const t = _direction.y * 0.5 + 0.5;
			color.lerpColors(this.bottomColor, this.topColor, t ** this.exponent);

		};

	}

	copy (other) {

		super.copy(other);

		this.topColor.copy(other.topColor);
		this.bottomColor.copy(other.bottomColor);
		return this;

	}

}

// Material that tone maps a texture before performing interpolation to prevent
// unexpected high values during texture stretching interpolation.
// Emulates browser image stretching
class ClampedInterpolationMaterial extends ShaderMaterial {

	get map () {

		return this.uniforms.map.value;

	}

	set map (v) {

		this.uniforms.map.value = v;

	}

	get opacity () {

		return this.uniforms.opacity.value;

	}

	set opacity (v) {

		if (this.uniforms) {

			this.uniforms.opacity.value = v;

		}

	}

	constructor(params) {

		super({
			uniforms: {

				map: { value: null },
				opacity: { value: 1 },

			},

			vertexShader: /* glsl */`
				varying vec2 vUv;
				void main() {

					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

				}
			`,

			fragmentShader: /* glsl */`
				uniform sampler2D map;
				uniform float opacity;
				varying vec2 vUv;

				vec4 clampedTexelFatch( sampler2D map, ivec2 px, int lod ) {

					vec4 res = texelFetch( map, ivec2( px.x, px.y ), 0 );

					#if defined( TONE_MAPPING )

					res.xyz = toneMapping( res.xyz );

					#endif

			  		return linearToOutputTexel( res );

				}

				void main() {

					vec2 size = vec2( textureSize( map, 0 ) );
					vec2 pxUv = vUv * size;
					vec2 pxCurr = floor( pxUv );
					vec2 pxFrac = fract( pxUv ) - 0.5;
					vec2 pxOffset;
					pxOffset.x = pxFrac.x > 0.0 ? 1.0 : - 1.0;
					pxOffset.y = pxFrac.y > 0.0 ? 1.0 : - 1.0;

					vec2 pxNext = clamp( pxOffset + pxCurr, vec2( 0.0 ), size - 1.0 );
					vec2 alpha = abs( pxFrac );

					vec4 p1 = mix(
						clampedTexelFatch( map, ivec2( pxCurr.x, pxCurr.y ), 0 ),
						clampedTexelFatch( map, ivec2( pxNext.x, pxCurr.y ), 0 ),
						alpha.x
					);

					vec4 p2 = mix(
						clampedTexelFatch( map, ivec2( pxCurr.x, pxNext.y ), 0 ),
						clampedTexelFatch( map, ivec2( pxNext.x, pxNext.y ), 0 ),
						alpha.x
					);

					gl_FragColor = mix( p1, p2, alpha.y );
					gl_FragColor.a *= opacity;
					#include <premultiplied_alpha_fragment>

				}
			`
		});

		this.setValues(params);

	}

}

class CubeToEquirectMaterial extends ShaderMaterial {

	constructor() {

		super({

			uniforms: {

				envMap: { value: null },
				flipEnvMap: { value: - 1 },

			},

			vertexShader: /* glsl */`
				varying vec2 vUv;
				void main() {

					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

				}`,

			fragmentShader: /* glsl */`
				#define ENVMAP_TYPE_CUBE_UV

				uniform samplerCube envMap;
				uniform float flipEnvMap;
				varying vec2 vUv;

				#include <common>
				#include <cube_uv_reflection_fragment>

				${util_functions}

				void main() {

					vec3 rayDirection = equirectUvToDirection( vUv );
					rayDirection.x *= flipEnvMap;
					gl_FragColor = textureCube( envMap, rayDirection );

				}`
		});

		this.depthWrite = false;
		this.depthTest = false;

	}

}

class CubeToEquirectGenerator {

	constructor(renderer) {

		this._renderer = renderer;
		this._quad = new FullScreenQuad(new CubeToEquirectMaterial());

	}

	generate (source, width = null, height = null) {

		if (!source.isCubeTexture) {

			throw new Error('CubeToEquirectMaterial: Source can only be cube textures.');

		}

		const image = source.images[0];
		const renderer = this._renderer;
		const quad = this._quad;

		// determine the dimensions if not provided
		if (width === null) {

			width = 4 * image.height;

		}

		if (height === null) {

			height = 2 * image.height;

		}

		const target = new WebGLRenderTarget(width, height, {
			type: FloatType,
			colorSpace: image.colorSpace,
		});

		// prep the cube map data
		const imageHeight = image.height;
		const maxMip = Math.log2(imageHeight) - 2;
		const texelHeight = 1.0 / imageHeight;
		const texelWidth = 1.0 / (3 * Math.max(Math.pow(2, maxMip), 7 * 16));

		quad.material.defines.CUBEUV_MAX_MIP = `${maxMip}.0`;
		quad.material.defines.CUBEUV_TEXEL_WIDTH = texelWidth;
		quad.material.defines.CUBEUV_TEXEL_HEIGHT = texelHeight;
		quad.material.uniforms.envMap.value = source;
		quad.material.uniforms.flipEnvMap.value = source.isRenderTargetTexture ? 1 : - 1;
		quad.material.needsUpdate = true;

		// save state and render the contents
		const currentTarget = renderer.getRenderTarget();
		const currentAutoClear = renderer.autoClear;
		renderer.autoClear = true;
		renderer.setRenderTarget(target);
		quad.render(renderer);
		renderer.setRenderTarget(currentTarget);
		renderer.autoClear = currentAutoClear;

		// read the data back
		const buffer = new Uint16Array(width * height * 4);
		const readBuffer = new Float32Array(width * height * 4);
		renderer.readRenderTargetPixels(target, 0, 0, width, height, readBuffer);
		target.dispose();

		for (let i = 0, l = readBuffer.length; i < l; i++) {

			buffer[i] = DataUtils.toHalfFloat(readBuffer[i]);

		}

		// produce the data texture
		const result = new DataTexture(buffer, width, height, RGBAFormat, HalfFloatType);
		result.minFilter = LinearMipMapLinearFilter;
		result.magFilter = LinearFilter;
		result.wrapS = RepeatWrapping;
		result.wrapT = RepeatWrapping;
		result.mapping = EquirectangularReflectionMapping;
		result.needsUpdate = true;

		return result;

	}

	dispose () {

		this._quad.dispose();

	}

}

function supportsFloatBlending (renderer) {

	return renderer.extensions.get('EXT_float_blend');

}

const _resolution = new Vector2();
class WebGLPathTracer {

	get multipleImportanceSampling () {

		return Boolean(this._pathTracer.material.defines.FEATURE_MIS);

	}

	set multipleImportanceSampling (v) {

		this._pathTracer.material.setDefine('FEATURE_MIS', v ? 1 : 0);

	}

	get transmissiveBounces () {

		return this._pathTracer.material.transmissiveBounces;

	}

	set transmissiveBounces (v) {

		this._pathTracer.material.transmissiveBounces = v;

	}

	get bounces () {

		return this._pathTracer.material.bounces;

	}

	set bounces (v) {

		this._pathTracer.material.bounces = v;

	}

	get filterGlossyFactor () {

		return this._pathTracer.material.filterGlossyFactor;

	}

	set filterGlossyFactor (v) {

		this._pathTracer.material.filterGlossyFactor = v;

	}

	get samples () {

		return this._pathTracer.samples;

	}

	get target () {

		return this._pathTracer.target;

	}

	get tiles () {

		return this._pathTracer.tiles;

	}

	get stableNoise () {

		return this._pathTracer.stableNoise;

	}

	set stableNoise (v) {

		this._pathTracer.stableNoise = v;

	}

	get isCompiling () {

		return Boolean(this._pathTracer.isCompiling);

	}

	constructor(renderer) {

		// members
		this._renderer = renderer;
		this._generator = new PathTracingSceneGenerator();
		this._pathTracer = new PathTracingRenderer(renderer);
		this._queueReset = false;
		this._clock = new Clock();
		this._compilePromise = null;

		this._lowResPathTracer = new PathTracingRenderer(renderer);
		this._lowResPathTracer.tiles.set(1, 1);
		this._quad = new FullScreenQuad(new ClampedInterpolationMaterial({
			map: null,
			transparent: true,
			blending: NoBlending,

			premultipliedAlpha: renderer.getContextAttributes().premultipliedAlpha,
		}));
		this._materials = null;

		this._previousEnvironment = null;
		this._previousBackground = null;
		this._internalBackground = null;

		// options
		this.renderDelay = 100;
		this.minSamples = 5;
		this.fadeDuration = 500;
		this.enablePathTracing = true;
		this.pausePathTracing = false;
		this.dynamicLowRes = false;
		this.lowResScale = 0.25;
		this.renderScale = 1;
		this.synchronizeRenderSize = true;
		this.rasterizeScene = true;
		this.renderToCanvas = true;
		this.textureSize = new Vector2(1024, 1024);
		this.rasterizeSceneCallback = (scene, camera) => {

			this._renderer.render(scene, camera);

		};

		this.renderToCanvasCallback = (target, renderer, quad) => {

			const currentAutoClear = renderer.autoClear;
			renderer.autoClear = false;
			quad.render(renderer);
			renderer.autoClear = currentAutoClear;

		};

		// initialize the scene so it doesn't fail
		this.setScene(new Scene(), new PerspectiveCamera());

	}

	setBVHWorker (worker) {

		this._generator.setBVHWorker(worker);

	}

	setScene (scene, camera, options = {}) {

		scene.updateMatrixWorld(true);
		camera.updateMatrixWorld();

		const generator = this._generator;
		generator.setObjects(scene);

		if (this._buildAsync) {

			return generator.generateAsync(options.onProgress).then(result => {

				return this._updateFromResults(scene, camera, result);

			});

		} else {

			const result = generator.generate();
			return this._updateFromResults(scene, camera, result);

		}

	}

	setSceneAsync (...args) {

		this._buildAsync = true;
		const result = this.setScene(...args);
		this._buildAsync = false;

		return result;

	}

	setCamera (camera) {

		this.camera = camera;
		this.updateCamera();

	}

	updateCamera () {

		const camera = this.camera;
		camera.updateMatrixWorld();

		this._pathTracer.setCamera(camera);
		this._lowResPathTracer.setCamera(camera);
		this.reset();

	}

	updateMaterials () {

		const material = this._pathTracer.material;
		const renderer = this._renderer;
		const materials = this._materials;
		const textureSize = this.textureSize;

		// reduce texture sources here - we don't want to do this in the
		// textures array because we need to pass the textures array into the
		// material target
		const textures = getTextures(materials);
		material.textures.setTextures(renderer, textures, textureSize.x, textureSize.y);
		material.materials.updateFrom(materials, textures);
		this.reset();

	}

	updateLights () {

		const scene = this.scene;
		const renderer = this._renderer;
		const material = this._pathTracer.material;

		const lights = getLights(scene);
		const iesTextures = getIesTextures(lights);
		material.lights.updateFrom(lights, iesTextures);
		material.iesProfiles.setTextures(renderer, iesTextures);
		this.reset();

	}

	updateEnvironment () {

		const scene = this.scene;
		const material = this._pathTracer.material;

		if (this._internalBackground) {

			this._internalBackground.dispose();
			this._internalBackground = null;

		}

		// update scene background
		material.backgroundBlur = scene.backgroundBlurriness;
		material.backgroundIntensity = scene.backgroundIntensity ?? 1;
		material.backgroundRotation.makeRotationFromEuler(scene.backgroundRotation).invert();
		if (scene.background === null) {

			material.backgroundMap = null;
			material.backgroundAlpha = 0;

		} else if (scene.background.isColor) {

			this._colorBackground = this._colorBackground || new GradientEquirectTexture(16);

			const colorBackground = this._colorBackground;
			if (!colorBackground.topColor.equals(scene.background)) {

				// set the texture color
				colorBackground.topColor.set(scene.background);
				colorBackground.bottomColor.set(scene.background);
				colorBackground.update();

			}

			// assign to material
			material.backgroundMap = colorBackground;
			material.backgroundAlpha = 1;

		} else if (scene.background.isCubeTexture) {

			if (scene.background !== this._previousBackground) {

				const background = new CubeToEquirectGenerator(this._renderer).generate(scene.background);
				this._internalBackground = background;
				material.backgroundMap = background;
				material.backgroundAlpha = 1;

			}

		} else {

			material.backgroundMap = scene.background;
			material.backgroundAlpha = 1;

		}

		// update scene environment
		material.environmentIntensity = scene.environmentIntensity ?? 1;
		material.environmentRotation.makeRotationFromEuler(scene.environmentRotation).invert();
		if (this._previousEnvironment !== scene.environment) {

			if (scene.environment !== null) {

				if (scene.environment.isCubeTexture) {

					const environment = new CubeToEquirectGenerator(this._renderer).generate(scene.environment);
					material.envMapInfo.updateFrom(environment);

				} else {

					// TODO: Consider setting this to the highest supported bit depth by checking for
					// OES_texture_float_linear or OES_texture_half_float_linear. Requires changes to
					// the equirect uniform
					material.envMapInfo.updateFrom(scene.environment);

				}

			} else {

				material.environmentIntensity = 0;

			}

		}

		this._previousEnvironment = scene.environment;
		this._previousBackground = scene.background;
		this.reset();

	}

	_updateFromResults (scene, camera, results) {

		const {
			materials,
			geometry,
			bvh,
			bvhChanged,
		} = results;

		this._materials = materials;

		const pathTracer = this._pathTracer;
		const material = pathTracer.material;

		if (bvhChanged) {

			material.bvh.updateFrom(bvh);
			material.attributesArray.updateFrom(
				geometry.attributes.normal,
				geometry.attributes.tangent,
				geometry.attributes.uv,
				geometry.attributes.color,
			);

			material.materialIndexAttribute.updateFrom(geometry.attributes.materialIndex);

		}

		// save previously used items
		this._previousScene = scene;
		this.scene = scene;
		this.camera = camera;

		this.updateCamera();
		this.updateMaterials();
		this.updateEnvironment();
		this.updateLights();

		return results;

	}

	renderSample () {

		const lowResPathTracer = this._lowResPathTracer;
		const pathTracer = this._pathTracer;
		const renderer = this._renderer;
		const clock = this._clock;
		const quad = this._quad;

		this._updateScale();

		if (this._queueReset) {

			pathTracer.reset();
			lowResPathTracer.reset();
			this._queueReset = false;

			quad.material.opacity = 0;
			clock.start();

		}

		// render the path tracing sample after enough time has passed
		const delta = clock.getDelta() * 1e3;
		const elapsedTime = clock.getElapsedTime() * 1e3;
		if (!this.pausePathTracing && this.enablePathTracing && this.renderDelay <= elapsedTime && !this.isCompiling) {

			pathTracer.update();

		}

		// when alpha is enabled we use a manual blending system rather than
		// rendering with a blend function
		pathTracer.alpha = pathTracer.material.backgroundAlpha !== 1 || !supportsFloatBlending(renderer);
		lowResPathTracer.alpha = pathTracer.alpha;

		if (this.renderToCanvas) {

			const renderer = this._renderer;
			const minSamples = this.minSamples;

			if (elapsedTime >= this.renderDelay && this.samples >= this.minSamples) {

				if (this.fadeDuration !== 0) {

					quad.material.opacity = Math.min(quad.material.opacity + delta / this.fadeDuration, 1);

				} else {

					quad.material.opacity = 1;

				}

			}

			// render the fallback if we haven't rendered enough samples, are paused, or are occluded
			if (!this.enablePathTracing || this.samples < minSamples || quad.material.opacity < 1) {

				if (this.dynamicLowRes && !this.isCompiling) {

					if (lowResPathTracer.samples < 1) {

						lowResPathTracer.material = pathTracer.material;
						lowResPathTracer.update();

					}

					const currentOpacity = quad.material.opacity;
					quad.material.opacity = 1 - quad.material.opacity;
					quad.material.map = lowResPathTracer.target.texture;
					quad.render(renderer);
					quad.material.opacity = currentOpacity;

				}

				if (!this.dynamicLowRes && this.rasterizeScene || this.dynamicLowRes && this.isCompiling) {

					this.rasterizeSceneCallback(this.scene, this.camera);

				}

			}


			if (this.enablePathTracing && quad.material.opacity > 0) {

				if (quad.material.opacity < 1) {

					// use additive blending when the low res texture is rendered so we can fade the
					// background out while the full res fades in
					quad.material.blending = this.dynamicLowRes ? AdditiveBlending : NormalBlending;

				}

				quad.material.map = pathTracer.target.texture;
				this.renderToCanvasCallback(pathTracer.target, renderer, quad);
				quad.material.blending = NoBlending;

			}

		}

	}

	reset () {

		this._queueReset = true;
		this._pathTracer.samples = 0;

	}

	dispose () {

		this._renderQuad.dispose();
		this._renderQuad.material.dispose();
		this._pathTracer.dispose();

	}

	_updateScale () {

		// update the path tracer scale if it has changed
		if (this.synchronizeRenderSize) {

			this._renderer.getDrawingBufferSize(_resolution);

			const w = Math.floor(this.renderScale * _resolution.x);
			const h = Math.floor(this.renderScale * _resolution.y);

			this._pathTracer.getSize(_resolution);
			if (_resolution.x !== w || _resolution.y !== h) {

				const lowResScale = this.lowResScale;
				this._pathTracer.setSize(w, h);
				this._lowResPathTracer.setSize(Math.floor(w * lowResScale), Math.floor(h * lowResScale));

			}

		}

	}

}

class EquirectCamera extends Camera {

	constructor() {

		super();

		this.isEquirectCamera = true;

	}

}

class PhysicalSpotLight extends SpotLight {

	constructor(...args) {

		super(...args);

		this.iesMap = null;
		this.radius = 0;

	}

	copy (source, recursive) {

		super.copy(source, recursive);

		this.iesMap = source.iesMap;
		this.radius = source.radius;

		return this;

	}

}

class ShapedAreaLight extends RectAreaLight {

	constructor(...args) {

		super(...args);
		this.isCircular = false;

	}

	copy (source, recursive) {

		super.copy(source, recursive);

		this.isCircular = source.isCircular;

		return this;

	}

}

class PMREMCopyMaterial extends MaterialBase {

	constructor() {

		super({

			uniforms: {

				envMap: { value: null },
				blur: { value: 0 },

			},

			vertexShader: /* glsl */`

				varying vec2 vUv;
				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}

			`,

			fragmentShader: /* glsl */`

				#include <common>
				#include <cube_uv_reflection_fragment>

				${util_functions}

				uniform sampler2D envMap;
				uniform float blur;
				varying vec2 vUv;
				void main() {

					vec3 rayDirection = equirectUvToDirection( vUv );
					gl_FragColor = textureCubeUV( envMap, rayDirection, blur );

				}

			`,

		});

	}

}

class BlurredEnvMapGenerator {

	constructor(renderer) {

		this.renderer = renderer;
		this.pmremGenerator = new PMREMGenerator(renderer);
		this.copyQuad = new FullScreenQuad(new PMREMCopyMaterial());
		this.renderTarget = new WebGLRenderTarget(1, 1, { type: FloatType, format: RGBAFormat });

	}

	dispose () {

		this.pmremGenerator.dispose();
		this.copyQuad.dispose();
		this.renderTarget.dispose();

	}

	generate (texture, blur) {

		const { pmremGenerator, renderTarget, copyQuad, renderer } = this;

		// get the pmrem target
		const pmremTarget = pmremGenerator.fromEquirectangular(texture);

		// set up the material
		const { width, height } = texture.image;
		renderTarget.setSize(width, height);
		copyQuad.material.envMap = pmremTarget.texture;
		copyQuad.material.blur = blur;

		// render
		const prevRenderTarget = renderer.getRenderTarget();
		const prevClear = renderer.autoClear;

		renderer.setRenderTarget(renderTarget);
		renderer.autoClear = true;
		copyQuad.render(renderer);

		renderer.setRenderTarget(prevRenderTarget);
		renderer.autoClear = prevClear;

		// read the data back
		const buffer = new Uint16Array(width * height * 4);
		const readBuffer = new Float32Array(width * height * 4);
		renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, readBuffer);

		for (let i = 0, l = readBuffer.length; i < l; i++) {

			buffer[i] = DataUtils.toHalfFloat(readBuffer[i]);

		}

		const result = new DataTexture(buffer, width, height, RGBAFormat, HalfFloatType);
		result.minFilter = texture.minFilter;
		result.magFilter = texture.magFilter;
		result.wrapS = texture.wrapS;
		result.wrapT = texture.wrapT;
		result.mapping = EquirectangularReflectionMapping;
		result.needsUpdate = true;

		// dispose of the now unneeded target
		pmremTarget.dispose();

		return result;

	}

}

class DenoiseMaterial extends MaterialBase {

	constructor(parameters) {

		super({

			blending: NoBlending,

			transparent: false,

			depthWrite: false,

			depthTest: false,

			defines: {

				USE_SLIDER: 0,

			},

			uniforms: {

				sigma: { value: 5.0 },
				threshold: { value: 0.03 },
				kSigma: { value: 1.0 },

				map: { value: null },
				opacity: { value: 1 },

			},

			vertexShader: /* glsl */`

				varying vec2 vUv;

				void main() {

					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

				}

			`,

			fragmentShader: /* glsl */`

				//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
				//  Copyright (c) 2018-2019 Michele Morrone
				//  All rights reserved.
				//
				//  https://michelemorrone.eu - https://BrutPitt.com
				//
				//  me@michelemorrone.eu - brutpitt@gmail.com
				//  twitter: @BrutPitt - github: BrutPitt
				//
				//  https://github.com/BrutPitt/glslSmartDeNoise/
				//
				//  This software is distributed under the terms of the BSD 2-Clause license
				//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

				uniform sampler2D map;

				uniform float sigma;
				uniform float threshold;
				uniform float kSigma;
				uniform float opacity;

				varying vec2 vUv;

				#define INV_SQRT_OF_2PI 0.39894228040143267793994605993439
				#define INV_PI 0.31830988618379067153776752674503

				// Parameters:
				//	 sampler2D tex	 - sampler image / texture
				//	 vec2 uv		   - actual fragment coord
				//	 float sigma  >  0 - sigma Standard Deviation
				//	 float kSigma >= 0 - sigma coefficient
				//		 kSigma * sigma  -->  radius of the circular kernel
				//	 float threshold   - edge sharpening threshold
				vec4 smartDeNoise( sampler2D tex, vec2 uv, float sigma, float kSigma, float threshold ) {

					float radius = round( kSigma * sigma );
					float radQ = radius * radius;

					float invSigmaQx2 = 0.5 / ( sigma * sigma );
					float invSigmaQx2PI = INV_PI * invSigmaQx2;

					float invThresholdSqx2 = 0.5 / ( threshold * threshold );
					float invThresholdSqrt2PI = INV_SQRT_OF_2PI / threshold;

					vec4 centrPx = texture2D( tex, uv );
					centrPx.rgb *= centrPx.a;

					float zBuff = 0.0;
					vec4 aBuff = vec4( 0.0 );
					vec2 size = vec2( textureSize( tex, 0 ) );

					vec2 d;
					for ( d.x = - radius; d.x <= radius; d.x ++ ) {

						float pt = sqrt( radQ - d.x * d.x );

						for ( d.y = - pt; d.y <= pt; d.y ++ ) {

							float blurFactor = exp( - dot( d, d ) * invSigmaQx2 ) * invSigmaQx2PI;

							vec4 walkPx = texture2D( tex, uv + d / size );
							walkPx.rgb *= walkPx.a;

							vec4 dC = walkPx - centrPx;
							float deltaFactor = exp( - dot( dC.rgba, dC.rgba ) * invThresholdSqx2 ) * invThresholdSqrt2PI * blurFactor;

							zBuff += deltaFactor;
							aBuff += deltaFactor * walkPx;

						}

					}

					return aBuff / zBuff;

				}

				void main() {

					gl_FragColor = smartDeNoise( map, vec2( vUv.x, vUv.y ), sigma, kSigma, threshold );
					#include <tonemapping_fragment>
					#include <colorspace_fragment>
					#include <premultiplied_alpha_fragment>

					gl_FragColor.a *= opacity;

				}

			`

		});

		this.setValues(parameters);

	}

}

class FogVolumeMaterial extends MeshStandardMaterial {

	constructor(params) {

		super(params);

		this.isFogVolumeMaterial = true;

		this.density = 0.015;
		this.emissive = new Color();
		this.emissiveIntensity = 0.0;
		this.opacity = 0.15;
		this.transparent = true;
		this.roughness = 1.0;
		this.metalness = 0.0;

		this.setValues(params);

	}

}

// core

export { BlurredEnvMapGenerator, DenoiseMaterial, DynamicPathTracingSceneGenerator, EquirectCamera, FogVolumeMaterial, GradientEquirectTexture, PathTracingRenderer, PathTracingSceneGenerator, PathTracingSceneWorker, PhysicalCamera, PhysicalPathTracingMaterial, PhysicalSpotLight, ProceduralEquirectTexture, ShapedAreaLight, WebGLPathTracer };
//# sourceMappingURL=index.module.js.map