NYU-CS2270-研究生计算机图形学笔记-全-
NYU CS2270 研究生计算机图形学笔记(全)
9 月 9 日课程笔记 -- 着色器简介
3D 坐标系
WebGL 存在于 3D 世界中:
-
x 向右移动
-
y 向上
-
z 向前(屏幕外)
这被称为右手坐标系。
现在我们将从 x、y 平面开始进行所有工作。 具体来说,我们将使用从 -1 → +1 的正方形的 x 和 y。

正方形作为三角带
我们的几何图形将是一个正方形。
在 WebGL 中,一切都由三角形组成,因此我们将需要两个三角形。
我们将这些定义为 三角带。
在三角带中,每三个连续的顶点构成一个新的三角形,因此我们需要指定总共四个顶点,如右图所示。

Z 缓冲算法
GPU(图形处理单元)使用 "Z 缓冲算法" 进行渲染。
对于每个动画帧,此算法由两个连续步骤组成:
-
对于每个顶点:
GPU 运行 顶点着色器 以:
- 找出包含此顶点的图像的像素;
- 设置要插值的 "varying" 值。
-
对于每个三角形:
GPU 从顶点到像素进行插值。
对于每个像素:
GPU 运行 片段着色器 以:
- 计算颜色;
- 如果这是像素上最近的片段,请替换图像中此像素的颜色和深度。

Vector3 对象
一个非常有用的数据结构是长度为 3 的向量,我们可以使用它来表示 x、y、z 坐标,如右图所示。
在 Javascript 中,我们可以通过构造函数定义此对象,该构造函数包含每个实例可以更改的所有属性,以及原型,该原型包含从一个实例到另一个实例不会更改的属性(例如设置值的函数等)。

Vector3 对象将随着学期的进展而增加功能,但右侧是一个起始版本。
请注意,x、y 和 z 属性是在构造函数中定义的,这些属性从实例到实例会改变。
set 属性,这将是所有实例的相同函数,定义在原型中。
function Vector3() {
this.x = 0;
this.y = 0;
this.z = 0;
}
Vector3.prototype = {
set : function(x,y,z) {
this.x = x;
this.y = y;
this.z = z;
},
}
Uniform 变量
GLSL("GL 着色语言")是用于 GPU 上着色器的类似 C 的语言。 其中一个关键构造是 uniform 变量。
GPU 上的 uniform 变量在每个像素处具有相同的值。 它们可以(并且经常)随时间改变。
按照惯例,uniform 变量名以字母 'u' 开头。
对于您的作业,我已经创建了一些有用的 uniform 变量:
float uTime; // time elapsed, in seconds
vec3 uCursor; // mouse position and state
// uCursor.x goes from -1 to +1
// uCursor.y goes from -1 to +1
// uCursor.z is 1 when mouse down, 0 when mouse up.
顶点着色器
顶点着色器 是您(应用程序员/艺术家)编写的在 GPU 上在每个顶点运行的程序。 它是用特殊用途语言 GLSL 编写的。
右侧是一个非常简单的顶点着色器程序。"属性"是从 CPU 传入的常量值。在这种情况下,它是aPosition,场景中每个顶点的 x、y、z 位置。它是vec3类型,这意味着它由三个 GLSL 浮点数组成。
attribute vec3 aPosition;
varying vec3 vPosition;
void main() {
gl_Position = vec4(aPosition, 1.0);
vPosition = aPosition;
}
顶点着色器最强大的功能之一是设置“varying”变量。然后 GPU 会在使用这个顶点的任何三角形的像素上对这些值进行插值。这个插值值将在每个像素处供片段着色器使用。
例如,“varying”变量vPosition是由这个顶点着色器设置的。按照惯例,变量的名称以字母'v'开头。
这个顶点着色器做了两件事:
-
通过设置
gl_Position,它确定了这个顶点将出现在图像的哪个像素上。 -
它将 varying 变量
vPosition设置为等于此顶点的属性位置aPosition。
片段着色器
片段着色器是您(应用程序员)编写的在每个像素处运行的程序。
因为不同三角形的片段可以在每个像素处可见(例如:当三角形非常小,或者像素在一个三角形的边缘部分遮挡另一个三角形时),通常我们实际上是在定义像素片段的颜色,这就是为什么这些被称为片段着色器。
由于我们的顶点着色器为vPosition设置了一个值,我们编写的任何片段着色器都将能够利用这个变量,其值现在已经被插值到像素级别。
WebGL 指南
有许多关于 WebGL 的在线指南。我发现这个WebGL 基础教程对于新手来说非常清晰易懂。
我还发现这个快速参考卡在编写 WebGL 着色器时非常有用。我认为你会发现最后一页的内置函数列表特别有用。
作业 1
本周的作业是从这个 zip 文件中的代码开始,我们在课堂上讨论过,然后用您自己的迷人、精彩和原创的片段着色器替换 index.html 中的片段着色器。
重要提示: 创造属于你自己的东西。不要只是对我在课堂上所做的进行微小变化。玩得开心。
你的作业要在下周课程开始之前完成。
9 月 16 日课程笔记--射线追踪简介
WebGL 速查表
正如我上周提到的,这里有一个方便的WebGL 紧凑指南。
特别是最后一页对于在 OpenGL ES 中编写着色器非常有用。
请注意,OpenGL ES 片段着色器不允许递归。
伽马校正
显示器使用“伽马曲线”调整人类感知,因为人们可以感知到非常大范围的亮度。
例如,右侧的图像显示了水平轴上的值 0...255,虚拟轴上的实际显示亮度。
不同的显示器有所不同,但这种调整通常大约是 x²
由于我们所有的计算实际上都是在求解实际光子的总和,我们需要在线性亮度下进行所有数学运算,然后在完成时进行伽马校正:
粗略地说:c → sqrt(c)
输出亮度

