사전 밉맵
자료구조

밉맵

gabury1고친 사람 github-actions[bot]

밉맵은 멀리 있어 작게 보이는 물체의 무늬가 지글거리는 것을 줄여 줍니다. 그림을 미리 여러 크기로 줄여 둡니다. 그리고 화면에 보이는 크기에 맞는 장을 골라 읽습니다. 그 대가로 메모리를 원본의 삼분의 일쯤 더 씁니다.

쉽고 빠른 이해

밉맵은 같은 그림을 절반씩 줄인 여러 장을 한 벌로 묶어 둡니다. 멀리 있는 벽에 벽돌 그림을 입히면 원본이 아니라 작게 줄인 장에서 색을 읽습니다.

이게 없으면 화면의 점 하나가 그림의 칸 여러 개를 덮어도 색은 몇 칸에서만 읽힙니다. 물체가 조금만 움직여도 읽히는 칸이 바뀝니다. 그래서 무늬가 지글거립니다.

어떻게 도나:

  1. 그림을 올릴 때 각 변을 절반으로 줄인 장을 한 칸이 될 때까지 만들어 둡니다
  2. 그리는 동안 물체가 화면에서 얼마나 줄어 보이는지 잽니다
  3. 그 크기에 맞는 장을 골라 읽습니다. 두 장 사이에 걸치면 양쪽에서 읽어 섞습니다

대가는 메모리입니다. 줄인 장을 다 합치면 원본의 삼분의 일쯤이 더 듭니다. 그래서 줄어 보일 일이 없는 그림에는 두지 않습니다. 원래 크기로만 그리는 그림이나, 그림이 아니라 값을 찾아 쓰는 표가 그렇습니다. 비스듬히 누운 면은 필요 이상으로 흐려지기도 합니다.

상세

이 절은 밉맵이 무엇을 막으려고 생겼는지, 어떤 모양으로 쌓이는지, 그리는 동안 어느 장이 읽히는지를 다룹니다. 벽돌 그림 한 장을 입힌 긴 벽 하나를 놓고 따라갑니다. 벽은 가까운 쪽이 크게 보입니다. 먼 쪽은 작게 보입니다.

축척이 다른 지도를 여러 장 묶은 지도책을 떠올려 봅시다. 나라 전체를 볼 때는 축척이 작은 장을 폅니다. 골목을 찾을 때는 축척이 큰 장을 폅니다.

밉맵도 같은 그림을 크기별로 여러 장 만들어 둡니다. 그리고 그때그때 알맞은 한 장을 폅니다.

밉맵이 줄여 쌓는 그림은 텍스처입니다. 텍스처는 물체 표면에 입힐 그림을 격자로 담은 자원입니다. 그 격자의 칸 하나가 텍셀입니다. 이 편에서는 칸이라고 쓰겠습니다.

줄인 장이 없을 때 생기는 깨짐

밉맵이 없으면 읽을 장이 원본 하나뿐입니다. 벽의 먼 쪽에서는 화면의 점 하나가 원본의 가로 두 칸·세로 두 칸, 모두 칸 넷을 덮습니다. 가장 단순하게 읽으면 색은 그중 한 칸에서만 가져옵니다. 나머지 셋의 색은 화면에 나타나지 못합니다.

block-beta
columns 2
  H1["원본을 읽을 때 · 점 하나가 가로 둘·세로 둘, 칸 넷을 덮는다"]:2
  a1["읽는 칸"] a2["건너뛴 칸"]
  a3["건너뛴 칸"] a4["건너뛴 칸"]
  H2["한 번 줄인 장을 읽을 때 · 그 칸들이 그리기 전에 한 칸으로 섞여 있다"]:2
  b1["읽는 칸 · 덮인 칸들을 섞은 값"]:2

가까운 네 칸을 섞어 읽어도 사정은 비슷합니다. 덮는 칸이 수십 개로 늘면 넷은 그 일부일 뿐입니다. 덮인 칸의 일부만 읽으면 물결 무늬나 들쭉날쭉한 경계가 생길 수 있습니다. 읽는 칸이 많을수록 이런 깨짐은 덜 드러납니다.

벽이 조금 움직이면 덮는 위치가 밀립니다. 그러면 읽히는 칸이 다른 칸으로 건너뜁니다. 그래서 무늬가 반짝이고 지글거립니다. 이런 깨짐을 에일리어싱이라고 합니다.

격자무늬처럼 규칙적인 그림에서는 원본에 없던 물결 무늬가 생기기도 합니다. 이것이 모아레입니다.

