<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>柚子其实是蜜柑</title><description>游戏开发、引擎技术与生活记录</description><link>https://www.tanedanobata.top/</link><language>zh_CN</language><item><title>远征IM@S Live</title><link>https://www.tanedanobata.top/posts/imas-live/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/imas-live/</guid><pubDate>Mon, 14 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;国内 P 如何远征偶像大师 Live
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;确定企划 Live 时间, 地点, 出演人员&lt;/h2&gt;
&lt;p&gt;偶像大师公式门户站 https://idolmaster-official.jp/&lt;br /&gt;
找到其中的ライブ/イベント, 可以看到相关的所有信息。&lt;/p&gt;
&lt;h2&gt;注册账号&lt;/h2&gt;
&lt;p&gt;Asobi Store https://asobistore.jp/&lt;br /&gt;
注册时居住国家需选择日本, 出生日期必须匹配, 出票会进行比对。&lt;/p&gt;
&lt;p&gt;注册成功后 Event 界面可以进行购票。&lt;/p&gt;
&lt;h2&gt;信息录入&lt;/h2&gt;
&lt;p&gt;填写个人信息时, 不要输入汉字, 输入拼音。&lt;/p&gt;
&lt;p&gt;地址在地图上随便找一个公寓地址, 不要填写一户建或商家, 避免被投诉。&lt;/p&gt;
&lt;p&gt;电话随便填写一个。&lt;/p&gt;
&lt;h2&gt;票种&lt;/h2&gt;
&lt;p&gt;出售票种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;抽选&lt;/strong&gt; 全席位指定(最普通的坐票)和更好的位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先着&lt;/strong&gt; 剩余的全席指定和好票, 立见指定票和位置较差的票。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公式リセール&lt;/strong&gt; 官方换票。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当日票&lt;/strong&gt; 售票时间较晚的票, 全席指定没卖完。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般抽选的票体验最好, 但官方公布较晚, 甚至当天才会公布, 需要多关注推特或者公演官网。&lt;/p&gt;
&lt;h2&gt;席位&lt;/h2&gt;
&lt;p&gt;席位种类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;全席指定&lt;/strong&gt; 坐票, 好坏皆有&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;立见指定&lt;/strong&gt; 站票, 告别好席, 主舞台可能被遮挡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;着席指定&lt;/strong&gt; 全程就坐, 不能爽打 Call&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;見切り席/完全見切り席&lt;/strong&gt; 被遮挡席位, 无法完整看到甚至无法看到表演&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SS 席/S 席&lt;/strong&gt; 好位置票, 通常带特典&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SPラウンジ席&lt;/strong&gt; VIP 休息间席位&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;バルコニー席&lt;/strong&gt; 露台席, 离舞台较远&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;大会员&lt;/h2&gt;
&lt;p&gt;大会员抽票一般先行且更可能抽到好票。&lt;/p&gt;
&lt;p&gt;在 ASOBI STORE 的プレミアム会とは 可以进入大会员商店。&lt;/p&gt;
&lt;p&gt;注意大会员要开就开年票, 月票是无法参与大会员抽选的。&lt;/p&gt;
&lt;p&gt;开大会员需要办理一张 JCB 的卡, Visa 卡基本不可以开通。&lt;/p&gt;
</content:encoded></item><item><title>国民经济核算</title><link>https://www.tanedanobata.top/Economics/%E5%AE%8F%E8%A7%82%E7%BB%8F%E6%B5%8E%E5%AD%A6%E5%85%A5%E9%97%A8/</link><guid isPermaLink="true">https://www.tanedanobata.top/Economics/%E5%AE%8F%E8%A7%82%E7%BB%8F%E6%B5%8E%E5%AD%A6%E5%85%A5%E9%97%A8/</guid><pubDate>Thu, 10 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;国民经济核算&lt;/h1&gt;
&lt;h2&gt;Introduce&lt;/h2&gt;
&lt;p&gt;宏观经济学主要围绕 &lt;strong&gt;经济增长&lt;/strong&gt; , &lt;strong&gt;就业&lt;/strong&gt; 和 &lt;strong&gt;物价&lt;/strong&gt; 展开研究。（产出, 事业和通胀）&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250710201435.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;宏观经济学核心研究的社会组织是 &lt;strong&gt;家庭&lt;/strong&gt; , &lt;strong&gt;企业&lt;/strong&gt; 和 &lt;strong&gt;部门&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;宏观经济学认为有 &lt;strong&gt;产品市场&lt;/strong&gt; , &lt;strong&gt;货币市场&lt;/strong&gt; 和 &lt;strong&gt;劳动力市场&lt;/strong&gt; 三个市场。&lt;/p&gt;
&lt;h2&gt;GDP&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Gross_domestic_product&quot;&gt;Gross domestic product - Wikipedia&lt;/a&gt;, 国内生产总值。&lt;/p&gt;
&lt;p&gt;如果所有环节的价值增值等于参与到这个环节的所有人的收入总和, 而一件商品的产出是所有这些环节价值增值的总和。那么一件商品的产出, 就应该等于生产这件商品的全部环节中, 所有各种要素提供者的收入总和, 也就是总收入。&lt;br /&gt;
需要理解, 产出=支出=收入。&lt;/p&gt;
&lt;p&gt;也就是说 GDP=总产出=总收入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GDP 是什么&lt;/strong&gt;：一段时间内, 一个地区新生产的所有最终产品和劳动的全部市场价值。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一段时间：指的是一个时间段&lt;/li&gt;
&lt;li&gt;一个地区：指的是一个地域范围&lt;/li&gt;
&lt;li&gt;新生产的：指的是在这段时间新创造的, 以前的不算&lt;/li&gt;
&lt;li&gt;最终产品和劳务：指的是最后的使用者使用且消费掉的产品或者劳务&lt;/li&gt;
&lt;li&gt;市场价值：指的是必须进入市场流通, 自产自销的不算&lt;/li&gt;
&lt;li&gt;价值：指的是用钱来衡量的价值&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;GDP 核算方法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;收入法&lt;/li&gt;
&lt;li&gt;支出法&lt;/li&gt;
&lt;li&gt;生产法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;支出法&lt;/strong&gt;：&lt;br /&gt;
家庭支出 $C$ -各种消费品和服务。&lt;br /&gt;
企业支出 $I$ -固定资产投资(新的, 不包括原材料, 支出法的投资算法包含了重置投资)；存货投资(库存, 注意已经核算的不会在第二年重复核算)。&lt;br /&gt;
政府支出 $G$ -政府购买(购买公共服务或公共产品)&lt;br /&gt;
外贸-净出口(出口总额 $X$ -进口总额 $M$)
$$
GDP=C+I+G+(X-M)
$$
也可以说 GDP=消费+投资+政府购买+净出口&lt;br /&gt;
存货投资：支出法认为, 在本段时间内新生产但没有卖出去的库存属于企业自己花钱买自己的东西, 然后放在仓库明年直接拿出来卖, 所以算作投资。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q&lt;/strong&gt;：为什么从公司债中得到的利息收入算作 GDP, 而政府公债得到的利息不算 GDP&lt;br /&gt;
&lt;strong&gt;A&lt;/strong&gt;：宏观经济学假定了公司会拿债务去进行投资, 债权人是资产要素提供者, 本金和利息是对提供者的补偿, 这是公司的投资支出, 也是债权人的收入, 故可以算入 GDP。政府举债并不会投入再生产, 政府的利息支出并不是生产所得, 而是用其他纳税人的前作为利息给债权人, 故是一种转移支付。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q&lt;/strong&gt;：买卖股票赚的钱为什么不计入 GDP&lt;br /&gt;
&lt;strong&gt;A&lt;/strong&gt;：买卖股票在经济学上是一种投机而不是投资, 钱只是从 A 转移到 B, 而没有投入生产, 故不算入 GDP&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q&lt;/strong&gt;：生产10亿, 卖了8亿, 剩下2亿算作存货投资。那么明年卖出2亿库存, 明年消费支出会增加2亿。是不是就重复计算了？&lt;br /&gt;
&lt;strong&gt;A&lt;/strong&gt;：明年的存货投资会减少, 实际上是当年的存货投资转换为了明年的消费支出, 故没有重复计算&lt;/p&gt;
&lt;p&gt;转移支付是不计入 GDP 的。&lt;br /&gt;
折旧：不能一次性用完的生产资料在生产过程中产生的磨损。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;收入法&lt;/strong&gt;：&lt;br /&gt;
把所有要素提供者的收入加起来核算 GDP 的方法, 收入法本质是核算成本, 这些成本包括&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生产要素者提供的补偿：工资, 利息, 租金(包括专利, 版权等)&lt;/li&gt;
&lt;li&gt;非公司企业主收入&lt;/li&gt;
&lt;li&gt;公司税前利润&lt;/li&gt;
&lt;li&gt;企业转移支付和间接税&lt;/li&gt;
&lt;li&gt;折旧(一种会计处理, 为了和支出法一致)&lt;br /&gt;
GDP=工资+利息+利润+租金+间接税+企业转移支付+折旧+统计误差&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;产出法&lt;/strong&gt;：&lt;br /&gt;
把一段时间内各个生产环节的价值增值全部相加&lt;/p&gt;
&lt;h2&gt;其他宏观经济指标&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;GNP&lt;/strong&gt;：国民生产总值, GDP 是属地原则, GNP 是属人原则&lt;br /&gt;
&lt;strong&gt;NDP(Net Domestic Produc)&lt;/strong&gt;：国内生产净值, NDP=GDP-折旧&lt;br /&gt;
&lt;strong&gt;NI(National Income)&lt;/strong&gt;：国民收入, NI=NDP-间接税-企业转移支付+政府给企业的补贴, 反映了劳动和土地要素提供者的总收入&lt;br /&gt;
&lt;strong&gt;PI&lt;/strong&gt;：个人收入, PI=NI-企业未分配利润-公司所得税-社会保险+政府转移支付&lt;br /&gt;
&lt;strong&gt;DPI(Disposable Personal Income)&lt;/strong&gt;：个人可支配收入, DPI=PI-个人所得税&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250714210633528.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;国民经济恒等式&lt;/h2&gt;
&lt;p&gt;下面是凯恩斯流派的观点：&lt;/p&gt;
&lt;p&gt;考虑两部门经济(家庭+企业)：&lt;br /&gt;
支出法：GDP=消费+投资&lt;br /&gt;
收入法：GDP=消费+储蓄&lt;/p&gt;
&lt;p&gt;投资=储蓄&lt;/p&gt;
&lt;p&gt;考虑三部分经济(家庭+企业+政府购买)：&lt;br /&gt;
支出法：GDP=消费+投资+政府购买&lt;br /&gt;
收入法：GDP=消费+储蓄+税收&lt;/p&gt;
&lt;p&gt;投资=储蓄+税收-政府购买=个人储蓄+政府储蓄=储蓄&lt;/p&gt;
&lt;p&gt;考虑四部门经济(家庭+企业+政府+外国)：&lt;br /&gt;
支出法：GDP=消费+投资+政府购买+净出口&lt;br /&gt;
收入法：GDP=消费+储蓄+税收+对外转移支付&lt;/p&gt;
&lt;p&gt;投资=个人储蓄+政府储蓄+外国人的储蓄(净进口-对外转移支付)&lt;/p&gt;
&lt;p&gt;上述三个公式成为投资储蓄恒等式, 或国民经济恒等式&lt;/p&gt;
&lt;p&gt;这是一种会计角度的恒等, 算完账后的事后恒等&lt;/p&gt;
&lt;h2&gt;名义 GDP 和实际 GDP&lt;/h2&gt;
&lt;p&gt;名义 GDP：单纯用当年物价水平确定的全部最终产品和劳务&lt;br /&gt;
实际 GDP：以某一年的物价作为基期价格计算出来的全部最终产品和劳务&lt;br /&gt;
名义 GDP/实际 GDP=GDP 折算指数或 GDP 平减指数&lt;/p&gt;
&lt;h2&gt;失业的衡量和失业率的计算&lt;/h2&gt;
&lt;p&gt;失业：有劳动能力且想工作的人找不到工作叫失业&lt;/p&gt;
&lt;p&gt;失业率=失业人数/劳动力人数&lt;/p&gt;
&lt;h2&gt;物价水平的衡量&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CPI(Consumer Price Index)&lt;/strong&gt;：消费者价格指数（服务和消费品）。表示消费品的价格变化, 计算的最终数值 x100&lt;br /&gt;
&lt;strong&gt;PPI(Producer Price Index)&lt;/strong&gt;：生产者价格指数（生产资料或工业品）。表示工业品的价格变化, 计算的最终数值 x100&lt;/p&gt;
&lt;p&gt;物价水平：所有商品和劳务价格总额的加权平均值。代表所有商品和劳务的平均价格。&lt;/p&gt;
&lt;p&gt;价格水平变化=一段时间结束时的物价水平/一段时间开始的物价水平&lt;br /&gt;
&amp;gt;1 物价水平上涨&lt;br /&gt;
&amp;lt;1 物价水平下降&lt;/p&gt;
&lt;p&gt;计算价格水平, 会采用加权平均, 会使用商品的数量作为加权值。注意计算时, 加权值不能改变。&lt;/p&gt;
&lt;p&gt;和上一期对比, 称为&lt;strong&gt;环比&lt;/strong&gt;, 和去年同期相比, 称为&lt;strong&gt;同比&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;CPI 算法的缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;商品加权和基期一样, 不能改变&lt;/li&gt;
&lt;li&gt;无法反映新商品淘汰旧商品, 导致 CPI 被高估, 产生了 CPI 替代偏向&lt;/li&gt;
&lt;li&gt;反映不出商品质量更好或更便宜, 消费数量被高估, 导致 CPI 被高估&lt;br /&gt;
普遍 CPI 偏高。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;CPI 和 PPI 都只衡量了部分商品, GDP 平减指数, 在衡量通货膨胀方面更加全面一些&lt;/p&gt;
&lt;p&gt;注意 CPI 和 GDP 平减指数的算法是不同的&lt;br /&gt;
CPI=计算期/基期(加权基准), 称为拉氏指数&lt;br /&gt;
GDP 平减指数=计算期(加权基准)/基期, 称为帕氏指数&lt;/p&gt;
&lt;h1&gt;国民收入决定理论&lt;/h1&gt;
&lt;h2&gt;收入-支出模型&lt;/h2&gt;
&lt;p&gt;凯恩斯定律：需求决定供给和和均衡产出, 一个经济社会的总产出是由需求决定的。&lt;br /&gt;
古典经济学萨伊定律：供给会创造自己的需求。&lt;/p&gt;
</content:encoded></item><item><title>贝塞尔曲线</title><link>https://www.tanedanobata.top/Math/%E5%9B%BE%E5%BD%A2%E5%87%A0%E4%BD%95%E5%AD%A6/%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF/</link><guid isPermaLink="true">https://www.tanedanobata.top/Math/%E5%9B%BE%E5%BD%A2%E5%87%A0%E4%BD%95%E5%AD%A6/%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF/</guid><pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What is Bezier Curve&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250709144541.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250709144633.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;贝塞尔曲线是控制点按一定规则和比例插值得到的曲线。&lt;br /&gt;
他的几何意义下计算规则如下：
$$
Given
\left( \begin{aligned}
p_{1} &amp;amp;= point_{1} \
p_{2} &amp;amp;= point_{2} \
distance &amp;amp;= (p_{2}-p_{1}) \
ratio &amp;amp;= \frac{percentage}{100}
\end{aligned}  \right),
new \space point = p_{1}+distance\cdot ratio
$$&lt;/p&gt;
&lt;h2&gt;Mathematics of Bezier Curve&lt;/h2&gt;
&lt;p&gt;贝塞尔曲线是一种&quot;参数化&quot;函数(&quot;parametric&quot; function)。贝塞尔曲线是关于参数 $t$ 的多项式, 其中 $t \in [0,1]$。&lt;br /&gt;
我们定义一个杨辉三角(&lt;a href=&quot;https://en.wikipedia.org/wiki/Pascal%27s_triangle&quot;&gt;Pascal&apos;s triangle&lt;/a&gt;) 的打表：
$$
\begin{aligned}
\text{linear} &amp;amp;= 1+1 \
\text{squre} &amp;amp;= 1+2+1 \
\text{cubic} &amp;amp;= 1+3+3+1 \
\text{quartic} &amp;amp;= 1+4+6+4+1
\end{aligned}
$$
我们容易得到贝塞尔曲线在一阶, 二阶, 三阶的情况:
$$
\begin{aligned}
\text{linear} &amp;amp;= (1-t)+t \
\text{squre} &amp;amp;= (1-t)^2+2t(1-t)+t^2 \
\text{cubic} &amp;amp;= (1-t)^3+3t(1-t)^2+3t^2(1-t)+t^3 \
\end{aligned}&lt;/p&gt;
&lt;p&gt;$$
进一步, 可以得到 Bezier 的通用公式：
$$
Bezier(n,t)= \sum^{n} \text{Pascal}(n,i)\cdot(1-t)^{n-i}\cdot t^i
$$&lt;/p&gt;
&lt;h2&gt;Control Bezier Curvatures&lt;/h2&gt;
&lt;p&gt;在上面的公式上, 再添加 $\text{Weight}$ 就是带有控制点的贝塞尔曲线公式：
$$
Bezier(n,t)= \sum^{n} \text{Pascal}(n,i)\cdot(1-t)^{n-i}\cdot t^i\cdot W_{i}
$$
$\text{Weight}$ 是控制点的坐标值, $W_{0}$ 是起始坐标, $W_{n}$ 是终点坐标, 中间的 $W_{i}$ 就是贝塞尔曲线的控制点。&lt;/p&gt;
&lt;h2&gt;Bezier Curvatures as Matrix Operations&lt;/h2&gt;
&lt;p&gt;贝塞尔曲线能表示为矩阵形式, 特别地, 针对三次贝塞尔曲线, 可以表示为：&lt;/p&gt;
&lt;p&gt;$$
B(t)=\begin{bmatrix}
1&amp;amp;t&amp;amp;t^2&amp;amp;t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-3 &amp;amp; 3 &amp;amp; 0 &amp;amp; 0 \
3 &amp;amp; -6 &amp;amp; 3 &amp;amp; 0 \
-1 &amp;amp; 3 &amp;amp; -3 &amp;amp; 1
\end{bmatrix}
\cdot
\begin{bmatrix}
p_{1} \&lt;br /&gt;
p_{2} \
p_{3}  \
p_{4}
\end{bmatrix}
$$&lt;/p&gt;
&lt;h2&gt;de Casteljau&apos;s Algorithm&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;德卡斯特里奥算法&lt;/strong&gt; 是计算&lt;a href=&quot;https://zh.wikipedia.org/w/index.php?title=%E4%BC%AF%E6%81%A9%E6%96%AF%E5%9D%A6%E5%BD%A2%E5%BC%8F&amp;amp;action=edit&amp;amp;redlink=1&quot;&gt;伯恩斯坦形式&lt;/a&gt;的多项式或&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%B2%9D%E8%8C%B2%E6%9B%B2%E7%B7%9A&quot;&gt;贝塞尔曲线&lt;/a&gt;的&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E9%80%92%E5%BD%92&quot;&gt;递归&lt;/a&gt;方法。&lt;br /&gt;
算法计算步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;取曲线定义点间的所有线段, 对于 $n$ 阶的曲线, 就是 $n$ 条线段。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;沿每条线段 $t$ 位置放置标记。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;连接这些标记点, 形成 $t-1$ 条线段。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在上述线段的 $t$ 位置再次放置标记。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;连接这些标记点, 形成 $t-2$ 条线段。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;递归上述流程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最终只剩下一条线段, 得到比率 $t$ 下的曲线位置。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250709155135.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Split Curve&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250709165620.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;德卡斯特里奥算法可以实现贝塞尔曲线的分割。&lt;/p&gt;
&lt;h2&gt;Splite Curve using Matrices&lt;/h2&gt;
&lt;p&gt;对于任意三次贝塞尔曲线
$$&lt;/p&gt;
&lt;p&gt;B(t)=\begin{bmatrix}
1&amp;amp;t&amp;amp;t^2&amp;amp;t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-3 &amp;amp; 3 &amp;amp; 0 &amp;amp; 0 \
3 &amp;amp; -6 &amp;amp; 3 &amp;amp; 0 \
-1 &amp;amp; 3 &amp;amp; -3 &amp;amp; 1
\end{bmatrix}
\cdot
\begin{bmatrix}
p_{1} \&lt;br /&gt;
p_{2} \
p_{3}  \
p_{4}
\end{bmatrix}&lt;/p&gt;
&lt;p&gt;$$
可以在 $t=z$ 处分割为两条贝塞尔曲线。&lt;/p&gt;
&lt;p&gt;那么引入 $z$ 的左曲线矩阵为:
$$
\begin{align*}
B(t) &amp;amp;=
\begin{bmatrix}
1 &amp;amp; (z \cdot t) &amp;amp; (z \cdot t)^2 &amp;amp; (z \cdot t)^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-3 &amp;amp; 3 &amp;amp; 0 &amp;amp; 0 \
3 &amp;amp; -6 &amp;amp; 3 &amp;amp; 0 \
-1 &amp;amp; 3 &amp;amp; -3 &amp;amp; 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \ P_2 \ P_3 \ P_4
\end{bmatrix}
\
&amp;amp;=
\begin{bmatrix}
1 &amp;amp; t &amp;amp; t^2 &amp;amp; t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; z &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; z^2 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-3 &amp;amp; 3 &amp;amp; 0 &amp;amp; 0 \
3 &amp;amp; -6 &amp;amp; 3 &amp;amp; 0 \
-1 &amp;amp; 3 &amp;amp; -3 &amp;amp; 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \ P_2 \ P_3 \ P_4
\end{bmatrix}
\end{align*}
$$
如果这个矩阵表达式侧能被表达为 $[\text{t Values}]\cdot[\text{Bezier matrix}]\cdot[\text{column matrix}]$ 的形式, 那么就可以得到 $t=0$ 到 $t=z$ 的贝塞尔曲线矩阵。&lt;/p&gt;
&lt;p&gt;简单起见, 推算过程以二次贝塞尔曲线矩阵为例：&lt;br /&gt;
首先, 我们定义三次贝塞尔矩阵为 $M$：
$$
M=
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-3 &amp;amp; 3 &amp;amp; 0 &amp;amp; 0 \
3 &amp;amp; -6 &amp;amp; 3 &amp;amp; 0 \
-1 &amp;amp; 3 &amp;amp; -3 &amp;amp; 1
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;定义 $Z$ 矩阵
$$
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; z &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; z^2 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; z^3
\end{bmatrix}
$$
那么
$$
\begin{align*}
B(t)&amp;amp;=
\begin{bmatrix}
1 &amp;amp; t &amp;amp; t^2 &amp;amp; t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; z &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; z^2 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-3 &amp;amp; 3 &amp;amp; 0 &amp;amp; 0 \
3 &amp;amp; -6 &amp;amp; 3 &amp;amp; 0 \
-1 &amp;amp; 3 &amp;amp; -3 &amp;amp; 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \ P_2 \ P_3 \ P_4
\end{bmatrix}
\
&amp;amp;=
\begin{bmatrix}
1 &amp;amp; t &amp;amp; t^2 &amp;amp; t^3
\end{bmatrix}
\cdot
Z
\cdot
M
\cdot
\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-3 &amp;amp; 3 &amp;amp; 0 &amp;amp; 0 \
3 &amp;amp; -6 &amp;amp; 3 &amp;amp; 0 \
-1 &amp;amp; 3 &amp;amp; -3 &amp;amp; 1
\end{bmatrix}
\
&amp;amp;=
\begin{bmatrix}
1 &amp;amp; t &amp;amp; t^2 &amp;amp; t^3
\end{bmatrix}
\cdot M\cdot M^{-1}\cdot Z \cdot M
\begin{bmatrix}
P_1 \ P_2 \ P_3 \ P_4
\end{bmatrix}
\end{align*}
$$&lt;/p&gt;
&lt;p&gt;也就是说, 最后得到的点的仅需要乘以矩阵
$$
Q=M^{-1}\cdot Z \cdot M
$$
就得到了分割后矩阵的新的控制点。&lt;/p&gt;
&lt;p&gt;另外, 这个算法是解析出来的, 针对我们常规的贝塞尔曲线解是唯一的(有理贝塞尔曲线会添加控制点影响度的参数, 解控制点的位置就不唯一)。&lt;/p&gt;
&lt;p&gt;最后得到左曲线：
$$&lt;/p&gt;
&lt;p&gt;\begin{align*}&lt;/p&gt;
&lt;p&gt;&amp;amp;\begin{bmatrix}
1 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
-(z-1) &amp;amp; z &amp;amp; 0 &amp;amp; 0 \
(z-1)^2 &amp;amp; -2 \cdot (z-1) \cdot z &amp;amp; z^2 &amp;amp; 0 \
-(z-1)^3 &amp;amp; 3 \cdot (z-1)^2 \cdot z &amp;amp; -3 \cdot (z-1) \cdot z^2 &amp;amp; z^3
\end{bmatrix}
\begin{bmatrix}
P_1 \ P_2 \ P_3 \ P_4
\end{bmatrix}
\=
&amp;amp;\begin{bmatrix}
P_1 \
z \cdot P_2 - (z-1) \cdot P_1 \
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1
\end{bmatrix}
\end{align*}&lt;/p&gt;
&lt;p&gt;$$
得到右曲线：
$$
\begin{align*}&lt;/p&gt;
&lt;p&gt;&amp;amp;\begin{bmatrix}
-(z-1)^3 &amp;amp; 3 \cdot (z-1)^2 \cdot z &amp;amp; -3 \cdot (z-1) \cdot z^2 &amp;amp; z^3 \
0 &amp;amp; (z-1)^2 &amp;amp; -2 \cdot (z-1) \cdot z &amp;amp; z^2 \
0 &amp;amp; 0 &amp;amp; -(z-1) &amp;amp; z \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \ P_2 \ P_3 \ P_4
\end{bmatrix}
\=
&amp;amp;\begin{bmatrix}
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 \
z^2 \cdot P_4 - 2 \cdot z \cdot (z-1) \cdot P_3 + (z-1)^2 \cdot P_2 \
z \cdot P_4 - (z-1) \cdot P_3 \
P_4
\end{bmatrix}
\end{align*}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h2&gt;Possibility of Curve Segmentation&lt;/h2&gt;
&lt;p&gt;我们已经得到了贝塞尔曲线的分割的解析解, 现在需要考察的是, 在 $1/3$ 和 $2/3$ 处是否一定能找到对应的解析解。&lt;br /&gt;
也就是说, 矩阵方程
$$&lt;/p&gt;
&lt;p&gt;\begin{align*}
&amp;amp;\begin{bmatrix}
x_1 \
z \cdot x_2 - (z-1) \cdot x_1 \
z^2 \cdot x_3 - 2 \cdot z \cdot (z-1) \cdot x_2 + (z-1)^2 \cdot x_1 \
x&apos;&lt;em&gt;{4}=z^3 \cdot x_4 - 3 \cdot z^2 \cdot (z-1) \cdot x_3 + 3 \cdot z \cdot (z-1)^2 \cdot x_2 - (z-1)^3 \cdot x_1
\end{bmatrix}
\=
&amp;amp;\begin{bmatrix}
x_1 \
\frac{2}{3}x&lt;/em&gt;{1}+\frac{1}{3}x&apos;&lt;em&gt;{4} \
\frac{1}{3}x&lt;/em&gt;{1}+\frac{2}{3}x&apos;&lt;em&gt;{4} \
x&apos;&lt;/em&gt;{4}
\end{bmatrix}
\end{align*}&lt;/p&gt;
&lt;p&gt;$$
尝试展开式子, 显然, 这个式子很容易看出是无解的。&lt;br /&gt;
所以得出结论, 用曲线分割的方式去得到 $1/3$ 和 $2/3$ 的固定三次贝塞尔贝塞尔表达任意三次贝塞尔曲线是没有解析解的。&lt;/p&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://pomax.github.io/BezierInfo-2/#explanation&quot;&gt;A Primer on Bézier Curves&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://zhuanlan.zhihu.com/p/366678047&quot;&gt;知乎 - 贝塞尔曲线&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://zh.wikipedia.org/zh-cn/%E8%B2%9D%E8%8C%B2%E6%9B%B2%E7%B7%9A&quot;&gt;贝塞尔曲线 - 维基百科，自由的百科全书&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm&quot;&gt;De Casteljau&apos;s algorithm - Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;原附件：&lt;code&gt;preview-9781483296999_A23889377.pdf&lt;/code&gt;（原仓库未包含此文件）。&lt;/p&gt;
</content:encoded></item><item><title>导数</title><link>https://www.tanedanobata.top/Math/%E5%BE%AE%E7%A7%AF%E5%88%86/%E5%AF%BC%E6%95%B0/</link><guid isPermaLink="true">https://www.tanedanobata.top/Math/%E5%BE%AE%E7%A7%AF%E5%88%86/%E5%AF%BC%E6%95%B0/</guid><pubDate>Mon, 16 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;导数的几何含义&lt;/h2&gt;
&lt;p&gt;函数上某一点切线的斜率。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250616203312876.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;求导公式&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;幂函数求导公式&lt;/strong&gt;
$$
\frac{d(x^n)}{dx}=nx^{n-1}
$$
&lt;strong&gt;三角函数&lt;/strong&gt;
$$
\begin{align}
\frac{d(\sin \theta)}{d\theta}=\cos \theta \
\frac{d(\cos \theta)}{d\theta}=-\sin \theta
\end{align}&lt;/p&gt;
&lt;p&gt;$$
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250616210943591.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;指数函数&lt;/strong&gt;的导数和函数本身成正比
$$
d\left( \frac{e^{ct}}{dt} \right) = ce^{ct}
$$
同时, 考虑到
$$
e^{\ln x}=x
$$
这样, 可以得到
$$
\frac{d(x^t)}{dt}=\frac{d}{dt} e^{\ln(x)t}=\ln(x)e^{\ln (x)t}
$$
同样, 考虑函数
$$
\begin{align}
y=\ln(x) \
e^y=x \
e^ydy=dx \
\frac{dy}{dx}=\frac{1}{e^y}=x
\end{align}
$$&lt;/p&gt;
&lt;h2&gt;链式求导法则和乘积法则&lt;/h2&gt;
&lt;h3&gt;加法法则&lt;/h3&gt;
&lt;p&gt;和函数的导数是导数的和。&lt;/p&gt;
&lt;h3&gt;乘法法则&lt;/h3&gt;
&lt;p&gt;左乘右导, 右乘左导。
$$
\frac{d(f(x)g(x))}{dx}=f(x)\cdot \frac{dg(x)}{dx} + g(x) \cdot \frac{df(x)}{dx}
$$
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250616212249643.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;复合求导法则(链式法则)&lt;/h3&gt;
&lt;p&gt;$$
\frac{d}{dx}g(h(x))=\frac{dg}{dh}(h(x))\cdot \frac{dh}{dx}(x)=\frac{dg}{dh} \frac{dh}{dx}
$$
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250616212840712.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;隐函数求导&lt;/h3&gt;
&lt;p&gt;对于
$$
f(x)+g(y) = 0
$$
可以看做
$$
h(x,y) = 0
$$
这样, 左侧函数的微小变化需要等于右侧函数的微小变化
$$
f&apos;(x)dx+g&apos;(y)dy=0
$$
就能自然得出 $\frac{dy}{dx}$ 或 $\frac{dx}{dy}$ 的值。&lt;/p&gt;
&lt;h3&gt;反函数求导&lt;/h3&gt;
</content:encoded></item><item><title>前向运动学和逆向运动学</title><link>https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E9%AA%A8%E9%AA%BC%E5%8A%A8%E7%94%BB/%E5%89%8D%E5%90%91%E8%BF%90%E5%8A%A8%E5%AD%A6%E5%92%8C%E9%80%86%E5%90%91%E8%BF%90%E5%8A%A8%E5%AD%A6/</link><guid isPermaLink="true">https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E9%AA%A8%E9%AA%BC%E5%8A%A8%E7%94%BB/%E5%89%8D%E5%90%91%E8%BF%90%E5%8A%A8%E5%AD%A6%E5%92%8C%E9%80%86%E5%90%91%E8%BF%90%E5%8A%A8%E5%AD%A6/</guid><pubDate>Thu, 05 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250611114147.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Kinematics of a Chain&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605201629.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605201741.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605202040.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250610125029.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据坐标转换公式&lt;a href=&quot;/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%9F%BA%E7%A1%80/#view-of-rotation&quot;&gt;线性代数基础&lt;/a&gt; , 可以得到每个关节点在全局坐标下的表示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250610125446.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同样对于任意的点, 也可以得到其在全局坐标系或任意局部坐标系下的坐标。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250610125544.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Types of Joints&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250610130117.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;IK&lt;/h2&gt;
&lt;h3&gt;Tow Joint IK&lt;/h3&gt;
</content:encoded></item><item><title>Parameterization of Rotation</title><link>https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E4%B8%89%E7%BB%B4%E6%97%8B%E8%BD%AC/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E4%B8%89%E7%BB%B4%E6%97%8B%E8%BD%AC/</guid><pubDate>Wed, 04 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Parameterization of Rotation&lt;/h1&gt;
&lt;h2&gt;Matrix&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%9F%BA%E7%A1%80/#rotation&quot;&gt;线性代数基础&lt;/a&gt; 中已经介绍过, 旋转矩阵是一个特殊的正交矩阵, 满足
$$
\begin{align}
R^TR=I \
\det R = 1
\end{align}
$$
实际上, 旋转矩阵有6个变量都是由其他变量决定的。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250604163824.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;旋转矩阵的实际自由度为3(degrees of freedom, DoF)。&lt;/p&gt;
&lt;h3&gt;旋转矩阵的优缺点&lt;/h3&gt;
&lt;p&gt;优点：&lt;br /&gt;
易于应用&lt;/p&gt;
&lt;p&gt;缺点：&lt;br /&gt;
难以构造&lt;br /&gt;
难以插值&lt;/p&gt;
&lt;h2&gt;Euler Angle&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;定理&lt;/strong&gt;：任何旋转都能表示为三个基本旋转的乘积。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250604181515.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;欧拉角旋转可以延世界坐标系或者局部坐标系旋转, 均能够表达所有的旋转。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605150143.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;欧拉角的特性：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605150740.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Rotation Vectors and Axis Angle&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605151119.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
应用轴角：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605151319.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
轴角的优缺点：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605164028.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Quaternions&lt;/h2&gt;
&lt;p&gt;基本性质：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605164504.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605164754.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;四元数的乘法：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605164852.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;四元数的向量定义形式：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605165133.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605165232.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605165348.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605165424.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Unity Quaternion&lt;/h3&gt;
&lt;p&gt;对于单位四元数：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605165550.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605172223.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
可以看做是四维空间上的一个单位球壳。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605174616.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Rotation Representation of Quaternion&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605174748.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605193732.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Combination of Quaternion&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605193835.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Interpolation of Quaternion&lt;/h3&gt;
&lt;h4&gt;Nlerp&lt;/h4&gt;
&lt;p&gt;四元数仅做线性插值得到的结果不是一个能作为旋转表示的四元数(单位四元数)。需要在插值之后再进行单位化。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605194415.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Slerp&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605194938.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605195009.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;优缺点&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250605195149.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>线性代数基础</title><link>https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%9F%BA%E7%A1%80/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%9F%BA%E7%A1%80/</guid><pubDate>Wed, 28 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Vector&lt;/h2&gt;
&lt;h3&gt;Vector Representation&lt;/h3&gt;
&lt;p&gt;$$
\vec{a} = (a_{1}, a_{2},\dots,a_{3})^T=\begin{bmatrix}
a_{1}\
a_{2} \
\vdots \
a_{n}
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
||\vec{a}||=\sqrt{ a_{x}^2+a_{y}^2+a_{z}^2 }
$$&lt;/p&gt;
&lt;h3&gt;Dot product&lt;/h3&gt;
&lt;p&gt;$$
\vec{a}\cdot\vec{b}=a_{1}b_{1}+a_{2}b_{2}+\cdots +a_{n}b_{n}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Eucidean Space，&lt;br /&gt;
$$\vec{a}\cdot\vec{b}=||\vec{a}||\space||\vec{b}||\cos \theta$$ &lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/202412262035538.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cross product&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/202412262037021.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/202412262038466.png&quot; alt=&quot;image.png&quot; /&gt;
$$
\vec{a} \times \vec{b} = ||\vec{a}|| ||\vec{b}|| \sin (\theta) \vec{n}
$$
注意, 叉积满足反交换律：
$$
\vec{a} \times \vec{b} = -\vec{b} \times \vec{a}
$$
叉乘不满足结合律。&lt;/p&gt;
&lt;h3&gt;How to find the rotation between vector&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/202412262055302.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Dodrigues&apos;s Rotation Formula&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/202412262103541.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;strong&gt;Dodrigues’ Rotation Fomula&lt;/strong&gt;:
$$
\vec{b}=\vec{a}+(\sin \theta)\vec{u} \times \vec{a}+(1-\cos \theta)\vec{u}\times (\vec{u}\times \vec{a})
$$
注意, 旋转方向的正负由左右手轴决定。针对左右手轴, 公式都是一致的。&lt;/p&gt;
&lt;p&gt;同时这个公式也可以写成矩阵的形式(见 &lt;a href=&quot;/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%9F%BA%E7%A1%80/#matrix-form-of-cross-product&quot;&gt;Matrix Form of Cross Product&lt;/a&gt;)。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529151214.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Orthogonal Coordinate&lt;/h3&gt;
&lt;p&gt;对于满足下面要求的轴满足右手系：
$$
\begin{split}
||e_{x}||=||e_{y}||=||e_{z}||=1  \
e_{x}\cdot e_{y}=e_{y}\cdot e_{z}=e_{z}\cdot e_{x}=0 \
e_{x}\times e_{y}=e_{z} \
e_{y}\times e_{z}=e_{x} \
e_{z}\times e_{x}=e_{y}
\end{split}&lt;/p&gt;
&lt;p&gt;$$
这样可以得出以下的结论：
$$
\begin{align}
\vec{a}\cdot \vec{b}= a_{x}b_{x}+a_{y}b_{y}+a_{z}b_{z} \
\vec{a} \times \vec{b} = (a_{y}b_{z}-a_{z}b_{y}, a_{z}b_{x}-a_{x}b_{z},a_{x}b_{y}-a_{y}b_{z})
\end{align}
$$&lt;/p&gt;
&lt;h2&gt;Matrix&lt;/h2&gt;
&lt;h3&gt;Matrix Operation&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250528202900.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Matrix Form of Dot Produnct&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250528203037.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Matrix Form of Cross Product&lt;/h3&gt;
&lt;p&gt;叉乘可以表达为一个反对称阵的形式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250528203210.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
叉乘也可以用行列式进行表示：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529114558.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;叉乘满足下面的一些性质：&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250528203502.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Orthogonal Matrix&lt;/h3&gt;
&lt;p&gt;如果矩阵的列向量互相正交，且长度为1，那么该矩阵称之为标准正交矩阵，不要求矩阵满秩。&lt;br /&gt;
如果满秩，即 $Q$ 是方阵，称之为&lt;strong&gt;正交矩阵(Orthogonal Matrix)&lt;/strong&gt;。标准正交矩阵有很多好的性质：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$Q^TQ=I$&lt;/li&gt;
&lt;li&gt;$Qx$ 不改变 $x$ 的长度。$|Qx|^2=(Qx)^TQx=x^TQ^TQx=x^Tx=|x|^2$&lt;/li&gt;
&lt;li&gt;$Q$ 不改变向量点积。$Qx\cdot Qy=(Qx)^TQy=x^TQ^TQy=x^Ty$&lt;/li&gt;
&lt;li&gt;如果 $Q$ 是方阵, $Q^TQ=QQ^T=I\implies Q^T=Q^{-1}$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Determinant of a Matrix&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529114205.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
行列式的性质：
$$
\begin{align}
\det I = 1 \
\det AB = \det A*\det B \
\det A^{T}= \det A \
if \  A \ is \ invertible, \det A^{-1} = (\det A)^{-1} \
if \  U \ is \ orthogonal, \det U = \pm 1
\end{align}
$$&lt;/p&gt;
&lt;h3&gt;Eigenvalues and Eigenvetors&lt;/h3&gt;
&lt;p&gt;对于矩阵 $A$, 存在一个非零向量 $x$ 满足：
$$
Ax=\lambda x
$$
则称 $\lambda$ 为 $A$ 的特征值, $x$ 为 $A$ 的特征向量。&lt;/p&gt;
&lt;p&gt;特别地, 对于 $3\times {3}$ 的正交矩阵 $U$, 它至少有一个特征值 $\lambda=\det U=\pm {1}$&lt;/p&gt;
&lt;h2&gt;Rigid Transformation&lt;/h2&gt;
&lt;h3&gt;Scaling&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529115532.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Rotation&lt;/h3&gt;
&lt;p&gt;向量 $a$ 经过旋转得到 $a&apos;$, 对于 $a&apos;=Ra$, 则称 $R$ 是 $a\implies a&apos;$ 的旋转矩阵。&lt;/p&gt;
&lt;p&gt;旋转矩阵是一个正交矩阵, 满足一些性质：
$$
\begin{align}
R^{-1}=R^{T}  \
R^{T}R = RR^{T} = I \
\det R = +1  \
||Rx||=||x||
\end{align}&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
&lt;h4&gt;Combination of Rotations&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529145125.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Rotation around Coordinate Axes&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529145214.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Rotation Axis and Angle&lt;/h4&gt;
&lt;p&gt;旋转矩阵始终有一个特征值 $+1$。&lt;/p&gt;
&lt;p&gt;也就是存在一个特征向量 $Ru=u$。&lt;/p&gt;
&lt;p&gt;这样, 旋转矩阵 $R$ 可以看做是 $a$ 绕轴 $u$ 旋转 $\theta$ 角度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529150046.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样反对称矩阵可以和叉乘建立关系(见&lt;a href=&quot;/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%9F%BA%E7%A1%80/#matrix-form-of-cross-product&quot;&gt;线性代数基础&lt;/a&gt;)&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529150243.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样 $u&apos;$ 和 $u$ 是平行的, 对 $u&apos;$ 进行标准化后就能得到旋转轴 $u$。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529150543.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同时根据&lt;a href=&quot;/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0%E5%9F%BA%E7%A1%80/#dodriguess-rotation-formula&quot;&gt;线性代数基础&lt;/a&gt; 可以计算得出旋转角 $\theta$&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250529150902.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;View of Rotation&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250604153337.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250604153814.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
对于原点相同的正交基, 可以看做 $object$ 的正交基是由 $global$ 的正交基旋转 $R$ 得到的。&lt;br /&gt;
也就是说旋转矩阵给出了从局部坐标系到全局坐标系的转换公式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250604160511.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;加上位移后就是上图所示的坐标变换公式。&lt;/p&gt;
</content:encoded></item><item><title>蒙皮和BS的Compute Shader参考</title><link>https://www.tanedanobata.top/posts/skinning-blendshape-compute/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/skinning-blendshape-compute/</guid><pubDate>Tue, 27 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Unity 的蒙皮和 Bs 的 ComputeShader 的一种实现的参考。
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/TwoTailsGames/Unity-Built-in-Shaders/blob/master/DefaultResources/Internal-BlendShape.compute&quot;&gt;Unity-Built-in-Shaders/DefaultResources/Internal-BlendShape.compute at master · TwoTailsGames/Unity-Built-in-Shaders · GitHub&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

#pragma kernel main
#pragma kernel main SKIN_NORM
#pragma kernel main SKIN_NORM SKIN_TANG

#include &quot;HLSLSupport.cginc&quot;
#include &quot;Internal-Skinning-Util.cginc&quot;

struct BlendShapeVertex
{
    int index;
    float3 pos;
    float3 norm;
    float3 tang;
};

uint g_FirstVert; // First vertex from blend shape buffer to use
uint g_VertCount; // Sparse vertex count, not the full amount of vertices in mesh
float g_Weight;

[numthreads(64, 1, 1)]
void main(uint3 threadID : SV_DispatchThreadID, SAMPLER_UNIFORM RWStructuredBuffer&amp;lt;MeshVertex&amp;gt; inOutMeshVertices, SAMPLER_UNIFORM StructuredBuffer&amp;lt;BlendShapeVertex&amp;gt; inBlendShapeVertices)
{
    const uint t = threadID.x;

    if (t &amp;gt;= g_VertCount)
    {
        return;
    }

    BlendShapeVertex blendShapeVert = inBlendShapeVertices[t + g_FirstVert];

    const uint vertIndex = blendShapeVert.index;

    inOutMeshVertices[vertIndex].pos += blendShapeVert.pos * g_Weight;
#if SKIN_NORM
    inOutMeshVertices[vertIndex].norm += blendShapeVert.norm * g_Weight;
#endif
#if SKIN_TANG
    inOutMeshVertices[vertIndex].tang.xyz += blendShapeVert.tang * g_Weight;
#endif
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/TwoTailsGames/Unity-Built-in-Shaders/blob/master/DefaultResources/Internal-Skinning.compute&quot;&gt;Unity-Built-in-Shaders/DefaultResources/Internal-Skinning.compute at master · TwoTailsGames/Unity-Built-in-Shaders · GitHub&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

// Generic compute skinning ... with pos, norm, and tangent skinning

#pragma kernel main SKIN_BONESFORVERT=0
#pragma kernel main SKIN_BONESFORVERT=0 SKIN_NORM
#pragma kernel main SKIN_BONESFORVERT=0 SKIN_TANG
#pragma kernel main SKIN_BONESFORVERT=0 SKIN_NORM SKIN_TANG
#pragma kernel main SKIN_BONESFORVERT=1
#pragma kernel main SKIN_BONESFORVERT=1 SKIN_NORM
#pragma kernel main SKIN_BONESFORVERT=1 SKIN_TANG
#pragma kernel main SKIN_BONESFORVERT=1 SKIN_NORM SKIN_TANG
#pragma kernel main SKIN_BONESFORVERT=2
#pragma kernel main SKIN_BONESFORVERT=2 SKIN_NORM
#pragma kernel main SKIN_BONESFORVERT=2 SKIN_TANG
#pragma kernel main SKIN_BONESFORVERT=2 SKIN_NORM SKIN_TANG
#pragma kernel main SKIN_BONESFORVERT=4
#pragma kernel main SKIN_BONESFORVERT=4 SKIN_NORM
#pragma kernel main SKIN_BONESFORVERT=4 SKIN_TANG
#pragma kernel main SKIN_BONESFORVERT=4 SKIN_NORM SKIN_TANG

#include &quot;HLSLSupport.cginc&quot;

uint g_VertCount;

#if defined(SHADER_API_D3D11) || defined(SHADER_API_XBOXONE) || defined(SHADER_API_PS4) || defined(SHADER_API_GLCORE) || defined(SHADER_API_GLES3) || defined(SHADER_API_VULKAN) || defined(SHADER_API_METAL) || defined(SHADER_API_PSSL) || defined(SHADER_API_SWITCH)
#define STRUCTURED_BUFFER_SUPPORT 1
#else
#define STRUCTURED_BUFFER_SUPPORT 0
#endif

struct MeshVertex
{
    float3 pos;
#if SKIN_NORM
    float3 norm;
#endif
#if SKIN_TANG
    float4 tang;
#endif
};

struct SkinInfluence
{
#if SKIN_BONESFORVERT &amp;lt;= 1
    int index0;
#elif SKIN_BONESFORVERT == 2
    float weight0, weight1;
    int index0, index1;
#elif SKIN_BONESFORVERT == 4
    float weight0, weight1, weight2, weight3;
    int index0, index1, index2, index3;
#endif
};

#if SKIN_NORM &amp;amp;&amp;amp; SKIN_TANG
#define VERTEX_STRIDE 10
#elif SKIN_NORM
#define VERTEX_STRIDE 6
#else
#define VERTEX_STRIDE 3
#endif


#if !STRUCTURED_BUFFER_SUPPORT
MeshVertex FetchVert(SAMPLER_UNIFORM Buffer&amp;lt;float&amp;gt; vertices, const uint index, const uint stride)
{
    MeshVertex vert;

    uint offset = index * stride;
    vert.pos.x = vertices[offset++];
    vert.pos.y = vertices[offset++];
    vert.pos.z = vertices[offset++];

#if SKIN_NORM
    vert.norm.x = vertices[offset++];
    vert.norm.y = vertices[offset++];
    vert.norm.z = vertices[offset++];
#endif

#if SKIN_TANG
    vert.tang.x = vertices[offset++];
    vert.tang.y = vertices[offset++];
    vert.tang.z = vertices[offset++];
    vert.tang.w = vertices[offset++];
#endif

    return vert;
}

void StoreVert(SAMPLER_UNIFORM RWBuffer&amp;lt;float&amp;gt; vertices, float3 pos, float3 normal, float4 tang, const uint index, const uint stride)
{
    uint offset = index * stride;
    vertices[offset++] = pos.x;
    vertices[offset++] = pos.y;
    vertices[offset++] = pos.z;

#if SKIN_NORM
    vertices[offset++] = normal.x;
    vertices[offset++] = normal.y;
    vertices[offset++] = normal.z;
#endif

#if SKIN_TANG
    vertices[offset++] = tang.x;
    vertices[offset++] = tang.y;
    vertices[offset++] = tang.z;
    vertices[offset++] = tang.w;
#endif
}

#endif


[numthreads(64, 1, 1)]
#if !STRUCTURED_BUFFER_SUPPORT
void main(uint3 threadID : SV_DispatchThreadID, SAMPLER_UNIFORM Buffer&amp;lt;float&amp;gt; inVertices, SAMPLER_UNIFORM StructuredBuffer&amp;lt;SkinInfluence&amp;gt; inSkin, SAMPLER_UNIFORM RWBuffer&amp;lt;float&amp;gt; outVertices, SAMPLER_UNIFORM StructuredBuffer&amp;lt;float4x4&amp;gt; inMatrices)
#else
void main(uint3 threadID : SV_DispatchThreadID, SAMPLER_UNIFORM StructuredBuffer&amp;lt;MeshVertex&amp;gt; inVertices, SAMPLER_UNIFORM StructuredBuffer&amp;lt;SkinInfluence&amp;gt; inSkin, SAMPLER_UNIFORM RWStructuredBuffer&amp;lt;MeshVertex&amp;gt; outVertices, SAMPLER_UNIFORM StructuredBuffer&amp;lt;float4x4&amp;gt; inMatrices)
#endif
{
    const uint t = threadID.x;

    if (t &amp;gt;= g_VertCount)
    {
        return;
    }

#if !STRUCTURED_BUFFER_SUPPORT
    const MeshVertex vert = FetchVert(inVertices, t, VERTEX_STRIDE);
#else
    const MeshVertex vert = inVertices[t];
#endif
    SkinInfluence si = inSkin[t];

    float3 vPos = vert.pos.xyz;
#if SKIN_NORM
    float3 vNorm = vert.norm.xyz;
#endif
#if SKIN_TANG
    float3 vTang = vert.tang.xyz;
#endif

    float3 oPos = float3(0, 0, 0);
    float3 oNorm = float3(0, 0, 0);
    float4 oTang = float4(0, 0, 0, 0);

#if SKIN_BONESFORVERT == 0
    uint startIndex = si.index0;
    uint endIndex = inSkin[t + 1].index0;
    float4x4 blendedMatrix = 0;
    for (uint i = startIndex; i &amp;lt; endIndex; i++)
    {
        uint weightAndIndex = inSkin[i].index0;
        float weight = float(weightAndIndex &amp;gt;&amp;gt; 16) * (1.0f / 65535.0f);
        uint index = weightAndIndex &amp;amp; 0xFFFF;
        blendedMatrix += inMatrices[index] * weight;
    }
#elif SKIN_BONESFORVERT == 1
    const float4x4 blendedMatrix = inMatrices[si.index0];
#elif SKIN_BONESFORVERT == 2
    const float4x4 blendedMatrix = inMatrices[si.index0] * si.weight0 +
                                   inMatrices[si.index1] * si.weight1;
#elif SKIN_BONESFORVERT == 4
    const float4x4 blendedMatrix = inMatrices[si.index0] * si.weight0 +
                                   inMatrices[si.index1] * si.weight1 +
                                   inMatrices[si.index2] * si.weight2 +
                                   inMatrices[si.index3] * si.weight3;
#endif

    oPos = mul(blendedMatrix, float4(vPos, 1)).xyz;

#if SKIN_NORM
    oNorm = mul(blendedMatrix, float4(vNorm, 0)).xyz;
#endif

#if SKIN_TANG
    oTang.xyz = mul(blendedMatrix, float4(vTang, 0)).xyz;
    oTang.w = vert.tang.w;
#endif

#if STRUCTURED_BUFFER_SUPPORT
    outVertices[t].pos = oPos;
#if SKIN_NORM
    outVertices[t].norm = oNorm;
#endif
#if SKIN_TANG
    outVertices[t].tang = oTang;
#endif
#else
    StoreVert(outVertices, oPos, oNorm, oTang, t, VERTEX_STRIDE);
#endif
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>UE 5.5.4 SkeletalRenderCPUSkin 源码深度解析</title><link>https://www.tanedanobata.top/posts/ue5-skeletal-render-cpu-skin/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/ue5-skeletal-render-cpu-skin/</guid><pubDate>Tue, 27 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;UE 5.5.4 SkeletalRenderCPUSkin 源码深度解析
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h1&gt;UE 5.5.4 SkeletalRenderCPUSkin 源码深度解析&lt;/h1&gt;
&lt;p&gt;基于您提供的真实源码，我来详细分析 UE 5.5.4中 CPU 蒙皮系统的实际实现原理。&lt;/p&gt;
&lt;h2&gt;核心架构分析&lt;/h2&gt;
&lt;h3&gt;1. 主要类结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 主要的CPU蒙皮对象
class FSkeletalMeshObjectCPUSkin : public FSkeletalMeshObject
{
    // LOD数据数组
    TArray&amp;lt;FSkeletalMeshObjectLOD&amp;gt; LODs;
    
    // 动态数据指针
    FDynamicSkelMeshObjectDataCPUSkin* DynamicData;
    
    // 缓存的顶点数据
    mutable TArray&amp;lt;FFinalSkinVertex&amp;gt; CachedFinalVertices;
    mutable int32 CachedVertexLOD;
    
    // 调试渲染相关
    bool bRenderOverlayMaterial;
    TArray&amp;lt;int32&amp;gt; BonesOfInterest;
    TArray&amp;lt;UMorphTarget*&amp;gt; MorphTargetOfInterest;
};

// LOD级别的数据管理
struct FSkeletalMeshObjectLOD
{
    // 顶点缓冲区
    FPositionVertexBuffer PositionVertexBuffer;
    FStaticMeshVertexBuffer StaticMeshVertexBuffer;
    
    // 顶点工厂
    FLocalVertexFactory VertexFactory;
    
    // 权重和颜色缓冲区指针
    FSkinWeightVertexBuffer* MeshObjectWeightBuffer;
    FColorVertexBuffer* MeshObjectColorBuffer;
    
    // 射线追踪支持
    #if RHI_RAYTRACING
    FRayTracingGeometry RayTracingGeometry;
    #endif
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 动态数据结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;class FDynamicSkelMeshObjectDataCPUSkin
{
public:
    int32 LODIndex;                                    // 当前LOD级别
    FMorphTargetWeightMap ActiveMorphTargets;          // 活跃的Morph Target
    TArray&amp;lt;float&amp;gt; MorphTargetWeights;                  // Morph Target权重
    TArray&amp;lt;FMatrix44f&amp;gt; ReferenceToLocal;               // 骨骼变换矩阵
    
    // 布料模拟相关
    TMap&amp;lt;int32, FClothSimulData&amp;gt; ClothSimulUpdateData; // 布料模拟数据
    float ClothBlendWeight;                            // 布料混合权重
    FMatrix WorldToLocal;                              // 世界到局部空间变换
    
    #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
    TArray&amp;lt;FTransform&amp;gt; MeshComponentSpaceTransforms;   // 组件空间变换（调试用）
    #endif
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;主要更新流程&lt;/h2&gt;
&lt;h3&gt;1. Update 函数 - 主线程调用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void FSkeletalMeshObjectCPUSkin::Update(
    int32 LODIndex,
    USkinnedMeshComponent* InMeshComponent,
    const FMorphTargetWeightMap&amp;amp; InActiveMorphTargets,
    const TArray&amp;lt;float&amp;gt;&amp;amp; InMorphTargetWeights,
    EPreviousBoneTransformUpdateMode PreviousBoneTransformUpdateMode,
    const FExternalMorphWeightData&amp;amp; InExternalMorphWeightData)
{
    if (InMeshComponent)
    {
        // 创建新的动态数据
        FDynamicSkelMeshObjectDataCPUSkin* NewDynamicData = 
            new FDynamicSkelMeshObjectDataCPUSkin(
                InMeshComponent, SkeletalMeshRenderData, LODIndex, 
                InActiveMorphTargets, InMorphTargetWeights);

        uint64 FrameNumberToPrepare = GFrameCounter;
        uint32 RevisionNumber = 0;

        if (InMeshComponent-&amp;gt;SceneProxy)
        {
            RevisionNumber = InMeshComponent-&amp;gt;GetBoneTransformRevisionNumber();
        }

        // 提交渲染命令到特定的渲染管道
        FSkeletalMeshObjectCPUSkin* MeshObject = this;
        ENQUEUE_RENDER_COMMAND(SkelMeshObjectUpdateDataCommand)(
            UE::RenderCommandPipe::SkeletalMesh,
            [MeshObject, FrameNumberToPrepare, RevisionNumber, NewDynamicData]
            (FRHICommandList&amp;amp; RHICmdList)
            {
                FScopeCycleCounter Context(MeshObject-&amp;gt;GetStatId());
                MeshObject-&amp;gt;UpdateDynamicData_RenderThread(
                    RHICmdList, NewDynamicData, FrameNumberToPrepare, RevisionNumber);
            }
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. UpdateDynamicData_RenderThread - 渲染线程执行&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void FSkeletalMeshObjectCPUSkin::UpdateDynamicData_RenderThread(
    FRHICommandList&amp;amp; RHICmdList, 
    FDynamicSkelMeshObjectDataCPUSkin* InDynamicData, 
    uint64 FrameNumberToPrepare, 
    uint32 RevisionNumber)
{
    // 删除旧数据，更新新数据
    delete DynamicData;
    DynamicData = InDynamicData; 
    check(DynamicData);

    // 执行顶点缓存更新
    CacheVertices(DynamicData-&amp;gt;LODIndex, true, RHICmdList);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;核心蒙皮算法实现&lt;/h2&gt;
&lt;h3&gt;1. CacheVertices - 顶点缓存核心函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void FSkeletalMeshObjectCPUSkin::CacheVertices(int32 LODIndex, bool bForce, FRHICommandList&amp;amp; RHICmdList) const
{
    SCOPE_CYCLE_COUNTER(STAT_CPUSkinUpdateRTTime);
    check(IsInParallelRenderingThread());

    FSkeletalMeshLODRenderData&amp;amp; LOD = SkeletalMeshRenderData-&amp;gt;LODRenderData[LODIndex];
    const FSkeletalMeshObjectLOD&amp;amp; MeshLOD = LODs[LODIndex];

    // 只在LOD变化或强制更新时重新计算
    if ((LODIndex != CachedVertexLOD || bForce) &amp;amp;&amp;amp;
        DynamicData &amp;amp;&amp;amp; 
        MeshLOD.StaticMeshVertexBuffer.IsValid())
    {
        // 获取骨骼变换矩阵
        FMatrix44f* ReferenceToLocal = DynamicData-&amp;gt;ReferenceToLocal.GetData();

        int32 CachedFinalVerticesNum = LOD.GetNumVertices();
        CachedFinalVertices.Empty(CachedFinalVerticesNum);
        CachedFinalVertices.AddUninitialized(CachedFinalVerticesNum);

        FFinalSkinVertex* DestVertex = CachedFinalVertices.GetData();

        if (DestVertex)
        {
            check(GIsEditor || LOD.StaticVertexBuffers.StaticMeshVertexBuffer.GetAllowCPUAccess());
            SCOPE_CYCLE_COUNTER(STAT_SkinningTime);

            // 根据UV精度选择不同的蒙皮路径
            if (LOD.StaticVertexBuffers.StaticMeshVertexBuffer.GetUseFullPrecisionUVs())
            {
                SKIN_LOD_VERTICES(TGPUSkinVertexFloat32Uvs, 
                    LOD.StaticVertexBuffers.StaticMeshVertexBuffer.GetNumTexCoords());
            }
            else
            {
                SKIN_LOD_VERTICES(TGPUSkinVertexFloat16Uvs, 
                    LOD.StaticVertexBuffers.StaticMeshVertexBuffer.GetNumTexCoords());
            }

            // 处理调试渲染（骨骼权重或Morph Target可视化）
            if (bRenderOverlayMaterial)
            {
                if (MorphTargetOfInterest.Num() &amp;gt; 0)
                {
                    CalculateMorphTargetWeights(DestVertex, LOD, LODIndex, MorphTargetOfInterest);
                }
                else
                {
                    CalculateBoneWeights(DestVertex, LOD, *MeshLOD.MeshObjectWeightBuffer, BonesOfInterest);
                }
            }
        }

        CachedVertexLOD = LODIndex;
        
        // 将计算结果写入GPU缓冲区
        UpdateGPUBuffers(LOD, MeshLOD, RHICmdList);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. SKIN_LOD_VERTICES 宏展开&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#define SKIN_LOD_VERTICES(VertexType, NumUVs) \
{\
    switch(NumUVs)\
    {\
        case 1:\
            SkinVertices&amp;lt;VertexType&amp;lt;1&amp;gt;, 1&amp;gt;(DestVertex, ReferenceToLocal, /*...*/);\
            break;\
        case 2:\
            SkinVertices&amp;lt;VertexType&amp;lt;2&amp;gt;, 2&amp;gt;(DestVertex, ReferenceToLocal, /*...*/);\
            break;\
        case 3:\
            SkinVertices&amp;lt;VertexType&amp;lt;3&amp;gt;, 3&amp;gt;(DestVertex, ReferenceToLocal, /*...*/);\
            break;\
        case 4:\
            SkinVertices&amp;lt;VertexType&amp;lt;4&amp;gt;, 4&amp;gt;(DestVertex, ReferenceToLocal, /*...*/);\
            break;\
        default:\
            checkf(false, TEXT(&quot;Invalid number of UV sets. Must be between 1 and 4&quot;));\
            break;\
    }\
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;高度优化的蒙皮核心算法&lt;/h2&gt;
&lt;h3&gt;1. SkinVertices 模板函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename VertexType, int32 NumberOfUVs&amp;gt;
static void SkinVertices(
    FFinalSkinVertex* DestVertex, 
    FMatrix44f* ReferenceToLocal, 
    int32 LODIndex, 
    FSkeletalMeshLODRenderData&amp;amp; LOD,
    FSkinWeightVertexBuffer&amp;amp; WeightBuffer,
    const FMorphTargetWeightMap&amp;amp; InActiveMorphTargets, 
    const TArray&amp;lt;float&amp;gt;&amp;amp; MorphTargetWeights, 
    const TMap&amp;lt;int32, FClothSimulData&amp;gt;&amp;amp; ClothSimulUpdateData, 
    float ClothBlendWeight, 
    const FMatrix&amp;amp; WorldToLocal,
    const FVector&amp;amp; WorldScale)
{
    // 设置SIMD寄存器控制
    uint32 StatusRegister = VectorGetControlRegister();
    VectorSetControlRegister(StatusRegister | VECTOR_ROUND_TOWARD_ZERO);

    // 初始化Morph Target评估信息
    TArray&amp;lt;FMorphTargetInfo&amp;gt; MorphEvalInfos;
    uint32 NumValidMorphs = InitEvalInfos(InActiveMorphTargets, MorphTargetWeights, 
                                         LODIndex, MorphEvalInfos);

    // 预取骨骼矩阵到缓存
    const uint32 MaxGPUSkinBones = FGPUBaseSkinVertexFactory::GetMaxGPUSkinBones();
    for (uint32 MatrixIndex = 0; MatrixIndex &amp;lt; MaxGPUSkinBones; MatrixIndex += 2)
    {
        FPlatformMisc::Prefetch(ReferenceToLocal + MatrixIndex);
    }

    int32 CurBaseVertIdx = 0;
    int32 VertexBufferBaseIndex = 0;
    const FVector WorldScaleAbs = WorldScale.GetAbs();

    // 按Section处理顶点
    for(int32 SectionIndex = 0; SectionIndex &amp;lt; LOD.RenderSections.Num(); SectionIndex++)
    {
        FSkelMeshRenderSection&amp;amp; Section = LOD.RenderSections[SectionIndex];
        const FClothSimulData* ClothSimData = ClothSimulUpdateData.Find(Section.CorrespondClothAssetIndex);

        SkinVertexSection&amp;lt;VertexType, NumberOfUVs&amp;gt;(
            DestVertex, MorphEvalInfos, MorphTargetWeights, Section, LOD, WeightBuffer, 
            VertexBufferBaseIndex, NumValidMorphs, CurBaseVertIdx, LODIndex, 
            ReferenceToLocal, ClothSimData, ClothBlendWeight, WorldToLocal, WorldScaleAbs);
    }

    // 恢复SIMD寄存器状态
    VectorSetControlRegister(StatusRegister);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. SkinVertexSection - SIMD 优化的蒙皮计算&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename VertexType, int32 NumberOfUVs&amp;gt;
static void SkinVertexSection(/* 参数列表 */)
{
    static constexpr VectorRegister VECTOR_INV_65535 = 
        MakeVectorRegisterDoubleConstant(1.0/65535, 1.0/65535, 1.0/65535, 1.0/65535);
    
    VertexType VertexCopy; // Morph Target处理用的临时顶点

    // 预取骨骼映射数据
    const FBoneIndexType* BoneMap = Section.BoneMap.GetData();
    FPlatformMisc::Prefetch(BoneMap);
    FPlatformMisc::Prefetch(BoneMap, PLATFORM_CACHE_LINE_SIZE);

    const int32 MaxSectionBoneInfluences = WeightBuffer.GetMaxBoneInfluences();
    const bool bLODUsesCloth = LOD.HasClothData() &amp;amp;&amp;amp; ClothSimData != nullptr &amp;amp;&amp;amp; ClothBlendWeight &amp;gt; 0.0f;
    const int32 NumSoftVertices = Section.GetNumVertices();
    
    if (NumSoftVertices &amp;gt; 0)
    {
        INC_DWORD_STAT_BY(STAT_CPUSkinVertices, NumSoftVertices);
        
        // 逐顶点处理
        for(int32 VertexIndex = VertexBufferBaseIndex; VertexIndex &amp;lt; NumSoftVertices; 
            VertexIndex++, DestVertex++)
        {
            const int32 VertexBufferIndex = Section.GetVertexBufferIndex() + VertexIndex;

            // 加载源顶点数据
            VertexType SrcSoftVertex;
            const FVector&amp;amp; VertexPosition = (FVector)LOD.StaticVertexBuffers.PositionVertexBuffer.VertexPosition(VertexBufferIndex);
            FPlatformMisc::Prefetch(&amp;amp;VertexPosition, PLATFORM_CACHE_LINE_SIZE);
            
            SrcSoftVertex.Position = (FVector3f)VertexPosition;
            SrcSoftVertex.TangentX = LOD.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentX(VertexBufferIndex);
            SrcSoftVertex.TangentZ = LOD.StaticVertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(VertexBufferIndex);
            
            // 加载UV坐标
            for (uint32 j = 0; j &amp;lt; VertexType::NumTexCoords; j++)
            {
                SrcSoftVertex.UVs[j] = LOD.StaticVertexBuffers.StaticMeshVertexBuffer.GetVertexUV_Typed&amp;lt;VertexType::StaticMeshVertexUVType&amp;gt;(VertexBufferIndex, j);
            }

            // 处理Morph Target
            VertexType* MorphedVertex = &amp;amp;SrcSoftVertex;
            if(NumValidMorphs) 
            {
                MorphedVertex = &amp;amp;VertexCopy;
                UpdateMorphedVertex&amp;lt;VertexType&amp;gt;(*MorphedVertex, SrcSoftVertex, CurBaseVertIdx, LODIndex, MorphEvalInfos, MorphTargetWeights);
            }

            // 获取蒙皮权重
            FSkinWeightInfo SrcWeights = WeightBuffer.GetVertexSkinWeights(VertexBufferIndex);
            
            // 执行SIMD优化的蒙皮计算
            PerformSIMDSkinning(MorphedVertex, SrcWeights, BoneMap, ReferenceToLocal, 
                               MaxSectionBoneInfluences, DestVertex);

            // 处理布料模拟
            ProcessClothSimulation(VertexIndex, ClothVertData, ClothSimData, 
                                 ClothBlendWeight, WorldToLocal, WorldScaleAbs, DestVertex);

            // 复制UV坐标
            CopyUVCoordinates(VertexIndex, Section, LOD, NumberOfUVs, DestVertex);

            CurBaseVertIdx++;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. SIMD 优化的蒙皮矩阵计算&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 核心的SIMD蒙皮计算（简化版）
void PerformSIMDSkinning(/* 参数 */)
{
    const FBoneIndexType* RESTRICT BoneIndices = SrcWeights.InfluenceBones;
    const uint16* RESTRICT BoneWeights = SrcWeights.InfluenceWeights;

    // 将顶点数据加载到SIMD寄存器
    static VectorRegister SrcNormals[3];
    VectorRegister DstNormals[3];
    SrcNormals[0] = VectorLoadFloat3_W1(&amp;amp;MorphedVertex-&amp;gt;Position);
    SrcNormals[1] = Unpack3(&amp;amp;MorphedVertex-&amp;gt;TangentX.Vector.Packed);
    SrcNormals[2] = Unpack4(&amp;amp;MorphedVertex-&amp;gt;TangentZ.Vector.Packed);
    
    // 权重归一化（从uint16转换为float）
    VectorRegister Weights = VectorMultiply(VectorLoadURGBA16N(BoneWeights), VECTOR_INV_65535);
    VectorRegister ExtraWeights = MakeVectorRegister(0.f, 0.f, 0.f, 0.f);
    VectorRegister ExtraWeights2 = MakeVectorRegister(0.f, 0.f, 0.f, 0.f);
    
    // 处理超过4个骨骼影响的情况
    if (MaxSectionBoneInfluences &amp;gt; 4)
    {
        ExtraWeights = VectorMultiply(VectorLoadURGBA16N(&amp;amp;BoneWeights[MAX_INFLUENCES_PER_STREAM]), VECTOR_INV_65535);
    }
    if (MaxSectionBoneInfluences &amp;gt; 8)
    {
        ExtraWeights2 = VectorMultiply(VectorLoadURGBA16N(&amp;amp;BoneWeights[EXTRA_BONE_INFLUENCES]), VECTOR_INV_65535);
    }
    
    VectorResetFloatRegisters();

    // 累积骨骼变换（支持最多12个骨骼影响）
    const FMatrix44f BoneMatrix0 = ReferenceToLocal[BoneMap[BoneIndices[INFLUENCE_0]]];
    VectorRegister Weight0 = VectorReplicate(Weights, INFLUENCE_0);
    VectorRegister M00 = VectorMultiply(VectorLoadAligned(&amp;amp;BoneMatrix0.M[0][0]), Weight0);
    VectorRegister M10 = VectorMultiply(VectorLoadAligned(&amp;amp;BoneMatrix0.M[1][0]), Weight0);
    VectorRegister M20 = VectorMultiply(VectorLoadAligned(&amp;amp;BoneMatrix0.M[2][0]), Weight0);
    VectorRegister M30 = VectorMultiply(VectorLoadAligned(&amp;amp;BoneMatrix0.M[3][0]), Weight0);

    // 展开循环处理其他骨骼影响（源码中有完整的if-else嵌套结构）
    if (MaxSectionBoneInfluences &amp;gt; 1)
    {
        const FMatrix44f BoneMatrix1 = ReferenceToLocal[BoneMap[BoneIndices[INFLUENCE_1]]];
        VectorRegister Weight1 = VectorReplicate(Weights, INFLUENCE_1);
        M00 = VectorMultiplyAdd(VectorLoadAligned(&amp;amp;BoneMatrix1.M[0][0]), Weight1, M00);
        M10 = VectorMultiplyAdd(VectorLoadAligned(&amp;amp;BoneMatrix1.M[1][0]), Weight1, M10);
        M20 = VectorMultiplyAdd(VectorLoadAligned(&amp;amp;BoneMatrix1.M[2][0]), Weight1, M20);
        M30 = VectorMultiplyAdd(VectorLoadAligned(&amp;amp;BoneMatrix1.M[3][0]), Weight1, M30);
        
        // ... 继续处理其他骨骼 (源码支持最多12个骨骼)
    }

    // 应用变换到顶点属性
    VectorRegister N_xxxx = VectorReplicate(SrcNormals[0], 0);
    VectorRegister N_yyyy = VectorReplicate(SrcNormals[0], 1);
    VectorRegister N_zzzz = VectorReplicate(SrcNormals[0], 2);
    DstNormals[0] = VectorMultiplyAdd(N_xxxx, M00, 
                    VectorMultiplyAdd(N_yyyy, M10, 
                    VectorMultiplyAdd(N_zzzz, M20, M30)));

    // 处理切线和法线
    N_xxxx = VectorReplicate(SrcNormals[1], 0);
    N_yyyy = VectorReplicate(SrcNormals[1], 1);
    N_zzzz = VectorReplicate(SrcNormals[1], 2);
    DstNormals[1] = VectorNormalize(VectorMultiplyAdd(N_xxxx, M00, 
                    VectorMultiplyAdd(N_yyyy, M10, VectorMultiply(N_zzzz, M20))));

    N_xxxx = VectorReplicate(SrcNormals[2], 0);
    N_yyyy = VectorReplicate(SrcNormals[2], 1);
    N_zzzz = VectorReplicate(SrcNormals[2], 2);
    DstNormals[2] = VectorNormalize(VectorMultiplyAdd(N_xxxx, M00, 
                    VectorMultiplyAdd(N_yyyy, M10, VectorMultiply(N_zzzz, M20))));

    // 保持W分量（基向量的行列式符号）
    DstNormals[2] = VectorMultiplyAdd(VECTOR_0001, SrcNormals[2], DstNormals[2]);

    // 写入内存
    VectorStoreFloat3(DstNormals[0], &amp;amp;DestVertex-&amp;gt;Position);
    Pack3(DstNormals[1], &amp;amp;DestVertex-&amp;gt;TangentX.Vector.Packed);
    Pack4(DstNormals[2], &amp;amp;DestVertex-&amp;gt;TangentZ.Vector.Packed);
    VectorResetFloatRegisters();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Morph Target 处理系统&lt;/h2&gt;
&lt;h3&gt;1. Morph Target 信息结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct FMorphTargetInfo
{
    int32 WeightIndex = INDEX_NONE;     // 权重列表中的索引
    int32 NextDeltaIndex = INDEX_NONE;  // 下一个要应用的Delta索引
    const FMorphTargetDelta* Deltas = nullptr;  // Delta数组
    int32 NumDeltas = 0;                // Delta数量
};

// 初始化Morph Target评估信息
static uint32 InitEvalInfos(const FMorphTargetWeightMap&amp;amp; InActiveMorphTargets, 
                           const TArray&amp;lt;float&amp;gt;&amp;amp; MorphTargetWeights, 
                           int32 LODIndex, 
                           TArray&amp;lt;FMorphTargetInfo&amp;gt;&amp;amp; OutEvalInfos)
{
    uint32 NumValidMorphTargets = 0;
    const float MorphTargetMaxBlendWeight = UE::SkeletalRender::Settings::GetMorphTargetMaxBlendWeight();

    for(const TTuple&amp;lt;const UMorphTarget*, int32&amp;gt;&amp;amp; MorphItem : InActiveMorphTargets)
    {
        FMorphTargetInfo NewInfo;
        const UMorphTarget* MorphTarget = MorphItem.Key;
        const int32 WeightIndex = MorphItem.Value;

        const float ActiveMorphAbsVertexWeight = FMath::Abs(MorphTargetWeights[WeightIndex]);

        // 检查Morph Target是否有效且权重在有效范围内
        if(MorphTarget != nullptr &amp;amp;&amp;amp;
           ActiveMorphAbsVertexWeight &amp;gt;= MinMorphTargetBlendWeight &amp;amp;&amp;amp;
           ActiveMorphAbsVertexWeight &amp;lt;= MorphTargetMaxBlendWeight &amp;amp;&amp;amp;
           MorphTarget-&amp;gt;HasDataForLOD(LODIndex))
        {
            NewInfo.WeightIndex = WeightIndex;
            NewInfo.NextDeltaIndex = 0;
            NewInfo.Deltas = MorphTarget-&amp;gt;GetMorphTargetDelta(LODIndex, NewInfo.NumDeltas);
            NumValidMorphTargets++;
        }

        OutEvalInfos.Add(NewInfo);
    }
    return NumValidMorphTargets;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Morph Target 混合算法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename VertexType&amp;gt;
FORCEINLINE void UpdateMorphedVertex(
    VertexType&amp;amp; MorphedVertex, 
    const VertexType&amp;amp; SrcVertex, 
    int32 CurBaseVertIdx, 
    int32 LODIndex, 
    TArray&amp;lt;FMorphTargetInfo&amp;gt;&amp;amp; EvalInfos, 
    const TArray&amp;lt;float&amp;gt;&amp;amp; MorphWeights)
{
    MorphedVertex = SrcVertex;

    // 遍历所有活跃的Morph Target
    for(int32 MorphIdx = 0; MorphIdx &amp;lt; EvalInfos.Num(); MorphIdx++)
    {
        FMorphTargetInfo&amp;amp; Info = EvalInfos[MorphIdx];

        // 检查下一个Delta是否匹配当前顶点
        if(Info.NextDeltaIndex != INDEX_NONE &amp;amp;&amp;amp;
           Info.NextDeltaIndex &amp;lt; Info.NumDeltas &amp;amp;&amp;amp;
           Info.Deltas[Info.NextDeltaIndex].SourceIdx == CurBaseVertIdx)
        {
            // 应用Morph Target混合
            ApplyMorphBlend(MorphedVertex, Info.Deltas[Info.NextDeltaIndex], 
                           MorphWeights[Info.WeightIndex]);

            // 更新下一个Delta索引
            Info.NextDeltaIndex += 1;
        }
    }

    // 重建正交标准化的切线基
    RebuildTangentBasis(MorphedVertex);
}

template&amp;lt;typename VertexType&amp;gt;
FORCEINLINE void ApplyMorphBlend(VertexType&amp;amp; DestVertex, 
                                const FMorphTargetDelta&amp;amp; SrcMorph, 
                                float Weight)
{
    // 添加位置偏移
    DestVertex.Position += SrcMorph.PositionDelta * Weight;

    // 保存W分量（TangentZ的W存储基向量行列式符号）
    int8 W = DestVertex.TangentZ.Vector.W;

    FVector TanZ = DestVertex.TangentZ.ToFVector();
    
    // 添加法线偏移（法线Delta权重最大限制为1.0）
    DestVertex.TangentZ = (TanZ + FVector(SrcMorph.TangentZDelta * FMath::Min(Weight, 1.0f))).GetUnsafeNormal();
    
    // 恢复W分量
    DestVertex.TangentZ.Vector.W = W;
}

template&amp;lt;typename VertexType&amp;gt;
FORCEINLINE void RebuildTangentBasis(VertexType&amp;amp; DestVertex)
{
    // 通过将新法线与基切线向量正交标准化来重建切线
    FVector Tangent = DestVertex.TangentX.ToFVector();
    FVector Normal = DestVertex.TangentZ.ToFVector();
    Tangent = Tangent - ((Tangent | Normal) * Normal);
    Tangent.Normalize();
    DestVertex.TangentX = Tangent;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;布料模拟集成&lt;/h2&gt;
&lt;h3&gt;1. 布料模拟数据结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 布料模拟的CPU实现
struct ClothCPU
{
    // 获取布料模拟位置
    FORCEINLINE static FVector GetClothSimulPosition(const FClothSimulData&amp;amp; InClothSimData, int32 InIndex)
    {
        if (InClothSimData.Positions.IsValidIndex(InIndex))
        {
            return FVector(InClothSimData.Transform.TransformPosition(
                (FVector)InClothSimData.Positions[InIndex]));
        }
        return FVector::ZeroVector;
    }

    // 获取布料模拟法线
    FORCEINLINE static FVector GetClothSimulNormal(const FClothSimulData&amp;amp; InClothSimData, int32 InIndex)
    {
        if (InClothSimData.Normals.IsValidIndex(InIndex))
        {
            return FVector(InClothSimData.Transform.TransformVector(
                (FVector)InClothSimData.Normals[InIndex]));
        }
        return FVector(0, 0, 1);
    }

    // 计算布料位置（使用重心坐标插值）
    FORCEINLINE static FVector ClothingPosition(const FMeshToMeshVertData&amp;amp; InClothVertData, 
                                               const FClothSimulData&amp;amp; InClothSimData, 
                                               const FVector&amp;amp; InWorldScaleAbs)
    {
        return InClothVertData.PositionBaryCoordsAndDist.X * 
               (GetClothSimulPosition(InClothSimData, InClothVertData.SourceMeshVertIndices[0]) + 
                GetClothSimulNormal(InClothSimData, InClothVertData.SourceMeshVertIndices[0]) * 
                InClothVertData.PositionBaryCoordsAndDist.W * InWorldScaleAbs.X)
             + /* 类似地处理Y和Z分量 */;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 布料与骨骼蒙皮的混合&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 在SkinVertexSection中应用布料模拟
if (ClothVertData != nullptr &amp;amp;&amp;amp; ClothVertData-&amp;gt;SourceMeshVertIndices[3] &amp;lt; FIXED_VERTEX_INDEX)
{
    // 构建模拟位置（世界空间）
    FVector SimulatedPositionWorld = ClothCPU::ClothingPosition(*ClothVertData, *ClothSimData, WorldScaleAbs);

    // 转换回局部空间
    FVector3f SimulatedPosition = (FVector4f)WorldToLocal.TransformPosition(SimulatedPositionWorld);

    // 计算顶点混合权重
    const float VertexBlend = ClothBlendWeight * (1.0f - (ClothVertData-&amp;gt;SourceMeshVertIndices[3] / 65535.0f));
    
    // 在蒙皮位置和模拟位置之间插值
    DestVertex-&amp;gt;Position = FMath::Lerp(DestVertex-&amp;gt;Position, SimulatedPosition, VertexBlend);

    // 重新计算切线和法线
    FVector TangentX, TangentZ;
    ClothCPU::ClothingTangents(*ClothVertData, *ClothSimData, SimulatedPositionWorld, 
                              WorldToLocal, WorldScaleAbs, TangentX, TangentZ);

    // 在蒙皮切线和模拟切线之间插值
    FVector SkinnedTangentX = DestVertex-&amp;gt;TangentX.ToFVector();
    FVector4 SkinnedTangentZ = DestVertex-&amp;gt;TangentZ.ToFVector4();
    DestVertex-&amp;gt;TangentX = (TangentX * VertexBlend) + (SkinnedTangentX * (1.0f - VertexBlend));
    DestVertex-&amp;gt;TangentZ = FVector4((TangentZ * VertexBlend) + (SkinnedTangentZ * (1.0f - VertexBlend)), 
                                   SkinnedTangentZ.W);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GPU 缓冲区更新机制&lt;/h2&gt;
&lt;h3&gt;1. 缓存顶点到 GPU 缓冲区更新&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 在CacheVertices函数的最后部分
void UpdateGPUBuffers(const FSkeletalMeshLODRenderData&amp;amp; LOD, 
                     const FSkeletalMeshObjectLOD&amp;amp; MeshLOD, 
                     FRHICommandList&amp;amp; RHICmdList)
{
    check(LOD.GetNumVertices() == CachedFinalVertices.Num());

    // 更新位置和切线数据
    for (int i = 0; i &amp;lt; CachedFinalVertices.Num(); i++)
    {
        // 位置缓冲区
        MeshLOD.PositionVertexBuffer.VertexPosition(i) = CachedFinalVertices[i].Position;
        
        // 切线缓冲区
        MeshLOD.StaticMeshVertexBuffer.SetVertexTangents(i, 
            (FVector3f)CachedFinalVertices[i].TangentX.ToFVector(), 
            CachedFinalVertices[i].GetTangentY(), 
            (FVector3f)CachedFinalVertices[i].TangentZ.ToFVector());

        // UV坐标
        for (uint32 UVIndex = 0; UVIndex &amp;lt; LOD.StaticVertexBuffers.StaticMeshVertexBuffer.GetNumTexCoords(); ++UVIndex)
        {
            MeshLOD.StaticMeshVertexBuffer.SetVertexUV(i, UVIndex, 
                FVector2f(CachedFinalVertices[i].TextureCoordinates[UVIndex].X, 
                         CachedFinalVertices[i].TextureCoordinates[UVIndex].Y));
        }
    }

    // 更新RHI缓冲区
    MeshLOD.PositionVertexBuffer.UpdateRHI(RHICmdList);
    MeshLOD.StaticMeshVertexBuffer.UpdateRHI(RHICmdList);

    // 重新绑定顶点工厂
    FLocalVertexFactory::FDataType Data;
    MeshLOD.PositionVertexBuffer.BindPositionVertexBuffer(&amp;amp;MeshLOD.VertexFactory, Data);
    MeshLOD.StaticMeshVertexBuffer.BindTangentVertexBuffer(&amp;amp;MeshLOD.VertexFactory, Data);
    MeshLOD.StaticMeshVertexBuffer.BindPackedTexCoordVertexBuffer(&amp;amp;MeshLOD.VertexFactory, Data, MAX_TEXCOORDS);
    MeshLOD.StaticMeshVertexBuffer.BindLightMapVertexBuffer(&amp;amp;MeshLOD.VertexFactory, Data, 0);
    MeshLOD.MeshObjectColorBuffer-&amp;gt;BindColorVertexBuffer(&amp;amp;MeshLOD.VertexFactory, Data);

    MeshLOD.VertexFactory.SetData(RHICmdList, Data);
    MeshLOD.VertexFactory.InitResource(RHICmdList);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调试和可视化功能&lt;/h2&gt;
&lt;h3&gt;1. 骨骼权重可视化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void CalculateBoneWeights(FFinalSkinVertex* DestVertex, 
                                FSkeletalMeshLODRenderData&amp;amp; LOD, 
                                FSkinWeightVertexBuffer&amp;amp; WeightBuffer, 
                                TArray&amp;lt;int32&amp;gt; InBonesOfInterest)
{
    int32 VertexBufferBaseIndex = 0;

    for(int32 SectionIndex = 0; SectionIndex &amp;lt; LOD.RenderSections.Num(); SectionIndex++)
    {
        FSkelMeshRenderSection&amp;amp; Section = LOD.RenderSections[SectionIndex];
        CalculateSectionBoneWeights(DestVertex, WeightBuffer, Section, InBonesOfInterest);
    }
}

static FORCEINLINE void CalculateSectionBoneWeights(FFinalSkinVertex*&amp;amp; DestVertex, 
                                                   FSkinWeightVertexBuffer&amp;amp; SkinWeightVertexBuffer, 
                                                   FSkelMeshRenderSection&amp;amp; Section, 
                                                   const TArray&amp;lt;int32&amp;gt;&amp;amp; BonesOfInterest)
{
    FBoneIndexType* BoneMap = Section.BoneMap.GetData();

    for(int32 VertexIndex = 0; VertexIndex &amp;lt; Section.GetNumVertices(); VertexIndex++, DestVertex++)
    {
        const int32 VertexBufferIndex = Section.GetVertexBufferIndex() + VertexIndex;
        FSkinWeightInfo SrcWeight = SkinWeightVertexBuffer.GetVertexSkinWeights(VertexBufferIndex);

        // 清零 UV 坐标
        DestVertex-&amp;gt;TextureCoordinates[0].X = 0.0f;
        DestVertex-&amp;gt;TextureCoordinates[0].Y = 0.0f;

        const FBoneIndexType* RESTRICT BoneIndices = SrcWeight.InfluenceBones;
        const uint16* RESTRICT BoneWeights = SrcWeight.InfluenceWeights;

        // 累积感兴趣骨骼的权重
        for (uint32 i = 0; i &amp;lt; SkinWeightVertexBuffer.GetMaxBoneInfluences(); i++)
        {
            if (BonesOfInterest.Contains(BoneMap[BoneIndices[i]]))
            {
                DestVertex-&amp;gt;TextureCoordinates[0].X += BoneWeights[i] / 65535.0; 
                DestVertex-&amp;gt;TextureCoordinates[0].Y += BoneWeights[i] / 65535.0;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Morph Target 可视化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;static void CalculateMorphTargetWeights(FFinalSkinVertex* DestVertex, 
                                       FSkeletalMeshLODRenderData&amp;amp; LOD, 
                                       int LODIndex, 
                                       TArray&amp;lt;UMorphTarget*&amp;gt; InMorphTargetOfInterest)
{
    const FFinalSkinVertex* EndVert = DestVertex + LOD.GetNumVertices();

    // 清零所有顶点的 UV 坐标
    for (FFinalSkinVertex* ClearVert = DestVertex; ClearVert != EndVert; ++ClearVert)
    {
        ClearVert-&amp;gt;TextureCoordinates[0].X = 0.0f;
        ClearVert-&amp;gt;TextureCoordinates[0].Y = 0.0f;
    }

    // 为每个感兴趣的 Morph Target 标记受影响的顶点
    for (const UMorphTarget* Morphtarget : InMorphTargetOfInterest)
    {
        int32 NumDeltas;
        const FMorphTargetDelta* MTLODVertices = Morphtarget-&amp;gt;GetMorphTargetDelta(LODIndex, NumDeltas);
        
        for (int32 MorphVertexIndex = 0; MorphVertexIndex &amp;lt; NumDeltas; ++MorphVertexIndex)
        {
            FFinalSkinVertex* SetVert = DestVertex + MTLODVertices[MorphVertexIndex].SourceIdx;
            SetVert-&amp;gt;TextureCoordinates[0].X += 1.0f;
            SetVert-&amp;gt;TextureCoordinates[0].Y += 1.0f;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;性能优化要点&lt;/h2&gt;
&lt;h3&gt;1. SIMD 指令使用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;VectorRegister&lt;/code&gt;类型进行4路并行计算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VectorLoadAligned&lt;/code&gt;、&lt;code&gt;VectorMultiplyAdd&lt;/code&gt;等优化指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Unpack3&lt;/code&gt;、&lt;code&gt;Pack4&lt;/code&gt;等紧凑数据格式处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 内存访问优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FPlatformMisc::Prefetch&lt;/code&gt;预取数据到缓存&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RESTRICT&lt;/code&gt;关键字告诉编译器指针不重叠&lt;/li&gt;
&lt;li&gt;顺序访问模式最大化缓存效率&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 分支预测优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;展开骨骼影响处理循环而非使用动态循环&lt;/li&gt;
&lt;li&gt;模板特化避免运行时分支&lt;/li&gt;
&lt;li&gt;常量条件编译时优化&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 内存布局优化&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;顶点数据紧凑排列&lt;/li&gt;
&lt;li&gt;Section-wise 处理提高局部性&lt;/li&gt;
&lt;li&gt;预分配缓冲区避免动态分配&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;UE 5.5.4的 CPU 蒙皮系统是一个&lt;strong&gt;高度优化的多功能渲染管线&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;核心特征&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SIMD 向量化&lt;/strong&gt;：充分利用现代 CPU 的并行计算能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多骨骼支持&lt;/strong&gt;：最多支持12个骨骼影响 per 顶点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Morph Target 集成&lt;/strong&gt;：高效的增量混合算法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;布料模拟&lt;/strong&gt;：与物理模拟的无缝集成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调试可视化&lt;/strong&gt;：完整的权重和变形可视化工具&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;适用场景&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高精度需求&lt;/strong&gt;：需要 CPU 端访问变形后顶点数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容性考虑&lt;/strong&gt;：不支持 GPU 蒙皮的平台&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调试开发&lt;/strong&gt;：实时权重调试和可视化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特殊效果&lt;/strong&gt;：复杂的程序化变形&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个系统为 UE 提供了强大的 CPU 端骨骼动画处理能力，确保在各种场景下都能提供高质量的骨骼动画渲染效果。&lt;/p&gt;
</content:encoded></item><item><title>GPGPU in Unity</title><link>https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/GPGPU/Compute%20Shader%20in%20Unity/</link><guid isPermaLink="true">https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/GPGPU/Compute%20Shader%20in%20Unity/</guid><pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;GPGPU in Unity&lt;/h1&gt;
&lt;p&gt;首先从 Unity 应用的角度来看 GPGPU。Unity 想要使用 GPGPU, 需要用到 Compute Shader。&lt;br /&gt;
定义一个简单的 ComputeShader：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma kernel CSMain

RWStructuredBuffer&amp;lt;float4&amp;gt; Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.xy] = float4(id.x &amp;amp; id.y, (id.x &amp;amp; 15)/15.0, (id.y &amp;amp; 15)/15.0, 0.0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;kernel&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kernel&lt;/code&gt; 指定核函数, Compute Shader 最终会在 C# 层调用 &lt;code&gt;Dispatch&lt;/code&gt; 函数来运行指定的核函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public extern void Dispatch(  
  int kernelIndex,  
  int threadGroupsX,  
  int threadGroupsY,  
  int threadGroupsZ);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Thread and Group&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Dispatch&lt;/code&gt; 定义了计算的 &lt;strong&gt;线程组&lt;/strong&gt; 的数量。&lt;br /&gt;
numthread 定义了一个 &lt;strong&gt;线程组 (Thread Group) 中被执行线程&lt;/strong&gt; 的总数量。&lt;/p&gt;
&lt;p&gt;Compute Shader 中需要理解与线程相关的 SV 的含义：&lt;br /&gt;
定义 &lt;strong&gt;由 Dispatch 函数决定&lt;/strong&gt; 的线程组 ID 定义为 $[gx, gy, gz]$ , 范围是 $[gX-1, gY-1, gZ-1]$。&lt;br /&gt;
定义 &lt;strong&gt;由 threadnumber 决定的&lt;/strong&gt; 线程组内线程 ID 定义为 $[tx,ty,tz]$, 范围是 $[tX-1, tY-1, tZ-1]$&lt;br /&gt;
定义线程的 &lt;strong&gt;全局唯一标识 ID&lt;/strong&gt; 的范围是 $[gX&lt;em&gt;tX-1,gY&lt;/em&gt;tY-1,gZ*tZ-1]$&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SV&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Desc&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SV_GroupID&lt;/td&gt;
&lt;td&gt;int3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;由 Dispatch 函数决定&lt;/strong&gt; 的线程组 ID&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SV_GroupThreadID&lt;/td&gt;
&lt;td&gt;int3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;由 threadnumber 决定的&lt;/strong&gt; 线程组内线程 ID&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SV_DispatchThreadID&lt;/td&gt;
&lt;td&gt;int3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;全局唯一标识 ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$[gx&lt;em&gt;tX+tx,gy&lt;/em&gt;tY+ty,gz*tZ+tz]$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SV_GroupIndex&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;线程所在线程组内的下标&lt;/td&gt;
&lt;td&gt;$x+y&lt;em&gt;tX+z&lt;/em&gt;tX*tY$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注意 group 和 thread 计数的时候, 都是将 x 作为最后的计数维度, z 作为最先技术的维度。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250526142026.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-attributes-numthreads&quot;&gt;numthreads - Win32 apps | Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Static Value&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#pragma kernel CSMain

float3 boxSize1 = float3(1.0f, 1.0f, 1.0f); // 方法1
const float3 boxSize2 = float3(2.0f, 2.0f, 2.0f); // 方法2
static float3 boxSize3 = float3(3.0f, 3.0f, 3.0f); // 方法3

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // 做判断
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Computer Shader 遵循 HLSL 的语法规定。方法一和方法二初始化是无效的, 在 CSMain 中会读取到 $(0,0,0)$。只有方法三才能读取到初始化的值。&lt;/p&gt;
&lt;h2&gt;Code in CPU&lt;/h2&gt;
&lt;p&gt;在 CPU 层, 核心只需要做两件事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Data Transfer&lt;/li&gt;
&lt;li&gt;Dispatch&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Data Transfer&lt;/h3&gt;
&lt;p&gt;核心是定义基础数据结构或者 StructBuffer, 然后通过 &lt;code&gt;ComputeShader&lt;/code&gt; 的 &lt;code&gt;Set&lt;/code&gt; 方法将 CPU 的数据传递给 GPU。&lt;/p&gt;
&lt;h3&gt;Dispatch&lt;/h3&gt;
&lt;p&gt;调用 &lt;code&gt;ComputeShader&lt;/code&gt; 的 &lt;code&gt;Dispatch&lt;/code&gt; 函数, 触发核函数的计算。一般是回写到 &lt;code&gt;RWStructBuffer&lt;/code&gt; 或者其他 RW 的 GPU 结构。&lt;/p&gt;
&lt;p&gt;回写之后, 可以在显存中直接取用已经计算好的数据。&lt;/p&gt;
</content:encoded></item><item><title>Hardware GPGPU</title><link>https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/GPGPU/Hardware%20GPGPU/</link><guid isPermaLink="true">https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/GPGPU/Hardware%20GPGPU/</guid><pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Thead and Group&lt;/h2&gt;
&lt;p&gt;在 GPU 编程的过程中，根据程序具体的执行需求，可将 &lt;strong&gt;线程&lt;/strong&gt; 划分为由 &lt;strong&gt;线程组（thread group）&lt;/strong&gt; 构成 的网格（grid）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;numthread&lt;/code&gt; 和 &lt;code&gt;Dispatch&lt;/code&gt; 的三维 Grid 的设置方式只是方便逻辑上的划分，硬件执行的时候还会把所有线程当成一维的。因此 &lt;code&gt;numthread(8, 8, 1)&lt;/code&gt; 和 &lt;code&gt;numthread(64, 1, 1)&lt;/code&gt; 只是对我们来说索引线程的方式不一样而已，除外没区别。&lt;/p&gt;
&lt;p&gt;具体划分见 &lt;a href=&quot;/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/GPGPU/Compute%20Shader%20in%20Unity/#thread-and-group&quot;&gt;Compute Shader in Unity&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Thread and Group in Hardware&lt;/h3&gt;
&lt;p&gt;GPU 会有上千个“核心”，用 NVIDIA 的说法就是 CUDA Core。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SP&lt;/strong&gt;：最基本的处理单元，streaming processor，也称为 CUDA core。最后具体的指令和任务都是在 SP 上处理的。GPU 进行并行计算，也就是很多个 SP 同时做处理。常说的几百核心的 GPU 值指的都是 SP 的数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SM&lt;/strong&gt;：多个 SP 加上其他的一些资源组成一个 streaming multiprocessor。也叫 GPU 大核，其他资源如：warp scheduler，register，shared memory 等。SM 可以看做 GPU 的心脏，register 和 shared memory 是 SM 的稀缺资源。CUDA 将这些资源分配给所有驻留在 SM 中的 threads。因此，这些有限的资源就使每个 SM 中 active warps 有非常严格的限制，也就限制了并行能力。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些核心被组织在流式多处理器（streaming multiprocessor, SM）中，一个线程组运行于一个多处理器（SM）之上。每一个 &lt;strong&gt;SP&lt;/strong&gt; 同一时间可以运行一个线程。&lt;/p&gt;
&lt;h3&gt;Warp and Wavefront&lt;/h3&gt;
&lt;p&gt;SM 会将它从 Gigathread 引擎（NVIDIA 技术，专门管理整个流水线）那收到的大线程块，拆分成许多更小的堆，每个堆包含 32 个线程，这样的堆也被称为：&lt;strong&gt;warp&lt;/strong&gt; (AMD 则称为 &lt;strong&gt;wavefront&lt;/strong&gt;)。多处理器会以 SIMD32 的方式（即 32 个线程同时执行相同的指令序列）来处理 warp，每个 CUDA 核心都可处理一个线程。&lt;/p&gt;
&lt;p&gt;每一个线程组都会被划分到一个 Compute Unit 来计算，线程组中的线程由 Compute Unit 中的 SIMD 部分来执行。&lt;br /&gt;
如果我们定义 &lt;code&gt;numthreads(8, 2, 4)&lt;/code&gt; ，那么每个线程组就有 $8×2×4=64$ 个线程，这一整个线程组会被分成两个 warp，调度到单个 SIMD 单元计算。&lt;/p&gt;
&lt;p&gt;单个 SM 处理逐个 warp，当一个 warp 暂时需要等待数据的时候，就可以先换其他 warp 继续执行。&lt;/p&gt;
&lt;h3&gt;Set Size of Thread Group&lt;/h3&gt;
&lt;p&gt;应当总是将线程组的大小设置为 &lt;strong&gt;warp 尺寸的整数倍&lt;/strong&gt;。让 SM 同时容纳多个 warp，能够以防一些情况。&lt;/p&gt;
&lt;p&gt;例如有时候为了等待某些数据就绪，不得不停下来。比如说，我们需要通过法线纹理贴图来计算法线光照，即使该法线纹理已经在 Cache 中了，访问该资源仍然会有所耗时，而如果它不在 Cache 中，那就更加耗时了。用专业术语讲就是 &lt;strong&gt;Memory Stall（内存延迟）&lt;/strong&gt;。与其什么事情也不做，不如将当前的 Warp 换成其它已经准备就绪的 Warp 继续执行。&lt;/p&gt;
&lt;p&gt;NVIDIA 公司生产的图形硬件所用的 warp 单位共有 32 个线程。而 ATI 公司采用的 “wavefront” 单位则具有 64 个线程，且建议为其分配的线程组大小应总为 wavefront 尺寸的整数倍。另外，值得一提的是，不管是 warp 还是 wavefront，它们的大小在未来几代中都有可能发生改变。&lt;/p&gt;
&lt;p&gt;总之，每个 SM 的操作度是 warp，但是每个 SM 可以同时处理多个 warp。然后因为有内存等待（memory stall）的问题，同一个 thread block 有可能需要等待内存才做，因此可以使用多个线程组交叉运行。warp 对我们是不可见和不可编程的，我们可编程的只有线程组。&lt;/p&gt;
&lt;h2&gt;Communication between Thread&lt;/h2&gt;
&lt;h3&gt;Communication between Multi Thread Groups&lt;/h3&gt;
&lt;p&gt;thread 不能访问其他 group 的共享内存。&lt;/p&gt;
&lt;p&gt;group 之间的通信需要 L2 Cache 支持。L2 Cache 性能有所折扣, 因此要见着 group 间的通信。&lt;/p&gt;
&lt;h3&gt;Communication in Single Thread Group&lt;/h3&gt;
&lt;p&gt;单个 group 之间的 thread 的通信, 通过 Local Data Share (LSD)来完成。LDS 的速度是快于 L1 Cache 的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250526171726.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;LDS 会被某些着色阶段使用, 如 fragment shader (pixel shader) 需要用 LDS 来进行插值。但 Compute Shader 和传统着色器不同, 不需要 LDS, 故可以随意只用 LDS。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;groupshared float data[8][8];  
  
[numthreads(8,8,1)]  
void main(ivec3 index : SV_GroupThreadID)  
{  
  data[index.x][index.y] = 0.0;  
  GroupMemoryBarrierWithGroupSync();  
  data[index.y][index.x] += index.x;  
  …  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要组内共享的变量加上 &lt;code&gt;groupshared&lt;/code&gt; , 同时为了保证其他线程正确读取到数据, 需要通过 Barrier 来保证读的时候 LDS 有需要的数据。&lt;/p&gt;
&lt;h2&gt;Vector Register and Scalar Register&lt;/h2&gt;
&lt;p&gt;如果有些变量是线程独立的，称之为 “non-uniform” 变量。（如果一个线程组内有 64 个线程，就要存 64 份数据）&lt;/p&gt;
&lt;p&gt;如果有些变量是线程间共享的，称之为 “uniform” 变量，例如线程组 id 是组内每个线程都一样的。（每个线程组内只存 1 份数据）&lt;/p&gt;
&lt;p&gt;“non-uniform” 变量会被储存到 Vector Register（VGPR, vector general purpose register）中。&lt;/p&gt;
&lt;p&gt;“uniform” 变量会被储存到 Scalar Register（SGPR, scalar general purpose register）中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250526173100.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果用了过多 “non-uniform” 变量导致 Vector Register 装不下，就会导致分配给 SIMD 的线程组数量降低。&lt;/p&gt;
&lt;h2&gt;Differences with Traditional Shader Execution Pipeline&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Vert-Frag Shader&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Command Processor 收集并处理所有命令，发送到 GPU。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Draw()&lt;/code&gt; 命令发送后，Command Processor 告知 Graphics Processor 要做的事情。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;顶点着色器和其他需要计算的阶段被送到 Compute Unit 去计算，处理完会到 Rasterizer (光栅器)，并返回处理好的像素到 Compute Unit 执行像素着色（Pixel shader）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输出到 RenderTarget。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250526174021.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compute Shader&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Command Processor 仍会收集并处理所有命令，发送到 GPU。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不需要传数据到 Graphics Processor, 而是直接传到 Compute Unit。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compute Unit 开始处理 Compute Shader，输入可以有 constants 和 resources（对应 DirectX 的 Resource 可以绑定到渲染管线的资源，例如顶点数组等），输出可以有 writable resources（UAV, Unordered Access View 能被着色器写入的资源视图）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250526174219.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此，如果我们用了 Compute Shader，&lt;strong&gt;可以不通过渲染管线，跳过 &lt;a href=&quot;https://en.wikipedia.org/wiki/Render_output_unit&quot;&gt;Render Output&lt;/a&gt;，使用更少硬件资源&lt;/strong&gt;，利用 GPU 来完成一些渲染不相关的工作。此外，Compute Shader 的流水线&lt;strong&gt;需要的信息也更少&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250526174242.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Disadvantages&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;难以 Debug。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;至少需要 DX11 和 OpenGL ES3。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不同平台针对错误的处理不一致。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;数组越界&lt;/strong&gt;：dx 返回0, 其他平台出错。&lt;br /&gt;
&lt;strong&gt;变量和关键字或内置函数重名&lt;/strong&gt;：dx 无影响, 其他平台出错。&lt;br /&gt;
&lt;strong&gt;SBuffer 和内存布局不一致&lt;/strong&gt;：dx 会自动转换, 其他平台会出错。&lt;br /&gt;
&lt;strong&gt;Buffer 未初始化&lt;/strong&gt;：dx 全是0, 但其他平台可能是任意值或 NaN。&lt;/p&gt;
&lt;h2&gt;Application&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Za4y1s7VL&quot;&gt;Unity线上技术大会-游戏专场｜从手机走向主机 -《原神》主机版渲染技术分享&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;http://youxiputao.com/articles/21061&quot;&gt;《天涯明月刀》手游引擎技术负责人：如何应用GPU Driven优化渲染效果？| TGDC 2020&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://mp.weixin.qq.com/s/y0QZ3aaJg8S7u1c5sCaZgg&quot;&gt;三七研发，这款被称作 “目前最原汁原味的”《斗罗大陆》3D 手游都用到了哪些 Unity 技术？&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://youtu.be/Xpf7Ua3UqOA?t=405&quot;&gt;DD2018: Sebastian Aaltonen - GPU based clay simulation and ray tracing tech in Claybook&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Blend Shape</title><link>https://www.tanedanobata.top/posts/blend-shape/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/blend-shape/</guid><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;BlendShape 技术的记录。
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;Why to Use Blend Shape&lt;/h2&gt;
&lt;p&gt;在制作面部动画的时候，由于人们对面部肌肉运动的敏感性，质量不佳的面部动画很容易出现恐怖谷效应。&lt;/p&gt;
&lt;p&gt;同时面部的肌肉数量众多，使用纯骨骼蒙皮动画的方案不仅会有性能上的问题同时对艺术家们的技术提出了极高的要求。&lt;/p&gt;
&lt;p&gt;基于BlendShape的方案允许将最终的面部姿势作为多个面部表情的线性组合。&lt;/p&gt;
&lt;p&gt;面部动作编码系统（FACS）为目标形状的构建提供了指导。这个系统详细阐述了表情与情绪之间的相对应关系。该系统中的许多目标近似于单个面部肌肉的线性化效果。通过改变线性组合的权重，可以用很少的计算来表达一系列面部表情。可以根据角色表情的需要扩展我们的BlendShape数据集，以细化角色可以产生的表情范围。&lt;/p&gt;
&lt;p&gt;与其他表示法相比，BS具有以下优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以通过使用ZBrush等软件雕刻混合形状目标来直接制定所需的面部形状。可以对形状产生直接的控制。其他方法提供的是对形状的间接控制。&lt;/li&gt;
&lt;li&gt;BlendShape采用一种语义参数化的控制方式。每一个权重对于动画师来说具有直观的意义，即各种面部表情的weight。&lt;/li&gt;
&lt;li&gt;混合动画迫使动画师的修改在各个BlendShape基所张成的空间中，即保持“在模型上”，即动画师不可能进行任意变形。当然，另一方面这代表着你不能通过使用BlendShape去制作形状数据集中没有的表情。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>一种使用多数地形的攀爬和翻越思路</title><link>https://www.tanedanobata.top/posts/climbing-and-vaulting/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/climbing-and-vaulting/</guid><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一种多地形适配的攀爬和翻越思路。
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;基本思路&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/%E4%B8%80%E7%A7%8D%E9%80%82%E7%94%A8%E5%A4%9A%E6%95%B0%E5%9C%B0%E5%BD%A2%E7%9A%84%E6%94%80%E7%88%AC%E5%92%8C%E7%BF%BB%E8%B6%8A%E6%80%9D%E8%B7%AF-%E5%9F%BA%E6%9C%AC%E6%80%9D%E8%B7%AF.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从膝盖发出射线检测墙体&lt;/li&gt;
&lt;li&gt;从上向下打射线检测出攀爬点&lt;/li&gt;
&lt;li&gt;向前从上到下再打一个射线&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;攀爬和翻越的区别&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/%E4%B8%80%E7%A7%8D%E9%80%82%E5%BA%94%E5%A4%9A%E6%95%B0%E5%9C%B0%E5%BD%A2%E7%9A%84%E6%94%80%E7%88%AC%E5%92%8C%E7%BF%BB%E8%B6%8A%E6%80%9D%E8%B7%AF-%E7%BF%BB%E8%B6%8A%E5%92%8C%E6%94%80%E7%88%AC%E7%9A%84%E5%8C%BA%E5%88%AB.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;攀爬的高度要求更高&lt;/p&gt;
&lt;h2&gt;存在的问题&lt;/h2&gt;
&lt;p&gt;无法检测窗框&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/%E7%A7%8D%E4%BD%BF%E7%94%A8%E5%A4%9A%E6%95%B0%E5%9C%B0%E5%BD%A2%E7%9A%84%E6%94%80%E7%88%AC%E5%92%8C%E7%BF%BB%E8%B6%8A%E6%80%9D%E8%B7%AF-%E6%97%A0%E6%B3%95%E6%A3%80%E6%B5%8B%E7%AA%97%E6%A1%86.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;陡峭楼梯会被识别为厚墙触发攀爬动作&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/%E7%A7%8D%E4%BD%BF%E7%94%A8%E5%A4%9A%E6%95%B0%E5%9C%B0%E5%BD%A2%E7%9A%84%E6%94%80%E7%88%AC%E5%92%8C%E7%BF%BB%E8%B6%8A%E6%80%9D%E8%B7%AF-%E9%99%A1%E5%B3%AD%E6%A5%BC%E6%A2%AF%E8%A2%AB%E8%AF%86%E5%88%AB%E4%B8%BA%E5%8E%9A%E5%A2%99.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有个洞，但太厚无法翻越&lt;/li&gt;
&lt;li&gt;即可翻越又可攀爬&lt;/li&gt;
&lt;li&gt;下方是破碎的&lt;/li&gt;
&lt;li&gt;悬空物体&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/%E4%B8%80%E7%A7%8D%E4%BD%BF%E7%94%A8%E5%A4%9A%E6%95%B0%E5%9C%B0%E5%BD%A2%E7%9A%84%E6%94%80%E7%88%AC%E5%92%8C%E7%BF%BB%E8%B6%8A%E6%80%9D%E8%B7%AF-%E6%9E%81%E7%AB%AF%E6%83%85%E5%86%B5%E7%9A%84%E8%AF%86%E5%88%AB.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;检测思路&lt;/h2&gt;
&lt;h3&gt;判断墙的厚度&lt;/h3&gt;
&lt;p&gt;碰撞检测能获得碰撞点的法向量，沿法向量反方向检测墙的厚度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;墙厚-&amp;gt;只能站上去，执行攀爬。&lt;/li&gt;
&lt;li&gt;墙薄-&amp;gt;玩家在移动，执行翻越。&lt;/li&gt;
&lt;li&gt;墙薄-&amp;gt;玩家未移动，执行攀爬。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Playable API构建动画系统</title><link>https://www.tanedanobata.top/posts/playable-animation-system/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/playable-animation-system/</guid><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Playable 尝试构建动画系统的记录。
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;Why&lt;/h2&gt;
&lt;p&gt;可控制动画加载策略，按需加载，异步加载。&lt;br /&gt;
灵活控制PlayableGraph数据流，可插入自定义AnimationJob。&lt;br /&gt;
加载自定义配置数据，和其他游戏系统配合。&lt;br /&gt;
更高自由度的Override。&lt;/p&gt;
&lt;h2&gt;动作系统机制&lt;/h2&gt;
&lt;p&gt;动态过度时间：&lt;br /&gt;
比较角色当前pose和目标pose，差异越大，过渡时间越长。&lt;/p&gt;
&lt;p&gt;按角色当前pose自动匹配目标动作：&lt;br /&gt;
如制作了多个跑步停止缓冲动作，不同脚部周期自动匹配到不同的动作&lt;/p&gt;
&lt;p&gt;动作后处理：&lt;br /&gt;
叠加受击动作（参数化表现受击方向、力度）&lt;br /&gt;
IK&lt;/p&gt;
&lt;h2&gt;运动系统&lt;/h2&gt;
&lt;p&gt;基础运动：&lt;br /&gt;
走跑，跳跃，蹲伏，滑铲&lt;br /&gt;
特殊运动：&lt;br /&gt;
翻越，攀爬，贴墙，&lt;/p&gt;
&lt;p&gt;触发方式：&lt;br /&gt;
离线标注-可控且精度高，但工作量大&lt;br /&gt;
物理检测-运行时更加灵活，但有性能开销&lt;/p&gt;
&lt;h3&gt;运行时环境检测&lt;/h3&gt;
&lt;p&gt;坡度检测，用于上下坡动作融合；&lt;/p&gt;
&lt;p&gt;自动跨越前方小障碍；&lt;/p&gt;
&lt;p&gt;贴墙；&lt;/p&gt;
&lt;p&gt;跳跃目标点连通性检测&lt;/p&gt;
&lt;h3&gt;优化方向&lt;/h3&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1QN411f7WJ/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=045b781ce97c08d03e566beb5f968ac4&quot;&gt;[Unity 活动]-Unity X 永劫无间「Unity大咖作客」线上分享会 — 动作动画专场【回放】_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>工具开发经验</title><link>https://www.tanedanobata.top/posts/tool-development/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/tool-development/</guid><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;工具开发的一些经验。
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;编辑和运行时数据分离&lt;/h2&gt;
&lt;p&gt;编辑状态数据：高可读，版本管理，协同编辑友好，可添加备注&lt;/p&gt;
&lt;p&gt;运行时数据：低冗余，高性能&lt;/p&gt;
&lt;h2&gt;编辑运行时重载&lt;/h2&gt;
&lt;p&gt;方便进行调试和编辑&lt;/p&gt;
&lt;h2&gt;推进工具比开发工具更加重要&lt;/h2&gt;
&lt;h2&gt;注重文档建设和规范性检查&lt;/h2&gt;
</content:encoded></item><item><title>Blend Shape in Unity</title><link>https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%A7%92%E8%89%B2%E6%BC%94%E5%87%BA/%E8%A1%A8%E6%83%85%E7%B3%BB%E7%BB%9F/Blend%20Shape%20in%20Unity/</link><guid isPermaLink="true">https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%A7%92%E8%89%B2%E6%BC%94%E5%87%BA/%E8%A1%A8%E6%83%85%E7%B3%BB%E7%BB%9F/Blend%20Shape%20in%20Unity/</guid><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Storage&lt;/h2&gt;
&lt;h3&gt;Storage of Imported Assets&lt;/h3&gt;
&lt;p&gt;BlendShape数据最初存储在导入的3D模型文件中(如FBX)&lt;/p&gt;
&lt;h3&gt;Storage in Runtime&lt;/h3&gt;
&lt;h4&gt;In Mesh&lt;/h4&gt;
&lt;p&gt;Mesh相关接口见&lt;a href=&quot;/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%A7%92%E8%89%B2%E6%BC%94%E5%87%BA/%E8%A1%A8%E6%83%85%E7%B3%BB%E7%BB%9F/%E5%85%B3%E4%BA%8EUnity%E4%B8%ADGetBlendShapeFrame%E5%8F%82%E6%95%B0%E7%9A%84%E8%A7%A3%E9%87%8A/&quot;&gt;关于Unity中GetBlendShapeFrame参数的解释&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在Mesh中，BlendShape的具体数据包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;顶点偏移量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;法线偏移量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;切线偏移量&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// Mesh类包含BlendShape数据
public sealed class Mesh : Object
{
    // 获取BlendShape数量
    public int blendShapeCount { get; }
    
    // 获取指定索引的BlendShape名称
    public string GetBlendShapeName(int shapeIndex);
    
    // 获取BlendShape的帧数
    public int GetBlendShapeFrameCount(int shapeIndex);
    
    // 重要：这个方法能获取原始BlendShape顶点位移数据
    public void GetBlendShapeFrameVertices(
        int shapeIndex, 
        int frameIndex, 
        Vector3[] deltaVertices, 
        Vector3[] deltaNormals, 
        Vector3[] deltaTangents
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在C++层中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 简化的C++伪代码示意内部结构
struct BlendShapeChannel
{
    std::string name;
    std::vector&amp;lt;BlendShapeFrame&amp;gt; frames;
};

struct BlendShapeFrame
{
    float weight;
    std::vector&amp;lt;Vector3&amp;gt; deltaVertices;  // 顶点位置增量
    std::vector&amp;lt;Vector3&amp;gt; deltaNormals;   // 法线增量
    std::vector&amp;lt;Vector3&amp;gt; deltaTangents;  // 切线增量
};

class Mesh
{
    // 其他网格数据...
    std::vector&amp;lt;BlendShapeChannel&amp;gt; blendShapes;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Storate In GPU&lt;/h3&gt;
&lt;p&gt;当BlendShape被应用时，数据会通过Mesh被上传到GPU顶点缓冲区：&lt;/p&gt;
&lt;h2&gt;Compute&lt;/h2&gt;
&lt;h3&gt;Compute In CPU&lt;/h3&gt;
&lt;p&gt;传统上，BlendShape的混合是在CPU端完成的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// CPU端BlendShape处理过程的伪代码
void ProcessBlendShapesOnCPU()
{
    // 获取基础网格数据
    Vector3[] baseVertices = mesh.vertices;
    Vector3[] result = new Vector3[baseVertices.Length];
    
    // 复制基础顶点数据
    System.Array.Copy(baseVertices, result, baseVertices.Length);
    
    // 对每个BlendShape进行处理
    for (int shapeIndex = 0; shapeIndex &amp;lt; mesh.blendShapeCount; shapeIndex++)
    {
        // 获取权重
        float weight = skinnedMeshRenderer.GetBlendShapeWeight(shapeIndex) / 100f;
        
        if (weight &amp;gt; 0)
        {
            // 获取此BlendShape的顶点偏移
            Vector3[] deltaVertices = new Vector3[baseVertices.Length];
            Vector3[] deltaNormals = new Vector3[baseVertices.Length];
            Vector3[] deltaTangents = new Vector3[baseVertices.Length];
            
            mesh.GetBlendShapeFrameVertices(shapeIndex, 0, deltaVertices, deltaNormals, deltaTangents);
            
            // 应用权重偏移
            for (int i = 0; i &amp;lt; baseVertices.Length; i++)
            {
                result[i] += deltaVertices[i] * weight;
            }
        }
    }
    
    // 最终更新网格并上传到GPU
    mesh.vertices = result;
    mesh.RecalculateBounds();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当在CPU上计算时，Unity使用SkinnedMeshRenderer组件进行处理，并将计算后的最终网格数据上传到GPU进行渲染。
实际上这些计算都是在C++层实现的，主要考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能考量&lt;/strong&gt;：处理大量顶点数据需要高效计算，C++比C# 更适合此类低级操作。C++实现允许使用SIMD、多线程等更低层次的优化&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;/ol&gt;
&lt;h3&gt;Compute In GPU&lt;/h3&gt;
&lt;p&gt;从Unity 2019版本开始，引入了&quot;GPU Skinning&quot;（GPU蒙皮）技术，允许BlendShape在GPU上直接计算。
GPU蒙皮将BlendShape计算移至顶点着色器阶段，大致实现方式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 顶点着色器中的伪代码（简化版）
void vert(inout appdata_full v) {
    // 基础顶点位置
    float3 basePosition = v.vertex.xyz;
    float3 finalPosition = basePosition;
    
    // 应用每个BlendShape的偏移
    // 这些值和权重由Unity自动传递到GPU
    finalPosition += _BlendShape0 * _Weight0;
    finalPosition += _BlendShape1 * _Weight1;
    // ... 更多BlendShape
    
    v.vertex.xyz = finalPosition;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>BlendShape</title><link>https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%A7%92%E8%89%B2%E6%BC%94%E5%87%BA/%E8%A1%A8%E6%83%85%E7%B3%BB%E7%BB%9F/BlendShape/</link><guid isPermaLink="true">https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%A7%92%E8%89%B2%E6%BC%94%E5%87%BA/%E8%A1%A8%E6%83%85%E7%B3%BB%E7%BB%9F/BlendShape/</guid><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Blend Shape&lt;/h2&gt;
&lt;p&gt;blendshape的核心原理是存储多个预定义的网格变形（称为morph target或blend key），这些变形通常基于一个中性姿态（neutral pose）创建。&lt;/p&gt;
&lt;p&gt;每个morph target代表一种特定的表情或姿势，例如“微笑”、“闭眼”或“扬眉”。&lt;/p&gt;
&lt;p&gt;在动画过程中，顶点的属性（如位置、法线、纹理坐标）会在顶点着色器中通过线性插值（linear interpolation）进行混合，生成最终的动画效果。&lt;/p&gt;
&lt;p&gt;数学上，blendshape可以表示为每个顶点的差值：​，然后通过权重组合计算最终位置。例如，对于面部动画，可以创建多个morph target（如“微笑”权重为0.5，“闭眼”权重为0.3），中性姿态的权重为1减去所有target权重的和，确保总和在0到1之间。&lt;/p&gt;
&lt;p&gt;顶点动画则更广义，涵盖任何直接操作顶点属性的动画技术。blendshape是顶点动画的一种具体实现，特别适合需要高精度控制的场景，如面部表情、布料或皮肤的变形。相比之下，骨骼动画通过骨骼层次结构驱动顶点变形，适合更通用的角色动画。&lt;/p&gt;
&lt;h2&gt;Tech&lt;/h2&gt;
&lt;p&gt;现代blendshape技术在GPU计算方面的优化已经非常成熟。DirectX 10引入了Stream-Out和Buffer-Template等特性，显著提升了blendshape的性能。例如，Stream-Out方法通过迭代处理将blendshape数据流式输出到缓冲区，适合处理多个blendshape的场景（如7次迭代处理54个blendshape中的50%以上）。Buffer-Template方法则允许在顶点着色器中直接访问blendshape数据，通过shader resource view实现单次绘制调用（draw call），性能更高。例如，在处理50个blendshape时，Buffer-Template方法比Stream-Out方法快2.4倍（见NVIDIA Developer的Figure 3-6）。&lt;br /&gt;
Shader Model 5以上才能支持GPU加速。&lt;/p&gt;
&lt;p&gt;另一个前沿技术是Vertex Animation Texture（VAT），它将动画数据烘焙（bake）到纹理中，在顶点着色器中通过采样纹理来驱动动画。这种方法特别适合处理大量简单动画对象，如游戏中的观众（例如Tennis Clash中的观众动画）。VAT将SkinnedMeshRenderer替换为MeshRenderer，保持网格在CPU上静态，减少CPU负载。在Samsung S6上测试，超过2000个实例（每个800顶点）无帧率下降。但VAT在动画混合方面存在局限性，可能会导致过渡时的“弹跳”效果（popping），通常通过确保初始和最终帧一致来缓解，如Tennis Clash的观众动画（闲置和鼓掌动画通过个体起始时间偏移实现有机效果）。VAT使用RGBAHalf（64位/像素）格式，但为支持更多设备，推荐使用RGB24（24位/像素），受限于OpenGLES 2.0的2048x2048纹理限制，适合顶点数≤2048的网格。&lt;/p&gt;
&lt;h2&gt;VS Skeletal Skinning Animation&lt;/h2&gt;
&lt;p&gt;blendshape和骨骼蒙皮动画各有优势和劣势，具体选择取决于动画需求。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;BlendShape&lt;/th&gt;
&lt;th&gt;骨骼蒙皮动画&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;面部表情、布料、皮肤等有机变形&lt;/td&gt;
&lt;td&gt;通用角色动画&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;控制精度&lt;/td&gt;
&lt;td&gt;高，允许直接定义每个顶点&lt;/td&gt;
&lt;td&gt;低，依赖骨骼变换进行间接控制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;动画混合&lt;/td&gt;
&lt;td&gt;线性插值，可能出现“弹跳”，混合效果有限&lt;/td&gt;
&lt;td&gt;支持复杂的状态过渡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存使用&lt;/td&gt;
&lt;td&gt;高，每个BS需要存储完整的顶点数据&lt;/td&gt;
&lt;td&gt;低，仅存储骨骼变换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;美术开发难度&lt;/td&gt;
&lt;td&gt;高，美术工作量大&lt;/td&gt;
&lt;td&gt;低，骨骼动画开发快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;GPU进行优化后高效&lt;/td&gt;
&lt;td&gt;复杂骨骼可能增加CPU负载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;技术局限&lt;/td&gt;
&lt;td&gt;线性插值不适合旋转运动，可能需多target模拟&lt;/td&gt;
&lt;td&gt;对精细变形（如面部细节）控制不足&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>关于Unity中GetBlendShapeFrame参数的解释</title><link>https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%A7%92%E8%89%B2%E6%BC%94%E5%87%BA/%E8%A1%A8%E6%83%85%E7%B3%BB%E7%BB%9F/%E5%85%B3%E4%BA%8EUnity%E4%B8%ADGetBlendShapeFrame%E5%8F%82%E6%95%B0%E7%9A%84%E8%A7%A3%E9%87%8A/</link><guid isPermaLink="true">https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%A7%92%E8%89%B2%E6%BC%94%E5%87%BA/%E8%A1%A8%E6%83%85%E7%B3%BB%E7%BB%9F/%E5%85%B3%E4%BA%8EUnity%E4%B8%ADGetBlendShapeFrame%E5%8F%82%E6%95%B0%E7%9A%84%E8%A7%A3%E9%87%8A/</guid><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Blend Shape Funcs in Unity&lt;/h2&gt;
&lt;p&gt;BlendShape在Unity中有以下的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GetBlendShapeFrameWeight
GetBlendShapeFrameCount
GetBlendShapeFrameVertices
GetBlendShapeIndex
GetBlendShapeName
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这几个函数涉及到两个参数 &lt;code&gt;shapeIndex&lt;/code&gt; 和 &lt;code&gt;frameIndex&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;shapeIndex&lt;/code&gt;&lt;br /&gt;
BlendShape按照导入时的顺序进行索引，可以通过 &lt;code&gt;GetBlendShapeName(index)&lt;/code&gt; 获取指定索引对应的BlendShape名称&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;frameIndex&lt;/code&gt;&lt;br /&gt;
指定BlendShape中的关键帧索引，大多数情况下每个BlendShape只有一个关键帧&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>策略模式</title><link>https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F/</link><guid isPermaLink="true">https://www.tanedanobata.top/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E6%8A%80%E6%9C%AF/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F/</guid><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Strategy Pattern&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;策略模式&lt;/strong&gt;是一种行为设计模式， 它能让你定义一系列算法， 并将每种算法分别放入独立的类中， 以使算法的对象能够相互替换。&lt;/p&gt;
&lt;h2&gt;Structure&lt;/h2&gt;
&lt;p&gt;策略模式建议找出负责用许多不同方式完成特定任务的类，然后将其中的算法抽取到一组被称为 &lt;em&gt;策略&lt;/em&gt; 的独立类中。&lt;/p&gt;
&lt;p&gt;名为 &lt;em&gt;上下文(Context)&lt;/em&gt; 的原始类必须包含一个成员变量来存储对于每种策略的引用。上下文并不执行任务，而是将工作委派给已连接的策略对象。&lt;/p&gt;
&lt;p&gt;上下文不负责选择符合任务需要的算法——客户端会将所需策略传递给上下文。 实际上， 上下文并不十分了解策略， 它会通过同样的通用接口与所有策略进行交互， 而该接口只需暴露一个方法来触发所选策略中封装的算法即可。&lt;/p&gt;
&lt;p&gt;因此， 上下文可独立于具体策略。 这样就可在不修改上下文代码或其他策略的情况下添加新算法或修改已有算法了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250512142916.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Pros and Cons&lt;/h2&gt;
&lt;h2&gt;Relationship with Other Patterns&lt;/h2&gt;
&lt;h2&gt;Code Example&lt;/h2&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://refactoringguru.cn/design-patterns/strategy&quot;&gt;策略设计模式&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>C++中调用蓝图函数</title><link>https://www.tanedanobata.top/UE/Core/%E5%8F%8D%E5%B0%84/C%2B%2B%E4%B8%AD%E8%B0%83%E7%94%A8%E8%93%9D%E5%9B%BE%E5%87%BD%E6%95%B0/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/Core/%E5%8F%8D%E5%B0%84/C%2B%2B%E4%B8%AD%E8%B0%83%E7%94%A8%E8%93%9D%E5%9B%BE%E5%87%BD%E6%95%B0/</guid><pubDate>Fri, 09 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;BlueprintImplementableEvent and BlueprintNativeEvent&lt;/h2&gt;
&lt;p&gt;这是在类定义时预先建立C++和蓝图间接口的方法，也是最常用的方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在头文件中声明函数
UCLASS()
class MYGAME_API AMyActor : public AActor
{
    GENERATED_BODY()
public:
    // 纯蓝图实现事件 - 没有C++定义
    UFUNCTION(BlueprintImplementableEvent, Category=&quot;MyGame&quot;)
    void OnBlueprintEvent(int32 Value, const FString&amp;amp; Message);
    
    // 带默认C++实现的蓝图事件
    UFUNCTION(BlueprintNativeEvent, Category=&quot;MyGame&quot;)
    float CalculateValue(float BaseValue);
    
    // 调用函数示例
    void CallBlueprintFunctions();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Interface&lt;/h2&gt;
&lt;p&gt;使用接口是一种更灵活的方法，特别适用于不同类型对象间的通信。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义接口
UINTERFACE(BlueprintType)
class MYGAME_API UMyInterface : public UInterface
{
    GENERATED_BODY()
};

class MYGAME_API IMyInterface
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, BlueprintImplementableEvent)
    void OnInteract(AActor* Instigator);
    
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
    bool CanInteract() const;
};

// 实现接口的默认C++方法
bool IMyInterface::CanInteract_Implementation() const
{
    return true;
}

// 在C++中调用接口方法
void AMyActor::InteractWithObject(AActor* TargetActor)
{
    if (TargetActor-&amp;gt;GetClass()-&amp;gt;ImplementsInterface(UMyInterface::StaticClass()))
    {
        // 检查是否可以交互
        if (IMyInterface::Execute_CanInteract(TargetActor))
        {
            // 调用蓝图实现的方法
            IMyInterface::Execute_OnInteract(TargetActor, this);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Delegate&lt;/h2&gt;
&lt;p&gt;使用动态委托将蓝图函数绑定到C++定义的事件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UCLASS()
class MYGAME_API AMyActor : public AActor
{
    GENERATED_BODY()
public:
    // 声明动态多播委托
    UPROPERTY(BlueprintAssignable, Category=&quot;Events&quot;)
    FOnCustomEventSignature OnCustomEvent;
    
    // 声明动态委托
    UPROPERTY(BlueprintAssignable, Category=&quot;Events&quot;)
    FOnCalculateValueSignature OnCalculateValue;
    
    // 触发事件
    void TriggerEvents();
};

// 在CPP中实现
void AMyActor::TriggerEvents()
{
    // 广播多播委托
    OnCustomEvent.Broadcast(42, TEXT(&quot;Event triggered&quot;));
    
    // 执行委托并获取结果
    if (OnCalculateValue.IsBound())
    {
        float Result = OnCalculateValue.Execute(100.0f);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Reflection&lt;/h2&gt;
&lt;p&gt;核心是 &lt;code&gt;FindFunction&lt;/code&gt; 拿到函数指针，然后通过 &lt;code&gt;ProcessEvent&lt;/code&gt; 接口调用相关函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取蓝图类实例
AActor* BlueprintActor = /* 获取BP实例的引用 */;

// 方法1: 使用ProcessEvent直接调用
FName FunctionName = &quot;BlueprintFunction&quot;;
BlueprintActor-&amp;gt;ProcessEvent(BlueprintActor-&amp;gt;FindFunction(FunctionName), &amp;amp;Params);

// 方法2: 使用ExecuteUbergraph (仅限事件图表入口点)
BlueprintActor-&amp;gt;ExecuteUbergraph(/* 图表入口点索引 */);

// 方法3: 推荐方式 - 使用InvokeFunction模板函数
FOutputParams Result;
UFunction* Function = BlueprintActor-&amp;gt;FindFunction(TEXT(&quot;MyBlueprintFunction&quot;));
if (Function)
{
    struct
    {
        float InParam1;
        FString InParam2;
        int32 ReturnValue;
    } Params;
    
    Params.InParam1 = 10.0f;
    Params.InParam2 = TEXT(&quot;Hello&quot;);
    
    BlueprintActor-&amp;gt;ProcessEvent(Function, &amp;amp;Params);
    int32 ReturnValue = Params.ReturnValue;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>图形学基础</title><link>https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%9F%BA%E7%A1%80/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%9F%BA%E7%A1%80/</guid><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;渲染管线&lt;/h2&gt;
&lt;h2&gt;各种缓冲和测试的含义&lt;/h2&gt;
&lt;p&gt;执行顺序 alpha test -&amp;gt; stencil test -&amp;gt; depth test&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alpha测试&lt;/strong&gt;：根据物体的透明度来决定是否渲染&lt;br /&gt;
像素值一般是由RGBA四个分量来表示的，其中的A是alpha，表示的是物体的不透明度。1代表完全不透明，0代表完全透明。可选的 alpha 测试可在深度测试执行前在传入片段上运行。片段的 alpha 值与参考值作某些特定的测试（如等于，大于等），如果片段未能通过测试，它将不再进行进一步的处理。 alpha 测试经常用于不影响深度缓存的全透明片段的处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;深度测试&lt;/strong&gt;：根据物体的深度决定是否渲染&lt;br /&gt;
图形管线会先对每一个位置的像素存储一个深度值，称为深度缓冲，代表了该像素点在3D世界中离相机最近物体的深度值。于是在计算每一个物体的像素值的时候，都会将它的深度值和缓冲器当中的深度值进行比较，如果这个深度值小于缓冲器中的深度值，就更新深度缓冲和颜色缓冲的值，否则就丢弃。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模板测试&lt;/strong&gt;：根据物体的位置范围决定是否渲染。&lt;br /&gt;
模板测试就是用片段指定的参考值与模板缓冲中的模板值进行比较，如果达到预设的比较结果，模板测试就通过了，然后用这个参考值更新模板缓冲中的模板值；如果没有达到预设的比较结果，就是没有通过测试，就不更新模板缓冲。&lt;/p&gt;
&lt;h2&gt;深度测试在哪个阶段，Early-Z呢，又会存在什么问题？&lt;/h2&gt;
&lt;p&gt;深度测试位于像素处理阶段的测试合并阶段，Early-Z是一种提前深度测试的技术，它位于光栅化阶段之后，像素处理阶段之前，目的是减少进入像素着色阶段的片段，优化性能。&lt;/p&gt;
&lt;p&gt;Early-Z会带来透明测试的冲突，例如某个片元A虽然遮挡了另一个片元B，但A却是透明的，GPU应当渲染的是片元B，这就产生了矛盾，这就是透明度测试会导致性能下降的原因(因为无法用Early-Z)&lt;/p&gt;
&lt;h2&gt;局部光照模型&lt;/h2&gt;
&lt;p&gt;由于环境光是常数&lt;/p&gt;
&lt;p&gt;经验化的简单光照模型。光栅化算法一次只考虑一个像素的光照强度，因此局部光照模型不能计算某像素受其他像素影响的光照强度部分。也就是说，局部光照模型只对物体进行直接光照的计算，而不考虑其他的间接影响。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Lambert漫反射模型&lt;/em&gt;，&lt;em&gt;phong光照明模型&lt;/em&gt;，&lt;em&gt;BlinnPhong光照模型&lt;/em&gt;&lt;br /&gt;
&lt;strong&gt;&lt;a href=&quot;https://zhida.zhihu.com/search?content_id=120283171&amp;amp;content_type=Article&amp;amp;match_order=1&amp;amp;q=Lambert%E5%85%89%E7%85%A7%E6%A8%A1%E5%9E%8B&amp;amp;zhida_source=entity&quot;&gt;Lambert光照模型&lt;/a&gt;主要是用来模拟粗糙物体表面的光照现象&lt;/strong&gt; ：&lt;strong&gt;反射强度与光线的入射角度有关系&lt;/strong&gt;，&lt;strong&gt;DiffuseLight=I*cosθ&lt;/strong&gt;，其中cosθ=dot(N,L)(L为入射光线的反向量，N为当前表面的法向量方向)&lt;/p&gt;
&lt;p&gt;Phone模型，镜面反射，反射光强度和反射光线与视线夹角有关。&lt;br /&gt;
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250509103943.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
其中 $R$ 向量可以计算公式为:
$$
R=2*(L \cdot N)*N - L
$$&lt;/p&gt;
&lt;p&gt;则整体公式为&lt;/p&gt;
&lt;p&gt;$$
Spec = (LightColor*SpecularColor)(V \cdot R)^{Glossiness}
$$&lt;/p&gt;
&lt;p&gt;BlinPhone基本和Phone一致，只是将反射向量 $R$ 换成半程向量 $H$
其中
$$
H = (V+L)/||V+H||
$$&lt;/p&gt;
&lt;p&gt;全局光照，像素会受到其他像素影响，光线追踪。&lt;/p&gt;
&lt;h2&gt;着色&lt;/h2&gt;
&lt;p&gt;Gouraud Shading 计算顶点光照，然后通过插值计算像素的着色
Phone Shading 每一个图元逐像素的法线进行光照计算，计算量较大，但是效果最好&lt;/p&gt;
&lt;h2&gt;AO&lt;/h2&gt;
&lt;p&gt;粗略的全局光照算法。用于计算没一点是如何接受环境光的。&lt;/p&gt;
&lt;p&gt;SSAO：对于屏幕空间(Screen-filled Quad)上的每一个片段，我们都会根据周边深度值计算一个遮蔽因子(Occlusion Factor)。这个遮蔽因子之后会被用来减少或者抵消片段的环境光照分量。遮蔽因子是通过采集片段周围球体/半球体核心(Kernel)的多个深度样本，并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。&lt;/p&gt;
&lt;h2&gt;抗锯齿&lt;/h2&gt;
&lt;p&gt;光栅化的时候，是以像素中心点是否被三角形覆盖来决定是否生成片段，因此有些片段覆盖了采样点就生成，有些没有覆盖就不生成，最终导致了锯齿现象。&lt;/p&gt;
&lt;h2&gt;MVP&lt;/h2&gt;
&lt;p&gt;模型矩阵M(Model)：将局部坐标变换到世界坐标；&lt;/p&gt;
&lt;p&gt;观察矩阵V(View)：将世界坐标转换为观察坐标，或者说，将物体的世界坐标，转换为在相机视角下的坐标；&lt;/p&gt;
&lt;p&gt;投影矩阵P(Projection)：将顶点坐标从观察空间变换到裁剪空间(clip space) ，后续的透视除法操作会将裁剪空间的坐标转换为标准化设备坐标系中（NDC）。
一旦顶点位于相机空间中，最终可以通过应用投影变换将其变换到裁剪空间。投影矩阵通过定义相机视图的范围来编码场景中有多少被捕获在渲染中。两种最常见的投影类型是透视投影和正交投影。&lt;/p&gt;
&lt;h2&gt;欧拉角、矩阵和四元数表示旋转的区别和优缺点&lt;/h2&gt;
&lt;p&gt;（1）欧拉角：定义了绕着三个坐标轴的旋转角，来确定刚体的旋转位置的方式，包括俯仰角pitch，偏航角yaw和滚动角roll；它的优点是比较直观，而且单个维度上的角度也比较容易插值；缺点是它不能进行任意方向的插值，而且会导致万向节死锁的问题，旋转的次序对结果也有影响。&lt;br /&gt;
万向锁的原因是内轴无法带动外轴旋转。&lt;br /&gt;
&lt;a href=&quot;https://www.bilibili.com/video/BV1Nr4y1j7kn/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=045b781ce97c08d03e566beb5f968ac4&quot;&gt;无伤理解欧拉角中的“万向死锁”现象_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;（2）矩阵：优点是不受万向节死锁的影响，可以独一无二的表达任意旋转，并且可以通过矩阵乘法来对点或矢量进行旋转变换；现在多数CPU以及所有GPU都有内置的硬件加速点积和矩阵乘法；缺点是不太直观，而且需要比较大的存储空间，也不太容易进行插值计算。&lt;/p&gt;
&lt;p&gt;（3）四元数：四元数的好处是能够串接旋转；能把旋转直接作用于点或者矢量；而且能够进行旋转插值；另外它所占用的存储空间也比矩阵小；四元数可以解决万向节死锁的问题。&lt;/p&gt;
&lt;h2&gt;判断点在凸多边体内&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;射线法，延点和任意多边体顶点方向做射线，偶数则在多边体外，奇数则在多边体内&lt;/li&gt;
&lt;li&gt;平面向量，对于没一个平面，沿法线向量和点与平面任一点的向量做内积，若均大于0则在多面体内部&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;法线贴图和切线空间&lt;/h2&gt;
&lt;p&gt;切线空间：顶点原点是顶点位置，Z轴是切线，一般指定纹理方向作为XY(且与Z轴垂直)
法线贴图的rgb为什么呈现出蓝色 &lt;strong&gt;(0.5, 0.5, 1)&lt;/strong&gt; ，因为法线某一个轴的范围在 （-1, 1）之间，需要和rgb的 （0，1）做映射。那么默认的法线是（0，0，1），映射公式为 $(tangent+1)/2$，这时候结果就是(0.5，0.5，1)。&lt;/p&gt;
&lt;p&gt;主要是光照计算一般放在切线空间中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;与模型无关，可以重用和变形(发生形变时，法线可以自适应变换)&lt;/li&gt;
&lt;li&gt;切线空间法线Z分量始终为正，这样，就可以从XY重建，节省通道&lt;/li&gt;
&lt;li&gt;切线空间允许容易地混合多个法线贴图：
基础法线贴图(如皮肤的主要特征)
细节法线贴图(如毛孔、皱纹等微观细节)
程序化生成的法线(如动态伤痕、积水效果)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Lerp&lt;/h2&gt;
&lt;p&gt;Lerp是圆上弦的等分，而Slerp是弧的等分&lt;/p&gt;
&lt;p&gt;Linear interpolation : 线性插值&lt;/p&gt;
&lt;p&gt;Normalized Linear interpolation : 归一化线性插值（唯一做的事情就是线性插值之后进行归一化）
由于是按照弦长的比例来插值的，即在弦上是匀速的，故在角速度或弧上并不匀速。
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250509115300.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Spherical Linear interpolation : 球面线性插值，对四元数做这个操作能保证角速度均匀
$$
SLerp(v_{0},v_{1},t)=\frac{\sin(1-t)\theta}{\sin \theta}v_{0}+\frac{\sin t\theta}{\sin \theta}v_{1}
$$
&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250509115328.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Alpha混合&lt;/h2&gt;
&lt;p&gt;Blend Src Dst
Blend SrcAlpha OneMinusSrcAlpha
其中Src为片元颜色，Dst为缓存颜色&lt;/p&gt;
&lt;p&gt;Blend DstColor Zero Multiply 正片叠底
Blend DstColor SrcColor 两倍相乘&lt;/p&gt;
&lt;p&gt;BlendOp Min
Blend One One 变暗&lt;/p&gt;
&lt;p&gt;BlendOp Max
Blend One One 变亮&lt;/p&gt;
&lt;p&gt;透明物体渲染需要关闭深度写入，否则会导致后面的物体无法正常渲染&lt;/p&gt;
&lt;h2&gt;延迟渲染&lt;/h2&gt;
&lt;p&gt;先进行深度测试，再进行着色渲染。且将光照计算从三维空间放到二维空间。
GBuffer-存储像素位置，法线，漫反射信息，方便直接在像空间进行逐像素光照处理。&lt;/p&gt;
&lt;p&gt;1、几何处理阶段(Geometry Pass)。这个阶段中，我们获取对象的各种几何信息，并将第二步所需的各种数据储存（也就是渲染）到多个G-buffer中；&lt;/p&gt;
&lt;p&gt;2、光照处理阶段(Lighting Pass)。在这个pass中，我们只需渲染出一个屏幕大小的二维矩形，使用第一步在G-buffer中存储的数据对此矩阵的每一个片段计算场景的光照；光照计算的过程还是和正向渲染以前一样，只是现在我们需要从对应的G-buffer而不是顶点着色器(和一些uniform变量)那里获取输入变量了。&lt;/p&gt;
&lt;p&gt;优点：复杂度仅O(m+n)，而前向渲染为O(m*n)
缺点：透明渲染存在问题&lt;/p&gt;
&lt;h2&gt;顺序无关渲染&lt;/h2&gt;
&lt;p&gt;深度剥离算法N个Pass&lt;/p&gt;
</content:encoded></item><item><title>AABB交叉检测</title><link>https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%A9%BA%E9%97%B4%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/AABB%E4%BA%A4%E5%8F%89%E6%A3%80%E6%B5%8B/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%A9%BA%E9%97%B4%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/AABB%E4%BA%A4%E5%8F%89%E6%A3%80%E6%B5%8B/</guid><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;定义需要检测的矩阵 $A$ 和 $B$。&lt;/p&gt;
&lt;p&gt;每个矩阵分别用左上角坐标和右下角坐标进行描述，这样，两个矩阵分别可以表示为 $(ax_{1},ay_{1}),(ax_{2},ay_{2})$ 和 $(bx_{1},by_{1}),(bx_{2},by_{2})$。&lt;/p&gt;
&lt;p&gt;检测是否有重叠的代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isOverlap = ax1 &amp;lt;= bx2 &amp;amp;&amp;amp; ax2 &amp;gt;= bx1 &amp;amp;&amp;amp; ay1 &amp;lt;= by2 &amp;amp;&amp;amp; ay2 &amp;gt;= by1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码有四个条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;$A$ 的上边界在 $B$ 的下边界上面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$A$ 的下边界在 $B$ 的上边界下面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$A$ 的左边界在 $B$ 的右边界左边。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$A$ 的右边界在 $B$ 的左边界右边。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>判断目标点是否在凸多边形区域</title><link>https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%A9%BA%E9%97%B4%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%88%A4%E6%96%AD%E7%9B%AE%E6%A0%87%E7%82%B9%E6%98%AF%E5%90%A6%E5%9C%A8%E5%87%B8%E5%A4%9A%E8%BE%B9%E5%BD%A2%E5%8C%BA%E5%9F%9F/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%A9%BA%E9%97%B4%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%88%A4%E6%96%AD%E7%9B%AE%E6%A0%87%E7%82%B9%E6%98%AF%E5%90%A6%E5%9C%A8%E5%87%B8%E5%A4%9A%E8%BE%B9%E5%BD%A2%E5%8C%BA%E5%9F%9F/</guid><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/aizawaayame/blogimage@main/img/20250508200038.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目标点 $p$ 对凸多边形每个顶点之间建立一个向量 $vec$（如：$\vec{v}&lt;em&gt;{p\to v&lt;/em&gt;{1}}$），该向量与其对应的顶点的边 $edge$（如：$v_{2}-v_{1}$）进行叉乘，得到一个叉积值。&lt;/p&gt;
&lt;p&gt;若每个叉积值的符号都一样（都是正数/都是负数），则证明点在凸多边形内。&lt;/p&gt;
&lt;p&gt;否则，则证明点不再凸多边形内。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
bool Chunk::inChunk(Vector2 p)
{   int size = vertexs.size(); 
    for(int i = 0; i &amp;lt; size; ++i)
    { 
        //假设凸多边形的边edge都是逆时针方向 
        Vector2 edge = vertex[(i+1)%size]-vertex[i]; 
        Vector2 vec = vertex[i] - p; 
        int result = cross(edge,vec); 
        //若点在凸多边形内，得到的叉积值应都是正数 
        if(sign(result) == 0)
            return false; 
    } 
    return true; 
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>四叉树</title><link>https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%A9%BA%E9%97%B4%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%9B%9B%E5%8F%89%E6%A0%91/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%A9%BA%E9%97%B4%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%9B%9B%E5%8F%89%E6%A0%91/</guid><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;四叉树&lt;/strong&gt; ：&lt;/p&gt;
&lt;p&gt;四叉树索引的基本思想是将地理空间递归划分为不同层次的树结构。&lt;/p&gt;
&lt;p&gt;它将已知范围的空间等分成四个相等的子空间，如此递归下去，直至树的层次达到一定深度或者满足某种要求后停止分割。&lt;/p&gt;
&lt;h2&gt;Splite&lt;/h2&gt;
&lt;p&gt;为了灵活性，设计一个 &lt;code&gt;ssf&lt;/code&gt; 分裂终止函数。当满足这个函数时，分裂终止；同时，这个是触发合并的对偶条件，只有当满足这个条件时，才会尝试合并。&lt;br /&gt;
对于一个 &lt;code&gt;ssf&lt;/code&gt; 函数，可以这样设计：&lt;code&gt;叶子结点&amp;lt;=4 || 节点面积&amp;lt;=最小的元素面积&lt;/code&gt;，也就是说只有叶子节点&amp;gt;4且节点面积&amp;gt;4才能触发分裂。&lt;/p&gt;
&lt;p&gt;分裂需要满足下面的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该节点是叶子节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!ssf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行分裂时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;element&lt;/code&gt; 可以以中心坐标为准，放入四分的新的叶子节点。&lt;/li&gt;
&lt;li&gt;对所有新的叶子节点再次尝试分裂。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Merge&lt;/h2&gt;
&lt;p&gt;合并需要满足下面的条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不是根节点。&lt;/li&gt;
&lt;li&gt;自身是叶子节点，且其兄弟节点均为叶子节点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssf&lt;/code&gt; 且父节点满足 &lt;code&gt;ssf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行分裂时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;自身及其兄弟节点的所有 &lt;code&gt;element&lt;/code&gt; 上挪到父节点。&lt;/li&gt;
&lt;li&gt;对父节点再次尝试合并。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When to Splite or Merge&lt;/h2&gt;
&lt;p&gt;并非只有添加对象才会引起分裂，同样并非只有移除对象才会引起合并。&lt;/p&gt;
&lt;p&gt;分裂和合并的触发，取决于终止分裂条件 &lt;code&gt;ssf()&lt;/code&gt; 的设计。&lt;/p&gt;
&lt;p&gt;举一个例子，我们希望叶子节点在下面的任一情况成立时终止分裂：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点内没有任何管理的对象。&lt;/li&gt;
&lt;li&gt;节点内每一个方格都是对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种条件十分适合地图中的固定障碍物的管理（地形、建筑等），四叉树可以把完全空旷的、和完全都是障碍物的地方不重叠地放到不同的叶子中去。&lt;/p&gt;
&lt;p&gt;这样可能发生：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当添加一个对象时，有可能一个大的矩形正好放满了，引起合并。&lt;/li&gt;
&lt;li&gt;当移除一个对象时，有可能一个满的大矩形变的不满了，引起分裂。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种情况下， &lt;strong&gt;不论是添加还是移除对象，分裂和合并都有可能发生&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但是，用户的条件是稳定的，也就是说保证 &lt;strong&gt;分裂和合并的条件不会同时成立，一定是时刻相反的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以在实现中，每次添加或移除对象时，都回去尝试分裂或合并，但 &lt;strong&gt;二者只发生一个&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 添加对象时, 先尝试分裂，否则尝试合并
trySplitDown(node) || tryMergeUp(node);
// 移除对象时，先尝试合并，否则尝试分裂
tryMergeUp(node) || trySplitDown(node);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Query Optimize&lt;/h2&gt;
&lt;p&gt;对于一个宽 $w$，高 $h$ 的带划分平面。可以将其进行 $d$ 次二分，这样对于点 $(x,y)$，一定在第 $(2^dx/w,2^dy/h)$ 个小区间内。&lt;/p&gt;
&lt;p&gt;也就是说，对于 $d$ 的节点而言，其覆盖区域内的任意一个位置 $(x,y)$ 都有：&lt;/p&gt;
&lt;p&gt;位置 $(x,y)$ 一定位于 $x$ 轴上第 $2^dx/w$ 个、$y$ 轴上第 $2^dy/h$ 个小矩形内。&lt;/p&gt;
&lt;p&gt;其中 $d$ 从 $0$ 开始计算的，计数也是从 $0$ 开始的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;构造哈希函数&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;可以构造一个参数为 $(d,x,y)$ 的哈希函数，这个函数输出为对于特定深度 $d$ 的某一区域内的 $(x,y)$，具有相同的ID。&lt;/p&gt;
&lt;p&gt;这样，每个特定深度 $d$ 的区域可以对应四叉树的某一可能的叶子节点，都具有了唯一的ID。&lt;/p&gt;
&lt;p&gt;接下来只需要建立从 $(d,x,y)$ 到该叶子结点指针的映射即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint64_t pack(
    uint64_t d, uint64_t x, uint64_t y, uint64_t w, uint64_t h) {
  // 0xfc00000000000000 : 高6位全是 1, 其余是0
  // 0x3ffffffe0000000  : 高6位全是 0, 接下来29位全是1，其余全是0
  // 0x1fffffff         : 低29位全是 1，其余全是0
  return ((d &amp;lt;&amp;lt; 58) &amp;amp; 0xfc00000000000000ULL) |
         ((((1 &amp;lt;&amp;lt; d) * x / h) &amp;lt;&amp;lt; 29) &amp;amp; 0x3ffffffe0000000ULL) |
         (((1 &amp;lt;&amp;lt; d) * y / w) &amp;amp; 0x1fffffffULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;查找位置为 $(x,y)$ 的叶子节点&lt;/strong&gt;，关键就在于 &lt;strong&gt;确定深度 $d$&lt;/strong&gt;。因为深度十分有限，遍历的开销十分低。但也可以通过二分进一步优化。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;提前维护整棵树的高度 $maxd$，这样初始可行域为 $[0,maxd]$，设定 $l=0,r=maxd$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二分猜 $m=(l+r)&amp;gt;&amp;gt;1$，用 &lt;code&gt;pack&lt;/code&gt; 函数查找计算出的ID，如果查找不到任何节点，说明太大了，需要收缩上界 $r=m-1$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果哈希表中存在，那就看是不是叶子节点。如果是叶子节点，则已经命中。如果不是，说明是一个中间的非叶子节点，需要收缩下界 $l=m+1$。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;$maxd$ 的维护&lt;/strong&gt;：&lt;br /&gt;
由于支持动态合并，维护 $maxd$ 时，需要同时维护 $maxd$ 的数量，只有当最后一个 $maxd$ 被销毁时，才减少 $maxd$。&lt;/p&gt;
&lt;h2&gt;Region Query&lt;/h2&gt;
&lt;p&gt;思路很简单，从根节点开始，不断 &lt;strong&gt;判断重叠&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果是叶子节点，则直接收集位于查询区域范围内的对象就可以了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;若果是非叶子节点，则继续判断子节点，递归下去。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Region Query Optimize&lt;/h2&gt;
&lt;p&gt;区域查询时，有了 &lt;a href=&quot;/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E7%A9%BA%E9%97%B4%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%9B%9B%E5%8F%89%E6%A0%91/#query-optimize&quot;&gt;Query Optimize&lt;/a&gt; 机制，就可以不从根节点开始查询。&lt;/p&gt;
&lt;p&gt;这时候其实是要找到包含所有区域的最小公共节点。也就是 &lt;strong&gt;同时包含查询区域左上角和右下角两个点的最小的节点&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;假设查询区域左上角是 $a$ 点，右上角是 $b$ 点，同时包含这两个点的最小节点是 $Q$，那么：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于 $Q$，及其祖先节点，一定同时包含 $a$ 和 $b$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于 $Q$ 的所有子孙节点，必然不会同时包含 $a$ 和 $b$。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Node* find_smallest_node_covering_range(int x1, int y1, int x2, int y2)
{
    int l = 0, r = maxd;
    Node* node = root;
    while (l &amp;lt; r)
    {
        int d = (l + r + 1) &amp;gt;&amp;gt; 1;
        auto id1 = pack(d, x1, y1, w, h);
        auto id2 = pack(d, x2, y2, w, h);
        if (id1 == id2)
        {
            node = id2NodeDict.find(id1);
            id1 = pack(d + 1, x1, y1, w, h);
            id2 = pack(d + 1，x2, y2, w, h);
            if (id1 == id2)
            {
                l = d;
                continue;
            }
        }
        // 猜得太细分了，缩小上界
        r = d - 1;
    }
    return node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Loose Quadtree Optimize&lt;/h2&gt;
&lt;p&gt;松散节点优化本质是用查询效率换取移动效率。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;element&lt;/code&gt; 覆盖多个像素的这种情况下。可以采取另一种思路，那就是如果横跨多个区域，可以将其同时放在多个区域中。同时要注意在 &lt;code&gt;ssf&lt;/code&gt; 设计时，要考虑到，当区域大小细分后小于了最大 &lt;code&gt;element&lt;/code&gt; 大小，则应当阻止继续细分。&lt;/p&gt;
&lt;p&gt;在实际情况中，很少会出现一个足够小的元素来回波动，更加常见的是一个元素始终在边界徘徊。这种情况下，认为为了查询效率，牺牲一定的移动效率是值得的。&lt;/p&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://writings.sh/post/quadtree&quot;&gt;四叉树的思路、优化 和 C++ 实现 | 春水煎茶&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Actor生命周期</title><link>https://www.tanedanobata.top/UE/Framework/Actor/Actor%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/Framework/Actor/Actor%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</guid><pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://d1iv7db44yhgxn.cloudfront.net/documentation/images/ad3b6cf4-7965-4a29-a44c-43183cf16fab/actorlifecycle1.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
/// Caption&lt;br /&gt;
&lt;strong&gt;Actor生命周期&lt;/strong&gt;&lt;br /&gt;
///&lt;/p&gt;
&lt;h2&gt;Create&lt;/h2&gt;
&lt;p&gt;创建Actor方式有以下几种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/UE/Framework/Actor/Actor%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/#load-from-disc&quot;&gt;从磁盘加载&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/UE/Framework/Actor/Actor%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/#play-in-editor&quot;&gt;Play In Editor&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/UE/Framework/Actor/Actor%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/#spawnactor&quot;&gt;SpawnActor&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Load From Disc&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;从磁盘加载&lt;/strong&gt; 适用于关卡中已存在的 Actor：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;UEngine::LoadMap&lt;/code&gt; 时。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关卡流式传输调用 &lt;code&gt;UWorld::AddToWorld&lt;/code&gt; 时。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Play In Editor&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Play in Editor&lt;/strong&gt; 的Actor由编辑器中复制而来。&lt;/p&gt;
&lt;h3&gt;SpawnActor&lt;/h3&gt;
&lt;h2&gt;BeginPlay&lt;/h2&gt;
&lt;p&gt;关卡开始后被调用。&lt;/p&gt;
&lt;h2&gt;EndPlay&lt;/h2&gt;
&lt;h2&gt;GC&lt;/h2&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-actor-lifecycle&quot;&gt;Unreal Engine Actor Lifecycle | Unreal Engine 5.5 Documentation&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>优先级队列</title><link>https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BC%98%E5%85%88%E7%BA%A7%E9%98%9F%E5%88%97/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BC%98%E5%85%88%E7%BA%A7%E9%98%9F%E5%88%97/</guid><pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;优先级队列是一种基于&lt;a href=&quot;/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/&quot;&gt;二叉堆&lt;/a&gt;的数据结构。&lt;/p&gt;
&lt;p&gt;优先级队列会使用到&lt;a href=&quot;/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/&quot;&gt;二叉堆&lt;/a&gt;的&lt;a href=&quot;/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/#%E5%A0%86%E5%8C%96%E4%B8%8B%E6%B2%89&quot;&gt;二叉堆&lt;/a&gt;和&lt;a href=&quot;/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/#%E5%A0%86%E5%8C%96%E4%B8%8A%E6%B5%AE&quot;&gt;二叉堆&lt;/a&gt; 操作。&lt;/p&gt;
&lt;h2&gt;入队&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
void Enqueue(T item, int priority)
{
    heap.Add(new PriorityItem(item, priority));
    ShiftUp(heap, heap.length, heap.length - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;出队&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
T Dequeue()
{
    if (heap.Count == 0)
        throw new InvalidOperationException(&quot;Queue is empty&quot;);

    T result = heap[0].Item;
    // 将最后一个元素移到堆顶
    heap[0] = heap[heap.Count - 1];
    heap.RemoveAt(heap.Count - 1);
    if (heap.Count &amp;gt; 0)
        HeapifyDown(heap, heap.length, 0);
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>复合节点</title><link>https://www.tanedanobata.top/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E5%A4%8D%E5%90%88%E8%8A%82%E7%82%B9/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E5%A4%8D%E5%90%88%E8%8A%82%E7%82%B9/</guid><pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;复合（Composite）&lt;/strong&gt; 节点定义分支的根，以及执行该分支的基本规则。可以对其应用&lt;a href=&quot;/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E8%A3%85%E9%A5%B0%E5%99%A8%E8%8A%82%E7%82%B9/&quot;&gt;装饰器节点&lt;/a&gt;，从而修改进入它们分支的条目，或取消执行中的条目。&lt;/p&gt;
&lt;p&gt;还可以连接&lt;a href=&quot;/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E6%9C%8D%E5%8A%A1%E8%8A%82%E7%82%B9/&quot;&gt;服务节点&lt;/a&gt;，这些&lt;a href=&quot;/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E6%9C%8D%E5%8A%A1%E8%8A%82%E7%82%B9/&quot;&gt;服务节点&lt;/a&gt;只有在复合节点的子节点正在被执行时才会激活。&lt;/p&gt;
&lt;h2&gt;Selector&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;选择器（Selector）&lt;/strong&gt; 节点按从左到右的顺序执行其子节点。当其中一个子节点执行成功时，选择器节点将停止执行。如果选择器的一个子节点成功运行，则选择器运行成功。如果选择器的所有子节点运行失败，则选择器运行失败。&lt;/p&gt;
&lt;h2&gt;Sequence&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;序列（Sequence）&lt;/strong&gt; 节点按从左到右的顺序执行其子节点。当其中一个子节点失败时，序列节点也将停止执行。如果有子节点失败，那么序列就会失败。如果该序列的所有子节点运行都成功执行，则序列节点成功。&lt;/p&gt;
&lt;h2&gt;Simple Parallel&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;简单平行（Simple Parallel）&lt;/strong&gt; 节点允许一个主任务节点沿整个的行为树执行。主任务完成后，&lt;strong&gt;结束模式（Finish Mode）&lt;/strong&gt; 中的设置会指示该节点是应该立即结束，同时中止次要树，还是应该推迟结束，直到次要树完成。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;结束模式（Finish Mode）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;- &lt;strong&gt;立即（Immediate）&lt;/strong&gt;：主任务完成后，后台树的执行将立即中止。&amp;lt;br&amp;gt;- &lt;strong&gt;推迟（Delayed）&lt;/strong&gt;：主任务完成后，允许后台树继续执行直至完成。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>服务节点</title><link>https://www.tanedanobata.top/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E6%9C%8D%E5%8A%A1%E8%8A%82%E7%82%B9/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E6%9C%8D%E5%8A%A1%E8%8A%82%E7%82%B9/</guid><pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;服务（Service）&lt;/strong&gt; 节点通常连接至&lt;a href=&quot;/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E5%A4%8D%E5%90%88%E8%8A%82%E7%82%B9/&quot;&gt;复合节点&lt;/a&gt;或任务，只要其分支被执行，它们就会以定义的频率执行。这些节点常用于检查和更新黑板。它们取代了其他行为树系统中的传统平行（Parallel）节点。&lt;/p&gt;
</content:encoded></item><item><title>装饰器节点</title><link>https://www.tanedanobata.top/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E8%A3%85%E9%A5%B0%E5%99%A8%E8%8A%82%E7%82%B9/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E8%A3%85%E9%A5%B0%E5%99%A8%E8%8A%82%E7%82%B9/</guid><pubDate>Mon, 05 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;装饰器节点&lt;/strong&gt;（在其他行为树系统中也称为条件语句）连接到&lt;a href=&quot;/UE/AI/%E8%A1%8C%E4%B8%BA%E6%A0%91/%E5%A4%8D%E5%90%88%E8%8A%82%E7%82%B9/&quot;&gt;复合节点&lt;/a&gt; 或任务节点，并定义树中的分支，甚至单个节点是否可以执行&lt;/p&gt;
</content:encoded></item><item><title>TopK</title><link>https://www.tanedanobata.top/Algorithm/%E6%8E%92%E5%BA%8F/TopK/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%8E%92%E5%BA%8F/TopK/</guid><pubDate>Sun, 04 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;线性找第 k 大的数&lt;/h2&gt;
&lt;p&gt;在下面的代码示例中，第 $k$ 大的数被定义为序列排成升序时，第 $k$ 个位置上的数（编号从 0 开始）。&lt;/p&gt;
&lt;p&gt;找第 $k$ 大的数（K-th order statistic），最简单的方法是先排序，然后直接找到第 $k$ 大的位置的元素。这样做的时间复杂度是 $O(n\log n)$，对于这个问题来说很不划算。&lt;/p&gt;
&lt;p&gt;我们可以借助快速排序的思想解决这个问题。考虑快速排序的划分过程，在快速排序的「划分」结束后，数列 $A_{p}\dots A_{r}$ 被分成了 $A_{p}\dots A_{q}$ 和 $A_{p+1}\dots A_{r}$，此时可以按照左边元素的个数（$q-p+1$）和 $k$ 的大小关系来判断是只在左边还是只在右边递归地求解。&lt;/p&gt;
&lt;p&gt;和快速排序一样，该方法的时间复杂度依赖于每次划分时选择的分界值。如果采用随机选取分界值的方式，可以证明在期望意义下，程序的时间复杂度为 $O(n)$。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;```cpp
// 模板的 T 参数表示元素的类型，此类型需要定义小于（&amp;lt;）运算
template &amp;lt;typename T&amp;gt;
// arr 为查找范围数组，rk 为需要查找的排名（从 0 开始），len 为数组长度
T find_kth_element(T arr[], int rk, const int len) {
if (len &amp;lt;= 1) 
    return arr[0];
// 随机选择基准（pivot）
const T pivot = arr[rand() % len];
// i：当前操作的元素下标
// arr[0, j)：存储小于 pivot 的元素
// arr[k, len)：存储大于 pivot 的元素
int i = 0, j = 0, k = len;
// 完成一趟三路快排，将序列分为：
// 小于 pivot 的元素 ｜ 等于 pivot 的元素 ｜ 大于 pivot 的元素
while (i &amp;lt; k) {
    if (arr[i] &amp;lt; pivot)
        swap(arr[i++], arr[j++]);
    else if (pivot &amp;lt; arr[i])
        swap(arr[i], arr[--k]);
    else
        i++;
}
// 根据要找的排名与两条分界线的位置，去不同的区间递归查找第 k 大的数
// 如果小于 pivot 的元素个数比k多，则第 k 大的元素一定是一个小于 pivot 的元素
if (rk &amp;lt; j) 
    return find_kth_element(arr, rk, j);
// 否则，如果小于 pivot 和等于 pivot 的元素加起来也没有 k 多，
// 则第 k 大的元素一定是一个大于 pivot 的元素
else if (rk &amp;gt;= k)
    return find_kth_element(arr + k, rk - k, len - k);
// 否则，pivot 就是第 k 大的元素
return pivot;
}
```
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>堆排序</title><link>https://www.tanedanobata.top/Algorithm/%E6%8E%92%E5%BA%8F/%E5%A0%86%E6%8E%92%E5%BA%8F/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%8E%92%E5%BA%8F/%E5%A0%86%E6%8E%92%E5%BA%8F/</guid><pubDate>Sun, 04 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;堆排序使用到了&lt;a href=&quot;/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/&quot;&gt;二叉堆&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;堆排序需要在建堆后进行二次操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
void HeapSort(T arr[], int n) {
    // 建堆   
    Heapify(arr, n);
    // 重新下沉
    for (int i = n - 1; i &amp;gt; 0; i--)
    {
        Swap(arr[0], arr[i]);
        ShiftDown(arr, i, 0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先建堆能确保堆有序，这时候首元素一定是最大的。&lt;/p&gt;
&lt;p&gt;取出首元素，将末尾元素作为新的根节点并进行下沉，下沉后能确保新的堆有序。&lt;/p&gt;
&lt;p&gt;重复进行上述操作。&lt;/p&gt;
</content:encoded></item><item><title>快速排序</title><link>https://www.tanedanobata.top/Algorithm/%E6%8E%92%E5%BA%8F/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%8E%92%E5%BA%8F/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F/</guid><pubDate>Sun, 04 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;三路快速排序（英语：3-way Radix Quicksort）是快速排序和 &lt;a href=&quot;https://oi-wiki.org/basic/radix-sort/&quot;&gt;基数排序&lt;/a&gt; 的混合。&lt;/p&gt;
&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;三路快速排序在随机选取分界点 $m$ 后，将待排数列划分为三个部分：小于 $m$ 、等于 $m$ 以及大于 $m$ 。这样做即实现了将与分界元素相等的元素聚集在分界元素周围这一效果。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;递归实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt;
void quick_sort(T arr[], const int len)
{
    if (len &amp;lt;= 1)
        return;

    const T pivot = arr[rand() % len];
    // i：当前下标
    // arr[0, j]，存储小于 pivot 的元素
    // arr[k, len - 1]，储大于 pivot 的元素
    int i = 0, j = 0, k = len;
    // 完成一趟三路快排，将序列分为：
    // 小于 pivot 的元素 | 等于 pivot 的元素 | 大于 pivot 的元素
    while (i &amp;lt; k)
    {
        if (arr[i] &amp;lt; pivot)
            swap(arr[i++], arr[j++]);
        else if (arr[i] &amp;gt; pivot)
            swap(arr[i], arr[k--])
        else
            i++;
    }
    // 递归完成对于两个子序列的快速排序
    quick_sort(arr, j);
    quick_sort(arr + k, len - k);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;非递归实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T&amp;gt;
void quick_sort_non_recursive(T arr[], const int len) {
    if (len &amp;lt;= 1)
    return;

    // 使用栈来存储待排序的子数组的起始和结束位置
    stack&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; stk;
    stk.push({0, len - 1});

    while (!stk.empty()) {
        auto [left, right] = stk.top();
        stk.pop();

        if (left &amp;gt;= right) continue;

        // 选择基准值
        const T pivot = arr[left + rand() % (right - left + 1)];
        int i = left, j = left, k = right;

        // 三路划分
        while (i &amp;lt;= k) {
            if (arr[i] &amp;lt; pivot) {
                swap(arr[i++], arr[j++]);
            } else if (arr[i] &amp;gt; pivot) {
                swap(arr[i], arr[k--]);
            } else {
                i++;
            }
        }

        // 将子数组的边界压入栈中
        stk.push({left, j - 1});
        stk.push({k + 1, right});
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;h3&gt;稳定性&lt;/h3&gt;
&lt;p&gt;快速排序是一种不稳定的排序算法。&lt;/p&gt;
&lt;h3&gt;时间复杂度&lt;/h3&gt;
&lt;p&gt;快速排序的最优时间复杂度和平均时间复杂度为 $O(n\log n)$，最坏时间复杂度为 $n^2$。&lt;br /&gt;
对于最优情况，每一次选择的分界值都是序列的中位数。&lt;br /&gt;
对于最坏情况，每一次选择的分界值都是序列的最值。&lt;/p&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/basic/quick-sort/#%E4%B8%89%E8%B7%AF%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F&quot;&gt;快速排序 - OI Wiki&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>二叉堆</title><link>https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%A0%86/%E4%BA%8C%E5%8F%89%E5%A0%86/</guid><pubDate>Sun, 04 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;从二叉堆的结构说起，它是一棵二叉树，并且是&lt;a href=&quot;https://en.wikipedia.org/wiki/Binary_tree#:~:text=A%20perfect%20binary%20tree%20is,is%20a%20full%20binary%20tree.&quot;&gt;完全二叉树&lt;/a&gt;，每个结点中存有一个元素（或者说，有个权值）。&lt;/p&gt;
&lt;p&gt;堆性质：父亲的权值不小于儿子的权值（大根堆）。同样的，我们可以定义小根堆。&lt;/p&gt;
&lt;p&gt;由堆性质，树根存的是最大值（getmax 操作就解决了）。&lt;/p&gt;
&lt;h3&gt;节点结构&lt;/h3&gt;
&lt;p&gt;对于任意节点 $i$&lt;br /&gt;
节点的父节点：$\frac{i-1}{2}$&lt;br /&gt;
节点的左子节点：$2i+1$&lt;br /&gt;
节点的右子节点：$2i+2$&lt;/p&gt;
&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;堆的核心操作有两个：&lt;/p&gt;
&lt;p&gt;堆化上浮：如果节点比父节点大,则与父节点交换,重复此过程直到根节点。操作结束后,能确保节点满足堆性质。&lt;/p&gt;
&lt;p&gt;堆化下沉：在节点的子节点中,找到一个最大的,与该节点交换,重复此过程到底层。操作结束后,能确保节点满足堆性质。&lt;/p&gt;
&lt;h3&gt;辅助函数&quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;    int GetParent(int i) =&amp;gt; (i - 1) / 2;  
    int GetLeftChild(int i) =&amp;gt; (i * 2) + 1;  
    int GetRightChild(int i) =&amp;gt; (i * 2) + 2;  
    int GetFirstLeaf(int n) =&amp;gt; n / 2;  
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;堆化下沉&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// n 是堆的大小，i 是当前节点的索引  
template&amp;lt;typename T&amp;gt;  
void ShiftDown(T arr[], int n, int i) {  
    while (true)  
    {  
        int largest = i;  
        int left = GetLeftChild(i);  
        int right = GetRightChild(i);  
        if (left &amp;lt; n &amp;amp;&amp;amp; arr[left] &amp;gt; arr[largest]) {  
            largest = left;  
        }  
        if (right &amp;lt; n &amp;amp;&amp;amp; arr[right] &amp;gt; arr[largest]) {  
            largest = right;  
        }  
        if (largest == i)  
            break;

        Swap(arr[i], arr[largest]);
        i = largest;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;堆化上浮&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;  
void ShiftDown(T arr[], int n, int i) {  
    while (i &amp;gt; 0)  
    {  
        int parent = GetParent(i);  
        if (arr[parent] &amp;gt;= arr[i])  
            break;

        Swap(arr[parent], arr[i]);
        i = parent;                    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;建堆&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;上浮实现堆有序&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;  
void Heapify(T arr[], int n) {  
    for (int i = 0; i &amp;lt; n; i++)  
    {  
        ShiftUp(arr, n, i);  
    }  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于第 $k$ 层的结点，向上调整的复杂度为 $O(k)$ 而不是 $O(\log n)$。&lt;/p&gt;
&lt;p&gt;总复杂度：$\log 1 + \log 2 + \cdots + \log n = \Theta(n \log n)$。&lt;/p&gt;
&lt;p&gt;上浮操作的复杂度是高于下沉的,所以一般采用下沉实现堆有序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下沉实现堆有序&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;  
void Heapify(T arr[], int n) {  
    for (int i = GetFirstLeaf(n) - 1; i &amp;gt;= 0; i--)  
    {  
        ShiftDown(arr, n, i);  
    }  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次 &lt;strong&gt;合并&lt;/strong&gt; 两个已经调整好的堆，这说明了正确性。&lt;/p&gt;
&lt;p&gt;注意到向下调整的复杂度，为 $O(\log n - k)$，另外注意到叶节点无需调整，因此可从序列约 $n/2$ 的位置开始调整，可减少部分常数但不影响复杂度。&lt;/p&gt;
&lt;p&gt;总复杂度为$ O(n) $。&lt;/p&gt;
&lt;h3&gt;堆排序&lt;/h3&gt;
&lt;h3&gt;优先级队列&lt;/h3&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/ds/binary-heap/&quot;&gt;二叉堆 - OI Wiki&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>简历归档</title><link>https://www.tanedanobata.top/posts/resume-archive/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/resume-archive/</guid><pubDate>Tue, 29 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;简历归档
&amp;lt;!-- more --&amp;gt;
&lt;a href=&quot;https://typst.app/project/pyA4QmHXNzyKmfwXBRjszS&quot;&gt;Typst&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#import &quot;template.typ&quot;: *

// 主题颜色
#let theme-color = rgb(&quot;#26267d&quot;)
#let icon = icon.with(fill: theme-color)

// 设置图标, 来源: https://fontawesome.com/icons/
#let fa-award = icon(&quot;icons/fa-award.svg&quot;)
#let fa-building-columns = icon(&quot;icons/fa-building-columns.svg&quot;)
#let fa-code = icon(&quot;icons/fa-code.svg&quot;)
#let fa-envelope = icon(&quot;icons/fa-envelope.svg&quot;)
#let fa-github = icon(&quot;icons/fa-github.svg&quot;)
#let fa-graduation-cap = icon(&quot;icons/fa-graduation-cap.svg&quot;)
#let fa-linux = icon(&quot;icons/fa-linux.svg&quot;)
#let fa-phone = icon(&quot;icons/fa-phone.svg&quot;)
#let fa-windows = icon(&quot;icons/fa-windows.svg&quot;)
#let fa-wrench = icon(&quot;icons/fa-wrench.svg&quot;)
#let fa-work = icon(&quot;icons/fa-work.svg&quot;)

// 设置简历选项与头部
#show: resume.with(
  // 字体和基准大小
  size: 10pt,
  // 标题颜色
  theme-color: theme-color,
  // 控制纸张的边距
  margin: (
    top: 1.5cm,
    bottom: 2cm,
    left: 2cm,
    right: 2cm,
  ),

  // 如果需要姓名及联系信息居中，请删除下面关于头像的三行参数，并取消headerCenter的注释
  //headerCenter : true,

  // 如果不需要头像，则将下面三行的参数注释或删除
  // photograph: &quot;profile.jpg&quot;,
  // photograph-width: 10em,
  // gutter-width: 2em,
)[
  = 王朝伟

  #info(
    color: theme-color,
    (
      icon: fa-phone,
      content: &quot;(+86) 189-1869-1145&quot;,
    ),
    (
      icon: fa-envelope,
      content: &quot;aizawaayame@outlook.com&quot;,
      link: &quot;mailto:aizawaayame@outlook.com&quot;,
    ),
    (
      icon: fa-github,
      content: &quot;github.com/aizawaayame/OnaGameSample&quot;,
      link: &quot;https://github.com/aizawaayame/OnaGameSample&quot;,
    ),
  )
][
  #h(2em)

  拥有 #text(weight: &quot;bold&quot;)[3 年] 游戏客户端开发经验，在米哈游期间，#text(weight: &quot;bold&quot;)[主导并深度参与] 了多个核心系统的设计、开发与优化，包括基于 PCG 的表情系统、高性能编辑器 Canvas 渲染基建、灵活的 Playable 动画系统以及组件化相机约束系统。

  擅长*演出动画系统和工具链开发*，在提升内容创作效率方面有丰富经验。具备跨部门协作能力，能将复杂技术转化为高效生产工具。

  同时对 UE 反射系统、垃圾回收、动画系统有了解和个人项目实践经验。对UE技术有浓厚兴趣。
]


== #fa-graduation-cap 教育背景

#sidebar(with-line: true, side-width: 12%)[
  2018.09

  2022.07
][
  *哈尔滨工业大学* 

  计算机科学与技术专业
]

== #fa-work 工作经历
#sidebar(with-line: true, side-width: 12%)[
  2022.08

  至今
][
  *米哈游* · 原神组 · 客户端开发

    - 主导多个核心功能模块的架构设计与开发，包括表情系统、编辑器UI基础设施和动画系统
    - 深入优化编辑器性能，利用GPU合批和GPGPU思想，大幅度优化项目组的编辑器开发效率和性能
    - 建立内容创作工具链和自动化流程，提升团队工作效率，支持每6周稳定迭代6000+对话内容
    - 了解UE核心功能，了解Gameplay框架、反射系统、垃圾回收和动画系统底层机制
]

#sidebar(with-line: true, side-width: 12%)[
  2021.07

  2021.11
][
  *腾讯* · j2工作室-全境封锁项目组 · UE客户端开发实习

  - 参与behaviac行为树逻辑的维护
  - 学习UE的整体架构，了解UE行为树的设计
]

== #fa-wrench 专业技能

#sidebar(with-line: false, side-width: 12%)[
  *编程语言*
  
  *图形渲染*
  
  *动画系统*
  

  *工具开发*
][
  C++(熟悉)、C\#(熟练)、HLSL/GLSL(了解)
  
  GPU实例化、计算着色器、了解渲染管线、空间数据结构
  
  理解骨骼动画系统、Playable架构、状态机设计、动画混合空间
  

  编辑器扩展开发、自动化生产流水线设计、美术工具链构建
]


== #fa-code 项目经历

#item(
  [ *表情系统架构升级与 PCG 系统实现* ]
)

#tech[ 表情系统，PCG ]

基于 PCG 的表情构建管线

 - 主导设计并实现了程序化表情生成系统，实现从文本和音频到面部表情的自动映射，大幅度提升表情制作效率。
 - 整合MFCC音频特征提取和NLP情感分析算法，构建精确的语音-表情映射关系，支持多语言本地化。
 - 设计标记驱动的分层编辑架构，支持表情批量生成和精细调整两种工作流，实现每6周迭代稳定产出6000+对话表情。
#item(
  [ *编辑器 Canvas 图形系统重构* ]
)

#tech[ 编辑器, 图形编程, UI架构 ]

一个支持高性能绘制的编辑器UI系统

- 主导编辑器 UI 渲染基础设施的重构，采用 GPU 实例化合批技术，极大提升编辑器 UI 渲染性能。
- 应用 GPGPU 计算着色器技术处理大量 UI 元素的变换计算，特别针对曲线绘制场景有大幅度的性能优化。
- 实现了自定义的四叉树空间管理算法，优化了大量 UI 元素的剔除和渲染提交，提高了编辑器响应速度。
- 设计了高效的 GPU 资源管理机制，实现不同视口大小的无缝切换。

#item(
  [ *演出动画系统迭代与 Playable 技术引入* ]
)

#tech[ 动画系统, Playable ]

一个支持自由拓展的基于Playable的动画系统

- 参与引入 Playable 动画系统，实现了非侵入式的动画叠加功能，使演出动画与角色状态机解耦。
- 开发了动态动画创建和运行时混合框架，支持在任意游戏时刻无缝插入和融合复杂动画序列。

#item(
  [ *演出相机系统架构优化* ]
)

#tech[ 相机，演出系统 ]

一个支持多种约束的演出相机系统

- 参与设计并实现了组件化的相机约束系统，支持多种约束条件的组合与优先级管理。
- 实现美术最常用的曲线约束，跟随约束和目标点旋转约束等约束功能，优化演出效果，提高了美术的产能。

== #fa-github 个人项目

#item(
  link(
    &quot;https://github.com/aizawaayame/OnaGameSample&quot;,
    [ *基于ALS的个人项目* ]
  )
)
#item(
link(
&quot;https://www.tanedanobata.top/Project/OnaGameSample/&quot;,
[ 技术博客: OnaGameSample ]
)
)
#tech[ 相机，动画，ALS ]

用C++重写ALS并重构部分系统

- 使用纯C++重写了Advanced Locomotion System的核心功能，实现了轻量级、高性能的角色运动系统。
- 设计了模块化的Camera Stack架构，支持多种相机效果叠加和平滑过渡。



&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原首页履历（2025-04-02）&lt;/h2&gt;
&lt;h1&gt;教育背景&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;学位&lt;/strong&gt;：哈尔滨工业大学-计算机科学与技术专业-本科&lt;br /&gt;
&lt;strong&gt;时间段&lt;/strong&gt;：2018 年 9 月-2022 年 7 月&lt;br /&gt;
&lt;strong&gt;学信网在线验证码&lt;/strong&gt;：AVT75TLTCJ0RM8XV&lt;/p&gt;
&lt;h1&gt;求职意向&lt;/h1&gt;
&lt;p&gt;游戏客户端开发工程师 / Unreal Engine (UE) C++ 工程师&lt;/p&gt;
&lt;h1&gt;个人概述&lt;/h1&gt;
&lt;p&gt;拥有 3 年游戏客户端开发经验。在动画系统、相机系统及编辑器工具开发方面积累了一定经验。了解虚幻引擎核心机制，包括反射系统、垃圾回收、动画系统。&lt;br /&gt;
专注于动画模块，演出模块开发及工具链构建&lt;/p&gt;
&lt;h1&gt;专业技能&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;编程语言：C++(熟悉)、C#(熟练)&lt;/li&gt;
&lt;li&gt;动画系统：熟悉 Unity Playable 动画系统，了解 UE 动画状态机&lt;/li&gt;
&lt;li&gt;工具开发：构建面向内容创作的编辑器工具链、自动化流程设计&lt;/li&gt;
&lt;li&gt;引擎使用：了解 UE Gameplay 架构，反射系统和垃圾回收机制&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;项目经验&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;米哈游|游戏客户端开发&lt;/strong&gt;：2022 年7 月-至今&lt;/p&gt;
&lt;h2&gt;表情系统架构升级与 PCG 系统实现&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;主导设计并实现了基于程序化内容生成 (PCG) 的表情系统，大幅度提升表情制作效率。每 6 周迭代能稳定产出 6000+条对话的表情。&lt;/li&gt;
&lt;li&gt;结合 MFCC 音频特征提取和 NLP 情感分析技术，构建了自动化表情生成管线，实现了从文本到面部表情的映射。&lt;/li&gt;
&lt;li&gt;设计了标记驱动的表情系统编辑架构，支持批量生成和精细调整两种生产模式，满足不同品质需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;编辑器 Canvas 图形系统重构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;主导编辑器 UI 渲染基础设施的重构，采用 GPU 实例化合批技术，极大提升编辑器 UI 渲染性能。&lt;/li&gt;
&lt;li&gt;应用 GPGPU 计算着色器技术处理大量 UI 元素的变换计算，特别针对曲线绘制场景有大幅度的性能优化。&lt;/li&gt;
&lt;li&gt;实现了自定义的四叉树空间管理算法，优化了大量 UI 元素的剔除和渲染提交，提高了编辑器响应速度。&lt;/li&gt;
&lt;li&gt;设计了高效的 GPU 资源管理机制，实现不同视口大小的无缝切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;演出动画系统迭代与 Playable 技术引入&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;参与引入 Playable 动画系统，实现了非侵入式的动画叠加功能，使演出动画与角色状态机解耦。&lt;/li&gt;
&lt;li&gt;开发了动态动画创建和运行时混合框架，支持在任意游戏时刻无缝插入和融合复杂动画序列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;演出相机系统架构优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;参与设计并实现了组件化的相机约束系统，支持多种约束条件的组合与优先级管理。&lt;/li&gt;
&lt;li&gt;实现美术最常用的曲线约束，跟随约束和目标点旋转约束等约束功能，优化演出效果，提高了美术的产能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;个人项目&lt;/h1&gt;
&lt;p&gt;用UE C++重写ALS项目。实现文档&lt;a href=&quot;/Project/OnaGameSample/%E8%B5%B0%E8%B7%91%E6%B7%B7%E5%90%88/&quot;&gt;走跑混合 - 柚子其实是蜜柑&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用C++逻辑重写动画相关逻辑。&lt;/li&gt;
&lt;li&gt;(进行中)用Camera Stack设计思路重构相机模块。&lt;br /&gt;
项目地址&lt;a href=&quot;https://github.com/aizawaayame/OnaGameSample&quot;&gt;GitHub - aizawaayame/OnaGameSample: Rewrite the ALS project using C++.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>TArray</title><link>https://www.tanedanobata.top/UE/Core/%E5%AE%B9%E5%99%A8/TArray/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/Core/%E5%AE%B9%E5%99%A8/TArray/</guid><pubDate>Fri, 25 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;h3&gt;TArray是值类型&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;TArray&lt;/code&gt; 是一种值类型，&lt;strong&gt;不要&lt;/strong&gt; 使用 &lt;code&gt;new&lt;/code&gt; 和 &lt;code&gt;delete&lt;/code&gt; 创建和销毁 &lt;code&gt;TArray&lt;/code&gt; 实例。&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;quote&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TArray&lt;/code&gt; is a value type, meaning that it should be treated similarly as any other built-in type, like &lt;code&gt;int32&lt;/code&gt; or &lt;code&gt;float&lt;/code&gt;. It is not designed to be extended, and creating or destroying &lt;code&gt;TArray&lt;/code&gt; instances with &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; is not a recommended practice. The elements are also value types, and the array owns them. Destruction of a &lt;code&gt;TArray&lt;/code&gt; will result in the destruction of any elements it still contains. Creating a TArray variable from another will copy its elements into the new variable; there is no shared state.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;基础接口&lt;/h2&gt;
&lt;h3&gt;创建&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;TArray&amp;lt;int32&amp;gt; IntArray;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将创建一个空数组，用于保存整数序列。元素类型可以是任何值类型，这些类型可以根据常规 C++ 值规则进行复制和销毁，例如 int32 、 FString 、 TSharedPtr 等。由于未指定分配器，因此 TArray 将使用默认的基于堆的分配器。此时，尚未分配任何内存。&lt;/p&gt;
&lt;h3&gt;填充&lt;/h3&gt;
&lt;p&gt;核心有两个系列 &lt;code&gt;Emplace&lt;/code&gt; 和 &lt;code&gt;Add&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;Emplace&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename... ArgsType&amp;gt;  
FORCEINLINE SizeType Emplace(ArgsType&amp;amp;&amp;amp;... Args)  
{  
    const SizeType Index = AddUninitialized();  
    new(GetData() + Index) ElementType(Forward&amp;lt;ArgsType&amp;gt;(Args)...);  
    return Index;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据传入参数，创建一个新的实例。&lt;/p&gt;
&lt;h4&gt;Add&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;左值&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FORCEINLINE SizeType Add(const ElementType&amp;amp; Item)  
{  
    CheckAddress(&amp;amp;Item);  
    return Emplace(Item);  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;右值&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FORCEINLINE SizeType Add(ElementType&amp;amp;&amp;amp; Item)  
{  
    CheckAddress(&amp;amp;Item);  
    return Emplace(MoveTempIfPossible(Item));  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接口最终还是会调用到 &lt;code&gt;Emplace&lt;/code&gt;，可以注意到，这个过程中会调用拷贝构造函数或移动构造函数，存在可能的开销。&lt;/p&gt;
&lt;h3&gt;删除&lt;/h3&gt;
&lt;h4&gt;触发重排(Shuffling)&lt;/h4&gt;
&lt;p&gt;包含 &lt;code&gt;Remove&lt;/code&gt;，&lt;code&gt;RemoveSingle&lt;/code&gt;，&lt;code&gt;RemoveAt&lt;/code&gt;，&lt;code&gt;RemoveAll&lt;/code&gt; 接口。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;保持数组中其余元素的原始顺序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移动被删除位置后的所有元素来填补空缺。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;不触发重排(Non-shuffling)&lt;/h4&gt;
&lt;p&gt;包含 &lt;code&gt;RemoveSwap&lt;/code&gt;，&lt;code&gt;RemoveAtSwap&lt;/code&gt;，&lt;code&gt;RemoveAllSwap&lt;/code&gt; 接口。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不保持数组中其余元素的原始顺序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用数组末尾的元素来填充被删除的位置&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;清空所有内容&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Empty&lt;/code&gt; 接口能直接清空所有元素。&lt;/p&gt;
&lt;h3&gt;迭代&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;for&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FString JoinedStr;  
for (auto&amp;amp; Str : StrArr)  
{  
    JoinedStr += Str;  
    JoinedStr += TEXT(&quot; &quot;);  
}  
// JoinedStr == &quot;Hello Brave World of Tomorrow ! &quot;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;读写迭代器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (auto It = StrArr.CreateConstIterator(); It; ++It)  
{  
    JoinedStr += *It;  
    JoinedStr += TEXT(&quot; &quot;);  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;只读迭代器&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (auto It = StrArr.CreateIterator(); It; ++It)  
{  
    JoinedStr += *It;  
    JoinedStr += TEXT(&quot; &quot;);  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查询&lt;/h3&gt;
&lt;h4&gt;Num&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;FORCEINLINE SizeType Num() const  
{  
    return ArrayNum;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;GetData&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;FORCEINLINE ElementType* GetData() UE_LIFETIMEBOUND  
{  
    return (ElementType*)AllocatorInstance.GetAllocation();  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取数组的原始指针，此指针仅在数组存在期间以及对数组进行任何修改操作之前有效。&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;quote&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;If you need direct access to the array memory, perhaps for interoperability with a C-style API, you can use the &lt;code&gt;GetData&lt;/code&gt; function to return a pointer to the elements in the array. This pointer is only valid as long as the array exists and before any mutating operations are made to the array. Only the first &lt;code&gt;Num&lt;/code&gt; indices from the &lt;code&gt;StrPtr&lt;/code&gt; are dereferenceable&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h4&gt;IsValidIndex&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;FORCEINLINE bool IsValidIndex(SizeType Index) const  
{  
    return Index &amp;gt;= 0 &amp;amp;&amp;amp; Index &amp;lt; ArrayNum;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Contains&lt;/h4&gt;
&lt;p&gt;包含 &lt;code&gt;Contains&lt;/code&gt; 和 &lt;code&gt;ContainsByPredicate&lt;/code&gt;，检查特定元素是否存在。&lt;/p&gt;
&lt;h4&gt;Find和IndexOf系列&lt;/h4&gt;
&lt;p&gt;查找特定元素。具体可见源码。遇到坑再记录。&lt;/p&gt;
&lt;h4&gt;Filter系列&lt;/h4&gt;
&lt;p&gt;函数检索与特定谓词匹配的元素数组。&lt;/p&gt;
</content:encoded></item><item><title>驾照</title><link>https://www.tanedanobata.top/posts/driving-license/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/driving-license/</guid><pubDate>Thu, 24 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;驾照记录。
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;驾驶证申领和使用&lt;/h2&gt;
&lt;h3&gt;考试科目及其合格标准&lt;/h3&gt;
&lt;p&gt;使用教练车或学车专用标识的车。&lt;/p&gt;
&lt;p&gt;不得搭载教练以外的其他人。&lt;/p&gt;
&lt;p&gt;科目二考倒车入库，侧方停车，曲线行驶，直角转弯。&lt;/p&gt;
&lt;h3&gt;申请条件&lt;/h3&gt;
&lt;p&gt;小车18无上限&lt;br /&gt;
轻牵20-70&lt;br /&gt;
大客，重牵22-63&lt;/p&gt;
&lt;p&gt;客车轻重牵不能初次申领&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不得申请&lt;/strong&gt;：&lt;br /&gt;
假 1 吊 2 撤 3 骗 3 毒 3 饮3 醉 5 逃终生。&lt;br /&gt;
(关于作弊，考试中 1 年，得逞 3 年)&lt;br /&gt;
(注意，如果醉驾犯罪了就是终生了)&lt;br /&gt;
运营车辆饮5醉10&lt;br /&gt;
竞驶，超员，超速，违法危险运输构成犯罪的，5年&lt;/p&gt;
&lt;h3&gt;驾驶证审验&lt;/h3&gt;
&lt;p&gt;小车死人交通事故，记分周期结束后&lt;br /&gt;
客车大货重牵，每个积分周期有扣分的&lt;br /&gt;
70岁以上每年体检，记分周期结束后&lt;br /&gt;
30日内审验&lt;/p&gt;
&lt;h3&gt;驾驶证遗失&lt;/h3&gt;
&lt;p&gt;扣押，扣留期间不得补发&lt;br /&gt;
继续使用原驾驶证的，200以下罚款，收回原驾驶证&lt;/p&gt;
&lt;p&gt;车找登记车管所，证找任意车管所&lt;/p&gt;
&lt;h3&gt;驾驶证有效期&lt;/h3&gt;
&lt;p&gt;6，10，长期&lt;/p&gt;
&lt;h3&gt;驾驶证注销&lt;/h3&gt;
&lt;p&gt;过期注销的，未满2考一，满二考一三，满三重考&lt;/p&gt;
&lt;p&gt;过期一年未换证的，注销&lt;/p&gt;
&lt;p&gt;(判断题注销选对，不会被注销选错)&lt;/p&gt;
&lt;h3&gt;申请增加准驾车型&lt;/h3&gt;
&lt;p&gt;小车满一年可以申请轻牵&lt;/p&gt;
&lt;h3&gt;实习期&lt;/h3&gt;
&lt;p&gt;上高速，三年司机陪同&lt;br /&gt;
实习开挂罚200(牵引挂车)&lt;/p&gt;
&lt;h3&gt;组织违规申领&lt;/h3&gt;
&lt;p&gt;虚假500，作弊1000，欺骗贿赂2000&lt;br /&gt;
五倍10万&lt;br /&gt;
组织作弊构成犯罪的，不严重3以下，严重3-7&lt;/p&gt;
&lt;p&gt;审验教育：&lt;br /&gt;
作假1000，代替2000&lt;/p&gt;
&lt;h3&gt;学习驾驶证&lt;/h3&gt;
&lt;p&gt;科二科三分别可以约考5次&lt;br /&gt;
学习驾驶证明有效期3年&lt;/p&gt;
&lt;h3&gt;有效期满，转入，变更换证&lt;/h3&gt;
&lt;p&gt;期满的，90日内向任意车管所申请换证&lt;br /&gt;
变更信息30日&lt;br /&gt;
超过有效期一年未换证的，注销驾照&lt;br /&gt;
户籍迁出，向迁入地车管所申请换证。换地居住，向居住地车管所申请换证&lt;/p&gt;
&lt;h3&gt;准驾车型&lt;/h3&gt;
&lt;p&gt;C1 小车，低速货车，三轮汽车&lt;br /&gt;
C2 自动小车&lt;br /&gt;
A1 大客&lt;br /&gt;
A2 重牵&lt;br /&gt;
B1 中客&lt;br /&gt;
B2 大货&lt;/p&gt;
&lt;h2&gt;机动车登记和使用&lt;/h2&gt;
&lt;h3&gt;机动车登记和行驶证&lt;/h3&gt;
&lt;p&gt;机动车登记证书和行驶证灭失, 丢失和损毁的, 向 &lt;strong&gt;登记地车管所&lt;/strong&gt; 申请补领, 换领。&lt;br /&gt;
自受理日 &lt;strong&gt;15日内&lt;/strong&gt; 补发, 换发号牌&lt;/p&gt;
&lt;h3&gt;年检&lt;/h3&gt;
&lt;p&gt;运营客车5年内每年一检，5年后半年一检&lt;br /&gt;
货车, 大中非运营客车10年内年检, 10年后半年一检&lt;br /&gt;
非运营小客车（面包车除外）, 摩托车, 6, 10 检查, 每两年申领检验标志，10年后年检&lt;/p&gt;
&lt;h3&gt;保险&lt;/h3&gt;
&lt;p&gt;保险期为1年&lt;br /&gt;
1 年未发生事故的, 保费降低&lt;/p&gt;
&lt;h3&gt;强制报废&lt;/h3&gt;
&lt;p&gt;驾驶报废车辆上路的, 吊销驾驶证&lt;br /&gt;
报废车交售给回收企业&lt;/p&gt;
&lt;h3&gt;临时号牌&lt;/h3&gt;
&lt;p&gt;临时号牌3日作废&lt;/p&gt;
&lt;h2&gt;交通违法记分&lt;/h2&gt;
&lt;h3&gt;12&lt;/h3&gt;
&lt;p&gt;酒驾&lt;br /&gt;
轻伤以上的交通事故 &lt;strong&gt;逃逸&lt;/strong&gt;。&lt;br /&gt;
伪造, 变更车牌, 行驶证, 驾驶证.....&lt;/p&gt;
&lt;p&gt;代替处罚牟利&lt;/p&gt;
&lt;p&gt;掉头：&lt;br /&gt;
高速公路和城市快车道倒车, 逆行, 穿越中央分隔带掉头&lt;/p&gt;
&lt;p&gt;超载, 超速：&lt;br /&gt;
&lt;strong&gt;校车, 公路客运汽车, 旅游客车&lt;/strong&gt;超载20, 或者其他客车超过100&lt;br /&gt;
&lt;strong&gt;校车, 中型以上客车, 危险品货车&lt;/strong&gt;在&lt;strong&gt;高速公路或城市快车道&lt;/strong&gt;超速20
其他机动车在高速公路, 城市快车道超速50&lt;/p&gt;
&lt;h3&gt;9&lt;/h3&gt;
&lt;p&gt;未取得资格驾驶校车的；&lt;br /&gt;
驾驶证和驾驶车辆不符的；&lt;br /&gt;
遮挡或未悬挂车牌；&lt;br /&gt;
中客和危险品超过4小时没有停车休息20分钟的。&lt;/p&gt;
&lt;p&gt;违停：&lt;br /&gt;
在高速公路或城市快速路上停车的；&lt;/p&gt;
&lt;p&gt;超载, 超速：&lt;br /&gt;
7座以上超载50-100的&lt;br /&gt;
&lt;strong&gt;校车, 中型以上客车, 危险品货车&lt;/strong&gt;在非高速公路或城市快车道超速50的&lt;/p&gt;
&lt;h3&gt;6&lt;/h3&gt;
&lt;p&gt;交通事故轻微伤逃逸&lt;br /&gt;
驾驶证暂扣期间驾驶机动车&lt;br /&gt;
闯红灯&lt;br /&gt;
运送危险物品未悬挂警告标识的或进入限制通行区域的&lt;/p&gt;
&lt;p&gt;违法占用：&lt;br /&gt;
在高速公路或城市快速路上违法占用应急车道；&lt;/p&gt;
&lt;p&gt;超载, 超速：&lt;br /&gt;
&lt;strong&gt;校车, 公路客运汽车, 旅游客车&lt;/strong&gt;超载0-20,或7座客车20-50 或者其他客车超过50-100&lt;br /&gt;
&lt;strong&gt;校车, 中型以上客车, 危险品货车&lt;/strong&gt;在快车道超速0-20, 或其他车道20-50&lt;br /&gt;
其他机动车在快车道超速20-50, 或其他车道50-100&lt;/p&gt;
&lt;h3&gt;3&lt;/h3&gt;
&lt;p&gt;快车道不按规定车道行驶&lt;br /&gt;
不按规定车道超车, 让行&lt;br /&gt;
非快车道逆行&lt;br /&gt;
玩手机&lt;br /&gt;
不礼让行人&lt;br /&gt;
不礼让校车&lt;br /&gt;
故障没有放置标识&lt;br /&gt;
未安全检查上路&lt;br /&gt;
货车4小时没有休息20分钟的&lt;br /&gt;
高速路上低于最低时速&lt;/p&gt;
&lt;p&gt;超载, 超速：&lt;/p&gt;
&lt;h3&gt;1&lt;/h3&gt;
&lt;p&gt;安全带, 头盔, 以及其他乱七八遭的&lt;br /&gt;
非高速路倒车, 掉头&lt;/p&gt;
&lt;p&gt;超载, 超速：&lt;/p&gt;
</content:encoded></item><item><title>二分搜索</title><link>https://www.tanedanobata.top/Algorithm/%E6%90%9C%E7%B4%A2/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%90%9C%E7%B4%A2/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/</guid><pubDate>Thu, 24 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;以在一个升序数组中查找一个数为例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal static int InternalBinarySearch(  
  T[] array,  
  int index,  
  int length,  
  T value,  
  IComparer&amp;lt;T&amp;gt; comparer)  
{  
  int num1 = index;  
  int num2 = index + length - 1;  
  while (num1 &amp;lt;= num2)  
  {    
    int index1 = num1 + (num2 - num1 &amp;gt;&amp;gt; 1);  
    int num3 = comparer.Compare(array[index1], value);  
    if (num3 == 0)  
      return index1;  
    if (num3 &amp;lt; 0)  
      num1 = index1 + 1;  
    else  
      num2 = index1 - 1;  
  }  
  return ~num1;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;输出为正数则代表找到该元素，返回其索引。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输出为负数则代表未找到该元素，返回其插入位置的索引取反。其中 &lt;code&gt;~0&lt;/code&gt; 代表小于所有元素，&lt;code&gt;~length&lt;/code&gt; 代表大于所有元素&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;性质&lt;/h2&gt;
&lt;h3&gt;时间复杂度&lt;/h3&gt;
&lt;p&gt;二分查找的最优时间复杂度为 $O(1)$。&lt;/p&gt;
&lt;p&gt;二分查找的平均时间复杂度和最坏时间复杂度均为 $O(\log n)$。因为在二分搜索过程中，算法每次都把查询的区间减半，所以对于一个长度为 $n$ 的数组，至多会进行 $O(\log n)$ 次查找。&lt;/p&gt;
&lt;h3&gt;空间复杂度&lt;/h3&gt;
&lt;p&gt;迭代版本的二分查找的空间复杂度为 $O(1)$&lt;/p&gt;
&lt;p&gt;递归（无尾调用消除）版本的二分查找的空间复杂度为 $O(\log n)$&lt;/p&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://oi-wiki.org/basic/binary/&quot;&gt;二分 - OI Wiki&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>BFS</title><link>https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E8%AE%BA/BFS/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E8%AE%BA/BFS/</guid><pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;概述&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;What Is&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;BFS 全称是 &lt;a href=&quot;https://en.wikipedia.org/wiki/Breadth-first_search&quot;&gt;Breadth First Search&lt;/a&gt;，也就是广度优先搜索。&lt;/p&gt;
&lt;p&gt;所谓广度优先。就是每次都尝试访问同一层的节点。如果同一层都访问完了，再访问下一层。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;结果&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这样做的结果是，BFS 算法找到的路径是从起点开始的 &lt;strong&gt;最短&lt;/strong&gt; 合法路径。换言之，这条路径所包含的边数最小。&lt;/p&gt;
&lt;p&gt;在 BFS 结束时，&lt;strong&gt;每个节点都是通过从起点到该点的最短路径访问的&lt;/strong&gt; 。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;本质&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在实现 BFS 的时候，本质上我们把未被访问过的节点放在一个称为 open 的容器中，而把已经访问过了的节点放在一个称为 closed 容器中。&lt;/p&gt;
&lt;h2&gt;链式前向星实现&lt;/h2&gt;
&lt;h3&gt;实现代码&lt;/h3&gt;
&lt;p&gt;链式前向星的实现见&lt;a href=&quot;/Algorithm/%E5%9B%BE%E8%AE%BA/%E5%9B%BE%E7%9A%84%E5%AD%98%E5%82%A8/&quot;&gt;图的存储&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;伪代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bfs(s){
    q = new queue()
    q.push(s);
    visited[s] = true;
    while(!q.empty())
    {
        u = q.pop();
        for each edge(u, v)
        {
            if (!visited[v])
            {
                q.push(v);
                visited[v] = true;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;C++&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Edge {
    int to;
    int next;
    int w;
};
// u代表起始的顶点
void bfs(int u) {
    queue&amp;lt;int&amp;gt; q;
    q.push(u);
    visited[u] = true;
    // 额外信息
    d[u] = 0;
    p[u] = -1;

    while (!q.empty())
    {
        u = q.front();
        q.pop();
        for (Edge e = head[u]; e.next != -1; e = edge[e.next].next)
        {
            if (!visited[e.to]) {
                q.push(e.to);
                visited[e.to] = true;

                d[e.to] = d[u] + 1;
                p[e.to] = u;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;执行流程&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;队列 &lt;code&gt;Q&lt;/code&gt; 记录需要处理的节点，以 &lt;code&gt;visited[]&lt;/code&gt; 类标记节点是否已经被访问。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化时，将起点 &lt;code&gt;s&lt;/code&gt; 入队，并标记 &lt;code&gt;visited[s] = true&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每次从队列 &lt;code&gt;Q&lt;/code&gt; 中取出队首的节点 &lt;code&gt;u&lt;/code&gt;，然后把与 &lt;code&gt;u&lt;/code&gt; 相邻的所有节点 &lt;code&gt;v&lt;/code&gt; 标记为已访问过并放入队列 &lt;code&gt;Q&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;性质&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;时间复杂度：$O(V + E)$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空间复杂度：$O(V)$&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;练习题&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/number-of-islands/description/&quot;&gt;200. 岛屿数量 - 力扣（LeetCode）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>图的存储</title><link>https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E8%AE%BA/%E5%9B%BE%E7%9A%84%E5%AD%98%E5%82%A8/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E5%9B%BE%E8%AE%BA/%E5%9B%BE%E7%9A%84%E5%AD%98%E5%82%A8/</guid><pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;约定&lt;/h2&gt;
&lt;p&gt;$n$ 指代图的点数，$m$ 指代图的边数，$d^{+}(u)$ 指代点 $u$ 的出度。&lt;/p&gt;
&lt;h2&gt;邻接矩阵&lt;/h2&gt;
&lt;h3&gt;方法&lt;/h3&gt;
&lt;p&gt;使用二维数组 &lt;code&gt;adj&lt;/code&gt; 存边，其中 &lt;code&gt;adj[u][v]&lt;/code&gt; 为 1 代表存在从 $u$ 到 $v$ 的边，0 表示不存在。如果是带权图，可以存储权重。&lt;/p&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;p&gt;查询是否存在某条边：$O(1)$&lt;br /&gt;
遍历一个点的所有出边：$O(n)$&lt;br /&gt;
遍历整张图：$O(n^2)$&lt;br /&gt;
空间复杂度：$O(n^2)$&lt;/p&gt;
&lt;h3&gt;应用&lt;/h3&gt;
&lt;p&gt;邻接矩阵只适用于没有重边（或重边可以忽略）的情况。&lt;br /&gt;
优点是可以以 $O(1)$ 查询边的存在性。&lt;br /&gt;
缺点是空间复杂度高，在稀疏图上空间浪费严重。&lt;/p&gt;
&lt;h2&gt;邻接表&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E9%82%BB%E6%8E%A5%E8%A1%A8&quot;&gt;邻接表 - 维基百科，自由的百科全书&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;方法&lt;/h3&gt;
&lt;p&gt;在邻接表的表示中，对于图中的每个&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E9%A1%B6%E7%82%B9_(%E5%9B%BE%E8%AE%BA)&quot;&gt;顶点&lt;/a&gt;，将保存所有其它与之相连的顶点（即“邻接表”）。&lt;br /&gt;
在实现上使用一个支持动态增加元素的数据结构构成的数组，如 &lt;code&gt;vector&amp;lt;int&amp;gt; adj[n + 1]&lt;/code&gt; 来存边，其中 &lt;code&gt;adj[u]&lt;/code&gt; 存储的是点 $u$ 的所有出边的相关信息(终点，边权)。&lt;/p&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;p&gt;查询是否存在从 $u$ 到 $v$ 的边：$O(d^+(u))$&lt;br /&gt;
遍历 $u$ 的所有出边：$O(d^+(u))$&lt;br /&gt;
遍历整张图：$O(n+m)$&lt;br /&gt;
空间复杂度：$O(m)$&lt;/p&gt;
&lt;h3&gt;应用&lt;/h3&gt;
&lt;p&gt;具有通用性，特别是需要对一个点的所有出边进行排序的场合。&lt;/p&gt;
&lt;h2&gt;链式前向星(Chain Forward Star)&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1n94y1u7s2/?spm_id_from=333.337.search-card.all.click&amp;amp;vd_source=045b781ce97c08d03e566beb5f968ac4&quot;&gt;【图论02】动画说图的三种保存方式 降低理解门槛 邻接表 链式前向星 邻接矩阵_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;以边为基本单位，记录每条边的目标点。&lt;/p&gt;
&lt;p&gt;链式前向星是一种静态链表存储，用边集数组和邻接表相结合，可以快速访问一个顶点的所有邻接点。&lt;/p&gt;
&lt;p&gt;链式前向星存储包括两种结构：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;头结点数组：head[]，head[i]存以i为起点的最后一条边的下标（在edge[]中的下标）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;边数组：edge[]，edge[i] 表示第 i 条边。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct node {
    int to; // 第 i 条边的目标点
    int w; // 第 i 条边的边权
    int next; // 第 i 条边同起点的下一跳边的位置
} edge[maxn]

// head[u]和cnt起始设置为-1
int head[maxn] = {-1, ..., -1}; // 头结点数组, 最近一输入的以 i 为起点的边在 edge[] 中的下标
int cnt = -1;

void add(int u, int v, int w) {
    ++cnt;
    
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    edge[cnt].w = w;
    head[u] = cnt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;p&gt;查询是否存在从 $u$ 到 $v$ 的边：$O(d^+(u))$&lt;br /&gt;
遍历 $u$ 的所有出边：$O(d^+(u))$&lt;br /&gt;
遍历整张图：$O(n+m)$&lt;br /&gt;
空间复杂度：$O(m)$&lt;/p&gt;
&lt;h3&gt;应用&lt;/h3&gt;
&lt;p&gt;存各种图都很适合，但不能快速查询一条边是否存在，也不能方便地对一个点的出边进行排序。&lt;/p&gt;
&lt;p&gt;优点是边都是带编号的。&lt;/p&gt;
</content:encoded></item><item><title>BFS(搜索)</title><link>https://www.tanedanobata.top/Algorithm/%E6%90%9C%E7%B4%A2/BFS(%E6%90%9C%E7%B4%A2)/</link><guid isPermaLink="true">https://www.tanedanobata.top/Algorithm/%E6%90%9C%E7%B4%A2/BFS(%E6%90%9C%E7%B4%A2)/</guid><pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;BFS 是图论中的一种遍历算法，详见 &lt;a href=&quot;/Algorithm/%E5%9B%BE%E8%AE%BA/BFS/&quot;&gt;BFS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;BFS 在搜索中也很常用，将每个状态对应为图中的一个点即可。&lt;/p&gt;
</content:encoded></item><item><title>相机系统</title><link>https://www.tanedanobata.top/UE/Framework/Camera/%E7%9B%B8%E6%9C%BA%E7%B3%BB%E7%BB%9F/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/Framework/Camera/%E7%9B%B8%E6%9C%BA%E7%B3%BB%E7%BB%9F/</guid><pubDate>Mon, 14 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;整体流程&lt;/h2&gt;
&lt;p&gt;UE的相机系统相比于更加复杂，涉及到 &lt;code&gt;CameraComponent&lt;/code&gt;，&lt;code&gt;CameraActor&lt;/code&gt;，&lt;code&gt;PlayerController&lt;/code&gt;，&lt;code&gt;PlayerCameraManager&lt;/code&gt; 几个类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;classDiagram
    UWorld --&amp;gt; APlayerController
    UGameViewportClient --&amp;gt; ULocalPlayer : 渲染画面时获取摄像机的POV
    ULocalPlayer --&amp;gt; APlayerController : 获取POV
    APlayerController &quot;1&quot; &amp;lt;--&amp;gt; &quot;1&quot; APlayerCameraManager : APlayerController创建并驱动PlayerCameraManager
    APlayerCameraManager ..&amp;gt; AActor : 更新ViewTarget
    AActor o--&amp;gt; UCameraComponent : 计算POV
    APlayerCameraManager &amp;lt;--&amp;gt; UCameraModifier : 最后修改POV
    UCameraModifier --&amp;gt; UCameraModifier_CameraShake : 示例说明

    class UWorld {
        +Tick(...)
    }
    
    class UGameViewportClient {
        +Draw()
    }
    
    class ULocalPlayer {
        +CalcSceneView(...)
        +GetViewPoint(...)
    }
    
    class APlayerController {
        +SpawnPlayerCameraManager()
        +UpdateCameraManager()
        +GetPlayerViewPoint(...)
    }
    
    class APlayerCameraManager {
        +UpdateCamera(...)
        +UpdateViewTarget(...)
        +GetCameraCacheView()
    }
    
    class AActor {
        +CalcCamera()
    }
    
    class UCameraComponent {
        +GetCameraView(...)
    }
    
    class UCameraModifier {
        +ModifyCamera()
    }
    
    class UCameraModifier_CameraShake {
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CameraActor&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CameraComponent&lt;/code&gt; 的包装容器，以使相机可以放置在关卡中。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CameraComponent&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;代表相机的各种数据。&lt;/li&gt;
&lt;li&gt;并不直接处理数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PlayerCameraManager&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;相机系统的核心，处理相机数据并应用到 &lt;code&gt;ViewTarget&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;管理 &lt;strong&gt;ViewTarget&lt;/strong&gt;，记录摄像机跟随对象Target和摄像机POV。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总体流程上：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化ViewTarget&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;PlayerController&lt;/code&gt; 会在 &lt;code&gt;PostInitializeComponents&lt;/code&gt; 函数中调用 &lt;code&gt;SpawnPlayerCameraManager&lt;/code&gt; 函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SpawnPlayerCameraManager&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// servers and owning clients get cameras
// If no archetype specified, spawn an Engine.PlayerCameraManager.  NOTE all games should specify an archetype.
FActorSpawnParameters SpawnInfo;
SpawnInfo.Owner = this;
SpawnInfo.Instigator = GetInstigator();
SpawnInfo.ObjectFlags |= RF_Transient;  // We never want to save camera managers into a map
if (PlayerCameraManagerClass != NULL)
{
    PlayerCameraManager = GetWorld()-&amp;gt;SpawnActor&amp;lt;APlayerCameraManager&amp;gt;(PlayerCameraManagerClass, SpawnInfo);
}
else
{
    PlayerCameraManager = GetWorld()-&amp;gt;SpawnActor&amp;lt;APlayerCameraManager&amp;gt;(SpawnInfo);
}

if (PlayerCameraManager != NULL)
{
    PlayerCameraManager-&amp;gt;InitializeFor(this);
}
else
{
    // log
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;每个 &lt;code&gt;PlayerController&lt;/code&gt; 都会在世界创建一个 &lt;code&gt;PlayerCameraManager&lt;/code&gt; Actor。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;针对每个创建的 &lt;code&gt;PlayerCameraManager&lt;/code&gt;，都会调用 &lt;code&gt;InitializeFor&lt;/code&gt; 函数初始化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;InitializeFor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FMinimalViewInfo DefaultFOVCache = GetCameraCacheView();
DefaultFOVCache.FOV = DefaultFOV;
SetCameraCachePOV(DefaultFOVCache);

PCOwner = PC;

SetViewTarget(PC);

// set the level default scale
SetDesiredColorScale(GetWorldSettings()-&amp;gt;DefaultColorScale, 5.f);

// Force camera update so it doesn&apos;t sit at (0,0,0) for a full tick.
// This can have side effects with streaming.
UpdateCamera(0.f);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新ViewTarget&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;UWorld&lt;/code&gt; 会在 &lt;code&gt;Tick&lt;/code&gt; 函数中调用 &lt;code&gt;AplayerController::UpdateCameraManager&lt;/code&gt;，驱动 &lt;code&gt;PlayerCameraManager::UpdateCamera&lt;/code&gt; 更新 &lt;code&gt;ViewTarget&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;World Tick&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...

for (int32 i = 0; i &amp;lt; LevelCollections.Num(); ++i)
{
    //...
    // Update cameras last. This needs to be done before NetUpdates, and after all actors have been ticked.
    for( FConstPlayerControllerIterator Iterator = GetPlayerControllerIterator(); Iterator; ++Iterator )
    {
        if (APlayerController* PlayerController = Iterator-&amp;gt;Get())
        {
            if (!bIsPaused || PlayerController-&amp;gt;ShouldPerformFullTickWhenPaused())
            {
                PlayerController-&amp;gt;UpdateCameraManager(DeltaSeconds);
            }
            else if (PlayerController-&amp;gt;PlayerCameraManager &amp;amp;&amp;amp; FCameraPhotographyManager::IsSupported(this))
            {
                PlayerController-&amp;gt;PlayerCameraManager-&amp;gt;UpdateCameraPhotographyOnly();
            }
        }
    }
    //...
}
//...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PlayerController UpdateCameraManager&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (PlayerCameraManager != NULL)
{
    PlayerCameraManager-&amp;gt;UpdateCamera(DeltaSeconds);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PlayerCameraManager UpdateCamera&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ((PCOwner-&amp;gt;Player &amp;amp;&amp;amp; PCOwner-&amp;gt;IsLocalPlayerController()) || !bUseClientSideCameraUpdates || bDebugClientSideCamera)
{
    DoUpdateCamera(DeltaTime);
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用ViewTarget&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;UGameViewportClient&lt;/code&gt; 的 &lt;code&gt;Draw&lt;/code&gt; 方法渲染画面时，会通过 &lt;code&gt;ULocalPlayer::CalcSceneView&lt;/code&gt; 方法，最终从 &lt;code&gt;APlayerCameraManager&lt;/code&gt; 中获取 &lt;code&gt;ViewTarget&lt;/code&gt; 中的POV数据用于渲染。&lt;/p&gt;
&lt;h2&gt;处理ViewTarget&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;FViewTarget&lt;/code&gt; 在 &lt;code&gt;APlayerCameraManager::UpdateViewTarget&lt;/code&gt; 函数中进行更新。ViewTarget核心关注两个参数。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Target&lt;/strong&gt; 相机看向的目标，后面的POV基于此进行计算。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;USTRUCT(BlueprintType)  
struct FTViewTarget  
{  
    // ...
    /** Target Actor used to compute POV */  
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=TViewTarget)  
    TObjectPtr&amp;lt;class AActor&amp;gt; Target;  
    // ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;POV&lt;/strong&gt; 相对看向目标的各种参数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;摄像机控制&lt;/h2&gt;
&lt;h3&gt;角色旋转控制&lt;/h3&gt;
&lt;h3&gt;摄像机旋转控制&lt;/h3&gt;
</content:encoded></item><item><title>攀爬</title><link>https://www.tanedanobata.top/Project/OnaGameSample/%E6%94%80%E7%88%AC/</link><guid isPermaLink="true">https://www.tanedanobata.top/Project/OnaGameSample/%E6%94%80%E7%88%AC/</guid><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本篇基于 &lt;a href=&quot;https://github.com/aizawaayame/OnaGameSample/tree/master&quot;&gt;GitHub - aizawaayame/OnaGameSample: Rewrite the ALS project using C++.&lt;/a&gt; 项目。&lt;/p&gt;
&lt;p&gt;具体为 &lt;a href=&quot;https://github.com/aizawaayame/OnaGameSample/blob/master/Source/Ona/Private/CharacterLogic/CharacterMovement/OnaMantleComponent.cpp&quot;&gt;OnaGameSample/Source/Ona/Private/CharacterLogic/CharacterMovement/OnaMantleComponent.cpp at master · aizawaayame/OnaGameSample · GitHub&lt;/a&gt; 部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    A[&quot;非攀爬状态&quot;] --&amp;gt; |&quot;玩家按下起跳键或角色处于下落状态时&quot;| B[&quot;攀爬检测(Mantle Check)&quot;]
    B --&amp;gt; C{&quot;是否符合攀爬条件&quot;}
    C --&amp;gt;|否| D[&quot;执行起跳或继续下落&quot;]
    C --&amp;gt;|是| E[&quot;开始攀爬(Mantle Start)&quot;]
    E --&amp;gt; F{&quot;攀爬是否结束&quot;}
    F --&amp;gt;|否| G[&quot;攀爬更新(Mantle Update)&quot;]
    G --&amp;gt; F
    F --&amp;gt;|是| H[&quot;结束攀爬(Mantle End)&quot;]
    H --&amp;gt; A
    
    style B fill:#00BFFF,stroke:#000,stroke-width:2px
    style E fill:#00BFFF,stroke:#000,stroke-width:2px
    style G fill:#00BFFF,stroke:#000,stroke-width:2px
    style H fill:#00BFFF,stroke:#000,stroke-width:2px
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;绑定和驱动&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ctor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PrimaryComponentTick.bCanEverTick = true;  
PrimaryComponentTick.bStartWithTickEnabled = true;

MantleTimeline = CreateDefaultSubobject&amp;lt;UTimelineComponent&amp;gt;(NAME_MantleTimeline);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;UTimelineComponent&lt;/code&gt; 对象以供后续驱动使用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;BeginPlay&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FOnTimelineFloat TimelineUpdated;  
FOnTimelineEvent TimelineFinished;

TimelineUpdated.BindUFunction(this, NAME_MantleUpdate);
TimelineFinished.BindUFunction(this, NAME_MantleEnd);

MantleTimeline-&amp;gt;SetTimelineFinishedFunc(TimelineFinished);
MantleTimeline-&amp;gt;SetLooping(false);
MantleTimeline-&amp;gt;SetTimelineLengthMode(TL_TimelineLength);
MantleTimeline-&amp;gt;AddInterpFloat(MantleTimelineCurve, TimelineUpdated);

OwnerCharacter-&amp;gt;JumpPressedDelegate.AddUniqueDynamic(this,
    &amp;amp;UOnaMantleComponent::OnOwnerJumpInput);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;绑定 &lt;code&gt;MantleUpdate&lt;/code&gt; 到  &lt;code&gt;TimelineUpdate&lt;/code&gt; 事件，绑定 &lt;code&gt;MantleEnd&lt;/code&gt; 到 &lt;code&gt;TimelineFinished&lt;/code&gt; 事件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绑定 &lt;code&gt;MantleTimeline&lt;/code&gt; 的 &lt;code&gt;MantleTimelineCurve&lt;/code&gt; 曲线事件为 &lt;code&gt;TimelineUpdated&lt;/code&gt;。绑定 &lt;code&gt;MantleTimeline&lt;/code&gt; 的结束事件为 &lt;code&gt;TimelineFinished&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;绑定 &lt;code&gt;OwnerCharacter&lt;/code&gt; 的 &lt;code&gt;JumpPressedDelegate&lt;/code&gt; 事件到 &lt;code&gt;OnOwnerJumpInput&lt;/code&gt; 方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Tick&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (OwnerCharacter &amp;amp;&amp;amp; OwnerCharacter-&amp;gt;GetMovementState() == EOnaMovementState::InAir)  
{  
    if (OwnerCharacter-&amp;gt;HasMovementInput())  
    {  
        MantleCheck(FallingTraceSettings, EDrawDebugTrace::Type::ForOneFrame);  
    }  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Tick &lt;code&gt;MantleCheck&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;攀爬检测(Mantle Check)&lt;/h2&gt;
&lt;p&gt;检测是否符合攀爬条件。&lt;/p&gt;
&lt;p&gt;核心思路是进行射线检测和碰撞检测，判断是否符合攀爬条件。&lt;br /&gt;
如果是，则关闭 &lt;code&gt;Component&lt;/code&gt; 的 &lt;code&gt;Tick&lt;/code&gt; 事件，并调用 &lt;code&gt;MantleStart&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h3&gt;从Pawn位置向前碰撞检测&lt;/h3&gt;
&lt;p&gt;向前打一个胶囊体来判断前面是否有可攀爬的障碍物。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FVector&amp;amp; TraceDirection = OwnerCharacter-&amp;gt;GetActorForwardVector();  
// GetCapsuleBaseLocation 获取 Capsule 底部位置
const FVector&amp;amp; CapsuleBaseLocation = 
    UOnaMathLibrary::GetCapsuleBaseLocation(
        2.f, 
        OwnerCharacter-&amp;gt;GetCapsuleComponent());  
    
FVector TraceStart = CapsuleBaseLocation + TraceDirection * -30.f;  
TraceStart.Z += (TraceSettings.MaxLedgeHeight + TraceSettings.MinLedgeHeight) / 2.f;  

FVector TraceEnd = TraceStart + TraceDirection * TraceSettings.ReachDistance;  
float HalfHeight = 1.f + (TraceSettings.MaxLedgeHeight - TraceSettings.MinLedgeHeight) / 2.f;

FHitResult HitResult;  
{  
    const FCollisionShape CapsuleCollisionShape = 
        FCollisionShape::MakeCapsule(
            TraceSettings.ForwardTraceRadius, 
            HalfHeight);  
            
    const bool bHit = World-&amp;gt;SweepSingleByProfile(
        HitResult, 
        TraceStart, 
        TraceEnd, 
        FQuat::Identity,
        MantleObjectDetectionProfile, 
        CapsuleCollisionShape, 
        Parameters);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;details open&amp;gt;
&amp;lt;summary&amp;gt;TraceSetting&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;TraceSetting在外部定义，是根据动画来定义的。本项目只有高攀爬和低攀爬两组动画。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;MinLedgeHeight&lt;/em&gt;，最低可攀爬障碍物的高度，低于这个高度就直接走过去，而不触发攀爬。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;MaxLedegeHeight&lt;/em&gt;，最高可攀爬动画，高于这个高度，没有合适的动画进行匹配。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;ForwardTraceRadius&lt;/em&gt;，根据当前角色的胶囊体设计的前向碰撞检测的胶囊体的半径。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Capsule&lt;/em&gt;： 碰撞检测&lt;br /&gt;
Raduis: 由外部配置 &lt;code&gt;TraceSetting.FrowarTraceRadius&lt;/code&gt; 决定。&lt;br /&gt;
Height: 高度就是整个可攀爬范围高度。&lt;br /&gt;
结合后面的TraceStart.Center的位置这个 Capsule 刚好覆盖整个可攀爬的范围。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;TraceStart&lt;/em&gt;： Center放在可检测范围的中点，起始点有个-30的常量，相比当前角色胶囊体略微靠后。这样能防止角色靠近障碍物碰撞检测打出时 Penetrate 导致不正常触发攀爬。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;TraceEnd&lt;/em&gt;： TraceStart+ TraceDirection * TraceSettings.ReachDistance。ReachDistance由动画的前倾动作来决定。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (!HitResult.IsValidBlockingHit() || OwnerCharacter-&amp;gt;GetCharacterMovement()-&amp;gt;IsWalkable(HitResult))  
    return false;  
  
if (HitResult.GetComponent() != nullptr)  
{  
    UPrimitiveComponent* PrimitiveComponent = HitResult.GetComponent();  
    if (PrimitiveComponent &amp;amp;&amp;amp; PrimitiveComponent-&amp;gt;GetComponentVelocity().Size() &amp;gt; AcceptableVelocityWhileMantling)  
    {      
         return false;  
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;未检测到障碍物退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;障碍物 &lt;code&gt;Iswalkable&lt;/code&gt; 退出&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;障碍物移动速度过快退出&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;从落脚点上方向下碰撞检测&lt;/h3&gt;
&lt;p&gt;检测落脚点是否能继续行走。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FVector InitialTraceImpactPoint = HitResult.ImpactPoint;  
const FVector InitialTraceNormal = HitResult.ImpactNormal;  
  
FVector DownwardTraceEnd = InitialTraceImpactPoint;  
DownwardTraceEnd.Z = CapsuleBaseLocation.Z;  
DownwardTraceEnd += InitialTraceNormal * -15.0f;  

FVector DownwardTraceStart = DownwardTraceEnd;  
DownwardTraceStart.Z += TraceSettings.MaxLedgeHeight + TraceSettings.DownwardTraceRadius + 1.0f;  

{  
    const FCollisionShape SphereCollisionShape = 
        FCollisionShape::MakeSphere(TraceSettings.DownwardTraceRadius);  
    const bool bHit = 
        World-&amp;gt;SweepSingleByChannel(
            HitResult, 
            DownwardTraceStart, 
            DownwardTraceEnd, 
            FQuat::Identity, 
            WalkableSurfaceDetectionChannel, 
            SphereCollisionShape);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;details open&amp;gt;
&amp;lt;summary&amp;gt;TraceSetting.DownwardTraceRadius&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;和其他TraceSetting参数一样，在外部配置，该参数由角色的实际大小来决定。&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Sphere&lt;/em&gt;： 碰撞检测&lt;br /&gt;
Raduis: 由外部配置 &lt;code&gt;TraceSetting.DownwardTraceRadius&lt;/code&gt; 决定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;TraceStart&lt;/em&gt;： $(x，y)$ 的位置根据第一次碰撞结果决定。$Z$ 先算出 &lt;code&gt;TraceEnd&lt;/code&gt;，再加上 &lt;code&gt;TraceSettings.MaxLedgeHeight + TraceSettings.DownwardTraceRadius + 1&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;TraceEnd&lt;/em&gt;：取当前的角色胶囊体底部位置，并添加一个 &lt;code&gt;InitialTraceNormal * -15&lt;/code&gt; 的偏移。15这个常量来源是源攀爬动画的根位移，执行一个攀爬动作，该动画在 $Y$ 方向上会+15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (!OwnerCharacter-&amp;gt;GetCharacterMovement()-&amp;gt;IsWalkable(HitResult))  
{  
    return false;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果落脚点 &lt;code&gt;!IsWalkable&lt;/code&gt; 则退出。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;检测落脚点是否有足够空间&lt;/h3&gt;
&lt;p&gt;逻辑比较简单，就是在落脚点处构造一个 Capsule 进行碰撞检测。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FVector&amp;amp; CapsuleLocationFromBase = 
    UOnaMathLibrary::GetCapsuleLocationFromBase(
        DownTraceLocation, 
        2.f, 
        OwnerCharacter-&amp;gt;GetCapsuleComponent());  
        
const bool bCapsuleHasRoom = 
    UOnaMathLibrary::CapsuleHasRoomCheck(
        OwnerCharacter-&amp;gt;GetCapsuleComponent(), 
        CapsuleLocationFromBase, 
        0.f, 
        0.f, 
        DebugType, 
        OnaCharacterDebugComponent &amp;amp;&amp;amp; OnaCharacterDebugComponent-&amp;gt;GetShowTraces());  
        
if (!bCapsuleHasRoom)  
{  
    return false;  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;触发MantleStart方法&lt;/h3&gt;
&lt;p&gt;触发 &lt;code&gt;MantleStart&lt;/code&gt;，开始进行攀爬。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FTransform TargetTransform(
    (InitialTraceNormal * FVector(-1, -1, 0)).ToOrientationRotator(),  
    CapsuleLocationFromBase,    
    FVector::OneVector);
    
const float MantleHeight = (CapsuleLocationFromBase - OwnerCharacter-&amp;gt;GetActorLocation()).Z;  
EOnaMantleType MantleType;  
if (OwnerCharacter-&amp;gt;GetMovementState() == EOnaMovementState::InAir)  
{  
    MantleType = EOnaMantleType::FallingCatch;  
}  
else  
{  
    MantleType = MantleHeight &amp;gt; 125.0f ? EOnaMantleType::HighMantle : EOnaMantleType::LowMantle;  
}

FOnaComponentAndTransform MantleWS;  
MantleWS.Component = HitComponent;  
MantleWS.Transform = TargetTransform;  
MantleStart(MantleHeight, MantleWS, MantleType);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;根据当前状态和攀爬高度决定 &lt;code&gt;MantleType&lt;/code&gt; (&lt;code&gt;LowMantle&lt;/code&gt;，&lt;code&gt;HighMantle&lt;/code&gt;，&lt;code&gt;FallingCatch&lt;/code&gt;)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递 &lt;code&gt;HitComponnet&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传递 &lt;code&gt;TargetTransform&lt;/code&gt;，这个参数是根据，&lt;code&gt;ImpactPoint&lt;/code&gt; 来动态构造的，注意这个 Transform 的 Rotator 的 $(X，Y)$ 和 &lt;code&gt;ImpactNormal&lt;/code&gt; 的方向是相反的。这样就能指向 &lt;code&gt;ImpactPoint&lt;/code&gt; 方向。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;攀爬开始(Mantle Start)&lt;/h2&gt;
&lt;p&gt;该函数核心作用有两个&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;禁用组件Tick&lt;/h3&gt;
&lt;p&gt;组件每帧Tick会去进行MantleCheck，在开始攀爬后，暂时禁用Tick。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SetComponentTickEnabledAsync(false);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;播放蒙太奇&lt;/h3&gt;
&lt;p&gt;完成了和动画匹配的Timeline逻辑驱动，执行蒙太奇动画播放。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (MantleParams.AnimMontage &amp;amp;&amp;amp; OwnerCharacter-&amp;gt;GetMesh()-&amp;gt;GetAnimInstance())  
{  
    OwnerCharacter-&amp;gt;GetMesh()-&amp;gt;GetAnimInstance()-&amp;gt;Montage_Play(
                MantleParams.AnimMontage, 
                MantleParams.PlayRate,  
                EMontagePlayReturnType::MontageLength,  
                MantleParams.StartingPosition, 
                false);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;攀爬更新(Mantle Update)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MantleUpdate&lt;/code&gt; 实际上是由 &lt;code&gt;MantleTimeline&lt;/code&gt; 进行驱动的。&lt;/p&gt;
&lt;p&gt;核心逻辑是 &lt;strong&gt;执行和动画匹配的位移和旋转&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;攀爬结束(Mantle End)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;MantleUpdate&lt;/code&gt; 实际上是由 &lt;code&gt;MantleTimeline&lt;/code&gt; 进行驱动的。&lt;/p&gt;
&lt;p&gt;在 Timeline 播放结束后，会触发该事件。&lt;/p&gt;
&lt;p&gt;核心逻辑是重新打开组件的Tick，以使能进行 &lt;code&gt;MantleCheck&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SetComponentTickEnabledAsync(true);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>this指针</title><link>https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%AF%B9%E8%B1%A1%E5%92%8C%E7%B1%BB/this%E6%8C%87%E9%92%88/</link><guid isPermaLink="true">https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%AF%B9%E8%B1%A1%E5%92%8C%E7%B1%BB/this%E6%8C%87%E9%92%88/</guid><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当类的成员函数被调用时，&lt;code&gt;this&lt;/code&gt; 指针会被自动传递给该函数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 指针的类型是 &lt;code&gt;const Type* const&lt;/code&gt;，它是一个只读指针，指向当前对象。&lt;/p&gt;
&lt;p&gt;不能修改 &lt;code&gt;this&lt;/code&gt; 指针的值，但可以通过 &lt;code&gt;this&lt;/code&gt; 访问对象的成员。&lt;/p&gt;
&lt;h2&gt;空指针调用&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;空指针调用不访问this的成员函数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
using namespace std;

class A {
public:
    void Print() {
        cout &amp;lt;&amp;lt; &quot;A::Print()&quot; &amp;lt;&amp;lt; endl;
    }
private:
    int _a;
};

int main() {
    A* p = nullptr;
    p-&amp;gt;Print();  // 能否正常运行？
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;div class=&quot;external-embed&quot; data-pagefind-ignore&amp;gt;&amp;lt;button type=&quot;button&quot; data-embed-src=&quot;https://godbolt.org/e?readOnly=true&amp;amp;hideEditorToolbars=true#g:!((g:!((g:!((h:codeEditor,i:(filename:&quot;&amp;gt;点击加载交互代码示例&amp;lt;/button&amp;gt;&amp;lt;a href=&quot;https://godbolt.org/e?readOnly=true&amp;amp;hideEditorToolbars=true#g:!((g:!((g:!((h:codeEditor,i:(filename:&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&amp;gt;在原网站打开&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当调用 &lt;code&gt;p-&amp;gt;Print()&lt;/code&gt; 时，&lt;code&gt;this&lt;/code&gt; 指针实际上等于 &lt;code&gt;nullptr&lt;/code&gt;，但由于 &lt;code&gt;Print()&lt;/code&gt; 函数没有访问任何成员变量，因此C++允许这个调用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 指针是隐含的，虽然在函数内部会传递 &lt;code&gt;this&lt;/code&gt;，但是如果成员函数不访问任何成员变量，C++不需要解引用这个空指针，因此不会出现空指针访问的错误。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;空指针调用访问this的成员函数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include&amp;lt;iostream&amp;gt;
using namespace std;

class A {
public:
    void Print() {
        cout &amp;lt;&amp;lt; &quot;A::Print()&quot; &amp;lt;&amp;lt; endl;
        cout &amp;lt;&amp;lt; _a &amp;lt;&amp;lt; endl;
    }
private:
    int _a;
};

int main() {
    A* p = nullptr;
    p-&amp;gt;Print();  // 能否正常运行？
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;div class=&quot;external-embed&quot; data-pagefind-ignore&amp;gt;&amp;lt;button type=&quot;button&quot; data-embed-src=&quot;https://godbolt.org/e?readOnly=true&amp;amp;hideEditorToolbars=true#g:!((g:!((g:!((h:codeEditor,i:(filename:&quot;&amp;gt;点击加载交互代码示例&amp;lt;/button&amp;gt;&amp;lt;a href=&quot;https://godbolt.org/e?readOnly=true&amp;amp;hideEditorToolbars=true#g:!((g:!((g:!((h:codeEditor,i:(filename:&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&amp;gt;在原网站打开&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;当 &lt;code&gt;this&lt;/code&gt; 指针为 &lt;code&gt;nullptr&lt;/code&gt; 时，访问 &lt;code&gt;this-&amp;gt;_a&lt;/code&gt; 等同于尝试通过空指针访问成员变量。这是一种未定义行为，在大多数系统中会导致程序崩溃。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;成员变量 &lt;code&gt;_a&lt;/code&gt; 存储在对象的内存空间中，而通过空指针访问成员变量时，由于没有实际的对象空间可用，因此程序在运行时会发生崩溃。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;空指针调用成员函数&lt;/strong&gt;
总结来说，&lt;strong&gt;空指针调用成员函数本身并不会报错&lt;/strong&gt; ，因为成员函数本来就不在类中，所以不是解引用，编译时的汇编代码这里就只是一段函数的地址而已，只是这里没有对象，传过去的 &lt;code&gt;this&lt;/code&gt; 指针就是空指针，但只要该成员函数不涉及访问成员变量或其他依赖对象内存的操作那就不会报错。&lt;/p&gt;
&lt;p&gt;然而，一旦成员函数试图通过 &lt;code&gt;this&lt;/code&gt; 指针访问成员变量，程序就会崩溃，因为 &lt;code&gt;this&lt;/code&gt; 为 &lt;code&gt;nullptr&lt;/code&gt;，没有有效的内存空间可供访问。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;this指针存放位置&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;this&lt;/code&gt; 指针作为成员函数的一个隐含参数，存储在栈中。每当一个成员函数被调用时，&lt;code&gt;this&lt;/code&gt; 指针会作为函数参数被压入栈中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;this指针其实就是函数的参数而已&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>轴向</title><link>https://www.tanedanobata.top/UE/Physics/%E8%BD%B4%E5%90%91/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/Physics/%E8%BD%B4%E5%90%91/</guid><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;每次都容易忘，故记录一下UE的轴向问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UE是左手系&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;移动&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1305696520.cos.ap-shanghai.myqcloud.com/obsidian/pic/202502261650457.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
/// caption&lt;br /&gt;
&lt;strong&gt;UE 的 X 轴向前(R)，Y 轴向右(G)，Z 轴向上(B)&lt;/strong&gt;&lt;br /&gt;
///&lt;/p&gt;
&lt;h2&gt;旋转&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1305696520.cos.ap-shanghai.myqcloud.com/obsidian/pic/202502261651377.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
/// Caption&lt;br /&gt;
&lt;strong&gt;X轴-Roll，Y轴-Pitch，Z轴-Yaw&lt;/strong&gt;&lt;br /&gt;
///&lt;/p&gt;
</content:encoded></item><item><title>Return Value Optimization</title><link>https://www.tanedanobata.top/C%2B%2B/%E7%BC%96%E8%AF%91%E5%99%A8/%E7%BC%96%E8%AF%91%E5%99%A8%E4%BC%98%E5%8C%96/%E6%8B%B7%E8%B4%9D%E7%9C%81%E7%95%A5%E3%80%81RVO%E3%80%81NRVO%E4%BC%98%E5%8C%96%E7%AD%96%E7%95%A5/</link><guid isPermaLink="true">https://www.tanedanobata.top/C%2B%2B/%E7%BC%96%E8%AF%91%E5%99%A8/%E7%BC%96%E8%AF%91%E5%99%A8%E4%BC%98%E5%8C%96/%E6%8B%B7%E8%B4%9D%E7%9C%81%E7%95%A5%E3%80%81RVO%E3%80%81NRVO%E4%BC%98%E5%8C%96%E7%AD%96%E7%95%A5/</guid><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在函数返回值的传递过程中，可能会触发多次对象的拷贝构造或移动操作。为了减少这些不必要的拷贝，C++ 编译器会采用一些优化技术，如 &lt;strong&gt;拷贝省略&lt;/strong&gt; (1)、&lt;strong&gt;返回值优化&lt;/strong&gt; (2)和 &lt;strong&gt;命名返回值优化&lt;/strong&gt; (3)。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy Elision&lt;/li&gt;
&lt;li&gt;Return Value Optimization，RVO&lt;/li&gt;
&lt;li&gt;Named Return Value Optimization，NRVO&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;RVO和NRVO的启用条件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RVO 和 NRVO 是编译器自动完成的优化，但是这些优化并不总是启用，具体取决于编译器的实现和配置&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;em&gt;C++17&lt;/em&gt; 之前，&lt;strong&gt;RVO&lt;/strong&gt; 是一个可选优化，但在 &lt;em&gt;C++17&lt;/em&gt; 标准之后，&lt;strong&gt;RVO&lt;/strong&gt; 被强制启用，编译器必须在符合条件的情况下执行拷贝省略。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NRVO&lt;/strong&gt; 通常依赖于编译器的智能分析，虽然大多数现代编译器都能支持 &lt;strong&gt;NRVO&lt;/strong&gt;，但其效果和激进程度因编译器和版本的不同而有所差异。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;按值传递和拷贝省略&lt;/h2&gt;
&lt;h2&gt;不具名返回值优化&lt;/h2&gt;
&lt;p&gt;不具名返回值优化发生在返回一个无名对象或者临时对象,，一般是 &lt;code&gt;Return&lt;/code&gt; 语句中直接创建并返回的对象。&lt;/p&gt;
&lt;p&gt;URVO 从 C++98 开始已被许可，但是一直到 C++17 编译器才强制返回值优化。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;无返回值优化&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Data GetData()  
{  
    return Data{};  // ctro1  
                    // copy ctor2, dtor1  
}

int main()
{
    Data d = GetData(); // copy ctor3, dtor2
    return 0;           
                        // dtor3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象发生多次拷贝。&lt;/p&gt;
&lt;p&gt;其中第三次拷贝和赋值右值相关。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有返回值优化&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Data GetData()  
{  
    return Data{};  // ctro1  
}

int main()
{
    Data d = GetData(); 
    return 0;           
                        // dtor1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象只被构造一次。&lt;/p&gt;
&lt;h2&gt;具名返回值优化&lt;/h2&gt;
&lt;p&gt;具名返回值优化一般发生在返回一个已经创建的对象。&lt;/p&gt;
&lt;p&gt;MSVC默认不开启具名返回值优化，如果需要开启优化，需要在 &lt;code&gt;/O2&lt;/code&gt; 下编译。&lt;/p&gt;
&lt;h2&gt;容器返回值优化&lt;/h2&gt;
&lt;p&gt;对于容器来说，若整个容器发生拷贝，代价很高。&lt;/p&gt;
&lt;p&gt;因此，非常有必要考虑返回值优化。C++17 开始，强制对容器进行返回值优化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;Data&amp;gt; GetDataContainers() {
    //!!! case 1
    // return std::vector&amp;lt;Data&amp;gt;{Data{}};    // ctor 1, copy ctor 2, dtor 1

    //!!! case 2
    std::vector&amp;lt;Data&amp;gt; vec{Data{}};          // ctor 1, copy ctor 2, dtor 1
    return vec;
}                                          

int main() {
    auto d = GetDataContainers();    
    return 0;
}                                           // dtor 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;div class=&quot;external-embed&quot; data-pagefind-ignore&amp;gt;&amp;lt;button type=&quot;button&quot; data-embed-src=&quot;https://godbolt.org/e#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1DIApACYAQuYukl9ZATwDKjdAGFUtAK4sGIM2akrgAyeAyYAHI%2BAEaYxP7SAA6oCoRODB7evv6ByamOAqHhUSyx8WbSdpgO6UIETMQEmT5%2BAbaY9gUMdQ0ERZExcQm29Y3N2W0Ko31hA6VDFQCUtqhexMjsHOYAzGHI3lgA1Cbbbk5TxJisJ9gmGgCCO3sHmMenyFPoWFQ3d49muwY%2By8RxObg%2BBGIYWAvweTyBLzebi8jlohAAnrD/oDgaDTgA3apEYhYv5TJiOZCHMIEQ5oLyCOInCyHAD0rLpq0ZxEORGpWEEeCo6OpDHJQMwCkOqCohyEfzJkK8DkOABEKUxjgB2KwPQ760W0vAmHWHDTa1XMv4Gw38q1wvUG9X1CCLQ4gO2m6zWenci3a3X3G02z4gFCrWlgsHHAIOEgxsxI6N4dBJ04JkwAVjcDHMZntQYNJstDsL%2BudTAgaDFtIr5gAbIcFG6PcbTQoAHR4C2kT0s72WX0EOL%2Bk2B4MG0PhlFptwJtCJEVxnl52f8td5rM5zfbcdFrUlx6O8sahvSxJxCkkE6qqsCKZq09mRvoDVusfWifUt6qw6v%2BpdgWX5NgQ6BhvSkanNGeYLiKTAKKkwAMAma7/kwXaplG6arnyq5YXOKYbgEW65sRu6fhOlwEGsyEAFQEAgeAKEB%2B6HhRhwAH4Vq6AbsSGoHgRGRFmOgxIofh64SZu2akfm5HHhaCoHgWipgSAhLLmCdbbNghwAOKYAQFYeIITCzMQCg8R%2BCnsmAdn2XS8GvFw7HsocVE0SBakacSWmnjpY51jqxYhbuNpucuhxcL2sF0mJgR/mJLmluFrL2Q5ohKIc%2BYKVOPnXqc2m6RpgWnsFynKWFwFshykXRZyi5xfGCWifGyVlu5hmeSV8mPAe1UDYNQ0Kg8NKHCwZkMFZwUKUwKKoH%2BP76YZxmyJNcSWYszI2uxHnEMhGgqf1Q0nadBpua1PJmBwyy0Jwma8H4HBaKQqCcG4A4sgoqzrK8Ow8KQBCaDdywANYSBoGj6Jwki8CwICZlDT0vW9HC8AoIBQ0Dz03aQcCwEgmCqNU81kBQEANMACjKIYHRCAgqAAO5PQDaAsIkdAUukNPhLQ9NM8jvBsxz9DxMAXBtMLdBxBErCbELqDs9LxAAPIovzzPA0ExPIPcxBU5wvBE9UdT4E9vD8IIIhiOwUgyIIigqOoOOkLo0UGEYKCffoeDRBjkDLKgiRdBjHAALSfDepiWNYZjmmHADqYi0IcidE5Cmphxer6CsgvCoISxBQlg/uuqQxAMo4bAACqoJ4pfLN9awbHonxhDzdMM5r3C8JCmCbADjPEEwiScDwt33Y9Wuo9gOuk4cqgABz1mH9aSIcwDIFSEsdomEAfTHli9rghDxv9iy98DizLAgVxYPEZd3RwsOkPDiOkILr2G7YmOA1fE8cG2FPF2qNL442vqQQuqRnCSCAA%3D%3D&quot;&amp;gt;点击加载交互代码示例&amp;lt;/button&amp;gt;&amp;lt;a href=&quot;https://godbolt.org/e#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAMzwBtMA7AQwFtMQByARg9KtQYEAysib0QXACx8BBAKoBnTAAUAHpwAMvAFYTStJg1DIApACYAQuYukl9ZATwDKjdAGFUtAK4sGIM2akrgAyeAyYAHI%2BAEaYxP7SAA6oCoRODB7evv6ByamOAqHhUSyx8WbSdpgO6UIETMQEmT5%2BAbaY9gUMdQ0ERZExcQm29Y3N2W0Ko31hA6VDFQCUtqhexMjsHOYAzGHI3lgA1Cbbbk5TxJisJ9gmGgCCO3sHmMenyFPoWFQ3d49muwY%2By8RxObg%2BBGIYWAvweTyBLzebi8jlohAAnrD/oDgaDTgA3apEYhYv5TJiOZCHMIEQ5oLyCOInCyHAD0rLpq0ZxEORGpWEEeCo6OpDHJQMwCkOqCohyEfzJkK8DkOABEKUxjgB2KwPQ760W0vAmHWHDTa1XMv4Gw38q1wvUG9X1CCLQ4gO2m6zWenci3a3X3G02z4gFCrWlgsHHAIOEgxsxI6N4dBJ04JkwAVjcDHMZntQYNJstDsL%2BudTAgaDFtIr5gAbIcFG6PcbTQoAHR4C2kT0s72WX0EOL%2Bk2B4MG0PhlFptwJtCJEVxnl52f8td5rM5zfbcdFrUlx6O8sahvSxJxCkkE6qqsCKZq09mRvoDVusfWifUt6qw6v%2BpdgWX5NgQ6BhvSkanNGeYLiKTAKKkwAMAma7/kwXaplG6arnyq5YXOKYbgEW65sRu6fhOlwEGsyEAFQEAgeAKEB%2B6HhRhwAH4Vq6AbsSGoHgRGRFmOgxIofh64SZu2akfm5HHhaCoHgWipgSAhLLmCdbbNghwAOKYAQFYeIITCzMQCg8R%2BCnsmAdn2XS8GvFw7HsocVE0SBakacSWmnjpY51jqxYhbuNpucuhxcL2sF0mJgR/mJLmluFrL2Q5ohKIc%2BYKVOPnXqc2m6RpgWnsFynKWFwFshykXRZyi5xfGCWifGyVlu5hmeSV8mPAe1UDYNQ0Kg8NKHCwZkMFZwUKUwKKoH%2BP76YZxmyJNcSWYszI2uxHnEMhGgqf1Q0nadBpua1PJmBwyy0Jwma8H4HBaKQqCcG4A4sgoqzrK8Ow8KQBCaDdywANYSBoGj6Jwki8CwICZlDT0vW9HC8AoIBQ0Dz03aQcCwEgmCqNU81kBQEANMACjKIYHRCAgqAAO5PQDaAsIkdAUukNPhLQ9NM8jvBsxz9DxMAXBtMLdBxBErCbELqDs9LxAAPIovzzPA0ExPIPcxBU5wvBE9UdT4E9vD8IIIhiOwUgyIIigqOoOOkLo0UGEYKCffoeDRBjkDLKgiRdBjHAALSfDepiWNYZjmmHADqYi0IcidE5Cmphxer6CsgvCoISxBQlg/uuqQxAMo4bAACqoJ4pfLN9awbHonxhDzdMM5r3C8JCmCbADjPEEwiScDwt33Y9Wuo9gOuk4cqgABz1mH9aSIcwDIFSEsdomEAfTHli9rghDxv9iy98DizLAgVxYPEZd3RwsOkPDiOkILr2G7YmOA1fE8cG2FPF2qNL442vqQQuqRnCSCAA%3D%3D&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&amp;gt;在原网站打开&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fsigcpp.github.io%2F&quot;&gt;sigcpp | Special Interest Group on C++&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fshaharmike.com%2Fcpp%2Frvo%2F&quot;&gt;Return Value Optimization | Shahar Mike&apos;s Web Spot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F12953127%2Fwhat-are-copy-elision-and-return-value-optimization&quot;&gt;c++ - What are copy elision and return value optimization? - Stack Overflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.cppreference.com/w/cpp/language/copy_elision&quot;&gt;Copy elision - cppreference.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/cpp/build/reference/o-options-optimize-code?view=msvc-170&quot;&gt;/O options (Optimize code) | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>const</title><link>https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/const/</link><guid isPermaLink="true">https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/const/</guid><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;只需要记住 &lt;strong&gt;const是优先左匹配的&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>nullptr</title><link>https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/nullptr/</link><guid isPermaLink="true">https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/nullptr/</guid><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 C++11 之前，使用 &lt;code&gt;NULL&lt;/code&gt; 来表示空指针。&lt;code&gt;NULL&lt;/code&gt; 在模版匹配和重载函数匹配语义不清晰，而 &lt;code&gt;nullptr&lt;/code&gt; 具有更加确定和清晰的语义。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nullptr&lt;/code&gt; 是 c 11 中引入的新关键字，专门表示空指针，且具有特殊的类型 &lt;code&gt;nullptr_t&lt;/code&gt;。&lt;br /&gt;
1. &lt;code&gt;nullptr_t&lt;/code&gt; 是一个类型，&lt;code&gt;nullptr&lt;/code&gt; 是一个实例。&lt;br /&gt;
2. &lt;code&gt;nullptr_t&lt;/code&gt; 并不是指针类型，但能隐式转换为任意指针类型。&lt;br /&gt;
3. &lt;code&gt;nullptr_t&lt;/code&gt; 能和指针类型进行关系运算。&lt;br /&gt;
&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef NULL  
    #ifdef __cplusplus        
        #define NULL 0  
    #else  
        #define NULL ((void *)0)  
    #endif  
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;cpp 中 &lt;code&gt;void*&lt;/code&gt; 类型不能隐式转换成其他的类型指针。&lt;code&gt;int* p = (void*)0&lt;/code&gt; 在 cpp 中是非法的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cpp 中 &lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;NULL&lt;/code&gt; 是能隐式转换成指针类型的。&lt;code&gt;int* p = NULL&lt;/code&gt; 和 &lt;code&gt;int* p = 0&lt;/code&gt; 是合法的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cpp 中认为一个指针变量为 0 时为空指针。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;MSVC 中如果存在函数 &lt;code&gt;f(int)&lt;/code&gt; 和重载版本 &lt;code&gt;f(int*)&lt;/code&gt;，那么 &lt;code&gt;NULL&lt;/code&gt; 会被匹配到 &lt;code&gt;f(int)&lt;/code&gt;，而 &lt;code&gt;int* p = NULL&lt;/code&gt; 会被匹配到 &lt;code&gt;f(int*)&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;Difference&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nullptr&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;模版推导中，&lt;code&gt;nullptr&lt;/code&gt; 会被推导为 &lt;code&gt;nullptr_t&lt;/code&gt; 类型。且可隐式转换为指针类型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;NULL&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;模版推导中，&lt;code&gt;NULL&lt;/code&gt; 会被推导为 &lt;code&gt;int&lt;/code&gt; 类型。作为 &lt;code&gt;int&lt;/code&gt; 类型，可以隐式转换为指针类型。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>sizeof</title><link>https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/sizeof/</link><guid isPermaLink="true">https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/sizeof/</guid><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;指针&lt;/strong&gt;:&lt;br /&gt;
1.指针大小永远固定。取决于 &lt;strong&gt;编译的目标平台的位数&lt;/strong&gt; 。32 位就是 4 字节，64 位就是 8 字节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数组&lt;/strong&gt;：&lt;br /&gt;
1.数组 &lt;strong&gt;作为函数参数时会退化为指针&lt;/strong&gt; ，大小按指针计算。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类&lt;/strong&gt;&lt;br /&gt;
1.struct 结构体要考虑 &lt;strong&gt;字节对齐&lt;/strong&gt; 。&lt;br /&gt;
2.如果 class 配置了虚函数，则会在 &lt;strong&gt;头部添加虚函数表&lt;/strong&gt; 。&lt;br /&gt;
3.空类会用 1 个字节进行 &lt;strong&gt;占位&lt;/strong&gt; 。&lt;/p&gt;
</content:encoded></item><item><title>static</title><link>https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/static/</link><guid isPermaLink="true">https://www.tanedanobata.top/C%2B%2B/%E8%AF%AD%E6%B3%95/%E5%85%B3%E9%94%AE%E5%AD%97/static/</guid><pubDate>Thu, 10 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;局部变量&lt;/strong&gt;：&lt;br /&gt;
1. 变量只被初始化一次，保留在 &lt;strong&gt;静态区&lt;/strong&gt; 直到程序结束。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;全局变量&lt;/strong&gt;：&lt;br /&gt;
1. 变量具有 &lt;strong&gt;文件作用域&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;成员变量&lt;/strong&gt;：&lt;br /&gt;
1. 变量不会绑定到对象上。&lt;/p&gt;
</content:encoded></item><item><title>UE垃圾回收概述和应用</title><link>https://www.tanedanobata.top/UE/GC/UE%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%A6%82%E8%BF%B0%E5%92%8C%E5%BA%94%E7%94%A8/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/GC/UE%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%A6%82%E8%BF%B0%E5%92%8C%E5%BA%94%E7%94%A8/</guid><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;算法&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;方式&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;分类一&lt;/td&gt;
&lt;td&gt;引用计数式&lt;/td&gt;
&lt;td&gt;通过额外的计数来实时计算对单个对象的引用次数，当引用次数为0时回收对象。&amp;lt;br&amp;gt;如：微软COM对象。句柄功能的引用值以及C++中的智能指针都是通过引用计数来实现GC的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;追踪式（UE）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;达到GC条件时（内存不够用，到达GC回收的时或者强制GC）通过引擎系统中是否有对象的引用来判断对象是否存活，然后回收无引用对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分类二&lt;/td&gt;
&lt;td&gt;保守式&lt;/td&gt;
&lt;td&gt;不能准确识别某一个无用的对象（比如在32位程序中的一个4字节的值，它是不能判断出它是一个对象指针还是一个数字的），但是能保证在不会错误回收存活的对象的情况下回收一部分无用对象。&amp;lt;br&amp;gt;不需要额外的数据来支持查找对象的引用，它所有的内存数据假定为指针。通过一些条件来判定这个指针是否是一个合法的对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;精确式（UE）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在回收过程中能准确得到别的回收每一个无用对象的GC方式，为了准确识别每一个对象的引用，通过需要一些额外的数据（比如在类中的属性UPROPERTY）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分类三&lt;/td&gt;
&lt;td&gt;搬迁式&lt;/td&gt;
&lt;td&gt;GC过程中需要移动对象在内存中的位置，当然移动对象位置后需要修正所有引用到这个对象的地方更新到新的位置（有的通过句柄来实现，而有的可能需要修改所有引用内存的指针）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;非搬迁式（UE）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在GC过程中不需要移动对象的内存位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分类五&lt;/td&gt;
&lt;td&gt;增量式&lt;/td&gt;
&lt;td&gt;不会在对象被弃时立即回收占用的内存资源，而是在GC达成一定条件时进行回收操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;非增量式（UE）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在对象被抛弃时立即回收占用的内存资源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;UE的垃圾回收机制采用 &lt;strong&gt;追踪式，精确式，非搬迁式，非渐进式&lt;/strong&gt; 的 &lt;strong&gt;Mark-Sweep&lt;/strong&gt; 算法。&lt;/p&gt;
&lt;h2&gt;直接或间接被根节点引用的方法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;AddToRoot&lt;/code&gt; 函数，将UObject对象添加到根节点。(1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RemoveFromRoot&lt;/code&gt; 能去除 &lt;code&gt;EInternalObjectFlags::RootSet&lt;/code&gt; 标记。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 &lt;code&gt;UPROPERTY&lt;/code&gt; 保持引用。(1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;UPROPERTY&lt;/code&gt; 宏修饰 &lt;code&gt;UObject*&lt;/code&gt; 成员变量。&lt;br /&gt;
&lt;code&gt;UObject*&lt;/code&gt; 放在 &lt;code&gt;UPROPERTY&lt;/code&gt; 宏修饰的 &lt;code&gt;TArray&lt;/code&gt;、&lt;code&gt;TMap&lt;/code&gt; 中也能保持引用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Unreal implements a garbage collection scheme whereby &lt;code&gt;UObjects&lt;/code&gt; that are no longer referenced or have been explicitly flagged for destruction will be cleaned up at regular intervals. The engine builds a reference graph to determine which &lt;code&gt;UObjects&lt;/code&gt; are still in use and which ones are orphaned. At the root of this graph is a set of &lt;code&gt;UObjects&lt;/code&gt; designated as the &quot;root set&quot;. Any &lt;code&gt;UObject&lt;/code&gt; can be added to the root set. When garbage collection occurs, the engine can track all referenced &lt;code&gt;UObjects&lt;/code&gt; by searching the tree of known &lt;code&gt;UObject&lt;/code&gt; references, starting from the root set. Any unreferenced &lt;code&gt;UObjects&lt;/code&gt;, meaning those which are not found in the tree search, will be assumed to be unneeded, and will be removed.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-object-handling-in-unreal-engine#garbagecollection&quot;&gt;Unreal Object Handling in Unreal Engine | Unreal Engine 5.5 Documentation | Epic Developer Community&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;针对UPROPERTY的优化设计&lt;/h2&gt;
&lt;p&gt;UE源码中，能看到很多的Property并没有被标记为 &lt;code&gt;UPROPERTY&lt;/code&gt;。这种设计往往会通过持有引用的方式来保证不被垃圾回收。&lt;/p&gt;
&lt;p&gt;这样做的目的是优化垃圾回收的性能，减少特别追踪的数量。&lt;/p&gt;
</content:encoded></item><item><title>Actor Role and Remote Role</title><link>https://www.tanedanobata.top/UE/Network/Actor%20Role%20and%20Remote%20Role/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/Network/Actor%20Role%20and%20Remote%20Role/</guid><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Get Role 接口&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;const ENetRole LocalRole = MyActor-&amp;gt;GetLocalRole();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取当前Actor的LocalRole&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ENetRole RemoteRole = MyActor-&amp;gt;GetRemoteRole();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取当前Actor的RemoteRole&lt;/p&gt;
&lt;h2&gt;Actor Role States&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无角色。此 Actor 不会被复制。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_SimulatedProxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;此 Actor 的模拟代理。此 Actor 模拟真实状态，但无权更改状态或调用远程函数。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_AutonomousProxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;此 Actor 的自主代理。此 Actor 模拟真实状态，并且有权更改状态和调用远程函数。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_Authority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对此 Actor 的权威控制。此 Actor 拥有网络连接的真实状态，并且有权更改状态和调用函数。此 Actor 还负责跟踪属性更改并将它们复制到连接的客户端。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;void APawn::PossessedBy(AController* NewController)
{
    //...
    if (APlayerController* PlayerController = Cast&amp;lt;APlayerController&amp;gt;(Controller))
    {
        if (GetNetMode() != NM_Standalone)  
        {  
            SetReplicates(true);  
            SetAutonomousProxy(true);  
        }
    }
    //...
}

void AActor::SetAutonomousProxy(const bool bInAutonomousProxy, const bool bAllowForcePropertyCompare)
{
    if (bReplicates)
    {
        //...
        const TEnumAsByte&amp;lt;enum ENetRole&amp;gt; OldRemoteRole = RemoteRole;  
        RemoteRole = (bInAutonomousProxy ? ROLE_AutonomousProxy : ROLE_SimulatedProxy);
        //...
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;PossessedBy&lt;/code&gt; 函数中这只 &lt;code&gt;RemoteRole&lt;/code&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置函数为 &lt;code&gt;Actor::SetAutonomousProxy&lt;/code&gt;, 如果存在主控客户端则设置为 &lt;code&gt;ROLE_AutonomousProxy&lt;/code&gt;，否则默认为 &lt;code&gt;ROLE_SimulatedProxy&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Actor Role Matrix&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Local Role  本地角色&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Remote Role  远程角色&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Server or Client  服务器或客户端&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description  描述&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_Authority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROLE_AutonomousProxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server  服务器&lt;/td&gt;
&lt;td&gt;此 Actor Pawn 由连接的客户端之一控制。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_AutonomousProxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROLE_Authority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client  客户端&lt;/td&gt;
&lt;td&gt;此 Actor Pawn 由此连接的客户端控制。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_SimulatedProxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROLE_Authority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client  客户端&lt;/td&gt;
&lt;td&gt;此 Actor Pawn 由其他连接的客户端之一控制。&amp;lt;br&amp;gt;未被任何客户端控制的复制 Actor 也可以具有此角色组合。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ROLE_Authority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROLE_None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client  客户端&lt;/td&gt;
&lt;td&gt;这是一个未复制的 Actor。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>相机模块设计</title><link>https://www.tanedanobata.top/Project/OnaGameSample/%E7%9B%B8%E6%9C%BA%E6%A8%A1%E5%9D%97%E8%AE%BE%E8%AE%A1/</link><guid isPermaLink="true">https://www.tanedanobata.top/Project/OnaGameSample/%E7%9B%B8%E6%9C%BA%E6%A8%A1%E5%9D%97%E8%AE%BE%E8%AE%A1/</guid><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ALS的相机模块利用一个函数实现，在实际项目中，这样的做法存在很多问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;多种相机模式耦合，不利于拓展和维护。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相机任何修改都需要QA全量回归，不利于项目迭代。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;整体设计&lt;/h2&gt;
&lt;p&gt;整体设计上，采用Stack-State-Processor的架构。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Stack层：负责相机模式切换，并提供一个统一的接口给上层使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;State层：负责相机的具体模式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Processor层：负责处理相机单一职责，如跟随、视角转换等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Actor</title><link>https://www.tanedanobata.top/UE/Framework/Actor/Actor/</link><guid isPermaLink="true">https://www.tanedanobata.top/UE/Framework/Actor/Actor/</guid><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;What Is&lt;/strong&gt; :&lt;br /&gt;
Actor 是可以放置在关卡中的任何对象，例如摄像机、静态网格体或玩家起始位置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Can Do&lt;/strong&gt; :&lt;br /&gt;
Actor 支持 3D 变换，例如平移、旋转和缩放。&lt;br /&gt;
Actor 可以通过游戏代码（C++或蓝图）创建（生成）和销毁。&lt;br /&gt;
Actor 的支持网络复制属性和函数调用。&lt;/p&gt;
&lt;h2&gt;组件&lt;/h2&gt;
&lt;p&gt;Actor 视为容纳称为组件的特殊类型对象的容器。不同类型的组件可用于控制 Actor 的移动方式、渲染方式等。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;UActorComponent&lt;/code&gt; 是所有组件的基类。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;USceneComponent&lt;/code&gt; 具有变换的 &lt;code&gt;UActorComponent&lt;/code&gt; 。&lt;br /&gt;
Actor 的变换由 &lt;code&gt;USceneComponent&lt;/code&gt; 层级结构根部的 &lt;code&gt;USceneComponent&lt;/code&gt; 决定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;UPrimitiveComponent&lt;/code&gt; 是所有图形表示几何组件的基类。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;根组件&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What Is&lt;/strong&gt; :&lt;br /&gt;
每个 &lt;code&gt;Actor&lt;/code&gt; 都有一个 &lt;code&gt;RootComponent&lt;/code&gt; 且 &lt;code&gt;RootComponent&lt;/code&gt; 必须是 &lt;code&gt;USceneComponent&lt;/code&gt; 的子类。&lt;br /&gt;
&lt;strong&gt;What Can Do&lt;/strong&gt; :&lt;br /&gt;
提供世界的 Transform 信息。&lt;br /&gt;
提供层次化组织 &lt;code&gt;Components&lt;/code&gt; 和 &lt;code&gt;Actor&lt;/code&gt; 的能力。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;类型为 &lt;code&gt;USceneComponent*&lt;/code&gt;，是 &lt;code&gt;Actor&lt;/code&gt; 的根场景组件，负责定义 &lt;code&gt;Actor&lt;/code&gt; 在世界中的位置、旋转和缩放。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有其他 &lt;code&gt;USceneComponent&lt;/code&gt; 类型的组件（例如 &lt;code&gt;UStaticMeshComponent&lt;/code&gt;、&lt;code&gt;USkeletalMeshComponent&lt;/code&gt;）通过 &lt;code&gt;AttachToComponent&lt;/code&gt; 形成层次结构，挂接到 &lt;code&gt;RootComponent&lt;/code&gt; 或其他场景组件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;RootComponent&lt;/code&gt; 为 &lt;code&gt;nullptr&lt;/code&gt;，&lt;code&gt;Actor&lt;/code&gt; 不会出现在场景中（无法进行变换）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;UPROPERTY(BlueprintGetter=K2_GetRootComponent, Category=&quot;Transformation&quot;)
TObjectPtr&amp;lt;USceneComponent&amp;gt; RootComponent;

/**
* Sets root component to be the specified component.  NewRootComponent&apos;s owner should be this actor.
* @return true if successful
*/
ENGINE_API bool SetRootComponent(USceneComponent* NewRootComponent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Actor本身没有变换，Actor的变换由根 &lt;code&gt;RootComponent&lt;/code&gt; 决定。&lt;/p&gt;
&lt;p&gt;具体来说，如果此 Root Component 是 &lt;code&gt;SceneComponent&lt;/code&gt;，它将提供 Actor 的变换信息。否则，Actor 将没有变换信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FTransform&amp;amp; AActor::GetTransform() const  
{  
    return ActorToWorld();  
}

FORCEINLINE const FTransform&amp;amp; AActor::ActorToWorld() const  
{  
    return (RootComponent ? RootComponent-&amp;gt;GetComponentTransform() : FTransform::Identity);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AActor::GetTransform()&lt;/code&gt; 返回 &lt;code&gt;ActorToWorld()&lt;/code&gt; 的结果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ActorToWorld()&lt;/code&gt; 返回 &lt;code&gt;RootComponent&lt;/code&gt; 的变换。如果没有 &lt;code&gt;RootComponent&lt;/code&gt; ，则返回 &lt;code&gt;FTransform::Identity&lt;/code&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;组件的绑定和注册&lt;/h2&gt;
&lt;h3&gt;静态绑定&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 Blueprint 或 C++ 类中定义的组件通过 UPROPERTY() 宏标记组件为 AActor 的成员变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在构建函数中通过 &lt;code&gt;CreateDefaultSubobject&lt;/code&gt; 创建实例绑定到变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是 &lt;code&gt;SceneComponent&lt;/code&gt;，调用 &lt;code&gt;SetupAttachment&lt;/code&gt; 函数设置层次结构。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;details open&amp;gt;
&amp;lt;summary&amp;gt;SetupAttachment 和 AttachtoComponent&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SetupAttachment&lt;/code&gt; 初始化组件预期附加的父组件(和插槽)信息，在组件注册时生效。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SetupAttachment&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设计上预期在构造函数中创建的组件应使用 SetupAttachment。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只是设置预期的附加信息，不会立即执行附加操作。(仅设置&lt;code&gt;AttachParent&lt;/code&gt;和&lt;code&gt;AttachSocketName&lt;/code&gt;两个变量，并标记&lt;code&gt;bShouldBeAttached&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;details open&amp;gt;
&amp;lt;summary&amp;gt;Comments In SceneComponent.h&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Initializes desired Attach Parent and SocketName to be attached to when the component is registered.&lt;/p&gt;
&lt;p&gt;Generally intended to be called from its Owning Actor&apos;s constructor and should be preferred over AttachToComponent when a component is not registered.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AttachtoComponent&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设计上预期在运行时动态创建的组件应使用 AttachToComponent。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;会立即执行附加操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无论是否注册，都可以调用这个方法。但是在注册前优先调用&lt;code&gt;SetupAttachment&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;details open&amp;gt;
&amp;lt;summary&amp;gt;Comments In SceneComponent.h&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Attach this component to another scene component, optionally at a named socket. It is valid to call this on components whether or not they have been Registered, however from constructor or when not registered it is preferable to use SetupAttachment.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;动态绑定&lt;/h3&gt;
&lt;p&gt;运行时通过 &lt;code&gt;AActor::AddComponent&lt;/code&gt; 添加一个新组建到Actor，并分配所有权到该 Actor。&lt;/p&gt;
&lt;p&gt;支持自动附加（第一个组件成为根组件，后续组件挂载到根下），但如果设置 &lt;code&gt;bManualAttachment&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;，则需要手动处理附加逻辑。&lt;/p&gt;
&lt;p&gt;&amp;lt;details open&amp;gt;
&amp;lt;summary&amp;gt;Comment In Actor.h&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;p&gt;Creates a new component and assigns ownership to the Actor this is called for.&lt;/p&gt;
&lt;p&gt;Automatic attachment causes the first component created to become the root, and all subsequent components to be attached under that root.&lt;/p&gt;
&lt;p&gt;When bManualAttachment is set, automatic attachment is skipped and it is up to the user to attach the resulting component (or set it up as the root) themselves.&lt;/p&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;注册和注销&lt;/h3&gt;
&lt;h2&gt;组件和Actor层级结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[Actor] --&amp;gt; B[&quot;Root Component(SceneComponent)&quot;]
    A --&amp;gt; C[Actor Component]
    A --&amp;gt; D[Actor Component]
    A --&amp;gt; E[&quot;...&quot;]
    B --&amp;gt; F[Scene Component]
    B --&amp;gt; G[Scene Component]
    F --&amp;gt; H[Scene Component]
    G --&amp;gt; I[Scene Component]
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>实现一个基础的智能指针</title><link>https://www.tanedanobata.top/posts/smart-pointer/</link><guid isPermaLink="true">https://www.tanedanobata.top/posts/smart-pointer/</guid><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;实现一个基础的智能指针。
&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;智能指针想要解决的问题&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class TestObject //测试用对象
{
public:
    //构造函数
    TestObject()
    {
        cout &amp;lt;&amp;lt; &quot;构造TestObject&quot; &amp;lt;&amp;lt; endl;
    }
    //析构函数
    ~TestObject()
    {
        cout &amp;lt;&amp;lt; &quot;析构TestObject&quot; &amp;lt;&amp;lt; endl;
    }
    //测试函数
    void TestFunc()
    {
        cout &amp;lt;&amp;lt; &quot;TestFunc&quot; &amp;lt;&amp;lt; endl;
    }
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以选择在“栈”中创建这个对象，或是在“堆”中创建这个对象（即用 &lt;code&gt;new&lt;/code&gt;），但他们都面临各自的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在栈中创建对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()  
{  
    TestObject* ptr;    //声明一个对象指针  
    {  
        TestObject test;//创建一个对象  
        ptr = &amp;amp;test;    //指针指向这个对象  
    }  
    ptr-&amp;gt;TestFunc();//此处虽然尝试调用，但是“test”已经被销毁了  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果在“栈”中创建这个对象，那么对象保证可以释放掉内存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但缺点是：它只能在作用域内起效，离开了作用域就将销毁。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;在堆中创建对象&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main()  
{  
    TestObject* ptr = new TestObject();  
    ptr-&amp;gt;TestFunc();  
}  
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在“堆”中创建对象，可以保证对象在离开作用域后仍然存在。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但缺点是：需要手动释放内存。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;智能指针&lt;/h2&gt;
&lt;p&gt;智能指针既能在离开作用域之后继续使用，又不用额外的设计来放置对象不被销毁。&lt;br /&gt;
一种实现这种理想的方式是：&lt;strong&gt;引用计数&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;思路是维护一个 &lt;strong&gt;引用计数器&lt;/strong&gt; 来统计这个对象被引用的次数，一旦其值变为0则销毁它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class SmartPointer //智能指针
{
private: //内部的定义：
    class Counter   //引用计数器，一个对象的所有智能指针应该公用一个计数器
    {
    public:
        T* ObjectPtr;       //对象的指针
        int ReferenceCount; //引用计数

        Counter(T* Ptr)//构造函数中得到对象指针，引用计数设为1
        {
            ObjectPtr = Ptr;
            ReferenceCount = 1;
        }
        ~Counter()//析构函数中销毁对象
        {
            delete ObjectPtr;
        }
    };

private: //私有成员：
    Counter* MyCounter;     //指向引用计数器的指针

public:
    //构造函数（使用普通对象指针）
    SmartPointer(T* Ptr)
    {
        MyCounter = new Counter(Ptr);
        cout &amp;lt;&amp;lt; &quot;使用普通对象指针来构造&quot; &amp;lt;&amp;lt; endl;
    }
    //构造函数（使用其他智能指针）
    SmartPointer(const SmartPointer &amp;amp;Other)
    {
        MyCounter = Other.MyCounter;
        MyCounter-&amp;gt;ReferenceCount++;
        cout &amp;lt;&amp;lt; &quot;使用其他智能指针来构造，引用计数：&quot;&amp;lt;&amp;lt; MyCounter-&amp;gt;ReferenceCount &amp;lt;&amp;lt; endl;
    }
    //析构函数
    ~SmartPointer()
    {
        MyCounter-&amp;gt;ReferenceCount--;
        cout &amp;lt;&amp;lt; &quot;智能指针对象析构，当前引用计数：&quot; &amp;lt;&amp;lt; MyCounter-&amp;gt;ReferenceCount &amp;lt;&amp;lt; endl;
        if (MyCounter-&amp;gt;ReferenceCount == 0)
            delete MyCounter;   
    }
    //重载“-&amp;gt;”运算符，让它能像普通对象指针一样使用
    T* operator-&amp;gt;() const
    {
        return MyCounter-&amp;gt;ObjectPtr;
    }
};

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>走跑混合</title><link>https://www.tanedanobata.top/Project/OnaGameSample/%E8%B5%B0%E8%B7%91%E6%B7%B7%E5%90%88/</link><guid isPermaLink="true">https://www.tanedanobata.top/Project/OnaGameSample/%E8%B5%B0%E8%B7%91%E6%B7%B7%E5%90%88/</guid><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Ciallo ～(∠・ω&amp;lt; )⌒★!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/482748722&quot;&gt;知乎&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本文讨论基于学习用的 ALSCPP 项目 &lt;a href=&quot;https://github.com/aizawaayame/OnaGameSample/tree/master&quot;&gt;GitHub - aizawaayame/OnaGameSample: Rewrite the ALS project using C++.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Concept&lt;/h2&gt;
&lt;p&gt;在一切开始前，先明确几个概念的约定，方便后续的讨论和实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步态&lt;/strong&gt; &lt;code&gt;Gait&lt;/code&gt;：&lt;br /&gt;
1. 代表了角色处于 Walk，Run，Sprint 三个状态。这三个状态由用户输入决定，而不是取决于角色当前的速度。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步型&lt;/strong&gt; &lt;code&gt;Walk/Run Blend&lt;/code&gt;：&lt;br /&gt;
1. 代表 Walk→Run 的混合度。不知道怎么命名了，就叫歩型吧。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步幅&lt;/strong&gt; &lt;code&gt;Stride&lt;/code&gt;：&lt;br /&gt;
1. 角色迈步的幅度。作为混合空间的其中一个轴向。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;步频&lt;/strong&gt; &lt;code&gt;Play Rate&lt;/code&gt;：&lt;br /&gt;
1. 角色迈步的频率。作为动画播放的频率。&lt;/p&gt;
&lt;h3&gt;定义2D混合空间&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1305696520.cos.ap-shanghai.myqcloud.com/obsidian/pic/202503171645577.png&quot; alt=&quot;纵轴定义 Walk/Run 的混合，横轴定义了步幅&quot; /&gt;&lt;br /&gt;
/// caption&lt;br /&gt;
纵轴定义 Walk/Run 的混合，横轴定义了步幅。&lt;/p&gt;
&lt;p&gt;Idle→walk→run 的一维混合空间无法处理直接起步跑的情景。而二维空间可以。&lt;br /&gt;
///&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1305696520.cos.ap-shanghai.myqcloud.com/obsidian/pic/202503171650126.png&quot; alt=&quot;image.png&quot; /&gt;&lt;br /&gt;
/// caption&lt;br /&gt;
混合空间左边起点设计了不同于 Idle 的起步姿势，且走跑各不相同。这是因为项目没有起步动画，而是通过 Idle 和起步姿势的混合来做到起步的。&lt;br /&gt;
///&lt;/p&gt;
&lt;h2&gt;步态，歩型，步幅和步频&lt;/h2&gt;
&lt;h3&gt;Gait&lt;/h3&gt;
&lt;p&gt;由用户的输入进行切换。在 ALS 中，采用 &lt;code&gt;Shift&lt;/code&gt; 和 &lt;code&gt;Ctrl&lt;/code&gt; 进行切换。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;逻辑步态&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;根据玩家输入进行切换。&lt;/li&gt;
&lt;li&gt;存储在&lt;code&gt;CharacterInformation.Gait&lt;/code&gt;，每帧更新。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;动画步态&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;当前实际播放的动画执行的步态。&lt;/li&gt;
&lt;li&gt;动画步态存储在骨骼曲线 &lt;code&gt;Weight_Gait&lt;/code&gt; 中，由动画驱动更新。执行不同动画时会被动画动态修改。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Walk/Run Blend&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;return CharacterInformation.Gait.IsWalking() ? 0.0f : 1.0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存储在 &lt;code&gt;Grounded.WalkRunBlend&lt;/code&gt;。每帧由函数 &lt;code&gt;CalclulateWalkRunBlend&lt;/code&gt; 计算。&lt;/p&gt;
&lt;p&gt;计算非常简单，单纯检查当前步态。&lt;/p&gt;
&lt;h3&gt;Stride&lt;/h3&gt;
&lt;h3&gt;Play Rate&lt;/h3&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/482748722&quot;&gt;UE4/UE5 骨骼动画 高级运动系统 走跑混合初步&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>八向移动</title><link>https://www.tanedanobata.top/Project/OnaGameSample/%E5%85%AB%E5%90%91%E7%A7%BB%E5%8A%A8/</link><guid isPermaLink="true">https://www.tanedanobata.top/Project/OnaGameSample/%E5%85%AB%E5%90%91%E7%A7%BB%E5%8A%A8/</guid><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Ciallo ～(∠・ω&amp;lt; )⌒★!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文讨论基于学习用的 ALSCPP 项目：&lt;br /&gt;
&lt;a href=&quot;https://github.com/aizawaayame/OnaGameSample&quot;&gt;GitHub - aizawaayame/OnaGameSample: Rewrite the ALS project using C++.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. 问题&lt;/h2&gt;
&lt;p&gt;在项目中我们有以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何在缺少具有方向的八向动画下实现转向?&lt;/li&gt;
&lt;li&gt;如何解决极端变换下导致的臀位扭转穿帮?&lt;/li&gt;
&lt;li&gt;如何实现轴心切换时的前倾和后仰?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 思路&lt;/h2&gt;
&lt;h3&gt;2.1. 转向思路&lt;/h3&gt;
&lt;p&gt;ALS 的只有六个动画。&lt;br /&gt;
分别是正前，正后，左进，左退，右进，右退。实际上只有四个方向的动画。&lt;br /&gt;
为了实现八向动画引入 YawOffset 来实现实际的转向。这个 YawOffset 会在代码中读取，并用于修改角色的 Rot。&lt;/p&gt;
&lt;h3&gt;2.2. 臀位思路&lt;/h3&gt;
&lt;p&gt;构建六向状态机，通过构造环形变换来限制极端变换的出现。这个见后文实现部分的描述。&lt;/p&gt;
&lt;h3&gt;2.3. 轴心切换思路&lt;/h3&gt;
&lt;p&gt;实现轴心切换可以在部分状态机切换时触发&quot;pivot&quot;事件，来添加前倾和后仰。后文的状态机切换部分会进行描述。&lt;/p&gt;
&lt;h2&gt;3. 实现&lt;/h2&gt;
&lt;h3&gt;3.1. 数据&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;digraph G {
  graph [rankdir=&quot;TB&quot;]; // Assuming a Top-to-Bottom layout

  // Define central nodes (adjust names based on the image)
  node [shape=ellipse, style=filled, fillcolor=lightblue];
  central_node1 [label=&quot;Data&quot;];


  // Define intermediate nodes (adjust names and shapes)
  node [shape=box, style=rounded];
  intermediate_a [label=&quot;Movement Data&quot;];
  intermediate_b [label=&quot;Anim Curve&quot;];
  intermediate_c [label=&quot;Anim Skeleton&quot;];

  // Define leaf nodes (adjust names and shapes)
  node [shape=plaintext];
  leaf_1 [label=&quot;MovementDirection&quot;];
  leaf_2 [label=&quot;VelocityBlend&quot;];
  leaf_3 [label=&quot;Feet_Crossin&quot;];
  leaf_4 [label=&quot;HipOrientation_Bias&quot;];
  leaf_YawOffset [label=&quot;YawOffset&quot;];
  leaf_5 [label=&quot;Blend Profile&quot;];


  // Define edges and their directions
  central_node1 -&amp;gt; intermediate_a;
  central_node1 -&amp;gt; intermediate_b;
  central_node1 -&amp;gt; intermediate_c;

  intermediate_a -&amp;gt; leaf_1;
  intermediate_a -&amp;gt; leaf_2;

  intermediate_b -&amp;gt; leaf_3;
  intermediate_b -&amp;gt; leaf_4;
  intermediate_b -&amp;gt; leaf_YawOffset
  
  intermediate_c -&amp;gt; leaf_5;

  // You might need to add more nodes and edges based on the image.
  // You can also customize node shapes, colors, and labels further.
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3.1.1. Movement 数据&lt;/h4&gt;
&lt;h5&gt;3.1.1.1. 移动方向 MovementDirection&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;digraph direction_diagram {
    // 设置图形属性
    bgcolor=&quot;#e0f0e0&quot;;
    layout=neato;
    size=&quot;6,6&quot;;
    
    // 隐藏默认节点和边的样式
    node [shape=none, fontname=&quot;Arial&quot;, fontsize=16];
    edge [color=blue, arrowhead=none];
    
    // 主要方向标签
    F [pos=&quot;0,2.5!&quot;, label=&quot;F&quot;, fontsize=24];
    B [pos=&quot;0,-2.5!&quot;, label=&quot;B&quot;, fontsize=24];
    L [pos=&quot;-2.5,0!&quot;, label=&quot;L&quot;, fontsize=24];
    R [pos=&quot;2.5,0!&quot;, label=&quot;R&quot;, fontsize=24];
    
    // 坐标轴标签
    X [pos=&quot;0.3,1.5!&quot;, label=&quot;x&quot;, fontsize=14];
    Y [pos=&quot;1.5,-0.2!&quot;, label=&quot;y&quot;, fontsize=14];
    
    // 象限标签
    FL [pos=&quot;-1.2,1.2!&quot;, label=&quot;FL&quot;];
    FR [pos=&quot;1.2,1.2!&quot;, label=&quot;FR&quot;];
    BL [pos=&quot;-1.2,-1.2!&quot;, label=&quot;BL&quot;];
    BR [pos=&quot;1.2,-1.2!&quot;, label=&quot;BR&quot;];
    
    // 角度标签
    angle70 [pos=&quot;0.5,0.3!&quot;, label=&quot;70°&quot;, fontsize=14];
    angle110 [pos=&quot;0.8,-0.3!&quot;, label=&quot;110°&quot;, fontsize=14];
    angle_neg70 [pos=&quot;-0.5,0.3!&quot;, label=&quot;-70°&quot;, fontsize=14];
    angle_neg110 [pos=&quot;-0.8,-0.3!&quot;, label=&quot;-110°&quot;, fontsize=14];
    
    // 坐标原点（隐藏）
    origin [pos=&quot;0,0!&quot;, shape=point, width=0.01, style=invis];
    
    // 坐标轴
    origin -&amp;gt; F [pos=&quot;0,0 0,2.5&quot;, color=blue, penwidth=1.5, arrowhead=normal];
    origin -&amp;gt; R [pos=&quot;0,0 2.5,0&quot;, color=blue, penwidth=1.5, arrowhead=normal];
    
    // 左区域框
    left_box_tl [pos=&quot;-2.2,1.7!&quot;, shape=point, width=0.01, style=invis];
    left_box_tr [pos=&quot;-0.7,1.7!&quot;, shape=point, width=0.01, style=invis];
    left_box_bl [pos=&quot;-2.2,-1.7!&quot;, shape=point, width=0.01, style=invis];
    left_box_br [pos=&quot;-0.7,-1.7!&quot;, shape=point, width=0.01, style=invis];
    
    left_box_tl -&amp;gt; left_box_tr -&amp;gt; left_box_br -&amp;gt; left_box_bl -&amp;gt; left_box_tl [color=blue, penwidth=2];
    
    // 右区域框
    right_box_tl [pos=&quot;0.7,1.7!&quot;, shape=point, width=0.01, style=invis];
    right_box_tr [pos=&quot;2.2,1.7!&quot;, shape=point, width=0.01, style=invis];
    right_box_bl [pos=&quot;0.7,-1.7!&quot;, shape=point, width=0.01, style=invis];
    right_box_br [pos=&quot;2.2,-1.7!&quot;, shape=point, width=0.01, style=invis];
    
    right_box_tl -&amp;gt; right_box_tr -&amp;gt; right_box_br -&amp;gt; right_box_bl -&amp;gt; right_box_tl [color=blue, penwidth=2];
    
    // 对角线（红色）
    dl1 [pos=&quot;-2.5,2.5!&quot;, shape=point, width=0.01, style=invis];
    dl2 [pos=&quot;2.5,-2.5!&quot;, shape=point, width=0.01, style=invis];
    dl3 [pos=&quot;-2.5,-2.5!&quot;, shape=point, width=0.01, style=invis];
    dl4 [pos=&quot;2.5,2.5!&quot;, shape=point, width=0.01, style=invis];
    
    dl1 -&amp;gt; dl2 [color=red, penwidth=1.5];
    dl3 -&amp;gt; dl4 [color=red, penwidth=1.5];
    
    // 角度弧线标记
    arc1 [pos=&quot;0.3,0.3!&quot;, shape=point, width=0.01, style=invis];
    arc2 [pos=&quot;0.3,-0.3!&quot;, shape=point, width=0.01, style=invis];
    arc3 [pos=&quot;-0.3,0.3!&quot;, shape=point, width=0.01, style=invis];
    arc4 [pos=&quot;-0.3,-0.3!&quot;, shape=point, width=0.01, style=invis];
    
    arc1 -&amp;gt; arc2 -&amp;gt; arc4 -&amp;gt; arc3 -&amp;gt; arc1 [color=blue, style=dashed, penwidth=0.5];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算 VelocityBlend 分为两步：&lt;br /&gt;
计算当前 ControlledRotation 和 VelocityRotation 之间的插值 Delta&lt;br /&gt;
根据 Delta 决定当前的移动方向&lt;/p&gt;
&lt;h5&gt;3.1.1.2. 速度混合 VelocityBlend&lt;/h5&gt;
&lt;p&gt;计算 VelocityBlend 分为两步：&lt;br /&gt;
计算目标 VelocityBlend (TargetBlend)&lt;br /&gt;
插值 VelocityBlend&lt;/p&gt;
&lt;h6&gt;3.1.1.2.1. 获取 TargetBlend&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;const FVector LocRelativeVeclocityDir = CharacterInformation.CharacterActorRotation.UnrotateVector(CharacterInformation.Velocity.GetSafeNormal(0.1f));
  
const float Sum = FMath::Abs(LocRelativeVeclocityDir.X) + FMath::Abs(LocRelativeVeclocityDir.Y) + FMath::Abs(LocRelativeVeclocityDir.Z);  
const FVector RelativeDir = LocRelativeVeclocityDir / Sum;  
FOnaVelocityBlend TargetBlend;  
TargetBlend.F = FMath::Clamp(RelativeDir.X, 0.0f, 1.0f);  
TargetBlend.B = FMath::Abs(FMath::Clamp(RelativeDir.X, -1.0f, 0.0f));  
TargetBlend.L = FMath::Abs(FMath::Clamp(RelativeDir.Y, -1.0f, 0.0f));  
TargetBlend.R = FMath::Clamp(RelativeDir.Y, 0.0f, 1.0f);  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;向量归一化&lt;/strong&gt;：&lt;br /&gt;
将速度向量归一化后从世界坐标系转换到角色的局部坐标系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const FVector LocRelativeVeclocityDir = CharacterInformation.CharacterActorRotation.UnrotateVector(CharacterInformation.Velocity.GetSafeNormal(0.1f));  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;混合方向权重&lt;/strong&gt;：&lt;br /&gt;
计算局部空间速度向量各分量绝对值的总和。这里使用绝对值是为了处理负方向值：&lt;code&gt;const float Sum = FMath::Abs(LocRelativeVeclocityDir.X) + FMath::Abs(LocRelativeVeclocityDir.Y) + FMath::Abs(LocRelativeVeclocityDir.Z); &lt;/code&gt;&lt;br /&gt;
将局部空间速度向量除以前面计算的绝对值总和，得到一个特殊的&quot;归一化&quot;向量：&lt;code&gt;const FVector RelativeDir = LocRelativeVeclocityDir / Sum;&lt;/code&gt;&lt;br /&gt;
计算混合权重&lt;/p&gt;
&lt;h6&gt;3.1.1.2.2. 插值 TargetBlend&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;const FOnaVelocityBlend&amp;amp; TargetBlend = CalculateVelocityBlend();  
VelocityBlend.F = FMath::FInterpTo(VelocityBlend.F, TargetBlend.F, DeltaSeconds, Config.VelocityBlendInterpSpeed);  
VelocityBlend.B = FMath::FInterpTo(VelocityBlend.B, TargetBlend.B, DeltaSeconds, Config.VelocityBlendInterpSpeed);  
VelocityBlend.L = FMath::FInterpTo(VelocityBlend.L, TargetBlend.L, DeltaSeconds, Config.VelocityBlendInterpSpeed);  
VelocityBlend.R = FMath::FInterpTo(VelocityBlend.R, TargetBlend.R, DeltaSeconds, Config.VelocityBlendInterpSpeed);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用插值得到更加平滑的结果&lt;/p&gt;
&lt;h4&gt;3.1.2. 动画曲线&lt;/h4&gt;
&lt;p&gt;使用了HipOrientation_Bias（臀位朝向）与Feet_Crossing（是否交叉脚）让动画表现层更加顺滑。&lt;br /&gt;
使用 YawOffset 来实现旋转偏移。&lt;/p&gt;
&lt;h5&gt;3.1.2.1. 交叉脚曲线&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Feet_Crossing&lt;/strong&gt;：存在与ALS_N_[Walk, Run]_[LB, LF, RB, RF]中的曲线，功能是标识什么时候为交叉脚&lt;br /&gt;
1: 交叉脚，切换姿势会穿帮&lt;br /&gt;
0: 非交叉，可以正常进行姿势切换&lt;/p&gt;
&lt;h5&gt;3.1.2.2. 臀位朝向曲线&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;HipOrientation_Bias&lt;/strong&gt;：臀部位置&lt;br /&gt;
&amp;lt;-0.5 臀位左&lt;br /&gt;
&amp;gt;0.5 臀位右&lt;/p&gt;
&lt;h4&gt;3.1.3. Blend Profile&lt;/h4&gt;
&lt;h3&gt;3.2. 表现&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1305696520.cos.ap-shanghai.myqcloud.com/obsidian/pic/202503201748605.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.2.1. States&lt;/h4&gt;
&lt;p&gt;State 内均采用四方向融合。分别对应 VelocityBlend.F(B，R，L)。&lt;br /&gt;
拥有六种动画资源 F，B，FL，BL，FR，BR。&lt;/p&gt;
&lt;p&gt;融合方式采用对角融合，F-B，FL-BR，FR-BL。对角融合才能正确的融合出臀部的位置。&lt;br /&gt;
这样融合方式应该是&lt;br /&gt;
Move F → F，B， FL，RL&lt;br /&gt;
Move B → F，B，FB，RB&lt;br /&gt;
Move LF → F，B，LF，RB&lt;br /&gt;
……&lt;/p&gt;
&lt;h4&gt;3.2.2. Transition&lt;/h4&gt;
&lt;p&gt;切换路径是有限制，跳变切换的路径被禁止：&lt;br /&gt;
如没有从 LF →RF 的路径; RF→B 必须途径 RB…&lt;/p&gt;
&lt;p&gt;切换规则分为三类：&lt;br /&gt;
&lt;strong&gt;ChangeDirection&lt;/strong&gt;：由玩家输入产生的移动意图决定，对操控立即响应，插值生效&lt;br /&gt;
&lt;strong&gt;SwitchHip&lt;/strong&gt;：又臀位和交叉脚部判断让融合效果更加自然&lt;/p&gt;
&lt;h5&gt;3.2.2.1. ChangeDirection 移动意图变换&lt;/h5&gt;
&lt;p&gt;由 &lt;code&gt;MoveDirection&lt;/code&gt; 变量决定，该变量在数据章节中有叙述。会在 &lt;code&gt;NativeUpdateAnimation&lt;/code&gt; 中进行更新。&lt;br /&gt;
&lt;strong&gt;路径&lt;/strong&gt;：这种方式的变换经历所有的合法的变换路径。&lt;br /&gt;
&lt;strong&gt;通知&lt;/strong&gt;：在 F←&amp;gt;B，FR←&amp;gt;BL，FL←&amp;gt;BR 会添加&quot;Pivot&quot;事件，通知轴心进行了更改&lt;/p&gt;
&lt;h5&gt;3.2.2.2. SwitchHip 臀部变换&lt;/h5&gt;
&lt;p&gt;由 HipOrientation_Bias 和 Feet_Crossing 决定变换。&lt;br /&gt;
Feet_Crossing 是一个消极变量，只有当为 0 时才能切换。原因是交叉脚时转身会出现明显的穿帮。&lt;br /&gt;
在这个变换中，忽略脚部，观察 FL 和 FR，发现一个其实就是左扭屁股，一个是右扭屁股。这个就可以用于类似瞄准等场景。&lt;br /&gt;
&lt;strong&gt;路径&lt;/strong&gt;：因为这是由臀位变换导致的，所以只有 FL&amp;lt;→FR，BL←&amp;gt;BR&lt;/p&gt;
&lt;h2&gt;4. Reference&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/638673655&quot;&gt;ALS高级运动系统解析 03 八向移动&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://zhuanlan.zhihu.com/p/489653380&quot;&gt;UE4 UE5 骨骼动画 高级运动系统 八向移动&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://zhuanlan.zhihu.com/p/80732458&quot;&gt;AdvancedLocomotion 拆解笔记 1：移动&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://zhuanlan.zhihu.com/p/424797720&quot;&gt;Site Unreachable&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://www.bilibili.com/video/av80124207/&quot;&gt;【新手向】虚幻4ARPG教程-重置版-第七集-使用弓箭时八方向移动状态_哔哩哔哩_bilibili&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://www.bilibili.com/video/BV12f4y1r71N&quot;&gt;虚幻4（UE4） 动画技术 深入浅出 高级运动系统_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>