본문 바로가기
셰이더 (Shader)/The Renderman Shading Language Guide

[RSL] Part2 : Color Math (134p ~ 155p)

by Minkyu Lee 2023. 5. 30.

색상 조작은 셰이더 개발의 중요한 부분이다.
다른 공간(space)에서 색상을 표현하면 작업이 훨씬 쉬워질 수 있는 상황에 자주 마주하게 될 것이다.

 

또한 색상을 곱셈, 덧셈, 뺄셈해야 할 수도 있다.

그래서 조작법과 그 결과를 알아야한다.

 

이 섹션에서는 색상 수학의 이론을 살펴본다.

두 가지 색상에 적용할 수 있는 모든 주요 작업에 대한 함수를 제시한다.

 

Color Theory and Spaces

색상이 작동하는 방식을 이해하기 위해 먼저 빛의 물리적 특성을 알아보자.

 

1665년 ~ 1666년 사이에, 아이작 뉴턴은 태양빛이 유리 프리즘을 통과하면 생기는 "색상의 스펙트럼"을 발견했다.

스펙트럼은 빨강부터 보라색까지 모든 색상으로 이루어져있다.

 

무지개를 보는 것이 바로 그런 현상이다.

무지개는 공기 중의 안개를 통해 굴절되는 태양빛이다.

 

빛이 물체에 비추면 그 물체는 일반적으로 스펙트럼에서 일정량의 색상을 흡수한다. 

흡수되지 않은 색상은 환경으로 반사된다.

 

물체가 반사하는 색상우리 눈이 보는 색상이다.

이것이 물체에 인식 가능한 색상을 부여한다.

예를 들어 빨간색으로 보이는 물체는 빨강색을 제외한 모든 색상을 흡수한다.

 

흰색 물체는 스펙트럼에서 모든 색상을 반사한다.

검은색 물체모든 색상을 흡수한다.

이것이 흑색 물체가 태양광에 노출될 때 흰색 물체보다 빠르고 강하게 가열되는 이유이다.

 

모든 색상을 생성하기 위해 여러 가지 방법이 있다.

과 같은 가산 색상에서는 주요 색상이 빨강, 녹색, 파랑(RGB)이다.

인쇄 잉크와 같은 감산 색상에서는 청색, 마젠타, 노랑, 검정(K)으로 표현되는 주요 색상이다. 

 

Figure 5.32는 가산 및 감산 주요 색상이 어떻게 결합되는지를 보여주는 예이다.

RenderMan은 float으로 RGB(빨강, 녹색, 파랑) 공간에 색상을 저장한다. 

각 채널은 다른 채널에 더해져서 최종 색상이 결정된다.

 

인터넷 상의 대부분의 이미지나 일반적인 디지털 카메라로 촬영한 이미지는 8비트 이미지이다.

각각의 색상을 구성하는 세 개의 채널마다 최대 256 단계의 값을 저장할 수 있다.

 

RGB / 8비트 이미지에서 (0,0,0)검은색을 나타낸다.

(255,255,255)흰색을 나타낸다.

이는 일상적인 디지털 이미지에는 충분한 단계이다.

 

그러나 영화, VFX, 애니메이션에서 사용될 이미지의 경우, 256 단계는 충분하지 않다.

그래서 RenderMan은 색상 값을 부동 소수점 값으로 계산한다.

각 채널은 최대 여섯 자리의 소수점 값을 가질 수 있다.

이는 필요한 모든 색상을 표현하기에 충분한 값들을 제공한다.

 

부동 소수점 값은 검은색은 (0,0,0)으로, 흰색은 (1,1,1)로 표현한다.

주의할 점은 부동 소수점 정밀도 이미지는 0과 1 사이의 값으로 제한되는 것은 아니라는 점이다.

(10,10,10) 값을 가진 이미지도 계산할 수 있다.

이 이미지는 눈으로는 흰색으로 보이지만, 합성 작업 시 도움이 된다.

 

Add, Subtract, Divide, and Multiply

가장 기본적인 수학 연산은 덧셈, 뺄셈, 나눗셈 및 곱셈이다.

이러한 연산을 색상 값에 적용할 때, 각 구성 요소에 대해 수행된다.

 

예를 들어, (0.5, 0.4, 0.1)를 (0.3, 0.2, 0.3)에 더하면 값이 (0.8, 0.6, 0.4)인 색상이 생성된다.

뺄셈, 나눗셈 및 곱셈에도 마찬가지다.

 

색상 곱셈도 마찬가지로 작동한다.