덮인 칸을 전부 읽으면 깨짐은 줄어듭니다. 그러나 멀어질수록 덮는 칸이 불어납니다. 점 하나를 칠하는 비용도 함께 불어납니다. 게다가 벽이 움직이면 덮는 모양과 크기가 장면마다 바뀝니다.

밉맵은 이 섞기를 그릴 때가 아니라 그리기 전에 해 둡니다.

절반씩 줄인 사슬

밉맵은 원본을 레벨 0 으로 삼습니다. 각 변을 절반으로 줄인 장이 레벨 1 입니다. 그것을 다시 절반으로 줄인 장이 레벨 2 입니다. 이렇게 긴 변이 한 칸이 될 때까지 이어지는 사슬의 한 장 한 장을 밉 레벨이라고 부릅니다.

block-beta
columns 8
  T["아래로 갈수록 한 변이 절반이다"]:8
  L0["레벨 0 · 한 변 256 칸"]:8
  L1["레벨 1 · 한 변 128 칸"]:4 space:4
  L2["레벨 2 · 한 변 64 칸"]:2 space:6
  L3["레벨 3 · 한 변 32 칸"]:1 space:7

변의 길이가 홀수면 절반에서 소수점 아래를 버립니다. 한 변이 300 칸이면 150, 75, 37, 18 순서로 줄어듭니다.

아래 레벨은 바로 위 레벨을 줄여 만듭니다. 원본에서 시작해 한 레벨씩 거듭 줄여 내려갑니다. 줄일 때 위 레벨의 이웃 칸들을 어떤 무게로 섞을지 정한 규칙이 필터입니다.

줄이는 필터는 하나로 정해져 있지 않습니다. 기본으로는 상자 필터가 권장됩니다. 상자 필터는 가로 둘·세로 둘, 이웃한 네 칸의 평균을 냅니다. 이 필터로 줄이면 아래 레벨의 한 칸에는 위 레벨 네 칸 몫의 색이 담깁니다.

원본 레벨에서 아래 레벨 전부를 이렇게 거듭 줄여 채우는 일을 이 편에서는 사슬 채우기라고 쓰겠습니다. 사슬 채우기는 아래 레벨에 있던 내용을 무엇이든 덮어씁니다. 원본 레벨은 건드리지 않습니다.

어느 레벨을 읽을지 고르기

고르는 일은 밉맵이 아니라 읽는 쪽이 합니다. 텍스처를 읽는 규칙을 모아 둔 것이 샘플러입니다. 레벨 고르기도 그 규칙의 하나입니다.

화면의 점 하나는 그림 위에서 타원 모양의 자국을 덮습니다. 이웃한 화면 점 둘이 그림의 어느 지점을 가리키는지를 견주면 그 자국의 크기를 알 수 있습니다. 이 크기는 넓이가 아니라 타원의 긴 지름, 곧 길이로 잽니다.

긴 지름이 칸 하나면 레벨 0, 칸 둘이면 레벨 1, 칸 넷이면 레벨 2 입니다. 긴 지름이 두 배가 될 때마다 레벨이 한 장씩 내려갑니다.

깨짐 소절의 가로 둘·세로 둘 자국은 긴 지름이 두 칸이라 레벨 1 을 고릅니다. 레벨 1 의 한 칸은 원본의 가로 둘·세로 둘을 줄여 만든 값입니다. 그래서 한 칸만 읽어도 덮인 네 칸의 색이 들어옵니다.

자국이 칸 하나보다 작으면 그림이 확대되어 보이는 것입니다. 이때는 줄인 장을 고를 일이 없습니다. 밉맵은 그림이 줄어 보일 때를 위한 구조입니다.

한 장 안에서도 한 칸만 집는 대신 여러 칸을 섞어 읽기도 합니다. 읽을 지점에 가장 가까운 네 칸에 무게를 달리 주어 평균을 냅니다. 이것이 쌍선형 필터링입니다.

레벨 번호는 대개 정수로 떨어지지 않습니다. 그때는 위아래 두 레벨에서 각각 읽습니다. 그리고 소수 부분만큼의 비율로 두 값을 섞습니다. 레벨 번호가 2.3 이면 레벨 2 의 값에 0.7, 레벨 3 의 값에 0.3 의 무게를 줍니다.

