색깔
이전 장에서 색상을 간략하게 사용하고 조작했지만, 제대로 정의하지는 않았습니다. 이번 장에서는 색상이 무엇인지 살펴보고, 앞으로 다룰 조명 관련 장을 위한 기초를 다져보겠습니다.
실제 세계에서 색상은 알려진 모든 색상 값을 가질 수 있으며, 각 물체는 고유한 색상을 지닙니다. 디지털 세계에서는 (무한한) 실제 색상을 (제한된) 디지털 값으로 변환해야 하므로 모든 실제 색상을 디지털로 표현할 수는 없습니다. 색상은 일반적으로 RGB로 약칭되는 빨강, 초록, 파랑 세 가지 구성 요소를 사용하여 디지털 방식으로 표현됩니다. 0~1 범위 내에서 이 세 가지 값의 다양한 조합을 사용하여 거의 모든 색상을 표현할 수 있습니다. 예를 들어 코랄색을 얻으려면 다음과 같이 색상 벡터를 정의합니다.
glm::vec3 coral(1.0f, 0.5f, 0.31f);
우리가 실생활에서 보는 물체의 색깔은 물체가 실제로 가지고 있는 색깔이 아니라 물체에서 반사된 색깔입니다. 물체에 흡수되지 않고 반사되는 색깔이 우리가 인식하는 색깔입니다. 예를 들어, 태양광은 여러 가지 색깔이 합쳐진 백색광으로 인식됩니다(이미지 참조). 이 백색광을 파란색 장난감에 비추면 장난감은 파란색을 제외한 모든 백색광의 색깔을 흡수합니다. 파란색은 흡수되지 않고 반사되기 때문에 우리 눈에 들어와 장난감이 파란색으로 보이는 것입니다. 다음 이미지는 코랄색 장난감이 다양한 강도의 여러 색깔을 반사하는 모습을 보여줍니다.

