找回密码
 立即注册
查看: 406|回复: 5

现代图形引擎入门指南(八)— 图形API概览

[复制链接]
发表于 2023-3-19 19:51 | 显示全部楼层 |阅读模式
Github Pages
在本章节学习之前,你需要确切地意识到 C/C++ 只是一个将 现实理论计算机中 变现工具 ,并且能够熟练使用它,否则,你应该继续潜修,在没构建好完备的基础知识体系和良好的代码素养之前,笔者认为是没有能力甚至没有资格去进一步学习的。
如何界定是一件比较困难的事情,可以当作像玩蜘蛛纸牌那样思考,最好在觉得无路可走的时候,才请求发牌,因为进入到新的领域确实会提供一些切入点,但也会面临更大的挑战,甚至走进死胡同。
此外,你还需要考虑是否有学习它的必要,如果未来的目标从业方向是:

  • 游戏引擎、图形
  • VR、AR、XR
  • 三维工业软件
  • 音视频
  • 图形驱动
那么学习 底层图形API 是必经之路,但如果更专注图形所呈现出的艺术效果和趣味性,直接上手游戏引擎和三维处理软件会是一个更好的开端。
基础体系

在上一节中,我们通过控制台程序去映射GUI的基础架构,而在这一节中,依然会采用这种方式,不过稍作修改,暂时去除SwapChain,我们以这样的代码开始:
void renderFrame(std::vector<std::vector<char>>& frameBuffer) {
}

int main() {
    const int width = 40;
    const int height = 20;
    const char clearCh = '0';

    while (true) {
        system("cls");              //控制台清屏
        std::vector<std::vector<char>> frameBuffer(width, std::vector<char>(height, clearCh));
        renderFrame(frameBuffer);

        /*上传到输出设备*/
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                std::cout<<(frameBuffer[x][y]);
            }
            std::cout<<std::endl;
        }
    }
    return 0;
}
这里通过一个二维的vector来表示屏幕上的像素数据,一般称这个结构为FrameBuffer,运行它你能看到:


我们要做的就是在这张二维的表上绘制我们的图形
光栅化

什么是图形?
三角形,长方形,矩形,圆形,杯子一样的形状...,如果这么多的东西要考虑的话,那计算机就有数不清的规则要去定义,因此需要进行简化,使用最小的基础单位来囊括自然界中的所有图形,而计算机就只需处理这些基础单位的绘制即可,而这些基础单元(图元)也就是:

  • 点(Point)
  • 线(Line)
  • 三角面(Triangle)
它们可以使用如下的数据结构进行描述:
struct Point {
    int x, y;
};

struct Line {
    std::array<Point, 2> points;
};

struct Triangle {
    std::array<Point, 3> vertices;
};
如果要将一个点绘制在FrameBuffer上,很简单:
void drawPoint(std::vector<std::vector<char>>& frameBuffer, const Point& point) {
    frameBuffer[point.x][point.y] = '1';
}
那如果要将一条线绘制在FrameBuffer上呢?
线由一系列的点组成,但需要注意的是,一条线上有无数的点,但FrameBuffer上的点却是有限的,因此绘制的时候会丢失一部分精度,而我们也只需要根据精度来计算对应点的坐标,而这个过程正是 光栅化(Rasterisation)
一条线可以使用斜切式方程表示:y = kx+b
根据两点坐标可以解出k和b的值,确定精度为1,那么很容易就能算出端点之间的其他点,但需要注意的是,斜切式不能表示垂直于x轴的直线,所以得绕开斜率的计算,这种做法是:DDA(Digital differential analyzer),简易实现如下:
void drawLine(std::vector<std::vector<char>>& frameBuffer, Line line) {
    int dx = line.points[1].x - line.points[0].x;
    int dy = line.points[1].y - line.points[0].y;

    if (dx == 0 && dy == 0) {           //端点重叠,直接绘制点   
        drawPoint(frameBuffer, line.points[0]);
    }

    float steps = abs(dx) > abs(dy) ? abs(dx) : abs(dy);

    float xInc = dx / (float)steps;     //x增量
    float yInc = dy / (float)steps;     //y增量

    float x = line.points[0].x;         //x起始值
    float y = line.points[0].y;         //y起始值

    for (int i = 0; i <= steps; i++) {  //
        drawPoint(frameBuffer, Point(round(x), round(y)));
        x += xInc;
        y += yInc;
    }
}
此外,还由一些其他的线条光栅化算法,这里不一一介绍:

  • Bresenham's line algorithm
  • Xiaolin Wu's line algorithm
