Unity自动化美术资源校验工具(模型/材质规范检测)技术详解

一、工具设计背景与核心价值

1. 美术资源常见问题分类

问题类型 典型表现 影响程度
模型规范 面数超标、非法几何体 ★★★★★
材质规范 非标准Shader、纹理过大 ★★★★☆
UV问题 重叠、拉伸、缺失 ★★★☆☆
命名规范 不符合命名约定 ★★☆☆☆
引用问题 材质球丢失、纹理缺失 ★★★★☆

2. 自动化校验工具优势

  • 一致性保障:确保所有资源符合项目技术规范

  • 效率提升:将手动检查时间从小时级降至分钟级

  • 问题预防:在资源导入前发现潜在问题

  • 标准化流程:建立统一的资源质量门禁

  • 对惹,这里有一 个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀

二、系统架构设计

三、核心实现代码

1. 基础校验框架

csharp

复制代码
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

public enum ValidationSeverity
{
    Info,
    Warning,
    Error,
    Critical
}

public struct ValidationResult
{
    public string AssetPath;
    public string RuleName;
    public string Message;
    public ValidationSeverity Severity;
    public object ContextData;
    public string FixSuggestion;
}

public interface IAssetValidator
{
    string ValidatorName { get; }
    ValidationSeverity MinimumSeverity { get; }
    bool CanValidate(string assetPath);
    List<ValidationResult> Validate(string assetPath);
}

public abstract class BaseAssetValidator : IAssetValidator
{
    public abstract string ValidatorName { get; }
    public virtual ValidationSeverity MinimumSeverity => ValidationSeverity.Warning;
    
    public virtual bool CanValidate(string assetPath)
    {
        string extension = Path.GetExtension(assetPath).ToLower();
        return SupportedExtensions.Contains(extension);
    }
    
    protected abstract HashSet<string> SupportedExtensions { get; }
    
    public abstract List<ValidationResult> Validate(string assetPath);
    
    protected ValidationResult CreateResult(
        string ruleName, 
        string message, 
        ValidationSeverity severity,
        string fixSuggestion = null,
        object context = null)
    {
        return new ValidationResult
        {
            RuleName = ruleName,
            Message = message,
            Severity = severity,
            FixSuggestion = fixSuggestion,
            ContextData = context
        };
    }
}

2. 模型校验器实现

csharp

复制代码
using System.Linq;
using UnityEditor;
using UnityEngine;

public class ModelValidator : BaseAssetValidator
{
    public override string ValidatorName => "模型校验器";
    
    protected override HashSet<string> SupportedExtensions => new HashSet<string>
    {
        ".fbx", ".obj", ".blend", ".max", ".ma", ".mb"
    };
    
    public override List<ValidationResult> Validate(string assetPath)
    {
        var results = new List<ValidationResult>();
        var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
        
        if (modelImporter == null)
            return results;
        
        // 检查模型设置
        ValidateModelSettings(modelImporter, assetPath, results);
        
        // 加载模型检查网格
        GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
        if (prefab != null)
        {
            ValidateMeshData(prefab, assetPath, results);
            ValidateRenderers(prefab, assetPath, results);
            ValidateAnimations(prefab, assetPath, results);
        }
        
        return results;
    }
    
    private void ValidateModelSettings(ModelImporter importer, string path, List<ValidationResult> results)
    {
        // 检查缩放系数
        if (importer.globalScale != 1.0f)
        {
            results.Add(CreateResult(
                "ModelScale",
                $"模型缩放系数不为1.0 (当前: {importer.globalScale})",
                ValidationSeverity.Warning,
                "在Model Importer中将Scale Factor设置为1.0"
            ));
        }
        
        // 检查导入设置
        if (!importer.importBlendShapes)
        {
            results.Add(CreateResult(
                "BlendShapes",
                "模型未导入BlendShapes",
                ValidationSeverity.Info
            ));
        }
        
        // 检查是否生成碰撞体
        if (importer.addCollider)
        {
            results.Add(CreateResult(
                "AutoCollider",
                "模型自动生成碰撞体,可能影响性能",
                ValidationSeverity.Warning,
                "在Model Importer中禁用Add Colliders选项,手动添加合适的碰撞体"
            ));
        }
    }
    
    private void ValidateMeshData(GameObject model, string path, List<ValidationResult> results)
    {
        var meshFilters = model.GetComponentsInChildren<MeshFilter>();
        var skinnedMeshRenderers = model.GetComponentsInChildren<SkinnedMeshRenderer>();
        
        int totalVertices = 0;
        int totalTriangles = 0;
        int meshCount = 0;
        
        // 统计所有网格数据
        foreach (var filter in meshFilters)
        {
            if (filter.sharedMesh != null)
            {
                totalVertices += filter.sharedMesh.vertexCount;
                totalTriangles += filter.sharedMesh.triangles.Length / 3;
                meshCount++;
                
                ValidateSingleMesh(filter.sharedMesh, path, results);
            }
        }
        
        foreach (var renderer in skinnedMeshRenderers)
        {
            if (renderer.sharedMesh != null)
            {
                totalVertices += renderer.sharedMesh.vertexCount;
                totalTriangles += renderer.sharedMesh.triangles.Length / 3;
                meshCount++;
                
                ValidateSingleMesh(renderer.sharedMesh, path, results);
            }
        }
        
        // 检查总面数限制
        if (totalTriangles > 100000)
        {
            results.Add(CreateResult(
                "TriangleCount",
                $"模型面数过多: {totalTriangles} 三角形 (建议<10万)",
                ValidationSeverity.Error,
                "使用LOD或优化模型以减少面数",
                totalTriangles
            ));
        }
        else if (totalTriangles > 50000)
        {
            results.Add(CreateResult(
                "TriangleCount",
                $"模型面数较高: {totalTriangles} 三角形",
                ValidationSeverity.Warning,
                "考虑优化模型或添加LOD"
            ));
        }
        
        // 检查网格数量
        if (meshCount > 20)
        {
            results.Add(CreateResult(
                "MeshCount",
                $"模型包含过多子网格: {meshCount}个 (建议<20)",
                ValidationSeverity.Warning,
                "合并相同材质的网格以减少DrawCall"
            ));
        }
    }
    
