[从零构建光栅渲染器] 5.移动摄像机

[从零构建光栅渲染器] 5.移动摄像机

非常感谢和推荐Sokolov的教程,Sokolov使用500行C++代码实现一个光栅渲染器。教程学习过程非常平滑,从画点、线和三角形开始教学,在逐步深入三维变换,投影,再到顶点着色器,片段着色器等等。教程地址:https://github.com/ssloy/tinyrenderer。Sokolov的教程为英文,我翻译了其文章。

在学习过程中,有些内容可能您可能云里雾里,这时就需要查阅《计算机图形学》的书籍了,这里面的算法和公式可以帮助您理解代码。

作者:尹豆(憨豆酒),联系我yindou97@163.com,熟悉图形学,图像处理领域,本章代码: https://github.com/douysu/computer-graphics-notes

本章运行结果

图片

几何当中最后一个重要的点

今天我们将要完成我比较喜欢的额部分,但是一部分读者可能会感到无聊。一旦你掌握了今天的资料,你可以转移到下一节课,今天我们要完成的是我很喜欢的部分,但很多读者觉得很无聊。一旦你掌握了今天的材料,你可以转移到下一节课,在那里我们将实际做渲染。为了让你眼前一亮,这里是我们已经知道的头,使用Gouraud着色。

图片

我把所有的纹理都去掉了。Gouraud的着色非常简单。我们的3D设计者给了我们模型的每个顶点的法线向量,它们可以在.obj文件的 "vn x y z "行中找到。我们计算出每个顶点的强度(而不是像以前的平面着色那样每个三角形的强度),然后简单地在每个三角形内插值,就像我们已经做了z或uv坐标一样。

顺便说一下,在3D艺术家不是那么好心的情况下,你可以重新计算法向量作为与点相关的所有面的平均向量。前我用来生成这个图像的代码可以在这里找到。here.

三维空间中的基准坐标系变换

在欧几里得空间中,坐标可以由一个点(原点)和一个基点给出。点P在坐标系(O,i,j,k)中的坐标是(x,y,z)意味着什么?意味着,向量OP使用如下表示:

图片

现在图片中有了另一个坐标系(O’, i’,j’,k’)。我们应该怎样从一个坐标系转换到另一个坐标系呢。首先我们需要明白(i,j,k) and (i’,j’,k’) 都是3D的基准坐标系,这里存在一个(非退化)矩阵M可以完成这个操作:

图片

让我们绘制一下插图:

图片

翻译作者内容:坐标系变换。

让我重新整理一下向量OP:

图片

翻译作者内容:这个公式这么理解,向量OO’这里的O’就相当于下面公式中的P点,代入即可

图片

现在,让我们用基矩阵替换右边的(i’,j’,k’),如下:

图片

这就给了我们转换公式,从一个基准坐标系到另一个基准坐标系:

图片

让我们创建我们的视角矩阵gluLookAt

OpenGL,因此,我们的小渲染器只能用位于Z轴上的摄像头来绘制场景。如果我们想移动摄像头,没有问题,我们可以移动所有的场景,让摄像头不动。

让我们这么说吧:我们要画一个位于e点(眼睛)的摄像头的场景,摄像头应该对准c点(中心),这样,给定的向量u(向上)在最终的渲染中是垂直的。

说明图:

图片

这意味着我们要在坐标系(c,x’,y’,z’)中进行渲染。但是,我们的模型是在坐标系(O,x,y,z)中给出的… 没有问题,我们需要的是计算坐标的变换。下面是一个计算必要的4x4矩阵ModelView的C++代码。

void lookat(Vec3f eye, Vec3f center, Vec3f up) {
    Vec3f z = (eye-center).normalize();
    Vec3f x = cross(up,z).normalize();
    Vec3f y = cross(z,x).normalize();
    Matrix Minv = Matrix::identity();
    Matrix Tr   = Matrix::identity();
    for (int i=0; i<3; i++) {
        Minv[0][i] = x[i];
        Minv[1][i] = y[i];
        Minv[2][i] = z[i];
        Tr[i][3] = -center[i];
    }
    ModelView = Minv*Tr;
}

