27.10.2026 18 мин чтения Просмотров:0

AssetPostprocessor в Unity: правила импорта текстур, моделей и звука

Пишем на C# постпроцессор, который сам настраивает импорт текстур, FBX и звука по папкам и суффиксам, не затирает ручные правки и переимпортирует ассеты через GetVersion.

AssetPostprocessor в Unity: правила импорта текстур, моделей и звука

В этой статье соберём на C# систему, которая сама выставляет настройки импорта для текстур, моделей и звука: по папке, по суффиксу в имени файла и по платформе. Разберём, какие методы AssetPostprocessor вызываются и когда, почему настройки нужно менять в Preprocess, а не в Postprocess, как не затереть ручные правки художника, как заставить Unity переимпортировать ассеты после изменения правил и какие ошибки чаще всего превращают постпроцессор в бесконечный цикл переимпорта.

Зачем автоматизировать импорт

В любом проекте старше пары месяцев появляется одна и та же проблема: настройки импорта расходятся. Кто-то перетащил в проект UI-иконки и забыл выключить мипмапы. Кто-то импортировал карту нормалей как обычную текстуру, и освещение на модели «поплыло». Фоновая музыка на три минуты висит в памяти целиком в режиме Decompress On Load, а у FBX-пропсов включён импорт камер, источников света и материалов, которые никому не нужны. Каждая такая мелочь стоит мегабайты в билде и минуты на поиск.

Ручные чек-листы не работают: их забывают. Пресеты в Preset Manager помогают, но они применяются только при первом импорте и плохо выражают логику вида «всё в папке UI — спрайты без мипмапов, а если имя кончается на _9s — ещё и с полной сеткой меша». Код справляется с этим лучше: правила лежат в репозитории, проходят ревью и одинаково работают у всей команды.

Инструмент для этого в Unity — класс UnityEditor.AssetPostprocessor. Достаточно унаследоваться от него и объявить методы с нужными именами: редактор найдёт их через рефлексию и вызовет в нужный момент импорта.

Как устроен AssetPostprocessor

Постпроцессор — это редакторский скрипт, поэтому он должен лежать в папке Editor или в сборке asmdef, у которой из платформ выбран только Editor. Иначе билд упадёт на ссылках на UnityEditor.

Минимальный пример:

using UnityEditor;

public class TextureImportRules : AssetPostprocessor
{
    void OnPreprocessTexture()
    {
        var importer = (TextureImporter)assetImporter;
        if (assetPath.StartsWith("Assets/Art/UI/"))
        {
            importer.textureType = TextureImporterType.Sprite;
            importer.mipmapEnabled = false;
        }
    }
}

Внутри методов доступны три свойства базового класса:

  • assetPath — путь импортируемого ассета от корня проекта, например Assets/Art/UI/button.png. Разделитель всегда прямой слеш, даже на Windows.
  • assetImporter — импортёр этого ассета. Для текстуры это TextureImporter, для FBX — ModelImporter, для звука — AudioImporter.
  • context — объект AssetImportContext, через который можно объявить зависимости импорта и писать ошибки и предупреждения, привязанные к ассету.

Для каждого импорта Unity создаёт новый экземпляр постпроцессора, так что хранить состояние в полях между ассетами не получится. И не нужно: о причине ниже, в разделе про параллельный импорт.

Preprocess и Postprocess: в чём разница

Методы делятся на две группы, и путать их — самая частая ошибка.

  • OnPreprocess* вызываются до того, как импортёр начнёт работу. Здесь меняют настройки импортёра: тип текстуры, сжатие, режим загрузки звука, импорт анимаций. Настройки, выставленные здесь, сразу участвуют в текущем импорте.
  • OnPostprocess* вызываются, когда результат уже получен. Здесь работают с результатом: правят пиксели текстуры до сжатия, переименовывают меши, добавляют компоненты в иерархию модели, проверяют размеры и пишут предупреждения.

Если поменять настройки импортёра в Postprocess, они в текущем импорте уже не учтутся, а .meta-файл окажется изменён. В лучшем случае ассет переимпортируется ещё раз, в худшем — получится цикл, который не даёт редактору выйти из импорта.

