+
+ 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.
+
[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].
[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:
+
+
useBoneIndexWeightsTexture — (optional) Controls whether or not a [page:SkinnedMesh SkinnedMesh] will create and use a bone weights texture instead of a vertex buffer. Defaults to [page:Loaders THREE.BoneIndexWeightsTextureAllow].
+
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 @@
[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:
+
+
useBoneIndexWeightsTexture — (optional) Controls whether or not a [page:SkinnedMesh SkinnedMesh] will create and use a bone weights texture instead of a vertex buffer. Defaults to [page:Loaders THREE.BoneIndexWeightsTextureAllow].
+
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
+ - 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)
+
+ 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' );