사전 sRGB 프레임버퍼
개념

sRGB 프레임버퍼

gabury1고친 사람 github-actions[bot]

sRGB 프레임버퍼는 그래픽 프로그램이 계산한 색을 모니터가 기대하는 숫자로 바꿔서 적어 줍니다. 프로그램은 색을 빛의 양대로 계산합니다. 모니터는 그와 다르게 적힌 모니터용 숫자를 받습니다. 색을 적는 순간 하드웨어가 이 둘 사이를 옮겨 줍니다.

쉽고 빠른 이해

화면에 낼 색을 적어 두는 메모리입니다. 적을 때 숫자를 모니터용으로 바꿔 줍니다. 프로그램이 「흰색 빛의 절반」이라는 뜻으로 0.5를 내면 메모리에는 128이 아니라 186 안팎이 적힙니다.

색을 섞고 빛을 더하는 계산은 빛의 양에 비례하는 숫자로 해야 맞습니다. 모니터는 그렇게 적힌 숫자를 받지 않습니다. 이 변환을 프로그램이 직접 하면 반투명한 색을 겹칠 때 섞기가 모니터용 숫자끼리 일어납니다. 그러면 겹친 색이 어둡게 나옵니다.

어떻게 도나:

  1. 프로그램이 빛의 양대로 계산한 색을 냅니다
  2. 반투명하게 겹칠 때는 이미 적힌 숫자를 빛의 양으로 되돌려서 섞습니다
  3. 섞은 결과를 모니터용 숫자로 바꿔 적습니다

대가는 프로그램 전체가 빛의 양대로만 색을 내야 한다는 것입니다. 프로그램이 모니터용으로 한 번 더 바꾸면 두 번 바뀌어 화면이 허옇게 뜹니다. 색이 아닌 값을 담는 메모리에는 이 방식을 쓰지 않습니다.

상세

회의실 안에서는 모두 한국어로 계산하고 이야기합니다. 문밖으로 나가는 서류는 문 앞에 선 번역가가 영어로 옮겨 붙입니다. 밖에서 서류를 다시 들여올 때도 번역가가 한국어로 되돌려 줍니다. 안에 있는 사람은 영어를 몰라도 일할 수 있습니다.

이 절은 색을 적는 숫자가 두 가지라는 데서 출발합니다. 이어 프레임버퍼에 색이 적히는 순간 무엇이 바뀌는지 봅니다.

반투명한 색을 겹칠 때 왜 하드웨어가 이 일을 맡아야 하는지는 흰색과 검정 한 쌍으로 따져 봅니다. 다음으로 8비트에 담을 때 어두운 쪽 눈금이 왜 중요한지 봅니다. 끝으로 그래픽 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)에서 이 기능을 고르는 이름과 쓰지 않는 경우를 짚습니다.

색을 적는 두 가지 숫자

화면의 색은 빨강·초록·파랑 세 빛의 세기를 숫자 셋으로 적습니다. 이 방식을 RGB(Red-Green-Blue, 빨강-초록-파랑)라고 부릅니다. 같은 숫자 셋을 어떤 색으로 읽을지는 규칙이 정합니다. 이 규칙이 색 공간입니다.

모니터와 이미지 파일이 거의 다 따르는 색 공간이 sRGB(standard RGB, 표준 RGB)입니다. sRGB 는 8비트로 적을 때 빨강·초록·파랑 값마다 0~255 숫자를 씁니다. 이 숫자는 빛의 양에 비례하지 않습니다. 가운데 숫자인 128은 흰색 빛의 절반이 아니라 5분의 1 남짓만 냅니다.

이렇게 휘게 적는 까닭은 사람 눈에 있습니다. 눈은 어두운 쪽의 밝기 차이를 밝은 쪽보다 잘 알아챕니다. sRGB 는 256칸 눈금을 어두운 쪽에 더 많이 나눠 줍니다. 이 문서는 이렇게 적힌 숫자를 「sRGB 숫자」라고 부릅니다.

다른 한쪽은 숫자가 빛의 양에 그대로 비례하는 표현입니다. 이것을 선형 색 공간이라고 부릅니다. 선형 값 0.5는 1.0이 내는 빛의 절반입니다. 이 문서는 이렇게 적힌 숫자를 「선형 값」이라고 부릅니다.

두 숫자 사이의 휜 관계는 대개 2.2 제곱으로 어림합니다. 둘 다 0~1 사이 값으로 놓고 계산합니다. 선형 값을 1/2.2 제곱하면 sRGB 숫자가 됩니다. 거꾸로 sRGB 숫자를 2.2 제곱하면 선형 값으로 돌아옵니다.