Основные методы, которые пригодятся на практике:

  • OnPreprocessTexture() и OnPostprocessTexture(Texture2D texture) — текстуры; второй вызывается до сжатия.
  • OnPostprocessSprites(Texture2D texture, Sprite[] sprites) — после нарезки спрайтов.
  • OnPreprocessModel() и OnPostprocessModel(GameObject root) — модели.
  • OnPreprocessAnimation() и OnPostprocessAnimation(GameObject root, AnimationClip clip) — анимации из моделей.
  • OnPostprocessMaterial(Material material) — материалы, созданные при импорте модели.
  • OnPreprocessAudio() и OnPostprocessAudio(AudioClip clip) — звук.
  • OnPreprocessAsset() — вызывается для любого ассета перед импортом.
  • статический OnPostprocessAllAssets(...) — после завершения пачки импортов, с массивами импортированных, удалённых и перемещённых путей.

Главное правило: не затирать ручные правки

Наивный постпроцессор из примера выше выставляет настройки при каждом импорте. Художник открывает текстуру, включает мипмапы для конкретной иконки, которая рисуется в мире, жмёт Apply — Unity переимпортирует текстуру, постпроцессор снова выключает мипмапы. Правка исчезает молча, и через пару таких случаев команда перестаёт доверять инструменту.

Есть два честных подхода.

Применять правила только при первом импорте. У импортёра есть свойство importSettingsMissing: оно равно true, когда у ассета ещё нет сохранённых настроек, то есть .meta-файл только создаётся. Это поведение «умных значений по умолчанию»: файл попадает в проект сразу правильно настроенным, а дальше человек вправе всё поменять.

void OnPreprocessTexture()
{
    if (!assetImporter.importSettingsMissing)
        return;

    // правила применяются один раз, при появлении файла в проекте
}

Применять правила всегда, но только к тому, что правила действительно контролируют. Например, для папки UI жёстко держать «спрайт, без мипмапов», а размер и сжатие оставить на усмотрение человека. Это строгий режим: подходит для настроек, которые ломают билд или бюджет памяти, если их нарушить.

На практике удобно совмещать: базовые настройки — только при первом импорте, критичные ограничения (например, максимальный размер текстуры на мобильных) — всегда.

Правила для текстур

Соберём реалистичный набор правил. Договоримся о структуре папок и суффиксах:

  • Assets/Art/UI/ — спрайты интерфейса;
  • суффикс _n или _normal — карта нормалей;
  • суффиксы _mask, _rough, _ao, _mt — линейные данные, sRGB нужно выключить;
  • для Android и iOS — ASTC и ограничение размера.
using System.IO;
using UnityEditor;
using UnityEngine;

public class TextureImportRules : AssetPostprocessor
{
    const string UiFolder = "Assets/Art/UI/";

    static readonly string[] NormalSuffixes = { "_n", "_normal" };
    static readonly string[] LinearSuffixes = { "_mask", "_rough", "_ao", "_mt" };

    public override uint GetVersion() => 3;

    void OnPreprocessTexture()
    {
        var importer = (TextureImporter)assetImporter;
        string name = Path.GetFileNameWithoutExtension(assetPath).ToLowerInvariant();

        if (importer.importSettingsMissing)
            ApplyDefaults(importer, name);

        // строгие ограничения: применяются при каждом импорте
        LimitMobileSize(importer, "Android", 2048);
        LimitMobileSize(importer, "iPhone", 2048);
    }

    void ApplyDefaults(TextureImporter importer, string name)
    {
        if (assetPath.StartsWith(UiFolder))
        {
            importer.textureType = TextureImporterType.Sprite;
            importer.spriteImportMode = SpriteImportMode.Single;
            importer.mipmapEnabled = false;
            importer.alphaIsTransparency = true;
            importer.wrapMode = TextureWrapMode.Clamp;
            return;
        }

        if (EndsWithAny(name, NormalSuffixes))
        {
            importer.textureType = TextureImporterType.NormalMap;
            return;
        }

        if (EndsWithAny(name, LinearSuffixes))
        {
            importer.textureType = TextureImporterType.Default;
            importer.sRGBTexture = false;
        }
    }