在renderFrame中进行测试:
void renderFrame(std::vector<std::vector<char>>& frameBuffer) {
    std::vector<Line> lines = {
        {0,0,39,0},
        {0,0,39,19}
    };

    for (const auto& line : lines) {
        drawLine(frameBuffer, line);
    }
}


而三角形的光栅化相对来说复杂一些,这里有几种算法:

  • 分割法
  • Bresenham 算法
  • Barycentric 算法
其中分割法比较常见,它的核心思想也很简单,就是将一个三角形划分成两个底边与X抽并行的三角形,然后在纵向上逐步的填充线条:


思路细节请阅读:

  • http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.htm
这里有一个简单的实现:
void drawTriangle(std::vector<std::vector<char>>& frameBuffer, Triangle triangle) {
    std::sort(triangle.vertices.begin(), triangle.vertices.end(), [](const Point& a, const Point& b) {
        return a.y < b.y;
    });
    if (triangle.vertices[0] == triangle.vertices[1]    //出现重叠顶点时不绘制
        || triangle.vertices[1] == triangle.vertices[2]
        || triangle.vertices[0] == triangle.vertices[2]
        )
        return;

    auto fillBottomFlatTriangle = [&](const Point& v1, const Point& v2, const Point& v3) {
        float invslope1 = (v2.x - v1.x) / (v2.y - v1.y);
        float invslope2 = (v3.x - v1.x) / (v3.y - v1.y);

        float curx1 = v1.x;
        float curx2 = v1.x;

        for (int scanlineY = v1.y; scanlineY <= v2.y; scanlineY++) {
            Line scanLine = {
             (int)curx1, scanlineY ,
             (int)curx2, scanlineY
            };
            drawLine(frameBuffer, scanLine);
            curx1 += invslope1;
            curx2 += invslope2;
        }
    };

    auto fillTopFlatTriangle = [&](const Point& v1, const Point& v2, const Point& v3) {
        float invslope1 = (v3.x - v1.x) / (float)(v3.y - v1.y);
        float invslope2 = (v3.x - v2.x) / (float)(v3.y - v2.y);

        float curx1 = v3.x;
        float curx2 = v3.x;

        for (int scanlineY = v3.y; scanlineY > v1.y; scanlineY--) {
            Line scanLine = {
             (int)curx1, scanlineY ,
             (int)curx2, scanlineY
            };
            drawLine(frameBuffer, scanLine);
            curx1 -= invslope1;
            curx2 -= invslope2;
        }
    };
    if (triangle.vertices[1].y == triangle.vertices[2].y) {
        fillBottomFlatTriangle(triangle.vertices[0], triangle.vertices[1], triangle.vertices[2]);
    }
    else if (triangle.vertices[0].y == triangle.vertices[1].y) {
        fillTopFlatTriangle(triangle.vertices[0], triangle.vertices[1], triangle.vertices[2]);
    }
    else {
        Point mid = Point({
            (int)(triangle.vertices[0].x + ((float)(triangle.vertices[1].y - triangle.vertices[0].y) / (float)(triangle.vertices[2].y - triangle.vertices[0].y)) * (triangle.vertices[2].x - triangle.vertices[0].x))
            , triangle.vertices[1].y
        });
        fillBottomFlatTriangle(triangle.vertices[0], triangle.vertices[1], mid);
        fillTopFlatTriangle(triangle.vertices[1], mid, triangle.vertices[2]);
    }
}
使用如下代码可以验证:
void renderFrame(std::vector<std::vector<char>>& frameBuffer) {
    std::vector<Triangle> triangles = {
        {0,0,20,0,10,9},
        {30,15,30,5,20,9},
    };

    for (const auto& triangle : triangles) {
        drawTriangle(frameBuffer,triangle);
    }
}


