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 offset = uniform.__offset; const values = Array.isArray( uniform.value ) ? uniform.value : [ uniform.value ]; let arrayOffset = 0; for ( let i = 0; i < values.length; i ++ ) { const value = values[ i ]; const info = getUniformSize( value ); if ( typeof value === 'number' ) { uniform.__data[ 0 ] = value; gl.bufferSubData( gl.UNIFORM_BUFFER, offset + arrayOffset, uniform.__data ); } else if ( value.isMatrix3 ) { // manually converting 3x3 to 3x4 uniform.__data[ 0 ] = value.elements[ 0 ]; uniform.__data[ 1 ] = value.elements[ 1 ]; uniform.__data[ 2 ] = value.elements[ 2 ]; uniform.__data[ 3 ] = value.elements[ 0 ]; uniform.__data[ 4 ] = value.elements[ 3 ]; uniform.__data[ 5 ] = value.elements[ 4 ]; uniform.__data[ 6 ] = value.elements[ 5 ]; uniform.__data[ 7 ] = value.elements[ 0 ]; uniform.__data[ 8 ] = value.elements[ 6 ]; uniform.__data[ 9 ] = value.elements[ 7 ]; uniform.__data[ 10 ] = value.elements[ 8 ]; uniform.__data[ 11 ] = value.elements[ 0 ]; } else { value.toArray( uniform.__data, arrayOffset ); arrayOffset += info.storage / Float32Array.BYTES_PER_ELEMENT; } } 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 { const values = Array.isArray( value ) ? value : [ value ]; const tempValues = []; for ( let i = 0; i < values.length; i ++ ) { tempValues.push( values[ i ].clone() ); } cache[ index ] = tempValues; } return true; } else { // compare current value with cached entry if ( typeof value === 'number' ) { if ( cache[ index ] !== value ) { cache[ index ] = value; return true; } } else { const cachedObjects = Array.isArray( cache[ index ] ) ? cache[ index ] : [ cache[ index ] ]; const values = Array.isArray( value ) ? value : [ value ]; for ( let i = 0; i < cachedObjects.length; i ++ ) { const cachedObject = cachedObjects[ i ]; if ( cachedObject.equals( values[ i ] ) === false ) { cachedObject.copy( values[ i ] ); 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 infos = { boundary: 0, // bytes storage: 0 // bytes }; const values = Array.isArray( uniform.value ) ? uniform.value : [ uniform.value ]; for ( let j = 0, jl = values.length; j < jl; j ++ ) { const value = values[ j ]; const info = getUniformSize( value ); infos.boundary += info.boundary; infos.storage += info.storage; } // the following two properties will be used for partial buffer updates uniform.__data = new Float32Array( infos.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 - infos.boundary ) < 0 ) { // add padding and adjust offset offset += ( chunkSize - chunkOffset ); uniform.__offset = offset; } } offset += infos.storage; } // ensure correct final padding chunkOffset = offset % chunkSize; if ( chunkOffset > 0 ) offset += ( chunkSize - chunkOffset ); // uniformsGroup.__size = offset; uniformsGroup.__cache = {}; return this; } function getUniformSize( 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 };