flowchart TD
    A["자국의 긴 지름을 잰다 · 예: 다섯 칸"] --> B["레벨 번호가 약 2.3 으로 나온다"]
    B --> C["레벨 2 에서 가까운 네 칸을 섞어 읽는다 · 무게 0.7"]
    B --> D["레벨 3 에서 가까운 네 칸을 섞어 읽는다 · 무게 0.3"]
    C --> E["두 값을 무게대로 더한다"]
    D --> E

두 레벨 안에서 각각 쌍선형으로 읽습니다. 그다음 두 레벨 사이를 한 번 더 섞습니다. 가로·세로·레벨 사이로 섞기가 셋이라 이 방식을 삼선형 필터링이라고 합니다.

섞지 않고 가장 가까운 레벨 하나만 읽는 방법도 있습니다. 읽는 장이 하나라 읽는 칸 수가 절반으로 줍니다. 대신 한 레벨에서 다음 레벨로 넘어가는 경계가 눈에 띌 수 있습니다. 두 레벨을 섞어 읽으면 그 넘어감을 덜 드러나게 할 수 있습니다.

이름과 원 논문

밉맵의 밉(MIP)은 라틴어 multum in parvo 의 머리글자입니다. 「작은 공간에 많은 것을」이라는 뜻입니다.

1983년 Lance Williams 가 「Pyramidal Parametrics」 라는 논문으로 이 구조를 내놓았습니다. 컴퓨터 그래픽스 학회 SIGGRAPH(Special Interest Group on Computer Graphics) 에서 발표한 논문입니다. 그는 이 구조를 섞어 줄여 둔 원본 그림의 묶음으로 세웠습니다.

이 방식은 애니메이션에서 특히 이득을 볼 수 있습니다. 애니메이션은 같은 원본 그림을 쓰는 장면을 수없이 그려야 하기 때문입니다. 한 번 섞어 둔 묶음을 그 장면들이 함께 씁니다.

복잡도

밉맵은 메모리를 더 쓰는 대신 점 하나를 칠할 때 읽는 칸 수를 물체의 거리와 상관없이 몇 칸으로 묶어 둡니다. 정사각형 그림이면 더 드는 메모리는 원본의 삼분의 일 미만입니다. 평면 그림에서 읽는 칸은 여덟 칸 이하입니다.

아래 표는 공간과 레벨 수, 읽기(레벨 고르기와 칸 읽기), 쓰기(사슬 채우기)를 나란히 놓습니다. 이 절의 n 은 원본 레벨의 칸 수입니다. 한 변이 256 칸인 정사각형 그림이면 n 은 65,536 입니다.

연산 비용 왜 그 값인가
공간 정사각형이면 원본의 4/3 배 미만 두 변이 함께 절반이 되는 동안 레벨마다 칸 수가 사분의 일이라 더해지는 몫이 n/3 을 넘지 않는다. 길쭉한 직사각형은 더 들지만 원본의 두 배를 넘지 않는다
레벨 수 ⌊log₂(긴 변)⌋ + 1 장 긴 변을 절반으로 나눌 수 있는 횟수에 원본 한 장을 더한다
레벨 고르기 점마다 상수 시간 이웃 점과의 좌표 차이로 자국 크기를 재고 로그를 한 번 취한다
읽기 평면 그림에서 점마다 1 · 2 · 4 · 8 칸 한 레벨에서 한 칸이나 네 칸, 두 레벨에서 한 칸씩이나 네 칸씩. 거리와 무관하다
사슬 채우기 n 에 비례 원본에서 아래 레벨 전부를 차례로 줄여 채운다. 정사각형이면 새로 쓰는 칸은 n/3 미만이다

공간이 삼분의 일에서 멈추는 까닭

한 변이 절반이면 칸 수는 사분의 일입니다. 정사각형 그림은 레벨을 내려갈 때마다 칸 수가 사분의 일씩 줄어듭니다.

레벨 한 변 칸 수가 원본 대비
0 256 칸 1
1 128 칸 사분의 일
2 64 칸 십육분의 일
3 32 칸 육십사분의 일

레벨 1 부터 끝까지 더해도 원본의 삼분의 일에 못 미칩니다. 원본까지 합친 사슬 전체는 원본의 4/3 배보다 작습니다. 변이 홀수라 소수점 아래를 버리는 레벨이 끼면 그만큼 더 작아집니다.

길쭉한 직사각형은 다릅니다. 짧은 변이 먼저 한 칸에 닿으면 그 뒤 레벨에서는 긴 변만 줄어 칸 수가 절반씩만 줍니다. 가로 1024 칸, 세로 2 칸이면 원본 2,048 칸에 줄인 장 1,023 칸이 붙어 원본의 약 1.5 배가 됩니다. 그래도 레벨마다 칸 수가 적어도 절반으로 줄어서 사슬 전체는 원본의 두 배를 넘지 않습니다.