关于相关细节,可阅读:

  • 图形渲染基础:光栅化算法
  • Rasterization: a Practical Implementation
处理管线

计算机中除了表示朴素的,静态的几何图形,有时候还需要绘制一些动态的,经处理的图形效果,这就需要

  • 可以给每个顶点做一些处理,比如移动,相对坐标原点旋转,缩放等
  • 可以对FrameBuffe上的像素进行一些额外的加工,比如上色,加各种滤镜等
结合上一节的SwapChain,可以实现这样的小程序:


上图中动态地缩放三角形的大小,并为其添加了一层 * 边框,完整代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include <array>
#include <Windows.h>

double GTime = 0;

struct Point {
    int x, y;
    bool operator == (const Point& other) { return x == other.x && y == other.y; }
};

struct Line {
    std::array<Point, 2> points;
};

struct Triangle {
    std::array<Point, 3> vertices;
};

void drawPoint(std::vector<std::vector<char>>& frameBuffer, const Point& point) {
    frameBuffer[point.x][point.y] = '1';
}

void drawLine(std::vector<std::vector<char>>& frameBuffer, Line line) {
    int dx = line.points[1].x - line.points[0].x;
    int dy = line.points[1].y - line.points[0].y;

    if (dx == 0 && dy == 0) {           //端点重叠,直接绘制点   
        drawPoint(frameBuffer, line.points[0]);
    }

    float steps = abs(dx) > abs(dy) ? abs(dx) : abs(dy);

    float xInc = dx / (float)steps;     //x增量
    float yInc = dy / (float)steps;     //y增量

    float x = line.points[0].x;         //x起始值
    float y = line.points[0].y;         //y起始值

    for (int i = 0; i <= steps; i++) {
        drawPoint(frameBuffer, Point(round(x), round(y)));
        x += xInc;
        y += yInc;
    }
}

void drawTriangle(std::vector<std::vector<char>>& frameBuffer, Triangle triangle) {
    std::sort(triangle.vertices.begin(), triangle.vertices.end(), [](const Point& a, const Point& b) {
        return a.y < b.y;
    });
    if (triangle.vertices[0] == triangle.vertices[1]    //出现重叠顶点时不绘制,暂时忽略共线的情况
        || triangle.vertices[1] == triangle.vertices[2]
        || triangle.vertices[0] == triangle.vertices[2]
        )
        return;

    auto fillBottomFlatTriangle = [&](const Point& v1, const Point& v2, const Point& v3) {
        float invslope1 = (v2.x - v1.x) / (v2.y - v1.y);
        float invslope2 = (v3.x - v1.x) / (v3.y - v1.y);

        float curx1 = v1.x;
        float curx2 = v1.x;

        for (int scanlineY = v1.y; scanlineY <= v2.y; scanlineY++) {
            Line scanLine = {
             (int)curx1, scanlineY ,
             (int)curx2, scanlineY
            };
            drawLine(frameBuffer, scanLine);
            curx1 += invslope1;
            curx2 += invslope2;
        }
    };

    auto fillTopFlatTriangle = [&](const Point& v1, const Point& v2, const Point& v3) {
        float invslope1 = (v3.x - v1.x) / (float)(v3.y - v1.y);
        float invslope2 = (v3.x - v2.x) / (float)(v3.y - v2.y);

        float curx1 = v3.x;
        float curx2 = v3.x;

        for (int scanlineY = v3.y; scanlineY > v1.y; scanlineY--) {
            Line scanLine = {
             (int)curx1, scanlineY ,
             (int)curx2, scanlineY
            };
            drawLine(frameBuffer, scanLine);
            curx1 -= invslope1;
            curx2 -= invslope2;
        }
    };
    if (triangle.vertices[1].y == triangle.vertices[2].y) {
        fillBottomFlatTriangle(triangle.vertices[0], triangle.vertices[1], triangle.vertices[2]);
    }
    else if (triangle.vertices[0].y == triangle.vertices[1].y) {
        fillTopFlatTriangle(triangle.vertices[0], triangle.vertices[1], triangle.vertices[2]);
    }
    else {
        Point mid = Point({
            (int)(triangle.vertices[0].x + ((float)(triangle.vertices[1].y - triangle.vertices[0].y) / (float)(triangle.vertices[2].y - triangle.vertices[0].y)) * (triangle.vertices[2].x - triangle.vertices[0].x))
            , triangle.vertices[1].y
        });
        fillBottomFlatTriangle(triangle.vertices[0], triangle.vertices[1], mid);
        fillTopFlatTriangle(triangle.vertices[1], mid, triangle.vertices[2]);
    }
}

