资讯动态

Laya粒子预览实战:本地.part文件选择与纹理重写

发布时间:2026/10/26 19:08:56 来源:尧图企业网站定制
简介面向LayaAir开发者与3D网页交互学习者的粒子预览示例包聚焦将Unity导出的.lh粒子文件接入Laya场景并在浏览器中调试。示例通过按钮弹出本地文件选择框导入仅支持最多两层嵌套的粒子支持播放与停止状态切换更换.lh文件无需重新运行配合键盘和鼠标切换视角可观察粒子在3D场景与网页混合环境中的表现也体现了操作DOM完成文件选取的用法。压缩包共302个文件约23.43MB以67个js脚本、51个png贴图、18个lmat材质、13个json配置和6个lh粒子资源为主另有ts源码及少量工程与页面文件便于按模块查看实现。已有744人学习适合需要理解Laya粒子加载、DOM文件操作和预览调试流程的初中级开发者参考也可作为Unity粒子转Laya展示的排错与验证样例并据此梳理工程结构需注意仅在LayaIDE上测试通过。1. 先别急着调参数弹出框选本地 .part 才是预览链路的第一道坎把 Laya 预览 Unity 导出粒子 demo 做成弹出框选择本地文件最先卡住的往往不是粒子参数而是文件怎么从磁盘进入 Laya 的加载器。很多人一上来就调发射器、生命周期和混合模式结果控制台里贴图 404粒子要么不显示要么整块白。这个 demo 的价值就在这儿它把 input[typefile] 选文件、解析 .part、把纹理挂回粒子、再挂到舞台这条链路串起来适合已经能在 Unity 里出效果、但被 Laya 资源路径和 DOM 文件入口绊住的人。Laya 粒子预览本身不玄学玄学的是文件路径和加载时序。2. 从 Unity 导出到 Laya 可读.part、贴图与目录约定的三个硬约束2.1 Unity 侧导出时先固定三件事坐标系、材质混合与纹理命名Unity 粒子导出到 Laya 可读的 .part 文件中间隔着一层导出插件。插件做的事不只是“把粒子转成 JSON”它还要把 Unity 的材质、纹理引用、发射器参数映射到 Laya 的 2D 粒子系统。我一般会在导出前先把三件事固定下来不然后面在 Laya 里调半天都不知道是导出问题还是加载问题。第一是坐标系。Unity 是 3D 场景粒子挂在某个 Transform 下Laya 的 Particle2D 最终落在 2D 舞台上发射器坐标是舞台坐标。导出前把粒子根节点归零不要带旋转和缩放尤其是 Y 轴不要带负缩放。常见做法是新建一个空场景把粒子放在原点导出后用particle.emitter.x、particle.emitter.y把它摆到屏幕中间。这样出问题的时候至少能排除“粒子其实在屏幕外”这种低级翻车。第二是材质混合模式。Unity 里粒子常用 Additive 和 AlphaBlendLaya 侧对混合状态的支持跟导出配置有关。我的习惯是先导一版 AlphaBlend确认粒子形状和贴图没问题再换 Additive 对比亮度。不要一次改混合模式又改贴图否则白块和过曝会混在一起排查成本翻倍。导出后的 .part 里如果带混合字段先保持默认等粒子能稳定显示再动。第三是纹理命名。贴图统一小写英文、无空格、无中文扩展名统一.png。这不是洁癖而是浏览器对 URL 大小写敏感Windows 资源管理器不敏感。你在本地双击能打开放到 Blob URL 或静态服务里就可能 404。导出目录我一般固定成resources/ particle/ unity_fx.part tex/ fx_01.png fx_02.png.part里的纹理字段写相对路径比如tex/fx_01.png。这样在默认目录加载时Laya 的加载器能按 .part 所在目录拼出纹理地址。这个约定是后面所有本地文件预览的基础目录一乱重写纹理字段的代码就得跟着打补丁。2.2 .part 文件不是黑匣子用 JSON 视角看发射器、纹理和时长.part本质是一个 JSON 配置文件不同导出器版本字段名会有差异但核心信息跑不出几类纹理引用、发射器位置、粒子生命周期、发射速率、最大粒子数、混合状态。你不需要背所有字段但要知道哪些字段会影响“粒子能不能看见”。我一般会先用编辑器打开 .part看三个地方字段/区域作用常见问题textureName或texture指向贴图文件路径大小写不一致、相对路径在 Blob URL 下失效duration/emissionRate控制发射时长和速率时长为 0 或速率极低看起来像没加载emitter/emitter.x/emitter.y发射器在舞台上的位置坐标在屏幕外粒子其实在跑但看不见maxParticles最大粒子数设得太小效果像断断续续blendState或材质相关字段混合模式Additive 过曝、AlphaBlend 发灰这张表不是让你逐项调参而是出问题时按顺序排除。先看纹理字段能不能对上文件再看发射器坐标是不是在可视区最后才看时长和速率。很多“粒子不显示”的工单最后都是纹理路径或发射器坐标的问题参数本身没毛病。另外.part里如果出现多个纹理引用比如textures数组或子配置不要假设只有一张图。导出器版本不同字段结构可能不一样。我的做法是在代码里做字段兜底先找textureName再找texture如果存在textures数组再逐项处理。这样同一套预览代码能兼容几种导出结果不至于换个导出器就重写。2.3 目录约定与最小加载代码先让粒子在默认路径跑通在接弹出框选文件之前先让粒子从默认路径跑通。这一步是把“Laya 粒子加载”和“DOM 文件选择”拆开验证。如果默认路径都加载不出来后面加 File API 只会多一层变量。最小加载代码可以写成这样const { regClass } Laya; regClass() export class ParticlePreview extends Laya.Script { private particle: Laya.Particle2D | null null; onAwake(): void { // 先走默认目录确认 .part 和贴图的相对路径能被 Laya 正确解析 Laya.loader.load( resources/particle/unity_fx.part, Laya.Handler.create(this, this.onPartLoaded) ); } private onPartLoaded(particle: Laya.Particle2D): void { if (!particle) { console.error(粒子加载失败先检查 .part 路径和贴图目录); return; } // 旧粒子先销毁避免重复加载时叠在一起 if (this.particle) { this.particle.destroy(); } this.particle particle; // 发射器摆到舞台中心排除“粒子在屏幕外”的干扰 particle.emitter.x Laya.stage.width / 2; particle.emitter.y Laya.stage.height / 2; particle.play(); Laya.stage.addChild(particle); } }这段代码的逻辑很直白加载 .part回调里拿到Particle2D设置发射器坐标播放并挂到舞台。关键是不要给Laya.loader.load硬指定 JSON 类型部分版本会走到错误的解析器反而加载失败。让加载器按扩展名和内容自己判断兼容性更好。参数上重点看两个particle.emitter.x和particle.emitter.y。它们决定粒子从哪儿发射不是粒子贴图的位置。有人把粒子挂上去看不见就是因为发射器还在(0,0)而舞台做了缩放或偏移。先摆到舞台中心确认效果再按业务位置调。提示默认路径跑通之前不要接 File API。先把 .part 和贴图放在同一目录下验证能少排查一半问题。3. 用 input[typefile] 接管文件入口DOM 覆盖层与 Laya Canvas 的边界3.1 为什么用 DOM 的 input[typefile] 而不是 Laya 自绘文件选择浏览器里选本地文件绕不开input[typefile]。Laya 是 Canvas 渲染自绘一个“选择文件”按钮看起来更统一但最终还是要触发一个真实的 file input。原因不复杂文件选择框属于浏览器安全边界必须由用户手势直接触发不能由脚本随便弹出。我见过两种翻车写法。一种是 Laya 的按钮点击后在异步回调里执行input.click()移动端直接没反应另一种是把 input 设成display:none再用 JS 触发部分浏览器也会拦截。稳妥做法是让 DOM 按钮和 input 保持在同一层按钮点击直接触发 input或者用label关联 input让浏览器认为这是用户主动操作。在 Laya 预览 Unity 导出粒子的场景里DOM 只负责“选文件”Laya 只负责“加载和渲染”。两边用自定义事件解耦不要让 DOM 代码直接去操作Particle2D否则后面换 UI 框架会很难受。3.2 覆盖层布局让按钮可点、Canvas 不拦截、移动端不弹不出Laya 的 Canvas 一般铺满全屏DOM 覆盖层要盖在它上面但不能挡住 Canvas 的交互。我的做法是给工具栏容器设pointer-events: none只让按钮和 input 恢复pointer-events: auto。这样工具栏空白区域不会拦截鼠标事件按钮又能正常点。div idparticle-toolbar label idpickLabel forparticleFileInput选择 .part 与贴图/label input idparticleFileInput typefile multiple accept.part,.json,.png,.jpg,.jpeg,.webp / span idfileNameTip未选择文件/span /div#particle-toolbar { position: fixed; left: 16px; top: 16px; z-index: 20; display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: rgba(20, 20, 20, 0.75); border-radius: 6px; color: #fff; font: 14px/1.4 sans-serif; pointer-events: none; /* 工具栏空白不拦截 Canvas */ } #pickLabel { padding: 6px 12px; background: #2f7cf6; border-radius: 4px; cursor: pointer; pointer-events: auto; /* 只有按钮可点 */ } #particleFileInput { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } #fileNameTip { pointer-events: auto; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }这里用label的for关联 input点击 label 等价于点击 input移动端和桌面端都稳。input 视觉上隐藏但不要用display:none有些浏览器对隐藏 input 的触发限制更多。accept只是给文件选择框的提示不能当校验用脚本里还要自己判断扩展名。注意工具栏的z-index要高于 Laya Canvas但不要高到盖住浏览器原生文件选择框。文件选择框是浏览器级弹层DOM 层级影响不到它。3.3 选中文件后的分发.part 主文件与贴图文件怎么配对用户一次可能选多个文件一个.part若干张.png。代码要先找出主文件再把贴图按文件名建映射。不要假设用户会按顺序选FileList 的顺序不可靠。const input document.getElementById(particleFileInput); const tip document.getElementById(fileNameTip); input.addEventListener(change, async () { const files Array.from(input.files || []); if (!files.length) return; const partFile files.find((file) /\.(part|json)$/i.test(file.name)); if (!partFile) { tip.textContent 没有找到 .part 文件; return; } const textureFiles files.filter((file) /\.(png|jpe?g|webp)$/i.test(file.name)); if (!textureFiles.length) { tip.textContent 没有找到贴图文件粒子可能显示为白块; } tip.textContent ${partFile.name} ${textureFiles.length} 张贴图; // 把文件交给 Laya 侧处理DOM 不直接碰 Particle2D window.dispatchEvent( new CustomEvent(laya-particle-files, { detail: { partFile, textureFiles }, }) ); });这段代码做了三件事找.part或.json主文件收集图片文件通过CustomEvent把 File 对象传出去。参数上multiple必须开否则一次只能选一个文件accept写常见扩展名减少用户选错正则里同时接受.part和.json是因为有些导出器会把粒子配置存成 JSON 后缀。Laya 侧监听事件window.addEventListener(laya-particle-files, (event: Event) { const detail (event as CustomEvent).detail as { partFile: File; textureFiles: File[]; }; this.loadFromFiles(detail.partFile, detail.textureFiles); });这样 DOM 和 Laya 的边界很清楚DOM 只负责拿到 FileLaya 负责解析和渲染。后面如果要把工具栏换成别的框架或者把入口挪到拖拽上传Laya 侧不用改。4. 把 File 变成可加载 URLBlob、basePath 与纹理重写的加载链路4.1 Blob URL 与 FileReader 的取舍谁适合 .part谁适合贴图File 对象不能直接交给Laya.loader.load加载器要的是 URL。常见转换方式有三种Blob URL、DataURL、本地静态服务器。它们各有适用场景选错了会在纹理路径上翻车。方式适合对象优点主要坑URL.createObjectURL(file)贴图、二进制文件不占内存字符串、速度快URL 生命周期要手动释放FileReader.readAsDataURL小体积 JSON、需要内嵌的场景可读、可塞进字符串大文件体积膨胀纹理相对路径仍要处理本地静态服务器完整目录预览路径关系保留最好需要额外启动服务不适合纯前端 demo对.part文件我优先用file.text()读成字符串解析 JSON 后重写纹理字段再把修改后的 JSON 做成 Blob URL。原因很直接Blob URL 没有“所在目录”的概念.part里写的tex/fx_01.png是相对路径加载器拿 Blob URL 去拼相对路径拼出来的地址一定不对。重写纹理字段是绕不开的一步。贴图则直接用URL.createObjectURL(file)。浏览器 Image 能加载 Blob URLLaya 的纹理加载最终也会走 Image 或类似的资源加载链路兼容性没问题。关键是每个 Blob URL 都要记录销毁粒子后统一revokeObjectURL否则反复选文件会涨内存。4.2 重写粒子 JSON 的纹理字段字段兜底与大小写映射不同导出器版本对纹理字段的命名不完全一致常见的是textureName和texture有的还会把纹理放在textures数组里。代码里做兜底不要只认一个字段。private objectUrls: string[] []; private async loadFromFiles(partFile: File, textureFiles: File[]): Promisevoid { this.releaseCurrent(); const text await partFile.text(); let json: any; try { json JSON.parse(text); } catch (error) { console.error(part JSON 解析失败先确认选中的是粒子配置而不是贴图, error); return; } // 贴图按文件名建映射统一小写避免大小写导致配不上 const textureMap new Mapstring, File(); textureFiles.forEach((file) { textureMap.set(file.name.toLowerCase(), file); }); const toTextureUrl (rawPath: string): string | null { const fileName rawPath.split(/[\\/]/).pop(); if (!fileName) return null; const file textureMap.get(fileName.toLowerCase()); if (!file) { console.warn(纹理没有配对成功, rawPath, Array.from(textureMap.keys())); return null; } const url URL.createObjectURL(file); this.objectUrls.push(url); return url; }; const rewriteTextureField (holder: any, key: string): void { if (!holder || typeof holder[key] ! string) return; const url toTextureUrl(holder[key]); if (url) holder[key] url; }; // 字段兜底不同导出器可能用 textureName 或 texture rewriteTextureField(json, textureName); rewriteTextureField(json, texture); // 兼容纹理数组结构逐项替换 if (Array.isArray(json.textures)) { json.textures json.textures.map((item: any) { if (typeof item string) { return toTextureUrl(item) || item; } if (item typeof item.url string) { const url toTextureUrl(item.url); if (url) item.url url; } return item; }); } const partBlob new Blob([JSON.stringify(json)], { type: application/json }); const partUrl URL.createObjectURL(partBlob); this.objectUrls.push(partUrl); Laya.loader.load( partUrl, Laya.Handler.create(this, (particle: Laya.Particle2D) { if (!particle) { console.error(粒子加载失败检查纹理字段是否已替换成可访问 URL); return; } if (this.particle) { this.particle.destroy(); } this.particle particle; particle.emitter.x Laya.stage.width / 2; particle.emitter.y Laya.stage.height / 2; particle.play(); Laya.stage.addChild(particle); }) ); }这段代码的关键点在toTextureUrl和rewriteTextureField。toTextureUrl只取文件名部分忽略原路径里的目录因为用户选文件时目录结构已经丢了只能按文件名配对。统一转小写是为了兼容 Windows 导出的文件名浏览器里 URL 大小写敏感不转就容易配不上。rewriteTextureField做了字段兜底先试textureName再试texture。如果两个字段都存在后一个会覆盖前一个实际项目里一般只会有一个问题不大。纹理数组的处理是额外保险避免某些导出结构漏掉。参数上注意partBlob的 MIME 类型写成application/json有些加载器会根据 MIME 判断解析方式。URL.createObjectURL(partBlob)生成的 URL 也要记进objectUrls后面统一释放。4.3 加载、销毁与资源释放一套不泄漏的调用顺序本地预览最容易忽略的是资源释放。用户反复选文件每次都会创建新的 Blob URL、新的 Particle2D、新的纹理。如果不销毁旧粒子、不清理加载器缓存内存会一路涨最后页面卡死。我一般按这个顺序释放private releaseCurrent(): void { // 先销毁粒子断开纹理引用 if (this.particle) { this.particle.destroy(); this.particle null; } // 再清加载器缓存最后释放 Blob URL this.objectUrls.forEach((url) { Laya.loader.clearRes(url); URL.revokeObjectURL(url); }); this.objectUrls []; }顺序不能反。先revokeObjectURL再销毁粒子纹理可能还在渲染会出现贴图丢失或报错。先销毁粒子再clearRes最后释放 URL引用关系断得干净。Laya.loader.clearRes不是所有资源类型都必须调用但对 Blob URL 这种一次性地址清缓存能避免加载器内部保留旧键。尤其是同一个文件名反复选URL 每次都是新的不清缓存会积累。提示每次选择新文件前先调releaseCurrent()不要等加载成功再释放旧粒子。加载失败时旧粒子还在至少能让用户看到上一版效果。5. 避坑与排查粒子预览链路上的五类翻车现场5.1 文件与路径类404、白块与大小写第 1 条控制台报纹理 404粒子完全不显示。现象是.part加载成功回调也进来了但控制台里贴图地址是blob:.../tex/fx_01.png这种拼出来的怪路径。原因是 Blob URL 没有目录概念.part里的相对纹理路径被加载器当成了相对于 Blob URL 的路径浏览器自然找不到。解决办法是在加载前把.part读成 JSON把textureName或texture字段替换成贴图的 Blob URL。不要试图给 Blob URL 设置 basePath加载器不认这种目录关系。重写字段是本地文件预览里最稳的方案。第 2 条粒子出现了但整块白或者整块黑。白块通常是贴图没配对成功粒子用了默认白色纹理黑块可能是混合模式不对Additive 叠在深色背景上过曝或者 AlphaBlend 的透明通道没读对。先检查textureMap里有没有对应文件再看贴图是不是带透明通道的 PNG。如果贴图是 JPG透明区域会变成黑色或白色粒子边缘会很难看。混合模式先保持导出默认确认形状后再改不要一次动两个变量。第 3 条本地能加载换台机器就配不上贴图。现象是同一个.part和贴图在 A 机器上正常B 机器上纹理映射失败。原因多半是文件名大小写或中文路径。Windows 资源管理器不区分大小写浏览器 URL 区分中文文件名在 Blob URL 和某些加载器里也可能被转义。解决办法是导出时统一小写英文、无空格、无中文代码里再做一层toLowerCase()映射。不要依赖用户选文件时的原始大小写统一转小写最省事。5.2 生命周期与交互类旧粒子残留与按钮没反应第 4 条第二次选择文件后旧粒子还在内存一直涨。现象是连续选几次文件舞台上叠了好几层粒子帧率越来越低。原因有三个旧Particle2D没销毁、Blob URL 没释放、加载器缓存没清。解决顺序是“先销毁粒子再清缓存最后 revoke URL”。代码里把releaseCurrent()放在每次加载新文件之前不要放在加载成功之后。加载失败时旧粒子还能保留用户至少能看到上一版效果。另外objectUrls数组要覆盖.part的 Blob URL 和所有贴图的 Blob URL漏一个都会泄漏。第 5 条点击“选择文件”没反应移动端尤其明显。现象是桌面端偶尔能弹移动端点了没动静。原因通常是input.click()不在用户手势调用栈里或者 input 被 Canvas 遮挡、被 CSSpointer-events: none干掉。解决办法是用label的for关联 input让浏览器把点击当成用户直接操作或者把 input 透明覆盖在按钮上保证点击命中真实 input。不要用setTimeout包一层再触发移动端会判定不是用户手势。工具栏容器可以设pointer-events: none但按钮和 input 必须恢复auto否则点击穿透不到。6. 进阶验证用调试面板核对贴图、发射器与帧率粒子能显示之后下一步是确认它“显示得对”。我一般会挂一个调试引用到window加载完成后在控制台直接看粒子对象比反复改代码打日志快。// 粒子加载成功后挂到 window方便控制台直接检查 (window as any).__layaParticle particle; console.table({ emitterX: particle.emitter.x, emitterY: particle.emitter.y, stageWidth: Laya.stage.width, stageHeight: Laya.stage.height, });控制台里重点看三个检查点检查点观察方式判定标准贴图尺寸控制台查看粒子纹理的宽高不是 1x1 或 0x0白块多半是纹理没加载发射器坐标对比emitter.x/y和舞台宽高在可视区内不要是 0,0 或负数帧率每秒累计帧数或引擎统计面板连续选文件后帧率不持续下降帧率可以用一个很土但有效的办法每秒累计一次帧数。let frames 0; Laya.timer.frameLoop(1, this, () { frames; }); Laya.timer.loop(1000, this, () { console.log(FPS 约等于, frames); frames 0; });如果每次选择文件后 FPS 都往下掉基本可以断定旧粒子和 Blob URL 没释放干净回到第 4 章的releaseCurrent()检查顺序。如果粒子显示但位置偏先看emitter坐标再看舞台有没有缩放。如果粒子是白块先看纹理映射再看贴图透明通道最后才看混合模式。血泪经验是不要一上来就调粒子的生命周期和发射速率。先把“文件进来了、纹理对上了、发射器在屏幕内”这三件事验证完再去调效果。从那以后我每次接 Unity 粒子预览都先把.part、贴图、命名映射这三样在控制台打一遍再谈效果调优。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