帧缓冲
需要预先学习: 缓冲对象与顶点 深度缓冲 帧缓冲(Framebuffer) 着色与渲染管线 的片元处理结束时,通过所有测试的片元会被写入帧缓冲(framebuffer)——渲染管线的最终目的地。默认情况下这个目的地是屏幕:窗口系统在创建窗口时自动配置好一块帧缓冲,渲染结果直接显示。帧缓冲对象(FBO,Framebuffer Object)允许你把渲染目标从屏幕换成自定义的纹理或缓冲,这是后处理、阴影贴图、环境光预计算等一切"渲染到纹理"技术的基础。这篇笔记介绍 FBO 的三种附件(颜色、深度、模板),并重点讲清片元何时写入附件。 默认帧缓冲 深度缓冲 中讲过,帧缓冲不是一个单独的缓冲,而是多块逐像素并列的存储的集合: 缓冲 存储内容 每个像素存什么 颜色缓冲(color buffer) 片元最终颜色 一个 RGBA 颜色值 深度缓冲(depth buffer) 当前最近的深度 一个深度值 模板缓冲(stencil buffer) 模板测试用的标记 一个整数 默认帧缓冲由窗口系统(Win32、X...
网格、材质与模型
需要预先学习: 几何体 网格(Mesh) 在 几何体 中你已经见过网格的定义:一组顶点,加上描述顶点如何连成三角形的索引。缓冲对象与顶点 里又把这些数据装进了 VertexBuffer 和 IndexBuffer,并用 VertexBufferLayout 描述顶点格式。这一篇把三样东西组合成 Mesh 类——一个构造即上传、析构即释放的几何体对象,并顺着它引出材质与模型两个更高层的封装。 网格类的设计 Mesh 不引入新的 OpenGL 概念,它是一层纯粹的组织:把"绘制一个几何体所需的三样数据"收进同一个类。 成员 存放的内容 对应的概念 VertexBuffer 一串 float 顶点数据(位置、UV、法线……) IndexBuffer 一串 uint32_t 顶点如何组成三角形 VertexBufferLayout 属性列表 每个顶点的数据怎么切分、解读 三者在绘制时总是同时出现:单有 VertexBuffer,GPU 眼里只是一串没有含义的数字,要靠 layout 才知道怎么切成一个个顶点;...
switch-case 语句
需要预先学习: if 语句 switch-case 语句 switch-case 语句是一种多分支选择结构,它根据一个整型表达式的值跳转到匹配的 case 标签处执行。基本语法如下: switch (表达式) { case 常量1: // 匹配常量1时执行的代码 break; case 常量2: // 匹配常量2时执行的代码 break; default: // 没有任何 case 匹配时执行的代码 break; } switch 后面括号中的表达式必须是整型(int、char、enum 等),case 后面必须是常量表达式。程序会拿表达式的值依次与每个 case 的值比较,找到匹配的就跳过去执行;如果没有任何匹配,就执行 default(如果有的话)。 break 的用法 break 语句用于跳出 switch 结构。如果没有 break,程序会继续执行下一个 case 中的代码——这个特性被称为"穿透"(fall-through)。 有 break 的情况(正常行为): i...
着色与渲染管线
需要预先学习: 光栅化 深度缓冲 着色(Shading) 着色(shading)指对物体表面应用材质(material)的过程——计算表面每个点呈现的颜色。在渲染管线中,光栅化产生片元之后,由着色阶段决定片元的颜色。 着色的输入包括:光源的位置与强度、观察方向、表面法线(normal,表面在该点的朝向)、材质属性(颜色、光泽度等)。 着色是局部的:每个点独立计算颜色,只考虑光源与表面本身,不考虑其他物体。物体之间的遮挡(阴影)与光线的多次反弹属于全局光照(global illumination),由光线追踪等更复杂的算法处理,不在本节范围。 Blinn-Phong 反射模型 Blinn-Phong 反射模型是最常用的局部光照模型(local illumination model)之一,它把表面颜色分解为三部分之和:环境光(ambient)、漫反射(diffuse)、高光(specular)。 基本约定 在着色点(shading point,表面上的一个点)处定义以下向量: 符号 含义 n\mathbf{n}n 表面法线,单位向量 l\m...
channel
需要预先了解: goroutine 与并发初探 channel channel 是 goroutine 之间传递数据的管道,也是 Go 并发模型的核心。它同时承担两件事:传递数据和同步。这一篇讲清楚无缓冲与有缓冲的差别、close 的规则、select 的多路复用,以及几个会让程序直接崩溃的操作。 设计哲学 Go 的并发信条是: 不要通过共享内存来通信,而要通过通信来共享内存。 意思是:与其让多个 goroutine 抢着读写同一块内存(然后用锁保护),不如让数据在 goroutine 之间传过来,同一时刻只有一方持有它。channel 就是传递的载体。 无缓冲 channel:同步语义 make(chan T) 创建的是无缓冲 channel。它的发送和接收必须同时就位才能完成——发送方会一直阻塞,直到有接收方来取: ch := make(chan int) go func() { fmt.Println("发送前") ch <- 1 fmt.Println("发送完成") }() time.Sleep(100 * ti...
GLSL 语法
需要预先了解: 着色器 程序结构 在着色器中你已经见过一段完整的 GLSL 代码。这篇笔记把它的语法规则系统梳理一遍。一个 GLSL 程序由版本声明、全局声明(变量、结构体、函数)和入口函数 main 组成: #version 330 core layout (location = 0) in vec3 aPos; // 全局变量声明 out vec4 FragColor; // 输出变量 void main() // 入口函数 { gl_Position = vec4(aPos, 1.0); } 几个语法层面的硬性规则: #version 声明必须出现在文件第一行(前面只允许空白和注释),且要先于任何其他声明 入口函数是 main,返回 void,不接受参数 每条语句以分号结尾,注释风格与 C 相同(// 与 /* */) 注:#version 330 core 中的 core 表示核心模式(core profile)。...
IDRenderer的设计与实现
IDRenderer的设计与实现 IDGameEngine 分两层:引擎本体 ID 负责场景、ECS、渲染管线这些"上层逻辑";而所有真正碰 GPU 的活,都收拢在一个独立的渲染后端库 IDLib/IDRenderer 里。这篇笔记从底往上梳理 IDRenderer:先看它如何封装 GPU 资源,再看它如何暴露渲染命令(RenderCommand),顺便记录一些设计上的取舍。 引擎中的位置 IDRenderer 是一个动态库,夹在引擎本体和 OpenGL 之间: ┌────────────────────────────────────────────┐ │ ID(引擎本体) │ │ Renderer / RenderGraph / 各种 RenderPass │ └──────────────────┬─────────────────────────┘ │ 只 include 公开头文件 ┌──────────────────▼─...
纹理映射
需要预先学习: 着色与渲染管线 光栅化 插值 在渲染管线中,光栅化阶段(见 着色与渲染管线)需要为三角形内部的每个片元确定颜色、法线、纹理坐标等属性,而顶点只提供了属性在三个顶点处的值。三角形内部的属性要靠插值(interpolation)推算:已知离散的顶点值,估计连续区域内的值。 线性插值 线性插值(linear interpolation)是最基本的插值方式:在两个已知值之间按比例取中间值。一维情况下,在 aaa、bbb 之间按参数 t∈[0,1]t \in [0, 1]t∈[0,1] 插值: lerp(a,b,t)=(1−t)⋅a+t⋅blerp(a, b, t) = (1 - t) \cdot a + t \cdot b lerp(a,b,t)=(1−t)⋅a+t⋅b t=0t = 0t=0 得到 aaa,t=1t = 1t=1 得到 bbb,t=0.5t = 0.5t=0.5 得到中点。这个式子对二维点、三维向量同样成立,逐分量计算即可。 重心坐标 三角形内部任意一点的位置可以用重心坐标(barycentric coordinates...
sync 包与原子操作
需要预先了解: goroutine 与并发初探 channel sync 包与原子操作 多个 goroutine 同时读写同一份数据,如果不加保护,就会产生竞态条件(race condition)。这一篇讲清楚竞态是怎么发生的、怎么用 -race 检测它,以及 sync.Mutex、sync.RWMutex、sync/atomic、sync.Map、sync.Pool 各自的适用场景。 竞态条件 var counter int func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ }() } wg.Wait() fmt.Println("counter =", counter) } 期望值是 1000,实际运行可能得到: counter = 968 原因在于 counter++ ...
指针
需要预先了解: 内存基础 &取地址符 & 是取地址符,用于获取变量的内存地址。大多数的数据类型往往占用不止一个字节的空间, & 取到的就是其中第一个字节的地址。 #include <iostream> int main() { int a = 10; // 创建一个 int 类型的变量 a std::cout << "a 的地址: " << &a << std::endl; std::cout << "地址的大小: " << sizeof(&a) << " 个字节" << std::endl; return 0; } 编译并运行代码后,看到这样的输出: a 的地址: 0x378cfffe2c 地址的大小: 8 个字节 地址大小与 CPU 架构的关联 对于x86_32架构的CPU,其地址总线为32位,对应地址的大小是32个二进制位,也就是4个字节。 对于x86_64...





