Shadertoy 教程 Part 9 - 相机的运动
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第9部分的教程。本次教程中,我们将会学习到如何在场景中移动相机。
初始化
首先,我们新建一个着色器,然后把下面的模板代码贴进去:
// 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)
);
}
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
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(0, 0.5, -4), vec3(1, 0, 0), identity()));
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.0, -1.0) * 0.0005; // 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);
}
运行以上的代码,我们就能看到一个方块格子的地板,天空(背景颜色)以及一个红色的立方体。代码中包含了我们在上一节教程中学习到的旋转矩阵。
平移相机
平移相机操作实际上非常基础。相机当前的位置就是正对着一个立方体,立方体悬浮在空中,距离相机z轴有一段距离。因为我们这里采用的是右手坐标系统,因此,z轴的负方向是远离相机的,而正方向则是朝向相机的。
相机所处的位置由ro
变量所定义,即射线的源头。它当前的位置是在vec3(0,0,0)
处。如果要让相机沿着x
轴方向移动,我们简单地调整ro
的x
元素即可:
vec3 ro = vec3(1,0,3);
相机现在移动到了右边,产生了向左移动的的效果。
同样,我们调整ro
的y元素,使其上下移动
vec3 ro = vec3(0,1,3);
向上移动相机产生了立方体和地板向下的效果。
可以沿着x轴和y轴对相机进行cos
和sin
函数的运动,从而产生旋转效果。
vec3 ro = vec3(cos(iTime), sin(iTime) + 0.1, 3);
显然,当我们的相机紧贴入地板后,看起来会有一点奇怪,所以我们在y轴上添加了0.1
个单位,以防出现闪烁的效果。
摆动/旋转 相机
现在,我们需要保持相机的位置在ro
点上,但我们需要对其在上下左右的方向上进行摆动。同时也需要尝试让相机能够朝着所有的方向进行任意地摆动,也就是180度全景旋转。这些效果都需要运用变换矩阵与射线方向(rd)的计算。我们把摄像机归位一下先吧:
vec3 ro = vec3(0, 0, 3);
现在立方体是处在画布的中间了。目前从侧面看我们的场景与下面这张图中的描绘是相似的:
位置保持不变,但是却可以沿着不同的角度进行摆动。假设我们需要向上进行摆动,场景图应该是下图描绘出来的样子:
请注意,发射出去的射线的方向也随着相机的摆动而发生了改变。摆动相机意味着把所有发射的射线做了一定角度的倾斜。
相机的摆动有点像飞行器主轴。
相机不仅仅能够沿着xy和z轴进行平移,同事也可以绕着pitch、yaw和roll轴进行摆动。这意味着相机有6个自由度:三个位置轴和三个旋转轴:
很幸运,我们可以使用我们之前学到的旋转矩阵来处理pitch、yaw和roll轴的变化。
“Pitch”被应用到rotateX
函数中,“yaw”被应用到rotateY
函数中,“roll”被应用到rotateZ
函数当中。
如果我们想要相机上下摆动(“pitch”),那么我们就需要为射线方向(rd)应用rotateX
函数。
vec3 rd = normalize(vec3(uv, -1));
rd *= rotateX(0.3);
我们简单地将一个或者多个旋转矩阵和射线方向进行了乘法运算,从而对相机进行摆动。这样做改变了所有的从相机发出的射线方向,从而也改变我们在Shadertoy中看到的物体的景象。
控制摆动角度在-0.5到0.5之间
vec3 rd = normalize(vec3(uv, -1));
rd *= rotateX(sin(iTime) * 0.5);
左右摆动相机(yaw),应用 rotateY
函数
vec3 rd = normalize(vec3(uv, -1));
rd *= rotateY(sin(iTime) * 0.5);
侧面摆动相机(roll),需要应用rotateZ
函数。是不是像滚筒洗衣机一样!??
vec3 rd = normalize(vec3(uv, -1));
rd *= rotateZ(sin(iTime) * 0.5);
360 度全景相机旋转
我们同样也可以在PI和负PI之间沿着yaw旋转,这样就能完成360度的旋转了。
vec3 rd = normalize(vec3(uv, -1));
rd *= rotateZ(sin(iTime) * 0.5);
仔细看相机的后方,我们会发现地板上有一个发光的区域。这个点就是光的位置,它目前的位置是vec3(2,2,7)。因为z轴的位置设置在相机后面,当你翻转相机之后,就会看到光。
你或许会认为这个光斑是一个四月的愚人节的玩笑,但它确实是漫反射的结果,这个我们在中提到过。
float dif = clamp(dot(normal, lightDirection), 0.3, 1.);
col = dif * co.col + backgroundColor * .2;
因为我们是基于漫反射和表面的法线给地板着色的,如果你要移除太阳斑点,你就需要在计算地板的颜色时候忽略光照的效果。
如果光线是被挡在相机的后面,这也许不是个问题。但如果你需要一个场景,里面有地板,并且相机会全景的转动,那么你就会想要移除所有的光斑了。
移除太阳光点或者太阳光照的思路是考虑给每个场景分配一个ID。首先通过光线步进算法得到一个采样点,然后检查这个点是否在地板上,最后通过ID区分地板和其他的物体,从而达到移除光照的效果。
// 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)
);
}
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
struct Surface {
float sd; // signed distance value
vec3 col; // color
int id; // identifier for each surface/object
};
/*
Surface IDs:
1. Floor
2. Box
*/
Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
p = (p - offset) * transform;
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, 2);
}
Surface sdFloor(vec3 p, vec3 col) {
float d = p.y + 1.;
return Surface(d, col, 1);
}
Surface minWithColor(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
Surface sdScene(vec3 p) {
vec3 floorColor = vec3(.5 + 0.3*mod(floor(p.x) + floor(p.z), 2.0));
Surface co = sdFloor(p, floorColor);
co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(1, 0, 0), identity()));
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.0, -1.0) * 0.0005; // 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
const float PI = 3.14159265359;
vec3 rd = normalize(vec3(uv, -1));
rd *= rotateY(sin(iTime * 0.5) * PI); // 0.5 is used to slow the animation down
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);
// check material ID
if( co.id == 1 ) // floor
{
col = co.col;
} else {
// lighting
vec3 lightPosition = vec3(2, 2, 7);
vec3 lightDirection = normalize(lightPosition - p);
// color
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);
}
利用此种方法,地板的和光线的作用会看起来有点不一样了,但是光斑却不见了。
通过给每个表面,材质或者物体一个id,在执行完光线步进算法之后,我们就能追踪到哪个物体被击中。这种方法在给物体单独上色的时候非常有用。
理解 iMouse
Shadertoy 为我们提供了一系列全局变量,你可以在你的着色器代码中使用他们更好的与屏幕交互。新建一个着色器,点击左上角的箭头,旁边的文字是Shader inputs。你就能看到一个全局变量的列表。
下面就这我们可以在Shadertoy着色器中使用的全局变量:
Shader Inputs
uniform vec3 iResolution; // 视口分辨率viewport resolution (in pixels)
uniform float iTime; // 着色器运行时间 shader playback time (in seconds)
uniform float iTimeDelta; // 渲染时间 render time (in seconds)
uniform int iFrame; // 着色器运行帧率 shader playback frame
uniform float iChannelTime[4]; // 通道运行时间 channel playback time (in seconds)
uniform vec3 iChannelResolution[4]; // 通道分辨率channel resolution (in pixels)
uniform vec4 iMouse; // 鼠标像素坐标mouse pixel coords. xy: current (if MLB down), zw: click
uniform samplerXX iChannel0..3; // 输入通道input channel. XX = 2D/Cube
uniform vec4 iDate; // 年,月,日(year, month, day, time in seconds)
uniform float iSampleRate; // 声音样本sound sample rate (i.e., 44100)
其中,iMouse
保存了鼠标点击画布上时的位置信息。这个变量是vec4
类型的,他里面保存了你点击鼠标左键时的四个信息。
vec4 mouse = iMouse;
mouse.xy = 最后一次鼠标按下时的位置 mouse position during last button down
abs(mouse.zw) = 最后一次鼠标点击的位置 mouse position during last button click
sign(mouze.z) = 鼠标被按下 button is down (positive if down)
sign(mouze.w) = 点击了鼠标 button is clicked (positive if clicked)
mouse click 表示你点击鼠标之后立即发生的事情,mouse down 事件表示你按下鼠标并且一直按住它的时候发生的事情。
在Inigo Quilez 的这篇教程中,展示了如何使用存储在iMouse
中的每一块数据。当你在场景中点击任意地方,会出现一个白色的圈圈。如果你按住鼠标不放然后移动鼠标,一条黄色线会在两个圈圈之间出现。一旦你松开,黄色的线就会消失。
我们在这篇教程中唯一关心的是鼠标的坐标位置。我们在这里做了一个小小的示例,向你展示如何使的鼠标在画布中移动圆圈。我们看看下面的代码吧:
float sdfCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
float d = length(vec2(x, y)) - r;
return step(0., -d);
}
vec3 drawScene(vec2 uv, vec2 mp) {
vec3 col = vec3(0);
float blueCircle = sdfCircle(uv, 0.1, mp);
col = mix(col, vec3(0, 1, 1), blueCircle);
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy - 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // fix aspect ratio
// mp = mouse position of the last click
vec2 mp = iMouse.xy/iResolution.xy - 0.5; // <-0.5,0.5>
mp.x *= iResolution.x/iResolution.y; // fix aspect ratio
vec3 col = drawScene(uv, mp);
// Output to screen
fragColor = vec4(col,1.0);
}
请注意我们获取鼠标的位置非常类似我们获取UV的坐标位置。通过下面这种方式,归一化坐标:
vec2 mp = iMouse.xy/iResolution.xy // range is between 0 and 1
通过减去0.5,会将鼠标的坐标归一化到0和1,我这里将鼠标坐标归一化 -0.5到0.5之间。
vec2 mp = iMouse.xy/iResolution.xy - 0.5 // range is between -0.5 and 0.5
用鼠标平移相机
现在我们知道了如何使用iMouse
全局变量,让我们应用到相机里面去吧。用鼠标来控制光源ro
的位置,从而移动相机:
vec2 mouse = iMouse.xy / iResolution.xy - 0.5; // <-0.5,0.5>
vec3 ro = vec3(mouse.x, mouse.y, 3); // 当你点击画布并且拖动鼠标,光源会随着鼠标移动 ray origin will move as you click on the canvas and drag the mouse
如果你点击画布,拖动鼠标,你会发现相机在-0.5和0.5的范围区间沿着x轴和y轴移动。画布的中心是点(0,0),同时可以让相机位置回到画布的中心:
如果你想平移得更远,你可以对他们进行乘法运算:
vec2 mouse = iMouse.xy / iResolution.xy - 0.5; // <-0.5,0.5>
vec3 ro = vec3(2. * mouse.x, 2. * mouse.y, 3);
用鼠标摆动相机
我们可以通过改变theta
的值来摆动我们的相机,theta
就是我们通过旋转矩阵(例如:ratateX
和rotateY
以及rotateZ
)处理后得到的角度。请确保你不再使用鼠标来控制光源了。否则,你会发现很怪异的现象。
沿着yaw轴左右摆动相机:
vec2 mouse = iMouse.xy / iResolution.xy - 0.5; // <-0.5,0.5>
vec3 rd = normalize(vec3(uv, -1)); // ray direction
rd *= rotateY(mouse.x); // apply yaw
目前,我们的mouse.x 的范围是在-0.5到0.5之间,如果我们把范围改成π到-π之间,那么我们的代码就看起来更为合理了。要做到这点,我们使用mix
函数,它生来就是用来做线性差值的,所以对于重新设定区间值再合适不过了。我们将mouse.x的区间 <-0.5, 0.5> 改为 <-π, π>.
vec2 mouse = iMouse.xy / iResolution.xy - 0.5; // <-0.5,0.5>
vec3 rd = normalize(vec3(uv, -1)); // ray direction
rd *= rotateY(mix(-PI, PI, mouse.x)); // apply yaw with a 360 degree range
现在我们可以360度旋转相机了。
你可能会想如何使用mouse.y
呢,我们可以用这个值上下摆动相机。意味着我们可以利用rotateX
函数。
vec2 mouse = iMouse.xy / iResolution.xy - 0.5; // <-0.5,0.5>
vec3 rd = normalize(vec3(uv, -1)); // ray direction
rd *= rotateX(mouse.y); // apply pitch
这样就可以让相机在-0.5到0.5之间上下摆动。
如果你想要用鼠标用mouse.x
改变yaw轴角度,并且同时用mouse.y
改变pitch轴的角度,那么我们需要将这两个矩阵结合在一起:
vec2 mouse = iMouse.xy / iResolution.xy - 0.5; // <-0.5,0.5>
vec3 rd = normalize(vec3(uv, -1));
rd *= rotateY(mouse.x) * rotateX(mouse.y); // apply yaw and pitch
现在你可以在我们的场景中自由地旋转摆动相机了!通过摆动相机,可以手动解决我们在3D场景中的一些困难了。在Unity或者Blender这种软件当中自带了一个非常强大的3D相机。完整的代码如下:
// 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)
);
}
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;
struct Surface {
float sd; // signed distance value
vec3 col; // color
int id; // identifier for each surface/object
};
/*
Surface IDs:
1. Floor
2. Box
*/
Surface sdBox( vec3 p, vec3 b, vec3 offset, vec3 col, mat3 transform)
{
p = (p - offset) * transform;
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, 2);
}
Surface sdFloor(vec3 p, vec3 col) {
float d = p.y + 1.;
return Surface(d, col, 1);
}
Surface minWithColor(Surface obj1, Surface obj2) {
if (obj2.sd < obj1.sd) return obj2;
return obj1;
}
Surface sdScene(vec3 p) {
vec3 floorColor = vec3(.5 + 0.3*mod(floor(p.x) + floor(p.z), 2.0));
Surface co = sdFloor(p, floorColor);
co = minWithColor(co, sdBox(p, vec3(1), vec3(0, 0.5, -4), vec3(1, 0, 0), identity()));
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.0, -1.0) * 0.0005; // 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
vec2 mouse = iMouse.xy / iResolution.xy - 0.5; // <-0.5,0.5>
vec3 rd = normalize(vec3(uv, -1)); // ray direction
rd *= rotateY(mouse.x) * rotateX(mouse.y); // apply yaw and pitch
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);
// check material ID
if( co.id == 1 ) // floor
{
col = co.col;
} else {
// lighting
vec3 lightPosition = vec3(2, 2, 7);
vec3 lightDirection = normalize(lightPosition - p);
// color
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);
}
总结
本篇教程中,我们学习了如何在六个自由的角度移动相机。我们会学会如何沿着x、y和z轴平移先相机,也学习了如何沿着yaw、pitch和roll轴摆动我们的相机。利用我们今天学会的知识,你可以在3D场景中调试bug以及做一些动画交互。
资源
- Aircraft Principal Axes
- Pitch, Yaw, and Roll
- Rotation Matrices
- Shadertoy: Input - Mouse
- Shadertoy: 2D Movement with Mouse
- Shadertoy: Panning and Tilting the Camera Example