输入值 0 ... 255
射线追踪:形成一条射线
在每个像素处,从原点 V =(0,0,0)射出一条射线,击中一个位于 z = -f 平面上的虚拟屏幕。
我们将 f 称为这个虚拟相机的“焦距”。
指向任何像素的光线目标是:(x, y, -f),其中-1 ≤ x ≤ +1,-1 ≤ y ≤ +1。
因此,射线方向 W = normalize(vec3(x,y,-f))。
为了在任何像素渲染场景的图像,我们需要跟随该像素处的射线,并查看射线首先遇到的对象(如果有)是什么。
换句话说,在点 V + Wt 处最近的对象,其中 t > 0。

射线追踪:定义一个球体
我们可以用长度为 4 的 GLSL 向量来描述一个球体:
vec4 sphere = vec4(x,y,z,r);其中(x,y,z)是球体的中心,r 是球体的半径。
正如我们在课堂上讨论的那样,GLSL 中的 vec4 的分量可以通过两种方式之一访问:
v.x, v.y, v.z, v.w // when thought of as a 4D point v.r, v.g, v.b, v.a // when thought of as a color + alpha因此,在片段着色器
vec4中访问你的球体半径的值时,你将希望将其称为 sphere.w(而不是 sphere.r)。

射线追踪:找到射线击中球体的位置
D = V - sph.xyz // 从球体中心到射线起点的向量。
这样做减法的目的是,我们实际上在解决射线追踪到原点(0,0,0)处的球体的问题,这个问题要容易得多。
因此,我们不是解决追踪射线 V+Wt 到以 sph.xyz 为中心的球体的更复杂问题,而是解决等效但简单得多的问题,即追踪射线 D+Wt 到以(0,0,0)为中心的球体。
(D + Wt)² = sph.r² //找到沿射线距离球体中心 r 的���。
(D + Wt)² - sph.r² = 0 //需要解决 t。
一般来说,如果 a 和 b 是向量,那么 a • b =(a[x] * b[x] + a[y] * b[y] + a[z] * b[z])
这个“内积”也等于:|a| * |b| * cos(θ)
将这些项相乘,我们需要解决以下二次方程:
(W • W)t² +
2(W • D)t +
(D • D) - r² = 0
由于射线方向 W 是单位长度,因此此方程中的第一项(W • W)只是 1。

光线追踪:找到最近的交点
计算场景中所有球体的交点。
在此像素处可见的球体(如果有的话),是具有最小正 t 值的球体。
光线追踪:计算表面点
一旦我们知道 t 的值,我们只需将其代入射线方程中,即可得到在此射线可见的球体表面点的位置,如下方程和右侧图所示:
S = V + W t

光线追踪:计算表面法线
现在我们需要计算表面法线,以便计算球体如何与光线相互作用,从而在此像素产生最终颜色。
“表面法线”是垂直于球体表面的单位长度向量,即如果你站在表面上的“向上”方向。
对于一个球体,我们可以通过从球体中心减去表面点 S,然后除以球体半径来获得这个值:
N = (S - sph.xyz) / sph.r

光线追踪:一个简单的着色器
无穷远处的简单光源可以定义为 rgb 颜色,以及 xyz 光线方向:
vec3 lightColor; // rgb
vec3 lightDirection; // xyz
一个简单的漫反射表面材质可以由独立于任何特定光源的 rgb“环境”分量和为每个光源添加的 rgb“漫反射”分量来定义:
vec3 ambient; // rgb
vec3 diffuse; // rgb
右侧的图显示了一个漫反射球体,其中每个像素点的位置计算如下:
环境光 + 光源颜色 * 漫反射 * max(0, 法线 • 光线方向)