/*逐顶点处理
* 根据时间动态缩放顶点位置
*/
Point processVertex(const Point& point) {
    Point newPoint = point;
    double scaleFactor = 0.5 + 0.4 * std::sin(GTime);       //保证缩放因子在[0.1,0.9]
    newPoint.x *= scaleFactor;
    newPoint.y *= scaleFactor;
    return newPoint;
}

/*逐像素处理
* 简单描边
*/
char processPixel(const std::vector<std::vector<char>>& frameBuffer, int x,int y) {
    static int direction[4][2] = { {0,1},{0,-1},{1,0},{-1,0} }; //四方向
    for (int i = 0; i < 4; i++) {
        int xOff = x + direction[0];
        int yOff = y + direction[1];
        if (xOff >= 0                                           //判断是否位于frame边界内,当前像素为0,周边像素有1
            && xOff < frameBuffer.size()
            && yOff >= 0
            && yOff < frameBuffer[0].size()
            && frameBuffer[x][y]=='0'
            && frameBuffer[xOff][yOff] == '1') {
            return '*';
        }
    }
    return frameBuffer[x][y];
}

void renderFrame(std::vector<std::vector<char>>& frameBuffer) {
    std::vector<Triangle> triangles = {
        {0,0,40,0,20,19},
    };

​    /*顶点处理*/
    for (auto& triangle : triangles) {
        for (auto& vertex : triangle.vertices) {
            vertex = processVertex(vertex);
        }
    }

    for (const auto& triangle : triangles) {
        drawTriangle(frameBuffer,triangle);
    }

    /*像素处理*/
    std::vector<std::vector<char>> newFrameBuffer(frameBuffer);
    for (int x = 0; x < frameBuffer.size(); x++) {
        for (int y = 0; y < frameBuffer[x].size(); y++) {
            newFrameBuffer[x][y] = processPixel(frameBuffer, x, y);
        }
    }
    frameBuffer = newFrameBuffer;
}

