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 );
 		geometries = new WebGLGeometries( _gl, attributes, info, bindingStates );
 		objects = new WebGLObjects( _gl, geometries, attributes, info );
-		morphtargets = new WebGLMorphtargets( _gl );
+		morphtargets = new WebGLMorphtargets( _gl, capabilities, textures );
 		clipping = new WebGLClipping( properties );
 		programCache = new WebGLPrograms( _this, cubemaps, cubeuvmaps, extensions, capabilities, bindingStates, clipping );
 		materials = new WebGLMaterials( properties );
@@ -1496,6 +1496,7 @@ function WebGLRenderer( parameters = {} ) {
 		materialProperties.skinning = parameters.skinning;
 		materialProperties.morphTargets = parameters.morphTargets;
 		materialProperties.morphNormals = parameters.morphNormals;
+		materialProperties.morphTargetsCount = parameters.morphTargetsCount;
 		materialProperties.numClippingPlanes = parameters.numClippingPlanes;
 		materialProperties.numIntersection = parameters.numClipIntersection;
 		materialProperties.vertexAlphas = parameters.vertexAlphas;
@@ -1517,6 +1518,7 @@ function WebGLRenderer( parameters = {} ) {
 		const vertexTangents = !! object.geometry && !! object.geometry.attributes.tangent;
 		const morphTargets = !! object.geometry && !! object.geometry.morphAttributes.position;
 		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 lights = currentRenderState.state.lights;
@@ -1598,6 +1600,10 @@ function WebGLRenderer( parameters = {} ) {
 
 				needsProgramChange = true;
 
+			} else if ( capabilities.isWebGL2 === true && materialProperties.morphTargetsCount !== morphTargetsCount ) {
+
+				needsProgramChange = true;
+
 			}
 
 		} 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 true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
 	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
 `;

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

@@ -3,13 +3,34 @@ export default /* glsl */`
 
 	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
 
-		uniform float morphTargetInfluences[ 4 ];
+		#ifndef USE_MORPHNORMALS
+
+			uniform float morphTargetInfluences[ 8 ];
+
+		#else
+
+			uniform float morphTargetInfluences[ 4 ];
+
+		#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 true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
 	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
 

+ 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 ) {
 
 	return a[ 0 ] - b[ 0 ];
@@ -10,10 +15,12 @@ function absNumericalSort( a, b ) {
 
 }
 
-function WebGLMorphtargets( gl ) {
+function WebGLMorphtargets( gl, capabilities, textures ) {
 
 	const influencesList = {};
 	const morphInfluences = new Float32Array( 8 );
+	const morphTextures = new WeakMap();
+	const morph = new Vector3();
 
 	const workInfluences = [];
 
@@ -27,115 +34,220 @@ function WebGLMorphtargets( gl ) {
 
 		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.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.flipSided ? '#define FLIP_SIDED' : '',
 
@@ -552,7 +554,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 
 			'#endif',
 
-			'#ifdef USE_MORPHTARGETS',
+			'#if ( defined( USE_MORPHTARGETS ) && ! defined( MORPHTARGETS_TEXTURE ) )',
 
 			'	attribute vec3 morphTarget0;',
 			'	attribute vec3 morphTarget1;',
@@ -708,6 +710,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 		versionString = '#version 300 es\n';
 
 		prefixVertex = [
+			'precision mediump sampler2DArray;',
 			'#define attribute in',
 			'#define varying out',
 			'#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',
 		'alphaMap', 'alphaTest', 'combine', 'vertexColors', 'vertexAlphas', 'vertexTangents', 'vertexUvs', 'uvsVertexOnly', 'fog', 'useFog', 'fogExp2',
 		'flatShading', 'sizeAttenuation', 'logarithmicDepthBuffer', 'skinning',
-		'maxBones', 'useVertexTexture', 'morphTargets', 'morphNormals', 'premultipliedAlpha',
+		'maxBones', 'useVertexTexture', 'morphTargets', 'morphNormals', 'morphTargetsCount', 'premultipliedAlpha',
 		'numDirLights', 'numPointLights', 'numSpotLights', 'numHemiLights', 'numRectAreaLights',
 		'numDirLightShadows', 'numPointLightShadows', 'numSpotLightShadows',
 		'shadowMapEnabled', 'shadowMapType', 'toneMapping', 'physicallyCorrectLights',
@@ -243,6 +243,7 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 
 			morphTargets: !! object.geometry && !! object.geometry.morphAttributes.position,
 			morphNormals: !! object.geometry && !! object.geometry.morphAttributes.normal,
+			morphTargetsCount: ( !! object.geometry && !! object.geometry.morphAttributes.position ) ? object.geometry.morphAttributes.position.length : 0,
 
 			numDirLights: lights.directional.length,
 			numPointLights: lights.point.length,