본문 바로가기
Graphics/GLSL

05. Colors

by Lein_ 2021. 12. 31.

Colors

 

GLSL vector 타입에 대해 제대로 아는 것이 중요하다. OOP에 익숙하다면 C처럼 vector 내부의 데이터에 접근할 수 있다는 것을 알 수 있을 것이다.

 

vec3 red = vec3(1.0, 0.0, 0.0);
red.x = 1.0;
red.y = 0.0;
red.z = 0.0;

 

x, y, z 표기법을 이용해 색을 정의하는 것은 혼란스럽고 오해의 소지가 있을 수 있다. 따라서 이 정보에 다른 이름으로 접근할 수 있는 다른 방법이 존재한다. .x, .y, .z의 값은 .r, .g, .b 로 쓸 수 있으며 .s, .t, .p로도 표현 가능하다(보통 텍스쳐의 공간 좌표에 사용된다) 인덱스 위치 [0], [1], [2]를 사용하여 vector의 데이터에 접근할 수도 있다.

 

다음 코드는 동일한 데이터에 접근할 수 있는 모든 방법을 보여준다.

 

vec4 vector;
vector[0] = vector.r = vector.x = vector.s;
vector[1] = vector.g = vector.y = vector.t;
vector[2] = vector.b = vector.z = vector.p;
vector[3] = vector.a = vector.w = vector.q;

 

vector 내부의 변수를 가리키는 이러한 다양한 방법은 명확한 코드를 작성하는 데 도움이 되도록 설계된 명명법일 뿐이다. 셰이더 언어에 들어간 이러한 유연성은 색상과 공간 좌표에 대해 상호 교환적으로 생각할 수 있도록 한다.

 

GLSL에서 vector의 또 다른 중요한 특징은 속성을 원하는 순서로 조합할 수 있어 값을 캐스팅하고 혼합하기 쉽다. 이것을 swizzle이라고 한다.

 

vec3 yellow, magenta, green;

// Making Yellow
yellow.rg = vec2(1.0);  // Assigning 1. to red and green channels
yellow[2] = 0.0;        // Assigning 0. to blue channel

// Making Magenta
magenta = yellow.rbg;   // Assign the channels with green and blue swapped

// Making Green
green.rgb = yellow.bgb; // Assign the blue channel of Yellow (0) to red and blue channels

Mixing Color

 

이전까지 색이 어떻게 정의되는지 알아보았다. 그렇다면 색상을 혼합할 수도 있지 않을까? GLSL에는 두 값을 백분율로 혼합할 수 있는 mix() 함수를 제공한다. 리턴되는 값의 범위는 0.0~1.0이다.

 

다음 코드의 18번째 줄을 주목하자. sin 함수의 절댓값을 비율로 colorA와 colorB가 혼합된다.

 

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform float u_time;

vec3 colorA = vec3(0.149,0.141,0.912);
vec3 colorB = vec3(1.000,0.833,0.224);

void main() {
    vec3 color = vec3(0.0);

    float pct = abs(sin(u_time));

    // Mix uses pct (a value from 0-1) to
    // mix the two colors
    color = mix(colorA, colorB, pct);

    gl_FragColor = vec4(color,1.0);
}

mix() 함수는 x * (1-a) + y * a와 동일하다. x는 시작값, y는 끝값, a는 x와 y사이의 보간시킬 비율(0~1)이다.

정의는 다음과 같으며, 예제는 https://thebookofshaders.com/glossary/?search=mix 에서 볼 수 있다.

float mix(float x, float y, float a)  
vec2 mix(vec2 x, vec2 y, vec2 a)  
vec3 mix(vec3 x, vec3 y, vec3 a)  
vec4 mix(vec4 x, vec4 y, vec4 a)

vec2 mix(vec2 x, vec2 y, float a)  
vec3 mix(vec3 x, vec3 y, float a)  
vec4 mix(vec4 x, vec4 y, float a)

Playing with gradients

 

mix() 함수는 단일 float대신 vec3와 같이 다른 타입에 대해서도 지원한다. 각 개별 색상 채널 r, g, b의 혼합 비율을 제어할 수 있다.

 

#ifdef GL_ES
precision mediump float;
#endif

#define PI 3.14159265359

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

vec3 colorA = vec3(0.149,0.141,0.912);
vec3 colorB = vec3(1.000,0.833,0.224);

float plot (vec2 st, float pct){
  return  smoothstep( pct-0.01, pct, st.y) -
          smoothstep( pct, pct+0.01, st.y);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);

    vec3 pct = vec3(st.x);

    pct.r = smoothstep(0.0,1.0, st.x);
    pct.g = sin(st.x*PI);
    pct.b = pow(st.x,0.5);

    color = mix(colorA, colorB, pct);

    // Plot transition lines for each channel
    color = mix(color,vec3(1.0,0.0,0.0),plot(st,pct.r));
    color = mix(color,vec3(0.0,1.0,0.0),plot(st,pct.g));
    color = mix(color,vec3(0.0,0.0,1.0),plot(st,pct.b));

    gl_FragColor = vec4(color,1.0);
}

25~27번째 줄에서 pct의 r, g, b 값이 결정되고 이 후 color에 mix 함수를 적용해 그라데이션이 형성된다. 예제에 많은 것들이 내포되어 있으므로 이해하고 넘어가는 것이 좋다.


HSB

 

이번에는 색 공간(color space)에 대해 알아보자. R, G, B 채널 외에도 색상을 구성하는 방법이 다양하다.

 

HSB는 색조(Hue), 채도(Saturation), 밝기(Brightness)를 의미하며 더 직관적이고 유용한 색 구성이다. 아래 코드에서 rgb2hsv() 와 hsv2rgb() 함수를 분석하길 바란다.

 

