fix(stage-ui-three): model offset and camera position setting is now persistent in stage-web app (#1543)
This commit is contained in:
@@ -110,6 +110,7 @@ import {
|
||||
const props = withDefaults(defineProps<{
|
||||
currentAudioSource?: AudioBufferSourceNode
|
||||
modelSrc?: string
|
||||
loadReason: VrmLifecycleReason
|
||||
idleAnimation: string
|
||||
// loadAnimations?: string[]
|
||||
paused?: boolean
|
||||
@@ -149,6 +150,7 @@ const emit = defineEmits<{
|
||||
const {
|
||||
currentAudioSource,
|
||||
modelSrc,
|
||||
loadReason,
|
||||
idleAnimation,
|
||||
// loadAnimations, // TBC
|
||||
paused,
|
||||
@@ -660,6 +662,11 @@ function buildSceneBootstrap(activeVrm: VRM, cacheHit: boolean): SceneBootstrap
|
||||
cameraPosition: { x: cameraPosition.x, y: cameraPosition.y, z: cameraPosition.z },
|
||||
eyeHeight: eyePositionY,
|
||||
lookAtTarget: defaultTookAt(eyePositionY),
|
||||
modelOffset: {
|
||||
x: bootstrapRoot.position.x,
|
||||
y: bootstrapRoot.position.y,
|
||||
z: bootstrapRoot.position.z,
|
||||
},
|
||||
modelOrigin: { x: modelCenter.x, y: modelCenter.y, z: modelCenter.z },
|
||||
modelSize: { x: modelSize.x, y: modelSize.y, z: modelSize.z },
|
||||
}
|
||||
@@ -667,7 +674,7 @@ function buildSceneBootstrap(activeVrm: VRM, cacheHit: boolean): SceneBootstrap
|
||||
|
||||
async function loadModel() {
|
||||
const requestId = invalidatePendingLoads()
|
||||
const loadReason: VrmLifecycleReason = vrmGroup.value ? 'model-switch' : 'initial-load'
|
||||
const currentLoadReason = loadReason.value
|
||||
const loadStartedAt = performance.now()
|
||||
let nextVrm: VRM | undefined
|
||||
let nextVrmGroup: Group | undefined
|
||||
@@ -689,7 +696,7 @@ async function loadModel() {
|
||||
if (isStageThreeRuntimeTraceEnabled()) {
|
||||
stageThreeRuntimeTraceContext.emit(stageThreeTraceVrmLoadStartEvent, {
|
||||
modelSrc: modelSrc.value,
|
||||
reason: loadReason,
|
||||
reason: currentLoadReason,
|
||||
rendererMemory: createThreeRendererMemorySnapshot(getRendererInstance()),
|
||||
sceneSummary: createVrmSceneSummarySnapshot(),
|
||||
ts: loadStartedAt,
|
||||
@@ -701,11 +708,11 @@ async function loadModel() {
|
||||
const reusableInstance = takeManagedVrmInstance(getManagedVrmScopeKey(), modelSrc.value)
|
||||
if (reusableInstance) {
|
||||
if (!isManagedVrmInstanceReusable(reusableInstance)) {
|
||||
destroyManagedVrmInstanceWithHooks(reusableInstance, loadReason)
|
||||
destroyManagedVrmInstanceWithHooks(reusableInstance, currentLoadReason)
|
||||
}
|
||||
else {
|
||||
if (!isLoadRequestCurrent(requestId)) {
|
||||
destroyManagedVrmInstanceWithHooks(stashManagedVrmInstance(reusableInstance), loadReason)
|
||||
destroyManagedVrmInstanceWithHooks(stashManagedVrmInstance(reusableInstance), currentLoadReason)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -717,14 +724,14 @@ async function loadModel() {
|
||||
if (!airiIblProbe && scene.value)
|
||||
airiIblProbe = createIblProbeController(scene.value)
|
||||
|
||||
if (loadReason === 'model-switch') {
|
||||
if (currentLoadReason === 'model-switch') {
|
||||
componentCleanUp('model-switch', { invalidate: false })
|
||||
}
|
||||
|
||||
runVrmLoadHooks({
|
||||
cacheHit: true,
|
||||
camera: camera.value,
|
||||
reason: loadReason,
|
||||
reason: currentLoadReason,
|
||||
vrm: reusableInstance.vrm,
|
||||
vrmGroup: reusableInstance.group,
|
||||
})
|
||||
@@ -736,7 +743,7 @@ async function loadModel() {
|
||||
stageThreeRuntimeTraceContext.emit(stageThreeTraceVrmLoadEndEvent, {
|
||||
durationMs: performance.now() - loadStartedAt,
|
||||
modelSrc: modelSrc.value,
|
||||
reason: loadReason,
|
||||
reason: currentLoadReason,
|
||||
rendererMemory: createThreeRendererMemorySnapshot(getRendererInstance()),
|
||||
sceneSummary: createVrmSceneSummarySnapshot({ mixer: reusableInstance.mixer, vrm: reusableInstance.vrm }),
|
||||
ts: performance.now(),
|
||||
@@ -755,7 +762,7 @@ async function loadModel() {
|
||||
})
|
||||
if (!_vrmInfo || !_vrmInfo._vrm || !_vrmInfo._vrmGroup) {
|
||||
if (isLoadRequestCurrent(requestId)) {
|
||||
emitVrmLoadError(loadReason, loadStartedAt, 'VRM model loading failure')
|
||||
emitVrmLoadError(currentLoadReason, loadStartedAt, 'VRM model loading failure')
|
||||
console.warn('VRM model loading failure!')
|
||||
emit('error', new Error('VRM model loading failure'))
|
||||
}
|
||||
@@ -776,7 +783,7 @@ async function loadModel() {
|
||||
runVrmLoadHooks({
|
||||
cacheHit: false,
|
||||
camera: camera.value,
|
||||
reason: loadReason,
|
||||
reason: currentLoadReason,
|
||||
vrm: _vrm,
|
||||
vrmGroup: _vrmGroup,
|
||||
})
|
||||
@@ -792,7 +799,7 @@ async function loadModel() {
|
||||
}
|
||||
if (!clip) {
|
||||
disposeDetachedVrm(nextVrm, nextVrmGroup)
|
||||
emitVrmLoadError(loadReason, loadStartedAt, 'No VRM animation loaded')
|
||||
emitVrmLoadError(currentLoadReason, loadStartedAt, 'No VRM animation loaded')
|
||||
console.warn('No VRM animation loaded')
|
||||
if (isLoadRequestCurrent(requestId))
|
||||
emit('error', new Error('No VRM animation loaded'))
|
||||
@@ -865,7 +872,7 @@ async function loadModel() {
|
||||
material: mat,
|
||||
materialIndex,
|
||||
mesh: child,
|
||||
reason: loadReason,
|
||||
reason: currentLoadReason,
|
||||
vrm: _vrm,
|
||||
vrmGroup: _vrmGroup,
|
||||
})
|
||||
@@ -873,7 +880,7 @@ async function loadModel() {
|
||||
}
|
||||
})
|
||||
|
||||
if (loadReason === 'model-switch') {
|
||||
if (currentLoadReason === 'model-switch') {
|
||||
componentCleanUp('model-switch', { invalidate: false })
|
||||
}
|
||||
|
||||
@@ -891,7 +898,7 @@ async function loadModel() {
|
||||
stageThreeRuntimeTraceContext.emit(stageThreeTraceVrmLoadEndEvent, {
|
||||
durationMs: performance.now() - loadStartedAt,
|
||||
modelSrc: modelSrc.value,
|
||||
reason: loadReason,
|
||||
reason: currentLoadReason,
|
||||
rendererMemory: createThreeRendererMemorySnapshot(getRendererInstance()),
|
||||
sceneSummary: createVrmSceneSummarySnapshot({ mixer: vrmAnimationMixer.value, vrm: _vrm }),
|
||||
ts: performance.now(),
|
||||
@@ -903,7 +910,7 @@ async function loadModel() {
|
||||
if (nextVrm && nextVrmGroup) {
|
||||
runVrmDisposeHooks({
|
||||
camera: camera.value,
|
||||
reason: loadReason,
|
||||
reason: currentLoadReason,
|
||||
vrm: nextVrm,
|
||||
vrmGroup: nextVrmGroup,
|
||||
})
|
||||
@@ -916,7 +923,7 @@ async function loadModel() {
|
||||
if (!isLoadRequestCurrent(requestId))
|
||||
return
|
||||
|
||||
emitVrmLoadError(loadReason, loadStartedAt, err)
|
||||
emitVrmLoadError(currentLoadReason, loadStartedAt, err)
|
||||
console.error(err)
|
||||
emit('error', err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { TresContext } from '@tresjs/core'
|
||||
import type { DirectionalLight, SphericalHarmonics3, Texture, WebGLRenderer, WebGLRenderTarget } from 'three'
|
||||
|
||||
import type { SceneBootstrap, ScenePhase, Vec3 } from '../stores/model-store'
|
||||
import type { VrmLifecycleReason } from '../trace'
|
||||
|
||||
import { Screen } from '@proj-airi/ui'
|
||||
import { TresCanvas } from '@tresjs/core'
|
||||
@@ -77,7 +78,7 @@ type SceneTracePhaseCause
|
||||
| 'vrm:error'
|
||||
| 'vrm:load-start'
|
||||
| 'vrm:loaded'
|
||||
type SceneTraceTransactionReason = 'component-unmount' | 'initial-load' | 'model-switch' | 'no-model' | 'subtree-remount' | 'unknown'
|
||||
type SceneTraceTransactionReason = 'component-unmount' | 'initial-load' | 'model-reload' | 'model-switch' | 'no-model' | 'subtree-remount' | 'unknown'
|
||||
|
||||
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
|
||||
|
||||
@@ -93,6 +94,7 @@ const {
|
||||
scenePhase,
|
||||
sceneTransactionDepth,
|
||||
|
||||
lastCommittedModelSrc,
|
||||
modelSize,
|
||||
modelOrigin,
|
||||
modelOffset,
|
||||
@@ -140,6 +142,9 @@ const stageThreeSceneTraceOriginId = `three-scene:${Math.random().toString(36).s
|
||||
const latestScenePhaseTraceCause = ref<SceneTracePhaseCause>('props:model-src')
|
||||
const latestSceneTransactionReason = ref<SceneTraceTransactionReason>('unknown')
|
||||
const activeModelSrc = ref<string>()
|
||||
const bindingRevision = ref(0)
|
||||
const pendingCommittedModelSrc = ref<string>()
|
||||
const pendingCommittedModelRevision = ref<number>()
|
||||
const pendingSceneBootstrap = shallowRef<SceneBootstrap>()
|
||||
|
||||
function emitThreeSceneTrace(label: string, event: any, payload: Record<string, unknown>) {
|
||||
@@ -222,6 +227,16 @@ function toVec3(value: Vector3): Vec3 {
|
||||
return { x: value.x, y: value.y, z: value.z }
|
||||
}
|
||||
|
||||
function clearPendingCommittedModel() {
|
||||
pendingCommittedModelSrc.value = undefined
|
||||
pendingCommittedModelRevision.value = undefined
|
||||
}
|
||||
|
||||
function invalidateBindingRevision() {
|
||||
bindingRevision.value += 1
|
||||
clearPendingCommittedModel()
|
||||
}
|
||||
|
||||
function applySceneBootstrap(value: SceneBootstrap) {
|
||||
const reason = latestSceneTransactionReason.value
|
||||
const previousOrigin = toVector3(modelOrigin.value)
|
||||
@@ -234,7 +249,8 @@ function applySceneBootstrap(value: SceneBootstrap) {
|
||||
modelSize.value = { ...value.modelSize }
|
||||
eyeHeight.value = value.eyeHeight
|
||||
|
||||
if (reason === 'initial-load' || reason === 'unknown' || reason === 'no-model') {
|
||||
if (reason === 'initial-load' || reason === 'unknown' || reason === 'no-model' || reason === 'model-switch') {
|
||||
modelOffset.value = { ...value.modelOffset }
|
||||
cameraDistance.value = value.cameraDistance
|
||||
cameraPosition.value = { ...value.cameraPosition }
|
||||
lookAtTarget.value = { ...value.lookAtTarget }
|
||||
@@ -304,6 +320,7 @@ const modelPhase = ref<ModelPhase>(props.modelSrc ? 'loading' : 'no-model')
|
||||
|
||||
function beginSceneBindingCycle(reason: SceneTraceTransactionReason) {
|
||||
latestSceneTransactionReason.value = reason
|
||||
invalidateBindingRevision()
|
||||
resetSceneBindingTransactions()
|
||||
emitSceneTransactionTrace('reset', reason)
|
||||
beginSceneBindingTransaction()
|
||||
@@ -313,6 +330,7 @@ function beginSceneBindingCycle(reason: SceneTraceTransactionReason) {
|
||||
|
||||
function beginSceneRebind(reason: SceneTraceTransactionReason = 'subtree-remount') {
|
||||
latestSceneTransactionReason.value = reason
|
||||
invalidateBindingRevision()
|
||||
|
||||
if (sceneTransactionDepth.value === 0) {
|
||||
beginSceneBindingTransaction()
|
||||
@@ -331,15 +349,52 @@ function resolveLoadTransactionReason(): SceneTraceTransactionReason {
|
||||
if (!props.modelSrc)
|
||||
return 'no-model'
|
||||
|
||||
if (!activeModelSrc.value)
|
||||
if (activeModelSrc.value === props.modelSrc)
|
||||
return 'subtree-remount'
|
||||
|
||||
if (!lastCommittedModelSrc.value)
|
||||
return 'initial-load'
|
||||
|
||||
if (props.modelSrc !== activeModelSrc.value)
|
||||
if (lastCommittedModelSrc.value !== props.modelSrc)
|
||||
return 'model-switch'
|
||||
|
||||
return 'subtree-remount'
|
||||
return 'model-reload'
|
||||
}
|
||||
|
||||
function commitLastCommittedModelSrc(expectedRevision: number, nextPhase: ScenePhase) {
|
||||
if (nextPhase !== 'mounted')
|
||||
return
|
||||
|
||||
if (expectedRevision !== bindingRevision.value)
|
||||
return
|
||||
|
||||
if (!pendingCommittedModelSrc.value || pendingCommittedModelRevision.value !== expectedRevision)
|
||||
return
|
||||
|
||||
if (!activeModelSrc.value || pendingCommittedModelSrc.value !== activeModelSrc.value)
|
||||
return
|
||||
|
||||
if (props.modelSrc !== activeModelSrc.value)
|
||||
return
|
||||
|
||||
lastCommittedModelSrc.value = pendingCommittedModelSrc.value
|
||||
clearPendingCommittedModel()
|
||||
}
|
||||
|
||||
const resolvedVrmLoadReason = computed<VrmLifecycleReason>(() => {
|
||||
switch (latestSceneTransactionReason.value) {
|
||||
case 'component-unmount':
|
||||
case 'initial-load':
|
||||
case 'model-switch':
|
||||
return latestSceneTransactionReason.value
|
||||
case 'model-reload':
|
||||
case 'subtree-remount':
|
||||
return 'model-reload'
|
||||
default:
|
||||
return 'initial-load'
|
||||
}
|
||||
})
|
||||
|
||||
function resolveScenePhaseAfterBinding(): ScenePhase {
|
||||
if (!canvasReady.value)
|
||||
return 'pending'
|
||||
@@ -356,7 +411,7 @@ function resolveScenePhaseAfterBinding(): ScenePhase {
|
||||
return 'loading'
|
||||
}
|
||||
|
||||
async function completeSceneBinding() {
|
||||
async function completeSceneBinding(expectedRevision = bindingRevision.value) {
|
||||
if (isCompletingBinding.value)
|
||||
return
|
||||
isCompletingBinding.value = true
|
||||
@@ -370,6 +425,10 @@ async function completeSceneBinding() {
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (expectedRevision !== bindingRevision.value)
|
||||
return
|
||||
|
||||
controlsRef.value?.update()
|
||||
|
||||
if (sceneTransactionDepth.value > 0) {
|
||||
@@ -377,7 +436,9 @@ async function completeSceneBinding() {
|
||||
emitSceneTransactionTrace('end', latestSceneTransactionReason.value)
|
||||
}
|
||||
|
||||
setScenePhaseWithTrace(resolveScenePhaseAfterBinding(), 'binding:complete')
|
||||
const nextPhase = resolveScenePhaseAfterBinding()
|
||||
setScenePhaseWithTrace(nextPhase, 'binding:complete')
|
||||
commitLastCommittedModelSrc(expectedRevision, nextPhase)
|
||||
}
|
||||
finally {
|
||||
isCompletingBinding.value = false
|
||||
@@ -414,10 +475,13 @@ function onVRMModelLookAtTarget(value: Vec3) {
|
||||
}
|
||||
function onVRMModelLoaded(value: string) {
|
||||
activeModelSrc.value = value
|
||||
pendingCommittedModelSrc.value = value
|
||||
pendingCommittedModelRevision.value = bindingRevision.value
|
||||
modelPhase.value = 'ready'
|
||||
void completeSceneBinding()
|
||||
void completeSceneBinding(bindingRevision.value)
|
||||
}
|
||||
function onVRMModelError(error: unknown) {
|
||||
invalidateBindingRevision()
|
||||
pendingSceneBootstrap.value = undefined
|
||||
modelPhase.value = props.modelSrc ? 'error' : 'no-model'
|
||||
resetSceneBindingTransactions()
|
||||
@@ -471,6 +535,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
invalidateBindingRevision()
|
||||
if (tresCanvasRef.value)
|
||||
emitSceneSubtreeTrace('tresCanvasRef', 'detached')
|
||||
|
||||
@@ -498,6 +563,7 @@ watch(() => props.modelSrc, (modelSrc) => {
|
||||
modelPhase.value = modelSrc ? 'loading' : 'no-model'
|
||||
|
||||
if (!modelSrc) {
|
||||
invalidateBindingRevision()
|
||||
activeModelSrc.value = undefined
|
||||
pendingSceneBootstrap.value = undefined
|
||||
resetSceneBindingTransactions()
|
||||
@@ -710,6 +776,7 @@ defineExpose({
|
||||
ref="modelRef"
|
||||
:current-audio-source="props.currentAudioSource"
|
||||
:model-src="props.modelSrc"
|
||||
:load-reason="resolvedVrmLoadReason"
|
||||
:idle-animation="props.idleAnimation"
|
||||
:paused="props.paused"
|
||||
:env-select="envSelect"
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface SceneBootstrap {
|
||||
cameraPosition: Vec3
|
||||
eyeHeight: number
|
||||
lookAtTarget: Vec3
|
||||
modelOffset: Vec3
|
||||
modelOrigin: Vec3
|
||||
modelSize: Vec3
|
||||
}
|
||||
@@ -133,6 +134,7 @@ export const useModelStore = defineStore('modelStore', () => {
|
||||
// REVIEW: `scale` is still shared with non-VRM view controls. The VRM path should
|
||||
// gradually move to `cameraDistance` as the primary user-facing zoom concept.
|
||||
const scale = useLocalStorage('settings/stage-ui-three/scale', 1)
|
||||
const lastCommittedModelSrc = useLocalStorage('settings/stage-ui-three/lastModelSrc', '')
|
||||
|
||||
// === Model lifecycle / bootstrap ===
|
||||
// These values are recalculated from the currently bound model instance whenever
|
||||
@@ -160,6 +162,7 @@ export const useModelStore = defineStore('modelStore', () => {
|
||||
scenePhase.value = 'pending'
|
||||
sceneTransactionDepth.value = 0
|
||||
|
||||
lastCommittedModelSrc.value = ''
|
||||
modelSize.value = { x: 0, y: 0, z: 0 }
|
||||
modelOrigin.value = { x: 0, y: 0, z: 0 }
|
||||
modelOffset.value = { x: 0, y: 0, z: 0 }
|
||||
@@ -216,6 +219,7 @@ export const useModelStore = defineStore('modelStore', () => {
|
||||
sceneMutationLocked,
|
||||
|
||||
scale,
|
||||
lastCommittedModelSrc,
|
||||
|
||||
modelSize,
|
||||
modelOrigin,
|
||||
|
||||
@@ -70,6 +70,7 @@ export type ThreeSceneTraceTransactionAction = 'begin' | 'end' | 'reset'
|
||||
export type ThreeSceneTraceTransactionReason
|
||||
= | 'component-unmount'
|
||||
| 'initial-load'
|
||||
| 'model-reload'
|
||||
| 'model-switch'
|
||||
| 'no-model'
|
||||
| 'subtree-remount'
|
||||
@@ -126,6 +127,7 @@ export type VrmLifecycleReason
|
||||
= | 'component-unmount'
|
||||
| 'initial-load'
|
||||
| 'manual-reload'
|
||||
| 'model-reload'
|
||||
| 'model-switch'
|
||||
|
||||
export interface VrmLifecycleTracePayload extends StageThreeRuntimeTraceBasePayload {
|
||||
|
||||
Reference in New Issue
Block a user