【VFX工具链源码拆解 07】不进 Play 模式也能看特效!手写 3D 离屏预览与封面自动烘焙引擎

发布时间:2026/11/3 19:21:29
【VFX工具链源码拆解 07】不进 Play 模式也能看特效!手写 3D 离屏预览与封面自动烘焙引擎 本文为《Unity VFX 提效工具源码拆解》系列第 7 篇承接第 6 篇的拖拽与引用隔离。本篇将已入库的 Prefab 转成可交互预览再把观察到的构图保存为封面。一、资源库缺少的不是另一张静态缩略图特效的主要信息存在于时间过程中一张缩略图难以说明粒子何时爆发、刀光如何移动、循环是否连贯。若每次浏览都进入 Play 模式或临时搭场景美术需要在检索与验证之间反复切换新人还要记住灯光、相机和重播方式。预览工具应当接受一个明确的 Prefab在资源浏览上下文中提供可重复的观察环境。这里的输入是GameObject prefab与预览矩形Rect输出有两种每次重绘显示的离屏图像以及用户烘焙时返回的Texture2D封面。VFXCategoryOverview负责传入资源和接收封面VFXStudioUtils.VFXPreviewEngine负责预览对象、时间与渲染。离屏渲染把观察环境与工作层级分开Unity 原生PreviewRenderUtility提供用于编辑器预览的场景、相机和灯光。工具把待预览 Prefab 实例化到该环境渲染后把纹理绘制到面板矩形。这样可以减少对当前工作场景层级的干扰并集中管理相机与临时实例。这并不等于完整复现游戏运行环境。依赖业务脚本生命周期、场景灯光、全局管理器、物理状态或特定渲染管线的效果仍需进入实际运行环境验证。在特效资产库中选中 Prefab 后右侧交互预览显示临时实例的动态效果拖动视图调整观察角度与平移滚轮改变视野。资源选择、时间推进和面板显示在同一窗口完成减少了为每次浏览建立测试场景的步骤。二、预览实例如何建立并复用相机和灯光在首次使用时创建RenderVFXPreview被资源概览反复调用因此不能每次调用都新建完整环境。下面节选采用延迟初始化_utility为空时才创建工具并配置相机裁剪面与基础灯光。// PreviewRenderUtility 延迟初始化以下保留实际处理语句外围上下文见正文。if(_utilitynull){_utilitynewPreviewRenderUtility();_utility.camera.fieldOfView30f;_utility.camera.nearClipPlane0.01f;_utility.camera.farClipPlane1000f;_utility.ambientColornewColor(0.2f,0.2f,0.2f);_utility.lights[0].intensity1.2f;_utility.lights[0].transform.rotationQuaternion.Euler(50,-30,0);}fieldOfView、nearClipPlane、farClipPlane属于相机参数决定能看见的空间ambientColor与预览灯光提供默认照明。它们是便于浏览的起始条件不是对所有特效正确曝光和管线兼容的保证。切换 Prefab 时更新实例和组件缓存若仍预览同一资源工具复用现有实例资源改变时销毁旧实例再收集需要驱动的组件。// 预览实例切换与组件缓存以下保留实际处理语句外围上下文见正文。if(_currentPrefab!prefab){if(_instance!null)Object.DestroyImmediate(_instance);_currentPrefabprefab;if(prefab!null){_instance_utility.InstantiatePrefabInScene(prefab);_cachedRenderers_instance.GetComponentsInChildrenRenderer(true);_cachedParticleSystems_instance.GetComponentsInChildrenParticleSystem(true);_cachedAnimators_instance.GetComponentsInChildrenAnimator(true);_cachedAnimations_instance.GetComponentsInChildrenAnimation(true);_cachedDirectors_instance.GetComponentsInChildrenUnityEngine.Playables.PlayableDirector(true);}}_currentPrefab是资源身份_instance是预览场景中的临时对象不能将二者混为同一份资产。InstantiatePrefabInScene建立预览实例随后把 Renderer、粒子、Animator、旧版 Animation 和 Director 分别存入数组避免每轮重绘重新扫描层级。组件数组包含未激活孩子。这扩大了可观察范围但也意味着后面的驱动逻辑需要关注父子组件是否被重复推进。三、编辑模式下工具需要主动维护时间用编辑器时钟生成增量与累计时间没有进入 Play 模式时不能假定所有组件会按游戏帧自行更新。工具从编辑器启动时钟取差值再乘以面板速度。// 编辑器时间与速度以下保留实际处理语句外围上下文见正文。floatrawDelta(float)(EditorApplication.timeSinceStartup-_lastEditorTime);_lastEditorTimeEditorApplication.timeSinceStartup;floatscaledDeltarawDelta*_timeSpeed;_timescaledDelta;rawDelta是两次执行间隔scaledDelta是乘过_timeSpeed的增量_time是累计预览时间。增量适合推进粒子与 Animator累计时间适合采样动画或设置 Director 时间。重绘调度受到编辑器负载影响因此这个时钟不是固定步长。暂停、切换资源或长时间未重绘后较大增量的处理需要特别测试不能把面板播放速度当成确定性的离线模拟。不同组件使用不同的驱动接口下面原文展示粒子生命检查、Animator 更新、旧动画采样与 Timeline 评估。// 粒子 Animator Animation Director驱动以下保留实际处理语句外围上下文见正文。boolisAlivefalse;foreach(varpsin_cachedParticleSystems)if(ps!nullps.IsAlive(true))isAlivetrue;if(!isAlive_cachedParticleSystems.Length0){_time0;foreach(varpsin_cachedParticleSystems)if(ps!null)ps.Simulate(0,true,true,true);}foreach(varpsin_cachedParticleSystems)if(ps!null)ps.Simulate(scaledDelta,true,false,false);foreach(varanimin_cachedAnimators)if(anim!null)anim.Update(scaledDelta);foreach(varanimin_cachedAnimations){if(anim!nullanim.clip!null){floatclipTime_time%Mathf.Max(0.01f,anim.clip.length);anim.clip.SampleAnimation(anim.gameObject,clipTime);}}foreach(vardirin_cachedDirectors){if(dir!null){dir.timeUpdateModeUnityEngine.Playables.DirectorUpdateMode.Manual;dir.time_time%Mathf.Max(0.01f,(float)dir.duration);dir.Evaluate();}}粒子全部不存活时工具把时间归零并重置模拟随后用Simulate(scaledDelta, true, false, false)增量推进。第二个参数会带上孩子而缓存中也包含孩子粒子因此嵌套系统存在被多次模拟的风险实际工具需要按层级策略验证不能宣称所有复杂粒子都严格只更新一次。Animator.Update使用增量AnimationClip.SampleAnimation以模长后的绝对时间采样Director 改为Manual后设置time并执行Evaluate。若同一对象同时被这些系统控制驱动顺序会影响结果当前实现没有建立完整的冲突调度器。渲染前临时替换 Shader 时钟结束后恢复纹理滚动或溶解常读取 Shader 的时间变量仅推进粒子组件不足以让材质动画显示。工具保存原值用预览时间构造替代值再在finally还原。// Shader时间替换与finally恢复以下保留实际处理语句外围上下文见正文。Vector4originalTimeShader.GetGlobalVector(_Time);Vector4originalSinTimeShader.GetGlobalVector(_SinTime);Vector4originalCosTimeShader.GetGlobalVector(_CosTime);floatoriginalTimeScaleTime.timeScale;Vector4fakeTimenewVector4(_time/20f,_time,_time*2f,_time*3f);Vector4fakeSinTimenewVector4(Mathf.Sin(_time/8f),Mathf.Sin(_time/4f),Mathf.Sin(_time/2f),Mathf.Sin(_time));Vector4fakeCosTimenewVector4(Mathf.Cos(_time/8f),Mathf.Cos(_time/4f),Mathf.Cos(_time/2f),Mathf.Cos(_time));try{Time.timeScale_timeSpeed;Shader.SetGlobalVector(_Time,fakeTime);Shader.SetGlobalVector(_SinTime,fakeSinTime);Shader.SetGlobalVector(_CosTime,fakeCosTime);_utility.BeginPreview(rect,GUIStyle.none);_utility.camera.Render();Textureresult_utility.EndPreview();GUI.DrawTexture(rect,result,ScaleMode.ScaleToFit);}finally{Shader.SetGlobalVector(_Time,originalTime);Shader.SetGlobalVector(_SinTime,originalSinTime);Shader.SetGlobalVector(_CosTime,originalCosTime);Time.timeScaleoriginalTimeScale;}fakeTime对应_Time的四个分量另两组向量供正弦和余弦动画使用。BeginPreview、camera.Render、EndPreview完成一轮离屏渲染GUI.DrawTexture把结果显示到窗口。保存并恢复全局状态是必要的因为这些变量不是当前材质私有字段。不同管线还可能在相机渲染时设置自己的常量这种替换并不覆盖所有 Shader 时间机制。这里的finally恢复时间与缩放但没有进一步包装所有预览资源和相机状态的异常恢复。四、让拖尾有运动让构图有可重复的输入按命名约定为拖尾实例制造位移静止对象的拖尾可能只是一点。源码以资源名称是否包含Trails或“拖尾”决定是否为实例增加圆周运动。// 拖尾预览位移以下保留实际处理语句外围上下文见正文。boolisTrailAssetprefab.name.Contains(Trails)||prefab.name.Contains(拖尾);if(isTrailAsset){floatspeed4f;floatmoveScale_radius*0.4f;_instance.transform.positionnewVector3(Mathf.Sin(_time*speed),Mathf.Cos(_time*speed),0)*moveScale;_instance.transform.rotationQuaternion.identity;}else{_instance.transform.positionVector3.zero;_instance.transform.rotationQuaternion.identity;}_radius来自 Renderer 包围盒影响运动尺度正弦和余弦让实例在一个平面内移动。它是预览用运动并不是读取游戏里的轨迹。依赖其他名称、真实速度和碰撞反馈的拖尾不会因此自动获得完整测试条件。安全框建立窗口构图与输出像素之间的对应相机围绕包围盒与平移偏移定位面板还提供预设视角、重播、速度与封面安全框。安全框用于说明即将裁出的区域不会修改游戏相机或资源自身的尺寸。选中符合命名约定的拖尾资源后临时实例沿圆周运动便于观察轨迹连续性。调整播放倍率、视角和安全框内构图再触发封面烘焙可以把同一观察角度用于资源卡片这些操作改变的是预览状态不是业务场景的运动逻辑。五、交互截图如何变成 256 像素封面先按安全框缩放再从中间裁切BakeCurrentInteractiveView不直接读取屏幕截图。它根据窗口大小与安全框比例计算离屏渲染分辨率使安全框对应最终256 × 256区域。// 安全框缩放与ReadPixels以下保留实际处理语句外围上下文见正文。intfinalSize256;floatsafeBoxSizeMathf.Min(viewWidth,viewHeight)*0.8f;floatscaleFactorfinalSize/safeBoxSize;intrtWidthPixelsMathf.RoundToInt(viewWidth*scaleFactor);intrtHeightPixelsMathf.RoundToInt(viewHeight*scaleFactor);rtWidthPixelsMathf.Max(rtWidthPixels,finalSize);rtHeightPixelsMathf.Max(rtHeightPixels,finalSize);RenderTexturetempRTRenderTexture.GetTemporary(rtWidthPixels,rtHeightPixels,24,RenderTextureFormat.ARGB32);RenderTextureoldRT_utility.camera.targetTexture;_utility.camera.targetTexturetempRT;_utility.camera.aspect(float)rtWidthPixels/rtHeightPixels;_utility.camera.Render();RenderTexture.activetempRT;Texture2Dtex2DnewTexture2D(finalSize,finalSize,TextureFormat.RGBA32,false);intstartX(rtWidthPixels-finalSize)/2;intstartY(rtHeightPixels-finalSize)/2;tex2D.ReadPixels(newRect(startX,startY,finalSize,finalSize),0,0);tex2D.Apply();RenderTexture.activenull;_utility.camera.targetTextureoldRT;RenderTexture.ReleaseTemporary(tempRT);scaleFactor将当前安全框边长映射到输出边长rtWidthPixels/rtHeightPixels保持窗口构图比例。渲染到临时RenderTexture后startX/startY定位居中的正方形区域ReadPixels将 GPU 图像读回 CPU 纹理。像素读回可能使 CPU 等待 GPU适合按钮触发不适合无条件放入高频重绘。当前交互烘焙分支恢复camera.targetTexture并释放临时纹理但直接把RenderTexture.active设为null没有恢复之前的活动目标也没有用try...finally包裹全部过程这应作为维护边界说明。PNG 文件与封面配置有两个保存步骤方法把纹理编码成 PNG写入Assets/VFXStudio_Data/Snapshots导入后返回资产对象。// PNG写入与TextureImporter以下保留实际处理语句外围上下文见正文。stringsavePath${saveDir}/{prefab.name}_snapshot.png;byte[]bytestex2D.EncodeToPNG();File.WriteAllBytes(savePath,bytes);Object.DestroyImmediate(tex2D);AssetDatabase.ImportAsset(savePath,ImportAssetOptions.ForceUpdate);TextureImporterimporterAssetImporter.GetAtPath(savePath)asTextureImporter;if(importer!null){importer.textureTypeTextureImporterType.GUI;importer.alphaIsTransparencytrue;importer.SaveAndReimport();}returnAssetDatabase.LoadAssetAtPathTexture2D(savePath);EncodeToPNG与WriteAllBytes写文件ImportAsset让 Unity 识别变化TextureImporter调整 GUI 类型与透明度处理。启用alphaIsTransparency只影响导入解释不能把原本不透明的背景变成透明。回调收到snapshot后把它写入VFXAssetItem.Icon。若当前卡片只是临时包装则先通过GetOrCreateConfig建立持久化配置再保存引用。这与第 1 篇的配置注册表是同一条数据持久化路径。六、自动快照与临时对象清理默认封面使用独立的固定观察策略另一个入口BakePrefabSnapshot为资源提供默认封面。它新建预览工具与实例把粒子模拟到0.5f根据包围盒估计观察距离再渲染固定尺寸图片。下面节选对应取样和清理阶段。instanceutility.InstantiatePrefabInScene(prefab);instance.transform.positionVector3.zero;ParticleSystem[]pssinstance.GetComponentsInChildrenParticleSystem(true);foreach(varpsinpss)ps.Simulate(0.5f,true,true,false);Renderer[]renderersinstance.GetComponentsInChildrenRenderer(true);BoundsboundsnewBounds(Vector3.zero,Vector3.zero);boolhasBoundsfalse;foreach(varrinrenderers){if(r.bounds.extents.magnitude50f){if(!hasBounds){boundsr.bounds;hasBoundstrue;}else{bounds.Encapsulate(r.bounds);}}}if(!hasBounds)boundsnewBounds(Vector3.zero,Vector3.one);floatradiusMathf.Clamp(bounds.extents.magnitude,0.5f,10f);// 此处省略与本段说明无关的代码。utility.BeginPreview(newRect(0,0,256,256),GUIStyle.none);utility.camera.Render();TexturerawTexutility.EndPreview();RenderTextureprevRTRenderTexture.active;RenderTexture.activerawTexasRenderTexture;tex2DnewTexture2D(256,256,TextureFormat.RGBA32,false);tex2D.ReadPixels(newRect(0,0,256,256),0,0);tex2D.Apply();RenderTexture.activeprevRT;File.WriteAllBytes(savePath,tex2D.EncodeToPNG());}finally{if(instance!null)Object.DestroyImmediate(instance);if(tex2D!null)Object.DestroyImmediate(tex2D);utility.Cleanup();}包围盒过大的 Renderer 被过滤半径会裁剪到预定区间相机目标也包含经验偏移。因此这是资源库默认视角不保证每个特效最强表现帧都出现在半秒也不会自动识别艺术构图。finally销毁临时实例与 CPU 纹理调用Cleanup释放预览环境。交互引擎还提供Dispose窗口销毁时由工作台统一清理。原始资产保留临时对象则应与窗口或烘焙任务保持同样的生命周期。同名资源与特殊渲染依赖需要单独验证两条烘焙路径都以prefab.name命名快照没有追加 GUID 或使用唯一文件路径。同名 Prefab 因而可能覆盖同一 PNG。依赖 VFX Graph、后处理、场景深度或特定光照的效果也不能仅凭组件数组推断已被完整支持。离屏预览的价值在于把动态判断留在资产浏览流程并让图像结果回到配置资产。维护这条链时要同时跟踪预览实例、时钟、渲染目标、CPU 纹理和配置引用避免只看到面板在播放却忽略其资源与状态成本。下一篇预告继续处理 Unity 之外的数据入口拆解 P4 进程执行、动作备注解析与可选宿主工具反射接入。【VFX工具链源码拆解 08】打通上下游管线在 Unity 中集成 P4 异步拉取与 3ds Max 备注智能解析

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询