덧셈과 곱셈은 교환법칙이 적용되므로, 요소의 순서는 중요하지 않다.

그러나 뺄셈과 나눗셈은 교환법칙이 적용되지 않는다.

뺄셈에서는 두 번째 요소가 첫 번째 요소에서 빼지고, 나눗셈에서는 첫 번째 요소가 두 번째 요소로 나누어진다.

(0.7,0.5,0.8) - (0.5,0.4,0.1) = (0.2,0.1,0.7)
(0.9,0.4,0.6) / (0.3,0.2,0.3) = (3,2,2);

 

이러한 연산을 수행시 주의해야한다.

왜냐하면 0에서 1 범위를 벗어나는 색상이 생성될 수 있기 때문이다.

이 계산 결과를 이미지로 저장하고 이를 합성에서 처리해야 할 수도 있기 때문이다.

 

다음은 연산 결과이다.

- Addition: 색상이 더 밝아지며 두 색상의 혼합색이 된다. 색상이 희미해 보일 수 있다. 값은 1을 초과할 수 있다.
- Subtraction: 색상이 어두워지며 탁하고 흐리멍텅한 음영이 될 수 있습니다. 값은 음수값이 될 수 있다.
- Multiplication: 색상이 어두워지며 마치 두 색상이 필터링된것처럼 보인다.
- Division: 대개 색상이 흰색이 된다. 값은 1을 초과할 수 있다.

 

Color to Float

RenderMan 표준은 float 값을 color로 변환하기 위한 기본 내장 함수를 제공한다.

이러한 경우, 셰이더 컴파일러는 전달된 값을 세 개의 채널로 복제하여 color를 생성한다.

따라서 다음과 같은 방식도 사용할 수 있다.
color white = 1;

 

RenderMan에서는 자동으로 색상을 부동 소수점으로 변환하는 기능을 제공하지 않는다.

색상을 부동 소수점으로 변환하려고 할 때, 조건문에서 색상을 비교하면 셰이더 컴파일 오류가 발생한다.

대부분의 색상은 변동하는 값이므로, 조건문이나 루프 문에서 변동 변수를 사용하는 것은 많은 문제를 일으킬 수 있다.

또한 성능에도 부정적인 영향을 미친다.

 

이러한 이유로 우리는 직접 "색상을 부동 소수점으로 변환"하는 함수를 만들 것이다.

이 변환 함수를 정의함으로써 조건문에서 색상을 비교하거나 변동 변수를 사용할 때 발생할 수 있는 문제를 피할 수 있다.

만약 색상이 세 개의 실수 값이라고 생각해보라.

해당 컬러의 세 구성 요소를 더한 다음 그 값을 3으로 나누면 해당 컬러 값을 평균 실수 값을 구할 수 있다.

이는 색상 값을 실수로 변환하는 가장 간단한 방법이다.

 

두 가지 변환 방법을 소개한다.

첫 번째는 RSL 함수이고, 두 번째는 CPP 매크로이다.

이 구현에서는 각 색상 채널에 접근하기 위해 새로운 배열 표기법을 사용한다.

렌더러가 이 새로운 RSL 기능을 지원하지 않는 경우, 컴포넌트 (comp()) 표기법을 사용할 수 있다.

float color2float (color col){
return (col[0]+col[1]+col[2])/3;}
#define COLOR2FLOAT(x) (x[0]+x[1]+x[2])/3

 

Advanced Color Mixing Operations

Photoshop을 사용해봤다면 다양한 레이어 블렌딩 모드에 익숙할 것이다.

스크린(screen)과 오버레이(overlay)와 같은 블렌딩 모드는 흔하게 사용된다. 

 

이러한 블렌딩 모드는 매우 유용하다.

이에 대한 RSL 함수를 만들어보자.

 

이 책에서 제시된 모든 블렌딩 모드 함수는 다음과 같은 네이밍 규칙을 사용한다.
■ colorMode은 함수가 수행하는 모드를 설명하는 부분이다.
■ BaseMap은 블렌딩의 아래쪽에 위치하는 색상 값이다.
■ Layer는 블렌딩 레이어의 색상이다.
■ LayerOpac은 블렌딩을 제어하는 불투명도 값이다.


Figure 5.33과 5.34의 이미지 맵을 색상 값으로 사용하여 효과를 만들어보자.

 

이 함수들은 Jens Gruschel의 작품을 기반으로 만들어졌다.

자세한 사항은 아래 링크를 참조하라.

http://pegtop.net/

colorOver()

