Skip to content

Commit

Permalink
Merge pull request #41 from M3-org/support-scale-export-vrm
Browse files Browse the repository at this point in the history
Support scale export vrm
  • Loading branch information
madjin authored Oct 24, 2023
2 parents cc723b0 + be62fa1 commit 2946f38
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 41 deletions.
4 changes: 2 additions & 2 deletions src/components/ExportMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const ExportMenu = ({getFaceScreenshot}) => {
// Translate hook
const { t } = useContext(LanguageContext);
const [name] = React.useState(localStorage.getItem("name") || defaultName)
const { model, avatar } = useContext(SceneContext)
const { model, avatar,templateInfo } = useContext(SceneContext)

return (
<React.Fragment>
Expand Down Expand Up @@ -46,7 +46,7 @@ export const ExportMenu = ({getFaceScreenshot}) => {
className={styles.button}
onClick={() => {
const screenshot = getFaceScreenshot();
downloadVRM(model, avatar, name, screenshot, 4096, true)
downloadVRM(model, avatar, name, screenshot, 4096,templateInfo.exportScale||1, true, templateInfo.vrmMeta)
}}
/>
</React.Fragment>
Expand Down
33 changes: 17 additions & 16 deletions src/library/download-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,52 +61,53 @@ function getUnopotimizedGLB (avatarToDownload){

return unoptimizedGLB;
}
function getOptimizedGLB(avatarToDownload, atlasSize, isVrm0 = false){
function getOptimizedGLB(avatarToDownload, atlasSize, scale = 1, isVrm0 = false){
const avatarToDownloadClone = cloneAvatarModel(avatarToDownload)
return combine({
transparentColor: new Color(1,1,1),
avatar: avatarToDownloadClone,
atlasSize,
scale
}, isVrm0)
}

export async function getGLBBlobData(avatarToDownload, atlasSize = 4096, optimized = true){
export async function getGLBBlobData(avatarToDownload, atlasSize = 4096, optimized = true, scale = 1){
const model = await (optimized ?
getOptimizedGLB(avatarToDownload, atlasSize) :
getOptimizedGLB(avatarToDownload, atlasSize,scale) :
getUnopotimizedGLB(avatarToDownload))
const glb = await parseGLB(model);
return new Blob([glb], { type: 'model/gltf-binary' });
}

export async function getVRMBlobData(avatarToDownload, avatar, screenshot = null, atlasSize = 4096, isVrm0 = false){
const model = await getOptimizedGLB(avatarToDownload, atlasSize, isVrm0)
const vrm = await parseVRM(model, avatar, screenshot, isVrm0);
export async function getVRMBlobData(avatarToDownload, avatar, screenshot = null, atlasSize = 4096, scale = 1, isVrm0 = false, vrmMeta= null){
const model = await getOptimizedGLB(avatarToDownload, atlasSize,scale, isVrm0)
const vrm = await parseVRM(model, avatar, screenshot, isVrm0, vrmMeta);
// save it as glb now
return new Blob([vrm], { type: 'model/gltf-binary' });
}

// returns a promise with the parsed data
async function getGLBData(avatarToDownload, atlasSize = 4096, optimized = true){
async function getGLBData(avatarToDownload, atlasSize = 4096, optimized = true, scale = 1){
if (optimized){
const model = await getOptimizedGLB(avatarToDownload, atlasSize)
const model = await getOptimizedGLB(avatarToDownload, atlasSize,scale)
return parseGLB(model);
}
else{
const model = getUnopotimizedGLB(avatarToDownload)
return parseGLB(model);
}
}
async function getVRMData(avatarToDownload, avatar, screenshot = null, atlasSize = 4096, isVrm0 = false){
async function getVRMData(avatarToDownload, avatar, screenshot = null, atlasSize = 4096, scale = 1, isVrm0 = false, vrmMeta = null){

const vrmModel = await getOptimizedGLB(avatarToDownload, atlasSize, isVrm0);
return parseVRM(vrmModel,avatar,screenshot, isVrm0)
const vrmModel = await getOptimizedGLB(avatarToDownload, atlasSize, scale, isVrm0);
return parseVRM(vrmModel,avatar,screenshot, isVrm0, vrmMeta)
}

export async function downloadVRM(avatarToDownload, avatar, fileName = "", screenshot = null, atlasSize = 4096, isVrm0 = false){
export async function downloadVRM(avatarToDownload, avatar, fileName = "", screenshot = null, atlasSize = 4096, scale = 1, isVrm0 = false, vrmMeta = null){
const downloadFileName = `${
fileName && fileName !== "" ? fileName : "AvatarCreatorModel"
}`
getVRMData(avatarToDownload, avatar, screenshot, atlasSize, isVrm0).then((vrm)=>{
getVRMData(avatarToDownload, avatar, screenshot, atlasSize,scale, isVrm0, vrmMeta).then((vrm)=>{
saveArrayBuffer(vrm, `${downloadFileName}.vrm`)
})
}
Expand All @@ -116,7 +117,7 @@ export async function downloadGLB(avatarToDownload, optimized = true, fileName
}`

const model = optimized ?
await getOptimizedGLB(avatarToDownload, atlasSize):
await getOptimizedGLB(avatarToDownload, atlasSize, scale):
getUnopotimizedGLB(avatarToDownload)

parseGLB(model)
Expand Down Expand Up @@ -153,12 +154,12 @@ function parseGLB (glbModel){
})
}

function parseVRM (glbModel, avatar, screenshot = null, isVrm0 = false){
function parseVRM (glbModel, avatar, screenshot = null, isVrm0 = false, vrmMeta = null){
return new Promise((resolve) => {
const exporter = isVrm0 ? new VRMExporterv0() : new VRMExporter()
const vrmData = {
...getVRMBaseData(avatar),
...getAvatarData(glbModel, "CharacterCreator"),
...getAvatarData(glbModel, "CharacterCreator", vrmMeta),
}
let skinnedMesh;
glbModel.traverse(child => {
Expand Down
43 changes: 27 additions & 16 deletions src/library/merge-geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function cloneSkeleton(skinnedMesh) {
return newSkeleton;
}

function createMergedSkeleton(meshes){
function createMergedSkeleton(meshes, scale){
/* user should be careful with naming convetions in custom bone names out from humanoids vrm definition,
for example ones that come from head (to add hair movement), should start with vrm's connected bone
followed by the number of the bone in reference to the base bone (head > head_hair_00 > head_hair_01),
Expand Down Expand Up @@ -67,7 +67,7 @@ function createMergedSkeleton(meshes){
const boneData = {
index,
boneInverses:mesh.skeleton.boneInverses[boneInd],
bone:bone.clone(false),
bone: bone.clone(false),
parentName: bone.parent?.type == "Bone" ? bone.parent.name:null
}
index++
Expand All @@ -91,7 +91,11 @@ function createMergedSkeleton(meshes){
}
});
const newSkeleton = new THREE.Skeleton(finalBones,finalBoneInverses);
newSkeleton.pose()
newSkeleton.pose();

newSkeleton.bones.forEach(bn => {
bn.position.set(bn.position.x *scale, bn.position.y*scale,bn.position.z*scale);
});
return newSkeleton
}
function getUpdatedSkinIndex(newSkeleton, mesh){
Expand Down Expand Up @@ -160,17 +164,16 @@ function removeUnusedAttributes(attribute,arrayMatch){
return new BufferAttribute(typedArr,attribute.itemSize,attribute.normalized)
}

export async function combine({ transparentColor, avatar, atlasSize = 4096 }, isVrm0 = false) {
export async function combine({ transparentColor, avatar, atlasSize = 4096, scale = 1 }, isVrm0 = false) {
const { bakeObjects, textures, vrmMaterial } =
await createTextureAtlas({ transparentColor, atlasSize, meshes: findChildrenByType(avatar, "SkinnedMesh")});
// if (vrmMaterial != null)
// vrmMaterial.userData.textureProperties = {_MainTex:0, _ShadeTexture:0
const meshes = bakeObjects.map((bakeObject) => bakeObject.mesh);

const newSkeleton = createMergedSkeleton(meshes);
const newSkeleton = createMergedSkeleton(meshes, scale);

meshes.forEach((mesh) => {

const geometry = mesh.geometry;

const baseIndArr = geometry.index.array
Expand Down Expand Up @@ -214,7 +217,7 @@ export async function combine({ transparentColor, avatar, atlasSize = 4096 }, is
}
});

const { dest } = mergeGeometry({ meshes },isVrm0);
const { dest } = mergeGeometry({ meshes, scale },isVrm0);
const geometry = new THREE.BufferGeometry();

if (isVrm0){
Expand All @@ -228,6 +231,14 @@ export async function combine({ transparentColor, avatar, atlasSize = 4096 }, is
geometry.morphAttributes = dest.morphAttributes;
geometry.morphTargetsRelative = true;
geometry.setIndex(dest.index);

const vertices = geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
vertices[i] *= scale;
vertices[i + 1] *= scale;
vertices[i + 2] *= scale;
}

const material = new THREE.MeshStandardMaterial({
map: textures["diffuse"],
});
Expand Down Expand Up @@ -257,12 +268,6 @@ export async function combine({ transparentColor, avatar, atlasSize = 4096 }, is


mesh.bind(newSkeleton);
// clones.forEach((clone) => {
// clone.bind(skeleton);
// });
//console.log(newSkeleton)
//console.log(mesh.geometry.attributes.skinIndex.array)


const group = new THREE.Object3D();
group.name = "AvatarRoot";
Expand Down Expand Up @@ -328,7 +333,7 @@ function mergeSourceMorphTargetDictionaries({ sourceMorphTargetDictionaries }) {
});
return destMorphTargetDictionary;
}
function mergeSourceMorphAttributes({ meshes, sourceMorphTargetDictionaries, sourceMorphAttributes, destMorphTargetDictionary, }, isVrm0 = false) {
function mergeSourceMorphAttributes({ meshes, sourceMorphTargetDictionaries, sourceMorphAttributes, destMorphTargetDictionary, scale}, isVrm0 = false) {
const propertyNameSet = new Set(); // e.g. ["position", "normal"]
const allSourceMorphAttributes = Array.from(sourceMorphAttributes.values());
allSourceMorphAttributes.forEach((sourceMorphAttributes) => {
Expand Down Expand Up @@ -363,13 +368,18 @@ function mergeSourceMorphAttributes({ meshes, sourceMorphTargetDictionaries, sou
merged[propName] = [];
for (let i =0; i < Object.entries(destMorphTargetDictionary).length ; i++){
merged[propName][i] = BufferGeometryUtils.mergeBufferAttributes(unmerged[propName][i]);
const buffArr = merged[propName][i].array;
if (isVrm0){
const buffArr = merged[propName][i].array;
for (let j = 0; j < buffArr.length; j+=3){
buffArr[j] *= -1;
buffArr[j+2] *= -1;
}
}
for (let j = 0; j < buffArr.length; j+=3){
buffArr[j] *= scale;
buffArr[j+1] *= scale;
buffArr[j+2] *= scale;
}
}
});
return merged;
Expand Down Expand Up @@ -559,7 +569,7 @@ function mergeSourceIndices({ meshes }) {
// function remapAnimationClips({ animationClips, sourceMorphTargetDictionaries, meshes, destMorphTargetDictionary }) {
// return animationClips.map((clip) => new THREE.AnimationClip(clip.name, clip.duration, clip.tracks.map((track) => remapKeyframeTrack({ track, sourceMorphTargetDictionaries, meshes, destMorphTargetDictionary })), clip.blendMode));
// }
export function mergeGeometry({ meshes }, isVrm0 = false) {
export function mergeGeometry({ meshes, scale }, isVrm0 = false) {
// eslint-disable-next-line no-unused-vars
let uvcount = 0;
meshes.forEach(mesh => {
Expand Down Expand Up @@ -591,6 +601,7 @@ export function mergeGeometry({ meshes }, isVrm0 = false) {
sourceMorphAttributes: source.morphAttributes,
sourceMorphTargetDictionaries: source.morphTargetDictionaries,
destMorphTargetDictionary,
scale,
},isVrm0);
dest.morphTargetInfluences = mergeMorphTargetInfluences({
meshes,
Expand Down
18 changes: 11 additions & 7 deletions src/library/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const cullHiddenMeshes = (avatar) => {
CullHiddenFaces(models)
}

export async function getModelFromScene(avatarScene, format = 'glb', skinColor = new THREE.Color(1, 1, 1)) {
export async function getModelFromScene(avatarScene, format = 'glb', skinColor = new THREE.Color(1, 1, 1), scale = 1) {
if (format && format === 'glb') {
const exporter = new GLTFExporter();
const options = {
Expand All @@ -91,7 +91,7 @@ export async function getModelFromScene(avatarScene, format = 'glb', skinColor =
maxTextureSize: 1024 || Infinity
};

const avatar = await combine({ transparentColor: skinColor, avatar: avatarScene });
const avatar = await combine({ transparentColor: skinColor, avatar: avatarScene, scale:scale });

const glb = await new Promise((resolve) => exporter.parse(avatar, resolve, (error) => console.error("Error getting model", error), options));
return new Blob([glb], { type: 'model/gltf-binary' });
Expand Down Expand Up @@ -477,20 +477,22 @@ export function findChildrenByType(root, type) {
predicate: (o) => o.type === type,
});
}
export function getAvatarData (avatarModel, modelName){
export function getAvatarData (avatarModel, modelName, vrmMeta){
const skinnedMeshes = findChildrenByType(avatarModel, "SkinnedMesh")
return{
humanBones:getHumanoidByBoneNames(skinnedMeshes[0]),
materials : [avatarModel.userData.atlasMaterial],
meta : getVRMMeta(modelName)
meta : getVRMMeta(modelName, vrmMeta)
}

}


function getVRMMeta(name){
return {
authors:["Webaverse"],
function getVRMMeta(name, vrmMeta){
vrmMeta = vrmMeta||{}

const defaults = {
authors:["CharacterCreator"],
metaVersion:"1",
version:"v1",
name:name,
Expand All @@ -505,6 +507,8 @@ function getVRMMeta(name){
allowRedistribution:false,
modification:"prohibited"
}

return { ...defaults, ...vrmMeta };
}

// function getVRMDefaultLookAt(){
Expand Down

0 comments on commit 2946f38

Please sign in to comment.