
上手欧美卡通风格的舞台场景最难的不是把画面画好看而是让美术在PS里画的东西原封不动、高效地变成Unity里的完整舞台。我在某项目里就吃过这个亏舞台帷幕、装饰摆件、地面光斑、背景大屏加起来几十张贴图美术手动导出一次程序手动拖进Unity一次再手动调一遍导入参数结果图没对齐、参差不齐、命名混乱返工改到深夜。这篇是我整理的 PS 到 Unity UI/2D 自动化工具链第一篇核心是把欧美卡通舞台的视觉规范定下来再让“PS 导出”和“Unity 导入”这两个环节尽量全自动。项目用的是最常规的 Unity 2019 LTS 加 2D 工作流适合独立开发者、小团队也适合被资源导入折磨到想写脚本的美术同学参考。1. 一次返工逼出来的自动化思路为什么先做工具链再搭舞台1.1 手动流程到底有多痛先说一个很典型的半天工作流。美术在 PS 里画好一张舞台背景导出 PNG命名叫“最终版2_改”拖进 Unity发现透明通道发灰手动把 Texture Type 改成 Sprite、把 Compression 改成无压缩再拖进场景调 Sorting Layer。搞定。下一张装饰摆件再来一遍。十几张图至少大半天没了。更要命的是Unity 的导入参数很多时候不是画完才设置而是每次改动贴图后都要确认一遍。如果你在 PS 里把帷幕颜色从红色改成了酒红色导出文件覆盖之后Unity 还得重新检查参数。如果某张图忘了关 mipmap移动端上就会平白多出几十MB内存这问题在项目后期才爆出来排查成本极高。我那时候总结了一句话在2D美术资源管线的场景里手动操作越少踩的坑越少。工具链的本质不是“做一个编辑器窗口”而是把“人肉重复劳动”替换成“脚本按规则执行”。1.2 工具链带来的三个变化这套工具链做完之后最直观的三个变化是第一命名规范了美术不需要记忆复杂规则直接在图层命名里打上标签导出脚本自动分类第二Unity 导入参数不再靠肉眼检查AssetPostprocessor 会在贴图导入时按目录自动配置第三PS 到 Unity 中间多了一层 JSON 元数据每张贴图的尺寸、偏移、排序层级都在 JSON 里场景搭建阶段可以直接读取减少大量重复拼图。很多刚开始做自动化的人会担心写脚本的时间比手动导图的时间还长值得吗我的回答是只要你的项目里同一个流程要重复三次以上就值得。我这边因为舞台场景会频繁换版本每隔两三周就要重新出一套背景和装饰脚本初版写了两天但后面每次改版都省下半天到一天回本很快。1.3 第一篇的边界这次做到哪一步下一步做什么既然是第一篇我不打算一次把所有功能都铺开。这一篇先把底子打好让一张图片从 PS 到 Unity 的路径自动跑通并且能搭出一个基础舞台场景。具体包括欧美卡通舞台的视觉拆解和美术规范、PS 导出脚本的核心设计、Unity 侧自动导入参数的处理、Sprite 图集构建思路、舞台排布和灯光氛围的基础做法以及我踩过的一堆坑。程序化摆放、舞台元素随机组合、音乐律动同步、换装系统这些属于后续第二篇、第三篇的内容这篇不展开。如果你只想看某一段可以直接跳到对应章节但我建议顺序读一遍因为很多坑是跨环节的比如 PS 里导出的颜色和 Unity 里看到的对不上这类问题只在整条链路都过一遍之后才会暴露。2. 欧美卡通舞台的视觉拆解剪影、配色与分层标准2.1 “卡通味”从哪来剪影、轮廓线与高饱和配色很多同学以为欧美卡通风格就等于“描个黑边”其实不对。你看那些经典的欧美卡通舞台场景第一眼抓人的其实是剪影大色块、大比例、大弧线。帷幕的波浪、舞台拱门的圆角、装饰物的夸张尺寸都在剪影阶段就定了调。所以我在美术规范里第一件事不是定描边粗细而是定剪影语言。比如舞台正面拱门要求是一条连贯的大弧线不要碎成一堆小装饰装饰树木或花盆要求比正常比例大 1.2 到 1.5 倍让视觉重心更稳。这个阶段可以用纯色块在 PS 里先摆一版不需要细节只需要确认整体轮廓的辨识度。然后是轮廓线。欧美卡通通常有明确的深色描边我的经验是描边宽度不统一是手绘转 Unity 时“最显廉价”的原因。我在规范里直接规定主体轮廓线 6px按 2048 尺寸基准内部结构线 3px最细的装饰线不低于 2px。这个数值不是固定的但你必须在项目里定一个统一基准Unity 侧的显示才稳定。配色上欧美卡通风格的特点是饱和度偏高、明暗对比强烈。通常我会让美术在 PS 里用一个受限的色板比如舞台帷幕红色系只允许 4 个色阶避免出现太多灰蒙蒙的中间调。写实风格需要大量中间过渡色但卡通舞台恰恰要敢用大平涂色阶分明才有“卡通感”。2.2 舞台场景的通用分层背景层、装饰层、角色层与前景层舞台场景从纵深上天然分几层最底下的背景墙、中间的舞台地板和拱门、再往前是道具装饰、角色站位区最前是帷幕和光晕等前景元素。我在 PS 的图层结构里也按这个顺序来建组导出脚本才能直接映射到 Unity 的 SortingLayer。这里有个很容易犯的错误美术喜欢在 PS 里把所有图层都画在同一个画布上背景和前景叠在一起导出成一张大图。这样做虽然省事但在 Unity 里想要做遮挡、做动画、做置换的时候就很麻烦。我的建议是相同纵深关系的元素合成一张图不同层级的元素必须拆开导出。比如“背景墙地面”可以合成一张但“前景帷幕”必须单独导出。分层还有一个好处处理灯光和后期时你可以只对前景层做模糊对背景层做不同色调视觉层次一下子就能拉开。后文工具链里的 JSON 清单也是基于这套分层来记录每张贴图应该落在哪个排序层级。2.3 色彩空间在 2D 项目里的决策Gamma 还是 Linear这个看起来像技术参数但直接影响美术在 PS 里调好的颜色会不会在 Unity 里“变灰”。 我在项目里用的是Gamma 色彩空间。原因很简单2D 手绘贴图本来就是在 sRGB 下绘制和查看的纹理文件里的颜色值就是最终显示的颜色。开了 Linear 之后贴图默认按 sRGB 解码再做光照计算如果材质和渲染管线的补偿没做好颜色会整体变暗或偏色。当然用 URP 或者 HDRP 做 3D 风格化渲染时Linear 是通用选择但纯 2D 卡通舞台项目我的经验是别折腾。这篇文章里的工具链默认“纹理保持 sRGB项目色彩空间用 Gamma”后面踩坑章节还会讲一次我试图改 Linear 之后的教训。3. 工具链整体架构PS导出、中间规整、Unity导入三段式3.1 完整链路拆分从 PS 到 Unity 要经过哪几步整套流程我分成三段。第一段是 PS 侧的 ExtendScript 脚本它负责读取一个约定好的 PSD 文档按照图层组和图层命名把目标元素导出成单独的 PNG并且在导出完成后生成一份 JSON 清单。第二段是中间规整也就是一个命令行或手动执行的小工具用来检查 PNG 文件和 JSON 清单是否完整文件命名是否规范这个步骤可以放在 CI 里也可以不放我最早是手动双击运行。第三段是 Unity 侧的脚本。这里分成两部分一部分是 AssetPostprocessor在贴图导入时自动设置参数另一部分是编辑器脚本在导入完成后读取同目录下的 JSON 清单把 Sprite 的元数据写到 ScriptableObject 里供场景搭建阶段使用。这根链条最核心的思路是让文件系统成为美术和程序之间的唯一接口。美术把 PSD 按规范建好脚本负责导出程序不关心美术在 PS 里怎么组织图层只关心最终 PNG 和 JSON 的字段是否合规。3.2 为什么用 JSON 清单而不是只靠文件名猜参数有人可能觉得PNG 文件名带个 _Back 后缀Unity 那边按后缀解析不就行了为什么还要一份 JSON我的理由是文件名可以做简单的分类标记但承载不了完整信息。比如一张装饰树的贴图它除了是装饰层之外还要记录它在画布中的原始位置、它是否需要开启 alpha、它的 pivot 应该放在哪里、它属于哪一组可交互元素。这些信息塞进文件名会乱七八糟可读性极差。JSON 清单就干净多了。PS 脚本导出时按元素把它的 bounds原始画布内的矩形位置、图层的可见性、所属分组名称、导出的 PNG 文件名、建议的 pivot 位置都记录下来。Unity 侧的导入器读取这份 JSON 后可以为每个 Sprite 自动创建导入配置。以后再想加字段比如“这个元素参与律动动画”“这个元素是聚光灯”直接在脚本和 JSON 结构里加不需要改 Unity 场景里的任意一个组件。3.3 命名规范与目录结构自动化工具链的地基没有命名规范脚本写得再漂亮也白搭。我最终定的规则是这样的资源类型目录前缀命名示例说明背景层BGStage_Back_MainHall_01通常指背景墙地板舞台结构FRAMEFrame_Arch_01拱门、立柱、舞台边缘装饰道具PROPProp_Plant_03可拆分的装饰物件前景层FGFg_Curtain_Left_01帷幕、挂帘、光晕灯光特效FXFx_Spotlight_Soft_01光斑、聚光灯贴图所有 PNG 统一放Assets/AutoImported/下的子目录比如Assets/AutoImported/BG/、Assets/AutoImported/FX/。Unity 侧的导入脚本只监听这个目录原生的项目美术资源目录不受影响。目录结构和命名规范是两件事但必须一起定。刚开始做自动化的人常常只定命名忘了定目录结果脚本到处监听项目层级一乱后续所有规则都失效。我的建议是先定目录再定命名最后才是脚本逻辑。4. 在PS里写自动导出脚本图层约定与批量出图4.1 前端约定图层组怎么建才能被脚本识别PS 导出脚本的第一步其实就是“美术怎么建图层组”。我在项目里约定的模板是这样的PSD 根目录下必须有一个顶层组叫StageRoot里面按BG、FRAME、PROP、FG、FX五个分组再建子组。每个需要单独导出的元素在子组里单独占一个图层或一个图层组。为了让脚本能识别“哪些图层要导出、哪些不用”我建议每个导出元素的名字里带一个标记比如[out]前缀或_out后缀。只有带标记的图层才会进入导出列表。这样做的好处是美术可以放参考图层、草稿图层、色板图层在同一个 PSD 里只要不写_out就不会污染导出结果。同时所有需要导出的小组PS 里默认必须是独立对象不要和别的元素共用蒙版或混合模式不然脚本很难稳定切图。这个环节没有捷径必须在项目一开始就给美术讲清楚否则后期靠脚本去猜全是在补坑。4.2 ExtendScript 脚本的核心逻辑其实 PS 的 ExtendScript 就是 JavaScript 的变种只要能遍历图层组、复制文档、隐藏多余图层、读取 bounds、保存 PNG这个脚本就能成立。我的核心代码大概长这样// PS导出脚本核心逻辑简化版 var doc app.activeDocument; var rootGroup null; for (var i 0; i doc.layerSets.length; i) { if (doc.layerSets[i].name StageRoot) { rootGroup doc.layerSets[i]; break; } } if (!rootGroup) { alert(未找到 StageRoot 组请检查PSD结构); } var exportDir D:/Projects/AutoExport/; var manifest []; var groupList rootGroup.layerSets; function isExportableLayer(layer) { return layer.name.indexOf(_out) -1; } function hideAllLayers() { for (var i 0; i doc.layers.length; i) { doc.layers[i].visible false; } } function exportLayerGroup(layerGroup) { var exportName layerGroup.name.replace(_out, ); var exportFile new File(exportDir exportName .png); // 复制文档避免动到原始PSD var tempDoc doc.duplicate(temp, true); hideAllLayers(); // 在临时文档中定位同名图层组只让它可见 var tempRoot tempDoc.layerSets.getByName(StageRoot); var targetGroup tempRoot.layerSets.getByName(layerGroup.name); targetGroup.visible true; // 获取 bounds记录到 JSON var bounds targetGroup.bounds; manifest.push({ name: exportName, x: bounds[0].value, y: bounds[1].value, width: bounds[2].value - bounds[0].value, height: bounds[3].value - bounds[1].value }); // 保存为PNG var pngOptions new PNGSaveOptions(); pngOptions.compression 6; tempDoc.saveAs(exportFile, pngOptions, true, Extension.LOWERCASE); tempDoc.close(SaveOptions.DONOTSAVECHANGES); } for (var g 0; g groupList.length; g) { exportLayerGroup(groupList[g]); } // 最后把 manifest 写成 JSON 文件 var jsonFile new File(exportDir manifest.json); jsonFile.open(w); jsonFile.write(JSON.stringify(manifest)); jsonFile.close();这段代码是思路演示真正跑起来还需要处理 PS 的坐标单位和众多 API 细节但核心逻辑就是这样。最关键的技巧是导出时不要直接保存原文档而是复制一份临时文档只把目标图层设为可见后再保存。这样原 PSD 的图层可见性状态永远不会被脚本破坏。4.3 一次真实导出的输出示例按上面的脚本逻辑假如我的舞台项目里有这几个元素Stage_Back_MainHall_outFrame_Arch_Left_outProp_Plant_03_outFg_Curtain_Left_out导出完成后AutoExport目录下会有四个 PNG同时manifest.json长这样[ { name: Stage_Back_MainHall, x: 0, y: 0, width: 2048, height: 1400 }, { name: Frame_Arch_Left, x: 120, y: 300, width: 320, height: 760 }, { name: Prop_Plant_03, x: 800, y: 900, width: 240, height: 380 } ]Unity 侧导入器拿到这份清单后就能知道每个 Sprite 在舞台画布中的原始位置、尺寸边界。后面做舞台摆位时甚至可以在脚本里按坐标自动摆放不需要人肉对齐。5. Unity侧自动导入管线AssetPostprocessor与图集构建5.1 自动识别目录并按类型配置导入参数Unity 里每一个资源导入时都会走AssetPostprocessor这是做自动化管线最常用的扩展点。我写了一个AutoTextureImporter监听Assets/AutoImported/目录下的所有贴图只要文件名以.png结尾就自动把导入参数设好public class AutoTextureImporter : AssetPostprocessor { private void OnPreprocessTexture() { string lowerPath assetPath.ToLower(); if (!lowerPath.StartsWith(assets/autoimported/)) { return; } TextureImporter importer (TextureImporter)assetImporter; importer.textureType TextureImporterType.Sprite; importer.spritePixelsPerUnit 100; importer.alphaIsTransparency true; importer.mipmapEnabled false; importer.sRGBTexture true; importer.filterMode FilterMode.Bilinear; importer.textureCompression TextureImporterCompression.Uncompressed; importer.spriteImportMode SpriteImportMode.Single; } }这个配置有几个细节值得展开说。第一2D 项目里 mipmap 必须默认关掉否则 UI 或 Sprite 在缩小时会有不自然的颜色过渡还会增加内存。第二spritePixelsPerUnit定为 100 是 Unity 默认值如果团队有不同想法最好从第一天就统一。第三alphaIsTransparency开启后Texture 边缘的透明度处理会友好很多但要注意它跟资源压缩方式有关某些压缩格式可能会忽略这个标记。如果你有特殊需求比如有些贴图不需要像素完美可以用文件名加_noAlpha后缀在脚本里做分支判断。文件命名规范在这里和导入脚本形成了闭环。5.2 Sprite图集构建自动放进一个图集还是手动分组图集这个东西小的项目可以拖后做但舞台场景这种资源量图集最好一开始就规划。我用的方案是PS 导出后的 PNG 先按目录自动导入然后由编辑器脚本分批创建SpriteAtlas一个目录对应一个图集比如Atlas_BG.spriteatlas管所有背景Atlas_Props.spriteatlas管所有装饰。用代码创建图集不复杂大致思路是在OnPostprocessAllAssets里检查新增资源是否落到了Assets/AutoImported/下然后找到对应的图集把新导入的 Sprite 塞进去。private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { foreach (string assetPath in importedAssets) { if (!assetPath.Contains(/AutoImported/)) continue; string atlasPath GetAtlasPathForAsset(assetPath); SpriteAtlasAsset atl AssetDatabase.LoadAssetAtPathSpriteAtlasAsset(atlasPath); if (atl null) continue; Object[] objects new Object[] { AssetDatabase.LoadAssetAtPathSprite(assetPath) }; atl.Add(objects); } }这个方案的好处是资源一进工程就自动入图集美术不需要手动拖拽。坏处是图集管理对目录结构很敏感如果你把资源放错目录图集就会缺失。所以一定要把目录规范做成团队默认共识。5.3 元数据配置文件让Unity场景知道“这张贴图该摆哪”PNG 和 JSON 都导入之后还差一步如何让场景搭建时知道每张 Sprite 的原始画布位置这个信息我存在 ScriptableObject 里名字就叫StageElementMeta。[CreateAssetMenu(fileName StageElementMeta, menuName Custom/StageElementMeta)] public class StageElementMeta : ScriptableObject { public string elementName; public string elementGroup; public Vector2 canvasPosition; public Vector2 canvasSize; }Unity 侧的导入器读 JSON 后为每个元素创建一个对应的 StageElementMeta 资源和 PNG 放在同一个目录下。后续写场景搭建编辑器时直接读这些 meta按 canvasPosition 和画布基准点把 SpriteRenderer 摆到对应世界坐标。这一步做完整条工具链才真正闭环。6. 搭建舞台场景排序、灯光与后期氛围6.1 搭建的第一步先把灰盒摆出来再细化很多人搭舞台场景直接往场景里拖贴图结果层次乱了再慢慢调。我习惯先做一版“灰盒”用纯色或最简单的占位方块按舞台的纵深关系摆出背景墙、拱门、道具、前景的位置确认大小比例、遮挡关系、镜头取景都没问题再换成正式贴图。灰盒阶段特别适合验证一件事角色站在舞台的哪个区域不会被前景遮挡。欧美卡通舞台的视觉重心通常偏中下方角色站位的中心点大概在舞台中偏左或中偏右具体要看设计稿。如果灰盒阶段发现前景帷幕挡住了角色站位区直接调灰盒位置就行成本几乎为零。6.2 SortingLayer排序规划一张表解决场景混乱Unity 的 2D 渲染排序由 SortingLayer 和 SortingOrder 两个参数决定。同一层级内SortingOrder 越大越靠前。我的舞台项目排序表如下SortingLayer典型元素默认 SortingOrderBG背景墙、地板0FRAME拱门、立柱100PROP装饰摆件、花盆200CHARACTER角色站位300FG前景帷幕、装饰挂链400FX光斑、聚光灯特效500这张表的实质是把美术分层直接映射成渲染排序。PS 里怎么分层Unity 里就怎么排。工具链里的 JSON 清单也写了每个元素所属的 groupUnity 导入器可以直接按 group 设置好 SortingLayer甚至不需要人工干预。6.3 卡通舞台的灯光怎么做2D光照与贴图模拟两手抓欧美卡通舞台通常都有明显的聚光灯效果让人一眼看出“这是舞台”。实现方案有两种一种是用 URP 的 2D Renderer 加 2D Light另一种是直接用光斑贴图配合 Additive 混合模式。我的建议是如果项目只是 2D 舞台不打算做复杂的动态光影贴图模拟方案更稳。做法很简单在 PS 里画一张中心亮、边缘柔和的圆形光斑贴图导出为Fx_Spotlight_Soft_01.png放到 FX 层用 Additive Shader 或材质混合模式叠加在舞台上。因为不需要光照计算这套方案在任何渲染管线都能跑。如果你想用 URP 的 2D Light也可以。给光源设置Sprite Light类型把光斑贴图当光源的 Sprite它就能按 Sprite 形状照亮场景。这个方案的好处是光斑可以被角色遮挡形成真实的影子效果。但要注意 URP 2D Light 需要对场景物体添加Shadow Caster 2D组件否则影子的形状不可控。6.4 后期效果和色彩统一让舞台“活”起来搭完所有贴图后灯光和后期才是让舞台从“死板”到“生动”的关键。我用的后期配置是Bloom泛光、Color Adjustments色彩微调和 Vignette暗角。Bloom 对舞台这种高饱和卡通场景特别有效可以让光斑和装饰上的亮色边缘有一点发光感瞬间有演出氛围。Color Adjustments 里我会把饱和度和对比度都稍微调高让颜色更有“卡通感”。这里注意如果你用的是 Gamma 色彩空间后期参数的调整幅度要保守一些不然容易过曝。Vignette 则是把画布四周压暗一点点让观众的视线集中在舞台中央角色身上。这三个参数够用不需要堆一大堆 Post Process。7. 实测踩坑透明边缘、渲染排序与颜色失真7.1 透明贴图边缘发灰看起来像一圈白边把 PS 导出的透明 PNG 放进 Unity最常遇到的情况是本来边缘渐变的透明区域出现了一圈灰色或白边。这个问题在浅色背景上尤其明显。根本原因是PS 里透明像素的颜色通道可能不是纯黑而是留下了抗锯齿的中间色。Unity 在按 alpha 混合时如果颜色通道数值偏高透过半透明像素看到的背景就会偏亮或偏灰。解决办法有两个。第一个是在 PS 导出时清理边缘使用“图层样式-去边”或“Matting”功能让透明边缘的颜色值统一收敛。第二个是在 Unity 导入参数里把alphaIsTransparency打开让纹理在导入时做一次 alpha 与颜色通道的补偿。如果你的材质是自定义 Shader也可以加一句col.rgb / max(col.a, 0.001);把颜色通道除以 alpha再做混合。这个操作能有效消除透明边缘发灰但会影响非透明区域的亮度用之前要测试一下。7.2 半透明物体排序错乱前景帷幕透底舞台场景里有大量半透明元素比如帷幕、光斑、装饰纱幔。排排序时最容易出的问题是前景的半透明帷幕被后面不透明的装饰物体挡住了或者两者穿插显示看起来像透明层叠错了。这个问题说起来简单排查起来最容易让人抓狂。Unity 的 SpriteRenderer 渲染排序同一 SortingLayer 内按 SortingOrder但不同 SortingLayer 之间先按 Layer 排序。如果你把帷幕放到了 FX 层但实际它的语义是前景那么它的渲染顺序就会被 FX 层一起带到最后导致后面的装饰物体盖过帷幕。我踩坑之后做的事是把所有元素按“语义”而不是按“美术效果”来分排序层。帷幕属于前景就必须在 FG 层哪怕它的视觉上像特效。另外半透明材质最好统一用同一渲染队列我一般设到Transparent队列避免某些元素跑到不透明队列里被深度缓冲卡住。7.3 颜色空间不一致导致色差PS里好看进Unity发暗一次项目里美术在 PS 里把光斑调成暖黄色导进 Unity 后发现整体发暗、偏棕怎么调色都不对。排查了半天最后发现是项目色彩空间被设置成了 Linear。这个项目虽然是 2D 的但当时有人为了接入某个 3D 特性改了设置忘了改回来。在 Linear 空间下纹理默认按 sRGB 参与光照计算再做 gamma 校正。SpriteRenderer 默认的材质如果没有做对应处理颜色就会比 PS 里暗一截。我们的舞台场景没有 3D 光照需求所以最终把 Player Settings 里的 Color Space 改回了 Gamma颜色立刻和 PS 里一致了。如果你必须用 Linear那么请在材质 Shader 里手动对贴图做sRGB采样half4 texColor tex2D(_MainTex, uv); #if defined(UNITY_COLORSPACE_LINEAR) texColor.rgb LinearToGammaSpace(texColor.rgb); #endif这样能让 Sprite 在 Linear 管线里也还原出 PS 里编辑的原始颜色。但说实话纯 2D 项目没这个必要别折腾。跌跌撞撞走到这里我最深的体会是这套工具链真正值钱的不是那些脚本代码而是项目里定下的“规范和边界”。美术知道怎么建图层组程序知道资源一进目录就会被自动处理策划知道改一版背景只需要重新出图再覆盖整个团队不用在某个人脑袋里找流程。做全自动化工具链很多时候是在做“团队之间如何沟通”这件事脚本只是把共识固化的工具。下一步我准备在这个基础上做程序化舞台摆位把最后一步搭场景也变成半自动操作等实践一轮再写第二篇。