가장 기본적인 블렌딩 모드는 Over이다.

이는 BaseMap과 Layer 사이에서 LayerOpac 값을 혼합값으로 사용하여 단순한 선형 보간을 한다.

코드에서는 내장 함수인 mix()를 사용하여 작업한다.

 

mix()를 사용해도 된다.

하지만 일관성을 위해 이 책에선 colorOver() 모드를 정의하여 사용한다.

 

Figure 5.35는 적용 결과이다.

return( color mix(BaseMap,Layer,LayerOpac));}

 

colorAdd(), colorSubtract(), and colorMultiply()

이 세 가지 모드는 일반적인 덧셈, 뺄셈 및 곱셈 연산자를 사용하는 것과 거의 같다.

다른 점은 이러한 연산을 사용하여 알파나 밝기 맵과 같은 혼합값을 사용할 수 있다는 점이다.

 

colorAdd() 및 colorSubtract()의 결과는 Figure 5.36 및 5.37에서 확인하라.

/**********
* Add mode
* Color add mode performs a simple color addition
**********/
color colorAdd(color BaseMap; color Layer; float LayerOpac){
	return BaseMap + (Layer * LayerOpac);}
    
/**********
* subtract mode
* Color subtract mode performs a simple color subtraction
***********/
color colorSubtract(color BaseMap; color Layer; float LayerOpac){
	return BaseMap + ((Layer-1)* LayerOpac);}

 

 

주목할 점은 곱셈 함수에 약간의 추가적인 코드가 있다는 것이다.

이것은 곱셈을 제어하는 혼합값으로 사용하기 위한 것이다.

 

먼저 Layer 색상에 LayerOpac 값을 곱한다.

그러면 Layer가 LayerOpac 값에 의해 어둡게 될 것이다.

 

이 값을 BaseMap에 바로 곱하면 곱셈의 출력은 LayerOpac 값에 기반하여, 전체 텍스처가 어둡게 된다.

(곱셈 시 0이 포함된 경우 결과는 항상 0이 된다)

이는 원하는 효과가 아니다.

 

이 문제를 해결하기 위해 LayerOpac의 반전 값을 Layer와 LayerOpac의 곱셈에 다시 더해준다.

 

Figure 5.38, 5.39, 5.40은 우리가 반전을 수행하지 않은 경우와,

반전을 수행한 경우(colorMultiply 함수)가 반환하는 차이를 보여준다.

/*********
* Multiply mode
* Multiply mode performs as simple multiplication, taking into account the
* value of LayerOpac
**********/

Figure 5.37 colorSubtract().
color colorMultiply(color BaseMap; color Layer; float LayerOpac){
	return BaseMap * ((Layer * LayerOpac) + ( 1 - LayerOpac));
}

colorDissolve()

Dissolve 는 두 개의 색상을 섞기 위해 무작위 노이즈 값을 사용한다.

 

실제로는, rand() 결과를 LayerOpac 값과 비교한다.

만약 rand 값이 작으면, Layer 색상이 사용되고, 더 크면 BaseMap 색상이 사용된다.

그 결과, BaseMap 위에 Layer 가 노이지한 혼합이 이루어진다.

 

Figure 5.41에서 결과를 확인해보자.

/******
* Dissolve
*
* Use the value of LayerOpac to randomly choose between Layer
* BaseMap
*
******/

color colorDissolve(color BaseMap; color Layer; float LayerOpac){
	color out;
    
	if (float random() < (LayerOpac))
		out = Layer;
	else
		out = BaseMap;
	return out;
}

 

colorScreen()

Screen 모드는 Photoshop에서 가장 유용한 블렌딩 모드 중 하나이다.

 

이는 반전 곱셈(inverted multiplication)이다.

두 색상이 모두 반전되어 곱해진다.

이후, 다시 한 번 반전되어 색상을 양수로 만든다.

 

LayerOpac 값이 원하는 효과를 내려면 약간의 트릭이 필요하다.

Figure 5.42에서 colorScreen()을 확인해보라.

/****
* Screen Mode
*
* Screen is almost the opposite of Multiply. Both layers are
* inverted, multiplied by each other and then the result is
* inverted one more time
*
*****/

color colorScreen(color BaseMap;color Layer; float LayerOpac){
	return 1 - ((1-BaseMap) * (1-Layer * LayerOpac));
}

 

colorOverlay()

Overlay 모드는 colorMultiply()와 colorScreen()의 결합이다.

블렌딩 레이어의 값에 따라 곱셈 또는 스크린을 수행할지 결정한다.

