Explorar o código

WebGL2: Added support for Uniform Buffer Objects. (#21558)

* WebGL2: Added support for Uniform Buffer Objects.

* Update WebGLState.js

Clean up.

* Fix example.
Michael Herzog %!s(int64=3) %!d(string=hai) anos
pai
achega
bd61eb4469

+ 1 - 0
examples/files.json

@@ -304,6 +304,7 @@
 		"webgl2_multiple_rendertargets",
 		"webgl2_multisampled_renderbuffers",
 		"webgl2_rendertarget_texture2darray",
+		"webgl2_ubo",
 		"webgl2_volume_cloud",
 		"webgl2_volume_instancing",
 		"webgl2_volume_perlin"

BIN=BIN
examples/screenshots/webgl2_ubo.jpg


+ 314 - 0
examples/webgl2_ubo.html

@@ -0,0 +1,314 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js WebGL 2 - Uniform Buffer Objects</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+
+	<body>
+
+		<div id="info">
+			<a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> - Uniform Buffer Objects
+		</div>
+		<div id="container"></div>
+
+		<script id="vertexShader1" type="x-shader/x-vertex">
+
+			uniform ViewData {
+				mat4 projectionMatrix;
+				mat4 viewMatrix;
+			};
+
+			uniform mat4 modelMatrix;
+			uniform mat3 normalMatrix;
+
+			in vec3 position;
+			in vec3 normal;
+
+			out vec3 vPositionEye;
+			out vec3 vNormalEye;
+
+			void main()	{
+
+				vec4 vertexPositionEye = viewMatrix * modelMatrix * vec4( position, 1.0 );
+
+				vPositionEye = vertexPositionEye.xyz;
+				vNormalEye = normalMatrix * normal;
+
+				gl_Position = projectionMatrix * vertexPositionEye;
+
+			}
+
+		</script>
+
+		<script id="fragmentShader1" type="x-shader/x-fragment">
+
+			precision highp float;
+
+			uniform LightingData {
+				vec3 position;
+				vec3 ambientColor;
+				vec3 diffuseColor;
+				vec3 specularColor;
+				float shininess;
+			} Light;
+
+			uniform vec3 color;
+
+			in vec3 vPositionEye;
+			in vec3 vNormalEye;
+
+			out vec4 fragColor;
+
+			void main()	{
+
+				// a very basic lighting equation (Phong reflection model) for testing
+
+				vec3 l = normalize( Light.position - vPositionEye );
+				vec3 n = normalize( vNormalEye );
+				vec3 e = - normalize( vPositionEye );
+				vec3 r = normalize( reflect( - l, n ) );
+
+				float diffuseLightWeighting = max( dot( n, l ), 0.0 );
+				float specularLightWeighting = max( dot( r, e ), 0.0 );
+
+				specularLightWeighting = pow( specularLightWeighting, Light.shininess );
+
+				vec3 lightWeighting = Light.ambientColor +
+					Light.diffuseColor * diffuseLightWeighting +
+					Light.specularColor * specularLightWeighting;
+
+				fragColor = vec4( color.rgb * lightWeighting.rgb, 1.0 );
+
+			}
+
+		</script>
+
+		<script id="vertexShader2" type="x-shader/x-vertex">
+
+			uniform ViewData {
+				mat4 projectionMatrix;
+				mat4 viewMatrix;
+			};
+
+			uniform mat4 modelMatrix;
+
+			in vec3 position;
+			in vec2 uv;
+
+			out vec2 vUv;
+
+			void main()	{
+
+				vUv = uv;
+
+				gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
+
+			}
+
+		</script>
+
+		<script id="fragmentShader2" type="x-shader/x-fragment">
+
+			precision highp float;
+
+			uniform sampler2D diffuseMap;
+
+			in vec2 vUv;
+
+			out vec4 fragColor;
+
+			void main()	{
+
+				fragColor = texture( diffuseMap, vUv );
+
+			}
+
+		</script>
+
+		<!-- Import maps polyfill -->
+		<!-- Remove this when import maps will be widely supported -->
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js"
+				}
+			}
+		</script>
+
+
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import WebGL from './jsm/capabilities/WebGL.js';
+
+			let camera, scene, renderer, clock;
+
+			init();
+			animate();
+
+			function init() {
+
+				if ( WebGL.isWebGL2Available() === false ) {
+
+					document.body.appendChild( WebGL.getWebGL2ErrorMessage() );
+					return;
+
+				}
+
+				const container = document.getElementById( 'container' );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 0, 0, 25 );
+
+				scene = new THREE.Scene();
+				camera.lookAt( scene.position );
+
+				clock = new THREE.Clock();
+
+				// geometry
+
+				const geometry1 = new THREE.TetrahedronGeometry();
+				const geometry2 = new THREE.BoxGeometry();
+
+				// texture
+
+				const texture = new THREE.TextureLoader().load( 'textures/crate.gif' );
+
+				// uniforms groups
+
+				// Camera and lighting related data are perfect examples of using UBOs since you have to store these
+				// data just once. They can be shared across all shader programs.
+
+				const cameraUniformsGroup = new THREE.UniformsGroup();
+				cameraUniformsGroup.setName( 'ViewData' );
+				cameraUniformsGroup.add( new THREE.Uniform( camera.projectionMatrix ) ); // projection matrix
+				cameraUniformsGroup.add( new THREE.Uniform( camera.matrixWorldInverse ) ); // view matrix
+
+				const lightingUniformsGroup = new THREE.UniformsGroup();
+				lightingUniformsGroup.setName( 'LightingData' );
+				lightingUniformsGroup.add( new THREE.Uniform( new THREE.Vector3( 0, 0, 10 ) ) ); // light position
+				lightingUniformsGroup.add( new THREE.Uniform( new THREE.Color( 0x333333 ) ) ); // ambient color
+				lightingUniformsGroup.add( new THREE.Uniform( new THREE.Color( 0xaaaaaa ) ) ); // diffuse color
+				lightingUniformsGroup.add( new THREE.Uniform( new THREE.Color( 0xcccccc ) ) ); // specular color
+				lightingUniformsGroup.add( new THREE.Uniform( 64 ) ); // shininess
+
+				// materials
+
+				const material1 = new THREE.RawShaderMaterial( {
+					uniforms: {
+						modelMatrix: { value: null },
+						normalMatrix: { value: null },
+						color: { value: null }
+					},
+					vertexShader: document.getElementById( 'vertexShader1' ).textContent,
+					fragmentShader: document.getElementById( 'fragmentShader1' ).textContent,
+					glslVersion: THREE.GLSL3
+				} );
+
+				const material2 = new THREE.RawShaderMaterial( {
+					uniforms: {
+						modelMatrix: { value: null },
+						diffuseMap: { value: null },
+					},
+					vertexShader: document.getElementById( 'vertexShader2' ).textContent,
+					fragmentShader: document.getElementById( 'fragmentShader2' ).textContent,
+					glslVersion: THREE.GLSL3
+				} );
+
+				// meshes
+
+				for ( let i = 0; i < 200; i ++ ) {
+
+					let mesh;
+
+					if ( i % 2 === 0 ) {
+
+						mesh = new THREE.Mesh( geometry1, material1.clone() );
+
+						mesh.material.uniformsGroups = [ cameraUniformsGroup, lightingUniformsGroup ];
+						mesh.material.uniforms.modelMatrix.value = mesh.matrixWorld;
+						mesh.material.uniforms.normalMatrix.value = mesh.normalMatrix;
+						mesh.material.uniforms.color.value = new THREE.Color( 0xffffff * Math.random() );
+
+					} else {
+
+						mesh = new THREE.Mesh( geometry2, material2.clone() );
+
+						mesh.material.uniformsGroups = [ cameraUniformsGroup ];
+						mesh.material.uniforms.modelMatrix.value = mesh.matrixWorld;
+						mesh.material.uniforms.diffuseMap.value = texture;
+
+					}
+
+					scene.add( mesh );
+
+					const s = 1 + Math.random() * 0.5;
+
+					mesh.scale.x = s;
+					mesh.scale.y = s;
+					mesh.scale.z = s;
+
+					mesh.rotation.x = Math.random() * Math.PI;
+					mesh.rotation.y = Math.random() * Math.PI;
+					mesh.rotation.z = Math.random() * Math.PI;
+
+					mesh.position.x = Math.random() * 40 - 20;
+					mesh.position.y = Math.random() * 40 - 20;
+					mesh.position.z = Math.random() * 20 - 10;
+
+				}
+
+				//
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				container.appendChild( renderer.domElement );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				const delta = clock.getDelta();
+
+				scene.traverse( function ( child ) {
+
+					if ( child.isMesh ) {
+
+						child.rotation.x += delta * 0.5;
+						child.rotation.y += delta * 0.3;
+
+					}
+
+				} );
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
src/Three.js