    private void ValidateSingleMesh(Mesh mesh, string path, List<ValidationResult> results)
    {
        // 检查法线
        if (!mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.Normal))
        {
            results.Add(CreateResult(
                "MissingNormals",
                $"网格 '{mesh.name}' 缺少法线",
                ValidationSeverity.Error,
                "在3D软件中生成法线后重新导入"
            ));
        }
        
        // 检查UV
        if (!mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.TexCoord0))
        {
            results.Add(CreateResult(
                "MissingUVs",
                $"网格 '{mesh.name}' 缺少主UV通道",
                ValidationSeverity.Error,
                "在3D软件中展开UV后重新导入"
            ));
        }
        
        // 检查UV范围
        if (mesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.TexCoord0))
        {
            var uvBounds = GetUVBounds(mesh);
            if (uvBounds.min.x < 0 || uvBounds.min.y < 0 || 
                uvBounds.max.x > 1 || uvBounds.max.y > 1)
            {
                results.Add(CreateResult(
                    "UVOutOfBounds",
                    $"网格 '{mesh.name}' UV超出[0,1]范围",
                    ValidationSeverity.Warning,
                    "将UV调整到[0,1]范围内以避免纹理重复问题"
                ));
            }
        }
        
        // 检查三角形有效性
        ValidateMeshTriangles(mesh, path, results);
    }
    
    private Bounds GetUVBounds(Mesh mesh)
    {
        var uvs = mesh.uv;
        Vector2 min = new Vector2(float.MaxValue, float.MaxValue);
        Vector2 max = new Vector2(float.MinValue, float.MinValue);
        
        foreach (var uv in uvs)
        {
            min = Vector2.Min(min, uv);
            max = Vector2.Max(max, uv);
        }
        
        return new Bounds((min + max) * 0.5f, max - min);
    }
    
    private void ValidateMeshTriangles(Mesh mesh, string path, List<ValidationResult> results)
    {
        int[] triangles = mesh.triangles;
        Vector3[] vertices = mesh.vertices;
        
        for (int i = 0; i < triangles.Length; i += 3)
        {
            int i1 = triangles[i];
            int i2 = triangles[i + 1];
            int i3 = triangles[i + 2];
            
            // 检查重复顶点索引
            if (i1 == i2 || i1 == i3 || i2 == i3)
            {
                results.Add(CreateResult(
                    "DegenerateTriangle",
                    $"网格 '{mesh.name}' 包含退化三角形",
                    ValidationSeverity.Error,
                    "修复网格中的退化三角形"
                ));
                break;
            }
            
            // 检查三角形面积
            Vector3 v1 = vertices[i1];
            Vector3 v2 = vertices[i2];
            Vector3 v3 = vertices[i3];
            
            float area = Vector3.Cross(v2 - v1, v3 - v1).magnitude * 0.5f;
            if (area < 0.0001f)
            {
                results.Add(CreateResult(
                    "ZeroAreaTriangle",
                    $"网格 '{mesh.name}' 包含零面积三角形",
                    ValidationSeverity.Warning,
                    "检查并修复网格中的微小三角形"
                ));
                break;
            }
        }
    }
    
    private void ValidateRenderers(GameObject model, string path, List<ValidationResult> results)
    {
        var renderers = model.GetComponentsInChildren<Renderer>();
        
        foreach (var renderer in renderers)
        {
            // 检查材质数量
            if (renderer.sharedMaterials.Length > 1)
            {
                results.Add(CreateResult(
                    "MultipleMaterials",
                    $"渲染器 '{renderer.name}' 使用多个材质({renderer.sharedMaterials.Length})",
                    ValidationSeverity.Info,
                    "考虑合并材质以提高性能"
                ));
            }
            
            // 检查材质球引用
            foreach (var material in renderer.sharedMaterials)
            {
                if (material == null)
                {
                    results.Add(CreateResult(
                        "MissingMaterial",
                        $"渲染器 '{renderer.name}' 材质球丢失",
                        ValidationSeverity.Error,
                        "重新分配材质球"
                    ));
                }
            }
        }
    }
    
    private void ValidateAnimations(GameObject model, string path, List<ValidationResult> results)
    {
        var animator = model.GetComponent<Animator>();
        var animation = model.GetComponent<Animation>();
        
        if (animator != null)
        {
            // 检查Animator Controller
            if (animator.runtimeAnimatorController == null)
            {
                results.Add(CreateResult(
                    "MissingAnimatorController",
                    "Animator缺少Controller",
                    ValidationSeverity.Warning
                ));
            }
            
            // 检查Avatar
            if (animator.avatar == null)
            {
                results.Add(CreateResult(
                    "MissingAvatar",
                    "Animator缺少Avatar",
                    ValidationSeverity.Error,
                    "导入模型时确保已生成Avatar"
                ));
            }
        }
    }
}

3. 材质校验器实现

csharp

