Shadertoy 教程 Part 10 - 视点相机
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教程系列的第10章。在本节教程中,我们将学习如何使用一个目标点让我们的相机模型更加灵活。使用这种方式,可以更容易地改变相机中的物体。
初始化
创建一个新的着色器程序,把下面的代码加入进去。请注意,我们现在已经把常量定义到了代码的顶部了:
// Constants 常量
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
const float PI = 3.14159265359;
// Rotation matrix around the X axis. X轴的旋转矩阵
mat3 rotateX(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(1, 0, 0),
vec3(0, c, -s),
vec3(0, s, c)
);
}
// Rotation matrix around the Y axis. Y轴上的旋转矩阵
mat3 rotateY(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, 0, s),
vec3(0, 1, 0),
vec3(-s, 0, c)
);
}
// Rotation matrix around the Z axis. Z轴上的旋转矩阵
mat3 rotateZ(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, -s, 0),
vec3(s, c, 0),
vec3(0, 0, 1)
);
}
// Identity matrix.
mat3 identity() {
return mat3(
vec3(1, 0, 0),
vec3(0, 1, 0),
vec3(0, 0, 1)
);
}
struct Surface {
float sd; // signed distance value
vec3 col; // color
};
Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
p = (p - offset) * transform; // apply transformation matrix
vec3 q = abs(p) - b;
float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
return Surface(d, col);
}
Surface sdFloor(vec3 p, vec3 col) {
float d = p.y + 1.;
return Surface(d, col);
}
Surface minWithColor(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
Surface sdScene(vec3 p) {
vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
Surface co = sdFloor(p, floorColor);
co = minWithColor(co, sdBox(p, vec3(1), vec3(-4, 0.5, -4), vec3(1, 0, 0), identity())); // left cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(0, 0.65, 0.2), identity())); // center cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(4, 0.5, -4), vec3(0, 0.55, 2), identity())); // right cube
return co;
}
Surface rayMarch(vec3 ro, vec3 rd, float start, float end) {
float depth = start;
Surface co; // closest object
for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
vec3 p = ro + depth * rd;
co = sdScene(p);
depth += co.sd;
if (co.sd < PRECISION || depth > end) break;
}
co.sd = depth;
return co;
}
vec3 calcNormal(in vec3 p) {
vec2 e = vec2(1, -1) * EPSILON;
return normalize(
e.xyy * sdScene(p + e.xyy).sd +
e.yyx * sdScene(p + e.yyx).sd +
e.yxy * sdScene(p + e.yxy).sd +
e.xxx * sdScene(p + e.xxx).sd);
}
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 ro = vec3(0, 0, 3); // ray origin that represents camera position
vec3 rd = normalize(vec3(uv, -1)); // ray direction
Surface co = rayMarch(ro, rd, MIN_DIST, MAX_DIST); // closest object
if (co.sd > MAX_DIST) {
col = backgroundColor; // ray didn't hit anything
} else {
vec3 p = ro + rd * co.sd; // point on cube or floor we discovered from ray marching
vec3 normal = calcNormal(p);
vec3 lightPosition = vec3(2, 2, 7);
vec3 lightDirection = normalize(lightPosition - p);
float dif = clamp(dot(normal, lightDirection), 0.3, 1.); // diffuse reflection
col = dif * co.col + backgroundColor * .2; // Add a bit of background color to the diffuse color
}
// Output to screen
fragColor = vec4(col, 1.0);
}
运行以上代码屏幕上会出现三个颜色分别为红,绿,蓝的立方体。
观察目标点
目前,我们只能通过调整射线的方向来移动我们的相机。而如果要摆动相机,则需要通过利用旋转矩阵和射线方向的乘积来实现。今天我们介绍的另外一种方式是采用相机和一个目标点来同时改变相机的位置。这个函数会返回一个3*3的变换矩阵,我们就是要利用这个矩阵与射线的乘积来达到摆动相机的效果。
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);
}
为了更好地理解这个矩阵,我们来看下面的图片。下面的图片来自Learn OpenGL 网站,这是一个非常棒的学习OpenGl图形API 的网站。
Camera/view space by Learn OpenGL
上图中传递了很多关于创建这个3*3矩阵的信息。我们需要分析计算出相机朝向的目标点以及另外三个重要的向量:相机正朝向,相机右朝向以及相机上方向。
第一步中,从相机的起始位置开始,这个位置也是射线(ro)所处的位置。
第二步,创建了表示一个相机朝向的向量,即观察目标点。在上图中,目标观察点处在3D空间的中心位置,但是我们可以任意地改变这个位置。请注意相机的方向是朝向相机的。这意味着我们可以利用我们在第六篇文章中学习到的右手规则的知识来计算相机的方向。
vec3 cd = normalize(lookAtPoint - cameraPos); // 相机方向
第三步,有一个灰色向量指向相机的上方。方向向量(0,1,0)表示y轴上的一个单位向量。利用y轴和相机反向向量的差积,我们可以计算出相机右边这个向量。就是我们在上图中看到的红色标识的那个向量
normalize(cross(vec3(0, 1, 0), cd)); // 相机右
第四步,我们需要找到相机上方向。通过相机的右向量和相机方向两个向量的叉积,我们就可以找到这个相机向上的向量。在图中我们用绿色的线条标识。
vec3 cu = normalize(cross(cd, cr)); // 相机右
最后,我们将三个向量放在一起,形成一个新的矩阵:
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); //负号可以改成正号,让常规的坐标系翻转过来。 negative signs can be turned positive (or vice versa) to flip coordinate space conventions
}
现在我们来看看返回的相机矩阵:
return mat3(-cr, cu, -cd);
也许你会好奇负号是怎么来的呢?这其实取决于我们是如何定义一个常规的坐标系。实际上我们可以通过改变正负符号来确定3D空间中的坐标方向。我们在这里中用到的就是这样一种常规的坐标系统,后面我们就能看到标识是如何翻转的。
应用相机矩阵
我们现在已经创建了一个相机矩阵函数,现在把它用到mainImage
函数中去。创建一个目标观察点然后将其传入到相机函数中去。然后将矩阵和射线方向相乘。就像我们在第9章教程中提到过的一样。
vec3 lp = vec3(0, 0, 0); // lookat point (aka camera target)
vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction
运行上面的代码,场景与之前无异,但相机现在已经朝着3D中的某一个点了。因为立方体距离地板0.5个单位,相机朝向会轻微的有些抬高。我们可以直接通过调整相机的中心位置,通过调整相机的观察目标点来匹配绿色立方体的位置:
vec3 lp = vec3(0, 0.5, -4);
假设我们现在需要聚焦在红色的立方体上,它现在在3D空间中的位置是(-4,0.5, -4)。我们来改变我们的观察目标点来匹配它的位置
vec3 lp = vec3(-4, 0.5, -4);
现在应该可以看到相机在观察的就是红色的立方体,并且立方体是处在画布的中心了:
按照相同的方式,我们现在切换观察目标点为蓝色立方体的位置(4, 0.5, -4):
vec3 lp = vec3(4, 0.5, -4);
现在你应该可以看到相机现在观察的就是蓝色的立方体,并且该立方体是处在画布的中心了。
以下是完整的代码:
// Constants
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
const float PI = 3.14159265359;
// Rotation matrix around the X axis.
mat3 rotateX(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(1, 0, 0),
vec3(0, c, -s),
vec3(0, s, c)
);
}
// Rotation matrix around the Y axis.
mat3 rotateY(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, 0, s),
vec3(0, 1, 0),
vec3(-s, 0, c)
);
}
// Rotation matrix around the Z axis.
mat3 rotateZ(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, -s, 0),
vec3(s, c, 0),
vec3(0, 0, 1)
);
}
// Identity matrix.
mat3 identity() {
return mat3(
vec3(1, 0, 0),
vec3(0, 1, 0),
vec3(0, 0, 1)
);
}
struct Surface {
float sd; // signed distance value
vec3 col; // color
};
Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
p = (p - offset) * transform; // apply transformation matrix
vec3 q = abs(p) - b;
float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
return Surface(d, col);
}
Surface sdFloor(vec3 p, vec3 col) {
float d = p.y + 1.;
return Surface(d, col);
}
Surface minWithColor(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
Surface sdScene(vec3 p) {
vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
Surface co = sdFloor(p, floorColor);
co = minWithColor(co, sdBox(p, vec3(1), vec3(-4, 0.5, -4), vec3(1, 0, 0), identity())); // left cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(0, 0.65, 0.2), identity())); // center cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(4, 0.5, -4), vec3(0, 0.55, 2), identity())); // right cube
return co;
}
Surface rayMarch(vec3 ro, vec3 rd, float start, float end) {
float depth = start;
Surface co; // closest object
for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
vec3 p = ro + depth * rd;
co = sdScene(p);
depth += co.sd;
if (co.sd < PRECISION || depth > end) break;
}
co.sd = depth;
return co;
}
vec3 calcNormal(in vec3 p) {
vec2 e = vec2(1, -1) * EPSILON;
return normalize(
e.xyy * sdScene(p + e.xyy).sd +
e.yyx * sdScene(p + e.yyx).sd +
e.yxy * sdScene(p + e.yxy).sd +
e.xxx * sdScene(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);
}
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(4, 0.5, -4); // lookat point (aka camera target)
vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction
Surface co = rayMarch(ro, rd, MIN_DIST, MAX_DIST); // closest object
if (co.sd > MAX_DIST) {
col = backgroundColor; // ray didn't hit anything
} else {
vec3 p = ro + rd * co.sd; // point on cube or floor we discovered from ray marching
vec3 normal = calcNormal(p);
vec3 lightPosition = vec3(2, 2, 7);
vec3 lightDirection = normalize(lightPosition - p);
float dif = clamp(dot(normal, lightDirection), 0.3, 1.); // diffuse reflection
col = dif * co.col + backgroundColor * .2; // Add a bit of background color to the diffuse color
}
// Output to screen
fragColor = vec4(col, 1.0);
}
调整符号规则
之前,我们已经看到相机函数是如何返回一个包含着三个相机向量的矩阵了。
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 lp = vec3(0, 0.5, -4); // lookat point (aka camera target)
vec3 ro = vec3(0, 0, 3); // ray origin that represents camera position
vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction
这样就做到了我们在本篇开头图片的效果即:绿色立方体在中间,红色在左侧,蓝色在右侧。
如果我们将cr
前面的符号改为正值,那么我们看到的就是下面这幅景象:
红色和蓝色的位置互换了。请注意地板格子的位置,它们也有变化。将相机右向量翻转之后,得到了翻转之前的一个镜像的场景。就像是一面镜子一样。
使用cr
的正值使得我们的相机看起来有点怪异。x轴的左侧本来是负方向的,而通过翻转cr
意味着我们颠倒了坐标规则。
如果我们翻转相机方向 cd
为一个正值,我们会将整个相机场景旋转因为这样做会翻转我们z轴的坐标方向。
翻转z轴方向的另外一种方式是给射线z元素使用一个正值。
vec3 rd = normalize(vec3(uv, 1)); // 用+1替换-1
使用任何一种相机模式来定义观察目标点,都需要好好地了解你使用坐标轴的规则。你可以调整cr
,cu
或者cd
来制造一些有趣的效果。请确保射线方向rd
调整到之前的-1。
下面的代码会在z轴上产生一个快速拉伸的弹弓效果。这样也许可以用来表示弯曲效果?
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, abs(cos(iTime)) * -cd);
}
在我们继续下面的课程之前,还是将相机矩阵恢复到之前的正常状态吧。
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);
}
让相机围绕着目标进行旋转
假设想要相机对着绿色的立方体在一个圆形的路径上进行旋转。我们将其保持在离地板一定的高度上。因为所有的立方体在y轴上都保持0.5的高度,我们确保y轴ro
射线方向(相机所在的点),也等于0.5.
想要沿着圆形轨道旋转,那么我们就要在x元素和z元素上做变化。从上到下的角度来看,就会看到下面图片所示的一个示意图:
上图所示,相机会绕着圆形路径(黑色)旋转。从上到下的观测,场景看起来只是一个包含x轴(红色)和z轴(蓝色)的2D场景。我们的目标是调整ro
的x元素和z元素,这样它沿着圆形路径旋转。通过转换ro.x和ro.z到极坐标我们就可以完成。
vec3 ro = vec3(0, 0.5, 0);
ro.x = cameraRadius * cos(theta);
ro.z = cameraRadius * sin(theta);
相机的半径会一直增加直到我们可以在场景中看到所有的立方体。我们目前在3D空间中可以看到三个立方体。(由sdScene
函数定义):
vec3(-4, 0.5, -4) // left cube
vec3(0, 0.5, -4) // center cube
vec3(4, 0.5, -4) // right cube
因此,为了保证安全起见,将半径设置为10,因为立方体的左右两侧的最大距离是4 - (-4) = 8 个单位。
在代码中,我们将射线源头的x元素和y元素进行了极坐标转换,半径为10.然后,我们给圆形路径一个偏移值,这样,我们的观察目标点就能在整个圆形的中间了。
vec3 lp = vec3(0, 0.5, -4); // lookat point (aka camera target)
vec3 ro = vec3(0, 0.5, 0); // ray origin that represents camera position
float cameraRadius = 10.;
ro.x = cameraRadius * cos(iTime) + lp.x; // convert x-component to polar and add offset
ro.z = cameraRadius * sin(iTime) + lp.z; // convert z-component to polar and add offset
vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction
运行以上的代码,你就能看到摄像机在旋转,因为它就是在沿着圆形的路径旋转,但是目标点依旧固定在绿色的立方体上面:
从一个自上而下的角度看过去,我们的相机是在沿着一个观察点,在x元素和y元素上移动,这样我们就可以确保观察目标点处于我们的圆圈中心。同时也保证了与绿色立方体距离等于半径的长度,从而确保整个旋转的过程中保持一致的距离。
你可以利用我在Desmos上创建的例子来体验这个圆圈路径。想像一下我们的绿色立方体就处在整个圆圈的中心点位置。使用观察目标点的概念让我们的相机固定化。我们可以将相机在y轴的距离上提高一些,同时还是绕着x轴旋转,这样就能可以俯瞰我们的整个场景了。调整相机的高度,看看会发生什么事情:
vec3 ro = vec3(0, 5, 0);
运行以上的代码,我们可以看到相机以一个更高的角度来绕这三个立方体运动了。就像一个坐在直升机上进行现场报道的新闻记者。
改变观察目标点,就可以围绕新的点进行旋转。下面是完整的代码:
// Constants
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
const float PI = 3.14159265359;
// Rotation matrix around the X axis.
mat3 rotateX(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(1, 0, 0),
vec3(0, c, -s),
vec3(0, s, c)
);
}
// Rotation matrix around the Y axis.
mat3 rotateY(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, 0, s),
vec3(0, 1, 0),
vec3(-s, 0, c)
);
}
// Rotation matrix around the Z axis.
mat3 rotateZ(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, -s, 0),
vec3(s, c, 0),
vec3(0, 0, 1)
);
}
// Identity matrix.
mat3 identity() {
return mat3(
vec3(1, 0, 0),
vec3(0, 1, 0),
vec3(0, 0, 1)
);
}
struct Surface {
float sd; // signed distance value
vec3 col; // color
};
Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
p = (p - offset) * transform; // apply transformation matrix
vec3 q = abs(p) - b;
float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
return Surface(d, col);
}
Surface sdFloor(vec3 p, vec3 col) {
float d = p.y + 1.;
return Surface(d, col);
}
Surface minWithColor(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
Surface sdScene(vec3 p) {
vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
Surface co = sdFloor(p, floorColor);
co = minWithColor(co, sdBox(p, vec3(1), vec3(-4, 0.5, -4), vec3(1, 0, 0), identity())); // left cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(0, 0.65, 0.2), identity())); // center cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(4, 0.5, -4), vec3(0, 0.55, 2), identity())); // right cube
return co;
}
Surface rayMarch(vec3 ro, vec3 rd, float start, float end) {
float depth = start;
Surface co; // closest object
for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
vec3 p = ro + depth * rd;
co = sdScene(p);
depth += co.sd;
if (co.sd < PRECISION || depth > end) break;
}
co.sd = depth;
return co;
}
vec3 calcNormal(in vec3 p) {
vec2 e = vec2(1, -1) * EPSILON;
return normalize(
e.xyy * sdScene(p + e.xyy).sd +
e.yyx * sdScene(p + e.yyx).sd +
e.yxy * sdScene(p + e.yxy).sd +
e.xxx * sdScene(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);
}
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, 0.5, -4); // lookat point (aka camera target)
vec3 ro = vec3(0, 5, 0); // ray origin that represents camera position
float cameraRadius = 10.;
ro.x = cameraRadius * cos(iTime) + lp.x; // convert to polar
ro.z = cameraRadius * sin(iTime) + lp.z;
vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction
Surface co = rayMarch(ro, rd, MIN_DIST, MAX_DIST); // closest object
if (co.sd > MAX_DIST) {
col = backgroundColor; // ray didn't hit anything
} else {
vec3 p = ro + rd * co.sd; // point on cube or floor we discovered from ray marching
vec3 normal = calcNormal(p);
vec3 lightPosition = vec3(2, 2, 7);
vec3 lightDirection = normalize(lightPosition - p);
float dif = clamp(dot(normal, lightDirection), 0.3, 1.); // diffuse reflection
col = dif * co.col + backgroundColor * .2; // Add a bit of background color to the diffuse color
}
// Output to screen
fragColor = vec4(col, 1.0);
}
用鼠标控制相机旋转
只需要多做几个步骤就可以使用鼠标控制相机来沿着整个场景进行旋转。我们之前在第九篇课程中学习到了,鼠标的信息都被存储在了iMouse
这个全局变量当中了。通过下面的方式,我们来创建一个“mouse UV”坐标:
vec2 mouseUV = iMouse.xy/iResolution.xy; // Range: <0, 1>
我们接下来需要修改下面这三行代码,将时间维度改成鼠标坐标的维度来做旋转运动。
float cameraRadius = 10.;
ro.x = cameraRadius * cos(iTime) + lp.x; // convert to polar
ro.z = cameraRadius * sin(iTime) + lp.z;
下面是修改过之后的代码:
float cameraRadius = 2.;
ro.yz = ro.yz * cameraRadius * rotate2d(mix(PI/2., 0., mouseUV.y));
ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z); // remap mouseUV.x to <-pi, pi> range
我们在这里又一次又使用到了mix
函数来重新定以鼠标的位置的x元素。这一次,我们将其从<0,1>的范围调整到了<-π, π>区间范围。同样需要再观察目标点添加x元素和z元素。请注意我们已经有一个rotate2d
函数。这个函数将会使用2D矩阵来做2D旋转。在下面的代码中加入此函数:
mat2 rotate2d(float theta) {
float s = sin(theta), c = cos(theta);
return mat2(c, -s, s, c);
}
像之前一样,你可以来回调整cameraRadius
一直到合适为止。下面是完整的代码:
// Constants
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
const float EPSILON = 0.0005;
const float PI = 3.14159265359;
// Rotate around a circular path
mat2 rotate2d(float theta) {
float s = sin(theta), c = cos(theta);
return mat2(c, -s, s, c);
}
// Rotation matrix around the X axis.
mat3 rotateX(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(1, 0, 0),
vec3(0, c, -s),
vec3(0, s, c)
);
}
// Rotation matrix around the Y axis.
mat3 rotateY(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, 0, s),
vec3(0, 1, 0),
vec3(-s, 0, c)
);
}
// Rotation matrix around the Z axis.
mat3 rotateZ(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat3(
vec3(c, -s, 0),
vec3(s, c, 0),
vec3(0, 0, 1)
);
}
// Identity matrix.
mat3 identity() {
return mat3(
vec3(1, 0, 0),
vec3(0, 1, 0),
vec3(0, 0, 1)
);
}
struct Surface {
float sd; // signed distance value
vec3 col; // color
};
Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
p = (p - offset) * transform; // apply transformation matrix
vec3 q = abs(p) - b;
float d = length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
return Surface(d, col);
}
Surface sdFloor(vec3 p, vec3 col) {
float d = p.y + 1.;
return Surface(d, col);
}
Surface minWithColor(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
Surface sdScene(vec3 p) {
vec3 floorColor = vec3(1. + 0.7*mod(floor(p.x) + floor(p.z), 2.0));
Surface co = sdFloor(p, floorColor);
co = minWithColor(co, sdBox(p, vec3(1), vec3(-4, 0.5, -4), vec3(1, 0, 0), identity())); // left cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(0, 0.65, 0.2), identity())); // center cube
co = minWithColor(co, sdBox(p, vec3(1), vec3(4, 0.5, -4), vec3(0, 0.55, 2), identity())); // right cube
return co;
}
Surface rayMarch(vec3 ro, vec3 rd, float start, float end) {
float depth = start;
Surface co; // closest object
for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
vec3 p = ro + depth * rd;
co = sdScene(p);
depth += co.sd;
if (co.sd < PRECISION || depth > end) break;
}
co.sd = depth;
return co;
}
vec3 calcNormal(in vec3 p) {
vec2 e = vec2(1, -1) * EPSILON;
return normalize(
e.xyy * sdScene(p + e.xyy).sd +
e.yyx * sdScene(p + e.yyx).sd +
e.yxy * sdScene(p + e.yxy).sd +
e.xxx * sdScene(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);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
vec2 mouseUV = iMouse.xy/iResolution.xy; // Range: <0, 1>
vec3 backgroundColor = vec3(0.835, 1, 1);
vec3 col = vec3(0);
vec3 lp = vec3(0, 0.5, -4); // lookat point (aka camera target)
vec3 ro = vec3(0, 5, 0); // ray origin that represents camera position
float cameraRadius = 2.;
ro.yz = ro.yz * cameraRadius * rotate2d(mix(PI/2., 0., mouseUV.y));
ro.xz = ro.xz * rotate2d(mix(-PI, PI, mouseUV.x)) + vec2(lp.x, lp.z);
vec3 rd = camera(ro, lp) * normalize(vec3(uv, -1)); // ray direction
Surface co = rayMarch(ro, rd, MIN_DIST, MAX_DIST); // closest object
if (co.sd > MAX_DIST) {
col = backgroundColor; // ray didn't hit anything
} else {
vec3 p = ro + rd * co.sd; // point on cube or floor we discovered from ray marching
vec3 normal = calcNormal(p);
vec3 lightPosition = vec3(2, 2, 7);
vec3 lightDirection = normalize(lightPosition - p);
float dif = clamp(dot(normal, lightDirection), 0.3, 1.); // diffuse reflection
col = dif * co.col + backgroundColor * .2; // Add a bit of background color to the diffuse color
}
// Output to screen
fragColor = vec4(col, 1.0);
}
现在,你就可以在场景中使用鼠标来控制相机的转动了!??你可以通过这种方式来决定相机围绕着哪个点旋转。
总结
采用这种形式的对相机进行运动是很重要的,观察目标点的概念使相机很容易地围绕着某个点做圆周运动。
资源
- Learn OpenGL: Camera
- Cross Product
- Polar Coordinates