예를 들어 선형 값 0.5를 1/2.2 제곱하면 0.73쯤입니다. 8비트로 적으려고 255를 곱하면 186입니다. 실제 sRGB 곡선은 이 어림과 조금 달라서 한두 칸 차이가 납니다.

그래픽 프로그램은 선형 값으로 계산합니다. 전등 두 개가 벽을 비추면 벽에 닿는 빛은 두 빛을 더한 양입니다. 숫자가 빛의 양에 비례해야 이 덧셈이 화면에서도 맞게 보입니다. 계산은 선형 값으로 합니다. 모니터에는 sRGB 숫자를 보내야 합니다.

색이 적히는 순간의 변환

화면을 이루는 작은 점 하나하나를 픽셀이라고 부릅니다. 프레임버퍼는 화면에 보여 줄 색을 픽셀마다 적어 두는 메모리입니다. 모니터 쪽 회로는 이 메모리를 1초에 수십 번 읽어 화면을 밝힙니다. 그래서 여기 적힌 숫자는 모니터가 기대하는 sRGB 숫자여야 합니다.

픽셀의 색을 계산하는 작은 프로그램을 셰이더라고 부릅니다. 셰이더는 화면을 그리는 계산을 맡는 칩인 GPU(Graphics Processing Unit, 그래픽 처리 장치) 위에서 돕니다. 셰이더는 대개 선형 값으로 색을 계산해 냅니다.

sRGB 프레임버퍼는 「여기 적힌 숫자는 sRGB 숫자다」라고 표시된 프레임버퍼입니다. 셰이더가 선형 값을 내면 GPU 가 적기 직전에 sRGB 숫자로 바꿉니다. 셰이더 쪽 코드는 변환을 몰라도 됩니다.

셰이더가 흰색 빛의 절반인 0.5를 낸다고 합시다. 일반 프레임버퍼라면 0.5에 255를 곱한 128이 적힙니다. sRGB 프레임버퍼에는 앞의 어림대로 186 안팎이 적힙니다. 모니터가 이 숫자를 받으면 흰색 빛의 절반을 냅니다.

바꾸는 것은 빨강·초록·파랑 세 값뿐입니다. 네 번째 값인 알파는 손대지 않습니다. 알파는 빛의 양이 아니라 얼마나 가리는지를 적은 비율이기 때문입니다.

읽는 쪽에도 짝이 있습니다. 물체 겉면에 입히는 이미지를 텍스처라고 부릅니다. 이런 이미지는 대개 sRGB 숫자로 저장돼 있습니다.

이 이미지를 sRGB 텍스처로 표시해 두면 GPU 가 읽는 순간 선형 값으로 되돌려 줍니다. 적을 때는 sRGB 프레임버퍼가 sRGB 숫자로 바꿉니다. 그래서 셰이더 안에서는 선형 값만 흐릅니다.

반투명한 색을 겹칠 때

이 소절은 sRGB 프레임버퍼가 왜 하드웨어 기능이어야 하는지를 봅니다. 답은 이미 그려진 색과 새 색을 섞는 단계에 있습니다.

새 색을 이미 그려진 색 위에 반투명하게 겹치는 일을 블렌딩이라고 부릅니다. 알파가 0.5면 새 색 반, 원래 색 반을 섞습니다.

이 섞기는 셰이더가 끝난 뒤에 GPU 가 따로 합니다. 셰이더는 보통 원래 적혀 있던 색을 읽지 못합니다. 이 점이 뒤에서 sRGB 프레임버퍼가 하드웨어 기능이어야 하는 까닭이 됩니다.

sRGB 프레임버퍼는 블렌딩이 켜져 있으면 한 단계를 더 거칩니다. 적혀 있던 sRGB 숫자를 먼저 선형 값으로 되돌립니다. 그다음 두 선형 값을 섞습니다. 섞은 결과는 다시 sRGB 숫자로 바꿔 적습니다.

flowchart TD
    A["셰이더가 선형 값을 낸다"] --> B{"블렌딩이 켜져 있나"}
    B -->|꺼져 있다| E["sRGB 숫자로 바꿔 적는다"]
    B -->|켜져 있다| C["적혀 있던 sRGB 숫자를 선형 값으로 되돌린다"]
    C --> D["두 선형 값을 섞는다"]
    D --> E
    E --> F["모니터가 읽어 간다"]

위 그림에서 섞기는 언제나 선형 값끼리 일어납니다. 블렌딩이 꺼져 있으면 되돌릴 것이 없으니 바꿔 적기만 합니다.

아래 코드는 검정 위에 흰색을 반투명하게 겹친 두 경우를 나란히 둡니다. 곡선은 앞의 2.2 제곱 어림을 씁니다.

