第十一章 掌握Unity渲染技术:从立方体纹理到程序化材质的实战指南
11.1 深入理解立方体贴图技术及应用
立方体贴图是3D图形渲染中的重要技术,它使用六个正方形纹理构成一个立方体,用于表现环境反射、天空背景等效果。这种技术在现代游戏中广泛应用,为场景提供了更加真实的视觉体验。
11.1.1 天空盒实现与优化方案
天空盒是立方体贴图最常见的应用之一,它为游戏场景提供了远处的背景环境。无论玩家如何移动,天空盒都保持在相对位置不变,创造出广阔空间的错觉。
理论基础:
天空盒本质上是一个围绕摄像机的立方体,内部贴有环境纹理。它具有以下特点:
- 位置始终跟随摄像机,给玩家一种无限远的感觉
- 渲染优先级低,确保场景中所有其他对象绘制在天空盒前面
- 不受场景光照影响,通常拥有自发光属性
下面是一个简单而高效的天空盒管理器实现:
csharp
using UnityEngine;
[ExecuteInEditMode]
public class SkyboxManager : MonoBehaviour
{
[Header("Skybox Settings")]
public Material skyboxMaterial;
[Range(0, 2)]
public float skyboxExposure = 1.0f;
[Range(0, 360)]
public float skyboxRotation = 0f;
public Color skyboxTint = Color.white;
[Header("Time Settings")]
public bool autoRotate = false;
public float rotationSpeed = 1.0f;
[Header("Reflection Settings")]
public bool updateReflectionProbes = true;
public ReflectionProbe[] reflectionProbes;
public float reflectionUpdateInterval = 1.0f;
// 内部变量
private Material originalSkybox;
private float lastUpdateTime = 0f;
void Start()
{
// 保存原始天空盒材质,以便后续恢复
originalSkybox = RenderSettings.skybox;
// 应用设置
ApplySkyboxSettings();
// 查找场景中的反射探针
if (reflectionProbes == null || reflectionProbes.Length == 0)
{
reflectionProbes = FindObjectsOfType<ReflectionProbe>();
}
}
void Update()
{
// 处理天空盒旋转
if (autoRotate)
{
skyboxRotation = (skyboxRotation + rotationSpeed * Time.deltaTime) % 360f;
RenderSettings.skybox.SetFloat("_Rotation", skyboxRotation);
}
// 更新反射探针
if (updateReflectionProbes && Time.time - lastUpdateTime > reflectionUpdateInterval)
{
UpdateReflectionProbes();
lastUpdateTime = Time.time;
}
}
[ContextMenu("Apply Skybox Settings")]
public void ApplySkyboxSettings()
{
if (skyboxMaterial != null)
{
// 应用天空盒材质
RenderSettings.skybox = skyboxMaterial;
// 设置天空盒参数
if (skyboxMaterial.HasProperty("_Exposure"))
{
skyboxMaterial.SetFloat("_Exposure", skyboxExposure);
}
if (skyboxMaterial.HasProperty("_Rotation"))
{
skyboxMaterial.SetFloat("_Rotation", skyboxRotation);
}
if (skyboxMaterial.HasProperty("_Tint"))
{
skyboxMaterial.SetColor("_Tint", skyboxTint);
}
// 强制更新光照
DynamicGI.UpdateEnvironment();
}
}
void UpdateReflectionProbes()
{
if (reflectionProbes == null || reflectionProbes.Length == 0)
return;
foreach (ReflectionProbe probe in reflectionProbes)
{
if (probe != null && probe.enabled)
{
probe.RenderProbe();
}
}
Debug.Log($"Updated {reflectionProbes.Length} reflection probes at time {Time.time}");
}
void OnDestroy()
{
// 恢复原始天空盒设置
if (originalSkybox != null && Application.isPlaying)
{
RenderSettings.skybox = originalSkybox;
DynamicGI.UpdateEnvironment();
}
}
// 编辑器下的控制面板增强
#if UNITY_EDITOR
[ContextMenu("Create HDR Cubemap")]
void CreateHDRCubemap()
{
// 编辑器工具函数,用于创建新的HDR天空盒资源
string path = UnityEditor.EditorUtility.SaveFilePanelInProject(
"Save HDR Cubemap",
"NewHDRSkybox",
"asset",
"Save HDR Cubemap asset"
);
if (string.IsNullOrEmpty(path))
return;
// 创建新的立方体贴图
Cubemap cubemap = new Cubemap(512, TextureFormat.RGBAHalf, true);
UnityEditor.AssetDatabase.CreateAsset(cubemap, path);
UnityEditor.AssetDatabase.SaveAssets();
Debug.Log("Created new HDR Cubemap at: " + path);
// 选择新资源
UnityEditor.Selection.activeObject = cubemap;
}
#endif
}
这个脚本实现了一个全面的天空盒管理系统,包括以下功能:
- 设置天空盒材质和相关参数
- 支持自动旋转,创造动态天空效果
- 自动更新反射探针,确保场景中的反射与天空盒同步
- 提供编辑器工具,方便创建新的HDR天空盒
要创建自定义的天空盒效果,我们可以使用以下着色器:
glsl
Shader "Custom/GradientSkybox"
{
Properties
{
_TopColor ("Top Color", Color) = (0.4, 0.6, 0.9, 1)
_BottomColor ("Bottom Color", Color) = (0.1, 0.1, 0.3, 1)
_ExposureAdjustment ("Exposure Adjustment", Range(0, 2)) = 1.0
_SunSize ("Sun Size", Range(0, 0.2)) = 0.04
_SunColor ("Sun Color", Color) = (1, 0.95, 0.9, 1)
_SunDirection ("Sun Direction", Vector) = (0, 0.5, 1, 0)
_StarTexture ("Star Texture", 2D) = "black" {}
_StarPower ("Star Power", Range(0, 10)) = 2.0
_StarIntensity ("Star Intensity", Range(0, 5)) = 1.0
}
SubShader
{
Tags { "Queue"="Background" "RenderType"="Background" "PreviewType"="Skybox" }
Cull Off ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldPos : TEXCOORD0;
float2 uv : TEXCOORD1;
};
fixed4 _TopColor;
fixed4 _BottomColor;
float _ExposureAdjustment;
float _SunSize;
fixed4 _SunColor;
float4 _SunDirection;
sampler2D _StarTexture;
float _StarPower;
float _StarIntensity;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = normalize(v.vertex.xyz);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 计算天空渐变
float gradientFactor = saturate(i.worldPos.y * 0.5 + 0.5);
fixed4 gradientColor = lerp(_BottomColor, _TopColor, gradientFactor);
// 添加太阳
float3 sunDir = normalize(_SunDirection.xyz);
float sunDot = saturate(dot(i.worldPos, sunDir));
float sunFactor = pow(sunDot, 1.0 / (_SunSize * _SunSize));
fixed3 sunColor = _SunColor.rgb * min(sunFactor, 1.0);
// 添加星星
fixed4 starTex = tex2D(_StarTexture, i.uv);
float starVisibility = 1.0 - gradientFactor; // 顶部星星更少,底部星星更多
fixed3 starColor = pow(starTex.rgb, _StarPower) * _StarIntensity * starVisibility;
// 合并所有颜色
fixed3 finalColor = gradientColor.rgb + sunColor + starColor;
// 应用曝光调整
finalColor *= _ExposureAdjustment;
return fixed4(finalColor, 1);
}
ENDCG
}
}
FallBack "Unlit/Color"
}
这个自定义天空盒着色器创建了一个从底部到顶部的渐变天空,并添加了可自定义的太阳和星星效果。它完全在GPU上生成,不需要预先绘制的立方体贴图,非常适合创建风格化的天空效果。
11.1.2 创建环境反射立方体贴图系统
环境反射是实现逼真材质效果的关键技术之一。立方体贴图通过存储从六个方向观察到的环境,可以为物体提供准确的反射信息。
实现环境反射系统:
以下是一个动态环境反射管理器,它可以实时更新场景中的环境反射:
csharp
using UnityEngine;
using System.Collections.Generic;
public class DynamicReflectionManager : MonoBehaviour
{
[System.Serializable]
public class ReflectionZone
{
public string zoneName = "New Zone";
public Transform zonePosition;
public float zoneRadius = 10f;
public ReflectionProbe reflectionProbe;
public float updateInterval = 1.0f;
[Range(16, 2048)]
public int resolution = 256;
public bool realtime = true;
public LayerMask cullingMask = -1;
[HideInInspector]
public float lastUpdateTime = 0f;
}
[Header("Global Settings")]
public bool enableDynamicReflections = true;
public float globalUpdateInterval = 0.5f;
public int defaultResolution = 256;
[Header("Reflection Zones")]
public List<ReflectionZone> reflectionZones = new List<ReflectionZone>();
[Header("Performance Settings")]
public int maxUpdatesPerFrame = 1;
public bool lowerResolutionInBackground = true;
[Range(0.1f, 1.0f)]
public float backgroundResolutionScale = 0.5f;
[Header("Debug")]
public bool showDebugVisuals = true;
public Color zoneColor = new Color(0, 1, 0, 0.2f);
public bool logReflectionUpdates = false;
// 内部变量
private Camera mainCamera;
private int updatesThisFrame = 0;
private int frameCount = 0;
void Start()
{
mainCamera = Camera.main;
// 初始化反射探针
InitializeReflectionProbes();
}
void Update()
{
if (!enableDynamicReflections)
return;
// 每帧重置计数器
updatesThisFrame = 0;
frameCount++;
// 处理反射区域更新
foreach (ReflectionZone zone in reflectionZones)
{
// 检查是否需要更新
if (zone.reflectionProbe != null && Time.time - zone.lastUpdateTime >= zone.updateInterval)
{
// 检查区域是否处于视图中
bool isInView = IsZoneInView(zone);
// 考虑性能设置
if (updatesThisFrame < maxUpdatesPerFrame)
{
if (zone.realtime || (frameCount % 10 == 0)) // 非实时探针较少更新
{
// 如果需要根据可见性调整分辨率
if (lowerResolutionInBackground && !isInView)
{
int originalResolution = zone.reflectionProbe.resolution;
zone.reflectionProbe.resolution = Mathf.Max(16, Mathf.RoundToInt(originalResolution * backgroundResolutionScale));
zone.reflectionProbe.RenderProbe();
zone.reflectionProbe.resolution = originalResolution;
}
else
{
// 正常更新
zone.reflectionProbe.RenderProbe();
}
zone.lastUpdateTime = Time.time;
updatesThisFrame++;
if (logReflectionUpdates)
{
Debug.Log($"Updated reflection probe for zone '{zone.zoneName}' at {Time.time}");
}
}
}
}
}
}
bool IsZoneInView(ReflectionZone zone)
{
if (mainCamera == null || zone.zonePosition == null)
return false;
// 检查是否在视锥体内
Vector3 zonePos = zone.zonePosition.position;
Vector3 dirToCamera = mainCamera.transform.position - zonePos;
// 简单检查:如果在相机后面,则不可见
if (Vector3.Dot(mainCamera.transform.forward, dirToCamera.normalized) > 0.1f)
return false;
// 距离检查
float distance = dirToCamera.magnitude;
if (distance > 100f) // 距离太远
return false;
// 视锥体检查
Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(mainCamera);
Bounds zoneBounds = new Bounds(zonePos, Vector3.one * zone.zoneRadius * 2);
return GeometryUtility.TestPlanesAABB(frustumPlanes, zoneBounds);
}
void InitializeReflectionProbes()
{
foreach (ReflectionZone zone in reflectionZones)
{
if (zone.reflectionProbe == null && zone.zonePosition != null)
{
// 为区域创建新的反射探针
GameObject probeObj = new GameObject(zone.zoneName + "_ReflectionProbe");
probeObj.transform.SetParent(zone.zonePosition);
probeObj.transform.localPosition = Vector3.zero;
ReflectionProbe probe = probeObj.AddComponent<ReflectionProbe>();
// 设置探针属性
probe.size = Vector3.one * zone.zoneRadius * 2;
probe.resolution = zone.resolution > 0 ? zone.resolution : defaultResolution;
probe.hdr = true;
probe.cullingMask = zone.cullingMask;
probe.mode = zone.realtime ? UnityEngine.Rendering.ReflectionProbeMode.Realtime : UnityEngine.Rendering.ReflectionProbeMode.Baked;
probe.refreshMode = UnityEngine.Rendering.ReflectionProbeRefreshMode.ViaScripting;
// 分配到区域
zone.reflectionProbe = probe;
// 立即渲染一次
probe.RenderProbe();
}
else if (zone.reflectionProbe != null)
{
// 更新现有探针的设置
zone.reflectionProbe.size = Vector3.one * zone.zoneRadius * 2;
zone.reflectionProbe.resolution = zone.resolution > 0 ? zone.resolution : defaultResolution;
zone.reflectionProbe.cullingMask = zone.cullingMask;
zone.reflectionProbe.mode = zone.realtime ? UnityEngine.Rendering.ReflectionProbeMode.Realtime : UnityEngine.Rendering.ReflectionProbeMode.Baked;
}
}
}
[ContextMenu("Add Reflection Zone At Position")]
void AddReflectionZoneAtPosition()
{
ReflectionZone newZone = new ReflectionZone();
newZone.zoneName = "Zone_" + reflectionZones.Count;
newZone.zonePosition = transform;
newZone.resolution = defaultResolution;
reflectionZones.Add(newZone);
// 初始化新区域的探针
InitializeReflectionProbes();
}
void OnDrawGizmos()
{
if (!showDebugVisuals)
return;
foreach (ReflectionZone zone in reflectionZones)
{
if (zone.zonePosition != null)
{
// 绘制区域范围
Gizmos.color = zoneColor;
Gizmos.DrawSphere(zone.zonePosition.position, zone.zoneRadius);
// 绘制线框
Gizmos.color = new Color(zoneColor.r, zoneColor.g, zoneColor.b, 1.0f);
Gizmos.DrawWireSphere(zone.zonePosition.position, zone.zoneRadius);
// 绘制标签
#if UNITY_EDITOR
UnityEditor.Handles.color = Color.white;
UnityEditor.Handles.Label(zone.zonePosition.position + Vector3.up * zone.zoneRadius,
zone.zoneName + "\nRes: " + zone.resolution + (zone.realtime ? " (Realtime)" : " (Baked)"));
#endif
}
}
}
}
这个管理器实现了以下功能:
- 在场景中创建多个反射区域,每个区域有独立的反射探针
- 基于距离、视锥体可见性和性能设置智能更新反射
- 提供可视化工具,方便在编辑器中管理反射区域
- 实现了性能优化,如降低背景反射分辨率和限制每帧更新数量
对于环境反射材质,我们可以使用以下着色器来实现可控制的反射效果:
glsl
Shader "Custom/EnvironmentReflection"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
[NoScaleOffset] _BumpMap ("Normal Map", 2D) = "bump" {}
_BumpScale ("Normal Scale", Range(0, 2)) = 1.0
[Header(Reflection)]
_ReflectionStrength ("Reflection Strength", Range(0, 1)) = 0.5
_ReflectionFresnel ("Reflection Fresnel", Range(0, 10)) = 1.0
[NoScaleOffset] _ReflectionMask ("Reflection Mask", 2D) = "white" {}
[Toggle] _UseProbeBlending ("Use Probe Blending", Float) = 1
_ReflectionRoughness ("Reflection Roughness", Range(0, 1)) = 0.0
[Toggle] _UnityReflectionControl ("Use Unity Reflection Control", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// 使用物理光照模型和环境反射
#pragma surface surf Standard fullforwardshadows
#pragma shader_feature _USEPROBEBLENDING_ON
#pragma shader_feature _UNITYREFLECTIONCONTROL_ON
#pragma target 3.0
sampler2D _MainTex;
sampler2D _BumpMap;
sampler2D _ReflectionMask;
struct Input
{
float2 uv_MainTex;
float3 worldPos;
float3 viewDir;
INTERNAL_DATA
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
float _BumpScale;
float _ReflectionStrength;
float _ReflectionFresnel;
float _ReflectionRoughness;
// 重写反射来添加自定义控制
void surf (Input IN, inout SurfaceOutputStandard o)
{
// 基本材质属性
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// 法线贴图
fixed3 normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
normal.xy *= _BumpScale;
o.Normal = normalize(normal);
// 反射掩码
fixed4 reflMask = tex2D(_ReflectionMask, IN.uv_MainTex);
// 计算菲涅尔因子 (边缘更反光)
float3 worldViewDir = normalize(IN.viewDir);
float fresnel = pow(1.0 - saturate(dot(worldViewDir, WorldNormalVector(IN, o.Normal))), _ReflectionFresnel);
#ifdef _UNITYREFLECTIONCONTROL_ON
// 使用标准的Unity反射控制
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
// 仅调整反射掩码和菲涅尔
o.Smoothness *= reflMask.r * (1.0 + fresnel * 0.5);
#else
// 使用自定义反射控制
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
// 保存光滑度到额外通道
o.Occlusion = reflMask.r * _ReflectionStrength * (1.0 + fresnel);
// 设置粗糙度以影响反射模糊
o.Smoothness = lerp(o.Smoothness, 0.0, _ReflectionRoughness);
#ifdef _USEPROBEBLENDING_ON
// 在表面属性上启用探针混合标志
o.Emission = fixed3(0.01, 0, 0); // 用于标记使用探针混合
#endif
#endif
}
ENDCG
}
FallBack "Diffuse"
}
这个着色器提供了对环境反射的精细控制,包括:
- 反射强度和菲涅尔效应控制,使边缘更反光
- 反射掩码纹理,允许对材质不同部分设置不同的反射强度
- 可选的探针混合功能,在多个反射探针之间平滑过渡
- 反射粗糙度控制,可以模拟磨砂表面的模糊反射
11.1.3 高效实现环境反射系统
环境反射是创建逼真材质的关键技术,下面我们将深入探讨如何在Unity中实现高效的反射系统。
理论基础:
反射的计算基于入射角和法线的关系,遵循公式:反射方向 = 入射方向 - 2 * (入射方向 · 法线) * 法线
在实际实现中,我们需要考虑以下因素:
- 反射向量的计算
- 采样环境贴图的策略
- 反射强度的控制
- 菲涅尔效应的模拟
以下是一个实时反射控制器,可以动态调整物体的反射属性:
csharp
using UnityEngine;
[RequireComponent(typeof(Renderer))]
public class ReflectionController : MonoBehaviour
{
[Header("Reflection Settings")]
[Range(0, 1)]
public float reflectivity = 0.5f;
[Range(0, 5)]
public float fresnelPower = 1.0f;
[Range(0, 1)]
public float reflectionBlur = 0.0f;
public bool useScreenSpaceReflection = false;
[Header("Environment Map")]
public Cubemap customReflectionCubemap;
public bool useProbeBlending = true;
[Header("Animation")]
public bool animateReflection = false;
[Range(0, 2)]
public float pulseSpeed = 0.5f;
[Range(0, 1)]
public float pulseAmount = 0.2f;
// 内部变量
private Renderer rend;
private Material[] materials;
private float initialReflectivity;
private float time = 0f;
void Start()
{
rend = GetComponent<Renderer>();
materials = rend.materials; // 创建材质实例
initialReflectivity = reflectivity;
ApplyReflectionSettings();
}
void Update()
{
if (animateReflection)
{
time += Time.deltaTime;
reflectivity = initialReflectivity + Mathf.Sin(time * pulseSpeed * Mathf.PI) * pulseAmount;
}
ApplyReflectionSettings();
}
void ApplyReflectionSettings()
{
foreach (Material mat in materials)
{
// 设置反射参数
if (mat.HasProperty("_ReflectionStrength"))
{
mat.SetFloat("_ReflectionStrength", reflectivity);
}
else if (mat.HasProperty("_Metallic") && mat.HasProperty("_Glossiness"))
{
// 对于标准着色器,调整金属度和光滑度以控制反射
mat.SetFloat("_Glossiness", Mathf.Lerp(0.1f, 1.0f, reflectivity));
}
// 设置菲涅尔参数
if (mat.HasProperty("_ReflectionFresnel"))
{
mat.SetFloat("_ReflectionFresnel", fresnelPower);
}
// 设置反射模糊
if (mat.HasProperty("_ReflectionRoughness"))
{
mat.SetFloat("_ReflectionRoughness", reflectionBlur);
}
// 启用/禁用探针混合
if (mat.HasProperty("_UseProbeBlending"))
{
if (useProbeBlending)
mat.EnableKeyword("_USEPROBEBLENDING_ON");
else
mat.DisableKeyword("_USEPROBEBLENDING_ON");
}
// 设置自定义反射贴图
if (customReflectionCubemap != null && mat.HasProperty("_ReflectionCubemap"))
{
mat.SetTexture("_ReflectionCubemap", customReflectionCubemap);
mat.EnableKeyword("_USECUSTOMCUBEMAP_ON");
}
else if (mat.HasProperty("_ReflectionCubemap"))
{
mat.DisableKeyword("_USECUSTOMCUBEMAP_ON");
}
// 控制屏幕空间反射
if (mat.HasProperty("_UseScreenSpaceReflection"))
{
if (useScreenSpaceReflection)
mat.EnableKeyword("_SCREENSPACEREFLECTION_ON");
else
mat.DisableKeyword("_SCREENSPACEREFLECTION_ON");
}
}
}
void OnDestroy()
{
// 清理材质实例
if (Application.isPlaying)
{
foreach (Material m in materials)
{
Destroy(m);
}
}
}
// 编辑器辅助方法
[ContextMenu("Capture Environment Reflection")]
void CaptureEnvironmentReflection()
{
#if UNITY_EDITOR
// 创建反射探针
GameObject probeObj = new GameObject("TempReflectionProbe");
probeObj.transform.position = transform.position;
ReflectionProbe probe = probeObj.AddComponent<ReflectionProbe>();
probe.resolution = 256;
probe.hdr = true;
probe.RenderProbe();
// 等待渲染完成并保存
UnityEditor.EditorApplication.delayCall += () => {
string path = UnityEditor.EditorUtility.SaveFilePanelInProject(
"Save Environment Cubemap",
gameObject.name + "_EnvMap",
"asset",
"Save environment cubemap"
);
if (!string.IsNullOrEmpty(path))
{
Cubemap cubemap = new Cubemap(256, TextureFormat.RGBAHalf, true);
Graphics.CopyTexture(probe.texture, cubemap);
UnityEditor.AssetDatabase.CreateAsset(cubemap, path);
UnityEditor.AssetDatabase.SaveAssets();
customReflectionCubemap = cubemap;
Debug.Log("Saved environment cubemap to: " + p