Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for skinning >4 bones per vertex with a bone weight texture #26222

Open
wants to merge 29 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
34d7408
Add support for skinning >4 bones per vertex with a bone weight texture
cstegel May 26, 2023
fc03f04
Fix examples; unused and uninitialized variables
cstegel Jun 9, 2023
fb59366
Add screenshots for 2 new skinning examples
cstegel Jun 9, 2023
94b7331
Fix access to potentially null member in SkinnedMesh
cstegel Jun 9, 2023
9183fd0
Increase e2e-test render timeout and parse time to avoid flaky tests
cstegel Jun 9, 2023
b000985
Add unit tests for new (Interleaved)BufferAttribute methods
cstegel Jun 9, 2023
461adde
Add env map and tone mapping for new >4 bone skinning examples
cstegel Jun 13, 2023
6aab6e7
Add artist attribution for the 16 per-vertex bone-skinning model
cstegel Jun 13, 2023
0de2851
Add bone weight texture support to all SkinnedMesh loaders
cstegel Jun 13, 2023
d9c777d
Add CC4 license attribution for new head model
cstegel Jun 13, 2023
4aebe19
Add directional light back into >4 bone skinning example for better v…
cstegel Jun 13, 2023
6d8e7f1
Rename "many bone influences" example to "weight-texture"
cstegel Jun 13, 2023
6b15533
Update docs for bone weight texture skinning
cstegel Jun 14, 2023
d47fa40
Add unit tests for SkinnedMesh creation
cstegel Jun 14, 2023
aa98ea6
Add unit tests for new BufferGeometry methods
cstegel Jun 14, 2023
86d53a3
Remove comment in shader chunk
cstegel Jun 17, 2023
5d46ce1
Fix tab/space issue in shader code
cstegel Jun 17, 2023
f4cfced
Remove flaky webgl_animation_skinning_performance from screenshot tests
cstegel Jun 20, 2023
aa34feb
Empty commit to re-trigger CI because unit tests didn't run
cstegel Jun 20, 2023
7721604
Merge branch 'dev' into more-bones
cstegel Jul 26, 2023
4ff7701
Restore old visuals after useLegacyLights default changed in dev
cstegel Jul 26, 2023
50a6da9
Update svg_sandbox screenshot because e2e test is flaky
cstegel Jul 26, 2023
d66eab4
Merge branch 'dev' into more-bones
cstegel Aug 7, 2023
b5e206e
Merge branch 'dev' into more-bones
cstegel Sep 11, 2023
be0b73d
Merge branch 'dev' into more-bones
cstegel Oct 17, 2023
fb8a84e
Merge branch 'dev' into more-bones
mrdoob Nov 9, 2023
8ca2e48
Merge branch 'dev' into more-bones
cstegel Jan 22, 2024
d3aa1b4
Merge branch 'dev' into more-bones
cstegel Apr 3, 2024
607c88a
Convert spaces to tabs
cstegel Apr 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions examples/jsm/loaders/GLTFLoader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AnimationClip,
Bone,
BoneIndexWeightsTextureAllow,
Box3,
BufferAttribute,
BufferGeometry,
Expand Down Expand Up @@ -66,10 +67,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;
Expand Down Expand Up @@ -350,7 +354,8 @@ class GLTFLoader extends Loader {
requestHeader: this.requestHeader,
manager: this.manager,
ktx2Loader: this.ktx2Loader,
meshoptDecoder: this.meshoptDecoder
meshoptDecoder: this.meshoptDecoder,
useBoneIndexWeightsTexture: this.useBoneIndexWeightsTexture

} );

Expand Down Expand Up @@ -1819,15 +1824,15 @@ class GLTFDracoMeshCompressionExtension {

for ( const attributeName in gltfAttributeMap ) {

const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();
const threeAttributeName = getThreeAttributeName( attributeName );

threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ];

}

