Vulkan - 13. 압축 텍스처

2022. 11. 8. 09:23Vulkan

개요

PNG와 같은 이미지 압축 형식은 내용을 (최대한) 유지하고 작은 크기로 보관하기 위한 알고리즘이 적용되었다면, 텍스처의 압축은 임의 접근에 유리하면서도 크기를 작게 하는 데에 초점을 맞춥니다. 오늘날의 선명한 이미지들은 한 픽셀에 24(RGB) 또는 32(RGBA)비트를 사용하는 만큼 메모리를 많이 차지하며 이는 공간 지역성과 직결되는 성능 문제이기도 한데요, 따라서 오늘날 게임 엔진들은 텍스처 매핑을 위해 압축 기술을 많이 사용합니다. 이들은 밉맵이나 비등방성 필터링과도 함께 사용할 수 있다고 합니다.

 

이번 글에서는 Vulkan에서 텍스처로 사용할 이미지 객체(VkImage)를 위해 압축 텍스처 형식을 사용하는 방법을 확인해 봅니다.

 

목차

1. Vulkan에서 압축 텍스처 형식 데이터를 넘겨 사용하기

2. 압축 텍스처 준비하기

  2.1. KTX 파일 작성하기

  2.2. KTX 파일 사용하기

요약

과제

 

본문

 

1. Vulkan에서 압축 텍스처 형식 데이터를 넘겨 사용하기

이전에 Vulkan에서 이미지 객체를 만들 때 각 픽셀 데이터와 함께 어떤 정보를 줬었던가요? 이미지 차원수, 크기, 밉 수준, 형식, 타일링 모드, 초기 레이아웃, 용도, 큐 공유 모드 등이 있었습니다. 이전 글에서 단순히 (32비트) 픽셀 값들의 나열인 버퍼와 바인드해서 원하는 대로 나왔던 것은 저기 있는 '형식'이 R8G8B8A8이었기 때문이죠. 하지만 1.0버전에, 별다른 확장 없이도 몇 가지의 압축 텍스처를 쓸 수 있습니다. (전체 내용은 여기를 참고하세요.)

 

사양을 조금 참고하면, 기본적으로는 (Vulkan 1.0버전, 확장을 활성화하지 않음) BC1, BC2, BC3, BC4, BC5, BC6, BC7, ETC2, EAC, ASTC 중 일부를 사용할 수 있습니다. 여기서는 format에 해당 값을 주고, 이미지 데이터를 준비하는 게 전부겠죠? 그 방법을 얘기하기에 앞서 먼저 앞의 형식들이 뭔지만 알아봅시다. 물론 거의 이론 이야기다 보니, 만드는 데에만 관심 있다면 넘어가길 말리진 않겠습니다.

 

형식 이름 특징 (별도 언급이 없으면 RGBA 채널) 비고
BC1 (Block compression) 4x4 텍셀 블록이 64비트에 인코딩되어 있습니다. 알파 값 없이 모두 1로 취급되거나, 완전 투명 / 완전 불투명을 뜻하는 한 개의 비트를 넣기도 합니다. 관련 문서(MS)
BC2 4x4 텍셀 블록이 128비트에 인코딩되어 있습니다. 그 중 64비트가 알파 값입니다. 관련 문서(MS)
BC3 4x4 텍셀 블록이 128비트에 인코딩되어 있습니다. 그 중 64비트가 알파 값입니다.
BC2와 다른 점은 문서를 참고하세요.
관련 문서(MS)
BC4 4x4 텍셀 블록이 64비트에 인코딩되어 있으며, 1채널(보통 흑백 or 알파)을 저장합니다. (그런 만큼 상대적으로 정밀도가 높음) 관련 문서(MS)
BC5 4x4 텍셀 블록이 128비트에 인코딩되어 있습니다. 2채널(RG)을 저장합니다. 관련 문서(MS)
BC6H 4x4 텍셀 블록이 128비트에 인코딩되어 있습니다. 3채널(RGB)을 저장합니다. 관련 문서(MS)
BC7 4x4 텍셀 블록이 128비트에 인코딩되어 있습니다. 관련 문서(MS)
ETC2 (Ericsson Texture Compression) 4x4 텍셀 블록이 64비트에 3채널(RGB) 인코딩되어 있거나(1비트 알파를 쓰기도 함), 128비트에 4채널(RGBA) 인코딩되어 있습니다. 관련 문서
EAC 4x4 텍셀 블록이 64비트에 1채널 인코딩 혹은 128비트에 2채널(RG) 인코딩되어 있습니다. (ETC2와 같은 알고리즘으로 압축하는데 채널 수가 다릅니다.)
 