블렌딩 레이어의 값이 0.5보다 크면 곱셈을 수행하고, 그렇지 않으면 스크린을 수행한다.

 

이 함수에서는 LayerOpac 값을 수정하여, 레이어의 불투명도가 0으로 설정되었을 때,

BaseMap이 변경되지 않도록 조정해야한다.

 

Figure 5.43을 확인해보자.

/****
* Overlay Mode
*
* is almost the opposite of Multiply. Both layers are
* inverted, multiplied by each other and then the result is
* inverted one more time
*
*****/

color colorOverlay(color BaseMap; color Layer; float LayerOpac){
	float layerval= colorToFloat(Layer);
	return (layerval > 0.5) ? (2 * BaseMap * Layer * LayerOpac)
		+ BaseMap * (1-LayerOpac):
		1 - ((1-BaseMap) * ( 1 - Layer * LayerOpac))*(2 - (1-LayerOpac));
}

 

colorDarken() and colorLighten()

Darken 블렌딩 모드는 Multiply 모드와 결과가 비슷해보인다.

완전히 흰색인 레이어는 이미지에 영향을 주지 않으며, 검은색 레이어는 검은색 이미지를 출력한다.

 

그러나 이 모드에서 사용되는 수학은 Multiply와 매우 다르다.

Darken 모드에서는 두 이미지를 픽셀 단위로 서로 비교한다.

만약 레이어의 값이 베이스보다 작으면 베이스 색상을 사용한다.

크면 레이어 색상을 사용합니다.

 

Figure 5.44에서 colorDarken()의 출력을 확인하라.

/****
* Darken Mode
* Both layers are compared to each other. If the layer is greater than
* the base then the base is used, if not then the layer is used
*****/

color colorDarken(color BaseMap; color Layer; float LayerOpac){
	float baseval = colorToFloat(BaseMap);
	float layerval= colorToFloat(Layer);
	return (baseval < layerval) ? BaseMap: Layer * LayerOpac +
		(BaseMap * (1-LayerOpac));
}

/****
* Lighten Mode
* Both layers are compared to each other. If the layer is smaller than
* the base then the base is used, if not then the layer is used
*****/

color colorLighten(color BaseMap; color Layer; float LayerOpac){
	float baseval = colorToFloat(BaseMap);
	float layerval= colorToFloat(Layer);
	return (baseval > layerval) ? BaseMap: Layer * LayerOpac +
		(BaseMap * (1-LayerOpac));
}

colorDifference()

베이스 레이어의 색상을 상위 레이어의 값으로 반전시켜보자.

1960년대와 1970년대의 사이키델릭 포스터를 연상시키는 매우 흥미로운 효과가 나타난다.

이 모드는 텍스처를 그릴 때 유용하게 사용될 수 있다.

 

작동 방식은 매우 간단하다.

레이어의 값을 베이스 맵에서 빼준다.

그런 다음 결과에 abs() 함수를 적용한다. (적용하지 않으면, 음수 값이 생긴다.)

 

RSL의 내장된 abs() 함수는 색상이 아니라 부동 소수점만 지원한다.

그래서 색상의 절댓값을 반환할 수 있는 함수를 만들 것이다.

/*********
* colorAbs()
* Returns the absolute value of a color
********/
color colorAbs(color col){
	return color( abs(col[0]), abs(col[1]), abs(col[2])); }

알파 값의 사용을 고려해야한다.

불투명도를 구현하는 것은 매우 간단하다.

불투명도 값에 레이어 색상을 곱한 다음 베이스 레이어에서 뺀다.

 

이것이 최종적인 colorDifference() 함수의 모습이다.

Figure 5.46은 함수의 결과이다.

/*********
* Difference mode
* Returns the absolute value of the subtraction of layer from basemap
********/
color colorDifference (color BaseMap; color Layer; float LayerOpac){
	return colorAbs(BaseMap – (Layer * LayerOpac));}

 

colorHardlight() and colorSoftlight()

이 두 가지도 자주 사용된다.

Hardlight 모드는 블렌딩 레이어가 어두운 경우, 베이스 레이어를 어둡게하고,

블렌딩 레이어가 밝은 경우, 밝게한다.

이는 Screen과 Multiply 모드의 혼합이다.

 

Softlight는 위와 비슷하지만 훨씬 덜 강렬하다.

블렌딩 레이어의 어두운 영역은 베이스 레이어를 약간 어둡게 만든다.


이전에 언급한 대로, Hardlight는 Screen과 Multiply의 혼합이다.

