前向渲染与延迟渲染
需要预先学习: Phong模型与Blinn-Phong模型 全局光照与局部光照 帧缓冲 PBR 前向渲染(Forward Rendering) 前向渲染(forward rendering)是 着色与渲染管线 描述的标准流程:对每个物体,用一套顶点着色器 + 片元着色器完成变换与光照,结果直接写入颜色缓冲。 基本流程 前向渲染中,光照发生在片元着色器里:片元拿到位置、法线、材质后,遍历场景中的每一个光源,逐个累加光照贡献: // 伪代码:前向渲染的片元着色器 vec3 lighting = vec3(0.0); for (int i = 0; i < lightCount; i++) // 遍历所有光源 { lighting += calcLighting(fragPos, normal, albedo, lights[i]); } FragColor = vec4(lighting, 1.0); 思路直接:每个片元、每个光源算一遍。于是总光照成本大致是: O(物体数×光源数×...
摄像机
需要预先学习: 视图(摄像机)与投影 网格、材质与模型
数组
需要预先了解: 变量与常量 数组 数组(Array):同一类型数据的连续内存集合。 数组中的所有元素的类型都相同,并且长度固定,C++中可以用[]运算符来定义数组: // 类型 数组名[数组长度] int arr[5]; // 长度为5的 int 类型的整数数组 char str[100]; // 长度为100的 char 类型变量的字符数组(字符串) 通过[]运算符来访问数组元素 数组的下标索引从0开始: int main() { int arr[5]; arr[0] = 10; arr[4] = 50; arr[5] = 100; // 错误,arr的下标范围是0~4,没有5 } 常量数组 可以在定义数组的时候用const关键字来修饰数组元素,使得数组的每一个元素的值都不能被修改。 #include <iostream> int main() { const int arr[5] = ...
坐标空间总览
需要预先学习: 变换 视图(摄像机)与投影 光栅化 一个顶点的旅程 前面几篇笔记分别讲了变换(变换)、视图与投影(视图(摄像机)与投影)、光栅化(光栅化),每一篇都工作在某一个特定的坐标空间里。这篇笔记把它们串成一条完整的链:一个顶点从模型文件出发,到成为屏幕上的像素,中间经过哪些空间、每一步由哪个矩阵驱动、www 分量经历了什么。 空间 又称 变换矩阵 关键操作 模型空间 局部空间(local space) —(起点) 顶点属性定义于此 世界空间 world space 模型矩阵 MMM 摆放物体、光源与摄像机 视图空间 摄像机空间 / 眼睛空间(eye space) 视图矩阵 VVV 摄像机移到原点、看向 −z-z−z 裁剪空间 clip space 投影矩阵 PPP 可视区域规范化,www 携带深度 NDC 标准化设备坐标 透视除法(除以 www) 坐标落入 [−1,1]3[-1, 1]^3[−1,1]3 屏幕空间 screen space 视口变换 映射到像素网格,进入光栅化 整...
切片、map 与字符串的底层实现
需要预先了解: 数组与切片 map 与字符串 内存分配与逃逸分析 切片、map 与字符串的底层实现 这一篇把三种最常用的类型拆到运行时层面:切片头到底存了什么、append 扩容的精确规则、map 的桶结构与渐进式搬迁、字符串为什么不可变。搞懂这些,前面几篇里"看起来反直觉"的行为就都有解释了。 切片头 切片在运行时是一个三元组: type slice struct { array unsafe.Pointer // 指向底层数组 len int // 长度 cap int // 容量 } 用 unsafe 观察: s := make([]int, 3, 5) fmt.Printf("切片: len=%d cap=%d 底层数组指针=%p 首元素地址=%p\n", len(s), cap(s), unsafe.SliceData(s), &s[0]) fmt.Println("切片头大小:", unsafe.Sizeof(s)) 切片: le...
数组名退化为指针
需要预先了解: 数组 指针 数组到指针的退化 数组名出现在大多数表达式中,会被编译器自动转化为指向数组首元素的指针: int arr[5] = { 1, 2, 3, 4, 5 }; int* p = arr; // arr 这里的含义为 &arr[0],类型为 int* 1. 下标访问arr[i]在底层等价于*(arr + i) 甚至可以可以有3[arr]这样的写法,和arr[3]等效: #include <iostream> int main() { int arr[5] = { 1, 2, 3, 4, 5 }; std::cout << 3[arr] << std::endl; // 并不建议的写法 return 0; } 2. 可以用指针的方式访问数组,或用数组的方式访问指针 #include <iostream> int main() { int arr[5] = { ...
透视校正插值
需要预先学习: 光栅化 纹理映射 坐标空间总览 屏幕空间插值的问题 纹理映射 介绍过属性插值:光栅化阶段用重心坐标(barycentric coordinates)把顶点属性(颜色、法线、UV)推算到三角形内部每个片元。纹理映射 的注里留了一个悬念:屏幕空间的重心坐标不能直接用于属性插值,透视投影下会产生扭曲。 先看症状。一块斜着放的地板(或墙),用均匀的棋盘格纹理铺满: 正确(透视校正) 错误(屏幕空间直接插值) ┌────┬──┬──┬────┐ ┌──┬──┬──┬──┬──┐ │ │ │ │ │ │ │ │ │ │ │ ← 远处:格子应越来越密 │ │ │ │ │ │ │ │ │ │ │ 却被均匀压缩 ├────┼──┼──┼────┤ ├──┼──┼──┼──┼──┤ │ │ │ │ │ │ │ │ │ │ │ │ │...
接口与 channel 的底层实现
需要预先了解: 接口 channel 内存分配与逃逸分析 接口与 channel 的底层实现 接口 里留下的 nil 陷阱、channel 里"收发是拷贝"的结论,都可以从运行时结构上得到解释。这一篇看 eface/iface/itab 和 hchan 的真实字段,以及一次发送、一次接收、一次关闭在底层分别发生了什么。 eface 与 iface 空接口和非空接口在运行时是两种结构: type eface struct { // 空接口 any / interface{} _type *_type // 动态类型 data unsafe.Pointer // 指向动态值的指针 } type iface struct { // 带方法的接口 tab *itab // 类型 + 方法表 data unsafe.Pointer // 指向动态值的指针 } 两者都是两个指针宽,在 64 位平台上是 16...
反射与 unsafe
需要预先了解: 接口 包与模块:代码组织 切片、map 与字符串的底层实现 反射与 unsafe 反射让程序在运行时检查类型、读取字段、调用方法;unsafe 则允许绕过类型系统直接操作内存。两者都是"不得已才用"的工具:反射带来灵活性和性能代价,unsafe 带来性能和不安全性。 为什么需要反射 编译期类型已知时,直接写代码就行。但有些场景类型只有在运行时才知道: encoding/json 要把任意 struct 转成 JSON,它不可能为每个类型写一份代码; ORM 要根据结构体字段生成 SQL; 配置加载要按 tag 把 YAML 映射到字段。 这些场景都需要"把类型信息当作数据来操作",这就是反射。 反射三定律 第一定律:接口值可以拆成反射对象。 var x any = User{Name: "一撮灰"} t := reflect.TypeOf(x) // 类型信息 v := reflect.ValueOf(x) // 值 第二定律:反射对象可以还原成接口值。 bac...
标准库(一):IO 与系统交互
需要预先了解: 接口 包与模块:代码组织 错误处理 标准库(一):IO 与系统交互 io.Reader 和 io.Writer 是 Go 标准库最重要的两个接口,几乎所有"数据从哪里来、到哪里去"的 API 都建立在这两个接口上。这一篇讲清楚它们的语义、如何用它们拼装数据流,以及 fmt、os、bufio、time、flag 的常用部分。 io.Reader 与 io.Writer 两个接口的签名极简: type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } Read 的语义:最多读 len(p) 个字节到 p 里,返回实际读到的字节数 n。 n > 0:读到了数据,err 可能是 nil,也可能是 io.EOF(最后一个数据块同时到达末尾); n == 0, err == io.EOF:正常...