ASTC (Adaptive Scalable Texture Compression) 4x4, 5x4, 5x5, 6x5, 6x6, 12x12 등 다양한 크기의 블록 단위의 압축이 있으며 각 블록은 128비트에 4채널 인코딩되어 있습니다. 즉 블록 크기가 클수록 텍스처가 더 가벼워지며 많이 뭉개지겠죠. 관련 문서(NVIDIA)

*참고 벡터양자화: 96년도에 스탠포드에서 올린 SIGGRAPH 논문

 

관련 문서가 하나씩 있긴 합니다만 당연히 거기 매일 필요는 없습니다. 구글에 이름만 쳐도 좋은 자료 많아요.

 

VkPhysicalDeviceFeatures를 조사해서 textureCompressionBC가 true라면 BC1 ~ BC7에 대한 모든 기능을 쓸 수 있습니다. BC1~BC7 중 어느 하나라도 사용할 수 없다면 해당 값은 false이며 개별 질의는 vkGetPhysicalDeviceFormatProperties로 얻거나(=이 포맷으로 뭘 할 수 있나요?) vkGetPhysicalDeviceImageFormatProperties를 써서 결과를 확인(=이 포맷으로 이것을 할 수 있나요?)하면 됩니다. 이는 textureCompressionASTC_LDR textureCompressionETC2도 마찬가지입니다. 이를 알아 두는 방법을 정말 단순무식하게 코드로 쓰면 이런 경우가 있을 수 있겠네요.

 

bool isThisFormatAvailable(VkPhysicalDevice physicalDevice, VkFormat format, uint32_t x, uint32_t y, VkImageCreateFlagBits flags=(VkImageCreateFlagBits)0){
    VkImageFormatProperties props;
    VkResult result = vkGetPhysicalDeviceImageFormatProperties(
        physicalDevice,
        format,
        VK_IMAGE_TYPE_2D,
        VK_IMAGE_TILING_OPTIMAL,
        VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
        flags, // 경우에 따라 VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT
        &props
    );
    return (result != VK_ERROR_FORMAT_NOT_SUPPORTED) &&
           (props.maxExtent.width >= x) &&
           (props.maxExtent.height >= y);
}

VkFormat fallback(){
    // physicalDevice와 x,y는 알려진 것으로 가정
#define CHECK_N_RETURN(f) if(isThisFormatAvailable(physicalDevice,f,x,y)) return f
    CHECK_N_RETURN(VK_FORMAT_ASTC_4x4_SRGB_BLOCK);
    CHECK_N_RETURN(VK_FORMAT_BC7_SRGB_BLOCK);
    CHECK_N_RETURN(VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK);
    CHECK_N_RETURN(VK_FORMAT_BC3_SRGB_BLOCK);
    return VK_FORMAT_R8G8B8A8_SRGB;
#undef CHECK_N_RETURN    
}

 

제 그래픽카드에서는 이걸 돌리니 128x128 기준, BC7을 쓸 수 있다고 하네요. 16텍셀이 128비트에 인코딩되어 있으니 32bpp인 R8G8B8A8에 비해서는 4배 가볍습니다.

 

