사전 텍스처 압축
개념

텍스처 압축

gabury1고친 사람 github-actions[bot]

텍스처 압축은 그림을 미리 다 풀어 두지 않고도, 그래픽 카드가 읽을 때 필요한 조각만 풀어 쓸 수 있는 모양으로 줄여 둡니다. 줄인 채로 그래픽 메모리에 올리므로 같은 메모리에 그림을 몇 배 더 담습니다. 파일만 작게 만드는 흔한 그림 압축과 달리 그리는 동안에도 작은 크기가 유지됩니다.

쉽고 빠른 이해

텍스처 압축은 게임 속 벽이나 옷에 입히는 그림을 그래픽 카드 전용 방식으로 줄여 두는 일입니다. 벽돌 그림 한 장이 그래픽 메모리를 원래의 몇 분의 일만 차지합니다.

흔한 그림 파일은 화면에 그리기 전에 원래 크기로 다 풀어야 합니다. 그러면 그래픽 메모리에는 결국 큰 그림이 올라갑니다. 그림이 많은 게임은 메모리가 금방 찹니다.

어떻게 도나:

  1. 그림을 가로세로 네 칸씩 작은 조각으로 자릅니다
  2. 조각마다 대표 색 둘을 적습니다. 두 색과 그 사이 색으로 쓸 색을 몇 가지 만듭니다. 칸마다 그 가운데 어느 색에 가까운지 짧은 번호만 적습니다
  3. 조각마다 차지하는 바이트가 같아서 그래픽 카드는 필요한 조각 하나만 찾아 읽고 바로 풉니다

대가는 화질입니다. 한 조각 안에 색이 많으면 뭉개집니다. 글자나 아이콘처럼 뭉개지면 티가 나는 그림에는 쓰지 않습니다.

줄이는 작업은 오래 걸립니다. 기기마다 읽는 방식이 달라 그림을 여러 벌 준비하기도 합니다.

상세

이 절은 텍스처 압축이 흔한 그림 압축과 왜 다른 길을 가는지, 조각 하나를 어떻게 적고 어떻게 찾아 읽는지를 다룹니다. 가로세로 2048칸짜리 그림 한 장을 놓고 따라갑니다.

번호가 붙은 사물함 한 줄을 떠올려 봅시다. 칸이 모두 같은 크기면 57번 사물함이 몇 걸음 앞에 있는지 세지 않아도 압니다. 칸 크기가 제각각이면 앞의 칸을 하나씩 지나가며 세어야 합니다.

텍스처 압축은 앞쪽을 고릅니다. 그림을 같은 크기의 조각으로 나눕니다. 조각마다 같은 바이트 수로 적습니다. 그러면 원하는 조각이 어디 있는지 셈 한 번으로 찾습니다.

텍스처는 물체 표면에 입힐 그림을 격자로 담은 데이터입니다. 격자 칸 하나를 텍셀이라고 부릅니다. 이 편에서는 그냥 칸이라고 쓰겠습니다.

텍스처를 읽어 화면을 칠하는 장치가 GPU(Graphics Processing Unit, 그래픽 처리 장치)입니다. GPU 가 텍스처를 올려 두는 전용 메모리를 VRAM(Video Random Access Memory, 그래픽 메모리)이라고 합니다. 텍스처 압축이 아끼려는 것이 이 VRAM 입니다.

그림 파일 압축이 못 하는 일

먼저 PNG(Portable Network Graphics)나 JPEG(Joint Photographic Experts Group)로 줄인 그림을 VRAM 에 올려 쓰면 안 되는 까닭부터 봅니다. 이런 파일은 그림마다, 그리고 한 그림 안에서도 곳곳마다 줄어드는 정도가 다릅니다. 무늬가 단순한 곳은 많이 줄고 복잡한 곳은 조금 줍니다.

그래서 그림 한가운데 칸이 파일의 몇 번째 바이트에 있는지 미리 알 수 없습니다. 앞에서부터 풀어 가야 거기 닿습니다.

GPU 는 화면 하나를 그릴 때 수백만 개의 점을 칠합니다. 점마다 텍스처의 아무 칸이나 골라 읽습니다. 이렇게 아무 칸이나 골라 곧바로 읽는 것을 임의 접근이라고 합니다.

앞에서부터 풀어야 하는 파일은 임의 접근을 버티지 못합니다. 그래서 PNG 나 JPEG 는 VRAM 에 올리기 전에 원래 크기로 다 풉니다. 내려받는 파일은 작아도 VRAM 에서는 원래 크기를 차지합니다.