레벨 수와 레벨 번호

레벨 수는 긴 변이 정합니다. 한 변이 256 칸이면 아홉 장입니다. 가로 1024 칸, 세로 512 칸이면 열한 장입니다. 한 변이 300 칸이면 300 에서 1 까지 아홉 장입니다.

레벨 번호는 자국의 긴 지름을 칸 단위로 잰 값의 log₂ 입니다. 긴 지름이 칸 넷이면 2, 칸 다섯이면 약 2.3 입니다. 이 값이 0 이하면 그림이 확대되어 보이는 경우입니다. 이 값이 마지막 레벨 번호에 닿거나 넘으면 가장 작은 레벨에서 읽습니다.

flowchart TD
    A["레벨 번호를 구한다"] --> B{"0 이하인가"}
    B -->|"예"| C["확대되어 보인다 · 줄인 장을 고르지 않는다"]
    B -->|"아니오"| D{"마지막 레벨 번호 이상인가"}
    D -->|"예"| E["가장 작은 레벨에서 읽는다"]
    D -->|"아니오"| F["걸친 위아래 레벨에서 읽는다"]

긴 지름을 정확히 구하는 계산은 번거로울 수 있습니다. 그래서 구현은 이 값을 근사하는 함수로 대신할 수 있습니다. 계산된 레벨 번호에 치우침 값을 더해 일부러 더 선명한 레벨이나 더 흐린 레벨을 고르게 하기도 합니다.

평균과 최악이 갈리지 않는 읽기

해시테이블은 키가 한 버킷에 몰리면 조회가 느려집니다. 밉맵 읽기에는 그런 최악이 없습니다. 평면 그림이라면 고르는 데 상수 시간, 읽는 데 많아야 여덟 칸입니다. 그림의 내용이나 물체의 거리에 따라 비용이 흔들리지 않습니다.

밉맵 없이 덮인 칸을 전부 섞어 읽으면 비용이 덮인 칸 수에 비례합니다. 멀어질수록 그 수가 불어납니다. 밉맵은 이 비용을 사슬 채우기 한 번과 메모리 약 삼분의 일로 바꿔 둔 것입니다.

거리에 따라 갈리는 것은 비용이 아니라 화질입니다. 면이 비스듬히 누울수록 흐려지는 문제는 대가 절이 다룹니다.

예시

OpenGL 의 glGenerateMipmap 과 GL_TEXTURE_MIN_FILTER

OpenGL 에서 밉맵을 쓰려면 두 가지를 정합니다. 사슬을 채우는 호출 하나와, 줄어 보일 때의 읽기 규칙을 정하는 값 하나입니다.

C
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

첫 줄이 사슬 채우기입니다. 원본 레벨에서 아래 레벨 전부를 거듭 줄여 채웁니다. 아래 레벨에 있던 내용은 무엇이든 덮어씁니다. 원본 레벨은 손대지 않습니다.

칸 하나에 색을 어떤 꼴로 담는지를 내부 형식이라고 합니다. 줄인 장들의 내부 형식은 원본과 같습니다.

둘째 줄은 줄어 보일 때 두 레벨을 골라 각각 네 칸을 섞어 읽으라는 뜻입니다. 그 두 값을 다시 섞습니다. 상세 절의 삼선형 필터링입니다.

GL_TEXTURE_MIN_FILTER 에 줄 수 있는 값은 여섯입니다. 그중 넷이 밉맵을 씁니다. 이름의 앞 낱말은 한 레벨 안에서 읽는 방식입니다. 뒤 낱말은 레벨을 고르는 방식입니다.

값 읽는 레벨 한 레벨 안에서
GL_NEAREST 원본만 가장 가까운 한 칸
GL_LINEAR 원본만 가장 가까운 네 칸의 가중 평균
GL_NEAREST_MIPMAP_NEAREST 크기가 가장 맞는 한 장 가장 가까운 한 칸
GL_LINEAR_MIPMAP_NEAREST 크기가 가장 맞는 한 장 네 칸의 가중 평균
GL_NEAREST_MIPMAP_LINEAR 크기가 가장 맞는 두 장을 섞음 가장 가까운 한 칸
GL_LINEAR_MIPMAP_LINEAR 크기가 가장 맞는 두 장을 섞음 네 칸의 가중 평균