注意,z’是由向量ce给出的(不要忘记将其归一化,这对以后的计算有帮助)。我们如何计算x’?很简单,就是通过u和z’叉乘。然后我们计算y’,使其与已经计算出的x’和z’正交(让我提醒你,在我们的问题设置中,ce和u不一定是正交的)。最后一步是将原点平移到中心c,我们的变换矩阵就准备好了。现在只需在模型帧中得到坐标为(x,y,z,1)的任意点,乘以矩阵ModelView,就可以得到相机坐标系中的坐标。顺便说一下,ModelView这个名字来自于OpenGL的术语。

Viewport视口矩阵

如果你从一开始就跟着这门课程走,你应该还记得像这样奇怪的代码。

screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);

它是什么意思?意思是我有一个点Vec2f v,它属于正方形[-1,1][-1,1]。我想把它画在(width,height)的图像中。值(v.x+1)在0和2之间变化,(v.x+1)/2在0和1之间变化,(v.x+1)*width/2也就是在0~width之间变化。这样,我们有效地将双单元方块映射到图像上。

但是现在我们要摆脱这些丑陋的构造,我想用矩阵形式重写所有的计算。让我们考虑一下下面的C++代码。

Matrix viewport(int x, int y, int w, int h) {
    Matrix m = Matrix::identity(4);
    m[0][3] = x+w/2.f;
    m[1][3] = y+h/2.f;
    m[2][3] = depth/2.f;
    m[0][0] = w/2.f;
    m[1][1] = h/2.f;
    m[2][2] = depth/2.f;
    return m;
}

图片

意思是把双单元立方体[-1,1][-1,1][-1,1][-1,1]映射到屏幕立方体[x,x+w][y,y+h]*[0,d]上。对了,是立方体,而不是矩形,这是因为用Z-缓冲区进行深度计算。这里d是z-缓冲区的分辨率。我喜欢把它等于255,因为这样做是为了简单地倾倒z-缓冲区的黑白图像进行调试。

在OpenGL的术语中,这个矩阵被称为视口矩阵。

坐标连续变换

所以,让我们总结一下。我们的模型(比如说角色)是在自己的局部框架(对象坐标)中创建的。它们被插入到以世界坐标表示的场景中。从一个到另一个的转换是用矩阵模型进行的。然后,我们要用相机(眼睛坐标)来表达,这个变换称为View。然后,我们用投影矩阵(第4课)对场景进行变形,产生透视变形,这个矩阵将场景变换成所谓的剪辑坐标。最后,我们绘制场景,将剪贴坐标转化为画面坐标的矩阵称为Viewport。

同样的,如果我们从.obj文件中读取一个点v,那么要在屏幕上画出这个点,需要经过以下的变换链。

Viewport * Projection * View * Model * v.

如果你看了这个提交 this,你会看到以下几行。

Vec3f v = model->vert(face[j]);
screen_coords[j] =  Vec3f(ViewPort*Projection*ModelView*Matrix(v));

由于我只画了一个对象,所以矩阵模型等于本身,我将其与矩阵视图合并。

法向量变换

有一个广为人知的事实。

如果我们有一个模型,它的法向量是由设计者给定的,并且这个模型是用仿射映射进行变换的,那么法向量要用映射进行变换,等于原映射矩阵的逆矩阵的转置

什么–什么–什么–!?我遇到不少程序员都知道这个事实,但对他们来说,这仍然是一个黑魔法。其实,其实也没那么复杂。拿一支铅笔,画一个二维三角形(0,0)、(0,1)、(1,0)和一个向量n,在下图中的法线上。自然,n等于(1,1)。然后,让我们将所有的y坐标扩展2的系数,保留x坐标不变。这样,我们的三角形就变成了(0,0), (0,2), (1,0). 如果我们用同样的方法变换向量n,它就会变成(1,2),并且它不再是三角形的正交边。