光线追踪:多光源
如果场景包含多个光源,则可以通过以下方式计算球体上点的像素颜色:
环境光 + ∑[n](光源颜色[n] * 漫反射 * max(0, 法线 • 光线方向[n]))
其中 n 在场景中的光源上进行迭代。
课程结束时我们观看了一个视频:
由 Chris Landreth 执导的获得奥斯卡奖的短片Ryan。
作业
-
在片段着色器中为球体实现简单的光线追踪。
正如我们在课堂上所说,您可以从此代码库开始。
-
实现简单的漫反射阴影。
-
额外加分:多个球体
提示: 您可以创建并使用球体数组,如下所示:
vec4 spheres[3]; // Note that I am explicitly setting the size of the array. ... for (int i = 0 ; i < 3 ; i++) { // and also the size of the loop. ... spheres[i] ... }如果尝试这个额外加分,请确保在每个像素处选择具有最小 t 值的球体。
-
额外加分:多个光源
-
额外加分:程序纹理
-
制作一些酷炫有趣的东西,尝试创建一些交互式的(使用 uCursor)和/或动画的(使用 uTime)。
9 月 23 日课堂笔记--基于程序化噪声的纹理
以下是我们在课堂上查看的一些链接:
我们观看了视频Carlitopolis
作业
-
如果你觉得上周的作业中的光线追踪器还没有达到你的满意程度,可以继续完善。
-
下周三,9 月 30 日课前作业,请将以下代码添加到你的片段着色器中,然后使用 noise()、fractal()和/或 turbulence()函数创建有趣和酷炫的程序化纹理。
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); } vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } vec3 fade(vec3 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); } float noise(vec3 P) { vec3 i0 = mod289(floor(P)), i1 = mod289(i0 + vec3(1.0)); vec3 f0 = fract(P), f1 = f0 - vec3(1.0), f = fade(f0); vec4 ix = vec4(i0.x, i1.x, i0.x, i1.x), iy = vec4(i0.yy, i1.yy); vec4 iz0 = i0.zzzz, iz1 = i1.zzzz; vec4 ixy = permute(permute(ix) + iy), ixy0 = permute(ixy + iz0), ixy1 = permute(ixy + iz1); vec4 gx0 = ixy0 * (1.0 / 7.0), gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; vec4 gx1 = ixy1 * (1.0 / 7.0), gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; gx0 = fract(gx0); gx1 = fract(gx1); vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0), sz0 = step(gz0, vec4(0.0)); vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1), sz1 = step(gz1, vec4(0.0)); gx0 -= sz0 * (step(0.0, gx0) - 0.5); gy0 -= sz0 * (step(0.0, gy0) - 0.5); gx1 -= sz1 * (step(0.0, gx1) - 0.5); gy1 -= sz1 * (step(0.0, gy1) - 0.5); vec3 g0 = vec3(gx0.x,gy0.x,gz0.x), g1 = vec3(gx0.y,gy0.y,gz0.y), g2 = vec3(gx0.z,gy0.z,gz0.z), g3 = vec3(gx0.w,gy0.w,gz0.w), g4 = vec3(gx1.x,gy1.x,gz1.x), g5 = vec3(gx1.y,gy1.y,gz1.y), g6 = vec3(gx1.z,gy1.z,gz1.z), g7 = vec3(gx1.w,gy1.w,gz1.w); vec4 norm0 = taylorInvSqrt(vec4(dot(g0,g0), dot(g2,g2), dot(g1,g1), dot(g3,g3))); vec4 norm1 = taylorInvSqrt(vec4(dot(g4,g4), dot(g6,g6), dot(g5,g5), dot(g7,g7))); g0 *= norm0.x; g2 *= norm0.y; g1 *= norm0.z; g3 *= norm0.w; g4 *= norm1.x; g6 *= norm1.y; g5 *= norm1.z; g7 *= norm1.w; vec4 nz = mix(vec4(dot(g0, vec3(f0.x, f0.y, f0.z)), dot(g1, vec3(f1.x, f0.y, f0.z)), dot(g2, vec3(f0.x, f1.y, f0.z)), dot(g3, vec3(f1.x, f1.y, f0.z))), vec4(dot(g4, vec3(f0.x, f0.y, f1.z)), dot(g5, vec3(f1.x, f0.y, f1.z)), dot(g6, vec3(f0.x, f1.y, f1.z)), dot(g7, vec3(f1.x, f1.y, f1.z))), f.z); return 2.2 * mix(mix(nz.x,nz.z,f.y), mix(nz.y,nz.w,f.y), f.x); } float noise(vec2 P) { return noise(vec3(P, 0.0)); } float fractal(vec3 P) { float f = 0., s = 1.; for (int i = 0 ; i < 9 ; i++) { f += noise(s * P) / s; s *= 2.; P = vec3(.866 * P.x + .5 * P.z, P.y + 100., -.5 * P.x + .866 * P.z); } return f; } float turbulence(vec3 P) { float f = 0., s = 1.; for (int i = 0 ; i < 9 ; i++) { f += abs(noise(s * P)) / s; s *= 2.; P = vec3(.866 * P.x + .5 * P.z, P.y + 100., -.5 * P.x + .866 * P.z); } return f; }
9 月 30 日课堂笔记 -- 更多光线追踪
Phong 镜面反射模型
表面反射的第一个真正有趣的模型是由 Bui-Tong Phong 在 1973 年开发的。在此之前,计算机图形学表面仅使用漫反射兰伯特反射来渲染。Phong 的模型是第一个考虑到镜面高光的模型。
Phong 模型首先通过定义一个反射向量 R 来开始,该向量是光源方向 L 关于表面法线 N 的反射。
正如我们在课堂上展示的,并且从右边的图表中可以看到的那样,它由以下公式给出:
R = 2 (N • L) N - L

