1. 项目概述:为什么视口预览值得你投入精力
在Unity项目开发中,尤其是涉及复杂场景、多摄像机切换或者需要精确构图时,我们常常会陷入一种困境:你需要在Game视图和Scene视图之间来回切换,反复调整摄像机参数,只为找到一个合适的拍摄角度或确认某个特效是否在画面内。这个过程不仅打断创作心流,更消耗大量时间。这就是“摄像机视口预览”功能要解决的核心痛点——它允许你在Scene视图中,实时看到选定摄像机的拍摄画面,将编辑与预览合二为一。
简单来说,视口预览(Camera Preview)就是在Scene视图的某个角落(或自定义位置)开一个“小窗”,这个窗口实时渲染并显示你指定的某个摄像机所“看到”的内容。它不同于Game视图的全局运行,也不同于Scene视图的上帝视角,而是将特定摄像机的输出画面“画中画”式地嵌入到编辑环境中。对于关卡设计师、灯光师、过场动画(Cinematic)制作者而言,这是一个能极大提升效率的“生产力神器”。
从网络热词的关联性来看,无论是“摄像机激活与访问”还是“unity地图”、“unity uitoolkit”,都指向了开发者对编辑时可视化与工作流效率的深层需求。一个配置得当的视口预览工作流,能让你在摆放道具、调整光源、设置后期效果时,所见即所得,无需猜测与反复验证。接下来,我将结合多年项目实战经验,从基础配置讲起,逐步深入到一套稳定、高效且可扩展的实战工作流。
2. 核心思路与方案选型:手动、脚本与编辑器扩展
实现视口预览,大体有三种思路,各有优劣,选择哪种取决于你的项目阶段和个人工作习惯。
2.1 基础手动配置:利用Unity内置功能
这是最直接、无需编码的方法。在Scene视图左上角的工具栏中,找到“Draw Mode”下拉菜单(默认显示“Shaded”),其下方有一个“Camera Preview”选项。点击后,Scene视图右下角会出现一个预览窗口。
操作流程:
- 在Hierarchy中选中你想要预览的摄像机(GameObject)。
- 在Scene视图中,点击工具栏的“Draw Mode” -> 选择“Camera Preview”。
- 此时,Scene视图内会显示该摄像机的实时画面。
优点:开箱即用,零成本。适合快速检查单个摄像机的视角。缺点与局限:
- 绑定单一:一次只能预览一个选中的摄像机。切换预览目标需要重新选中摄像机并可能重新点击菜单。
- 窗口固定:预览窗口的位置和大小相对固定,无法自定义,有时会遮挡场景编辑区域。
- 功能单一:缺乏快速切换、对比、参数同步等高级功能。
- 状态易失:预览状态不会随项目保存,重新打开Unity或Scene视图后需要重新设置。
注意:这个内置预览窗口的渲染精度和效果,取决于Scene视图本身的显示设置(如渲染路径、后期处理开关)。如果发现预览效果与Game视图不符,请检查Scene视图的“Effects”等工具栏按钮是否开启。
2.2 脚本驱动动态预览:灵活性与可控性
当内置功能无法满足需求时,我们可以通过编写编辑器脚本(Editor Script)来创建更强大的预览工具。核心原理是利用Handles.DrawCamera方法或创建一个临时的RenderTexture并赋值给一个自定义的GUI窗口。
方案A:使用Handles.DrawCamera这是在Scene视图的GUI绘制回调(如OnSceneGUI)中直接绘制摄像机视图的方法。它更轻量,集成度更高。
using UnityEditor; using UnityEngine; [ExecuteInEditMode] public class CameraPreviewInScene : MonoBehaviour { public Camera targetCamera; public Rect previewRect = new Rect(10, 10, 320, 240); // 预览窗口位置和大小 #if UNITY_EDITOR void OnDrawGizmos() { if (targetCamera == null || !targetCamera.isActiveAndEnabled) return; // 在Scene视图绘制预览 Handles.BeginGUI(); GUILayout.BeginArea(previewRect); Handles.DrawCamera(previewRect, targetCamera); GUILayout.EndArea(); Handles.EndGUI(); } #endif }优点:代码简洁,与场景对象绑定,移动摄像机GameObject时预览窗口可能跟随(需额外处理)。缺点:渲染控制相对底层,自定义UI(如标题栏、按钮)需要额外使用GUI API绘制,较为繁琐。
方案B:创建自定义EditorWindow与RenderTexture这是更主流、功能更强大的方式。我们创建一个独立的编辑器窗口,在该窗口中利用RenderTexture来渲染目标摄像机的画面。
核心步骤:
- 创建一个继承自
EditorWindow的类。 - 在
OnGUI方法中,定义一块区域(Rect)。 - 获取或创建目标摄像机,并将其
targetTexture设置为一个RenderTexture。 - 在定义的区域中,使用
GUI.DrawTexture绘制这个RenderTexture。
using UnityEditor; using UnityEngine; public class AdvancedCameraPreviewWindow : EditorWindow { [MenuItem("Tools/Camera Preview")] static void Init() { var window = GetWindow<AdvancedCameraPreviewWindow>(); window.titleContent = new GUIContent("Camera Preview"); window.Show(); } public Camera previewCamera; private RenderTexture previewRenderTexture; void OnGUI() { // 摄像机选择栏 previewCamera = EditorGUILayout.ObjectField("Target Camera", previewCamera, typeof(Camera), true) as Camera; if (previewCamera != null) { // 确保RenderTexture存在且尺寸匹配 if (previewRenderTexture == null || previewRenderTexture.width != (int)position.width || previewRenderTexture.height != (int)position.height) { if (previewRenderTexture != null) previewRenderTexture.Release(); previewRenderTexture = new RenderTexture((int)position.width, (int)position.height, 24); previewRenderTexture.Create(); } // 将摄像机的输出重定向到RenderTexture previewCamera.targetTexture = previewRenderTexture; previewCamera.Render(); previewCamera.targetTexture = null; // 渲染后重置,避免影响Game视图 // 绘制预览 GUI.DrawTexture(new Rect(0, 40, position.width, position.height - 40), previewRenderTexture); } else { EditorGUILayout.HelpBox("请指定一个摄像机进行预览。", MessageType.Info); } } void OnDestroy() { // 窗口关闭时清理RenderTexture,防止内存泄漏 if (previewRenderTexture != null) { previewRenderTexture.Release(); previewRenderTexture = null; } } }优点:
- 独立窗口:可以拖拽、停靠,不影响Scene视图布局。
- 完全可控:可以轻松添加UI控件,如摄像机列表、分辨率切换、画面暂停/录制、显示参数叠加(如FOV、Clipping Planes)等。
- 性能隔离:渲染在独立窗口进行,可以设置不同的更新频率以节省性能。
缺点:需要编写和维护更多代码。
2.3 方案选型背后的考量
对于大多数进阶需求,方案B(自定义EditorWindow)是更优选择。原因如下:
- 工作流友好:独立窗口可以停靠在编辑器界面两侧,形成固定的预览面板,与Scene视图并排工作,这是最高效的布局。
- 功能可扩展:你可以在此基础上增加“多摄像机切换”、“画中画”、“参考帧对比”、“安全框显示”、“曝光/色彩分析”等专业功能,这些在内置预览或简单Gizmos中难以实现。
- 状态持久化:
EditorWindow可以方便地利用EditorPrefs或ScriptableObject保存窗口位置、选择的摄像机、预览设置等,实现“一次设置,长期使用”。 - 资源管理清晰:
RenderTexture的生命周期可以明确管理(在OnEnable创建,OnDisable释放),避免内存泄漏。
因此,后续的实战工作流优化将主要围绕自定义EditorWindow方案展开。
3. 构建健壮的摄像机预览编辑器工具
一个用于生产的预览工具,不能只是一个简单的画面显示器。它需要健壮、易用且功能丰富。我们来一步步构建它。
3.1 工具架构与核心类设计
我们将创建一个名为CameraPreviewTool的编辑器窗口。其核心职责包括:
- 管理预览摄像机列表:支持从场景中添加/移除摄像机,并记住它们。
- 渲染与显示:为每个被预览的摄像机创建并管理独立的
RenderTexture。 - 提供控制界面:切换摄像机、调整预览分辨率、暂停渲染等。
- 持久化配置:保存用户的工具设置。
首先,我们需要一个数据结构来保存每个预览任务的信息:
using System; using UnityEngine; [Serializable] public class CameraPreviewEntry { public string name; // 显示名称 public Camera targetCamera; public RenderTexture renderTexture; public bool isActive = true; public Vector2Int resolution = new Vector2Int(640, 360); // 默认16:9 public float aspectRatio => resolution.x / (float)resolution.y; }然后,是主窗口类的基本骨架:
using UnityEditor; using UnityEngine; using System.Collections.Generic; public class CameraPreviewTool : EditorWindow { private List<CameraPreviewEntry> previewEntries = new List<CameraPreviewEntry>(); private Vector2 scrollPosition; private bool autoRefresh = true; private float refreshInterval = 0.033f; // 约30FPS private double lastRefreshTime; [MenuItem("Window/General/Camera Preview Tool")] public static void ShowWindow() { var window = GetWindow<CameraPreviewTool>(); window.titleContent = new GUIContent("Camera Preview"); window.minSize = new Vector2(350, 200); window.LoadSettings(); } void OnGUI() { DrawToolbar(); DrawPreviewList(); DrawPreviewWindows(); } void Update() { if (autoRefresh && EditorApplication.timeSinceStartup - lastRefreshTime > refreshInterval) { Repaint(); // 触发OnGUI重绘,从而重新渲染摄像机 lastRefreshTime = EditorApplication.timeSinceStartup; } } void OnDestroy() { SaveSettings(); CleanupRenderTextures(); } // ... 其他方法将在下文实现 }3.2 关键功能实现详解
1. 绘制工具栏与列表工具栏提供全局控制,列表管理所有被监视的摄像机。
void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); { if (GUILayout.Button("Add Current Selection", EditorStyles.toolbarButton)) { AddSelectedCameras(); } if (GUILayout.Button("Add All Cameras", EditorStyles.toolbarButton)) { AddAllCamerasInScene(); } GUILayout.FlexibleSpace(); autoRefresh = GUILayout.Toggle(autoRefresh, "Auto Refresh", EditorStyles.toolbarButton); if (GUILayout.Button("Refresh Now", EditorStyles.toolbarButton)) { Repaint(); } } EditorGUILayout.EndHorizontal(); } void DrawPreviewList() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(150)); for (int i = 0; i < previewEntries.Count; i++) { EditorGUILayout.BeginHorizontal(); { previewEntries[i].isActive = EditorGUILayout.Toggle(previewEntries[i].isActive, GUILayout.Width(20)); EditorGUI.BeginChangeCheck(); previewEntries[i].targetCamera = EditorGUILayout.ObjectField(previewEntries[i].targetCamera, typeof(Camera), true) as Camera; if (EditorGUI.EndChangeCheck()) { UpdateEntryName(previewEntries[i]); } EditorGUILayout.LabelField(previewEntries[i].name, GUILayout.MinWidth(100)); if (GUILayout.Button("X", GUILayout.Width(20))) { RemoveEntry(i); i--; // 列表变化,调整索引 } } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); }2. 渲染与绘制预览窗口这是核心,我们需要为每个激活的条目创建/更新RenderTexture并绘制。
void DrawPreviewWindows() { if (previewEntries.Count == 0) { EditorGUILayout.HelpBox("点击上方按钮添加摄像机以开始预览。", MessageType.Info); return; } EditorGUILayout.Space(); EditorGUILayout.LabelField("Live Previews", EditorStyles.boldLabel); // 计算布局:这里简单采用垂直排列,可根据需要改为网格 foreach (var entry in previewEntries) { if (!entry.isActive || entry.targetCamera == null) continue; EnsureRenderTexture(entry); EditorGUILayout.BeginVertical(EditorStyles.helpBox); { EditorGUILayout.LabelField(entry.name, EditorStyles.miniBoldLabel); // 显示分辨率信息 EditorGUILayout.BeginHorizontal(); GUILayout.Label($"Res: {entry.resolution.x}x{entry.resolution.y}"); if (GUILayout.Button("1:1", EditorStyles.miniButtonLeft)) { entry.resolution = new Vector2Int(256, 256); } if (GUILayout.Button("16:9", EditorStyles.miniButtonMid)) { entry.resolution = new Vector2Int(640, 360); } if (GUILayout.Button("4:3", EditorStyles.miniButtonRight)) { entry.resolution = new Vector2Int(640, 480); } EditorGUILayout.EndHorizontal(); // 渲染摄像机到RenderTexture var oldTarget = entry.targetCamera.targetTexture; entry.targetCamera.targetTexture = entry.renderTexture; entry.targetCamera.Render(); entry.targetCamera.targetTexture = oldTarget; // 计算预览绘制区域,保持宽高比 Rect controlRect = EditorGUILayout.GetControlRect(false, entry.resolution.y); Rect previewRect = GetCenteredRect(controlRect, entry.resolution.x, entry.resolution.y); // 绘制背景和纹理 EditorGUI.DrawRect(previewRect, Color.black); if (entry.renderTexture != null) { GUI.DrawTexture(previewRect, entry.renderTexture, ScaleMode.ScaleToFit); } // 在预览上叠加信息(可选) DrawCameraInfoOverlay(previewRect, entry.targetCamera); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(5); } } void EnsureRenderTexture(CameraPreviewEntry entry) { if (entry.renderTexture == null || entry.renderTexture.width != entry.resolution.x || entry.renderTexture.height != entry.resolution.y || !entry.renderTexture.IsCreated()) { if (entry.renderTexture != null) entry.renderTexture.Release(); entry.renderTexture = new RenderTexture(entry.resolution.x, entry.resolution.y, 24, RenderTextureFormat.ARGB32); entry.renderTexture.Create(); } } Rect GetCenteredRect(Rect container, float contentWidth, float contentHeight) { float aspect = contentWidth / contentHeight; float containerAspect = container.width / container.height; float width, height; if (aspect > containerAspect) { // 宽度受限 width = container.width; height = width / aspect; } else { // 高度受限 height = container.height; width = height * aspect; } float x = container.x + (container.width - width) * 0.5f; float y = container.y + (container.height - height) * 0.5f; return new Rect(x, y, width, height); }3. 添加与更新摄像机逻辑
void AddSelectedCameras() { foreach (var obj in Selection.gameObjects) { var cam = obj.GetComponent<Camera>(); if (cam != null && !previewEntries.Exists(e => e.targetCamera == cam)) { AddNewEntry(cam); } } } void AddAllCamerasInScene() { var allCams = FindObjectsOfType<Camera>(); foreach (var cam in allCams) { if (!previewEntries.Exists(e => e.targetCamera == cam)) { AddNewEntry(cam); } } } void AddNewEntry(Camera cam) { var newEntry = new CameraPreviewEntry { targetCamera = cam, name = cam.gameObject.name, resolution = new Vector2Int(640, 360) }; previewEntries.Add(newEntry); } void UpdateEntryName(CameraPreviewEntry entry) { if (entry.targetCamera != null) entry.name = entry.targetCamera.gameObject.name; else entry.name = "(None)"; }4. 配置持久化与资源清理使用EditorPrefs或ScriptableObject保存列表。这里使用JsonUtility和EditorPrefs做一个简单示例。
void LoadSettings() { string json = EditorPrefs.GetString("CameraPreviewTool_Entries", ""); if (!string.IsNullOrEmpty(json)) { JsonUtility.FromJsonOverwrite(json, new Wrapper { list = previewEntries }); } // 加载后需要重新创建RenderTexture foreach (var entry in previewEntries) { entry.renderTexture = null; // 强制下一帧重新创建 } } void SaveSettings() { string json = JsonUtility.ToJson(new Wrapper { list = previewEntries }); EditorPrefs.SetString("CameraPreviewTool_Entries", json); } void CleanupRenderTextures() { foreach (var entry in previewEntries) { if (entry.renderTexture != null) { entry.renderTexture.Release(); entry.renderTexture = null; } } } [System.Serializable] private class Wrapper { public List<CameraPreviewEntry> list; }3.3 高级功能扩展思路
基础工具搭建好后,可以根据项目需求添加以下高级功能,使其真正成为工作流的核心:
- 画中画与布局模式:允许用户选择单视图、四视图等布局,同时预览多个摄像机。
- 参考图像对比:拖入一张参考图(如概念图、截图),将其半透明叠加在预览画面上,方便进行构图、色彩或灯光匹配。
- 安全框与辅助线:绘制电视安全框(Action Safe、Title Safe)、黄金分割线、三分线等,用于影视级构图。
- 摄像机参数同步与联动:选中预览窗口中的某个画面,可以一键将Scene视图的观察视角对齐到该摄像机,或者将多个摄像机的FOV、Clipping Planes等参数进行联动调整。
- 快照与录制:添加按钮,将当前预览画面保存为PNG,或录制一段预览动画(序列帧)。
- 性能模式:当预览窗口不在焦点时,自动降低渲染分辨率或帧率,节省编辑器性能。
4. 集成到日常开发工作流:实战技巧与优化
工具造好了,关键在于如何用好。以下是我在多个项目中总结出的实战工作流。
4.1 场景构建与布局阶段
在搭建白模或摆放基础道具时,我通常会打开Camera Preview Tool,并添加1-3个关键摄像机(如玩家主视角、过场动画起始/结束机位)。将工具窗口停靠在编辑器右侧或第二块屏幕上。
操作流:
- 在Scene视图中选择并移动一个摄像机。
- 右侧的预览窗口实时反馈画面变化。
- 如果需要调整构图,直接参考预览窗口,无需切换视图。
- 使用“对齐视图”功能(需扩展工具),可以快速将Scene视图的观察角度同步到选中的预览摄像机,进行微调。
心得:这个阶段重点是快速验证视角和空间关系。将预览分辨率调低(如320x180),以换取更流畅的编辑器响应速度。
4.2 灯光与后期处理调试阶段
这是预览工具价值最大化的阶段。灯光效果、雾效、体积光、后期处理栈(Post-processing Stack)的调整,极度依赖实时视觉反馈。
工作流:
- 固定摄像机视角。选择几个最具代表性的镜头(如全景、特写、逆光场景)。
- 在预览工具中锁定这些摄像机。
- 在Scene视图或Inspector中调整光源强度、颜色、阴影设置。
- 同时观察所有锁定摄像机的预览画面,确保在不同构图下灯光都表现良好。例如,调整主光时,要同时看全景镜头的光照范围和特写镜头的面部阴影是否自然。
- 调整后期效果(如Bloom, Color Grading)时,预览窗口能最直观地反映色调和氛围变化,避免进入Game视图后才发现效果过曝或色彩失衡。
重要提示:确保预览窗口的渲染效果与Game视图一致。检查两点:一是预览摄像机的
Render Path和HDR设置是否与主摄像机一致;二是在渲染RenderTexture时,可能需要手动调用Graphics.Blit或设置特定的Camera.Render参数来应用全屏后处理效果。对于URP/HDRP,可能需要使用UniversalRenderPipeline.RenderSingleCamera或类似API。
4.3 过场动画(Timeline/Cinemachine)制作阶段
使用Cinemachine或直接制作Timeline动画时,预览工具可以作为一个强大的“监视器”。
应用场景:
- 虚拟摄像机(Cinemachine Virtual Camera)预览:将Cinemachine Brain的输出摄像机或某个Virtual Camera添加到预览列表。在Timeline中拖动播放头,预览窗口会实时显示该时间点的镜头画面,方便精确调整关键帧。
- 多机位切换预览:在制作快速剪辑或对话场景时,经常需要在多个机位间切换。将所有这些机位对应的摄像机加入预览工具,以画中画形式同时查看,可以极其高效地决定切换时机和构图连贯性。
- 摄像机动画曲线微调:调整摄像机移动、旋转、FOV动画曲线时,一边看曲线编辑器,一边看预览窗口的画面效果,调整更加精准。
4.4 性能优化与注意事项
- 渲染开销:每个预览窗口都意味着一次额外的摄像机渲染。同时预览4个1080p的摄像机会带来显著性能负担。务必根据需求调整预览分辨率。在布局阶段可用低分辨率,在最终调试时再提高。
- 编辑器卡顿:如果开启
autoRefresh并设置高帧率(如60),在复杂场景中可能导致编辑器UI卡顿。建议设置合理的刷新间隔(如0.033s对应30fps),或提供“手动刷新”按钮。 - 内存管理:
RenderTexture是显存/内存消耗大户。工具在窗口关闭、条目移除或分辨率更改时,必须及时调用RenderTexture.Release()。我们的EnsureRenderTexture和CleanupRenderTextures方法正是为此设计。 - 脚本执行顺序:如果预览摄像机绑定了需要
Update中计算的脚本(如跟随脚本),确保编辑器模式下这些脚本能正常执行(ExecuteInEditMode)。 - 与游戏逻辑的隔离:预览时,我们调用了
Camera.Render(),这可能会触发某些依赖于摄像机渲染的脚本逻辑(如OnRenderImage)。确保这些逻辑在编辑器预览模式下不会产生副作用(如保存文件、发送网络请求)。可以通过Application.isPlaying或自定义编辑器标记进行判断。
5. 常见问题排查与实战心得
即使工具完善,在实际使用中还是会遇到各种问题。这里记录一些典型的“坑”和解决方法。
5.1 预览画面黑屏或显示异常
这是最常见的问题,排查思路如下:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 预览窗口全黑 | 1. 摄像机被禁用(enabled = false)2. 摄像机 Culling Mask未包含任何图层3. RenderTexture创建失败(格式不支持)4. 摄像机 Depth值过低,被其他摄像机覆盖 | 1. 检查摄像机GameObject和Camera组件是否激活。 2. 检查摄像机 Culling Mask是否至少包含一个场景中有物体的图层。3. 尝试使用更通用的 RenderTextureFormat.ARGB32。4. 提高预览摄像机的 Depth值,或确保它是唯一渲染到该RenderTexture的摄像机。 |
| 预览画面扭曲、拉伸 | 预览绘制区域(Rect)的宽高比与RenderTexture的宽高比不匹配 | 使用GetCenteredRect等函数确保绘制时保持原始宽高比(ScaleMode.ScaleToFit)。 |
| 画面颜色异常(过暗/过亮) | 1. 线性空间与伽马空间问题 2. HDR未正确处理 3. 后期处理效果未应用 | 1. 在Player Settings中确认颜色空间。对于预览,可能需要在渲染时进行sRGB转换。 2. 如果使用HDR,确保 RenderTexture格式支持(如ARGBHalf),并且Tonemapping等后处理已正确应用。3. 对于内置渲染管线,可能需要手动调用 Graphics.Blit应用后处理;对于URP/HDRP,使用其提供的渲染方法。 |
| 画面更新卡顿或不更新 | 1.autoRefresh未开启且未手动刷新2. 工具窗口不在焦点, Update方法未被调用3. 渲染开销太大 | 1. 点击“Refresh Now”按钮或开启自动刷新。 2. 编辑器窗口即使不在焦点, Update也会调用,但频率可能降低。可以改用EditorApplication.update委托。3. 降低预览分辨率或减少预览的摄像机数量。 |
5.2 编辑器性能显著下降
当场景复杂时,多个高清预览可能导致编辑器变慢。
优化策略:
- 分级预览:工具可以增加一个“质量”下拉菜单,提供“草稿”、“中”、“高”等选项,对应不同的
RenderTexture分辨率(如240p, 480p, 720p)。在移动物体、调整Transform时使用草稿质量,在调整材质和灯光时再切换到高质量。 - 按需渲染:实现一个“暂停渲染”复选框,或者更智能地,当工具窗口被其他窗口完全遮挡时,自动暂停渲染。
- 减少重绘:在
Update中判断,如果自上次渲染后,所有被预览的摄像机及其相关参数(位置、旋转、FOV等)都没有发生变化,则跳过本次重绘(Repaint)。
5.3 与URP/HDRP的兼容性问题
现代项目多使用URP或HDRP,它们的渲染管线与内置管线不同。
URP适配要点:
- 渲染入口:不能直接调用
Camera.Render()。需要使用UniversalRenderPipeline.RenderSingleCamera方法,并传入一个ScriptableRenderContext。 - 后处理:URP的后处理通常通过
Volume系统和摄像机上的Post Process Layer组件管理。确保预览摄像机也有相应的设置,并且UniversalRenderPipeline.RenderSingleCamera会处理这些Volume。 - 示例代码片段:
实操心得:对于URP/HDRP,一个更稳定但取巧的方法是:创建一个隐藏的、使用相同渲染管线的“渲染用Camera”,在需要预览时,将其Transform、参数同步到目标摄像机,然后渲染这个隐藏摄像机。这样可以避免干扰原摄像机的状态,兼容性更好。#if USING_URP using UnityEngine.Rendering.Universal; private void RenderCameraURP(Camera cam, RenderTexture targetRT) { var urpAsset = GraphicsSettings.renderPipelineAsset as UniversalRenderPipelineAsset; if (urpAsset == null) return; var cameraData = cam.GetUniversalAdditionalCameraData(); // 可能需要临时设置一些参数 var originalTarget = cam.targetTexture; cam.targetTexture = targetRT; // 创建一个临时上下文并进行渲染(此处为简化概念,实际需处理CommandBuffer等) // 具体实现需参考URP的编辑器渲染API,可能较为复杂 // 一种常见做法是:复制一个临时摄像机,使用`UniversalRenderPipeline.RenderSingleCamera`进行渲染 cam.Render(); cam.targetTexture = originalTarget; } #endif
5.4 工具本身的可用性打磨
- 撤销(Undo)支持:在工具中修改了摄像机的任何参数(如通过工具界面调整了FOV),应该支持Ctrl+Z撤销。可以使用
Undo.RecordObject来实现。 - 快捷键操作:为“添加选中摄像机”、“刷新预览”等常用操作分配快捷键(如
%#a对应Ctrl+Shift+A),能极大提升操作速度。 - 场景加载感知:当打开新场景或场景中的摄像机被销毁时,工具内的列表应该能自动清理无效引用,或给出明确提示。
- 皮肤与主题适配:使用
EditorGUIUtility.isProSkin来判断当前是个人版还是专业版皮肤,调整UI颜色,使工具与编辑器整体风格一致。
从最初的手动点击内置预览,到编写一个简单的预览脚本,再到打造一个功能全面、性能可控的编辑器工具,这个过程本身就是对Unity编辑器扩展能力的一次深度实践。最终,这个工具会成为你场景制作、镜头设计和效果调试中不可或缺的伙伴。它节省的不仅仅是切换视图的时间,更是将创作过程中的“猜测”变为“确认”,让决策更快,让效果更可控。