Сжатие текстур, часть V. ASTC и Basis Universal

Продолжение серии постов о сжатых текстурных форматах. Предыдущие части: часть I, часть II, часть III, часть IV.

Вот мы и подошли к state of the art текстурного сжатия по состоянию на 2025 год.

ASTC

Adaptive Scalable Texture Compression

Современный формат, созданный с учетом особенностей мобильных платформ. На сегодняшний день крайне мало распространен. Программно поддерживается в Vulkan и новейших версиях OpenGL ES, но аппаратно — далеко не всеми видеокартами. В десктопном OpenGL поддержки нет и, судя по всему, не предвидится (но это не проблема — см. ниже).

В отличие от S3TC, BPTC и RGTC, формат работает с блоками от 4×4 до 12×12. Размер блока ASTC всегда составляет 16 байт, поэтому чем больше блок, тем выше степень сжатия, но ниже качество. Можно выбирать формат блока (например, 5×5, 6×6, 8×8 и т.д.) в зависимости от баланса между качеством и размером. ASTC поддерживает от 1 до 4 каналов.

ASTC поддерживает HDR и является на сегодняшний день единственным универсальным решением для сжатия HDR-текстур на мобильных GPU.

Basis Universal

Использовать ASTC напрямую непрактично, если вы хотите охватить широкий спектр платформ и конфигураций. Для этого была придумана особая система хранения сжатых текстур, которая позволяет приложениям на лету транскодировать их в формат, подходящий для каждой конкретной платформы — ASTC, BPTC, S3TC, RGTC, PVRTC и в несжатые форматы. Basis Universal охватывает как LDR, так и HDR-текстуры в рамках пяти режимов сжатия: ETC1S, UASTC LDR 4×4, UASTC HDR 4×4, UASTC HDR 6×6, UASTC HDR 6×6 intermediate.

ETC1S. Режим низкого и среднего качества, основанный на подмножестве ETC1. Поддерживает переменный баланс веса/качества (наподобие JPEG). Поддерживает альфа-канал. Этот вид текстур можно быстро транскодировать практически в любые другие сжатые форматы.

UASTC (Universal ASTC) LDR 4×4. Подмножество ASTC для стандартных LDR-текстур с 8 битами на канал. Транскодируется также очень эффективно, особенно в ASTC и BC7.

UASTC HDR 4×4. Режим для HDR-текстур, подмножество ASTC HDR 4×4 8bpp. Разработан для обеспечения высокого качества, эффективно транскодируется (с небольшими потерями) в BC6H. В ASTC HDR превращается вообще без какого-либо оверхеда и без потерь. Этот режим также можно транскодировать в различные несжатые HDR-форматы 32-64 bpp.

UASTC HDR 6×6. Режим HDR-текстур 3.56 bpp. Также на 100% эквивалентен ASTC.

UASTC HDR 6×6 Intermediate («GPU Photo»). Быстро транскодирутся в ASTC HDR 6×6, BC6H и различные несжатые HDR-форматы.

ETC1S и UASTC LDR 4×4 могут быть транскодированы в:

  • ASTC LDR 4×4 L/LA/RGB/RGBA 8bpp
  • BC1-5 RGB/RGBA/X/XY
  • BC7 RGB/RGBA
  • ETC1 RGB, ETC2 RGBA, ETC2 EAC R11/RG11
  • PVRTC1 4bpp RGB/RGBA, PVRTC2 RGB/RGBA
  • ATC RGB/RGBA, FXT1 RGB
  • Несжатые LDR-форматы.

UASTC HDR 4×4 и UASTC HDR 6×6 могут быть транскодированы в:

  • ASTC HDR 4×4 (8bpp, только UASTC HDR 4×4)
  • ASTC HDR 6×6 RGB (3.56bpp, ASTC HDR 6×6 или UASTC HDR 6×6 intermediate)
  • BC6H RGB (8bpp, UASTC HDR 4×4 или UASTC HDR 6×6)
  • Несжатые HDR-форматы.

