diff --git a/src/components/ExportMenu.jsx b/src/components/ExportMenu.jsx index 97b17871..319c5df0 100644 --- a/src/components/ExportMenu.jsx +++ b/src/components/ExportMenu.jsx @@ -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 ( @@ -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) }} /> diff --git a/src/library/download-utils.js b/src/library/download-utils.js index d6a0a477..256c6dd4 100644 --- a/src/library/download-utils.js +++ b/src/library/download-utils.js @@ -61,34 +61,35 @@ 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{ @@ -96,17 +97,17 @@ async function getGLBData(avatarToDownload, atlasSize = 4096, optimized = true) 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`) }) } @@ -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) @@ -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 => { diff --git a/src/library/merge-geometry.js b/src/library/merge-geometry.js index 344a9479..91651960 100644 --- a/src/library/merge-geometry.js +++ b/src/library/merge-geometry.js @@ -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), @@ -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++ @@ -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){ @@ -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 @@ -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){ @@ -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"], }); @@ -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"; @@ -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) => { @@ -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; @@ -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 => { @@ -591,6 +601,7 @@ export function mergeGeometry({ meshes }, isVrm0 = false) { sourceMorphAttributes: source.morphAttributes, sourceMorphTargetDictionaries: source.morphTargetDictionaries, destMorphTargetDictionary, + scale, },isVrm0); dest.morphTargetInfluences = mergeMorphTargetInfluences({ meshes, diff --git a/src/library/utils.js b/src/library/utils.js index 4d5ff0b6..8a6468bc 100644 --- a/src/library/utils.js +++ b/src/library/utils.js @@ -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 = { @@ -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' }); @@ -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, @@ -505,6 +507,8 @@ function getVRMMeta(name){ allowRedistribution:false, modification:"prohibited" } + + return { ...defaults, ...vrmMeta }; } // function getVRMDefaultLookAt(){