绘制 grid
Grid 绘制方式
之前一直想知道各种三维软件中虚拟网格是怎么绘制的,就上网搜了一下相关实现,没想到里面小细节还挺多的,就用一篇博客记录一下吧 ( •̀ ω •́ )。
下图展示了 blender 中网格的效果:

从图中可以看到几个基本特征:
网格绘制在世界坐标系下的底面(在一般的右手系坐标系中,Z轴向上,X轴向前,Y轴向右),则对应 XY 平面;
![]()
x 轴标记为红色 \((1,0,0)\),y轴标记为 \((0,1,0)\)
按照固定间隔绘制网格
之前一直想知道各种三维软件中虚拟网格是怎么绘制的,就上网搜了一下相关实现,没想到里面小细节还挺多的,就用一篇博客记录一下吧 ( •̀ ω •́ )。
下图展示了 blender 中网格的效果:

从图中可以看到几个基本特征:
网格绘制在世界坐标系下的底面(在一般的右手系坐标系中,Z轴向上,X轴向前,Y轴向右),则对应 XY 平面;

x 轴标记为红色 \((1,0,0)\),y轴标记为 \((0,1,0)\)
按照固定间隔绘制网格
Shader(着色器)简单来说就是运行在 GPU 上的一小段程序,用来控制图形渲染的方式。
在传统渲染管线中,GPU 会自动执行一套固定流程,但 shader 的出现让开发者能够自定义这些流程,从而实现更灵活和更复杂的图形效果。
Shader Toy 是一个在线的 Shader 变成和展示网站,可以展示实时的 Shader 编程效果。

在图形 API (OpenGL)中,支持多种 Shader 类型:
而在 Shader Toy 中的 Shader 特指 Fragment Shader,其输入的是一个覆盖整个显示区域的四边形,可以在 Fragment Shader 中决定显示区域中每一个像素的颜色(rgba)。
??
所谓投影,就是 将高维空间中的对象,通过某种方式映射到低维或另外一个空间中。如下图所示(图片来自网络)。

当在二维平面上展示三维物体时,其展示的是三维物体在二维空间上的一个投影,而这个三维到二维的转换过程就是投影变换。
如果投影过程是一个线性变换,则可以通过一个矩阵描述变换,如下式所示,其中 \(M_\text{projection}\) 就称为投影变换矩阵。 \[ P^{\prime} = M_{\text{projection}} \cdot P \]
最近写博客时,发现 github markdown 中提供的 Alerts 很有用,能够作为提示信息进行展示。
其实际上是在 引用文本(Quoting text) 的基础上添加了一个特殊的 tag,使其渲染时有特殊的显示效果,markdown 代码如下:
1 | > [!NOTE] |
渲染结果如下:

本来以为这是 gfm(GitHub Flavored Markdown Spec)规范的一部分,但是发现这个功能是 Github 单独实现的一个功能,hexo 上默认是不支持的,不过搜了一下似乎实现起来并不复杂,就用一篇博客记录下如何实现的。
Conan 作为一个 C++
包管理工具,相对于之前介绍的 CMake FetchContent 或者 ExternalProject
方式添加第三方依赖会更加简单,但是凡是碰上了
C++,都简单不到哪去,学习成本还是有一点的,就用一篇笔记介绍一下
Conan 的基本使用。
由于我们后面会大量使用 conanfile.py,且也可以借助 Python 插件实现
conanfile.py 的代码补全,直接使用 python 库的方式安装
conan,安装命令如下:
1 | python -m pip install conan |
安装之后例行检查一下版本
1 | conan --version |

通过 $PROFILE 变量,可以查询到 Powershell
默认加载的配置文件路径,如下所示

如果文件不存在,直接创建一个即可,然后填入下面两个预定义的函数(默认代理的地址为
127.0.0.1:7890,纪念天国的CFW🙏)
1 | function proxy_on { |
当更改完 $PROFILE 文件后,使用 . $PROFILE
加载更新后的配置文件,并通过简单的 curl
命令判断是否可用
1 | git clone <repo-url> |
示例命令如下
1 | git clone https://github.com/boostorg/boost.git |
一些常用的参数(全部参数选项可以通过 git clone --help
查看)
--recursive 或
--recurse-submodules(在较新版本的 git
中使用,命令的语义更加清晰):当要拉取的仓库中包含 submodule
时,是默认不会拉取子仓库的,而指定这个参数即可拉取所有的子仓库。
如果我们在下载的时候忘记指定
--recursive,也可以通过下面命令重新拉取子仓库
1 | git submodule update --init --recursive |
--depth:当我们只想下载指定更新次数的分支(这样可以减少下载的大小,在我们只需要最新的提交记录时很有用,当然也可以直接下载
zip 或 tar.gz 等压缩文件来解决。
-b 或者
--branch:指定要拉取的分支名称,例如下载 boost-1.86.0
(这个实际上是一个 tag,但也可以通过这种方式下载)。
1 | git clone https://github.com/boostorg/boost.git --branch boost-1.86.0 |
对于一个\(2^n\times2^n\)的二维网格由于地址空间都是一维的(例如0x0000到0xffff),其实际在计算机中会将多维压缩成一维,可以形式化表示为
\[ \begin{align} M_{\text{2D}}&:(x,y)\to v \\ M_{\text{2D}}^{-1}&:v\to (x,y) \\ \end{align} \]
对于\(M_{\text{2D}}(x,y)\)也可记作\(\text{encode}(x,y)\),\(M^{-1}_{\text{2D}}(v)\)为\(decode(v)\)
一种最为简单的存储方式就是逐行地或逐列地存储二维数据,对于一个指定大小的二维方块(\(w\times h\)),从0开始的坐标\((x,y)\) 对应的一维地址为(一行包含\(w\)个元素) \[ v = x \times w + y \] 同理,将一维坐标\((v)\)转换二维方块坐标也很简单,只需要对行/列进行整除和取余操作即可 \[ \begin{align} x &= \lfloor v / w \rfloor \\ y &= v - x \times w \end{align} \] 上面这种方式在一般情况下足够使用,但是对于纹理/体素等数据的读取时,通常会利用到二维/三维坐标的局部性(即当我们访问坐标\((x,y)\)时,我们有很大概率将访问该坐标周围的坐标),而使用线性存储方式则会丢失这种局部性。
例如对于一个\(16 \times 16\)的方块中坐标\((4,4)\)和坐标\((6,6)\),其二维曼哈顿距离为 \[ (6-4)+(6-4) = 4 \] 而经过线性存储映射后距离为 \[ (6*16+6)-(4*16+4)=38 \] ,可以观察到经过一维映射后两个数据距离相差较远。
下图展示了 linear curve 的示意图

直接说距离似乎难以理解这种差距,考虑一个实际读取场景,假设CPU的cache一次可以存放16个数据,当我们读取纹理中\((4,4)\)位置的数据后,下一个准备读取其周围\((6,6)\)的数据,由于\((6,6)\)的数据在内存中地址距离\((4,4)\)数据地址较远,无法一次加载到cache中,即出现cache miss,这样我们就只能从内存中重新加载数据,无法利用高速缓存,影响数据读取性能。
针对这个问题,可以使用Hilbert曲线或者Morton曲线这类特殊的映射方式进行坐标映射