int main() {
    HANDLE frontendBuffer = GetStdHandle(STD_OUTPUT_HANDLE);        //获取默认的缓冲区
    HANDLE backendBuffer = CreateConsoleScreenBuffer(               //创建一个新的缓冲区作为后台缓冲区
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        CONSOLE_TEXTMODE_BUFFER,
        NULL
    );
    //隐藏两个缓冲区的光标
    CONSOLE_CURSOR_INFO cci;
    cci.bVisible = 0;
    cci.dwSize = 1;
    SetConsoleCursorInfo(frontendBuffer, &cci);
    SetConsoleCursorInfo(backendBuffer, &cci);

    const int width = 40;
    const int height = 20;
    const char clearCh = '0';
    const int MaxBufferSize = width * height * 10;

    char bufferData[MaxBufferSize];     //缓存数据暂存区
    DWORD bufferLength = 0;
    COORD zeroCoord = { 0,0 };
    while (true) {
        GTime += 0.01;
        bufferLength = 0;
        CONSOLE_SCREEN_BUFFER_INFO backendBufferInfo;      
        GetConsoleScreenBufferInfo(backendBuffer, &backendBufferInfo);
        int lineWidth = backendBufferInfo.srWindow.Right - backendBufferInfo.srWindow.Left + 1;

        std::vector<std::vector<char>> frameBuffer(width, std::vector<char>(height, clearCh));
        renderFrame(frameBuffer);

        //提交到缓冲区
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                bufferData[bufferLength++] = frameBuffer[x][y];
            }
            while (bufferLength % lineWidth != 0) {            
                bufferData[bufferLength++] = ' ';
            }
        }
        WriteConsoleOutputCharacterA(backendBuffer, bufferData, bufferLength, zeroCoord, &bufferLength);
        SetConsoleActiveScreenBuffer(backendBuffer);           
        std::swap(backendBuffer, frontendBuffer);              
    }
    return 0;
}
现代图形渲染管线中除了上面的逐顶点处理以及逐像素处理,还支持其他的可编程阶段,这些会在后面的章节中深入说明。
三维的图形渲染,无非就是通过一些数值计算将三维的顶点投影到二维的屏幕坐标上,再进行绘制,这里推荐大家可以完整地过一遍:

  • 《3D数学基础 - 图形与游戏开发(第2版)》
现在绘制的理论有了,但在计算机中,还需要考虑性能,因为这就决定了一定的时间范围内,所能绘制三角形数量的上限。
总结上面的代码中,我们可以发现:

  • 代码里面有许多的数值计算
  • 顶点之间的处理,像素之间的处理,图元之间的绘制是没有相互影响的,这也就意味着这些操作都可以并行
为了加速这个过程,现代计算机体系结构中增加了一种专门用于图形绘制的处理器 ——GPU(Graphics Processing Unit,图形处理单元)
GPU

现代 GPU 在操纵计算机图形和图像处理方面非常高效。对于并行处理大块数据的算法,它们的并行结构使得它们比通用中央处理器(CPU)更高效。
这里有一些文章很好地解释了它的作用:

  • 图形流水线的GPU架构
  • 理解GPU的底层架构
另外,笔者强烈简易观看 Games104 的这一节内容:

  • 第四节:游戏引擎中的渲染实践
图形API

图形 API  提供了一种抽象的GPU硬件访问方式,简化了计算机图形生成的各个阶段,使得开发者无需深入了解硬件细节,而专注于图形的构建。
它可以纯粹在软件中完成并在CPU上运行,这在嵌入式系统中很常见,或者由GPU进行硬件加速,在PC中更常见,它主要用于视频游戏和模拟。
当下主流的3D图形API有:

  • DX11、DX12:微软公司在Windows系统上所开发的3D图形编程接口
  • OpenGL:OpenGL是一套跨语言、跨平台的API,它的实现存在于Windows、部分UNIX和Mac OS,这些实现一般由显卡厂商提供,而且非常依赖于该厂商提供的硬件。
  • Vulkan:下一代的OpenGL,相比之下,Vulkan更接近底层,并且能很好地分配CPU核心来执行并行任务
  • Metal:Metal API 由苹果公司提供,它旨在为iOS、iPadOS、macOS和tvOS上的应用程序提供对GPU硬件的低级访问来提高性能,它与Vulkan、DX12都属于低级别的API
相信很多小伙伴看到它们的第一反应是:

  • 这些API里面,谁最强呢?该学哪一个?
在这个问题上,每个人都有自己的见解,比如:

  • 知乎:Vulkan相比于OpenGL、DX12、Metal和Mantle有什么优势、劣势?