대체 여기서 뭘 쓰란 말이냐?
유니티에 친절한 비교 자료가 있는 것 같아. 다만 이 글에서 다룰 KTX 라이브러리는 용도에 맞는 것 중 가능한 것 우선으로 고르는 기능이 있지.
그럼 모든 종류의 텍스처를 준비해야 한다는 거야?
아니. Basis Universal 데이터를 준비해 두면 유연한 대응이 가능하다고 하네. 바로 아래에서 다시 보자.

 

2. 압축 텍스처 준비하기

역시나 게임 엔진이라면 그런 압축 파일을 빌드 타임에 제공하는 게 미덕이겠죠. 방법 중 하나는 NVIDIA texture tools Exporter와 같은 해당 목적의 프로그램을 사용하는 겁니다. 이것은 별 제한 없이 무료로 사용할 수 있고, BC(n), ASTC 압축을 지원하며 법선맵 자동 생성 등 다른 도구도 있습니다. 단 Windows, NVIDIA GPU에서만 사용할 수 있다고 하네요. ETC2는 구글이 만든 etc2comp라는 CLI 도구를 이용할 수도 있습니다. AMD GPUOpen의 Compressonator도 있어요. 찾아본 결과 웹 도구는 없거나 마이너하네요. 이들 도구에 보통 이미지(PNG, JPG, BMP 등)를 넣고 압축 텍스처 형식으로 바꾼 후 그대로 사용하면 되는 거죠.

 

이렇게 얻어낸 텍스처 데이터 중 적절한 것을 사용하는 방법은 정말 다양합니다. 아마 좀 큰 집단의 게임이라면 하드웨어 스펙을 보고 필요한 것만 다운로드하게 만들 수도 있겠죠. 하드웨어가 바뀌어서 되던 게 안 되는지는 매번 확인하게 하고요. 물론 한 종류의 텍스처를 여러 압축 방식으로 모두 배포하여 따로 불러오는 것도 딱히 잘못은 아니라고 봅니다.

 

하지만 여기서 말하고자 하는 방법은 이전 글들에서도 몇 번 이름을 언급했던 KTX에서, Basis Universal Supercompressed 데이터를 쓰는 것입니다. 들어가서 환경에 맞는 버전을 받아주세요. 설치 파일을 실행하여 모든 것에 체크하고 설치를 마무리하면, 그 위치에서 헤더 파일 2개(ktx.h, ktxvulkan.h)와 링킹 라이브러리(ktx.lib, ktx.dll)를 찾을 수 있을 겁니다. 보시는 바와 같이 기본적으로 동적 링킹 방식인데 그게 마음에 안 들면 같은 곳에서 소스를 받아 CMake를 돌리면 됩니다. (오히려 이쪽이 필요한 것만 빌드하기 좋으므로 하실 줄 안다면 그렇게 하는 것이 여러모로 유리합니다.)

 

KTX는 크로노스 그룹이 낸, OpenGL과 Vulkan을 위한 가벼운 텍스처 파일 형식입니다. 단순한 이미지도 가질 수 있고 큐브맵, 밉맵도 포함할 수 있습니다. 위에 나온 형식을 비롯한 다양한 형식을 커버할 수도 있으며 BasisU를 통해 여러 가지 방식으로 빠르게 변형될 수도 있습니다. KTX 라이브러리(libktx)의 자세한 사용법은 여기를 참고하시고요, 여기서는 기초적 사용법을 보여드립니다. 주의하실 점은 enum 상수나 구조체 멤버 등에 대한 주석이 보통 해당 멤버의 아래에 달려 있기 때문에, IDE에서 마우스를 올리면 설명을 보여주는 것을 그대로 사용할 수 없다는 겁니다. 직접 해당 헤더로 이동하거나 문서를 확인하여 의미를 파악해 주세요.

 

2.1. KTX 파일 작성하기

파일 작성은 딱히 기존의 Vulkan 프로그램에 붙어 있어야 할 이유는 없습니다. (프로그래머 성향에 따라 보통 이미지를 불러와 메모리 상에서 텍스처 압축 형식을 일시적으로 갖게 할 수도 있겠죠.) 그러니 이번에는 잠깐 새 프로젝트 파일을 만들어, 일반 이미지 파일을 불러와 BasisU ETC1S 또는 UASTC 압축 형식으로 저장하는 프로그램을 만들어 봅시다.

 

