Browse Source

WebGLRenderer: Support more than 8 morph targets. (#22293)

* WebGLRenderer: Support more than 8 morph targets.

* WebGLProgram: Use mediump for sampler2DArray.
Michael Herzog 3 years ago
parent
commit
bf904c7445

+ 7 - 1
src/renderers/WebGLRenderer.js

@@ -298,7 +298,7 @@ function WebGLRenderer( parameters = {} ) {
 		bindingStates = new WebGLBindingStates( _gl, extensions, attributes, capabilities );
 		bindingStates = new WebGLBindingStates( _gl, extensions, attributes, capabilities );
 		geometries = new WebGLGeometries( _gl, attributes, info, bindingStates );
 		geometries = new WebGLGeometries( _gl, attributes, info, bindingStates );
 		objects = new WebGLObjects( _gl, geometries, attributes, info );
 		objects = new WebGLObjects( _gl, geometries, attributes, info );
-		morphtargets = new WebGLMorphtargets( _gl );
+		morphtargets = new WebGLMorphtargets( _gl, capabilities, textures );
 		clipping = new WebGLClipping( properties );
 		clipping = new WebGLClipping( properties );
 		programCache = new WebGLPrograms( _this, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping );
 		programCache = new WebGLPrograms( _this, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping );
 		materials = new WebGLMaterials( properties );
 		materials = new WebGLMaterials( properties );
@@ -1496,6 +1496,7 @@ function WebGLRenderer( parameters = {} ) {
 		materialProperties.skinning = parameters.skinning;
 		materialProperties.skinning = parameters.skinning;
 		materialProperties.morphTargets = parameters.morphTargets;
 		materialProperties.morphTargets = parameters.morphTargets;
 		materialProperties.morphNormals = parameters.morphNormals;
 		materialProperties.morphNormals = parameters.morphNormals;
+		materialProperties.morphTargetsCount = parameters.morphTargetsCount;
 		materialProperties.numClippingPlanes = parameters.numClippingPlanes;
 		materialProperties.numClippingPlanes = parameters.numClippingPlanes;
 		materialProperties.numIntersection = parameters.numClipIntersection;
 		materialProperties.numIntersection = parameters.numClipIntersection;
 		materialProperties.vertexAlphas = parameters.vertexAlphas;
 		materialProperties.vertexAlphas = parameters.vertexAlphas;
@@ -1517,6 +1518,7 @@ function WebGLRenderer( parameters = {} ) {
 		const vertexTangents = !! object.geometry && !! object.geometry.attributes.tangent;
 		const vertexTangents = !! object.geometry && !! object.geometry.attributes.tangent;
 		const morphTargets = !! object.geometry && !! object.geometry.morphAttributes.position;
 		const morphTargets = !! object.geometry && !! object.geometry.morphAttributes.position;
 		const morphNormals = !! object.geometry && !! object.geometry.morphAttributes.normal;
 		const morphNormals = !! object.geometry && !! object.geometry.morphAttributes.normal;
+		const morphTargetsCount = ( !! object.geometry && !! object.geometry.morphAttributes.position ) ? object.geometry.morphAttributes.position.length : 0;
 
 
 		const materialProperties = properties.get( material );
 		const materialProperties = properties.get( material );
 		const lights = currentRenderState.state.lights;
 		const lights = currentRenderState.state.lights;
@@ -1598,6 +1600,10 @@ function WebGLRenderer( parameters = {} ) {
 
 
 				needsProgramChange = true;
 				needsProgramChange = true;
 
 
+			} else if ( capabilities.isWebGL2 === true && materialProperties.morphTargetsCount !== morphTargetsCount ) {
+
+				needsProgramChange = true;
+
 			}
 			}
 
 
 		} else {
 		} else {

+ 17 - 4
src/renderers/shaders/ShaderChunk/morphnormal_vertex.glsl.js

@@ -5,10 +5,23 @@ export default /* glsl */`
 	// When morphTargetsRelative is false, this is set to 1 - sum(influences); this results in normal = sum((target - base) * influence)
 	// When morphTargetsRelative is false, this is set to 1 - sum(influences); this results in normal = sum((target - base) * influence)
 	// When morphTargetsRelative is true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
 	// When morphTargetsRelative is true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
 	objectNormal *= morphTargetBaseInfluence;
 	objectNormal *= morphTargetBaseInfluence;
-	objectNormal += morphNormal0 * morphTargetInfluences[ 0 ];
-	objectNormal += morphNormal1 * morphTargetInfluences[ 1 ];
-	objectNormal += morphNormal2 * morphTargetInfluences[ 2 ];
-	objectNormal += morphNormal3 * morphTargetInfluences[ 3 ];
+
+	#ifdef MORPHTARGETS_TEXTURE
+
+		for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {
+
+			if ( morphTargetInfluences[ i ] > 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1, 2 ) * morphTargetInfluences[ i ];
+
+		}
+
+	#else
+
+		objectNormal += morphNormal0 * morphTargetInfluences[ 0 ];
+		objectNormal += morphNormal1 * morphTargetInfluences[ 1 ];
+		objectNormal += morphNormal2 * morphTargetInfluences[ 2 ];
+		objectNormal += morphNormal3 * morphTargetInfluences[ 3 ];
+
+	#endif
 
 
 #endif
 #endif
 `;
 `;

+ 24 - 3
src/renderers/shaders/ShaderChunk/morphtarget_pars_vertex.glsl.js

@@ -3,13 +3,34 @@ export default /* glsl */`
 
 
 	uniform float morphTargetBaseInfluence;
 	uniform float morphTargetBaseInfluence;
 
 
-	#ifndef USE_MORPHNORMALS
+	#ifdef MORPHTARGETS_TEXTURE
 
 
-		uniform float morphTargetInfluences[ 8 ];
+		uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ];
+		uniform sampler2DArray morphTargetsTexture;
+		uniform vec2 morphTargetsTextureSize;
+
+		vec3 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset, const in int stride ) {
+
+			float texelIndex = float( vertexIndex * stride + offset );
+			float y = floor( texelIndex / morphTargetsTextureSize.x );
+			float x = texelIndex - y * morphTargetsTextureSize.x;
+
+			vec3 morphUV = vec3( ( x + 0.5 ) / morphTargetsTextureSize.x, y / morphTargetsTextureSize.y, morphTargetIndex );
+			return texture( morphTargetsTexture, morphUV ).xyz;
+
+		}
 
 
 	#else
 	#else
 
 
-		uniform float morphTargetInfluences[ 4 ];
+		#ifndef USE_MORPHNORMALS
+
+			uniform float morphTargetInfluences[ 8 ];
+
+		#else
+
+			uniform float morphTargetInfluences[ 4 ];
+
+		#endif
 
 
 	#endif
 	#endif
 
 

+ 30 - 9
src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js

@@ -5,17 +5,38 @@ export default /* glsl */`
 	// When morphTargetsRelative is false, this is set to 1 - sum(influences); this results in position = sum((target - base) * influence)
 	// When morphTargetsRelative is false, this is set to 1 - sum(influences); this results in position = sum((target - base) * influence)
 	// When morphTargetsRelative is true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
 	// When morphTargetsRelative is true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
 	transformed *= morphTargetBaseInfluence;
 	transformed *= morphTargetBaseInfluence;
-	transformed += morphTarget0 * morphTargetInfluences[ 0 ];
-	transformed += morphTarget1 * morphTargetInfluences[ 1 ];
-	transformed += morphTarget2 * morphTargetInfluences[ 2 ];
-	transformed += morphTarget3 * morphTargetInfluences[ 3 ];
 
 
-	#ifndef USE_MORPHNORMALS
+	#ifdef MORPHTARGETS_TEXTURE
 
 
-		transformed += morphTarget4 * morphTargetInfluences[ 4 ];
-		transformed += morphTarget5 * morphTargetInfluences[ 5 ];
-		transformed += morphTarget6 * morphTargetInfluences[ 6 ];
-		transformed += morphTarget7 * morphTargetInfluences[ 7 ];
+		for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {
+
+			#ifndef USE_MORPHNORMALS
+
+				if ( morphTargetInfluences[ i ] > 0.0 ) transformed += getMorph( gl_VertexID, i, 0, 1 ) * morphTargetInfluences[ i ];
+
+			#else
+
+				if ( morphTargetInfluences[ i ] > 0.0 ) transformed += getMorph( gl_VertexID, i, 0, 2 ) * morphTargetInfluences[ i ];
+
+			#endif
+
+		}
+
+	#else
+
+		transformed += morphTarget0 * morphTargetInfluences[ 0 ];
+		transformed += morphTarget1 * morphTargetInfluences[ 1 ];
+		transformed += morphTarget2 * morphTargetInfluences[ 2 ];
+		transformed += morphTarget3 * morphTargetInfluences[ 3 ];
+
+		#ifndef USE_MORPHNORMALS
+
+			transformed += morphTarget4 * morphTargetInfluences[ 4 ];
+			transformed += morphTarget5 * morphTargetInfluences[ 5 ];
+			transformed += morphTarget6 * morphTargetInfluences[ 6 ];
+			transformed += morphTarget7 * morphTargetInfluences[ 7 ];
+
+		#endif
 
 
 	#endif
 	#endif
 
 

+ 170 - 58
src/renderers/webgl/WebGLMorphtargets.js

@@ -1,3 +1,8 @@
+import { FloatType, RGBAFormat } from '../../constants.js';
+import { DataTexture2DArray } from '../../textures/DataTexture2DArray.js';
+import { Vector3 } from '../../math/Vector3.js';
+import { Vector2 } from '../../math/Vector2.js';
+
 function numericalSort( a, b ) {
 function numericalSort( a, b ) {
 
 
 	return a[ 0 ] - b[ 0 ];
 	return a[ 0 ] - b[ 0 ];
@@ -10,10 +15,12 @@ function absNumericalSort( a, b ) {
 
 
 }
 }
 
 
-function WebGLMorphtargets( gl ) {
+function WebGLMorphtargets( gl, capabilities, textures ) {
 
 
 	const influencesList = {};
 	const influencesList = {};
 	const morphInfluences = new Float32Array( 8 );
 	const morphInfluences = new Float32Array( 8 );
+	const morphTextures = new WeakMap();
+	const morph = new Vector3();
 
 
 	const workInfluences = [];
 	const workInfluences = [];
 
 
@@ -27,115 +34,220 @@ function WebGLMorphtargets( gl ) {
 
 
 		const objectInfluences = object.morphTargetInfluences;
 		const objectInfluences = object.morphTargetInfluences;
 
 
-		// When object doesn't have morph target influences defined, we treat it as a 0-length array
-		// This is important to make sure we set up morphTargetBaseInfluence / morphTargetInfluences
+		if ( capabilities.isWebGL2 === true ) {
 
 
-		const length = objectInfluences === undefined ? 0 : objectInfluences.length;
+			// instead of using attributes, the WebGL 2 code path encodes morph targets
+			// into an array of data textures. Each layer represents a single morph target.
 
 
-		let influences = influencesList[ geometry.id ];
+			const numberOfMorphTargets = geometry.morphAttributes.position.length;
 
 
-		if ( influences === undefined || influences.length !== length ) {
+			let entry = morphTextures.get( geometry );
 
 
-			// initialise list
+			if ( entry === undefined || entry.count !== numberOfMorphTargets ) {
 
 
-			influences = [];
+				if ( entry !== undefined ) entry.texture.dispose();
 
 
-			for ( let i = 0; i < length; i ++ ) {
+				const hasMorphNormals = geometry.morphAttributes.normal !== undefined;
 
 
-				influences[ i ] = [ i, 0 ];
+				const morphTargets = geometry.morphAttributes.position;
+				const morphNormals = geometry.morphAttributes.normal || [];
 
 
-			}
+				const numberOfVertices = geometry.attributes.position.count;
+				const numberOfVertexData = ( hasMorphNormals === true ) ? 2 : 1; // (v,n) vs. (v)
 
 
-			influencesList[ geometry.id ] = influences;
+				let width = numberOfVertices * numberOfVertexData;
+				let height = 1;
 
 
-		}
+				if ( width > capabilities.maxTextureSize ) {
 
 
-		// Collect influences
+					height = Math.ceil( width / capabilities.maxTextureSize );
+					width = capabilities.maxTextureSize;
 
 
-		for ( let i = 0; i < length; i ++ ) {
+				}
 
 
-			const influence = influences[ i ];
+				const buffer = new Float32Array( width * height * 4 * numberOfMorphTargets );
 
 
-			influence[ 0 ] = i;
-			influence[ 1 ] = objectInfluences[ i ];
+				const texture = new DataTexture2DArray( buffer, width, height, numberOfMorphTargets );
+				texture.format = RGBAFormat; // using RGBA since RGB might be emulated (and is thus slower)
+				texture.type = FloatType;
 
 
-		}
+				// fill buffer
+
+				const vertexDataStride = numberOfVertexData * 4;
+
+				for ( let i = 0; i < numberOfMorphTargets; i ++ ) {
 
 
-		influences.sort( absNumericalSort );
+					const morphTarget = morphTargets[ i ];
+					const morphNormal = morphNormals[ i ];
 
 
-		for ( let i = 0; i < 8; i ++ ) {
+					const offset = width * height * 4 * i;
 
 
-			if ( i < length && influences[ i ][ 1 ] ) {
+					for ( let j = 0; j < morphTarget.count; j ++ ) {
 
 
-				workInfluences[ i ][ 0 ] = influences[ i ][ 0 ];
-				workInfluences[ i ][ 1 ] = influences[ i ][ 1 ];
+						morph.fromBufferAttribute( morphTarget, j );
 
 
-			} else {
+						const stride = j * vertexDataStride;
+
+						buffer[ offset + stride + 0 ] = morph.x;
+						buffer[ offset + stride + 1 ] = morph.y;
+						buffer[ offset + stride + 2 ] = morph.z;
+
+						if ( hasMorphNormals === true ) {
+
+							morph.fromBufferAttribute( morphNormal, j );
+
+							buffer[ offset + stride + 3 ] = morph.x;
+							buffer[ offset + stride + 4 ] = morph.y;
+							buffer[ offset + stride + 5 ] = morph.z;
+
+						}
+
+					}
+
+				}
 
 
-				workInfluences[ i ][ 0 ] = Number.MAX_SAFE_INTEGER;
-				workInfluences[ i ][ 1 ] = 0;
+				entry = {
+					count: numberOfMorphTargets,
+					texture: texture,
+					size: new Vector2( width, height )
+				};
+
+				morphTextures.set( geometry, entry );
 
 
 			}
 			}
 
 
-		}
+			//
 
 
-		workInfluences.sort( numericalSort );
+			let morphInfluencesSum = 0;
 
 
-		const morphTargets = geometry.morphAttributes.position;
-		const morphNormals = geometry.morphAttributes.normal;
+			for ( let i = 0; i < objectInfluences.length; i ++ ) {
 
 
-		let morphInfluencesSum = 0;
+				morphInfluencesSum += objectInfluences[ i ];
 
 
-		for ( let i = 0; i < 8; i ++ ) {
+			}
 
 
-			const influence = workInfluences[ i ];
-			const index = influence[ 0 ];
-			const value = influence[ 1 ];
+			const morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;
 
 
-			if ( index !== Number.MAX_SAFE_INTEGER && value ) {
+			program.getUniforms().setValue( gl, 'morphTargetBaseInfluence', morphBaseInfluence );
+			program.getUniforms().setValue( gl, 'morphTargetInfluences', objectInfluences );
 
 
-				if ( morphTargets && geometry.getAttribute( 'morphTarget' + i ) !== morphTargets[ index ] ) {
+			program.getUniforms().setValue( gl, 'morphTargetsTexture', entry.texture, textures );
+			program.getUniforms().setValue( gl, 'morphTargetsTextureSize', entry.size );
 
 
-					geometry.setAttribute( 'morphTarget' + i, morphTargets[ index ] );
 
 
-				}
+		} else {
 
 
-				if ( morphNormals && geometry.getAttribute( 'morphNormal' + i ) !== morphNormals[ index ] ) {
+			// When object doesn't have morph target influences defined, we treat it as a 0-length array
+			// This is important to make sure we set up morphTargetBaseInfluence / morphTargetInfluences
 
 
-					geometry.setAttribute( 'morphNormal' + i, morphNormals[ index ] );
+			const length = objectInfluences === undefined ? 0 : objectInfluences.length;
 
 
-				}
+			let influences = influencesList[ geometry.id ];
+
+			if ( influences === undefined || influences.length !== length ) {
 
 
-				morphInfluences[ i ] = value;
-				morphInfluencesSum += value;
+				// initialise list
 
 
-			} else {
+				influences = [];
 
 
-				if ( morphTargets && geometry.hasAttribute( 'morphTarget' + i ) === true ) {
+				for ( let i = 0; i < length; i ++ ) {
 
 
-					geometry.deleteAttribute( 'morphTarget' + i );
+					influences[ i ] = [ i, 0 ];
 
 
 				}
 				}
 
 
-				if ( morphNormals && geometry.hasAttribute( 'morphNormal' + i ) === true ) {
+				influencesList[ geometry.id ] = influences;
+
+			}
+
+			// Collect influences
+
+			for ( let i = 0; i < length; i ++ ) {
+
+				const influence = influences[ i ];
+
+				influence[ 0 ] = i;
+				influence[ 1 ] = objectInfluences[ i ];
+
+			}
+
+			influences.sort( absNumericalSort );
+
+			for ( let i = 0; i < 8; i ++ ) {
+
+				if ( i < length && influences[ i ][ 1 ] ) {
+
+					workInfluences[ i ][ 0 ] = influences[ i ][ 0 ];
+					workInfluences[ i ][ 1 ] = influences[ i ][ 1 ];
 
 
-					geometry.deleteAttribute( 'morphNormal' + i );
+				} else {
+
+					workInfluences[ i ][ 0 ] = Number.MAX_SAFE_INTEGER;
+					workInfluences[ i ][ 1 ] = 0;
 
 
 				}
 				}
 
 
-				morphInfluences[ i ] = 0;
+			}
+
+			workInfluences.sort( numericalSort );
+
+			const morphTargets = geometry.morphAttributes.position;
+			const morphNormals = geometry.morphAttributes.normal;
+
+			let morphInfluencesSum = 0;
+
+			for ( let i = 0; i < 8; i ++ ) {
+
+				const influence = workInfluences[ i ];
+				const index = influence[ 0 ];
+				const value = influence[ 1 ];
+
+				if ( index !== Number.MAX_SAFE_INTEGER && value ) {
+
+					if ( morphTargets && geometry.getAttribute( 'morphTarget' + i ) !== morphTargets[ index ] ) {
+
+						geometry.setAttribute( 'morphTarget' + i, morphTargets[ index ] );
+
+					}
+
+					if ( morphNormals && geometry.getAttribute( 'morphNormal' + i ) !== morphNormals[ index ] ) {
+
+						geometry.setAttribute( 'morphNormal' + i, morphNormals[ index ] );
+
+					}
+
+					morphInfluences[ i ] = value;
+					morphInfluencesSum += value;
+
+				} else {
+
+					if ( morphTargets && geometry.hasAttribute( 'morphTarget' + i ) === true ) {
+
+						geometry.deleteAttribute( 'morphTarget' + i );
+
+					}
+
+					if ( morphNormals && geometry.hasAttribute( 'morphNormal' + i ) === true ) {
+
+						geometry.deleteAttribute( 'morphNormal' + i );
+
+					}
+
+					morphInfluences[ i ] = 0;
+
+				}
 
 
 			}
 			}
 
 
-		}
+			// GLSL shader uses formula baseinfluence * base + sum(target * influence)
+			// This allows us to switch between absolute morphs and relative morphs without changing shader code
+			// When baseinfluence = 1 - sum(influence), the above is equivalent to sum((target - base) * influence)
+			const morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;
 
 
-		// GLSL shader uses formula baseinfluence * base + sum(target * influence)
-		// This allows us to switch between absolute morphs and relative morphs without changing shader code
-		// When baseinfluence = 1 - sum(influence), the above is equivalent to sum((target - base) * influence)
-		const morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;
+			program.getUniforms().setValue( gl, 'morphTargetBaseInfluence', morphBaseInfluence );
+			program.getUniforms().setValue( gl, 'morphTargetInfluences', morphInfluences );
 
 
-		program.getUniforms().setValue( gl, 'morphTargetBaseInfluence', morphBaseInfluence );
-		program.getUniforms().setValue( gl, 'morphTargetInfluences', morphInfluences );
+		}
 
 
 	}
 	}
 
 

+ 4 - 1
src/renderers/webgl/WebGLProgram.js

@@ -501,6 +501,8 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 
 
 			parameters.morphTargets ? '#define USE_MORPHTARGETS' : '',
 			parameters.morphTargets ? '#define USE_MORPHTARGETS' : '',
 			parameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '',
 			parameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '',
+			( parameters.morphTargets && parameters.isWebGL2 ) ? '#define MORPHTARGETS_TEXTURE' : '',
+			( parameters.morphTargets && parameters.isWebGL2 ) ? '#define MORPHTARGETS_COUNT ' + parameters.morphTargetsCount : '',
 			parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
 			parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
 			parameters.flipSided ? '#define FLIP_SIDED' : '',
 			parameters.flipSided ? '#define FLIP_SIDED' : '',
 
 
@@ -552,7 +554,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 
 
 			'#endif',
 			'#endif',
 
 
-			'#ifdef USE_MORPHTARGETS',
+			'#if ( defined( USE_MORPHTARGETS ) && ! defined( MORPHTARGETS_TEXTURE ) )',
 
 
 			'	attribute vec3 morphTarget0;',
 			'	attribute vec3 morphTarget0;',
 			'	attribute vec3 morphTarget1;',
 			'	attribute vec3 morphTarget1;',
@@ -708,6 +710,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 		versionString = '#version 300 es\n';
 		versionString = '#version 300 es\n';
 
 
 		prefixVertex = [
 		prefixVertex = [
+			'precision mediump sampler2DArray;',
 			'#define attribute in',
 			'#define attribute in',
 			'#define varying out',
 			'#define varying out',
 			'#define texture2D texture'
 			'#define texture2D texture'

+ 2 - 1
src/renderers/webgl/WebGLPrograms.js

@@ -43,7 +43,7 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 		'specularMap', 'specularIntensityMap', 'specularTintMap', 'specularTintMapEncoding', 'roughnessMap', 'metalnessMap', 'gradientMap',
 		'specularMap', 'specularIntensityMap', 'specularTintMap', 'specularTintMapEncoding', 'roughnessMap', 'metalnessMap', 'gradientMap',
 		'alphaMap', 'alphaTest', 'combine', 'vertexColors', 'vertexAlphas', 'vertexTangents', 'vertexUvs', 'uvsVertexOnly', 'fog', 'useFog', 'fogExp2',
 		'alphaMap', 'alphaTest', 'combine', 'vertexColors', 'vertexAlphas', 'vertexTangents', 'vertexUvs', 'uvsVertexOnly', 'fog', 'useFog', 'fogExp2',
 		'flatShading', 'sizeAttenuation', 'logarithmicDepthBuffer', 'skinning',
 		'flatShading', 'sizeAttenuation', 'logarithmicDepthBuffer', 'skinning',
-		'maxBones', 'useVertexTexture', 'morphTargets', 'morphNormals', 'premultipliedAlpha',
+		'maxBones', 'useVertexTexture', 'morphTargets', 'morphNormals', 'morphTargetsCount', 'premultipliedAlpha',
 		'numDirLights', 'numPointLights', 'numSpotLights', 'numHemiLights', 'numRectAreaLights',
 		'numDirLights', 'numPointLights', 'numSpotLights', 'numHemiLights', 'numRectAreaLights',
 		'numDirLightShadows', 'numPointLightShadows', 'numSpotLightShadows',
 		'numDirLightShadows', 'numPointLightShadows', 'numSpotLightShadows',
 		'shadowMapEnabled', 'shadowMapType', 'toneMapping', 'physicallyCorrectLights',
 		'shadowMapEnabled', 'shadowMapType', 'toneMapping', 'physicallyCorrectLights',
@@ -243,6 +243,7 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 
 
 			morphTargets: !! object.geometry && !! object.geometry.morphAttributes.position,
 			morphTargets: !! object.geometry && !! object.geometry.morphAttributes.position,
 			morphNormals: !! object.geometry && !! object.geometry.morphAttributes.normal,
 			morphNormals: !! object.geometry && !! object.geometry.morphAttributes.normal,
+			morphTargetsCount: ( !! object.geometry && !! object.geometry.morphAttributes.position ) ? object.geometry.morphAttributes.position.length : 0,
 
 
 			numDirLights: lights.directional.length,
 			numDirLights: lights.directional.length,
 			numPointLights: lights.point.length,
 			numPointLights: lights.point.length,