复制代码
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class MaterialValidator : BaseAssetValidator
{
    // 允许的Shader白名单
    private static readonly HashSet<string> AllowedShaders = new HashSet<string>
    {
        "Universal Render Pipeline/Lit",
        "Universal Render Pipeline/Simple Lit",
        "Universal Render Pipeline/Unlit",
        "HDRP/Lit",
        "HDRP/Unlit",
        "Standard",
        "Standard (Specular setup)"
    };
    
    public override string ValidatorName => "材质校验器";
    
    protected override HashSet<string> SupportedExtensions => new HashSet<string>
    {
        ".mat", ".physicmaterial", ".physicsmaterial2d"
    };
    
    public override List<ValidationResult> Validate(string assetPath)
    {
        var results = new List<ValidationResult>();
        var material = AssetDatabase.LoadAssetAtPath<Material>(assetPath);
        
        if (material == null)
            return results;
        
        ValidateShader(material, assetPath, results);
        ValidateTextures(material, assetPath, results);
        ValidateMaterialProperties(material, assetPath, results);
        
        return results;
    }
    
    private void ValidateShader(Material material, string path, List<ValidationResult> results)
    {
        string shaderName = material.shader.name;
        
        // 检查是否使用非标准Shader
        if (!AllowedShaders.Contains(shaderName))
        {
            results.Add(CreateResult(
                "NonStandardShader",
                $"材质使用非标准Shader: {shaderName}",
                ValidationSeverity.Error,
                $"更换为允许的Shader: {string.Join(", ", AllowedShaders)}",
                shaderName
            ));
        }
        
        // 检查Shader关键字
        if (material.IsKeywordEnabled("_SPECULARHIGHLIGHTS_OFF"))
        {
            results.Add(CreateResult(
                "DisabledSpecular",
                "材质禁用了高光反射",
                ValidationSeverity.Info
            ));
        }
    }
    
    private void ValidateTextures(Material material, string path, List<ValidationResult> results)
    {
        int textureCount = 0;
        int maxTextureSize = 0;
        long totalTextureSize = 0;
        
        // 获取材质所有纹理属性
        int propertyCount = ShaderUtil.GetPropertyCount(material.shader);
        for (int i = 0; i < propertyCount; i++)
        {
            if (ShaderUtil.GetPropertyType(material.shader, i) == ShaderUtil.ShaderPropertyType.TexEnv)
            {
                string propertyName = ShaderUtil.GetPropertyName(material.shader, i);
                Texture texture = material.GetTexture(propertyName);
                
                if (texture != null)
                {
                    textureCount++;
                    ValidateSingleTexture(texture, propertyName, path, results, 
                        ref maxTextureSize, ref totalTextureSize);
                }
            }
        }
        
        // 检查纹理总数
        if (textureCount > 8)
        {
            results.Add(CreateResult(
                "TooManyTextures",
                $"材质使用过多纹理: {textureCount}张 (建议≤8)",
                ValidationSeverity.Warning,
                "考虑合并纹理或使用纹理图集"
            ));
        }
        
        // 检查纹理总大小
        if (totalTextureSize > 50 * 1024 * 1024) // 50MB
        {
            results.Add(CreateResult(
                "LargeTextures",
                $"材质纹理总大小过大: {totalTextureSize / (1024*1024)}MB",
                ValidationSeverity.Warning,
                "压缩纹理或降低分辨率"
            ));
        }
    }
    
    private void ValidateSingleTexture(Texture texture, string propertyName, 
        string path, List<ValidationResult> results, 
        ref int maxTextureSize, ref long totalTextureSize)
    {
        string texturePath = AssetDatabase.GetAssetPath(texture);
        
        if (!string.IsNullOrEmpty(texturePath))
        {
            var importer = AssetImporter.GetAtPath(texturePath) as TextureImporter;
            
            if (importer != null)
            {
                // 检查纹理尺寸是否为2的幂
                bool isPowerOfTwo = Mathf.IsPowerOfTwo(texture.width) && 
                                   Mathf.IsPowerOfTwo(texture.height);
                
                if (!isPowerOfTwo)
                {
                    results.Add(CreateResult(
                        "NonPowerOfTwoTexture",
                        $"纹理 '{texture.name}' 尺寸非2的幂: {texture.width}x{texture.height}",
                        ValidationSeverity.Warning,
                        "调整纹理尺寸为2的幂以提高兼容性"
                    ));
                }
                
                // 检查纹理尺寸限制
                int maxDimension = Mathf.Max(texture.width, texture.height);
                maxTextureSize = Mathf.Max(maxTextureSize, maxDimension);
                
                if (maxDimension > 4096)
                {
                    results.Add(CreateResult(
                        "OversizedTexture",
                        $"纹理 '{texture.name}' 尺寸过大: {texture.width}x{texture.height}",
                        ValidationSeverity.Error,
                        "将纹理尺寸缩小到4096x4096以内"
                    ));
                }
                else if (maxDimension > 2048)
                {
                    results.Add(CreateResult(
                        "LargeTexture",
                        $"纹理 '{texture.name}' 尺寸较大: {texture.width}x{texture.height}",
                        ValidationSeverity.Warning,
                        "考虑使用2048x2048或更小的纹理"
                    ));
                }
                
                // 检查纹理格式
                ValidateTextureFormat(importer, texture, path, results);
                
                // 估算纹理内存大小
                totalTextureSize += EstimateTextureSize(texture, importer);
            }
        }
    }
    
    private void ValidateTextureFormat(TextureImporter importer, Texture texture, 
        string path, List<ValidationResult> results)
    {
        // 检查压缩格式
        bool isCompressed = false;
        
        #if UNITY_ANDROID
        isCompressed = importer.GetPlatformTextureSettings("Android").format == 
            TextureImporterFormat.ASTC_6x6;
        #elif UNITY_IOS
        isCompressed = importer.GetPlatformTextureSettings("iPhone").format == 
            TextureImporterFormat.ASTC_6x6;
        #else
        isCompressed = importer.textureCompression != TextureImporterCompression.Uncompressed;
        #endif
        
        if (!isCompressed)
        {
            results.Add(CreateResult(
                "UncompressedTexture",
                $"纹理 '{texture.name}' 未压缩",
                ValidationSeverity.Warning,
                "启用纹理压缩以减少内存占用"
            ));
        }
        
        // 检查Mipmap
        if (!importer.mipmapEnabled)
        {
            results.Add(CreateResult(
                "MissingMipmaps",
                $"纹理 '{texture.name}' 未生成Mipmaps",
                ValidationSeverity.Info,
                "为3D模型纹理启用Mipmaps以改善渲染质量"
            ));
        }
    }
    
    private long EstimateTextureSize(Texture texture, TextureImporter importer)
    {
        // 简化的纹理大小估算
        int mipmapFactor = importer.mipmapEnabled ? 4 : 1;
        int bpp = 4; // 假设RGBA32
        
        return (long)texture.width * texture.height * bpp * mipmapFactor;
    }
    
    private void ValidateMaterialProperties(Material material, string path, List<ValidationResult> results)
    {
        // 检查透明材质设置
        if (material.renderQueue >= 3000)
        {
            results.Add(CreateResult(
                "TransparentMaterial",
                "材质使用透明渲染队列",
                ValidationSeverity.Info,
                "确保透明材质正确排序以避免渲染问题"
            ));
        }
        
        // 检查双面渲染
        if (material.doubleSidedGI)
        {
            results.Add(CreateResult(
                "DoubleSidedGI",
                "材质启用了双面全局光照",
                ValidationSeverity.Info,
                "双面GI会增加光照计算开销"
            ));
        }
    }
}

