Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been authorized by the author. If reprinted or reposted, please be sure to mark the original link and description in the key position of the article after obtaining the author’s consent as well as the translator's. If the article is helpful to you, click this Donation link to buy the author a cup of coffee.
说明:该系列博文翻译自Nathan Vaughn的着色器语言教程。文章已经获得作者翻译授权,如有转载请务必在取得作者和译者同意之后在文章的重点位置标明原文链接以及说明。如果你觉得文章对你有帮助,点击此打赏链接请作者喝一杯咖啡。
朋友们,你们好!欢迎来到Shadertoy系列教程的第十一章课。在本篇教程中,我们会使用一个种改进过的光照模型来让我们的3D物体变得更加真实,这种模型就是冯式反射模型。
冯式反射模型
在第六篇教程中,我们学会了使用博朗反射来为3D物体上色,但是这种模型有一些限制。
冯氏放射模型,以它的发明者Bui Tuong Phong 的名字命名,它也经常被叫做冯氏照明或者冯氏光照。它由三部分组成:环境光(ambient)、漫反射(diffuse reflection)以及镜面反射(specular)。
针对每一个点,冯式反射模型为我们提供了一个计算光照效果公式。
这个公式看起来有些复杂,稍后我会为你详细解释。它分为三个部分:ambient
、diffuse
、和specular
。角标m,表示我们场景中的任一一束光,假设现在只有一束光。
首先是第一部分:环境光照。在GLSL代码中,它是这样表示的:
float k_a = 0.6; // 0到1之间的任意值
vec3 i_a = vec3(0.7, 0.7, 0); // 颜色值
vec3 ambient = k_a * i_a;
k_a
值是一个环境光照的固定值。环境光对于屏幕上出现的每一个点来说,都是一样的。i_a
值表示环境光的颜色,有时它表示所有光源照射的集合。
冯式反射模型公式的第二部分代表着漫反射。在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 reflectance。dotLNg
则是我们之前教程中使用的漫反射——朗伯反射。i_d
,表示场景中的光照强度,它由一个颜色值所定义。
冯式反射公式的第三部分稍微有一点复杂,表示的是镜面反射。在现实的生活中,有些金属或者经过打磨过的表面,如果我们从不同的角度去观察它们的话,会显得更加的光亮,这种现象就是镜面反射。因此,它表示的就是一个决定我们相机所处位置的一个方法。
在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
,是一束光线被放射后的方向:
根据维基百科解释,冯式反射模型通过下面的公式来计算反射的方向:
正如我们之前提到的,m角标,表示我们场景中的光线,在每个字母之上的小帽^
,表示一个被归一化后的向量。向量L
,表示光线的方向,向量N
,表示表面的法线。GLSL为我们提供了现成的方法reflect
来计算被反射出来的随机光线。此函数需要两个入参:随机的光线向量以及法线向量。
本质上,reflect
函数其实就是算式:I - 2.0 * dot(N, I) * N 的实现,其中I
表示光线的方向,N
表示法线。如果为这个公式乘以一个负值1,我们就可以的到一个和维基百科上一样的反射公式。这其实都取决于我们对轴的规范。向量V
,在特殊的放射代码中方向指向我们的观察者或者相机。我们可以将其设置为与光线相反的方向rd
。alpha是被用来控制shininess
球体上的总量。值越低,越是闪亮。
把所有的都放到一起
让我们把目前学到的都用到我们的代码中去吧。我们首先利用第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);
}
运行以上的代码,我们就可以看到一个简单的球出现在画布上,带一点漫反射:
看起来有点枯燥,我们给他一点颜色瞧瞧吧!目前我们只是给了球体一个漫反射的效果即伯朗反射效果。我们添加一个漫反射和一个镜面反射来完成我们的冯式反射模型。我们同样也会调整光线方向。这样我们就能在右上角看到一块亮斑。
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);
}
和之前一样,我们将点积的范围限制在0~1之间,运行上面的代码,我们就能在一个橄榄色的球上看到一个亮点了。
多束光线
你可能注意到了,冯式关照使用了一个求和等式。如果你在场景中添加更多的光照,那么就会有很多不同的镜面放射了。
为了方便我们创建多种光源,创建一个phong
方法,因为场景中只有一个物体,我们可以把反射系数(k_a
,k_d
,k_s
)以及强度都放到phong
函数中去。
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);
}
vec3 phong(vec3 lightDir, vec3 normal, vec3 rd) {
// 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(lightDir, 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(lightDir, normal), -rd), 0., 1.);
vec3 i_s = vec3(1, 1, 1);
float alpha = 10.;
vec3 specular = k_s * pow(dotRV, alpha) * i_s;
return ambient + diffuse + specular;
}
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 #1
vec3 lightPosition1 = vec3(-8, -6, -5);
vec3 lightDirection1 = normalize(lightPosition1 - p);
float lightIntensity1 = 0.6;
// light #2
vec3 lightPosition2 = vec3(1, 1, 1);
vec3 lightDirection2 = normalize(lightPosition2 - p);
float lightIntensity2 = 0.7;
// final sphere color
col = lightIntensity1 * phong(lightDirection1, normal, rd);
col += lightIntensity2 * phong(lightDirection2, normal , rd);
}
fragColor = vec4(col, 1.0);
}
为了使我们的球看起来不太亮,我们可以通过乘以一个强度的值来降低光照亮度。运行上面的代码,就能看到下面的图:
为多个物体上色
在phong
函数中,给所有的反射都加上统一的系数和强度看起来世不太现实。很多时候我们的场景中会有多个物体,他们的材质却并不统一。有些物体看起来会非常光滑,而有些物体则是完全不会有镜面反射光线。因此有必要创建一个能够应用到一个或者多个物体上的材质。每个材质都会有自己的环境反射、漫反射以及镜面反射系数。我们可以创建一个struct
来为材质保存所有的冯氏反射模型的信息。
struct Material {
vec3 ambientColor; // k_a * i_a
vec3 diffuseColor; // k_d * i_d
vec3 specularColor; // k_s * i_s
float alpha; // shininess
};
为场景中的每个物体或者表面创建另外一个struct
struct Surface {
int id; // id of object
float sd; // signed distance value from SDF
Material mat; // material of object
}
创建一个场景,包含地板和两个球。首先,创建三个材质。gold
函数用来返回一个黄金材质,silver
函数返回白银材质,checkerboard
函数返回棋盘模式。如你所想,棋盘不会有闪亮的反射效果,但是金属会有的。
Material gold() {
vec3 aCol = 0.5 * vec3(0.7, 0.5, 0);
vec3 dCol = 0.6 * vec3(0.7, 0.7, 0);
vec3 sCol = 0.6 * vec3(1, 1, 1);
float a = 5.;
return Material(aCol, dCol, sCol, a);
}
Material silver() {
vec3 aCol = 0.4 * vec3(0.8);
vec3 dCol = 0.5 * vec3(0.7);
vec3 sCol = 0.6 * vec3(1, 1, 1);
float a = 5.;
return Material(aCol, dCol, sCol, a);
}
Material checkerboard(vec3 p) {
vec3 aCol = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0)) * 0.3;
vec3 dCol = vec3(0.3);
vec3 sCol = vec3(0);
float a = 1.;
return Material(aCol, dCol, sCol, a);
}
我们新建一个opUnion
函数,这个函数跟我们之前使用的minWithColor
函数的作用是完全一样的。
Surface opUnion(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
利用opUnion
函数,可以为我们的场景中添加地板和球:
Surface scene(vec3 p) {
Surface sFloor = Surface(1, p.y + 1., checkerboard(p));
Surface sSphereGold = Surface(2, sdSphere(p - vec3(-2, 0, 0), 1.), gold());
Surface sSphereSilver = Surface(3, sdSphere(p - vec3(2, 0, 0), 1.), silver());
Surface co = opUnion(sFloor, sSphereGold);
co = opUnion(co, sSphereSilver);
return co;
}
然后为phong
函数添加一个入参,用来接收Material
。material
保存了我们在冯式反射模型中将会用到的所有的颜色值。
vec3 phong(vec3 lightDir, vec3 normal, vec3 rd, Material mat) {
// ambient
vec3 ambient = mat.ambientColor;
// diffuse
float dotLN = clamp(dot(lightDir, normal), 0., 1.);
vec3 diffuse = mat.diffuseColor * dotLN;
// specular
float dotRV = clamp(dot(reflect(lightDir, normal), -rd), 0., 1.);
vec3 specular = mat.specularColor * pow(dotRV, mat.alpha);
return ambient + diffuse + specular;
}
在mainImage
函数里面,我们可以传递最近物体的材质给到phong函数:
col = lightIntensity1 * phong(lightDirection1, normal, rd, co.mat);
col += lightIntensity2 * phong(lightDirection2, normal , rd, co.mat);
将所有的代码整合到一起,得到如下代码:
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;
}
struct Material {
vec3 ambientColor; // k_a * i_a
vec3 diffuseColor; // k_d * i_d
vec3 specularColor; // k_s * i_s
float alpha; // shininess
};
struct Surface {
int id; // id of object
float sd; // signed distance
Material mat;
};
Material gold() {
vec3 aCol = 0.5 * vec3(0.7, 0.5, 0);
vec3 dCol = 0.6 * vec3(0.7, 0.7, 0);
vec3 sCol = 0.6 * vec3(1, 1, 1);
float a = 5.;
return Material(aCol, dCol, sCol, a);
}
Material silver() {
vec3 aCol = 0.4 * vec3(0.8);
vec3 dCol = 0.5 * vec3(0.7);
vec3 sCol = 0.6 * vec3(1, 1, 1);
float a = 5.;
return Material(aCol, dCol, sCol, a);
}
Material checkerboard(vec3 p) {
vec3 aCol = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0)) * 0.3;
vec3 dCol = vec3(0.3);
vec3 sCol = vec3(0);
float a = 1.;
return Material(aCol, dCol, sCol, a);
}
Surface opUnion(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
Surface scene(vec3 p) {
Surface sFloor = Surface(1, p.y + 1., checkerboard(p));
Surface sSphereGold = Surface(2, sdSphere(p - vec3(-2, 0, 0), 1.), gold());
Surface sSphereSilver = Surface(3, sdSphere(p - vec3(2, 0, 0), 1.), silver());
Surface co = opUnion(sFloor, sSphereGold); // closest object
co = opUnion(co, sSphereSilver);
return co;
}
Surface rayMarch(vec3 ro, vec3 rd) {
float depth = MIN_DIST;
Surface co;
for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
vec3 p = ro + depth * rd;
co = scene(p);
depth += co.sd;
if (co.sd < PRECISION || depth > MAX_DIST) break;
}
co.sd = depth;
return co;
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(1.0, -1.0) * 0.0005;
return normalize(
e.xyy * scene(p + e.xyy).sd +
e.yyx * scene(p + e.yyx).sd +
e.yxy * scene(p + e.yxy).sd +
e.xxx * scene(p + e.xxx).sd);
}
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);
}
vec3 phong(vec3 lightDir, vec3 normal, vec3 rd, Material mat) {
// ambient
vec3 ambient = mat.ambientColor;
// diffuse
float dotLN = clamp(dot(lightDir, normal), 0., 1.);
vec3 diffuse = mat.diffuseColor * dotLN;
// specular
float dotRV = clamp(dot(reflect(lightDir, normal), -rd), 0., 1.);
vec3 specular = mat.specularColor * pow(dotRV, mat.alpha);
return ambient + diffuse + specular;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
vec3 backgroundColor = mix(vec3(1, .341, .2), vec3(0, 1, 1), uv.y) * 1.6;
vec3 col = vec3(0);
vec3 lp = vec3(0); // lookat point (aka camera target)
vec3 ro = vec3(0, 0, 5);
vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction
Surface co = rayMarch(ro, rd); // closest object
if (co.sd > MAX_DIST) {
col = backgroundColor;
} else {
vec3 p = ro + rd * co.sd; // point on surface found by ray marching
vec3 normal = calcNormal(p); // surface normal
// light #1
vec3 lightPosition1 = vec3(-8, -6, -5);
vec3 lightDirection1 = normalize(lightPosition1 - p);
float lightIntensity1 = 0.9;
// light #2
vec3 lightPosition2 = vec3(1, 1, 1);
vec3 lightDirection2 = normalize(lightPosition2 - p);
float lightIntensity2 = 0.5;
// final color of object
col = lightIntensity1 * phong(lightDirection1, normal, rd, co.mat);
col += lightIntensity2 * phong(lightDirection2, normal , rd, co.mat);
}
fragColor = vec4(col, 1.0);
}
运行上面的代码,我们就可以看到一个金色的球和一个银色的球悬浮在地板上。日落时分甚是漂亮!
总结
在本节课程中,我们学会了冯式反射模型,为物体添加一些表面的粗糙度就可以改进场景的真实度。我们还学会了通过使用struct
为每个物体添加上了不同的材质。编写着色器真是一件有趣的事情!