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

Support scale export vrm #41

Merged
merged 6 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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