diff --git a/dist/preview release/babylon.d.ts b/dist/preview release/babylon.d.ts index 6d905002ff2..d253c0a1b2b 100644 --- a/dist/preview release/babylon.d.ts +++ b/dist/preview release/babylon.d.ts @@ -25861,9 +25861,19 @@ declare module BABYLON { render(subMesh: SubMesh, enableAlphaMode: boolean): Mesh; private _onBeforeDraw; /** - * Normalize matrix weights so that all vertices have a total weight set to 1 + * Renormalize the mesh and patch it up if there are no weights + * Similar to normalization by adding the weights comptue the reciprical and multiply all elements. this wil ensure that everything adds to 1. + * However in the case of 0 weights then we set just a single influence to 1. + * We check in the function for extra's present and if so we use the normalizeSkinWeightsWithExtras rather than the FourWeights version. */ - cleanMatrixWeights(): void; + public cleanMatrixWeights(): void; + /** + * ValidateSkinning is used to determin that a mesh has valid skinning data along with skin metrics, if missing weights, + * or not normalized it is returned as invalid mesh the string can be used for console logs, or on screen messages to let + * the user know there was an issue with importing the mesh + * @returns a validation object with skinned, valid and report string + */ + public validateSkinning() : {skinned:boolean, valid:boolean, report:string}; /** @hidden */ _checkDelayState(): Mesh; private _queueLoad; @@ -31095,7 +31105,7 @@ declare module BABYLON { /** * The SPS is a single updatable mesh. The solid particles are simply separate parts or faces fo this big mesh. *As it is just a mesh, the SPS has all the same properties than any other BJS mesh : not more, not less. It can be scaled, rotated, translated, enlighted, textured, moved, etc. - + * The SPS is also a particle system. It provides some methods to manage the particles. * However it is behavior agnostic. This means it has no emitter, no particle physics, no particle recycler. You have to implement your own behavior. * diff --git a/dist/preview release/what's new.md b/dist/preview release/what's new.md index 6358c11af11..977729d7b6b 100644 --- a/dist/preview release/what's new.md +++ b/dist/preview release/what's new.md @@ -128,6 +128,8 @@ - Added FXAA and MSAA support to the StandardRenderingPipeline ([julien-moreau](https://github.com/julien-moreau)) - Make teleportCamera public in VR experience helper ([TrevorDev](https://github.com/TrevorDev)) - Added optional alphaFilter parameter to ```CreateGroundFromHeightMap``` to allow for heightmaps to be created that ignore any transparent data ([Postman-nz](https://github.com/Postman-nz)) +- Fixed renormalization of mesh weights to in cleanMatrixWeights function. ([Bolloxim](https://github.com/Bolloxim)) +- Added a validationSkin function to report out any errors on skinned meshes. ([Bolloxim](https://github.com/Bolloxim)) ### glTF Loader @@ -140,6 +142,7 @@ - Added support for MSFT_audio_emitter ([najadojo](http://www.github.com/najadojo)) - Added support for custom loader extensions ([bghgary](http://www.github.com/bghgary)) - Added support for validating assets using [glTF-Validator](https://github.com/KhronosGroup/glTF-Validator) ([bghgary](http://www.github.com/bghgary)) +- Added automatically renormalizes skinweights when loading geometry. Calls core mesh functions to do this ([Bolloxim](https://github.com/Bolloxim)) ### glTF Serializer - Added support for exporting the scale, rotation and offset texture properties ([kcoley](http://www.github.com/kcoley)) diff --git a/src/Mesh/babylon.mesh.ts b/src/Mesh/babylon.mesh.ts index 5fe6f693792..30e6ab23fae 100644 --- a/src/Mesh/babylon.mesh.ts +++ b/src/Mesh/babylon.mesh.ts @@ -1621,80 +1621,164 @@ } /** - * Normalize matrix weights so that all vertices have a total weight set to 1 + * Renormalize the mesh and patch it up if there are no weights + * Similar to normalization by adding the weights compute the reciprocal and multiply all elements, this wil ensure that everything adds to 1. + * However in the case of zero weights then we set just a single influence to 1. + * We check in the function for extra's present and if so we use the normalizeSkinWeightsWithExtras rather than the FourWeights version. */ public cleanMatrixWeights(): void { - const epsilon: number = 1e-3; - let noInfluenceBoneIndex = 0.0; - if (this.skeleton) { - noInfluenceBoneIndex = this.skeleton.bones.length; - } else { - return; - } + if (this.isVerticesDataPresent(VertexBuffer.MatricesWeightsKind)) { + if (this.isVerticesDataPresent(VertexBuffer.MatricesWeightsExtraKind)) { + this.normalizeSkinWeightsAndExtra(); + } + else { + this.normalizeSkinFourWeights(); + } + } + } + + // faster 4 weight version. + private normalizeSkinFourWeights(): void { - let matricesIndices = (this.getVerticesData(VertexBuffer.MatricesIndicesKind)); - let matricesIndicesExtra = (this.getVerticesData(VertexBuffer.MatricesIndicesExtraKind)); let matricesWeights = (this.getVerticesData(VertexBuffer.MatricesWeightsKind)); - let matricesWeightsExtra = (this.getVerticesData(VertexBuffer.MatricesWeightsExtraKind)); - let influencers = this.numBoneInfluencers; - let size = matricesWeights.length; - - for (var i = 0; i < size; i += 4) { - let weight = 0.0; - let firstZeroWeight = -1; - for (var j = 0; j < 4; j++) { - let w = matricesWeights[i + j]; - weight += w; - if (w < epsilon && firstZeroWeight < 0) { - firstZeroWeight = j; - } + let numWeights = matricesWeights.length; + + for (var a = 0; a < numWeights; a += 4) { + // accumulate weights + var t = matricesWeights[a] + matricesWeights[a+1] +matricesWeights[a+2] +matricesWeights[a+3]; + // check for invalid weight and just set it to 1. + if (t === 0) matricesWeights[a] = 1; + else{ + // renormalize so everything adds to 1 use reciprical + let recip = 1 / t; + matricesWeights[a] *= recip; + matricesWeights[a+1] *= recip; + matricesWeights[a+2] *= recip; + matricesWeights[a+3] *= recip; } - if (matricesWeightsExtra) { - for (var j = 0; j < 4; j++) { - let w = matricesWeightsExtra[i + j]; - weight += w; - if (w < epsilon && firstZeroWeight < 0) { - firstZeroWeight = j + 4; - } - } + + } + this.setVerticesData(VertexBuffer.MatricesWeightsKind, matricesWeights); + } + // handle special case of extra verts. (in theory gltf can handle 12 influences) + private normalizeSkinWeightsAndExtra(): void { + + let matricesWeightsExtra = (this.getVerticesData(VertexBuffer.MatricesWeightsExtraKind)); + let matricesWeights = (this.getVerticesData(VertexBuffer.MatricesWeightsKind)); + let numWeights = matricesWeights.length; + + for (var a = 0; a < numWeights; a += 4){ + // accumulate weights + var t = matricesWeights[a] + matricesWeights[a+1] +matricesWeights[a+2] +matricesWeights[a+3]; + t += matricesWeightsExtra[a] + matricesWeightsExtra[a+1] +matricesWeightsExtra[a+2] +matricesWeightsExtra[a+3]; + // check for invalid weight and just set it to 1. + if (t === 0) matricesWeights[a] = 1; + else { + // renormalize so everything adds to 1 use reciprical + let recip = 1 / t; + matricesWeights[a] *= recip; + matricesWeights[a+1] *= recip; + matricesWeights[a+2] *= recip; + matricesWeights[a+3] *= recip; + // same goes for extras + matricesWeightsExtra[a] *= recip; + matricesWeightsExtra[a+1] *= recip; + matricesWeightsExtra[a+2] *= recip; + matricesWeightsExtra[a+3] *= recip; } - if (firstZeroWeight < 0 || firstZeroWeight > (influencers - 1)) { - firstZeroWeight = influencers - 1; + + } + this.setVerticesData(VertexBuffer.MatricesWeightsKind, matricesWeights); + this.setVerticesData(VertexBuffer.MatricesWeightsKind, matricesWeightsExtra); + } + + /** + * ValidateSkinning is used to determine that a mesh has valid skinning data along with skin metrics, if missing weights, + * or not normalized it is returned as invalid mesh the string can be used for console logs, or on screen messages to let + * the user know there was an issue with importing the mesh + * @returns a validation object with skinned, valid and report string + */ + public validateSkinning() : {skinned:boolean, valid:boolean, report:string} { + + let matricesWeightsExtra = (this.getVerticesData(VertexBuffer.MatricesWeightsExtraKind)); + let matricesWeights = (this.getVerticesData(VertexBuffer.MatricesWeightsKind)); + if (matricesWeights === null || this.skeleton == null) { + return {skinned:false, valid: true, report:"not skinned"} + } + + let numWeights = matricesWeights.length; + let numberNotSorted : number = 0; + let missingWeights : number = 0; + let maxUsedWeights : number = 0; + let numberNotNormalized :number = 0; + let numInfluences : number = matricesWeightsExtra === null ? 4 : 8; + var usedWeightCounts = new Array(); + for (var a = 0; a <= numInfluences; a++) { + usedWeightCounts[a] = 0; + } + const toleranceEpsilon : number = 0.001; + + for (var a = 0; a < numWeights; a += 4) { + + let lastWeight : number = matricesWeights[a]; + var t = lastWeight; + let usedWeights : number = t===0 ? 0 : 1; + + for (var b = 1; b < numInfluences; b++) { + var d = b < 4 ? matricesWeights[a + b] : matricesWeightsExtra[a + b-4]; + if (d > lastWeight) numberNotSorted++; + if (d !== 0) usedWeights++; + t += d; + lastWeight = d; } - if (weight > epsilon) { - let mweight = 1.0 / weight; - for (var j = 0; j < 4; j++) { - matricesWeights[i + j] *= mweight; - } - if (matricesWeightsExtra) { - for (var j = 0; j < 4; j++) { - matricesWeightsExtra[i + j] *= mweight; - } - } - } else { - if (firstZeroWeight >= 4) { - matricesWeightsExtra[i + firstZeroWeight - 4] = 1.0 - weight; - matricesIndicesExtra[i + firstZeroWeight - 4] = noInfluenceBoneIndex; - } else { - matricesWeights[i + firstZeroWeight] = 1.0 - weight; - matricesIndices[i + firstZeroWeight] = noInfluenceBoneIndex; + // count the buffer weights usage + usedWeightCounts[usedWeights]++; + + // max influences + if (usedWeights > maxUsedWeights) maxUsedWeights = usedWeights; + + // check for invalid weight and just set it to 1. + if (t === 0) { + missingWeights++; + } + else { + // renormalize so everything adds to 1 use reciprical + let recip = 1 / t; + let tolerance = 0; + for (b = 0; b < numInfluences; b++) { + if (b < 4) + tolerance += Math.abs(matricesWeights[a + b] - (matricesWeights[a + b] * recip)); + else + tolerance += Math.abs(matricesWeightsExtra[a + b-4] - (matricesWeightsExtra[a + b-4] * recip)); } - } + // arbitary epsilon value for dicdating not normalized + if (tolerance > toleranceEpsilon) numberNotNormalized++; + } } - this.setVerticesData(VertexBuffer.MatricesIndicesKind, matricesIndices); - if (matricesIndicesExtra) { - this.setVerticesData(VertexBuffer.MatricesIndicesExtraKind, matricesIndicesExtra); + // validate bone indices are in range of the skeleton + let numBones:number = this.skeleton.bones.length; + let matricesIndices = (this.getVerticesData(VertexBuffer.MatricesIndicesKind)); + let matricesIndicesExtra = (this.getVerticesData(VertexBuffer.MatricesIndicesExtraKind)); + let numBadBoneIndices : number = 0; + for (var a = 0; a < numWeights; a++) { + for (var b = 0; b < numInfluences; b++) { + let index = b < 4 ? matricesIndices[b] : matricesIndicesExtra[b-4]; + if (index >= numBones || index < 0) numBadBoneIndices++; + } } - this.setVerticesData(VertexBuffer.MatricesWeightsKind, matricesWeights); - if (matricesWeightsExtra) { - this.setVerticesData(VertexBuffer.MatricesWeightsExtraKind, matricesWeightsExtra); - } + + // log mesh stats + var output = "Number of Weights = " + numWeights/4 + "\nMaximum influences = " + maxUsedWeights + + "\nMissing Weights = " + missingWeights + "\nNot Sorted = " + numberNotSorted + + "\nNot Normalized = " + numberNotNormalized + "\nWeightCounts = [" + usedWeightCounts + "]" + + "\nNumber of bones = " + numBones + "\nBad Bone Indices = " + numBadBoneIndices ; + + return {skinned:true, valid: missingWeights===0 && numberNotNormalized===0 && numBadBoneIndices===0, report: output}; } - /** @hidden */ public _checkDelayState(): Mesh { var scene = this.getScene();