KTX2 и суперкомпрессия

Basis Universal — это отлично, но как с ним работать и с удобством хранить? Для упрощения этой задачи Khronos Group предложили формат контейнера KTX2, развитие KTX. Если Basis Universal — это механизм сжатия, то KTX2 — чемодан, в который можно упаковать вообще все. Он поддерживает все существующие форматы сжатия, включая ASTC, UASTC, ETC1S, BC1-7, ETC, PVRTC, и сверх того позволяет сжимать текстуры при помощи lossless-алгоритмов (Zstandard, Zlib, BasisLZ). Несжатые форматы, определенные спецификацией Vulkan, также поддерживаются в полной мере. В KTX можно хранить не только 1D-, 2D- и 3D-текстуры, но и кубические карты, mip-уровни и массивы текстур.

Khronos предоставляет набор утилит KTX Software для конвертации изображений в KTX/KTX2, а также для валидации и анализа файлов. Плюс, разумеется, библиотека libktx для декодирования файлов в приложениях — она полностью берет на себя задачу по транскодированию, и на выходе вы получаете текстуру в нужном вам формате, которую остается лишь отправить в графический API.

SDL_Image и загрузчики текстур

Вслед за KTX я решил улучшить в движке ситуацию с поддержкой стандартных форматов изображений. Отныне Dagon загружает текстуры с помощью SDL_Image, если библиотека присутствует, системно или локально. В противном случае используется старый загрузчик на основе dlib.image. Преимуществом такого подхода является гарантированная под Windows поддержка огромного числа форматов, включая современные WebP и AVIF; также автоматически решается застарелая проблема с декодированием прогрессивных JPEG.

А еще одно важное нововведение — механизм расширения этой системы. Теперь, если нужно добавить новый формат текстур, вместо кастомного ассета достаточно написать и зарегистрировать в AssetManager‘е кастомный загрузчик текстур — реализацию абстрактного класса TextureLoader. Он работает почти аналогично ассету, но предназначен для прямого декодирования данных в TextureBuffer.

(далее…)

Chillwave Drive

Значительно обновлена демка физики автомобиля на Dagon, которая отныне называется Chillwave Drive. Трение колес теперь моделируется на чистых силах вместо встроенных джоинтов Newton, что сделало симуляцию более точной и стабильной. При движении используется динамическое трение (продольное и поперечное) на основе формул Pacejka ’94, при остановке — статическое, препятствующее боковому скольжению машины на склоне. Улучшено управление с контроллера, также добавлена новая модель машины и шейдер неба с облаками и сменой дня и ночи.

Скачать готовую сборку демки под Windows можно тут.

Dagon 0.20.0

Вышло небольшое, но важное обновление Dagon. Добавлена поддержка кастомных шейдеров для текстурирующих слоев материала ландшафта — это делает систему невероятно гибкой, так как вы можете рисовать на ландшафте все, что захотите. В качестве примера в движок добавлен шейдер луж (dagon.extra.puddle) с эффектом кругов от дождевых капель. Также появилась поддержка Matcap для фейковых отражений (особенно полезная фича в в SimpleRenderer, где нет честных отражений), поддержка glTF-расширения KHR_materials_emissive_strength, доступ к материалам glTF по именам. Обновлена библиотека Newton под Linux.

В репозитории проекта открыты обсуждения: https://github.com/gecko0307/dagon/discussions

KTX в Dagon

Наконец-то реализовал давнюю идею поддержки текстур в формате KTX (Khronos Texture). Это контейнер, специально созданный для OpenGL и Vulkan и поддерживающий большое количество форматов текстур, включая сжатые. Особенно интересен KTX2, который позволяет хранить текстуры в формате Basis Universal — он хорош тем, что позволяет при создании игровых ресурсов не волноваться, что сжатие не будет поддерживаться на системах каких-то пользователей. Формат сжатия выбирается движком игры на основе информации от видеодрайвера, а затем текстура на лету транскодируется в этот формат. Эта фича нужна главным образом на мобильных платформах, но и в десктопном движке не помешает — Basis Universal сжимает очень эффективно и транскодируется в S3TC или BPTC за считанные мгновения.

