refactor(stage-ui): VRM & Live2D view load

This commit is contained in:
Neko Ayaka
2025-08-16 22:00:03 +08:00
parent 33501a0857
commit c9d166cfe8
6 changed files with 108 additions and 11 deletions
@@ -50,6 +50,7 @@ modelFileDialog.onChange((files) => {
motionMap.value = {}
modelFile.value = files[0]
loadingModel.value = true
live2d.shouldUpdateView()
}
})
@@ -64,6 +64,7 @@ const trackingOptions = computed(() => [
modelFileDialog.onChange((files) => {
if (files && files.length > 0) {
modelFile.value = files[0]
vrm.shouldUpdateView()
}
})
@@ -151,7 +151,12 @@ async function loadModel() {
}
const modelInstance = new Live2DModel<PixiLive2DInternalModel>()
await Live2DFactory.setupLive2DModel(modelInstance, modelSrcNormalized.value, { autoInteract: false })
if (modelFile.value) {
await Live2DFactory.setupLive2DModel(modelInstance, [modelFile.value], { autoInteract: false })
}
else {
await Live2DFactory.setupLive2DModel(modelInstance, modelSrcNormalized.value, { autoInteract: false })
}
model.value = modelInstance
pixiApp.value.stage.addChild(model.value)
@@ -270,7 +275,7 @@ function updateDropShadowFilter() {
}
watch([() => props.width, () => props.height], () => handleResize())
watch(modelSrcNormalized, () => loadModel(), { immediate: true })
watch(modelSrcNormalized, () => loadModel())
watch(dark, updateDropShadowFilter, { immediate: true })
watch([model, themeColorsHue], updateDropShadowFilter)
watch(offset, setScaleAndPosition)
@@ -15,7 +15,13 @@ export async function loadVrm(model: string, options?: {
scene?: Scene
lookAt?: boolean
onProgress?: (progress: ProgressEvent<EventTarget>) => void | Promise<void>
}): Promise<{ _vrm: VRMCore, _vrmGroup: Group, modelCenter: Vector3, modelSize: Vector3, initialCameraOffset: Vector3 } | undefined> {
}): Promise<{
_vrm: VRMCore
_vrmGroup: Group
modelCenter: Vector3
modelSize: Vector3
initialCameraOffset: Vector3
} | undefined> {
const loader = useVRMLoader()
const gltf = await loader.loadAsync(model, progress => options?.onProgress?.(progress))
+58 -3
View File
@@ -1,13 +1,65 @@
import localforage from 'localforage'
import { computedAsync, useLocalStorage } from '@vueuse/core'
import { useBroadcastChannel, useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
type BroadcastChannelEvents
= | BroadcastChannelEventShouldUpdateView
interface BroadcastChannelEventShouldUpdateView {
type: 'should-update-view'
}
export const useLive2d = defineStore('live2d', () => {
const { post, data } = useBroadcastChannel<BroadcastChannelEvents, BroadcastChannelEvents>({ name: 'airi-stores-live2d' })
const shouldUpdateViewHooks = ref<Array<() => void>>([])
const onShouldUpdateView = (hook: () => void) => {
shouldUpdateViewHooks.value.push(hook)
}
function shouldUpdateView() {
post({ type: 'should-update-view' })
shouldUpdateViewHooks.value.forEach(hook => hook())
}
watch(data, (event) => {
if (event.type === 'should-update-view') {
shouldUpdateViewHooks.value.forEach(hook => hook())
}
})
const indexedDbModelFile = ref<File | null>(null)
onMounted(async () => await loadModelFileFromIndexedDb())
onShouldUpdateView(async () => await loadModelFileFromIndexedDb())
async function loadModelFileFromIndexedDb() {
const file = await localforage.getItem<File>('assets-models-live2d')
if (file) {
indexedDbModelFile.value = file
}
}
const modelFile = computed({
get: () => {
return indexedDbModelFile.value
},
set: (file: File | null) => {
if (file) {
localforage.setItem('assets-models-live2d', file)
}
else {
localforage.removeItem('assets-models-live2d')
}
indexedDbModelFile.value = file
},
})
const defaultModelUrl = '/assets/live2d/models/hiyori_pro_zh.zip'
const modelUrl = useLocalStorage<string>('settings/live2d/model-src', defaultModelUrl)
const modelFile = computedAsync(async () => localforage.getItem<File>('assets-models-live2d'))
const loadingModel = ref(false) // if set to true, the model will be loaded
const position = useLocalStorage('settings/live2d/position', { x: 0, y: 0 }) // position is relative to the center of the screen, units are %
@@ -30,5 +82,8 @@ export const useLive2d = defineStore('live2d', () => {
availableMotions,
motionMap,
scale,
onShouldUpdateView,
shouldUpdateView,
}
})
+34 -5
View File
@@ -1,20 +1,48 @@
import localforage from 'localforage'
import { useLocalStorage } from '@vueuse/core'
import { useBroadcastChannel, useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import defaultSkyBoxSrc from '../components/Scenes/Tres/assets/sky_linekotsi_23_HDRI.hdr?url'
type BroadcastChannelEvents
= | BroadcastChannelEventShouldUpdateView
interface BroadcastChannelEventShouldUpdateView {
type: 'should-update-view'
}
export const useVRM = defineStore('vrm', () => {
const { post, data } = useBroadcastChannel<BroadcastChannelEvents, BroadcastChannelEvents>({ name: 'airi-stores-vrm' })
const shouldUpdateViewHooks = ref<Array<() => void | Promise<void>>>([])
const onShouldUpdateView = (hook: () => void | Promise<void>) => {
shouldUpdateViewHooks.value.push(hook)
}
function shouldUpdateView() {
post({ type: 'should-update-view' })
shouldUpdateViewHooks.value.forEach(hook => hook())
}
watch(data, (event) => {
if (event.type === 'should-update-view') {
shouldUpdateViewHooks.value.forEach(hook => hook())
}
})
const indexedDbModelFile = ref<File | null>(null)
onMounted(async () => {
async function loadModelFileFromIndexedDb() {
const file = await localforage.getItem<File>('assets-models-vrm')
if (file) {
indexedDbModelFile.value = file
}
})
}
onMounted(async () => loadModelFileFromIndexedDb())
// onShouldUpdateView(() => loadModelFileFromIndexedDb())
const modelFile = computed({
get: () => {
@@ -121,9 +149,10 @@ export const useVRM = defineStore('vrm', () => {
isTracking,
trackingMode,
eyeHeight,
envSelect,
skyBoxSrc,
shouldUpdateView,
onShouldUpdateView,
}
})