Skip to content

Commit

Permalink
Merge pull request #171 from Benjythebee/fix/spring-bones-on-VRM0
Browse files Browse the repository at this point in the history
fix VRM0 spring bones, works great!
  • Loading branch information
memelotsqui authored Nov 19, 2024
2 parents f762d46 + 6c9388c commit dce68ba
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 70 deletions.
15 changes: 10 additions & 5 deletions src/components/ExportMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export const ExportMenu = () => {
const currentOption = local["mergeOptions_sel_option"] || 0;
const createTextureAtlas = local["mergeOptions_create_atlas"] == null ? true:local["mergeOptions_create_atlas"]
return {
// isVrm0 : true,
createTextureAtlas : createTextureAtlas,
mToonAtlasSize:getAtlasSize(local["mergeOptions_atlas_mtoon_size"] || 6),
mToonAtlasSizeTransp:getAtlasSize(local["mergeOptions_atlas_mtoon_transp_size"] || 6),
Expand All @@ -33,10 +32,16 @@ export const ExportMenu = () => {
}
}

const downloadVRM = () =>{
const downloadVRM = (version) =>{
const options = getOptions();
/**
* Blindly assume the whole avatar is VRM0 if the first vrm is VRM0
*/
options.isVrm0 = Object.values(characterManager.avatar)[0].vrm.meta.metaVersion=='0'
options.outputVRM0 = !(version === 1)
characterManager.downloadVRM(name, options);
}

const downloadGLB = () =>{
const options = getOptions();
characterManager.downloadGLB(name, options);
Expand All @@ -54,13 +59,13 @@ export const ExportMenu = () => {
downloadGLB()
}}
/>
<CustomButton
<CustomButton
theme="light"
text="VRM"
text="VRM 0"
icon="download"
size={14}
className={styles.button}
onClick={downloadVRM}
onClick={()=>downloadVRM(0)}
/>
</React.Fragment>
)
Expand Down
92 changes: 60 additions & 32 deletions src/library/VRMExporterv0.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,26 @@ function getVRM0BoneName(name) {
return name;
}
export default class VRMExporterv0 {
async parse(vrm, avatar, screenshot, rootSpringBones, isktx2, scale, onDone) {
/**
*
* @param {*} vrm
* @param {*} avatar
* @param {Object} screenshot
* @param {{
* bones:{
* name:string,
* bone:Object3D,
* }[]
* settings:VRMSpringBoneJointSettings,
* center:any,
* colliderGroups:VRMSpringBoneColliderGroup[],
* name:string
* }[] } springBoneGroups
* @param {boolean} isktx2
* @param {number} scale
* @param {*} onDone
*/
async parse(vrm, avatar, screenshot, springBoneGroups, isktx2, scale, onDone) {
const vrmMeta = convertMetaToVRM0(vrm.meta);
const humanoid = convertHumanoidToVRM0(vrm.humanoid);
const materials = vrm.materials;
Expand Down Expand Up @@ -546,17 +565,21 @@ export default class VRMExporterv0 {
});
//const outputVrmMeta = ToOutputVRMMeta(vrmMeta, icon, outputImages);
const outputVrmMeta = vrmMeta;
const rootSpringBonesIndexes = [];
rootSpringBones.forEach(rootSpringBone => {

const rootGroupSpringBonesIndexes = {}

springBoneGroups.forEach(group => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.name === rootSpringBone.name) {
rootSpringBonesIndexes.push(i);
if(!rootGroupSpringBonesIndexes[group.name]){
rootGroupSpringBonesIndexes[group.name] = []
}
if (node.name === group.bones.name) {
rootGroupSpringBonesIndexes[group.name].push(i);
break;
}
}
})

// should be fetched from rootSpringBonesIndexes instead
const colliderGroups = [];

Expand Down Expand Up @@ -596,16 +619,31 @@ export default class VRMExporterv0 {
}

const boneGroups = [];
rootSpringBones.forEach(springBone => {
//const boneIndices = findBoneIndices(springBone.name);
const boneIndex = findBoneIndex(springBone.name)
if (boneIndex === -1) {
console.warn("Spring bone " + springBone.name + " was removed during cleanup process. Skipping.");
return; // Skip to the next iteration
springBoneGroups.forEach(boneGroup => {
const settings = boneGroup.settings
const group = {
bones: [],
center: -1,
colliderGroups: [],
dragForce: settings.dragForce,
gravityDir: { x: settings.gravityDir.x, y: settings.gravityDir.y, z: settings.gravityDir.z },
gravityPower: settings.gravityPower,
hitRadius: settings.hitRadius,
stiffiness: settings.stiffness // for some reason specs mark as stiffiness, but loads it as stiffness
}
for(const bone of boneGroup.bones){
//const boneIndices = findBoneIndices(springBone.name);
const boneIndex = findBoneIndex(bone.name)
if (boneIndex === -1) {
console.warn("Spring bone " + bone.name + " was removed during cleanup process. Skipping.");
return; // Skip to the next iteration
}
// FIX!!
group.bones.push(boneIndex)
}
// get the collider group indices
const colliderIndices = [];
springBone.colliderGroups.forEach(colliderGroup => {
boneGroup.colliderGroups.forEach(colliderGroup => {
const springCollider = colliderGroup.colliders[0];
// sometimes there are no colliders defined in collidersGroup
if (springCollider != null) {
Expand All @@ -616,33 +654,23 @@ export default class VRMExporterv0 {
colliderIndices.push(ind);
}
else {
if (debug) console.warn("No collider group for bone name: ", springParent.name + " was found");
console.warn("No collider group for bone name: ", springParent.name + " was found");
}
}
else {
if(debug) console.log("No colliders definition were present in vrm file file for: ", springBone.name + " spring bones")
console.warn("No colliders definition were present in vrm file file for: ", boneGroup.name + " spring bones")
}
});
group.colliderGroups.push(...colliderIndices);
group.center = findBoneIndex(boneGroup.center?.name)


let centerIndex = findBoneIndex(springBone.center?.name);
if (centerIndex == -1) console.warn("no center bone for spring bone " + springBone.name);
// springBone: bone:boneObject, center:boneObject, string:name, array:colliderGroup, settings:object,
const settings = springBone.settings;

// FIX!!
if (group.center == -1) {
// it's ok if there is no center bone; it's optional
console.debug("no center bone for spring bone " + boneGroup.name);
}

boneGroups.push(
{
bones: [boneIndex],
center: centerIndex,
colliderGroups: colliderIndices,
dragForce: settings.dragForce,
gravityDir: { x: settings.gravityDir.x, y: settings.gravityDir.y, z: settings.gravityDir.z },
gravityPower: settings.gravityPower,
hitRadius: settings.hitRadius,
stiffiness: settings.stiffness // for some reason specs mark as stiffiness, but loads it as stiffness
}
group
);
});

Expand Down
116 changes: 104 additions & 12 deletions src/library/characterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"
import { AnimationManager } from "./animationManager"
import { ScreenshotManager } from "./screenshotManager";
import { BlinkManager } from "./blinkManager";
import { VRMLoaderPlugin } from "@pixiv/three-vrm";

import { VRMLoaderPlugin, VRMSpringBoneCollider } from "@pixiv/three-vrm";
import { getAsArray, disposeVRM, renameVRMBones, addModelData } from "./utils";
import { downloadGLB, downloadVRMWithAvatar } from "../library/download-utils"
import { saveVRMCollidersToUserData, renameMorphTargets} from "./load-utils";
import { getNodesWithColliders, saveVRMCollidersToUserData, renameMorphTargets} from "./load-utils";
import { cullHiddenMeshes, setTextureToChildMeshes, addChildAtFirst } from "./utils";
import { LipSync } from "./lipsync";
import { LookAtManager } from "./lookatManager";
Expand Down Expand Up @@ -70,10 +71,32 @@ export class CharacterManager {

}

update(){
/**
* toggle whether the spring bone animations are paused or not; this is useful when taking screenshots or calculating bone offsets
* @param x true to pause, false to unpause
*/
togglePauseSpringBoneAnimation(x){
for(const [_,trait] of Object.entries(this.avatar)){
if(trait.vrm.springBoneManager){
trait.vrm.springBoneManager.paused =x
}
}
}

update(deltaTime){
if (this.lookAtManager != null){
this.lookAtManager.update();
}
if(this.avatar){
for (const prop in this.avatar){
if (this.avatar[prop]?.vrm != null){
if(this.avatar[prop].vrm.springBoneManager?.paused){
return
}
this.avatar[prop].vrm.springBoneManager?.update(deltaTime);
}
}
}
}

addLookAtMouse(screenPrecentage, canvasID, camera, enable = true){
Expand Down Expand Up @@ -230,7 +253,6 @@ export class CharacterManager {
exportOptions = exportOptions || {};
const manifestOptions = this.manifestData.getExportOptions();
const finalOptions = { ...manifestOptions, ...exportOptions };
finalOptions.isVrm0 = true; // currently vrm1 not supported
finalOptions.screenshot = this._getPortaitScreenshotTexture(false, finalOptions);

// Log the final export options
Expand Down Expand Up @@ -1100,8 +1122,12 @@ export class CharacterManager {

addModelData(vrm, {isVRM0:vrm.meta?.metaVersion === '0'})

if (this.manifestData.isColliderRequired(traitID))
if (this.manifestData.isColliderRequired(traitID)){
saveVRMCollidersToUserData(m);
}

// apply colliders to the spring manager
this._applySpringBoneColliders(vrm);

renameVRMBones(vrm);

Expand Down Expand Up @@ -1151,10 +1177,79 @@ export class CharacterManager {
}

/**
*
* @param {import("@pixiv/three-vrm").VRM} vrm
* @returns
* Naive Method that will apply all colliders to all spring bones;
* @param {import('@pixiv/three-vrm').VRM} vrm
*/
_applySpringBoneColliders(vrm){

/**
* method to add collider groups to the joints of the new VRM
* @param {import('@pixiv/three-vrm').VRMSpringBoneColliderGroup[]} colliderGroups
*/
function addToJoints(colliderGroups){
vrm.springBoneManager.joints.forEach((joint)=>{
for(const group of colliderGroups){
const isSameName = joint.colliderGroups.find((cg) => cg.name === group.name)
if(isSameName){
return;
}
if (joint.colliderGroups.indexOf(group) === -1){
joint.colliderGroups.push(group)
}
}
})

}

// Iterate through the avatar record;
Object.entries(this.avatar).map(([key, entry]) => {

// Step 1: Check if other trait have colliders; if they do, just copy them over to new trait
const colliderGroups = []
// first check if the collider group already exists in vrm.springBoneManager
if(entry.vrm.springBoneManager?.colliderGroups.length){
colliderGroups.push(...entry.vrm.springBoneManager.colliderGroups)
}

if(colliderGroups.length){
addToJoints(colliderGroups)
// done
return
}

// Step 2: If no colliders found, check for saved colliders in the userData
// This is useful for VRMs that have colliders but no spring bones

// get nodes with colliders
const nodes = getNodesWithColliders(entry.vrm);
if(nodes.length == 0) return

// For each node with colliders info
nodes.forEach((node) => {
if(!vrm.springBoneManager){
return
}

const colliderGroup = {
colliders: [],
name: node.name
}
// Only direct children
for(const child of node.children){
if (child instanceof VRMSpringBoneCollider){
if(colliderGroup.colliders.indexOf(child) === -1){
colliderGroup.colliders.push(child);
}
}
}

if(colliderGroup.colliders.length){
addToJoints([colliderGroup])
}
})

})
}
_unregisterMorphTargetsFromManifest(vrm){
const manifestBlendShapes = this.manifestData.getAllBlendShapeTraits()
const expressions = vrm.expressionManager?.expressions
Expand Down Expand Up @@ -1340,7 +1435,7 @@ export class CharacterManager {
// just dispose for now
this._disposeTrait(this.avatar[traitGroupID].vrm)

this.avatar[traitGroupID] = {}
delete this.avatar[traitGroupID]
// XXX restore effects without setTimeout
}
return;
Expand Down Expand Up @@ -1398,9 +1493,6 @@ class TraitLoadingManager{
const gltfLoader = new GLTFLoader(loadingManager);
gltfLoader.crossOrigin = 'anonymous';
gltfLoader.register((parser) => {
// return new VRMLoaderPlugin(parser, {autoUpdateHumanBones: true, helperRoot:vrmHelperRoot})
// const springBoneLoader = new VRMSpringBoneLoaderPlugin(parser);
// return new VRMLoaderPlugin(parser, {autoUpdateHumanBones: true, springBonePlugin:springBoneLoader})
return new VRMLoaderPlugin(parser, {autoUpdateHumanBones: true})
})

Expand Down
Loading

0 comments on commit dce68ba

Please sign in to comment.