@@ -90,6 +90,7 @@ export { AnimationObjectGroup } from './animation/AnimationObjectGroup.js';
 export { AnimationMixer } from './animation/AnimationMixer.js';
 export { AnimationClip } from './animation/AnimationClip.js';
 export { Uniform } from './core/Uniform.js';
+export { UniformsGroup } from './core/UniformsGroup.js';
 export { InstancedBufferGeometry } from './core/InstancedBufferGeometry.js';
 export { BufferGeometry } from './core/BufferGeometry.js';
 export { InterleavedBufferAttribute } from './core/InterleavedBufferAttribute.js';

+ 93 - 0
src/core/UniformsGroup.js

@@ -0,0 +1,93 @@
+import { EventDispatcher } from './EventDispatcher.js';
+import { StaticDrawUsage } from '../constants.js';
+
+let id = 0;
+
+class UniformsGroup extends EventDispatcher {
+
+	constructor() {
+
+		super();
+
+		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 );
+
+	}
+
+}
+
+UniformsGroup.prototype.isUniformsGroup = true;
+
+
+export { UniformsGroup };

+ 3 - 1
src/materials/ShaderMaterial.js

@@ -1,5 +1,5 @@
 import { Material } from './Material.js';
-import { cloneUniforms } from '../renderers/shaders/UniformsUtils.js';
+import { cloneUniforms, cloneUniformsGroups } from '../renderers/shaders/UniformsUtils.js';
 
 import default_vertex from '../renderers/shaders/ShaderChunk/default_vertex.glsl.js';
 import default_fragment from '../renderers/shaders/ShaderChunk/default_fragment.glsl.js';
