Browse Source

Updated builds.

Mr.doob 3 years ago
parent
commit
3a76bc7d21
4 changed files with 1810 additions and 594 deletions
  1. 579 199
      build/three.cjs
  2. 579 199
      build/three.js
  3. 0 0
      build/three.min.js
  4. 652 196
      build/three.module.js

+ 579 - 199
build/three.cjs

@@ -8684,6 +8684,15 @@ function mergeUniforms(uniforms) {
 	}
 	}
 
 
 	return merged;
 	return merged;
+}
+function cloneUniformsGroups(src) {
+	const dst = [];
+
+	for (let u = 0; u < src.length; u++) {
+		dst.push(src[u].clone());
+	}
+
+	return dst;
 } // Legacy
 } // Legacy
 
 
 const UniformsUtils = {
 const UniformsUtils = {
@@ -8702,6 +8711,7 @@ class ShaderMaterial extends Material {
 		this.type = 'ShaderMaterial';
 		this.type = 'ShaderMaterial';
 		this.defines = {};
 		this.defines = {};
 		this.uniforms = {};
 		this.uniforms = {};
+		this.uniformsGroups = [];
 		this.vertexShader = default_vertex;
 		this.vertexShader = default_vertex;
 		this.fragmentShader = default_fragment;
 		this.fragmentShader = default_fragment;
 		this.linewidth = 1;
 		this.linewidth = 1;
@@ -8748,6 +8758,7 @@ class ShaderMaterial extends Material {
 		this.fragmentShader = source.fragmentShader;
 		this.fragmentShader = source.fragmentShader;
 		this.vertexShader = source.vertexShader;
 		this.vertexShader = source.vertexShader;
 		this.uniforms = cloneUniforms(source.uniforms);
 		this.uniforms = cloneUniforms(source.uniforms);
+		this.uniformsGroups = cloneUniformsGroups(source.uniformsGroups);
 		this.defines = Object.assign({}, source.defines);
 		this.defines = Object.assign({}, source.defines);
 		this.wireframe = source.wireframe;
 		this.wireframe = source.wireframe;
 		this.wireframeLinewidth = source.wireframeLinewidth;
 		this.wireframeLinewidth = source.wireframeLinewidth;
@@ -15878,6 +15889,8 @@ function WebGLState(gl, extensions, capabilities) {
 	const colorBuffer = new ColorBuffer();
 	const colorBuffer = new ColorBuffer();
 	const depthBuffer = new DepthBuffer();
 	const depthBuffer = new DepthBuffer();
 	const stencilBuffer = new StencilBuffer();
 	const stencilBuffer = new StencilBuffer();
+	const uboBindings = new WeakMap();
+	const uboProgamMap = new WeakMap();
 	let enabledCapabilities = {};
 	let enabledCapabilities = {};
 	let currentBoundFramebuffers = {};
 	let currentBoundFramebuffers = {};
 	let currentDrawbuffers = new WeakMap();
 	let currentDrawbuffers = new WeakMap();
@@ -16376,6 +16389,33 @@ function WebGLState(gl, extensions, capabilities) {
 			gl.viewport(viewport.x, viewport.y, viewport.z, viewport.w);
 			gl.viewport(viewport.x, viewport.y, viewport.z, viewport.w);
 			currentViewport.copy(viewport);
 			currentViewport.copy(viewport);
 		}
 		}
+	}
+
+	function updateUBOMapping(uniformsGroup, program) {
+		let mapping = uboProgamMap.get(program);
+
+		if (mapping === undefined) {
+			mapping = new WeakMap();
+			uboProgamMap.set(program, mapping);
+		}
+
+		let blockIndex = mapping.get(uniformsGroup);
+
+		if (blockIndex === undefined) {
+			blockIndex = gl.getUniformBlockIndex(program, uniformsGroup.name);
+			mapping.set(uniformsGroup, blockIndex);
+		}
+	}
+
+	function uniformBlockBinding(uniformsGroup, program) {
+		const mapping = uboProgamMap.get(program);
+		const blockIndex = mapping.get(uniformsGroup);
+
+		if (uboBindings.get(uniformsGroup) !== blockIndex) {
+			// bind shader specific block index to global block point
+			gl.uniformBlockBinding(program, blockIndex, uniformsGroup.__bindingPointIndex);
+			uboBindings.set(uniformsGroup, blockIndex);
+		}
 	} //
 	} //
 
 
 
 
@@ -16468,6 +16508,8 @@ function WebGLState(gl, extensions, capabilities) {
 		compressedTexImage2D: compressedTexImage2D,
 		compressedTexImage2D: compressedTexImage2D,
 		texImage2D: texImage2D,
 		texImage2D: texImage2D,
 		texImage3D: texImage3D,
 		texImage3D: texImage3D,
+		updateUBOMapping: updateUBOMapping,
+		uniformBlockBinding: uniformBlockBinding,
 		texStorage2D: texStorage2D,
 		texStorage2D: texStorage2D,
 		texStorage3D: texStorage3D,
 		texStorage3D: texStorage3D,
 		texSubImage2D: texSubImage2D,
 		texSubImage2D: texSubImage2D,
@@ -19185,6 +19227,249 @@ function WebGLMaterials(renderer, properties) {
 	};
 	};
 }
 }
 
 
