From 704f6a30e0dc6d8f394e05e8d75cfb24d7fc3ebf Mon Sep 17 00:00:00 2001 From: Linzm <1015157648@qq.com> Date: Tue, 20 Oct 2026 17:34:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E7=AB=AF=E7=9A=84=E7=82=B9=E4=BA=91=E6=95=88?= =?UTF-8?q?=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../douyin/components/ImagePointCloud.vue | 3 +- .../components/image-point-cloud/state.js | 39 ++++++++++++------- .../image-point-cloud/webgl/renderer.js | 8 +++- .../webgl/texture-manager.js | 14 ++++++- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/views/douyin/components/ImagePointCloud.vue b/src/views/douyin/components/ImagePointCloud.vue index 528f091..4c7f638 100644 --- a/src/views/douyin/components/ImagePointCloud.vue +++ b/src/views/douyin/components/ImagePointCloud.vue @@ -313,7 +313,8 @@ function init() { onSwitchToSubject: () => { const src = props.imageUrl const entry = renderer.textures.getSubject(src) || renderer.textures.get(src) - if (entry && entry.subjectCloudBuffer) { + // 缓冲无效则不切,避免切到空点云后整页变黑 + if (entry && entry.subjectCloudBuffer && entry.subjectCloudBuffer.buffer) { renderer.transitionToCloud({ pointCloudBuffer: entry.subjectCloudBuffer, aspect: entry.aspect, diff --git a/src/views/douyin/components/image-point-cloud/state.js b/src/views/douyin/components/image-point-cloud/state.js index 9d19813..d7e90f8 100644 --- a/src/views/douyin/components/image-point-cloud/state.js +++ b/src/views/douyin/components/image-point-cloud/state.js @@ -78,8 +78,11 @@ function createStateMachine(hooks) { subjectReady = false } + let resultReadyAt = 0 + function setResultPreloadStatus(s) { resultPreloadStatus = s + if (s === 'ready' && !resultReadyAt) resultReadyAt = Date.now() } function getUniforms() { @@ -104,11 +107,15 @@ function createStateMachine(hooks) { function tryEnterSubject() { if (subjectEntered || subjectFailed || !subjectReady) return false - if (phase !== PHASE.IMAGE_COLOR_POINT_CLOUD) return false + if (phase !== PHASE.IMAGE_COLOR_POINT_CLOUD && phase !== PHASE.RESULT_PRELOADING) { + return false + } + // 等效果图点云采样完成后再进 mask 人物段,避免真机提前切空缓冲黑屏 + if (resultPreloadStatus !== 'ready') return false // 原图彩色点云需先完整展示 - if (Date.now() - phaseStartedAt < TIMING.IMAGE_COLOR_MS) return false - // 结果已好则优先进结果,跳过人物段 - if (resultPreloadStatus === 'ready') return false + if (Date.now() - phaseStartedAt < TIMING.IMAGE_COLOR_MS && phase === PHASE.IMAGE_COLOR_POINT_CLOUD) { + return false + } subjectEntered = true if (hooks && hooks.onSwitchToSubject) hooks.onSwitchToSubject() beginMorph() @@ -232,16 +239,22 @@ function createStateMachine(hooks) { ) { // 彩色原图已展示够再标预加载;人物段优先于 RESULT_PRELOADING 文案 } - if (!tryStartResultTransition() && phase === PHASE.IMAGE_COLOR_POINT_CLOUD) { - tryEnterSubject() - } - // 人物未就绪且结果仍在加载:原图彩色展示满后再进预加载相位 - if ( - !subjectEntered && - !subjectReady && - resultPreloadStatus === 'loading' && + // 效果图点云就绪后再进 mask 人物段;识别失败或超时则直接切效果图点云 + if (resultPreloadStatus === 'ready') { + const entered = tryEnterSubject() + const subjectWaitedOut = + !subjectReady && + !subjectFailed && + resultReadyAt && + Date.now() - resultReadyAt > 12000 + if (!entered && (subjectFailed || subjectWaitedOut)) { + if (subjectWaitedOut) subjectFailed = true + tryStartResultTransition() + } + } else if ( phase === PHASE.IMAGE_COLOR_POINT_CLOUD && - Date.now() - phaseStartedAt >= TIMING.IMAGE_COLOR_MS + Date.now() - phaseStartedAt >= TIMING.IMAGE_COLOR_MS && + (resultPreloadStatus === 'loading' || resultPreloadStatus === 'idle') ) { enter(PHASE.RESULT_PRELOADING) } diff --git a/src/views/douyin/components/image-point-cloud/webgl/renderer.js b/src/views/douyin/components/image-point-cloud/webgl/renderer.js index 95fe1b7..fae861b 100644 --- a/src/views/douyin/components/image-point-cloud/webgl/renderer.js +++ b/src/views/douyin/components/image-point-cloud/webgl/renderer.js @@ -201,16 +201,20 @@ function createRenderer(canvas, cssW, cssH, dpr, scaleCanvas) { cloudA = entry.pointCloudBuffer cloudB = entry.pointCloudBuffer contentAspect = entry.aspect || cloudA.aspect || 1 + // count 是整段缓冲长度。点按从上到下写入,只画 liveCount 会把下半身裁掉 pointCount = cloudA.count || maxPoints } /** Promote current target to A, set B = new target (particle morph) */ function transitionToCloud(entry) { - if (!entry || !entry.pointCloudBuffer) return + if (!entry || !entry.pointCloudBuffer || !entry.pointCloudBuffer.buffer) return cloudA = cloudB cloudB = entry.pointCloudBuffer contentAspect = entry.aspect || cloudB.aspect || contentAspect - pointCount = Math.max(cloudA.count || 0, cloudB.count || 0, maxPoints) + const countA = cloudA.count || 0 + const countB = cloudB.count || 0 + // 取两边缓冲容量的较小值,避免越界;不要用有效点数,否则下半身被截掉 + pointCount = Math.max(1, Math.min(countA || countB, countB || countA)) } function setResultTexture(entry) { diff --git a/src/views/douyin/components/image-point-cloud/webgl/texture-manager.js b/src/views/douyin/components/image-point-cloud/webgl/texture-manager.js index 4ba791c..66a6da4 100644 --- a/src/views/douyin/components/image-point-cloud/webgl/texture-manager.js +++ b/src/views/douyin/components/image-point-cloud/webgl/texture-manager.js @@ -44,7 +44,17 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) { try { const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1 const cores = (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 4 - if (dpr >= 2.5 && cores >= 8) { + const ua = (typeof navigator !== 'undefined' && navigator.userAgent) || '' + const mobile = /Android|iPhone|iPad|iPod|Mobile/i.test(ua) + // 真机内存紧,高点数上传容易丢 WebGL 上下文,之后整页黑屏 + if (mobile) { + maxPoints = POINT_CLOUD.LOW + sampleSide = POINT_CLOUD.SAMPLE_LOW + maxTexSize = VISUAL.LOW_TEX_SIZE + maskSide = POINT_CLOUD.MASK_LOW + subjectPoints = POINT_CLOUD.SUBJECT_LOW + quality = 'low' + } else if (dpr >= 2.5 && cores >= 8) { maxPoints = POINT_CLOUD.HIGH sampleSide = POINT_CLOUD.SAMPLE_HIGH maxTexSize = VISUAL.MAX_TEX_SIZE @@ -475,7 +485,7 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) { const mask = maskProcessor.decodeMaskImage( maskRes.image, scaleCtx, - scaleCanvas, + sampleCanvas, aligned.width, aligned.height )