Skip to main content

11.1 Phong 模型

在本系列教程的第 6 部分中,我们学习了如何使用漫反射(Lambertian reflection)为 3D 对象着色。到目前为止,我们一直在使用这个光照模型,但这个模型有点受限。

Phong 反射模型以创作者 Bui Tuong Phong 的名字命名,有时称为Phong illuminationPhong lighting。它由三部分组成:环境照明(ambient lighting)、漫反射(lambertian reflection)和镜面反射(specular reflection)。

-

Phong Reflection Model 来自 维基百科上的数据

Phong 反射模型提供了一个方程,用于计算表面上每个点的照明度I_p

-

Phong Reflection Equation 来自 维基百科上的数据

这个方程可能看起来很复杂,但我会解释它的每个部分!此方程由三个主要部分组成:环境、漫反射和高光。下标 m 是指场景中的灯光数量。我们假设现在只存在一个灯。

-

第一部分表示环境光项。在 GLSL 代码中,它可以由以下内容表示:

float k_a = 0.6; // a value of our choice, typically between zero and one
vec3 i_a = vec3(0.7, 0.7, 0); // a color of our choice

vec3 ambient = k_a * i_a;

k_a 值是环境反射常数,即渲染的场景中所有点中存在的环境项的反射比率。i_a 值控制环境照明,有时计算为所有光源的贡献之和。

Phong 反射方程的第二部分表示扩散反射项。在 GLSL 代码中,它可以由以下内容表示:

vec3 p = ro + rd * d; // point on surface found by ray marching
vec3 N = calcNormal(p); // surface normal
vec3 lightPosition = vec3(1, 1, 1);
vec3 L = normalize(lightPosition - p);

float k_d = 0.5; // a value of our choice, typically between zero and one
vec3 dotLN = dot(L, N);
vec3 i_d = vec3(0.7, 0.5, 0); // a color of our choice

vec3 diffuse = k_d * dotLN * i_d;

k_d 是漫反射常数,即入射光漫射项的反射率 Lambertian 反射率的反射率。值 dotLN 是我们在前面的教程中一直使用的漫反射。它代表了朗伯式的倒影。值 i_d 是场景中光源的强度,在本例中由 color 值定义。

Phong 反射方程的第三部分稍微复杂一些。它表示镜面反射项。在现实生活中,金属和抛光表面等材质具有镜面反射,根据摄像机角度或观看者面对对象的位置,这些反射看起来更亮。因此,该项是场景中摄像机位置的函数。

-

GLSL 代码中,它可以由以下内容表示:

vec3 p = ro + rd * d; // point on surface found by ray marching
vec3 N = calcNormal(p); // surface normal
vec3 lightPosition = vec3(1, 1, 1);
vec3 L = normalize(lightPosition - p);

float k_s = 0.6; // a value of our choice, typically between zero and one

vec3 R = reflect(L, N);
vec3 V = -rd; // direction pointing toward viewer (V) is just the negative of the ray direction

vec3 dotRV = dot(R, V);
vec3 i_s = vec3(1, 1, 1); // a color of our choice
float alpha = 10.;

vec3 specular = k_s * pow(dotRV, alpha) * i_s;

k_s 是镜面反射常数,即入射光的镜面反射项的反射比率。

矢量 R是镜面反射的光线从表面反射时所采用的方向。

-

根据维基百科,Phong 反射模型使用以下公式计算反射光线方向。

-

如前所述,下标 m 指的是场景中的灯光数量。每个字母上方的小帽子 ^ 表示我们应该使用每个向量的归一化版本。矢量 L 是指光线方向。向量 N 是指表面法线。

GLSL 提供了一个名为 reflect 的便捷函数,它可以为我们计算来自入射光线的反射光线的方向。此函数采用两个参数:入射光线方向矢量和法线矢量。

在内部,反射函数等于 I - 2.0 * dot(N, I) * N,其中 I 是入射光线方向,N 是法线矢量。如果我们将这个方程乘以 -1,我们最终会得到与维基百科上的反射方程相同的方程。这完全是轴约定的问题。