기본값은 GL_NEAREST_MIPMAP_LINEAR 입니다. GL_NEAREST 와 GL_LINEAR 는 나머지 넷보다 빠를 수 있습니다. 대신 읽는 칸이 적어 물결 무늬나 들쭉날쭉한 경계를 낼 수 있습니다.

Vulkan 의 VkSamplerCreateInfo

Vulkan 은 같은 선택을 샘플러를 만들 때 넘기는 구조체에 담습니다. 한 레벨 안에서 읽는 방식과 레벨을 고르는 방식을 두 필드로 가릅니다.

C
VkSamplerCreateInfo info = {
    /* 나머지 필드 생략 */
    .minFilter  = VK_FILTER_LINEAR,
    .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
    .maxLod     = VK_LOD_CLAMP_NONE,
};

위 코드처럼 minFilter 를 VK_FILTER_LINEAR 로, mipmapMode 를 VK_SAMPLER_MIPMAP_MODE_NEAREST 로 두면 OpenGL 의 GL_LINEAR_MIPMAP_NEAREST 와 같습니다. 한 장을 골라 그 안에서 네 칸을 섞어 읽습니다.

필드 이름의 LOD(Level of Detail, 세부 수준)는 이 편의 레벨 번호를 가리킵니다.

필드 무엇을 정하나
mipmapMode VK_SAMPLER_MIPMAP_MODE_NEAREST 와 VK_SAMPLER_MIPMAP_MODE_LINEAR 둘 중 하나. 레벨 사이를 가장 가까운 쪽으로 고를지 섞을지
mipLodBias 계산된 레벨 번호에 더할 치우침 값
minLod 계산된 레벨 번호의 아래 한계
maxLod 계산된 레벨 번호의 위 한계. VK_LOD_CLAMP_NONE 이면 위 한계를 두지 않는다

WebGL 의 generateMipmap

웹 브라우저의 WebGL 에서는 generateMipmap() 이 사슬을 채웁니다. 텍스처의 원본 해상도에서 시작해 해상도를 절반씩 줄입니다. 1×1 짜리 장이 만들어지면 멈춥니다. 가까운 물체에는 해상도가 높은 장이, 먼 물체에는 해상도가 낮은 장이 쓰입니다.

대가

이 절은 밉맵을 켜면 무엇을 치르는지를 다섯 가지로 봅니다. 대상은 밉맵을 단 텍스처 한 장과 그것을 다루는 쪽의 제약입니다. 메모리, 비스듬한 면의 흐릿함, 사슬 채우기 비용, 아틀라스의 번짐, 크기 제약 순서입니다.

삼분의 일 더 드는 메모리

Unity 에서는 밉맵이 텍스처 크기를 디스크와 메모리 양쪽에서 33% 늘립니다. 복잡도 절에서 본 정사각형 그림의 4/3 배와 맞는 값입니다.

얻는 것 없이 이 값만 치르는 텍스처도 있습니다. 늘이거나 줄이지 않고 원래 해상도로만 그리는 텍스처입니다. 크기를 바꾸지 않는 UI(User Interface, 사용자 인터페이스) 텍스처가 그 예입니다. 줄어 보일 일이 없으니 고를 레벨도 없습니다.

그림이 아니라 값을 찾아 쓰는 표를 담은 텍스처도 있습니다. 이런 조회표 LUT(Look-Up Table)는 다른 레벨을 둘 뜻이 없는 텍스처의 예로 들 수 있습니다. 줄인 장의 칸은 이웃 칸을 섞은 값이라 표에 적힌 원래 값이 아닙니다.

비스듬한 면의 흐릿함

밉맵의 레벨은 전부 가로와 세로를 같은 비율로 줄인 장입니다. 그래서 밉맵으로 읽을 때는 점이 덮는 자국을 정사각형으로 봅니다. 그러나 면이 비스듬히 누우면 자국이 길고 좁아질 수 있습니다.

레벨은 자국의 긴 지름에 맞춰 고릅니다. 자국이 가로 네 칸·세로 한 칸이면 긴 지름이 네 칸이라 레벨 2 를 고릅니다. 레벨 2 의 한 칸은 원본의 가로 넷·세로 넷, 열여섯 칸을 줄여 만든 값입니다.

자국이 덮은 칸은 그중 넷뿐입니다. 나머지 열두 칸의 색까지 섞여 들어 짧은 축 쪽이 필요 이상으로 흐려집니다. 보는 사람에게서 비스듬히 멀어지는 면에서 이 흐림이 심해집니다.

