Multiple lights
이전 튜토리얼에서, 우리는 OpenGL에서 조명에 대해 꽤 많이 배웠었다. 우리는 Phong shading, materials, lighting maps 그리고 light casters의 다른 종류들. 이 튜토리얼에서, 우리는 6개의 active light sources들로 완전히 비춰지는 scene을 만들어 이전에 얻어진 모든 지식들을 합칠 것이다. 우리는 directional light source로서 태양같은 조명과 신 도처에 흩뿌려진 4개의 point lights를 재현할 것이고, 우리는 또한 flashlight를 또한 더할 것이다.
scene에서 한 개 이상의 light source를 쓰기 위해서, 우리는 GLSL 함수에 조명 계산을 encapsulate(캡슐화, 담아야) 해야만한다. 그것에 대한 이유는 우리가 다른 연산을 요구하는 각각의 조명 타입과 함께 여러 조명들의 연산을 하려고 할 때 코드가 빠르게 더러워지기 때문이다. 만약 우리가 main 함수에서만 이러한 계산들을 모두 하려고 한다면, 그 코드는 빠르게 이해하기 어려워질 것이다.
GLSL에서의 함수들은 C언어의 함수들과 같다. 우리는 함수이름, return type을 가지고, 우리는 main 함수 이전에 그 함수가 선언되지 않았다면, 그 코드파일의 맨 위에 prototype을 선언해야만 한다. 우리는 directional lights, point lights and spotlights를 위한 각각의 타입에 대해 다른 함수를 만들 것이다.
한 scene에서 다양한 빛들을 사용할 때, 접근법은 보통 다음과 같다: 우리는 fragment의 output color를 나타내는 하나의 single color vector를 갖는다. 각 light에 대해, fragment에 대해 light의 contribution color는 fragment의 output color에 더해진다. 그래서 scene에 있는 각 light는 앞에서 언급된 fragment에 대한 그것의 각각의 영향을 계산하고 최종 output color에 기여할 것이다. 일반적인 구조는 이것과 같아 보일 것이다:
out vec4 FragColor; void main() { // define an output color value vec3 output = vec3(0.0); // add the directional light's contribution to the output output += someFunctionToCalculateDirectionalLight(); // do the same for all point lights for(int i = 0; i < nr_of_point_lights; ++i) output += someFunctionToCalculatePointLight(); // and add others lights as well (like spotlights) output += someFunctionToCalculateSpotLight(); FragColor = vec4(output, 1.0); }
실제 코드는 구현마다 다를 것이지만, 일반적인 구조는 같게 된다. 우리는 light source마다의 영향을 계산하는 몇 가지 함수들을 정의하고, 그것의 최종 결과를 output color vector에 더한다. 만약 예를들어, 두 광ㅇ원들이 fragment에 가깝다면, 그것들의 결홥된 기여는 단일 light source에 의해 비춰지는 것보다 좀 더 밝게 빛나느 fragment를 만들어 낼 것이다.
Direction light
우리가 하기 원하는 것은 fragment shader에서 상응하는 fragment에 directional light가 가진 공헌을 계산하는 함수를 정의하는 것이다: 몇 개의 인자를 받고 계산된 directional lighting color를 반환하는 함수.
처음에 우리는 directional light source를 위해 최소한으로 필요한 요구되는 변수를 설정할 필요가 있다. 우리는 DirLight라고 불려지는 구조체에 변수를 저장할 수 있고, 그것을 uniform으로 정의할 수 있다. 요구되는 변수는 이전 튜토리얼에 있는 친숙한 것이다:
struct DirLight {// Directional Light such as sun vec3 direction; vec3 ambient; vec3 diffuse; vec3 specular; }; uniform DirLight dirLight;
우리는 dirLight uniform을 다음의 프로토타입으로 함수에 넘겨줄 수 있다:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
Green Box!
C와 C++처럼, 만약 우리가 함수를 호출 하길 원한다면 (이 경우에, main 함수 안에), 그 함수는 호출자의 line number 이전 어딘가에 정의되어야만 한다. 이 경우에, 우리는 main 함수 아래에 함수들을 정의하기를 선호한다. 그래서 이 요구사항은 유효하지 않다. 그러므로, 우리는 main 함수 위 어딘가에 함수의 프로토타입을 선언한다. 우리가 C에서 하던 것 처럼.
너는 그 함수가 연산을 위해 DirLight 구조체와 두 개의 다른 벡터들을 요구하는 것을 볼 수 있다. 만약 너가 성공적으로 이전 튜토리얼을 완료했다면, 그러면 이 함수의 내용은 놀랍지 않게 다가올 것이다:
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir) { vec3 lightDir = normalize(-light.direction); // diffuse shading float diff = max(dot(normal, lightDir), 0.0); //specualr shading vec3 reflectDir = reflect(-lightDir, normal); float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); // combine results vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords)); vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords)); vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords)); return (ambient + diffuse + specular); }
우리는 기본적으로 이전 튜토리얼의 코드를 복사했고, directional light의 관련 벡터를 계산하기 위해 함수인자로 주어진 벡터르 사용했다.
Point light
directional lights처럼 우리는 attenuation을 포함하여 주어진 fragment에 대해 한 point light가 가진 기여를 계산하는 한 함수를 정의하고 싶다. directional lights 처럼 우리는 point light를 위해 요구되는 모든 변수를 정의하는 구조체를 정의하고 싶다:
struct PointLight { vec3 position; float constant; float linear; float quadratic; vec3 ambient; vec3 diffuse; vec3 specular; }; #define NR_POINT_LIGHTS 4 uniform PointLight pointLights[NR_POINT_LIGHTS];
너도 볼 수 있듯이, 우리는 우리가 scene에서 가지길 원하는 point lights의 갯수를 정의하기 위해 GLSL에서 전처리기 지시문을 사용했다. 우리는 그러고나서 PointLight 구조체의 배열을 만들기 위해서 이 NR_POINT_LIGHTS 상수를 사용한다. GLSL에서의 배열은 C의 배열과 같다. 그리고 두 개의 사각형 brackets으로 만들어 질 수 있다. 지금 당장 우리는 데이터를 채울 4개의 PointLight 구조체를 가지고 있따.
Green Box
우리는 또한 간단히 모든 다른 light types에 필요한 변수들을 포함하는 하나의 큰 구조체를 정의할 수 있다 (light type마다 다른 구조체 대신에) 그리고 각 함수에 대해 그 구조체를 사용할 수 있고, 우리가 필요하지 않은 변수들을 무시할 수 있다. 그러나, 나는 개인적으로 현대적인 접근법이 좀 더 직관적인 것을 알게 되었고, 몇 가지 추가 코드를 제쳐두고, 그것은 모든 light types들이 모든 변수를 필요로 하지 않기 때문에 메모리를 절약할 수 있다.
point light의 함수의 프로토타입은 다음과 같다:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
그 함수는 그것의 인자로서 필요한 모든 데이터를 받는다. 그리고 이 specific point light가 fragment에 대해 같는 color contribution을 나타내는 vec3를 반환한다. 다시, 똑똑한 복붙이 다음의 함수를 만들어 낸다:
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) { vec3 lightDir = normalize(light.position - fragPos); // diffuse shading float diff = max(dot(normal, lightDir), 0.0); // specular shading vec3 reflectDir = reflect(-lightDir, normal); float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); // attenuation float attn_distance = length(light.position - fragPos); float attenuation = 1.0 / (light.constant + light.linear * attn_distance + light.quadratic * (attn_distance * attn_distance)); // combine results vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords)); vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords)); vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular); }
이 처럼 한 함수에서 그 기능을 추상화하는 것은 더럽게 복사된 코드없이도 다양한 point lights에 대해 조명을 쉽게 계싼할 수 있는 장점을 가진다. main 함수에서 우리는 간단히 각 point light에 대해 CalcPointLight를 호출하고 point light array에 대해 반복하는 루프를 만든다.
Putting it all together
directional lights에 대한 함수와 point lights에 대한 함수 둘 다 정의 했으니, 우리는 main 함수에서 합칠 수 있다.
void main() { // Common Factors vec3 surface_norm = normalize(Normal); vec3 viewDir = normalize(-FragPos); // phase 1: Directional lighting vec3 result = CalcDirLight(dirLight, surface_norm, viewDir); // phase 2: Point lights for(int i = 0; i < NR_POINT_LIGHTS; ++i) result += CalcPointLight(pointLights[i], norm, FragPos, viewDir); // phase 3: Spot light // result += CalcSpotLight(spotLight, norm, FragPos, viewDir); // Result FragColor = vec4(result, 1.0); }
각각의 light type은 모든 light source들이 처리될 때 까지, 최종 output color에 그것의 기여를 더한다. 최종 컬러는 scene에서 결합된 모든 광원들의 color impact를 포함한다. 만약 너가 원한다면, 너는 또한 spotlight와 output color에 효과를 구현할 수 있다. 우리는 CalcSpotLight 함수는 독자를 위해 연습문제로 남겨 놓는다.
directional light struct에 대해 uniforms를 설정하는 것은 친숙할 것이지만, 너는 어떻게 우리가 point lights의 uniform value들을 설정할 수 있는지 궁금해 할지도 모른다. 왜냐하면 point light uniform은 이제 PointLight 구조체의 한 배열이기 때문이다. 이것은 우리가 이제까지 이야기했던 것이 아니다.
우리에게 운 좋게도, 그것은 너무 복잡하지는 않다. 구조체의 한 배열의 uniform을 설정하기 위해서, 단일 구조체의 uniforms에 설정하던 것처럼 작업해라. 비록 이번에 우리가 uniform의 위치를 query할 떄 쓰는 적절한 index를 저으이할 필요가 있지만:
lightingShader.setFloat("pointLight[0].constant", 1.0f);
여기에서 우리는 처음에 pointLights 배열에서 처음의 PointLight구조체를 인덱싱하고, 그것의 constant 변수의 위치를 얻는다. 이것은 불행하게도 우리가 그 4개의 point lights들의 모든 uniforms을 수동으로 설정해야하는 것을 의미한다. 그리고 이것은 꽤 지루한 point light에 대해 28개의 uniform calls을 하도록 한다. 너는 uniforms를 설정하는 point light class를 정의하여 이것으로 부터 조금 추상화 하려고 노력할 수 있다. 그러나 결국에, 너는 여전히 모든 light의 uniform value들을 이러한 방식으로 설정해야만 한다.
우리는 또한 point lights의 각각의 position vector를 정의할 필요가 있다는 것을 잊지말자. 그것들을 scene 주변에 퍼뜨리자. 우리는 pointlight의 위치를 포함하는 또 다른 glm::vec3 array를 정의할 것이다:
glm::vec3 pointLightPositions[] = { glm::vec3(0.7f, 0.2f, 2.0f), glm::vec3(2.3f, -3.3f, -4.0f), glm::vec3(-4.0f, 2.0f, -12.0f), glm::vec3(0.0f, 0.0f, -3.0f) };
그러고나서 pointLights array로부터 상응하는 PointLight 구조체에 인덱싱하고, 그것의 position attribute를 우리가 정의한 포지션 중 하나로 설정해라. 또한 이제 1개 대신에 4개의 light cubes를 그리려고 해라. 우리가 컨테이너가지고 했던 것 처럼, 간단히 각각의 light objects들에 대해 다른 model matrix를 만들어라.
만약 너가 플래쉬라이트를 사용한다면, 모든 결합된 조명의 결과는 이것처럼 보인다:
Directional Light (like sun) Two Point Lights (one is green and the other is red) Spot Light with Blue light, accompanying with the viewer. |
너도 볼 수 있듯이, 하늘 어딘가에 (태양처럼) global light의 형태가 있는 것처럼 보이기 때문에, 우리는 4개의 빛을 scene 구석구석에 뿌려놓았고, flashlight는 플레이어의 관점으로 부터 보인다. 꽤 깔끔해 보인다. 그렇지 않냐?
너는 최종 프로그램의 전체 소스코드를 여기에서 볼 수 있다.
그 이미지는 우리가 이전의 모든 튜토리얼들에서 사용했던 기본 light properties가 있는 light sources set을 보여준다. 그러나 만약 너가 이러한 값들을 가지고 논다면, 너는 꽤 흥미로운 결과를 얻을 수 있다. 아티스트들과 level editors들은 일반적으로 큰 에디터에서 이러한 모든 조명 변수들을 변경한다. 조명이 환경에 어울리게 하기 위해서이다. 우리가 만든 간단히 조명된 환경을 사용하여, 너는 그것들의 특성을 바꾸어 흥미로운 비쥬얼을 만들수 있다:
우리는 또한 조명을 더 잘 반영하기 위해, clear color를 바꿀 수 있다. 너는 간단히 조명 파라미터를 조절하여, 완전히 다른 분위기를 만들 수 있는 것을 볼 수 있다.
이제 너는 OpenGL에서 조명에 대해 꽤 좋은 이해를 가질 것이다. 지금까지의 지식으로, 우리는 벌써 흥미롭고 시각적으로 풍부한 환경과 분위기를 만들 수 있다. 너만의 분위기를 만들기 위해 모든 다른 값들을 가지고 시험삼아 놀아보아라.
Exercises
- 너는 (어느정도) light의 속성값을 변경하여 마지막 이미지의 다른 이미지를 재창조할 수 있는가?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | class chanSpotLight { public: chanSpotLight(); chanSpotLight(Shader* _shader, const char* shaderVariableName, glm::vec3& position, glm::vec3& direction, float constant, float linear, float quadratic, float inner_cutoff, float outer_cutoff, glm::vec3& ambient, glm::vec3& diffuse, glm::vec3& specular); void setShaderValue(); void setShaderValue(const glm::mat4& transform); void setPosition(const glm::vec3& position); void setDirection(const glm::vec3& direction); private: Shader * m_shader; char m_shaderNameBuffer[NAME_BUFFER_SIZE]; int shaderName_PickLocation; glm::vec3 m_position; glm::vec3 m_direction; float m_constant; float m_linear; float m_quadratic; float m_inner_cutoff; float m_outer_cutoff; glm::vec3 m_ambient; glm::vec3 m_diffuse; glm::vec3 m_specular; }; |
이러한 구조를 통해서, 처음에 값만 설정하면, setShaderValue 함수를 통해서 쉽게 shader 값을 설정할 수 있게 해 놓았다.
모델링 섹션을 공부하고, light bulb같은 class를 만들 때, 이 때 PointLight나 SpotLight같은 클래스를 함께 넣어서 활용할 수 있겠다.
이제 여러가지를 운용할 수 있게 되면서 느낀 것은, C++의 Class 기능에 대해서 좀 여러가지를 알아야 겠다는 것이다. 왜냐하면 여러 기능을 통해서 좀 더 코딩하지 않고 쉽게 짤 수 있는데, 그렇지 않으면 일일이 하드코딩 하게 되기 때문이다.
댓글 없음:
댓글 쓰기