镜面反射代码片段中的矢量 V 表示指向查看器或摄像机的方向。我们可以将其设置为等于光线方向的负值 rd

alpha 项用于控制球体上的“光泽度”量。较低的值会使其看起来更亮。

把它们放在一起

让我们将到目前为止学到的所有内容放在代码中。我们将从场景中的简单球体开始,然后像我们在第 10 部分中学到的那样,为摄像机模型使用注视点。

const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;

float sdSphere(vec3 p, float r )
{
return length(p) - r;
}

float sdScene(vec3 p) {
return sdSphere(p, 1.);
}

float rayMarch(vec3 ro, vec3 rd) {
float depth = MIN_DIST;

for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
vec3 p = ro + depth * rd;
float d = sdScene(p);
depth += d;
if (d < PRECISION || depth > MAX_DIST) break;
}

return depth;
}

vec3 calcNormal(vec3 p) {
vec2 e = vec2(1.0, -1.0) * 0.0005;
return normalize(
e.xyy * sdScene(p + e.xyy) +
e.yyx * sdScene(p + e.yyx) +
e.yxy * sdScene(p + e.yxy) +
e.xxx * sdScene(p + e.xxx));
}

mat3 camera(vec3 cameraPos, vec3 lookAtPoint) {
vec3 cd = normalize(lookAtPoint - cameraPos); // camera direction
vec3 cr = normalize(cross(vec3(0, 1, 0), cd)); // camera right
vec3 cu = normalize(cross(cd, cr)); // camera up

return mat3(-cr, cu, -cd);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
vec3 backgroundColor = vec3(0.835, 1, 1);
vec3 col = vec3(0);

vec3 lp = vec3(0); // lookat point (aka camera target)
vec3 ro = vec3(0, 0, 3);

vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

float d = rayMarch(ro, rd);

if (d > MAX_DIST) {
col = backgroundColor;
} else {
vec3 p = ro + rd * d;
vec3 normal = calcNormal(p);
vec3 lightPosition = vec3(2, 2, 7);
vec3 lightDirection = normalize(lightPosition - p);

float diffuse = clamp(dot(lightDirection, normal), 0., 1.);

col = diffuse * vec3(0.7, 0.5, 0);
}

fragColor = vec4(col, 1.0);
}

运行代码时,您应该会在场景中看到一个具有漫射照明的简单球体。

-

不过这很无聊。我们想要一个闪亮的球体!目前,我们只根据漫反射照明或朗伯反射为球体着色。让我们添加一个环境光和镜面反射组件来完成 Phong 反射模型。我们还将稍微调整光线方向,以便球体的右上角出现光泽。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
vec3 backgroundColor = vec3(0.835, 1, 1);
vec3 col = vec3(0);

vec3 lp = vec3(0); // lookat point (aka camera target)
vec3 ro = vec3(0, 0, 3);

vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction

float d = rayMarch(ro, rd);

if (d > MAX_DIST) {
col = backgroundColor;
} else {
vec3 p = ro + rd * d; // point on surface found by ray marching
vec3 normal = calcNormal(p); // surface normal

// light
vec3 lightPosition = vec3(-8, -6, -5);
vec3 lightDirection = normalize(lightPosition - p);

// ambient
float k_a = 0.6;
vec3 i_a = vec3(0.7, 0.7, 0);
vec3 ambient = k_a * i_a;

// diffuse
float k_d = 0.5;
float dotLN = clamp(dot(lightDirection, normal), 0., 1.);
vec3 i_d = vec3(0.7, 0.5, 0);
vec3 diffuse = k_d * dotLN * i_d;

// specular
float k_s = 0.6;
float dotRV = clamp(dot(reflect(lightDirection, normal), -rd), 0., 1.);
vec3 i_s = vec3(1, 1, 1);
float alpha = 10.;
vec3 specular = k_s * pow(dotRV, alpha) * i_s;

// final sphere color
col = ambient + diffuse + specular;
}

fragColor = vec4(col, 1.0);
}

和以前一样,我们固定每个点积的结果,使值介于 01 之间。当我们运行代码时,我们应该看到球体在球体的右上角闪闪发光。

-