block-beta
columns 4
  H1["점이 덮는 자국 · 가로 네 칸 · 세로 한 칸"]:4
  a1["자국"] a2["자국"] a3["자국"] a4["자국"]
  H2["긴 지름 네 칸에 맞춘 레벨 2 의 한 칸 · 원본 가로 넷·세로 넷 몫"]:4
  b1["자국"] b2["자국"] b3["자국"] b4["자국"]
  b5["자국 밖"] b6["자국 밖"] b7["자국 밖"] b8["자국 밖"]
  b9["자국 밖"] b10["자국 밖"] b11["자국 밖"] b12["자국 밖"]
  b13["자국 밖"] b14["자국 밖"] b15["자국 밖"] b16["자국 밖"]

이방성 필터링이 이 흐림을 줄이는 데 쓰입니다. 자국을 정사각형으로 보지 않고 길쭉한 모양대로 다루는 방식입니다. 이방성의 정도를 높이면 화질이 나아질 수 있습니다. 대신 텍스처를 읽는 속도가 크게 떨어질 수 있습니다.

내용이 바뀌는 텍스처

사슬 채우기는 부를 때마다 원본 레벨에서 아래 레벨 전부를 새로 줄여 이전 내용을 덮어씁니다. 원본 칸 수에 비례하는 일입니다. 원본을 장면마다 고쳐 쓰는 텍스처에서 장면마다 사슬 채우기를 부르면 그만큼 비용이 쌓입니다.

텍스처 아틀라스의 번짐

텍스처 아틀라스는 작은 그림 여러 장을 큰 텍스처 한 장에 모아 붙인 것입니다. 붙인 그림 하나하나를 조각이라고 쓰겠습니다. 밉맵을 단 텍스처를 아틀라스로 묶으면 레벨을 내려갈수록 조각끼리 색이 섞이는 것처럼 보입니다. 끝내 1×1 레벨에서는 모든 조각이 한 칸으로 뭉개집니다.

이웃 조각의 칸이 섞여 드는 것은 피할 수 있습니다. 줄인 장을 만드는 필터가 조각의 경계를 넘지 않게 하면 됩니다. 그러면 레벨을 내려가도 서로 다른 조각의 칸이 섞이지 않습니다. 다만 2×2 레벨에 이르기 전까지만 그렇습니다.

한 변이 32 칸인 아틀라스에 16×16 조각 하나와 8×8 조각 열둘을 붙였다고 해 봅시다. 4×4 레벨은 각 변을 8분의 1로 줄인 장입니다. 여기서 8×8 조각은 각각 한 칸으로 줄어듭니다. 16×16 조각은 2×2 칸이 됩니다.

16×16 조각을 한 칸으로 나타내려면 2×2 레벨까지 만들어야 합니다. 2×2 레벨에서는 한 칸이 16×16 조각을 나타냅니다. 남은 세 칸은 8×8 조각 넷씩을 섞은 값입니다. 경계를 지켜 줄여도 여기서는 서로 다른 그림의 색이 한 칸에 섞입니다.

block-beta
columns 4
  H1["한 변 32 칸 아틀라스의 4×4 레벨 · 칸마다 조각 하나뿐이다"]:4
  a1["16×16"] a2["16×16"] a3["8×8"] a4["8×8"]
  a5["16×16"] a6["16×16"] a7["8×8"] a8["8×8"]
  a9["8×8"] a10["8×8"] a11["8×8"] a12["8×8"]
  a13["8×8"] a14["8×8"] a15["8×8"] a16["8×8"]
  H2["2×2 레벨 · 세 칸에서 조각 넷이 섞인다"]:4
  b1["16×16 하나"]:2 b2["8×8 넷이 섞임"]:2
  b3["8×8 넷이 섞임"]:2 b4["8×8 넷이 섞임"]:2

아틀라스 텍스처 자체의 사슬 길이는 긴 변이 정합니다. 한 변이 32 칸이면 1×1 까지 여섯 장입니다. 그중 어디까지 만들어 두어야 하는가는 아틀라스에 붙인 가장 큰 조각이 정합니다. 16×16 조각을 한 칸으로 나타내려면 2×2 레벨이 있어야 하기 때문입니다.

작은 조각에게는 제 몫보다 긴 사슬이 생깁니다. 8×8 조각은 4×4 레벨에서 이미 한 칸이 되어 제 사슬을 끝냅니다. 그보다 아래 레벨은 그 조각 쪽에서 보면 초기화되지 않은 자리입니다. 그 조각의 그림을 줄여 채운 값이 없다는 뜻입니다.

