OpenGL Shader – 25

GLSL 예제 : Lighting(Directional Lights 1) – 2/6

원본 : http://www.lighthouse3d.com/opengl/glsl/index.php?ogldir1

OpenGL Programming Guide(일명: Red Book)라는 책에 The Mathematics of Lights 챕터에 본 내용에 나오는 공식이 있다.

먼저 Diffuse 용어부터 시작해보자. OpenGL에서 Diffuse 빛은 관찰자의 위치에 상관없이 일정한 빛의 강도이다.Diffuse는 재질의 Diffuse 반사 계수뿐만이 아니라 빛의 Diffuse 강도에 대해 비례한다. 강도는 빛의 방향과 표면의 법선벡터 사이의 각도에 비례한다.


다음 공식은 OpenGL에서 Diffuse를 계산하는데 사용되는 공식이다.


위의 식에서, I는 반사강도, Ld는 빛의 Diffuse 색상(gl_LightSource[0].diffuse), 그리고 Md는 재질의 Diffuse 계수(gl_FrontMaterial.diffuse)이다.

위의 식은 Lambertian 반사식으로 알려져있다. Lambert의 Cosine 법칙은 평면에 대한 표면의 Diffuse 밝기는 시선과 표면의 법선벡터로 형성되어진 Cosine 각에 비례함을 나타낸다. 이 법칙은 200년전의 이론이다. (Johann Heinrich Lambert, 1728~1777)

버텍스 쉐이더에서 이 공식을 구현하기 위해서는 빛의 속성을 사용할 것이고, 주로 빛의 위치와 Diffuse 강도에 대한 속성이다. 또한 재질에 대한 Diffuse 설정 속성도 이용할 것이다. 따라서 버텍스 쉐이더에서는 OpenGL에서 했던 것처럼 빛을 설정하면된다. 그러나 주의할 것은 고정기능을 사용하지 않을 것이기때문에, 빛을 활성화 시킬 필요는 없다.

Cosine을 계산할 필요가 있으므로, 먼저 우리는 법선벡터와 빛의 방향 벡터(gl_LightSource[0].position)를 정규화할 필요가 있다. 그리고 Cosine 값을 얻기 위해서 내적을 사용할 것이다. Directional Lights에 대해서는, OpenGL이 빛의 방향을 위의 그림에서 보는 것처럼 반대로 하고 있음을 알수 있다.

OpenGL은 빛의 방향을 Eye 공간좌표계로 저장하고 있다; 그러므로 우리는 내적을 계산하기 위해서 법선벡터를 Eye 공간좌표계로 변환해야한다. 법선벡터를 Eye 공간좌표계로 변환하기 위해서는, 미리 정의된 Uniform 변수인 mat3 gl_NormalMatrix를 사용해야할 것이다. 이 행렬은 모델뷰행렬로부터 3×3 좌상단매트릭스의 역전치행렬이다.

다음 버텍스 쉐이더는 위에서 언급한 것들을 위한 버텍스 쉐이더 코드이다.

void main() {
    vec3 normal, lightDir;
    vec4 diffuse;
    float NdotL;

    /* 먼저 법선벡터를 Eye 공간좌표계로 변환하고 정규화한다 */
    normal = normalize(gl_NormalMatrix * gl_Normal);
		
    /* 이제 빛의 방향 벡터를 정규화한다. OpenGL의 소펙을 보면, 빛은 Eye 공간좌표계이다.
    또한 우리가 지금 Directional Light에 대해 이야기하고 있으므로, 빛의 위치 필드는
     실제 빛의 방향이다. */
    lightDir = normalize(vec3(gl_LightSource[0].position));

    /* 법선벡터와 빛의 방향 사이의 각도에 대한 Cos 값을 계산한다. 빛은 Directional이므로
     모든 좌표에 대해서 빛의 방향은 일정하다. 이 두벡터는 정규화되었으므로 내적을 구하면된다.
    결과는 0~1 범위 사이로 맞춘다. */
    NdotL = max(dot(normal, lightDir), 0.0);

    /* Diffuse를 계산한다. */
    diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;
		
    gl_FrontColor =  NdotL * diffuse;
		
    gl_Position = ftransform();
}

이제, 프레그먼트 쉐이더에서 해야할 것이 남았는데, gl_Color Varying 변수를 사용해서 프레그먼트의 색상을 설정해보자.

void main()
{
    gl_FragColor = gl_Color;
}