    static void LimitMobileSize(TextureImporter importer, string platform, int maxSize)
    {
        var settings = importer.GetPlatformTextureSettings(platform);
        if (!settings.overridden)
            settings.format = TextureImporterFormat.ASTC_6x6;
        settings.overridden = true;
        settings.maxTextureSize = Mathf.Min(settings.maxTextureSize, maxSize);
        importer.SetPlatformTextureSettings(settings);
    }

    static bool EndsWithAny(string value, string[] suffixes)
    {
        foreach (var suffix in suffixes)
            if (value.EndsWith(suffix))
                return true;
        return false;
    }
}

Несколько моментов, на которые стоит обратить внимание.

Имена платформ в GetPlatformTextureSettings — строки: "Standalone", "Android", "iPhone" (именно так, не "iOS"), "WebGL". Метод возвращает копию настроек, поэтому изменения обязательно нужно записать обратно через SetPlatformTextureSettings, иначе они пропадут.

Ограничение размера сделано через Mathf.Min: если художник сам выставил 512, постпроцессор не поднимет его до 2048. Формат ASTC ставится только когда платформенный оверрайд ещё не включён, поэтому выбранный вручную формат тоже сохраняется. Строгое правило ограничивает сверху, а не навязывает конкретное значение.

Суффиксы сравниваются по имени без расширения и в нижнем регистре. Сравнение по полному пути вида assetPath.Contains("_n") — классическая ошибка: под него попадёт и Assets/Art/Env_nature/rock.png.

Проверки в OnPostprocessTexture

Postprocess удобен для проверок, которые нельзя сделать до импорта, потому что нужны реальные размеры картинки. Например, предупредить о текстурах, стороны которых не кратны четырём: блочные форматы сжатия вроде DXT/BC и ETC работают блоками 4×4, и такие текстуры на части платформ не сжимаются или требуют дополнительной обработки.

void OnPostprocessTexture(Texture2D texture)
{
    if (texture.width % 4 != 0 || texture.height % 4 != 0)
    {
        context.LogImportWarning(
            $"Размер {texture.width}x{texture.height} не кратен 4: сжатие может не сработать",
            texture);
    }
}

context.LogImportWarning лучше, чем Debug.LogWarning: предупреждение привязывается к ассету, видно в его импортёре и не теряется в консоли после следующей компиляции. Для ошибок есть парный context.LogImportError.

Правила для моделей

FBX из DCC-пакетов приходит с кучей лишнего. Для типичного пропа не нужны камеры, источники света, материалы из файла и анимации, а включённый isReadable держит копию меша в оперативной памяти. Договоримся так: в Assets/Art/Props/ лежат статичные модели, в Assets/Art/Characters/ — персонажи, а анимации хранятся в отдельных файлах с символом @ в имени: hero@run.fbx. Это стандартное соглашение Unity для анимаций гуманоидов.

using UnityEditor;
using UnityEngine;

public class ModelImportRules : AssetPostprocessor
{
    public override uint GetVersion() => 2;

    void OnPreprocessModel()
    {
        var importer = (ModelImporter)assetImporter;
        if (!importer.importSettingsMissing)
            return;

        importer.importCameras = false;
        importer.importLights = false;
        importer.isReadable = false;
        importer.materialImportMode = ModelImporterMaterialImportMode.None;

        bool isAnimationFile = assetPath.Contains("@");

        if (assetPath.StartsWith("Assets/Art/Props/"))
        {
            importer.animationType = ModelImporterAnimationType.None;
            importer.importAnimation = false;
            importer.importBlendShapes = false;
            importer.meshCompression = ModelImporterMeshCompression.Medium;
        }
        else if (assetPath.StartsWith("Assets/Art/Characters/"))
        {
            importer.animationType = ModelImporterAnimationType.Human;
            importer.importAnimation = isAnimationFile;
        }
    }
}

