diff --git a/public/manifest.json b/public/manifest.json index 3c8b1f2b2..69fd585e3 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -4,7 +4,7 @@ "name": "Anata", "description": "Anata", "portrait": "./assets/portraitImages/anata.png", - "manifest":"./character-assets/anata/manifest.json", + "manifest":"./loot-assets/anata/female/manifest.json", "icon": "./assets/icons/class-neural-hacker.svg", "format": "vrm" }, @@ -12,15 +12,7 @@ "name": "Anata Male", "description": "Anata Male", "portrait": "./assets/portraitImages/anata_male.png", - "manifest":"./character-assets/anata_male/manifest.json", - "icon": "|", - "format": "vrm" - }, - { - "name": "Anata Male", - "description": "Anata Male", - "portrait": "./assets/portraitImages/anata_male.png", - "manifest":"./character-assets/test.json", + "manifest":"./loot-assets/anata/male/manifest.json", "icon": "|", "format": "vrm" } @@ -60,25 +52,25 @@ { "name": "T-Pose", "description": "T-Pose", - "location":"./animations/T-Pose.fbx", + "location":"./animations/1_T-Pose.fbx", "icon": "|" }, { - "name": "Dancing", - "description": "Basic Dance Animation", - "location":"./animations/Dancing.fbx", + "name": "Idle", + "description": "Basic Idle Animation", + "location":"./animations/2_Idle.fbx", "icon": "|" }, { "name": "Walking", "description": "Basic Walk Animation", - "location":"./animations/Walking.fbx", + "location":"./animations/3_Walking.fbx", "icon": "|" }, { "name": "Waving", "description": "Basic Waving Animation", - "location":"./animations/Waving.fbx", + "location":"./animations/4_Waving.fbx", "icon": "|" } ] diff --git a/src/components/BoneSelector.css b/src/components/BoneSelector.css new file mode 100644 index 000000000..db0eb6144 --- /dev/null +++ b/src/components/BoneSelector.css @@ -0,0 +1,51 @@ +.bone-selector { + position: absolute; + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ + height: 100vh; /* Full viewport height */ + width: 100vw; /* Full viewport height */ + pointer-events: none; + top:-30px; +} + +.character-base { + /* position: relative; + display: inline-block; + width: 300px; + height: 600px; + display: flex; + justify-content: center; + text-align: center; */ + user-select: none; + + height: 70%; + position: relative; + display: inline-block; +} + +.character-base img { + height: 100%; + /* width: 100%; */ +} + +.bone-dot { + pointer-events: auto; + user-select: none; + position: relative; + width: 0px; + height: 0px; + background-size: cover; + + cursor: pointer; +} + +.bone-dot img { + width: 30px; + height: 30px; + transform: translate(-50%, -50%); /* Center the dot */ +} + +.bone-dot-position { + position: absolute; +} \ No newline at end of file diff --git a/src/components/BoneSelector.jsx b/src/components/BoneSelector.jsx new file mode 100644 index 000000000..197d0a4d3 --- /dev/null +++ b/src/components/BoneSelector.jsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import './BoneSelector.css'; // Import CSS for styling +import boneDot from "../images/humanoid_option.png" +import humanoidUI from "../images/humanoid_ui.png" + +// Example CharacterBase component +const CharacterBase = ({}) => { + return ( +
+ +
+ ); +}; + +// Example BoneDot component +const BoneDot = ({ position, onSelect }) => { + return ( +
onSelect(position)}> + +
+ ); +}; + +// Example BoneSelector component +export const BoneSelector = ({onSelect}) => { + const [bonePositions, setBonePositions] = useState([ + { x: 0, y: 10, name:"hips" }, + + { x: 0, y: 60, name:"spine" }, + { x: 0, y: 120, name:"chest" }, + { x: 0, y: 180, name:"upperChest" }, + { x: 0, y: 220, name:"neck" }, + { x: 0, y: 260, name:"head" }, + + { x: -40, y: 180, name:"leftShoulder" }, + { x: 40, y: 180, name:"rightShoulder" }, + + { x: -70, y: 160, name:"leftUpperArm" }, + { x: 70, y: 160, name:"rightUpperArm" }, + + { x: -80, y: 80, name:"leftLowerArm" }, + { x: 80, y: 80, name:"rightLowerArm" }, + + { x: -80, y: -20, name:"leftHand" }, + { x: 80, y: -20, name:"rightHand" }, + + { x: -30, y: -20, name:"leftUpperLeg" }, + { x: 30, y: -20, name:"rightUpperLeg" }, + + { x: -22, y: -120, name:"leftLowerLeg" }, + { x: 20, y: -120, name:"rightLowerLeg" }, + + { x: -17, y: -260, name:"leftFoot" }, + { x: 17, y: -260, name:"rightFoot" }, + // Add more positions as needed + ]); + + const handleSelect = (position) => { + console.log('Bone selected at:', position); + onSelect(position.name); + // Additional logic for selecting a bone can be added here + }; + + return ( +
+ +
+ {bonePositions.map((pos, index) => ( + + ))} +
+
+ ); +}; diff --git a/src/context/SceneContext.jsx b/src/context/SceneContext.jsx index 7efa737fc..d67f1faa0 100644 --- a/src/context/SceneContext.jsx +++ b/src/context/SceneContext.jsx @@ -120,7 +120,7 @@ export const SceneProvider = (props) => { controls.minPolarAngle = 0 controls.maxPolarAngle = 3.1415 controls.minDistance = 0.5 - controls.maxDistance = 10 + controls.maxDistance = 20 controls.minAzimuthAngle = Infinity controls.maxAzimuthAngle = Infinity }) diff --git a/src/images/humanoid_option.png b/src/images/humanoid_option.png new file mode 100644 index 000000000..edd905c52 Binary files /dev/null and b/src/images/humanoid_option.png differ diff --git a/src/images/humanoid_ui.png b/src/images/humanoid_ui.png new file mode 100644 index 000000000..93f6c1308 Binary files /dev/null and b/src/images/humanoid_ui.png differ diff --git a/src/library/CharacterManifestData.js b/src/library/CharacterManifestData.js index 407960013..9d2c4a9e6 100644 --- a/src/library/CharacterManifestData.js +++ b/src/library/CharacterManifestData.js @@ -11,6 +11,7 @@ export class CharacterManifestData{ exportScale, displayScale, initialTraits, + mainTrait, requiredTraits, randomTraits, colliderTraits, @@ -35,7 +36,10 @@ export class CharacterManifestData{ this.displayScale = displayScale || exportScale || 1; this.animationPath = getAsArray(animationPath); + this.requiredTraits = getAsArray(requiredTraits); + + this.mainTrait = mainTrait || this.requiredTraits[0] || traits[0].trait; this.randomTraits = getAsArray(randomTraits); this.initialTraits = initialTraits || [...new Set(this.requiredTraits.concat(this.randomTraits))]; this.colliderTraits = getAsArray(colliderTraits); diff --git a/src/library/characterManager.js b/src/library/characterManager.js index 6352629fc..2df4ec080 100644 --- a/src/library/characterManager.js +++ b/src/library/characterManager.js @@ -51,6 +51,7 @@ export class CharacterManager { this.renderCamera = renderCamera; this.manifestData = null; + this.baseSkeletonVRM = null this.manifest = null if (manifestURL){ this.loadManifest(manifestURL) @@ -199,6 +200,7 @@ export class CharacterManager { clearTraitData.push(new LoadedData({traitGroupID:prop, traitModel:null})) } + this.baseSkeletonVRM = null; clearTraitData.forEach(itemData => { this._addLoadedData(itemData) }); @@ -471,12 +473,12 @@ export class CharacterManager { * or rejects with an error message if not. */ loadInitialTraits() { + return new Promise(async(resolve, reject) => { // Check if manifest data is available if (this.manifestData) { // Load initial traits using the _loadTraits method await this._loadTraits(this.manifestData.getInitialTraits()); - resolve(); } else { // Manifest data is not available, log an error and reject the Promise @@ -583,6 +585,34 @@ export class CharacterManager { }); } + loadCustomModelTrait(groupTraitID, url, parentBoneName){ + return new Promise(async (resolve, reject) => { + // Check if manifest data is available + if (this.manifestData) { + try { + // Retrieve the selected custom trait using manifest data + const selectedTrait = this.manifestData.getCustomTraitOption(groupTraitID, url); + + // If the custom trait is found, load it into the avatar using the _loadTraits method + if (selectedTrait) { + await this._loadTraits(getAsArray(selectedTrait),false, parentBoneName); + resolve(); + } + + } catch (error) { + // Reject the Promise with an error message if there's an error during custom trait retrieval + console.error("Error loading custom trait:", error.message); + reject(new Error("Failed to load custom trait.")); + } + } else { + // Manifest data is not available, log an error and reject the Promise + const errorMessage = "No manifest was loaded, custom trait cannot be loaded."; + console.error(errorMessage); + reject(new Error(errorMessage)); + } + }); + } + /** * Loads a custom texture to the specified group trait's model. * @@ -857,9 +887,15 @@ export class CharacterManager { //const selectedTrait = this.manifestData.getTraitOption(groupTraitID, traitID); } - async _loadTraits(options, fullAvatarReplace = false){ + async _loadTraits(options, fullAvatarReplace = false, parentBoneName = null){ console.log("laoded traits:", options) + + await this._createBaseSkeleton(options); + + console.log("parent bone name: ", parentBoneName) + await this.traitLoadManager.loadTraitOptions(getAsArray(options)).then(loadedData=>{ + if (fullAvatarReplace){ // add null loaded options to existingt traits to remove them; const groupTraits = this.getGroupTraits(); @@ -874,8 +910,10 @@ export class CharacterManager { } loadedData.forEach(itemData => { - this._addLoadedData(itemData) + console.log(itemData); + this._addLoadedData(itemData, parentBoneName) }); + cullHiddenMeshes(this.avatar); }) } @@ -897,6 +935,13 @@ export class CharacterManager { return data } + _createBoneSphere(radius){ + const geometry = new THREE.SphereGeometry( radius, 32, 16 ); + const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } ); + const sphere = new THREE.Mesh( geometry, material ); + return sphere; + } + _getPortaitScreenshotTexture(getBlob, options){ this.blinkManager.enableScreenshot(); @@ -934,6 +979,8 @@ export class CharacterManager { return screenshot; } + + _setupWireframeMaterial(mesh){ // Set Wireframe material with random colors for each material the object has mesh.origMat = mesh.material; @@ -973,25 +1020,28 @@ export class CharacterManager { // } } - _VRMBaseSetup(m, item, traitID, textures, colors){ + _VRMBaseSetup(m, item, traitID, textures, colors, isSkeleton = false){ let vrm = m.userData.vrm; if (m.userData.vrm == null){ - console.error("No valid VRM was provided for " + traitID + " trait, skipping file.") return null; } addModelData(vrm, {isVRM0:vrm.meta?.metaVersion === '0'}) - if (this.manifestData.isColliderRequired(traitID)) + + if (this.manifestData.isColliderRequired(traitID) && !isSkeleton) saveVRMCollidersToUserData(m); + renameVRMBones(vrm); - if (this.manifestData.isLipsyncTrait(traitID)) + + if (this.manifestData.isLipsyncTrait(traitID) && !isSkeleton) this.lipSync = new LipSync(vrm); - this._modelBaseSetup(vrm, item, traitID, textures, colors); + if (!isSkeleton) + this._modelBaseSetup(vrm, item, traitID, textures, colors); // Rotate model 180 degrees @@ -1029,6 +1079,8 @@ export class CharacterManager { return vrm; } + + _modelBaseSetup(model, item, traitID, textures, colors){ const meshTargets = []; @@ -1140,14 +1192,28 @@ export class CharacterManager { if (this.animationManager) this.animationManager.addVRM(vrm) } - _displayModel(model){ + _displayModel(model, parentBoneName = null){ if(model) { // call transition const m = model.scene; //m.visible = false; // add the now model to the current scene + const targetBone = parentBoneName != null ? this.baseSkeletonVRM.humanoid.humanBones[parentBoneName]?.node : null; + + - this.characterModel.attach(m) + if (targetBone != null){ + targetBone.add(m) + } + else{ + this.characterModel.attach(m) + } + + + + + + //animationManager.update(); // note: update animation to prevent some frames of T pose at start. @@ -1193,8 +1259,49 @@ export class CharacterManager { disposeVRM(vrm) } + async _createBaseSkeleton(traitOptions){ + if (this.baseSkeletonVRM == null){ + const mainAsset = traitOptions.find(obj => obj.traitModel?.traitGroup.trait === this.manifestData.mainTrait); + await this.traitLoadManager.loadTraitOptions(getAsArray(mainAsset)).then(loadedData=>{ + this._addLoadedDataSkeleton(loadedData[0]) + }); + } + } + _addLoadedDataSkeleton(itemData){ + const { + models + } = itemData; + + let vrm = null; + models.map((m)=>{ + if (m != null) + vrm = this._VRMBaseSetup(m, null, null, null,null, true); + }) + + this._positionModel(vrm) + + this._applyManagers(vrm) - _addLoadedData(itemData){ + let targetSkinnedMesh = null; + vrm.scene.traverse((object) => { + if (object.isSkinnedMesh) { // Check if the object is a SkinnedMesh + targetSkinnedMesh = object; + // object.skeleton.bones.forEach((bn)=>{ + // const sphere = this._createBoneSphere(.05); + // bn.add(sphere); + // }) + } + }); + if (targetSkinnedMesh != null ){ + targetSkinnedMesh.parent.remove(targetSkinnedMesh); + } + + this.characterModel.attach(vrm.scene) + + this.baseSkeletonVRM = vrm; + } + + _addLoadedData(itemData, parentBoneName){ const { traitGroupID, traitModel, @@ -1226,31 +1333,49 @@ export class CharacterManager { }) // do nothing, an error happened - if (vrm == null) - return; + if (vrm == null){ + // found model that is not vrmc + let gltfModel = models[0] + if (this.avatar[traitGroupID] && this.avatar[traitGroupID].vrm) { + this._disposeTrait(this.avatar[traitGroupID].vrm) + } + this._positionModel(gltfModel) + this._displayModel(gltfModel, parentBoneName) // probably attach to bone instead + this.avatar[traitGroupID] = { + traitInfo: traitModel, + textureInfo: textureTrait, + colorInfo: colorTrait, + name: traitModel.name, + model: gltfModel, + vrm: vrm + } + } + else{ // If there was a previous loaded model, remove it (maybe also remove loaded textures?) - if (this.avatar[traitGroupID] && this.avatar[traitGroupID].vrm) { - this._disposeTrait(this.avatar[traitGroupID].vrm) - // XXX restore effects - } + if (this.avatar[traitGroupID] && this.avatar[traitGroupID].vrm) { + this._disposeTrait(this.avatar[traitGroupID].vrm) + // XXX restore effects + } - this._positionModel(vrm) - - this._displayModel(vrm) - - this._applyManagers(vrm) + this._positionModel(vrm) - console.log(this.characterModel) - // and then add the new avatar data - // to do, we are now able to load multiple vrm models per options, set the options to include vrm arrays - this.avatar[traitGroupID] = { - traitInfo: traitModel, - textureInfo: textureTrait, - colorInfo: colorTrait, - name: traitModel.name, - model: vrm && vrm.scene, - vrm: vrm + this._displayModel(vrm) + + this._applyManagers(vrm) + + console.log(this.characterModel) + // and then add the new avatar data + // to do, we are now able to load multiple vrm models per options, set the options to include vrm arrays + this.avatar[traitGroupID] = { + traitInfo: traitModel, + textureInfo: textureTrait, + colorInfo: colorTrait, + name: traitModel.name, + model: vrm && vrm.scene, + vrm: vrm + } + console.log(this.avatar[traitGroupID]); } } } diff --git a/src/pages/Appearance.jsx b/src/pages/Appearance.jsx index be6dcdd98..e2ef6450f 100644 --- a/src/pages/Appearance.jsx +++ b/src/pages/Appearance.jsx @@ -19,6 +19,7 @@ import randomizeIcon from "../images/randomize.png" import colorPicker from "../images/color-palette.png" import { ChromePicker } from 'react-color' import RightPanel from "../components/RightPanel" +import { BoneSelector } from "../components/BoneSelector" function Appearance() { const { isLoading, setViewMode, setIsLoading } = React.useContext(ViewContext) @@ -55,6 +56,8 @@ function Appearance() { const [loadedAnimationName, setLoadedAnimationName] = React.useState(""); const [isPickingColor, setIsPickingColor] = React.useState(false) const [colorPicked, setColorPicked] = React.useState({ background: '#ffffff' }) + const [selectingBone, setSelectingBone] = React.useState(false) + const [modelFile, setModelFile] = React.useState(null) const next = () => { !isMute && playSound('backNextButton'); @@ -106,6 +109,23 @@ function Appearance() { console.warn("Please select a group trait first.") } } + + // const loadGLTFModel = async (file)=>{ + // const url = URL.createObjectURL(file); + + // URL.revokeObjectURL(url); + // } + + const handleModelDrop = (file) =>{ + if (traitGroupName != ""){ + setSelectingBone(true); + setModelFile(file); + } + else{ + console.warn("Please select a group trait first.") + } + } + const handleVRMDrop = (file) =>{ setIsPickingColor(false); if (traitGroupName != ""){ @@ -113,6 +133,7 @@ function Appearance() { const path = URL.createObjectURL(file); characterManager.loadCustomTrait(traitGroupName, path).then(()=>{ setIsLoading(false); + URL.revokeObjectURL(path); }) } else{ @@ -204,6 +225,9 @@ function Appearance() { if (file && file.name.toLowerCase().endsWith('.json')) { handleJsonDrop(files); } + if (file && (file.name.toLowerCase().endsWith('.gltf') || file.name.toLowerCase().endsWith('.glb') )) { + handleModelDrop(file); + } }; const selectTraitGroup = (traitGroup) => { @@ -243,6 +267,17 @@ function Appearance() { } input.click(); } + const placeModelOnBone = async (boneName) => { + setSelectingBone(false); + setIsLoading(true); + const path = URL.createObjectURL(modelFile); + characterManager.loadCustomModelTrait(traitGroupName, path, boneName).then(()=>{ + setIsLoading(false); + setModelFile(null) + URL.revokeObjectURL(path); + }) + + } return (
@@ -253,6 +288,9 @@ function Appearance() { + {selectingBone && } {/* Main Menu section */}
diff --git a/src/pages/Optimizer.jsx b/src/pages/Optimizer.jsx index 60a4043a5..1de43c7e7 100644 --- a/src/pages/Optimizer.jsx +++ b/src/pages/Optimizer.jsx @@ -13,6 +13,7 @@ import MergeOptions from "../components/MergeOptions" import { local } from "../library/store" import { ZipManager } from "../library/zipManager" import BottomDisplayMenu from "../components/BottomDisplayMenu" +import { BoneSelector } from "../components/BoneSelector" function Optimizer() { const { @@ -222,6 +223,7 @@ function Optimizer() { nextVrm={loadNextVRM} previousVrm={loadPreviousVRM} /> + {/* */}