从零开始实现3D软光栅渲染器 (1) 简介

如何在2D屏幕上表示3D物体?这是学习3D编程必须要搞明白的事情。大家都知道,调用OpenGL的函数,给定三角形的3个顶点位置,颜色,就能在屏幕上画一个三角形,再加载一幅图片,就可以给这个三角形附上纹理,还能让这个三角形绕某个坐标轴发生旋转… 这些看似简单的问题的背后,实则是3D编程的内功。大家都知道,学武之人,拼的是内力,花里胡哨的招式的确很博人眼球,但是从长远来看,收益远没有修炼内力高。

而一个软光栅渲染器几乎涵盖了所有的3D渲染知识,从本篇开始,我们将从零开始实现一个3D软光栅渲染器,学习3D渲染背后的数学原理。

光栅化

所谓光栅化就是将你想画的东西转换到2D屏幕上的像素的过程。这个过程涉及到很多光栅化算法,比如说,画一条直线,大家都知道直线的方程:y = k * x + b (斜截式) 这里的 x , y 的取值范围是全体实数,但是屏幕是像素组成的,你要想在屏幕上显示,你就得使用一些算法将这些实数集映射到相应的像素集(离散化)。

3D流水线

光栅化是OpenGL渲染流水线的一个阶段,而这个阶段是由GPU完成的。一方面,由于现在很多成熟的光栅化算法已经被集成到GPU中,基本不用开发人员手动实现了。另一方面,由于GPU具有很强的并行计算能力,相比在CPU中实现这些算法,图形的渲染会大大提升。

那么什么是渲染管线呢?你可以想象一下iphone的生成车间,先制作地板,然后焊接电路,再安装电池…这就是一条流水线。3D编程中,将物体最终显示到屏幕上也要经历类似的过程。

我们以游戏开发为例,简单介绍一下这个流程:

首先,我们需要一个坐标系来描述场景中各物体的位置,否则你根本无法确定游戏角色、道具等的位置,这个坐标就叫世界坐标。就像它的名字那样,我们可以把它理解成描述我们构建的3D世界的坐标系,它是唯一的,它是固定不变的。

而我们的游戏模型一般都是在3D软件中创建的,一般建模的时候,也需要一个坐标系用来描述各个顶点的位置,这就是局部坐标系。如下图就是blender中的局部坐标系,顺便说一下,它是右手坐标系,红绿蓝三个箭头分别对应x,y,z三个坐标轴。局部坐标系的原点一般是由建模者设置的,比如建一个游戏人物的模型,有的人喜欢把局部坐标系的原点放到模型双脚中心,而有的人喜欢把它放在角色的腰部位置,这都是可以的。

一个游戏场景一般包含很多模型(房屋,角色,道具,树木 etc.),而要将这些分别来自不同建模者的模型绘制到同一个场景(同一个世界坐标系)中来,就需要进行坐标变换。没有找到合适的模型,就拿我珍藏的一张合影来举个例子吧。一图胜前言,不需要解释。

好了,现在模型们已经变换到世界坐标系了。你玩游戏的时候,是不是可以控制人物走动,走到不同的位置,会看到不同的景象?此时,你看到视图,是有一个叫摄像机的东东控制的,又叫虚拟相机。这其实是一个概念,就是为了方便观察世界坐标系中而抽象出来的一个模型(这里的模型是一种概念上的模型)。即使不同摄像机,我们照样也可以观察世界坐标系中各个物体,这个我们在后面介绍。现在你只要知道,有一个叫摄像机东西,我们可以很方便的通过控制它来观察世界坐标系中的物体。此时,我们看到的物体是相对摄像机的位置而言的。比如,你拿手机拍照的时候,虽然你拍的是上海的东方明珠,等你拍下来了,就是你手机上的东方明珠,同一个物体,只是描述的方式不同,这个好好品一下。为什么这么干,我们以后再说。

现实世界是3D的,而计算机屏幕是2D的。我们怎么将3D的世界绘制到2D的屏幕上呢?此时,我们就需要选择一种投影算法,将3D世界中的坐标点投影到2D屏幕上。

3D渲染最注重的就是效率。我们其实只要绘制我们人眼能看见的东西就好了。一个三角形,其实是有2个面的,一般在某一时刻,我们只能看到一个面,所以,另一个面就不需要绘制了。比如,一个精细的人物模型,可能有几千万个三角形构成的,而我们只能看到游戏人物的外表,其内部的三角形面片其实就不用绘制了,因为没人看。

到这里,我们就已经拿到了要进行绘制的顶点数据了。接下来,就是前面说的进行光栅化操作了。也就是说,我给你一堆顶点以及顶点之间的连接关系,你要能够在屏幕上正确显示出来(决定到底屏幕上哪些像素被着色,哪些不被着色)。假设我就绘制一个三角形,给3个顶点,光栅化的过程,就是将这三个顶点之间的连线映射到屏幕上相应的像素,如果给了颜色信息,还要给三角形区域着色。到这里,就能够在屏幕上现实3D图形了。

接下来,就是一些优化操作了,比如各种测试(像素包含测试、裁剪测试、alpha测试、模板测试、深度测试 etc.)、混合操作等,反正目的就是提升性能、优化渲染效果。

这就是大概的3D渲染流水线。我们的软光栅渲染器就是要自己编码实现这一套流程,这样在调用OpenGL函数的时候,你才知道它背后到底发生了什么,当你遇到问题的时候,你才有能力尝试猜测可能什么地方出了问题。

开发环境

最后,说一下开发环境。因为我们是学习3D渲染流水线,这个本身已经够复杂了。为了能把注意力放到软光栅的实现上,我们的开发环境越简单越好,所以,我们选择JavaScript开发一个Web-based软光栅渲染器,开发环境VS code,记事本也可以。

其实,只要你掌握了这一套流程,随便什么语言,只是显示环境不同,核心原理都是相同的。对着我们的教程,你完全可以实现其他语言的软光栅渲染器。

欢迎大家关注我的公众号【OpenGL编程】,定期分享OpenGL相关的3D编程教程、算法、小项目。欢迎大家一起交流。

OpenGL编程

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值