이전 장에서 우리는 씬의 빛에 숫자를 부여했습니다. 특히, 각 광선을 따라 일정하며 픽셀의 색상으로 직접 변환되는 복사휘도(radiance) L(단위 투영 면적 및 단위 입체각 당 파워)과, 표면에 도달하는 밝기로 생각할 수 있는 복사조도(irradiance) E(단위 면적 당 파워)에 대해 배웠습니다. 표면의 한 지점에서의 복사조도가 그 지점을 떠나는 빛의 복사휘도와 어떻게 대응하는지 아는 것은 렌더링의 중요한 측면입니다.
이 둘의 관계가 비례 관계라는 것은 타당합니다. 들어오는 파워가 두 배가 되면 나가는 복사휘도도 두 배가 되어야 합니다. 기본적으로: L = f E 이며, f는 비례 계수(단위 1/sr)입니다.
모든 빛이 똑같지는 않습니다. 빛의 방향이 중요합니다. 거울을 생각해 보세요. 거울에서 눈으로 오는 방향과 표면 법선에 대해 같은 각도를 가진 한 방향에서 오는 빛만 볼 수 있습니다. 따라서 비례 계수는 방향에 따라 달라져야 합니다. 사실, 방향들에 따라 달라집니다: 들어오는 방향
과 나가는 방향
(모든 방향은 표면에서 멀어지는 쪽을 가리키는 것이 관례입니다). 즉,
와 같이 두 방향에 의존하는 함수, 즉 “양방향(bidirectional)” 함수여야 합니다.
이제, E는 단일 방향에 대해 유용한 양이 아닙니다. (방향에 무관한 복사휘도의 경우, E는 입체각에 비례하는데, 단일 방향의 입체각은 0입니다.)
하지만 복사휘도 L은 단일 방향에 대해 자연스럽게 고려될 수 있습니다. 그리고 L은 E를 계산하는 데 사용될 수 있습니다. 결국, 둘은 기본적으로 같았고 단지 입체각 당 값이라는 차이만 있었습니다. 따라서, 크기가
인 각도에 대해,
일까요? 조심하세요:
— L의 정의에는 “단위 투영 면적 당”, E의 정의에는 “단위 면적 당”이라는 말이 포함되어 있었습니다.
만약 우리가 두 방향(ω₁과 ω₂, 각각 dω₁과 dω₂ 크기의 각도를 가짐)에서 오는 빛에 관심이 있고, 법선을 n으로 표기한다면, E는
로 계산할 수 있습니다. 하지만 이것은 완전히 사실이 아닙니다. L이 넓은 각도(“넓다”는 것은 dω₁이 양수라는 의미)에 걸쳐 일정할 수 있지만, 이 공식의 코사인(즉, 내적)은 결코 그렇지 않을 것입니다. 이것은 ω₁ 방향만의 코사인이 아니라, dω₁ 각도에 포함된 모든 방향(해당 입체각이 0이 아닌 한 무한히 많음)에 걸쳐 변해야 하며, ω₂ 주변에서도 마찬가지입니다.
따라서 우리는 이 각도들을 매우 작게 만들고, 극한 과정을 거쳐 다음과 같은 적분 형태에 도달합니다:
.
(여기서 Ω는 관심 있는 모든 방향의 집합입니다. 전체 구가 될 수도 있지만, 보통은 n을 중심으로 한 반구입니다.)
이 적분 형태에서는 f의 올바른 위치를 찾기가 더 쉽습니다:
이 방정식은 빛이 어떻게 (그리고 어떤 빛이) 반사되는지를 설명하기 때문에 “반사율 방정식(reflectance equation)”이라고 불리며, f는 “양방향 반사 분포 함수(bidirectional reflectance distribution function)”, 줄여서 BRDF라고 불립니다.
BRDF의 몇 가지 일반적인 속성: 음수가 아니며,
. 인자에 대해 대칭적이며,
(“내가 당신을 볼 수 있다면, 당신도 나를 볼 수 있다”), 에너지를 생성하지 않습니다:
(빛이 흡수될 수 있기 때문에 “<1”이 가능합니다).
물론, 이러한 일반적인 속성들은 f의 구체적인 형태에 대해 많은 것을 알려주지는 않습니다.
표면에 부딪힌 빛은 어떻게 될까요? 일부는 반사되고(specular reflection), 나머지 부분은 굴절됩니다(refraction, 표면으로 들어가 방향을 바꿈).
반사된 빛은 입사 방향과 법선이 이루는 각과 같지만 반대인 각도를 가진 방향에서 보입니다(그리고 이 두 벡터와 같은 평면에 놓입니다).
굴절된 빛에 일어나는 일은 재질에 따라 상당히 다릅니다. 첫 번째 가능성: 그냥 새로운 방향으로 계속 나아갑니다. 이것은 물이나 다른 (거의) 투명한 재질의 경우이며, 우리가 무시할 경우입니다. 두 번째 가능성: 흡수됩니다(따라서 즉시 고려 대상에서 제외됨). 이것은 금속의 경우입니다. 세 번째 가능성: 재질 내부에 있는 이 빛이 그곳의 원자들과 상호작용합니다. 일부는 흡수되고, 일부는 다음 원자에 부딪힐 때마다 방향을 바꿉니다. 최종적으로 다시 표면을 떠납니다. 방향은 원래 방향과 아무 관련이 없으며, 흡수된 빛이 파장에 따라 다르기 때문에 색상도 변했습니다.
우리의 새로운 과제: 먼저 들어오는 빛의 어느 부분이 반사되고 어느 부분이 굴절되는지 알아내는 것입니다.
이 질문은 프레넬 방정식(Fresnel’s equations)으로 답할 수 있습니다. (때로는 이 방정식이 파워 대신 전기장에 대해 주어지기도 하는데, 그럴 때는 파워가 전기장 벡터의 (제곱된) 길이에 비례한다는 것을 기억하면 도움이 됩니다. 그리고 보통 빛의 편광, 즉 전기장이 진동하는 방향(법선과 입사 방향의 평면에 평행하거나 수직인가?)에 관련되는데, 이는 광학적 외관에 (거의?) 기여하지 않기 때문에 우리는 신경 쓰지 않습니다 — 모든 방향에 대해 평균을 내고 입사 평면에 평행한 빛과 수직인 빛에 대한 두 계수를 각각의 절반씩 더하여 결합할 수 있습니다(전기장이 아닌 파워에 대한 공식에서).)
이 방정식에서 중요한 의존성은 각도에 대한 것입니다. 전체 공식은 굴절률(과 자기 투자율) 및 몇 개의 사인과 코사인을 포함합니다. 표면에 수직으로 부딪히는 빛에 대한 반사율 F₀와 각도를 기반으로 한 멋진 근사식이 있습니다. θ 각도(표면 법선에 대한)에서의 반사되는 부분(0과 1 사이의 숫자)은 — 근사적으로! — 다음과 같습니다.
이 근사식은 슐릭 근사(Schlick’s approximation)라고 불립니다.
만약 우리가 잠시 반사된 빛에 무슨 일이 일어나는지 무시한다면, 우리의 compute_radiance 함수는 다음과 같을 수 있습니다:
vec3 compute_radiance(vec3 irradiance, vec3 light_direction, vec3 normal, vec3 surface_colour){
float NdotL = max(dot(normal,light_direction),0);
vec3 irradiance_on_surface = irradiance*NdotL;
float F0 = 0.03;
vec3 reflected_irradiance = (F0 + (1 - F0)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)) * irradiance_on_surface;
vec3 refracted_irradiance = irradiance_on_surface - reflected_irradiance;
return refracted_irradiance*surface_colour;
}
이 표면에 도달하는 복사조도(함수에 전달하는 것은 빛에 수직인 표면에서의 복사조도임을 기억하세요. 왜냐하면 그것이 광원으로부터 얻는 정보이고 따라서 여전히 법선과 빛 방향의 내적 계수로 조정해야 하기 때문입니다)로부터 얼마나 많이 반사되고(프레넬 방정식의 슐릭 근사 사용) 얼마나 많이 굴절되는지(나머지)를 계산합니다.
이 “나머지”가 얼마나 되는지 보려면, 우리는 복사휘도(색상을 무시한다면 반환값)를 반구(모든 가능한 나가는 방향 - 굴절되고(나중에는 산란된) 빛이 각 방향으로 얼마나 받는지는 방향에 무관합니다)에 대해 코사인 계수로 가중치를 주어 적분해야 합니다:
. 이런. 들어오는 빛보다 나가는 빛이 더 많습니다. 이렇게 하는 게 낫습니다:
return refracted_irradiance*surface_colour/PI;
(종종 이 π는 빛의 세기 정의에 포함된 다른 π 계수와 상쇄됩니다. (종종 빛의 속성이 복사조도가 아닌 복사휘도로 지정될 때와 함께.))
(그리고 셰이더 코드에서는 아마도 상수 PI의 정의를 더 앞부분으로 옮겨야 할 것입니다.)
수직 입사 시 프레넬 계수 0.03은 플라스틱에 대한 현실적인 값입니다. 플라스틱(및 다른 “유전체(dielectric materials)”)의 경우, 모든 굴절된 빛을 표면 색상의 빛으로 바꾸어 반환하는 것도 적절합니다. 금속의 경우, 그렇지 않습니다.
이 차이를 처리하기 위해 0 또는 1의 값을 갖는 metallic 매개변수를 포함해 봅시다:
vec3 compute_radiance(vec3 irradiance, vec3 light_direction, vec3 normal, vec3 surface_colour){
float NdotL= max(dot(normal,light_direction),0);
vec3 irradiance_on_surface=irradiance*NdotL;
float metallic = 0.0;
float F0 = 0.03;
vec3 reflected_irradiance = (F0 + (1 - F0)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)) * irradiance_on_surface;
vec3 refracted_irradiance = irradiance_on_surface - reflected_irradiance;
vec3 refracted_not_absorbed_irradiance = refracted_irradiance * (1-metallic);
return refracted_not_absorbed_irradiance*surface_colour/PI;
}
만약 metallic을 1로 바꾸면, 구는 이제 검은색으로 남게 됩니다. 다른 한편으로, 금속의 경우 반사가 매우 중요하며, 우리는 이것을 무시하고 있습니다. 반사를 위해서는 빛의 방향뿐만 아니라 시점 방향도 알아야 합니다.
프래그먼트 셰이더는 카메라의 위치를 모릅니다. 이에 대한 두 가지 해결책이 있습니다: 셰이더에 카메라 위치를 알려주거나, 알려줄 필요가 없게 만드는 것입니다. “알려줄 필요가 없게 만든다”는 것은 모든 셰이딩 계산을 카메라 공간에서 변환함으로써 달성할 수 있습니다 (점들을 변환하기 위해 모델 행렬만 사용하는 것이 아니라, 모델 행렬과 뷰 행렬을 사용하고, 각 정점의 위치를 월드 위치 대신 카메라에 대한 상대 위치로 전달). 셰이더에 알려주는 것은 뷰 행렬에서 읽는 것을 의미할 수 있습니다. 결국, 카메라에 대한 모든 위치 (및 방향) 정보는 그 행렬에 포함되어 있으며, 우리는 이미 그것을 셰이더에 전달하고 있습니다.
음, 사실 버텍스 셰이더에만 전달합니다. (어느 셰이더에서 유니폼에 접근할지 어딘가에서 선언해야 했습니다.)
새로운 유니폼을 도입하고 거기서 값을 읽을 수도 있습니다. (그리고 그것이 아마 합리적일 것입니다: 데이터 양은 프레임당 12바이트이며, 이는 정점당 또는 프래그먼트당 계산량보다 아마 더 나을 것입니다.) 하지만 우리는 뷰 행렬에서 정말로 값을 얻을 수 있습니다. 버텍스 셰이더에서 layout (location=3) out vec3 camera_coordinates;를 추가하고 다음을 추가합니다.
camera_coordinates =
- ubo.view_matrix[3][0] * vec3 (ubo.view_matrix[0][0],ubo.view_matrix[1][0],ubo.view_matrix[2][0])
- ubo.view_matrix[3][1] * vec3 (ubo.view_matrix[0][1],ubo.view_matrix[1][1],ubo.view_matrix[2][1])
- ubo.view_matrix[3][2] * vec3 (ubo.view_matrix[0][2],ubo.view_matrix[1][2],ubo.view_matrix[2][2]);
matrix[a][b]는 행렬의 a번째 열(matrix[a])의 b번째 성분을 의미합니다 (따라서
또는 관례적인 표기법으로는
).
이 행렬의 행에 포함된 “오른쪽”, “아래”, “시점” 벡터들이 서로 직교하고 단위 길이를 갖는다는 사실을 이용하면, 뷰 행렬이
vec4(camera_coordinates,1.0)를 vec4(0)으로 변환한다는 것, 즉 camera_coordinates가 실제로 카메라 좌표를 포함하고 있음을 쉽게 확인할 수 있습니다.
프래그먼트 셰이더에서는: layout (location=3) in vec3 camera_coordinates;를 추가합니다. main 함수의 시작 부분에서 카메라로 향하는 방향을 계산합니다:
vec3 direction_to_camera = normalize(camera_coordinates - worldpos);
그리고 이것을 compute_radiance 함수에 인자로 전달합니다. 함수의 시그니처는 다음과 같이 됩니다:
vec3 compute_radiance(vec3 irradiance, vec3 light_direction, vec3 normal, vec3 camera_direction, vec3 surface_colour)
이 방향으로 무엇을 할까요? 우리는 반사된 빛이 우리에게 관련이 있는지, 즉 시점 방향과 일치하는지를 알아내고, 그렇다면 반환된 복사휘도에 추가합니다. 대략 다음과 같습니다:
vec3 compute_radiance(vec3 irradiance, vec3 light_direction, vec3 normal, vec3 camera_direction, vec3 surface_colour){
float NdotL= max(dot(normal,light_direction),0);
vec3 irradiance_on_surface=irradiance*NdotL;
float metallic = 0.0;
float F0 = 0.03;
vec3 reflected_irradiance = (F0 + (1 - F0)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)) * irradiance_on_surface;
vec3 refracted_irradiance = irradiance_on_surface - reflected_irradiance;
vec3 refracted_not_absorbed_irradiance = refracted_irradiance * (1-metallic);
vec3 relevant_reflection = vec3(0);
if (false) {
relevant_reflection = reflected_irradiance;
}
return refracted_not_absorbed_irradiance*surface_colour/PI + relevant_reflection;
}
물론 if (false)보다는 더 나은 조건으로 말이죠. (하지만 우리는 이미 GLSL에서 if 문의 형식을 기억에 새길 수 있습니다; 이것이 우리의 첫 번째 if문인 것 같네요.)
이제: 조건은 무엇이어야 할까요? 반사된 빛의 경우, 법선과 나가는 빛 사이의 각도는 법선과 들어오는 빛 사이의 각도와 같아야 합니다. 이것은 맞지만 도움이 되지는 않습니다: 사실, 이 속성을 가진 방향은 훨씬 더 많습니다. (만약 세상이 2차원이고 표면이 1D라면, 그것으로 충분했을 것입니다.)
법선은 들어오고 나가는 광선의 방향들 중간에 놓여야 합니다. 이것이 더 낫습니다 - 그리고 유일한 방향을 설명합니다. 그래서, 아마도 이렇게 할 수 있습니다:
if (length(normalize(0.5*(camera_direction + light_direction))-normal)<0.01) {
(등호는 단일 점을 의미합니다; 그 단일 점이 픽셀의 중앙에 있는 바로 그 점일 확률은 낮습니다; 대신 이 작은 숫자를 사용합시다.)
또한: 법선이 단위 길이를 갖도록 합시다. 여기서 이것은 매우 중요합니다. 그래서:
vec3 normal = normalize(normal);
을 main의 맨 위에 두는 것이 좋습니다. (왜 이것이 필요한가요? 법선은 버텍스 셰이더의 끝에서 이미 단위 길이를 가져야 하지 않을까요? 그렇습니다, 하지만 그 후에는 보간됩니다. 삼각형 위에서 선형으로. 그리고 거기서 우리는 이 좋은 속성을 쉽게 잃을 수 있습니다.)
아직도 아무것도 보이지 않나요?
테스트를 위해:
relevant_reflection = reflected_irradiance+vec3(10000);
아, 이제 흰 점들이 보입니다. 반사가 일어나는 지점들에서 반사된 빛의 양이 그렇게 크지 않았던 것 같습니다. 뭐, 우리가 거울을 다루고 있는 것은 아니니까요.
거울 얘기가 나왔으니 말인데: 금속을 살펴봅시다. 다음 코드를 고려해 보세요:
vec3 compute_radiance(vec3 irradiance, vec3 light_direction, vec3 normal, vec3 camera_direction, vec3 surface_colour){
float NdotL= max(dot(normal,light_direction),0);
vec3 irradiance_on_surface=irradiance*NdotL;
float metallic = 0.0;
float F0 = 0.03;
vec3 reflected_irradiance = (F0 + (1 - F0)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)) * irradiance_on_surface;
vec3 refracted_irradiance = irradiance_on_surface - reflected_irradiance;
vec3 refracted_not_absorbed_irradiance = refracted_irradiance * (1-metallic);
vec3 relevant_reflection = vec3(0);
if (length(normalize(0.5*(camera_direction + light_direction))-normal)<0.01) {
relevant_reflection = reflected_irradiance;
}
return refracted_not_absorbed_irradiance*surface_colour/PI + relevant_reflection;
}
— 금속은 어떤 색을 가질까요? 답: 반사된 빛의 색이며, 표면 색상의 영향은 없습니다. 그것은 그리 부자연스럽지 않습니다: 우리는 색상이 재료 내부의 원자와 상호작용하는 빛에서 비롯된다고 말했고, 금속의 경우 이 빛이 즉시 흡수된다고 말했습니다.
하지만 저는 철, 구리, 금(모두 금속)이 서로 상당히 다르게 보인다고 확신합니다. 왜 그럴까요?
음, float F0=0.03은 거짓말입니다. 값이 금속에 대해 틀렸기 때문만이 아니라, 이것이 단일 숫자가 아니어야 하기 때문입니다: 굴절률(그리고 그에 따라 F0)은 파장에 따라 다릅니다. 유전체에서는 그다지 그렇지 않지만, 금속에서는 매우 그렇습니다.
우리 프레임워크에 맞는 최상의 근사는 빨강, 초록, 파랑에 대해 다른 F0 값을 사용하는 것입니다. 예를 들어, 구리의 경우 빨강은 0.955, 초록은 0.638, 파랑은 0.538입니다 (Real-Time Rendering, 4th edition, p.323).
float metallic = 1.0;
vec3 F0 = vec3(0.955,0.638,0.538);
F0을 float에서 vec3로 바꾸어도 공식에 추가적인 변경이 필요하지 않습니다. GLSL은 “모든 곳에서 성분별 연산을 사용하여” 이를 처리할 수 있습니다.
결과는 다음과 같습니다:

빛이 반사되는 지점들은… 구리처럼 보이긴 하네요.
구의 다른 지점들을 처리하기 전에: F0이 셰이더에 하드코딩되어 있는 것은 최적이 아닙니다. 동시에, 우리는 금속에 대해 표면 색상을 사용하지 않고 있습니다.
색상을 금속의 F0 값으로 사용하는 것이 좋은 생각입니다. 비금속의 경우, 색상의 의미를 이전과 같이 유지합니다. 그렇다면 비금속의 F0 값은 어디에 저장할까요? 음, 사실, 그 값들은 서로 크게 다르지 않습니다: 물은 0.02, 치아는 0.058, 돌은 0.03에서 0.056 사이, 유리는 0.04… 다이아몬드를 제외한 보석은 최대 0.08입니다. 또 다른 “색상”과 같은 매개변수를 다루고 싶지 않다면, 이전에 사용했던 0.03을 유지할 수 있습니다.
비금속(metallic=0.0) 재료의 경우: F0=0.03, 금속의 경우: F0=색상. 또는 결합하여:
vec3 F0 = mix(vec3(0.03),colour_in,vec3(metallic));
어쩐지, 금속이 반사 지점 밖에서는 완전히 어둡다는 것이 여전히 마음에 걸립니다. 다른 한편으로, 어떤 금속에게는 그것이 꽤 현실적입니다. 거울이나 극도로 매끄러운 표면을 생각해 보세요. 하지만 모든 표면이 거울은 아닙니다.
어떻게든, 우리는 표면의 거칠기를 포함해야 합니다.
거칠기란 무엇일까요?
표면은 매끄럽지 않거나, 평평하지 않을 때 거칠다고 합니다. 이것을 이렇게 설명할 수 있습니다: 표면 법선이 하나만 있는 것이 아닙니다.
그리고, 물론, 다른 법선들이 있습니다. 각 지점마다 다른 법선을 가집니다. 여기서 요점은 우리가 이제 단일 지점(완벽한 이상적인 차원 없는 점이 아니라, 픽셀 하나 크기의 점으로 생각해야 함) 내에서도 다른 법선을 허용하고 싶다는 것입니다. (하나의 표면이 아니라 이 지점의 미세한 “미세면(microsurfaces)”) 다른 법선들은 갑자기 구의 한 지점에서만 표면이 빛을 보여줄 “올바른 법선”을 갖는 것이 아니라, 모든 지점에서 “일부 법선들이 올바르다”는 것을 의미합니다.
실용적인 용어로, 이것은 “각도가 맞으면 반사된 복사조도를 더한다”는 구성을 “항상 복사조도의 특정 부분을 더한다”로 대체하게 됨을 의미합니다. “특정 부분”이 얼마나 되어야 하는지는 여전히 빛, 시점, (주요) 법선의 방향에 따라 달라집니다.
이 “특정 부분”을 설명하는 데 두 가지 요소가 역할을 합니다: 분포 함수(미세면의 어느 부분이 이 법선을 가지고 있는가?)와 마스킹-섀도잉 (또는 “지오메트리”) 함수(이 미세면들이 얼마나 보이는가? — 큰 부분이 이 방향을 가리키더라도, 빛이 도달할 수 없거나 다른 미세면들이 가로막고 있어 시점에서 보이지 않는다면 중요하지 않을 것입니다).
vec3 compute_radiance(vec3 irradiance, vec3 light_direction, vec3 normal, vec3 camera_direction, vec3 surface_colour){
float NdotL= max(dot(normal,light_direction),0);
vec3 irradiance_on_surface=irradiance*NdotL;
float metallic = 1.0;
float roughness = 0.1;
roughness=roughness*roughness;
vec3 F0 = mix(vec3(0.03),surface_colour,vec3(metallic));
vec3 reflected_irradiance = (F0 + (1 - F0)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)*(1-NdotL)) * irradiance_on_surface;
vec3 refracted_irradiance = irradiance_on_surface - reflected_irradiance;
vec3 refracted_not_absorbed_irradiance = refracted_irradiance * (1-metallic);
vec3 halfvector=normalize(0.5*(camera_direction + light_direction));
float NdotH=max(dot(normal,halfvector),0);
vec3 F=(F0 + (1 - F0)*(1-NdotH)*(1-NdotH)*(1-NdotH)*(1-NdotH)*(1-NdotH));
vec3 relevant_reflection = reflected_irradiance*F*geometry(light_direction,normal,camera_direction,roughness)*distribution(normal,halfvector,roughness);
return refracted_not_absorbed_irradiance*surface_colour/PI + relevant_reflection;
}
첫 단계에서 거칠기 매개변수를 제곱하는 것은 [0,1] 구간에서 값을 더 합리적으로 분포시키기 위함입니다. (이것이 표준인 것 같습니다. “거칠기”는 직접적인 물리적 매개변수라기보다는 현상학적인 매개변수이고 우리가 거칠기 값을 더하지 않기 때문에, 이 변환(또는 [0,1]을 [0,1]로 매핑하는 임의의 다른 변환)은 모델을 해치지 않을 것입니다.)
우리는 여전히 “빛과 시점 방향의 중간” 벡터(이전에 (거시적) 법선과 비교했던 것)를 사용하고 있습니다. 우리는 이 벡터에 대한 프레넬 계수(이것이 반사에 기여하는 “법선” 방향이므로), 지오메트리 함수, 그리고 분포 함수로 반사된 빛을 수정합니다.
이 함수들에 대해서는 다른 모델링 선택, 다른 유도 과정, 다른 추측 또는 다른 근사에서 비롯된 여러 가능한 선택지가 있습니다.
우리는 다음을 선택합니다:
float distribution(vec3 normal,vec3 halfvector,float roughness){
float NdotH=dot(halfvector,normal);
if (NdotH>0){
float r=roughness*roughness;
return r / (PI* (1 + NdotH*NdotH*(r-1))*(1 + NdotH*NdotH*(r-1)));
}else{
return 0.0;
}
}
그리고
float geometry(vec3 light, vec3 normal, vec3 view, float roughness){
float NdotL=abs(dot(normal,light));
float NdotV=abs(dot(normal,view));
return 0.5/max(0.01,mix(2*NdotL*NdotV,NdotL+NdotV,roughness));
}
다음 구리 구를 가지고 살펴봅시다.
sphere.insert_visibly(InstanceData::from_matrix_and_colour(
na::Matrix4::new_scaling(0.5),
[0.955, 0.638, 0.538],
));
roughness=0.1일 때: 
그리고 roughness=0.5일 때: 