fix: 修复光栅卡陀螺仪部分问题

This commit is contained in:
liulong 2026-05-15 14:12:55 +08:00
parent c57bae20a9
commit 15834c6719
2 changed files with 93 additions and 23 deletions

View File

@ -169,7 +169,7 @@ function uvForPlane(nx, ny, nz, mode) {
/**
* @param {object} opts
* @param {(x: number, y: number) => void} opts.simulate
* @param {(tiltMagDeg: number) => void} [opts.simulateFromSignedDegrees] 若提供用相对进入时基准的**倾角标量**驱动预览由页面做离散档位等原生/加速度计侧会喂入非负幅度为主
* @param {(tiltMagDeg: number, signedDegHint?: number) => void} [opts.simulateFromSignedDegrees] 若提供用相对进入时基准的**倾角标量**驱动预览由页面做离散档位等可选第二参为有符号倾角加速度计路径可传以平衡两侧跟手
* @param {import('vue').Ref<string>} opts.gyroSourceLabel
* @param {boolean} [opts.useStudioAccelDirect] 光栅工作室加速度计用重力投影直连免原 warmup
* @param {() => void} [opts.onTiltDriverFallback] 从原生陀螺失败/超时回退到加速度计时调用用于重置离散档位等 UI 状态
@ -194,6 +194,8 @@ export function useLenticularStudioTilt(opts) {
let nativeBase = { x: 0, y: 0, z: 0 }
let nativeBaselineReady = false
let nativeSmoothed = 0
/** 离散档位simulateFromSignedDegrees下对 max(|Δ|) 的轻低通,减轻三轴取 max 的瞬时毛刺 */
let nativeDiscreteDegLp = 0
/** 连续模式下与基线样本推断的主倾角轴0=x,1=y,2=z避免每帧在 x/y 间抢主导 */
let nativeLockedAxisIdx = 1
/** 递增以丢弃 stop 之后的原生回调 / 延时任务 */
@ -205,6 +207,8 @@ export function useLenticularStudioTilt(opts) {
let studioAccelBaselineRads = []
/** 加速度计直连模式下「校零」后的基准(弧度),仅与 simulateFromSignedDegrees 联用 */
let studioAccelBaseRad = null
/** 加速度计直连 + 离散档位:与原生侧一致的轻低通(度),避免切到加速度计后手感突变 */
let studioAccelDiscreteDegLp = 0
let accelHandler = null
let accelSmoothed = 0
@ -241,12 +245,14 @@ export function useLenticularStudioTilt(opts) {
nativeBase = { x: 0, y: 0, z: 0 }
nativeBaselineReady = false
nativeSmoothed = 0
nativeDiscreteDegLp = 0
nativeLockedAxisIdx = 1
}
function resetStudioAccelBaseline() {
studioAccelBaseRad = null
studioAccelBaselineRads = []
studioAccelDiscreteDegLp = 0
}
function accelToRelativeTilt01(ax, ay, az) {
@ -397,7 +403,11 @@ export function useLenticularStudioTilt(opts) {
while (delta > Math.PI) delta -= 2 * Math.PI
while (delta < -Math.PI) delta += 2 * Math.PI
const signedDeg = delta * (180 / Math.PI)
simulateFromSignedDegrees(Math.abs(signedDeg))
const rawAbs = Math.abs(signedDeg)
const prevLp = studioAccelDiscreteDegLp
const kLp = rawAbs >= prevLp ? 0.27 : 0.4
studioAccelDiscreteDegLp += (rawAbs - studioAccelDiscreteDegLp) * kLp
simulateFromSignedDegrees(studioAccelDiscreteDegLp, signedDeg)
} else {
const tiltRaw = Math.atan2(-ax, az) / (Math.PI / 5)
const tilt01 = Math.max(-1, Math.min(1, tiltRaw))
@ -488,7 +498,11 @@ export function useLenticularStudioTilt(opts) {
const dy = deltaDeg(vy, nativeBase.y)
const dz = deltaDeg(vz, nativeBase.z)
if (typeof simulateFromSignedDegrees === 'function') {
simulateFromSignedDegrees(maxAbsTiltDeltaDeg(dx, dy, dz))
const rawMag = maxAbsTiltDeltaDeg(dx, dy, dz)
const prevLp = nativeDiscreteDegLp
const kLp = rawMag >= prevLp ? 0.28 : 0.38
nativeDiscreteDegLp += (rawMag - nativeDiscreteDegLp) * kLp
simulateFromSignedDegrees(nativeDiscreteDegLp)
} else {
const axisDeltas = [dx, dy, dz]
const chosen = axisDeltas[nativeLockedAxisIdx] ?? pickDominantTiltDelta(dx, dy, dz)

View File

@ -91,26 +91,83 @@ const { physics, layerTransforms, simulate, relax, snapSimulatedTilt } = useLent
const STUDIO_DISCRETE_STEP_DEG = 13
/** 档位施密特迟滞(度),减轻在档位边界来回跳;换向经过水平附近时略加大更稳 */
const DISCRETE_STEP_HYST_DEG = 5
/** 进入离散逻辑前对倾角幅度做 EMA上升快、下降慢减轻换向时「先掉档再升档」的抖动 */
const STUDIO_TILT_MAG_EMA = 0.17
const STUDIO_TILT_MAG_EMA_DECAY_RATIO = 0.42
/**
* 倾角幅度一阶低通的时间常数ms对称跟随进档/退档节奏一致避免抬手慢压手快的忽快忽慢
* dt 按实际回调间隔估算与陀螺轮询/加速度计频率解耦
*/
const STUDIO_TILT_MAG_TAU_MS = 150
/** 平滑后倾角幅度最大爬升速率(度/秒),抑制某一侧翻转时 raw 陡增导致的瞬间跳档 */
const STUDIO_TILT_MAG_MAX_RISE_DPS = 82
/** 回落可略快,减档仍跟手 */
const STUDIO_TILT_MAG_MAX_FALL_DPS = 168
/**
* 有符号倾角相对水平远离的速率阈值/超过则认为该帧在快速掰离水平
* 再收紧爬升限幅典型从下往上翻时 |atan2| 一侧梯度更大
*/
const STUDIO_TILT_AWAY_FROM_LEVEL_DPS = 72
/** 触发「快速远离水平」时,爬升限幅乘数(越小越匀) */
const STUDIO_TILT_RISE_TIGHTEN = 0.42
/** 进入页后延迟开启倾斜监听ms避免首帧/基线采集/大图解码时画面跟着抖 */
const STUDIO_TILT_START_DELAY_MS = 560
const discreteStableStep = ref(0)
/** 与离散档位同步平滑的倾角幅度(度),换图时缓变避免观感断裂 */
/** 与离散档位同步平滑的倾角幅度(度) */
let studioTiltMagEma = 0
/** 上次幅度采样时间戳,用于按真实 dt 做指数平滑 */
let lastStudioTiltMagSampleAt = 0
/** 上一帧有符号倾角(度),仅加速度计路径传入;用于检测快速远离水平 */
let lastStudioSignedDegHint = null
function resetStudioTiltMagFilter() {
studioTiltMagEma = 0
lastStudioTiltMagSampleAt = 0
lastStudioSignedDegHint = null
}
/**
* 将相对基准的倾角标量非负映射为 LenticularEngine gamma
* @param {number} tiltMagDeg 相对校零的倾角幅度 composable 已做低通时可为非负标量
* @param {number} tiltMagDeg 相对校零的倾角幅度非负
* @param {number} [signedDegHint] 有符号倾角仅加速度计路径传入用于识别快速远离水平的一侧并收紧爬升
*/
function simulateFromSignedDegrees(tiltMagDeg) {
function simulateFromSignedDegrees(tiltMagDeg, signedDegHint) {
const ls = layers.value || []
const n = Math.max(1, ls.length)
const raw = Math.abs(Number(tiltMagDeg) || 0)
const emaAlpha = raw >= studioTiltMagEma ? STUDIO_TILT_MAG_EMA : STUDIO_TILT_MAG_EMA * STUDIO_TILT_MAG_EMA_DECAY_RATIO
studioTiltMagEma += (raw - studioTiltMagEma) * emaAlpha
const now = Date.now()
let dt = 33
if (lastStudioTiltMagSampleAt > 0) {
dt = now - lastStudioTiltMagSampleAt
}
lastStudioTiltMagSampleAt = now
dt = Math.max(16, Math.min(100, dt))
const alpha = 1 - Math.exp(-dt / STUDIO_TILT_MAG_TAU_MS)
let nextEma = studioTiltMagEma + (raw - studioTiltMagEma) * alpha
const sec = dt * 0.001
let maxRise = STUDIO_TILT_MAG_MAX_RISE_DPS * sec
const maxFall = STUDIO_TILT_MAG_MAX_FALL_DPS * sec
if (Number.isFinite(signedDegHint)) {
if (lastStudioSignedDegHint != null && sec > 1e-6) {
const dAbsSignedDt =
(Math.abs(signedDegHint) - Math.abs(lastStudioSignedDegHint)) / sec
if (dAbsSignedDt > STUDIO_TILT_AWAY_FROM_LEVEL_DPS) {
maxRise *= STUDIO_TILT_RISE_TIGHTEN
}
}
lastStudioSignedDegHint = signedDegHint
} else {
lastStudioSignedDegHint = null
}
let dMag = nextEma - studioTiltMagEma
if (dMag > 0) {
dMag = Math.min(dMag, maxRise)
} else {
dMag = Math.max(dMag, -maxFall)
}
studioTiltMagEma += dMag
const absDeg = studioTiltMagEma
const STEP = STUDIO_DISCRETE_STEP_DEG
const MARGIN = DISCRETE_STEP_HYST_DEG
@ -126,10 +183,8 @@ function simulateFromSignedDegrees(tiltMagDeg) {
const gPick = Math.max(-1, Math.min(1, 2 * u - 1))
const gamma = Math.max(-1, Math.min(1, gPick / mul))
simulate(gamma, 0)
/* 档位切换瞬间把引擎 displayGamma 对齐到目标,避免多帧渐近 + 宽叠化带叠成「糊、抖」 */
if (s !== stepBefore) {
snapSimulatedTilt({ resetLayerSmoothing: false })
}
/* 不在此 snap保留引擎内 displayGamma 渐近angleStability / transitionSmoothness
* 切换档位时 u 连续扫过叠化带才能看到一张渐隐另一张渐显 */
}
const { start: startTilt, stop: stopTilt, recalibrate: recalibrateTilt } = useLenticularStudioTilt({
@ -139,14 +194,14 @@ const { start: startTilt, stop: stopTilt, recalibrate: recalibrateTilt } = useLe
useStudioAccelDirect: true,
onTiltDriverFallback: () => {
discreteStableStep.value = 0
studioTiltMagEma = 0
resetStudioTiltMagFilter()
},
})
/** 首屏与换素材后:档位与 EMA 归零,并写入与 0° 档位一致的 gamma传感器未开时保持静止 */
function lockStudioPreviewStill() {
discreteStableStep.value = 0
studioTiltMagEma = 0
resetStudioTiltMagFilter()
simulateFromSignedDegrees(0)
snapSimulatedTilt({ resetLayerSmoothing: true })
}
@ -171,7 +226,7 @@ function onMore() {
const p = typeof raw === 'string' ? JSON.parse(raw) : raw
layers.value = buildLenticularLayersTwo(p.bgPath || '', p.subjectPath || '')
discreteStableStep.value = 0
studioTiltMagEma = 0
resetStudioTiltMagFilter()
nextTick(() => relax(0.86))
uni.showToast({ title: '已恢复', icon: 'none' })
} catch (e) {
@ -229,7 +284,7 @@ function resetLayer(layerId) {
function onRecalibrate() {
discreteStableStep.value = 0
studioTiltMagEma = 0
resetStudioTiltMagFilter()
recalibrateTilt()
nextTick(() => {
simulateFromSignedDegrees(0)
@ -248,17 +303,18 @@ onMounted(() => {
}
const p = typeof raw === 'string' ? JSON.parse(raw) : raw
layers.value = buildLenticularLayersTwo(p.bgPath || '', p.subjectPath || '')
/* 光栅感:略抬残影/ghost + 叠化带宽;开启轻量视差,换图时另一张轮廓更易分辨 */
/* 光栅感:略抬残影/ghost;叠化带宽略加宽,配合非 snap 的 gamma 渐近,换图时渐隐渐显更明显 */
Object.assign(physics, {
angleStability: 40,
transitionSmoothness: 28,
/* 略提高:让 smoothedSensor → displayGamma 在档位间多走几帧,叠化更明显 */
angleStability: 52,
transitionSmoothness: 40,
tiltSensitivity: 96,
sensorDeadzoneStrength: 0,
parallaxDepth: 18,
lenticularAnchorFloor: 0.1,
lenticularNonDominantResidualMin: 0.092,
lenticularPrevLayerGhostMin: 0.098,
lenticularBlendBaseScale: 0.95,
lenticularBlendBaseScale: 1.1,
})
} catch (e) {
console.error('[lenticular-studio] load payload', e)