4. 纹理校验器实现

csharp

复制代码
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class TextureValidator : BaseAssetValidator
{
    public override string ValidatorName => "纹理校验器";
    
    protected override HashSet<string> SupportedExtensions => new HashSet<string>
    {
        ".png", ".jpg", ".jpeg", ".tga", ".tif", ".tiff", ".bmp", ".psd", ".exr"
    };
    
    public override List<ValidationResult> Validate(string assetPath)
    {
        var results = new List<ValidationResult>();
        var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
        
        if (importer == null)
            return results;
        
        ValidateImportSettings(importer, assetPath, results);
        ValidateTextureType(importer, assetPath, results);
        ValidateReadability(importer, assetPath, results);
        
        return results;
    }
    
    private void ValidateImportSettings(TextureImporter importer, string path, List<ValidationResult> results)
    {
        // 检查最大尺寸设置
        int maxSize = importer.maxTextureSize;
        if (maxSize > 4096)
        {
            results.Add(CreateResult(
                "MaxSizeTooLarge",
                $"纹理最大尺寸设置过大: {maxSize}",
                ValidationSeverity.Warning,
                "根据实际使用情况调整最大尺寸"
            ));
        }
        
        // 检查非2的幂处理策略
        if (importer.npotScale != TextureImporterNPOTScale.ToNearest)
        {
            results.Add(CreateResult(
                "NPOTHandling",
                $"非2的幂纹理处理方式为: {importer.npotScale}",
                ValidationSeverity.Info,
                "建议使用ToNearest确保纹理尺寸为2的幂"
            ));
        }
        
        // 检查Alpha通道处理
        bool hasAlpha = importer.DoesSourceTextureHaveAlpha();
        if (hasAlpha && importer.alphaSource == TextureImporterAlphaSource.None)
        {
            results.Add(CreateResult(
                "AlphaChannelIgnored",
                "纹理包含Alpha通道但导入设置忽略Alpha",
                ValidationSeverity.Warning,
                "如果不需要Alpha通道,建议在源文件中移除"
            ));
        }
    }
    
    private void ValidateTextureType(TextureImporter importer, string path, List<ValidationResult> results)
    {
        // 检查纹理类型设置
        switch (importer.textureType)
        {
            case TextureImporterType.Default:
                // 默认纹理检查
                if (importer.textureShape != TextureImporterShape.Texture2D)
                {
                    results.Add(CreateResult(
                        "Non2DTexture",
                        "纹理不是2D类型",
                        ValidationSeverity.Warning,
                        "2D纹理通常性能更好"
                    ));
                }
                break;
                
            case TextureImporterType.NormalMap:
                // 法线贴图检查
                if (importer.convertToNormalMap)
                {
                    results.Add(CreateResult(
                        "AutoNormalMap",
                        "纹理设置为自动转换为法线贴图",
                        ValidationSeverity.Info,
                        "确保源图像适合做法线贴图"
                    ));
                }
                break;
                
            case TextureImporterType.Sprite:
                // Sprite检查
                var spriteSettings = importer.spritePixelsPerUnit;
                if (spriteSettings != 100)
                {
                    results.Add(CreateResult(
                        "SpritePPU",
                        $"Sprite Pixels Per Unit不是标准值: {spriteSettings}",
                        ValidationSeverity.Info,
                        "建议使用100作为标准值"
                    ));
                }
                break;
        }
    }
    
    private void ValidateReadability(TextureImporter importer, string path, List<ValidationResult> results)
    {
        // 检查是否启用Read/Write
        if (importer.isReadable)
        {
            results.Add(CreateResult(
                "TextureReadable",
                "纹理启用了Read/Write选项",
                ValidationSeverity.Warning,
                "除非需要CPU访问纹理数据,否则禁用此选项以减少内存占用"
            ));
        }
        
        // 检查Streaming Mipmaps
        if (importer.streamingMipmaps)
        {
            results.Add(CreateResult(
                "StreamingMipmaps",
                "纹理启用了Streaming Mipmaps",
                ValidationSeverity.Info,
                "适合大型纹理,但可能增加加载时间"
            ));
        }
    }
}

5. 编辑器界面实现

csharp

复制代码
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

public class AssetValidationWindow : EditorWindow
{
    [MenuItem("Tools/美术资源校验工具")]
    public static void ShowWindow()
    {
        GetWindow<AssetValidationWindow>("资源校验工具");
    }
    
    // 校验器列表
    private List<IAssetValidator> validators = new List<IAssetValidator>
    {
        new ModelValidator(),
        new MaterialValidator(),
        new TextureValidator()
    };
    
    // UI状态
    private string targetFolder = "Assets/";
    private bool isScanning = false;
    private Vector2 scrollPosition;
    private ValidationSeverity minimumSeverity = ValidationSeverity.Warning;
    
    // 结果存储
    private List<ValidationResult> allResults = new List<ValidationResult>();
    private Dictionary<string, List<ValidationResult>> resultsByAsset = 
        new Dictionary<string, List<ValidationResult>>();
    