다음 이미지가 주전자 모델에 대해 적용된 결과이다. 주전자의 밑바닦을 보면 매우 어둡게 표현이 되었음을 주목하기 바란다. 이것은 OpenGL에서 Ambient 빛을 아직 적용하지 않았기 때문이다.
Ambient 빛을 통합시키는 것은 어렵지 않다. 아래의 식은 Ambient 빛에 대한 공식이다.


버텍스 쉐이더에서 Ambient를 계산하기 위해서 몇가지 코드를 추가할 필요가 있다.

void main()
{
    vec3 normal, lightDir;
    vec4 diffuse, ambient, globalAmbient;
    float NdotL;
		
    normal = normalize(gl_NormalMatrix * gl_Normal);
    lightDir = normalize(vec3(gl_LightSource[0].position));
    NdotL = max(dot(normal, lightDir), 0.0);
    diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;
		
    /* Compute the ambient and globalAmbient terms */
    ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
    globalAmbient = gl_LightModel.ambient * gl_FrontMaterial.ambient;
		
    gl_FrontColor =  NdotL * diffuse + globalAmbient + ambient;
		
    gl_Position = ftransform();
}

다음 이미지가 최종 결과이다. Ambient 빛을 추가하자 색상을 씻어내는 듯(밝게하다)하고 있다. 결국 효과가 별로라는 얘기이다.
이제 다음 색선에서 Specular 요소에 대해서 살펴보겠다.

 

OpenGL Shader – 24

GLSL 예제 : Lighting(광원) – 1/6
원문 : http://www.lighthouse3d.com/opengl/glsl/index.php?lights

OpenGL에는 세가지 종류의 빛이 있습니다: Directional, Point, Spotlight. 이 장에서는 Directional 광원을 구현하는 것으로 시작해 보겠다. 먼저 GLSL을 이용해서 OpenGL의 광원효과를 모방해 보겠다.

우리는 Ambient 빛으로 시작해서 GLSL을 점진적으로 발전시켜 Specular 빛까지 구현해보겠다.

다음으로 좀더 나은 결과를 제공하는 Lighting Per Pixel을 구현해보겠다.

그리고 이 다음으로는, Point와 Spot Lighting Per Pixel을 구현해보겠다. 빛에 대한 총 6개의 장은 Directional Lights에 관련된 장에서의 코드의 내용을 공통적으로 사용할 것이다.

툰쉐이더에서 언급했듯이, GLSL은 광원 설정에 대한 데이터를 포함하는 OpenGL 상태값에 접근할 수 있다. 이 데이터는 광원 설정에 대한 세세한 내용을 담고 있는 구조체형식의 전역변수이다.

struct gl_LightSourceParameters {
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    vec4 position;
    vec4 halfVector;
    vec3 spotDirection;
    float spotExponent;
    float spotCutoff; // (range: [0.0, 90.0], 180.0)
    float spotCosCutoff; // (range: [1.0, 0.0], -1.0)
    float constantAttenuation;
    float linearAttenuation;
    float quadraticAttenuation;
};

uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];

struct gl_LightModelParameters {
    vec4 ambient;
};

uniform gl_LightModelParameters gl_LightModel;

재질 속성도 역시 GLSL에서 접근할 수 있다.

struct gl_MaterialParameters {
    vec4 emission;
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float shiness;
};

uniform gl_MaterialParameters gl_FrontMaterial;
uniform gl_MaterialParameters gl_BackMaterial;

광원과 재질에 대한 이 파라메터들의 대부분의 사용은 OpenGL 어플리케이션에서 사용하는 것과 유사하다. 이제 우리는 이들 속성을 사용해서 Directional 광원을 구현해 볼 것이다.

OpenGL Shader – 23

GLSL 예제 – 툰쉐이딩 마지막 장.. (4/4)
원본 : http://www.lighthouse3d.com/opengl/glsl/index.php?toon3

툰쉐이딩을 끝내기 전에 한가지 더 살펴보자 : lightDir 변수를 사용하는 대신에 OpenGL 빛을 사용한 것. OpenGL에서 빛을 하나 정의하고 이 빛의 방향을 쉐이더에서 사용하는 방법이다. 주의: glEnable을 사용해서 빛을 활성화할 필요가 없는데, OpenGL에서 이 빛을 실제로 사용하지는 않을 것이기 때문이다.

우리는 OpenGL에서 첫번째 빛(GL_LIGHT0)이 Directional 빛이라고 가정하겠다.

GLSL은 OpenGL 상태의 일부에 접근할 수 있는데, 바로 빛과 같은 속성에 접근할 수 있다. GLSL은 빛의 속성에 대한 C언어 형식의 구조체를 정의하고 있는데 각 빛에 대한 속성을 정의하기 위한 배열로 존재한다.