作为一个过来人,笔者的看法是:

  • 不要指望学了其中任何一个API,就能高枕无忧,在实际工作开发中,至少都会接触到上述API中的两个及以上,并且更大的可能性是在这些API的上层接口上开发。
大多数游戏引擎都对这些图形API封装成统一的接口,可以在不同的平台上切换来追求更好的图形性能,我们一般称这套接口为 RHI (Rendering Hardware Interface)
虽然OpenGL、Vulkan支持 绝大多数 的平台,但它们并不完美,此外,苹果在它的操作系统上要求必须使用Metal
如果在相关行业持续发展,深入到底层,一定会接触到DX12、Vulkan、Metal的各种疑难杂症,不过好在这类API的基础结构都相差不多,只要会一个,其他的很容易触类旁通。关于它们的细致研究,可参阅:

  • 剖析虚幻渲染体系(13)- RHI补充篇:现代图形API之奥义与指南
  • 木头骨头石头:实时渲染管线:(四)图形程序接口简介
对于初学者而言,DX12、Vulkan、Metal几乎是一道令人望而生畏的天堑
笔者个人认为选择学习曲线更平缓的路线先入门才是更明智的做法
笔者的学习路线如下:
通过Learn OpenGL入门:

  • https://learnopengl-cn.github.io/
并复刻这个网站的代码:

  • https://ogldev.org/
值得一提的是,笔者并没有使用GLFW作为窗口框架,而是使用Qt,当时匮乏的文档无疑给笔者的学习增加了很多困难,但正因为如此,笔者才在不断的试错过程中,一点一点地扫除了自己认知中的雾区,从而有能力去探索更深层次的领域。
因此,笔者更建议大家不要按部就班地模仿教程,可以根据思路大胆尝试,举一反三,锻炼自己寻找问题,解决问题的能力,因为在之后的工作中,会遇到很多意料之外的困难,需要你能够做到 — “兵来将挡,水来土掩”
那现在通过OpenGL入门还是一个不错的选择吗?
笔者必须承认 OpenGL 是一个简单能快速上手的框架,也有很优秀的教程,但可惜的是,现代图形API的架构跟OpenGL API的使用方式已经截然不同,它依然可以用作图形学入门,但整体来说,收益并没有那么高。
感叹的是,笔者几年前还听说很多高校使用固定管线的OpenGL做教学,现在却说出了可编程管线的OpenGL都快要过时的话
笔者做过OpenGL的简易教程:

  • https://www.bilibili.com/read/readlist/rl394647
也写过Vulkan的Demo:

  • https://github.com/Italink/HelloVulkan
深入了解过Bgfx:

  • https://zhuanlan.zhihu.com/p/609349255
尝试过O3DE的RHI:

  • https://github.com/o3de/o3de-atom-sampleviewer
目前在 Unreal Engine 5 中开发
在这个过程中,笔者学到了很多东西,并且迫切地想要把它们给记录下来,复刻一遍来加深理解,然而,却受尽苦难 —— 废弃的OpenGL,繁琐的Vulkan,魔改的bgfx,错综复杂的O3DE、UE...都不是我想要的,直到,笔者无意间发现了 Qt 的 RHI,它的源码就是一件艺术品,它有着高度简化的现代图形API架构:


QRhi

Qt6 的主要目标之一是让 Qt 摆脱直接使用 OpenGL,并且通过适当的抽象,允许在更广泛的图形上进行操作API,例如Vulkan、Metal和Direct3D。OpenGL(和OpenGL ES),这背后的主要动机不是获得性能,而是在 OpenGL 不可用或不再需要的平台和设备上,仍能确保 Qt无处不在 ,将来也会如此。同时,能够在现代的、较低级别的、显式 API 上进行构建也可以在提高性能(例如,由于 API 开销较少而降低 CPU 使用率),它是Qt Quick和其他模块背后的渲染引擎,它的架构如下:


在官方目前的开发者分支上,QRhi已经支持DX12,预计会在Qt6.6上线
而在Qt6中,它也不再使用与OpenGL兼容的GLSL着色器代码,而是使用Vulkan风格的GLSL编写,然后反射并翻译成其他着色器语言(HLSL,MSL),最后打包成一个可序列化的QShader对象,供QRhi使用,它的工作流程如下:


你能在如下位置,找到它的源码:


其中关键文件为:

  • qrhi_p.h:定义了各种图形资源的结构和操作
  • qrhi.cpp:QRhi的调度实现,里面有大量的注释说明
  • qrhi_p_p.h:定义了QRhi完整的结构骨架
其他以qrhi开头的文件是特定图形API的具体实现
示例

在如下目录有很多QRhi的测试工程:


只需修改当前目录下的CMakeLists.txt,在内容开头增加:
cmake_minimum_required(VERSION 3.12)
project(QRhiTests VERSION 0.0.1)
find_package(Qt6 COMPONENTS Core Widgets BuildInternals REQUIRED)
include(QtSetup)
include(QtCMakeHelpers)
include(QtTestHelpers)就能使用Cmake对该目录生成工程文件
在这些工程示例中,可以在的项目属性 - 调试 - 命令参数中调整Rhi的配置,可配置项可参考examplefw.h中的QCommandLineParser:


例如追加 -v,能使用vulkan作为渲染后端,部分示例在DX11中无法运行
如果有一定图形API基础,通过这些工程示例能快速上手QRhi
helloinimalcrossgfxtriangle:简单的三角形绘制程序


multiwindow:多个Rhi窗口


multiwindow_threaded:多窗口,多线程


rhiwidget:在QWidget上使用Rhi


offscreen:离屏(无窗口)渲染


triquadcube:绘制一些简单的几何图形


polygonmode:线框模式


msaatexture:使用 MSAA 的纹理


msaarenderbuffer:使用 MSAA 的RenderBuffer


tst_manual_instancing:实例化渲染


noninstanced:非实例化渲染,使用Buffer来达到与实例化相同的目的


mrt:多渲染目标(Multiple Render Target):一条流水线有多个输出


texuploads:上传纹理


floattexture:浮点纹理:原本纹理的数值存储范围为[0,1]


texturearray:纹理数组


tex3d:3D纹理


cubemap:立方体纹理


cubemap_scissor:裁剪


cubemap_render:渲染输出到立方体纹理


compressedtexture_bc1:压缩纹理


compressedtexture_bc1_subupload:上传部分压缩纹理


computebuffer:使用 Compute Shader 制作简易的GPU粒子


computeimage:使用 Compute Shader 动态生成图像


float16texture_with_compute:计算生成16位的浮点纹理


geometryshader:使用几何着色器


tessellation:使用镶嵌着色器


shadowmap:绘制阴影


QEngineUtilities

QEngineUtilities 是笔者一直在迭代的渲染工具库,它包含三个Target:

  • QEngineCore:渲染架构,包含RHI、FrameGraph、RenderPass、RenderComponent、Asset的简易封装。
  • QEngineEditor:编辑器套件,包含一些基础属性调整控件,以及基于QtMoc的DetailView。
  • QEngineUtilities:Lanuch层,对上面两个模块进行组装,例如在DebugEditor配置下,会嵌入编辑器,而在Debug配置下,就只有Core模块。
它主要用于教学和尝试:

  • 强调可读性是第一要素
  • 没有细致地追求性能(代码细节上有一些瑕疵,在笔者察觉到的时候已经太晚了,由于精力有限,目前笔者也只能选择妥协,非常抱歉...不过放心,这些影响微乎其微)
  • 以渲染为核心,包含少量编辑器架构,不会引入一些会导致代码臃肿的模块,如资产管理,网络,异步,ECS...
一个简单的使用示例如下:




在接下来的教程中,将会在这个模块上一点一点的累积入门的基础知识,目前Github仓库位于:

  • https://github.com/Italink/ModernGraphicsEngineGuide/tree/main/Source