flowchart TD
    subgraph S1["그림 파일 압축"]
        A1["PNG · JPEG 파일"] --> A2["원래 크기로 다 푼다"] --> A3["VRAM 에 원래 크기로 올린다"]
    end
    subgraph S2["텍스처 압축"]
        B1["압축 텍스처"] --> B2["VRAM 에 줄인 채로 올린다"] --> B3["GPU 가 읽을 때 조각 하나만 푼다"]
    end
    S1 ~~~ S2

위쪽 길은 파일 크기만 줄입니다. 아래쪽 길은 VRAM 에 올라간 뒤에도 작습니다.

같은 크기의 블록

텍스처 압축은 그림을 가로세로 4칸씩, 16칸짜리 조각으로 자릅니다. 이 조각을 블록이라고 부릅니다.

블록은 무엇을 담든 같은 바이트 수로 적습니다. 단순한 블록이라고 덜 쓰지 않습니다. 복잡한 블록이라고 더 쓰지도 않습니다. 줄어드는 비율이 늘 같아서 이런 방식을 고정 비율 압축이라고 합니다.

비율을 고정하면 잃는 것이 있습니다. 복잡한 블록도 정해진 바이트 안에 욱여넣어야 하므로 원래 색을 다 지키지 못합니다. 이 대가는 아래 「잃는 화질」에서 봅니다.

블록 하나를 적는 방법

가장 널리 알려진 방식인 BC1(Block Compression 1, 블록 압축 1번)으로 블록 하나를 적어 봅니다. BC1 은 16칸의 색을 하나하나 적지 않습니다. 대표 색 둘만 적습니다. 두 색은 블록의 색들을 잘 감싸도록 압축 도구가 고릅니다.

색 하나는 빨강·초록·파랑 세 값으로 되어 있습니다. 이 세 값을 좌표로 삼아 색을 공간의 한 점으로 보면, 두 대표 색 사이는 곧은 선 하나가 됩니다.

이 선을 셋으로 나누면, 나누는 점에 색이 둘 더 생깁니다. 그러면 쓸 수 있는 색이 넷이 됩니다. 칸마다 이 넷 가운데 어느 것에 가장 가까운지 번호만 적습니다. 넷 중 하나를 가리키는 데는 2비트면 됩니다.

block-beta
columns 4
  A["대표 색 1 · 16비트"]:2 B["대표 색 2 · 16비트"]:2
  C["16칸의 번호 · 칸마다 2비트 · 모두 32비트"]:4

대표 색 하나는 16비트입니다. 빨강 5비트, 초록 6비트, 파랑 5비트로 줄여 적습니다. 번호 16개가 32비트이니 블록 하나는 합쳐 64비트, 곧 8바이트입니다.

압축하지 않은 그림은 흔히 칸마다 빨강·초록·파랑·투명도를 1바이트씩, 4바이트로 담습니다. 그러면 16칸이 64바이트입니다. 그것이 8바이트가 되니 8분의 1로 줄어듭니다.

원하는 칸을 찾아 읽기

블록이 모두 8바이트이므로 읽을 칸이 주어지면 그 칸이 든 블록의 위치를 셈으로 바로 냅니다. 가로 2048칸 그림에서 가로 1000번째, 세로 600번째 칸을 찾는 셈을 코드로 적으면 이렇습니다. 오른쪽 주석이 그 줄에서 나오는 값입니다.

Python
x, y = 1000, 600
bx = x // 4            # 250
by = y // 4            # 150
per_row = 2048 // 4    # 512
index = by * per_row + bx   # 77050
offset = index * 8     # 616400

블록의 가로세로 번호를 구합니다. 한 줄에 블록이 몇 개인지로 몇 번째 블록인지를 냅니다. 거기에 블록 크기 8바이트를 곱하면 그 블록이 시작하는 바이트가 나옵니다. 앞의 블록은 하나도 풀지 않았습니다.

GPU 는 이 8바이트만 가져옵니다. 대표 색 둘로 네 색을 만듭니다. 그 칸의 번호로 한 색을 고릅니다. 이 풀기는 GPU 안의 전용 회로가 텍스처를 읽는 길에서 합니다. 그래서 프로그램 쪽에서는 압축하지 않은 텍스처를 읽을 때와 똑같이 읽습니다.

줄어드는 메모리와 대역폭

압축 텍스처는 줄인 채로 VRAM 에 올라가므로 VRAM 사용량이 줄어듭니다. 가로세로 2048칸 그림 한 장으로 재면 이렇습니다.