colorOverlay() 모드와 거의 동일하지만, 곱셈과 스크린을 바꿨다는 점이 다르다.

 

Overlay에서는 블렌딩 레이어의 값이 0.5보다 큰 경우에 곱셈을 했다.

Hardlight는 정확히 반대이다.

 

Figure 5.47에서 확인해보자.

/**************
* Hardlight mode
* Hardlight is pretty much the same as overlay but we reverse the conditional
* statement so that if the value of the blending layers is LESS than 0.5
* we multiply and if it is larger we screen
*********/
color colorHardlight(color BaseMap;color Layer; float LayerOpac){
	float layerval= colorToFloat(Layer);
	return (layerval < 0.5) ? (2 *BaseMap*Layer * LayerOpac) +
		BaseMap * (1-LayerOpac):
		1 - ((1-BaseMap) * ( 1 - Layer *
LayerOpac))*
		(2 - (1-LayerOpac));
}

 

소프트라이트 모드는 하드라이트와 결과가 비슷하다.

하지만 내부적으로는 전혀 다른 코드를 사용한다.

 

두 개의 레이어를 곱셈한다.

이후에 베이스 레이어를 더한다.

또한 스크린된 블렌드 레이어와 곱해진 베이스 레이어를 더하는 연속적인 연산을 한다.

이 블렌드 모드에서는 LayerOpac의 기여를 제어하기 위해 mix() 함수를 사용한다.

 

Figure 5.48은 colorSoftlight()의 결과이다.

/**************
* Softlight mode
* Softlight generates an output similar to Hardlight but with a more subtle
* effect.
*********/
color colorSoftlight(color BaseMap; color Layer; float LayerOpac){
	color ctemp = BaseMap * Layer;
	return mix(BaseMap,ctemp + (BaseMap * (1-((1-BaseMap)*
		(1-Layer)) - ctemp)),LayerOpac);
}

 


colorDodge() and colorBurn()

마지막으로 소개할 두 가지는 Dodge(밝게 만들기)와 Burn(어둡게 만들기)이다.

 

Dodge의 밝게 만드는 효과는 colorLighten()을 적용한 것과는 다르다.

Dodge 작업은 블렌딩 레이어의 강도에 따라 BaseMap의 색상 값을 증가시킨다.

블렌딩 레이어가 검정색인 경우 Dodge 작업은 효과가 없다.

블렌딩 레이어가 밝은 곳에서 훨씬 더 강렬해진다.

 

colorDodge()는 BaseMap의 색상 값을 블렌딩 레이어의 색상 값으로 나눈다.

간단해보인다. 하지만 제대로 동작하는 colorDodge() 함수를 만들기 위해선 고려해야 할 점이 몇가지 있다.

 

첫째로, 항상 그렇듯 0으로 나누는 경우를 주의해야한다.

이는 렌더링 오류를 발생시킬 수 있다.

 

이를 해결하기 위해 max() 함수를 사용한다.

또한 mix() 함수를 사용하여 레이어가 BaseMap 위에 얼마나 혼합되는지를 제어한다.

나눗셈은 0에서 1 사이의 범위를 초과하는 값을 반환할 수 있다. 그래서 clamp() 함수를 사용해 제한한다.

 

Figure 5.49는 colorDodge()의 결과이다.

/**********
* Dodge mode
* The Dodge mode is obtained by dividing the BaseMap by the inverse of the
* Layer value.
* NOTE - we must use a max() to prevent a division by zero which
* would result in rendering errors.
***************/
color colorDodge(color BaseMap; color Layer; float LayerOpac){
	color ctemp = mix(BaseMap,BaseMap / max(1-Layer,color(0.00001)),
LayerOpac);
	return clamp(ctemp,color(0),color(1));
}

/**********
* Burn mode
* The burn mode is obtained by dividing the inverse BaseMap by the
* Layer value and then inverting that result.
* NOTE - we must use a max() to prevent a division by zero which
* would result in rendering errors.
***************/
color colorBurn(color BaseMap; color Layer; float LayerOpac){
	color ctemp = mix(BaseMap,1-((1-
BaseMap)/max(Layer,color(0.00001))),LayerOpac);
	return clamp(ctemp,color(0),color(1));
}

색상 값을 조합하는 다른 방법도 많이 있다.

또한 여기에서 다루지 않는 다른 블렌딩 모드들도 있다. 

예를 들면, Jens Gruschel의 블렌딩 모드 페이지를 참조하여 새로운 블렌딩 모드를 쉽게 만들 수 있다.

댓글