materialImportMode = None отключает создание материалов из FBX: материалы делаются в Unity один раз и назначаются на префаб. Если в вашем пайплайне материалы всё же извлекают из файла, этот режим нужно поменять, но решение должно быть осознанным, а не дефолтным.

С meshCompression будьте аккуратнее: сжатие квантует позиции вершин и на крупных моделях с мелкими деталями может дать заметные швы. Для пропов среднего размера Medium обычно безопасен, для архитектуры и террейн-мешей лучше оставить Off.

Чистка иерархии в OnPostprocessModel

В OnPostprocessModel приходит корневой объект импортированной модели. С ним можно работать как с обычной иерархией: удалять служебные узлы, добавлять компоненты, переименовывать. Изменения попадают в результат импорта, а не в сцену.

Пример: художники помечают коллизии мешами с префиксом UCX_. Превратим их в MeshCollider и уберём рендер:

void OnPostprocessModel(GameObject root)
{
    foreach (var filter in root.GetComponentsInChildren<MeshFilter>(true))
    {
        if (!filter.name.StartsWith("UCX_"))
            continue;

        var collider = filter.gameObject.AddComponent<MeshCollider>();
        collider.sharedMesh = filter.sharedMesh;
        collider.convex = true;

        Object.DestroyImmediate(filter.GetComponent<MeshRenderer>());
        Object.DestroyImmediate(filter);
    }
}

Удалять компоненты внутри постпроцессора нужно через DestroyImmediate: обычный Destroy в редакторе вне Play Mode не работает. Сам меш при этом остаётся в результате импорта: на него ссылается коллайдер.

Правила для звука

Звук — место, где ошибки импорта бьют по памяти сильнее всего. Короткие эффекты и длинная музыка требуют противоположных настроек:

  • Короткие SFX (выстрелы, шаги, клики UI) — Decompress On Load: распакованы в памяти, проигрываются без нагрузки на процессор. Они маленькие, поэтому это дёшево.
  • Средние звуки (реплики, длинные эффекты) — Compressed In Memory: хранятся сжатыми, распаковываются при проигрывании.
  • Музыка и эмбиент — Streaming: читаются с диска кусками, в памяти держится только небольшой буфер.

Раскладываем по папкам: Assets/Audio/SFX/, Assets/Audio/Voice/, Assets/Audio/Music/.

using UnityEditor;
using UnityEngine;

public class AudioImportRules : AssetPostprocessor
{
    public override uint GetVersion() => 1;

    void OnPreprocessAudio()
    {
        var importer = (AudioImporter)assetImporter;
        if (!importer.importSettingsMissing)
            return;

        var settings = importer.defaultSampleSettings;
        settings.sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate;

        if (assetPath.StartsWith("Assets/Audio/SFX/"))
        {
            settings.loadType = AudioClipLoadType.DecompressOnLoad;
            settings.compressionFormat = AudioCompressionFormat.ADPCM;
            settings.preloadAudioData = true;
            importer.forceToMono = true;
        }
        else if (assetPath.StartsWith("Assets/Audio/Voice/"))
        {
            settings.loadType = AudioClipLoadType.CompressedInMemory;
            settings.compressionFormat = AudioCompressionFormat.Vorbis;
            settings.quality = 0.6f;
            settings.preloadAudioData = false;
            importer.forceToMono = true;
        }
        else if (assetPath.StartsWith("Assets/Audio/Music/"))
        {
            settings.loadType = AudioClipLoadType.Streaming;
            settings.compressionFormat = AudioCompressionFormat.Vorbis;
            settings.quality = 0.7f;
            settings.preloadAudioData = false;
            importer.loadInBackground = true;
        }

        importer.defaultSampleSettings = settings;
    }
}

AudioImporterSampleSettings — структура, поэтому схема та же, что с текстурами: прочитать, изменить, записать обратно. Флаг preloadAudioData в современных версиях Unity живёт именно в этой структуре: старое свойство AudioImporter.preloadAudioData объявлено устаревшим. Если у клипа предзагрузка выключена, данные стоит загрузить заранее вызовом AudioClip.LoadAudioData(), чтобы первое проигрывание не ждало чтения с диска.