Текстура KTX1/KTX2 загружается в объект KTXAsset, при его создании необходимо указать приоритет транскодирования. Если это TranscodePriority.Size, то загрузчик отдает предпочтение S3TC, если TranscodePriority.Quality — BPTC (при наличии поддержки). Также можно использовать TranscodePriority.Uncompressed, чтобы получить наилучшее качество — текстура будет распакована в RGBA8.

Пример:

TextureAsset aTextureBox;
TextureAsset aTextureEnvmap;

// В конструкторе сцены:

registerKTXLoader(assetManager);

// На стадии запроса ассетов:

aTextureBox = addTextureAsset("data/box.ktx2");
aTextureBox.conversion.hint = TranscodeHint.Size;

aTextureEnvmap = addTextureAsset("data/cubemap.ktx2");
aTextureEnvmap.conversion.hint = TranscodeHint.Quality;

// При создании материалов:

material.baseColorTexture = aTextureBox.texture;
environment.ambientMap = aTextureEnvmap.texture;

Обновления

dlib 1.3.1

Вышло небольшое обновление dlib 1.3, в котором исправлена совместимость с отличными от x86 платформами.

BindBC-SPIRVCross

Написал D-биндинг к SPIRV-Cross, тулкиту для работы со SPIR-V. Теперь потенциально можно создать продвинутый фреймвок компиляции шейдеров — например, систему автоматической адаптации шейдеров к разным графическим API в одном приложении. Пригодится, когда я буду переходить на WebGPU.

dcore

Библиотека dcore, на основе которой я собираюсь писать вторую версию dlib, будет развиваться в качестве самостоятельного проекта: https://github.com/DLangGamedev/dcore. В связи с этим я решил создать организацию DLangGamedev, в которую перевел все BindBC-биндинги библиотек для разработки игр:

LDC поддерживает SPIR-V!

Не секрет, что компилятор LDC, благодаря бэкенду на LLVM, поддерживает множество разных целевых платформ — и в их числе, оказывается, есть SPIR-V. Мне удалось собрать вот такое минимальное вычислительное ядро (которое, правда, ничего не делает):

module main;

import ldc.attributes;

@callingConvention("spir_kernel")
@llvmAttr("hlsl.shader", "compute")
extern(C) void compute_main() nothrow @nogc
{
    //
}
ldc2 -betterC -vgc -c --output-o -of=main.spv --mtriple=spirv-unknown-vulkan -mattr=+spirv1.0 source/main.d

Если вывести ассемблерный листинг, получается следующее:

OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %3 "compute_main"
OpSource Unknown 0
OpName %3 "compute_main"
%1 = OpTypeVoid
%2 = OpTypeFunction %1
%3 = OpFunction %1 None %2
%4 = OpLabel
OpReturn
OpFunctionEnd

Как видно, можно использовать этот SPIR-V модуль как вычислительный шейдер в Vulkan и OpenGL 4.6. Поддержки других видов шейдеров пока нет, но кто знает — может быть в один прекрасный день получится использовать D в качестве полноценного шейдерного языка! Я лично в это верю.

Статус dlib2

В последнее время редко пишу о dlib, но хочу заверить: разработка библиотеки на месте не стоит, в данный момент я разрабатываю dlib2, а точнее — ее низкоуровневый компонент dcore. dcore — это процедурный API, совместимый с betterC и минимально заменяющий стандартную библиотеку. Он полностью автономный, не зависит от Phobos, поддерживает компиляцию в машинный код под bare metal (то есть, без операционной системы) и в WebAssembly. Он предназначен для создания компактных приложений в стиле C, а также для написания высокоуровневых частей библиотеки на его основе.