더보기
더보기
/*
 * Copyright 2010-2018 The Khronos Group, Inc.
 * SPDX-License-Identifier: Apache-2.0
 *
 * See the accompanying LICENSE.md for licensing details for all files in
 * the KTX library and KTX loader tests.
 */

 /**
  * @file
  * @~English
  *
  * @brief Declares the public functions and structures of the
  *        KTX API.
  *
  * @author Mark Callow, Edgewise Consulting and while at HI Corporation
  * @author Based on original work by Georg Kolling, Imagination Technology
  *
  * @snippet{doc} version.h API version
  */

 

먼저 헤더입니다. 저의 경우는 CMake로 정적 링킹 라이브러리를 빌드했기 때문에 KHRONOS_STATIC을 헤더 포함 전에 붙였습니다. 기본적으로 주어지는 라이브러리는 동적 링킹 라이브러리이며, 이때는 해당 정의를 붙이지 않고 실행 파일 위치 혹은 system32에 같이 주어진 ktx.dll을 넣어야 합니다.

 

#include <filesystem>
#include <cstdio>

#define KHRONOS_STATIC
#include "ktx.h"
#pragma comment(lib, "ktx.lib")

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

 

먼저 파일 이름을 명령줄에서 받도록 합니다. 출력 파일 이름은 확장자만 바꾸게 할게요.

 

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::filesystem::path path(argv[0]);
        printf("usage: %s [image file]\n", path.filename().string().c_str());
        return 0;
    }
    std::filesystem::path path(argv[1]);
    path.replace_extension(".ktx2");

 

그 다음 이미지 파일을 읽어옵니다.

 

    int x, y, ch;
    uint8_t* img = stbi_load(argv[1], &x, &y, &ch, 0);
    if (img == nullptr) {
        printf("stbi_load failed. File may be an unavailable image\n");
        return 0;
    }

 

텍스처 객체를 구성합니다.

 

    ktxTexture2* texture;
    ktxTextureCreateInfo info{};
    switch (ch)
    {
    case 1:
        info.vkFormat = 9; // VK_FORMAT_R8_UNORM
        break;
    case 2:
        info.vkFormat = 16; // VK_FORMAT_R8G8_UNORM
        break;
    case 3:
        info.vkFormat = 23; // VK_FORMAT_R8G8B8_UNORM
        break;
    case 4:
        info.vkFormat = 37; // VK_FORMAT_R8G8B8A8_UNORM
        break;
    default:
        break;
    }
    info.baseWidth = x; // 기본 크기
    info.baseHeight = y; // 기본 크기
    info.baseDepth = 1; // 기본 크기
    info.numDimensions = 2; // 이미지 차원수(대부분 2)
    info.numFaces = 1; // 큐브맵일 때만 6, 나머지 1
    info.numLayers = 1; // 이미지 배열일 때 구성 이미지 수
    info.numLevels = 1; // 원하는 밉 수준 수
    info.isArray = KTX_FALSE;
    info.generateMipmaps = KTX_FALSE;
    ktx_error_code_e result = ktxTexture2_Create(&info, KTX_TEXTURE_CREATE_ALLOC_STORAGE, &texture);
    if (result != KTX_SUCCESS) {
        printf("create failed %d\n", result);
        return 0;
    }

 

이미지 데이터를 넘깁니다. 위의 info에 넘긴 format(vkFormat 혹은 glInternalFormat)과 같은 형식을 넘기면 됩니다.

 

    result = ktxTexture_SetImageFromMemory(ktxTexture(texture), 0, 0, 0, img, x * y * ch);
    // 3개의 0은 각각 level, layer, face입니다. 즉 그 밉 수준/레이어(배열)/면(큐브맵)마다 이 함수를 계속 호출해야 합니다.
    if (result != KTX_SUCCESS) {
        printf("image delivery failed %d\n",result);
        return 0;
    }

 