빈 칸이라는 뜻은 아닙니다. 그 자리를 채운 것은 앞에서 본 섞인 값입니다. 2×2 레벨의 세 칸은 8×8 조각 넷씩을 섞은 값입니다. 그중 어느 칸도 8×8 조각 하나만 담지 않습니다.

아래 그림은 같은 아틀라스에서 두 조각이 레벨마다 몇 칸이 되는지를 나란히 놓습니다. 1×1 레벨에서는 두 조각이 한 칸에 함께 뭉개집니다.

block-beta
columns 3
  h0["아틀라스 레벨"] h1["16×16 조각"] h2["8×8 조각"]
  r0["32×32"] c0["16×16"] d0["8×8"]
  r1["16×16"] c1["8×8"] d1["4×4"]
  r2["8×8"] c2["4×4"] d2["2×2"]
  r3["4×4"] c3["2×2"] d3["1×1 · 제 사슬 끝"]
  r4["2×2"] c4["1×1 · 제 사슬 끝"] d4["제 값 없음 · 이웃 넷과 섞인 칸"]
  r5["1×1"] c5["두 조각이 한 칸에 뭉개짐"]:2

한 변이 2의 거듭제곱이 아닌 텍스처

WebGL 1 은 한 변이 2의 거듭제곱이 아닌 텍스처의 밉맵을 만들지 못합니다. 이런 텍스처에는 NEAREST 나 LINEAR 필터만 쓸 수 있습니다.

감싸기 방식은 읽을 위치가 그림 범위 밖으로 나갔을 때 어느 칸을 읽을지 정하는 규칙입니다. 이런 텍스처는 감싸기 방식도 CLAMP_TO_EDGE 여야 합니다. 범위 밖에서도 가장자리 칸을 읽는 방식입니다.

두 변이 모두 2의 거듭제곱이면 밉맵을 쓸 수 있습니다. 감싸기 방식도 그림을 되풀이해 읽는 REPEAT 나 MIRRORED_REPEAT 로 둘 수 있습니다.

Unity 도 밉맵을 자동으로 만들게 하려면 원본 해상도가 2의 거듭제곱이어야 합니다.

사용처

밉맵은 네 갈래의 소프트웨어에 박혀 있습니다. 3D 그래픽스 API(Application Programming Interface), 게임 엔진, 텍스처 파일 형식, 2D 그리기 라이브러리입니다. 아래는 그 안의 어느 호출·설정·필드가 밉맵을 다루는지와 왜 두었는지입니다.

Direct3D 11 의 GenerateMips

ID3D11DeviceContext::GenerateMips 는 사슬 채우기 호출입니다. 인자로 셰이더 리소스 뷰 하나를 받습니다.

셰이더는 그래픽 처리 장치(GPU, Graphics Processing Unit)에서 도는 작은 프로그램입니다. 셰이더 리소스 뷰는 셰이더가 읽을 텍스처와 그중 쓸 레벨 범위를 묶어 가리키는 객체입니다. 이 호출은 뷰의 가장 큰 레벨에서 시작해 뷰가 정한 가장 작은 레벨까지 거듭 줄입니다.

원본 리소스를 만들 때 아래 플래그 셋을 모두 달아야 합니다. 하나라도 빠지면 이 호출은 아무 일도 하지 않습니다.

플래그 뜻
D3D11_BIND_RENDER_TARGET 그림을 그려 넣는 대상인 렌더 타깃으로 쓸 수 있다
D3D11_BIND_SHADER_RESOURCE 셰이더가 읽을 수 있다
D3D11_RESOURCE_MISC_GENERATE_MIPS 이 호출로 밉맵을 만들 수 있다

줄어 보이는 텍스처를 레벨을 골라 읽으려면 사슬이 채워져 있어야 합니다. 그래서 API 가 채우는 호출을 따로 둡니다.

Unity 의 텍스처 임포트 설정

Unity 는 텍스처 에셋마다 Texture Import Settings 에서 밉맵을 켜고 끕니다. 밉맵을 직접 만들어 넣을 수 있습니다. Unity 가 만들게 할 수도 있습니다.

Unity 는 밉맵을 원본 텍스처를 줄여 저장해 둔 사본으로 다룹니다. GPU 가 원본에서 여러 번 읽는 대신 줄여 둔 장에서 더 적게 읽게 하려는 것입니다. 두 레벨을 섞는 삼선형 필터링도 고를 수 있습니다.

