diff --git a/docs/api/en/constants/Loaders.html b/docs/api/en/constants/Loaders.html new file mode 100644 index 00000000000000..079bb0a996a060 --- /dev/null +++ b/docs/api/en/constants/Loaders.html @@ -0,0 +1,33 @@ + + + + + + + + + +

Loader Constants

+ +

Skinning Modes

+ + Controls whether or not a SkinnedMesh will create and use a bone index/weights texture instead of a vertex buffer. + + +THREE.BoneIndexWeightsTextureNever +THREE.BoneIndexWeightsTextureAlways +THREE.BoneIndexWeightsTextureAllow + +

+ [page:constant BoneIndexWeightsTextureNever] Never uses a bone index/weights texture. A single vertex buffer -- up to 4 weights per-vertex -- will always be used. If a SkinnedMesh has more than 4 weights per-vertex, the 4 highest-weighted bones will be used for each vertex.
+ [page:constant BoneIndexWeightsTextureAlways] Always use a bone index/weights texture instead of a vertex buffer. Any number of vertex weights is supported per-vertex.
+ [page:constant BoneIndexWeightsTextureAllow] (Default) Use a bone index/weights texture instead of a vertex buffer if a mesh has more than 4 weights per-vertex.
+

+ +

Source

+ +

+ [link:https://github.com/mrdoob/three.js/blob/master/src/constants.js src/constants.js] +

+ + diff --git a/docs/api/en/objects/SkinnedMesh.html b/docs/api/en/objects/SkinnedMesh.html index aaf27d36b2347d..d32c0fb9c9ddac 100644 --- a/docs/api/en/objects/SkinnedMesh.html +++ b/docs/api/en/objects/SkinnedMesh.html @@ -83,12 +83,13 @@

Code Example

Constructor

- [name]( [param:BufferGeometry geometry], [param:Material material] ) + [name]( [param:BufferGeometry geometry], [param:Material material], [param:Constant useBoneIndexWeightsTexture] )

[page:BufferGeometry geometry] - an instance of [page:BufferGeometry].
[page:Material material] - (optional) an instance of [page:Material]. Default is a new [page:MeshBasicMaterial]. + [page:Constant useBoneIndexWeightsTexture] — (optional) Controls whether or not a bone weights texture will be created and used instead of a vertex buffer for skinning. Defaults to [page:Loaders THREE.BoneIndexWeightsTextureAllow].

Properties

diff --git a/docs/examples/en/loaders/GLTFLoader.html b/docs/examples/en/loaders/GLTFLoader.html index cdd5fdd1d931a2..c5c7aba9815960 100644 --- a/docs/examples/en/loaders/GLTFLoader.html +++ b/docs/examples/en/loaders/GLTFLoader.html @@ -163,9 +163,13 @@

Custom extensions

Constructor

-

[name]( [param:LoadingManager manager] )

+

[name]( [param:LoadingManager manager], [param:Object options] )

[page:LoadingManager manager] — The [page:LoadingManager loadingManager] for the loader to use. Default is [page:LoadingManager THREE.DefaultLoadingManager]. + [page:Object options] — (optional) Change loading behavior. Properties:
+

Creates a new [name]. diff --git a/docs/examples/en/loaders/MMDLoader.html b/docs/examples/en/loaders/MMDLoader.html index 89b5c416a7ad10..0f248e75e44083 100644 --- a/docs/examples/en/loaders/MMDLoader.html +++ b/docs/examples/en/loaders/MMDLoader.html @@ -68,9 +68,13 @@

Examples

Constructor

-

[name]( [param:LoadingManager manager] )

+

[name]( [param:LoadingManager manager], [param:Object options] )

[page:LoadingManager manager] — The [page:LoadingManager loadingManager] for the loader to use. Default is [page:LoadingManager THREE.DefaultLoadingManager]. + [page:Object options] — (optional) Change loading behavior. Properties:
+

Creates a new [name]. diff --git a/docs/list.json b/docs/list.json index 807ff542572637..90429bd6088ed3 100644 --- a/docs/list.json +++ b/docs/list.json @@ -72,6 +72,7 @@ "Core": "api/en/constants/Core", "CustomBlendingEquation": "api/en/constants/CustomBlendingEquations", "BufferAttributeUsage": "api/en/constants/BufferAttributeUsage", + "Loaders": "api/en/constants/Loaders", "Materials": "api/en/constants/Materials", "Renderer": "api/en/constants/Renderer", "Textures": "api/en/constants/Textures" diff --git a/examples/files.json b/examples/files.json index 1a4f9a01325b75..436171e3c69761 100644 --- a/examples/files.json +++ b/examples/files.json @@ -4,7 +4,9 @@ "webgl_animation_skinning_blending", "webgl_animation_skinning_additive_blending", "webgl_animation_skinning_ik", + "webgl_animation_skinning_weight-texture", "webgl_animation_skinning_morph", + "webgl_animation_skinning_performance", "webgl_animation_multiple", "webgl_camera", "webgl_camera_array", diff --git a/examples/jsm/loaders/ColladaLoader.js b/examples/jsm/loaders/ColladaLoader.js index f3497f33e7d7e8..a4423afb718b18 100644 --- a/examples/jsm/loaders/ColladaLoader.js +++ b/examples/jsm/loaders/ColladaLoader.js @@ -42,6 +42,13 @@ import { TGALoader } from '../loaders/TGALoader.js'; class ColladaLoader extends Loader { + constructor( manager, options = {} ) { + + super( manager ); + this.options = options; + + } + load( url, onLoad, onProgress, onError ) { const scope = this; @@ -80,6 +87,8 @@ class ColladaLoader extends Loader { parse( text, path ) { + const options = this.options; + function getElementsByTagName( xml, name ) { // Non recursive xml.getElementsByTagName() ... @@ -3626,7 +3635,6 @@ class ColladaLoader extends Loader { if ( object.isSkinnedMesh ) { object.bind( skeleton, controller.skin.bindMatrix ); - object.normalizeSkinWeights(); } @@ -3814,7 +3822,9 @@ class ColladaLoader extends Loader { case 'polylist': if ( skinning ) { - object = new SkinnedMesh( geometry.data, material ); + object = new SkinnedMesh( geometry.data, material, { + useBoneIndexWeightsTexture: options.useBoneIndexWeightsTexture + } ); } else { diff --git a/examples/jsm/loaders/FBXLoader.js b/examples/jsm/loaders/FBXLoader.js index 0f23faf695afb2..bda05f9fe15fc1 100644 --- a/examples/jsm/loaders/FBXLoader.js +++ b/examples/jsm/loaders/FBXLoader.js @@ -68,9 +68,10 @@ let sceneGraph; class FBXLoader extends Loader { - constructor( manager ) { + constructor( manager, options = {} ) { super( manager ); + this.options = options; } @@ -142,7 +143,7 @@ class FBXLoader extends Loader { const textureLoader = new TextureLoader( this.manager ).setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin ); - return new FBXTreeParser( textureLoader, this.manager ).parse( fbxTree ); + return new FBXTreeParser( textureLoader, this.manager, this.options ).parse( fbxTree ); } @@ -151,10 +152,11 @@ class FBXLoader extends Loader { // Parse the FBXTree object returned by the BinaryParser or TextParser and return a Group class FBXTreeParser { - constructor( textureLoader, manager ) { + constructor( textureLoader, manager, options = {} ) { this.textureLoader = textureLoader; this.manager = manager; + this.options = options; } @@ -1310,8 +1312,9 @@ class FBXTreeParser { if ( geometry.FBX_Deformer ) { - model = new SkinnedMesh( geometry, material ); - model.normalizeSkinWeights(); + model = new SkinnedMesh( geometry, material, { + useBoneIndexWeightsTexture: this.options.useBoneIndexWeightsTexture + } ); } else { diff --git a/examples/jsm/loaders/GLTFLoader.js b/examples/jsm/loaders/GLTFLoader.js index 0679071232d106..8402cb26724737 100644 --- a/examples/jsm/loaders/GLTFLoader.js +++ b/examples/jsm/loaders/GLTFLoader.js @@ -1,6 +1,7 @@ import { AnimationClip, Bone, + BoneIndexWeightsTextureAllow, Box3, BufferAttribute, BufferGeometry, @@ -69,10 +70,13 @@ import { toTrianglesDrawMode } from '../utils/BufferGeometryUtils.js'; class GLTFLoader extends Loader { - constructor( manager ) { + constructor( manager, options = {} ) { super( manager ); + this.useBoneIndexWeightsTexture = + options.useBoneIndexWeightsTexture ?? BoneIndexWeightsTextureAllow; + this.dracoLoader = null; this.ktx2Loader = null; this.meshoptDecoder = null; @@ -371,7 +375,8 @@ class GLTFLoader extends Loader { requestHeader: this.requestHeader, manager: this.manager, ktx2Loader: this.ktx2Loader, - meshoptDecoder: this.meshoptDecoder + meshoptDecoder: this.meshoptDecoder, + useBoneIndexWeightsTexture: this.useBoneIndexWeightsTexture } ); @@ -1952,7 +1957,7 @@ class GLTFDracoMeshCompressionExtension { for ( const attributeName in gltfAttributeMap ) { - const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); + const threeAttributeName = getThreeAttributeName( attributeName ); threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; @@ -1960,7 +1965,7 @@ class GLTFDracoMeshCompressionExtension { for ( const attributeName in primitive.attributes ) { - const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); + const threeAttributeName = getThreeAttributeName( attributeName ); if ( gltfAttributeMap[ attributeName ] !== undefined ) { @@ -2239,8 +2244,6 @@ const ATTRIBUTES = { TEXCOORD_2: 'uv2', TEXCOORD_3: 'uv3', COLOR_0: 'color', - WEIGHTS_0: 'skinWeight', - JOINTS_0: 'skinIndex', }; const PATH_PROPERTIES = { @@ -2263,6 +2266,28 @@ const ALPHA_MODES = { BLEND: 'BLEND' }; +function getThreeAttributeName( gltfAttributeName ) { + + if ( ATTRIBUTES[ gltfAttributeName ] !== undefined) { + + return ATTRIBUTES[ gltfAttributeName ]; + + } else if ( gltfAttributeName.startsWith( 'JOINTS_' ) ) { + + const index = gltfAttributeName.substring(7); + return 'skinIndex' + ( index === '0' ? '' : index ); + + } else if ( gltfAttributeName.startsWith( 'WEIGHTS_' ) ) { + + const index = gltfAttributeName.substring(8); + return 'skinWeight' + ( index === '0' ? '' : index ); + + } + + return gltfAttributeName.toLowerCase(); + +} + /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material */ @@ -3788,16 +3813,11 @@ class GLTFParser { // .isSkinnedMesh isn't in glTF spec. See ._markDefs() mesh = meshDef.isSkinnedMesh === true - ? new SkinnedMesh( geometry, material ) + ? new SkinnedMesh( geometry, material, { + useBoneIndexWeightsTexture: parser.options.useBoneIndexWeightsTexture + } ) : new Mesh( geometry, material ); - if ( mesh.isSkinnedMesh === true ) { - - // normalize skin weights to fix malformed assets (see #15319) - mesh.normalizeSkinWeights(); - - } - if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode ); @@ -4678,7 +4698,7 @@ function addPrimitiveAttributes( geometry, primitiveDef, parser ) { for ( const gltfAttributeName in attributes ) { - const threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); + const threeAttributeName = getThreeAttributeName( gltfAttributeName ); // Skip attributes already provided by e.g. Draco extension. if ( threeAttributeName in geometry.attributes ) continue; diff --git a/examples/jsm/loaders/MMDLoader.js b/examples/jsm/loaders/MMDLoader.js index 1b0c2dad972cf6..c5f916173e4640 100644 --- a/examples/jsm/loaders/MMDLoader.js +++ b/examples/jsm/loaders/MMDLoader.js @@ -76,14 +76,14 @@ import { MMDParser } from '../libs/mmdparser.module.js'; */ class MMDLoader extends Loader { - constructor( manager ) { + constructor( manager, options = {} ) { super( manager ); this.loader = new FileLoader( this.manager ); this.parser = null; // lazy generation - this.meshBuilder = new MeshBuilder( this.manager ); + this.meshBuilder = new MeshBuilder( this.manager, options ); this.animationBuilder = new AnimationBuilder(); } @@ -414,11 +414,12 @@ const NON_ALPHA_CHANNEL_FORMATS = [ */ class MeshBuilder { - constructor( manager ) { + constructor( manager, options = {} ) { this.crossOrigin = 'anonymous'; this.geometryBuilder = new GeometryBuilder(); this.materialBuilder = new MaterialBuilder( manager ); + this.options = options; } @@ -448,7 +449,9 @@ class MeshBuilder { .setResourcePath( resourcePath ) .build( data, geometry, onProgress, onError ); - const mesh = new SkinnedMesh( geometry, material ); + const mesh = new SkinnedMesh( geometry, material, { + useBoneIndexWeightsTexture: this.options.useBoneIndexWeightsTexture + } ); const skeleton = new Skeleton( initBones( mesh ) ); mesh.bind( skeleton ); diff --git a/examples/models/gltf/HeadWithMax16Joints.glb b/examples/models/gltf/HeadWithMax16Joints.glb new file mode 100644 index 00000000000000..87cce56313ca52 Binary files /dev/null and b/examples/models/gltf/HeadWithMax16Joints.glb differ diff --git a/examples/screenshots/svg_sandbox.jpg b/examples/screenshots/svg_sandbox.jpg index 0d81d9f62debbf..cea6e79813153e 100644 Binary files a/examples/screenshots/svg_sandbox.jpg and b/examples/screenshots/svg_sandbox.jpg differ diff --git a/examples/screenshots/webgl_animation_skinning_performance.jpg b/examples/screenshots/webgl_animation_skinning_performance.jpg new file mode 100644 index 00000000000000..41c86be3cc1ada Binary files /dev/null and b/examples/screenshots/webgl_animation_skinning_performance.jpg differ diff --git a/examples/screenshots/webgl_animation_skinning_weight-texture.jpg b/examples/screenshots/webgl_animation_skinning_weight-texture.jpg new file mode 100644 index 00000000000000..1e62f2e80021f5 Binary files /dev/null and b/examples/screenshots/webgl_animation_skinning_weight-texture.jpg differ diff --git a/examples/webgl_animation_skinning_performance.html b/examples/webgl_animation_skinning_performance.html new file mode 100644 index 00000000000000..ebf32d663b858d --- /dev/null +++ b/examples/webgl_animation_skinning_performance.html @@ -0,0 +1,671 @@ + + + + three.js webgl - animation - skinning - performance + + + + + + +

+
+ three.js + - Performance Test Scene of GPU Implementations for Skeletal Animations
+ (Soldier model from mixamo.com, + mask model made by Iker J. de los Mozos, CC Attribution)
+ Note: crossfades are possible with blend weights being set to (1,0,0), (0,1,0) or (0,0,1) +
+ + + + + + + + + + + diff --git a/examples/webgl_animation_skinning_weight-texture.html b/examples/webgl_animation_skinning_weight-texture.html new file mode 100644 index 00000000000000..580cf09d5b9d64 --- /dev/null +++ b/examples/webgl_animation_skinning_weight-texture.html @@ -0,0 +1,241 @@ + + + + three.js webgl - animation - skinning - many bone influences + + + + + + +
+
+ three.js + - Skeletal Animations with <= 4 (left) and > 4 (right) Bone Influences Per Vertex
+ (Mask model made by Iker J. de los Mozos, CC Attribution) +
+ + + + + + + + + + + diff --git a/src/constants.js b/src/constants.js index 7457702bcd8232..ae338433289fd7 100644 --- a/src/constants.js +++ b/src/constants.js @@ -156,6 +156,10 @@ export const RGBADepthPacking = 3201; export const TangentSpaceNormalMap = 0; export const ObjectSpaceNormalMap = 1; +export const BoneIndexWeightsTextureNever = 0; +export const BoneIndexWeightsTextureAlways = 1; +export const BoneIndexWeightsTextureAllow = 2; + // Color space string identifiers, matching CSS Color Module Level 4 and WebGPU names where available. export const NoColorSpace = ''; export const SRGBColorSpace = 'srgb'; diff --git a/src/core/BufferAttribute.js b/src/core/BufferAttribute.js index d3540089831944..44b9802b686abd 100644 --- a/src/core/BufferAttribute.js +++ b/src/core/BufferAttribute.js @@ -218,81 +218,49 @@ class BufferAttribute { getX( index ) { - let x = this.array[ index * this.itemSize ]; - - if ( this.normalized ) x = denormalize( x, this.array ); - - return x; + return this.getComponent( index, 0 ); } setX( index, x ) { - if ( this.normalized ) x = normalize( x, this.array ); - - this.array[ index * this.itemSize ] = x; - - return this; + return this.setComponent( index, 0, x ); } getY( index ) { - let y = this.array[ index * this.itemSize + 1 ]; - - if ( this.normalized ) y = denormalize( y, this.array ); - - return y; + return this.getComponent( index, 1 ); } setY( index, y ) { - if ( this.normalized ) y = normalize( y, this.array ); - - this.array[ index * this.itemSize + 1 ] = y; - - return this; + return this.setComponent( index, 1, y ); } getZ( index ) { - let z = this.array[ index * this.itemSize + 2 ]; - - if ( this.normalized ) z = denormalize( z, this.array ); - - return z; + return this.getComponent( index, 2 ); } setZ( index, z ) { - if ( this.normalized ) z = normalize( z, this.array ); - - this.array[ index * this.itemSize + 2 ] = z; - - return this; + return this.setComponent( index, 2, z ); } getW( index ) { - let w = this.array[ index * this.itemSize + 3 ]; - - if ( this.normalized ) w = denormalize( w, this.array ); - - return w; + return this.getComponent( index, 3 ); } setW( index, w ) { - if ( this.normalized ) w = normalize( w, this.array ); - - this.array[ index * this.itemSize + 3 ] = w; - - return this; + return this.setComponent( index, 3, w ); } diff --git a/src/core/BufferGeometry.js b/src/core/BufferGeometry.js index 531f039f8d5c60..457f1d7fed8748 100644 --- a/src/core/BufferGeometry.js +++ b/src/core/BufferGeometry.js @@ -51,6 +51,50 @@ class BufferGeometry extends EventDispatcher { } + getSkinIndexBuffers() { + + const buffers = []; + + for ( let ii = 0; ; ++ ii ) { + + const name = 'skinIndex' + ( ii === 0 ? '' : ii.toString() ); + + if ( ! this.hasAttribute( name ) ) { + + break; + + } + + buffers.push( this.getAttribute( name ) ); + + } + + return buffers; + + } + + getSkinWeightBuffers() { + + const buffers = []; + + for ( let ii = 0; ; ++ ii ) { + + const name = 'skinWeight' + ( ii === 0 ? '' : ii.toString() ); + + if ( ! this.hasAttribute( name ) ) { + + break; + + } + + buffers.push( this.getAttribute( name ) ); + + } + + return buffers; + + } + getIndex() { return this.index; diff --git a/src/core/InterleavedBufferAttribute.js b/src/core/InterleavedBufferAttribute.js index 47b0f8fb13bc1d..65dbc3b7b32106 100644 --- a/src/core/InterleavedBufferAttribute.js +++ b/src/core/InterleavedBufferAttribute.js @@ -86,16 +86,6 @@ class InterleavedBufferAttribute { } - getComponent( index, component ) { - - let value = this.array[ index * this.data.stride + this.offset + component ]; - - if ( this.normalized ) value = denormalize( value, this.array ); - - return value; - - } - setComponent( index, component, value ) { if ( this.normalized ) value = normalize( value, this.array ); @@ -108,81 +98,59 @@ class InterleavedBufferAttribute { setX( index, x ) { - if ( this.normalized ) x = normalize( x, this.array ); - - this.data.array[ index * this.data.stride + this.offset ] = x; - - return this; + return this.setComponent( index, 0, x ); } setY( index, y ) { - if ( this.normalized ) y = normalize( y, this.array ); - - this.data.array[ index * this.data.stride + this.offset + 1 ] = y; - - return this; + return this.setComponent( index, 1, y ); } setZ( index, z ) { - if ( this.normalized ) z = normalize( z, this.array ); + return this.setComponent( index, 2, z ); + + } - this.data.array[ index * this.data.stride + this.offset + 2 ] = z; + setW( index, w ) { - return this; + return this.setComponent( index, 3, w ); } - setW( index, w ) { + getComponent( index, component ) { - if ( this.normalized ) w = normalize( w, this.array ); + let item = this.data.array[ index * this.data.stride + this.offset + component ]; - this.data.array[ index * this.data.stride + this.offset + 3 ] = w; + if ( this.normalized ) item = denormalize( item, this.array ); - return this; + return item; } getX( index ) { - let x = this.data.array[ index * this.data.stride + this.offset ]; - - if ( this.normalized ) x = denormalize( x, this.array ); - - return x; + return this.getComponent( index, 0 ); } getY( index ) { - let y = this.data.array[ index * this.data.stride + this.offset + 1 ]; - - if ( this.normalized ) y = denormalize( y, this.array ); - - return y; + return this.getComponent( index, 1 ); } getZ( index ) { - let z = this.data.array[ index * this.data.stride + this.offset + 2 ]; - - if ( this.normalized ) z = denormalize( z, this.array ); - - return z; + return this.getComponent( index, 2 ); } getW( index ) { - let w = this.data.array[ index * this.data.stride + this.offset + 3 ]; - - if ( this.normalized ) w = denormalize( w, this.array ); - - return w; + return this.getComponent( index, 3 ); } diff --git a/src/loaders/ObjectLoader.js b/src/loaders/ObjectLoader.js index d430386baac682..7ac81ea346e198 100644 --- a/src/loaders/ObjectLoader.js +++ b/src/loaders/ObjectLoader.js @@ -65,9 +65,10 @@ import { Sphere } from '../math/Sphere.js'; class ObjectLoader extends Loader { - constructor( manager ) { + constructor( manager, options = {} ) { super( manager ); + this.options = options; } @@ -872,7 +873,9 @@ class ObjectLoader extends Loader { geometry = getGeometry( data.geometry ); material = getMaterial( data.material ); - object = new SkinnedMesh( geometry, material ); + object = new SkinnedMesh( geometry, material, { + useBoneIndexWeightsTexture: this.options.useBoneIndexWeightsTexture + } ); if ( data.bindMode !== undefined ) object.bindMode = data.bindMode; if ( data.bindMatrix !== undefined ) object.bindMatrix.fromArray( data.bindMatrix ); diff --git a/src/objects/SkinnedMesh.js b/src/objects/SkinnedMesh.js index 1fc327a567a850..bfb5463a4a5f51 100644 --- a/src/objects/SkinnedMesh.js +++ b/src/objects/SkinnedMesh.js @@ -5,7 +5,9 @@ import { Sphere } from '../math/Sphere.js'; import { Vector3 } from '../math/Vector3.js'; import { Vector4 } from '../math/Vector4.js'; import { Ray } from '../math/Ray.js'; -import { AttachedBindMode, DetachedBindMode } from '../constants.js'; +import { AttachedBindMode, BoneIndexWeightsTextureAllow, BoneIndexWeightsTextureNever, DetachedBindMode, FloatType, IntType, RGFormat } from '../constants.js'; +import { DataTexture } from '../textures/DataTexture.js'; +import { BufferAttribute } from '../core/BufferAttribute.js'; const _basePosition = /*@__PURE__*/ new Vector3(); @@ -22,7 +24,7 @@ const _ray = /*@__PURE__*/ new Ray(); class SkinnedMesh extends Mesh { - constructor( geometry, material ) { + constructor( geometry, material, options = {} ) { super( geometry, material ); @@ -37,6 +39,104 @@ class SkinnedMesh extends Mesh { this.boundingBox = null; this.boundingSphere = null; + this.useBoneIndexWeightsTexture = + options.useBoneIndexWeightsTexture !== undefined ? + options.useBoneIndexWeightsTexture : BoneIndexWeightsTextureAllow; + + this.boneIndexWeightsTexture = null; + + // Don't process geometry data if this constructor is being called as part + // of a .clone() operation. The object's members will already be set but the + // constructor parameters will be undefined. + if ( geometry ) { + + this.normalizeSkinWeights(); + this.createBoneIndexWeightsTexture(); + + } + + } + + createBoneIndexWeightsTexture() { + + if ( ! this.geometry + || this.useBoneIndexWeightsTexture === BoneIndexWeightsTextureNever + || ( this.useBoneIndexWeightsTexture === BoneIndexWeightsTextureAllow + && this.geometry.getSkinIndexBuffers().length <= 1 ) ) { + + return; + + } + + const indexBuffers = this.geometry.getSkinIndexBuffers(); + const weightBuffers = this.geometry.getSkinWeightBuffers(); + + const vertexCount = indexBuffers[ 0 ].count; + const boneStartIndex = new Int32Array( vertexCount ); + + const influences = []; + + for ( let vi = 0; vi < vertexCount; ++ vi ) { + + boneStartIndex[ vi ] = influences.length / 2; + + for ( let attrIndex = 0; attrIndex < 4 * indexBuffers.length; ++ attrIndex ) { + + const bufferIndex = Math.trunc( attrIndex / 4 ); + const component = attrIndex % 4; + + const weight = weightBuffers[ bufferIndex ].getComponent( vi, component ); + + if ( weight == 0 ) { + + // weights are sorted so once 0 is seen, there are no more weights + break; + + } + + const boneIndex = indexBuffers[ bufferIndex ].getComponent( vi, component ); + influences.push( boneIndex, weight ); + + } + + influences.push( - 1, - 1 ); + + } + + // make the texture width a power of 2 + const influenceCount = influences.length / 2; + const textureWidth = Math.pow( 2, Math.ceil( Math.log2( Math.sqrt( influenceCount ) ) ) ); + const textureHeight = Math.ceil( influenceCount / textureWidth ); + const influenceArray = new Float32Array( 2 * textureWidth * textureHeight ); + + for ( let ii = 0; ii < influences.length; ++ ii ) { + + influenceArray[ ii ] = influences[ ii ]; + + } + + for ( let ii = influences.length; ii < influenceArray.length; ++ ii ) { + + influenceArray[ ii ] = - 1; + + } + + if ( this.boneIndexWeightsTexture ) { + + this.boneIndexWeightsTexture.dispose(); + + } + + this.boneIndexWeightsTexture = new DataTexture( + influenceArray, textureWidth, textureHeight, RGFormat, FloatType ); + + this.boneIndexWeightsTexture.needsUpdate = true; + + const attrib = new BufferAttribute( boneStartIndex, 1 ); + attrib.gpuType = IntType; + + this.geometry.setAttribute( 'bonePairTexStartIndex', attrib ); + } computeBoundingBox() { @@ -94,7 +194,9 @@ class SkinnedMesh extends Mesh { this.bindMatrixInverse.copy( source.bindMatrixInverse ); this.skeleton = source.skeleton; + this.useBoneIndexWeightsTexture = source.useBoneIndexWeightsTexture; + if ( source.boneIndexWeightsTexture !== null ) this.boneIndexWeightsTexture = source.boneIndexWeightsTexture.clone(); if ( source.boundingBox !== null ) this.boundingBox = source.boundingBox.clone(); if ( source.boundingSphere !== null ) this.boundingSphere = source.boundingSphere.clone(); @@ -174,27 +276,83 @@ class SkinnedMesh extends Mesh { normalizeSkinWeights() { - const vector = new Vector4(); + const weightBuffers = this.geometry.getSkinWeightBuffers(); + const indexBuffers = this.geometry.getSkinIndexBuffers(); + const vertexCount = weightBuffers[ 0 ].count; + const maxInfluences = + this.useBoneIndexWeightsTexture == BoneIndexWeightsTextureNever ? 4 : 4 * indexBuffers.length; + + const tempBoneIndexes = [ ...Array( maxInfluences ) ]; + const tempBoneWeights = [ ...Array( maxInfluences ) ]; + + for ( let vi = 0; vi < vertexCount; vi ++ ) { + + const sortedSkinPairIndexes = []; + + for ( let jj = 0; jj < 4 * weightBuffers.length; ++ jj ) { - const skinWeight = this.geometry.attributes.skinWeight; + sortedSkinPairIndexes.push( jj ); - for ( let i = 0, l = skinWeight.count; i < l; i ++ ) { + } + + // Sort by descending weight so when weights are limited we only take the + // highest ones + sortedSkinPairIndexes.sort( ( a, b ) => { + + const bufIndexA = Math.trunc( a / 4 ); + const bufIndexB = Math.trunc( b / 4 ); + + return weightBuffers[ bufIndexB ].getComponent( vi, b % 4 ) + - weightBuffers[ bufIndexA ].getComponent( vi, a % 4 ); + + } ); - vector.fromBufferAttribute( skinWeight, i ); + let vertexWeight = 0; - const scale = 1.0 / vector.manhattanLength(); + for ( let destAttrIndex = 0; destAttrIndex < maxInfluences; ++ destAttrIndex ) { + + const srcAttrIndex = sortedSkinPairIndexes[ destAttrIndex ]; + const bufIndex = Math.trunc( srcAttrIndex / 4 ); + const component = srcAttrIndex % 4; + const weight = weightBuffers[ bufIndex ].getComponent( vi, component ); + vertexWeight += weight; + + tempBoneIndexes[ destAttrIndex ] = indexBuffers[ bufIndex ].getComponent( vi, component ); + tempBoneWeights[ destAttrIndex ] = weight; + + } - if ( scale !== Infinity ) { + if ( vertexWeight === 0 ) { - vector.multiplyScalar( scale ); + // do something reasonable + weightBuffers[ 0 ].setXYZW( 1, 0, 0, 0 ); + for ( let bufIndex = 1; bufIndex < weightBuffers.length; ++ bufIndex ) { + + weightBuffers[ bufIndex ].setXYZW( 0, 0, 0, 0 ); + + } } else { - vector.set( 1, 0, 0, 0 ); // do something reasonable + for ( let attrIndex = 0; attrIndex < maxInfluences; ++ attrIndex ) { + + const bufIndex = Math.trunc( attrIndex / 4 ); + const component = attrIndex % 4; + const normalizedWeight = tempBoneWeights[ attrIndex ] / vertexWeight; + + indexBuffers[ bufIndex ].setComponent( vi, component, tempBoneIndexes[ attrIndex ] ); + weightBuffers[ bufIndex ].setComponent( vi, component, normalizedWeight ); + + } } - skinWeight.setXYZW( i, vector.x, vector.y, vector.z, vector.w ); + } + + for ( let bufIndex = 0; bufIndex < maxInfluences / 4; ++ bufIndex ) { + + weightBuffers[ bufIndex ].needsUpdate = true; + indexBuffers[ bufIndex ].needsUpdate = true; } diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 901af91dbaac4c..f5aab9a0420d95 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -1953,6 +1953,7 @@ class WebGLRenderer { if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture(); p_uniforms.setValue( _gl, 'boneTexture', skeleton.boneTexture, textures ); + p_uniforms.setValue( _gl, 'boneIndexWeightsTexture', object.boneIndexWeightsTexture, textures ); } diff --git a/src/renderers/shaders/ShaderChunk/skinbase_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/skinbase_vertex.glsl.js index 17d8f40acbefeb..be0d50c576f810 100644 --- a/src/renderers/shaders/ShaderChunk/skinbase_vertex.glsl.js +++ b/src/renderers/shaders/ShaderChunk/skinbase_vertex.glsl.js @@ -1,10 +1,14 @@ export default /* glsl */` #ifdef USE_SKINNING - mat4 boneMatX = getBoneMatrix( skinIndex.x ); - mat4 boneMatY = getBoneMatrix( skinIndex.y ); - mat4 boneMatZ = getBoneMatrix( skinIndex.z ); - mat4 boneMatW = getBoneMatrix( skinIndex.w ); + #ifndef USE_BONE_WEIGHTS_TEX + + mat4 boneMatX = getBoneMatrix( skinIndex.x ); + mat4 boneMatY = getBoneMatrix( skinIndex.y ); + mat4 boneMatZ = getBoneMatrix( skinIndex.z ); + mat4 boneMatW = getBoneMatrix( skinIndex.w ); + + #endif #endif `; diff --git a/src/renderers/shaders/ShaderChunk/skinning_pars_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/skinning_pars_vertex.glsl.js index 10b4dc5222ff53..d9fa0774ac3332 100644 --- a/src/renderers/shaders/ShaderChunk/skinning_pars_vertex.glsl.js +++ b/src/renderers/shaders/ShaderChunk/skinning_pars_vertex.glsl.js @@ -6,6 +6,14 @@ export default /* glsl */` uniform highp sampler2D boneTexture; + #ifdef USE_BONE_WEIGHTS_TEX + + #define MAX_BONES_PER_VERT 64 + + uniform sampler2D boneIndexWeightsTexture; + + #endif + mat4 getBoneMatrix( const in float i ) { int size = textureSize( boneTexture, 0 ).x; diff --git a/src/renderers/shaders/ShaderChunk/skinning_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/skinning_vertex.glsl.js index a07418c4798f42..de36a7da7a2f18 100644 --- a/src/renderers/shaders/ShaderChunk/skinning_vertex.glsl.js +++ b/src/renderers/shaders/ShaderChunk/skinning_vertex.glsl.js @@ -4,10 +4,48 @@ export default /* glsl */` vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); vec4 skinned = vec4( 0.0 ); - skinned += boneMatX * skinVertex * skinWeight.x; - skinned += boneMatY * skinVertex * skinWeight.y; - skinned += boneMatZ * skinVertex * skinWeight.z; - skinned += boneMatW * skinVertex * skinWeight.w; + + #ifdef USE_BONE_WEIGHTS_TEX + + { + + // boneTexWidth name conflicts with skinnormal_vertex.glsl.js if it's + // enabled so we nest an extra block + int boneTexWidth = int( textureSize( boneIndexWeightsTexture, 0 ).x ); + + for ( int ii = 0; ii < MAX_BONES_PER_VERT; ++ ii ) { + + int bonePairTexIndex = bonePairTexStartIndex + ii; + + vec2 boneIndexWeight = + texelFetch( boneIndexWeightsTexture, + ivec2( bonePairTexIndex % boneTexWidth, + bonePairTexIndex / boneTexWidth), + 0 ).xy; + + int boneIndex = int(boneIndexWeight.x); + + if ( boneIndex < 0 ) { + + break; + + } + + mat4 boneMatrix = getBoneMatrix( float( boneIndex ) ); + skinned += ( boneMatrix * skinVertex ) * boneIndexWeight.y; + + } + + } + + #else + + skinned += boneMatX * skinVertex * skinWeight.x; + skinned += boneMatY * skinVertex * skinWeight.y; + skinned += boneMatZ * skinVertex * skinWeight.z; + skinned += boneMatW * skinVertex * skinWeight.w; + + #endif transformed = ( bindMatrixInverse * skinned ).xyz; diff --git a/src/renderers/shaders/ShaderChunk/skinnormal_vertex.glsl.js b/src/renderers/shaders/ShaderChunk/skinnormal_vertex.glsl.js index 457ee10b2db943..3caadf54ef7f28 100644 --- a/src/renderers/shaders/ShaderChunk/skinnormal_vertex.glsl.js +++ b/src/renderers/shaders/ShaderChunk/skinnormal_vertex.glsl.js @@ -1,20 +1,65 @@ export default /* glsl */` #ifdef USE_SKINNING - mat4 skinMatrix = mat4( 0.0 ); - skinMatrix += skinWeight.x * boneMatX; - skinMatrix += skinWeight.y * boneMatY; - skinMatrix += skinWeight.z * boneMatZ; - skinMatrix += skinWeight.w * boneMatW; - skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; + #ifdef USE_BONE_WEIGHTS_TEX - objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; + vec3 skinnedNormal = vec3( 0.0 ); - #ifdef USE_TANGENT + { - objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; + // boneTexWidth name conflicts with skinning_vertex.glsl.js if it's + // enabled so we nest an extra block + int boneTexWidth = int( textureSize( boneIndexWeightsTexture, 0 ).x ); + + for ( int ii = 0; ii < MAX_BONES_PER_VERT; ++ ii ) { + + int bonePairTexIndex = bonePairTexStartIndex + ii; + + vec2 boneIndexWeight = + texelFetch( boneIndexWeightsTexture, + ivec2( bonePairTexIndex % boneTexWidth, + bonePairTexIndex / boneTexWidth ), + 0 ).xy; + + int boneIndex = int( boneIndexWeight.x ); + + if (boneIndex < 0) { + + break; + + } + + mat4 boneMatrix = getBoneMatrix( float( boneIndex ) ); + skinnedNormal += + normalize( mat3( bindMatrixInverse ) + * mat3( boneMatrix ) + * mat3( bindMatrix ) * objectNormal ) + * boneIndexWeight.y; + + } + + } + + objectNormal = skinnedNormal; + + #else + + mat4 skinMatrix = mat4( 0.0 ); + skinMatrix += skinWeight.x * boneMatX; + skinMatrix += skinWeight.y * boneMatY; + skinMatrix += skinWeight.z * boneMatZ; + skinMatrix += skinWeight.w * boneMatW; + skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; + + objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; #endif + #ifdef USE_TANGENT + + objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; + + #endif + #endif `; diff --git a/src/renderers/webgl/WebGLProgram.js b/src/renderers/webgl/WebGLProgram.js index d69a82a3ec16af..4bf7fe8ee96a6d 100644 --- a/src/renderers/webgl/WebGLProgram.js +++ b/src/renderers/webgl/WebGLProgram.js @@ -618,6 +618,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { parameters.flatShading ? '#define FLAT_SHADED' : '', parameters.skinning ? '#define USE_SKINNING' : '', + parameters.skinWeightsTexture ? '#define USE_BONE_WEIGHTS_TEX' : '', parameters.morphTargets ? '#define USE_MORPHTARGETS' : '', parameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '', @@ -730,8 +731,16 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) { '#ifdef USE_SKINNING', - ' attribute vec4 skinIndex;', - ' attribute vec4 skinWeight;', + ' #ifdef USE_BONE_WEIGHTS_TEX', + + ' attribute int bonePairTexStartIndex;', + + ' #else', + + ' attribute vec4 skinIndex;', + ' attribute vec4 skinWeight;', + + ' #endif', '#endif', diff --git a/src/renderers/webgl/WebGLPrograms.js b/src/renderers/webgl/WebGLPrograms.js index f4701d9006314d..957aafe3e2f028 100644 --- a/src/renderers/webgl/WebGLPrograms.js +++ b/src/renderers/webgl/WebGLPrograms.js @@ -1,4 +1,4 @@ -import { BackSide, DoubleSide, CubeUVReflectionMapping, ObjectSpaceNormalMap, TangentSpaceNormalMap, NoToneMapping, NormalBlending, LinearSRGBColorSpace, SRGBTransfer } from '../../constants.js'; +import { BackSide, BoneIndexWeightsTextureAllow, BoneIndexWeightsTextureAlways, DoubleSide, CubeUVReflectionMapping, ObjectSpaceNormalMap, TangentSpaceNormalMap, NoToneMapping, NormalBlending, LinearSRGBColorSpace, SRGBTransfer } from '../../constants.js'; import { Layers } from '../../core/Layers.js'; import { WebGLProgram } from './WebGLProgram.js'; import { WebGLShaderCache } from './WebGLShaderCache.js'; @@ -306,6 +306,14 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities logarithmicDepthBuffer: logarithmicDepthBuffer, skinning: object.isSkinnedMesh === true, + skinWeightsTexture: object.isSkinnedMesh === true + && ( + object.useBoneIndexWeightsTexture === BoneIndexWeightsTextureAlways + || ( + object.useBoneIndexWeightsTexture === BoneIndexWeightsTextureAllow + && object.boneIndexWeightsTexture !== null + ) + ), morphTargets: geometry.morphAttributes.position !== undefined, morphNormals: geometry.morphAttributes.normal !== undefined, @@ -461,6 +469,7 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities array.push( parameters.numClippingPlanes ); array.push( parameters.numClipIntersection ); array.push( parameters.depthPacking ); + array.push( parameters.skinWeightsTexture ); } diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index f626b27c76bbaa..740290e8e885c7 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -38,7 +38,7 @@ class PromiseQueue { /* CONFIG VARIABLES START */ const idleTime = 9; // 9 seconds - for how long there should be no network requests -const parseTime = 6; // 6 seconds per megabyte +const parseTime = 7; // 7 seconds per megabyte const exceptionList = [ @@ -77,6 +77,7 @@ const exceptionList = [ // Unknown // TODO: most of these can be fixed just by increasing idleTime and parseTime 'webgl_animation_skinning_blending', + 'webgl_animation_skinning_performance', 'webgl_buffergeometry_glbufferattribute', 'webgl_clipping_advanced', 'webgl_lensflares', @@ -158,7 +159,7 @@ const pixelThreshold = 0.1; // threshold error in one pixel const maxDifferentPixels = 0.3; // at most 0.3% different pixels const networkTimeout = 5; // 5 minutes, set to 0 to disable -const renderTimeout = 5; // 5 seconds, set to 0 to disable +const renderTimeout = 7; // 7 seconds, set to 0 to disable const numAttempts = 2; // perform 2 attempts before failing diff --git a/test/unit/src/core/BufferAttribute.tests.js b/test/unit/src/core/BufferAttribute.tests.js index 4f096dc7f2eaff..af1ca377acd0a8 100644 --- a/test/unit/src/core/BufferAttribute.tests.js +++ b/test/unit/src/core/BufferAttribute.tests.js @@ -257,6 +257,33 @@ export default QUnit.module( 'Core', () => { } ); + QUnit.test( 'getComponent', ( assert ) => { + + const f32a = new Float32Array( [ 1, 2, 3, 4, 5, 6, 7, 8 ] ); + const a = new BufferAttribute( f32a, 4, false ); + + assert.equal( a.getComponent( 0, 0 ), 1, 'v0.x was not retrieved' ); + assert.equal( a.getComponent( 0, 1 ), 2, 'v0.y was not retrieved' ); + assert.equal( a.getComponent( 1, 2 ), 7, 'v1.z was not retrieved' ); + assert.equal( a.getComponent( 1, 3 ), 8, 'v1.w was not retrieved' ); + + } ); + + QUnit.test( 'setComponent', ( assert ) => { + + const f32a = new Float32Array( [ 0, 0, 0, 0, 0, 0, 0, 0 ] ); + const a = new BufferAttribute( f32a, 4, false ); + const expected = new Float32Array( [ 1, 2, 0, 0, 0, 0, 3, 4 ] ); + + a.setComponent( 0, 0, 1 ); + a.setComponent( 0, 1, 2 ); + a.setComponent( 1, 2, 3 ); + a.setComponent( 1, 3, 4 ); + + assert.deepEqual( a.array, expected, 'Check for the correct values' ); + + } ); + QUnit.test( 'onUpload', ( assert ) => { const a = new BufferAttribute(); diff --git a/test/unit/src/core/BufferGeometry.tests.js b/test/unit/src/core/BufferGeometry.tests.js index c9cb0a02195838..b37c714f140259 100644 --- a/test/unit/src/core/BufferGeometry.tests.js +++ b/test/unit/src/core/BufferGeometry.tests.js @@ -196,6 +196,76 @@ export default QUnit.module( 'Core', () => { } ); + QUnit.module( 'getSkinIndexBuffers', () => { + + QUnit.test( 'has no buffers', ( assert ) => { + + const geometry = new BufferGeometry(); + assert.deepEqual( geometry.getSkinIndexBuffers(), [] ); + + } ); + + QUnit.test( 'has one buffer', ( assert ) => { + + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( new Uint16Array( [] ), 4 ); + geometry.setAttribute( 'skinIndex', skinIndex ); + + assert.deepEqual( geometry.getSkinIndexBuffers(), [ skinIndex ] ); + + } ); + + QUnit.test( 'has multiple buffers', ( assert ) => { + + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + const skinIndex1 = new BufferAttribute( new Uint16Array( [ 4, 5, 6, 7 ] ), 4 ); + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinIndex1', skinIndex1 ); + + assert.deepEqual( geometry.getSkinIndexBuffers(), [ skinIndex, skinIndex1 ] ); + + } ); + + } ); + + QUnit.module( 'getSkinWeightBuffers', () => { + + QUnit.test( 'has no buffers', ( assert ) => { + + const geometry = new BufferGeometry(); + assert.deepEqual( geometry.getSkinWeightBuffers(), [] ); + + } ); + + QUnit.test( 'has one buffer', ( assert ) => { + + const geometry = new BufferGeometry(); + + const skinWeight = new BufferAttribute( new Float32Array( [] ), 4 ); + geometry.setAttribute( 'skinWeight', skinWeight ); + + assert.deepEqual( geometry.getSkinWeightBuffers(), [ skinWeight ] ); + + } ); + + QUnit.test( 'has multiple buffers', ( assert ) => { + + const geometry = new BufferGeometry(); + + const skinWeight = new BufferAttribute( new Float32Array( [ 0, 1, 2, 3 ] ), 4 ); + const skinWeight1 = new BufferAttribute( new Float32Array( [ 4, 5, 6, 7 ] ), 4 ); + geometry.setAttribute( 'skinWeight', skinWeight ); + geometry.setAttribute( 'skinWeight1', skinWeight1 ); + + assert.deepEqual( geometry.getSkinWeightBuffers(), [ skinWeight, skinWeight1 ] ); + + } ); + + } ); + QUnit.test( 'setIndex/getIndex', ( assert ) => { const a = new BufferGeometry(); @@ -463,14 +533,20 @@ export default QUnit.module( 'Core', () => { } ); const toHalfFloatArray = ( f32Array ) => { + const f16Array = new Uint16Array( f32Array.length ); - for ( let i = 0, n = f32Array.length; i < n; ++i ) { + for ( let i = 0, n = f32Array.length; i < n; ++ i ) { + f16Array[ i ] = toHalfFloat( f32Array[ i ] ); + } + return f16Array; + }; QUnit.test( 'computeBoundingBox - Float16', ( assert ) => { + const vertices = [ - 1, - 2, - 3, 13, - 2, - 3.5, - 1, - 20, 0, - 4, 5, 6 ]; const geometry = new BufferGeometry(); @@ -490,6 +566,7 @@ export default QUnit.module( 'Core', () => { } ); QUnit.test( 'computeBoundingSphere - Float16', ( assert ) => { + const vertices = [ - 10, 0, 0, 10, 0, 0 ]; const geometry = new BufferGeometry(); diff --git a/test/unit/src/core/InterleavedBufferAttribute.tests.js b/test/unit/src/core/InterleavedBufferAttribute.tests.js index 739d1c8ae9ae69..ce5aa99e89afaf 100644 --- a/test/unit/src/core/InterleavedBufferAttribute.tests.js +++ b/test/unit/src/core/InterleavedBufferAttribute.tests.js @@ -98,6 +98,41 @@ export default QUnit.module( 'Core', () => { } ); + QUnit.test( 'getComponent', ( assert ) => { + + const buffer = new InterleavedBuffer( new Float32Array( [ + 0, 1, 2, 3, 4, + 0, 5, 6, 7, 8 + ] ), 5 ); + const attribute = new InterleavedBufferAttribute( buffer, 4, 1, false ); + + assert.equal( attribute.getComponent( 0, 0 ), 1, 'v0.x was not retrieved' ); + assert.equal( attribute.getComponent( 0, 1 ), 2, 'v0.y was not retrieved' ); + assert.equal( attribute.getComponent( 1, 2 ), 7, 'v1.z was not retrieved' ); + assert.equal( attribute.getComponent( 1, 3 ), 8, 'v1.w was not retrieved' ); + + } ); + + QUnit.test( 'setComponent', ( assert ) => { + + const buffer = new InterleavedBuffer( new Float32Array( [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ] ), 5 ); + const attribute = new InterleavedBufferAttribute( buffer, 4, 1, false ); + + attribute.setComponent( 0, 0, 1 ); + attribute.setComponent( 0, 1, 2 ); + attribute.setComponent( 1, 2, 3 ); + attribute.setComponent( 1, 3, 4 ); + + assert.deepEqual( attribute.data.array, new Float32Array( [ + 0, 1, 2, 0, 0, + 0, 0, 0, 3, 4 + ] ), 'check for incorrect values' ); + + } ); + // setY, setZ and setW are calculated in the same way so not QUnit.testing this // TODO: ( you can't be sure that will be the case in future, or a mistake was introduce in one off them ! ) QUnit.test( 'setX', ( assert ) => { diff --git a/test/unit/src/objects/SkinnedMesh.tests.js b/test/unit/src/objects/SkinnedMesh.tests.js index 617c398e3cbae1..f1c18132669d9c 100644 --- a/test/unit/src/objects/SkinnedMesh.tests.js +++ b/test/unit/src/objects/SkinnedMesh.tests.js @@ -3,12 +3,325 @@ import { Object3D } from '../../../../src/core/Object3D.js'; import { Mesh } from '../../../../src/objects/Mesh.js'; import { SkinnedMesh } from '../../../../src/objects/SkinnedMesh.js'; -import { AttachedBindMode } from '../../../../src/constants.js'; +import { BufferGeometry } from '../../../../src/core/BufferGeometry.js'; +import { BufferAttribute } from '../../../../src/core/BufferAttribute.js'; +import { AttachedBindMode, BoneIndexWeightsTextureAllow, BoneIndexWeightsTextureAlways, BoneIndexWeightsTextureNever } from '../../../../src/constants.js'; + +QUnit.assert.arrayApproxEqual = function ( actual, expected, epsilon = 1e-14, message = '' ) { + + let result = true; + if ( actual.length !== expected.length ) { + + result = false; + message = 'The arrays had different sizes. ' + message; + + } else { + + for ( let ii = 0; ii < expected.length; ++ ii ) { + + const diff = actual[ ii ] - expected[ ii ]; + if ( Math.abs( actual[ ii ] - expected[ ii ] ) > epsilon ) { + + result = false; + message = `The arrays differed by more than ${epsilon} at index ${ii}\n` + + ` diff: ${diff}\n` + + ` actual: ${actual[ ii ]}\n` + + ` expected: ${expected[ ii ]}\n` + + message; + break; + + } + + } + + } + + this.pushResult( { + result: result, + actual: actual, + expected: expected, + message: message + } ); + +}; export default QUnit.module( 'Objects', () => { QUnit.module( 'SkinnedMesh', () => { + // CONSTRUCTOR + QUnit.module( 'constructor', () => { + + QUnit.test( 'normalizes 1 buffer of already-sorted float32 skin weights', ( assert ) => { + + // Given a sorted, non-normalized pair of bone index/weight buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 4, 3, 2, 1 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinWeight', skinWeight ); + + // When a SkinnedMesh is created + new SkinnedMesh( geometry, null ); + + // Then the buffers are normalized + assert.arrayApproxEqual( skinIndex.array, new Uint16Array( [ 0, 1, 2, 3 ] ) ); + assert.arrayApproxEqual( skinWeight.array, new Float32Array( [ 0.4, 0.3, 0.2, 0.1 ] ) ); + + } ); + + QUnit.test( 'sorts 1 buffer of already-normalized float32 skin weights', ( assert ) => { + + // Given an unsorted, normalized pair of bone index/weight buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 0.1, 0.2, 0.3, 0.4 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinWeight', skinWeight ); + + // When a SkinnedMesh is created + new SkinnedMesh( geometry, null ); + + // Then the buffers are sorted + assert.arrayApproxEqual( skinIndex.array, new Uint16Array( [ 3, 2, 1, 0 ] ) ); + assert.arrayApproxEqual( skinWeight.array, new Float32Array( [ 0.4, 0.3, 0.2, 0.1 ] ) ); + + } ); + + QUnit.test( 'sorts and normalizes multiple buffers of skin weights', ( assert ) => { + + // Given two pairs of bone index/weight buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + const skinIndex1 = new BufferAttribute( + new Uint16Array( [ 4, 5, 6, 7 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 1, 2, 3, 4 ] ), 4 ); + const skinWeight1 = new BufferAttribute( + new Float32Array( [ 10, 20, 29, 31 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinIndex1', skinIndex1 ); + geometry.setAttribute( 'skinWeight', skinWeight ); + geometry.setAttribute( 'skinWeight1', skinWeight1 ); + + // When a SkinnedMesh is created + new SkinnedMesh( geometry, null ); + + // Then weights are sorted and normalized across both pairs of buffers + assert.arrayApproxEqual( skinIndex.array, new Uint16Array( [ 7, 6, 5, 4 ] ) ); + assert.arrayApproxEqual( skinWeight.array, new Float32Array( [ 0.31, 0.29, 0.20, 0.10 ] ) ); + + assert.arrayApproxEqual( skinIndex1.array, new Uint16Array( [ 3, 2, 1, 0 ] ) ); + assert.arrayApproxEqual( skinWeight1.array, new Float32Array( [ 0.04, 0.03, 0.02, 0.01 ] ) ); + + } ); + + QUnit.test( 'sorts and normalizes only the biggest 4 weights when skin weight texture is disabled', ( assert ) => { + + // Given two pairs of bone index/weight buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + const skinIndex1 = new BufferAttribute( + new Uint16Array( [ 4, 5, 6, 7 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 10, 2, 3, 4 ] ), 4 ); + const skinWeight1 = new BufferAttribute( + new Float32Array( [ 1, 20, 30, 40 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinIndex1', skinIndex1 ); + geometry.setAttribute( 'skinWeight', skinWeight ); + geometry.setAttribute( 'skinWeight1', skinWeight1 ); + + // When a SkinnedMesh is created but bone textures are not used + new SkinnedMesh( geometry, null, { useBoneIndexWeightsTexture: BoneIndexWeightsTextureNever } ); + + // Then the highest 4 weights are sorted and normalized into the first + // pair of bone index/weight buffers + assert.arrayApproxEqual( skinIndex.array, new Uint16Array( [ 7, 6, 5, 0 ] ) ); + assert.arrayApproxEqual( skinWeight.array, new Float32Array( [ 0.40, 0.30, 0.20, 0.10 ] ) ); + + } ); + + QUnit.test( 'BoneIndexWeightsTextureAllow does not create a skin weight texture for <= 4 weights', ( assert ) => { + + // Given a pair of bone weight/index buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 1, 2, 3, 4 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinWeight', skinWeight ); + + // When a SkinnedMesh is created and bone weight textures are "allowed" + const mesh = new SkinnedMesh( geometry, null, { + useBoneIndexWeightsTexture: BoneIndexWeightsTextureAllow + } ); + + // Then a bone weight texture is not created + assert.equal( mesh.boneIndexWeightsTexture, null ); + assert.false( geometry.hasAttribute( 'bonePairTexStartIndex' ) ); + + } ); + + QUnit.test( 'BoneIndexWeightsTextureAllow creates a skin weight texture for > 4 weights', ( assert ) => { + + // Given a pair of bone weight/index buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + const skinIndex1 = new BufferAttribute( + new Uint16Array( [ 4, 5, 6, 7 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 1, 2, 3, 4 ] ), 4 ); + const skinWeight1 = new BufferAttribute( + new Float32Array( [ 1, 2, 3, 4 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinIndex1', skinIndex1 ); + geometry.setAttribute( 'skinWeight', skinWeight ); + geometry.setAttribute( 'skinWeight1', skinWeight1 ); + + // When a SkinnedMesh is created and bone weight textures are "allowed" + const mesh = new SkinnedMesh( geometry, null, { + useBoneIndexWeightsTexture: BoneIndexWeightsTextureAllow + } ); + + // Then a bone weight texture is created + assert.ok( mesh.boneIndexWeightsTexture ); + assert.true( geometry.hasAttribute( 'bonePairTexStartIndex' ) ); + + } ); + + QUnit.test( 'BoneIndexWeightsTextureNever does not creates a skin weight texture for > 4 weights', ( assert ) => { + + // Given a pair of bone weight/index buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + const skinIndex1 = new BufferAttribute( + new Uint16Array( [ 4, 5, 6, 7 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 1, 2, 3, 4 ] ), 4 ); + const skinWeight1 = new BufferAttribute( + new Float32Array( [ 1, 2, 3, 4 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinIndex1', skinIndex1 ); + geometry.setAttribute( 'skinWeight', skinWeight ); + geometry.setAttribute( 'skinWeight1', skinWeight1 ); + + // When a SkinnedMesh is created and bone weight textures are disabled + const mesh = new SkinnedMesh( geometry, null, { + useBoneIndexWeightsTexture: BoneIndexWeightsTextureNever + } ); + + // Then a bone weight texture is not created + assert.equal( mesh.boneIndexWeightsTexture, null ); + assert.false( geometry.hasAttribute( 'bonePairTexStartIndex' ) ); + + } ); + + QUnit.test( 'BoneIndexWeightsTextureAlways creates a skin weight texture for <= 4 weights', ( assert ) => { + + // Given a pair of bone weight/index buffers + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 1, 2, 3, 4 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinWeight', skinWeight ); + + // When a SkinnedMesh is created and bone weight textures are forced on + const mesh = new SkinnedMesh( geometry, null, { + useBoneIndexWeightsTexture: BoneIndexWeightsTextureAlways + } ); + + // Then a bone weight texture is created + assert.ok( mesh.boneIndexWeightsTexture ); + assert.true( geometry.hasAttribute( 'bonePairTexStartIndex' ) ); + + } ); + + QUnit.test( 'creates a bone index/weight texture for multiple vertices', ( assert ) => { + + // Given multiple bone weight/index buffers for multiple vertices + // where some of the weights are 0 + const geometry = new BufferGeometry(); + + const skinIndex = new BufferAttribute( + new Uint16Array( [ 0, 1, 2, 3, 0, 1, 2, 3 ] ), 4 ); + const skinIndex1 = new BufferAttribute( + new Uint16Array( [ 4, 5, 6, 7, 4, 5, 6, 7 ] ), 4 ); + + const skinWeight = new BufferAttribute( + new Float32Array( [ 0, 0, 30, 40, 4, 6, 0, 40 ] ), 4 ); + const skinWeight1 = new BufferAttribute( + new Float32Array( [ 10, 19, 1, 0, 0, 20, 30, 0 ] ), 4 ); + + geometry.setAttribute( 'skinIndex', skinIndex ); + geometry.setAttribute( 'skinIndex1', skinIndex1 ); + geometry.setAttribute( 'skinWeight', skinWeight ); + geometry.setAttribute( 'skinWeight1', skinWeight1 ); + + // When a SkinnedMesh is created + const mesh = new SkinnedMesh( geometry, null ); + + // Then the bone weight texture is as expected + assert.equal( mesh.boneIndexWeightsTexture.source.data.width, 4, 'incorrect width' ); + assert.equal( mesh.boneIndexWeightsTexture.source.data.height, 3, 'incorrect height' ); + + assert.arrayApproxEqual( mesh.boneIndexWeightsTexture.source.data.data, new Float32Array( [ + 3, 0.4, + 2, 0.3, + 5, 0.19, + 4, 0.1, + 6, 0.01, + - 1, - 1, + 3, 0.4, + 6, 0.3, + 5, 0.2, + 1, 0.06, + 0, 0.04, + - 1, - 1 + ] ) ); + + assert.true( geometry.hasAttribute( 'bonePairTexStartIndex' ) ); + assert.arrayApproxEqual( geometry.getAttribute( 'bonePairTexStartIndex' ).array, [ 0, 6 ] ); + + } ); + + } ); + // INHERITANCE QUnit.test( 'Extending', ( assert ) => { @@ -89,12 +402,6 @@ export default QUnit.module( 'Objects', () => { } ); - QUnit.todo( 'normalizeSkinWeights', ( assert ) => { - - assert.ok( false, 'everything\'s gonna be alright' ); - - } ); - QUnit.todo( 'updateMatrixWorld', ( assert ) => { assert.ok( false, 'everything\'s gonna be alright' );