Python
w, h = 2048, 2048
raw = w * h * 4               # 16 MiB
bc1 = (w // 4) * (h // 4) * 8 # 2 MiB

칸마다 4바이트로 담으면 16MiB(메비바이트, 1MiB 는 1024×1024바이트)입니다. BC1 로 담으면 2MiB 입니다. 같은 VRAM 에 그림을 여덟 장 담을 수 있다는 뜻입니다.

줄어드는 것은 VRAM 만이 아닙니다. 메모리에서 GPU 로 한 번에 실어 나를 수 있는 양을 대역폭이라고 합니다. GPU 는 칠하는 점마다 텍스처를 읽으므로 이 대역폭이 자주 막히는 길목이 됩니다. 8바이트로 16칸을 가져오면 같은 대역폭으로 더 많은 칸을 읽습니다. 그래서 텍스처를 압축하면 그리기가 빨라지기도 합니다.

잃는 화질

텍스처 압축은 손실 압축입니다. 풀어 낸 색이 원래 색과 조금 다릅니다. BC1 에서는 16칸이 네 색 가운데 하나로만 칠해지기 때문입니다.

블록 하나에 전혀 다른 색이 셋 이상 모이면 가장 티가 납니다. 네 색은 두 대표 색을 잇는 그 선 위에만 놓입니다. 빨강·초록·파랑이 한 블록에 섞이면 선 하나로는 셋을 다 지나가지 못해 색이 번지거나 얼룩이 집니다.

손상이 블록마다 조금씩 다르게 나면 블록 경계를 따라 네모 무늬가 보입니다. 이것을 블록 아티팩트라고 부릅니다.

사진이나 돌·나무 결 같은 그림에서는 이 차이가 잘 안 보입니다. 텍스처는 늘이고 돌리고 조명과 섞어 칠하므로 칸 하나하나의 색이 그대로 드러나는 일이 드뭅니다.

압축하는 쪽과 푸는 쪽의 비용

푸는 쪽은 단순하고 빨라야 합니다. 칠하는 점마다 풀기 때문입니다. 반대로 대표 색과 번호를 고르는 쪽은 오래 걸려도 됩니다. 한 번만 하면 되기 때문입니다. 이렇게 한쪽에 일을 몰아 둔 짜임을 비대칭 압축이라고 부릅니다.

압축 도구는 화질을 높이려고 대표 색 후보를 여럿 시험해 보느라 오래 걸립니다. 그래서 압축은 게임을 실행할 때가 아니라 만들 때 미리 해 둡니다.

원본 그림을 기기에 맞는 모양으로 바꿔 두는 이 단계를 에셋 파이프라인이 맡습니다. 엔진에 따라 이 단계를 쿠킹, 곧 텍스처를 굽는다고도 부릅니다. 그림을 그리는 사람은 PNG 같은 원본을 넘깁니다. 압축 텍스처는 빌드가 만듭니다.

기기마다 다른 계열

블록 압축 방식은 하나가 아닙니다. GPU 마다 회로로 풀 수 있는 방식이 정해져 있습니다. 기기에 따라 쓰는 계열이 갈리는 까닭입니다. 대표적인 것이 BC1 이 속한 BC(Block Compression) 계열, ETC(Ericsson Texture Compression) 계열, ASTC(Adaptive Scalable Texture Compression) 셋입니다.

계열 주로 읽는 기기 블록 한 개
BC 계열 데스크톱 PC · 콘솔 4×4 칸 · 8바이트 또는 16바이트
ETC 계열 안드로이드 같은 모바일 기기 4×4 칸 · 8바이트 또는 16바이트
ASTC 최근의 모바일 기기 칸 수는 골라 정함 · 늘 16바이트

표의 16바이트짜리 블록은 8바이트 블록보다 두 배를 써서 더 많은 것을 적습니다. 대신 압축률은 절반입니다.

더 적는 방법의 하나는 블록 안을 여러 구역으로 나누는 것입니다. 구역마다 대표 색 둘을 따로 둡니다. 그러면 색이 여럿 섞인 블록도 덜 뭉개집니다. BC 계열에서는 BC7(Block Compression 7)이 이 방법을 씁니다.

ASTC 는 블록이 늘 16바이트입니다. 그 안에 담을 칸 수를 고릅니다. 칸을 적게 담으면 화질이 오릅니다. 많이 담으면 압축률이 오릅니다. 그림마다 둘 사이를 조절하려고 나온 방식입니다.

한 벌로 여러 기기에 보내기

한 기기가 모든 계열을 읽지는 못합니다. 데스크톱용으로 BC 계열로 구운 텍스처는 모바일 GPU 가 대개 풀지 못합니다. 그래서 여러 기기에 내는 게임은 기기마다 텍스처를 따로 굽습니다.

웹처럼 어떤 기기에서 열릴지 모르는 곳에서는 따로 굽기가 어렵습니다. 이때는 어느 계열로든 빨리 바꿀 수 있는 중간 모양으로 한 벌만 담아 둡니다. 실행하는 기기가 그것을 자기가 읽는 계열로 바꿉니다. 이렇게 한 압축 모양을 다른 압축 모양으로 바꾸는 일을 트랜스코딩이라고 합니다.

flowchart TD
    A["원본 그림"] --> B["중간 모양 한 벌 · 만들 때 한 번"]
    B --> C{"실행하는 기기가 읽는 계열"}
    C -->|데스크톱| D["BC 계열로 바꾼다"]
    C -->|모바일| E["ETC 나 ASTC 로 바꾼다"]
    D --> F["VRAM 에 올린다"]
    E --> F

Basis Universal이 이런 중간 모양의 대표입니다. 한 벌만 담아 두었다가 실행하는 기기에서 BC·ETC·ASTC 가운데 그 기기가 읽는 계열로 바꿉니다.

KTX2(Khronos Texture 2)는 텍스처를 담는 파일 포맷입니다. Basis Universal 로 만든 중간 모양도 이 파일에 담깁니다. 3D 모델을 담는 포맷인 glTF(GL Transmission Format)는 확장 기능, 곧 기본 규칙에 덧붙이는 선택 규칙을 써서 모델의 텍스처로 KTX2 파일을 가리킵니다.

밉맵과 블록

밉맵은 한 그림을 절반씩 줄인 장을 여러 장 함께 두는 방식입니다. 멀리 있는 물체에는 작게 줄인 장을 읽히려고 둡니다. 텍스처 압축은 이 장들을 하나씩 따로 압축합니다.

장이 가로세로 4칸보다 작아져도 블록 하나를 다 씁니다. 가로세로 2칸짜리 장도 4칸 블록 하나를 차지합니다. 남는 칸은 버립니다. 작은 장에서 이렇게 버리는 칸은 피할 수 없습니다.

맨 큰 장은 사정이 다릅니다. 가로세로가 4의 배수가 아니면 오른쪽과 아래 가장자리의 블록마다 빈 칸이 생겨 버려집니다. 가로세로를 4의 배수로 맞춰 두면 맨 큰 장에서는 버리는 칸이 없습니다. 압축할 그림의 가로세로를 흔히 4의 배수로 맞추는 까닭입니다.

압축하지 않는 텍스처

글자나 아이콘처럼 화면의 점과 칸이 하나씩 맞게 붙는 그림은 흔히 압축하지 않습니다. 늘이거나 돌리지 않고 보이므로 뭉개진 블록이 눈에 바로 띕니다.

GPU 가 그린 결과를 받아 적는 텍스처도 압축하지 않습니다. 이런 텍스처를 렌더 타깃이라고 합니다.

GPU 는 점을 하나씩 칠해 그 결과를 적습니다. 점 하나를 적을 때마다 블록 전체의 대표 색을 다시 고를 수는 없습니다. 그러니 블록 압축 포맷은 그리기의 결과를 받는 데 쓰지 못합니다.

관련 항목

텍스처 압축이 줄여 담는 그림 자원

텍스처 · 텍셀 · 밉맵 · 밉 레벨 · 노멀 맵 · 텍스처 아틀라스 · 큐브 맵 · 렌더 타깃

텍스처 압축에 쓰이는 블록 압축 포맷

BC1 · BC3 · BC4 · BC5 · BC6H · BC7 · S3TC · ETC · ETC2 · ASTC · PVRTC

압축 텍스처를 담아 나르는 파일과 중간 포맷

KTX2 · DDS · Basis Universal · 트랜스코딩 · glTF · GPU 텍스처 포맷

텍스처 압축이 아끼는 하드웨어 자원

GPU · VRAM · 대역폭 · 텍스처 캐시 · 텍스처 샘플링 · 샘플러 · 임의 접근

텍스처 압축과 맞세워지는 그림 파일 압축

압축 · 손실 압축 · 무손실 압축 · PNG · JPEG · WebP · 엔트로피 부호화

텍스처 압축을 미리 해 두는 제작 단계

에셋 파이프라인 · 쿠킹 · 비대칭 압축 · 텍스처 스트리밍 · 게임 개발

텍스처 압축이 남기는 화질 손상

블록 아티팩트 · 밴딩 · 양자화 · 압축 아티팩트

다른 이름: texture compression · compressed texture · 압축 텍스처