Может возникнуть вопрос — для чего это нужно, если есть libc? dcore, конечно, использует стандартные функции C при компиляции в таргеты, которые поддерживают libc. Но все упирается в WebAssembly: своего Emscripten на D не существует. Поэтому, для упрощения написания WASM-модулей, я решил сделать минимальный набор абстракций, которые скрывают платформозависимые реализации самых важных компонентов приложения под универсальным низкоуровневым API. dcore элементарно позволяет использовать printf, malloc и т.д., не задумываясь, доступны ли они на целевой платформе.

Еще один важный момент — математика. Я уже публиковал пост о бенчмарках различных реализаций синуса и косинуса — dcore поддерживает все стандартные математические функции (sin, cos, tan, asin, acos, sqrt, cbrt и др.), предоставляя на каждой платформе оптимальные реализации: LLVM-интринсики, функции C, функции Phobos и кастомные, без зависимостей, предназначенные для нестандартных таргетов.

Из нового:

  • dcore.time — удобный враппер над C-шными localtime и gmtime. Позволяет получать текущую системную время-дату, в текущем часовом поясе или по Гринвичу;
  • dcore.sys — информация о системе: архитектура CPU, количество ядер, объем памяти, название и версия ОС. Модуль поддерживает x86/x64, ARM, ARM64, IA64, MIPS, SPARC, PPC. Информация об ОС поддерживается для всех версий Windows и двух больших семейств Unix — Linux/Solaris/AIX (через sysconf) и BSD/OSX (через sysctl);
  • dcore.process — позволяет кроссплатформенно получить PID.

Мини-проект: радиоуправляемая платформа на Arduino

Пост не имеет отношения к D, но я решил опубликовать его тут, так как тематика относительно близкая.

Arduino я начал изучать год назад. Для непосвященных: это созданный итальянскими инженерами недорогой 8-битный микроконтроллер с распиновкой, очень простой для понимания начинающими, в комплекте с работающим из коробки C-тулчейном, юзер-френдли IDE и инструментарием для прошивки платы по COM-порту. Вообще электрическими схемами я увлекаюсь с самого раннего детства — всегда любил что-то делать своими руками из железного конструктора, проводов и лампочек =) С появлением компьютера это хобби не угасло, хотя до недавних пор и отошло на второй план. Мечту сделать игрушечный радиоуправляемый аппарат я не оставлял много лет — и вот, наконец, сумел воплотить ее в жизнь!

За основу я взял китайский клон Arduino Uno R3 (благо они очень дешевые, можно не жалеть для экспериментов). Я использовал, в основном, прилагавшиеся к плате детали, но пришлось также докупить электромоторы и драйвер L298N. Что удобно, моторы можно купить сразу с колесом — таким образом, остается только укрепить ходовую часть на шасси.

Шасси, за неимением лучшего материала, я сделал из Лего. На этом этапе, как ни странно, я застрял надолго, так как электронные компоненты оказалось очень непросто надежно прикрепить к лего-деталям. Для удобства я докупил лего-совместимый корпус для Arduino, однако толкового способа закрепить моторы я так, к сожалению, и не придумал — в итоге ходовая часть получилась немного расхлябанная, но тем не менее работоспособная.

Отдельной проблемой стало питание. Сначала я думал запитать плату от USB-пауэрбанка, но ничего не вышло: Ардуино работает пару секунд и выключается. Пришлось добавить батарейный отсек на 6 элементов AA (9В). Драйвер моторов запитал параллельно с платой, от разъема VIN (который, в отличие от обычных 5-вольтных разъемов питания, напрямую отдает 9В от адаптера, что очень удобно). Теоретически, для большей надежности можно было бы запитать драйвер полностью независимо от Ардуино, добавив клемму между DC-штекером и платой, но я не стал так заморачиваться.