    // 树状视图
    private TreeViewState treeViewState;
    private ValidationTreeView validationTreeView;
    private SearchField searchField;
    private string searchString = "";
    
    private void OnEnable()
    {
        // 初始化树状视图
        treeViewState = new TreeViewState();
        validationTreeView = new ValidationTreeView(treeViewState);
        searchField = new SearchField();
    }
    
    private void OnGUI()
    {
        DrawToolbar();
        DrawFolderSelection();
        DrawSeverityFilter();
        
        EditorGUILayout.Space();
        
        if (isScanning)
        {
            EditorGUILayout.HelpBox("正在扫描资源...", MessageType.Info);
            return;
        }
        
        if (allResults.Count == 0)
        {
            EditorGUILayout.HelpBox("选择文件夹并点击扫描按钮开始校验", MessageType.Info);
        }
        else
        {
            DrawResultsSummary();
            DrawResultsTree();
            DrawActionButtons();
        }
    }
    
    private void DrawToolbar()
    {
        GUILayout.BeginHorizontal(EditorStyles.toolbar);
        
        if (GUILayout.Button("扫描选中文件夹", EditorStyles.toolbarButton, GUILayout.Width(120)))
        {
            StartValidation();
        }
        
        if (GUILayout.Button("扫描整个项目", EditorStyles.toolbarButton, GUILayout.Width(120)))
        {
            targetFolder = "Assets";
            StartValidation();
        }
        
        GUILayout.FlexibleSpace();
        
        if (GUILayout.Button("导出报告", EditorStyles.toolbarButton, GUILayout.Width(80)))
        {
            ExportReport();
        }
        
        if (GUILayout.Button("自动修复", EditorStyles.toolbarButton, GUILayout.Width(80)))
        {
            AutoFixIssues();
        }
        
        GUILayout.EndHorizontal();
    }
    
    private void DrawFolderSelection()
    {
        EditorGUILayout.BeginHorizontal();
        
        EditorGUILayout.LabelField("目标文件夹:", GUILayout.Width(80));
        EditorGUILayout.LabelField(targetFolder, EditorStyles.textField);
        
        if (GUILayout.Button("选择...", GUILayout.Width(60)))
        {
            string newFolder = EditorUtility.OpenFolderPanel("选择文件夹", targetFolder, "");
            if (!string.IsNullOrEmpty(newFolder))
            {
                if (newFolder.StartsWith(Application.dataPath))
                {
                    targetFolder = "Assets" + newFolder.Substring(Application.dataPath.Length);
                }
                else
                {
                    EditorUtility.DisplayDialog("错误", "请选择项目内的文件夹", "确定");
                }
            }
        }
        
        EditorGUILayout.EndHorizontal();
    }
    
    private void DrawSeverityFilter()
    {
        EditorGUILayout.BeginHorizontal();
        
        EditorGUILayout.LabelField("最低严重等级:", GUILayout.Width(100));
        minimumSeverity = (ValidationSeverity)EditorGUILayout.EnumPopup(minimumSeverity, GUILayout.Width(100));
        
        EditorGUILayout.EndHorizontal();
    }
    
    private void DrawResultsSummary()
    {
        int errorCount = allResults.Count(r => r.Severity == ValidationSeverity.Error || 
                                               r.Severity == ValidationSeverity.Critical);
        int warningCount = allResults.Count(r => r.Severity == ValidationSeverity.Warning);
        int infoCount = allResults.Count(r => r.Severity == ValidationSeverity.Info);
        
        GUILayout.BeginHorizontal();
        
        EditorGUILayout.HelpBox(
            $"扫描完成! 发现 {allResults.Count} 个问题 " +
            $"(错误: {errorCount}, 警告: {warningCount}, 信息: {infoCount})",
            errorCount > 0 ? MessageType.Error : 
            warningCount > 0 ? MessageType.Warning : MessageType.Info
        );
        
        GUILayout.EndHorizontal();
    }
    
    private void DrawResultsTree()
    {
        // 搜索框
        Rect searchRect = GUILayoutUtility.GetRect(1, 1, 20, 20);
        searchString = searchField.OnGUI(searchRect, searchString);
        
        // 树状视图
        Rect treeRect = GUILayoutUtility.GetRect(1, 1, 200, 1000);
        validationTreeView.searchString = searchString;
        validationTreeView.OnGUI(treeRect);
    }
    
    private void DrawActionButtons()
    {
        GUILayout.BeginHorizontal();
        
        if (GUILayout.Button("选择所有错误", GUILayout.Width(120)))
        {
            SelectAllAssetsWithSeverity(ValidationSeverity.Error);
        }
        
        if (GUILayout.Button("选择所有警告", GUILayout.Width(120)))
        {
            SelectAllAssetsWithSeverity(ValidationSeverity.Warning);
        }
        
        GUILayout.EndHorizontal();
    }
    
    private void StartValidation()
    {
        isScanning = true;
        
        // 清空之前的结果
        allResults.Clear();
        resultsByAsset.Clear();
        
        // 开始异步扫描
        EditorApplication.delayCall += () =>
        {
            ScanAssetsRecursive(targetFolder);
            BuildTreeView();
            isScanning = false;
            Repaint();
        };
    }
    
    private void ScanAssetsRecursive(string folderPath)
    {
        // 获取文件夹内所有资源
        string[] assetPaths = Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories)
            .Where(p => !p.EndsWith(".meta"))
            .ToArray();
        
        int total = assetPaths.Length;
        int current = 0;
        
        foreach (var assetPath in assetPaths)
        {
            current++;
            
            // 显示进度
            if (EditorUtility.DisplayCancelableProgressBar(
                "扫描资源", 
                $"正在扫描: {Path.GetFileName(assetPath)}", 
                (float)current / total))
            {
                break;
            }
            
            ValidateAsset(assetPath);
        }
        
