From 605bd894c52349307124f0d9875184dbb9a45eb3 Mon Sep 17 00:00:00 2001 From: Linzm <1015157648@qq.com> Date: Tue, 20 Oct 2026 16:59:06 +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=E4=B8=8A=E6=95=88=E6=9E=9C=E5=9B=BE=E7=82=B9?= =?UTF-8?q?=E4=BA=91=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../douyin/components/ImagePointCloud.vue | 23 ++++- .../components/image-point-cloud/state.js | 4 + .../webgl/texture-manager.js | 86 +++++++++++++------ src/views/douyin/generating.vue | 4 +- 4 files changed, 87 insertions(+), 30 deletions(-) diff --git a/src/views/douyin/components/ImagePointCloud.vue b/src/views/douyin/components/ImagePointCloud.vue index b484c4c..ed59c3b 100644 --- a/src/views/douyin/components/ImagePointCloud.vue +++ b/src/views/douyin/components/ImagePointCloud.vue @@ -64,8 +64,8 @@ let renderer: any = null let sm: any = null let raf = 0 let lastTs = 0 -let cssW = 320 -let cssH = 320 +let cssW = 340 +let cssH = 340 let logoFromCache = false let resultKey = '' @@ -186,12 +186,23 @@ function preloadResult(url: string) { if (!url || !renderer || !sm) return resultKey = url sm.setResultPreloadStatus('loading') + // 效果图保留完整画面做点云(去背景会在手机上采空,表现为「没有效果图点云」) renderer.textures - .load(url, { removeBackground: true }) + .load(url, { removeBackground: false }) .then((entry: any) => { if (destroyed || !sm) return + if (!entry || !entry.pointCloudBuffer) { + console.error('[ImagePointCloud] result cloud buffer missing') + sm.setResultPreloadStatus('error') + return + } renderer.setResultTexture(entry) sm.setResultPreloadStatus('ready') + // 若状态机已进入 RESULT_*(异步晚到),立刻切到效果图点云 + const phase = String(sm.getPhase?.() || '') + if (phase.startsWith('RESULT')) { + renderer.transitionToCloud(entry) + } }) .catch((err: any) => { console.error('[ImagePointCloud] result preload fail', err) @@ -310,11 +321,15 @@ function init() { } }, onSwitchToResult: () => { - const entry = renderer.textures.get(resultKey || props.resultUrl) + const key = resultKey || props.resultUrl + const entry = key ? renderer.textures.get(key) : null if (entry && entry.pointCloudBuffer) { renderer.transitionToCloud(entry) renderer.setResultTexture(entry) + return } + // 缓冲晚到时由 preloadResult 成功回调补切 + if (key) preloadResult(key) }, onHandoff: () => emit('handoff'), }) diff --git a/src/views/douyin/components/image-point-cloud/state.js b/src/views/douyin/components/image-point-cloud/state.js index 3b0b863..9d19813 100644 --- a/src/views/douyin/components/image-point-cloud/state.js +++ b/src/views/douyin/components/image-point-cloud/state.js @@ -124,6 +124,8 @@ function createStateMachine(hooks) { phase !== PHASE.RESULT_PRELOADING && phase !== PHASE.SUBJECT_DETECTING && phase !== PHASE.SUBJECT_OUTLINE && + phase !== PHASE.SUBJECT_CLOUD_GRAY && + phase !== PHASE.SUBJECT_CLOUD_COLORIZE && phase !== PHASE.SUBJECT_CLOUD_COLOR && phase !== PHASE.SUBJECT_POINT_CLOUD ) { @@ -292,6 +294,7 @@ function createStateMachine(hooks) { if (!morphing) assembly = 1 subjectProgress = 1 outlineProgress = Math.max(0.35, 1 - Math.min(1, elapsed / 2400) * 0.55) + if (tryStartResultTransition()) return getUniforms() if (!morphing && elapsed >= TIMING.SUBJECT_GRAY_MS) { enter(PHASE.SUBJECT_CLOUD_COLORIZE) } @@ -307,6 +310,7 @@ function createStateMachine(hooks) { if (!morphing) assembly = 1 subjectProgress = 1 outlineProgress = 0.35 + if (tryStartResultTransition()) return getUniforms() if (t >= 1) { colorProgress = 1 enter(PHASE.SUBJECT_CLOUD_COLOR) 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 ce16755..00c702e 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 @@ -115,7 +115,8 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) { } /** - * 加载图片并严格在 onload 之后再交给后续逻辑(避免 TMA 未定义行为告警) + * 加载图片并严格在 onload 之后再交给后续逻辑。 + * http(s) 优先 fetch→blob,避免手机 WebView 跨域导致 getImageData 采点失败。 */ function loadImage(src) { return new Promise(function (resolve, reject) { @@ -123,38 +124,73 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) { reject(new Error('empty src')) return } - let settled = false - const img = new Image() - // 跨域效果图需 CORS,否则无法 getImageData 采样 - if (/^https?:\/\//i.test(src)) { - img.crossOrigin = 'anonymous' - } - function finishOk() { - if (settled) return - setTimeout(function () { + function bindImage(imgSrc, revokeUrl) { + let settled = false + const img = new Image() + if (/^https?:\/\//i.test(imgSrc) && imgSrc.indexOf('blob:') !== 0) { + img.crossOrigin = 'anonymous' + } + + function finishOk() { if (settled) return - const w = img.width || 0 - const h = img.height || 0 - if (!w || !h) { + setTimeout(function () { + if (settled) return + const w = img.width || 0 + const h = img.height || 0 + if (!w || !h) { + settled = true + if (revokeUrl) { + try { URL.revokeObjectURL(revokeUrl) } catch (e) {} + } + reject(new Error('image empty after onload')) + return + } settled = true - reject(new Error('image empty after onload')) - return - } + resolve({ + image: img, + path: src, + width: w, + height: h, + revokeUrl: revokeUrl || '', + }) + }, 0) + } + + function finishErr(err) { + if (settled) return settled = true - resolve({ image: img, path: src, width: w, height: h }) - }, 0) + if (revokeUrl) { + try { URL.revokeObjectURL(revokeUrl) } catch (e) {} + } + reject(err || new Error('image load fail')) + } + + img.onload = finishOk + img.onerror = finishErr + img.src = imgSrc } - function finishErr(err) { - if (settled) return - settled = true - reject(err || new Error('image load fail')) + // 远程图:先 blob 同源化,再采样(手机端效果图点云依赖此路径) + if (/^https?:\/\//i.test(src) && typeof fetch === 'function') { + fetch(src, { mode: 'cors', credentials: 'omit', cache: 'force-cache' }) + .then(function (res) { + if (!res || !res.ok) throw new Error('fetch image http ' + (res && res.status)) + return res.blob() + }) + .then(function (blob) { + if (!blob || !blob.size) throw new Error('empty image blob') + const objUrl = URL.createObjectURL(blob) + bindImage(objUrl, objUrl) + }) + .catch(function () { + // CORS 拉流失败时退回 + bindImage(src, null) + }) + return } - img.onload = finishOk - img.onerror = finishErr - img.src = src + bindImage(src, null) }) } diff --git a/src/views/douyin/generating.vue b/src/views/douyin/generating.vue index 76832ca..5be101f 100644 --- a/src/views/douyin/generating.vue +++ b/src/views/douyin/generating.vue @@ -333,10 +333,12 @@ function pollStatus() { imageId.value = parsed.imageId sessionStorage.setItem('douyin_image_id', parsed.imageId) } + // 不再 10s 强切预览:会掐掉 RESULT_CLOUD_* 效果图点云段 + // 正常走点云状态机 handoff;仅作兜底(给足灰/彩点云播放时间) if (fadeTimer) clearTimeout(fadeTimer) fadeTimer = setTimeout(() => { if (!handoffDone) enterResultView() - }, 10000) + }, 60000) if (pollTimer) { clearInterval(pollTimer) pollTimer = null