opengl
可编程渲染管线
先来说说什么是渲染管线。管线的英文是pipeline,其实正确的翻译应该是流水线,或者一系列流程。那么渲染管线就可以看作我可以输入一堆原始图形数据,经过这个流水线,能得到每个像素颜色的输出。
渲染管线大致分为两个部分:一是将3D信息转换为2D信息,二是将2D信息转换为一系列有颜色的像素。具体流程如下图:

所有的这些阶段在不同的图形API(例如本文涉及的OpenGL)上都是高度专门化的,并且它们十分容易并行执行,因此可以将各个阶段的程序搬到GPU上去运行,由这些程序操纵被绑到GPU显存上的顶点数据,完成渲染工作。
其中有几个阶段是用户可编程的(上图蓝色部分),例如最常用到的vertex shader(将3D坐标转换为另一种3D坐标)和fragment shader(计算某个像素的颜色)。我们可以向OpenGL注入我们自己写的shader,所使用的语言就是GLSL(Games202需用)。
crash course
NDC(Normalized Device Coordinates)
这个在Games101提到MVP变换中有提及,它的中文叫做标准化设备坐标,就是x,y,z都是在[-1,1]的空间,对应的就是MVP中的Projection。
前面提到,Vertex shader的作用是将3D坐标装换成另外的3D坐标,因此可以推测出,MVP变换通常是在Vertex shader中进行的。
VBO(Vertex Buffer Objects)
顶点缓冲对象,听上去是拿来存储顶点信息的,事实也正是如此,只不过后面加了个对象的概念。
对于OpenGL中的对象,它们都对应着一个ID。对于VBO,可以使用
glGenBuffers
来生成并获得对应ID:
1 | unsigned int VBO; |
关于OpenGL的API详细文档,推荐查阅 docs.gl
既然VBO内部其实是Buffer,那么就会有很多其它的Buffer Object,因此需要分个类,俗称缓冲类型。VBO的缓冲类型是GL_ARRAY_BUFFER(没毛病,其实就是数组)。
OpenGL实质上是一个状态机,因此在渲染流程中,要渲染的VBO应该属于当前的OpenGL上下文,即被绑定。要绑定某个VBO,可以:
1 | glBindBuffer(GL_ARRAY_BUFFER, VBO); |
绑定完成后,我们对于任何VBO(GL_ARRAY_BUFFER)的修改,都会影响到这个被绑定的VBO。
说了半天,我们VBO内部是要有顶点的,那么数据在哪呢?我们可以自己定义一个float数组:
1 | float vertices[] = { |
这里有三个顶点,顶点属性尚且只有坐标(其实可以加上颜色、法向等)。可以观察到所有坐标都是在NDC范围内的。
好了,现在我们有了直接在cpp里hard code的顶点属性,我们需要和当前绑定的VBO(并不是指定VBO哦,记住opengl的逻辑是一个状态机):
1 | glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); |
这里的最后一个参数涉及到API实现方的优化,具体见 docs.gl
Vertex shader
这里的Shader language是GLSL,对于当前的Vertex shader,我们并不需要给它进行什么复杂的变换(例如MVP变换),因此直接返回对应的齐次坐标即可:
1 | #version 330 core |
第一行是版本说明,也明确表示我们使用核心模式。第二行指定了输入数据。main
函数内则指定了输出位置 gl_Position
。
接下来我们要在openGL创建Vertex shader(注意没有绑定,后面会提到,绑定的是整个program):
1 | unsigned int vertexShader; |
Fragment shader
和Vertex shader相似:
1 | #version 330 core |
Fragment shader只需要一个输出变量RGBA,我们可以在开头声明。
链接着色器程序
创建一个program对象,然后将shader链接进去即可,最后如果不需要的话可以删掉前面创建的shader,我们只需要program:
1
2
3
4
5
6
7
8
9
10unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
解释顶点属性分布
我们前面用 glBufferData
绑定了VBO,现在需要解释顶点属性分布,这样shader才知道输入数据。
1 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); |
这里第一个参数对应着Vertex shader的输入变量
layout (location = 0) in vec3 aPos;
,第二个参数是顶点属性的大小,第三个参数是类型,第四个参数表示是否希望数据被标准化,第五个参数是步长,即两个顶点属性之间的间隔,第六个参数是偏移量。
glEnableVertexAttribArray
表示启用顶点属性,这里0表示第一个顶点属性,即
layout (location = 0) in vec3 aPos;
。
至此其实已经具备了渲染一个简单三角形的能力,代码会长的像这样:
1 | // 0. 复制顶点数组到缓冲中供OpenGL使用 |
VAO (Vertex Array Object)
VAO会记录以下顶点属性调用: 1、glEnableVertexAttribArray和glDisableVertexAttribArray的调用 2、通过glVertexAttribPointer设置的顶点属性配置 3、通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象VBO
创建一个VAO: 1
2unsigned int VAO;
glGenVertexArrays(1, &VAO);
因此,在绘制一个物体的时候,我们可以首先绑定一个VAO,然后绑定配置VBO及其属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
啊啊啊
本来打算今天拆笔记本的,结果没动力,啥都不想干。
前几天被舍友叫去打CS2,然后正在等人的时候,突然steam上一个不认识但是是好友的人问我能不能一起打。
我说可以,但我都忘记我什么时候加的这个人的了。后面他开语音,我才记得是高一的舍友。
他问我是不是保去复旦了,我说我爱南航。他是厦大的,我自然不如他,当然打游戏也不如他。他5e1900分,我1000分。
没什么脸,哪怕是去回忆高中的那些同学。除了我全是985本科,我还搁哪苦恼能不能保研成功呢,他们已经有工作了。
小学同学肯定是都忘了,估计也死了几个。初中同学当然也忘的差不多了。
大学,呵呵。
回望大一的时候,特别喜欢玩Unity,特别喜欢算法竞赛。
现在已经没什么欲望了。
要我说,在南航的四年,不至于生不如死,因为我还不希望我出生呢。
这就是个垃圾学校,经费全喂狗的学校,或者拿去充榜单排名,然后天天碰瓷985的出生学校。
以前的我满怀期待地去做大创,满怀期待地加入ACM校队,满怀期待地上机器学习课。
我不知道我在期待什么,明知道自己的水平肯定不只有南航的水平,但是就是想要得到指导,想要从学校学点知识。神经病啊。想在南航学习?
自从搬到天目湖后,南航的ACM就是个狗屎。但我还比较贱,即便身边根本没人打CF,我还搁哪训。
结果训emo了。不是因为我水平低,而是因为我发现自己复健后的水平其实比拿银牌的时候还要高。
发现很有潜力?呵呵,南航直接毁了你的潜力。
我的水平在这里一直都是独一档,断档压制一般的oier和其他人,但是比不过中学学过很多年的有NOI牌子的人。
他们固然是成功的,其他人固然是失败的,我是莫名其妙的死的。
我没有报过任何网课,我全靠看博客起来的。不过管你什么样,只要你初始没有省队的水平,再怎么努力都是失败,因为这里是南航。
这里是能偷改你学分,让你大四下还得上课的学校。
这里是能毁掉你的技术热情,让你被病态的内卷环境压力到变形的学校。
南航的同学,大部分也是傻逼。天天搁哪看AI论文,搁哪训神经网络,甚至在ACM实验室都没人刷题,一个个在哪看paper。懂点技术的又菜又爱装,眼高手低。
哦,我才是傻逼。我是AI的专业第一,但是天天逃课,骂学校骂老师骂同学,现在天天在那整游戏开发,大作业都让别人做。
我才是有错的人,身边的同学都太厉害拉,我是害虫。
南航太好了,是个好学校,如果你想死,建议你报考。
好了,正常点。我最近情绪又变得比较低迷,虽然从南航回到家里很开心,但是我发现我找不到动力了。明明大一的时候我能摸早起来启动Unity或者启动洛谷直接撸码,而现在做什么都没精神。原因只能是,我的本科经历,真的痛不欲生。
我懒吗?我学习能力差吗?
还是那句话,我痛苦的原因,不是我做错了什么,而是我什么都没做错,但是下场跟什么都做错了一样。
毕业了就和所有南航的人永别吧,这会让我更快乐,不要再想和南航有关的事了,肯定和解不了。好在现在认识的朋友都是校外的网友,互联网当e人准没错。
离南航和南航的人远点。
离南航和南航的人远点。
离南航和南航的人远点。
tech3
每周总结#3
游戏引擎
跟了个油管的博主,动手实现自己的game engine。
premake.lua
一个构建系统,相对cmake简单许多。
links
:这个命令是我一开始不太清楚的。因为在我们的解决方案中,我们实际上将游戏引擎和Sandbox作为两个项目,让游戏引擎engine这个项目的最终文件是个动态链接库(windows是.dll),运行时和sandbox生成的.exe文件放在一块。
事实上如果用premake构建vs项目,links其实是对于一个项目创建了另一个项目的引用,而不是链接了一些库。引用是告诉链接器,某些符号是可以使用的,但是它的具体实现在dll文件里(因此有必要运行一些复制dll到指定路径的命令)。
defines
:定义一些宏,这些宏在对应的项目中可以使用。
例如针对dll的导出导入符号:
1 | #ifdef E_PLATFORM_WINDOWS |
我们可以在构建文件中,针对设备的系统,以及项目,定义一些特定的宏。
事件系统
目前的Event System都是Block的,就是对于事件的执行都是立刻的,会阻塞当前线程。
考虑如何设计,首先我们很容易想到用enum或enum class来设置事件的种类,类型。
然后设置Event基类,后续具体的事件继承这个类:
1 | class E_API Event |
由于事件很多,对于每一个子类都要重写方法很累,因此有个比较骚的操作就是将重写的内容用宏替代:
1 | #define EVENT_CLASS_CATEGORY(category) virtual int GetCategoryFlags() const override { return category; } |
EventType::##type 中的 ## 是预处理操作符,用于将宏参数 type 与 EventType:: 连接起来。例如,如果 type 是 Mouse,那么 EventType::##type 就会被展开为 EventType::Mouse。 return #type; 中的 # 是字符串化操作符,将宏参数转换为字符串。如果 type 是 Mouse,那么 #type 就会被展开为 "Mouse"。
然后在类里面根据具体事件类型进行宏展开。
事件分发器,这个东西负责将其对应的事件传到某个函数进行执行:
1 | class EventDispatcher |
std::function
我并不是很熟悉(虽然见过好多次了),暂且将其理解为函数指针。然后对于这里强制转换:func(*(T*)&m_Event)
,个人认为没有什么必要?
由于还没使用到事件分发器,这里先略过。
软光栅
使用类似engine的设计思路,将渲染器的主体封装成dll,给具体的系统(前端233)作为运行时库调用。我目前希望能够跨平台(虽然现在只知道在windows下是用dll进行符号传递),所以采用了premake作为构建系统,然后不调win32。
目前完成了:
- I/O
- obj导入
- 贴图导入
- ppm形式输出
- gl
- 画线
- 画三角形
- Z-buffer
大部分时间都在调bug吧,今天调了一上午bug,发现是重心坐标公式抄错了。然后CursorAI把width和height弄反了,又搞了半天。
因为感觉渲染一张图有点慢了,因此尝试用了vs的性能分析,结果发现:

好家伙,时间全浪费在输出流上了。这个后续实现实时渲染的时候再考虑吧,看看能不能和QT对接(outputer使用特定平台开发,跨平台只针对我的AYR项目),然后去掉这个流。
UE
这个嘛,因为我更偏向做游戏客户端(引擎感觉太难了,可能要paper,然而我已经没时间走学术了),所以目前有空就熟悉一下UE的使用。后续找实习的时候肯定是需要一个具体游戏项目放在简历的。
毕设
懒得搞。
TODO
得处理一些学校的破事了。然后继续学习。
最近在考虑做游戏客户端是不是容易被AI替代,但是目前的我应该是没有能力去分析这个的。因为目前我还在图形学这里自娱自乐,整天盯着我那屎山项目。但如果以我几年前玩Unity的经验,我觉得短期容易被替代的是那种只会写简单逻辑的程序员。我相信gameplay会是一个很大的,充满想象力的学问,而AI是不会取代具有想象力的工作的。
所以该做什么就做什么,先把手上的软光栅做了,然后尝试构思自己的玩法,开发一个有意思的FPS游戏。
一个很不成熟的做题家思维:你一个南大的会被替代,那其他人不是更寄?。但是回过来看,光焦虑也没用,反而影响自己的进度。这种做题家思维反而会有效减少一些焦虑。
tech2
每周总结#2
1. 毕设
目前为止还是不知道自己要做什么。
不过就算知道了,我估计也做不出来。python代码有个弊端就是,对于一个变量它的类型需要你无数次Ctrl+F来自行推导。对于pytorch这里面无数的张量,你要一个一个推出它的shape,然后来确保自己在某个函数的代码能够理解了。
当然我要写的话应该是写cuda代码,然后在python这里看看怎么调用cuda。唉,真头疼,感觉选了个最难的毕设。
我要做的是研究3DGS与光照结合的方法。之前在games101学的无论是光栅化还是光线追踪,都是基于mesh的模型,而3DGS是点云模型,因此需要研究方法能让点云模型具有光照特性。老师给的论文介绍了,在3DGS的基础上,增加一些诸如法向量,以及一些PBR材质相关的可训练的参数。然而我不是做AI的,不知道在代码层面上是怎么在cuda里实现反向传播的(只知道pytorch里的optimizer.step),结果要我改这部分cuda的代码,。。。
希望能水过去,搞的我现在有点烦。
2. 学习
games104
开始了games104的课程,这是个讲游戏引擎怎么构建的课。
目前听了前三节,感觉讲的太笼统了,我还是比较喜欢看代码。
稍微总结一下吧:
首先是引擎架构,其根据功能进行分层,有:
- 平台层:不同平台(比如操作系统,当然还有所谓的Graphics API,比如opengl,dx,vulkan,当然这些我也不是很懂)会提供不同的底层API,游戏引擎需要为开发者提供统一的API,这东西好像叫RHI。
- 核心层:一些数学运算、内存管理、数据结构的实现,比较底层
- 资源层:将不同类型的资源(resource)导入引擎成为对应的资产(asset),每个asset有对应的GUID方便管理,每个asset也有对应的生命周期,需要handle系统去管理
- 功能层:每一帧会调用tick函数,它大致执行两个任务:tick logic和tick render。当然也不止这两个任务,功能层是最多的内容,它可能还涉及到并行化。
- 工具层:编辑器那些,让开发者进行开发的空间。
然后是对于游戏世界的认识,它包括一些动态物、静态物、环境等等。对于基本的游戏世界的对象,用GO(Game Object)来表示。
每个GO有许多的组件(compoment,玩过Unity应该很熟悉这个),当然这样的组件化设计其实是有很多缺点的,但我没写过对应的代码,没什么认识。
其次就是tick,它是让游戏世界动起来的原因。一种tick方式是一个GO一个GO的tick,还有一种tick方式是按照组件类型进行tick,后者可能会更有效。(cache的空间局限性?)
其次就是交互,采用event事件机制,将不同对象的不同组件之间进行解耦合。
对于场景的管理,引擎的核心层(我猜的哈)可能会用一些数据结构进行高效管理,例如毕设接触到的bvh树来通过场景内的物体位置进行构建,加速查询流程。
对于tick,其实它的时序很有讲究。针对event的邮局机制则有效地解决了事件机制与并行化结合可能导致的时序问题。
总结完了,感觉啥都没总结到。慢慢学吧,我总是太急,想看代码。
tinyrenderer
之前说过,结束了games101的学习,大部分同学可能都会尝试地去写一个自己的软光栅。我也不例外,因此我开始了这个项目的学习。
目前呢?刚学会画直线???2333
misc
好消息,离开NUAA了,真的非常开心。
下一次就可以永别这里了,希望再也不用看见这里的人,再也想不起这里的事。
3. TODO
白天搞学校的毕设和其它事情(读论文?做实验?反正都是浪费时间的事情)
晚上学自己喜欢的东西。
没时间打acm了,认命吧,拿不了金牌的fw。