Go 常见陷阱与最佳实践(全系列总结)
需要预先了解: 数组与切片 map 与字符串 函数 接口 错误处理 并发设计模式与陷阱总结 切片、map 与字符串的底层实现 接口与 channel 的底层实现 性能分析与调优 Go 常见陷阱与最佳实践(全系列总结) 这一篇不引入新知识,只做三件事:把散落在各篇的陷阱集中起来、把最佳实践整理成清单、给出一张全系列的知识地图。 陷阱大汇总 每一条都给出"现象 → 原因 → 出处",方便按需回查。 1. 切片共享底层数组 a := []int{1, 2, 3, 4, 5} b := a[:3] b = append(b, 100) // 容量够,直接写进 a[3] fmt.Println(a) // [1 2 3 100 5] 原因:切片的 array 指针指向同一块底层数组,容量足够时 append 就地写入。 出处:数组与切片、切片、map 与字符串的底层实现 2. range 拿到的是副本 points := []Point{...
PBR
需要预先学习: Phong模型与Blinn-Phong模型 渲染方程与重要性采样 PBR 基于物理的渲染 Phong模型与Blinn-Phong模型 中的两个模型都是经验模型(empirical model):用环境光、漫反射、高光三项拼凑出"看起来对"的结果,系数(shininess、ambientStrength)没有物理含义,换个光照环境就得重新调参。 PBR(Physically Based Rendering,基于物理的渲染)从物理规律出发建立反射模型:能量守恒、微表面反射、菲涅尔效应都被显式建模,材质参数直接对应真实世界的属性(金属度、粗糙度)。同一套参数在任何光照环境下都成立,这是 PBR 被游戏与影视工业广泛采用的原因。这篇笔记介绍渲染方程与 Cook-Torrance BRDF,并给出直接光照的 GLSL 实现。 渲染方程与 BRDF 辐射度量学速览 描述光的能量需要一套度量单位,辐射度量学(radiometry)提供这套语言: 物理量 符号 含义 单位 辐射通量 Φ\PhiΦ 单位时间通过的能...
IBL
需要预先学习: PBR 纹理(cubemap 部分) 帧缓冲(渲染到纹理部分) IBL 概述 PBR 的直接光照部分把渲染方程(渲染方程与重要性采样)的积分退化成对光源求和,但环境光仍是个常数——没有方向性,金属物体反射不出环境的颜色,粗糙金属的反射也不会模糊。要补全 PBR 观感,需要来自环境各方向的光。 IBL(Image Based Lighting,基于图像的光照)的做法:用一张环境贴图(cubemap,见 纹理)记录周围环境各方向的入射光 LiL_iLi,把渲染方程的积分预先算好存进纹理,运行时查表代替求积分。 这套"预计算换实时性"的思路与 ShadowMap与PCF、PCSS、纹理 的 mipmap 一脉相承,是实时渲染的核心套路。 渲染方程的拆分 漫反射 IBL 与高光 IBL 分开处理,因为两者的积分性质不同: 项 积分 依赖 漫反射 ∫ΩLicosθ dωi\int_\Omega L_i \cos\theta\, d\omega_i∫ΩLicosθ...
伽马校正
需要预先学习: 纹理 着色器 为什么需要伽马校正 显示器是非线性的 伽马(gamma)问题源于一个物理事实:显示器的亮度响应不是线性的。CRT 时代的显像管,输入电压与输出亮度的关系近似幂函数 L=VγL = V^{\gamma}L=Vγ,γ≈2.2\gamma \approx 2.2γ≈2.2——输入电压翻倍,亮度远达不到两倍。LCD 虽原理不同,但为了兼容,也模拟这条曲线。 所以显示器上"0.5 的像素值"显示出来的物理亮度,只有"1.0 的像素值"的 0.52.2≈0.220.5^{2.2} \approx 0.220.52.2≈0.22,而不是一半。 人眼对暗部更敏感 人眼对暗部的亮度差异比亮部敏感得多——暗处稍微亮一点就能察觉,亮处变化却不易发现。利用这个特性,sRGB 编码(sRGB encoding)在存储颜色时给暗部分配更多精度:编码曲线近似 γ=1/2.2\gamma = 1/2.2γ=1/2.2 的幂函数,正好与显示器的 2.22.22.2 抵消。 于是图像文件(.png、.jpg 等)里存的不...
色调映射
需要预先学习: 帧缓冲 伽马校正 从 LDR 到 HDR 真实场景的动态范围 传统渲染的帧缓冲用 8bit 无符号整数存储颜色,取值范围 [0,1][0, 1][0,1],称为 LDR(Low Dynamic Range,低动态范围)。但真实场景的亮度范围远不止如此:阳光直射下的亮度是阴影处的几万倍(动态范围约 10510^5105)。把这样的场景塞进 [0,1][0, 1][0,1],结果只有两种: 把曝光调到阴影可见,太阳就变成一片纯白; 把曝光调到太阳可见,阴影就变成一片纯黑。 无论哪种,亮部或暗部的细节都被截断(clamp)了。 HDR 渲染:浮点帧缓冲 HDR(High Dynamic Range,高动态范围)渲染的做法:中间结果用浮点纹理存储,亮度超过 1.0 的值不被截断,等最后再压缩到显示范围。创建浮点帧缓冲: // 创建 HDR 帧缓冲:颜色附件用半浮点纹理 unsigned int hdrFBO; glGenFramebuffers(1, &hdrFBO); glBindFramebuffer(GL_FRAMEBUFFER...
Bloom
需要预先学习: 帧缓冲 色调映射 纹理 Bloom 原理 Bloom(辉光)是 HDR 渲染的经典后处理效果:场景中的亮部向周围"渗光",形成柔和的光晕。看夜空中的路灯、显示器上的高光,人眼与镜头都会看到这种光晕——光线在眼球晶状体/镜头内部散射,把亮部的能量摊到了周围。 实现的思路很直接,三步: 提取亮部:从 HDR 画面中取出超过亮度阈值的部分; 模糊:把亮部图模糊成光晕; 合成:模糊结果叠加回原图。 为什么必须用 HDR 亮度阈值要区分"亮"与"不亮",只能在 HDR 空间做。LDR 画面里所有像素都被截断在 1.0,无法区分"亮度 1.0 的灰"和"亮度 100 的太阳"——两者提取出来一模一样。HDR 帧缓冲(色调映射)保留了超过 1.0 的亮度信息,阈值才有意义。 实现步骤 1. 渲染 HDR 场景 场景渲染进浮点帧缓冲(色调映射 中的 hdrFBO),光源亮度可以超过 1.0,例如点光源给 vec3(10.0) ...
屏幕空间技术
需要预先学习: 帧缓冲 深度缓冲 前向渲染与延迟渲染 Bloom 什么是屏幕空间 前向渲染与延迟渲染 的光照 Pass 展示了一种新视角:不再遍历场景,而是对屏幕上每个像素从纹理里取数据做计算。把这种思路推广——所有输入只有"当前视角下已渲染的屏幕图像(颜色、深度、法线)"、所有计算都发生在全屏 pass 上的技术,统称屏幕空间(screen space)技术。 它与 全局光照与局部光照 的关系:环境光遮蔽、间接光、反射这些 GI 效果需要整条光路的信息,实时算不动;屏幕空间技术退而求其次——屏幕上看得见的部分,就是我能拿来用的全部: 真实 GI: 光线在完整场景中反弹(包括屏幕外、被遮挡的部分) 屏幕空间: 光线只在一层"屏幕贴片"上反弹(屏幕拍到的 2.5D 深度场) 代价与收益同样明确: 优点 缺点 输入只是几张全屏纹理,复杂度与场景规模无关 屏幕外的信息不存在:反射/AO 在物体进入屏幕的边缘消失 天然适配全屏 pass / compute shader,实现...
Compute Shader
需要预先学习: 着色器 缓冲对象与顶点 帧缓冲 Compute Shader 简介 计算着色器(compute shader)是 OpenGL 4.3 引入的着色器阶段。与顶点、片元着色器不同,它不在渲染管线里——不处理顶点、不产生片元,而是直接执行通用计算(GPGPU,General-Purpose GPU computing,用 GPU 做非图形计算)。 普通管线是单向的数据流:顶点 → 光栅化 → 片元 → 帧缓冲,着色器只是管道上的加工站。计算着色器是一块独立的"通用计算单元":你告诉它"开多少个线程、每个线程干什么",它就并行执行,结果可以写进纹理、缓冲,供后续渲染使用,也可以被 CPU 读回做逻辑。 线程模型:工作组 计算着色器的执行模型分两级:一次调度(dispatch)启动若干个工作组(work group),每个工作组内又有若干调用(invocation,即线程)。 概念 说明 工作组 一次调度划分出的执行块,数量由调度参数决定 调用 工作组内的单个线程,数量由着色器内的 ...
UniformBuffer
需要预先学习: 着色器 缓冲对象与顶点 glsl语法 UniformBuffer 回顾一下常规的 uniform 设置方式:glUniform* 逐个把值传进当前着色器程序。在 Phong模型与Blinn-Phong模型 里已经能看到问题的一角——光颜色、光位置、视角位置……每个变量一次调用。当场景里的着色器程序越来越多,每个程序又都要一份相同的 view、projection 矩阵时,这种方式的 CPU 开销会变得难以忽视。 UniformBuffer(uniform buffer object,UBO,统一变量缓冲对象)把一批 uniform 打包进一个缓冲对象:一次上传整块数据,绑定到绑定点(binding point)之后,任意多个着色器程序都可以直接读取同一份数据。 这篇文章先走完 UBO 的完整使用流程,再分析 std140 布局规则,最后从底层回答一个问题:它到底比 glUniform* 快在哪里。 常规方式的问题 先看常规方式的做法与它的两个问题。 每帧渲染时,你需要先激活程序,再逐个设置 uniform: glUsePro...
纹理压缩格式
需要预先学习: 纹理 伽马校正 为什么不用 PNG 和 JPG 纹理 里加载纹理用的是 png、jpg 这类常见图片格式,解码后上传给 glTexImage2D。这在学习阶段没问题,但看一下 GPU 视角就会发现两个问题: 解码成本。png 与 jpg 是为"存储与传输"设计的压缩:压缩率高,但必须整体解码成未压缩像素才能使用。GPU 无法直接采样一张 png——每帧渲染前都得把整张图解压,显存里实际存放的还是未压缩数据,压缩只在磁盘上省了空间。 随机访问。GPU 采样纹理是随机的:一次双线性插值(见 纹理)要读 4 个相邻纹素,三线性过滤还要跨 mip 层。jpg 的压缩依赖整张图的频域信息(DCT 变换块之间有关联),想取"坐标 (x,y)(x, y)(x,y) 的一个像素",必须把周围一大片都解出来——随机访问的代价是灾难性的。 显存带宽。未压缩的 RGBA8 纹理每像素 4 字节。一张 4096×4096 的纹理 64 MB,加上 mipmap 约 85 MB;采样时的带宽、显存的占用,都会成为...