struct gl_LightSourceParameters {
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    vec4 position;
};

uniform gl_LightSourceParamters gl_LightSource[gl_MaxLights];

위의 구조체와 변수를 이용해서 버텍스 쉐이더에서 빛의 방향을 얻어낼 수 있는데, 구조체의 필드중에 position을 이용하면 된다. 여기서 다시 우리는 OpenGL 어플리케이션에서 빛의 방향 벡터가 정규화되었다고 가장하겠다.

OpenGL은 스펙상 빛의 위치가 지정되면 이 위치 좌표가 자동으로 눈 공간 좌표계(eye space coordinate), 예를 들어서 카메라 좌표계로 바뀐다. 우리는 좌표체계가 바뀌어도 빛의 위치가 정규화된 상태로 유지된다고 가정할 수 있다. 이 가정은 모델뷰해열의 좌측상단의 3×3 부분의 행렬이 직교일때 옳다(만약 gluLookAt함수를 사용하고, 어플리케이션에서 좌표계의 크기조정을 하지 않았다면 확실히 옳다).

우리는 법선벡터를 눈 공간 좌표계(카메라 좌표계)로 변환해야 하며, 빛의 방향벡터와 법선벡터 사이의 각을 계산하기 위해 내적 계산을 해야 한다.

법선벡터를 카메라 좌표계로  변환하기 위해서는 미리 정의된 Uniform  변수인 gl_NormalMatrix를 사용한다. 이 행렬 변수는 모델뷰 매트릭스의 좌상단의 3×3 부분의 역행렬의 전치 행렬이다. 우리는 하나의 버텍스 마다 법선 변환을 수행할 것인데 아래의 코드가 바로 이 변환에 대한 코드이다.

varying vec3 normal;

void main()
{
    normal = gl_NormalMatrix * gl_Normal;
    gl_Position = ftransform();
}

아래의 코드처럼 프레그먼트 쉐이더에서 빛의 위치를 얻어와 빛의 밝기값을 계산한다.

varying vec3 normal;