그 다음 Basis 압축을 진행하는데요, 인수의 자세한 사항은 여길 참고해 주세요(어려움). 값들에 0을 주면 기본값을 사용하도록 되어 있는데, compressionLevel는 0이 기본값이 아니지만 유효한 값이라 별도로 주어야 하며, structSize는 그와는 별개로 꼭 주어야 합니다.

 

    ktxBasisParams params{};
    params.compressionLevel = KTX_ETC1S_DEFAULT_COMPRESSION_LEVEL; // ETC1S 압축 시, Higher values are slower, but give higher quality
    params.uastc = KTX_TRUE; // true면 UASTC로 압축합니다. 대체로 품질 저하가 거의 없는 선에서 꽤 많이 압축됩니다.
    params.verbose = KTX_TRUE; // 압축 시 정보를 stdout에 출력합니다.
    params.structSize = sizeof(params); // 고정.
    
    result = ktxTexture2_CompressBasisEx(texture, &params);
    if (result != KTX_SUCCESS) {
        printf("compress failed %d\n", result);
        return 0;
    }

 

마지막으로 그 내용을 파일 혹은 메모리의 값으로 받을 수 있습니다.

 

    result = ktxTexture_WriteToNamedFile(ktxTexture(texture), path.string().c_str());
    if (result != KTX_SUCCESS) {
        printf("write failed %d\n", result);
        return 0;
    }
    /* 메모리에 파일 내용을 쓰기
    uint8_t* mem;
    size_t size;
    result = ktxTexture_WriteToMemory(ktxTexture(texture), &mem, &size);
    if (result != KTX_SUCCESS) {
        printf("fail %d\n",result);
        return 0;
    }
    free(mem); // 위 함수에서 할당된 메모리는 호출자가 해제합니다.
    */

 

메모리 해제는 다음 함수를 쓰면 됩니다.

 

    ktxTexture_Destroy(ktxTexture(texture));
    stbi_image_free(img);
}

 

이렇게 하니 1025KB 정도의 ktx2 파일을 얻었는데요, 동일 사양의 24bpp 픽셀 데이터의 1/3 가량 되네요.

 

 

2.2. KTX 파일 사용하기

그런데 유니버설 압축이라 그런지 내용을 쉽게 볼 수가 없습니다. (github kopaka1822의 ImageViewer를 다운받아 실행하여 확인해볼 수 있습니다.) 어쩔 수 없이 Vulkan에서 샘플링을 하여 보여줄 수밖에 없겠네요. 그걸 위해서는 위에 나온 형식 중 하나로 변형해야 합니다. 선택은 이것이것을 따르도록 하면 되겠지만, 일단 여기서는 보여주기만 하면 되니까 간략화해서 구현하겠습니다.

 

먼저 위에도 나왔던 그 함수입니다.

 

bool isThisFormatAvailable(VkPhysicalDevice physicalDevice, VkFormat format, uint32_t x, uint32_t y, VkImageCreateFlagBits flags = (VkImageCreateFlagBits)0) {
    VkImageFormatProperties props;
    VkResult result = vkGetPhysicalDeviceImageFormatProperties(
        physicalDevice,
        format,
        VK_IMAGE_TYPE_2D,
        VK_IMAGE_TILING_OPTIMAL,
        VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
        flags, // 경우에 따라 VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT
        &props
    );
    return (result != VK_ERROR_FORMAT_NOT_SUPPORTED) &&
        (props.maxExtent.width >= x) &&
        (props.maxExtent.height >= y);
}

VkFormat fallback(VkPhysicalDevice physicalDevice, int x, int y) {
#define CHECK_N_RETURN(f) if(isThisFormatAvailable(physicalDevice,f,x,y)) return f
    CHECK_N_RETURN(VK_FORMAT_ASTC_4x4_SRGB_BLOCK);
    CHECK_N_RETURN(VK_FORMAT_BC7_SRGB_BLOCK);
    CHECK_N_RETURN(VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK);
    CHECK_N_RETURN(VK_FORMAT_BC3_SRGB_BLOCK);
    return VK_FORMAT_R8G8B8A8_SRGB;
#undef CHECK_N_RETURN    
}

 