for ( const attributeName in primitive.attributes ) {

const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();
const threeAttributeName = getThreeAttributeName( attributeName );

if ( gltfAttributeMap[ attributeName ] !== undefined ) {

Expand Down Expand Up @@ -2106,8 +2111,6 @@ const ATTRIBUTES = {
TEXCOORD_2: 'uv2',
TEXCOORD_3: 'uv3',
COLOR_0: 'color',
WEIGHTS_0: 'skinWeight',
JOINTS_0: 'skinIndex',
};

const PATH_PROPERTIES = {
Expand All @@ -2130,6 +2133,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
*/
Expand Down Expand Up @@ -3654,8 +3679,15 @@ class GLTFParser {
if ( mesh.isSkinnedMesh === true ) {

// normalize skin weights to fix malformed assets (see #15319)
// normalization behavior changes if skin weights texture is used
mesh.useBoneIndexWeightsTexture = parser.options.useBoneIndexWeightsTexture;
mesh.normalizeSkinWeights();

// TODO: the loaders wouldn't have to call this method if the
// skinning buffers were normalized on/before SkinnedMesh
// construction.
mesh.createBoneIndexWeightsTexture();

}

if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {
Expand Down Expand Up @@ -4483,7 +4515,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;
Expand Down
Binary file added examples/models/gltf/HeadWithMax16Joints.glb
Binary file not shown.
225 changes: 225 additions & 0 deletions examples/webgl_animation_skinning_many_bone_influences.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - animation - skinning - many bone influences</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
a {
color: blue;
}
.control-inactive button {
color: #888;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Skeletal Animations with >4 Bone Influences Per Vertex<br/>
</div>

<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>

<script type="module">

import * as THREE from 'three';

import Stats from 'three/addons/libs/stats.module.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';

let scene, renderer, stats;
let skeleton, mixer, clock;
Fixed Show fixed Hide fixed

const views = [
{
left: 0,
bottom: 0,
width: 0,
height: 0,
offset: new THREE.Vector3( 0, 0, 0 ),
camera: undefined,
},
{
left: 0,
bottom: 0,
width: 0,
height: 0,
offset: new THREE.Vector3( 2, 0, 0 ),
camera: undefined,
}
];


init();

function init() {

const container = document.getElementById( 'container' );
clock = new THREE.Clock();

renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
container.appendChild( renderer.domElement );

scene = new THREE.Scene();
scene.background = new THREE.Color( 0xa0a0a0 );
scene.fog = new THREE.Fog( 0xa0a0a0, 10, 50 );

const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x8d8d8d );
hemiLight.position.set( 0, 20, 0 );
scene.add( hemiLight );

const dirLight = new THREE.DirectionalLight( 0xffffff );
dirLight.position.set( 30, 10, 10 );
dirLight.castShadow = true;
dirLight.shadow.camera.top = 2;
dirLight.shadow.camera.bottom = - 2;
dirLight.shadow.camera.left = - 2;
dirLight.shadow.camera.right = 2;
dirLight.shadow.camera.near = 0.1;
dirLight.shadow.camera.far = 40;
scene.add( dirLight );

// ground

const mesh = new THREE.Mesh(
new THREE.PlaneGeometry( 100, 100 ),
new THREE.MeshPhongMaterial( { color: 0xcbcbcb, depthWrite: true } ) );

mesh.rotation.x = - Math.PI / 2;
mesh.receiveShadow = true;
scene.add( mesh );

views[ 0 ].camera = new THREE.PerspectiveCamera( 45, window.innerWidth / 2 / window.innerHeight, 0.1, 100 );
views[ 0 ].camera.position.set( 0, 1, 1 );

views[ 1 ].camera = new THREE.PerspectiveCamera( 45, window.innerWidth / 2 / window.innerHeight, 0.1, 100 );
views[ 1 ].camera.position.copy( views[ 0 ].camera.position ).add( views[ 1 ].offset );

const controls = new OrbitControls( views[ 0 ].camera, renderer.domElement );
controls.target.set( 0, 1, 0 );
controls.update();

const controls2 = new OrbitControls( views[ 1 ].camera, renderer.domElement );
controls2.target.set( 0, 1, 0 ).add( views[ 1 ].offset );
controls2.update();


stats = new Stats();
container.appendChild( stats.dom );

onWindowResize();
window.addEventListener( 'resize', onWindowResize );

// Box

const loader = new GLTFLoader();
loader.load( 'models/gltf/HeadWithMax16Joints.glb', function ( gltf ) {

const unlimitedBonesModel = gltf.scene;

unlimitedBonesModel.traverse( ( object ) => {

if ( object.isMesh ) object.castShadow = true;

} );

scene.add( unlimitedBonesModel );

const limitedBonesModel = SkeletonUtils.clone( unlimitedBonesModel );

limitedBonesModel.traverse( ( object ) => {

if ( object.isSkinnedMesh ) {

// don't share the material with the "unlimited" bones model
// otherwise the renderer doesn't know to use different shaders
// for each of the models
object.material = object.material.clone();

// need to re-normalize the skinning buffer to 4 weights / vertex
object.useBoneIndexWeightsTexture = THREE.BoneIndexWeightsTextureNever;
object.normalizeSkinWeights();
object.material.needsUpdate = true;

}

} );

limitedBonesModel.position.set( 0, 0.8, 0 );
unlimitedBonesModel.position.copy( limitedBonesModel.position ).add( views[ 1 ].offset );
scene.add( limitedBonesModel );

mixer = new THREE.AnimationMixer( scene );
const animations = gltf.animations;
mixer.clipAction( animations[ 0 ], limitedBonesModel ).play();
mixer.clipAction( animations[ 0 ], unlimitedBonesModel ).play();

animate();

} );

}

function onWindowResize() {

for ( const view of views ) {

view.width = window.innerWidth / 2;
view.height = window.innerHeight;
view.camera.aspect = view.width / view.height;
view.camera.updateProjectionMatrix();

}

views[ 1 ].left = views[ 0 ].width;
renderer.setSize( window.innerWidth, window.innerHeight );

}

function animate() {

// Render loop

requestAnimationFrame( animate );

const mixerUpdateDelta = clock.getDelta();

mixer.update( mixerUpdateDelta );

stats.update();

for ( const view of views ) {

renderer.setViewport( view.left, view.bottom, view.width, view.height );
renderer.setScissor( view.left, view.bottom, view.width, view.height );
renderer.setScissorTest( true );
renderer.setClearColor( 'white' );

renderer.render( scene, view.camera );

}

}

</script>

</body>
</html>
Loading