KTX 2.0 의 levelCount

KTX(Khronos Texture) 2.0 은 텍스처를 싣는 파일 형식입니다. levelCount 가 파일에 든 밉 레벨 수를 적습니다. 사슬을 끝까지 다 담지 않아도 됩니다.

levelCount 가 1 이면 원본 레벨만 있고 다른 레벨을 둘 뜻이 없는 텍스처입니다. 0 이면 원본 레벨만 담았으니 읽어 들이는 쪽이 필요하면 다른 레벨을 만들라는 뜻입니다.

블록 압축은 칸 여러 개를 한 덩어리로 묶어 압축해 담는 텍스처 형식입니다. 블록 압축 형식의 파일은 levelCount 를 0 으로 둘 수 없습니다.

파일 안에서 레벨은 가장 작은 장부터 원본 순서로 놓입니다. 파일을 다 받기 전에 그리기 시작할 때 작은 레벨을 먼저 보내면 낮은 해상도 그림을 먼저 띄울 수 있습니다. 텍스처 전체를 기다리지 않아도 됩니다.

block-beta
columns 1
  A["가장 작은 레벨 · 파일 앞쪽"]
  B["그 위 레벨들"]
  C["레벨 1"]
  D["레벨 0 · 원본 · 파일 뒤쪽"]

DDS 의 dwMipMapCount

DDS(DirectDraw Surface) 파일 헤더의 dwMipMapCount 가 밉 레벨 수를 담습니다. 밉맵을 단 텍스처를 쓸 때는 DDSD_MIPMAPCOUNT 플래그(0x20000)도 켜야 합니다.

파일을 읽는 쪽에는 이 플래그가 켜져 있다고 기대지 말라는 권고가 붙어 있습니다. 파일을 쓰는 도구가 이 플래그를 안 켤 수도 있기 때문입니다. KTX 2.0 과 마찬가지로 사슬을 파일 안에 구워 두는 형식입니다.

Skia 의 SkMipmapMode

Skia 는 2D 그리기 라이브러리입니다. 이미지를 그릴 때 넘기는 샘플링 옵션에 SkMipmapMode 가 있습니다. 값은 셋입니다.

값 읽는 레벨
kNone 밉 레벨을 무시하고 원본에서 읽는다
kNearest 가장 가까운 레벨에서 읽는다
kLinear 가장 가까운 두 레벨 사이를 보간한다

기본값은 kNone 입니다. 그래서 이미지를 줄여 그릴 때 밉맵을 쓰려면 이 값을 따로 골라야 합니다.

관련 항목

밉맵이 줄여 쌓는 그림 자원

텍스처 · 텍셀 · 밉 레벨 · 텍스처 좌표 · 큐브 맵 · 텍스처 배열 · 텍스처 아틀라스 · 노멀 맵 · 2의 거듭제곱 텍스처 · 조회 테이블

밉맵에서 읽을 값을 고르고 섞는 읽기 규칙

샘플러 · 텍스처 필터링 · 쌍선형 필터링 · 삼선형 필터링 · 이방성 필터링 · 밉 레벨 바이어스 · 최근접 이웃 샘플링 · 레벨 오브 디테일

밉맵 레벨을 만드는 축소 방식

상자 필터 · 다운샘플링 · 리샘플링 · 이미지 피라미드

밉맵처럼 미리 줄이거나 미리 더해 두는 이웃 구조

립맵 · 클립맵 · 합산 영역 테이블 · 가우시안 피라미드

밉맵이 줄이려는 화면의 깨짐

에일리어싱 · 모아레 · 안티앨리어싱 · 샘플링

밉맵을 읽어 화면을 칠하는 단계

그래픽스 파이프라인 · 래스터화 · 프래그먼트 셰이더 · 텍스처 샘플링

밉맵 사슬을 만들고 읽게 하는 그래픽스 API와 엔진

OpenGL · Vulkan · Direct3D · WebGL · WebGPU · Skia · Unity · 언리얼 엔진

밉맵 사슬을 미리 구워 싣는 텍스처 파일 형식

KTX · DDS · 블록 압축 · 텍스처 압축

밉맵이 더 쓰는 메모리를 덜어 내는 방법

텍스처 스트리밍 · 밉맵 스트리밍 · VRAM · 텍스처 해상도

다른 이름: mipmap · Mipmap · MIP map · 밉맵 사슬 · mipmap chain · 밉맵 피라미드 · mipmapping · 밉매핑