第9章 2D渲染与Sprite光照
理论讲解
9.1 URP 2D Renderer配置
URP为2D渲染提供了专门的渲染器,称为Universal 2D Renderer。这个渲染器优化了2D渲染流程,支持2D光照、精灵遮罩等功能。与3D渲染不同,2D渲染更注重性能和简洁性,同时保持视觉效果。
Universal 2D Renderer的主要特性:
- 2D光照系统:支持点光源、方向光和聚光灯
- 精灵遮罩:支持Sprite Mask功能
- Sorting Layers:支持多层排序
- 优化的渲染流程:减少批处理和绘制调用
9.2 2D Lights系统
URP中的2D光照系统是专门为2D游戏设计的光照解决方案。与3D光照不同,2D光照在二维平面上工作,提供更高效的性能。
2D光源类型
- Light 2D (Point Light):从一点向四周发射光线
- Light 2D (Freeform Light):自定义形状的面光源
- Light 2D (Sprite Light):基于精灵纹理的光照
- Light 2D (Parametric Light):参数化形状的光照
光源属性
- Light Type:光源类型(Point, Freeform, Sprite, Parametric)
- Light Color:光照颜色
- Alpha Blend On:是否启用Alpha混合
- Intensity:光照强度
- Volume Scale:体积缩放
- Blend Style:混合样式(Additive, Multiply, Multiply RGB)
9.3 Normal Maps在2D中的应用
法线贴图在2D中用于创建更丰富的光照效果,使2D精灵看起来更有立体感。通过法线贴图,可以在2D环境中模拟3D光照效果。
法线贴图制作
- 使用专门的2D法线贴图生成工具
- 确保法线贴图格式正确(通常为RGB格式)
- 与原精灵贴图保持相同尺寸
2D法线贴图应用
- 在Sprite Renderer上应用法线贴图
- 配置光照材质以支持法线贴图
- 调整光照参数以获得最佳效果
9.4 Sprite Mask与Sorting Layers
Sprite Mask
Sprite Mask用于遮罩精灵,只显示在遮罩区域内的部分。在URP中,Sprite Mask与2D光照系统配合使用,可以创建复杂的遮罩效果。
Mask属性:
- Front Sorting Layer:前景排序层
- Front Sorting Order:前景排序顺序
- Back Sorting Layer:背景排序层
- Back Sorting Order:背景排序顺序
Sorting Layers
Sorting Layers用于控制渲染顺序,确保精灵按正确的前后关系显示:
- Default:默认排序层
- UI:UI排序层
- Effects:特效排序层
- Custom layers:自定义排序层
9.5 2D阴影投射
URP 2D支持2D阴影投射,通过Light 2D组件实现。2D阴影系统包括:
- Shadow Casters:阴影投射器
- Shadow Receiving:阴影接收
- Shadow Distance:阴影距离
9.6 Pixel Perfect Camera
Pixel Perfect Camera确保精灵在屏幕上以精确的像素显示,避免模糊和失真。这对于像素艺术风格的游戏尤为重要。
Pixel Perfect Camera特性:
- Assets Pixels Per Unit:每单位的像素数
- Ref Resolution X/Y:参考分辨率
- Upscale Render Texture:放大渲染纹理
- Crop Frame X/Y:裁剪帧
代码示例
9.7 2D光照控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| using UnityEngine; using UnityEngine.Rendering.Universal;
[RequireComponent(typeof(Light2D))] public class Light2DController : MonoBehaviour { [Header("Light Configuration")] public Light2D light2D; [Header("Animation Settings")] public bool animateIntensity = false; public AnimationCurve intensityCurve = AnimationCurve.EaseInOut(0, 0.5f, 1, 1.5f); public float animationDuration = 2f; [Header("Color Settings")] public Gradient colorGradient = new Gradient(); public float colorChangeDuration = 5f; [Header("Flicker Settings")] public bool enableFlicker = false; public float flickerMin = 0.8f; public float flickerMax = 1.2f; public float flickerSpeed = 10f; private float animationTimer = 0f; private float colorTimer = 0f; private float flickerTimer = 0f; void Start() { if (light2D == null) light2D = GetComponent<Light2D>(); InitializeColorGradient(); } void Update() { if (light2D == null) return; if (animateIntensity) { animationTimer += Time.deltaTime; float progress = (animationTimer % animationDuration) / animationDuration; float intensity = intensityCurve.Evaluate(progress); light2D.intensity = intensity; } if (colorGradient.colorKeys.Length > 1) { colorTimer += Time.deltaTime; float colorProgress = (colorTimer % colorChangeDuration) / colorChangeDuration; light2D.color = colorGradient.Evaluate(colorProgress); } if (enableFlicker) { flickerTimer += Time.deltaTime * flickerSpeed; float flickerValue = Mathf.Lerp(flickerMin, flickerMax, Mathf.PerlinNoise(flickerTimer, 0)); light2D.intensity *= flickerValue; } } private void InitializeColorGradient() { if (colorGradient.colorKeys.Length == 0) { GradientColorKey[] colorKeys = new GradientColorKey[2]; colorKeys[0] = new GradientColorKey(Color.white, 0.0f); colorKeys[1] = new GradientColorKey(Color.red, 1.0f); colorGradient.SetKeys(colorKeys, new GradientAlphaKey[2] { new GradientAlphaKey(1.0f, 0.0f), new GradientAlphaKey(1.0f, 1.0f) }); } } public void SetLightRange(float range) { if (light2D != null) { light2D.pointLightOuterRadius = range; } } public void SetLightType(Light2D.LightType type) { if (light2D != null) { light2D.lightType = type; } } public void SetLightEnabled(bool enabled) { if (light2D != null) { light2D.enabled = enabled; } } public float GetCurrentIntensity() { return light2D != null ? light2D.intensity : 0f; } }
|
9.8 2D精灵光照材质管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
| using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering.Universal;
public class SpriteLightingManager : MonoBehaviour { [System.Serializable] public class SpriteLightingConfig { public string name; public Material material; public bool useNormalMap = false; public bool receiveShadows = true; public bool castShadows = true; } [Header("Lighting Configurations")] public List<SpriteLightingConfig> lightingConfigs = new List<SpriteLightingConfig>(); [Header("Default Settings")] public Material defaultLitMaterial; public Material defaultUnlitMaterial; private Dictionary<string, Material> materialMap = new Dictionary<string, Material>(); private Dictionary<SpriteRenderer, Material> originalMaterials = new Dictionary<SpriteRenderer, Material>(); void Start() { InitializeMaterials(); } private void InitializeMaterials() { foreach (var config in lightingConfigs) { if (config.material != null) { materialMap[config.name] = config.material; } } } public void ApplyLightingToSprite(SpriteRenderer spriteRenderer, string configName) { if (materialMap.ContainsKey(configName)) { if (!originalMaterials.ContainsKey(spriteRenderer)) { originalMaterials[spriteRenderer] = spriteRenderer.material; } spriteRenderer.material = materialMap[configName]; } } public void RestoreOriginalMaterial(SpriteRenderer spriteRenderer) { if (originalMaterials.ContainsKey(spriteRenderer)) { spriteRenderer.material = originalMaterials[spriteRenderer]; originalMaterials.Remove(spriteRenderer); } } public void ApplyLightingToGroup(GameObject groupRoot, string configName) { var spriteRenderers = groupRoot.GetComponentsInChildren<SpriteRenderer>(); foreach (var spriteRenderer in spriteRenderers) { ApplyLightingToSprite(spriteRenderer, configName); } } public Material CreateNormalMappedMaterial(Texture normalMap, string shaderName = "Universal Render Pipeline/Lit 2D") { Shader shader = Shader.Find(shaderName); if (shader == null) { Debug.LogError($"Shader {shaderName} not found!"); return null; } Material material = new Material(shader); if (normalMap != null) { material.SetTexture("_BumpMap", normalMap); material.EnableKeyword("_NORMALMAP"); } return material; } public void AdjustMaterialParameters(SpriteRenderer spriteRenderer, float smoothness = 0.5f, float metallic = 0f, Color emissionColor = default(Color)) { Material material = spriteRenderer.material; if (material.HasProperty("_Smoothness")) material.SetFloat("_Smoothness", smoothness); if (material.HasProperty("_Metallic")) material.SetFloat("_Metallic", metallic); if (emissionColor != default(Color)) { material.SetColor("_EmissionColor", emissionColor); material.EnableKeyword("_EMISSION"); } } public void SetShadowSettings(SpriteRenderer spriteRenderer, bool castShadows, bool receiveShadows) { var light2D = spriteRenderer.GetComponent<Light2D>(); if (light2D != null) { light2D.lightCastsShadows = castShadows; light2D.lightOrder = receiveShadows ? 0 : -1; } } }
|
9.9 2D排序层管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| using System.Collections.Generic; using UnityEngine;
public class SortingLayerManager : MonoBehaviour { [System.Serializable] public class SortingLayerDefinition { public string layerName; public int sortingOrder; public string description; } [Header("Sorting Layer Definitions")] public List<SortingLayerDefinition> layerDefinitions = new List<SortingLayerDefinition>(); [Header("Default Settings")] public string defaultLayer = "Default"; public int defaultOrder = 0; private Dictionary<string, int> layerOrderMap = new Dictionary<string, int>(); private Dictionary<SpriteRenderer, string> spriteOriginalLayers = new Dictionary<SpriteRenderer, string>(); void Start() { InitializeLayerMap(); } private void InitializeLayerMap() { foreach (var layerDef in layerDefinitions) { layerOrderMap[layerDef.layerName] = layerDef.sortingOrder; } if (!layerOrderMap.ContainsKey(defaultLayer)) { layerOrderMap[defaultLayer] = defaultOrder; } } public void SetSpriteSortingLayer(SpriteRenderer spriteRenderer, string layerName) { if (spriteRenderer != null && layerOrderMap.ContainsKey(layerName)) { if (!spriteOriginalLayers.ContainsKey(spriteRenderer)) { spriteOriginalLayers[spriteRenderer] = spriteRenderer.sortingLayerName; } spriteRenderer.sortingLayerName = layerName; spriteRenderer.sortingOrder = layerOrderMap[layerName]; } } public void SetSpriteSortingOrder(SpriteRenderer spriteRenderer, int order) { if (spriteRenderer != null) { spriteRenderer.sortingOrder = order; } } public void UpdateSortingOrderBasedOnPosition(SpriteRenderer spriteRenderer) { if (spriteRenderer != null) { int order = (int)(-spriteRenderer.transform.position.z * 100); spriteRenderer.sortingOrder = order; } } public void SetGroupSortingLayer(GameObject groupRoot, string layerName) { var spriteRenderers = groupRoot.GetComponentsInChildren<SpriteRenderer>(); foreach (var spriteRenderer in spriteRenderers) { SetSpriteSortingLayer(spriteRenderer, layerName); } } public void RestoreOriginalLayer(SpriteRenderer spriteRenderer) { if (spriteOriginalLayers.ContainsKey(spriteRenderer)) { spriteRenderer.sortingLayerName = spriteOriginalLayers[spriteRenderer]; spriteOriginalLayers.Remove(spriteRenderer); } } public int GetSortingLayerID(string layerName) { return SortingLayer.NameToID(layerName); } public string GetSortingLayerName(int layerID) { return SortingLayer.IDToName(layerID); } public void CreateSortingLayerIfNotExists(string layerName, int order) { if (!layerOrderMap.ContainsKey(layerName)) { layerOrderMap[layerName] = order; } } public void DynamicSortByDistance(SpriteRenderer[] sprites, Transform referencePoint) { System.Array.Sort(sprites, (a, b) => { float distA = Vector3.Distance(a.transform.position, referencePoint.position); float distB = Vector3.Distance(b.transform.position, referencePoint.position); return distA.CompareTo(distB); }); for (int i = 0; i < sprites.Length; i++) { sprites[i].sortingOrder = i; } } }
|
9.10 Pixel Perfect相机控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
| using UnityEngine; using UnityEngine.Rendering.Universal;
[RequireComponent(typeof(PixelPerfectCamera))] public class PixelPerfectCameraController : MonoBehaviour { [Header("Pixel Perfect Settings")] public PixelPerfectCamera pixelPerfectCamera; [Header("Resolution Settings")] public int assetsPixelsPerUnit = 16; public Vector2Int referenceResolution = new Vector2Int(1920, 1080); [Header("Advanced Settings")] public bool upscaleRenderTexture = false; public bool cropFrameX = true; public bool cropFrameY = true; [Header("Dynamic Adjustment")] public bool autoAdjustForScreenSize = true; public float minScale = 0.5f; public float maxScale = 4f; private Camera mainCamera; private Vector2Int currentResolution; void Start() { if (pixelPerfectCamera == null) pixelPerfectCamera = GetComponent<PixelPerfectCamera>(); if (pixelPerfectCamera == null) pixelPerfectCamera = gameObject.AddComponent<PixelPerfectCamera>(); mainCamera = pixelPerfectCamera.GetComponent<Camera>(); ConfigurePixelPerfectCamera(); if (autoAdjustForScreenSize) { AdjustForCurrentScreenSize(); } } private void ConfigurePixelPerfectCamera() { pixelPerfectCamera.assetsPPU = assetsPixelsPerUnit; pixelPerfectCamera.refResolutionX = referenceResolution.x; pixelPerfectCamera.refResolutionY = referenceResolution.y; pixelPerfectCamera.upscaleRT = upscaleRenderTexture; pixelPerfectCamera.cropFrameX = cropFrameX; pixelPerfectCamera.cropFrameY = cropFrameY; } private void AdjustForCurrentScreenSize() { currentResolution = new Vector2Int(Screen.width, Screen.height); float scaleX = (float)currentResolution.x / referenceResolution.x; float scaleY = (float)currentResolution.y / referenceResolution.y; float scale = Mathf.Min(scaleX, scaleY); scale = Mathf.Clamp(scale, minScale, maxScale); float orthoSize = mainCamera.orthographicSize; float adjustedOrthoSize = orthoSize / scale; mainCamera.orthographicSize = adjustedOrthoSize; } public void SetAssetsPixelsPerUnit(int ppu) { if (pixelPerfectCamera != null) { pixelPerfectCamera.assetsPPU = ppu; assetsPixelsPerUnit = ppu; } } public void SetReferenceResolution(int x, int y) { if (pixelPerfectCamera != null) { pixelPerfectCamera.refResolutionX = x; pixelPerfectCamera.refResolutionY = y; referenceResolution = new Vector2Int(x, y); } } public bool IsPixelPerfect() { if (pixelPerfectCamera != null) { return pixelPerfectCamera.isBlendingPresent; } return false; } public float GetPixelScale() { if (pixelPerfectCamera != null) { return pixelPerfectCamera.pixelScale; } return 1f; } public void RecalculatePixelPerfect() { if (pixelPerfectCamera != null) { pixelPerfectCamera.Reset(); ConfigurePixelPerfectCamera(); } } public void AdaptToDevice(string deviceName) { switch (deviceName.ToLower()) { case "mobile": SetAssetsPixelsPerUnit(16); SetReferenceResolution(1080, 1920); break; case "desktop": SetAssetsPixelsPerUnit(32); SetReferenceResolution(1920, 1080); break; case "console": SetAssetsPixelsPerUnit(16); SetReferenceResolution(1920, 1080); break; default: break; } } private void OnRectTransformDimensionsChange() { if (autoAdjustForScreenSize) { AdjustForCurrentScreenSize(); } } private void OnValidate() { if (pixelPerfectCamera != null) { ConfigurePixelPerfectCamera(); } } }
|
实践练习
9.11 练习1:创建2D光照场景
目标:创建一个包含多种2D光源的场景
步骤:
- 创建一个2D场景,包含多个精灵对象
- 添加Point Light 2D,设置为暖色调
- 添加Freeform Light 2D,创建窗户光效
- 添加Parametric Light 2D,创建聚光灯效果
- 调整各光源的强度和颜色,观察混合效果
具体参数设置:
- Point Light:Color=#FFD700, Intensity=1.2, Outer Radius=5
- Freeform Light:Color=#87CEEB, Intensity=0.8, Shape=Rectangle
- Parametric Light:Color=#FFFFFF, Intensity=1.5, Inner Angle=30, Outer Angle=60
9.12 练习2:实现2D阴影系统
目标:创建2D阴影投射和接收效果
步骤:
- 创建多个Sprite对象作为遮挡物
- 添加Light 2D并启用阴影
- 配置Sprite对象的阴影投射属性
- 观察阴影的形成和变化
- 调整阴影参数优化效果
技术要点:
- 使用Sprite Light Renderer组件
- 配置Shadow Caster 2D组件
- 调整Shadow Distance参数
9.13 练习3:法线贴图应用
目标:为2D精灵应用法线贴图,增强立体感
步骤:
- 准备带法线贴图的精灵资源
- 创建支持法线贴图的材质
- 应用材质到Sprite Renderer
- 添加2D光源观察法线贴图效果
- 调整光照角度观察立体感变化
材质设置:
- Shader: Universal Render Pipeline/Lit 2D
- Enable Normal Map keyword
- Assign normal map texture
9.14 练习4:精灵遮罩效果
目标:使用Sprite Mask创建遮罩效果
步骤:
- 创建Sprite Mask对象
- 设置遮罩形状和大小
- 创建多个Sprite对象
- 调整Sprite的Sorting Layer
- 观察遮罩效果
参数配置:
- Mask:Sorting Layer=UI, Order=10
- Sprites:Sorting Layer=Default, Order=0
9.15 练习5:像素完美相机设置
目标:配置Pixel Perfect Camera实现像素艺术效果
步骤:
- 添加Pixel Perfect Camera组件
- 设置Assets Pixels Per Unit=16
- 设置参考分辨率为1080p
- 测试不同分辨率下的显示效果
- 观察像素对齐效果
测试分辨率:
- 1920x1080
- 1280x720
- 960x540
9.16 练习6:动态光照系统
目标:实现可动态控制的2D光照系统
步骤:
- 编写光照控制器脚本
- 实现光照强度动画
- 添加颜色渐变效果
- 创建UI控制面板
- 测试动态调整功能
代码实现要点:
- 使用协程实现平滑动画
- 实现光照参数的实时调整
- 添加光照效果预设
总结
第9章详细介绍了URP中2D渲染与Sprite光照的相关技术。URP为2D游戏开发提供了强大的光照系统,包括多种光源类型、法线贴图支持、精灵遮罩等功能。
关键要点总结:
- 2D Renderer:专门优化的2D渲染器,支持2D光照和遮罩
- 2D Lights:多种光源类型,支持动态调整和动画
- 法线贴图:增强2D精灵的立体感和细节
- 排序层管理:精确控制2D对象的渲染顺序
- 像素完美:确保像素艺术风格的精确显示
- 性能优化:2D渲染的性能考虑和优化策略
2D渲染在URP中的实现平衡了视觉效果和性能需求,为2D游戏开发者提供了强大的工具集。通过合理配置2D光照系统,可以创建出具有深度和氛围的2D场景。
下一章将探讨URP中的粒子系统与VFX Graph集成,了解如何在URP管线中实现高质量的粒子效果。