1. 项目概述:为什么动态加载纹理是个“技术活”?
在Unity3D项目里,动态加载图片资源,尤其是Texture2D,几乎是每个开发者都会遇到的常规操作。无论是从网络下载用户头像、加载本地相册的图片作为游戏贴图,还是运行时根据配置切换UI背景,都离不开它。听起来很简单,不就是调用WWW、UnityWebRequest或者File.ReadAllBytes,然后塞给Texture2D的构造函数吗?但很多朋友,包括我自己在早期,都踩过一个大坑:图片加载出来,边缘模糊、像素拉伸,甚至出现奇怪的马赛克,完全不是原图该有的样子。这就是典型的“纹理失真”问题。
这个问题之所以棘手,是因为它不像代码报错那样有明确的日志。图片显示出来了,但“不对味”,问题可能隐藏在资源导入设置、纹理压缩格式、尺寸规范,甚至是加载后对纹理参数的细微调整中。特别是当你从非标准来源(比如用户上传、外部工具生成)加载图片时,Unity的默认处理机制可能并不适用。最近在社区里,看到不少朋友在讨论从SolidWorks导出的模型贴图在Unity里失真的问题,或者在做AR/VR项目时,动态加载的环境贴图出现色差和模糊,其根源往往与此类似。
所以,这篇内容不是简单地罗列API,而是想和你深入聊聊,在Unity3D里动态加载Texture2D时,如何系统性地避免和解决纹理失真。我们会从原理出发,拆解Unity的纹理处理管线,然后给出从加载、设置到应用的全链路解决方案和避坑指南。无论你是正在处理一个简单的2D小游戏素材,还是在为一个复杂的PC串联VR项目处理动态资源,这里面的思路都是相通的。
2. 纹理失真根源深度解析:不止是“加载”那一步
很多人认为失真发生在LoadImage的那一刻,其实不然。失真是一个累积效应,可能源于源文件、Unity的导入预设、运行时设置等多个环节。我们必须像侦探一样,逐一排查。
2.1 非2的幂次方(NPOT)尺寸:最常见的“元凶”
这是Unity官方文档和无数教程都会强调的第一点,但很多人知其然不知其所以然。为什么Unity“偏爱”2的幂次方(Power of Two, POT)纹理?比如32x32, 64x128, 256x1024这样的尺寸。
核心原因在于GPU和纹理压缩算法。现代GPU的纹理采样器和内存控制器针对POT尺寸的纹理进行了高度优化。许多纹理压缩格式(如DXT/BC系列, ETC, ASTC)的压缩块大小也是基于POT的。当你使用一个NPOT(如513x513)纹理时,GPU驱动或Unity底层可能会在后台将其填充或缩放到最接近的POT尺寸(如512x512或1024x1024)进行处理。这个缩放过程,如果处理不当(比如用了低质量的缩放算法),就会导致边缘像素模糊或拉伸。
注意:在Unity 2018 LTS及以后的版本中,对于支持“NPOT扩展”的现代GPU平台(如PC、大部分移动平台),NPOT纹理的限制已经大大放宽,通常可以正常工作。但是,这并不意味着你可以高枕无忧。问题出在“纹理压缩”和“Mipmap生成”上。即使GPU支持渲染NPOT纹理,但如果你启用了Mipmap或特定的压缩格式,Unity可能仍然需要将其转换为POT纹理来满足这些功能的要求,从而引发失真。
如何判断与解决?
- 检查源文件尺寸:在加载前,最好能获取图片的宽高。如果是来自不可控的源(如网络),可以考虑在服务器端或加载后,使用一个简单的脚本来调整尺寸。
// 示例:将纹理调整为最接近的2的幂次方尺寸(上取整) public static Texture2D ResizeToPOT(Texture2D source) { int potWidth = Mathf.NextPowerOfTwo(source.width); int potHeight = Mathf.NextPowerOfTwo(source.height); // 使用Graphics.CopyTexture或缩放算法进行高质量调整 // ... 具体实现取决于需求 } - 理解Unity的“Non Power of 2”导入设置:对于通过Asset Import Pipeline导入的图片,在Inspector面板的
Advanced模式下,Non Power of 2选项至关重要。它决定了Unity如何处理NPOT纹理:None:不进行缩放。这是动态加载纹理时最需要小心的参照,因为这意味着纹理会保持原样进入运行时,失真风险留给了GPU驱动。ToNearest:缩放到最接近的POT尺寸。可能导致轻微拉伸。ToLarger:缩放到更大的POT尺寸。保留原内容,但浪费显存。ToSmaller:缩放到更小的POT尺寸。可能导致细节丢失。对于动态加载的纹理,我们没有这个面板可以点。因此,我们需要在代码中模拟“None”或主动进行高质量缩放,这就是我们后面要做的。
2.2 纹理过滤模式(Filter Mode)设置不当
过滤模式决定了当纹理被拉伸或缩小时,像素如何被采样。动态加载纹理后,如果不显式设置,可能会使用材质或Unity的默认设置,这很可能不合适。
- Point (No Filter):最近邻过滤。纹理像素化,锯齿感强。适合像素风游戏。如果你加载的是一张像素艺术图,但用了双线性过滤,它就会变模糊。
- Bilinear:双线性过滤。在2x2像素区域内进行线性插值,效果平滑,但有轻微模糊。是3D模型纹理的常用选择。
- Trilinear:三线性过滤。在双线性基础上,还在Mipmap层级之间插值,更加平滑,但也更模糊。
实操心得:对于动态加载的UI精灵(Sprite)或2D游戏元素,如果你希望它边缘锐利,特别是在缩放时,Filter Mode设置为Point可能是更好的选择。对于3D模型贴图,Bilinear是安全的选择。关键点是:动态加载后,必须根据纹理用途显式设置这个属性。
Texture2D tex = new Texture2D(2, 2); // ... 加载图片数据到tex ... tex.filterMode = FilterMode.Bilinear; // 或 Point, Trilinear2.3 不正确的纹理压缩格式
这是另一个深水区。为了节省内存和带宽,纹理在GPU中通常以压缩格式存储。Unity会根据目标平台自动选择默认压缩格式(如Android用ETC2, iOS用ASTC)。但对于动态加载的Texture2D,它默认是RGBA32这样的未压缩格式,占用内存大。如果你直接将其用于需要压缩格式的场合(比如复制到一张压缩格式的RenderTexture),或者与使用压缩格式的静态资源混合使用,就可能出问题。
更常见的问题是,你加载的图片本身可能是JPG/PNG,但你的Shader或材质期望的是带有Alpha通道的纹理,或者反之。例如,一张不带透明通道的JPG图片,被当作RGBA32加载并用于需要Alpha剪裁的Shader,边缘会出现杂色。
解决方案:明确纹理用途。如果是用于UI且不需要Alpha,可以尝试用RGB24格式加载以减少内存。如果需要高质量且无压缩,就用RGBA32或RGBAFloat。对于移动平台,如果性能压力大,可以考虑在加载后使用Texture2D.Compress进行有损压缩(但这是CPU端压缩,且会改变纹理数据)。
2.4 Mipmap的启用与关闭
Mipmap是一系列预先计算好的、逐渐缩小的纹理副本,用于在物体远离摄像机时进行采样,可以有效地减少远处物体的闪烁(摩尔纹)。但是,对于动态加载的2D UI元素、屏幕空间特效或始终满屏显示的Sprite,启用Mipmap不仅是浪费内存,还会导致纹理在屏幕上看起来始终是模糊的,因为Unity可能会错误地选用低层级的Mipmap。
关键操作:动态加载纹理后,务必根据其用途设置tex.mipMapBias或直接关闭Mipmap生成。对于Texture2D,在加载数据后,你可以设置:
tex.mipMapBias = -1; // 偏向使用更清晰的Mipmap层级,但更好的做法是... // 或者,对于不需要Mipmap的纹理,在可能的情况下,确保它不被生成。 // 注意:对于动态创建的Texture2D,mipmap参数在构造函数中设置。 Texture2D tex = new Texture2D(width, height, TextureFormat.RGBA32, false); // 最后一个参数`mipChain`设为false,不生成Mipmap3. 动态加载Texture2D的正确姿势与代码实战
理解了病因,我们来开药方。下面我将展示几种常见的动态加载方法,并附上针对性的防失真配置。
3.1 从本地文件系统加载(适用于PC、Mac、Linux Standalone)
这是最直接的方式。注意文件路径和平台差异。
using UnityEngine; using System.IO; public class TextureLoaderFromFile : MonoBehaviour { public string filePath = “Assets/ExternalImages/photo.jpg”; // 示例路径 IEnumerator Start() { // 1. 构建完整路径(注意:在移动平台上,文件路径受限) string fullPath = Path.Combine(Application.streamingAssetsPath, filePath); // 或者对于可写路径:Application.persistentDataPath // 2. 读取字节数据 if (!File.Exists(fullPath)) { Debug.LogError(“文件不存在: “ + fullPath); yield break; } byte[] fileData = File.ReadAllBytes(fullPath); // 3. 创建Texture2D并加载 // **关键点1:根据是否需要Alpha通道选择格式** Texture2D tex = new Texture2D(2, 2, TextureFormat.RGBA32, false); // 无Mipmap // **关键点2:使用LoadImage加载,它会根据图片数据自动调整纹理尺寸** bool success = tex.LoadImage(fileData); if (!success) { Debug.LogError(“加载图片失败!”); Destroy(tex); yield break; } // 4. 应用防失真设置 ApplyAntiDistortionSettings(tex); // 5. 使用纹理(例如赋值给RawImage) GetComponent<UnityEngine.UI.RawImage>().texture = tex; } void ApplyAntiDistortionSettings(Texture2D texture) { // 设置过滤模式 texture.filterMode = FilterMode.Bilinear; // 根据需求调整 // 设置环绕模式 texture.wrapMode = TextureWrapMode.Clamp; // 防止边缘重复导致接缝 // **关键点3:应用设置!这步至关重要,它将设置提交给GPU。** texture.Apply(false, true); // 第一个参数:是否生成Mipmap(我们已设为false)。第二个参数:是否设为非可读,节省内存。 } }注意事项:
File.ReadAllBytes是同步操作,对于大文件会卡顿主线程。对于大纹理,应考虑使用FileStream异步读取或放在子线程中。Texture2D.LoadImage是一个重量级函数,它会对图片进行解码(JPG/PNG)。它也会自动调整Texture2D的尺寸以匹配图片。texture.Apply()是将CPU端的纹理数据上传到GPU的关键调用。没有它,你的设置不会生效。第二个参数makeNoLongerReadable设为true可以释放CPU端的内存副本,但之后你就无法从纹理中GetPixels了,请根据后续需求决定。
3.2 从网络加载(UnityWebRequest)
这是加载网络图片的现代推荐方式,支持异步,不阻塞主线程。
using UnityEngine; using UnityEngine.Networking; using System.Collections; public class TextureLoaderFromWeb : MonoBehaviour { public string imageUrl = “https://example.com/image.png”; IEnumerator Start() { using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(imageUrl)) { yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError(“下载失败: “ + request.error); yield break; } // 直接获取DownloadHandlerTexture中的纹理,它已经是一个配置好的Texture2D Texture2D downloadedTexture = DownloadHandlerTexture.GetContent(request); // **但是!这个纹理的默认设置可能不符合我们的要求。** // 我们需要检查并重新配置它。 if (downloadedTexture != null) { // 重新应用我们的防失真设置 // 注意:由于downloadedTexture可能已经是压缩格式或只读,我们需要创建一个新的纹理来应用设置 Texture2D finalTexture = new Texture2D(downloadedTexture.width, downloadedTexture.height, downloadedTexture.format, false); Graphics.CopyTexture(downloadedTexture, finalTexture); ApplyAntiDistortionSettings(finalTexture); GetComponent<UnityEngine.UI.RawImage>().texture = finalTexture; // 销毁原始的下载纹理以释放内存 Destroy(downloadedTexture); } } } void ApplyAntiDistortionSettings(Texture2D texture) { // 同上例,根据用途设置 texture.filterMode = FilterMode.Trilinear; // 网络图片常用于3D场景,可能用三线性 texture.wrapMode = TextureWrapMode.Repeat; // 根据UV需求设置 texture.Apply(false, true); } }避坑技巧:
UnityWebRequestTexture.GetTexture有一个可选的nonReadable参数,默认为true,即下载的纹理在CPU端不可读。这能节省内存。如果你后续不需要修改像素,保持true。- 直接使用
DownloadHandlerTexture.GetContent得到的纹理,其过滤模式等是Unity默认的。对于需要精确控制的场景(如显示高清Logo),最好像上面一样,复制一份并重新配置。Graphics.CopyTexture是GPU间的数据拷贝,非常高效。 - 务必处理网络错误和超时,并为加载中的状态设置占位图。
3.3 处理NPOT纹理的实战策略
当你无法控制源图片尺寸时,这里是几种处理策略的对比:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 策略1:保持原样 (None) | 无缩放损失,速度最快。 | 在某些平台/GPU上可能导致性能下降或渲染瑕疵;启用Mipmap或压缩时可能出问题。 | 目标平台明确支持NPOT(如现代PC、高端移动设备),且纹理不用于需要Mipmap或特定压缩的场合。 |
| 策略2:运行时缩放至POT | 兼容性最好,确保Mipmap和压缩正常工作。 | 有一次性缩放开销,可能引入轻微的缩放失真(取决于算法质量)。 | 通用解决方案,特别是纹理需要用于3D模型、需要Mipmap或跨平台发布时。 |
| 策略3:填充至POT | 保留全部原始图像信息。 | 浪费显存,边缘填充部分可能需要特殊处理(如设为透明)。 | 源图像尺寸非常接近POT,且不能接受任何内容裁剪的场景。 |
推荐实现(策略2:高质量运行时缩放): Unity本身没有提供完美的运行时缩放API。我们可以使用Graphics.CopyTexture配合一个临时的RenderTexture来实现相对高质量的缩放。
public Texture2D ScaleTextureToPOT(Texture2D source) { // 计算目标POT尺寸 int targetWidth = Mathf.NextPowerOfTwo(source.width); int targetHeight = Mathf.NextPowerOfTwo(source.height); // 如果已经是POT,直接返回原纹理(或副本) if (source.width == targetWidth && source.height == targetHeight) { Texture2D potTex = new Texture2D(source.width, source.height, source.format, false); Graphics.CopyTexture(source, potTex); return potTex; } // 创建一个临时的RenderTexture用于缩放 RenderTexture rt = RenderTexture.GetTemporary(targetWidth, targetHeight, 0, RenderTextureFormat.ARGB32); RenderTexture previous = RenderTexture.active; RenderTexture.active = rt; // 使用Graphics.Blit进行缩放(使用默认的双线性过滤) Graphics.Blit(source, rt); // 从RenderTexture读回数据到新的Texture2D Texture2D resultTex = new Texture2D(targetWidth, targetHeight, TextureFormat.RGBA32, false); resultTex.ReadPixels(new Rect(0, 0, targetWidth, targetHeight), 0, 0); resultTex.Apply(); // 恢复原来的RenderTexture并释放临时RT RenderTexture.active = previous; RenderTexture.ReleaseTemporary(rt); // 销毁原始的source纹理(如果它不再需要) // Destroy(source); return resultTex; }提示:
Graphics.Blit的缩放质量取决于源纹理和RenderTexture的过滤设置。对于要求极高的缩放质量(如美术资源),建议在专业的图像处理工具(如Photoshop)中预处理为POT尺寸,而非运行时处理。
4. 高级议题与平台特定问题排查
解决了基础加载和设置,我们来看看一些更复杂的情况。
4.1 与Shader的配合问题:sRGB与线性空间
这是一个颜色失真的高级原因。Unity的Color Space(颜色空间)设置会影响纹理的采样。在Linear颜色空间下,Unity会假设纹理颜色数据存储在sRGB空间中,并在采样时自动将其转换到线性空间进行计算,最后再转换回sRGB输出到屏幕。这对于大多数颜色贴图(Albedo)是正确的。
但是,如果你加载的纹理是非颜色数据,比如法线贴图(Normal Map)、金属度/光滑度贴图(Metallic/Smoothness)、高度图等,它们的数据本身就在线性空间。如果Unity错误地对其进行了sRGB转换,就会导致颜色/数值错误,看起来就是严重的失真。
解决方案:在Unity 2017.1+,你可以通过代码设置纹理的sRGB属性(对应导入设置中的“sRGB (Color Texture)”复选框)。但请注意,动态创建的Texture2D无法直接设置此属性。一个变通方法是使用Graphics.CopyTexture将数据复制到一个通过Texture2D构造函数创建、但格式为Linear的纹理上,但这比较复杂。
更实用的做法是:确保你的Shader正确采样纹理。在Shader中,对于非颜色数据纹理,使用tex2D函数的linear版本(如果支持),或者通过UNITY_SAMPLE_TEX2D宏,并确保纹理在Shader中声明为正确的类型(如UNITY_DECLARE_TEX2D_NOSAMPLER用于法线贴图)。对于动态加载的纹理,你可能需要准备两套材质/Shader,一套用于颜色纹理,一套用于非颜色数据纹理。
4.2 移动平台(Android/iOS)上的特殊考量
移动平台内存和带宽更紧张,纹理压缩至关重要。
- 格式选择:使用
TextureFormat.ETC2_RGBA8(OpenGL ES 3.0) 或TextureFormat.ASTC_6x6等压缩格式创建纹理可以极大节省内存。但动态加载的JPG/PNG数据是未压缩的,你需要先加载到RGBA32纹理,然后调用Texture2D.Compress进行压缩(高耗时,建议在加载场景时异步进行)。 - 内存峰值:一次性加载大量高清纹理会导致内存峰值,引发应用崩溃。必须实现分帧加载、LRU缓存或使用
AssetBundle的异步加载机制。 - NPOT支持:虽然现代移动GPU基本支持NPOT,但为了保险和最佳性能,尤其是低端设备,仍建议处理为POT。
4.3 常见问题排查清单(Debug Checklist)
当遇到纹理失真时,请按此清单逐一核对:
- 尺寸检查:纹理的宽和高是否是2的幂次方?用
tex.width和tex.height打印出来看。 - 过滤模式:
tex.filterMode设置是否正确?尝试改为Point看是否变清晰,改为Bilinear看是否变模糊,以判断问题。 - Mipmap:纹理是否错误地生成了Mipmap?检查创建纹理时的
mipChain参数,或通过tex.mipmapCount查看。对于2D UI,确保它为1。 - 压缩格式:纹理的
format是什么?是否与Shader或平台期望的格式冲突?尝试用RGBA32这种无损格式加载对比。 - Apply调用:是否在修改纹理属性(
filterMode,wrapMode,anisoLevel)后调用了tex.Apply()? - 原始数据:加载的图片文件本身是否清晰?用图片查看器打开源文件确认。
- 渲染组件设置:如果是
RawImage,检查UV Rect是否设置正确,是否被意外拉伸。如果是Sprite Renderer,检查Sprite的Pixels Per Unit设置。 - Shader采样:如果是自定义Shader,检查纹理采样坐标(UV)计算是否正确,纹理声明和采样函数是否匹配纹理类型(sRGB vs Linear)。
5. 性能优化与最佳实践
在解决了正确性的问题后,我们还要关注效率。动态加载纹理很容易成为性能瓶颈。
5.1 纹理缓存池
避免同一张纹理被重复加载。建立一个简单的字典缓存。
using System.Collections.Generic; public class TextureCache { private static Dictionary<string, Texture2D> _cache = new Dictionary<string, Texture2D>(); public static Texture2D GetTexture(string key) { if (_cache.TryGetValue(key, out Texture2D tex)) { return tex; } return null; } public static void AddTexture(string key, Texture2D texture) { if (!_cache.ContainsKey(key)) { _cache.Add(key, texture); } } public static void ClearUnused() { // 可以结合Resources.UnloadUnusedAssets或自定义引用计数来清理 List<string> toRemove = new List<string>(); foreach (var kvp in _cache) { if (kvp.Value == null) // 纹理已被销毁 toRemove.Add(kvp.Key); } foreach (var key in toRemove) { _cache.Remove(key); } } }5.2 异步加载与分帧
绝不在主线程进行大型纹理的同步加载。使用UnityWebRequest、ThreadPool或JobSystem配合MonoBehaviour的协程。
IEnumerator LoadLargeTextureAsync(string path) { // 使用UnityWebRequest异步加载本地文件(需要以file://开头) string url = “file://” + path; using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url)) { AsyncOperation operation = request.SendWebRequest(); while (!operation.isDone) { // 可以在这里更新加载进度条 float progress = request.downloadProgress; yield return null; // 等待一帧 } // ... 处理纹理 } }5.3 纹理尺寸与内存管理
- 按需加载合适尺寸:不要总是加载原图。根据显示区域的大小,加载缩略图或中等尺寸的版本。可以借鉴
Image组件的RemoteImage思路,先加载小图,再异步加载大图。 - 及时销毁:当纹理不再需要时(如切换场景、关闭界面),立即调用
Destroy(texture)。对于缓存中的纹理,实现一个基于最近使用时间(LRU)的清理策略。 - 使用
Texture2D.Apply(true):在纹理加载并应用后,如果确定不再需要从CPU读取像素,调用Apply时传入makeNoLongerReadable: true,可以释放CPU端的内存副本,通常能节省等量于纹理尺寸的内存。
6. 实战案例:构建一个健壮的动态图片加载器
最后,我们整合以上所有知识点,设计一个相对健壮的DynamicTextureLoader组件。这个组件挂在UI对象上,可以处理网络和本地路径,自动处理NPOT,应用防失真设置,并带有简单的缓存和异步加载。
using UnityEngine; using UnityEngine.Networking; using System.Collections; using System.IO; [RequireComponent(typeof(RawImage))] public class DynamicTextureLoader : MonoBehaviour { public enum SourceType { Local, Web } public SourceType sourceType = SourceType.Local; public string pathOrUrl; public bool useCache = true; public bool generateMipmaps = false; public FilterMode filterMode = FilterMode.Bilinear; public bool forcePowerOfTwo = true; private static Dictionary<string, Texture2D> s_TextureCache = new Dictionary<string, Texture2D>(); private string _cacheKey; void Start() { if (!string.IsNullOrEmpty(pathOrUrl)) { LoadTexture(); } } public void LoadTexture() { _cacheKey = sourceType + “:” + pathOrUrl; if (useCache && s_TextureCache.TryGetValue(_cacheKey, out Texture2D cachedTex)) { ApplyTexture(cachedTex); return; } StartCoroutine(LoadTextureRoutine()); } IEnumerator LoadTextureRoutine() { byte[] fileData = null; if (sourceType == SourceType.Local) { string fullPath = Path.IsPathRooted(pathOrUrl) ? pathOrUrl : Path.Combine(Application.streamingAssetsPath, pathOrUrl); if (!File.Exists(fullPath)) { Debug.LogError($“本地文件不存在: {fullPath}”, this); yield break; } // 简单同步读取,实际项目应用异步文件I/O fileData = File.ReadAllBytes(fullPath); } else // Web { using (UnityWebRequest request = UnityWebRequest.Get(pathOrUrl)) { yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($“网络下载失败: {request.error}”, this); yield break; } fileData = request.downloadHandler.data; } } // 在主线程创建纹理并加载(Texture2D.LoadImage必须在主线程) Texture2D loadedTex = new Texture2D(2, 2, TextureFormat.RGBA32, generateMipmaps); if (!loadedTex.LoadImage(fileData)) { Debug.LogError(“解析图片数据失败”, this); Destroy(loadedTex); yield break; } // 后处理:强制POT Texture2D finalTex = loadedTex; if (forcePowerOfTwo && (!Mathf.IsPowerOfTwo(loadedTex.width) || !Mathf.IsPowerOfTwo(loadedTex.height))) { finalTex = ScaleTextureToPOT(loadedTex); Destroy(loadedTex); // 销毁中间纹理 } // 应用设置 finalTex.filterMode = filterMode; finalTex.wrapMode = TextureWrapMode.Clamp; finalTex.Apply(true, true); // 不生成mipmap(已在构造函数决定),且设为不可读 if (useCache) { s_TextureCache[_cacheKey] = finalTex; } ApplyTexture(finalTex); } private Texture2D ScaleTextureToPOT(Texture2D source) { // 此处省略具体实现,可参考前面章节的ScaleTextureToPOT函数 // 返回一个POT尺寸的新纹理 // 简单实现:使用Graphics.Blit方法 int targetW = Mathf.NextPowerOfTwo(source.width); int targetH = Mathf.NextPowerOfTwo(source.height); RenderTexture rt = RenderTexture.GetTemporary(targetW, targetH, 0); RenderTexture prev = RenderTexture.active; RenderTexture.active = rt; Graphics.Blit(source, rt); Texture2D result = new Texture2D(targetW, targetH, TextureFormat.RGBA32, false); result.ReadPixels(new Rect(0, 0, targetW, targetH), 0, 0); result.Apply(); RenderTexture.active = prev; RenderTexture.ReleaseTemporary(rt); return result; } private void ApplyTexture(Texture2D texture) { var rawImage = GetComponent<RawImage>(); if (rawImage != null) { rawImage.texture = texture; // 可选:根据纹理尺寸调整RawImage的RectTransform,保持比例 } } void OnDestroy() { // 组件销毁时,如果纹理不是缓存的,则销毁纹理 if (!useCache || (useCache && _cacheKey != null && !s_TextureCache.ContainsKey(_cacheKey))) { var rawImage = GetComponent<RawImage>(); if (rawImage != null && rawImage.texture != null) { // 注意:这里直接Destroy,如果纹理还被其他地方引用会有问题。 // 更健壮的做法是使用引用计数或AssetBundle管理。 Destroy(rawImage.texture); rawImage.texture = null; } } } }这个加载器只是一个起点,你可以根据项目需求扩展它,比如增加加载优先级队列、失败重试机制、加载动画、占位图替换等功能。纹理管理是Unity项目中一个细致且重要的工作,希望这篇内容能帮你理清思路,避开那些恼人的“坑”。记住,清晰的思路和系统的排查方法,比记住某个特定的API参数更重要。