@@ -16,6 +16,7 @@ class ShaderMaterial extends Material {
 
 		this.defines = {};
 		this.uniforms = {};
+		this.uniformsGroups = [];
 
 		this.vertexShader = default_vertex;
 		this.fragmentShader = default_fragment;
@@ -71,6 +72,7 @@ class ShaderMaterial extends Material {
 		this.vertexShader = source.vertexShader;
 
 		this.uniforms = cloneUniforms( source.uniforms );
+		this.uniformsGroups = cloneUniformsGroups( source.uniformsGroups );
 
 		this.defines = Object.assign( {}, source.defines );
 

+ 29 - 1
src/renderers/WebGLRenderer.js

@@ -44,6 +44,7 @@ import { WebGLUniforms } from './webgl/WebGLUniforms.js';
 import { WebGLUtils } from './webgl/WebGLUtils.js';
 import { WebXRManager } from './webxr/WebXRManager.js';
 import { WebGLMaterials } from './webgl/WebGLMaterials.js';
+import { WebGLUniformsGroups } from './webgl/WebGLUniformsGroups.js';
 import { createElementNS } from '../utils.js';
 
 function createCanvasElement() {
@@ -307,7 +308,7 @@ function WebGLRenderer( parameters = {} ) {
 
 	let background, morphtargets, bufferRenderer, indexedBufferRenderer;
 
-	let utils, bindingStates;
+	let utils, bindingStates, uniformsGroups;
 
 	function initGLContext() {
 
@@ -338,6 +339,7 @@ function WebGLRenderer( parameters = {} ) {
 		renderStates = new WebGLRenderStates( extensions, capabilities );
 		background = new WebGLBackground( _this, cubemaps, state, objects, _alpha, _premultipliedAlpha );
 		shadowMap = new WebGLShadowMap( _this, objects, capabilities );
+		uniformsGroups = new WebGLUniformsGroups( _gl, info, capabilities, state );
 
 		bufferRenderer = new WebGLBufferRenderer( _gl, extensions, info, capabilities );
 		indexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info, capabilities );
@@ -603,6 +605,7 @@ function WebGLRenderer( parameters = {} ) {
 		cubeuvmaps.dispose();
 		objects.dispose();
 		bindingStates.dispose();
+		uniformsGroups.dispose();
 		programCache.dispose();
 
 		xr.dispose();
@@ -1783,6 +1786,31 @@ function WebGLRenderer( parameters = {} ) {
 		p_uniforms.setValue( _gl, 'normalMatrix', object.normalMatrix );
 		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;
 
 	}

+ 14 - 0
src/renderers/shaders/UniformsUtils.js

@@ -59,6 +59,20 @@ export function mergeUniforms( uniforms ) {
 
 }
 
+export function cloneUniformsGroups( src ) {
+
+	const dst = [];
+
+	for ( let u = 0; u < src.length; u ++ ) {
+
+		dst.push( src[ u ].clone() );
+
+	}
+
+	return dst;
+
+}
+
 // Legacy
 
 const UniformsUtils = { clone: cloneUniforms, merge: mergeUniforms };

+ 47 - 0
src/renderers/webgl/WebGLState.js

@@ -314,6 +314,9 @@ function WebGLState( gl, extensions, capabilities ) {
 	const depthBuffer = new DepthBuffer();
 	const stencilBuffer = new StencilBuffer();
 
+	const uboBindings = new WeakMap();
+	const uboProgamMap = new WeakMap();
+
 	let enabledCapabilities = {};
 
 	let currentBoundFramebuffers = {};
@@ -1064,6 +1067,47 @@ function WebGLState( gl, extensions, capabilities ) {
 
 	}
 
+	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 );
+
+		}
+
+	}
+
 	//
 
 	function reset() {
@@ -1191,6 +1235,9 @@ function WebGLState( gl, extensions, capabilities ) {
 		texImage2D: texImage2D,
 		texImage3D: texImage3D,
 
+		updateUBOMapping: updateUBOMapping,
+		uniformBlockBinding: uniformBlockBinding,
+
 		texStorage2D: texStorage2D,
 		texStorage3D: texStorage3D,
 		texSubImage2D: texSubImage2D,

+ 372 - 0
src/renderers/webgl/WebGLUniformsGroups.js

@@ -0,0 +1,372 @@
+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
+
+	};
+
+}
+
+
+export { WebGLUniformsGroups };

+ 3 - 1
utils/build/rollup.config.js

@@ -122,6 +122,7 @@ export function glconstants() {
 		MAX_VERTEX_ATTRIBS: 34921,
 		MAX_TEXTURE_IMAGE_UNITS: 34930,
 		ARRAY_BUFFER: 34962,
+		UNIFORM_BUFFER: 35345,
 		ELEMENT_ARRAY_BUFFER: 34963,
 		STATIC_DRAW: 35044,
 		DYNAMIC_DRAW: 35048,
@@ -164,7 +165,8 @@ export function glconstants() {
 		DRAW_FRAMEBUFFER: 36009,
 		SAMPLE_ALPHA_TO_COVERAGE: 32926,
 		SRGB8: 35905,
-		SRGB8_ALPHA8: 35907
+		SRGB8_ALPHA8: 35907,
+		MAX_UNIFORM_BUFFER_BINDINGS: 35375
 	};
 
 	return {