因此,为了消除所有的黑魔法,我们需要明白一个简单的道理:我们不需要简单的变换法向量(因为它们可以变得不再是法向量),我们需要计算(新的)法向量到变换后的模型。

回到三维,我们有一个向量n = (A,B,C)。我们知道经过原点的平面,法线为n,有一个方程Ax+By+Cz=0,让我们把它写成矩阵形式(我从一开始就用其次坐标)。

图片

回想一下,(A,B,C)-是一个向量,所以我们在嵌入到4D中时将其增强为0,而(x,y,z)因为是一个点,所以将其增强为1。

让我们在两者之间插入一个身份矩阵(逆向M乘以M等于身份矩阵)。

图片

右边括号中的表达式–是对被变换的对象的点。左边的表达式–是对被变换对象的法向量! 在标准的惯例中,我们通常把坐标写成列(请大家不要把反向量和共变向量的东西都提出来),所以我们可以把前面的表达式改写成如下。

图片

而左边的括号告诉我们,通过应用仿射映射的反转置矩阵,可以从旧法线计算出被变换对象的法线。

请注意,如果我们变换矩阵M是缩放,旋转,平移组成,那么M和它的逆转职是相等的,这种情况下逆和转置是相互抵消的。但是如果包括了投影,其是仿射变换,就需要上面的技巧了。

在目前的代码中,我们没有使用法线向量的变换,但在下一节课中,它将会非常非常方便。

