<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>limil&#39;s blog</title>
  
  
  <link href="https://www.limil.top/atom.xml" rel="self"/>
  
  <link href="https://www.limil.top/"/>
  <updated>2026-09-27T05:29:20.000Z</updated>
  <id>https://www.limil.top/</id>
  
  <author>
    <name>limil</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>法线矩阵和TBN矩阵</title>
    <link href="https://www.limil.top/normal-transformation-and-tbn/"/>
    <id>https://www.limil.top/normal-transformation-and-tbn/</id>
    <published>2024-09-25T18:55:58.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="法线变换矩阵">法线变换矩阵</h2><p>经过模型变换后，法线也要跟着一起变换。假设物体发生了缩放形变，如果对法线应用相同的变换，可能会得到错误结果： <span id="more"></span></p><p><img src="../images/normal-transformation-and-tbn/image-20220818231018047_repeat_1660835421244__621687.png" /></p><p>假设平面上一条切线<span class="math inline">\(\mathbf{t}\)</span>（比如说，向量<span class="math inline">\(\text{AB}\)</span>），变换矩阵为<span class="math inline">\(M\)</span>。经过变换后<span class="math inline">\(\mathbf{t}&#39;=M\mathbf{t}\)</span>（变换后<span class="math inline">\(\mathbf{t}&#39;=M\text{B}-M\text{A}=M(\text{B}-\text{A})=M\mathbf{t}\)</span>）。变换前后，<span class="math inline">\(\mathbf{n}\)</span>都应该和切线垂直，可得： <span class="math display">\[\text{dot}(\mathbf{t&#39;}, \mathbf{n}&#39;)=\text{dot}(M\mathbf{t},N\mathbf{n})=\mathbf{t}^TM^TN\mathbf{n}=0\]</span> 很显然，当<span class="math inline">\(M^TN\)</span>为单位矩阵时上式成立。可得<span class="math inline">\(N=(M^{T})^{-1}\)</span>。因此对法线应该作用变换矩阵转置的逆，可以保持法线垂直于变换后的平面。可以看出，如果只有平移和旋转，那么<span class="math inline">\(N=M\)</span>（平移对法线没影响，旋转矩阵的转置即为逆），这也说明了法线变换问题是形变带来的。</p><h2 id="tbn矩阵">TBN矩阵</h2><h3 id="用途">用途</h3><p>为物体添加细节的方法除了提高模型的精度，可以使用法线贴图。法线贴图利用图片的三通道存储法线的三个分量。法线贴图可以存储模型空间下法线的值，这样出来的法线贴图看起来就五彩缤纷；也可以存储为在<strong>切线空间</strong>下的值，在这个空间中，平面的法线为N轴，u、v的方向为T、B轴，如下图所示：</p><p><img src="../images/normal-transformation-and-tbn/normal_mapping_tbn_vectors.png" /></p><p>在法线贴图中三个通道RGB分别用于存储T（tangent）、B（bitangent）、N（normal）轴的分量。</p><p>模型空间下法线存储贴图是法线变化更加平滑，缺点是一张贴图只能对应一个模型。而切线空间下的法线贴图可以在任何模型之间切换，而且大部分法线都和N靠的比较近（更加靠近<span class="math inline">\((0,0,1)\)</span>，这就是为什么大部分法线贴图都偏蓝），更加便于压缩。</p><p>因此，只要能求出在模型空间下T、B、N轴的向量，按照贴图中记录的分量线性组合就可以得到模型空间下的法线。<strong>TBN矩阵就是贴图中向量到世界空间的变换矩阵</strong>。注意，T、B、N是正交的单位坐标系，因此TBN矩阵也是正交矩阵。</p><h3 id="求法">求法</h3><p><img src="../images/normal-transformation-and-tbn/normal_mapping_surface_edges.png" /></p><p>因为U，V方向分别是T，B轴，如上图所示，模型空间下三角面片边<span class="math inline">\(E_1\)</span>和<span class="math inline">\(E_2\)</span>可以表示成<span class="math inline">\(T\)</span>和<span class="math inline">\(B\)</span>轴的线性组合： <span class="math display">\[\left[ \begin{matrix} E_1\\ E_2 \end{matrix} \right]=\left[ \begin{matrix} \Delta U_1 &amp; \Delta V_1\\ \Delta U_2 &amp; \Delta V_2\end{matrix} \right] \left[ \begin{matrix} T\\ B \end{matrix} \right]\]</span> 很显然，我们除了<span class="math inline">\(T\)</span>、<span class="math inline">\(B\)</span>，其它信息都知道，所以可以得到： <span class="math display">\[\left[ \begin{matrix} \Delta U_1 &amp; \Delta V_1\\ \Delta U_2 &amp; \Delta V_2\end{matrix} \right]^{-1} \left[ \begin{matrix} E_1\\ E_2 \end{matrix} \right]= \frac{1}{\Delta U_1\Delta V_2 - \Delta U_2\Delta V_1} \left[ \begin{matrix} \Delta V_2 &amp; -\Delta V_1\\ -\Delta U_2 &amp; \Delta U_1\end{matrix} \right] \left[ \begin{matrix} E_1\\ E_2 \end{matrix} \right]= \left[ \begin{matrix} T\\ B \end{matrix} \right]\]</span></p><p>这样就得到T，B轴，加上顶点法线N，就可以构造TBN矩阵了。等等！其实还没完。有以下几点需要注意：</p><ol type="1"><li>顶点法线<span class="math inline">\(\neq\)</span>平面法线：平面法线是垂直于面片的向量；顶点法线可以是任意的，比如艺术家的调整、相邻平面法线的平均（平滑）...</li><li>这样求出的T、B、N不一定正交。这是很正常的，可能物体被拉伸，可能uv映射不是完美对应的，加上N不是平面法线而是顶点法线，大部分情况下这样算出来的T、B、N都不正交。</li></ol><p>TBN正交是很重要的，如果不正交，变换后的法线可能被拉伸。而理想情况下（TBN为正交矩阵）应该只是对法线进行旋转而已。</p><p>因此还应当进行格拉姆-施密特正交化（<span class="math inline">\(T&#39;\)</span>代表上面求出的，<span class="math inline">\(T\)</span>和<span class="math inline">\(B\)</span>是真正的切线和副切线）： <span class="math display">\[\begin{aligned}T &amp;= \text{normalize}(T-(N\cdot T&#39;)N) \\B &amp;= N \times T\end{aligned}\]</span> 上式其实就是正交分解的逆过程。</p><p>最后，假设从法线贴图中得到的法线是<span class="math inline">\(n\)</span>（注意贴图中的法线非负做了偏移，真正的法线为<span class="math inline">\(2n-1\)</span>），那么真正模型空间的法线为： <span class="math display">\[n_xT+n_yB+n_zN=[T,B,N]n\]</span> 这个<span class="math inline">\([T,B,N]\)</span>就是TBN矩阵。</p><p>在加载模型时，就可以对每个面片计算出T和B。因此T，B和N都是作为顶点属性传入着色器。为了保证TBN正交，可以在片元着色器中正交化后构造为TBN矩阵后使用。</p><h2 id="reference">Reference</h2><ul><li><a href="https://learnopengl-cn.github.io/05%20Advanced%20Lighting/04%20Normal%20Mapping/">法线贴图 - LearnOpenGL CN (learnopengl-cn.github.io)</a></li><li><a href="https://zhuanlan.zhihu.com/p/139593847">切线空间（Tangent Space）完全解析 - 知乎 (zhihu.com)</a></li><li><a href="https://paroj.github.io/gltut/Illumination/Tut09%20Normal%20Transformation.html">Normal Transformation</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;法线变换矩阵&quot;&gt;法线变换矩阵&lt;/h2&gt;
&lt;p&gt;经过模型变换后，法线也要跟着一起变换。假设物体发生了缩放形变，如果对法线应用相同的变换，可能会得到错误结果：</summary>
    
    
    
    <category term="图形" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/"/>
    
    <category term="基础" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/%E5%9F%BA%E7%A1%80/"/>
    
    
  </entry>
  
  <entry>
    <title>图形渲染管线</title>
    <link href="https://www.limil.top/render-pipeline/"/>
    <id>https://www.limil.top/render-pipeline/</id>
    <published>2024-09-25T18:50:59.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<p>大体上分，可以分为：应用阶段、几何处理阶段、光栅化阶段。在知乎上偷了一个图：</p><p><img src="../images/render-pipeline/v2-b6bf72b3de3a5603a9048522f8886f03_720w.webp" /></p><span id="more"></span><h3 id="应用阶段">应用阶段</h3><p>CPU传入数据到显存，设置参数，调用DrawCall通知显卡绘制图像。</p><h3 id="几何处理阶段">几何处理阶段</h3><p>对顶点数据的几何处理，例如位移、细分网格等。<strong>顶点着色器</strong>、<strong>曲面细分着色器</strong>、几何着色器在这个阶段发挥作用。</p><p>在这个阶段，会进行各种<strong>变换</strong>：MVP变换、裁剪、透视除法、视口变换，转换到屏幕空间上，以便光栅化。</p><h3 id="光栅化阶段">光栅化阶段</h3><p>首先是<strong>图元装配</strong>，将一个个顶点组装为三角形。可能是每3个顶点一组一个三角形，也可能是triangle-strip那样组织三角形。然后是遍历三角形，<strong>插值属性</strong>，调用片元着色器着色。这里的片元不一定是像素，像MSAA一个像素可能会渲染4个片元求平均。插值也要进行<strong>透视矫正</strong>，直接按照屏幕空间坐标插值会出来错误的效果。</p><p>在这个阶段，会可能会进行：深度测试，混合（或透明度测试）。以便实现遮挡或半透明效果。如果深度测试在片元着色器之前进行，称为early-z，可以减少片元着色器的大开销（因为深度值可以提前插值出来，不依赖片元着色器）。</p><h3 id="参考">参考</h3><ul><li><a href="https://zhuanlan.zhihu.com/p/137780634">猴子也能看懂的渲染管线（Render Pipeline） - 知乎 (zhihu.com)</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;大体上分，可以分为：应用阶段、几何处理阶段、光栅化阶段。在知乎上偷了一个图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/render-pipeline/v2-b6bf72b3de3a5603a9048522f8886f03_720w.webp&quot; /&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="图形" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/"/>
    
    <category term="渲染" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/%E6%B8%B2%E6%9F%93/"/>
    
    
  </entry>
  
  <entry>
    <title>光栅管线中的变换</title>
    <link href="https://www.limil.top/transform/"/>
    <id>https://www.limil.top/transform/</id>
    <published>2024-09-25T18:45:16.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="../images/transform/image_62_repeat_1658128206100__904881.png" /></p><p>物体在呈现在屏幕前，要经过一系列的变换。前三步（Modeling transformation、Camera transformation、Projection transformation）都由我们自己完成，最后一步由显卡完成（包括裁剪、Viewport transformation、光栅化等步骤）。前三步也称为MVP变换。</p><p>下文均是建立在右手坐标系，相机朝向z轴负方向。</p><span id="more"></span><h2 id="一模型变换">一、模型变换</h2><p>模型变换就是将模型从模型坐标系变换到世界坐标系中。</p><p>位移矩阵： <span class="math display">\[T(x,y,z)=\left[ \begin{matrix} 1 &amp; 0 &amp; 0 &amp; x \\ 0 &amp; 1 &amp; 0 &amp; y \\ 0 &amp; 0 &amp; 1 &amp; z \\ 0 &amp; 0 &amp; 0 &amp; 1\end{matrix} \right]\]</span> 缩放矩阵： <span class="math display">\[S(x,y,z)=\left[ \begin{matrix} x &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; y &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; z &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 1\end{matrix} \right]\]</span> 旋转矩阵：（略）</p><p>最后先缩放，再旋转，再位移，可得模型矩阵： <span class="math display">\[M=T R S\]</span></p><h2 id="二相机变换">二、相机变换</h2><p>相机变换是将世界坐标系下所有物体转换到以相机位置为原点的相机坐标系。由于相机朝向z轴负方向，转换后只有那些z坐标为负值的物体才会被处理。</p><p>只需给每个物体乘上相机模型变换矩阵的逆矩阵即可（相机一般不考虑缩放）： <span class="math display">\[V=M_{\text{camera}}^{-1}=R^{-1}T^{-1}=R^T(-T)\]</span> 相机的旋转可以用欧拉角表示，可以用四元数表示，可以用lookat矩阵表示。只需转成旋转矩阵，转置即可。</p><h2 id="三透视变换">三、透视变换</h2><p>用于将相机坐标系中可见区域（即相机观察的范围）拉伸变换成为<span class="math inline">\([-1,1]^3\)</span>的区域，便于后续处理。</p><h3 id="正交投影矩阵">1、正交投影矩阵</h3><p>相机朝向负半轴，z坐标越大代表离相机越近。为了符合直觉，这里会翻转一下z轴，使得z坐标越大代表离相机越远，这样旋性就从右手系到左手系了。</p><p>假设近平面（n）、远平面（f）、左平面（l）、右平面（r）、上平面（t）、下平面（b）坐标已知，有正交投影矩阵（注意缩放部分有个负项，用于翻转z轴）： <span class="math display">\[M_{\text{ortho}}=\underbrace{\left[ \begin{matrix} \frac{2}{|r-l|} &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; \frac{2}{|t-b|} &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; -\frac{2}{|n-f|} &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 1\end{matrix} \right]}_{缩放} \underbrace{\left[ \begin{matrix} 1 &amp; 0 &amp; 0 &amp; -\frac{l+r}{2} \\ 0 &amp; 1 &amp; 0 &amp; -\frac{t+b}{2} \\ 0 &amp; 0 &amp; 1 &amp; \frac{n+f}{2} \\ 0 &amp; 0 &amp; 0 &amp; 1\end{matrix} \right]}_{位移}\]</span></p><p><img src="../images/transform/image-20220820131810133_repeat_1660972691450__977171.png" /></p><h3 id="透视投影矩阵">2、透视投影矩阵</h3><p><img src="../images/transform/image-20220820131949747_repeat_1660972790989__464379.png" /></p><p>透视投影分两步，第一步透视变换将视锥体区域变换成矩形体区域，然后第二步应用正交矩阵即可。</p><p>透视变换参数有张角（fov，设为<span class="math inline">\(\theta\)</span>）、宽高比（ratio，设为<span class="math inline">\(d\)</span>）、远近平面距离（<span class="math inline">\(n\)</span>，<span class="math inline">\(f\)</span>，由于是距离，所以是绝对值）。</p><p>由于三维空间下该变换不是线性变换，无法用矩阵的乘法表；所以使用齐次坐标来表示，在四维空间下透视变换就是线性的了。以下是等价的表示： <span class="math display">\[\left[ \begin{matrix} x \\ y \\ z \\ w\end{matrix} \right]\equiv \left[ \begin{matrix} \frac{x}{w} \\ \frac{y}{w} \\ \frac{z}{w} \\ 1\end{matrix} \right]\]</span> 对于点<span class="math inline">\((x,y,z,1)\)</span>，由相似三角形，经过变换以后有<span class="math inline">\(y&#39;=\frac{n}{|z|}y\)</span>、<span class="math inline">\(x&#39;=\frac{n}{|z|}x\)</span>。</p><p><img src="../images/transform/image-20220820134620101_repeat_1660974381520__870506.png" /></p><p>$$ M_{ } =</p><p><span class="math display">\[由上式可以确定\]</span> M_{ }= $$ 其中<span class="math inline">\(A\)</span>、<span class="math inline">\(B\)</span>、<span class="math inline">\(C\)</span>、<span class="math inline">\(D\)</span>是未知数。</p><p>由变换前后，近平面和远平面点的z坐标不变这个性质，可得： <span class="math display">\[M_{\text{perspective} \to \text{ortho}}\left[ \begin{matrix} x \\ y \\ -n \\ 1\end{matrix}\right]=\left[ \begin{matrix} nx \\ ny \\ Ax+By-nC+D \\ n\end{matrix}\right]=\left[ \begin{matrix} nx \\ ny \\ n\cdot (-n) \\ n\end{matrix}\right]\]</span></p><p><span class="math display">\[M_{\text{perspective} \to \text{ortho}}\left[ \begin{matrix} x \\ y \\ -f \\ 1\end{matrix}\right]=\left[ \begin{matrix} fx \\ fy \\ Ax+By-fC+D \\ f\end{matrix}\right]=\left[ \begin{matrix} fx \\ fy \\ f\cdot (-f) \\ f\end{matrix}\right]\]</span></p><p>解得<span class="math inline">\(A=B=0\)</span>，<span class="math inline">\(C=n+f\)</span>，<span class="math inline">\(D=nf\)</span></p><p>故 <span class="math display">\[M_{\text{perspective} \to \text{ortho}}=\left[ \begin{matrix} n &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; n &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; n+f &amp; nf \\ 0 &amp; 0 &amp; -1 &amp; 0\end{matrix}\right]\]</span> 而<span class="math inline">\(t=-b=\tan(\frac{\theta}{2})\cdot n\)</span>，<span class="math inline">\(r=-l=t \cdot d\)</span>。至此，所有量都知道了，可得 <span class="math display">\[M_{\text{perspective}}=M_{\text{ortho}}M_{\text{perspective} \to \text{ortho}}\]</span></p><h3 id="透视投影带来的问题">3、透视投影带来的问题</h3><p>经过透视投影后的坐标，称为齐次坐标，本质上是四维空间中的坐标。要执行一次透视除法，即各分量除以w分量，才是真正转换到NDC空间中。</p><p>透视除法将变换前后z的线性关系变成了非线性关系，将直线从0中间断开。下图是透视变换前后z的关系。</p><p><img src="../images/transform/image-20220820143527647_repeat_1660977328897__726496.png" /></p><p>可以发现，如果有一个三角面片跨越了z轴零点，会导致值符号的突变。<span class="math inline">\(0&lt;z&lt;n\)</span>时，<span class="math inline">\(z&#39;\)</span>小于0；<span class="math inline">\(z&lt;0\)</span>时，<span class="math inline">\(z&#39;\)</span>突然又大于0了。想象一下，一个三角形一个角明明穿过相机到相机的背面，但是计算出来的结果突然就变成在相机前面的无穷远处。当z坐标为0时，w分量也为0，此时一旦进行透视除法，会导致除0操作，搞出一个无穷出来，这将导致后面光栅化插值出非常鬼畜的结果。</p><p>如果所有点都处于视锥体范围内时，直接进行透视除法就不会有上面问题。因此乘完透视矩阵之后，不要急着进行透视除法，而是应该先将物体在视锥体外的部分裁剪掉，再进行透视除法。所有齐次坐标<span class="math inline">\(-w\le x,y,z\le w\)</span>范围外的点都应当被裁剪掉。如果图省事，也可以只裁剪穿过近平面的点。</p><p>裁剪无需我们自己完成，显卡会帮我们完成。OpenGL会在顶点着色器到片元着色器之间进行裁剪，再进行透视除法。这就是为什么我们无需在顶点着色器中手动进行透视除法，因为OpenGL会在后面做一次透视除法。反而，如果我们在没裁剪前手动进行了透视除法，OpenGL就没办法在齐次坐标下进行裁剪，最后反而会出错。</p><p>下面将介绍如何进行裁剪。</p><h4 id="裁剪三角形">3.1、裁剪三角形</h4><p>传统剔除有三种，发生顺序依次是：<strong>视锥剔除、视口剔除、背面剔除</strong>。其中视锥剔除和视口剔除比较难以区分。视锥剔除发生CPU中，通过AABB盒、OBB盒等将物体包围起来，然后与视锥体做碰撞检测，直接剔除掉完全不可见的物体，粒度较大；视口剔除就是上面提到的裁剪：将视锥体外不可见部分剔除，可见部分留下。下面介绍的是视口剔除。</p><p>回顾一下，平面方程：<span class="math inline">\(N\cdot(q-p)=0\)</span>，三维平面可以用四维向量表示为<span class="math inline">\(C=(N_x,N_y,N_z,-N\cdot q)\)</span>，其中<span class="math inline">\(N\)</span>是平面法线，<span class="math inline">\(q\)</span>是平面上一点。如果齐次坐标<span class="math inline">\(p=(x,y,z,w)\)</span>在平面上，当且仅当<span class="math inline">\(p\cdot C=0\)</span>。如果<span class="math inline">\(w=1\)</span>，<span class="math inline">\(N\)</span>为单位向量，<span class="math inline">\(p\cdot C\)</span>的几何意义就是三维点<span class="math inline">\(p\)</span>到平面<span class="math inline">\(C\)</span>的距离。<span class="math inline">\(p\cdot C\)</span>的符号代表点在<span class="math inline">\(p\)</span>的那一侧（大于0代表在法线指向一侧）。</p><p>裁剪三角形就是用一个平面分别截取三角形的两条边，本质上就是求线段和平面的交点。</p><p><img src="../images/transform/image-20220820164324164_repeat_1660985005288__335315_repeat_1661527720377__264048.png" /></p><p>假设线段两端点的分别为<span class="math inline">\(a\)</span>、<span class="math inline">\(b\)</span>，线段上点<span class="math inline">\(p\)</span>在平面<span class="math inline">\(C\)</span>上，有 <span class="math display">\[\begin{aligned}p\cdot C=0 \\(a+t(b-a))\cdot C = 0 \\t = \frac{a\cdot C}{a\cdot C - b \cdot C}\end{aligned}\]</span> 通过<span class="math inline">\(t\)</span>可以求得边<span class="math inline">\(ab\)</span>和平面的交点。而且，顶点除了位置信息，交点的属性也可以使用这个<span class="math inline">\(t\)</span>插值出来。</p><p>有了上面基础后，下面介绍一个算法，叫做<strong>Sutherland–Hodgman</strong>多边形裁剪算法。</p><p><img src="../images/transform/v2-f7f5afaeaa3237b414c1edc2e6e317ab_r.jpg" /></p><p>它主要思想就是依次使用裁剪平面对多边形裁剪，具体流程如下：</p><ul><li>将多边形的所有顶点输入一个数组output作为原始输入；</li><li>for循环，每次操作一条裁剪框边缘；</li><li>使用一个input数组接收output数组，output清零；</li><li>对于input，把第一个点置为当前点P，把最后一个点置为上一点S；</li><li>这时候S-P连线共有四种情况：<ul><li>线段两端均在平面可见侧（点乘大于0）：将P放入output</li><li>线段两端均在平面不可见侧（点乘小于0）：不做操作</li><li>P在不可见侧，S在可见侧：将S-P与裁剪框交点L放入output</li><li>P在可见侧，S在不可见侧：先放入交点L再放入交点P</li></ul></li><li>令S = P ，P取数组中下一点，重复操作直到遍历到数组中最后一点。</li></ul><p>这样遍历所有裁剪面，就能获得新的顶点序列，对这个序列三角剖分即可。</p><p>透视变换后，视锥体变成了统一的立方体，各个面的法线朝内指向零点，方程非常简单，分别是</p><table><thead><tr class="header"><th>平面</th><th>方程</th></tr></thead><tbody><tr class="odd"><td>near</td><td><span class="math inline">\((0,0,1,1)\)</span></td></tr><tr class="even"><td>far</td><td><span class="math inline">\((0,0,-1,1)\)</span></td></tr><tr class="odd"><td>left</td><td><span class="math inline">\((1,0,0,1)\)</span></td></tr><tr class="even"><td>right</td><td><span class="math inline">\((-1,0,0,1)\)</span></td></tr><tr class="odd"><td>top</td><td><span class="math inline">\((0,1,0,1)\)</span></td></tr><tr class="even"><td>bottom</td><td><span class="math inline">\((0,-1,0,1)\)</span></td></tr></tbody></table><p><img src="../images/transform/image-20220909111433270_repeat_1662693278723__413108.png" /></p><p>综上，用上面平面依次对三角形裁剪后，在组装起来，就可以实现视口剔除。</p><p>题外话，可以指定容易的平面进行裁剪，不一定只能是视锥体的各个面。如果想要实现一些特殊效果（如传送门、镜子），需要剔除虚拟相机前面遮挡的物体，就可以用上面的方法。当然，一个设定容易裁剪平面的更简单的方法请参考Referenece。</p><h2 id="四视点变换">四、视点变换</h2><p>视点变换就相当简单了，就是将NDC空间xy的<span class="math inline">\([-1,1] \times [-1,1]\)</span>拉伸到屏幕大小<span class="math inline">\([0,w]\times [0,h]\)</span>，z坐标不用管。这样处理后方便后续光栅化。</p><p><span class="math display">\[M_{\text{viewport}}=\left[ \begin{matrix} \frac{w}{2} &amp; 0 &amp; 0 &amp; \frac{w}{2} \\ 0 &amp; \frac{h}{2} &amp; 0 &amp; \frac{h}{2} \\ 0 &amp; 0 &amp; 1 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 1\end{matrix}\right]\]</span></p><h2 id="reference">Reference</h2><ul><li>Oblique View Frustum Depth Projection and Clipping - Eric Lengyel</li><li><a href="https://zhuanlan.zhihu.com/p/97371838">从零开始的软渲染器（2.5）- 再谈裁剪与剔除 - 知乎 (zhihu.com)</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;../images/transform/image_62_repeat_1658128206100__904881.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;物体在呈现在屏幕前，要经过一系列的变换。前三步（Modeling transformation、Camera transformation、Projection transformation）都由我们自己完成，最后一步由显卡完成（包括裁剪、Viewport transformation、光栅化等步骤）。前三步也称为MVP变换。&lt;/p&gt;
&lt;p&gt;下文均是建立在右手坐标系，相机朝向z轴负方向。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>光栅化</title>
    <link href="https://www.limil.top/rasterization/"/>
    <id>https://www.limil.top/rasterization/</id>
    <published>2024-09-25T18:39:41.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="一三角形光栅化">一、三角形光栅化</h2><p>三角形光栅化就是将三角面片覆盖的像素绘制出来，从而在屏幕上可见。有几个目标需要实现：</p><ul><li><p><strong>不重复的</strong>：不重复绘制像素，例如相邻的三角形之间公共边覆盖到的像素应当只被绘制一次。</p></li><li><p><strong>无缝的</strong>：例如相邻的三角形之间光栅化出来应当是没有缝隙的，连续的。</p></li></ul><span id="more"></span><h3 id="遍历三角形">遍历三角形</h3><p><img src="../images/rasterization/v2-ef7ec28244a75fa536834456fc6d4b1b_720w.webp" /></p><p>这一步用来确定哪些范围内含有被三角形覆盖的像素。</p><p>有几种方法：</p><ul><li>遍历三角形<strong>包围盒</strong>范围内像素，逐像素判断是否被三角形覆盖。判断使用的是边界函数算法，也称为半空间法。</li><li>扫描线法。</li></ul><p>最简单就是半空间法，虽然暴力，但是容易并行执行。</p><h3 id="边界函数算法">边界函数算法</h3><h4 id="判断点三角形内部">判断点三角形内部</h4><p>以半空间法为例，最重要的就是判断一个像素点是否在三角形的内部了。如果在内部，我们就插值并着色，否则就丢弃。</p><p>三角形每条边所在的直线分别将空间分成了两个部分。通过直线的边界函数的符号，可以判断一个点在直线的左侧还是右侧。如果一个点均在三边的同侧（即符号相同），说明点在三角形内侧；如果规定了三角形的顺序对应的正反，也可以通过符号确定当前三角形是正面还是反面（常用的是三角形逆时针方向为正面）。</p><p><img src="../images/rasterization/rasterization-triangle4_repeat_1666493239643__913538.png" /></p><p>对于边<span class="math inline">\(AB\)</span>，其边界函数为： <span class="math display">\[E_{AB}(P)=(x_B-x_A)(y_P-y_A)-(x_P-x_A)(y_B-y_A)\]</span> 其实本质就是一个叉乘，反映了向量<span class="math inline">\(AB\)</span>和<span class="math inline">\(AP\)</span>构成的平行四边形的有向面积。</p><p>由此可得： <span class="math display">\[\begin{aligned}E_{AB}(P)&gt;0 \\E_{BC}(P)&gt;0 \\E_{CA}(P)&gt;0\end{aligned}\]</span></p><p>满足上式，说明点在三角形内部。</p><h4 id="当点在三角形边上">当点在三角形边上</h4><p>当边界函数值为0时，说明点可能在三角形的边界上（如图中红色点）。最简单的方法是直接将在三角形边界上的点视作在三角形内部，直接绘制。但是这样做会重复绘制共边上的像素。在不透明渲染中这样没问题，但是在半透明渲染时，多余的绘制会导致颜色不正确。因此需要一些方法来避免重复绘制。</p><p><img src="../images/rasterization/image_103_repeat_1666494117472__896545.png" /></p><p>常用<strong>top-left</strong>规则来渲染在三角形边上的像素，根据所在边的类型不同，选择是否渲染。这个方法对于<strong>水密（watertight）</strong>的模型是没问题的，但是对于面片（例如一面仅由两个三角面片构成的墙），可能出现边渲染缺失的情况。</p><p>边的类型如下图所示（注意三角形顶点顺序！图中将<strong>顺时针</strong>作为三角形<strong>正面</strong>，如果你用的是<strong>逆时针</strong>顺序为正面，就要反过来）：</p><p><img src="../images/rasterization/image_104_repeat_1666495783356__941503.png" /></p><ul><li>top边就是完全水平的边，并且其两端点在第三个点的上面。具体来说（以上图顺时针为例），就是边的y坐标差值为0，x坐标差值为正。</li><li>left边就是往上走的边（如果是逆时针，那就往下走，就是反过来嘛）。具体来说（以上图顺时针为例），就是y坐标差值为正。</li></ul><p>综上，我们就达成了光栅化第一目标。下面给出伪代码，注意下面的伪代码顶点是定义在<strong>逆时针</strong>顺序上的，请自行转换：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">vec2 edge0 = v[<span class="number">2</span>] - v[<span class="number">1</span>]; </span><br><span class="line">vec2 edge1 = v[<span class="number">0</span>] - v[<span class="number">2</span>]; </span><br><span class="line">vec2 edge2 = v[<span class="number">1</span>] - v[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="type">bool</span> overlaps = <span class="literal">true</span>;</span><br><span class="line"><span class="function">vec2 <span class="title">P</span><span class="params">(x + <span class="number">0.5</span>, y + <span class="number">0.5</span>)</span></span>;</span><br><span class="line"><span class="type">float</span> w0 = <span class="built_in">edge_function</span>(v[<span class="number">1</span>], v[<span class="number">2</span>], P);</span><br><span class="line"><span class="type">float</span> w1 = <span class="built_in">edge_function</span>(v[<span class="number">2</span>], v[<span class="number">0</span>], P);</span><br><span class="line"><span class="type">float</span> w2 = <span class="built_in">edge_function</span>(v[<span class="number">0</span>], v[<span class="number">1</span>], P);</span><br><span class="line">                        <span class="comment">//top edge                       //left edge    //inside</span></span><br><span class="line">overlaps &amp;= (w0 == <span class="number">0</span> ? ((edge0.y == <span class="number">0</span> &amp;&amp; edge0.x &lt; <span class="number">0</span>) || edge0.y &lt; <span class="number">0</span>) : (w0 &gt; <span class="number">0</span>); </span><br><span class="line">overlaps &amp;= (w1 == <span class="number">0</span> ? ((edge1.y == <span class="number">0</span> &amp;&amp; edge1.x &lt; <span class="number">0</span>) || edge1.y &lt; <span class="number">0</span>) : (w1 &gt; <span class="number">0</span>)); </span><br><span class="line">overlaps &amp;= (w2 == <span class="number">0</span> ? ((edge2.y == <span class="number">0</span> &amp;&amp; edge2.x &lt; <span class="number">0</span>) || edge2.y &lt; <span class="number">0</span>) : (w2 &gt; <span class="number">0</span>)); </span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(overlaps) <span class="built_in">shade</span>(P);</span><br></pre></td></tr></table></figure><h3 id="如何无缝">如何无缝</h3><p>光栅化第二个目标是无缝。缝隙往往出现在模型的接缝处，并且出现缝的原因往往是精度误差带来的。</p><p>如果使用浮点数参与边界函数的计算，误差是无可避免的。对于接缝处的计算，需要判0，这是浮点数不擅长的。而且涉及很多乘法，这会放大误差。有一些方法可以改善，例如先做差再相乘比先相乘再做差更好；双精度浮点数。但这都是治标不治本的。</p><p>可以发现，整个光栅化整个过程中（不包括插值和着色），只涉及到乘法和加法（比如边界函数）。因此如果能转换为整数，虽然整体上可能会丢失一些精度，但是这样边界函数结果将不会有误差，接缝处的计算将永远是准确的。</p><p>因此对可以预先对三角形三个顶点四舍无入为整数，再进行后续判断。改进后的伪代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vec2i是vec2的整数版本，vec2到vec2i转换过程中各分量会四舍五入</span></span><br><span class="line">vi[<span class="number">0</span>] = <span class="built_in">vec2i</span>(v[<span class="number">0</span>]);</span><br><span class="line">vi[<span class="number">1</span>] = <span class="built_in">vec2i</span>(v[<span class="number">1</span>]);</span><br><span class="line">vi[<span class="number">2</span>] = <span class="built_in">vec2i</span>(v[<span class="number">2</span>]);</span><br><span class="line">vec2i edge0 = vi[<span class="number">2</span>] - vi[<span class="number">1</span>]; </span><br><span class="line">vec2i edge1 = vi[<span class="number">0</span>] - vi[<span class="number">2</span>]; </span><br><span class="line">vec2i edge2 = vi[<span class="number">1</span>] - vi[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="type">bool</span> overlaps = <span class="literal">true</span>;</span><br><span class="line"><span class="function">vec2i <span class="title">P</span><span class="params">(x, y)</span></span>;</span><br><span class="line"><span class="type">int</span> w0 = <span class="built_in">edge_function</span>(vi[<span class="number">1</span>], vi[<span class="number">2</span>], P);</span><br><span class="line"><span class="type">int</span> w1 = <span class="built_in">edge_function</span>(vi[<span class="number">2</span>], vi[<span class="number">0</span>], P);</span><br><span class="line"><span class="type">int</span> w2 = <span class="built_in">edge_function</span>(vi[<span class="number">0</span>], vi[<span class="number">1</span>], P);</span><br><span class="line">                        <span class="comment">//top edge                       //left edge    //inside</span></span><br><span class="line">overlaps &amp;= (w0 == <span class="number">0</span> ? ((edge0.y == <span class="number">0</span> &amp;&amp; edge0.x &lt; <span class="number">0</span>) || edge0.y &lt; <span class="number">0</span>) : (w0 &gt; <span class="number">0</span>); </span><br><span class="line">overlaps &amp;= (w1 == <span class="number">0</span> ? ((edge1.y == <span class="number">0</span> &amp;&amp; edge1.x &lt; <span class="number">0</span>) || edge1.y &lt; <span class="number">0</span>) : (w1 &gt; <span class="number">0</span>)); </span><br><span class="line">overlaps &amp;= (w2 == <span class="number">0</span> ? ((edge2.y == <span class="number">0</span> &amp;&amp; edge2.x &lt; <span class="number">0</span>) || edge2.y &lt; <span class="number">0</span>) : (w2 &gt; <span class="number">0</span>)); </span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(overlaps) <span class="built_in">shade</span>(<span class="built_in">vec2</span>(P));</span><br></pre></td></tr></table></figure><p>仅仅一个像素的精度丢失，无伤大雅。</p><h2 id="二直线光栅化">二、直线光栅化</h2><h3 id="数值微分dda">1. 数值微分DDA</h3><p>根据斜截：<span class="math inline">\(y=kx+b\)</span>。假设<span class="math inline">\(k&lt;1\)</span>，这样<span class="math inline">\(y\)</span>增长比<span class="math inline">\(x\)</span>慢，所以就可以每次向右前进一个像素，判断这个<span class="math inline">\(x\)</span>坐标下的<span class="math inline">\(y\)</span>是多少。</p><p>对于<span class="math inline">\(k\)</span>的其它情况，做相应的交换即可。例如<span class="math inline">\(k&gt;1\)</span>，就交换<span class="math inline">\(x\)</span>和<span class="math inline">\(y\)</span>，就变回<span class="math inline">\(k&lt;1\)</span>的情况了。</p><p>由<span class="math inline">\(y_{i+1}=k(x+1)+b=(kx+b)+k=y_{i}+k\)</span>，可以发现<span class="math inline">\(x\)</span>每加1，<span class="math inline">\(y\)</span>加<span class="math inline">\(k\)</span>即可。代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">drawlineDDA</span><span class="params">(<span class="type">int</span> x0,<span class="type">int</span> y0,<span class="type">int</span> x1,<span class="type">int</span> y1, Framebuffer &amp;fb)</span> </span>&#123;</span><br><span class="line"><span class="type">int</span> dx = <span class="built_in">abs</span>(x0 - x1), dy = <span class="built_in">abs</span>(y0 - y1);</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, y0);</span><br><span class="line"><span class="built_in">swap</span>(x1, y1);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(x0 &gt; x1) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, x1);</span><br><span class="line"><span class="built_in">swap</span>(y0, y1);</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">double</span> k = (y1 - y0) * <span class="number">1.0</span> / (x1 - x0);</span><br><span class="line"><span class="type">double</span> y = y0;</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">auto</span> x = x0 ; x &lt;= x1 ; ++x) &#123;</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(y, x);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(x, y);</span><br><span class="line">&#125;</span><br><span class="line">y += k;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>涉及浮点数的计算，效率较慢。</p><h3 id="中点画线算法">2. 中点画线算法</h3><p>将直线写成<span class="math inline">\(Ax+By+C=0\)</span>。整个方程将平面划分成三部分：<span class="math inline">\(Ax+By+C\)</span>分别大于等于小于0三部分。<strong>当<span class="math inline">\(B\)</span>为正数时</strong>，<span class="math inline">\(Ax+By+C\)</span>大于0代表点<span class="math inline">\((x,y)\)</span>在直线上面；小于0在下面；等于0在直线上。</p><p>以<span class="math inline">\(0&lt;k&lt;1\)</span>为例，<span class="math inline">\(x\)</span>增加1时，<span class="math inline">\(y\)</span>要么增加1，要么不变。因此通过判断<span class="math inline">\(y\)</span>和<span class="math inline">\(y+1\)</span>中点<span class="math inline">\(y+0.5\)</span>相对直线位置，就可以知道哪个离直线更近。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 前面乘B的目的是让B系数为正</span></span><br><span class="line"><span class="keyword">if</span>(B * (A*(x+<span class="number">1</span>) + B*(y+<span class="number">0.5</span>) + C) &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 中点在直线下面，说明(x+1,y+1)离直线更近</span></span><br><span class="line">    y = y + <span class="number">1</span>;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 中点在直线上面，说明(x+1,y)离直线更近</span></span><br><span class="line">y = y;</span><br><span class="line">&#125;</span><br><span class="line">x = x + <span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>下面进行一些优化：</p><ul><li><p>如果已知下一点画在<span class="math inline">\((x_i+1,y_i+1)\)</span>，那么接下来要考虑<span class="math inline">\(M_1(x_i+2,y_i+1.5)\)</span>，带入<span class="math inline">\(M_1\)</span>到直线方程，有 <span class="math display">\[d_1=A(x_i+2)+B(y_i+1.5)+C=(A(x_i+1)+B(y_i+0.5)+C)+A+B=d_0+A+B\]</span></p></li><li><p>如果已知下一个点画在<span class="math inline">\((x_i+1,y_i)\)</span>，那么接下来要考虑<span class="math inline">\(M_1(x_i+2,y_i+0.5)\)</span>，带入<span class="math inline">\(M_1\)</span>到直线方程，有 <span class="math display">\[d_1=A(x_i+2)+B(y_i+0.5)+C=(A(x_i+1)+B(y_i+0.5)+C)+A=d_0+A\]</span></p></li></ul><p>因此判断中点和直线相对位置这里可以用增量的方式计算，避免了乘法。</p><p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初值</span></span><br><span class="line"><span class="type">double</span> d = A * (x0 + <span class="number">1</span>) + B * (y0 + <span class="number">0.5</span>) + C;</span><br><span class="line"><span class="comment">// 前面乘B的目的是让B系数为正</span></span><br><span class="line"><span class="keyword">if</span>(B * d &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 中点在直线下面，说明(x+1,y+1)离直线更近</span></span><br><span class="line">    y = y + <span class="number">1</span>;</span><br><span class="line">    d = d + A + B;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 中点在直线上面，说明(x+1,y)离直线更近</span></span><br><span class="line">y = y;</span><br><span class="line">d = d + A;</span><br><span class="line">&#125;</span><br><span class="line">x = x + <span class="number">1</span>;</span><br></pre></td></tr></table></figure></p><p>可以发现只有<span class="math inline">\(0.5\)</span>这个浮点数，可以全部乘以2，避免浮点数的使用，全部用整数代替。</p><p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初值</span></span><br><span class="line"><span class="type">int</span> d = <span class="number">2</span> * A * (x0 + <span class="number">1</span>) + B * (<span class="number">2</span> * y0 + <span class="number">1</span>) + <span class="number">2</span> * C;</span><br><span class="line"><span class="comment">// 前面乘B的目的是让B系数为正</span></span><br><span class="line"><span class="keyword">if</span>(B * d &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment">// 中点在直线下面，说明(x+1,y+1)离直线更近</span></span><br><span class="line">    y = y + <span class="number">1</span>;</span><br><span class="line">    d = d + <span class="number">2</span> * (A + B);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 中点在直线上面，说明(x+1,y)离直线更近</span></span><br><span class="line">y = y;</span><br><span class="line">d = d + <span class="number">2</span> * A;</span><br><span class="line">&#125;</span><br><span class="line">x = x + <span class="number">1</span>;</span><br></pre></td></tr></table></figure></p><p>对于<span class="math inline">\(k\)</span>的其它情况，做各种转化为<span class="math inline">\(0&lt;k&lt;1\)</span>即可。注意如果<span class="math inline">\(k&lt;0\)</span>，中点判断规则是反过来的。代码如下：</p><p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">drawlineMid</span><span class="params">(<span class="type">int</span> x0,<span class="type">int</span> y0,<span class="type">int</span> x1,<span class="type">int</span> y1, Framebuffer &amp;fb)</span> </span>&#123;</span><br><span class="line"><span class="type">int</span> dx = <span class="built_in">abs</span>(x0 - x1), dy = <span class="built_in">abs</span>(y0 - y1);</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, y0);</span><br><span class="line"><span class="built_in">swap</span>(x1, y1);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(x0 &gt; x1) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, x1);</span><br><span class="line"><span class="built_in">swap</span>(y0, y1);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> A = y1 - y0;</span><br><span class="line"><span class="type">int</span> B = x0 - x1;</span><br><span class="line"><span class="type">int</span> C = -(A * x0 + B * y0);</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> sgn = y0 &lt; y1 ? <span class="number">1</span> : <span class="number">-1</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> d = <span class="number">2</span> * A * (x0 + <span class="number">1</span>) + B * (<span class="number">2</span> * y0 + sgn) + <span class="number">2</span> * C;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> y = y0;</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">auto</span> x = x0 ; x &lt;= x1 ; ++x) &#123;</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(y, x);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(x, y);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(sgn * B * d &lt; <span class="number">0</span>) &#123;</span><br><span class="line">d += <span class="number">2</span> * (A + sgn * B);</span><br><span class="line">y += sgn;</span><br><span class="line">&#125; <span class="keyword">else</span> d += <span class="number">2</span> * A; </span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h3 id="完整代码">完整代码</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Framebuffer</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">Framebuffer</span>(<span class="type">int</span> w, <span class="type">int</span> h) : <span class="built_in">width</span>(w), <span class="built_in">height</span>(h), <span class="built_in">buffer</span>(<span class="keyword">new</span> <span class="type">bool</span>[w * h]) &#123;</span><br><span class="line">        <span class="built_in">clear</span>();</span><br><span class="line">    &#125;</span><br><span class="line">~<span class="built_in">Framebuffer</span>() &#123;<span class="keyword">delete</span> [] buffer;&#125; </span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">set</span><span class="params">(<span class="type">int</span> x, <span class="type">int</span> y, <span class="type">bool</span> value = <span class="literal">true</span>)</span> </span>&#123;</span><br><span class="line"><span class="keyword">if</span>(x &lt; <span class="number">0</span> || x &gt;= width || y &lt; <span class="number">0</span> || y &gt;= height) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">buffer[y * width + x] = value;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">print</span><span class="params">()</span> </span>&#123;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">2</span> * width + <span class="number">2</span>; i++) <span class="built_in">putchar</span>(<span class="string">&#x27;o&#x27;</span>);</span><br><span class="line"><span class="built_in">putchar</span>(<span class="string">&#x27;\n&#x27;</span>);</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = height - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line"><span class="built_in">putchar</span>(<span class="string">&#x27;o&#x27;</span>);</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> j = <span class="number">0</span>; j &lt; width; j++) &#123;</span><br><span class="line"><span class="type">int</span> p = i * width + j;</span><br><span class="line"><span class="keyword">if</span>(buffer[p]) <span class="built_in">printf</span>(<span class="string">&quot;XX&quot;</span>);</span><br><span class="line"><span class="keyword">else</span> <span class="built_in">printf</span>(<span class="string">&quot;  &quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">putchar</span>(<span class="string">&#x27;o&#x27;</span>);</span><br><span class="line"><span class="built_in">putchar</span>(<span class="string">&#x27;\n&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">2</span> * width + <span class="number">2</span>; i++) <span class="built_in">putchar</span>(<span class="string">&#x27;o&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">clear</span><span class="params">()</span> </span>&#123;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = height - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--) &#123;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> j = <span class="number">0</span>; j &lt; width; j++) &#123;</span><br><span class="line"><span class="type">int</span> p = i * width + j;</span><br><span class="line">buffer[p] = <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"><span class="type">int</span> width, height;</span><br><span class="line"><span class="type">bool</span>* buffer;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> w = <span class="number">50</span>, h = <span class="number">50</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">drawlineDDA</span><span class="params">(<span class="type">int</span> x0,<span class="type">int</span> y0,<span class="type">int</span> x1,<span class="type">int</span> y1, Framebuffer &amp;fb)</span> </span>&#123;</span><br><span class="line"><span class="type">int</span> dx = <span class="built_in">abs</span>(x0 - x1), dy = <span class="built_in">abs</span>(y0 - y1);</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, y0);</span><br><span class="line"><span class="built_in">swap</span>(x1, y1);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(x0 &gt; x1) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, x1);</span><br><span class="line"><span class="built_in">swap</span>(y0, y1);</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">double</span> k = (y1 - y0) * <span class="number">1.0</span> / (x1 - x0);</span><br><span class="line"><span class="type">double</span> y = y0;</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">auto</span> x = x0 ; x &lt;= x1 ; ++x) &#123;</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(y, x);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(x, y);</span><br><span class="line">&#125;</span><br><span class="line">y += k;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">drawlineMid</span><span class="params">(<span class="type">int</span> x0,<span class="type">int</span> y0,<span class="type">int</span> x1,<span class="type">int</span> y1, Framebuffer &amp;fb)</span> </span>&#123;</span><br><span class="line"><span class="type">int</span> dx = <span class="built_in">abs</span>(x0 - x1), dy = <span class="built_in">abs</span>(y0 - y1);</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, y0);</span><br><span class="line"><span class="built_in">swap</span>(x1, y1);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(x0 &gt; x1) &#123;</span><br><span class="line"><span class="built_in">swap</span>(x0, x1);</span><br><span class="line"><span class="built_in">swap</span>(y0, y1);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> A = y1 - y0;</span><br><span class="line"><span class="type">int</span> B = x0 - x1;</span><br><span class="line"><span class="type">int</span> C = -(A * x0 + B * y0);</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> sgn = y0 &lt; y1 ? <span class="number">1</span> : <span class="number">-1</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> d = <span class="number">2</span> * A * (x0 + <span class="number">1</span>) + B * (<span class="number">2</span> * y0 + sgn) + <span class="number">2</span> * C;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> y = y0;</span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">auto</span> x = x0 ; x &lt;= x1 ; ++x) &#123;</span><br><span class="line"><span class="keyword">if</span>(dx &lt; dy) &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(y, x);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">fb.<span class="built_in">set</span>(x, y);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(sgn * B * d &lt; <span class="number">0</span>) &#123;</span><br><span class="line">d += <span class="number">2</span> * (A + sgn * B);</span><br><span class="line">y += sgn;</span><br><span class="line">&#125; <span class="keyword">else</span> d += <span class="number">2</span> * A; </span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line"><span class="function">Framebuffer <span class="title">fb</span><span class="params">(w, h)</span></span>;</span><br><span class="line"><span class="comment">// drawlineDDA(2, 2, 18, 10, fb);</span></span><br><span class="line"><span class="built_in">drawlineMid</span>(<span class="number">2</span>, <span class="number">2</span>, <span class="number">18</span>, <span class="number">10</span>, fb);</span><br><span class="line">fb.<span class="built_in">print</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="参考">参考</h2><ul><li><p>fundamentals of computer graphics 4th, 11.2.4: Perspective Correct Interpolation</p></li><li><p><a href="https://zhuanlan.zhihu.com/p/140926917">图形学底层探秘 - 更现代的三角形光栅化与插值算法的实现与优化 - 知乎 (zhihu.com)</a></p></li><li><p><a href="https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage">Rasterization: a Practical Implementation (The Rasterization Stage) (scratchapixel.com)</a></p></li><li><p><a href="https://zhuanlan.zhihu.com/p/20213658">绘制直线的光栅化算法 - 知乎 (zhihu.com)</a></p></li></ul>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;一三角形光栅化&quot;&gt;一、三角形光栅化&lt;/h2&gt;
&lt;p&gt;三角形光栅化就是将三角面片覆盖的像素绘制出来，从而在屏幕上可见。有几个目标需要实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;不重复的&lt;/strong&gt;：不重复绘制像素，例如相邻的三角形之间公共边覆盖到的像素应当只被绘制一次。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;无缝的&lt;/strong&gt;：例如相邻的三角形之间光栅化出来应当是没有缝隙的，连续的。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="图形" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/"/>
    
    <category term="渲染" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/%E6%B8%B2%E6%9F%93/"/>
    
    
  </entry>
  
  <entry>
    <title>三角形插值</title>
    <link href="https://www.limil.top/triangular-interpolation/"/>
    <id>https://www.limil.top/triangular-interpolation/</id>
    <published>2024-09-25T18:30:30.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="d平面中三角形">2D平面中三角形</h2><p>对于2D空间三角形，假设三个顶点分别为<span class="math inline">\(\mathbf{a} ,\mathbf{b},\mathbf{c}\)</span>。则2D空间任意点都可以被下式表示出来：</p><p><span class="math display">\[\mathbf{p}=\mathbf{a}+\beta(\mathbf{b}-\mathbf{a})+\gamma(\mathbf{c}-\mathbf{a})\tag{1}\]</span></p><p>可以理解为<span class="math inline">\(\mathbf{a}\)</span>为起点，向量<span class="math inline">\((\mathbf{b}-\mathbf{a})\)</span>和<span class="math inline">\((\mathbf{c}-\mathbf{a})\)</span>构成的基张成一个平面空间。</p><span id="more"></span><p>整理可得：</p><p><span class="math display">\[\mathbf{p}=(1-\beta-\gamma)\mathbf{a}+\beta \mathbf{b}+\gamma \mathbf{c} \tag{2}\]</span></p><p><img src="../images/triangular-interpolation/image-20221023143019617_repeat_1666506621042__170536.png" /></p><p>令<span class="math inline">\(\alpha\equiv 1-\beta-\gamma\)</span>，得到重心坐标<span class="math inline">\((\alpha, \beta,\gamma)\)</span>。当<span class="math inline">\(\mathbf{p}\)</span>在三角形内部时，假设<span class="math inline">\(0&lt;\beta&lt;1\)</span>，由相似三角形，有<span class="math inline">\(0&lt;\gamma&lt;1-\beta\)</span>。因此，点<span class="math inline">\(\mathbf{p}\)</span>在三角形内部当且仅当满足： <span class="math display">\[\begin{aligned}0 &amp;&lt; \alpha &lt; 1 \\0 &amp;&lt; \beta  &lt; 1 \\0 &amp;&lt; \gamma &lt; 1\end{aligned}\]</span></p><p>当<span class="math inline">\(p\)</span>是重心时，重心坐标为<span class="math inline">\((\frac{1}{3},\frac{1}{3},\frac{1}{3})\)</span>。</p><p>如何对于给定点<span class="math inline">\(\mathbf{p}=(x_p,y_p)\)</span>，如何计算重心坐标？可以根据式<span class="math inline">\((1)\)</span>解方程组。假设<span class="math inline">\(\mathbf{a}=(x_a,y_a)\)</span>，<span class="math inline">\(\mathbf{b}\)</span>、<span class="math inline">\(\mathbf{c}\)</span>同理。 <span class="math display">\[\left[ \begin{matrix} x_b - x_a &amp; x_c-x_a\\ y_b-y_a &amp; y_c-y_a \end{matrix} \right]\left[ \begin{matrix} \beta\\ \gamma \end{matrix} \right]=\left[ \begin{matrix} x_p - x_a\\ y_p-y_a \end{matrix} \right]\tag{3}\]</span></p><p>除此之外，根据式<span class="math inline">\((1)\)</span>的几何意义，可得重心坐标等价于面积比。</p><p><img src="../images/triangular-interpolation/image-20221023143801584_repeat_1666507083014__031413.png" /></p><p>容易证明 <span class="math display">\[\frac{S_{abp}}{S_{abc}}=\frac{\gamma}{1}\]</span> 可以使用叉乘求出三角形面积，或者使用一个叫边界函数的东西。对于边<span class="math inline">\(AB\)</span>，其边界函数为： <span class="math display">\[E_{AB}(P)=(x_B-x_A)(y_P-y_A)-(x_P-x_A)(y_B-y_A)\]</span> 这个函数值为三角形<span class="math inline">\(S_{ABP}\)</span>的有符号面积的两倍，本质上就是一个行列式的结果。</p><p>因此，各个重心坐标就可以表示为： <span class="math display">\[\begin{aligned}S = E_{ab}(c) \\\alpha = \frac{S_{bcp}}{S_{abc}}=\frac{E_{bc}(p)}{S} \\\beta = \frac{S_{cap}}{S_{abc}}=\frac{E_{ca}(p)}{S} \\ \gamma = \frac{S_{abp}}{S_{abc}}=\frac{E_{ab}(p)}{S}\end{aligned}\]</span></p><p><img src="../images/triangular-interpolation/image-20220714182527910_repeat_1657794328108__687275.png" /></p><h3 id="透视矫正">透视矫正</h3><p>光栅化过程中，三角形中间的点的属性值将通过重心坐标插值出来。如果直接对屏幕空间的三角形插值将无法反映近大远小的效果。这种情况下，无论远近都被“均匀”地插值了，如下图所示，左边是正确的结果，符合近大远小的规律；右边是则是直接对屏幕空间插值，深度的效果消失了：</p><p><img src="../images/triangular-interpolation/image_63_repeat_1658128567885__818435.png" /></p><p>问题出在透视投影这一步。世界坐标系中两点<span class="math inline">\(\mathbf{q}\)</span>、<span class="math inline">\(\mathbf{Q}\)</span>构成的线段可以表示成如下（<span class="math inline">\(0\le k \le 1\)</span>）：</p><p><span class="math display">\[\mathbf{q}+k(\mathbf{Q}-\mathbf{q})\]</span></p><p>对于某点<span class="math inline">\(\mathbf{p}\)</span>，假如它的<span class="math inline">\(k=\alpha\)</span>，可以表示为<span class="math inline">\(\mathbf{p}=\mathbf{q}+\alpha(\mathbf{Q}-\mathbf{q})\)</span>，此时如果有某个属性<span class="math inline">\(I\)</span>需要插值，那么该点的属性自然可以表示成：<span class="math inline">\(I_\mathbf{p}=I_\mathbf{q}+\alpha(I_\mathbf{Q}-I_\mathbf{q})\)</span>。</p><p>Let's say <span class="math inline">\(\mathbf{p}\)</span> 经过透视变换后（相当于乘上一个矩阵<span class="math inline">\(\mathbf{M}\)</span>）：</p><p><span class="math display">\[\mathbf{p}&#39;=\mathbf{Mp}=\mathbf{Mq}+\alpha(\mathbf{MQ}-\mathbf{Mq})\equiv \mathbf{r}+\alpha(\mathbf{R}-\mathbf{r})\tag{1}\]</span></p><p>此时由于透视投影后的坐标（称为齐次坐标，homogeneous point），还要进行一次透视除法才能变成真正3维空间中的坐标（这个过程称为homogenized）。</p><p><span class="math display">\[\frac{\mathbf{p}&#39;}{\omega_{\mathbf{p}&#39;}}=\frac{\mathbf{r}+\alpha(\mathbf{R}-\mathbf{r})}{\omega_\mathbf{r}+\alpha(\omega_\mathbf{R}-\omega_\mathbf{r})}\]</span></p><p>整理一下上式，化为如下形式：</p><p><span class="math display">\[\frac{\mathbf{p}&#39;}{\omega_{\mathbf{p}&#39;}}=\frac{\mathbf{r}}{\omega_\mathbf{r}}+f(\alpha)(\frac{\mathbf{R}}{\omega_\mathbf{R}}-\frac{\mathbf{r}}{\omega_\mathbf{\mathbf{r}}})\tag{2}\]</span></p><p>其中</p><p><span class="math display">\[f(\alpha)=\frac{\omega_\mathbf{R}\alpha}{\omega_{\mathbf{r}}+\alpha(\omega_\mathbf{R}-\omega_\mathbf{r})}\tag{3}\]</span></p><p>式<span class="math inline">\((2)\)</span>中，<span class="math inline">\(\frac{\mathbf{r}}{\omega_\mathbf{r}}\)</span>和<span class="math inline">\(\frac{\mathbf{R}}{\omega_\mathbf{R}}\)</span>分别是<span class="math inline">\(\mathbf{q}\)</span>和<span class="math inline">\(\mathbf{Q}\)</span>透视投影后点！这意味者透视投影后的线段依旧是线段，同理还可以推出：对于在视锥体内的线段，投影后的线段点次序不变，平面投影后还是一个平面等非常有用的性质。</p><p>而且，原本<span class="math inline">\(\mathbf{p}\)</span>的参数坐标是<span class="math inline">\(\alpha\)</span>，经过投影后参数变为<span class="math inline">\(f(\alpha)\)</span>！这里找到了投影前后参数并不相同，不能直接用<span class="math inline">\(f(\alpha)\)</span>代替<span class="math inline">\(\alpha\)</span>，但是可以通过两者的关系式求出<span class="math inline">\(\alpha\)</span>。</p><p><span class="math inline">\((2)\)</span>式中投影后的点，可以理解为屏幕空间中的点。因为除了透视投影对<span class="math inline">\(\omega\)</span>项发生修改，其余变换都不对<span class="math inline">\(w\)</span>项修改，因此其余变换都不会改变<span class="math inline">\(\alpha\)</span>。</p><p>已知参数<span class="math inline">\(f(\alpha)\)</span>（它就是在屏幕空间中计算得到插值系数），就可以根据<span class="math inline">\((2)\)</span>式推出<span class="math inline">\(\alpha\)</span>。这个<span class="math inline">\(\alpha\)</span>就是世界坐标系中的插值系数，就是我们想要的。这个过程就是透视纠正。</p><p>事实上，在屏幕空间对<span class="math inline">\(\frac{1}{\omega}\)</span>插值等价于在世界坐标系中对<span class="math inline">\(\omega\)</span>插值。证明如下：</p><p><span class="math display">\[\frac{1}{\omega_\mathbf{r}}+f(\alpha)(\frac{1}{\omega_\mathbf{R}}-\frac{1}{\omega_\mathbf{\mathbf{r}}})=\frac{1}{\omega_\mathbf{r}+\alpha(\omega_\mathbf{R}-\omega_\mathbf{r})}=\frac{1}{\omega_{\mathbf{p}}}\]</span></p><p>所以，对于<span class="math inline">\(\mathbf{R}\)</span>、<span class="math inline">\(\mathbf{r}\)</span>之间的某点<span class="math inline">\(\mathbf{p}\)</span>，想得到它的任意某种属性<span class="math inline">\(I_{\mathbf{p}}\)</span>，直接对除以<span class="math inline">\(\omega\)</span>后的<span class="math inline">\(I_\mathbf{R}\)</span>、<span class="math inline">\(I_\mathbf{r}\)</span>插值得到<span class="math inline">\(I_{\mathbf{p}}&#39;\)</span>：</p><p><span class="math display">\[I_{\mathbf{p}}&#39;=\frac{I_{\mathbf{r}}}{\omega_\mathbf{r}}+f(\alpha)(\frac{I_\mathbf{R}}{\omega_\mathbf{R}}-\frac{I_\mathbf{r}}{\omega_\mathbf{\mathbf{r}}})=\frac{I_\mathbf{r}+\alpha(I_\mathbf{R}-I_\mathbf{r})}{\omega_\mathbf{r}+\alpha(\omega_\mathbf{R}-\omega_\mathbf{r})}=\frac{I_\mathbf{p}}{\omega_\mathbf{r}+\alpha(\omega_\mathbf{R}-\omega_\mathbf{r})}\]</span></p><p>然后再对<span class="math inline">\(\frac{1}{\omega}\)</span>直接在屏幕空间中插值，得到：</p><p><span class="math display">\[1&#39;=\frac{1}{\omega_\mathbf{r}}+f(\alpha)(\frac{1}{\omega_\mathbf{R}}-\frac{1}{\omega_\mathbf{\mathbf{r}}})=\frac{1}{\omega_\mathbf{r}+\alpha(\omega_\mathbf{R}-\omega_\mathbf{r})}\]</span></p><p>最后：</p><p><span class="math display">\[\frac{I_p&#39;}{1&#39;}=I_\mathbf{r}+\alpha(I_\mathbf{R}-I_\mathbf{r})=I_{\mathbf{p}}\]</span></p><p>可以看到<span class="math inline">\(\frac{I&#39;}{1&#39;}\)</span>就是正确的插值结果。</p><p>上面只是线性情况下的透视矫正，但是和重心坐标插值是完全等价的。重心坐标插值可以视作两个线性插值的组合，因此矫正过程和上面相同。</p><p>综上，对于纹理坐标<span class="math inline">\((u,v)\)</span>，可以如下计算正确的插值：（假设<span class="math inline">\(\alpha\)</span>，<span class="math inline">\(\beta\)</span>，<span class="math inline">\(\gamma\)</span>是屏幕空间下的计算得到的重心坐标）：</p><p><span class="math display">\[\begin{aligned}u&#39; &amp;=\alpha(\frac{u_a}{\omega_a})+\beta(\frac{u_b}{\omega_b})+\gamma(\frac{u_c}{\omega_c} ) \\v&#39; &amp;=\alpha(\frac{v_a}{\omega_a})+\beta(\frac{v_b}{\omega_b})+\gamma(\frac{v_c}{\omega_c}) \\1&#39; &amp;= \alpha(\frac{1}{\omega_a})+\beta(\frac{1}{\omega_b})+\gamma(\frac{1}{\omega_c}) \\(u,v) &amp;= (\frac{u&#39;}{1&#39;}, \frac{v&#39;}{1&#39;})\end{aligned}\]</span></p><blockquote><p>更进一步，可以证明，所有在三维空间里同x,y,z成线性关系的变量，不管是纹理坐标，顶点色或者法向还是其他，他们在屏幕空间里的插值规则都可以通过：插值前先/w，插值后要用时再 * w得到具体值，然后我们把这类三维空间里同x,y,z成线性关系的变量统进行统一的批量处理，和OpenGL的 attribute，varying处理方法相同。</p><p>作者：韦易笑 链接：https://www.zhihu.com/question/40624282/answer/87501965 来源：知乎</p></blockquote><h2 id="d空间中三角形">3D空间中三角形</h2><p>3D空间重心坐标依旧满足式<span class="math inline">\((2)\)</span>，只需使用叉乘求面积，然后计算面积比即可得到重心坐标： <span class="math display">\[\begin{aligned}\mathbf{n}_a=(\mathbf{c}-\mathbf{b}) \times (\mathbf{p} - \mathbf{b}) \\\mathbf{n}_b=(\mathbf{a}-\mathbf{c}) \times (\mathbf{p} - \mathbf{c}) \\\mathbf{n}_c=(\mathbf{b}-\mathbf{a}) \times (\mathbf{p} - \mathbf{a}) \\\mathbf{n}=(\mathbf{b}-\mathbf{a}) \times (\mathbf{c} - \mathbf{a}) \\\alpha=\frac{S_A}{S}=\frac{\mathbf{n}\cdot \mathbf{n}_a}{\parallel \mathbf{n}\parallel^2} \\\beta=\frac{S_B}{S}=\frac{\mathbf{n}\cdot \mathbf{n}_b}{\parallel \mathbf{n}\parallel^2} \\\gamma=\frac{S_C}{S}=\frac{\mathbf{n}\cdot \mathbf{n}_c}{\parallel \mathbf{n}\parallel^2}\end{aligned}\]</span></p><p>回顾一下：</p><p><span class="math display">\[\mathbf{a}\times\mathbf{b}=(x_a,y_a,z_a)\times(x_b,y_b,z_b)=(y_az_b-y_bz_a,x_bz_a-x_az_b,x_ay_b-x_by_a)\]</span></p><h2 id="reference">Reference</h2><ul><li>fundamentals of computer graphics 4th, 8.1.2: Triangle Rasterization</li><li><a href="https://www.zhihu.com/question/40624282">3D 图形光栅化的透视校正问题？</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;d平面中三角形&quot;&gt;2D平面中三角形&lt;/h2&gt;
&lt;p&gt;对于2D空间三角形，假设三个顶点分别为&lt;span class=&quot;math inline&quot;&gt;&#92;(&#92;mathbf{a} ,&#92;mathbf{b},&#92;mathbf{c}&#92;)&lt;/span&gt;。则2D空间任意点都可以被下式表示出来：&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;math display&quot;&gt;&#92;[
&#92;mathbf{p}=&#92;mathbf{a}+&#92;beta(&#92;mathbf{b}-&#92;mathbf{a})+&#92;gamma(&#92;mathbf{c}-&#92;mathbf{a})&#92;tag{1}
&#92;]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;可以理解为&lt;span class=&quot;math inline&quot;&gt;&#92;(&#92;mathbf{a}&#92;)&lt;/span&gt;为起点，向量&lt;span class=&quot;math inline&quot;&gt;&#92;((&#92;mathbf{b}-&#92;mathbf{a})&#92;)&lt;/span&gt;和&lt;span class=&quot;math inline&quot;&gt;&#92;((&#92;mathbf{c}-&#92;mathbf{a})&#92;)&lt;/span&gt;构成的基张成一个平面空间。&lt;/p&gt;</summary>
    
    
    
    <category term="图形" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/"/>
    
    <category term="基础" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/%E5%9F%BA%E7%A1%80/"/>
    
    
  </entry>
  
  <entry>
    <title>三维空间中的旋转</title>
    <link href="https://www.limil.top/rotate-in-3d/"/>
    <id>https://www.limil.top/rotate-in-3d/</id>
    <published>2024-09-25T15:52:27.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<p>在介绍旋转之前，先分析一下两个概念：旋转和方向。方向即一个单位向量；旋转用来描述一个方向如何变换到另一个方向。在3维空间中，使一个方向变换到另一个方向的旋转有无数种，因为变换后的向量还可以绕以自己为旋转轴轴旋转。这意味着将旋转说成是一个方向变换到另一个方向的过程是不严谨的。准确地说，旋转描述的是坐标系到坐标系的变换。在3维空间中，只知道一个方向会变换到另一个方向，是无法唯一确定一个旋转的（当然也是可以构造出满足条件的旋转的，比如指定一个上向量）。</p><span id="more"></span><p>既然旋转是坐标轴的变换，旋转的本质就是旋转矩阵。旋转矩阵都是正交矩阵，但是正交矩阵不一定是旋转矩阵。</p><p>旋转矩阵对人来说不直观，因此有了一些方法，可以指定诸如角度、旋转轴等参数，表示相应的旋转。通过对参数的插值还能得到对旋转的插值，这是直接用旋转矩阵难以做到的。</p><p>下面介绍的方法（欧拉角、轴角、四元数）均可以表示所有的旋转，都可以生成任意旋转矩阵。这些方法的局限性（比如所谓的万向节死锁）并不是指无法表示某些旋转，而是在一些情景下会有不方便的情况。</p><p>下面描述的都是<strong>右手坐标系</strong>下的旋转，旋转方向是逆时针。</p><h2 id="一旋转矩阵">一、旋转矩阵</h2><p>对于2维旋转：设旋转矩阵是<span class="math inline">\(M=\bigl[\begin{smallmatrix}a&amp;b\\c&amp;d\end{smallmatrix}\bigr]\)</span>。假设逆时针旋转<span class="math inline">\(\theta\)</span>，分别对<span class="math inline">\((1,0)\)</span>和<span class="math inline">\((0,1)\)</span>作用旋转可得：</p><p><span class="math display">\[\begin{aligned} \left[\begin{matrix}a &amp; b \\c &amp; d\end{matrix}\right]\left[\begin{matrix}1 \\0\end{matrix}\right] &amp;= \left[\begin{matrix}\cos\theta \\\sin\theta\end{matrix}\right] \\\left[\begin{matrix}a &amp; b \\c &amp; d\end{matrix}\right]\left[\begin{matrix}0 \\1\end{matrix}\right] &amp;= \left[\begin{matrix}-\sin\theta \\\cos\theta\end{matrix}\right]\end{aligned}\]</span></p><p>解得</p><p><span class="math display">\[M=\left[\begin{matrix}\cos\theta &amp; -sin\theta \\\sin\theta &amp; \cos\theta\end{matrix}\right]\]</span> 对于3维旋转，可以类比上面的方法。使用待定系数法求出绕某个坐标轴的旋转矩阵，通过它们的组合就可以表示所有旋转： <span class="math display">\[M_x(\theta)=\left[\begin{matrix}1 &amp; 0 &amp; 0 \\0 &amp; \cos\theta &amp; -\sin\theta \\0 &amp; \sin\theta &amp; \cos\theta\end{matrix}\right]\]</span></p><p><span class="math display">\[M_y(\theta)=\left[\begin{matrix}\cos\theta &amp; 0 &amp; \sin\theta \\0 &amp; 1 &amp; 0 \\-\sin\theta &amp; 0 &amp; \cos\theta\end{matrix}\right]\]</span></p><p><span class="math display">\[M_z(\theta)=\left[\begin{matrix}\cos\theta &amp; -\sin\theta &amp; 0 \\\sin\theta &amp; \cos\theta &amp; 0 \\0 &amp; 0 &amp; 1\end{matrix}\right]\]</span></p><h2 id="二欧拉角">二、欧拉角</h2><p>欧拉角就是按照次序分别绕3个轴旋转。例如XYZ规顺下(20°,30°,40°)代表的旋转就是先绕X轴旋转20°，再绕Y轴旋转30°，最后绕Z轴旋转40°。欧拉角又分静态欧拉角和动态欧拉角。静态欧拉角是每次都绕世界坐标系下的轴旋转，动态欧拉角则是每次都绕局部坐标系下的轴旋转（意味着每次旋转的旋转轴会受到前面旋转的影响，除了第一次，最开始局部坐标系和世界坐标系重合）。</p><p>下图是欧拉角的模型：</p><figure><img src="../images/rotate-in-3d/normal_repeat_1659674804292__924967.gif" alt="" /><figcaption>normal</figcaption></figure><p>外圈（红色）控制绕Y轴旋转，中圈（绿色）绕X轴旋转，内圈（蓝色）绕Z轴旋转。外部圈的旋转会影响到内部圈，内部圈旋转不会影响到外部圈。从外到内旋转，每一圈都是绕局部坐标轴旋转的，这代表了YXZ规顺的动态欧拉角。类似的，从内到外旋转，内圈的旋转不会影响外圈，每次旋转都相当于绕的是世界坐标系下的轴。由此可以发现静态欧拉角和动态欧拉角的关系，它们其实就是旋转顺序不同而已。</p><p>综上，先绕世界坐标系Z轴旋转<span class="math inline">\(\alpha\)</span>度，再绕世界坐标系X轴旋转<span class="math inline">\(\beta\)</span>度，最后绕世界坐标系Y轴旋转<span class="math inline">\(\gamma\)</span>度，等价于，先绕局部坐标系Y轴旋转<span class="math inline">\(\gamma\)</span>度，再绕局部坐标系X轴旋转<span class="math inline">\(\beta\)</span>度，最后绕局部坐标系Z轴旋转<span class="math inline">\(\alpha\)</span>度。即ZXY规顺的静态欧拉角等价于YXZ规顺的动态欧拉角。其它规顺类似。</p><p>由于动态欧拉角作为参数更加直观，通过上面静态欧拉角和动态欧拉角关系，可以得到动态欧拉角的旋转矩阵： <span class="math display">\[R_{\mathbf{YXZ}}(\alpha,\beta,\gamma)=M_y(\alpha)\cdot M_x(\beta) \cdot M_z(\gamma)\]</span> 欧拉角有个问题就是万向节死锁（Gimbal lock）。当任意两个旋转轴重合，就会少一个自由度。如下图，蓝圈绕的轴和红圈的重合，此时旋转红圈和旋转蓝圈效果是等价的。</p><p>这种情况下，同一个旋转可以被多个不同欧拉角表示。例如对于YXZ规顺动态欧拉角，(0°, 90°, 0°)和(-30°, 90°, 30°)是等价的旋转。这可能导致用欧拉角对旋转插值时出现诡异情况，例如从旋转(0°, 90°, 0°)插值到旋转(0°,0°,0°)和从旋转(-30°, 90°, 30°)插值到旋转(0°,0°,0°)不一样，即使旋转是等价，但是只有前者是预期的插值结果。</p><figure><img src="../images/rotate-in-3d/lock_repeat_1659676871122__477216.gif" alt="" /><figcaption>lock</figcaption></figure><p>关于万向节死锁相关的内容可以看这个视频：<a href="https://www.bilibili.com/video/BV1MZ4y1m7CT">欧拉角-万向节死锁问题直观理解</a>，也可以看这个文章从代数角度的分析：<a href="https://krasjet.github.io/quaternion/bonus_gimbal_lock.pdf">bonus_gimbal_lock.pdf (krasjet.github.io)</a></p><h2 id="三轴-角旋转">三、轴-角旋转</h2><p>旋转可以表示为绕一个特定的轴旋转<span class="math inline">\(\theta\)</span>度，即参数为轴向量和旋转角度。3D空间中任意一个<span class="math inline">\(\mathbf{v}\)</span>沿着单位向量<span class="math inline">\(\mathbf{u}\)</span>旋转 <span class="math inline">\(\theta\)</span> 角度之后的 <span class="math inline">\(\mathbf{v}′\)</span> 为： <span class="math display">\[\mathbf{v}′=\cos(\theta)\mathbf{v}+(1-\cos(\theta))(\mathbf{u}\cdot \mathbf{v})\mathbf{u}+\sin(\theta)(\mathbf{u}\cdot \mathbf{v})\]</span> 证明思路是先将<span class="math inline">\(\mathbf{v}\)</span>分别投影到平行于轴的分量<span class="math inline">\(\mathbf{v_{\parallel}}\)</span>和垂直于轴的分量<span class="math inline">\(\mathbf{v_{\perp}}\)</span>。然后对<span class="math inline">\(\mathbf{v}\)</span>旋转等价于对两个分量进行同样的旋转再求和（旋转矩阵满足分配律）。这样<span class="math inline">\(\mathbf{v_{\perp}}\)</span>就退化到平面的旋转，而<span class="math inline">\(\mathbf{v_{\parallel}}\)</span>转了和没转一样。具体证明过程参考Reference里面的文章，写得超级好。</p><h2 id="四四元数">四、四元数</h2><p>正如复数可以表示二维空间中的旋转，四元数可以表示三维空间中的旋转。每个单位四元数都对应一个旋转。</p><p>用四元数表示旋转和轴-角旋转有密切关系。为了不重复造轮子，下面只给出一些结论。具体原理和证明见Reference里面的文章，写得超级好。</p><p>和复数不同，四元数虽然满足结合律、分配律，但不满足交换律。一个四元数可以表示成：<span class="math inline">\(q=a+bi+cj+dk\)</span>，其中<span class="math inline">\(i^2=j^2=k^2=ijk=-1\)</span>。四元数的加减乘除和复数类似，就是不停应用这些运算规则和分配律、结合律。设向量<span class="math inline">\(\mathbf{v}=[b,c,d]^T\)</span>，用<span class="math inline">\([a,\mathbf{v}]\)</span>表示四元数<span class="math inline">\(q=a+bi+cj+dk\)</span>。</p><p>有了新的表示方法，对于任意四元数<span class="math inline">\(q_1=[s,\mathbf{v}]\)</span>，<span class="math inline">\(q_2=[t, \mathbf{u}]\)</span>，<span class="math inline">\(q_1q_2\)</span>的结果是 <span class="math display">\[q_1q_2=[st-\mathbf{v}\cdot\mathbf{u},s\mathbf{u}+t\mathbf{v}+\mathbf{v}\times \mathbf{u}]\]</span> 这个结果也叫做 Graßmann 积。</p><p>对于四元数<span class="math inline">\(q=[s,\mathbf{v}]\)</span>，其共轭为<span class="math inline">\(q^*=[s,-\mathbf{v}]\)</span>。其逆为<span class="math inline">\(q^{-1}=\frac{q^*}{\parallel q \parallel^2}\)</span>。这样对于单位四元数，其逆就是其共轭四元数。</p><p>可以用纯四元数表示一个三维空间的向量<span class="math inline">\(\mathbf{v}\)</span>：<span class="math inline">\(v=[0, \mathbf{v}]\)</span>。</p><p>任意向量<span class="math inline">\(\mathbf{v}\)</span>绕单位向量定义的旋转轴<span class="math inline">\(\mathbf{u}\)</span> 旋转<span class="math inline">\(\theta\)</span>度之后的 <span class="math inline">\(\mathbf{v}′\)</span> 可以使用四元数乘法来获得。令<span class="math inline">\(v=[0, \mathbf{v}]\)</span>，<span class="math inline">\(q=[\cos(\frac{1}{2}\theta), \sin(\frac{1}{2}\theta)\mathbf{u}]\)</span>，那么： <span class="math display">\[v&#39;=qvq^*=qvq^{-1}\]</span></p><p>可以证明，<span class="math inline">\(v&#39;\)</span>是一个纯四元数。</p><p>根据上面的介绍，你可以从一个单位四元数中提取出旋转角度和旋转轴。</p><h3 id="四元数转旋转矩阵">四元数转旋转矩阵</h3><p>单位四元数对应一种旋转，可以转为旋转矩阵，任意向量 <span class="math inline">\(\mathbf{v}\)</span>沿着以单位向量定义的旋转轴<span class="math inline">\(\mathbf{u}\)</span>旋转<span class="math inline">\(θ\)</span>角度之后的<span class="math inline">\(\mathbf{v}′\)</span> 可以使用矩阵乘法来获得。令<span class="math inline">\(a=\cos(\frac{1}{2}\theta)\)</span>，<span class="math inline">\(b=\sin(\frac{1}{2}\theta)u_x\)</span>，<span class="math inline">\(c=\sin(\frac{1}{2}\theta)u_y\)</span>，<span class="math inline">\(d=\sin(\frac{1}{2}\theta)u_z\)</span>，那么 <span class="math display">\[\mathbf{v}&#39;=\left[\begin{matrix}1-2c^2-2d^2 &amp; 2bc-2ad &amp; 2ac+2bd \\2bc+2ad &amp; 1-2b^2-2d^2 &amp; 2cd-2ab \\2bd-2ac &amp; 2ab+2cd &amp; 1-2b^2-2c^2\end{matrix}\right]\mathbf{v}\]</span></p><p>注意旋转和单位四元数不是一一对应的。单位四元数与 3D 旋转之间是2对1满射同态的。关键词：双层覆盖。</p><h3 id="四元数插值">四元数插值</h3><p>对四元数的插值就是<strong>对旋转的插值</strong>。</p><h4 id="nlerp">Nlerp</h4><p>线性插值： <span class="math display">\[q_t=\text{Nlerp}(q_0, q_1,t)=\frac{(1-t)q_0+tq_1}{\parallel (1-t)q_0+tq_1 \parallel}\]</span> 缺点是在插值弧度比较大时，角速度变换不均匀效果比较明显。</p><h4 id="slerp">Slerp</h4><p>为了解决角速度变换不均匀，可以对角度插值。假设四元数之间的夹角是<span class="math inline">\(\theta\)</span>，有 <span class="math display">\[q_t=\text{Slerp}(q_0,q_1,t)=\frac{\sin(1-t)\theta}{\sin(\theta)}q_0+\frac{\sin(t\theta)}{\sin(\theta)}q_1 \\\]</span> 四元数之间的夹角可以用点乘法求出： <span class="math display">\[\theta=\cos^{-1}(q_0\cdot q_1)\]</span> 四元数的点乘就是各分量相乘相加。</p><p>在<span class="math inline">\(\theta\)</span>非常小时<span class="math inline">\(\sin(\theta)\)</span>会非常小，此时应该用Nlerp替代。在<span class="math inline">\(\theta\)</span>非常小的情况下Nlerp误差非常小。</p><p>除此之外，为了确保插值是最短路径，要检查<span class="math inline">\(\theta\)</span>是否是钝角。如果<span class="math inline">\(q_0\cdot q_1&lt;0\)</span>，那就反转一个四元数，如<span class="math inline">\(-q_0\)</span>，再对<span class="math inline">\(-q_0\)</span>和<span class="math inline">\(q_1\)</span>插值。</p><p>更多插值见Rerefence。</p><h2 id="五reference">五、Reference</h2><ul><li><a href="https://krasjet.github.io/quaternion/quaternion.pdf">四元数与三维旋转</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;在介绍旋转之前，先分析一下两个概念：旋转和方向。方向即一个单位向量；旋转用来描述一个方向如何变换到另一个方向。在3维空间中，使一个方向变换到另一个方向的旋转有无数种，因为变换后的向量还可以绕以自己为旋转轴轴旋转。这意味着将旋转说成是一个方向变换到另一个方向的过程是不严谨的。准确地说，旋转描述的是坐标系到坐标系的变换。在3维空间中，只知道一个方向会变换到另一个方向，是无法唯一确定一个旋转的（当然也是可以构造出满足条件的旋转的，比如指定一个上向量）。&lt;/p&gt;</summary>
    
    
    
    <category term="图形" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/"/>
    
    <category term="基础" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/%E5%9F%BA%E7%A1%80/"/>
    
    
  </entry>
  
  <entry>
    <title>makefile学习笔记</title>
    <link href="https://www.limil.top/makefile-tutorials/"/>
    <id>https://www.limil.top/makefile-tutorials/</id>
    <published>2024-09-25T15:44:21.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="概述">概述</h2><p>C/C++ 编译流程： 编译、汇编、链接</p><p>在汇编完成后，会生成中间文件。windows下是<code>.obj</code>，Unix下是<code>.o</code>。</p><p>编译时，只要源代码的语法正确，编译器就可以编译出中间目标文件。</p><p>链接时，主要是链接函数和全局变量。所以，我们可以使用这些中间目标文件（ <code>.o</code> 文件或 <code>.obj</code> 文件）来链接我们的应用程序。在大多数时候，由于源文件太多，编译生成的中间目标文件太多，而在链接时需要明显地指出中间目标文件名，这对于编译很不方便。所以，我们要给中间目标文件打个包，在Windows下这种包叫“库文件”（Library File），也就是 <code>.lib</code> 文件，在UNIX下，是Archive File，也就是 <code>.a</code> 文件。</p><h3 id="参考">参考</h3><p>基本就是下面的教程的搬运+一点的自己理解</p><ul><li><p><a href="https://seisman.github.io/how-to-write-makefile/overview.html">概述 — 跟我一起写Makefile 1.0 文档 (seisman.github.io)</a></p></li><li><p><a href="https://zhuanlan.zhihu.com/p/376493209">GNU make/Makefile 简明实用教程 - 知乎 (zhihu.com)</a></p></li></ul><span id="more"></span><h2 id="makefile介绍">makefile介绍</h2><p>make命令执行时，需要一个makefile文件，以告诉make命令需要怎么样的去编译和链接程序。 在第一个项目中，有一个<code>fun.h</code>的头文件，<code>main.c</code> <code>fun.c</code>两个c文件。</p><h3 id="makefile规则">makefile规则</h3><p>粗略地看一下makefile规则： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">target ... : prerequisites ...</span><br><span class="line">    command</span><br><span class="line">    ...</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure></p><p><strong>target</strong></p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">可以是一个object file（目标文件），也可以是一个执行文件，还可以是一个标签（label）。对于标签这种特性，在后续的“伪目标”章节中会有叙述。</span><br></pre></td></tr></table></figure><p><strong>prerequisites</strong></p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">生成该target所依赖的文件和/或target</span><br></pre></td></tr></table></figure><p><strong>command</strong></p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">该target要执行的命令（任意的shell命令）</span><br></pre></td></tr></table></figure><h3 id="第一个示例">第一个示例</h3><p>第一个项目有1个头文件和2个c文件，对应地makefile如下： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">main : main.o fun.o</span><br><span class="line">cc -o main main.o fun.o</span><br><span class="line"></span><br><span class="line">main.o : main.c fun.h</span><br><span class="line">cc -c main.c</span><br><span class="line"></span><br><span class="line">fun.o : fun.c fun.h</span><br><span class="line">cc -c fun.c</span><br><span class="line"></span><br><span class="line">clean :</span><br><span class="line">rm main main.o fun.o</span><br></pre></td></tr></table></figure></p><p>当make发现main未生成或main后面的依赖列表中存在比main更新的，就会执行下面的语句重新编译。</p><p>同理，当它后面的依赖没生成或太旧了，也会执行其对应的指令。整个编译过程环环相扣。</p><p>这就是整个make的依赖性，make会一层又一层地去找文件的依赖关系，直到最终编译出<strong>第一个目标文件</strong>（make会找makefile的第一个目标文件（target），作为最终的目标文件）。在找寻的过程中，如果出现错误，比如最后被依赖的文件找不到，那么make就会直接退出，并报错。而<strong>对于所定义的命令的错误，或是编译不成功，make根本不理</strong>。<strong>make只管文件的依赖性</strong>，即，如果在找了依赖关系之后，冒号后面的文件还是不在，make就会直接退出，并报错。</p><p>还有一点，这里 clean 不是一个文件，它只不过是一个动作名字，有点像c语言中的label一样，其冒号后什么也没有，那么，make就不会自动去找它的依赖性，也就不会自动执行其后所定义的命令。要执行其后的命令，就要在make命令后明显得指出这个label的名字。这样的方法非常有用，我们可以在一个makefile中定义不用的编译或是和编译无关的命令，比如程序的打包，程序的备份，等等。</p><h3 id="makefile中使用变量">makefile中使用变量</h3><p>从上面的例子可以看到，依赖列表被重复了两次（一次在依赖中，一次在下面的编译命令中）。这时可以使用<strong>变量</strong>，代表这个依赖。</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">objects = main.o fun.o</span><br></pre></td></tr></table></figure><p>使用变量后，makefile变成这样： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">objs = main.o fun.o</span><br><span class="line"></span><br><span class="line">main : <span class="variable">$(objs)</span></span><br><span class="line">cc -o main <span class="variable">$(objs)</span></span><br><span class="line"></span><br><span class="line">main.o : main.c fun.h</span><br><span class="line">cc -c main.c</span><br><span class="line"></span><br><span class="line">fun.o : fun.c fun.h</span><br><span class="line">cc -c fun.c</span><br><span class="line"></span><br><span class="line">clean :</span><br><span class="line">rm main <span class="variable">$(objs)</span></span><br></pre></td></tr></table></figure></p><p>这样一来，如果有新的 .o 文件加入，只需简单地修改一下 objects 变量就可以了。</p><h3 id="让make自动推导">让make自动推导</h3><p>GNU的make很强大，它可以自动推导文件以及文件依赖关系后面的命令，于是我们就没必要去在每一个 .o 文件后都写上类似的命令，因为，make会自动识别，并自己推导命令。</p><p>只要make看到一个 .o 文件，它就会自动的把 .c 文件加在依赖关系中，如果make找到一个 whatever.o ，那么 whatever.c 就会是 whatever.o 的依赖文件。并且 cc -c whatever.c 也会被推导出来，于是，makefile再也不用写得这么复杂。新makefile如下：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">objs = main.o fun.o</span><br><span class="line"></span><br><span class="line">main : <span class="variable">$(objs)</span></span><br><span class="line">cc -o main <span class="variable">$(objs)</span></span><br><span class="line"></span><br><span class="line">main.o : fun.h</span><br><span class="line">fun.o : fun.h</span><br><span class="line"></span><br><span class="line">.PHONY : clean</span><br><span class="line">clean :</span><br><span class="line">rm main <span class="variable">$(objs)</span></span><br></pre></td></tr></table></figure><p>上面文件内容中， .PHONY 表示 clean 是个伪目标文件。</p><h3 id="清空目标文件">清空目标文件</h3><p>每个makefile都最好提供一个清空目标文件的指令。约定熟成：将这个指令放在makefile最后面。</p><h3 id="makefile里面有什么">makefile里面有什么</h3><p>Makefile里主要包含了五个东西：显式规则、隐晦规则、变量定义、文件指示和注释。</p><ol type="1"><li><p>显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。</p></li><li><p>隐晦规则。由于我们的make有自动推导的功能，所以隐晦的规则可以让我们比较简略地书写 Makefile，这是由make所支持的。</p></li><li><p>变量的定义。在Makefile中我们要定义一系列的变量，变量一般都是字符串，这个有点像你C语言中的宏，当Makefile被执行时，其中的变量都会被扩展到相应的引用位置上。</p></li><li><p>文件指示。其包括了三个部分，一个是在一个Makefile中引用另一个Makefile，就像C语言中的include一样；另一个是指根据某些情况指定Makefile中的有效部分，就像C语言中的预编译#if一样；还有就是定义一个多行的命令。有关这一部分的内容，我会在后续的部分中讲述。</p></li><li><p>注释。Makefile中只有行注释，和UNIX的Shell脚本一样，其注释是用 <code>#</code> 字符，这个就像C/C++中的 <code>//</code> 一样。如果你要在你的Makefile中使用 <code>#</code> 字符，可以用反斜杠进行转义，如： <code>\#</code> 。</p></li></ol><p>最后，还值得一提的是，在Makefile中的命令，<strong>必须要以 <code>Tab</code> 键开始</strong>。</p><h3 id="makefile-文件名">makefile 文件名</h3><p>默认的情况下，make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件，找到了解释这个文件。 你可以使用别的文件名来书写Makefile，比如：“Make.Linux”，“Make.Solaris”，“Make.AIX”等，如果要指定特定的Makefile，你可以使用make的 <code>-f</code> 和 <code>--file</code> 参数，如： <code>make -f Make.Linux</code> 或 <code>make --file Make.AIX</code> 。</p><h3 id="引用其它makefile">引用其它makefile</h3><p>在Makefile使用 include 关键字可以把别的Makefile包含进来，这很像C语言的 <code>#include</code> ，被包含的文件会<strong>原模原样</strong>的放在当前文件的包含位置。 <code>include</code> 的语法是（）： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">include</span> filename ...</span><br></pre></td></tr></table></figure></p><p>filename可以是shell的格式（可以包含路径和通配符），可以有多个filename（用空格隔开）。</p><p>一个例子： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">include</span> foo.make *.mk <span class="variable">$(bar)</span></span><br></pre></td></tr></table></figure> make命令开始时，会找寻 <code>include</code> 所指出的其它Makefile，并把其内容安置在当前的位置。就好像C/C++的 <code>#include</code> 指令一样。如果文件都没有指定绝对路径或是相对路径的话，make会在当前目录下首先寻找，如果当前目录下没有找到，那么，make还会在下面的几个目录下找： 1. 如果make执行时，有 <code>-I</code> 或 <code>--include-dir</code> 参数，那么make就会在这个参数所指定的目录下去寻找。</p><ol start="2" type="1"><li>如果目录 <code>&lt;prefix&gt;/include</code> （一般是： <code>/usr/local/bin</code> 或 <code>/usr/include</code> ）存在的话，make也会去找。</li></ol><p>如果有文件没有找到的话，make会生成一条警告信息，但不会马上出现致命错误。它会继续载入其它的文件，一旦完成makefile的读取，make会再重试这些没有找到，或是不能读取的文件，如果还是不行，make才会出现一条致命信息。如果你想让make不理那些无法读取的文件，而继续执行，你可以在include前加一个减号“-”。如： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">-include</span> &lt;filename&gt;</span><br></pre></td></tr></table></figure></p><p>其表示，无论include过程中出现什么错误，都不要报错继续执行。和其它版本make兼容的相关命令是sinclude，其作用和这一个是一样的。</p><h3 id="环境变量makefiles">环境变量<code>MAKEFILES</code></h3><p>如果你的当前环境中定义了环境变量 <code>MAKEFILES</code> ，那么，make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的Makefile，用空格分隔。只是，它和 include 不同的是，从这个环境变量中引入的Makefile的“目标”不会起作用，如果环境变量中定义的文件发现错误，make也会不理。</p><p>建议不要使用这个环境变量，因为只要这个变量一被定义，所有的Makefile都会受到它的影响，也许有时候Makefile出现了怪事，可以看看当前环境中有没有定义这个变量。</p><h3 id="make工作方式">make工作方式</h3><p>GNU的make工作时的执行步骤如下：（想来其它的make也是类似）</p><ol type="1"><li>读入所有的Makefile。</li><li>读入被include的其它Makefile。</li><li>初始化文件中的变量。</li><li>推导隐晦规则，并分析所有规则。</li><li>为所有的目标文件创建依赖关系链。</li><li>根据依赖关系，决定哪些目标要重新生成。</li><li>执行生成命令。</li></ol><p>1-5步为第一个阶段，6-7为第二个阶段。第一个阶段中，如果定义的变量被使用了，那么，make会把其展开在使用的位置。但make并不会完全马上展开，make使用的是拖延战术，如果变量出现在依赖关系的规则中，那么仅当这条依赖被决定要使用了，变量才会在其内部展开。</p><h3 id="参考-1">参考</h3><ul><li><a href="https://seisman.github.io/how-to-write-makefile/introduction.html#makefile">makefile介绍</a></li></ul><h2 id="书写规则">书写规则</h2><h3 id="文件搜寻">文件搜寻</h3><p>Makefile文件中的特殊变量 VPATH。当指明了该变量，make就会在当前目录找不到的情况下，到所指定的目录中去找寻文件了。 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">VPATH = src:../headers</span><br></pre></td></tr></table></figure> 上面的定义指定两个目录，“src”和“../headers”，make会按照这个顺序进行搜索。目录由“:”分隔。（当然，当前目录永远是最高优先搜索的地方） 另一个设置文件搜索路径的方法是使用make的“vpath”关键字（注意，它是全小写的），这不是变量，这是一个make的关键字，这和上面提到的那个VPATH变量很类似，但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种：</p><ul><li><p><code>vpath &lt;pattern&gt; &lt;directories&gt;</code> 为符合模式<code>&lt;pattern&gt;</code>的文件指定搜索目录<code>&lt;directories&gt;</code>。</p></li><li><p><code>vpath &lt;pattern&gt;</code> 清除符合模式<code>&lt;pattern&gt;</code>的文件的搜索目录。</p></li><li><p><code>vpath</code> 清除所有已被设置好了的文件搜索目录。</p><p>例如</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vpath = %.h ../headers</span><br></pre></td></tr></table></figure><p>该语句表示，要求make在''../headers'目录下搜索所有以 .h 结尾的文件。（如果某文件在当前目录没有找到的话）</p></li></ul><p>我们可以连续地使用vpath语句，以指定不同搜索策略。如果连续的vpath语句中出现了相同的<pattern> ，或是被重复了的<pattern>，那么，make会按照vpath语句的先后顺序来执行搜索。</p><h3 id="伪目标">伪目标</h3><p>“伪目标”并不是一个文件，只是一个标签，由于“伪目标”不是文件，所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效。当然，“伪目标”的取名不能和文件名重名，不然其就失去了“伪目标”的意义了。</p><p>当然，为了避免和文件重名的这种情况，我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”，向make说明，不管是否有这个文件，这个目标就是“伪目标”。 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.PHONY : clean</span><br></pre></td></tr></table></figure> 伪目标一般没有依赖的文件。但是，我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”，只要将其放在第一个。<strong>一个示例就是，如果你的Makefile需要一口气生成若干个可执行文件，但你只想简单地敲一个make完事，并且，所有的目标文件都写在一个Makefile中，那么你可以使用“伪目标”这个特性：</strong> <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">all : prog1 prog2 prog3</span><br><span class="line">.PHONY : all</span><br><span class="line"></span><br><span class="line">prog1 : prog1.o utils.o</span><br><span class="line">    cc -o prog1 prog1.o utils.o</span><br><span class="line"></span><br><span class="line">prog2 : prog2.o</span><br><span class="line">    cc -o prog2 prog2.o</span><br><span class="line"></span><br><span class="line">prog3 : prog3.o sort.o utils.o</span><br><span class="line">    cc -o prog3 prog3.o sort.o utils.o</span><br></pre></td></tr></table></figure></p><p>目标也可以成为依赖。所以，伪目标同样也可成为依赖。看下面的例子： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">.PHONY : cleanall cleanobj cleandiff</span><br><span class="line"></span><br><span class="line">cleanall : cleanobj cleandiff</span><br><span class="line">    rm program</span><br><span class="line"></span><br><span class="line">cleanobj :</span><br><span class="line">    rm *.o</span><br><span class="line"></span><br><span class="line">cleandiff :</span><br><span class="line">    rm *.diff</span><br></pre></td></tr></table></figure> “make cleanall”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这<strong>两个伪目标有点像“子程序”的意思</strong>。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。</p><h3 id="多目标">多目标</h3><p>Makefile的规则中的目标可以不止一个，其支持多目标，有可能我们的多个目标同时依赖于一个文件，并且其生成的命令大体类似。于是我们就能把其合并起来。减少冗余代码。多个目标的生成规则的执行命令不是同一个，这可能会给我们带来麻烦，不过好在我们可以使用一个自动化变量 <span class="math inline">\(@ （关于自动化变量，将在后面讲述），这个变量表示着目前规则中所有的目标的集合，这样说可能很抽象，还是看一个例子吧。 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">bigoutput littleoutput : text.g</span><br><span class="line">    generate text.g -<span class="variable">$(<span class="built_in">subst</span> output,,<span class="variable">$@</span>)</span> &gt; <span class="variable">$@</span></span><br></pre></td></tr></table></figure> 等价于 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">bigoutput : text.g</span><br><span class="line">    generate text.g -big &gt; bigoutput</span><br><span class="line">littleoutput : text.g</span><br><span class="line">    generate text.g -little &gt; littleoutput</span><br></pre></td></tr></table></figure> 其中， `-\)</span>(subst output,,<span class="math inline">\(@)` 中的 `\)</span><code>表示执行一个Makefile的函数，函数名为subst，后面的为参数。关于函数，将在后面讲述。这里的这个函数是替换字符串的意思，</code><span class="math inline">\(@` 表示目标的集合，就像一个数组， `\)</span>@` 依次取出目标，并执于命令。</p><h3 id="静态模式">静态模式</h3><p>静态模式可以更加容易地定义<strong>多目标</strong>的规则，可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;targets ...&gt; : &lt;target-pattern&gt; : &lt;prereq-patterns ...&gt;</span><br><span class="line">    &lt;commands&gt;</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure> + targets定义了一系列的目标文件，可以有通配符。是目标的一个集合。</p><ul><li><p>target-pattern是指明了targets的模式，也就是的目标集模式。</p></li><li><p>prereq-patterns是目标的依赖模式，它对target-pattern形成的模式再进行一次依赖目标的定义。</p></li></ul><p>例子： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">objects = foo.o bar.o</span><br><span class="line"></span><br><span class="line"><span class="section">all: <span class="variable">$(objects)</span></span></span><br><span class="line"></span><br><span class="line"><span class="variable">$(objects)</span>: %.o: %.c</span><br><span class="line">    <span class="variable">$(CC)</span> -c <span class="variable">$(CFLAGS)</span> <span class="variable">$&lt;</span> -o <span class="variable">$@</span></span><br></pre></td></tr></table></figure> 上面的例子中，指明了我们的目标从<code>$(objects)</code>中获取， <code>%.o</code> 表明要所有以 <code>.o</code> 结尾的目标，也就是 <code>foo.o bar.o</code> ，也就是变量 <code>$(objects)</code> 集合的模式，而依赖模式 <code>%.c</code> 则取模式 <code>%.o</code> 的 <code>%</code> ，也就是 <code>foo bar</code> ，并为其加下 <code>.c</code> 的后缀，于是，我们的依赖目标就是 <code>foo.c bar.c</code> 。而命令中的 <code>$&lt;</code> 和 <code>$@</code> 则是自动化变量， <code>$&lt;</code> 表示第一个依赖文件， <code>$@</code> 表示目标集（也就是“foo.o bar.o”）。于是，上面的规则展开后等价于下面的规则： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">foo.o : foo.c</span><br><span class="line">    <span class="variable">$(CC)</span> -c <span class="variable">$(CFLAGS)</span> foo.c -o foo.o</span><br><span class="line">bar.o : bar.c</span><br><span class="line">    <span class="variable">$(CC)</span> -c <span class="variable">$(CFLAGS)</span> bar.c -o bar.o</span><br></pre></td></tr></table></figure> 试想，如果我们的 %.o 有几百个，那么我们只要用这种很简单的“静态模式规则”就可以写完一堆规则，实在是太有效率了。“静态模式规则”的用法很灵活，如果用得好，那会是一个很强大的功能。再看一个例子：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">files = foo.elc bar.o lose.o</span><br><span class="line"></span><br><span class="line"><span class="variable">$(<span class="built_in">filter</span> %.o,<span class="variable">$(files)</span>)</span>: %.o: %.c</span><br><span class="line">    <span class="variable">$(CC)</span> -c <span class="variable">$(CFLAGS)</span> <span class="variable">$&lt;</span> -o <span class="variable">$@</span></span><br><span class="line"><span class="variable">$(<span class="built_in">filter</span> %.elc,<span class="variable">$(files)</span>)</span>: %.elc: %.el</span><br><span class="line">    emacs -f batch-byte-compile <span class="variable">$&lt;</span></span><br></pre></td></tr></table></figure><p><code>$(filter %.o,$(files))</code>表示调用Makefile的filter函数，过滤“<code>$(files)</code>”集，只要其中模式为“%.o”的内容。其它的内容，我就不用多说了吧。这个例子展示了Makefile中更大的弹性。</p><h3 id="自动生成依赖">自动生成依赖</h3><p>源文件可能包含头文件，会产生一个依赖关系。例如如果<code>main.c</code>包含头文件<code>defs.h</code>。那么就有一个依赖关系： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">main.o : main.c defs.h</span><br></pre></td></tr></table></figure> 由于头文件不编译生成文件，很容易会缺了这个依赖，这样一旦对这个头文件发生修改，make会察觉不到，导致修改不生效。 但是项目一大，这个依赖关系就会变得非常多而且复杂。每次添加或删除一个头文件引用，就要修改大量依赖关系。因此，可以使用在编译器使用<code>-M</code>参数，会打印出依赖关系，例如 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cc -M main.c</span><br></pre></td></tr></table></figure> 会输出 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">main.o : main.c defs.h</span><br></pre></td></tr></table></figure> 注意，如果你使用GNU的C/C++编译器，你得用 -MM 参数，不然， -M 参数会把一些标准库的头文件也包含进来。例如会输出 <figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">main.o: main.c /usr/include/stdc-predef.h defs.h /usr/include/stdio.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/libc-header-start.h \</span><br><span class="line"> /usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/wordsize.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/long-double.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/gnu/stubs.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/gnu/stubs-64.h \</span><br><span class="line"> /usr/lib/gcc/x86_64-linux-gnu/9/include/stddef.h \</span><br><span class="line"> /usr/lib/gcc/x86_64-linux-gnu/9/include/stdarg.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/types.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/timesize.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/typesizes.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/time64.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/types/__FILE.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/types/FILE.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/stdio_lim.h \</span><br><span class="line"> /usr/include/x86_64-linux-gnu/bits/sys_errlist.h</span><br></pre></td></tr></table></figure> 如何在makefile使用这个生成的依赖？不太可能是根据生成的依赖再生成一份makefile（每次修改都要重新生成一次），这个功能并不现实，不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中，<strong>为每一个 name.c 的文件都生成一个 name.d 的Makefile文件， .d 文件中就存放对应 .c 文件的依赖关系。</strong> 于是，我们可以写出 .c 文件和 .d 文件的依赖关系，并让make自动更新或生成 .d 文件，并把其包含在我们的主Makefile中，这样，我们就可以自动化地生成每个文件的依赖关系了。 这里，我们给出了一个模式规则来产生 .d 文件：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">%.d: %.c</span></span><br><span class="line">    @set -e; rm -f <span class="variable">$@</span>; \</span><br><span class="line">    <span class="variable">$(CC)</span> -M <span class="variable">$(CPPFLAGS)</span> <span class="variable">$&lt;</span> &gt; <span class="variable">$@</span>.$$$$; \</span><br><span class="line">    sed &#x27;s,\(<span class="variable">$*</span>\)\.o[ :]*,\1.o <span class="variable">$@</span> : ,g&#x27; &lt; <span class="variable">$@</span>.$$$$ &gt; <span class="variable">$@</span>; \</span><br><span class="line">    rm -f <span class="variable">$@</span>.$$$$</span><br></pre></td></tr></table></figure><p>这个规则的意思是，所有的 <code>.d</code> 文件依赖于 <code>.c</code> 文件， <code>rm -f $@</code> 的意思是删除所有的目标，也就是 <code>.d</code> 文件，第二行的意思是，为每个依赖文件 <code>$&lt;</code> ，也就是 <code>.c</code> 文件生成依赖文件， <code>$@</code> 表示模式 <code>%.d</code> 文件，如果有一个C文件是name.c，那么 <code>%</code> 就是 name ， <code>$$$$</code> 意为一个随机编号，第二行生成的文件有可能是“name.d.12345”，第三行使用<code>sed</code>命令做了一个替换，关于<code>sed</code>命令的用法请参看相关的使用文档。第四行就是删除临时文件。 总而言之，这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖，即把依赖关系：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">main.o : main.c defs.h</span><br></pre></td></tr></table></figure><p>转成： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">main.o main.d : main.c defs.h</span><br></pre></td></tr></table></figure></p><p>这样每个<code>.d</code>文件文件都可以自动更新了。最后，添加<code>include</code>指令将这些<code>.d</code>文件全部导入进来，这些<code>.d</code>文件就是内含依赖关系，这样就可以导入依赖关系了。</p><h2 id="书写命令">书写命令</h2><h3 id="命令的书写">命令的书写</h3><ul><li>每条命令会按照shell命令按顺序运行，每条命令必须以<code>Tab</code>开头，除非命令是紧跟在依赖规则后面的分号后的。</li><li>在命令行之间中的空格或是空行会被忽略，但是如果该空格或空行是以Tab键开头的，那么make会认为其是一个空命令。</li><li>make的命令默认是被 <code>/bin/sh</code> ——UNIX的标准Shell 解释执行的。除非特别指定一个其它的Shell。</li></ul><h3 id="显示命令">显示命令</h3><p>使用@不显示命令</p><ul><li><p>例：<code>@echo 123</code>可以不打印命令到屏幕上，只是运行它。</p></li><li><p>使用make的参数<code>-n</code>或<code>--just-print</code>可以只显示命令而不运行，有助于调试。(此时加了@的命令同样会被打印)</p></li><li><p>使用make的<code>-s</code>或 <code>--silent</code> 或 <code>--quiet</code> 则是全面禁止命令的显示。</p></li></ul><h3 id="命令执行">命令执行</h3><p>make执行命令应该是为每个命令fork一个进程来运行，因此如果使用了<code>cd</code>，并不会改变后续命令的工作目录。 如果需要将将第一条命令的结果应用到第二条命令，用<code>;</code>，例如：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">exec:</span><br><span class="line">cd /home/ubuntu ; pwd</span><br></pre></td></tr></table></figure><p>将打印<code>/home/ubuntu</code>， 而 <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">exec:</span><br><span class="line">cd /home/ubuntu</span><br><span class="line">pwd</span><br></pre></td></tr></table></figure> 将打印当前工作目录，并未发送改变</p><h3 id="命令出错">命令出错</h3><p>当命令运行完成后，make会检查每条命令是否出错（返回值非0），如果出错，会终止执行当前<strong>规则</strong>。这可能导致终止所有规则。</p><p>对于不打算检查出错命令，例如<code>mkdir</code>（文件夹存在时会放回非0值，但确保文件夹存在就是我们期望的）或<code>rm</code>（如果删除文件不存在，会放回非0值，确保文件不存在就是我们期望的），在命令前面添加<code>-</code>（在<code>Tab</code>后面）。 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">clean:</span></span><br><span class="line">-rm -f *.o</span><br></pre></td></tr></table></figure></p><p>make使用参数<code>-i</code>或<code>--ignore-errors</code>可以忽略所有错误； 使用参数<code>-k</code>或<code>--keep-going</code>可以终止出错的规则，但继续执行其它规则。 而如果一个规则是以 <code>.IGNORE</code> 作为目标的，那么这个规则中的所有命令将会忽略错误。例如 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.IGNORE : clean</span><br></pre></td></tr></table></figure></p><h3 id="嵌套执行make">嵌套执行make</h3><p>当项目很大时，不同模块文件夹下可能有各自的makefile，这时需要一个总控makefile来调用分散在其它模块中的makefile。 make提供了一些参数，有助于在makefile中嵌套使用make。例如进入某一目录执行该目录下的makefile： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">subsystem:</span></span><br><span class="line">    cd subdir &amp;&amp; <span class="variable">$(MAKE)</span></span><br></pre></td></tr></table></figure> 等价于 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">subsystem:</span></span><br><span class="line">    <span class="variable">$(MAKE)</span> -C subdir</span><br></pre></td></tr></table></figure></p><p>这里$(MAKE)是make命令的宏变量，因为嵌套运行make可能还需要带有一些参数。</p><p>Makefile的变量可以传递到下级的Makefile中（如果你显示的声明），但是<strong>不会覆盖下层的Makefile中所定义的变量</strong>，除非指定了 -e 参数。</p><p>在make指令后面指定变量，例如<code>make var=value -C subdir</code>，然后在子makefile中就可以使用传入的<code>var</code>变量</p><p>或是在父makefile中<code>export &lt;variable ...&gt;</code>显式指定要传入的子makefile的变量。单独的<code>export</code>会把所有变量传入子makefile。例如： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> variable1 = value</span><br><span class="line"><span class="keyword">export</span> variable2 += value</span><br><span class="line"><span class="keyword">export</span> variable2 := value</span><br></pre></td></tr></table></figure></p><p>如果不想让某些变量传递到下一层，可以声明<code>unexport &lt;variable ...&gt;</code></p><p>有两个变量，一个是 SHELL ，一个是 MAKEFLAGS ，这两个变量不管你是否export，其总是要传递到下层 Makefile中，特别是 MAKEFLAGS 变量，<strong>其中包含了make的参数信息</strong>，如果我们执行“总控Makefile”时有make参数或是在上层 Makefile中定义了这个变量，那么 MAKEFLAGS 变量将会是这些参数，并会传递到下层Makefile中，这是一个系统级的环境变量。但是make命令中的有几个参数并不往下传递，它们是 -C , -f , -h, -o 和 -W 。如果不想往下层传递参数，可以 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">subsystem:</span></span><br><span class="line">    cd subdir &amp;&amp; <span class="variable">$(MAKE)</span> MAKEFLAGS=</span><br></pre></td></tr></table></figure> 使用参数<code>-w</code>或<code>--print-directory</code>会在make过程中输出工作目录的变化。当使用 <code>-C</code> 参数指定make下层makefile时，<code>-w</code>会被自动打开。如果参数中含有<code>-s</code>(<code>--slient</code>或<code>--no-print-directory</code>)，那么<code>-w</code>会失效。</p><h3 id="定义命令包">定义命令包</h3><p>除了定义值的变量，还可以为命令序列定义一个变量，便于复用。命令序列的语法以 <code>define</code> 开始，以 <code>endef</code> 结束，如（注意可以不用<code>Tap</code>开头）: <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">define</span> run-yacc</span><br><span class="line">yacc <span class="variable">$(<span class="built_in">firstword</span> <span class="variable">$^</span>)</span></span><br><span class="line">mv y.tab.c <span class="variable">$@</span></span><br><span class="line"><span class="keyword">endef</span></span><br></pre></td></tr></table></figure> 可以像使用变量一样使用这个命令包 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">foo.c : foo.y</span><br><span class="line">    $(run-yacc)</span><br></pre></td></tr></table></figure></p><h2 id="使用变量">使用变量</h2><h3 id="变量基础">变量基础</h3><p>变量使用时要在变量名前面加上<code>$</code>符号，最好用<code>()</code>或<code>&#123;&#125;</code>把变量包起来（给变量加上括号完全是为了更加安全地使用这个变量）。如果要使用真实的 <code>$</code> 字符，那么你需要用 <code>$$</code> 来表示。 变量会在使用它的地方精确地展开，就像C/C++中的宏一样。</p><h3 id="变量中的变量">变量中的变量</h3><p>变量可以被一个变量定义，一种定义方法如下： <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">foo = <span class="variable">$(hi)</span></span><br><span class="line">hi = 123</span><br></pre></td></tr></table></figure> 右侧的变量不一定是要有定义的，这意味着可以把变量的真实值推后定义。 但是，这也可能导致递归定义，造成无限展开（make还是有能力检测这样的定义的，会报错）。例如：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CFLAGS = <span class="variable">$(CFLAGS)</span> -O</span><br></pre></td></tr></table></figure><p>如果在变量中使用函数（例如<code>$(shell pwd)</code>），可能会导致未知错误，你不知道这个函数会被调用多少次。</p><p>为了避免这种情况，可以使用<code>:=</code>定义，这样一个变量只能使用前面定义好的变量，不能使用后面的变量。 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">x := foo</span><br><span class="line">y := <span class="variable">$(x)</span> bar // 将等价于 y := foo bar</span><br><span class="line">x := later</span><br></pre></td></tr></table></figure></p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">y := <span class="variable">$(x)</span> bar // 将等价于 y := bar</span><br><span class="line">x := later</span><br></pre></td></tr></table></figure><p>定义变量时有一些需要注意的事情。如果需要定义一个空格，可以借助注释符<code>#</code>实现 <figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">nullstr :=</span><br><span class="line">space := <span class="variable">$(nullstr)</span> <span class="comment"># end of the line</span></span><br></pre></td></tr></table></figure> 直接为变量定义一个空格是困难的，这里可以先使用一个空变量确定开头，然后添加一个空格，最后使用一个注释符<code>#</code>结尾标识结尾。这样就有了一个空格的变量。 注释符<code>#</code>这个特性要特别地注意，如果不小心误用，可以会出现意想不到的结果。例如</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dir = /foo/bar    <span class="comment"># dir</span></span><br></pre></td></tr></table></figure><p>此时dir的值会是<code>/foo/bar</code>（后跟4个空格），如果后面用了<code>$(dir)/file</code>，就完蛋了。</p><p>还有一个有用的操作符<code>?=</code>。如果它左侧的变量没被定义过，那就将该变量赋为右侧的值；否则什么也不干。</p><h3 id="变量高级用法">变量高级用法</h3><ol type="1"><li>变量值的替换</li></ol><p>格式：<code>$(var:a=b)</code>或<code>$&#123;var:a=b&#125;</code>。将变量<code>var</code>中以a串结尾替换为b串，’结尾‘代表被空格或结束符隔开。例子：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">foo := a.o b.o c.o</span><br><span class="line">bar := <span class="variable">$(foo .o=.c)</span></span><br></pre></td></tr></table></figure><p><code>bar</code>的值为"a.c b.c c.c"。</p><ol start="2" type="1"><li>“把变量的值再当成变量”</li></ol><p>例子：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">x = y</span><br><span class="line">y = z</span><br><span class="line">a := $(<span class="variable">$(x)</span>)</span><br></pre></td></tr></table></figure><p><code>a</code>的值为"z"。</p><p>使用这种方式，还可以使用多个变量组成一个变量的名字：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">first_second = Hello</span><br><span class="line">a = first</span><br><span class="line">b = second</span><br><span class="line">all = $($a_$b)</span><br></pre></td></tr></table></figure><p><code>all</code>的值为"Hello"。</p><h3 id="追加变量值">追加变量值</h3><p>使用<code>+=</code>追加。</p><p>如果变量之前没有定义过，那么， <code>+=</code> 会自动变成 <code>=</code> ，如果前面有变量定义，那么 <code>+=</code> 会继承于前次操作的赋值符。如果前一次的是 <code>:=</code> ，那么 <code>+=</code> 会以 <code>:=</code> 作为其赋值符，如：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">variable := value</span><br><span class="line">variable += more</span><br></pre></td></tr></table></figure><p>等价于</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">variable := value</span><br><span class="line">variable := <span class="variable">$(variable)</span> more</span><br></pre></td></tr></table></figure><h3 id="多行变量">多行变量</h3><p><code>define</code>指示符可以定义含有多行内容的变量。前面讲过的“命令包”的技术就是利用这个关键字。</p><p><code>define</code>指示符后面跟着变量名，从第二行开始是变量内容，以endef结尾。</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">define</span> two-lines</span><br><span class="line">echo foo</span><br><span class="line">echo <span class="variable">$(bar)</span></span><br><span class="line"><span class="keyword">endef</span></span><br></pre></td></tr></table></figure><h3 id="override指示符">override指示符</h3><p>如果有变量是通过命令行指定的，那么在makefile中对该变量的赋值就会被忽略。如果不希望被忽略，可以使用在赋值语句前使用override指示符赋值。</p><p>格式如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">override &lt;variable&gt; = &lt;value&gt;</span><br><span class="line">override &lt;variable&gt; := &lt;value&gt;</span><br><span class="line">override &lt;variable&gt; += &lt;value&gt;</span><br></pre></td></tr></table></figure><p>例如对于<code>CFLAG</code>变量，无论如何都要加一个<code>-g</code>参数来调试。为了防止被命令行赋值覆盖，可以</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">override</span> CFLAGS += –g</span><br></pre></td></tr></table></figure><p>对于多行变量定义同样可以，在<code>define</code>前使用<code>override</code>即可。</p><h3 id="环境变量">环境变量</h3><p>make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中，但是如果Makefile中已定义了这个变量，或是这个变量由make命令行带入，那么系统的环境变量的值将被覆盖。（如果make指定了“-e”参数，那么，系统环境变量将覆盖Makefile中定义的变量）</p><p>因此，如果我们在环境变量中设置了 <code>CFLAGS</code> 环境变量，那么我们就可以在所有的Makefile中使用这个变量了。这对于我们使用统一的编译参数有比较大的好处。如果Makefile中定义了CFLAGS，那么则会使用Makefile中的这个变量，如果没有定义则使用系统环境变量的值，一个共性和个性的统一，很像“全局变量”和“局部变量”的特性。</p><p>当make嵌套调用时（参见前面的“嵌套调用”章节），上层Makefile中定义的变量会以系统环境变量的方式传递到下层的Makefile 中。当然，默认情况下，只有通过命令行设置的变量会被传递。而定义在文件中的变量，如果要向下层Makefile传递，则需要使用export关键字来声明。（参见前面章节）</p><h3 id="目标变量">目标变量</h3><p>前面都是全局变量，同样可以对特定的目标设置变量。这样设置的变量可以传递到由这个目标引发的所有规则过去。格式如下：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;target ...&gt; : &lt;variable-assignment&gt;</span><br><span class="line"></span><br><span class="line">&lt;target ...&gt; : overide &lt;variable-assignment&gt;</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">prog : CFLAGS = -g</span><br><span class="line">prog : prog.o foo.o bar.o</span><br><span class="line">    <span class="variable">$(CC)</span> <span class="variable">$(CFLAGS)</span> prog.o foo.o bar.o</span><br><span class="line"></span><br><span class="line">prog.o : prog.c</span><br><span class="line">    <span class="variable">$(CC)</span> <span class="variable">$(CFLAGS)</span> prog.c</span><br><span class="line"></span><br><span class="line">foo.o : foo.c</span><br><span class="line">    <span class="variable">$(CC)</span> <span class="variable">$(CFLAGS)</span> foo.c</span><br><span class="line"></span><br><span class="line">bar.o : bar.c</span><br><span class="line">    <span class="variable">$(CC)</span> <span class="variable">$(CFLAGS)</span> bar.c</span><br></pre></td></tr></table></figure><p>这样不管全局的CFLAGS是什么，在prog目标以及其引发的所有规则中，<code>$(CFLAGS)</code>都是<code>-g</code>。</p><h3 id="模式变量">模式变量</h3><p>还可以对模式变量设置目标变量，格式如下：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;pattern ...&gt;; : &lt;variable-assignment&gt;;</span><br><span class="line"></span><br><span class="line">&lt;pattern ...&gt;; : <span class="keyword">override</span> &lt;variable-assignment&gt;;</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">%.o : CFLAGS = -O</span><br></pre></td></tr></table></figure><p>这样所有<code>.o</code>结尾的目标的<code>CFLAGS</code>都是<code>-O</code>了。</p><h2 id="条件判断">条件判断</h2><h3 id="ifeqifneq"><code>ifeq</code>、<code>ifneq</code></h3><p>示例</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">libs_for_gcc = -lgnu</span><br><span class="line">normal_libs =</span><br><span class="line"></span><br><span class="line"><span class="section">foo: <span class="variable">$(objects)</span></span></span><br><span class="line"><span class="keyword">ifeq</span> (<span class="variable">$(CC)</span>,gcc)</span><br><span class="line">    <span class="variable">$(CC)</span> -o foo <span class="variable">$(objects)</span> <span class="variable">$(libs_for_gcc)</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    <span class="variable">$(CC)</span> -o foo <span class="variable">$(objects)</span> <span class="variable">$(normal_libs)</span></span><br><span class="line"><span class="keyword">endif</span></span><br></pre></td></tr></table></figure><p>ifeq的语法如下：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ifeq</span> (&lt;arg1&gt;, &lt;arg2&gt;)</span><br><span class="line"><span class="keyword">ifeq</span> &#x27;&lt;arg1&gt;&#x27; &#x27;&lt;arg2&gt;&#x27;</span><br><span class="line"><span class="keyword">ifeq</span> <span class="string">&quot;&lt;arg1&gt;&quot;</span> <span class="string">&quot;&lt;arg2&gt;&quot;</span></span><br><span class="line"><span class="keyword">ifeq</span> <span class="string">&quot;&lt;arg1&gt;&quot;</span> &#x27;&lt;arg2&gt;&#x27;</span><br><span class="line"><span class="keyword">ifeq</span> &#x27;&lt;arg1&gt;&#x27; <span class="string">&quot;&lt;arg2&gt;&quot;</span></span><br></pre></td></tr></table></figure><p>ifneq类似</p><h3 id="ifdefifndef"><code>ifdef</code>、<code>ifndef</code></h3><p>ifdef可以查看变量是否被定义，语法如下：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ifdef</span> &lt;variable-name&gt;</span><br></pre></td></tr></table></figure><p>注意<code>ifdef</code> 只是测试一个变量是否有值，其并不会把变量扩展到当前位置。例如</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">bar =</span><br><span class="line">foo = <span class="variable">$(bar)</span></span><br><span class="line"><span class="keyword">ifdef</span> foo</span><br><span class="line">    frobozz = yes</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    frobozz = no</span><br><span class="line"><span class="keyword">endif</span></span><br></pre></td></tr></table></figure><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">foo =</span><br><span class="line"><span class="keyword">ifdef</span> foo</span><br><span class="line">    frobozz = yes</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    frobozz = no</span><br><span class="line"><span class="keyword">endif</span></span><br></pre></td></tr></table></figure><p>第一个例子中， <code>$(frobozz)</code> 值是 <code>yes</code> ，第二个则是 <code>no</code>。</p><p>还有<code>ifndef</code>，就是<code>ifdef</code>的反义词。</p><p>特别注意的是，make是在读取Makefile时就计算条件表达式的值，并根据条件表达式的值来选择语句，所以，你最好不要把自动化变量（如 <code>$@</code> 等）放入条件表达式中，因为自动化变量是在运行时才有的。</p><h2 id="使用函数">使用函数</h2><h3 id="函数的调用语法">函数的调用语法</h3><p>和变量非常类似，语法如下：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$(&lt;function&gt; &lt;arguments&gt;)</span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line">$&#123;&lt;function&gt; &lt;arguments&gt;&#125;</span><br></pre></td></tr></table></figure><p>函数名和参数间用空格隔开，参数之间用逗号隔开。例子：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">comma:= ,</span><br><span class="line">empty:=</span><br><span class="line">space:= <span class="variable">$(empty)</span> <span class="variable">$(empty)</span></span><br><span class="line">foo:= a b c</span><br><span class="line">bar:= <span class="variable">$(<span class="built_in">subst</span> <span class="variable">$(space)</span>,<span class="variable">$(comma)</span>,<span class="variable">$(foo)</span>)</span></span><br></pre></td></tr></table></figure><p><code>subst</code>是一个替换函数，第一个参数是被替换字符串，第二个参数替换的字符串，第三个参数替换操作作用的字符串。执行完上面的指令后，<code>$(foo)</code>的<code>a b c</code>替换成<code>a,b,c</code>赋给<code>bar</code>。</p><p>make有很多函数可用，具体查看<a href="https://seisman.github.io/how-to-write-makefile/functions.html">使用函数 — 跟我一起写Makefile</a>。</p><h2 id="make的运行">make的运行</h2><h3 id="make退出码">make退出码</h3><p><strong>0</strong>：表示成功执行</p><p><strong>1</strong>：如果make运行时出现任何错误，返回1</p><p><strong>2</strong>：如果使用"-q"参数，并且make使得一些目标不需要更新，那么返回2</p><h3 id="指定makefile">指定makefile</h3><p>假如你有一个makefile文件<code>hchen.mk</code>，可用使用<code>-f</code>（或<code>--file</code>，<code>--makefile</code>）参数告诉make使用哪个makefile。</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">make -f hchen.mk</span><br></pre></td></tr></table></figure><h3 id="指定目标">指定目标</h3><p>默认情况下，make的最终目标是makefile中第一个目标。也可以在make之后添加目标名指定最终目标，例如<code>make clean</code>，说明最终目标是<code>clean</code>。任何在makefile中的目标都可以被指定成终极目标，但是除了以 <code>-</code> 打头，或是包含了 <code>=</code> 的目标，因为有这些字符的目标，会被解析成命令行参数或是变量。甚至没有被我们明确写出来的目标也可以成为make的终极目标，也就是说，只要make可以在隐含规则推导中找到这个隐含目标，同样可以被指定成终极目标。</p><p>make的环境变量<code>MAKECMDGOALS</code>存放你指定的终极目标列表。如果在命令行上没有指定最终目标，这个变量是空值。这个变量可用用在一些特殊情况下，例如：</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sources = foo.c bar.c</span><br><span class="line"><span class="keyword">ifneq</span> ( <span class="variable">$(MAKECMDGOALS)</span>,clean)</span><br><span class="line">    <span class="keyword">include</span> $(sources:.c=.d)</span><br><span class="line"><span class="keyword">endif</span></span><br></pre></td></tr></table></figure><p>上面指令功能是只要输入的命令不是"make clean"，那么makefile会自动加载"foo.d"和"bar.d"这两个makefile。</p><p>在Unix世界中，软件发布时，特别是GNU这种开源软件的发布时，其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。</p><ul><li>all:这个伪目标是所有目标的目标，其功能一般是编译所有的目标。</li><li>clean:这个伪目标功能是删除所有被make创建的文件。</li><li>install:这个伪目标功能是安装已编译好的程序，其实就是把目标执行文件拷贝到指定的目标中去。</li><li>print:这个伪目标的功能是例出改变过的源文件。</li><li>tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。</li><li>dist:这个伪目标功能是创建一个压缩文件，一般是把tar文件压成Z文件。或是gz文件。</li><li>TAGS:这个伪目标功能是更新所有的目标，以备完整地重编译使用。</li><li>check和test:这两个伪目标一般用来测试makefile的流程。</li></ul><p>为自己的makefile添加上面功能，更显专业。</p><h3 id="检查规则">检查规则</h3><p>有时我们并不想要运行makefile，而是想要检查makefile执行情况，下面参数会有所帮助：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-n`, `--just-print`, `--dry-run`, `--recon</span><br></pre></td></tr></table></figure><p>不执行参数，这些参数只是打印命令，不管目标是否更新，把规则和连带规则下的命令打印出来，但不执行，这些参数对于我们调试makefile很有用处。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-t`, `--touch</span><br></pre></td></tr></table></figure><p>这个参数的意思就是把目标文件的时间更新，但不更改目标文件。也就是说，make假装编译目标，但不是真正的编译目标，只是把目标变成已编译过的状态。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-q`, `--question</span><br></pre></td></tr></table></figure><p>这个参数的行为是找目标的意思，也就是说，如果目标存在，那么其什么也不会输出，当然也不会执行编译，如果目标不存在，其会打印出一条出错信息。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-W &lt;file&gt;`, `--what-if=&lt;file&gt;`, `--assume-new=&lt;file&gt;`, `--new-file=&lt;file&gt;</span><br></pre></td></tr></table></figure><p>这个参数需要指定一个文件。一般是是源文件（或依赖文件），Make会根据规则推导来运行依赖于这个文件的命令，一般来说，可以和“-n”参数一同使用，来查看这个依赖文件所发生的规则命令。</p><h3 id="make参数">make参数</h3><p>不同版本厂商make参数大同小异，GNU make 3.80版见<a href="https://seisman.github.io/how-to-write-makefile/invoke.html">make的参数</a>。</p><h2 id="makefile常用的代码片段">makefile常用的代码片段</h2><p>下面列出一些常用需求对应的makefile代码，以便快速上手：</p><blockquote><p>施工中</p></blockquote>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;概述&quot;&gt;概述&lt;/h2&gt;
&lt;p&gt;C/C++ 编译流程： 编译、汇编、链接&lt;/p&gt;
&lt;p&gt;在汇编完成后，会生成中间文件。windows下是&lt;code&gt;.obj&lt;/code&gt;，Unix下是&lt;code&gt;.o&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;编译时，只要源代码的语法正确，编译器就可以编译出中间目标文件。&lt;/p&gt;
&lt;p&gt;链接时，主要是链接函数和全局变量。所以，我们可以使用这些中间目标文件（ &lt;code&gt;.o&lt;/code&gt; 文件或 &lt;code&gt;.obj&lt;/code&gt; 文件）来链接我们的应用程序。在大多数时候，由于源文件太多，编译生成的中间目标文件太多，而在链接时需要明显地指出中间目标文件名，这对于编译很不方便。所以，我们要给中间目标文件打个包，在Windows下这种包叫“库文件”（Library File），也就是 &lt;code&gt;.lib&lt;/code&gt; 文件，在UNIX下，是Archive File，也就是 &lt;code&gt;.a&lt;/code&gt; 文件。&lt;/p&gt;
&lt;h3 id=&quot;参考&quot;&gt;参考&lt;/h3&gt;
&lt;p&gt;基本就是下面的教程的搬运+一点的自己理解&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://seisman.github.io/how-to-write-makefile/overview.html&quot;&gt;概述 — 跟我一起写Makefile 1.0 文档 (seisman.github.io)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/376493209&quot;&gt;GNU make/Makefile 简明实用教程 - 知乎 (zhihu.com)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="技术" scheme="https://www.limil.top/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="构建系统" scheme="https://www.limil.top/categories/%E6%8A%80%E6%9C%AF/%E6%9E%84%E5%BB%BA%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="makefile" scheme="https://www.limil.top/tags/makefile/"/>
    
  </entry>
  
  <entry>
    <title>OpenGL踩过的坑</title>
    <link href="https://www.limil.top/opengl-trouble-shotting/"/>
    <id>https://www.limil.top/opengl-trouble-shotting/</id>
    <published>2024-09-25T15:13:24.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<p>学习OpenGL的过程中想要实现一些效果，遇到的坑和找到的解决方案，记录一下。</p><span id="more"></span><ul><li><p>模板缓冲<code>glStencilMask(0x00)</code>会使缓冲无效，也会使清空无效！清空之前模板缓冲要设置<code>glStencilMask(0xFF)</code>。</p></li><li><p>使用<code>glViewport</code>设置视口大小。如果不想结果看起来拉伸，视口的长宽比要和相机相同</p></li><li><p>技巧：使用<code>glViewport</code>来渲染小窗，而不是使用子窗口：<a href="https://stackoverflow.com/questions/10468327/how-to-use-subwindows-in-opengl">How to use subwindows in opengl - Stack Overflow</a>。使用<code>glViewport</code>来渲染小窗时要避免<code>glClear</code>清空带来的影响，<code>glClear</code>会直接清空整个页面。解决方法是使用<code>glScissor</code>：<a href="https://stackoverflow.com/questions/18830589/shouldnt-glclearcolor-obey-the-drawing-area-set-by-glviewport">opengl - Shouldn't glClearColor() obey the drawing area set by glViewport()? - Stack Overflow</a>。如果是重叠的窗口，注意渲染顺序，先渲染大的，再渲染小的。</p></li><li><p>几何着色器中，<code>gl_position</code>应该是在NDC空间中，所以对它的处理也要在NDC中处理。例如法向量必须通过<code>gl_position</code>求出NDC空间下的法向量，不能使用相机坐标系中的法向量；同理对不能用NDC的法向量对相机坐标系中点做处理。它们必须分开处理。传递给片元着色器计算光照应该是相机坐标系中的点，而不是NDC空间中的点。</p></li><li><p>帧缓冲附加纹理要在帧缓冲激活前附加上去！</p></li><li><p>模板帧缓冲的使用：<a href="https://stackoverflow.com/questions/11084961/binding-a-stencil-render-buffer-to-a-frame-buffer-in-opengl">framebuffer - Binding a stencil render buffer to a frame buffer in opengl - Stack Overflow</a></p></li><li><p><a href="https://stackoverflow.com/questions/19169576/access-depth-stencil-texture-in-a-shader-program">opengl - Access depth-stencil texture in a shader program - Stack Overflow</a></p></li><li><p><a href="https://stackoverflow.com/questions/13171036/visualizing-the-stencil-buffer-to-a-texture/13171346#13171346">macos - Visualizing the Stencil Buffer to a texture - Stack Overflow</a></p></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;学习OpenGL的过程中想要实现一些效果，遇到的坑和找到的解决方案，记录一下。&lt;/p&gt;</summary>
    
    
    
    <category term="图形" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/"/>
    
    <category term="渲染" scheme="https://www.limil.top/categories/%E5%9B%BE%E5%BD%A2/%E6%B8%B2%E6%9F%93/"/>
    
    
    <category term="opengl" scheme="https://www.limil.top/tags/opengl/"/>
    
  </entry>
  
  <entry>
    <title>C++中左值和右值的理解</title>
    <link href="https://www.limil.top/lvalue-and-rvalue/"/>
    <id>https://www.limil.top/lvalue-and-rvalue/</id>
    <published>2024-09-25T15:07:07.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<p>先看看C++ Primer的解释：</p><blockquote><p>C++的表达式要不然是右值，要不然就是左值。这两个名词是从C语言继承过来的，原本是为了帮助记忆：左值可以位于赋值语句的左侧，右值则不能。</p><p>在C++语言中，二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数，然而以常量对象为代表的某些左值实际上是不能作为赋值语句的左侧运算对象。此外，虽然某些表达式的求值结果是对象，但它们是右值而非左值。<strong>可以做一个简单的归纳；当一个对象被用作右值的时候，用的是对象的值（内容）；当对象被用作左值的时候，用的是对象的身份（在内存中的位置）。</strong></p><p>使用关键字<code>decltype</code>的时候，左值和右值也有所不同。如果表达式的求值结果是左值，<code>decltype</code>作用于该表达式（不是变量）得到一个引用类型。举个例子，假定<code>p</code>的类型是<code>int*</code>，因为解引用运算符生成左值，所以<code>decltype(*p)</code>的结果是<code>int&amp;</code>。另一方面，因为取地址运算符生成右值，所以<code>decltype(&amp;p)</code>的结果<code>int**</code>，也就是说，结果过是一个指向整数指针的指针。</p></blockquote><span id="more"></span><p>右值是值。一些字面值常量是右值，例如<code>1</code>。这样的右值可以不需要内存，如果编译成汇编来看，<code>1</code>可能仅仅是存在寄存器里面的，并没有使用任何内存。</p><p>但是并不是所有右值都是没有内存的。例如函数返回的非引用对象，这样的对象称为<strong>亡值</strong>。这样的对象相当于一块没有名字的临时内存，除了在返回处马上被用来初始化一个新对象，过了这一句后没有任何方法可以访问到它，语义上就像值一样。被用完后这个临时对象马上就会被销毁。（当然，有RVO优化，返回值不一定会创建一个临时对象，而是会通过栈直接传递对象，减少一次拷贝构造的调用）。</p><p>因此不能简单地用有没有地址，能放在等号哪侧来判断左右值。要看<strong>语义上</strong>表达式结果是对象（左值）还是值（右值）。例如：返回类型为引用类型代表结果是左值，非引用类型代表结果是右值；解引用结果是左值，取地址结果是右值。 ## 右值引用</p><p>和左值引用一样，右值引用也是一种别名。区别在于，左值引用只能引用左值；右值引用只能引用右值。</p><p>右值引用相当于给无名的临时对象起了个名字。一个块有名字的内存，它和变量没有什么区别。事实上，右值引用就是变量，你甚至可以对它取地址。即使是对<code>1</code>这样的不需要内存的字面值，其右值引用会为其创建一个内存空间，并和引用名绑定起来。因此右值引用本身是左值也不出奇了。</p><p>右值引用延长了右值的生命周期，将右值（临时对象）的寿命和右值引用名的寿命绑定起来了。为的就是可以在移动语义下对临时对象进行操作，执行移动构造等操作。<strong>右值引用相当于一个承诺，承诺它引用的对象（或其资源）马上就会被销毁（比如右值引用名的寿命结束后），不再会被别人使用，因此你才可以自由接管这个对象的资源，而无需拷贝。</strong></p><p>如果使用<code>std::move</code>，可以将一个左值当成右值来处理，这样也就可以将其用在移动语义下了。此时这个左值的生命周期并不会随着右值引用名寿命结束而结束，因此被移动后的左值可能会失去它原有的资源（比如被移动后的<code>string</code>字符串会变成空串），这是右值引用的“承诺”带来的。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;先看看C++ Primer的解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;C++的表达式要不然是右值，要不然就是左值。这两个名词是从C语言继承过来的，原本是为了帮助记忆：左值可以位于赋值语句的左侧，右值则不能。&lt;/p&gt;
&lt;p&gt;在C++语言中，二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数，然而以常量对象为代表的某些左值实际上是不能作为赋值语句的左侧运算对象。此外，虽然某些表达式的求值结果是对象，但它们是右值而非左值。&lt;strong&gt;可以做一个简单的归纳；当一个对象被用作右值的时候，用的是对象的值（内容）；当对象被用作左值的时候，用的是对象的身份（在内存中的位置）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用关键字&lt;code&gt;decltype&lt;/code&gt;的时候，左值和右值也有所不同。如果表达式的求值结果是左值，&lt;code&gt;decltype&lt;/code&gt;作用于该表达式（不是变量）得到一个引用类型。举个例子，假定&lt;code&gt;p&lt;/code&gt;的类型是&lt;code&gt;int*&lt;/code&gt;，因为解引用运算符生成左值，所以&lt;code&gt;decltype(*p)&lt;/code&gt;的结果是&lt;code&gt;int&amp;amp;&lt;/code&gt;。另一方面，因为取地址运算符生成右值，所以&lt;code&gt;decltype(&amp;amp;p)&lt;/code&gt;的结果&lt;code&gt;int**&lt;/code&gt;，也就是说，结果过是一个指向整数指针的指针。&lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    <category term="技术" scheme="https://www.limil.top/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="C/C++" scheme="https://www.limil.top/categories/%E6%8A%80%E6%9C%AF/C-C/"/>
    
    
    <category term="C/C++" scheme="https://www.limil.top/tags/C-C/"/>
    
  </entry>
  
  <entry>
    <title>C语言中的位域</title>
    <link href="https://www.limil.top/bit-field/"/>
    <id>https://www.limil.top/bit-field/</id>
    <published>2024-09-25T14:39:26.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<p>表示一个bool值，只要0和1两种状态，但是不得不使用char来表示，这会造成浪费。而位域就是可以让我们精细地控制位大小的方法。</p><p>所谓"位域"是把一个字节中的二进位<strong>划分为几个不同的区域</strong>，并说明每个区域的位数。每个域有一个域名，允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。</p><p>位域的类型不可以是浮点型，可以是整型或字符型。它会像正常的类型一样参与运算，只是比特位不同，运算规则是相同的。</p><span id="more"></span><p>一个例子：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">bs</span>&#123;</span><br><span class="line">    <span class="type">int</span> a:<span class="number">8</span>;</span><br><span class="line">    <span class="type">int</span> b:<span class="number">2</span>;</span><br><span class="line">    <span class="type">int</span> c:<span class="number">6</span>;</span><br><span class="line">&#125;data;</span><br></pre></td></tr></table></figure><p>可以存在空域</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">bs</span>&#123;</span><br><span class="line">    <span class="type">unsigned</span> a:<span class="number">4</span>;</span><br><span class="line">    <span class="type">unsigned</span>  :<span class="number">4</span>;    <span class="comment">/* 空域 */</span></span><br><span class="line">    <span class="type">unsigned</span> b:<span class="number">4</span>;    <span class="comment">/* 从下一单元开始存放 */</span></span><br><span class="line">    <span class="type">unsigned</span> c:<span class="number">4</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>位域的宽度不能超过它所依附的数据类型的长度，成员变量都是有类型的，这个类型限制了成员变量的最大长度，<code>:</code> 后面的数字不能超过这个长度。</li><li>位域可以是无名位域，这时它只用来作填充或调整位置。无名的位域是不能使用的。</li></ul><h2 id="字节对齐">字节对齐</h2><p>结构体大小与内部声明的成员变量类型和顺序有关，也与对齐方式有关。</p><p>C语言结构体字节对齐有几个准则：</p><ol type="1"><li><p>各成员变量在类内起始地址偏移必须是成员大小的整数倍。<strong>因为访问数据的地址要满足一定的条件，能被这个数据的长度所整除。 例如，1字节数据必定是对齐的，2字节的数据的偏移要被2整除，4字节的数据地址要被4整除。</strong></p></li><li><p>这个类的总大小必须是最大成员的整数倍。（或者可以说：struct末尾如果紧贴着一个相同类型的struct，必须满足够使下一个struct内成员对齐）</p></li></ol><p>强制对齐可以使结构体按照指定大小对齐，使用<code>#pragma pack(n)</code>指定对齐大小；使用<code>#pragma pop</code>恢复对齐状态。</p><p>未对齐的数据会导致CPU：</p><ol type="1"><li>产生一个异常条件</li><li>执行多次对齐访问，以便读取完整的未对齐数据值。（因为地址存取粒度不是单字节，而是双字节、四字节等）</li></ol><h2 id="位域的对齐">位域的对齐</h2><ul><li>如果相邻位域字段的类型相同，且其位宽之和小于类型的 sizeof 大小，则后面的字段将紧邻前一个字段存储，直到不能容纳为止；</li><li>如果相邻位域字段的类型相同，但其位宽之和大于类型的 sizeof 大小，则后面的字段将从新的存储单元开始，其偏移量为其类型大小的整数倍；</li><li>如果相邻的位域字段的类型不同，后面字段从下一个单元开始。</li><li>整个结构体的总大小为最宽基本类型成员大小的整数倍。</li></ul><h2 id="参考">参考</h2><ul><li><a href="https://www.cnblogs.com/bakari/archive/2012/08/27/2658956.html">数据对齐详解 - 猿大白 - 博客园 (cnblogs.com)</a></li><li><a href="https://blog.csdn.net/dongyanxia1000/article/details/50772264">位域及对齐_cany1000的博客-CSDN博客_位域对齐</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;表示一个bool值，只要0和1两种状态，但是不得不使用char来表示，这会造成浪费。而位域就是可以让我们精细地控制位大小的方法。&lt;/p&gt;
&lt;p&gt;所谓&quot;位域&quot;是把一个字节中的二进位&lt;strong&gt;划分为几个不同的区域&lt;/strong&gt;，并说明每个区域的位数。每个域有一个域名，允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。&lt;/p&gt;
&lt;p&gt;位域的类型不可以是浮点型，可以是整型或字符型。它会像正常的类型一样参与运算，只是比特位不同，运算规则是相同的。&lt;/p&gt;</summary>
    
    
    
    <category term="技术" scheme="https://www.limil.top/categories/%E6%8A%80%E6%9C%AF/"/>
    
    <category term="C/C++" scheme="https://www.limil.top/categories/%E6%8A%80%E6%9C%AF/C-C/"/>
    
    
    <category term="C/C++" scheme="https://www.limil.top/tags/C-C/"/>
    
  </entry>
  
  <entry>
    <title>关于我</title>
    <link href="https://www.limil.top/about-me/"/>
    <id>https://www.limil.top/about-me/</id>
    <published>2024-09-16T10:43:28.000Z</published>
    <updated>2026-09-27T05:29:20.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="博客发展历程">博客发展历程</h2><p>博客园 -&gt; WordPress -&gt; Hexo</p><p>全面支持k3s部署！</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;博客发展历程&quot;&gt;博客发展历程&lt;/h2&gt;
&lt;p&gt;博客园 -&amp;gt; WordPress -&amp;gt; Hexo&lt;/p&gt;
&lt;p&gt;全面支持k3s部署！&lt;/p&gt;
</summary>
      
    
    
    
    
  </entry>
  
</feed>