        EditorUtility.ClearProgressBar();
    }
    
    private void ValidateAsset(string assetPath)
    {
        var assetResults = new List<ValidationResult>();
        
        foreach (var validator in validators)
        {
            if (validator.CanValidate(assetPath) && validator.MinimumSeverity <= minimumSeverity)
            {
                var results = validator.Validate(assetPath);
                if (results != null)
                {
                    // 过滤低于最小严重等级的结果
                    var filteredResults = results.Where(r => r.Severity >= minimumSeverity).ToList();
                    assetResults.AddRange(filteredResults);
                }
            }
        }
        
        if (assetResults.Count > 0)
        {
            allResults.AddRange(assetResults);
            resultsByAsset[assetPath] = assetResults;
        }
    }
    
    private void BuildTreeView()
    {
        // 构建树状视图的数据
        var treeData = new List<ValidationTreeElement>();
        int id = 0;
        
        // 按严重等级分组
        var groupedBySeverity = allResults
            .GroupBy(r => r.Severity)
            .OrderByDescending(g => g.Key);
        
        foreach (var severityGroup in groupedBySeverity)
        {
            string severityName = GetSeverityDisplayName(severityGroup.Key);
            Color severityColor = GetSeverityColor(severityGroup.Key);
            
            var severityItem = new ValidationTreeElement
            {
                id = id++,
                name = $"{severityName} ({severityGroup.Count()})",
                depth = 0,
                severity = severityGroup.Key,
                assetPath = null
            };
            
            treeData.Add(severityItem);
            
            // 按资源分组
            var groupedByAsset = severityGroup.GroupBy(r => r.AssetPath);
            
            foreach (var assetGroup in groupedByAsset)
            {
                var assetItem = new ValidationTreeElement
                {
                    id = id++,
                    name = Path.GetFileName(assetGroup.Key),
                    depth = 1,
                    severity = severityGroup.Key,
                    assetPath = assetGroup.Key
                };
                
                treeData.Add(assetItem);
                
                // 添加具体问题
                foreach (var result in assetGroup)
                {
                    var problemItem = new ValidationTreeElement
                    {
                        id = id++,
                        name = $"{result.RuleName}: {result.Message}",
                        depth = 2,
                        severity = result.Severity,
                        assetPath = assetGroup.Key,
                        validationResult = result
                    };
                    
                    treeData.Add(problemItem);
                }
            }
        }
        
        validationTreeView.SetData(treeData);
    }
    
    private string GetSeverityDisplayName(ValidationSeverity severity)
    {
        return severity switch
        {
            ValidationSeverity.Critical => "严重错误",
            ValidationSeverity.Error => "错误",
            ValidationSeverity.Warning => "警告",
            ValidationSeverity.Info => "信息",
            _ => "未知"
        };
    }
    
    private Color GetSeverityColor(ValidationSeverity severity)
    {
        return severity switch
        {
            ValidationSeverity.Critical => Color.red,
            ValidationSeverity.Error => new Color(1, 0.5f, 0), // 橙色
            ValidationSeverity.Warning => Color.yellow,
            ValidationSeverity.Info => Color.white,
            _ => Color.gray
        };
    }
    
    private void SelectAllAssetsWithSeverity(ValidationSeverity severity)
    {
        var assets = allResults
            .Where(r => r.Severity == severity)
            .Select(r => r.AssetPath)
            .Distinct()
            .ToArray();
        
        var assetObjects = assets
            .Select(p => AssetDatabase.LoadAssetAtPath<Object>(p))
            .Where(o => o != null)
            .ToArray();
        
        Selection.objects = assetObjects;
    }
    
    private void ExportReport()
    {
        string reportPath = EditorUtility.SaveFilePanel(
            "导出报告", 
            Application.dataPath, 
            $"AssetValidationReport_{System.DateTime.Now:yyyyMMdd_HHmmss}", 
            "csv");
        
        if (string.IsNullOrEmpty(reportPath))
            return;
        
        using (var writer = new StreamWriter(reportPath))
        {
            // 写入CSV标题
            writer.WriteLine("资源路径,严重等级,规则名称,问题描述,修复建议");
            
            // 写入数据
            foreach (var result in allResults.OrderByDescending(r => r.Severity))
            {
                writer.WriteLine($"\"{result.AssetPath}\"," +
                               $"\"{result.Severity}\"," +
                               $"\"{result.RuleName}\"," +
                               $"\"{result.Message}\"," +
                               $"\"{result.FixSuggestion}\"");
            }
        }
        
        EditorUtility.RevealInFinder(reportPath);
    }
    
    private void AutoFixIssues()
    {
        // 实现自动修复逻辑
        int fixedCount = 0;
        
        foreach (var assetPath in resultsByAsset.Keys)
        {
            var results = resultsByAsset[assetPath];
            
            foreach (var result in results)
            {
                if (TryAutoFix(result, assetPath))
                {
                    fixedCount++;
                }
            }
        }
        
        EditorUtility.DisplayDialog("自动修复完成", 
            $"已尝试修复 {fixedCount} 个问题", "确定");
        
        // 重新扫描验证修复结果
        StartValidation();
    }
    
    private bool TryAutoFix(ValidationResult result, string assetPath)
    {
        // 根据规则名称实现具体的自动修复逻辑
        switch (result.RuleName)
        {
            case "ModelScale":
                var modelImporter = AssetImporter.GetAtPath(assetPath) as ModelImporter;
                if (modelImporter != null)
                {
                    modelImporter.globalScale = 1.0f;
                    modelImporter.SaveAndReimport();
                    return true;
                }
                break;
                
            case "TextureReadable":
                var textureImporter = AssetImporter.GetAtPath(assetPath) as TextureImporter;
                if (textureImporter != null && textureImporter.isReadable)
                {
                    textureImporter.isReadable = false;
                    textureImporter.SaveAndReimport();
                    return true;
                }
                break;
                
            // 添加更多自动修复规则...
        }
        
        return false;
    }
}