void main()
{
    float intensity;
    vec3 color;
    vec3 n = normalize(normal);

    intensity = dot(vec3(gl_LightSource[0].position, n);

    if(intensity > 0.95)
        color = vec4(1.0, 0.5, 0.5, 1.0);
    else if(intensity > 0.5)
        color = vec4(0.6, 0.3, 0.3, 1.0);
    else if(intensity > 0.25)
        color = vec4(0.4, 0.2, 0.2, 1.0);
    else
        color = vec4(0.2, 0.1, 0.1, 1.0);

    gl_FragColor = color;
}

최종 소스 코드는 다음을 통해 다운로드 받길 바란다.1264150041.zip1096160345.zip

OpenGL Shader – 22

GLSL 예제 – 툰쉐이딩 3(총4장)
원문 : http://www.lighthouse3d.com/opengl/glsl/index.php?toon2

GLSL은 OpenGL의 상태의 일부에 접근할 수 있다. 이 강좌에서는 OpenGL 어플리케이션에서 glColor로 설정된 색을 읽는 방법에 대해서 살펴보겠다.

GLSL은 현재의 색상값을 가지고 있는 Attribute 변수가 있다. 이 센션에서는, 프레그먼트 마다 툰 쉐이딩 효과를 적용할 것이다. 이렇게 하기 위해서는, 프레그먼트 마다에 대한 법선벡터값을 읽어야 한다. 버텍스 쉐이더는 Varying 변수에 법선벡터를 기록할 필요만 있는 반면, 프레그먼트 쉐이더는 보간된 법선벡터를 읽어야한다.

프레그먼트 쉐이더에서 즉시 빛의 밝기값이 계산되므로 버텍스 쉐이더의 코드는 간단해진다. lightDir은 Uniform 변수인데, 이 변수는 프레그먼트 쉐이더로 옮겨지게되며, 버텍스 쉐이더에서는 더 이상 사용되지 않는다.

varying vec3 normal;

void main()
{
    normal = gl_Normal;
    gl_Position = ftransform();
}

프레그먼트 쉐이더에서, Uniform 변수인 lightDir를 선언할 필요가 있는데, 이 변수가 빛의 밝기값을 계산하는데 사용된다. 보간된 법선벡터를 받기 위해 Varying 변수도 정의해야한다. 아래 프레그먼트 쉐이더의 코드 내용이다.

uniform vec3 lightDir;
varying vec3 normal;

void main()
{
    float intensity;
    vec4 color;

    intensity = dot(lightDir, normal);

    if(intensity > 0.95)
        color = vec4(1.0, 0.5, 0.5, 1.0);
    if(intensity > 0.5)
        color = vec4(0.6, 0.3, 0.3, 1.0);
    if(intensity > 0,25)
        color = vec4(0.4, 0.2, 0.2, 1.0);
    else
        color = vec4(0.2, 0.1, 0.1, 1.0);

    gl_FragColor = color;
}

결과는 다음과 같다.
이전이랑 결과가 똑같네? =_=;; 뭐여….?

이전 장에서 살펴본 것과 이번 장의 것의 차이점을 좀더 살펴보자. 첫번째 것은 빛의 밝기를 버텍스 쉐이더에서 계산을 했고 프레그먼트 쉐이더에서 보간된 값을 사용했다. 두번째 것은 내적을 계산한 프레그먼트 쉐이더를 위해 버텍스쉐이더에서 법선벡터를 보간했다. 보간과 내적 연산은 둘다 선형 연산이므로 내적연산을 수행한 다음에 보간 연산을 수행하나 보간 연산을 수행하고 선형 연산을 수행하나 결과는 동일하다.

그럼 도데체 프레그먼트 쉐이더에서 내적을 위한 법선벡터의 보간의 사용에 뭐가 문제가 있다는 것인가!! 법선벡터가 옳바른 방향을 가지고 있을지라도 법선벡터가 잘못인데, 이유는 법선벡터가 정확히 단위벡터의 길이(1)이 아니기 때문이다.

We know that the direction is right because we assumed that the normals that arrived at the vertex shader were normalized, and interpolating normalized vectors, provides a vector with the correct direction. However the length is wrong in the general case because interpolating normalized normals only yields a unit length vector if the normals being interpolated have the same direction, which is highly unlikely in smooth surfaces. 보다 자세한 내용은 이후에 Normalization 이슈에서 다시 살펴보겠다.

버텍스 쉐이더로부터 프레그먼트 쉐이더로 빛의 밝기계산을 옮긴 주요 이유는 프레그먼트에 대해 적당한 법선벡터를 사용해 계산하기 위함이다. 방향은 옳지만 단위벡터가 아닌 법선 벡터를 가지고 있다. 단위 벡터가 아닌 문제를 해결하려면, 프레그먼트 쉐이더에서 법선벡터를 정규화해주면 된다. 다음의 코드가 이런 문제를 해결한 완벽한 튠쉐이더이다.

uniform vec3 lightDir;
varying vec3 normal;
	
void main()
{
	float intensity;
	vec4 color;

	intensity = dot(lightDir, normalize(normal));
		
	if (intensity > 0.95)
		color = vec4(1.0,0.5,0.5,1.0);
	else if (intensity > 0.5)
		color = vec4(0.6,0.3,0.3,1.0);
	else if (intensity > 0.25)
		color = vec4(0.4,0.2,0.2,1.0);
	else
		color = vec4(0.2,0.1,0.1,1.0);
	
	gl_FragColor = color;
}

위 코드에 대한 결과는 다음과 같다. 훨씬 멋있어 졌당~ 하지만 여전이 완벽하지 않다. 그것은 모서리 부분이 계단처럼 나타나는 문제인데, 이 문제는 이 장의 범위를 벗어난다.
다음 장에서는 쉐이더를 통해 다양한 광원에 대해 살펴보겠다.

OpenGL Shader – 21

GLSL 예제 – 툰 쉐이딩(Toon Shading) – 2장(총4장)
원문 : http://www.lighthouse3d.com/opengl/glsl/index.php?toon1

이 장에서 보일 첫번째 툰 쉐이딩은 버텍스에 대한 명도를 계산한다. 다음으로 프레그먼트 쉐이더는 프레그먼트에 대한 톤을 계산하기 위해 버텍스에 대한 보간된 명도를 사용한다. 그래서 버텍스 쉐이더는 반드시 명도를 저장하는 변수를 Varying으로 선언해야 한다. 프레그먼트 쉐이더는 같은 변수를 선언해야하며 Varying 지정자를 사용하여 버텍스 쉐이더에서 계산된 명도값을 받을 수 있다.

빛의 방향은 지역변수나 상수로써 버텍스 쉐이더 안에 정의될 수 있다. 그러나 Uniform 변수로써 정의되면 융통성이 좋은데, OpenGL 어플리케이션에서 자유롭게 설정할 수 있기 때문이다. 빛의 방향에 대한 변수는 다음과 같이 쉐이더 안에서 정의된다.

uniform vec3 lightDir;

지금부터는,  빛의 방향을 World 공간에서 정의되었다고 가정하겠다.

버텍스 쉐이더는 OpenGL 어플리케이션에서 지정된 법선벡터에 접근할 수 있는데, gl_Normal이라는 Attribute 변수를 통해 가능하다. 이 법선벡터는 OpenGL 어플리케이션에서 glNormal 함수를 통해 정의한 것이며 Local 공간에서 정의된다.

만약 OpenGL 어플리케이션에서 모델에 대해 회전이나 크기조정이 수행되지 않는다면, World 공간에서 정의된 법선벡터는 gl_Normal 변수를 통해 버텍스 쉐이더에 제공되며 Local 공간에서 정의된 법선벡터와 일치한다. 법선 벡터는 방향벡터이므로 이동에는 영향을 받지 않는다. 법선벡터와 빛의 방향 모두 같은 공간에서 지정되므로, 버텍스 쉐이더는빛의 방향 사이의 각(밫의 방향벡터와 법선벡터 사이 각)에 대한 코사인(cos) 계산으로 바로 계산할 수 있다. 코사인(cos)은 다음 공식을 사용해 계산할 수 있다.

cos(lightDir, normal) = (lightDir dot-product normal) / ( |lightDir| * |normal| )

법선벡터(normal)과 빛의방향벡터(lightDir)가 단위벡터라면 위의 공식은 다음처럼 간단하게 된다.

cos(빛과 법선벡터의 사이각) = lightDir dot-product normal

lightDir 변수는 OpenGL 어플리케이션에서 제공받으므로, 이 변수는 이미 단위벡터라고 가정할 수 있다. 이런 가정이 가능하다면 모든 버텍스에 대해 매번 단위벡터화를 해주지 않고도, 단지 lightDir 변수가 바뀔때 단 한번만 단위벡터로 바꾸면 된다.

GLSL에서 제공하는 dot 함수를 사용해서 intensity라는 이름의 변수에 위에서 설명한 값을 저장한다.

intensity = dot(lightDir, gl_Normal);

이제 버텍스 쉐이더 부분에서 마지막으로 해야할 것은 버텍스 좌표로 변환하는 것이다. 아래가 완성된 코드이다.

uniform vec3 lightDir;
varying float intensity;

void main()
{
    intensity = dot(lightDir, gl_Normal);

    gl_Position = ftransform();
}

이제, 프레그먼트 쉐이더에서 해야할 것은 intensity에 기반해서 프레그먼트의 색을 지정하는 것이다. intensity는 반드시 프레그먼트 쉐이더로 넘겨줘야하는데, 프레그먼트의 색상을 설정할 책임이 바로 프레그먼트 쉐이더에 있기 때문이다. 이전에 언급했듯이 intensity는 버텍스 쉐이더나 프레그먼트 쉐이더 모두에서 Varying 지정자로 정의된다. Varying 변수는 버텍스 쉐이더에서 설정되고 난뒤에 프레그먼트 쉐이더에서 읽혀진다.

색상은 다음처럼 프레그먼트 쉐이더에서 계산되어진다.

vec4 color;

if(intensity > 0.95)
    color = vec4(1.0, 0.5, 0.5, 1.0);
else if(intensity > 0.5)
    color = vec4(0.6, 0.3, 0.3, 1.0);
else if(intensity > 0.25)
    color = vec4(0.4, 0.2, 0.2, 1.0);
else
    color = vec4(0.2, 0.1, 0.1, 1.0);

위의 코드를 보면, 코사인값(intensity)이 0.95보다 크면 가장 밝은 색상으로 지정하고 0.25보다 작으면 가장 어두운 색상으로 지정하고 있다. 프레그먼트 쉐이더에서 해야할 것은 color 변수수를 기반으로 해서 gl_FragColor를 설정하는 것이다. 프레그먼트 쉐이더에 대한 전체 코드는 다음과 같다.

varying float intensity;

void main()
{
    vec4 color;

    if(intensity > 0.95)
        color = vec4(1.0, 0.5, 0.5, 1.0);
    else if(intensity > 0.5)
        color = vec4(0.6, 0.3, 0.3, 1.0);
    else if(intensity > 0.25)
        color = vec4(0.2, 0.1, 0.1, 1.0);

    gl_FragColor = color;
}

아래의 이미지가 최종 결과이다. 근데 그다지 멋있지는 않은것 같은데, 어떻게 생각하는가? 좀더 근사한 툰 쉐이딩을 연출하기 위해서는 intensity 계산(보간) 방법을 개선해야 한다. 이에 대해서는 다음 섹션에서 다루도록 하겠다.