一旦 R 被定义,那么 Phong 模型近似表面反射的 镜面 组件为:
s[rgb] max(0, E • R)^p )
其中 s[rgb] 是镜面反射的颜色,p 是镜面功率,E 是指向眼睛的方向(在我们的情况下,E = -W,即光线方向的反向)。镜面功率 p 越大,表面看起来就越 "光滑"。
要得到完整的 Phong 反射,我们对场景中的光源求和:
a[rgb] + ∑[i] lightColor[i] ( d[rgb] max(0, N • L[i]) + s[rgb] max(0, E • R) ^p )
a[rgb]、d[rgb] 和 s[rgb] 分别表示环境光、漫反射和镜面反射颜色,p 是镜面功率。

Blinn 镜面反射模型
几年后,吉姆·布林创建了 Phong 模型的一个变体,以在极端情况下(当观察者从远离表面法线的角度观看时)产生更逼真的高光。
在我们通过在整个物体上近似 E 为一个常矢量并且假设光源方向 L 在整个物体上都是恒定的特殊情况下,Blinn 模型也相对较快。
基本思想是中间向量 H 被定义为从光源方向 L 到眼睛方向 E 的中点。如果 E 和 L 是恒定的,这只需要对每个光源执行一次。
然后在每个像素点,我们只需计算 N 和 H 的内积:
s[rgb] max(0, N • H)^p
因为 H 倾向于保持相对接近 N,所以为了获得相同水平的镜面反射,Blinn 模型中的功率 p 需要比 Phong 模型中的对应值大大约三倍。

阴影
在光线追踪中,投射阴影相对容易。一旦我们找到表面点 S,然后对于每个光源,我们发射另一条光线,其起点 V' 就是表面点 S,方向 W' 就是指向那个光源 L[i] 的方向。
我们要确保光线未击中我们起始的对象,所以我们将新光线的起点 V' 稍微向表面朝向光源的方向移动。因此,我们的 "阴影光线" 将是:
V' = S + ε L[i]
W' = L[i]
其中 ε 可以是任何微小的正值,例如 0.001。
如果此阴影射线遇到任何其他物体,则此像素处的表面处于阴影中,我们不会添加表面反射的漫反射和镜面反射分量。
右上方是一个表面不在阴影中的示例。就在下方,是一个表面在阴影中的示例,因为它的光线路径被另一个物体挡住了。


反射
光线追踪的另一个伟大之处在于,我们可以从摄像机向后继续跟踪光线的路径,以模拟镜面反射的行为。我们采用了计算菲涅尔反射模型的反射方向 R 的技术,但在该方程中用-W(沿着入射光线的反方向)替换了 L:
W' = 2 (N • (-W)) N - (-W)
我们可以计算出一个新的射线,它从表面点 S 开始,朝着那个反射方向前进:
V' = S + ε W'
如右图所示,我们希望将射线的起点偏移一点出表面,这样射线就不会意外地遇到物体本身。
由此射线计算出的任何颜色,我们都将其混合到菲涅尔反射算法的结果中。结果就是具有镜面光泽的阴影表面的外观。

布尔交集
光线追踪的另一个好处是,我们可以用它来进行布尔建模,正如我们在课堂上讨论的那样。例如,给定两个球体 A 和 B,我们可以通过沿着射线计算它们的交点(如果有)来计算这些球体的交集。
假设,沿着给定的射线,进入和退出球体 A 的值分别为 A[in]和 A[out]。
还假设沿着这条射线,进入和退出球体 B 时的值分别为 B[in]和 B[out]。
然后,两个形状沿射线的交点由进入(in)值的最大值和退出(out)值的最小值给出:
进入交点形状的 t[in] = max(A[in],B[in])
从交点形状退出的 t[out] = min(A[out],B[out])
如果 t[in] < t[out],那么射线就与交点形状相交。否则,射线就未与交点形状相交。
要在像素处着色所得的交点形状,我们需要使用实际被射线击中的表面的法线。例如,如果 A[in] > B[in],则我们需要使用 A 的表面法线。

折射
在现实世界中,许多材料,如油、水、塑料、玻璃和钻石,都是透明的。透明材料具有折射率,它衡量了光线进入介质时减速的程度。例如,水的折射率约为 1.333,玻璃的折射率约为 1.5。钻石的折射率是已知折射率中最高的物质,为 2.42。
如右侧图中所示,您可以通过遵循斯涅尔定律为您的光线追踪添加折射:
n1 / n2 = sin(θ2) / sin(θ1)
以确定光线进入或退出透明物体时应该弯曲多少。
请注意,您需要更改您的光线追踪模型以包含折射。除了您最初的入射光线和任何阴影或反射光线外,您还需要添加一个折射光线,该光线从表面内部开始,并向内延伸。
请注意,如果您已经对一个球体进行了光线追踪,并且现在正在计算折射光线将从该球体退出的位置,您将需要计算二次方程的第二根。
然后,在这个折射光线从透明球体的背面退出后,您将需要计算它在出射时折射多少,并从那里向球体后面的场景发射一道光线。
一般来说,您可以通过混合返回的颜色与由纯反射或 Blinn/Phong 反射计算出的表面颜色来使用折射的结果。

