From 7c62bb0908db6bf324d2d92877d275f7c9e6306b Mon Sep 17 00:00:00 2001 From: Lilia_Chen Date: Tue, 22 Jul 2025 05:27:27 +0100 Subject: [PATCH] feat(stage-ui): VRM model setting: camera psotion (view scale) (#309) --- packages/i18n/src/locales/en/settings.yaml | 1 + .../i18n/src/locales/zh-Hans/settings.yaml | 1 + .../Scenarios/Settings/ModelSettings/VRM.vue | 69 +++++++------------ .../stage-ui/src/components/Scenes/VRM.vue | 37 +++++++++- .../src/components/Scenes/VRM/Model.vue | 16 ++--- .../components/Scenes/VRM/OrbitControls.vue | 11 +++ packages/stage-ui/src/stores/vrm.ts | 2 + 7 files changed, 83 insertions(+), 54 deletions(-) diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index c792f3240..97338f3a1 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -566,6 +566,7 @@ vrm: z: Z Offset fov: FOV (degree) rotation-y: Rotation (Y-axis) + camera-distance: Camera distance switch-to-vrm: title: Switch to Live2D Avatar? change-to-vrm: Click here to switch to the Live2D avatar setting diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index c08b53358..a67c5c3ae 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -542,6 +542,7 @@ vrm: z: Z轴偏移 fov: 视角调整(度) rotation-y: 模型朝向(Y轴旋转) + camera-distance: 相机距离(画面缩放) switch-to-vrm: title: 想切换至Live2D虚拟形象? change-to-vrm: 切换至Live2D虚拟形象设定页面 diff --git a/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/VRM.vue b/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/VRM.vue index 2b63670bb..b898c410e 100644 --- a/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/VRM.vue +++ b/packages/stage-ui/src/components/Scenarios/Settings/ModelSettings/VRM.vue @@ -36,6 +36,7 @@ const { cameraFOV, selectedModel, modelRotationY, + cameraDistance, } = storeToRefs(vrm) const localModelUrl = ref(modelUrl.value) @@ -66,6 +67,12 @@ function urlUploadClick() { loadingModel.value = true localModelUrl.value = selectedModel.value } + +function resetCameraDistance() { + // Calculate the camera distance that can fit the up-2/3 part of the model in the view + const radians = (cameraFOV.value / 2 * Math.PI) / 180 + cameraDistance.value = (modelSize.value.y / 3) / Math.tan(radians) +} + + + >() const camera = shallowRef(new THREE.PerspectiveCamera()) +const controlsRef = ref>() onMounted(() => { if (vrmContainerRef.value) { @@ -64,6 +66,39 @@ watch(selectedModel, () => { } }) +// Bidirectional watch between slider and OrbitControls +watch(() => controlsRef.value?.getDistance(), (newDistance) => { + if (newDistance !== undefined) { + // To avoid floating point inaccuracies causing a feedback loop with the other watcher, + // we can check if the distance has changed significantly. + if (Math.abs(cameraDistance.value - newDistance) > 1e-6) { + cameraDistance.value = newDistance + initialCameraPosition.value = { + x: camera.value.position.x, + y: camera.value.position.y, + z: camera.value.position.z, + } + } + } +}) +watch(cameraDistance, (newDistance) => { + if (camera.value && controlsRef.value) { + const newPosition = new THREE.Vector3() + newPosition.copy(camera.value.position.normalize().multiplyScalar(newDistance)) + camera.value.position.set( + newPosition.x, + newPosition.y, + newPosition.z, + ) + controlsRef.value.update() + initialCameraPosition.value = { + x: newPosition.x, + y: newPosition.y, + z: newPosition.z, + } + } +}) + defineExpose({ setExpression: (expression: string) => { modelRef.value?.setExpression(expression) @@ -77,6 +112,7 @@ defineExpose({ + - diff --git a/packages/stage-ui/src/components/Scenes/VRM/Model.vue b/packages/stage-ui/src/components/Scenes/VRM/Model.vue index 1a8631f95..50e081eb1 100644 --- a/packages/stage-ui/src/components/Scenes/VRM/Model.vue +++ b/packages/stage-ui/src/components/Scenes/VRM/Model.vue @@ -161,14 +161,6 @@ watch(modelRotationY, (newRotationY) => { } }) -onUnmounted(() => { - disposeBeforeRenderLoop?.() - if (vrm.value) { - vrm.value.scene.removeFromParent() - VRMUtils.deepDispose(vrm.value.scene) - } -}) - defineExpose({ setExpression(expression: string) { vrmEmote.value?.setEmotionWithResetAfter(expression, 1000) @@ -181,6 +173,14 @@ const { pause, resume } = useLoop() watch(() => props.paused, (value) => { value ? pause() : resume() }) + +onUnmounted(() => { + disposeBeforeRenderLoop?.() + if (vrm.value) { + vrm.value.scene.removeFromParent() + VRMUtils.deepDispose(vrm.value.scene) + } +})