myroom-react-private
Version:
React components for MyRoom 3D scene integration with Babylon.js
1,283 lines (1,248 loc) • 843 kB
JavaScript
import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
import { Vector3, SceneLoader, Animation, CubicEase, EasingFunction, Engine, Scene, Color4, Color3, ArcRotateCamera, HemisphericLight, DirectionalLight, ShadowGenerator, MeshBuilder, StandardMaterial, TransformNode, UtilityLayerRenderer, PointerEventTypes, AbstractMesh, Matrix, DefaultRenderingPipeline, SSAO2RenderingPipeline, ScaleGizmo, RotationGizmo, PositionGizmo, Effect, ShaderMaterial } from '@babylonjs/core';
import '@babylonjs/loaders';
import { jsxs, jsx } from 'react/jsx-runtime';
import { __decorate } from '@babylonjs/core/tslib.es6.js';
import { Logger } from '@babylonjs/core/Misc/logger.js';
import { Observable } from '@babylonjs/core/Misc/observable.js';
import { Vector2, Vector3 as Vector3$1, Matrix as Matrix$1, TmpVectors, Vector4 } from '@babylonjs/core/Maths/math.vector.js';
import { PointerEventTypes as PointerEventTypes$1 } from '@babylonjs/core/Events/pointerEvents.js';
import { Tools } from '@babylonjs/core/Misc/tools.js';
import { Epsilon } from '@babylonjs/core/Maths/math.constants.js';
import { RegisterClass, GetClass } from '@babylonjs/core/Misc/typeStore.js';
import { serialize, expandToProperty, serializeAsColor3, serializeAsColor4, serializeAsVector3, serializeAsTexture } from '@babylonjs/core/Misc/decorators.js';
import { SerializationHelper } from '@babylonjs/core/Misc/decorators.serialization.js';
import { EngineStore } from '@babylonjs/core/Engines/engineStore.js';
import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js';
import { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
import { Constants } from '@babylonjs/core/Engines/constants.js';
import { ClipboardEventTypes, ClipboardInfo } from '@babylonjs/core/Events/clipboardEvents.js';
import { Color3 as Color3$1, Color4 as Color4$1 } from '@babylonjs/core/Maths/math.color.js';
import { AbstractMesh as AbstractMesh$1 } from '@babylonjs/core/Meshes/abstractMesh.js';
import { KeyboardEventTypes } from '@babylonjs/core/Events/keyboardEvents.js';
import { Layer } from '@babylonjs/core/Layers/layer.js';
import { Viewport } from '@babylonjs/core/Maths/math.viewport.js';
import { WebRequest } from '@babylonjs/core/Misc/webRequest.js';
import { RandomGUID } from '@babylonjs/core/Misc/guid.js';
import { DecodeBase64ToBinary } from '@babylonjs/core/Misc/stringTools.js';
import '@babylonjs/core/Misc/perfCounter.js';
import { FrameGraphTask } from '@babylonjs/core/FrameGraph/frameGraphTask.js';
import { NodeRenderGraphBlock } from '@babylonjs/core/FrameGraph/Node/nodeRenderGraphBlock.js';
import { NodeRenderGraphBlockConnectionPointTypes } from '@babylonjs/core/FrameGraph/Node/Types/nodeRenderGraphTypes.js';
import '@babylonjs/core/Meshes/transformNode.js';
import '@babylonjs/core/Meshes/Builders/boxBuilder.js';
import '@babylonjs/core/Materials/standardMaterial.js';
import '@babylonjs/core/Maths/math.axis.js';
import '@babylonjs/core/Meshes/mesh.js';
import { MaterialDefines } from '@babylonjs/core/Materials/materialDefines.js';
import { PushMaterial } from '@babylonjs/core/Materials/pushMaterial.js';
import { VertexBuffer } from '@babylonjs/core/Buffers/buffer.js';
import { ShaderStore } from '@babylonjs/core/Engines/shaderStore.js';
import { PrepareUniformsAndSamplersList, PrepareDefinesForAttributes, HandleFallbacksForShadows, PrepareAttributesForInstances } from '@babylonjs/core/Materials/materialHelper.functions.js';
import '@babylonjs/core/Behaviors/Meshes/handConstraintBehavior.js';
import { EffectFallbacks } from '@babylonjs/core/Materials/effectFallbacks.js';
import '@babylonjs/core/Loading/sceneLoader.js';
import '@babylonjs/core/Meshes/Builders/planeBuilder.js';
import '@babylonjs/core/Behaviors/Meshes/fadeInOutBehavior.js';
import '@babylonjs/core/Misc/domManagement.js';
import '@babylonjs/core/Maths/math.scalar.js';
import '@babylonjs/core/Behaviors/Meshes/followBehavior.js';
import '@babylonjs/core/Behaviors/Meshes/sixDofDragBehavior.js';
import '@babylonjs/core/Behaviors/Meshes/surfaceMagnetismBehavior.js';
import '@babylonjs/core/Gizmos/gizmo.js';
import '@babylonjs/core/Misc/pivotTools.js';
import '@babylonjs/core/Materials/shaderMaterial.js';
import '@babylonjs/core/Behaviors/Meshes/baseSixDofDragBehavior.js';
import '@babylonjs/core/Behaviors/Meshes/pointerDragBehavior.js';
import '@babylonjs/core/Maths/math.js';
import '@babylonjs/core/Meshes/mesh.vertexData.js';
import '@babylonjs/core/Animations/animation.js';
import '@babylonjs/core/Animations/animationGroup.js';
import '@babylonjs/core/Lights/hemisphericLight.js';
import '@babylonjs/core/Rendering/utilityLayerRenderer.js';
// src/components/IntegratedBabylonScene.tsx
// src/config/appConfig.ts
var getConfiguredDomain = () => {
if (typeof window !== "undefined" && window.MYROOM_CONFIG && window.MYROOM_CONFIG.baseDomain) {
return window.MYROOM_CONFIG.baseDomain;
}
return "https://myroom.petarainsoft.com";
};
var domainConfig = {
// The base domain URL (without trailing slash)
baseDomain: getConfiguredDomain(),
// Paths for different resources
paths: {
webComponent: "/dist/myroom-webcomponent.umd.js",
embedHtml: "/embed.html",
models: {
rooms: "/models/rooms",
items: "/models/items",
avatars: "/models/avatars"
}
}
};
// src/components/RoomLoader.tsx
var useRoomLoader = ({ scene, roomPath, isSceneReady, roomRef }) => {
useEffect(() => {
if (!isSceneReady || !scene || !roomPath || !roomRef.current) return;
const loadRoom = async () => {
try {
roomRef.current.getChildMeshes().forEach((mesh) => mesh.dispose());
const fullRoomUrl = roomPath.startsWith("http") ? roomPath : `${domainConfig.baseDomain}${roomPath}`;
const result = await SceneLoader.ImportMeshAsync(
"",
fullRoomUrl,
"",
scene
);
result.meshes.forEach((mesh) => {
if (mesh.parent === null) {
mesh.parent = roomRef.current;
}
mesh.receiveShadows = true;
});
console.log("Room loaded:", roomPath);
} catch (error) {
console.error("Error loading room:", error);
}
};
loadRoom();
}, [isSceneReady, roomPath, scene, roomRef]);
};
var RoomLoader_default = useRoomLoader;
var useItemLoader = ({
scene,
loadedItems,
isSceneReady,
itemsRef,
loadedItemMeshesRef,
shadowGeneratorRef
}) => {
useEffect(() => {
if (!isSceneReady || !scene || !loadedItems || !itemsRef.current) return;
const loadItems = async () => {
try {
if (loadedItemMeshesRef.current.length > 0) {
loadedItemMeshesRef.current.forEach((container) => {
if (container && container.dispose) {
container.dispose();
}
});
loadedItemMeshesRef.current = [];
}
if (itemsRef.current) {
itemsRef.current.getChildMeshes().forEach((mesh) => {
if (mesh && mesh.dispose) {
mesh.dispose();
}
});
}
for (const item of loadedItems) {
if (!item || !item.path || typeof item.path !== "string") {
console.warn("Skipping invalid item:", item);
continue;
}
const fullItemUrl = item.path.startsWith("http") ? item.path : `${domainConfig.baseDomain}${item.path}`;
const result = await SceneLoader.ImportMeshAsync(
"",
fullItemUrl,
"",
scene
);
const itemContainer = new TransformNode(item.id, scene);
itemContainer.position = new Vector3(item.position.x, item.position.y, item.position.z);
if (item.rotation) {
itemContainer.rotation = new Vector3(item.rotation.x, item.rotation.y, item.rotation.z);
}
if (item.scale) {
itemContainer.scaling = new Vector3(item.scale.x, item.scale.y, item.scale.z);
}
itemContainer.parent = itemsRef.current;
result.meshes.forEach((mesh) => {
if (mesh.parent === null) {
mesh.parent = itemContainer;
}
mesh.isPickable = true;
mesh.metadata = { isFurniture: true };
if (shadowGeneratorRef.current)
shadowGeneratorRef.current.addShadowCaster(mesh);
});
loadedItemMeshesRef.current.push(itemContainer);
}
console.log(`Loaded ${loadedItems.length} items`);
} catch (error) {
console.error("Error loading items:", error);
}
};
loadItems();
}, [isSceneReady, loadedItems, scene, itemsRef, loadedItemMeshesRef]);
};
var ItemLoader_default = useItemLoader;
var useItemManipulator = ({
gizmoMode,
selectedItem,
utilityLayerRef,
onItemTransformChange,
onSelectItem,
loadedItemMeshesRef,
highlightDiscRef
}) => {
const gizmoRef = useRef(null);
const selectedItemRef = useRef(null);
const transformTimeoutRef = useRef(null);
const isDraggingRef = useRef(false);
useEffect(() => {
selectedItemRef.current = selectedItem;
}, [selectedItem]);
const selectItem = (mesh) => {
console.log("selectItem called with mesh:", mesh.name);
const itemContainer = loadedItemMeshesRef.current.find((container) => {
const isDirectChild = mesh.parent === container;
let isDescendant = false;
let currentParent = mesh.parent;
while (currentParent && !isDescendant) {
if (currentParent === container) {
isDescendant = true;
}
currentParent = currentParent.parent;
}
console.log("Checking container in selectItem:", container.name, "Direct child:", isDirectChild, "Descendant:", isDescendant);
return isDirectChild || isDescendant;
});
console.log("Found item container:", itemContainer?.name);
if (itemContainer) {
selectedItemRef.current = itemContainer;
onSelectItem?.(itemContainer);
console.log("Item selected:", itemContainer.name);
if (highlightDiscRef.current) {
highlightDiscRef.current.position = itemContainer.position.clone();
highlightDiscRef.current.position.y += 0.02;
highlightDiscRef.current.isVisible = true;
}
updateGizmo(itemContainer);
} else {
console.log("No item container found for mesh:", mesh.name);
}
};
const deselectItem = () => {
selectedItemRef.current = null;
onSelectItem?.(null);
if (gizmoRef.current) {
try {
if ("setEnabled" in gizmoRef.current && typeof gizmoRef.current.setEnabled === "function") {
gizmoRef.current.setEnabled(false);
}
gizmoRef.current.dispose();
gizmoRef.current = null;
console.log("Gizmo hidden and disposed in deselectItem");
} catch (error) {
console.warn("Error disposing gizmo:", error);
gizmoRef.current = null;
}
}
if (highlightDiscRef.current) {
highlightDiscRef.current.isVisible = false;
}
};
const updateItemTransform = useCallback(
(itemId, mesh, immediate = false) => {
if (!onItemTransformChange) return;
const doUpdate = () => {
const transform = {
position: { x: mesh.position.x, y: mesh.position.y, z: mesh.position.z },
rotation: { x: mesh.rotation.x, y: mesh.rotation.y, z: mesh.rotation.z },
scale: { x: mesh.scaling.x, y: mesh.scaling.y, z: mesh.scaling.z }
};
onItemTransformChange(itemId, transform);
if (highlightDiscRef.current) {
highlightDiscRef.current.position = mesh.position.clone();
highlightDiscRef.current.position.y += 0.02;
}
};
if (immediate) {
if (transformTimeoutRef.current) {
clearTimeout(transformTimeoutRef.current);
transformTimeoutRef.current = null;
}
doUpdate();
} else if (isDraggingRef.current) {
if (transformTimeoutRef.current) {
clearTimeout(transformTimeoutRef.current);
}
transformTimeoutRef.current = setTimeout(doUpdate, 150);
} else {
if (transformTimeoutRef.current) {
clearTimeout(transformTimeoutRef.current);
}
transformTimeoutRef.current = setTimeout(doUpdate, 50);
}
},
[onItemTransformChange, highlightDiscRef]
);
const updateGizmo = (mesh) => {
if (!utilityLayerRef.current) return;
if (gizmoRef.current) {
try {
gizmoRef.current.dispose();
} catch (error) {
console.warn("Error disposing existing gizmo:", error);
}
gizmoRef.current = null;
}
const currentGizmoMode = gizmoMode || "position";
switch (currentGizmoMode) {
case "position":
gizmoRef.current = new PositionGizmo(utilityLayerRef.current);
if (gizmoRef.current) {
gizmoRef.current.scaleRatio = 1.2;
gizmoRef.current.sensitivity = 1.5;
if (gizmoRef.current.yGizmo) {
gizmoRef.current.yGizmo.isEnabled = false;
}
}
break;
case "rotation":
gizmoRef.current = new RotationGizmo(utilityLayerRef.current);
if (gizmoRef.current) {
gizmoRef.current.scaleRatio = 1.2;
gizmoRef.current.sensitivity = 2;
gizmoRef.current.snapDistance = Math.PI / 12;
if (gizmoRef.current.xGizmo) {
gizmoRef.current.xGizmo.isEnabled = false;
}
if (gizmoRef.current.zGizmo) {
gizmoRef.current.zGizmo.isEnabled = false;
}
if (gizmoRef.current.yGizmo) {
gizmoRef.current.yGizmo.isEnabled = true;
}
}
break;
case "scale":
gizmoRef.current = new ScaleGizmo(utilityLayerRef.current);
if (gizmoRef.current) {
gizmoRef.current.scaleRatio = 1.2;
gizmoRef.current.sensitivity = 1.8;
}
break;
default:
return;
}
gizmoRef.current.attachedMesh = mesh;
if ("updateGizmoRotationToMatchAttachedMesh" in gizmoRef.current) {
gizmoRef.current.updateGizmoRotationToMatchAttachedMesh = true;
}
if ("updateGizmoPositionToMatchAttachedMesh" in gizmoRef.current) {
gizmoRef.current.updateGizmoPositionToMatchAttachedMesh = true;
}
if (gizmoRef.current) {
const gizmo = gizmoRef.current;
if (gizmo.onDragStartObservable) {
gizmo.onDragStartObservable.add(() => {
isDraggingRef.current = true;
});
}
if (gizmo.onDragObservable) {
gizmo.onDragObservable.add(() => {
if (mesh.position) {
mesh.position.x = Math.max(-2, Math.min(2, mesh.position.x));
mesh.position.z = Math.max(-2, Math.min(2, mesh.position.z));
}
updateItemTransform(mesh.name, mesh, false);
});
}
if (gizmo.onDragEndObservable) {
gizmo.onDragEndObservable.add(() => {
isDraggingRef.current = false;
if (transformTimeoutRef.current) {
clearTimeout(transformTimeoutRef.current);
transformTimeoutRef.current = null;
}
if (mesh.position) {
mesh.position.x = Math.max(-2, Math.min(2, mesh.position.x));
mesh.position.z = Math.max(-2, Math.min(2, mesh.position.z));
}
updateItemTransform(mesh.name, mesh, true);
});
}
}
};
useEffect(() => {
if (selectedItemRef.current && utilityLayerRef.current) {
console.log("Updating gizmo due to mode change:", gizmoMode);
updateGizmo(selectedItemRef.current);
}
}, [gizmoMode]);
useEffect(() => {
console.log("selectedItem changed effect triggered", { selectedItem });
if (!selectedItem && gizmoRef.current) {
console.log("Attempting to hide and dispose gizmo", {
gizmoExists: !!gizmoRef.current,
gizmoType: gizmoRef.current?.constructor.name,
hasSetEnabled: "setEnabled" in gizmoRef.current
});
try {
if (gizmoRef.current && "setEnabled" in gizmoRef.current && typeof gizmoRef.current.setEnabled === "function") {
gizmoRef.current.setEnabled(false);
console.log("Gizmo disabled successfully");
}
gizmoRef.current.dispose();
gizmoRef.current = null;
console.log("Gizmo hidden and disposed due to selectedItem being null");
} catch (error) {
console.warn("Error disposing gizmo when selectedItem changed:", error);
if (error instanceof Error) {
console.log("Error details:", {
errorName: error.name,
errorMessage: error.message,
stack: error.stack
});
}
gizmoRef.current = null;
}
}
}, [selectedItem]);
useEffect(() => {
return () => {
if (transformTimeoutRef.current) {
clearTimeout(transformTimeoutRef.current);
}
if (gizmoRef.current) {
try {
gizmoRef.current.dispose();
} catch (error) {
console.warn("Error disposing gizmo on unmount:", error);
}
gizmoRef.current = null;
}
};
}, []);
return {
selectItem,
deselectItem,
updateGizmo,
updateItemTransform
};
};
var ItemManipulator_default = useItemManipulator;
var useSkybox = ({ scene, isSceneReady }) => {
useEffect(() => {
if (!isSceneReady || !scene) return;
const createSkyboxMaterial = () => {
const skyboxVertexShader = `
precision highp float;
// Attributes
attribute vec3 position;
attribute vec2 uv;
// Uniforms
uniform mat4 world;
uniform mat4 viewProjection;
// Varying
varying vec3 vPosition;
varying vec3 vDirectionW;
varying vec2 vUV;
void main() {
vec4 worldPosition = world * vec4(position, 1.0);
gl_Position = viewProjection * worldPosition;
// S\u1EED d\u1EE5ng v\u1ECB tr\xED ch\xEDnh x\xE1c \u0111\u1EC3 t\xEDnh to\xE1n h\u01B0\u1EDBng
vPosition = position;
// T\xEDnh to\xE1n h\u01B0\u1EDBng trong kh\xF4ng gian th\u1EBF gi\u1EDBi - quan tr\u1ECDng cho skybox li\u1EC1n m\u1EA1ch
vDirectionW = normalize(worldPosition.xyz);
vUV = uv;
}
`;
const skyboxFragmentShader = `
precision highp float;
varying vec3 vPosition;
varying vec3 vDirectionW;
varying vec2 vUV;
uniform float time;
uniform vec3 cameraPosition;
// Simplified FXAA anti-aliasing for skybox (without texture sampling)
vec3 applyFXAA(vec3 color, vec2 fragCoord) {
// Simplified version that just applies some edge detection and smoothing
// This version doesn't require texture sampling which was causing issues
// Apply a simple sharpening filter instead
float sharpen = 0.5;
vec3 blurred = color * (1.0 - sharpen);
return color * (1.0 + sharpen) - blurred;
}
// Atmospheric scattering
vec3 atmosphere(vec3 r) {
float atmosphere = 1.0 - r.y;
return vec3(0.3, 0.6, 1.0) * pow(atmosphere, 1.5);
}
// Improved noise function for clouds - s\u1EED d\u1EE5ng hash \u0111\u1EC3 t\u1EA1o gi\xE1 tr\u1ECB ng\u1EABu nhi\xEAn
float hash(float n) {
return fract(sin(n) * 43758.5453);
}
float noise(vec3 p) {
// S\u1EED d\u1EE5ng thu\u1EADt to\xE1n noise c\u1EA3i ti\u1EBFn \u0111\u1EC3 t\u1EA1o k\u1EBFt qu\u1EA3 m\u01B0\u1EE3t m\xE0 h\u01A1n
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f); // Smooth interpolation
float n = i.x + i.y * 157.0 + 113.0 * i.z;
return mix(mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y),
mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y), f.z);
}
// Improved cloud function
float clouds(vec3 p) {
// S\u1EED d\u1EE5ng nhi\u1EC1u l\u1EDBp noise \u0111\u1EC3 t\u1EA1o m\xE2y chi ti\u1EBFt h\u01A1n
float n = noise(p * 0.3);
n += noise(p * 0.6) * 0.5;
n += noise(p * 1.2) * 0.25;
n = n / 1.75; // Normalize
return smoothstep(0.4, 0.6, n);
}
void main() {
// S\u1EED d\u1EE5ng vDirectionW \u0111\u1EC3 \u0111\u1EA3m b\u1EA3o c\xE1c m\u1EB7t li\u1EC1n m\u1EA1ch
vec3 dir = normalize(vDirectionW);
// Base sky color based on height (y coordinate)
float height = dir.y * 0.5 + 0.5; // Normalize to 0-1 range
vec3 skyColor = mix(
vec3(0.1, 0.4, 0.8), // Horizon color (blue)
vec3(0.0, 0.15, 0.4), // Zenith color (darker blue)
pow(height, 0.5) // Gradient power
);
// Add atmospheric scattering - s\u1EED d\u1EE5ng vDirectionW \u0111\u1EC3 \u0111\u1EA3m b\u1EA3o li\u1EC1n m\u1EA1ch
skyColor += atmosphere(dir) * 0.3;
// Add subtle time-based color variation
skyColor += vec3(sin(time * 0.1) * 0.02, sin(time * 0.13) * 0.02, sin(time * 0.15) * 0.02);
// C\u1EA3i thi\u1EC7n hi\u1EC7u \u1EE9ng m\xE2y \u0111\u1EC3 li\u1EC1n m\u1EA1ch h\u01A1n
// S\u1EED d\u1EE5ng vDirectionW \u0111\u1EC3 \u0111\u1EA3m b\u1EA3o m\xE2y li\u1EC1n m\u1EA1ch gi\u1EEFa c\xE1c m\u1EB7t c\u1EE7a skybox
// Th\xEAm tham s\u1ED1 th\u1EDDi gian \u0111\u1EC3 m\xE2y di chuy\u1EC3n
vec3 cloudDir = dir;
// T\u1EA1o nhi\u1EC1u l\u1EDBp m\xE2y v\u1EDBi t\u1EF7 l\u1EC7 kh\xE1c nhau
float cloudDensity1 = clouds(cloudDir * 8.0 + vec3(time * 0.02, 0.0, 0.0)) * 0.5;
float cloudDensity2 = clouds(cloudDir * 4.0 + vec3(time * 0.04, 0.0, 0.0)) * 0.3;
float cloudDensity3 = clouds(cloudDir * 2.0 + vec3(time * 0.01, 0.0, 0.0)) * 0.2;
// K\u1EBFt h\u1EE3p c\xE1c l\u1EDBp m\xE2y
float cloudDensity = cloudDensity1 + cloudDensity2 + cloudDensity3;
cloudDensity = min(cloudDensity, 1.0); // Gi\u1EDBi h\u1EA1n m\u1EADt \u0111\u1ED9 t\u1ED1i \u0111a
// Ch\u1EC9 hi\u1EC3n th\u1ECB m\xE2y tr\xEAn \u0111\u01B0\u1EDDng ch\xE2n tr\u1EDDi v\u1EDBi transition m\u01B0\u1EE3t m\xE0 h\u01A1n
cloudDensity *= smoothstep(-0.05, 0.15, dir.y);
// T\u1EA1o m\xE0u m\xE2y v\u1EDBi gradient t\u1EEB tr\u1EAFng \u0111\u1EBFn x\xE1m nh\u1EA1t
vec3 cloudColor = mix(vec3(1.0, 1.0, 1.0), vec3(0.9, 0.9, 0.95), cloudDensity * 0.3);
// Tr\u1ED9n m\xE2y v\u1EDBi b\u1EA7u tr\u1EDDi
skyColor = mix(skyColor, cloudColor, cloudDensity * 0.6);
// Th\xEAm hi\u1EC7u \u1EE9ng sun rays (tia n\u1EAFng)
vec3 sunDir = normalize(vec3(0.0, 0.2, 1.0)); // H\u01B0\u1EDBng m\u1EB7t tr\u1EDDi
float sunIntensity = max(0.0, dot(dir, sunDir));
vec3 sunColor = vec3(1.0, 0.9, 0.7) * pow(sunIntensity, 32.0) * 2.0;
skyColor += sunColor;
// C\u1EA3i thi\u1EC7n contrast v\xE0 sharpening
skyColor = mix(skyColor, skyColor * skyColor, 0.2);
// Apply FXAA
skyColor = applyFXAA(skyColor, vUV);
// \u0110\u1EA3m b\u1EA3o m\xE0u s\u1EAFc n\u1EB1m trong ph\u1EA1m vi h\u1EE3p l\u1EC7
skyColor = clamp(skyColor, 0.0, 1.0);
// Final color
gl_FragColor = vec4(skyColor, 1.0);
}
`;
Effect.ShadersStore["skyboxVertexShader"] = skyboxVertexShader;
Effect.ShadersStore["skyboxFragmentShader"] = skyboxFragmentShader;
const skyboxMaterial = new ShaderMaterial(
"skyboxMaterial",
scene,
{
vertex: "skybox",
fragment: "skybox"
},
{
attributes: ["position", "uv"],
uniforms: ["world", "viewProjection", "time", "cameraPosition"]
}
);
skyboxMaterial.backFaceCulling = false;
skyboxMaterial.disableDepthWrite = true;
let time = 0;
scene.registerBeforeRender(() => {
time += scene.getEngine().getDeltaTime() / 1e3;
skyboxMaterial.setFloat("time", time);
skyboxMaterial.setVector3("cameraPosition", scene.activeCamera.position);
});
return skyboxMaterial;
};
const createSkybox = () => {
const skyboxMaterial = createSkyboxMaterial();
const skybox2 = MeshBuilder.CreateSphere(
"skyBox",
{
diameter: 2e3,
segments: 32,
// Số lượng segments cao hơn cho hình cầu mượt mà
sideOrientation: 1
// BACKSIDE - để render bên trong sphere
},
scene
);
skybox2.infiniteDistance = true;
skybox2.renderingGroupId = 0;
skybox2.material = skyboxMaterial;
if (scene.activeCamera) {
skybox2.position = scene.activeCamera.position.clone();
scene.registerBeforeRender(() => {
if (scene.activeCamera) {
skybox2.position = scene.activeCamera.position.clone();
}
});
}
return skybox2;
};
const skybox = createSkybox();
return () => {
if (skybox && !skybox.isDisposed()) {
skybox.dispose(false, true);
}
};
}, [isSceneReady, scene]);
};
var usePostProcessing = ({
scene,
camera,
isSceneReady,
options = {}
}) => {
const pipelineRef = useRef(null);
const ssaoRef = useRef(null);
const optionsRef = useRef(options);
optionsRef.current = options;
useEffect(() => {
if (!isSceneReady || !scene || !camera) return;
if (pipelineRef.current) {
pipelineRef.current.dispose();
pipelineRef.current = null;
}
if (ssaoRef.current) {
ssaoRef.current.dispose();
ssaoRef.current = null;
}
const {
enableFXAA = true,
enableMSAA = false,
enableBloom = false,
bloomIntensity = 0.15,
bloomThreshold = 0.1,
enableDOF = false,
enableSSAO = false,
enableImageProcessing = true,
contrast = 1.1,
exposure = 1.2
} = optionsRef.current;
const pipeline = new DefaultRenderingPipeline(
"defaultPipeline",
true,
// HDR
scene,
[camera]
);
pipelineRef.current = pipeline;
pipeline.fxaaEnabled = enableFXAA;
if (pipeline.fxaa) {
pipeline.fxaa.samples = 1;
}
scene.getEngine().setHardwareScalingLevel(1);
if (enableMSAA) {
const samples = 4;
scene.getEngine().setHardwareScalingLevel(1 / Math.sqrt(samples));
}
pipeline.bloomEnabled = enableBloom;
if (pipeline.bloomEnabled) {
pipeline.bloomThreshold = bloomThreshold;
pipeline.bloomWeight = bloomIntensity;
pipeline.bloomKernel = 12;
pipeline.bloomScale = 0.5;
}
pipeline.depthOfFieldEnabled = enableDOF;
if (pipeline.depthOfFieldEnabled && pipeline.depthOfField) {
pipeline.depthOfField.focalLength = 150;
pipeline.depthOfField.fStop = 2;
pipeline.depthOfField.focusDistance = 2e3;
pipeline.depthOfField.lensSize = 50;
}
pipeline.imageProcessingEnabled = enableImageProcessing;
if (pipeline.imageProcessing) {
pipeline.imageProcessing.contrast = contrast;
pipeline.imageProcessing.exposure = exposure;
pipeline.imageProcessing.toneMappingEnabled = true;
pipeline.imageProcessing.toneMappingType = 1;
pipeline.imageProcessing.vignetteEnabled = false;
}
if (enableSSAO) {
const ssao = new SSAO2RenderingPipeline(
"ssao",
scene,
{
ssaoRatio: 0.5,
// SSAO resolution (0.5 = half screen resolution)
blurRatio: 1
// Blur resolution
},
[camera]
);
ssaoRef.current = ssao;
ssao.radius = 2;
ssao.totalStrength = 0.4;
ssao.expensiveBlur = true;
ssao.samples = 16;
ssao.maxZ = 100;
}
return () => {
if (pipelineRef.current) {
pipelineRef.current.dispose();
pipelineRef.current = null;
}
if (ssaoRef.current) {
ssaoRef.current.dispose();
ssaoRef.current = null;
}
};
}, [isSceneReady, scene, camera]);
};
var ItemManipulationControls = ({ gizmoMode, onGizmoModeChange }) => {
return /* @__PURE__ */ jsxs("div", { style: {
position: "absolute",
top: "10px",
left: "10px",
display: "flex",
flexDirection: "column",
gap: "8px",
zIndex: 100
}, children: [
/* @__PURE__ */ jsx(
"button",
{
onClick: () => onGizmoModeChange?.("position"),
style: {
backgroundColor: gizmoMode === "position" ? "rgba(33, 150, 243, 0.8)" : "rgba(0, 0, 0, 0.15)",
color: "white",
border: gizmoMode === "position" ? "2px solid #2196F3" : "none",
borderRadius: "1px",
padding: "5px",
cursor: "pointer",
fontSize: "13px",
width: "50px",
height: "25px",
display: "flex",
alignItems: "center",
justifyContent: "center",
backdropFilter: "blur(10px)",
transition: "all 0.2s ease",
boxShadow: gizmoMode === "position" ? "0 0 10px rgba(33, 150, 243, 0.5)" : "none"
},
title: "Move Items (Position)",
onMouseOver: (e) => {
if (gizmoMode !== "position") {
e.target.style.backgroundColor = "rgba(33, 150, 243, 0.3)";
}
},
onMouseOut: (e) => {
if (gizmoMode !== "position") {
e.target.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
}
},
children: "Pos"
}
),
/* @__PURE__ */ jsx(
"button",
{
onClick: () => onGizmoModeChange?.("rotation"),
style: {
backgroundColor: gizmoMode === "rotation" ? "rgba(255, 152, 0, 0.8)" : "rgba(0, 0, 0, 0.15)",
color: "white",
border: gizmoMode === "rotation" ? "2px solid #FF9800" : "none",
borderRadius: "1px",
padding: "5px",
cursor: "pointer",
fontSize: "13px",
width: "50px",
height: "25px",
display: "flex",
alignItems: "center",
justifyContent: "center",
backdropFilter: "blur(10px)",
transition: "all 0.2s ease",
boxShadow: gizmoMode === "rotation" ? "0 0 10px rgba(255, 152, 0, 0.5)" : "none"
},
title: "Rotate Items",
onMouseOver: (e) => {
if (gizmoMode !== "rotation") {
e.target.style.backgroundColor = "rgba(255, 152, 0, 0.3)";
}
},
onMouseOut: (e) => {
if (gizmoMode !== "rotation") {
e.target.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
}
},
children: "Rot"
}
)
] });
};
var ItemManipulationControls_default = ItemManipulationControls;
function useAvatarMovement({
sceneRef,
cameraRef,
avatarRef,
touchMovement,
isSceneReady,
idleAnimRef,
walkAnimRef,
currentAnimRef,
allIdleAnimationsRef,
allWalkAnimationsRef,
allCurrentAnimationsRef,
AVATAR_BOUNDARY_LIMIT = 2.2,
CAMERA_TARGET_HEAD_OFFSET = 1
}) {
const avatarMovementStateRef = useRef({
isMoving: false,
targetPosition: null,
startPosition: null,
animationProgress: 0,
movementSpeed: 1.5,
totalDistance: 0,
targetRotation: 0,
startRotation: 0,
shouldRotate: false
});
const cameraFollowStateRef = useRef({
currentTarget: new Vector3(0, 1, 0),
dampingFactor: 0.1,
shouldFollowAvatar: false
});
const isRightMouseDownRef = useRef(false);
const animationBlendingRef = useRef({
isBlending: false,
blendDuration: 0.3,
blendProgress: 0,
fromAnimations: [],
toAnimations: [],
startTime: 0
});
const avatarMovementObserverRef = useRef(null);
const moveAvatarToPosition = useCallback((targetPosition, targetDisc) => {
if (!avatarRef.current || !sceneRef.current) return;
console.log("Moving avatar to position:", targetPosition);
const constrainedX = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, targetPosition.x));
const constrainedZ = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, targetPosition.z));
const targetPos = new Vector3(constrainedX, 0, constrainedZ);
const currentPos = avatarRef.current.position;
if (targetDisc) {
targetDisc.position = targetPos.clone();
targetDisc.position.y += 0.02;
targetDisc.isVisible = true;
}
const direction = targetPos.subtract(currentPos);
const targetRotationY = Math.atan2(direction.x, direction.z);
const distance = Vector3.Distance(currentPos, targetPos);
const currentRotY = avatarRef.current.rotation.y;
let rotDiff = targetRotationY - currentRotY;
if (rotDiff > Math.PI) rotDiff -= 2 * Math.PI;
if (rotDiff < -Math.PI) rotDiff += 2 * Math.PI;
const rotationDuration = 0.1;
let accumulatedRotationTime = 0;
sceneRef.current.registerBeforeRender(function rotateBeforeMove() {
const deltaTime = sceneRef.current.getEngine().getDeltaTime() / 1e3;
accumulatedRotationTime += deltaTime;
const completionRatio = Math.min(accumulatedRotationTime / rotationDuration, 1);
const newRotationY = currentRotY + rotDiff * completionRatio;
if (avatarRef.current) {
avatarRef.current.rotation.y = newRotationY;
}
if (completionRatio >= 1 && avatarRef.current) {
avatarRef.current.rotation.y = targetRotationY;
avatarMovementStateRef.current.startPosition = avatarRef.current.position.clone();
avatarMovementStateRef.current.targetPosition = targetPos;
avatarMovementStateRef.current.totalDistance = distance;
avatarMovementStateRef.current.startRotation = targetRotationY;
avatarMovementStateRef.current.targetRotation = targetRotationY;
avatarMovementStateRef.current.isMoving = true;
avatarMovementStateRef.current.shouldRotate = false;
avatarMovementStateRef.current.animationProgress = 0;
if (sceneRef.current) {
sceneRef.current.unregisterBeforeRender(rotateBeforeMove);
}
}
});
cameraFollowStateRef.current.shouldFollowAvatar = true;
}, [avatarRef, sceneRef, AVATAR_BOUNDARY_LIMIT]);
const resetAvatarMovement = useCallback(() => {
if (avatarRef.current) {
avatarRef.current.position = new Vector3(0, 0, 0);
avatarRef.current.rotation = new Vector3(0, 0, 0);
avatarMovementStateRef.current = {
isMoving: false,
targetPosition: null,
startPosition: null,
animationProgress: 0,
movementSpeed: 1.5,
totalDistance: 0,
targetRotation: 0,
startRotation: 0,
shouldRotate: false
};
}
}, [avatarRef]);
useEffect(() => {
if (!isSceneReady || !sceneRef.current || !avatarRef.current) return;
if (avatarMovementObserverRef.current) {
sceneRef.current.onBeforeRenderObservable.remove(avatarMovementObserverRef.current);
}
avatarMovementObserverRef.current = sceneRef.current.onBeforeRenderObservable.add(() => {
if (!avatarRef.current) return;
if (cameraFollowStateRef.current.currentTarget.equals(Vector3.Zero())) {
const headPosition = avatarRef.current.position.clone();
headPosition.y += CAMERA_TARGET_HEAD_OFFSET;
cameraFollowStateRef.current.currentTarget = headPosition;
}
let isMoving = false;
if (touchMovement && (Math.abs(touchMovement.x) > 1e-3 || Math.abs(touchMovement.y) > 1e-3)) {
isMoving = true;
const moveSpeed = 1.25;
const deltaTime = sceneRef.current.getEngine().getDeltaTime() / 1e3;
const moveX = touchMovement.x * moveSpeed * deltaTime;
const moveZ = -touchMovement.y * moveSpeed * deltaTime;
if (Math.abs(moveX) > 1e-3 || Math.abs(moveZ) > 1e-3) {
const targetRotationY = Math.atan2(moveX, moveZ);
const currentRotY = avatarRef.current.rotation.y;
let rotDiff = targetRotationY - currentRotY;
if (rotDiff > Math.PI) rotDiff -= 2 * Math.PI;
if (rotDiff < -Math.PI) rotDiff += 2 * Math.PI;
const rotationDuration = 0.1;
const elapsedTime = Math.min(deltaTime, rotationDuration);
const completionRatio = elapsedTime / rotationDuration;
if (completionRatio < 1) {
avatarRef.current.rotation.y += rotDiff * completionRatio;
} else {
avatarRef.current.rotation.y = targetRotationY;
}
}
const newX = avatarRef.current.position.x + moveX;
const newZ = avatarRef.current.position.z + moveZ;
avatarRef.current.position.x = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, newX));
avatarRef.current.position.z = Math.max(-AVATAR_BOUNDARY_LIMIT, Math.min(AVATAR_BOUNDARY_LIMIT, newZ));
}
const movementState = avatarMovementStateRef.current;
if (movementState.isMoving && movementState.targetPosition && movementState.startPosition) {
isMoving = true;
const deltaTime = sceneRef.current.getEngine().getDeltaTime() / 1e3;
if (movementState.totalDistance > 0) {
const progressIncrement = movementState.movementSpeed * deltaTime / movementState.totalDistance;
movementState.animationProgress += progressIncrement;
} else {
movementState.animationProgress = 1;
}
if (movementState.animationProgress >= 1) {
avatarRef.current.position.copyFrom(movementState.targetPosition);
movementState.isMoving = false;
movementState.shouldRotate = false;
movementState.targetPosition = null;
movementState.startPosition = null;
movementState.totalDistance = 0;
movementState.animationProgress = 0;
} else {
const currentPos = Vector3.Lerp(
movementState.startPosition,
movementState.targetPosition,
movementState.animationProgress
);
avatarRef.current.position.copyFrom(currentPos);
if (movementState.shouldRotate) {
let startRot = movementState.startRotation;
let targetRot = movementState.targetRotation;
let diff = targetRot - startRot;
if (diff > Math.PI) {
startRot += 2 * Math.PI;
} else if (diff < -Math.PI) {
targetRot += 2 * Math.PI;
}
const currentRotY = startRot + (targetRot - startRot) * movementState.animationProgress;
avatarRef.current.rotation.y = currentRotY;
}
if (cameraRef.current && cameraFollowStateRef.current.shouldFollowAvatar) {
const headPosition = avatarRef.current.position.clone();
headPosition.y += CAMERA_TARGET_HEAD_OFFSET;
cameraRef.current.setTarget(headPosition);
}
}
}
const blendState = animationBlendingRef.current;
const currentTime = performance.now() / 1e3;
if (blendState.isBlending) {
const elapsedTime = currentTime - blendState.startTime;
blendState.blendProgress = Math.min(elapsedTime / blendState.blendDuration, 1);
if (blendState.fromAnimations.length > 0 && blendState.toAnimations.length > 0) {
const fromWeight = 1 - blendState.blendProgress;
const toWeight = blendState.blendProgress;
blendState.fromAnimations.forEach((anim) => {
if (anim && !anim.isDisposed) {
anim.setWeightForAllAnimatables(fromWeight);
}
});
blendState.toAnimations.forEach((anim) => {
if (anim && !anim.isDisposed) {
anim.setWeightForAllAnimatables(toWeight);
}
});
}
if (blendState.blendProgress >= 1) {
blendState.fromAnimations.forEach((anim) => {
if (anim && !anim.isDisposed) {
anim.stop();
anim.setWeightForAllAnimatables(0);
}
});
blendState.toAnimations.forEach((anim) => {
if (anim && !anim.isDisposed) {
anim.setWeightForAllAnimatables(1);
}
});
allCurrentAnimationsRef.current = [...blendState.toAnimations];
currentAnimRef.current = blendState.toAnimations[0];
blendState.isBlending = false;
blendState.fromAnimations = [];
blendState.toAnimations = [];
blendState.blendProgress = 0;
console.log(`\u2705 Animation blend completed for ${allCurrentAnimationsRef.current.length} parts`);
}
}
const targetAnimations = isMoving ? allWalkAnimationsRef.current : allIdleAnimationsRef.current;
const targetMainAnimation = isMoving ? walkAnimRef.current : idleAnimRef.current;
if (targetMainAnimation && targetMainAnimation !== currentAnimRef.current && !blendState.isBlending && targetAnimations.length > 0) {
const animationType = isMoving ? "walk" : "idle";
console.log(`\u{1F3AD} Starting blend to ${animationType} animations: ${targetAnimations.length} parts`);
blendState.isBlending = true;
blendState.fromAnimations = [...allCurrentAnimationsRef.current];
blendState.toAnimations = [...targetAnimations];
blendState.blendProgress = 0;
blendState.startTime = currentTime;
blendState.toAnimations.forEach((anim) => {
if (anim && !anim.isDisposed) {
anim.setWeightForAllAnimatables(0);
anim.play(true);
}
});
blendState.fromAnimations.forEach((anim) => {
if (anim && !anim.isDisposed) {
anim.setWeightForAllAnimatables(1);
}
});
console.log(`\u{1F3AC} Started blending ${blendState.fromAnimations.length} \u2192 ${blendState.toAnimations.length} animations`);
}
if (cameraRef.current && avatarRef.current && !isRightMouseDownRef.current && cameraFollowStateRef.current.shouldFollowAvatar) {
const cameraFollowState = cameraFollowStateRef.current;
const avatarHeadPosition = avatarRef.current.position.clone();
avatarHeadPosition.y += CAMERA_TARGET_HEAD_OFFSET;
cameraFollowState.currentTarget = Vector3.Lerp(
cameraFollowState.currentTarget,
avatarHeadPosition,
cameraFollowState.dampingFactor
);
cameraRef.current.setTarget(cameraFollowState.currentTarget);
}
});
return () => {
if (avatarMovementObserverRef.current && sceneRef.current) {
sceneRef.current.onBeforeRenderObservable.remove(avatarMovementObserverRef.current);
avatarMovementObserverRef.current = null;
}
};
}, [isSceneReady, touchMovement, sceneRef, avatarRef, cameraRef, AVATAR_BOUNDARY_LIMIT, CAMERA_TARGET_HEAD_OFFSET, idleAnimRef, walkAnimRef, currentAnimRef, allIdleAnimationsRef, allWalkAnimationsRef, allCurrentAnimationsRef]);
return {
// State refs
avatarMovementStateRef,
cameraFollowStateRef,
isRightMouseDownRef,
animationBlendingRef,
avatarMovementObserverRef,
// Functions
moveAvatarToPosition,
resetAvatarMovement,
// Constants
AVATAR_BOUNDARY_LIMIT,
CAMERA_TARGET_HEAD_OFFSET
};
}
var SceneControlButtons = ({
onReset,
onToggleFullscreen,
onToggleUIOverlay,
isFullscreen
}) => {
return /* @__PURE__ */ jsxs(
"div",
{
style: {
position: "absolute",
top: "10px",
left: "0",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 100,
gap: "10px"
},
children: [
/* @__PURE__ */ jsx(
"button",
{
onClick: onReset,
style: {
backgroundColor: "rgba(0, 0, 0, 0.3)",
color: "white",
border: "none",
borderRadius: "4px",
padding: "8px",
cursor: "pointer",
fontSize: "16px",
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
backdropFilter: "blur(2px)",
transition: "background-color 0.2s ease",
outline: "none"
},
title: "Reset Camera and Avatar",
children: "\u{1F504}"
}
),
/* @__PURE__ */ jsx(
"button",
{
onClick: onToggleFullscreen,
style: {
backgroundColor: "rgba(0, 0, 0, 0.3)",
color: "white",
border: "none",
borderRadius: "4px",
padding: "8px",
cursor: "pointer",
fontSize: "16px",
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
backdropFilter: "blur(2px)",
transition: "background-color 0.2s ease",
outline: "none"
},
title: isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen",
children: isFullscreen ? "\u2913" : "\u2922"
}
),
/* @__PURE__ */ jsx(
"button",
{
onClick: onToggleUIOverlay,
style: {
backgroundColor: "rgba(0, 0, 0, 0.3)",
color: "white",
border: "none",
borderRadius: "4px",
padding: "8px",
cursor: "pointer",
fontSize: "16px",
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
backdropFilter: "blur(2px)",
transition: "background-color 0.2s ease",
outline: "none"
},
title: "Toggle UI Controls",
children: "\u2699"
}
)
]
}
);
};
var SceneControlButtons_default = SceneControlButtons;
// src/data/avatarPartsData.ts
var availablePartsData = {
male: {
fixedParts: {
body: "/models/male/male_body/male_body.glb"
},
selectableParts: {
hair: [
{ name: "No Hair", fileName: null },
{ name: "Hair Style 1", fileName: "/models/male/male_hair/male_hair_001.glb" },
{ name: "Hair Style 2", fileName: "/models/male/male_hair/male_hair_002.glb" },
{ name: "Hair Style 3", fileName: "/models/male/male_hair/male_hair_003.glb" }
],
top: [
{ name: "Male T-Shirt 1", fileName: "/models/male/male_top/male_top_001.glb" },
{ name: "Male Jacket 1", fileName: "/models/male/male_top/male_top_002.glb" }
],
bottom: [
{ name: "Male Pant 1", fileName: "/models/male/male_bottom/male_bottom_001.glb" }
],
shoes: [
{ name: "No Shoes", fileName: null },
{ name: "Shoes 1", fileName: "/models/male/male_shoes/male_shoes_001.glb" },
{ name: "Shoes 2", fileName: "/models/male/male_shoes/male_shoes_002.glb" },
{ name: "Shoes 3", fileName: "/models/male/male_shoes/male_shoes_003.glb" }
],
fullset: [
{ name: "No Fullset", fileName: null },
{ name: "Fullset 1", fileName: "/models/male/male_fullset/male_fullset_001.glb" },
{ name: "Fullset 2", fileName: "/models/male/male_fullset/male_fullset_002.glb" },
{ name: "Fullset 3", fileName: "/models/male/male_fullset/male_fullset_003.glb" },
{ name: "Fullset 4", fileName: "/models/male/male_fullset/male_fullset_004.glb" },
{ name: "Fullset 5", fileName: "/models/male/male_fullset/male_fullset_005.glb" }
],
accessory: [
{ name: "No Accessory", fileName: null },
{ name: "Earing 1", fileName: "/models/male/male_acc/male_acc_001.glb" },
{ name: "Glasses 1", fileName: "/models/male/male_acc/male_acc_002.glb" }
]
},
defaultColors: {
hair: "#4A301B",
top: "#1E90FF"
}
},
female: {
fixedParts: {
body: "/models/female/female_body/female_body.glb"
},
selectableParts: {
hair: [
{ name: "No Hair", fileName: null },
{ name: "Hair Style 1", fileName: "/models/female/female_hair/female_hair_001.glb" },
{ name: "Hair Style 2", fileName: "/models/female/female_hair/female_hair_002.glb" }
],
top: [
{ name: "Blazer", fileName: "/models/female/female_top/female_top_001.glb" },
{ name: "Shirt", fileName: "/models/female/female_top/female_top_002.glb" }
],
bottom: [
{ name: "Cute Pants", fileName: "/models/female/female_bottom/female_bottom_001.glb" },
{ name: "Flower Pants", fileName: "/models/female/female_bottom/female_bottom_002.glb" }
],
shoes: [
{ name: "No Shoes", fileName: null },
{ name: "Sport Shoes", fileName: "/models/female/female_shoes/female_shoes_001.glb" }
],
fullset: [
{ name: "No Fullset", fileName: null }
],
accessory: [
{ name: "No Accessory", fileName: null },
{ name: "Sun Glasses", fileName: "/models/female/female_acc/female_acc_001.glb" },
{ name: "Earing Set", fileName: "/models/female/female_acc/female_acc_002.glb" },
{ name: "