Драйвер L298N — это ключевой компонент двухмоторной ходовой части. Пара моторов нужна для того, чтобы не делать рулевое управление: для поворота аппарата влево-вправо нужно просто вращать моторы в противположных направлениях. L298N нужен именно для этого. Управление этой штукой осуществляется путем подачи сигналов на командные входы. В качестве remote-контроллера я использовал инфракрасный приемник в паре с комплектным пультом — соответствующая программа получилась очень простой:

#include "IRremote.h"

#define IR_RECEIVE_PIN 7

// IN1, IN2 - motor A
// IN3, IN4 - motor B
#define IN1 2
#define IN2 3
#define IN3 4
#define IN4 5

#define ENA A0
#define ENB A1

int receivedValue;
int buttonPressed = 0;

void setup()
{
    pinMode(IN1, OUTPUT);  //motor A
    pinMode(IN2, OUTPUT);  //motor A
    pinMode(IN3, OUTPUT);  //motor B
    pinMode(IN4, OUTPUT);  //motor B
  
    IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
  
    buttonPressed = 0;
}

void loop()
{
    receivedValue = 0;
    
    if (IrReceiver.decode())
    {
        receivedValue = IrReceiver.decodedIRData.command;
        IrReceiver.resume();
    }
  
    if (receivedValue == 28) // forward
    {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, HIGH);
    }
    else if (receivedValue == 82) // back
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
        digitalWrite(IN3, HIGH);
        digitalWrite(IN4, LOW);
    }
    else if (receivedValue == 8) // left
    {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, HIGH);
        digitalWrite(IN4, LOW);
    }
    else if (receivedValue == 90) // right
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, HIGH);
    }
    else // stop
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, LOW);
    }
    
    delay(100);
}

Аппарат ездит со скоростью около метра в секунду. Минусом двухколесной системы является то, что нельзя поворачиваться во время движения, поэтому более интересным вариантом было бы, конечно, добавить руль — например, сервомотор, поворачивающий дополнительное рулевое колесо.

Dagon 0.19.0

Вышла новая версия движка! Релиз очень большой, все изменения детально описывать в этом посте не буду, перечислю только самое основное. Подробности — на странице релиза.

  • Добавлен модуль dagon.core.persistent — простая БД типа «ключ-значение», которую можно использовать для управления пользовательскими данными игры, такими как настройки и сохранения;
  • InputManager теперь поддерживает разделенные пробелами имена кодов клавиш в привязках. Вместо пробела используется символ плюса, например, kb_left+ctrl для левого Ctrl;
  • Dagon-приложение теперь устанавливает кодировку консоли на UTF-8 в Windows, чтобы нормально выводился нелатинский текст;
  • Добавлен SimpleRenderer, о котором я уже писал ранее — облегченный рендерер для казуальной и стилизованной графики;
  • Добавлена поддержка бокс-проекции для световых зондов. Ее можно включить с помощью свойства Entity.probeUseBoxProjection. Зонды теперь используют альфа-спад для плавного смешивания с данными в G-буфере — благодаря этой фиче сглаживается граница между интерьерным и уличным освещением. Для управления этим эффектом введено свойство Entity.probeFalloffMargin;
  • Добавлена поддержка ортогональных проекций: свойство RenderView.projection теперь принимает константы Perspective, Ortho и OrthoScreen. RenderView.orthoScale управляет масштабом проекции в режиме Ortho;
  • Добавлен новый примитив — цилиндр ShapeCylinder, а также Billboard (прямоугольник, который всегда направлен в сторону камеры);
  • Материалы теперь поддерживают произвольные преобразования текстур (аффинные матрицы 3×3) благодаря свойству Material.textureTransformation. Метод Material.setSprite(Vector2f uvSize, Vector2f uvPosition) можно использовать для наложения на квады фрагментов текстуры — полезно для рендеринга спрайтов и покадровой анимации;
  • Добавлен новый пакет dagon.collision — базовая система обнаружения столкновений;
  • В контроллере персонажей NewtonCharacterComponent появилась поддержка приседания (метод crouch) и улучшенная проверка земли.