보시다시피, 하얀 햇빛은 가시광선의 모든 색이 모여 있는 것이며, 물체는 그 색들 중 상당 부분을 흡수합니다. 물체는 자신의 색을 나타내는 색들만 반사하고, 이 색들이 합쳐져 우리가 인지하는 색(이 경우 코랄색)이 됩니다.
엄밀히 말하면 좀 더 복잡하지만, PBR 챕터에서 자세히 다루겠습니다.
이러한 색상 반사 규칙은 그래픽 영역에 직접 적용됩니다. OpenGL에서 광원을 정의할 때 이 광원에 색상을 지정해야 합니다. 이전 단락에서 흰색을 사용했으므로 광원에도 흰색을 지정합니다. 그런 다음 광원의 색상과 객체의 색상 값을 곱하면 결과 색상이 객체의 반사 색상(즉, 인식되는 색상)이 됩니다. 코랄색 값을 가진 예제를 다시 살펴보고 그래픽 영역에서 인식되는 색상을 어떻게 계산하는지 알아보겠습니다. 광원과 객체의 색상 벡터를 각 요소별로 곱하여 결과 색상 벡터를 얻습니다.
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
장난감의 색깔이 백색광의 상당 부분을 흡수하지만, 자체 색 값에 따라 여러 가지 빨강, 초록, 파랑 값을 반사하는 것을 볼 수 있습니다. 이것은 실제 생활에서 색이 작용하는 방식을 나타낸 것입니다. 따라서 물체의 색은 광원에서 반사되는 각 색 성분의 양으로 정의할 수 있습니다. 그렇다면 초록색 빛을 사용하면 어떻게 될까요?
glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);
보시다시피, 이 장난감은 흡수하거나 반사할 빨간색과 파란색 빛이 없습니다. 또한 장난감은 빛의 초록색 성분의 절반을 흡수하고, 나머지 절반은 반사합니다. 따라서 우리가 인지하는 장난감의 색은 어두운 녹색이 될 것입니다. 초록색 빛을 사용하면 초록색 성분만 반사되어 인지되고, 빨간색과 파란색은 인지되지 않는다는 것을 알 수 있습니다. 결과적으로 산호 모양의 물체는 갑자기 어두운 녹색으로 보이게 됩니다. 어두운 올리브 그린색 빛을 사용한 다른 예를 살펴보겠습니다.
glm::vec3 lightColor(0.33f, 0.42f, 0.18f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);
보시다시피, 다양한 색상의 빛을 이용하면 물체에서 흥미로운 색상을 만들어낼 수 있습니다. 색상을 활용해서 창의적인 표현을 하는 것은 어렵지 않습니다.
하지만 색깔 이야기는 이쯤 하고, 이제 실험을 해볼 수 있는 장면을 만들어 봅시다.
조명이 포함된 장면
이제 광원을 사용하려고 하므로, 장면에서 광원을 시각적 객체로 표시하고 조명을 시뮬레이션할 객체를 하나 이상 추가하고자 합니다.
먼저 빛을 비출 대상이 필요하며, 이전 장에서 다뤘던 컨테이너 큐브를 사용하겠습니다. 또한 3D 장면에서 광원의 위치를 나타낼 광원 객체도 필요합니다. 간단하게 설명하기 위해 광원도 큐브로 표현하겠습니다(정점 데이터는 이미 있으니까요).
정점 버퍼 객체를 채우고, 정점 속성 포인터를 설정하는 등의 과정은 이제 여러분에게 익숙할 테니 자세히 설명하지는 않겠습니다. 만약 여전히 이러한 내용이 이해가 안 된다면, 이전 장들을 복습하고 가능하면 연습 문제를 풀어본 후 다음 장으로 넘어가시기 바랍니다.
우선 컨테이너를 그리기 위한 정점 셰이더가 필요합니다. 컨테이너의 정점 위치는 이전과 동일하게 유지되므로(이번에는 텍스처 좌표가 필요하지 않지만) 코드는 크게 달라지지 않을 것입니다. 지난 장에서 사용했던 정점 셰이더의 간소화된 버전을 사용하겠습니다.
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
새로운 정점 셰이더에 맞게 정점 데이터와 속성 포인터를 업데이트해야 합니다(원한다면 텍스처 데이터와 속성 포인터를 계속 활성화 상태로 유지할 수도 있지만, 현재는 사용하지 않습니다).
광원 큐브도 렌더링할 예정이므로 광원 전용으로 새로운 VAO를 생성하려고 합니다. 동일한 VAO를 사용하여 광원을 렌더링한 다음 모델 행렬에 몇 가지 광원 위치 변환을 적용할 수도 있지만, 향후 챕터에서 컨테이너 객체의 정점 데이터와 속성 포인터를 자주 변경할 것이며 이러한 변경 사항이 광원 객체에 반영되는 것을 원하지 않습니다(광원 큐브의 정점 위치만 중요합니다). 따라서 새로운 VAO를 생성하겠습니다.
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 우리는 VBO에 바인딩하기만 하면 됩니다. 컨테이너의 VBO 데이터에는 이미 데이터가 포함되어 있습니다.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 정점 속성을 설정합니다
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
코드는 비교적 간단할 것입니다. 컨테이너와 광원 큐브를 모두 생성했으므로 이제 남은 것은 컨테이너와 광원 모두에 대한 프래그먼트 셰이더를 정의하는 것입니다.
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
FragColor = vec4(lightColor * objectColor, 1.0);
}
프래그먼트 셰이더는 유니폼 변수에서 객체 색상과 광원 색상을 모두 입력받습니다. 여기서는 이 장의 시작 부분에서 설명했듯이 광원 색상과 객체의 (반사된) 색상을 곱합니다. 이 셰이더 역시 이해하기 쉬울 것입니다. 이제 객체 색상을 이전 섹션에서 다룬 코랄색으로 설정하고 광원을 흰색으로 설정해 보겠습니다.
// 유니폼을 설정하기 전에 셰이저 프로그램을 먼저 활성화 하는것을 잊지 마세요.
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
마지막으로 한 가지 더 말씀드릴 점은 다음 장에서 조명 셰이더를 업데이트할 때 광원 큐브에도 영향을 미친다는 것입니다. 하지만 이는 우리가 원하는 바가 아닙니다. 광원 객체의 색상이 조명 계산에 영향을 미치는 것을 원하지 않으며, 광원을 다른 부분과 완전히 분리해야 합니다. 광원은 다른 색상 변화에 영향을 받지 않고 일정한 밝은 색상을 유지해야 합니다. (이렇게 하면 광원 큐브가 실제로 광원인 것처럼 보입니다.)
이를 위해 광원 큐브를 그리는 데 사용할 두 번째 셰이더 세트를 생성해야 합니다. 이렇게 하면 조명 셰이더의 변경 사항으로부터 안전하게 보호할 수 있습니다. 정점 셰이더는 조명 정점 셰이더와 동일하므로 소스 코드를 그대로 복사하면 됩니다. 광원 큐브의 프래그먼트 셰이더는 램프에 일정한 흰색을 정의하여 큐브의 색상이 밝게 유지되도록 합니다.
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0); // 벡터의 모든 값을 1.0으로 설정합니다.
}
렌더링을 할 때는 방금 정의한 조명 셰이더를 사용하여 컨테이너 객체(또는 다른 여러 객체)를 렌더링하고, 광원을 그릴 때는 광원의 셰이더를 사용합니다. 조명 관련 챕터에서는 점차 조명 셰이더를 업데이트하여 더욱 사실적인 결과를 얻을 것입니다.
광원 큐브의 주된 목적은 빛이 어디에서 오는지를 보여주는 것입니다. 일반적으로 장면의 특정 위치에 광원의 위치를 지정하지만, 이는 시각적인 의미는 없는 단순한 위치일 뿐입니다. 광원의 실제 위치를 보여주기 위해 광원과 동일한 위치에 큐브를 렌더링합니다. 이 큐브는 광원 큐브 셰이더를 사용하여 렌더링되므로 장면의 조명 조건에 관계없이 항상 흰색을 유지합니다.
이제 광원의 위치를 월드 좌표계로 나타내는 전역 vec3 변수를 선언해 보겠습니다.
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
다음으로 광원 큐브를 광원의 위치로 이동시키고 크기를 줄인 후 렌더링합니다.
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));
그러면 광원 큐브에 대한 최종 렌더링 코드는 다음과 같아야 합니다.
lightCubeShader.use();
// 모델, 뷰 및 투영 행렬의 유니폼을 설정합니다.
[...]
// 빛 큐브 그리기
glBindVertexArray(lightCubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
모든 코드 조각을 적절한 위치에 삽입하면 조명 실험에 적합하게 구성된 깔끔한 OpenGL 애플리케이션이 생성됩니다. 모든 것이 컴파일되면 다음과 같은 결과가 나타납니다.

지금은 딱히 볼 만한 게 없지만, 앞으로 나올 챕터들에서는 훨씬 더 흥미로워질 거라고 약속드려요.
전체 애플리케이션에서 모든 코드 조각이 어디에 위치하는지 파악하는 데 어려움이 있다면, 여기에서 소스 코드를 확인하고 코드와 주석을 꼼꼼히 살펴보세요.
이제 색에 대한 상당한 지식을 얻었고 조명 실험을 위한 기본적인 장면을 만들었으니, 진정한 마법이 시작되는 다음 장으로 넘어가 보겠습니다.