非常抱歉目前还有一些遗留的小问题,它们会在后续的章节中修复,真的太肝了~
学习资源

该教程的主要目的是为了让一些小伙伴能够入门,并培养良好的代码习惯。
对于深层次的技术,不要指望有太多的文章和教程来阐述它们的原理,主流引擎中的代码绝对是最好的参考,不过想要阅读它们,不仅需要足够的理论知识,还需要过硬的工程能力。
对于图形开发中的常用数学知识,强烈推荐《3D数学基础》,以及其他几册书籍:


关于实时渲染,这里有一些比较完整的资源合集:

  • http://www.realtimerendering.com
  • GameDev |  Samsung Developers
关于引擎技术的概览,可以观看:

  • Games104 - 现代游戏引擎:入门到实践
对于引擎的基础知识体系,可以阅读下面的文章:

  • 剖析虚幻渲染体系(14)- 延展篇:现代渲染引擎演变史Part 1(萌芽期)
  • 剖析虚幻渲染体系(14)- 延展篇:现代渲染引擎演变史Part 2(成长期)
  • 剖析虚幻渲染体系(14)- 延展篇:现代渲染引擎演变史Part 3(开花期)
  • 剖析虚幻渲染体系(14)- 延展篇:现代渲染引擎演变史Part 4(结果期)
  • 剖析虚幻渲染体系(16)- 图形驱动的秘密
  • 剖析虚幻渲染体系(17)- 实时光线追踪
  • 剖析虚幻渲染体系(18)- 操作系统
  • 剖析虚幻渲染体系(19)- 计算机硬件体系
发展资讯,可以关注:

  • 游戏开发者大会:https://gdconf.com/
  • Nvidia:https://www.nvidia.com/en-sg/geforce/news/
  • UnrealEngine :https://www.unrealengine.com/zh-CN/feed
  • Vulkan:https://www.vulkan.org/
与之关联的微信公众号、知乎、哔哩哔哩等
此外,这里罗列了一份比较完整的开源图形库列表:

  • https://github.com/Gforcex/OpenGraphicGameDev |  Samsung Developershttps://github.com/Gforcex/OpenGraphic
另外还有一些书籍,有更深层次的技术讲解:

  • Graphics Gems
  • GPU Pro
Github上也有很多有意思的图形项目,可以搜搜看~
读者可能会好奇,这么多东西,难道都需要了解吗?
非也,学海无涯生有涯
上面这么多内容也不是一个人的成果,而是一个庞大群体几十年堆叠出来的技术结晶。
笔者从不相信个人主义英雄,因为他们基本都是“媒体”包装出来的产物,现代技术的发展不是依赖某几个人,而是靠着群体里的每个人各司其职,相互信任,不断传承,一点一点积累出来的,群体里的每个人都很重要,他们的工作和努力值得信任和托付。
对于个人来讲,最重要的是确定自己在这个群体中的位置以及所扮演的角色,其次,我们可以选择一个自己感兴趣的方向去深入研究,因为这不仅仅是为了一份糊口的工作,而是为了能够有能力探索遥远的未来~

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-3-19 19:58 | 显示全部楼层
Github Pages上的text-shadow让我还以为我眼睛坏了[捂脸]
发表于 2023-3-19 20:06 | 显示全部楼层
多看了一会,确实晕,已经去掉了[开心]
发表于 2023-3-19 20:12 | 显示全部楼层
qt也要做三维渲染引擎吗
发表于 2023-3-19 20:20 | 显示全部楼层
很多行业其实都有QT的影子,只不过国内大多数关注点在于它只是一个GUI框架,早期的qt 3d就是一个通用渲染引擎,自从qt6以后,重新封装了rhi接口,目前它被用做 qt quick 和 qt quick 3d 的渲染底层
发表于 2023-3-19 20:28 | 显示全部楼层
了解,多谢
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-21 23:37 , Processed in 0.181260 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表