// 树状视图元素
public class ValidationTreeElement
{
    public int id;
    public string name;
    public int depth;
    public ValidationSeverity severity;
    public string assetPath;
    public ValidationResult validationResult;
}

// 树状视图实现
public class ValidationTreeView : TreeView
{
    private List<ValidationTreeElement> treeElements = new List<ValidationTreeElement>();
    
    public ValidationTreeView(TreeViewState state) : base(state)
    {
        showAlternatingRowBackgrounds = true;
        showBorder = true;
    }
    
    public void SetData(List<ValidationTreeElement> elements)
    {
        treeElements = elements;
        Reload();
    }
    
    protected override TreeViewItem BuildRoot()
    {
        var root = new TreeViewItem { id = -1, depth = -1, displayName = "Root" };
        
        if (treeElements.Count == 0)
        {
            var item = new TreeViewItem { id = 0, depth = 0, displayName = "无校验结果" };
            root.AddChild(item);
        }
        else
        {
            foreach (var element in treeElements)
            {
                var item = new TreeViewItem 
                { 
                    id = element.id, 
                    depth = element.depth, 
                    displayName = element.name 
                };
                root.AddChild(item);
            }
        }
        
        SetupDepthsFromParentsAndChildren(root);
        return root;
    }
    
    protected override void RowGUI(RowGUIArgs args)
    {
        var element = treeElements.FirstOrDefault(e => e.id == args.item.id);
        
        if (element != null)
        {
            // 根据严重等级设置颜色
            Color color = GetSeverityColor(element.severity);
            
            using (new GUILayout.HorizontalScope())
            {
                // 绘制严重等级图标
                Rect iconRect = args.rowRect;
                iconRect.width = 20;
                
                if (Event.current.type == EventType.Repaint)
                {
                    EditorGUI.DrawRect(iconRect, color);
                }
                
                // 绘制文本
                Rect labelRect = args.rowRect;
                labelRect.x += 25;
                labelRect.width -= 25;
                
                GUI.Label(labelRect, args.item.displayName);
                
                // 如果是具体问题,显示修复按钮
                if (element.validationResult != null && !string.IsNullOrEmpty(element.validationResult.FixSuggestion))
                {
                    Rect buttonRect = labelRect;
                    buttonRect.x = buttonRect.xMax - 80;
                    buttonRect.width = 80;
                    
                    if (GUI.Button(buttonRect, "修复"))
                    {
                        ShowFixDialog(element.validationResult, element.assetPath);
                    }
                }
            }
        }
        else
        {
            base.RowGUI(args);
        }
    }
    
    private Color GetSeverityColor(ValidationSeverity severity)
    {
        return severity switch
        {
            ValidationSeverity.Critical => Color.red,
            ValidationSeverity.Error => new Color(1, 0.5f, 0),
            ValidationSeverity.Warning => Color.yellow,
            ValidationSeverity.Info => Color.cyan,
            _ => Color.gray
        };
    }
    
    protected override void DoubleClickedItem(int id)
    {
        var element = treeElements.FirstOrDefault(e => e.id == id);
        
        if (element != null && !string.IsNullOrEmpty(element.assetPath))
        {
            // 在Project窗口中选择资源
            var asset = AssetDatabase.LoadAssetAtPath<Object>(element.assetPath);
            if (asset != null)
            {
                Selection.activeObject = asset;
                EditorGUIUtility.PingObject(asset);
            }
        }
    }
    
    private void ShowFixDialog(ValidationResult result, string assetPath)
    {
        if (EditorUtility.DisplayDialog("修复问题", 
            $"问题: {result.Message}\n\n建议修复方式: {result.FixSuggestion}", 
            "自动修复", "手动修复"))
        {
            // 尝试自动修复
            if (!TryAutoFix(result, assetPath))
            {
                EditorUtility.DisplayDialog("无法自动修复", 
                    "此问题无法自动修复,请手动处理", "确定");
            }
        }
        else
        {
            // 选中资源让用户手动修复
            var asset = AssetDatabase.LoadAssetAtPath<Object>(assetPath);
            if (asset != null)
            {
                Selection.activeObject = asset;
                EditorGUIUtility.PingObject(asset);
            }
        }
    }
    
    private bool TryAutoFix(ValidationResult result, string assetPath)
    {
        // 这里可以调用主窗口的自动修复逻辑
        return false;
    }
}

四、高级功能扩展

1. 自定义规则系统

csharp

复制代码
using System;
using UnityEngine;

[CreateAssetMenu(fileName = "ValidationRule", menuName = "Tools/校验规则")]
public class ValidationRule : ScriptableObject
{
    public string RuleId;
    public string DisplayName;
    public ValidationSeverity Severity;
    
    [TextArea(3, 10)]
    public string Description;
    
    [TextArea(2, 5)]
    public string FixSuggestion;
    
    // 条件判断
    public RuleCondition[] Conditions;
    
    // 适用于的资源类型
    public AssetType TargetAssetType;
    
    public enum AssetType
    {
        Model,
        Material,
        Texture,
        Animation,
        Prefab,
        Any
    }
    
    [Serializable]
    public struct RuleCondition
    {
        public ConditionType Type;
        public string PropertyPath;
        public CompareOperator Operator;
        public string Value;
        
        public enum ConditionType
        {
            PropertyValue,
            FileSize,
            Dimension,
            Count
        }
        
        public enum CompareOperator
        {
            Equal,
            NotEqual,
            GreaterThan,
            LessThan,
            Contains,
            StartsWith,
            EndsWith
        }
    }
}

2. 批量处理工具

csharp

复制代码
using System.Threading.Tasks;
using UnityEditor;

