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}
/>
+ {/* */}