第八章 高级游戏材质表现:Unity引擎中的纹理渲染全解析
8.1 理解和应用基础纹理技术
纹理是游戏开发中为3D模型表面添加细节和真实感的重要元素。本节将深入探讨单张纹理的基本概念和应用方法,帮助开发者掌握纹理映射的核心技术。
8.1.1 纹理映射实践指南
纹理映射是将2D图像应用到3D模型表面的过程。这一技术能够在不增加几何复杂度的情况下,为模型添加细节、颜色和表面特性。
要在Unity中实现基础纹理映射,我们首先需要了解UV坐标系统。UV坐标是二维纹理空间上的坐标,用于确定3D模型上每个点对应纹理上的哪个像素。
以下是在Unity 2021.3.8f1c1 LTS中实现基础纹理映射的C#脚本示例:
csharp
using UnityEngine;
public class BasicTextureMapper : MonoBehaviour
{
[Header("纹理设置")]
public Texture2D mainTexture;
public Vector2 tiling = Vector2.one;
public Vector2 offset = Vector2.zero;
[Header("实时调整")]
public bool animateUVs = false;
public float scrollSpeedU = 0.1f;
public float scrollSpeedV = 0.0f;
private Renderer objectRenderer;
private MaterialPropertyBlock propertyBlock;
void Start()
{
objectRenderer = GetComponent<Renderer>();
propertyBlock = new MaterialPropertyBlock();
if (mainTexture != null)
{
// 确保纹理具有正确的导入设置
ConfigureTextureImportSettings();
// 应用纹理到材质
ApplyTexture();
}
else
{
Debug.LogWarning("未指定主纹理,请指定一个纹理。");
}
}
void Update()
{
if (animateUVs)
{
// 计算UV偏移
offset.x = (offset.x + scrollSpeedU * Time.deltaTime) % 1.0f;
offset.y = (offset.y + scrollSpeedV * Time.deltaTime) % 1.0f;
// 更新材质
ApplyTexture();
}
}
void ApplyTexture()
{
if (objectRenderer == null || mainTexture == null)
return;
// 使用MaterialPropertyBlock避免创建材质实例
objectRenderer.GetPropertyBlock(propertyBlock);
propertyBlock.SetTexture("_MainTex", mainTexture);
propertyBlock.SetVector("_MainTex_ST", new Vector4(tiling.x, tiling.y, offset.x, offset.y));
objectRenderer.SetPropertyBlock(propertyBlock);
}
void ConfigureTextureImportSettings()
{
// 注意:在编辑器模式下才能修改导入设置
#if UNITY_EDITOR
if (mainTexture != null)
{
string assetPath = UnityEditor.AssetDatabase.GetAssetPath(mainTexture);
if (!string.IsNullOrEmpty(assetPath))
{
UnityEditor.TextureImporter importer = UnityEditor.AssetImporter.GetAtPath(assetPath) as UnityEditor.TextureImporter;
if (importer != null)
{
// 检查并配置最佳设置
bool needsReimport = false;
// 对于大多数颜色纹理,使用压缩格式
if (importer.textureCompression != UnityEditor.TextureImporterCompression.Compressed)
{
importer.textureCompression = UnityEditor.TextureImporterCompression.Compressed;
needsReimport = true;
}
// 启用Mipmap以提高性能和质量
if (!importer.mipmapEnabled)
{
importer.mipmapEnabled = true;
needsReimport = true;
}
// 应用设置并重新导入
if (needsReimport)
{
Debug.Log($"优化纹理导入设置: {mainTexture.name}");
importer.SaveAndReimport();
}
}
}
}
#endif
}
// 提供一个便捷方法以在编辑器中创建测试平面
public static GameObject CreateTexturedPlane(Texture2D texture, Vector3 position, Vector2 size)
{
GameObject plane = GameObject.CreatePrimitive(PrimitiveType.Plane);
plane.transform.position = position;
plane.transform.localScale = new Vector3(size.x/10f, 1f, size.y/10f); // 平面默认大小为10x10
Renderer renderer = plane.GetComponent<Renderer>();
if (renderer != null && texture != null)
{
Material material = new Material(Shader.Find("Standard"));
material.mainTexture = texture;
renderer.material = material;
}
return plane;
}
}
对应的基础纹理着色器示例:
glsl
Shader "Custom/BasicTexture"
{
Properties
{
_MainTex ("纹理", 2D) = "white" {}
_Color ("颜色", Color) = (1,1,1,1)
_Glossiness ("光泽度", Range(0,1)) = 0.5
_Metallic ("金属度", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// 使用物理光照模型
#pragma surface surf Standard fullforwardshadows
#pragma target 3.0
sampler2D _MainTex;
fixed4 _Color;
half _Glossiness;
half _Metallic;
struct Input
{
float2 uv_MainTex;
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
// 采样纹理并应用颜色调整
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
如果你需要完全自定义UV坐标的处理方式,可以使用以下更高级的着色器:
glsl
Shader "Custom/AdvancedTextureMapping"
{
Properties
{
_MainTex ("纹理", 2D) = "white" {}
_Color ("颜色", Color) = (1,1,1,1)
_Tiling ("平铺", Vector) = (1,1,0,0)
_Offset ("偏移", Vector) = (0,0,0,0)
_RotationAngle ("旋转角度", Range(0, 360)) = 0
}
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_ST;
fixed4 _Color;
float4 _Tiling;
float4 _Offset;
float _RotationAngle;
// UV坐标变换:旋转
float2 rotateUV(float2 uv, float angle)
{
// 转换角度为弧度
float radian = angle * UNITY_PI / 180;
// 将UV中心移到(0,0)
float2 center = float2(0.5, 0.5);
float2 uv_centered = uv - center;
// 旋转矩阵
float2x2 rotation = float2x2(
cos(radian), -sin(radian),
sin(radian), cos(radian)
);
// 应用旋转
float2 rotated_uv = mul(rotation, uv_centered);
// 移回原来的中心
return rotated_uv + center;
}
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 应用缩放和偏移
float2 uv = v.uv * _Tiling.xy + _Offset.xy;
// 应用旋转
uv = rotateUV(uv, _RotationAngle);
o.uv = uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.uv);
// 应用颜色调整
col *= _Color;
return col;
}
ENDCG
}
}
}
为了更好地展示这些技术,我们可以创建一个编辑器工具来可视化和操作UV坐标:
csharp
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
public class UVVisualizer : EditorWindow
{
private GameObject targetObject;
private Mesh targetMesh;
private Material previewMaterial;
private bool showUVs = true;
private bool showGrid = true;
private Color uvLineColor = new Color(0, 1, 0, 0.7f);
private float uvLineThickness = 1.0f;
[MenuItem("工具/UV可视化工具")]
public static void ShowWindow()
{
GetWindow<UVVisualizer>("UV可视化工具");
}
void OnEnable()
{
// 创建用于预览的材质
previewMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));
}
void OnGUI()
{
EditorGUILayout.LabelField("UV坐标可视化", EditorStyles.boldLabel);
EditorGUI.BeginChangeCheck();
targetObject = EditorGUILayout.ObjectField("目标对象", targetObject, typeof(GameObject), true) as GameObject;
if (EditorGUI.EndChangeCheck())
{
if (targetObject != null)
{
MeshFilter meshFilter = targetObject.GetComponent<MeshFilter>();
if (meshFilter != null)
{
targetMesh = meshFilter.sharedMesh;
}
else
{
targetMesh = null;
EditorGUILayout.HelpBox("所选对象没有MeshFilter组件", MessageType.Warning);
}
}
else
{
targetMesh = null;
}
}
showUVs = EditorGUILayout.Toggle("显示UV线", showUVs);
showGrid = EditorGUILayout.Toggle("显示网格", showGrid);
uvLineColor = EditorGUILayout.ColorField("UV线颜色", uvLineColor);
uvLineThickness = EditorGUILayout.Slider("UV线粗细", uvLineThickness, 0.1f, 3.0f);
if (targetMesh != null)
{
EditorGUILayout.LabelField("UV统计", EditorStyles.boldLabel);
EditorGUILayout.LabelField("顶点数: " + targetMesh.vertexCount);
EditorGUILayout.LabelField("UV集数: " + targetMesh.uv.Length / targetMesh.vertexCount);
Rect previewRect = GUILayoutUtility.GetRect(200, 200);
DrawUVPreview(previewRect);
}
}
void DrawUVPreview(Rect rect)
{
if (targetMesh == null || Event.current.type != EventType.Repaint)
return;
// 准备绘制UV
GUI.Box(rect, "");
// 设置材质
previewMaterial.SetPass(0);
// 开始绘制
GL.PushMatrix();
GL.LoadPixelMatrix();
// 绘制背景网格
if (showGrid)
{
DrawGrid(rect, 10, Color.gray);
}
// 绘制UV线
if (showUVs)
{
DrawUVs(rect);
}
GL.PopMatrix();
}
void DrawGrid(Rect rect, int gridSize, Color gridColor)
{
GL.Begin(GL.LINES);
GL.Color(gridColor);
// 绘制垂直线
for (int i = 0; i <= gridSize; i++)
{
float x = rect.x + rect.width * i / gridSize;
GL.Vertex3(x, rect.y, 0);
GL.Vertex3(x, rect.y + rect.height, 0);
}
// 绘制水平线
for (int i = 0; i <= gridSize; i++)
{
float y = rect.y + rect.height * i / gridSize;
GL.Vertex3(rect.x, y, 0);
GL.Vertex3(rect.x + rect.width, y, 0);
}
GL.End();
// 特别标记UV空间的[0,0]到[1,1]区域
GL.Begin(GL.LINES);
GL.Color(Color.red);
// 绘制UV单位正方形
float x0 = rect.x;
float y0 = rect.y + rect.height; // 注意:Y轴在GUI中是反的
float x1 = rect.x + rect.width;
float y1 = rect.y;
// 绘制UV单位正方形的边界
GL.Vertex3(x0, y0, 0); GL.Vertex3(x1, y0, 0);
GL.Vertex3(x1, y0, 0); GL.Vertex3(x1, y1, 0);
GL.Vertex3(x1, y1, 0); GL.Vertex3(x0, y1, 0);
GL.Vertex3(x0, y1, 0); GL.Vertex3(x0, y0, 0);
GL.End();
}
void DrawUVs(Rect rect)
{
if (targetMesh == null)
return;
Vector2[] uvs = targetMesh.uv;
int[] triangles = targetMesh.triangles;
if (uvs.Length == 0 || triangles.Length == 0)
return;
GL.Begin(GL.LINES);
GL.Color(uvLineColor);
for (int i = 0; i < triangles.Length; i += 3)
{
int idx1 = triangles[i];
int idx2 = triangles[i + 1];
int idx3 = triangles[i + 2];
if (idx1 >= uvs.Length || idx2 >= uvs.Length || idx3 >= uvs.Length)
continue;
Vector2 uv1 = uvs[idx1];
Vector2 uv2 = uvs[idx2];
Vector2 uv3 = uvs[idx3];
// 转换UV坐标到屏幕空间
Vector2 screen1 = new Vector2(
rect.x + uv1.x * rect.width,
rect.y + (1 - uv1.y) * rect.height
);
Vector2 screen2 = new Vector2(
rect.x + uv2.x * rect.width,
rect.y + (1 - uv2.y) * rect.height
);
Vector2 screen3 = new Vector2(
rect.x + uv3.x * rect.width,
rect.y + (1 - uv3.y) * rect.height
);
// 绘制三角形的三条边
GL.Vertex3(screen1.x, screen1.y, 0);
GL.Vertex3(screen2.x, screen2.y, 0);
GL.Vertex3(screen2.x, screen2.y, 0);
GL.Vertex3(screen3.x, screen3.y, 0);
GL.Vertex3(screen3.x, screen3.y, 0);
GL.Vertex3(screen1.x, screen1.y, 0);
}
GL.End();
}
}
#endif
8.1.2 纹理参数与属性解析
纹理有许多重要的属性和参数,这些参数会影响纹理的显示效果和性能。了解这些属性对于优化游戏性能和提高视觉质量至关重要。
以下是一个用于分析和控制纹理属性的工具脚本:
csharp
using UnityEngine;
using System.Collections.Generic;
public class TexturePropertyAnalyzer : MonoBehaviour
{
[System.Serializable]
public class TextureInfo
{
public Texture2D texture;
public string name;
public Vector2Int dimensions;
public TextureFormat format;
public bool hasMipMaps;
public int memorySize; // 单位:KB
public FilterMode filterMode;
public TextureWrapMode wrapMode;
public int anisoLevel;
}
[Header("纹理分析")]
public Renderer targetRenderer;
public List<TextureInfo> analyzedTextures = new List<TextureInfo>();
[Header("纹理设置")]
public FilterMode defaultFilterMode = FilterMode.Bilinear;
public TextureWrapMode defaultWrapMode = TextureWrapMode.Repeat;
public int defaultAnisoLevel = 1;
void Start()
{
if (targetRenderer == null)
{
targetRenderer = GetComponent<Renderer>();
}
if (targetRenderer != null)
{
AnalyzeTextures();
}
}
[ContextMenu("分析纹理")]
public void AnalyzeTextures()
{
if (targetRenderer == null)
return;
analyzedTextures.Clear();
Material[] materials = targetRenderer.sharedMaterials;
foreach (Material material in materials)
{
if (material == null)
continue;
// 获取材质的所有属性
Shader shader = material.shader;
int propertyCount = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < propertyCount; i++)
{
ShaderUtil.ShaderPropertyType propertyType = ShaderUtil.GetPropertyType(shader, i);
if (propertyType == ShaderUtil.ShaderPropertyType.TexEnv)
{
string propertyName = ShaderUtil.GetPropertyName(shader, i);
Texture texture = material.GetTexture(propertyName);
if (texture != null && texture is Texture2D)
{
Texture2D tex2D = texture as Texture2D;
TextureInfo info = new TextureInfo
{
texture = tex2D,
name = propertyName,
dimensions = new Vector2Int(tex2D.width, tex2D.height),
format = tex2D.format,
hasMipMaps = tex2D.mipmapCount > 1,
memorySize = Mathf.RoundToInt(GetEstimatedMemorySize(tex2D) / 1024f),
filterMode = tex2D.filterMode,
wrapMode = tex2D.wrapMode,
anisoLevel = tex2D.anisoLevel
};
analyzedTextures.Add(info);
}
}
}
}
Debug.Log($"分析完成,找到 {analyzedTextures.Count} 个纹理");
}
// 估算纹理内存占用(粗略计算)
private int GetEstimatedMemorySize(Texture2D texture)
{
int bytesPerPixel = GetBytesPerPixel(texture.format);
int mipMapReduction = texture.mipmapCount > 1 ? 3 : 4; // 有MipMap时内存约为原始大小的1/3
return (texture.width * texture.height * bytesPerPixel) / mipMapReduction;
}
// 获取每种纹理格式的每像素字节数
private int GetBytesPerPixel(TextureFormat format)
{
switch (format)
{
case TextureFormat.Alpha8:
return 1;
case TextureFormat.RGB24:
return 3;
case TextureFormat.RGBA32:
return 4;
case TextureFormat.ARGB32:
return 4;
case TextureFormat.RGB565:
return 2;
case TextureFormat.DXT1:
return 1; // 压缩格式,近似值
case TextureFormat.DXT5:
return 1; // 压缩格式,近似值
default:
return 4; // 默认假设4字节
}
}
// 应用纹理设置
public void ApplyTextureSettings(int textureIndex, FilterMode filterMode, TextureWrapMode wrapMode, int anisoLevel)
{
if (textureIndex < 0 || textureIndex >= analyzedTextures.Count)
return;
TextureInfo info = analyzedTextures[textureIndex];
Texture2D texture = info.texture;
if (texture != null)
{
texture.filterMode = filterMode;
texture.wrapMode = wrapMode;
texture.anisoLevel = anisoLevel;
// 更新信息
info.filterMode = filterMode;
info.wrapMode = wrapMode;
info.anisoLevel = anisoLevel;
analyzedTextures[textureIndex] = info;
}
}
// 应用默认设置到所有纹理
[ContextMenu("应用默认设置到所有纹理")]
public void ApplyDefaultSettingsToAll()
{
for (int i = 0; i < analyzedTextures.Count; i++)
{
ApplyTextureSettings(i, defaultFilterMode, defaultWrapMode, defaultAnisoLevel);
}
Debug.Log("已应用默认设置到所有纹理");
}
}
// 添加编辑器界面以便更好地可视化和控制
#if UNITY_EDITOR
using UnityEditor;
[CustomEditor(typeof(TexturePropertyAnalyzer))]
public class TexturePropertyAnalyzerEditor : Editor
{
public override void OnInspectorGUI()
{
TexturePropertyAnalyzer analyzer = (TexturePropertyAnalyzer)target;
DrawDefaultInspector();
EditorGUILayout.Space();
if (GUILayout.Button("分析纹理"))
{
analyzer.AnalyzeTextures();
}
EditorGUILayout.Space();
// 显示分析结果
if (analyzer.analyzedTextures.Count > 0)
{
EditorGUILayout.LabelField("纹理分析结果", EditorStyles.boldLabel);
for (int i = 0; i < analyzer.analyzedTextures.Count; i++)
{
var info = analyzer.analyzedTextures[i];
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(info.name, EditorStyles.boldLabel);
if (GUILayout.Button("应用默认设置", GUILayout.Width(120)))
{
analyzer.ApplyTextureSettings(i, analyzer.defaultFilterMode, analyzer.defaultWrapMode, analyzer.defaultAnisoLevel);
}
EditorGUILayout.EndHorizontal();
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField("纹理", info.texture, typeof(Texture2D), false);
EditorGUI.EndDisabledGroup();
EditorGUILayout.LabelField($"尺寸: {info.dimensions.x} x {info.dimensions.y}");
EditorGUILayout.LabelField($"格式: {info.format}");
EditorGUILayout.LabelField($"MipMap: {(info.hasMipMaps ? "是" : "否")}");
EditorGUILayout.LabelField($"内存占用: ~{info.memorySize} KB");
EditorGUI.BeginChangeCheck();
FilterMode filterMode = (FilterMode)EditorGUILayout.EnumPopup("过滤模式", info.filterMode);
TextureWrapMode wrapMode = (TextureWrapMode)EditorGUILayout.EnumPopup("环绕模式", info.wrapMode);
int anisoLevel = EditorGUILayout.IntSlider("各向异性过滤", info.anisoLevel, 0, 16);
if (EditorGUI.EndChangeCheck())
{
analyzer.A