forceToMono для SFX и голоса — почти бесплатная экономия половины памяти: для звуков, которые позиционируются в 3D, стерео в исходнике обычно ничего не добавляет. Для музыки моно включать не стоит.

Для отдельной платформы настройки задаются через SetOverrideSampleSettings("Android", settings): например, чтобы на мобильных понизить quality музыки.

GetVersion: как заставить Unity переимпортировать ассеты

Допустим, вы поменяли правило: теперь UI-спрайты должны импортироваться с FilterMode.Point. Код обновлён, но существующие ассеты уже импортированы и сами собой не переимпортируются. Делать Reimport All на большом проекте — это десятки минут.

Для этого существует GetVersion(). Asset Database запоминает, какие постпроцессоры участвовали в импорте ассета и с какой версией. Если вы увеличили число, которое возвращает метод, Unity переимпортирует только те ассеты, которые этот постпроцессор обрабатывал. Отсюда простое правило: изменили логику постпроцессора — увеличьте версию.

// было 3, поменяли правила для UI — стало 4
public override uint GetVersion() => 4;

Здесь есть важная связка с importSettingsMissing. При переимпорте у ассета уже есть .meta, поэтому код внутри проверки на первый импорт не выполнится. Увеличение версии переприменит только «строгие» правила. Если нужно поменять дефолты для уже существующих файлов, это делается отдельным одноразовым скриптом-миграцией, который проходит по ассетам, правит импортёры и вызывает SaveAndReimport(). И это правильно: массовое переписывание чужих ручных настроек должно быть явным действием, а не побочным эффектом коммита.

Есть тонкость с OnPreprocessAsset. Методы для конкретных типов (OnPreprocessTexture, OnPreprocessModel и так далее) регистрируют зависимость импорта от постпроцессора автоматически. А если в классе объявлен только OnPreprocessAsset, такая зависимость не создаётся, и увеличение версии ничего не переимпортирует. В этом случае зависимость объявляют вручную через context.DependsOnCustomDependency.

Своя зависимость: правила в ScriptableObject

Хардкодить пути в коде удобно для старта, но рано или поздно правила захочется вынести в ассет, чтобы технический художник правил их без программиста. Тут возникает проблема: если правила лежат в ScriptableObject, Unity не знает, что импорт текстуры от него зависит, и после изменения правил ничего не переимпортируется.

Механизм кастомных зависимостей решает это. Регистрируем зависимость с хешем содержимого правил и объявляем её в каждом импорте:

using UnityEditor;
using UnityEngine;

public class RulesDrivenTexturePostprocessor : AssetPostprocessor
{
    const string DependencyName = "MyProject/TextureImportRules";

    public static void RegisterRulesHash(TextureRulesAsset rules)
    {
        string json = EditorJsonUtility.ToJson(rules);
        AssetDatabase.RegisterCustomDependency(DependencyName, Hash128.Compute(json));
        AssetDatabase.Refresh();
    }

    void OnPreprocessTexture()
    {
        context.DependsOnCustomDependency(DependencyName);

        var rules = AssetDatabase.LoadAssetAtPath<TextureRulesAsset>(
            "Assets/Settings/TextureRules.asset");
        if (rules == null)
            return;

        // применяем rules к (TextureImporter)assetImporter
    }
}

RegisterRulesHash вызывается при сохранении ассета правил — например, из OnValidate редакторского кода или из кнопки в кастомном инспекторе. Когда хеш меняется, Unity сам переимпортирует все текстуры, которые объявили эту зависимость. Хеш хранится в памяти редактора, поэтому его нужно регистрировать и при загрузке редактора, например в методе с атрибутом [InitializeOnLoadMethod].

Будьте внимательны с загрузкой самого ScriptableObject внутри импорта: если правила лежат рядом и импортируются в той же пачке, на момент импорта текстуры их может ещё не быть. Проверка на null обязательна, а на очень больших проектах правила проще читать из JSON-файла по пути, чем из ассета.

OnPostprocessAllAssets: реакция на пачку импортов

Этот метод статический и вызывается после того, как завершилась пачка импортов. Сигнатура в актуальных версиях Unity:

static void OnPostprocessAllAssets(
    string[] importedAssets,
    string[] deletedAssets,
    string[] movedAssets,
    string[] movedFromAssetPaths,
    bool didDomainReload)

Последний параметр говорит, была ли перезагрузка домена. Старая версия без него тоже поддерживается.

Здесь, в отличие от методов отдельных ассетов, можно безопасно работать с Asset Database: загружать, создавать, перемещать ассеты. Типичные задачи:

  • обновить реестр или базу данных (например, список всех ScriptableObject-предметов) после добавления новых файлов;
  • перегенерировать код с константами — имена сцен, тегов, адресов Addressables;
  • проверить, что файлы лежат по правильным папкам, и сообщить о нарушениях.

Пример: проверка, что в папку с текстурами не попали файлы с пробелами и кириллицей в имени — такие имена часто ломают сборочные скрипты и CI.

using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

public class NamingRules : AssetPostprocessor
{
    static readonly Regex ValidName = new Regex(@"^[A-Za-z0-9_\-@.]+$");

    static void OnPostprocessAllAssets(
        string[] imported, string[] deleted, string[] moved, string[] movedFrom,
        bool didDomainReload)
    {
        foreach (string path in imported.Concat(moved))
        {
            if (!path.StartsWith("Assets/Art/"))
                continue;

            string fileName = System.IO.Path.GetFileName(path);
            if (!ValidName.IsMatch(fileName))
            {
                Debug.LogWarning(
                    $"Недопустимое имя файла: {path}",
                    AssetDatabase.LoadMainAssetAtPath(path));
            }
        }
    }
}

Учтите, что при массовой операции над отдельными файлами (а не над папкой) метод может вызваться несколько раз — по разу на файл. Поэтому тяжёлую работу вроде полной перегенерации кода лучше откладывать через EditorApplication.delayCall и выполнять один раз, а не в каждом вызове.

Порядок выполнения нескольких постпроцессоров

Когда в проекте несколько постпроцессоров (свои и из пакетов из Asset Store), они вызываются по очереди. Порядок задаётся методом GetPostprocessOrder(): меньшее значение выполняется раньше, по умолчанию 0.

public override int GetPostprocessOrder() => 100;

Это полезно, когда ваши строгие ограничения должны сработать последними и перекрыть настройки, выставленные сторонним плагином. Но лучше не строить сложных цепочек: если два постпроцессора спорят за одну настройку, это проблема архитектуры правил, а не порядка вызова.

Параллельный импорт и чего не делать внутри постпроцессора

Современные версии Unity умеют импортировать часть ассетов в отдельных процессах-воркерах (параллельный импорт включается в Project Settings → Editor, блок Asset Pipeline; даже если сейчас он выключен, лучше писать код так, будто включён). Код постпроцессора при этом выполняется не в основном процессе редактора. Из этого следуют правила.

  • Никакого статического изменяемого состояния. Счётчики, кэши и списки «уже обработанных ассетов» в статических полях будут у каждого воркера свои. Результат импорта должен зависеть только от самого файла, его настроек и объявленных зависимостей.
  • Не вызывать AssetDatabase.ImportAsset, CreateAsset, SaveAssets и Refresh внутри методов отдельного ассета. Импорт внутри импорта — прямой путь к циклам и непредсказуемому порядку. Всё, что требует Asset Database, переносите в OnPostprocessAllAssets.
  • Не читать другие ассеты без объявления зависимости. Если результат импорта зависит от другого файла, объявите это через context.DependsOnSourceAsset(path) или context.DependsOnArtifact(path). Иначе при изменении того файла ассет не переимпортируется, а кэш импорта у коллег и в CI разойдётся с вашим.
  • Не менять настройки импортёра в Postprocess. Уже обсуждали: настройки — только в Preprocess.
  • Не делать постпроцессор медленным. Он выполняется для каждого ассета нужного типа. Регулярное выражение, созданное заново в каждом вызове, или поиск по всему проекту через AssetDatabase.FindAssets умножаются на тысячи текстур при первом открытии проекта.

