从真实生活的3D绘画开始
我们如何在真实生活中绘制一个3D物体
我们在真实生活中是如何绘制3D物体的呢,比如绘制一个四面体。
首先我们得知道四面体的几何特性,它有4个顶点,假设我们要绘制的是一个等边的四面体,那么底面就是一个等边三角形,如下图蓝色顶点的三角形所示,第四个顶点从顶部看位于等边三角形的中心,并且与其他三个顶点的距离和底边三角形边长相同,如下图红色顶点所示。
然后我们选择一个视角,利用一些透视的技法画出其在纸上的平面框图
最后我们给每个面涂上颜色,被遮挡的面实际上并不会被涂色,比如底面。但是如果你要画一个透明的四面体,那么被遮挡的面的颜色就要被考虑到了。
使用计算机能够理解的方式来描述绘制过程
接下来我们对上述的过程做一些细化和抽象,变得更容易被计算机理解。
这里我们将上色的方式设定为一个点一个点的上色,类似于打印机,大概如下图所示,我们以最小的网格为上色单位,根据网格所在的面涂不同的颜色
你也可以将这些上色的最小点理解为像素点,对于电子显示屏幕而言,最小的上色单位就是一个像素点。
然后我们将步骤细化为计算机可以理解的方式
1. 计算几何体顶点集合,通过构建一个坐标系,根据等边三角形的特性,可以计算出各个点的坐标。
这一步我们可以得到四个顶点,用Vertex前缀来表示
Vertex1: (x1, y1, z1)
Vertex2: (x2, y2, z2)
Vertex3: (x3, y3, z3)
Vertex4: (x4, y4, z4)
2. 对四个顶点进行移动,透视等变换操作,得到在画布上的映射点坐标,这里我们假设一个公式F(x)可以做到这样的计算。进过这一步,我们可以得到画布坐标系上的四个坐标点,用Point前缀来表示
Point1: (x1, y1) = F(Vertex1)
Point2: (x2, y2) = F(Vertex2)
Point3: (x3, y3) = F(Vertex3)
Point4: (x4, y4) = F(Vertex4)
3. 画布上每三个点组成一个三角形,一共可以得到四个三角形,如下图四个蓝色三角形所示
在这一步,我们得到4个三角形,用Triangle前缀来表示
Triangle1: (Point1, Point2, Point3)
Triangle2: (Point1, Point2, Point4)
Triangle3: (Point1, Point3, Point4)
Triangle4: (Point2, Point3, Point4)
4. 使用网格对每个三角形进行分割,得到最小的上色点并上色
这一步我们分割每个三角形,为每个被三角形覆盖的点上色,如果一个上色点对应多个三角形,使用最上面三角形颜色进行上色。上图中网格间隔比较大,如果你严格按照三角形所占用的网格上色,会发现绘制出来的图形边缘呈阶梯状,这也就是我们常说的边缘锯齿问题。
使用计算机的语言来描述绘制过程
最后我们用伪代码来表示一下这个过程,如果你觉得伪代码会让你的思路更加混乱,你可以忽略他们,等到真正的代码学习阶段再了解这些。
// ================== 使用的数据结构 ==================
// 几何体顶点
Vertex {
float x;
float y;
float z;
}
// 画布上的点
Point {
float x;
float y;
}
// 可上色点
Pixel {
float x;
float y;
}
// 三角形
Triangle {
Vertex vertex1;
Vertex vertex2;
Vertex vertex3;
}
// 颜色
Color {
float r,
float g,
float b
}
// ================== 定义的方法 ==================
// 此方法将顶点Vertex变换成画布上的点Point
Point perspectiveTransform(Vertex vertex);
// 获得三角形所有可上色点
List<Pixel> pixelsFromTriangle(Triangle triangle);
// 绘制单个上色点
func drawPixel(Pixel pixel, Color color);
// ================== 绘制流程 ==================
// 原始的顶点信息
List<Vertex> vertices = [vertex1, vertex2, vertex3, vertex4];
// 顶点变换到画布上
List<Point> pointsOnCanvas = [perspectiveTransform(vertex1),perspectiveTransform(vertex2),perspectiveTransform(vertex3),perspectiveTransform(vertex4)];
// 生成4个三角形
Triangle triangle1 = Triangle(pointsOnCanvas[0], pointsOnCanvas[1], pointsOnCanvas[2]);
Triangle triangle2 = Triangle(pointsOnCanvas[0], pointsOnCanvas[1], pointsOnCanvas[3]);
Triangle triangle3 = Triangle(pointsOnCanvas[0], pointsOnCanvas[2], pointsOnCanvas[3]);
Triangle triangle4 = Triangle(pointsOnCanvas[1], pointsOnCanvas[2], pointsOnCanvas[3]);
// 获得4个三角形可以上色的点
List<Pixel> pixels = [];
pixels.addMany(pixelsFromTriangle(triangle1));
pixels.addMany(pixelsFromTriangle(triangle2));
pixels.addMany(pixelsFromTriangle(triangle3));
pixels.addMany(pixelsFromTriangle(triangle4));
// 给每个上色点上色
for (let pixel in pixels) {
// 如果上色点在最顶上,才上色
if (pixel.isTop()) {
// 用上色点所在三角形的颜色进行上色
drawPixel(pixel, colorForTriangle);
}
}
看完后你可能会有很多疑问,比如perspectiveTransform
应该怎么实现,如何获得一个三角形所包含的可上色点,上色点是否位于最上层又是怎么界定的。如果你以2D绘图的思路去思考,可能会发现这些问题都很难以高效的方式去解决。不过这些正是WebGL技术发挥作用的地方,在下一小节中将会简明的阐述WebGL是如何解决这些问题的。