feat(stage-ui): VRM model setting: camera psotion (view scale) (#309)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -542,6 +542,7 @@ vrm:
|
||||
z: Z轴偏移
|
||||
fov: 视角调整(度)
|
||||
rotation-y: 模型朝向(Y轴旋转)
|
||||
camera-distance: 相机距离(画面缩放)
|
||||
switch-to-vrm:
|
||||
title: 想切换至Live2D虚拟形象?
|
||||
change-to-vrm: 切换至Live2D虚拟形象设定页面
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -127,51 +134,6 @@ function urlUploadClick() {
|
||||
{{ t('settings.vrm.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</Section>
|
||||
<Section
|
||||
:title="t('settings.vrm.change-model.title')"
|
||||
icon="i-solar:magic-stick-3-bold-duotone"
|
||||
inner-class="text-sm"
|
||||
:class="[
|
||||
'rounded-xl',
|
||||
'bg-white/80 dark:bg-black/75',
|
||||
'backdrop-blur-lg',
|
||||
]"
|
||||
>
|
||||
<Button
|
||||
variant="secondary" @click=" () => {
|
||||
modelFileDialog.reset()
|
||||
modelFileDialog.open()
|
||||
}"
|
||||
>
|
||||
{{ t('settings.vrm.change-model.from-file') }}...
|
||||
</Button>
|
||||
<div flex items-center gap-2>
|
||||
<Input
|
||||
v-model="localModelUrl"
|
||||
:disabled="loadingModel"
|
||||
class="flex-1"
|
||||
:placeholder="t('settings.vrm.change-model.from-url-placeholder')"
|
||||
/>
|
||||
<Button size="sm" variant="secondary" @click="urlUploadClick">
|
||||
{{ t('settings.vrm.change-model.from-url') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Section>
|
||||
<Section
|
||||
:title="t('settings.vrm.theme-color-from-model.title')"
|
||||
icon="i-solar:magic-stick-3-bold-duotone"
|
||||
inner-class="text-sm"
|
||||
:class="[
|
||||
'rounded-xl',
|
||||
'bg-white/80 dark:bg-black/75',
|
||||
'backdrop-blur-lg',
|
||||
]"
|
||||
>
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.vrm.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</Section>
|
||||
<Section
|
||||
:title="t('settings.vrm.scale-and-position.title')"
|
||||
icon="i-solar:scale-bold-duotone"
|
||||
@@ -281,6 +243,23 @@ function urlUploadClick() {
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
<FieldRange
|
||||
v-model="cameraDistance"
|
||||
as="div"
|
||||
:min="modelSize.z"
|
||||
:max="modelSize.z * 20"
|
||||
:step="modelSize.z / 100"
|
||||
:label="t('settings.vrm.scale-and-position.camera-distance')"
|
||||
>
|
||||
<template #label>
|
||||
<div flex items-center>
|
||||
<div>{{ t('settings.vrm.scale-and-position.camera-distance') }}</div>
|
||||
<button px-2 text-xs outline-none title="Reset value to default" @click="resetCameraDistance">
|
||||
<div i-solar:forward-linear transform-scale-x--100 text="neutral-500 dark:neutral-400" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FieldRange>
|
||||
<FieldRange
|
||||
:model-value="modelRotationY"
|
||||
as="div"
|
||||
|
||||
@@ -19,11 +19,13 @@ const {
|
||||
selectedModel,
|
||||
cameraFOV,
|
||||
initialCameraPosition,
|
||||
cameraDistance,
|
||||
} = storeToRefs(useVRM())
|
||||
|
||||
const modelRef = ref<InstanceType<typeof VRMModel>>()
|
||||
|
||||
const camera = shallowRef(new THREE.PerspectiveCamera())
|
||||
const controlsRef = ref<InstanceType<typeof OrbitControls>>()
|
||||
|
||||
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({
|
||||
<TresAxesHelper :size="1" />
|
||||
<TresDirectionalLight :color="0xFFFFFF" :intensity="1.2" :position="[1, 1, 1]" />
|
||||
<TresAmbientLight :color="0xFFFFFF" :intensity="1.5" />
|
||||
<OrbitControls ref="controlsRef" />
|
||||
<VRMModel
|
||||
ref="modelRef"
|
||||
:key="selectedModel"
|
||||
@@ -87,7 +123,6 @@ defineExpose({
|
||||
@error="(val) => emit('error', val)"
|
||||
/>
|
||||
<!-- <TresPerspectiveCamera :position="[initialCameraPosition.x, initialCameraPosition.y, initialCameraPosition.z]" :fov="cameraFOV"/> -->
|
||||
<OrbitControls />
|
||||
</TresCanvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -14,6 +14,7 @@ const { camera, renderer } = useTresContext()
|
||||
const controls = shallowRef<OrbitControls>()
|
||||
const {
|
||||
modelSize,
|
||||
cameraDistance,
|
||||
} = storeToRefs(useVRM())
|
||||
|
||||
// initialize the OrbitControls
|
||||
@@ -35,11 +36,21 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
controls.value.enablePan = false
|
||||
// How near and far the camera can zoom
|
||||
controls.value.minDistance = modelSize.value.z
|
||||
controls.value.maxDistance = modelSize.value.z * 20
|
||||
|
||||
controls.value.update()
|
||||
|
||||
cameraDistance.value = controls.value.getDistance()
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
controls,
|
||||
getDistance: () => controls.value?.getDistance(),
|
||||
update: () => controls.value?.update(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -31,6 +31,7 @@ export const useVRM = defineStore('vrm', () => {
|
||||
|
||||
const modelObjectUrl = ref<string>()
|
||||
const modelRotationY = useLocalStorage('settings/vrm/modelRotationY', 0)
|
||||
const cameraDistance = useLocalStorage('settings/vrm/cameraDistance', 0)
|
||||
|
||||
// Manage the object URL lifecycle to prevent memory leaks
|
||||
watch(modelFile, (newFile) => {
|
||||
@@ -70,5 +71,6 @@ export const useVRM = defineStore('vrm', () => {
|
||||
cameraFOV,
|
||||
initialCameraPosition,
|
||||
modelRotationY,
|
||||
cameraDistance,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user