+function WebGLUniformsGroups(gl, info, capabilities, state) {
+	let buffers = {};
+	let updateList = {};
+	let allocatedBindingPoints = [];
+	const maxBindingPoints = capabilities.isWebGL2 ? gl.getParameter(gl.MAX_UNIFORM_BUFFER_BINDINGS) : 0; // binding points are global whereas block indices are per shader program
+
+	function bind(uniformsGroup, program) {
+		const webglProgram = program.program;
+		state.uniformBlockBinding(uniformsGroup, webglProgram);
+	}
+
+	function update(uniformsGroup, program) {
+		let buffer = buffers[uniformsGroup.id];
+
+		if (buffer === undefined) {
+			prepareUniformsGroup(uniformsGroup);
+			buffer = createBuffer(uniformsGroup);
+			buffers[uniformsGroup.id] = buffer;
+			uniformsGroup.addEventListener('dispose', onUniformsGroupsDispose);
+		} // ensure to update the binding points/block indices mapping for this program
+
+
+		const webglProgram = program.program;
+		state.updateUBOMapping(uniformsGroup, webglProgram); // update UBO once per frame
+
+		const frame = info.render.frame;
+
+		if (updateList[uniformsGroup.id] !== frame) {
+			updateBufferData(uniformsGroup);
+			updateList[uniformsGroup.id] = frame;
+		}
+	}
+
+	function createBuffer(uniformsGroup) {
+		// the setup of an UBO is independent of a particular shader program but global
+		const bindingPointIndex = allocateBindingPointIndex();
+		uniformsGroup.__bindingPointIndex = bindingPointIndex;
+		const buffer = gl.createBuffer();
+		const size = uniformsGroup.__size;
+		const usage = uniformsGroup.usage;
+		gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
+		gl.bufferData(gl.UNIFORM_BUFFER, size, usage);
+		gl.bindBuffer(gl.UNIFORM_BUFFER, null);
+		gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPointIndex, buffer);
+		return buffer;
+	}
+
+	function allocateBindingPointIndex() {
+		for (let i = 0; i < maxBindingPoints; i++) {
+			if (allocatedBindingPoints.indexOf(i) === -1) {
+				allocatedBindingPoints.push(i);
+				return i;
+			}
+		}
+
+		console.error('THREE.WebGLRenderer: Maximum number of simultaneously usable uniforms groups reached.');
+		return 0;
+	}
+
+	function updateBufferData(uniformsGroup) {
+		const buffer = buffers[uniformsGroup.id];
+		const uniforms = uniformsGroup.uniforms;
+		const cache = uniformsGroup.__cache;
+		gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
+
+		for (let i = 0, il = uniforms.length; i < il; i++) {
+			const uniform = uniforms[i]; // partly update the buffer if necessary
+
+			if (hasUniformChanged(uniform, i, cache) === true) {
+				const value = uniform.value;
+				const offset = uniform.__offset;
+
+				if (typeof value === 'number') {
+					uniform.__data[0] = value;
+					gl.bufferSubData(gl.UNIFORM_BUFFER, offset, uniform.__data);
+				} else {
+					if (uniform.value.isMatrix3) {
+						// manually converting 3x3 to 3x4
+						uniform.__data[0] = uniform.value.elements[0];
+						uniform.__data[1] = uniform.value.elements[1];
+						uniform.__data[2] = uniform.value.elements[2];
+						uniform.__data[3] = uniform.value.elements[0];
+						uniform.__data[4] = uniform.value.elements[3];
+						uniform.__data[5] = uniform.value.elements[4];
+						uniform.__data[6] = uniform.value.elements[5];
+						uniform.__data[7] = uniform.value.elements[0];
+						uniform.__data[8] = uniform.value.elements[6];
+						uniform.__data[9] = uniform.value.elements[7];
+						uniform.__data[10] = uniform.value.elements[8];
+						uniform.__data[11] = uniform.value.elements[0];
+					} else {
+						value.toArray(uniform.__data);
+					}
+
+					gl.bufferSubData(gl.UNIFORM_BUFFER, offset, uniform.__data);
+				}
+			}
+		}
+
+		gl.bindBuffer(gl.UNIFORM_BUFFER, null);
+	}
+
+	function hasUniformChanged(uniform, index, cache) {
+		const value = uniform.value;
+
+		if (cache[index] === undefined) {
+			// cache entry does not exist so far
+			if (typeof value === 'number') {
+				cache[index] = value;
+			} else {
+				cache[index] = value.clone();
+			}
+
+			return true;
+		} else {
+			// compare current value with cached entry
+			if (typeof value === 'number') {
+				if (cache[index] !== value) {
+					cache[index] = value;
+					return true;
+				}
+			} else {
+				const cachedObject = cache[index];
+
+				if (cachedObject.equals(value) === false) {
+					cachedObject.copy(value);
+					return true;
+				}
+			}
+		}
+
+		return false;
+	}
+
+	function prepareUniformsGroup(uniformsGroup) {
+		// determine total buffer size according to the STD140 layout
+		// Hint: STD140 is the only supported layout in WebGL 2
+		const uniforms = uniformsGroup.uniforms;
+		let offset = 0; // global buffer offset in bytes
+
+		const chunkSize = 16; // size of a chunk in bytes
+
+		let chunkOffset = 0; // offset within a single chunk in bytes
+
+		for (let i = 0, l = uniforms.length; i < l; i++) {
+			const uniform = uniforms[i];
+			const info = getUniformSize(uniform); // the following two properties will be used for partial buffer updates
+
+			uniform.__data = new Float32Array(info.storage / Float32Array.BYTES_PER_ELEMENT);
+			uniform.__offset = offset; //
+
+			if (i > 0) {
+				chunkOffset = offset % chunkSize;
+				const remainingSizeInChunk = chunkSize - chunkOffset; // check for chunk overflow
+
+				if (chunkOffset !== 0 && remainingSizeInChunk - info.boundary < 0) {
+					// add padding and adjust offset
+					offset += chunkSize - chunkOffset;
+					uniform.__offset = offset;
+				}
+			}
+
+			offset += info.storage;
+		} // ensure correct final padding
+
+
+		chunkOffset = offset % chunkSize;
+		if (chunkOffset > 0) offset += chunkSize - chunkOffset; //
+
+		uniformsGroup.__size = offset;
+		uniformsGroup.__cache = {};
+		return this;
+	}
+
+	function getUniformSize(uniform) {
+		const value = uniform.value;
+		const info = {
+			boundary: 0,
+			// bytes
+			storage: 0 // bytes
+
+		}; // determine sizes according to STD140
+
+		if (typeof value === 'number') {
+			// float/int
+			info.boundary = 4;
+			info.storage = 4;
+		} else if (value.isVector2) {
+			// vec2
+			info.boundary = 8;
+			info.storage = 8;
+		} else if (value.isVector3 || value.isColor) {
+			// vec3
+			info.boundary = 16;
+			info.storage = 12; // evil: vec3 must start on a 16-byte boundary but it only consumes 12 bytes
+		} else if (value.isVector4) {
+			// vec4
+			info.boundary = 16;
+			info.storage = 16;
+		} else if (value.isMatrix3) {
+			// mat3 (in STD140 a 3x3 matrix is represented as 3x4)
+			info.boundary = 48;
+			info.storage = 48;
+		} else if (value.isMatrix4) {
+			// mat4
+			info.boundary = 64;
+			info.storage = 64;
+		} else if (value.isTexture) {
+			console.warn('THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group.');
+		} else {
+			console.warn('THREE.WebGLRenderer: Unsupported uniform value type.', value);
+		}
+
+		return info;
+	}
+
+	function onUniformsGroupsDispose(event) {
+		const uniformsGroup = event.target;
+		uniformsGroup.removeEventListener('dispose', onUniformsGroupsDispose);
+		const index = allocatedBindingPoints.indexOf(uniformsGroup.__bindingPointIndex);
+		allocatedBindingPoints.splice(index, 1);
+		gl.deleteBuffer(buffers[uniformsGroup.id]);
+		delete buffers[uniformsGroup.id];
+		delete updateList[uniformsGroup.id];
+	}
+
+	function dispose() {
+		for (const id in buffers) {
+			gl.deleteBuffer(buffers[id]);
+		}
+
+		allocatedBindingPoints = [];
+		buffers = {};
+		updateList = {};
+	}
+
+	return {
+		bind: bind,
+		update: update,
+		dispose: dispose
+	};
+}
+
 function createCanvasElement() {
 function createCanvasElement() {
 	const canvas = createElementNS('canvas');
 	const canvas = createElementNS('canvas');
 	canvas.style.display = 'block';
 	canvas.style.display = 'block';
@@ -19387,7 +19672,7 @@ function WebGLRenderer(parameters = {}) {
 	let properties, textures, cubemaps, cubeuvmaps, attributes, geometries, objects;
 	let properties, textures, cubemaps, cubeuvmaps, attributes, geometries, objects;
 	let programCache, materials, renderLists, renderStates, clipping, shadowMap;
 	let programCache, materials, renderLists, renderStates, clipping, shadowMap;
 	let background, morphtargets, bufferRenderer, indexedBufferRenderer;
 	let background, morphtargets, bufferRenderer, indexedBufferRenderer;
-	let utils, bindingStates;
+	let utils, bindingStates, uniformsGroups;
 
 
 	function initGLContext() {
 	function initGLContext() {
 		extensions = new WebGLExtensions(_gl);
 		extensions = new WebGLExtensions(_gl);
@@ -19412,6 +19697,7 @@ function WebGLRenderer(parameters = {}) {
 		renderStates = new WebGLRenderStates(extensions, capabilities);
 		renderStates = new WebGLRenderStates(extensions, capabilities);
 		background = new WebGLBackground(_this, cubemaps, state, objects, _alpha, _premultipliedAlpha);
 		background = new WebGLBackground(_this, cubemaps, state, objects, _alpha, _premultipliedAlpha);
 		shadowMap = new WebGLShadowMap(_this, objects, capabilities);
 		shadowMap = new WebGLShadowMap(_this, objects, capabilities);
+		uniformsGroups = new WebGLUniformsGroups(_gl, info, capabilities, state);
 		bufferRenderer = new WebGLBufferRenderer(_gl, extensions, info, capabilities);
 		bufferRenderer = new WebGLBufferRenderer(_gl, extensions, info, capabilities);
 		indexedBufferRenderer = new WebGLIndexedBufferRenderer(_gl, extensions, info, capabilities);
 		indexedBufferRenderer = new WebGLIndexedBufferRenderer(_gl, extensions, info, capabilities);
 		info.programs = programCache.programs;
 		info.programs = programCache.programs;
@@ -19594,6 +19880,7 @@ function WebGLRenderer(parameters = {}) {
 		cubeuvmaps.dispose();
 		cubeuvmaps.dispose();
 		objects.dispose();
 		objects.dispose();
 		bindingStates.dispose();
 		bindingStates.dispose();
+		uniformsGroups.dispose();
 		programCache.dispose();
 		programCache.dispose();
 		xr.dispose();
 		xr.dispose();
 		xr.removeEventListener('sessionstart', onXRSessionStart);
 		xr.removeEventListener('sessionstart', onXRSessionStart);
@@ -20357,7 +20644,22 @@ function WebGLRenderer(parameters = {}) {
 
 
 		p_uniforms.setValue(_gl, 'modelViewMatrix', object.modelViewMatrix);
 		p_uniforms.setValue(_gl, 'modelViewMatrix', object.modelViewMatrix);
 		p_uniforms.setValue(_gl, 'normalMatrix', object.normalMatrix);
 		p_uniforms.setValue(_gl, 'normalMatrix', object.normalMatrix);
-		p_uniforms.setValue(_gl, 'modelMatrix', object.matrixWorld);
+		p_uniforms.setValue(_gl, 'modelMatrix', object.matrixWorld); // UBOs
+
+		if (material.isShaderMaterial || material.isRawShaderMaterial) {
+			const groups = material.uniformsGroups;
+
+			for (let i = 0, l = groups.length; i < l; i++) {
+				if (capabilities.isWebGL2) {
+					const group = groups[i];
+					uniformsGroups.update(group, program);
+					uniformsGroups.bind(group, program);
+				} else {
+					console.warn('THREE.WebGLRenderer: Uniform Buffer Objects can only be used with WebGL 2.');
+				}
+			}
+		}
+
 		return program;
 		return program;
 	} // If uniforms are marked as clean, they don't need to be loaded to the GPU.
 	} // If uniforms are marked as clean, they don't need to be loaded to the GPU.
 
 
@@ -23546,7 +23848,7 @@ class Path extends CurvePath {
 }
 }
 
 
 class LatheGeometry extends BufferGeometry {
 class LatheGeometry extends BufferGeometry {
-	constructor(points = [new Vector2(0, 0.5), new Vector2(0.5, 0), new Vector2(0, -0.5)], segments = 12, phiStart = 0, phiLength = Math.PI * 2) {
+	constructor(points = [new Vector2(0, -0.5), new Vector2(0.5, 0), new Vector2(0, 0.5)], segments = 12, phiStart = 0, phiLength = Math.PI * 2) {
 		super();
 		super();
 		this.type = 'LatheGeometry';
 		this.type = 'LatheGeometry';
 		this.parameters = {
 		this.parameters = {
@@ -26797,236 +27099,252 @@ class LineDashedMaterial extends LineBasicMaterial {
 
 
 }
 }
 
 
-const AnimationUtils = {
-	// same as Array.prototype.slice, but also works on typed arrays
-	arraySlice: function (array, from, to) {
-		if (AnimationUtils.isTypedArray(array)) {
-			// in ios9 array.subarray(from, undefined) will return empty array
-			// but array.subarray(from) or array.subarray(from, len) is correct
-			return new array.constructor(array.subarray(from, to !== undefined ? to : array.length));
-		}
+function arraySlice(array, from, to) {
+	if (isTypedArray(array)) {
+		// in ios9 array.subarray(from, undefined) will return empty array
+		// but array.subarray(from) or array.subarray(from, len) is correct
+		return new array.constructor(array.subarray(from, to !== undefined ? to : array.length));
+	}
 
 
-		return array.slice(from, to);
-	},
-	// converts an array to a specific type
-	convertArray: function (array, type, forceClone) {
-		if (!array || // let 'undefined' and 'null' pass
-		!forceClone && array.constructor === type) return array;
+	return array.slice(from, to);
+} // converts an array to a specific type
 
 
-		if (typeof type.BYTES_PER_ELEMENT === 'number') {
-			return new type(array); // create typed array
-		}
 
 
-		return Array.prototype.slice.call(array); // create Array
-	},
-	isTypedArray: function (object) {
-		return ArrayBuffer.isView(object) && !(object instanceof DataView);
-	},
-	// returns an array by which times and values can be sorted
-	getKeyframeOrder: function (times) {
-		function compareTime(i, j) {
-			return times[i] - times[j];
-		}
+function convertArray(array, type, forceClone) {
+	if (!array || // let 'undefined' and 'null' pass
+	!forceClone && array.constructor === type) return array;
 
 
-		const n = times.length;
-		const result = new Array(n);
+	if (typeof type.BYTES_PER_ELEMENT === 'number') {
+		return new type(array); // create typed array
+	}
 
 
-		for (let i = 0; i !== n; ++i) result[i] = i;
+	return Array.prototype.slice.call(array); // create Array
+}
 
 
-		result.sort(compareTime);
-		return result;
-	},
-	// uses the array previously returned by 'getKeyframeOrder' to sort data
-	sortedArray: function (values, stride, order) {
-		const nValues = values.length;
-		const result = new values.constructor(nValues);
+function isTypedArray(object) {
+	return ArrayBuffer.isView(object) && !(object instanceof DataView);
+} // returns an array by which times and values can be sorted
 
 
-		for (let i = 0, dstOffset = 0; dstOffset !== nValues; ++i) {
-			const srcOffset = order[i] * stride;
 
 
-			for (let j = 0; j !== stride; ++j) {
-				result[dstOffset++] = values[srcOffset + j];
-			}
-		}
+function getKeyframeOrder(times) {
+	function compareTime(i, j) {
+		return times[i] - times[j];
+	}
 
 
-		return result;
-	},
-	// function for parsing AOS keyframe formats
-	flattenJSON: function (jsonKeys, times, values, valuePropertyName) {
-		let i = 1,
-				key = jsonKeys[0];
+	const n = times.length;
+	const result = new Array(n);
 
 
-		while (key !== undefined && key[valuePropertyName] === undefined) {
-			key = jsonKeys[i++];
+	for (let i = 0; i !== n; ++i) result[i] = i;
+
+	result.sort(compareTime);
+	return result;
+} // uses the array previously returned by 'getKeyframeOrder' to sort data
+
+
+function sortedArray(values, stride, order) {
+	const nValues = values.length;
+	const result = new values.constructor(nValues);
+
+	for (let i = 0, dstOffset = 0; dstOffset !== nValues; ++i) {
+		const srcOffset = order[i] * stride;
+
+		for (let j = 0; j !== stride; ++j) {
+			result[dstOffset++] = values[srcOffset + j];
 		}
 		}
+	}
 
 
-		if (key === undefined) return; // no data
+	return result;
+} // function for parsing AOS keyframe formats
 
 
-		let value = key[valuePropertyName];
-		if (value === undefined) return; // no data
 
 
-		if (Array.isArray(value)) {
-			do {
-				value = key[valuePropertyName];
+function flattenJSON(jsonKeys, times, values, valuePropertyName) {
+	let i = 1,
+			key = jsonKeys[0];
 
 
-				if (value !== undefined) {
-					times.push(key.time);
-					values.push.apply(values, value); // push all elements
-				}
+	while (key !== undefined && key[valuePropertyName] === undefined) {
+		key = jsonKeys[i++];
+	}
 
 
-				key = jsonKeys[i++];
-			} while (key !== undefined);
-		} else if (value.toArray !== undefined) {
-			// ...assume THREE.Math-ish
-			do {
-				value = key[valuePropertyName];
+	if (key === undefined) return; // no data
 
 
-				if (value !== undefined) {
-					times.push(key.time);
-					value.toArray(values, values.length);
-				}
+	let value = key[valuePropertyName];
+	if (value === undefined) return; // no data
 
 
-				key = jsonKeys[i++];
-			} while (key !== undefined);
-		} else {
-			// otherwise push as-is
-			do {
-				value = key[valuePropertyName];
+	if (Array.isArray(value)) {
+		do {
+			value = key[valuePropertyName];
 
 
-				if (value !== undefined) {
-					times.push(key.time);
-					values.push(value);
-				}
+			if (value !== undefined) {
+				times.push(key.time);
+				values.push.apply(values, value); // push all elements
+			}
 
 
-				key = jsonKeys[i++];
-			} while (key !== undefined);
-		}
-	},
-	subclip: function (sourceClip, name, startFrame, endFrame, fps = 30) {
-		const clip = sourceClip.clone();
-		clip.name = name;
-		const tracks = [];
+			key = jsonKeys[i++];
+		} while (key !== undefined);
+	} else if (value.toArray !== undefined) {
+		// ...assume THREE.Math-ish
+		do {
+			value = key[valuePropertyName];
 
 
-		for (let i = 0; i < clip.tracks.length; ++i) {
-			const track = clip.tracks[i];
-			const valueSize = track.getValueSize();
-			const times = [];
-			const values = [];
+			if (value !== undefined) {
+				times.push(key.time);
+				value.toArray(values, values.length);
+			}
 
 
-			for (let j = 0; j < track.times.length; ++j) {
-				const frame = track.times[j] * fps;
-				if (frame < startFrame || frame >= endFrame) continue;
-				times.push(track.times[j]);
+			key = jsonKeys[i++];
+		} while (key !== undefined);
+	} else {
+		// otherwise push as-is
+		do {
+			value = key[valuePropertyName];
 
 
-				for (let k = 0; k < valueSize; ++k) {
-					values.push(track.values[j * valueSize + k]);
-				}
+			if (value !== undefined) {
+				times.push(key.time);
+				values.push(value);
 			}
 			}
 
 
-			if (times.length === 0) continue;
-			track.times = AnimationUtils.convertArray(times, track.times.constructor);
-			track.values = AnimationUtils.convertArray(values, track.values.constructor);
-			tracks.push(track);
-		}
+			key = jsonKeys[i++];
+		} while (key !== undefined);
+	}
+}
 
 
-		clip.tracks = tracks; // find minimum .times value across all tracks in the trimmed clip
+function subclip(sourceClip, name, startFrame, endFrame, fps = 30) {
+	const clip = sourceClip.clone();
+	clip.name = name;
+	const tracks = [];
 
 
-		let minStartTime = Infinity;
+	for (let i = 0; i < clip.tracks.length; ++i) {
+		const track = clip.tracks[i];
+		const valueSize = track.getValueSize();
+		const times = [];
+		const values = [];
 
 
-		for (let i = 0; i < clip.tracks.length; ++i) {
-			if (minStartTime > clip.tracks[i].times[0]) {
-				minStartTime = clip.tracks[i].times[0];
+		for (let j = 0; j < track.times.length; ++j) {
+			const frame = track.times[j] * fps;
+			if (frame < startFrame || frame >= endFrame) continue;
+			times.push(track.times[j]);
+
+			for (let k = 0; k < valueSize; ++k) {
+				values.push(track.values[j * valueSize + k]);
 			}
 			}
-		} // shift all tracks such that clip begins at t=0
+		}
+
+		if (times.length === 0) continue;
+		track.times = convertArray(times, track.times.constructor);
+		track.values = convertArray(values, track.values.constructor);
+		tracks.push(track);
+	}
+
+	clip.tracks = tracks; // find minimum .times value across all tracks in the trimmed clip
 
 
+	let minStartTime = Infinity;
 
 
-		for (let i = 0; i < clip.tracks.length; ++i) {
-			clip.tracks[i].shift(-1 * minStartTime);
+	for (let i = 0; i < clip.tracks.length; ++i) {
+		if (minStartTime > clip.tracks[i].times[0]) {
+			minStartTime = clip.tracks[i].times[0];
 		}
 		}
+	} // shift all tracks such that clip begins at t=0
 
 
-		clip.resetDuration();
-		return clip;
-	},
-	makeClipAdditive: function (targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30) {
-		if (fps <= 0) fps = 30;
-		const numTracks = referenceClip.tracks.length;
-		const referenceTime = referenceFrame / fps; // Make each track's values relative to the values at the reference frame
 
 
-		for (let i = 0; i < numTracks; ++i) {
-			const referenceTrack = referenceClip.tracks[i];
-			const referenceTrackType = referenceTrack.ValueTypeName; // Skip this track if it's non-numeric
+	for (let i = 0; i < clip.tracks.length; ++i) {
+		clip.tracks[i].shift(-1 * minStartTime);
+	}
 
 
-			if (referenceTrackType === 'bool' || referenceTrackType === 'string') continue; // Find the track in the target clip whose name and type matches the reference track
+	clip.resetDuration();
+	return clip;
+}
 
 
-			const targetTrack = targetClip.tracks.find(function (track) {
-				return track.name === referenceTrack.name && track.ValueTypeName === referenceTrackType;
-			});
-			if (targetTrack === undefined) continue;
-			let referenceOffset = 0;
-			const referenceValueSize = referenceTrack.getValueSize();
+function makeClipAdditive(targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30) {
+	if (fps <= 0) fps = 30;
+	const numTracks = referenceClip.tracks.length;
+	const referenceTime = referenceFrame / fps; // Make each track's values relative to the values at the reference frame
 
 
-			if (referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
-				referenceOffset = referenceValueSize / 3;
-			}
+	for (let i = 0; i < numTracks; ++i) {
+		const referenceTrack = referenceClip.tracks[i];
+		const referenceTrackType = referenceTrack.ValueTypeName; // Skip this track if it's non-numeric
 
 
-			let targetOffset = 0;
-			const targetValueSize = targetTrack.getValueSize();
+		if (referenceTrackType === 'bool' || referenceTrackType === 'string') continue; // Find the track in the target clip whose name and type matches the reference track
 
 
-			if (targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
-				targetOffset = targetValueSize / 3;
-			}
+		const targetTrack = targetClip.tracks.find(function (track) {
+			return track.name === referenceTrack.name && track.ValueTypeName === referenceTrackType;
+		});
+		if (targetTrack === undefined) continue;
+		let referenceOffset = 0;
+		const referenceValueSize = referenceTrack.getValueSize();
 
 
-			const lastIndex = referenceTrack.times.length - 1;
-			let referenceValue; // Find the value to subtract out of the track
+		if (referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
+			referenceOffset = referenceValueSize / 3;
+		}
 
 
-			if (referenceTime <= referenceTrack.times[0]) {
-				// Reference frame is earlier than the first keyframe, so just use the first keyframe
-				const startIndex = referenceOffset;
-				const endIndex = referenceValueSize - referenceOffset;
-				referenceValue = AnimationUtils.arraySlice(referenceTrack.values, startIndex, endIndex);
-			} else if (referenceTime >= referenceTrack.times[lastIndex]) {
-				// Reference frame is after the last keyframe, so just use the last keyframe
-				const startIndex = lastIndex * referenceValueSize + referenceOffset;
-				const endIndex = startIndex + referenceValueSize - referenceOffset;
-				referenceValue = AnimationUtils.arraySlice(referenceTrack.values, startIndex, endIndex);
-			} else {
-				// Interpolate to the reference value
-				const interpolant = referenceTrack.createInterpolant();
-				const startIndex = referenceOffset;
-				const endIndex = referenceValueSize - referenceOffset;
-				interpolant.evaluate(referenceTime);
-				referenceValue = AnimationUtils.arraySlice(interpolant.resultBuffer, startIndex, endIndex);
-			} // Conjugate the quaternion
+		let targetOffset = 0;
+		const targetValueSize = targetTrack.getValueSize();
 
 
+		if (targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
+			targetOffset = targetValueSize / 3;
+		}
 
 
-			if (referenceTrackType === 'quaternion') {
-				const referenceQuat = new Quaternion().fromArray(referenceValue).normalize().conjugate();
-				referenceQuat.toArray(referenceValue);
-			} // Subtract the reference value from all of the track values
+		const lastIndex = referenceTrack.times.length - 1;
+		let referenceValue; // Find the value to subtract out of the track
 
 
+		if (referenceTime <= referenceTrack.times[0]) {
+			// Reference frame is earlier than the first keyframe, so just use the first keyframe
+			const startIndex = referenceOffset;
+			const endIndex = referenceValueSize - referenceOffset;
+			referenceValue = arraySlice(referenceTrack.values, startIndex, endIndex);
+		} else if (referenceTime >= referenceTrack.times[lastIndex]) {
+			// Reference frame is after the last keyframe, so just use the last keyframe
+			const startIndex = lastIndex * referenceValueSize + referenceOffset;
+			const endIndex = startIndex + referenceValueSize - referenceOffset;
+			referenceValue = arraySlice(referenceTrack.values, startIndex, endIndex);
+		} else {
+			// Interpolate to the reference value
+			const interpolant = referenceTrack.createInterpolant();
+			const startIndex = referenceOffset;
+			const endIndex = referenceValueSize - referenceOffset;
+			interpolant.evaluate(referenceTime);
+			referenceValue = arraySlice(interpolant.resultBuffer, startIndex, endIndex);
+		} // Conjugate the quaternion
 
 
-			const numTimes = targetTrack.times.length;
 
 
-			for (let j = 0; j < numTimes; ++j) {
-				const valueStart = j * targetValueSize + targetOffset;
+		if (referenceTrackType === 'quaternion') {
+			const referenceQuat = new Quaternion().fromArray(referenceValue).normalize().conjugate();
+			referenceQuat.toArray(referenceValue);
+		} // Subtract the reference value from all of the track values
 
 
-				if (referenceTrackType === 'quaternion') {
-					// Multiply the conjugate for quaternion track types
-					Quaternion.multiplyQuaternionsFlat(targetTrack.values, valueStart, referenceValue, 0, targetTrack.values, valueStart);
-				} else {
-					const valueEnd = targetValueSize - targetOffset * 2; // Subtract each value for all other numeric track types
 
 
-					for (let k = 0; k < valueEnd; ++k) {
-						targetTrack.values[valueStart + k] -= referenceValue[k];
-					}
+		const numTimes = targetTrack.times.length;
+
+		for (let j = 0; j < numTimes; ++j) {
+			const valueStart = j * targetValueSize + targetOffset;
+
+			if (referenceTrackType === 'quaternion') {
+				// Multiply the conjugate for quaternion track types
+				Quaternion.multiplyQuaternionsFlat(targetTrack.values, valueStart, referenceValue, 0, targetTrack.values, valueStart);
+			} else {
+				const valueEnd = targetValueSize - targetOffset * 2; // Subtract each value for all other numeric track types
+
+				for (let k = 0; k < valueEnd; ++k) {
+					targetTrack.values[valueStart + k] -= referenceValue[k];
 				}
 				}
 			}
 			}
 		}
 		}
-
-		targetClip.blendMode = AdditiveAnimationBlendMode;
-		return targetClip;
 	}
 	}
-};
+
+	targetClip.blendMode = AdditiveAnimationBlendMode;
+	return targetClip;
+}
+
+var AnimationUtils = /*#__PURE__*/Object.freeze({
+	__proto__: null,
+	arraySlice: arraySlice,
+	convertArray: convertArray,
+	isTypedArray: isTypedArray,
+	getKeyframeOrder: getKeyframeOrder,
+	sortedArray: sortedArray,
+	flattenJSON: flattenJSON,
+	subclip: subclip,
+	makeClipAdditive: makeClipAdditive
+});
 
 
 /**
 /**
  * Abstract base class of interpolants over parametric samples.
  * Abstract base class of interpolants over parametric samples.
@@ -27360,8 +27678,8 @@ class KeyframeTrack {
 		if (name === undefined) throw new Error('THREE.KeyframeTrack: track name is undefined');
 		if (name === undefined) throw new Error('THREE.KeyframeTrack: track name is undefined');
 		if (times === undefined || times.length === 0) throw new Error('THREE.KeyframeTrack: no keyframes in track named ' + name);
 		if (times === undefined || times.length === 0) throw new Error('THREE.KeyframeTrack: no keyframes in track named ' + name);
 		this.name = name;
 		this.name = name;
-		this.times = AnimationUtils.convertArray(times, this.TimeBufferType);
-		this.values = AnimationUtils.convertArray(values, this.ValueBufferType);
+		this.times = convertArray(times, this.TimeBufferType);
+		this.values = convertArray(values, this.ValueBufferType);
 		this.setInterpolation(interpolation || this.DefaultInterpolation);
 		this.setInterpolation(interpolation || this.DefaultInterpolation);
 	} // Serialization (in static context, because of constructor invocation
 	} // Serialization (in static context, because of constructor invocation
 	// and automatic invocation of .toJSON):
 	// and automatic invocation of .toJSON):
@@ -27377,8 +27695,8 @@ class KeyframeTrack {
 			// by default, we assume the data can be serialized as-is
 			// by default, we assume the data can be serialized as-is
 			json = {
 			json = {
 				'name': track.name,
 				'name': track.name,
-				'times': AnimationUtils.convertArray(track.times, Array),
-				'values': AnimationUtils.convertArray(track.values, Array)
+				'times': convertArray(track.times, Array),
+				'values': convertArray(track.values, Array)
 			};
 			};
 			const interpolation = track.getInterpolation();
 			const interpolation = track.getInterpolation();
 
 
@@ -27510,8 +27828,8 @@ class KeyframeTrack {
 			}
 			}
 
 
 			const stride = this.getValueSize();
 			const stride = this.getValueSize();
-			this.times = AnimationUtils.arraySlice(times, from, to);
-			this.values = AnimationUtils.arraySlice(this.values, from * stride, to * stride);
+			this.times = arraySlice(times, from, to);
+			this.values = arraySlice(this.values, from * stride, to * stride);
 		}
 		}
 
 
 		return this;
 		return this;
@@ -27557,7 +27875,7 @@ class KeyframeTrack {
 		}
 		}
 
 
 		if (values !== undefined) {
 		if (values !== undefined) {
-			if (AnimationUtils.isTypedArray(values)) {
+			if (isTypedArray(values)) {
 				for (let i = 0, n = values.length; i !== n; ++i) {
 				for (let i = 0, n = values.length; i !== n; ++i) {
 					const value = values[i];
 					const value = values[i];
 
 
@@ -27577,8 +27895,8 @@ class KeyframeTrack {
 
 
 	optimize() {
 	optimize() {
 		// times or values may be shared with other tracks, so overwriting is unsafe
 		// times or values may be shared with other tracks, so overwriting is unsafe
-		const times = AnimationUtils.arraySlice(this.times),
-					values = AnimationUtils.arraySlice(this.values),
+		const times = arraySlice(this.times),
+					values = arraySlice(this.values),
 					stride = this.getValueSize(),
 					stride = this.getValueSize(),
 					smoothInterpolation = this.getInterpolation() === InterpolateSmooth,
 					smoothInterpolation = this.getInterpolation() === InterpolateSmooth,
 					lastIndex = times.length - 1;
 					lastIndex = times.length - 1;
@@ -27637,8 +27955,8 @@ class KeyframeTrack {
 		}
 		}
 
 
 		if (writeIndex !== times.length) {
 		if (writeIndex !== times.length) {
-			this.times = AnimationUtils.arraySlice(times, 0, writeIndex);
-			this.values = AnimationUtils.arraySlice(values, 0, writeIndex * stride);
+			this.times = arraySlice(times, 0, writeIndex);
+			this.values = arraySlice(values, 0, writeIndex * stride);
 		} else {
 		} else {
 			this.times = times;
 			this.times = times;
 			this.values = values;
 			this.values = values;
@@ -27648,8 +27966,8 @@ class KeyframeTrack {
 	}
 	}
 
 
 	clone() {
 	clone() {
-		const times = AnimationUtils.arraySlice(this.times, 0);
-		const values = AnimationUtils.arraySlice(this.values, 0);
+		const times = arraySlice(this.times, 0);
+		const values = arraySlice(this.values, 0);
 		const TypedKeyframeTrack = this.constructor;
 		const TypedKeyframeTrack = this.constructor;
 		const track = new TypedKeyframeTrack(this.name, times, values); // Interpolant argument to constructor is not saved, so copy the factory method directly.
 		const track = new TypedKeyframeTrack(this.name, times, values); // Interpolant argument to constructor is not saved, so copy the factory method directly.
 
 
@@ -27806,9 +28124,9 @@ class AnimationClip {
 			let values = [];
 			let values = [];
 			times.push((i + numMorphTargets - 1) % numMorphTargets, i, (i + 1) % numMorphTargets);
 			times.push((i + numMorphTargets - 1) % numMorphTargets, i, (i + 1) % numMorphTargets);
 			values.push(0, 1, 0);
 			values.push(0, 1, 0);
-			const order = AnimationUtils.getKeyframeOrder(times);
-			times = AnimationUtils.sortedArray(times, 1, order);
-			values = AnimationUtils.sortedArray(values, 1, order); // if there is a key at the first frame, duplicate it as the
+			const order = getKeyframeOrder(times);
+			times = sortedArray(times, 1, order);
+			values = sortedArray(values, 1, order); // if there is a key at the first frame, duplicate it as the
 			// last frame as well for perfect loop.
 			// last frame as well for perfect loop.
 
 
 			if (!noLoop && times[0] === 0) {
 			if (!noLoop && times[0] === 0) {
@@ -27883,7 +28201,7 @@ class AnimationClip {
 			if (animationKeys.length !== 0) {
 			if (animationKeys.length !== 0) {
 				const times = [];
 				const times = [];
 				const values = [];
 				const values = [];
-				AnimationUtils.flattenJSON(animationKeys, times, values, propertyName); // empty keys are filtered out, so check again
+				flattenJSON(animationKeys, times, values, propertyName); // empty keys are filtered out, so check again
 
 
 				if (times.length !== 0) {
 				if (times.length !== 0) {
 					destTracks.push(new trackType(trackName, times, values));
 					destTracks.push(new trackType(trackName, times, values));
@@ -28048,7 +28366,7 @@ function parseKeyframeTrack(json) {
 	if (json.times === undefined) {
 	if (json.times === undefined) {
 		const times = [],
 		const times = [],
 					values = [];
 					values = [];
-		AnimationUtils.flattenJSON(json.keys, times, values, 'value');
+		flattenJSON(json.keys, times, values, 'value');
 		json.times = times;
 		json.times = times;
 		json.values = values;
 		json.values = values;
 	} // derived classes can define a static parse method
 	} // derived classes can define a static parse method
@@ -33278,6 +33596,67 @@ class Uniform {
 
 
 }
 }
 
 
+let id = 0;
+
+class UniformsGroup extends EventDispatcher {
+	constructor() {
+		super();
+		this.isUniformsGroup = true;
+		Object.defineProperty(this, 'id', {
+			value: id++
+		});
+		this.name = '';
+		this.usage = StaticDrawUsage;
+		this.uniforms = [];
+	}
+
+	add(uniform) {
+		this.uniforms.push(uniform);
+		return this;
+	}
+
+	remove(uniform) {
+		const index = this.uniforms.indexOf(uniform);
+		if (index !== -1) this.uniforms.splice(index, 1);
+		return this;
+	}
+
+	setName(name) {
+		this.name = name;
+		return this;
+	}
+
+	setUsage(value) {
+		this.usage = value;
+		return this;
+	}
+
+	dispose() {
+		this.dispatchEvent({
+			type: 'dispose'
+		});
+		return this;
+	}
+
+	copy(source) {
+		this.name = source.name;
+		this.usage = source.usage;
+		const uniformsSource = source.uniforms;
+		this.uniforms.length = 0;
+
+		for (let i = 0, l = uniformsSource.length; i < l; i++) {
+			this.uniforms.push(uniformsSource[i].clone());
+		}
+
+		return this;
+	}
+
+	clone() {
+		return new this.constructor().copy(this);
+	}
+
+}
+
 class InstancedInterleavedBuffer extends InterleavedBuffer {
 class InstancedInterleavedBuffer extends InterleavedBuffer {
 	constructor(array, stride, meshPerAttribute = 1) {
 	constructor(array, stride, meshPerAttribute = 1) {
 		super(array, stride);
 		super(array, stride);
@@ -35439,6 +35818,7 @@ exports.Uint32BufferAttribute = Uint32BufferAttribute;
 exports.Uint8BufferAttribute = Uint8BufferAttribute;
 exports.Uint8BufferAttribute = Uint8BufferAttribute;
 exports.Uint8ClampedBufferAttribute = Uint8ClampedBufferAttribute;
 exports.Uint8ClampedBufferAttribute = Uint8ClampedBufferAttribute;
 exports.Uniform = Uniform;
 exports.Uniform = Uniform;
+exports.UniformsGroup = UniformsGroup;
 exports.UniformsLib = UniformsLib;
 exports.UniformsLib = UniformsLib;
 exports.UniformsUtils = UniformsUtils;
 exports.UniformsUtils = UniformsUtils;
 exports.UnsignedByteType = UnsignedByteType;
 exports.UnsignedByteType = UnsignedByteType;

+ 579 - 199
build/three.js

@@ -8686,6 +8686,15 @@
 		}
 		}
 
 
 		return merged;
 		return merged;
+	}
+	function cloneUniformsGroups(src) {
+		const dst = [];
+
+		for (let u = 0; u < src.length; u++) {
+			dst.push(src[u].clone());
+		}
+
+		return dst;
 	} // Legacy
 	} // Legacy
 
 
 	const UniformsUtils = {
 	const UniformsUtils = {
@@ -8704,6 +8713,7 @@
 			this.type = 'ShaderMaterial';
 			this.type = 'ShaderMaterial';
 			this.defines = {};
 			this.defines = {};
 			this.uniforms = {};
 			this.uniforms = {};
+			this.uniformsGroups = [];
 			this.vertexShader = default_vertex;
 			this.vertexShader = default_vertex;
 			this.fragmentShader = default_fragment;
 			this.fragmentShader = default_fragment;
 			this.linewidth = 1;
 			this.linewidth = 1;
@@ -8750,6 +8760,7 @@
 			this.fragmentShader = source.fragmentShader;
 			this.fragmentShader = source.fragmentShader;
 			this.vertexShader = source.vertexShader;
 			this.vertexShader = source.vertexShader;
 			this.uniforms = cloneUniforms(source.uniforms);
 			this.uniforms = cloneUniforms(source.uniforms);
+			this.uniformsGroups = cloneUniformsGroups(source.uniformsGroups);
 			this.defines = Object.assign({}, source.defines);
 			this.defines = Object.assign({}, source.defines);
 			this.wireframe = source.wireframe;
 			this.wireframe = source.wireframe;
 			this.wireframeLinewidth = source.wireframeLinewidth;
 			this.wireframeLinewidth = source.wireframeLinewidth;
@@ -15880,6 +15891,8 @@
 		const colorBuffer = new ColorBuffer();
 		const colorBuffer = new ColorBuffer();
 		const depthBuffer = new DepthBuffer();
 		const depthBuffer = new DepthBuffer();
 		const stencilBuffer = new StencilBuffer();
 		const stencilBuffer = new StencilBuffer();
+		const uboBindings = new WeakMap();
+		const uboProgamMap = new WeakMap();
 		let enabledCapabilities = {};
 		let enabledCapabilities = {};
 		let currentBoundFramebuffers = {};
 		let currentBoundFramebuffers = {};
 		let currentDrawbuffers = new WeakMap();
 		let currentDrawbuffers = new WeakMap();
@@ -16378,6 +16391,33 @@
 				gl.viewport(viewport.x, viewport.y, viewport.z, viewport.w);
 				gl.viewport(viewport.x, viewport.y, viewport.z, viewport.w);
 				currentViewport.copy(viewport);
 				currentViewport.copy(viewport);
 			}
 			}
+		}
+
+		function updateUBOMapping(uniformsGroup, program) {
+			let mapping = uboProgamMap.get(program);
+
+			if (mapping === undefined) {
+				mapping = new WeakMap();
+				uboProgamMap.set(program, mapping);
+			}
+
+			let blockIndex = mapping.get(uniformsGroup);
+
+			if (blockIndex === undefined) {
+				blockIndex = gl.getUniformBlockIndex(program, uniformsGroup.name);
+				mapping.set(uniformsGroup, blockIndex);
+			}
+		}
+
+		function uniformBlockBinding(uniformsGroup, program) {
+			const mapping = uboProgamMap.get(program);
+			const blockIndex = mapping.get(uniformsGroup);
+
+			if (uboBindings.get(uniformsGroup) !== blockIndex) {
+				// bind shader specific block index to global block point
+				gl.uniformBlockBinding(program, blockIndex, uniformsGroup.__bindingPointIndex);
+				uboBindings.set(uniformsGroup, blockIndex);
+			}
 		} //
 		} //
 
 
 
 
@@ -16470,6 +16510,8 @@
 			compressedTexImage2D: compressedTexImage2D,
 			compressedTexImage2D: compressedTexImage2D,
 			texImage2D: texImage2D,
 			texImage2D: texImage2D,
 			texImage3D: texImage3D,
 			texImage3D: texImage3D,
+			updateUBOMapping: updateUBOMapping,
+			uniformBlockBinding: uniformBlockBinding,
 			texStorage2D: texStorage2D,
 			texStorage2D: texStorage2D,
 			texStorage3D: texStorage3D,
 			texStorage3D: texStorage3D,
 			texSubImage2D: texSubImage2D,
 			texSubImage2D: texSubImage2D,
@@ -19187,6 +19229,249 @@
 		};
 		};
 	}
 	}
 
 
+	function WebGLUniformsGroups(gl, info, capabilities, state) {
+		let buffers = {};
+		let updateList = {};
+		let allocatedBindingPoints = [];
+		const maxBindingPoints = capabilities.isWebGL2 ? gl.getParameter(gl.MAX_UNIFORM_BUFFER_BINDINGS) : 0; // binding points are global whereas block indices are per shader program
+
+		function bind(uniformsGroup, program) {
+			const webglProgram = program.program;
+			state.uniformBlockBinding(uniformsGroup, webglProgram);
+		}
+
+		function update(uniformsGroup, program) {
+			let buffer = buffers[uniformsGroup.id];
+
+			if (buffer === undefined) {
+				prepareUniformsGroup(uniformsGroup);
+				buffer = createBuffer(uniformsGroup);
+				buffers[uniformsGroup.id] = buffer;
+				uniformsGroup.addEventListener('dispose', onUniformsGroupsDispose);
+			} // ensure to update the binding points/block indices mapping for this program
+
+
+			const webglProgram = program.program;
+			state.updateUBOMapping(uniformsGroup, webglProgram); // update UBO once per frame
+
+			const frame = info.render.frame;
+
+			if (updateList[uniformsGroup.id] !== frame) {
+				updateBufferData(uniformsGroup);
+				updateList[uniformsGroup.id] = frame;
+			}
+		}
+
+		function createBuffer(uniformsGroup) {
+			// the setup of an UBO is independent of a particular shader program but global
+			const bindingPointIndex = allocateBindingPointIndex();
+			uniformsGroup.__bindingPointIndex = bindingPointIndex;
+			const buffer = gl.createBuffer();
+			const size = uniformsGroup.__size;
+			const usage = uniformsGroup.usage;
+			gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
+			gl.bufferData(gl.UNIFORM_BUFFER, size, usage);
+			gl.bindBuffer(gl.UNIFORM_BUFFER, null);
+			gl.bindBufferBase(gl.UNIFORM_BUFFER, bindingPointIndex, buffer);
+			return buffer;
+		}
+
+		function allocateBindingPointIndex() {
+			for (let i = 0; i < maxBindingPoints; i++) {
+				if (allocatedBindingPoints.indexOf(i) === -1) {
+					allocatedBindingPoints.push(i);
+					return i;
+				}
+			}
+
+			console.error('THREE.WebGLRenderer: Maximum number of simultaneously usable uniforms groups reached.');
+			return 0;
+		}
+
+		function updateBufferData(uniformsGroup) {
+			const buffer = buffers[uniformsGroup.id];
+			const uniforms = uniformsGroup.uniforms;
+			const cache = uniformsGroup.__cache;
+			gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
+
+			for (let i = 0, il = uniforms.length; i < il; i++) {
+				const uniform = uniforms[i]; // partly update the buffer if necessary
+
+				if (hasUniformChanged(uniform, i, cache) === true) {
+					const value = uniform.value;
+					const offset = uniform.__offset;
+
+					if (typeof value === 'number') {
+						uniform.__data[0] = value;
+						gl.bufferSubData(gl.UNIFORM_BUFFER, offset, uniform.__data);
+					} else {
+						if (uniform.value.isMatrix3) {
+							// manually converting 3x3 to 3x4
+							uniform.__data[0] = uniform.value.elements[0];
+							uniform.__data[1] = uniform.value.elements[1];
+							uniform.__data[2] = uniform.value.elements[2];
+							uniform.__data[3] = uniform.value.elements[0];
+							uniform.__data[4] = uniform.value.elements[3];
+							uniform.__data[5] = uniform.value.elements[4];
+							uniform.__data[6] = uniform.value.elements[5];
+							uniform.__data[7] = uniform.value.elements[0];
+							uniform.__data[8] = uniform.value.elements[6];
+							uniform.__data[9] = uniform.value.elements[7];
+							uniform.__data[10] = uniform.value.elements[8];
+							uniform.__data[11] = uniform.value.elements[0];
+						} else {
+							value.toArray(uniform.__data);
+						}
+
+						gl.bufferSubData(gl.UNIFORM_BUFFER, offset, uniform.__data);
+					}
+				}
+			}
+
+			gl.bindBuffer(gl.UNIFORM_BUFFER, null);
+		}
+
+		function hasUniformChanged(uniform, index, cache) {
+			const value = uniform.value;
+
+			if (cache[index] === undefined) {
+				// cache entry does not exist so far
+				if (typeof value === 'number') {
+					cache[index] = value;
+				} else {
+					cache[index] = value.clone();
+				}
+
+				return true;
+			} else {
+				// compare current value with cached entry
+				if (typeof value === 'number') {
+					if (cache[index] !== value) {
+						cache[index] = value;
+						return true;
+					}
+				} else {
+					const cachedObject = cache[index];
+
+					if (cachedObject.equals(value) === false) {
+						cachedObject.copy(value);
+						return true;
+					}
+				}
+			}
+
+			return false;
+		}
+
+		function prepareUniformsGroup(uniformsGroup) {
+			// determine total buffer size according to the STD140 layout
+			// Hint: STD140 is the only supported layout in WebGL 2
+			const uniforms = uniformsGroup.uniforms;
+			let offset = 0; // global buffer offset in bytes
+
+			const chunkSize = 16; // size of a chunk in bytes
+
+			let chunkOffset = 0; // offset within a single chunk in bytes
+
+			for (let i = 0, l = uniforms.length; i < l; i++) {
+				const uniform = uniforms[i];
+				const info = getUniformSize(uniform); // the following two properties will be used for partial buffer updates
+
+				uniform.__data = new Float32Array(info.storage / Float32Array.BYTES_PER_ELEMENT);
+				uniform.__offset = offset; //
+
+				if (i > 0) {
+					chunkOffset = offset % chunkSize;
+					const remainingSizeInChunk = chunkSize - chunkOffset; // check for chunk overflow
+
+					if (chunkOffset !== 0 && remainingSizeInChunk - info.boundary < 0) {
+						// add padding and adjust offset
+						offset += chunkSize - chunkOffset;
+						uniform.__offset = offset;
+					}
+				}
+
+				offset += info.storage;
+			} // ensure correct final padding
+
+
+			chunkOffset = offset % chunkSize;
+			if (chunkOffset > 0) offset += chunkSize - chunkOffset; //
+
+			uniformsGroup.__size = offset;
+			uniformsGroup.__cache = {};
+			return this;
+		}
+
+		function getUniformSize(uniform) {
+			const value = uniform.value;
+			const info = {
+				boundary: 0,
+				// bytes
+				storage: 0 // bytes
+
+			}; // determine sizes according to STD140
+
+			if (typeof value === 'number') {
+				// float/int
+				info.boundary = 4;
+				info.storage = 4;
+			} else if (value.isVector2) {
+				// vec2
+				info.boundary = 8;
+				info.storage = 8;
+			} else if (value.isVector3 || value.isColor) {
+				// vec3
+				info.boundary = 16;
+				info.storage = 12; // evil: vec3 must start on a 16-byte boundary but it only consumes 12 bytes
+			} else if (value.isVector4) {
+				// vec4
+				info.boundary = 16;
+				info.storage = 16;
+			} else if (value.isMatrix3) {
+				// mat3 (in STD140 a 3x3 matrix is represented as 3x4)
+				info.boundary = 48;
+				info.storage = 48;
+			} else if (value.isMatrix4) {
+				// mat4
+				info.boundary = 64;
+				info.storage = 64;
+			} else if (value.isTexture) {
+				console.warn('THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group.');
+			} else {
+				console.warn('THREE.WebGLRenderer: Unsupported uniform value type.', value);
+			}
+
+			return info;
+		}
+
+		function onUniformsGroupsDispose(event) {
+			const uniformsGroup = event.target;
+			uniformsGroup.removeEventListener('dispose', onUniformsGroupsDispose);
+			const index = allocatedBindingPoints.indexOf(uniformsGroup.__bindingPointIndex);
+			allocatedBindingPoints.splice(index, 1);
+			gl.deleteBuffer(buffers[uniformsGroup.id]);
+			delete buffers[uniformsGroup.id];
+			delete updateList[uniformsGroup.id];
+		}
+
+		function dispose() {
+			for (const id in buffers) {
+				gl.deleteBuffer(buffers[id]);
+			}
+
+			allocatedBindingPoints = [];
+			buffers = {};
+			updateList = {};
+		}
+
+		return {
+			bind: bind,
+			update: update,
+			dispose: dispose
+		};
+	}
+
 	function createCanvasElement() {
 	function createCanvasElement() {
 		const canvas = createElementNS('canvas');
 		const canvas = createElementNS('canvas');
 		canvas.style.display = 'block';
 		canvas.style.display = 'block';
@@ -19389,7 +19674,7 @@
 		let properties, textures, cubemaps, cubeuvmaps, attributes, geometries, objects;
 		let properties, textures, cubemaps, cubeuvmaps, attributes, geometries, objects;
 		let programCache, materials, renderLists, renderStates, clipping, shadowMap;
 		let programCache, materials, renderLists, renderStates, clipping, shadowMap;
 		let background, morphtargets, bufferRenderer, indexedBufferRenderer;
 		let background, morphtargets, bufferRenderer, indexedBufferRenderer;
-		let utils, bindingStates;
+		let utils, bindingStates, uniformsGroups;
 
 
 		function initGLContext() {
 		function initGLContext() {
 			extensions = new WebGLExtensions(_gl);
 			extensions = new WebGLExtensions(_gl);
@@ -19414,6 +19699,7 @@
 			renderStates = new WebGLRenderStates(extensions, capabilities);
 			renderStates = new WebGLRenderStates(extensions, capabilities);
 			background = new WebGLBackground(_this, cubemaps, state, objects, _alpha, _premultipliedAlpha);
 			background = new WebGLBackground(_this, cubemaps, state, objects, _alpha, _premultipliedAlpha);
 			shadowMap = new WebGLShadowMap(_this, objects, capabilities);
 			shadowMap = new WebGLShadowMap(_this, objects, capabilities);
+			uniformsGroups = new WebGLUniformsGroups(_gl, info, capabilities, state);
 			bufferRenderer = new WebGLBufferRenderer(_gl, extensions, info, capabilities);
 			bufferRenderer = new WebGLBufferRenderer(_gl, extensions, info, capabilities);
 			indexedBufferRenderer = new WebGLIndexedBufferRenderer(_gl, extensions, info, capabilities);
 			indexedBufferRenderer = new WebGLIndexedBufferRenderer(_gl, extensions, info, capabilities);
 			info.programs = programCache.programs;
 			info.programs = programCache.programs;
@@ -19596,6 +19882,7 @@
 			cubeuvmaps.dispose();
 			cubeuvmaps.dispose();
 			objects.dispose();
 			objects.dispose();
 			bindingStates.dispose();
 			bindingStates.dispose();
+			uniformsGroups.dispose();
 			programCache.dispose();
 			programCache.dispose();
 			xr.dispose();
 			xr.dispose();
 			xr.removeEventListener('sessionstart', onXRSessionStart);
 			xr.removeEventListener('sessionstart', onXRSessionStart);
@@ -20359,7 +20646,22 @@
 
 
 			p_uniforms.setValue(_gl, 'modelViewMatrix', object.modelViewMatrix);
 			p_uniforms.setValue(_gl, 'modelViewMatrix', object.modelViewMatrix);
 			p_uniforms.setValue(_gl, 'normalMatrix', object.normalMatrix);
 			p_uniforms.setValue(_gl, 'normalMatrix', object.normalMatrix);
-			p_uniforms.setValue(_gl, 'modelMatrix', object.matrixWorld);
+			p_uniforms.setValue(_gl, 'modelMatrix', object.matrixWorld); // UBOs
+
+			if (material.isShaderMaterial || material.isRawShaderMaterial) {
+				const groups = material.uniformsGroups;
+
+				for (let i = 0, l = groups.length; i < l; i++) {
+					if (capabilities.isWebGL2) {
+						const group = groups[i];
+						uniformsGroups.update(group, program);
+						uniformsGroups.bind(group, program);
+					} else {
+						console.warn('THREE.WebGLRenderer: Uniform Buffer Objects can only be used with WebGL 2.');
+					}
+				}
+			}
+
 			return program;
 			return program;
 		} // If uniforms are marked as clean, they don't need to be loaded to the GPU.
 		} // If uniforms are marked as clean, they don't need to be loaded to the GPU.
 
 
@@ -23548,7 +23850,7 @@
 	}
 	}
 
 
 	class LatheGeometry extends BufferGeometry {
 	class LatheGeometry extends BufferGeometry {
-		constructor(points = [new Vector2(0, 0.5), new Vector2(0.5, 0), new Vector2(0, -0.5)], segments = 12, phiStart = 0, phiLength = Math.PI * 2) {
+		constructor(points = [new Vector2(0, -0.5), new Vector2(0.5, 0), new Vector2(0, 0.5)], segments = 12, phiStart = 0, phiLength = Math.PI * 2) {
 			super();
 			super();
 			this.type = 'LatheGeometry';
 			this.type = 'LatheGeometry';
 			this.parameters = {
 			this.parameters = {
@@ -26799,236 +27101,252 @@
 
 
 	}
 	}
 
 
-	const AnimationUtils = {
-		// same as Array.prototype.slice, but also works on typed arrays
-		arraySlice: function (array, from, to) {
-			if (AnimationUtils.isTypedArray(array)) {
-				// in ios9 array.subarray(from, undefined) will return empty array
-				// but array.subarray(from) or array.subarray(from, len) is correct
-				return new array.constructor(array.subarray(from, to !== undefined ? to : array.length));
-			}
+	function arraySlice(array, from, to) {
+		if (isTypedArray(array)) {
+			// in ios9 array.subarray(from, undefined) will return empty array
+			// but array.subarray(from) or array.subarray(from, len) is correct
+			return new array.constructor(array.subarray(from, to !== undefined ? to : array.length));
+		}
 
 
-			return array.slice(from, to);
-		},
-		// converts an array to a specific type
-		convertArray: function (array, type, forceClone) {
-			if (!array || // let 'undefined' and 'null' pass
-			!forceClone && array.constructor === type) return array;
+		return array.slice(from, to);
+	} // converts an array to a specific type
 
 
-			if (typeof type.BYTES_PER_ELEMENT === 'number') {
-				return new type(array); // create typed array
-			}
 
 
-			return Array.prototype.slice.call(array); // create Array
-		},
-		isTypedArray: function (object) {
-			return ArrayBuffer.isView(object) && !(object instanceof DataView);
-		},
-		// returns an array by which times and values can be sorted
-		getKeyframeOrder: function (times) {
-			function compareTime(i, j) {
-				return times[i] - times[j];
-			}
+	function convertArray(array, type, forceClone) {
+		if (!array || // let 'undefined' and 'null' pass
+		!forceClone && array.constructor === type) return array;
 
 
-			const n = times.length;
-			const result = new Array(n);
+		if (typeof type.BYTES_PER_ELEMENT === 'number') {
+			return new type(array); // create typed array
+		}
 
 
-			for (let i = 0; i !== n; ++i) result[i] = i;
+		return Array.prototype.slice.call(array); // create Array
+	}
 
 
-			result.sort(compareTime);
-			return result;
-		},
-		// uses the array previously returned by 'getKeyframeOrder' to sort data
-		sortedArray: function (values, stride, order) {
-			const nValues = values.length;
-			const result = new values.constructor(nValues);
+	function isTypedArray(object) {
+		return ArrayBuffer.isView(object) && !(object instanceof DataView);
+	} // returns an array by which times and values can be sorted
 
 
-			for (let i = 0, dstOffset = 0; dstOffset !== nValues; ++i) {
-				const srcOffset = order[i] * stride;
 
 
-				for (let j = 0; j !== stride; ++j) {
-					result[dstOffset++] = values[srcOffset + j];
-				}
-			}
+	function getKeyframeOrder(times) {
+		function compareTime(i, j) {
+			return times[i] - times[j];
+		}
 
 
-			return result;
-		},
-		// function for parsing AOS keyframe formats
-		flattenJSON: function (jsonKeys, times, values, valuePropertyName) {
-			let i = 1,
-					key = jsonKeys[0];
+		const n = times.length;
+		const result = new Array(n);
 
 
-			while (key !== undefined && key[valuePropertyName] === undefined) {
-				key = jsonKeys[i++];
+		for (let i = 0; i !== n; ++i) result[i] = i;
+
+		result.sort(compareTime);
+		return result;
+	} // uses the array previously returned by 'getKeyframeOrder' to sort data
+
+
+	function sortedArray(values, stride, order) {
+		const nValues = values.length;
+		const result = new values.constructor(nValues);
+
+		for (let i = 0, dstOffset = 0; dstOffset !== nValues; ++i) {
+			const srcOffset = order[i] * stride;
+
+			for (let j = 0; j !== stride; ++j) {
+				result[dstOffset++] = values[srcOffset + j];
 			}
 			}
+		}
 
 
-			if (key === undefined) return; // no data
+		return result;
+	} // function for parsing AOS keyframe formats
 
 
-			let value = key[valuePropertyName];
-			if (value === undefined) return; // no data
 
 
-			if (Array.isArray(value)) {
-				do {
-					value = key[valuePropertyName];
+	function flattenJSON(jsonKeys, times, values, valuePropertyName) {
+		let i = 1,
+				key = jsonKeys[0];
 
 
-					if (value !== undefined) {
-						times.push(key.time);
-						values.push.apply(values, value); // push all elements
-					}
+		while (key !== undefined && key[valuePropertyName] === undefined) {
+			key = jsonKeys[i++];
+		}
 
 
-					key = jsonKeys[i++];
-				} while (key !== undefined);
-			} else if (value.toArray !== undefined) {
-				// ...assume THREE.Math-ish
-				do {
-					value = key[valuePropertyName];
+		if (key === undefined) return; // no data
 
 
-					if (value !== undefined) {
-						times.push(key.time);
-						value.toArray(values, values.length);
-					}
+		let value = key[valuePropertyName];
+		if (value === undefined) return; // no data
 
 
-					key = jsonKeys[i++];
-				} while (key !== undefined);
-			} else {
-				// otherwise push as-is
-				do {
-					value = key[valuePropertyName];
+		if (Array.isArray(value)) {
+			do {
+				value = key[valuePropertyName];
 
 
-					if (value !== undefined) {
-						times.push(key.time);
-						values.push(value);
-					}
+				if (value !== undefined) {
+					times.push(key.time);
+					values.push.apply(values, value); // push all elements
+				}
 
 
-					key = jsonKeys[i++];
-				} while (key !== undefined);
-			}
-		},
-		subclip: function (sourceClip, name, startFrame, endFrame, fps = 30) {
-			const clip = sourceClip.clone();
-			clip.name = name;
-			const tracks = [];
+				key = jsonKeys[i++];
+			} while (key !== undefined);
+		} else if (value.toArray !== undefined) {
+			// ...assume THREE.Math-ish
+			do {
+				value = key[valuePropertyName];
 
 
-			for (let i = 0; i < clip.tracks.length; ++i) {
-				const track = clip.tracks[i];
-				const valueSize = track.getValueSize();
-				const times = [];
-				const values = [];
+				if (value !== undefined) {
+					times.push(key.time);
+					value.toArray(values, values.length);
+				}
 
 
-				for (let j = 0; j < track.times.length; ++j) {
-					const frame = track.times[j] * fps;
-					if (frame < startFrame || frame >= endFrame) continue;
-					times.push(track.times[j]);
+				key = jsonKeys[i++];
+			} while (key !== undefined);
+		} else {
+			// otherwise push as-is
+			do {
+				value = key[valuePropertyName];
 
 
-					for (let k = 0; k < valueSize; ++k) {
-						values.push(track.values[j * valueSize + k]);
-					}
+				if (value !== undefined) {
+					times.push(key.time);
+					values.push(value);
 				}
 				}
 
 
-				if (times.length === 0) continue;
-				track.times = AnimationUtils.convertArray(times, track.times.constructor);
-				track.values = AnimationUtils.convertArray(values, track.values.constructor);
-				tracks.push(track);
-			}
+				key = jsonKeys[i++];
+			} while (key !== undefined);
+		}
+	}
 
 
-			clip.tracks = tracks; // find minimum .times value across all tracks in the trimmed clip
+	function subclip(sourceClip, name, startFrame, endFrame, fps = 30) {
+		const clip = sourceClip.clone();
+		clip.name = name;
+		const tracks = [];
 
 
-			let minStartTime = Infinity;
+		for (let i = 0; i < clip.tracks.length; ++i) {
+			const track = clip.tracks[i];
+			const valueSize = track.getValueSize();
+			const times = [];
+			const values = [];
 
 
-			for (let i = 0; i < clip.tracks.length; ++i) {
-				if (minStartTime > clip.tracks[i].times[0]) {
-					minStartTime = clip.tracks[i].times[0];
+			for (let j = 0; j < track.times.length; ++j) {
+				const frame = track.times[j] * fps;
+				if (frame < startFrame || frame >= endFrame) continue;
+				times.push(track.times[j]);
+
+				for (let k = 0; k < valueSize; ++k) {
+					values.push(track.values[j * valueSize + k]);
 				}
 				}
-			} // shift all tracks such that clip begins at t=0
+			}
+
+			if (times.length === 0) continue;
+			track.times = convertArray(times, track.times.constructor);
+			track.values = convertArray(values, track.values.constructor);
+			tracks.push(track);
+		}
+
+		clip.tracks = tracks; // find minimum .times value across all tracks in the trimmed clip
 
 
+		let minStartTime = Infinity;
 
 
-			for (let i = 0; i < clip.tracks.length; ++i) {
-				clip.tracks[i].shift(-1 * minStartTime);
+		for (let i = 0; i < clip.tracks.length; ++i) {
+			if (minStartTime > clip.tracks[i].times[0]) {
+				minStartTime = clip.tracks[i].times[0];
 			}
 			}
+		} // shift all tracks such that clip begins at t=0
 
 
-			clip.resetDuration();
-			return clip;
-		},
-		makeClipAdditive: function (targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30) {
-			if (fps <= 0) fps = 30;
-			const numTracks = referenceClip.tracks.length;
-			const referenceTime = referenceFrame / fps; // Make each track's values relative to the values at the reference frame
 
 
-			for (let i = 0; i < numTracks; ++i) {
-				const referenceTrack = referenceClip.tracks[i];
-				const referenceTrackType = referenceTrack.ValueTypeName; // Skip this track if it's non-numeric
+		for (let i = 0; i < clip.tracks.length; ++i) {
+			clip.tracks[i].shift(-1 * minStartTime);
+		}
 
 
-				if (referenceTrackType === 'bool' || referenceTrackType === 'string') continue; // Find the track in the target clip whose name and type matches the reference track
+		clip.resetDuration();
+		return clip;
+	}
 
 
-				const targetTrack = targetClip.tracks.find(function (track) {
-					return track.name === referenceTrack.name && track.ValueTypeName === referenceTrackType;
-				});
-				if (targetTrack === undefined) continue;
-				let referenceOffset = 0;
-				const referenceValueSize = referenceTrack.getValueSize();
+	function makeClipAdditive(targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30) {
+		if (fps <= 0) fps = 30;
+		const numTracks = referenceClip.tracks.length;
+		const referenceTime = referenceFrame / fps; // Make each track's values relative to the values at the reference frame
 
 
-				if (referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
-					referenceOffset = referenceValueSize / 3;
-				}
+		for (let i = 0; i < numTracks; ++i) {
+			const referenceTrack = referenceClip.tracks[i];
+			const referenceTrackType = referenceTrack.ValueTypeName; // Skip this track if it's non-numeric
 
 
-				let targetOffset = 0;
-				const targetValueSize = targetTrack.getValueSize();
+			if (referenceTrackType === 'bool' || referenceTrackType === 'string') continue; // Find the track in the target clip whose name and type matches the reference track
 
 
-				if (targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
-					targetOffset = targetValueSize / 3;
-				}
+			const targetTrack = targetClip.tracks.find(function (track) {
+				return track.name === referenceTrack.name && track.ValueTypeName === referenceTrackType;
+			});
+			if (targetTrack === undefined) continue;
+			let referenceOffset = 0;
+			const referenceValueSize = referenceTrack.getValueSize();
 
 
-				const lastIndex = referenceTrack.times.length - 1;
-				let referenceValue; // Find the value to subtract out of the track
+			if (referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
+				referenceOffset = referenceValueSize / 3;
+			}
 
 
-				if (referenceTime <= referenceTrack.times[0]) {
-					// Reference frame is earlier than the first keyframe, so just use the first keyframe
-					const startIndex = referenceOffset;
-					const endIndex = referenceValueSize - referenceOffset;
-					referenceValue = AnimationUtils.arraySlice(referenceTrack.values, startIndex, endIndex);
-				} else if (referenceTime >= referenceTrack.times[lastIndex]) {
-					// Reference frame is after the last keyframe, so just use the last keyframe
-					const startIndex = lastIndex * referenceValueSize + referenceOffset;
-					const endIndex = startIndex + referenceValueSize - referenceOffset;
-					referenceValue = AnimationUtils.arraySlice(referenceTrack.values, startIndex, endIndex);
-				} else {
-					// Interpolate to the reference value
-					const interpolant = referenceTrack.createInterpolant();
-					const startIndex = referenceOffset;
-					const endIndex = referenceValueSize - referenceOffset;
-					interpolant.evaluate(referenceTime);
-					referenceValue = AnimationUtils.arraySlice(interpolant.resultBuffer, startIndex, endIndex);
-				} // Conjugate the quaternion
+			let targetOffset = 0;
+			const targetValueSize = targetTrack.getValueSize();
 
 
+			if (targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) {
+				targetOffset = targetValueSize / 3;
+			}
 
 
-				if (referenceTrackType === 'quaternion') {
-					const referenceQuat = new Quaternion().fromArray(referenceValue).normalize().conjugate();
-					referenceQuat.toArray(referenceValue);
-				} // Subtract the reference value from all of the track values
+			const lastIndex = referenceTrack.times.length - 1;
+			let referenceValue; // Find the value to subtract out of the track
 
 
+			if (referenceTime <= referenceTrack.times[0]) {
+				// Reference frame is earlier than the first keyframe, so just use the first keyframe
+				const startIndex = referenceOffset;
+				const endIndex = referenceValueSize - referenceOffset;
+				referenceValue = arraySlice(referenceTrack.values, startIndex, endIndex);
+			} else if (referenceTime >= referenceTrack.times[lastIndex]) {
+				// Reference frame is after the last keyframe, so just use the last keyframe
+				const startIndex = lastIndex * referenceValueSize + referenceOffset;
+				const endIndex = startIndex + referenceValueSize - referenceOffset;
+				referenceValue = arraySlice(referenceTrack.values, startIndex, endIndex);
+			} else {
+				// Interpolate to the reference value
+				const interpolant = referenceTrack.createInterpolant();
+				const startIndex = referenceOffset;
+				const endIndex = referenceValueSize - referenceOffset;
+				interpolant.evaluate(referenceTime);
+				referenceValue = arraySlice(interpolant.resultBuffer, startIndex, endIndex);
+			} // Conjugate the quaternion
 
 
-				const numTimes = targetTrack.times.length;
 
 
-				for (let j = 0; j < numTimes; ++j) {
-					const valueStart = j * targetValueSize + targetOffset;
+			if (referenceTrackType === 'quaternion') {
+				const referenceQuat = new Quaternion().fromArray(referenceValue).normalize().conjugate();
+				referenceQuat.toArray(referenceValue);
+			} // Subtract the reference value from all of the track values
 
 
-					if (referenceTrackType === 'quaternion') {
-						// Multiply the conjugate for quaternion track types
-						Quaternion.multiplyQuaternionsFlat(targetTrack.values, valueStart, referenceValue, 0, targetTrack.values, valueStart);
-					} else {
-						const valueEnd = targetValueSize - targetOffset * 2; // Subtract each value for all other numeric track types
 
 
-						for (let k = 0; k < valueEnd; ++k) {
-							targetTrack.values[valueStart + k] -= referenceValue[k];
-						}
+			const numTimes = targetTrack.times.length;
+
+			for (let j = 0; j < numTimes; ++j) {
+				const valueStart = j * targetValueSize + targetOffset;
+
+				if (referenceTrackType === 'quaternion') {
+					// Multiply the conjugate for quaternion track types
+					Quaternion.multiplyQuaternionsFlat(targetTrack.values, valueStart, referenceValue, 0, targetTrack.values, valueStart);
+				} else {
+					const valueEnd = targetValueSize - targetOffset * 2; // Subtract each value for all other numeric track types
+
+					for (let k = 0; k < valueEnd; ++k) {
+						targetTrack.values[valueStart + k] -= referenceValue[k];
 					}
 					}
 				}
 				}
 			}
 			}
-
-			targetClip.blendMode = AdditiveAnimationBlendMode;
-			return targetClip;
 		}
 		}
-	};
+
+		targetClip.blendMode = AdditiveAnimationBlendMode;
+		return targetClip;
+	}
+
+	var AnimationUtils = /*#__PURE__*/Object.freeze({
+		__proto__: null,
+		arraySlice: arraySlice,
+		convertArray: convertArray,
+		isTypedArray: isTypedArray,
+		getKeyframeOrder: getKeyframeOrder,
+		sortedArray: sortedArray,
+		flattenJSON: flattenJSON,
+		subclip: subclip,
+		makeClipAdditive: makeClipAdditive
+	});
 
 
 	/**
 	/**
 	 * Abstract base class of interpolants over parametric samples.
 	 * Abstract base class of interpolants over parametric samples.
@@ -27362,8 +27680,8 @@
 			if (name === undefined) throw new Error('THREE.KeyframeTrack: track name is undefined');
 			if (name === undefined) throw new Error('THREE.KeyframeTrack: track name is undefined');
 			if (times === undefined || times.length === 0) throw new Error('THREE.KeyframeTrack: no keyframes in track named ' + name);
 			if (times === undefined || times.length === 0) throw new Error('THREE.KeyframeTrack: no keyframes in track named ' + name);
 			this.name = name;
 			this.name = name;
-			this.times = AnimationUtils.convertArray(times, this.TimeBufferType);
-			this.values = AnimationUtils.convertArray(values, this.ValueBufferType);
+			this.times = convertArray(times, this.TimeBufferType);
+			this.values = convertArray(values, this.ValueBufferType);
 			this.setInterpolation(interpolation || this.DefaultInterpolation);
 			this.setInterpolation(interpolation || this.DefaultInterpolation);
 		} // Serialization (in static context, because of constructor invocation
 		} // Serialization (in static context, because of constructor invocation
 		// and automatic invocation of .toJSON):
 		// and automatic invocation of .toJSON):
@@ -27379,8 +27697,8 @@
 				// by default, we assume the data can be serialized as-is
 				// by default, we assume the data can be serialized as-is
 				json = {
 				json = {
 					'name': track.name,
 					'name': track.name,
-					'times': AnimationUtils.convertArray(track.times, Array),
-					'values': AnimationUtils.convertArray(track.values, Array)
+					'times': convertArray(track.times, Array),
+					'values': convertArray(track.values, Array)
 				};
 				};
 				const interpolation = track.getInterpolation();
 				const interpolation = track.getInterpolation();
 
 
@@ -27512,8 +27830,8 @@
 				}
 				}
 
 
 				const stride = this.getValueSize();
 				const stride = this.getValueSize();
-				this.times = AnimationUtils.arraySlice(times, from, to);
-				this.values = AnimationUtils.arraySlice(this.values, from * stride, to * stride);
+				this.times = arraySlice(times, from, to);
+				this.values = arraySlice(this.values, from * stride, to * stride);
 			}
 			}
 
 
 			return this;
 			return this;
@@ -27559,7 +27877,7 @@
 			}
 			}
 
 
 			if (values !== undefined) {
 			if (values !== undefined) {
-				if (AnimationUtils.isTypedArray(values)) {
+				if (isTypedArray(values)) {
 					for (let i = 0, n = values.length; i !== n; ++i) {
 					for (let i = 0, n = values.length; i !== n; ++i) {
 						const value = values[i];
 						const value = values[i];
 
 
@@ -27579,8 +27897,8 @@
 
 
 		optimize() {
 		optimize() {
 			// times or values may be shared with other tracks, so overwriting is unsafe
 			// times or values may be shared with other tracks, so overwriting is unsafe
-			const times = AnimationUtils.arraySlice(this.times),
-						values = AnimationUtils.arraySlice(this.values),
+			const times = arraySlice(this.times),
+						values = arraySlice(this.values),
 						stride = this.getValueSize(),
 						stride = this.getValueSize(),
 						smoothInterpolation = this.getInterpolation() === InterpolateSmooth,
 						smoothInterpolation = this.getInterpolation() === InterpolateSmooth,
 						lastIndex = times.length - 1;
 						lastIndex = times.length - 1;
@@ -27639,8 +27957,8 @@
 			}
 			}
 
 
 			if (writeIndex !== times.length) {
 			if (writeIndex !== times.length) {
-				this.times = AnimationUtils.arraySlice(times, 0, writeIndex);
-				this.values = AnimationUtils.arraySlice(values, 0, writeIndex * stride);
+				this.times = arraySlice(times, 0, writeIndex);
+				this.values = arraySlice(values, 0, writeIndex * stride);
 			} else {
 			} else {
 				this.times = times;
 				this.times = times;
 				this.values = values;
 				this.values = values;
@@ -27650,8 +27968,8 @@
 		}
 		}
 
 
 		clone() {
 		clone() {
-			const times = AnimationUtils.arraySlice(this.times, 0);
-			const values = AnimationUtils.arraySlice(this.values, 0);
+			const times = arraySlice(this.times, 0);
+			const values = arraySlice(this.values, 0);
 			const TypedKeyframeTrack = this.constructor;
 			const TypedKeyframeTrack = this.constructor;
 			const track = new TypedKeyframeTrack(this.name, times, values); // Interpolant argument to constructor is not saved, so copy the factory method directly.
 			const track = new TypedKeyframeTrack(this.name, times, values); // Interpolant argument to constructor is not saved, so copy the factory method directly.
 
 
@@ -27808,9 +28126,9 @@
 				let values = [];
 				let values = [];
 				times.push((i + numMorphTargets - 1) % numMorphTargets, i, (i + 1) % numMorphTargets);
 				times.push((i + numMorphTargets - 1) % numMorphTargets, i, (i + 1) % numMorphTargets);
 				values.push(0, 1, 0);
 				values.push(0, 1, 0);
-				const order = AnimationUtils.getKeyframeOrder(times);
-				times = AnimationUtils.sortedArray(times, 1, order);
-				values = AnimationUtils.sortedArray(values, 1, order); // if there is a key at the first frame, duplicate it as the
+				const order = getKeyframeOrder(times);
+				times = sortedArray(times, 1, order);
+				values = sortedArray(values, 1, order); // if there is a key at the first frame, duplicate it as the
 				// last frame as well for perfect loop.
 				// last frame as well for perfect loop.
 
 
 				if (!noLoop && times[0] === 0) {
 				if (!noLoop && times[0] === 0) {
@@ -27885,7 +28203,7 @@
 				if (animationKeys.length !== 0) {
 				if (animationKeys.length !== 0) {
 					const times = [];
 					const times = [];
 					const values = [];
 					const values = [];
-					AnimationUtils.flattenJSON(animationKeys, times, values, propertyName); // empty keys are filtered out, so check again
+					flattenJSON(animationKeys, times, values, propertyName); // empty keys are filtered out, so check again
 
 
 					if (times.length !== 0) {
 					if (times.length !== 0) {
 						destTracks.push(new trackType(trackName, times, values));
 						destTracks.push(new trackType(trackName, times, values));
@@ -28050,7 +28368,7 @@
 		if (json.times === undefined) {
 		if (json.times === undefined) {
 			const times = [],
 			const times = [],
 						values = [];
 						values = [];
-			AnimationUtils.flattenJSON(json.keys, times, values, 'value');
+			flattenJSON(json.keys, times, values, 'value');
 			json.times = times;
 			json.times = times;
 			json.values = values;
 			json.values = values;
 		} // derived classes can define a static parse method
 		} // derived classes can define a static parse method
@@ -33280,6 +33598,67 @@
 
 
 	}
 	}
 
 
+	let id = 0;
+
+	class UniformsGroup extends EventDispatcher {
+		constructor() {
+			super();
+			this.isUniformsGroup = true;
+			Object.defineProperty(this, 'id', {
+				value: id++
+			});
+			this.name = '';
+			this.usage = StaticDrawUsage;
+			this.uniforms = [];
+		}
+
+		add(uniform) {
+			this.uniforms.push(uniform);
+			return this;
+		}
+
+		remove(uniform) {
+			const index = this.uniforms.indexOf(uniform);
+			if (index !== -1) this.uniforms.splice(index, 1);
+			return this;
+		}
+
+		setName(name) {
+			this.name = name;
+			return this;
+		}
+
+		setUsage(value) {
+			this.usage = value;
+			return this;
+		}
+
+		dispose() {
+			this.dispatchEvent({
+				type: 'dispose'
+			});
+			return this;
+		}
+
+		copy(source) {
+			this.name = source.name;
+			this.usage = source.usage;
+			const uniformsSource = source.uniforms;
+			this.uniforms.length = 0;
+
+			for (let i = 0, l = uniformsSource.length; i < l; i++) {
+				this.uniforms.push(uniformsSource[i].clone());
+			}
+
+			return this;
+		}
+
+		clone() {
+			return new this.constructor().copy(this);
+		}
+
+	}
+
 	class InstancedInterleavedBuffer extends InterleavedBuffer {
 	class InstancedInterleavedBuffer extends InterleavedBuffer {
 		constructor(array, stride, meshPerAttribute = 1) {
 		constructor(array, stride, meshPerAttribute = 1) {
 			super(array, stride);
 			super(array, stride);
@@ -35441,6 +35820,7 @@
 	exports.Uint8BufferAttribute = Uint8BufferAttribute;
 	exports.Uint8BufferAttribute = Uint8BufferAttribute;
 	exports.Uint8ClampedBufferAttribute = Uint8ClampedBufferAttribute;
 	exports.Uint8ClampedBufferAttribute = Uint8ClampedBufferAttribute;
 	exports.Uniform = Uniform;
 	exports.Uniform = Uniform;
+	exports.UniformsGroup = UniformsGroup;
 	exports.UniformsLib = UniformsLib;
 	exports.UniformsLib = UniformsLib;
 	exports.UniformsUtils = UniformsUtils;
 	exports.UniformsUtils = UniformsUtils;
 	exports.UnsignedByteType = UnsignedByteType;
 	exports.UnsignedByteType = UnsignedByteType;

File diff suppressed because it is too large
+ 0 - 0
build/three.min.js


File diff suppressed because it is too large
+ 652 - 196
build/three.module.js


Some files were not shown because too many files changed in this diff