enc 는 선형 값을 sRGB 숫자로, dec 는 그 반대로 바꿉니다. src 는 새로 겹칠 흰색, a 는 알파, dst 는 이미 적혀 있던 검정입니다.

Python
def enc(x):
    return round(255 * x ** (1 / 2.2))

def dec(v):
    return (v / 255) ** 2.2

src, a, dst = 1.0, 0.5, 0

enc(src * a + dec(dst) * (1 - a))    # 186
round(enc(src) * a + dst * (1 - a))  # 128

186이 나온 줄은 sRGB 프레임버퍼가 하는 계산입니다. 적혀 있던 검정을 선형 값으로 되돌립니다. 선형 값끼리 섞은 뒤 결과를 sRGB 숫자로 바꿔 적습니다. 186은 흰색 빛의 절반입니다.

128이 나온 줄은 셰이더가 직접 흰색을 sRGB 숫자 255로 바꿔 일반 프레임버퍼에 겹친 경우입니다. 섞기가 sRGB 숫자끼리 일어나서 128이 나옵니다. 128은 흰색 빛의 5분의 1 남짓이라 반투명한 흰색이 너무 어둡게 보입니다.

셰이더는 대개 적혀 있던 색을 읽을 수 없으니 이 되돌림을 스스로 하지 못합니다. 그래서 되돌림과 바꿔 적기를 섞기 단계에 붙여 GPU 가 맡습니다. 이것이 sRGB 프레임버퍼가 따로 있는 까닭입니다.

8비트에 담을 때의 어두운 쪽 눈금

선형 값을 8비트 프레임버퍼에 적어 두고 화면으로 보내기 직전에 한 번 바꾸는 방법도 있습니다. 그러면 섞기는 맞습니다. 문제는 어두운 쪽 눈금이 모자란다는 것입니다.

아래 표는 아주 어두운 빛 두 개를 두 방식으로 적은 숫자입니다. sRGB 곡선은 여기서도 2.2 제곱으로 어림했습니다.

빛의 양 선형 값을 8비트에 적으면 sRGB 숫자로 적으면
0.01 3 31
0.02 5 43

선형으로 적으면 두 빛 사이에 눈금이 두 칸뿐입니다. sRGB 숫자로 적으면 열두 칸입니다. 칸이 성기면 부드러워야 할 어두운 그러데이션이 층층이 끊겨 보입니다. 이 계단 자국을 밴딩이라고 부릅니다.

눈금을 늘리려고 빨강·초록·파랑·알파 값마다 16비트 부동소수점을 쓸 수도 있습니다. 그러면 픽셀 하나가 4바이트에서 8바이트로 두 배가 됩니다. sRGB 프레임버퍼는 픽셀당 4바이트를 지킵니다. 그러면서도 어두운 쪽 눈금을 촘촘하게 둡니다. 섞기도 선형 값으로 맞게 합니다.

그래픽 API 에서 고르는 이름

프로그램은 그래픽 API 를 거쳐 GPU 에 일을 시킵니다. sRGB 프레임버퍼를 쓰는 방법은 API 마다 조금씩 다릅니다. 대개 프레임버퍼의 포맷으로 고릅니다.

포맷은 픽셀 하나를 몇 바이트에 어떤 순서로 적을지 정한 것입니다. 포맷 이름의 UNORM 은 부호 없는 정수를 0~1 사이 값으로 읽는다는 표시(unsigned normalized)입니다. 그 뒤에 붙은 SRGB 가 이 정수를 sRGB 숫자로 읽으라는 표시입니다.

OpenGL 에는 프레임버퍼가 두 가지 있습니다. 기본 프레임버퍼는 창을 만들 때 함께 생겨 화면으로 나가는 프레임버퍼입니다. 프레임버퍼 객체는 프로그램이 직접 만들어 쓰는 프레임버퍼입니다. 거기 붙인 텍스처에 그림이 그려집니다.

아래 표는 흔히 쓰는 API 셋을 모았습니다.

그래픽 API 무엇을 고르나 변환이 일어나는 조건
OpenGL sRGB 를 지원하는 기본 프레임버퍼, 또는 sRGB 텍스처를 붙인 프레임버퍼 객체 GL_FRAMEBUFFER_SRGB 를 켜야 변환한다
Vulkan 이름 끝이 _SRGB 인 포맷. 예: VK_FORMAT_B8G8R8A8_SRGB 포맷을 고르면 변환한다
Direct3D 이름 끝이 _UNORM_SRGB 인 포맷. 예: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB 포맷을 고르면 변환한다

Vulkan 과 Direct3D 는 포맷 이름만으로 정해집니다. OpenGL 은 sRGB 포맷을 고른 뒤 스위치를 한 번 더 켜야 합니다. 스위치를 안 켜면 sRGB 포맷이어도 변환이 일어나지 않습니다.