public class BatchAssetProcessor
{
    public async Task ProcessFolderAsync(string folderPath, System.Action<string> onProgress)
    {
        var assetPaths = AssetDatabase.FindAssets("", new[] { folderPath })
            .Select(AssetDatabase.GUIDToAssetPath)
            .Where(p => !p.EndsWith(".meta"))
            .ToArray();
        
        int total = assetPaths.Length;
        int processed = 0;
        
        foreach (var assetPath in assetPaths)
        {
            processed++;
            onProgress?.Invoke($"处理中 ({processed}/{total}): {assetPath}");
            
            await ProcessAssetAsync(assetPath);
            
            if (processed % 10 == 0)
            {
                // 每处理10个资源保存一次,避免数据丢失
                AssetDatabase.SaveAssets();
            }
        }
        
        onProgress?.Invoke("批量处理完成");
    }
    
    private async Task ProcessAssetAsync(string assetPath)
    {
        // 模拟异步处理
        await Task.Delay(10);
        
        // 实际处理逻辑
        // ...
    }
}

3. CI/CD集成

csharp

复制代码
using UnityEditor;
using UnityEditor.Build.Reporting;

public class ValidationBuildPreprocess : IPreprocessBuildWithReport
{
    public int callbackOrder => 0;
    
    public void OnPreprocessBuild(BuildReport report)
    {
        var validator = new AssetValidationSystem();
        var results = validator.ValidateAllAssets();
        
        int errorCount = results.Count(r => 
            r.Severity == ValidationSeverity.Error || 
            r.Severity == ValidationSeverity.Critical);
        
        if (errorCount > 0)
        {
            string errorMessage = $"发现 {errorCount} 个资源错误,构建中止。\n";
            errorMessage += string.Join("\n", 
                results.Where(r => r.Severity >= ValidationSeverity.Error)
                       .Select(r => $"- {r.AssetPath}: {r.Message}"));
            
            throw new BuildFailedException(errorMessage);
        }
        
        int warningCount = results.Count(r => r.Severity == ValidationSeverity.Warning);
        if (warningCount > 0)
        {
            Debug.LogWarning($"构建前发现 {warningCount} 个资源警告");
        }
    }
}

五、最佳实践建议

1. 项目配置策略

yaml

复制代码
# Assets/Editor/AssetValidationConfig.yaml
validation_rules:
  models:
    max_triangles: 100000
    max_meshes: 20
    require_normals: true
    require_uv1: false
    
  materials:
    allowed_shaders:
      - "Universal Render Pipeline/Lit"
      - "Universal Render Pipeline/Simple Lit"
    max_textures: 8
    max_texture_size: 4096
    
  textures:
    require_power_of_two: true
    max_dimension: 4096
    require_compression: true
    
  file_naming:
    pattern: "^[A-Z][a-zA-Z0-9_]*$"
    prefix_mapping:
      material: "M_"
      texture: "T_"
      model: "MDL_"

2. 性能优化建议

场景 优化策略 效果
大型项目 增量校验,只检查修改的资源 扫描时间减少80%
频繁校验 缓存校验结果 重复校验时间减少95%
批量处理 并行校验 处理速度提升3-5倍

六、完整项目结构

text

复制代码
Assets/
├── Editor/
│   ├── AssetValidation/
│   │   ├── Core/
│   │   │   ├── ValidationFramework.cs
│   │   │   ├── AssetValidator.cs
│   │   │   └── ValidationResult.cs
│   │   ├── Validators/
│   │   │   ├── ModelValidator.cs
│   │   │   ├── MaterialValidator.cs
│   │   │   ├── TextureValidator.cs
│   │   │   └── AnimationValidator.cs
│   │   ├── UI/
│   │   │   ├── AssetValidationWindow.cs
│   │   │   ├── ValidationTreeView.cs
│   │   │   └── ValidationReportView.cs
│   │   ├── Rules/
│   │   │   ├── ValidationRule.cs
│   │   │   └── RuleLibrary.cs
│   │   └── Utilities/
│   │       ├── BatchProcessor.cs
│   │       └── AutoFixSystem.cs
│   └── AssetValidationMenu.cs
├── Configs/
│   └── ValidationRules.asset
└── Documentation/
    └── AssetValidationGuide.md

总结

通过本文实现的自动化美术资源校验工具,项目团队可以获得以下收益:

  1. 质量保障:确保所有美术资源符合项目技术规范

  2. 效率提升:自动化检查替代繁琐的手动验证

  3. 知识沉淀:将技术规范转化为可执行的检查规则

  4. 流程标准化:建立统一的资源质量门禁流程

工具的核心优势在于其可扩展性,通过实现IAssetValidator接口,可以轻松添加新的校验规则,适应不同项目的特定需求。同时,可视化界面和自动化修复功能大大降低了使用门槛,使美术和QA团队都能有效利用该工具。

建议将工具集成到日常开发流程中,如在资源导入时自动校验、在版本提交前强制检查、在CI/CD流水线中作为质量门禁等,从而全面提升项目资源质量。

相关推荐
自动化控制仿真经验汇总8 小时前
Simulink 程序状态声音提示方案-PART-蜂鸣-程序提示
自动化
Sator19 小时前
Unity烘焙光打包后光照丢失问题
unity·光照贴图
m0_6948455711 小时前
网站账号太多难管理?Enterr 开源自动化工具搭建教程
运维·服务器·前端·开源·自动化·云计算
青槿吖11 小时前
第二篇:JDBC进阶骚操作:防注入、事务回滚、连接池优化,一篇封神
java·开发语言·jvm·算法·自动化
历程里程碑14 小时前
Linux 10:make Makefile自动化编译实战指南及进度条解析
linux·运维·服务器·开发语言·c++·笔记·自动化
Aloudata15 小时前
EAST 口径文档自动化生成:破解 SQL 过滤条件解析难题,实现 20 倍效率提升
sql·自动化·数据治理·元数据·数据血缘
缺点内向16 小时前
Word 自动化处理:如何用 C# 让指定段落“隐身”?
开发语言·c#·自动化·word·.net
0思必得016 小时前
[Web自动化] 数据抓取、解析与存储
运维·前端·爬虫·selenium·自动化·web自动化