From f1ca24cde4a37e08bc51872b971b330ca0f150cc Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Sat, 28 Aug 2021 11:25:51 +0200 Subject: [PATCH 1/2] WebGLRenderer: Support more than 8 morph targets. --- src/renderers/WebGLRenderer.js | 8 +- .../ShaderChunk/morphnormal_vertex.glsl.js | 21 +- .../morphtarget_pars_vertex.glsl.js | 27 ++- .../ShaderChunk/morphtarget_vertex.glsl.js | 39 ++- src/renderers/webgl/WebGLMorphtargets.js | 228 +++++++++++++----- src/renderers/webgl/WebGLProgram.js | 5 +- src/renderers/webgl/WebGLPrograms.js | 3 +- 7 files changed, 254 insertions(+), 77 deletions(-) diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index a35d36419c53a8..503367244a7013 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -297,7 +297,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 ); @@ -1499,6 +1499,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; @@ -1520,6 +1521,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; @@ -1601,6 +1603,10 @@ function WebGLRenderer( parameters = {} ) { needsProgramChange = true; + } else if ( capabilities.isWebGL2 === true && materialProperties.morphTargetsCount !== morphTargetsCount ) { + + needsProgramChange = true; + } } else { diff --git a/src/renderers/shaders/ShaderChunk/morphnormal_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/morphnormal_vertex.glsl.js index ba9cf24eeb89b6..5549864ffa3cdd 100644 --- a/src/renderers/shaders/ShaderChunk/morphnormal_vertex.glsl.js +++ b/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 `; diff --git a/src/renderers/shaders/ShaderChunk/morphtarget_pars_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/morphtarget_pars_vertex.glsl.js index 1046b94682f1fa..fd92e7300110b2 100644 --- a/src/renderers/shaders/ShaderChunk/morphtarget_pars_vertex.glsl.js +++ b/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 diff --git a/src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js index dfa405851c1324..5353a4b0f8fc47 100644 --- a/src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js +++ b/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 diff --git a/src/renderers/webgl/WebGLMorphtargets.js b/src/renderers/webgl/WebGLMorphtargets.js index 1a0a004b5476f3..94b060c08b0978 100644 --- a/src/renderers/webgl/WebGLMorphtargets.js +++ b/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 ); + } } diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js index c4026983810f3b..77dbdd900a373a 100644 --- a/src/renderers/webgl/WebGLProgram.js +++ b/src/renderers/webgl/WebGLProgram.js @@ -487,6 +487,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' : '', @@ -538,7 +540,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { '#endif', - '#ifdef USE_MORPHTARGETS', + '#if ( defined( USE_MORPHTARGETS ) && ! defined( MORPHTARGETS_TEXTURE ) )', ' attribute vec3 morphTarget0;', ' attribute vec3 morphTarget1;', @@ -689,6 +691,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { versionString = '#version 300 es\n'; prefixVertex = [ + 'precision highp sampler2DArray;', '#define attribute in', '#define varying out', '#define texture2D texture' diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js index 28c209a808d140..5d2d2bbf4f0226 100644 --- a/src/renderers/webgl/WebGLPrograms.js +++ b/src/renderers/webgl/WebGLPrograms.js @@ -41,7 +41,7 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities 'specularMap', 'specularIntensityMap', 'specularTintMap', 'specularTintMapEncoding', 'roughnessMap', 'metalnessMap', 'gradientMap', 'alphaMap', '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', @@ -233,6 +233,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, From f48431f8a863a85587ad4dd9fa5cda14c66736e5 Mon Sep 17 00:00:00 2001 From: Mugen87 Date: Sat, 28 Aug 2021 14:05:56 +0200 Subject: [PATCH 2/2] WebGLProgram: Use mediump for sampler2DArray. --- src/renderers/webgl/WebGLProgram.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js index 77dbdd900a373a..5f9d048825caf0 100644 --- a/src/renderers/webgl/WebGLProgram.js +++ b/src/renderers/webgl/WebGLProgram.js @@ -691,7 +691,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { versionString = '#version 300 es\n'; prefixVertex = [ - 'precision highp sampler2DArray;', + 'precision mediump sampler2DArray;', '#define attribute in', '#define varying out', '#define texture2D texture'