모니터로 나가는 프레임버퍼 여러 장을 묶어 돌려 쓰는 것을 스왑 체인이라고 부릅니다. Vulkan 과 Direct3D 에서는 스왑 체인을 만들 때 위 포맷을 고릅니다. 그러면 화면에 나가는 프레임버퍼가 sRGB 프레임버퍼가 됩니다.

쓰지 않는 경우와 흔한 실수

sRGB 프레임버퍼는 셰이더가 선형 값을 낸다는 약속 위에서만 맞게 돕니다. 셰이더 쪽 변환과 프레임버퍼 쪽 변환이 어긋나면 화면이 바로 틀어집니다. 아래 표가 네 조합입니다.

셰이더가 직접 sRGB 숫자로 바꾸나 프레임버퍼가 sRGB 인가 화면
아니오 예 맞게 나온다
아니오 아니오 선형 값이 바뀌지 않고 나가 전체가 어둡다
예 아니오 불투명한 색은 맞다. 반투명하게 겹친 곳이 어둡다
예 예 두 번 바뀌어 전체가 허옇게 뜬다

마지막 줄이 가장 흔한 실수입니다. 셰이더 끝에 sRGB 로 바꾸는 코드를 남겨 둔 채 프레임버퍼를 sRGB 로 바꾸면 이렇게 됩니다. 변환은 둘 중 한 곳에서만 합니다.

색이 아닌 값을 적는 프레임버퍼에는 쓰지 않습니다. 표면이 향하는 방향이나 거칠기처럼 다음 계산의 재료가 되는 값이 그렇습니다. 이런 값을 sRGB 숫자로 휘게 적으면 읽어 쓸 때 값이 틀어집니다.

셰이더가 그림을 그려 넣는 대상을 넓게 렌더 타깃이라고 부릅니다. 화면으로 나가는 프레임버퍼도, 다음 단계의 재료로 쓸 텍스처도 렌더 타깃입니다. sRGB 프레임버퍼는 렌더 타깃의 한 가지입니다.

중간 결과를 담는 렌더 타깃에는 대개 쓰지 않습니다. 빛을 여럿 더하면 값이 1을 넘기도 합니다. 그래서 중간 결과는 부동소수점 포맷에 선형 값으로 담습니다. 부동소수점 포맷에는 sRGB 로 읽는 짝이 따로 없습니다.

sRGB 프레임버퍼는 대개 화면으로 나가기 직전 단계에서 씁니다. 스왑 체인을 sRGB 포맷으로 만드는 경우가 그렇습니다. 마지막 결과를 담는 프레임버퍼 객체에 sRGB 텍스처를 붙이는 경우도 그렇습니다.

sRGB 프레임버퍼 없이 만든 프로그램에서는 반투명 색이 sRGB 숫자끼리 섞였습니다. 그 어두운 결과를 보고 색을 미리 밝게 골라 둔 그림이 있습니다. 이런 그림을 sRGB 프레임버퍼로 옮기면 반투명하게 겹친 곳이 전보다 밝게 나옵니다. 앞의 코드에서 128이 186으로 바뀐 것과 같은 일입니다.

관련 항목

sRGB 프레임버퍼가 속하는 상위 분류

프레임버퍼 · 렌더 타깃 · 그래픽스 · 렌더링

sRGB 프레임버퍼가 서로 옮겨 주는 두 색 표현

sRGB · 선형 색 공간 · 색 공간 · 감마 보정 · 전달 함수 · IEC 61966-2-1

sRGB 프레임버퍼가 변환을 붙이는 처리 단계

셰이더 · 프래그먼트 셰이더 · 블렌딩 · 알파 블렌딩 · 출력 병합기 · 알파 채널

읽는 쪽에서 짝을 이루는 텍스처 형식

sRGB 텍스처 · 텍스처 · 텍스처 필터링 · 밉맵

sRGB 프레임버퍼를 고르는 그래픽 API 설정

OpenGL · Vulkan · Direct3D · Metal · WebGL · FRAMEBUFFER_SRGB · 프레임버퍼 객체 · 스왑 체인 · 픽셀 포맷

sRGB 프레임버퍼 대신 선형 값을 담는 저장 형식

부동소수점 · 반정밀도 부동소수점 · 색 깊이 · 비트 깊이

어두운 쪽 눈금이 모자랄 때 생기는 계단 자국과 처방

밴딩 · 디더링 · 양자화

선형 값을 화면용 숫자로 줄이는 마지막 처리

톤 매핑 · HDR · 후처리 · 색 관리

다른 이름: sRGB framebuffer · sRGB 프레임 버퍼 · sRGB 렌더 타깃