이미지 뷰
고친 사람 github-actions[bot]
이미지 뷰는 그래픽 카드에 올린 이미지를 어떤 식으로 읽을지 정해 줍니다. 이미지를 복사하지 않고 그중 어느 부분을 무슨 형식으로 볼지만 적습니다. 안드로이드 앱에서는 화면에 그림을 띄우는 부품도 이미지 뷰라고 부릅니다. 이 문서는 그래픽 카드에서 쓰는 뜻을 다룹니다.
쉽고 빠른 이해
이미지 뷰는 그림 한 벌에 붙이는 「이렇게 보라」는 쪽지입니다. 여섯 장이 든 그림 한 벌을 두고 한 쪽지는 「여섯 장 전부를 상자 한 개로」, 다른 쪽지는 「셋째 장만」이라고 적습니다.
이게 없으면 그림만 건네받아서는 여섯 장이 따로 쓰는 그림인지 상자의 여섯 면인지 알 수 없습니다. 그림을 쓰는 방식마다 따로 만들어 올리면 메모리도 그만큼 더 듭니다.
어떻게 도나:
- 그림을 한 벌 만들어 그래픽 카드에 올립니다
- 쓰는 방식마다 쪽지를 하나씩 만듭니다. 몇째 장인지, 얼마나 줄인 그림인지, 무슨 형식으로 읽을지를 적습니다
- 그림을 읽거나 그림에 그릴 때는 그림 대신 쪽지를 건넵니다
그림을 복사하거나 새 내용을 채워 넣을 때처럼 바이트만 옮기는 일에는 쪽지가 필요 없습니다. 읽는 방식을 정할 일이 없기 때문입니다.
대가는 쪽지를 따로 챙겨야 한다는 것입니다. 쪽지가 가리키는 그림을 먼저 치우면 안 됩니다. 쓰는 방식이 많으면 쪽지 수도 금세 불어납니다.
상세
이 절은 이미지 한 벌이 무엇을 담는지부터 봅니다. 그다음 누가 이미지 뷰를 받아 쓰는지, 이미지 뷰가 그 가운데 무엇을 고르는지를 따라갑니다. 끝으로 제품마다 붙인 이름과 짧은 코드 한 토막, 이 방식의 대가를 봅니다.
창문 여럿과 정원 하나
정원이 딸린 집에 창문이 여럿 있다고 해 봅시다. 좁은 창으로는 화단 한 귀퉁이만, 큰 창으로는 정원 전체가 보입니다. 색유리를 끼운 창으로 보면 같은 꽃이 다른 빛깔로 보입니다. 어느 창으로 보든 정원 자체는 하나입니다.
이미지 뷰는 그래픽 카드 메모리에 있는 이미지 하나를 두고 내는 창문입니다. 이미지의 바이트를 옮기거나 복사하지 않습니다. 이미지의 어느 부분을 볼지, 그 바이트를 어떤 값으로 읽을지만 적어 둡니다. 창문 하나에 이미지 뷰 하나가 대응합니다.
이미지 한 벌에 든 것
그래픽 카드는 GPU(Graphics Processing Unit, 그래픽 처리 장치)라고도 부릅니다. 그래픽스에서 이미지는 그래픽 카드 메모리에 올려 둔 그림 데이터입니다. 물체 표면에 입힐 무늬, 화면에 내보낼 결과 그림, 물체까지의 거리를 적은 그림이 전부 이미지입니다.
표면에 입히는 그림은 흔히 텍스처라고 부릅니다. 이미지를 통틀어 텍스처라고 부르는 경우도 있습니다. 이 문서에서는 둘을 가리지 않고 이미지라고 부릅니다.
이미지는 네모 칸이 모인 격자입니다. 그 칸 하나를 텍셀이라고 부릅니다. 화면의 점 하나가 픽셀이듯 이미지 안의 점 하나가 텍셀입니다.
텍셀 하나에 무엇을 몇 바이트로 담는지는 형식이 정합니다. 「빨강·초록·파랑·투명도를 1바이트씩, 모두 4바이트」가 흔한 형식 하나입니다. 형식을 알아야 바이트 네 개를 색 하나로 읽을 수 있습니다.
형식 안의 값 하나하나를 채널이라고 부릅니다. 위 형식은 빨강·초록·파랑·투명도 네 채널을 가집니다. 흑백 그림처럼 채널이 하나뿐인 형식도 있습니다.
이미지 한 벌에는 같은 크기의 그림을 여러 장 묶어 담을 수 있습니다. 그 한 장 한 장을 레이어라고 부릅니다. 레이어가 여럿인 이미지를 배열 텍스처라고 합니다.
레이어마다 줄인 그림이 딸릴 수도 있습니다. 원래 그림을 가로세로 절반씩 줄인 그림을 차례로 만들어 함께 담은 것이 밉맵입니다. 멀리 있는 물체를 그릴 때 작은 그림을 읽어서 무늬가 자글거리지 않게 합니다.
밉맵의 크기 하나하나를 레벨이라고 부릅니다. 원래 크기가 레벨 0 입니다. 한 번 줄일 때마다 레벨이 하나 올라갑니다.
그래서 이미지 한 벌은 「레이어 몇 장 × 밉맵 몇 레벨」 모양입니다. 레이어 셋에 밉맵 레벨 셋을 둔 이미지를 그리면 아래와 같습니다.
flowchart TD
subgraph IMG["이미지 한 벌"]
subgraph L0["레이어 0"]
L0M0["레벨 0 · 원래 크기"] -->|절반으로 줄임| L0M1["레벨 1 · 절반"] -->|절반으로 줄임| L0M2["레벨 2 · 4분의 1"]
end
subgraph L1["레이어 1"]
L1M0["레벨 0"] --> L1M1["레벨 1"] --> L1M2["레벨 2"]
end
subgraph L2["레이어 2"]
L2M0["레벨 0"] --> L2M1["레벨 1"] --> L2M2["레벨 2"]
end
end
레이어 하나와 밉맵 레벨 하나를 고르면 그림 한 장이 정해집니다. 이미지 뷰는 이 격자에서 어느 레이어들과 어느 레벨들을 볼지 고릅니다.
이미지 뷰를 받는 셰이더와 렌더 패스
이미지 뷰를 받아 쓰는 것은 둘입니다. 하나는 이미지를 읽는 셰이더입니다. 다른 하나는 이미지에 그리는 렌더 패스입니다.
셰이더는 그래픽 카드에서 도는 작은 프로그램입니다. 화면의 점마다 무슨 색을 칠할지 계산합니다. 그 계산에 쓸 값을 이미지에서 읽습니다.
셰이더에 이미지를 넘길 때는 디스크립터에 이미지 뷰를 적어 넘깁니다. 디스크립터는 셰이더가 읽을 자원을 가리키는 기록입니다.
셰이더가 이미지를 읽을 때는 샘플러가 함께 붙습니다. 샘플러는 텍셀과 텍셀 사이를 읽을 때 색을 어떻게 섞을지, 그림 밖을 읽으면 어떻게 할지를 정합니다. 이미지 뷰가 「무엇을 읽나」를 정한다면 샘플러는 「어떻게 읽나」를 정합니다.
렌더 패스는 그리기 명령을 한 묶음으로 그래픽 카드에 넘기는 단위입니다. 렌더 패스를 열 때 이번 묶음이 어느 이미지에 그릴지를 알립니다.
그렇게 그리는 대상이 된 이미지를 어태치먼트라고 부릅니다. 셰이더가 점마다 계산한 색은 이 어태치먼트에 적힙니다. 어태치먼트도 이미지 뷰로 가리킵니다.
그래서 한 번 그릴 때 이미지 뷰는 두 군데에 쓰입니다. 셰이더는 읽기용 뷰로 무늬를 읽습니다. 계산한 색은 그리기용 뷰가 가리키는 이미지에 적힙니다.
그리기용 뷰는 밉맵 레벨을 하나만 고릅니다. 한 번에 그리는 그림은 한 장이기 때문입니다.
flowchart TD
IMG["이미지 · 바이트 한 벌"] --> VR["읽기용 이미지 뷰"]
IMG --> VW["그리기용 이미지 뷰"]
VR --> D["디스크립터"]
SM["샘플러"] --> SH
D --> SH["셰이더가 읽는다"]
VW --> RP["렌더 패스의 어태치먼트 · 여기에 그린다"]
도해처럼 이미지에서 보면 뷰가 갈림길입니다. 같은 이미지가 한 렌더 패스에서는 그려지는 대상이 됩니다. 다음 렌더 패스에서는 셰이더가 읽는 무늬가 될 수 있습니다.
바이트를 옮기기만 하는 일에는 뷰가 필요 없습니다. 이미지에 데이터를 채워 넣거나 이미지끼리 복사할 때는 이미지를 바로 넘깁니다. 바이트를 무슨 값으로 읽을지 정할 일이 없기 때문입니다.
이미지 뷰가 적는 것
이미지만 건네받은 셰이더나 렌더 패스는 이 격자를 어떻게 읽을지 알 수 없습니다. 여섯 레이어가 따로 쓰는 그림 여섯 장일 수 있습니다. 정육면체의 여섯 면일 수도 있습니다. 이미지 뷰가 그 해석을 적습니다.
뷰를 만들 때 해석을 한 번 정해 두므로 그릴 때마다 다시 정하지 않습니다. 이미지 뷰가 적는 항목은 여섯입니다.
| 항목 | 무엇을 정하나 | 예 |
|---|---|---|
| 대상 이미지 | 어느 이미지를 보나 | 하늘 그림을 담은 이미지 |
| 종류 | 격자를 어떤 모양으로 읽나 | 평면 한 장 · 여러 장 묶음 · 정육면체 |
| 밉맵 범위 | 몇 레벨부터 몇 레벨을 보나 | 레벨 0 하나만 |
| 레이어 범위 | 몇째 레이어부터 몇 장을 보나 | 셋째 레이어 한 장 |
| 형식 | 텍셀의 바이트를 무슨 값으로 읽나 | 같은 4바이트를 sRGB 로 적힌 값으로 읽는다 |
| 애스펙트 | 텍셀에 함께 든 값 가운데 어느 값을 보나 | 물체까지의 거리 값만 |
표의 위 넷은 이미지에서 무엇을 볼지 고릅니다. 아래 둘은 고른 텍셀의 바이트를 어떤 값으로 읽을지 정합니다.
형식 행의 sRGB는 사람 눈이 밝기를 느끼는 대로 색 값을 적는 방식입니다. 사람 눈이 잘 가려내는 어두운 색에 값을 더 촘촘히 씁니다.
종류 가운데 정육면체는 큐브맵입니다. 큐브맵은 레이어 여섯 장을 정육면체의 여섯 면으로 읽습니다. 주변 풍경을 사방으로 담아 두었다가 하늘이나 반사를 그릴 때 씁니다.
이미지 가운데에는 색이 아닌 값을 담는 것도 있습니다. 깊이 값은 화면의 그 점에서 물체까지 얼마나 먼지를 적습니다. 이 값을 모은 이미지를 깊이 버퍼라고 합니다. 가까운 물체가 먼 물체를 가리도록 그릴 때 씁니다.
스텐실 값은 화면의 점마다 붙이는 작은 정수 표시입니다. 「이 점에는 그리지 마라」 같은 조건을 거는 데 씁니다. 이 값을 모은 이미지가 스텐실 버퍼입니다.
깊이 값과 스텐실 값은 텍셀 하나에 함께 담기도 합니다. 애스펙트(aspect)는 이런 이미지에서 어느 값을 볼지 고르는 항목입니다. 셰이더가 읽을 때는 둘 가운데 하나만 고른 뷰를 씁니다.
프로그램은 그래픽 카드에 직접 명령을 넣지 않고 정해진 통로를 거칩니다. 그 통로를 그래픽 API(Application Programming Interface, 응용 프로그래밍 인터페이스)라고 합니다. 이미지와 이미지 뷰도 이 API 로 만듭니다. 그래서 뷰에 적는 항목도 API 마다 조금씩 다릅니다.
Vulkan 같은 일부 API 에서는 한 항목이 더 붙습니다. 빨강·초록·파랑·투명도 채널을 바꿔 읽는 스위즐(swizzle)입니다. 흑백 값 하나만 담은 한 채널 이미지를 세 색 채널에 똑같이 퍼뜨려 읽을 때 씁니다.
한 이미지에 뷰를 여럿 두는 경우
뷰를 따로 두는 까닭은 이미지 한 벌을 여러 방식으로 쓰기 때문입니다. 이미지를 방식마다 따로 만들면 메모리가 그만큼 더 듭니다. 한 이미지에 그린 결과를 다른 이미지로 복사하는 일도 생깁니다.
큐브맵이 대표입니다. 큐브맵을 그려서 만들 때는 한 면씩 그립니다. 그래서 면마다 레이어 한 장만 고른 그리기용 뷰를 여섯 개 둡니다.
다 그린 뒤 셰이더가 읽을 때는 여섯 장 전부를 정육면체로 읽는 뷰 하나를 씁니다. 바이트는 한 벌입니다. 뷰는 일곱 개입니다.
flowchart TD
subgraph IMG["큐브맵 이미지 · 레이어 6장"]
F0["레이어 0"]
F1["레이어 1"]
F2["레이어 2"]
F3["나머지 레이어 3장"]
end
F2 --> V2["뷰 · 레이어 2 한 장 · 평면"]
IMG --> VC["뷰 · 레이어 6장 전부 · 정육면체"]
위 도해는 뷰 일곱 가운데 둘만 그렸습니다. 한 뷰는 레이어 2 한 장만 평면으로 봅니다. 다른 뷰는 이미지 전체를 정육면체로 봅니다.
밉맵을 그려서 만들 때도 뷰가 둘씩 쓰입니다. 레벨 0 을 읽어 절반 크기로 줄인 그림을 레벨 1 에 그립니다. 다음에는 레벨 1 을 읽어 레벨 2 에 그립니다. 매 차례 읽을 레벨을 고른 뷰와 그릴 레벨을 고른 뷰가 하나씩 필요합니다.
형식을 바꿔 읽는 경우도 있습니다. 앞에서 본 sRGB 가 대표입니다. 한 뷰는 형식을 sRGB 가 아닌 것으로 잡아 바이트를 손대지 않고 읽고 씁니다.
다른 뷰는 형식을 sRGB 로 잡습니다. 이 뷰로 읽으면 그래픽 카드가 sRGB 로 적힌 값을 밝기에 정비례하는 선형 값으로 바꿔 줍니다. 빛 계산은 선형 값으로 해야 맞습니다. 그래서 셰이더가 무늬를 읽어 빛을 계산할 때는 이 뷰를 씁니다.
이렇게 바꿔 읽으려면 이미지를 만들 때 어느 형식으로 바꿔 볼지를 미리 밝혀야 합니다. 텍셀 하나의 바이트 수가 같은 형식끼리만 바꿀 수 있습니다.
그래픽 API 마다 붙인 이름
그래픽 API 는 여럿입니다. 이미지 뷰에 해당하는 객체도 저마다 다른 이름으로 부릅니다. 주요 API 다섯을 나란히 놓으면 이렇습니다.
| 그래픽 API | 이미지 | 이미지 뷰 |
|---|---|---|
| Vulkan | VkImage |
VkImageView |
| WebGPU | GPUTexture |
GPUTextureView |
| Direct3D 11 | 텍스처 자원 | 쓰임새별 뷰 넷 · 셰이더 읽기용 · 그리기용 · 깊이 스텐실용 · 읽고 쓰기용 |
| Metal | MTLTexture |
다른 MTLTexture 를 뷰로 만든다 |
| OpenGL | 텍스처 객체 | 텍스처 뷰 |
Vulkan 과 WebGPU 는 뷰 하나를 읽기에도 그리기에도 씁니다. Direct3D 11 은 쓰임새마다 뷰 종류를 갈라 둡니다. 셰이더 리소스 뷰(Shader Resource View)가 셰이더 읽기용, 렌더 타깃 뷰(Render Target View)가 그리기용입니다.
Vulkan 과 Direct3D 11 에서는 셰이더와 렌더 패스에 이미지를 넘길 때 반드시 뷰를 거칩니다. OpenGL 과 Metal 은 뷰 없이 이미지를 바로 넘깁니다. 이미지를 다른 방식으로 읽고 싶을 때만 뷰를 따로 만듭니다.
WebGPU 코드로 본 이미지 뷰
앞의 큐브맵 예를 WebGPU 코드로 옮겨 봅니다. 먼저 이미지를 한 벌 만듭니다. 256×256 크기에 레이어 여섯 장입니다. 256 을 절반씩 줄여 1 에 이르기까지 밉맵이 아홉 레벨 나옵니다.
const tex = device.createTexture({
size: [256, 256, 6], // 가로 세로 레이어
format: "rgba8unorm", // 텍셀당 4바이트
mipLevelCount: 9, // 256부터 1까지
usage: GPUTextureUsage.TEXTURE_BINDING
| GPUTextureUsage.RENDER_ATTACHMENT,
});
usage 는 이 이미지를 셰이더가 읽기도 하고 그리는 대상으로도 쓴다고 밝힌 값입니다. 이제 이 이미지 위에 뷰 둘을 만듭니다.
const sky = tex.createView({
dimension: "cube", // 레이어 6장 전부
});
const face = tex.createView({
dimension: "2d", // 평면 한 장
baseArrayLayer: 2, // 레이어 2부터
arrayLayerCount: 1, // 한 장
baseMipLevel: 0, // 레벨 0부터
mipLevelCount: 1, // 한 레벨
});
sky 는 셰이더가 하늘을 그릴 때 읽을 뷰입니다. face 는 렌더 패스가 레이어 2 한 면에 그릴 때 쓸 뷰입니다. 두 뷰가 가리키는 바이트는 tex 한 벌입니다.
적지 않은 항목은 이미지를 보고 정해집니다. 형식을 안 적으면 이미지의 형식을 그대로 씁니다. 밉맵 범위를 안 적으면 sky 처럼 모든 레벨을 봅니다.
이미지 뷰의 대가
뷰는 이미지를 가리키기만 합니다. 그래서 뷰를 쓰는 동안 이미지를 없애면 안 됩니다. Vulkan 처럼 자원을 손으로 치우는 API 에서는 이미지를 치울 때 그 이미지를 가리키는 뷰도 함께 치웁니다.
쓰는 방식이 늘면 뷰 수도 곱으로 불어납니다. 위 큐브맵의 여섯 면을 밉맵 아홉 레벨마다 따로 그리려면 그리기용 뷰만 6 × 9 = 54 개가 필요합니다. 이 뷰들을 만들고 들고 있다가 이미지와 함께 치우는 일을 프로그램이 맡습니다.
형식을 바꿔 읽는 일은 이미지를 만들 때 정해야 합니다. 이미지를 다 만든 뒤에 새 형식으로 보고 싶어지면 이미지를 다시 만들어야 합니다.
관련 항목
이미지 뷰가 가리키는 원본 자원
이미지 · 텍스처 · 배열 텍스처 · 큐브맵 · 3D 텍스처 · 깊이 버퍼 · 스텐실 버퍼 · 비디오 메모리
이미지 뷰가 골라내는 이미지의 구성 요소
텍셀 · 밉맵 · 밉맵 레벨 · 배열 레이어 · 서브리소스 · 픽셀 포맷 · 색 채널 · sRGB · 선형 색 공간 · 스위즐
이미지 뷰를 받아 쓰는 파이프라인 단계
셰이더 · 디스크립터 · 디스크립터 세트 · 바인드 그룹 · 렌더 패스 · 어태치먼트 · 프레임버퍼 · 렌더 타깃
이미지 뷰와 짝을 이루는 그래픽 객체
샘플러 · 텍스처 필터링 · 텍스처 좌표 · 버퍼 뷰 · 버퍼 · 이미지 레이아웃
이미지 뷰를 정의하는 그래픽 API
Vulkan · WebGPU · Direct3D 11 · Direct3D 12 · Metal · OpenGL · API
Direct3D 가 쓰임새별로 가른 뷰 종류
셰이더 리소스 뷰 · 렌더 타깃 뷰 · 깊이 스텐실 뷰 · 순서 없는 접근 뷰
이미지 뷰를 여럿 두어 만드는 그림
환경 매핑 · 그림자 맵 · 밉맵 생성 · 스카이박스
이미지 뷰와 이름이 겹치는 다른 분야 용어
ImageView · UIImageView · 뷰
이미지 뷰가 속하는 상위 분류
그래픽스 · 그래픽스 파이프라인 · 렌더링 · GPU
다른 이름: image view · VkImageView · texture view · 텍스처 뷰 · GPUTextureView