纹渊 HarmonyOS 7 工程实战(14):AI 生成图回填到 3D 材质的异步链路

发布时间:2026/7/21 16:06:29
纹渊 HarmonyOS 7 工程实战(14):AI 生成图回填到 3D 材质的异步链路 一、网络 URL 不能直接等同于三维纹理AI 服务返回的通常是远程图片 URL三维资源工厂需要的却是可读取、生命周期明确的图像资源。若直接把远程字符串传进材质网络波动、URL 过期和组件销毁都会变成难以定位的渲染问题。稳定链路应分为五步确认当前结果仍有效、下载二进制、写入应用沙箱、创建三维图像资源、把材质绑定到所有目标子网格。下面的运行结果展示了纹样已经贴合到陶瓷杯表面。它证明的不只是“生成了一张图片”还包括纹样选择、载体选择、预览渲染和导出区域已经消费同一份结果。阶段输入输出失败时保留什么AI 生成prompt、授权、接口配置图片 URL纹样与 prompt下载HTTPS URLArrayBuffer旧业务选择不保留假图片沙箱落盘字节数组稳定文件路径可重试下载资源创建沙箱路径G3DImage原模型材质材质绑定图像与场景节点已贴图模型二维或原材质预览二、请求结果先经过业务有效性检查异步请求返回时用户可能已经切换纹样、载体或退出页面。只检查 HTTP 200 不够还要验证结果属于当前请求。可以用递增令牌标记最新任务旧请求即使成功也不能覆盖新选择。State aiImageUrl: string State aiBusy: boolean false private requestToken: number 0 private async generateImage(prompt: string): Promisevoid { const token this.requestToken this.aiBusy true this.aiImageUrl try { const result await requestAiImage( this.apiKey, prompt, this.imageApiUrl ) if (token ! this.requestToken || result.imageUrl.length 0) { return } this.aiImageUrl result.imageUrl this.syncContinuationSnapshot() } finally { if (token this.requestToken) { this.aiBusy false } } }清空旧 URL 放在请求开始处避免新请求失败时页面继续显示旧图。令牌不等于真正取消网络连接但能阻止过期回调修改当前状态若底层请求支持取消还应在页面退出时同时关闭连接。三、下载与落盘要形成原子边界下载代码必须区分响应码、结果类型和文件写入是否完成。直接用最终文件名写入时组件可能在写到一半读取到损坏图片。更稳妥的做法是先写临时文件完成后再替换目标文件。async function downloadTexture(url: string, finalPath: string): Promisestring { const request http.createHttp() const tempPath ${finalPath}.part try { const response await request.request(url, { method: http.RequestMethod.GET, expectDataType: http.HttpDataType.ARRAY_BUFFER, connectTimeout: 15000, readTimeout: 30000 }) if (response.responseCode ! 200 || !(response.result instanceof ArrayBuffer)) { return } const file fs.openSync(tempPath, fs.OpenMode.CREATE | fs.OpenMode.TRUNC | fs.OpenMode.WRITE_ONLY) fs.writeSync(file.fd, response.result) fs.closeSync(file) fs.renameSync(tempPath, finalPath) return finalPath } finally { request.destroy() } }真实工程还应限制 Content-Type 和最大字节数避免错误页或超大文件进入纹理创建阶段。目标文件名至少包含纹样 ID 与结果版本不能让并发任务写入同一路径。四、沙箱缓存要可失效、可回收缓存的价值是把网络不确定性隔离在资源创建之前但缓存不能无限增长。可用patternId 内容摘要作为键新的 AI 结果出现时生成新键旧文件由定期清理策略回收。内置纹样与 AI 结果也应走统一的“准备沙箱纹理”接口。async function prepareTexture(input: TextureInput): Promisestring { const dir ${getContext().cacheDir}/pattern_tex ensureDirectory(dir) if (input.aiImageUrl.length 0) { const key hashText(input.aiImageUrl).substring(0, 12) const output ${dir}/ai_${input.patternId}_${key}.png if (fs.accessSync(output)) { return output } return await downloadTexture(input.aiImageUrl, output) } const output ${dir}/builtin_${input.patternId}.png if (!fs.accessSync(output)) { await packMediaResourceToPng(input.patternResource, output) } return output }“AI URL 为空”不代表失败它表示使用内置纹样资源。只有准备函数返回空路径时三维组件才进入可见错误或原材质回退。五、创建图像资源后再遍历几何节点沙箱文件可读后资源工厂创建G3DImage再为当前纹理构造 PBR 材质。模型可能包含多个 Geometry 和多个 SubMesh只替换第一个材质会出现“杯身已贴图、杯把仍是旧色”的不完整结果。private async applyPatternTexture(scene: Scene, factory: SceneResourceFactory, sandboxPath: string): Promiseboolean { const image await factory.createImage({ name: patternTexture, uri: sandboxPath }) const geometries this.findGeometryNodes(scene) if (geometries.length 0) { return await this.applyShaderFallback(factory, image) } const material await factory.createMaterial( { name: patternPbrMaterial }, MaterialType.METALLIC_ROUGHNESS ) as MetallicRoughnessMaterial material.baseColor { image, factor: { x: 1, y: 1, z: 1, w: 1 } } material.cullMode 0 geometries.forEach((geometry: Geometry) { geometry.mesh?.subMeshes?.forEach((subMesh) { subMesh.material material }) }) return true }遍历函数需要处理空根节点和非 Geometry 节点不能假设 GLB 结构永远固定。材质绑定成功后再把textureApplied设为真UI 上的“纹样已贴合”才具有真实含义。六、回退顺序要从局部到整体找不到 Geometry 时可以尝试 Shader 材质输入Shader 也不可用时继续展示原始模型模型本身加载失败时再切到二维预览。每次只缩减一层能力避免一个材质异常直接清空整个创作状态。private async applyShaderFallback(factory: SceneResourceFactory, image: G3DImage): Promiseboolean { try { const material await factory.createMaterial( { name: patternShaderMaterial }, MaterialType.SHADER ) const shader (material as ShaderMaterial).colorShader if (shader undefined || shader null) { return false } shader.inputs[BASE_COLOR_Image] image return true } catch { return false } } private showPreview(result: TextureResult): void { if (result.textureApplied) { this.previewState textured3d } else if (result.modelLoaded) { this.previewState plain3d } else { this.previewState canvas2d } }状态标签应真实反映结果textured3d才能显示“纹样已贴合”原模型只能显示“模型已加载”二维回退则明确提示“当前设备使用二维预览”。七、异常矩阵异常检测点页面状态恢复动作AI 返回空 URL生成结果解析失败不展示旧图修改 prompt 后重试图片 URL 过期下载响应码保留选择纹理未应用重新生成返回非图片数据Content-Type/结果类型拒绝落盘更换接口或重试写文件失败临时文件写入清理.part文件检查空间后重试页面已切换请求令牌丢弃旧回调使用新任务结果Geometry 为空场景遍历尝试 Shader保留原模型材质创建失败资源工厂原模型状态切二维预览设备不支持 3D模型加载二维状态继续导出图片把错误写入日志还不够用户至少要知道当前显示的是 AI 结果、内置纹样、原模型还是二维回退否则无法判断生成按钮是否真正生效。八、验证步骤1. 生成一张新图确认旧预览立即清空加载结束后只出现新结果。 2. 切换纹样后立即再次生成确认第一个请求即使晚返回也不会覆盖第二个结果。 3. 断网后触发下载确认不会生成半文件三维组件显示可恢复状态。 4. 检查缓存键确认不同 AI URL 不会写入同一个目标文件。 5. 使用包含多个子网格的模型确认所有可见表面材质一致更新。 6. 模拟 Geometry 为空确认先走 Shader 回退继续失败时仍显示原模型或二维预览。 7. 退出页面后等待旧请求完成确认旧回调不会修改新页面状态。 8. 观察“纹样已贴合”标签只在真实材质绑定成功后出现。九、总结AI 图片进入三维材质不是一次简单赋值而是一条跨网络、文件系统和渲染资源的异步流水线。请求令牌阻止过期结果回写临时文件保证落盘完整沙箱缓存隔离远程 URLPBR 材质遍历覆盖所有子网格分层回退则让原模型和二维预览继续承接业务。每个阶段都有可观察状态才能证明最终贴图属于当前纹样和当前载体。网络请求的基础用法可参考HTTP 数据请求。