Как проверить, что правила работают

Постпроцессор — такой же код, как и всё остальное, и его стоит покрыть тестами в Edit Mode. Сценарий простой: скопировать тестовый файл во временную папку с нужным путём, импортировать, проверить настройки импортёра, удалить.

using NUnit.Framework;
using UnityEditor;

public class TextureImportRulesTests
{
    const string Source = "Assets/Tests/Fixtures/sample.png";
    const string Target = "Assets/Art/UI/__test_icon.png";

    [TearDown]
    public void Cleanup() => AssetDatabase.DeleteAsset(Target);

    [Test]
    public void UiTexture_IsSpriteWithoutMipmaps()
    {
        AssetDatabase.CopyAsset(Source, Target);
        AssetDatabase.ImportAsset(Target, ImportAssetOptions.ForceSynchronousImport);

        var importer = (TextureImporter)AssetImporter.GetAtPath(Target);

        Assert.AreEqual(TextureImporterType.Sprite, importer.textureType);
        Assert.IsFalse(importer.mipmapEnabled);
    }
}

Тонкость: CopyAsset переносит и настройки импорта исходника, поэтому importSettingsMissing у копии будет false, и дефолтные правила не применятся. Для честной проверки первого импорта копируйте файл через System.IO.File.Copy без .meta, а затем вызывайте AssetDatabase.ImportAsset. Тест на этом месте полезен ещё и как документация: он фиксирует, что именно считается «первым импортом» в вашем проекте.

Второй способ проверки — аудит. Небольшой пункт меню, который проходит по всем текстурам через AssetDatabase.FindAssets("t:Texture2D"), читает импортёры и выводит список нарушений: UI-текстуры с мипмапами, нормали без типа NormalMap, музыка без Streaming. Его удобно запускать перед релизом и в CI, чтобы ловить файлы, попавшие в проект до появления правил.

Типичные ошибки

  1. Скрипт лежит не в Editor-сборке. В редакторе всё работает, сборка игрока падает с ошибкой про пространство имён UnityEditor.
  2. Настройки меняются в OnPostprocessTexture. Текущий импорт их не видит, .meta меняется, ассет переимпортируется снова.
  3. Правила затирают ручные правки. Нет проверки importSettingsMissing для дефолтов, и художник не может поменять ни одной настройки.
  4. Забыли записать платформенные настройки обратно. GetPlatformTextureSettings и defaultSampleSettings возвращают копии; без SetPlatformTextureSettings и присваивания изменения теряются.
  5. Изменили код и не подняли GetVersion. Новые файлы импортируются по новым правилам, старые остаются как были, и в проекте живут две несовместимые конвенции.
  6. Сравнение путей через Contains. Под Contains("UI") попадут Assets/Art/GUIDE/ и Assets/Building/. Сравнивайте по префиксу папки с завершающим слешем и по имени файла без расширения.
  7. Тяжёлая работа на каждый ассет. Первый импорт проекта на новой машине растягивается на час из-за постпроцессора, который на каждую текстуру ищет что-то по всему проекту.

Итог

AssetPostprocessor превращает договорённости о настройках импорта из текста в вики в код, который работает у всех одинаково. Рабочая схема выглядит так:

  • настройки импортёра меняются в OnPreprocess*, проверки и правка результата — в OnPostprocess*;
  • дефолты применяются только при первом импорте через importSettingsMissing, строгие ограничения — всегда, и они только ограничивают, а не навязывают;
  • любое изменение логики сопровождается увеличением GetVersion(), а внешние источники правил объявляются как зависимости импорта;
  • работа с Asset Database — только в OnPostprocessAllAssets;
  • правила покрыты тестами в Edit Mode, а для старых файлов есть аудит.

Начните с малого: одного класса для UI-текстур и одного для звука. Эти два правила обычно дают самую заметную экономию памяти, а остальные можно добавлять по мере того, как в проекте находятся новые неправильно импортированные файлы.

Комментарии

Чтобы оставить комментарий, войдите в аккаунт.

Пока нет комментариев. Будьте первым!

Смотрите также