修改:优化手机端的点云效果
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/prod Build is passing

This commit is contained in:
Linzm committed 2026-10-20 17:34:05 +08:00
1 parent 5892c47a6b
commit 704f6a30e0
4 files changed
+46 -18

No files matched your search

@@ -313,7 +313,8 @@ function init() {
onSwitchToSubject: () => { onSwitchToSubject: () => {
const src = props.imageUrl const src = props.imageUrl
const entry = renderer.textures.getSubject(src) || renderer.textures.get(src) const entry = renderer.textures.getSubject(src) || renderer.textures.get(src)
if (entry && entry.subjectCloudBuffer) { // 缓冲无效则不切,避免切到空点云后整页变黑
if (entry && entry.subjectCloudBuffer && entry.subjectCloudBuffer.buffer) {
renderer.transitionToCloud({ renderer.transitionToCloud({
pointCloudBuffer: entry.subjectCloudBuffer, pointCloudBuffer: entry.subjectCloudBuffer,
aspect: entry.aspect, aspect: entry.aspect,
@@ -78,8 +78,11 @@ function createStateMachine(hooks) {
subjectReady = false subjectReady = false
} }
let resultReadyAt = 0
function setResultPreloadStatus(s) { function setResultPreloadStatus(s) {
resultPreloadStatus = s resultPreloadStatus = s
if (s === 'ready' && !resultReadyAt) resultReadyAt = Date.now()
} }
function getUniforms() { function getUniforms() {
@@ -104,11 +107,15 @@ function createStateMachine(hooks) {
function tryEnterSubject() { function tryEnterSubject() {
if (subjectEntered || subjectFailed || !subjectReady) return false 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 (Date.now() - phaseStartedAt < TIMING.IMAGE_COLOR_MS && phase === PHASE.IMAGE_COLOR_POINT_CLOUD) {
// 结果已好则优先进结果,跳过人物段 return false
if (resultPreloadStatus === 'ready') return false }
subjectEntered = true subjectEntered = true
if (hooks && hooks.onSwitchToSubject) hooks.onSwitchToSubject() if (hooks && hooks.onSwitchToSubject) hooks.onSwitchToSubject()
beginMorph() beginMorph()
@@ -232,16 +239,22 @@ function createStateMachine(hooks) {
) { ) {
// 彩色原图已展示够再标预加载;人物段优先于 RESULT_PRELOADING 文案 // 彩色原图已展示够再标预加载;人物段优先于 RESULT_PRELOADING 文案
} }
if (!tryStartResultTransition() && phase === PHASE.IMAGE_COLOR_POINT_CLOUD) { // 效果图点云就绪后再进 mask 人物段;识别失败或超时则直接切效果图点云
tryEnterSubject() if (resultPreloadStatus === 'ready') {
} const entered = tryEnterSubject()
// 人物未就绪且结果仍在加载:原图彩色展示满后再进预加载相位 const subjectWaitedOut =
if ( !subjectReady &&
!subjectEntered && !subjectFailed &&
!subjectReady && resultReadyAt &&
resultPreloadStatus === 'loading' && Date.now() - resultReadyAt > 12000
if (!entered && (subjectFailed || subjectWaitedOut)) {
if (subjectWaitedOut) subjectFailed = true
tryStartResultTransition()
}
} else if (
phase === PHASE.IMAGE_COLOR_POINT_CLOUD && 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) enter(PHASE.RESULT_PRELOADING)
} }
@@ -201,16 +201,20 @@ function createRenderer(canvas, cssW, cssH, dpr, scaleCanvas) {
cloudA = entry.pointCloudBuffer cloudA = entry.pointCloudBuffer
cloudB = entry.pointCloudBuffer cloudB = entry.pointCloudBuffer
contentAspect = entry.aspect || cloudA.aspect || 1 contentAspect = entry.aspect || cloudA.aspect || 1
// count 是整段缓冲长度。点按从上到下写入,只画 liveCount 会把下半身裁掉
pointCount = cloudA.count || maxPoints pointCount = cloudA.count || maxPoints
} }
/** Promote current target to A, set B = new target (particle morph) */ /** Promote current target to A, set B = new target (particle morph) */
function transitionToCloud(entry) { function transitionToCloud(entry) {
if (!entry || !entry.pointCloudBuffer) return if (!entry || !entry.pointCloudBuffer || !entry.pointCloudBuffer.buffer) return
cloudA = cloudB cloudA = cloudB
cloudB = entry.pointCloudBuffer cloudB = entry.pointCloudBuffer
contentAspect = entry.aspect || cloudB.aspect || contentAspect 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) { function setResultTexture(entry) {
@@ -44,7 +44,17 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
try { try {
const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1 const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1
const cores = (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 4 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 maxPoints = POINT_CLOUD.HIGH
sampleSide = POINT_CLOUD.SAMPLE_HIGH sampleSide = POINT_CLOUD.SAMPLE_HIGH
maxTexSize = VISUAL.MAX_TEX_SIZE maxTexSize = VISUAL.MAX_TEX_SIZE
@@ -475,7 +485,7 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
const mask = maskProcessor.decodeMaskImage( const mask = maskProcessor.decodeMaskImage(
maskRes.image, maskRes.image,
scaleCtx, scaleCtx,
scaleCanvas, sampleCanvas,
aligned.width, aligned.width,
aligned.height aligned.height
) )