x축의 위치를 Hue에 매핑하고 y축의 위치를 Brightness에 매핑하면 가시적인 색상의 예쁜 스펙트럼을 얻을 수 있다. 이러한 색의 공간적 분포는 매우 편리해서 RGB보다 HSB를 사용하여 색상을 선택하는 것이 더 직관적일 수 있다.

 

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform float u_time;

vec3 rgb2hsb( in vec3 c ){
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz),
                 vec4(c.gb, K.xy),
                 step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r),
                 vec4(c.r, p.yzx),
                 step(p.x, c.r));
    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),
                d / (q.x + e),
                q.x);
}

//  Function from Iñigo Quiles
//  https://www.shadertoy.com/view/MsS3Wc
vec3 hsb2rgb( in vec3 c ){
    vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),
                             6.0)-3.0)-1.0,
                     0.0,
                     1.0 );
    rgb = rgb*rgb*(3.0-2.0*rgb);
    return c.z * mix(vec3(1.0), rgb, c.y);
}

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution;
    vec3 color = vec3(0.0);

    // We map x (0.0 - 1.0) to the hue (0.0 - 1.0)
    // And the y (0.0 - 1.0) to the brightness
    color = hsb2rgb(vec3(st.x,1.0,st.y));

    gl_FragColor = vec4(color,1.0);
}

HSB in polar coordinates

 

HSB는 원래 데카르트 좌표(cartesian coordinates) 대신 극좌표(polar coordinates)로 표현되도록 설계되었다. HSB함수를 극좌표에 매핑하기 위해서는 스크린의 중심에서 픽셀 좌표까지의 각도와 거리를 구해야 한다. 이를 위해 length() 함수와 atan(y, x) (일반적으로 사용되는 atan2(y, x)의 GLSL 버전)을 사용하고자 한다.

 

벡터와 삼각함수를 사용할 때 vec2, vec3, vec4는 색을 나타낼 때도 벡터로 취급한다. 여기서는 색상과 벡터를 비슷하게 다룰 것이다.

 

Note: 기하학적 함수가 더 많이 존재한다. distance(), dot(), cross, normalize(), faceforward(), reflect(), refract() 등이 있으니 찾아보길 바란다. 또한 GLSL은 다음과 같은 특별한 벡터 관계 함수를 갖고 있다. lessThan(), lessThanEqual(), greaterThan(), greaterThanEqual(), equal(), notEqual()

 

각도와 길이를 얻으면 값들을 0.0과 1.0사이의 범위로 정규화(normalize)해야 한다. 27행에서 atan(y,x)은 -PI와 PI 사이의 각(-3.14~3.14)을 라디안으로 반환하므로, 우리는 이 숫자를 -0.5에서 0.5 사이의 값으로 바꾸기 위해 TWO_PI로 나누어야 한다. 여기서 0.5를 더하면 0.0~1.0 범위로 변경할 수 있다. 반지름은 최대 0.5(뷰포트 중심으로부터의 거리를 계산하기 때문에)를 반환하므로 최대 1.0을 얻으려면 이 범위를 두 배로 늘려야 한다.

 

#ifdef GL_ES
precision mediump float;
#endif

#define TWO_PI 6.28318530718

uniform vec2 u_resolution;
uniform float u_time;

//  Function from Iñigo Quiles
//  https://www.shadertoy.com/view/MsS3Wc
vec3 hsb2rgb( in vec3 c ){
    vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),
                             6.0)-3.0)-1.0,
                     0.0,
                     1.0 );
    rgb = rgb*rgb*(3.0-2.0*rgb);
    return c.z * mix( vec3(1.0), rgb, c.y);
}

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution;
    vec3 color = vec3(0.0);

    // Use polar coordinates instead of cartesian
    vec2 toCenter = vec2(0.5)-st;
    float angle = atan(toCenter.y,toCenter.x);
    float radius = length(toCenter)*2.0;

    // Map the angle (-PI to PI) to the Hue (from 0 to 1)
    // and the Saturation to the radius
    color = hsb2rgb(vec3((angle/TWO_PI)+0.5,radius,1.0));

    gl_FragColor = vec4(color,1.0);
}

연습 문제

1. 위 예제를 수정하여 실시간으로 회전하는 색상환으로 만들어보자.

 

2. shaping function을 HSB to RGB 변환 함수와 함께 사용해 특정 Hue값을 확장하고 나머지를 축소해보자. 아래 그림을 보면 이해가 빠를 것이다.

3. 색상환을 자세히 보면 RYB 색 공간에 따라 다른 스펙트럼을 사용한다. 예를 들어 빨간색의 반대 편은 녹색이어야 하지만, 위에서 작성한 예제는 녹색이 아니다. 아래와 같은 색상환으로 고치는 방법은 무엇인가?

 

Note about functions and arguments

 

위 예제들에서 함수의 파라미터에 인수의 타입을 눈치챘는가? 이것은 한정자(qualifier)이며 in은 변수가 읽기 전용으로 지정된다. 또한 out은 해당 변수가 레퍼런스처럼 수정이 된다는 의미이며, inout은 in과 out의 성질 두 가지를 모두 지닌다.

int newFunction(in vec4 aVec4,      // read-only
                out vec3 aVec3,     // write-only
                inout int aInt);    // read-write

 

이제 멋진 그림을 그릴 수 있는 모든 요소는 다 갖춰졌다. 이제부터는 트릭들을 알아나갈 단계이다.

'Graphics > GLSL' 카테고리의 다른 글

06. Shapes  (0) 2022.01.02
04. Shaping functions  (0) 2021.12.30
03. Uniforms, gl_FragCoord  (0) 2021.12.30
02. Hello world!  (0) 2021.12.29
01. Shader란 무엇인가?  (0) 2021.12.29

댓글