作业(截止日期为 10 月 8 日星期三课前)
-
实现 Phong 或 Blinn 反射模型之一。
-
实现阴影。
-
实现两个球体之间的布尔交集。
-
额外加分:
- 实现 Phong 和 Blinn 反射模型。
- 实现反射。
- 实现折射。
- 实现两个以上球体之间的布尔交集。
-
一如既往,制作一些酷炫有趣的东西,尝试创建一些交互式的(使用 uCursor)和/或动画的(使用 uTime)东西。
10 月 7 日课程笔记 -- 矩阵简介
齐次坐标
我们可以通过添加额外的坐标来处理场景中的点和方向(本质上是无穷远处的点),我们称之为齐次坐标。
例如,在二维中,我们会写成 [x,y,w] 来表示 (x/w, y/w)。
在右侧的示例中,我们既有点也有方向。点 (1,0) 由 [1,0,1] 表示,而方向向量 (1,0)(红色显示)由 [1,0,0] 表示。
按照约定,我们将所有点放在 w=1 平面上(灰色显示),尽管任意缩放到 [cx,cy,cz] 仍描述相同的点 (x/w, y/w),如图中向上倾斜的箭头所示。

坐标变换
三维空间中的默认坐标系具有坐标轴 [1,0,0,0]、[0,1,0,0] 和 [0,0,1,0](分别为 x、y 和 z 全局方向向量)。
其原点是 [0,0,0,1](点 (0,0,0)),如右侧图中黑色所示。
我们可以通过重新定义每个 x、y 和 z 轴,并将原点平移到点 t,来描述一个转换后的坐标系,如右侧图中蓝色所示。
请注意,这是一个非常通用的表示。例如,新的 x、y 和 z 方向不需要彼此垂直。它们也不需要单位长度。对于

变换矩阵
所有坐标变换的信息都可以放在一个 4×4 矩阵中,如右侧图所示。
x、y 和 z 轴形成矩阵的前三列。
原点 t 形成矩阵的最右侧列。
在这个课程中,我们将遵循将 16 个矩阵值按行主序存储的约定。
[ x[0], x[1], x[2], x[3], y[0], y[1], y[2], y[3], z[0], z[1], z[2], z[3], t[0], t[1], t[2], t[3] ]

转换点
我们可以使用 4×4 矩阵来变换一个向量。
按照约定,我们将输入表示为列向量,并将其放在矩阵的右侧。
同样,按照约定,如果我们省略输入的第四个(齐次)坐标,我们假定其齐次坐标的值为 1.0,除非另有说明。
变换的结果是另一个列向量,我们通过将矩阵的每一行与输入向量进行内积来获得。
在这种情况下,矩阵正在围绕 z 轴旋转点 (1,0,0)。

单位变换
单位矩阵是“不做任何事情”的变换。
它将任何点或方向转换为其自身。通常,您希望在矩阵对象上调用 identity() 方法来初始化该矩阵。

平移变换
要平移一个点,我们只使用矩阵的最右列。矩阵的其余部分与单位矩阵相同。
请注意,平移只影响点,不影响方向。因为方向的齐次坐标为零,其值不会受到矩阵最右列的影响。

围绕 x 轴旋转
围绕 x 轴旋转只影响 y 和 z 轴。
从正 x 方向看,"正" 旋转是逆时针的。

围绕 y 轴旋转
围绕 y 轴旋转只影响 z 和 x 轴。
从正 y 方向看,"正" 旋转是逆时针的。

围绕 z 轴旋转
围绕 z 轴旋转只影响 x 和 y 轴。
从正 z 方向看,"正" 旋转是逆时针的。

缩放变换
像旋转一样,缩放变换(使形状变大或变小)只使用 4#215;4 矩阵的左上 3×3 部分。
在这种情况下,我们通过在矩阵的对角线上使用相同的值来执行均匀缩放。
如果我们在这三个位置使用不同的值,那么我们将执行非均匀缩放,这将导致形状被挤压或拉伸。

