UNPKG

myroom-react-private

Version:

React components for MyRoom 3D scene integration with Babylon.js

1,283 lines (1,248 loc) 843 kB
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: "