祝你编码愉快!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
光栅渲染器是一种基于图形学渲染方式,它通过将三维模型转换为二维图像进行渲染。下面是一个简单的光栅渲染器的C++实现示例: ```cpp #include <iostream> #include <fstream> #include <cstring> #include <cmath> using namespace std; // 定义三维向量结构体 struct Vec3f { float x, y, z; // 向量加法 Vec3f operator+(const Vec3f& v) const { return Vec3f(x + v.x, y + v.y, z + v.z); } // 向量减法 Vec3f operator-(const Vec3f& v) const { return Vec3f(x - v.x, y - v.y, z - v.z); } // 向量点乘 float operator*(const Vec3f& v) const { return x * v.x + y * v.y + z * v.z; } // 向量叉乘 Vec3f operator^(const Vec3f& v) const { return Vec3f(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x); } // 向量数乘 Vec3f operator*(const float& f) const { return Vec3f(x * f, y * f, z * f); } // 向量模长 float length() const { return sqrtf(x * x + y * y + z * z); } // 向量归一化 void normalize() { float len = length(); x /= len; y /= len; z /= len; } }; // 定义三角形结构体 struct Triangle { Vec3f v1, v2, v3; }; // 定义光线结构体 struct Ray { Vec3f origin; // 光线起点 Vec3f direction; // 光线方向 }; // 定义颜色结构体 struct Color { float r, g, b; // 颜色加法 Color operator+(const Color& c) const { return Color(r + c.r, g + c.g, b + c.b); } // 颜色数乘 Color operator*(const float& f) const { return Color(r * f, g * f, b * f); } // 颜色乘法 Color operator*(const Color& c) const { return Color(r * c.r, g * c.g, b * c.b); } }; // 定义相机结构体 struct Camera { Vec3f position; // 相机位置 Vec3f direction; // 相机方向 float fov; // 视场角 }; // 定义画布结构体 struct Canvas { int width, height; // 画布宽度和高度 Color* pixels; // 像素数组 // 获取指定位置的像素颜色 Color& getPixel(int x, int y) const { return pixels[y * width + x]; } }; // 定义场景结构体 struct Scene { Triangle* triangles; // 三角形数组 int numTriangles; // 三角形数量 Color ambientLight; // 环境光颜色 Vec3f lightPosition; // 光源位置 Color lightColor; // 光源颜色 }; // 计算光线和三角形的交点 bool intersect(const Ray& ray, const Triangle& triangle, Vec3f& intersection) { Vec3f edge1 = triangle.v2 - triangle.v1; Vec3f edge2 = triangle.v3 - triangle.v1; Vec3f h = ray.direction ^ edge2; float a = edge1 * h; if (a > -0.00001f && a < 0.00001f) { return false; } float f = 1.0f / a; Vec3f s = ray.origin - triangle.v1; float u = f * (s * h); if (u < 0.0f || u > 1.0f) { return false; } Vec3f q = s ^ edge1; float v = f * (ray.direction * q); if (v < 0.0f || u + v > 1.0f) { return false; } float t = f * (edge2 * q); if (t > 0.00001f) { intersection = ray.origin + ray.direction * t; return true; } return false; } // 计算光线和场景的交点 bool intersect(const Ray& ray, const Scene& scene, Vec3f& intersection) { bool intersected = false; float nearestDistance = INFINITY; for (int i = 0; i < scene.numTriangles; i++) { Vec3f intsec; if (intersect(ray, scene.triangles[i], intsec)) { float distance = (intsec - ray.origin).length(); if (distance < nearestDistance) { nearestDistance = distance; intersection = intsec; intersected = true; } } } return intersected; } // 计算点在三角形上的投影颜色 Color shade(const Vec3f& point, const Triangle& triangle, const Scene& scene) { Vec3f normal = (triangle.v3 - triangle.v1) ^ (triangle.v2 - triangle.v1); normal.normalize(); Vec3f toLight = scene.lightPosition - point; toLight.normalize(); float diffuse = normal * toLight; diffuse = max(diffuse, 0.0f); Color color = scene.lightColor * diffuse; color = color + scene.ambientLight; return color; } // 渲染场景 void render(const Scene& scene, const Camera& camera, const Canvas& canvas) { float fovScale = tanf(camera.fov / 2.0f * M_PI / 180.0f); for (int y = 0; y < canvas.height; y++) { for (int x = 0; x < canvas.width; x++) { float px = (2.0f * ((x + 0.5f) / canvas.width) - 1.0f) * fovScale; float py = (1.0f - 2.0f * ((y + 0.5f) / canvas.height)) * fovScale; Vec3f direction = camera.direction + Vec3f(px, py, 1.0f); direction.normalize(); Ray ray = { camera.position, direction }; Vec3f intersection; if (intersect(ray, scene, intersection)) { Color color = shade(intersection, scene.triangles[0], scene); canvas.getPixel(x, y) = color; } } } } // 保存渲染结果到文件 void saveCanvas(const Canvas& canvas, const char* filename) { ofstream file(filename); file << "P3\n" << canvas.width << ' ' << canvas.height << "\n255\n"; for (int y = 0; y < canvas.height; y++) { for (int x = 0; x < canvas.width; x++) { Color color = canvas.getPixel(x, y); file << (int)(color.r * 255.0f) << ' ' << (int)(color.g * 255.0f) << ' ' << (int)(color.b * 255.0f) << '\n'; } } file.close(); } int main() { // 定义场景 Scene scene; scene.numTriangles = 1; scene.triangles = new Triangle[scene.numTriangles]; scene.triangles[0] = { { -1.0f, -1.0f, 0.0f }, { 1.0f, -1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f } }; scene.ambientLight = { 0.1f, 0.1f, 0.1f }; scene.lightPosition = { 0.0f, 0.0f, -10.0f }; scene.lightColor = { 1.0f, 1.0f, 1.0f }; // 定义相机 Camera camera; camera.position = { 0.0f, 0.0f, -5.0f }; camera.direction = { 0.0f, 0.0f, 1.0f }; camera.fov = 60.0f; // 定义画布 Canvas canvas; canvas.width = 640; canvas.height = 480; canvas.pixels = new Color[canvas.width * canvas.height]; // 渲染场景 render(scene, camera, canvas); // 保存渲染结果到文件 saveCanvas(canvas, "output.ppm"); // 释放内存 delete[] scene.triangles; delete[] canvas.pixels; return 0; } ``` 这个渲染器实现了一个简单的场景渲染,包含一个三角形和一个白色光源。它使用了光线追踪算法进行渲染,可以在输出文件中看到渲染结果。不过,这个渲染器还有很多可以优化的地方,比如增加阴影、反射、抗锯齿等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值