对于本周的任务,我们将定义一个形状为:
-
一组顶点
-
一组边缘
示例:一个正方形
-
顶点:
作为一个数组的数组:
- [-1,-1, 0],
- [ 1,-1, 0],
- [-1, 1, 0],
- [ 1, 1, 0]
或作为对象数组(更灵活):
- new Vector3(-1,-1,0);
- new Vector3( 1,-1,0);
- new Vector3(-1, 1,0);
- new Vector3( 1, 1,0);
-
边缘(一对顶点索引的数组):
[0,1], [1,3], [3,2], [2,0]
示例:一个立方体
-
顶点:
- new Vector3(-1,-1,-1);
- new Vector3( 1,-1,-1);
- new Vector3(-1, 1,-1);
- new Vector3( 1, 1,-1);
- new Vector3(-1,-1, 1);
- new Vector3( 1,-1, 1);
- new Vector3(-1, 1, 1);
- new Vector3( 1, 1, 1);
-
边缘:
留给读者作为练习。☺
视口变换:
你将需要在"模型空间"中进行所有的 3D 建模和矩阵运算:
值在 x 和 y 方向上从 -1 到 +1。
但你需要在"图像空间"中以像素形式绘制:
值从 0 到 canvas.width 在 x 轴上(从左到右)。
值在 y 轴上从 0 到 canvas.height(从上到下)。
从 3D 建模空间转换到图像空间中的像素:
px = (width / 2) + x * (width / 2); py = (height / 2) - y * (width / 2);
HTML5 画布对象:
对于这个任务,你只需要了解以下几个 Canvas 方法:
beginPath(), moveTo(x,y), lineTo(x,y), stroke()但你可能对探索 Canvas 对象的全部功能感兴趣。
通过点击此链接,你可以找到一个关于在 HTML5 Canvas 上绘制可用方法的 全面参考。
我们在课堂上开始的示例:
以下两个交互式图表都使用了我的小便利库 drawlib1.js。
要了解图表本身是如何实现的,你可以查看此页面的 Javascript 源代码:
在 Chrome 中:视图 → 开发者 → 查看源代码
在 Firefox 中:工具 → 网页开发者 → 页面源代码
在 Safari 中:开发 → 显示页面源代码
例如,在这种情况下,我展示了如何使用 cursor.x 和 cursor.y 定位一个正方形,同时使用 cursor.z(鼠标按钮是否被按下的指示器)来改变正方形的颜色。
例如,在这里我展示了如何使用变量 time 来制作动画。
上述示例也可以通过这个 zip 文件 获得。
作业(截止日期为 10 月 14 日星期三课前)
在 Javascript 中创建一个矩阵对象类。对于该类,实现以下方法:
identity() translate(x, y, z) rotateX(theta) rotateY(theta) rotateZ(theta) scale(x, y, z) transform(src, dst) // arguments are vectors通过创建一些酷炫形状或一组形状的顶点和边缘,并在每个动画帧上对它们应用矩阵变换,通过在 Canvas 上绘制来显示结果,以创建一个动画图解,来展示你已经做到了这些。
如果你创建了一些真正美丽或有趣的交互式内容,也许讲述一个故事或形成一个有趣的游戏或谜题,你将获得额外的学分。
10 月 21 日课堂笔记 -- 参数化曲面
参数化圆柱
你可以用两个参数 0 ≤ u ≤ 1 和 0 ≤ v ≤ 1 来参数化描述许多曲面,从而定义曲面上的 x、y 和 z 的值。
例如,右侧的开放圆柱截面描述如下:
y = cos(θ)
x = sin(θ)
z = 2 * v - 1
其中:
θ = 2 π u

参数化球体
同样地,右侧球体的经度/纬度参数化描述如下:
x = cos(φ) * cos(θ)
y = cos(φ) * sin(θ)
z = sin(φ)
其中:
θ = 2 π u
φ = π v - π / 2

参数化环面(甜甜圈)
环面的经度/纬度参数化描述如下:
x = (1 + r * cos(φ)) * cos(θ)
y = (1 + r * cos(φ)) * sin(θ)
z = r * sin(φ)
其中:
θ = 2 π u
φ = 2 π v
r = "内管"的半径。

作业,截止日期为 10 月 28 日星期三课堂开始前
实现参数化圆柱,球体和环面。
利用这些形状,以及你已经掌握的矩阵知识,创建一个带有一些有趣对象的场景(例如:人物,动物,机器等)。
看看你是否可以使你的场景动态化,使用时间变量,并且对鼠标的响应。
额外加分: 修改圆柱方程,使得结果曲面还包括圆柱的顶部和底部面。
额外加分: 尝试想出如何创建其他有趣的参数化形状,比如锥体,曲线管,或者像瓶子一样的形状。
额外加分: 尝试使用这个噪声函数的 Javascript 实现来变化或改变形状和/或动画。
像往常一样,如果制作出有趣、令人兴奋、美丽或独特的作品,你将获得额外的分数。
10 月 28 日课程笔记 -- 样条介绍
样条
在计算机图形学中,我们可能有很多原因要创建平滑可控的曲线。也许我们想创建一个有机形状,或者沿着连续路径动画某物。
如右侧所示,我们可以通过将我们的光滑曲线分解为更简单的片段来实现这一点。
如果我们将样条曲线看作是运动路径,那么每个片段都必须与其邻居匹配,无论是位置还是速率,这意味着对于每个坐标 x 和 y,我们需要四个值:开始和结束时的位置,以及开始和结束时的速率(或导数)。
可以满足四个约束条件的最低阶多项式是立方多项式。因此,我们用参数 t 中的参数立方多项式来描述每个片段的 x 和 y 坐标:
x = a[x]t³ + b[x]t² + c[x]t + d[x]
y = a[y]t³ + b[y]t² + c[y]t + d[y]
其中 (a[x]、b[x]、c[x]、d[x]) 和 (a[y]、b[y]、c[y]、d[y]) 是常值多项式系数,t 在曲线上沿着 t = 0 到 t = 1 变化。

