四、Unity 高级主题 四、Unity 高级主题 4.1 脚本化渲染管线 (Scriptable Render Pipeline, SRP) 传统的 Unity 渲染管线虽然易于上手,但在定制化和性能优化方面存在局限性。脚本化渲染管线 (SRP) 的出现,彻底改变了这一局面。SRP 允许开发者使用 C# 脚本完全掌控渲染流程,实现高度定制化的渲染效果,并针对特定平台进行深度优化。 4.1.1 SRP 的核心概念 SRP 的核心在于将原本固定的渲染流程解耦,分解为一系列可编程的阶段,开发者可以通过编写脚本来定义这些阶段的具体行为。SRP 主要包含以下几个核心概念: 渲染管线资源 (Render Pipeline Asset): 定义了整个渲染管线的配置,包括使用的渲染器、渲染通道等。
四、Unity 高级主题
4.1 脚本化渲染管线 (Scriptable Render Pipeline, SRP)
传统的 Unity 渲染管线虽然易于上手,但在定制化和性能优化方面存在局限性。脚本化渲染管线 (SRP) 的出现,彻底改变了这一局面。SRP 允许开发者使用 C# 脚本完全掌控渲染流程,实现高度定制化的渲染效果,并针对特定平台进行深度优化。
4.1.1 SRP 的核心概念
SRP 的核心在于将原本固定的渲染流程解耦,分解为一系列可编程的阶段,开发者可以通过编写脚本来定义这些阶段的具体行为。SRP 主要包含以下几个核心概念:
渲染管线资源 (Render Pipeline Asset): 定义了整个渲染管线的配置,包括使用的渲染器、渲染通道等。它是 SRP 的入口点,需要在 Graphics Settings 中指定。
渲染器 (Renderer): 负责实际的渲染工作,例如前向渲染器 (Forward Renderer) 和延迟渲染器 (Deferred Renderer)。在 URP 和 HDRP 中,渲染器可以配置多个渲染通道和渲染特性。
渲染通道 (Render Pass): 表示渲染流程中的一个独立阶段,例如阴影渲染通道、不透明物体渲染通道、后处理通道等。开发者可以自定义渲染通道来实现特定的渲染效果。
渲染特性 (Render Feature): 一种可重用的渲染功能模块,可以添加到渲染器中,例如 SSAO、Bloom、Custom Post-Processing 等。渲染特性可以封装复杂的渲染逻辑,方便在不同的渲染管线中复用。
可以用 Mermaid 图表来表示 SRP 的结构:
4.1.2 通用渲染管线 (Universal Render Pipeline, URP) 实践
URP 是 Unity 官方提供的轻量级 SRP,适用于移动平台、VR/AR 以及低端 PC 平台。它在性能和效果之间取得了良好的平衡。
代码实践 4.1.2.1: 自定义渲染特性 (Render Feature) 实现屏幕后处理效果
以下代码示例演示如何创建一个简单的自定义渲染特性,实现一个屏幕颜色反转的后处理效果。
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class ColorInversionRenderFeature : ScriptableRendererFeature { [System.Serializable] public class ColorInversionSettings { public Material Material = null; [Range(0f, 1f)] public float Intensity = 1f; } public ColorInversionSettings settings = new ColorInversionSettings(); private ColorInversionRenderPass _renderPass; public override void Create() { _renderPass = new ColorInversionRenderPass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (settings.Material == null) { Debug.LogWarning("Color Inversion Material is not assigned."); return; } _renderPass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; // 在后处理之后执行 renderer.EnqueuePass(_renderPass); } class ColorInversionRenderPass : ScriptableRenderPass { private ColorInversionSettings _settings; private RTHandle _cameraColorTarget; private RTHandle _tempRT; private int _intensityPropertyID; public ColorInversionRenderPass(ColorInversionSettings settings) { _settings = settings; _intensityPropertyID = Shader.PropertyToID("_Intensity"); } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { _cameraColorTarget = renderingData.cameraData.renderer.cameraColorTargetHandle; RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor; descriptor.depthBufferBits = 0; // 不需要深度缓冲 RTHandles.Alloc(descriptor, "_TempColorInversionRT", out _tempRT); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (_settings.Material == null) return; CommandBuffer cmd = CommandBufferPool.Get("Color Inversion Post-Processing"); using (new ProfilingScope(cmd, new ProfilingSampler("ColorInversion"))) { cmd.SetGlobalFloat(_intensityPropertyID, _settings.Intensity); Blit(cmd, _cameraColorTarget, _tempRT, _settings.Material, 0); // 使用 Material 的 pass 0 进行 Blit Blit(cmd, _tempRT, _cameraColorTarget); // 将处理后的 RT 渲染回 Camera Target } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void OnCameraCleanup(CommandBuffer cmd) { _tempRT.Release(); } } }
代码详解 4.1.2.1:
ColorInversionRenderFeature 类: 继承自 ScriptableRendererFeature,是渲染特性的入口点。
Create() 方法创建 ColorInversionRenderPass 实例。
AddRenderPasses() 方法将渲染通道添加到渲染器的渲染队列中。RenderPassEvent.AfterRenderingPostProcessing 指定在后处理之后执行。
ColorInversionSettings 类: 定义渲染特性的可配置参数,这里包含一个 Material 和一个强度值 Intensity。
ColorInversionRenderPass 类: 继承自 ScriptableRenderPass,实现具体的渲染逻辑。
OnCameraSetup() 方法在相机渲染前调用,用于获取相机颜色目标 RTHandle,并创建临时 RTHandle _tempRT。
Execute() 方法是渲染通道的核心,在每一帧渲染时执行。
使用 CommandBuffer 记录渲染命令。
Blit() 函数将相机颜色目标 RT 拷贝到临时 RT,并应用指定的 Material (Color Inversion Shader)。
再次 Blit() 将临时 RT 渲染回相机颜色目标,完成后处理效果。
SetGlobalFloat() 将强度值传递给 Shader。
OnCameraCleanup() 方法在相机渲染结束后调用,释放临时 RT。
Shader 代码示例 (ColorInversion.shader):
Shader "Hidden/ColorInversion" { Properties { _MainTex ("Texture", 2D) = "white" {} _Intensity ("Intensity", Range(0, 1)) = 1 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_TexelSize; float _Intensity; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); col.rgb = lerp(col.rgb, 1 - col.rgb, _Intensity); // 颜色反转 return col; } ENDCG } } }
使用步骤:
创建一个 Material,Shader 选择 "Hidden/ColorInversion"。
创建 C# 脚本 ColorInversionRenderFeature.cs 并粘贴上述代码。
在 URP Renderer 的 Renderer Features 中添加 "Color Inversion Render Feature"。
将创建的 Material 拖拽到 Color Inversion Render Feature 的 Material 槽位。
调整 Intensity 参数观察颜色反转效果。
4.2 Job System 和 Burst Compiler - 高性能计算
Unity 的 Job System 和 Burst Compiler 是为了解决 C# 脚本在性能上的瓶颈而设计的。它们允许开发者将计算密集型的任务放到独立的线程中执行,并利用 Burst Compiler 将 C# 代码编译为高度优化的本地机器码,显著提升性能。
4.2.1 Job System 的核心概念
Job System 是一种多线程编程模型,它将任务分解为独立的 "Job",并由 Unity 的工作线程池进行调度和执行。Job System 的核心概念包括:
Job: 表示一个独立的工作单元,通常是一个结构体,需要实现 IJob 或其变体接口。
JobHandle: 用于管理 Job 的依赖关系和完成状态。
Schedule() 和 Run(): Schedule() 将 Job 放入工作线程池异步执行,Run() 在主线程同步执行 Job。
NativeContainer: 用于在 Job 之间安全地共享数据,例如 NativeArray, NativeList, NativeHashMap 等。NativeContainer 提供了严格的内存安全检查,防止多线程并发访问数据导致的数据竞争问题。
可以用 Mermaid 图表表示 Job System 的工作流程:
4.2.2 Burst Compiler 的作用
Burst Compiler 是一个高性能的 LLVM-based 后端编译器,它可以将使用了特定子集的 C# 代码 (Job System 代码以及部分数学运算代码) 编译成高度优化的本地机器码,例如 SSE2, SSE4, AVX 等指令集。Burst Compiler 可以显著提升 CPU 密集型任务的执行效率,通常可以达到数倍甚至数十倍的性能提升。
代码实践 4.2.2.1: 使用 Job System 和 Burst Compiler 加速数组求和
以下代码示例演示如何使用 Job System 和 Burst Compiler 加速一个大型数组的求和运算。
using UnityEngine; using Unity.Jobs; using Unity.Collections; using Unity.Burst; public class JobSystemExample : MonoBehaviour { public int arraySize = 1000000; void Start() { // 初始化 NativeArray NativeArray<float> data = new NativeArray<float>(arraySize, Allocator.Persistent); for (int i = 0; i < arraySize; i++) { data[i] = Random.value; } // 传统 C# 循环求和 float sumCSharp = 0f; float startTimeCSharp = Time.realtimeSinceStartup; for (int i = 0; i < arraySize; i++) { sumCSharp += data[i]; } float endTimeCSharp = Time.realtimeSinceStartup; Debug.Log($"C# Loop Sum: {sumCSharp}, Time: {(endTimeCSharp - startTimeCSharp) * 1000f} ms"); // 使用 Job System 和 Burst Compiler 求和 SumJob sumJob = new SumJob { Data = data, Result = new NativeArray<float>(1, Allocator.Persistent) // 用于存储结果的 NativeArray }; float startTimeJob = Time.realtimeSinceStartup; JobHandle jobHandle = sumJob.Schedule(); jobHandle.Complete(); // 等待 Job 完成 float sumJobSystem = sumJob.Result[0]; float endTimeJob = Time.realtimeSinceStartup; Debug.Log($"Job System Sum: {sumJobSystem}, Time: {(endTimeJob - startTimeJob) * 1000f} ms"); // 释放 NativeArray 内存 data.Dispose(); sumJob.Result.Dispose(); } // 定义 Burst Compiler 加速的 Job [BurstCompile] struct SumJob : IJob { [ReadOnly] public NativeArray<float> Data; // 只读输入数据 public NativeArray<float> Result; // 输出结果 public void Execute() { float sum = 0f; for (int i = 0; i < Data.Length; i++) { sum += Data[i]; } Result[0] = sum; } } }
代码详解 4.2.2.1:
SumJob 结构体: 实现了 IJob 接口,定义了求和 Job。
[BurstCompile] 属性标记该结构体将被 Burst Compiler 编译。
[ReadOnly] public NativeArray<float> Data; 声明输入数据 Data 为只读 NativeArray。ReadOnly 属性用于告知 Job System 和 Burst Compiler 该数据不会被 Job 修改,可以进行更激进的优化。
public NativeArray<float> Result; 声明输出结果 Result 为 NativeArray。
Execute() 方法实现具体的求和逻辑。
JobSystemExample 类: MonoBehaviour 脚本,用于演示 Job System 的使用。
在 Start() 方法中,创建 NativeArray<float> 存储随机数据。
分别使用传统的 C# 循环和 Job System 进行求和,并记录运行时间。
创建 SumJob 实例,初始化输入数据 Data 和结果存储 Result。
sumJob.Schedule() 将 Job 放入工作线程池异步执行。
jobHandle.Complete() 阻塞主线程,等待 Job 完成。
从 sumJob.Result[0] 中获取计算结果。
最后,调用 Dispose() 释放 NativeArray 的内存。
性能对比:
运行上述代码,可以看到使用 Job System 和 Burst Compiler 的版本,其运行时间远低于传统的 C# 循环版本,尤其是在大型数组的情况下,性能提升非常显著。Burst Compiler 将 SumJob.Execute() 方法编译为高度优化的机器码,并利用多线程并行执行,从而大幅度提升了计算效率。
4.3 高级动画 - 动画绑定 (Animation Rigging)
Unity 的动画系统功能强大,但对于复杂角色的动画控制,例如程序化动画、实时 IK 等,传统的 Animator Controller 可能显得力不从心。Animation Rigging 是一套 Unity 官方提供的包,它为开发者提供了在运行时动态修改动画骨骼姿态的能力,从而实现更加灵活和高级的动画效果。
4.3.1 Animation Rigging 的核心概念
Animation Rigging 基于 Rig Constraints (绑定约束) 的概念,允许开发者在 Unity 编辑器中,通过可视化的方式,创建各种约束关系,将 Rig 骨骼的运动与目标物体或骨骼关联起来。Animation Rigging 的核心概念包括:
Rig Setup: 用于创建和配置 Rig 绑定骨骼层级结构的组件。
Rig Layer: 用于组织和管理 Rig Constraints 的层级结构。
Rig Constraint: 各种类型的绑定约束,例如:
Two Bone IK Constraint: 两骨骼反向动力学约束。
Multi-Parent Constraint: 多父物体约束。
Override Transform Constraint: 覆盖变换约束。
Look At Constraint: 注视约束。
Rotation Constraint: 旋转约束。
Scale Constraint: 缩放约束。
Translation Constraint: 平移约束。
Weight 和 Hint: 约束的权重值用于控制约束的影响程度,Hint 用于辅助 IK 约束的骨骼方向。
可以用 Mermaid 图表表示 Animation Rigging 的结构:
4.3.2 Two Bone IK Constraint 实践 - 实现简单的手臂 IK
以下代码示例演示如何使用 Animation Rigging 的 Two Bone IK Constraint 实现一个简单的手臂 IK 控制。
场景设置:
创建一个包含骨骼的角色模型,手臂部分至少包含三节骨骼:肩膀 (Shoulder)、上臂 (UpperArm)、下臂 (LowerArm)、手 (Hand)。
在角色模型上添加 Animator 组件,并配置好动画 Controller。
安装 Animation Rigging 包 (通过 Package Manager)。
在角色模型的根节点或手臂父节点上添加 "Rig Setup" 组件。
在 Rig Setup 组件下创建 "Rig Layer"。
在 Rig Layer 下添加 "Two Bone IK Constraint" 组件。
配置 Two Bone IK Constraint:
Root: 拖拽上臂骨骼 (UpperArm) 到 Root 槽位。
Mid: 拖拽下臂骨骼 (LowerArm) 到 Mid 槽位。
Tip: 拖拽手骨骼 (Hand) 到 Tip 槽位。
Target: 创建一个空的 GameObject 作为 IK 目标点,拖拽到 Target 槽位。
Hint: 创建一个空的 GameObject 作为 IK Hint,拖拽到 Hint 槽位 (可选,用于控制肘关节方向)。
C# 脚本控制 IK Target:
using UnityEngine; using UnityEngine.Animations.Rigging; public class IKController : MonoBehaviour { public Rigidbody targetRigidbody; // IK 目标点的 Rigidbody,用于跟随鼠标位置 public float smoothTime = 0.1f; // 平滑移动时间 private Vector3 _velocity = Vector3.zero; void Update() { // 将 IK 目标点移动到鼠标点击位置 (在世界空间中) Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 100f)) { Vector3 targetPosition = hit.point; targetRigidbody.MovePosition(Vector3.SmoothDamp(targetRigidbody.position, targetPosition, ref _velocity, smoothTime)); } } }
代码详解 4.3.2.1:
IKController 类: MonoBehaviour 脚本,用于控制 IK Target 的位置。
targetRigidbody 变量用于引用 IK Target 的 Rigidbody 组件,使用 Rigidbody 的 MovePosition 方法进行移动可以更好地与物理系统交互。
smoothTime 变量用于控制平滑移动的时间,使 IK 目标点的移动更加平滑自然。
Update() 方法在每一帧更新时执行。
Camera.main.ScreenPointToRay() 将鼠标屏幕坐标转换为世界空间射线。
Physics.Raycast() 进行射线检测,获取鼠标点击的世界空间位置。
Vector3.SmoothDamp() 平滑地将 IK Target 的 Rigidbody 移动到鼠标点击位置。
运行效果:
运行场景,将 IKController 脚本挂载到任意 GameObject 上,并将 IK Target 的 GameObject 的 Rigidbody 组件拖拽到 targetRigidbody 槽位。点击屏幕,可以看到角色的手臂会跟随鼠标点击位置移动,实现简单的手臂 IK 控制。
4.4 编辑器扩展 - 自定义编辑器工具
Unity 编辑器具有强大的扩展性,开发者可以使用 C# 脚本创建自定义的编辑器工具,例如自定义 Inspector 面板、自定义 Editor Window、自定义 Scene View 工具等,从而提升开发效率和工作流程。
4.4.1 编辑器脚本的核心概念
编辑器脚本与运行时脚本不同,它们运行在 Unity 编辑器环境中,可以访问 Editor API,对编辑器进行各种操作。编辑器脚本的核心概念包括:
Editor 类: 继承自 Editor 类,用于创建自定义 Inspector 面板。
EditorWindow 类: 继承自 EditorWindow 类,用于创建独立的编辑器窗口。
MenuItem 属性: 用于在 Unity 编辑器菜单栏中添加自定义菜单项。
Handles 类: 用于在 Scene View 中绘制 Gizmos 和交互式控件。
Serialization: 编辑器脚本中的数据需要使用 SerializedProperty 和 SerializedObject 进行序列化,以便在编辑器重启后数据不会丢失。
可以用 Mermaid 图表表示编辑器脚本的类型:
4.4.2 自定义 Editor Window 实践 - 创建资源批量重命名工具
以下代码示例演示如何创建一个自定义 Editor Window,实现资源批量重命名的功能。
using UnityEngine; using UnityEditor; public class BatchRenameWindow : EditorWindow { private string _prefix = "NewName_"; private int _startIndex = 1; [MenuItem("Tools/Batch Rename")] // 添加菜单项 public static void ShowWindow() { GetWindow<BatchRenameWindow>("Batch Rename"); // 创建窗口 } void OnGUI() { GUILayout.Label("批量重命名设置", EditorStyles.boldLabel); _prefix = EditorGUILayout.TextField("前缀", _prefix); _startIndex = EditorGUILayout.IntField("起始序号", _startIndex); if (GUILayout.Button("批量重命名选中资源")) { BatchRenameSelectedAssets(); } } void BatchRenameSelectedAssets() { Object[] selectedAssets = Selection.objects; int count = _startIndex; foreach (Object asset in selectedAssets) { if (asset is UnityEngine.Object) // 确保是 Unity 资源 { string newName = _prefix + count.ToString("D3"); // 生成新名称,序号三位补零 AssetDatabase.RenameAsset(AssetDatabase.GetAssetPath(asset), newName); count++; } } AssetDatabase.SaveAssets(); // 保存资源修改 AssetDatabase.Refresh(); // 刷新资源窗口 Debug.Log("批量重命名完成!"); } }
代码详解 4.4.2.1:
BatchRenameWindow 类: 继承自 EditorWindow,创建自定义编辑器窗口。
_prefix 和 _startIndex 变量用于存储重命名前缀和起始序号。
[MenuItem("Tools/Batch Rename")] 属性在 "Tools" 菜单下添加 "Batch Rename" 菜单项。
ShowWindow() 静态方法用于创建和显示窗口。
OnGUI() 方法绘制窗口 UI 界面。
GUILayout.Label() 显示标签。
EditorGUILayout.TextField() 创建文本输入框,用于输入前缀。
EditorGUILayout.IntField() 创建整数输入框,用于输入起始序号。
GUILayout.Button() 创建按钮,点击时调用 BatchRenameSelectedAssets() 方法。
BatchRenameSelectedAssets() 方法实现批量重命名逻辑。
Selection.objects 获取当前选中的资源数组。
遍历选中的资源,使用 AssetDatabase.RenameAsset() 重命名资源。
AssetDatabase.GetAssetPath() 获取资源的 Asset Path。
newName 变量生成新的资源名称,序号使用 ToString("D3") 格式化为三位数字。
AssetDatabase.SaveAssets() 保存资源修改到磁盘。
AssetDatabase.Refresh() 刷新 Project 窗口,使修改生效。
使用步骤:
创建 C# 脚本 BatchRenameWindow.cs 并粘贴上述代码到 Editor 文件夹下 (例如 Editor/BatchRenameWindow.cs)。
在 Unity 编辑器菜单栏中选择 "Tools/Batch Rename",打开 "Batch Rename" 窗口。
在 Project 窗口中选择需要批量重命名的资源。
在 "Batch Rename" 窗口中设置前缀和起始序号。
点击 "批量重命名选中资源" 按钮,即可完成批量重命名操作。
总结
希望本章节的内容能够帮助您在 Unity3D 的高级开发道路上更进一步。 这只是 Unity 高级主题的冰山一角,还有更多深入的领域等待您去探索和学习。