이것은 이전에 만들었던 함수인 createTex0에서 사용합니다. 바로 아래 코드블록까진 위의 내용 그대로이기 때문에 설명은 생략합니다.

 

bool VkPlayer::createTex0() {
    int w, h, ch;
    unsigned char* pix = readImageFile("no1.png", &w, &h, &ch);
    if (!pix) {
        fprintf(stderr, "Failed to read image file\n");
        return false;
    }

    ktxTexture2* texture;
    ktxTextureCreateInfo k2info{};
    k2info.vkFormat = VK_FORMAT_R8G8B8A8_SRGB;
    k2info.baseWidth = w;
    k2info.baseHeight = h;
    k2info.baseDepth = 1;
    k2info.numDimensions = 2;
    k2info.numFaces = 1;
    k2info.numLayers = 1;
    k2info.numLevels = 1;
    k2info.isArray = KTX_FALSE;
    k2info.generateMipmaps = KTX_FALSE;
    ktx_error_code_e k2result = ktxTexture2_Create(&k2info, KTX_TEXTURE_CREATE_ALLOC_STORAGE, &texture);
    if (k2result != KTX_SUCCESS) {
        fprintf(stderr, "Failed to initialize ktx2 object: %d\n",k2result);
        free(pix);
        return false;
    }
    
    k2result = ktxTexture_SetImageFromMemory(ktxTexture(texture), 0, 0, 0, pix, w * h * ch);
    if (k2result != KTX_SUCCESS) {
        fprintf(stderr, "Failed to set ktx2 data: %d\n",k2result);
        free(pix);
        return false;
    }
    
    ktxBasisParams params{};
    params.uastc = KTX_TRUE;
    params.structSize = sizeof(params);
    k2result = ktxTexture2_CompressBasisEx(texture, &params);
    if (k2result != KTX_SUCCESS) {
        printf("basis compress failed: %d\n",k2result);
        free(pix);
        return false;
    }

 

(KTX 파일이 이미 있었다면, 위의 모든 과정 대신 이렇게 하면 됩니다)

 

k2result = ktxTexture_CreateFromNamedFile("mytex3d.ktx", KTX_TEXTURE_CREATE_NO_FLAGS, &texture);

 

이제 basis 형식을 실제 쓰이는 압축 형식으로 변환(transcode)해야 합니다. KTX_TTF로 시작하는 저 열거형 값은 헤더파일이나 문서에서 직접 내용을 확인하세요. 그냥 쓰려 하면 deprecate된 게 많아서 그렇습니다.

 

    ktx_transcode_fmt_e tf;
    
    VkFormat availableFormat;
    switch (availableFormat = fallback(physicalDevice.card, w, h))
    {
    case VK_FORMAT_ASTC_4x4_SRGB_BLOCK:
        printf("ASTC\n");
        tf = KTX_TTF_ASTC_4x4_RGBA;
        break;
    case VK_FORMAT_BC7_SRGB_BLOCK:
        printf("BC7\n");
        tf = KTX_TTF_BC7_RGBA;
        break;
    case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK:
        printf("ETC2\n");
        tf = KTX_TTF_ETC2_RGBA;
        break;
    case VK_FORMAT_BC3_SRGB_BLOCK:
        printf("BC3\n");
        tf = KTX_TTF_BC3_RGBA;
        break;
    default:
        printf("RGBA\n");
        tf = KTX_TTF_RGBA32;
        break;
    }
    k2result = ktxTexture2_TranscodeBasis(texture, tf, 0);
    if (k2result != KTX_SUCCESS) {
        printf("basis transcode failed: %d\n", k2result);
        free(pix);
        return false;
    }

 

이걸 WriteToMemory한 후 장치에 올려 이미지 장벽을 통과하고 그리면 이렇게 됩니다.

 

왼쪽: 원본 이미지, 오른쪽: 샘플링 후 결과

색깔은 저렇게 나오는 게 맞는데(SRGB 공간으로 취급 후 반전 적용중. 그렇게 된 과정은 이전 글들 참조) 픽셀 배치가 많이 이상합니다. (배치가 어떻게 된 건지 이해하기 위해 잠깐 바둑판식 샘플러를 써 보면 아래와 같이 나옵니다.)

그대로 파일로 저장해 보고 위의 도구로 열어 보면 정상적으로 나옵니다. 즉, 트랜스코드를 했다 하더라도 그 ktx 파일 형식전체를 그대로 넘기는 게 (당연하게도) 잘못된 방법이라는 말이 되겠군요. ktx 헤더(메타데이터)가 들어가지 않는 올바른 방법은 WriteToMemory로 얻은 데이터가 아니라 바로 이겁니다.

 

// VkImage 구성에서 format은 방금 트랜스코드한 형식으로..

VkBufferCreateInfo info{};
info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
info.size = texture->dataSize; // 여기!!!
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

... // 버퍼 요구사항 획득 -> 버퍼 생성 -> 메모리 매핑 후..

memcpy(data, texture->pData, info.size); // 여기!!
ktxTexture_Destroy(ktxTexture(texture));

// 버퍼 -> 이미지 복사하면서 device_local로 장벽을 통과시켜 optimal로 바꾸는 내용

 

이 방법은 ktxTexture_GetData 함수를 쓰는 것과 방식이 같습니다. 공식 문서에 나온 방법인, ktxTexture_VkUploadEx 이후 ktxVulkanTexture 객체의 VkImage 핸들을 사용하는 방식 등은 조만간 다룰 수도 있겠지만 아무래도 지금 알려드린 방법이 VMA 등과 연계하는 것도 더 쉽고 하니 전 이 방법을 쓸 것 같네요. 참고로 밉맵, 큐브맵을 위해 여러 번의 데이터 전달이 필요한 것은 다음 함수를 쓰면 됩니다.

 

ktxTexture_GetImageOffset(texture, level, layer, faceSlice, &offset);

 

오프셋은 저렇게 하면 되고 데이터 크기는 ktxTexture_GetImageSize를 써서 가로/세로를 구한 다음, 블록당 몇 바이트인지로 계산하거나 다음 오프셋과의 차이를 이용하면 됩니다. (LOD, 레이어, 페이스 중에서 뭐가 오프셋이 먼저 넘어가는지는 저도 모르는데 직접 실험해 보면 되니까요.)

 

그런데 이런 식으로 png 파일 같은 걸 불러올 거라면 basis를 안 거치고 바로 적절한 방식으로 압축하는 게 좋은 거 아닌가?
제대로 보았단다. 한 번에 여러 개 설명하려고 이런 것이지. 다만 라이브러리 목적 자체가 그걸 지원할 건 아니었던지라 문서에 안 거치는 방법이 안 나와 있거든. 그런 게 필요하면 다른 라이브러리를 사용하자.

 

전체 코드는 여길 참고하세요.

 

(뭔가 더 작성될 수도 있습니다.)

 

요약

압축 텍스처는 퀄리티 저하 거의 없이 더 가벼운 메모리 사용 혹은 퀄리티 저하를 감수하고 훨씬 가벼운 메모리 사용을 위해 좋은 방식입니다. libktx를 통해 Basis Universal 초과압축을 이용하여 파일 하나로 유연하게 여러 압축 방식을 지원할 수 있고, physical device에 이를 질의하여 선택해 보았습니다.

 

* CMake 구성은 이렇게 ON 했습니다.

BASISU_SUPPORT_SSE

ISA_SSE2

KTX_FEATURE_KTX1

KTX_FEATURE_KTX2

KTX_FEATURE_STATIC_LIBRARY

KTX_FEATURE_VULKAN

 

과제

직접 BasisLZ/ETC1S로도 압축해 보고 파일 크기와 시각적 퀄리티를 비교해 보면 좋겠네요.