立方样条
尽管从技术上讲,可以通过调整它们的多项式系数来设计立方样条,但实际上这通常不太有效。
如右侧示例所示,立方多项式的形状与其四个系数的值之间没有直观的联系 -- 在这种情况下分别为 7.7、-11.7、5.0 和 -0.6,分别对应 t³、t²、t 和常数项。
出于这个原因,我们需要一种更好的方式来指定立方样条曲线。与其使用 t³、t²、t 和 1 作为我们的四个基础函数,不如使用四个具有更直观几何意义的不同基础函数。
在接下来的几节中,我们将看到两个不同的示例,展示这种替代基函数的用法。

Hermite 样条,第 1 部分
我们可以选择四个基函数,使我们能够独立控制在 t = 0 和 t = 1 时的位置,以及在 t = 0 和 t = 1 时的变化率。这被称为Hermite基础,以法国数学家的名字命名,他设计了它。
如果我们希望在 t = 0 时位置为 A,在 t = 1 时位置为 B,在 t = 0 时率为 C,在 t = 1 时率为 D,我们可以使用右侧的四个函数来计算我们正在寻找的立方多项式。
位置

2t³ - 3t² + 1

-2t³ + 3t²
变化率

t³ - 2t² + t

t³ - t²
Hermite 样条,第 2 部分
由于这四个 Hermite 基多项式永远不会改变,而我们想要的立方多项式只是这四个多项式的加权和,因此我们可以将这个加权和表示为权重乘以一个矩阵的乘积,我们称之为Hermite 矩阵。
换句话说,表达式:
A (2t³ - 3t² + 1) + B (-2t³ + 3t²) + C (t³ - 3t² + t) + D (t³ - t²)
可以表示为矩阵向量乘积:
a
b
c
d
=
A
B
C
D
将两端的位置和速率转换为所需的立方多项式:
at³ + bt² + ct + d.
贝塞尔样条,第 1 部分
艺术家和设计师通常发现通过移动点来创建样条比处理导数更方便。贝塞尔样条使得样条曲线的设计者可以这样工作。
贝塞尔样条通过重复的线性插值来工作。例如,右侧的图像显示了贝塞尔样条的简化版本,使用三个关键点来指定一个参数化二次样条。
请注意,曲线上的点是通过线性插值线性插值得到的。我们首先通过线性插值找到边界 AB 和 BC 上的点(以蓝点表示的点):
(1-t) A + t B
(1-t) B + t C
然后我们再次进行插值(以红点表示的点):
P = (1-t) ( (1-t) A + t B ) + t ( (1-t) B + t C )
如果我们将所有项相乘,我们得到:
A (1-t)² + 2 B (1-t) t + C t²
注意,系数的权重(1 2 1)遵循帕斯卡三角形。
贝塞尔样条,第 2 部分
现在更容易看出使用四个关键点的完整参数化立方贝塞尔样条的情况是什么:基本设置是线性插值的线性插值的线性插值。
所以我们从蓝色的点开始:
P = (1-t) A + t B
Q = (1-t) B + t C
R = (1-t) C + t D
当第一和第二项进行线性插值时,我们得到两个紫色的点:
S = (1-t) P + t Q
T = (1-t) Q + t R
最后我们线性插值这两点:(1-t) S + t T
当我们将方程写成关于我们原来的四个关键点 A、B、C 和 D 的形式时,权重形成了帕斯卡三角形的下一级(1 3 3 1):
A (1-t)³ + 3 B (1-t)² t + 3 C (1-t) t² + D t³
贝塞尔样条,第 3 部分
我们可以将上述多项式的项相乘,并按 t 的幂重新分组,得到:
(-A + 3B - 3C + D) t³ + (3A - 6B + 3C) t² + (-3A + 3B) t + D
这使得很容易看出,就像埃尔米特样条的情况一样,贝塞尔样条具有一个特征矩阵,可以用来将上述多项式平移到标准的立方多项式,其中系数为 (a,b,c,d):
a
b
c
d
=
A
B
C
D
贝塞尔样条的一个强大特性是,A 和 B 之间的方向决定了 t=0 时样条曲线的方向,而 C 和 D 之间的方向决定了 t=1 时样条曲线的方向。
这使得将样条端对端匹配起来变得很容易,从而得到的复合曲线具有连续的导数。
透视
正如我们在课堂上提到的,您可以通过在执行视口变换之前执行以下操作来实现透视:
- 将所有顶点平移至(0,0,-f),使得您的场景位于 z=-f 处,其中 f 是虚拟摄像机的“焦距”。
- 对每个顶点应用以下变换:
x → fx/z
y → fy/z
z → f/z
正如我在课堂上提到的,我们知道上述第 2 步是一个线性变换,因为它等价于以下矩阵变换:
1 0 0 0 0 1 0 0 0 0 0 1 0 0 1/f 0
对象层次结构
本周的作业中,我不会要求您对对象层次结构做任何事情,但我希望您了解它。基本上,我们可以将整个可渲染场景描述为对象树,每个对象具有以下结构:
Object3D Geometry vertices edges or faces Material Matrix Object3D children[...]作业,截止日期为 11 月 4 日星期三上课前
通过使用时间变化的样条曲线对您已经制作的形状进行动画处理,同时使用 Hermite 和 Bezier 技术。您可以使用样条曲线的值来输入到您已经实现的平移、旋转和缩放基元的参数中。
使用基于样条曲线的动画软件制作有趣和引人入胜的动画。进行透视。
创建您自己的曲线编辑器,以创建基于 Hermite 或 Bezier 的曲线。您的编辑器应允许用户添加、移动和删除关键点,并应允许用户指定两个相邻样条曲线是否具有匹配的导数。
使用您的编辑器创建有趣的形状,例如动物轮廓,字体字母,或者您认为很酷和有趣的东西。
11 月 4 日课堂笔记--WebGL 简介
WebGL 的简单示例
如果你解压这个文件,并在浏览器中启动其 index.html 文件,你会看到我在课堂上展示的 webgl 示例的进展。
这个例子没有相关的作业。我真的只是希望你学习它,这样你就能为我们下周要讲的内容做好准备。
使用地垫 VR 控制器
任何想要研究我在课堂最后描述的 VR 地垫导航界面的人,请给我发送电子邮件。
这是我们正在努力做的事情的大致概念。你可能对额外的应用程序有一些很酷的想法:
作业,截止到 11 月 11 日星期三课堂开始前
这是一个“赶上进度”的周。如果你的作业还没有完成,请专注于在下周课程开始前赶上进度。
如果你对材料有任何问题,请给我发送电子邮件,我会帮助你解决。
- 11 月 11 日课堂笔记 -- 更多关于 WebGL 的内容
-
在课堂上,我们讨论了 WebGL 的更高级功能。但是在即将到来的星期三,我们会保持简单。从我上周发布的示例开始,到 11 月 18 日星期三截止的作业中,我希望你创建非平凡的 3D 形状,使用三角带。你应该至少制作以下三种形状:球体、圆柱体管和环面(甜甜圈)。
-
如果你想变得更加雄心勃勃并创建更有趣的形状(例如:看看你是否可以利用样条曲线),请随意这样做。在未来几周,这些更高级的形状将会派上用场。
-
此外,如果你有雄心壮志,你可以开始向你的顶点添加法线,将顶点作为六个值的组进行传递,而不是三个值。
-
要使其起作用(如我们在课堂上所描述的),你需要替换以下行:
var attr = gl.getAttribLocation(prog, 'aPos');
gl.enableVertexAttribArray(attr);
gl.vertexAttribPointer(attr, 3, gl.FLOAT, false, 0, 0);
- 类似于以下内容:
var posAttr = gl.getAttribLocation(prog, 'aPos');
var normalAttr = gl.getAttribLocation(prog, 'aNormal');
gl.enableVertexAttribArray(posAttr);
gl.enableVertexAttribArray(normalAttr);
var bpe = Float32Array.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(posAttr , 3, gl.FLOAT, false, 6 * bpe, 0);
gl.vertexAttribPointer(normalAttr, 3, gl.FLOAT, false, 6 * bpe, 3 * bpe);
-
还要在你的顶点着色器中添加
vec3 aNormal的声明。 -
如果你感到非常雄心勃勃,你可以尝试将这些普通值传递给你的片段着色器,并使用它们来着色你的 3D 形状:
***// in vertex shader***
attribute vec3 aNormal;
varying vec3 vNormal;
main() {
vNormal = aNormal;
...
}
...
***// in fragment shader***
vec3 vNormal;
main() {
vec3 normal = normalize(vNormal);
...
}
12 月 2 日课堂笔记 -- 使用简单的建模/动画软件包
开始使用更高级别的软件包
本周我们开始使用一个基于 HTML5 的简单建模和动画软件包。这几乎是我将用于 Chalktalk 的相同软件包。
我编写这个软件包并非为了完整和全面,而是为了易于理解。如果你感到特别勇敢或受到启发,可以随意对其进行更改、添加或其他自定义。
作业,截止日期为 12 月 9 日星期三课堂开始前
从现在开始到本学期结束,你将使用这个软件包来“做你自己的事情”。例如,你可能想编写自己定制的顶点和片段着色器,或者通过使用自己的样条软件包创建复杂的有机形状,或者创建分层机制,比如机械钟表或汽车发动机或发条玩具。你可能想尝试创建动画角色,无论是类似人类的还是奇幻生物,或者整个建筑环境。你可以朝着许多方向发展。
在最后两节课(12 月 9 日和 12 月 16 日),你将有机会展示你正在构建的内容,并在课堂上得到我和其他学生的反馈。我非常鼓励你利用这个机会。
你的最终项目需要在三周后完成,具体来说是在 12 月 23 日星期三晚上之前。





A
浙公网安备 33010602011771号