<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Granvallen;Nest</title>
  
  <subtitle>面白く生きる</subtitle>
  <link href="https://granvallen.github.io/atom.xml" rel="self"/>
  
  <link href="https://granvallen.github.io/"/>
  <updated>2026-08-05T14:21:44.577Z</updated>
  <id>https://granvallen.github.io/</id>
  
  <author>
    <name>Granvallen</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>好孩子不要看尼古喵喵</title>
    <link href="https://granvallen.github.io/yanineko/"/>
    <id>https://granvallen.github.io/yanineko/</id>
    <published>2026-08-01T16:54:00.000Z</published>
    <updated>2026-08-05T14:21:44.577Z</updated>
    
    <content type="html"><![CDATA[<p>人懒起来真是太可怕了, 意识到时又大半年没更新了, にゃーは悪いことしてないのに…可恶, 没办法了, 这次就先搬个日记凑下数好了…</p><p>就随便聊聊最近比较上头的一个动画, 上头到看了也买了原作漫画, 甚至不可思议地写了这篇日记来聊. 这是一个几乎没什么剧情可言的动画, 讲了一个嗜烟如命的猫娘和同公寓其他有着各种不良嗜好的猫娘们的屎尿屁日常, 没错, 正是<a href="https://movie.douban.com/subject/38194261/">尼古喵喵</a>「ヤニねこ」. 完全可以想象, 这种设定必会劝退大部分人. 因此就算本作作画质量上乘, 我也不会试图向任何人安利这部充斥着令人生理不适的屎尿屁, 低俗成人笑话, 以及影射阴暗现实的黑色幽默的作品. 尽管如此, 我依然”不幸”地被这部作品吸引, 并试图弄明白是怎么回事. 我很难不联想到更早时候播出的<a href="https://movie.douban.com/subject/20458842/">丧女</a>, 两者在我的观看体验上似乎有某种相似之处.</p><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="https://music.163.com/outchain/player?type=2&id=1471567202&auto=0&height=66"></iframe><p>首先想到的可能吸引我的便是某种猎奇, 他们都满足部分人窥探他人生活的好奇. 不过在我涉猎范围里, 这也并非是纯正意义上的「グロ」. 只是说在角色选择与故事设定上, 是商业作品中相对少见的类型, 这些设定确实会引起大部分人的观看不适. 大部分的观众多少是带着一种娱乐或欣赏的心态来观看作品, 预期获得的是正面的感受, 像是热血, 爽快, 治愈, 可爱, 而对于人性丑恶的展现通常是反感的. 这类带着丑恶设定的人物是期望作为反派被主角击败出现的, 而且既然都是反派了, 甚至会期望这类人物也有自己”崇高”的邪恶理想, 在某种信仰的高度上是与主角相当的, 我们可能还会说这叫优秀的反派塑造. 要不然, 他就只能是一个卑微的龙套或喽啰, 几乎不会被观众所关注到, 也永远不会有什么高光时刻. 而尼古喵喵与丧女似乎就是让这样的角色成为了主角, 即社会的某种边缘人, 而打败他们的甚至都不是什么残酷的现实, 只是那个不成熟的自己. 丧女是缺少基本的陌生人社交能力而沉溺于幻想. 尼古猫则是缺少基本的自制力而沉溺于当下的享受, 两者可以说是处在相反的极端. 我觉得最有意思和有争议的是, 两部作品能否归类到”搞笑”这个门类. 这种割裂的原因如前所述, 有观众可能并不会觉得看一个人尴尬到死或自甘堕落是用来搞笑的, 因为对于这些观众而言, 这无异于是对”弱者”的嘲笑, 而这又是不道德的, 我完全能够理解.</p><p>而对于另一部分观众, 我依然想提示一种可能忽略的理解, 那就是自嘲. 我知道很多人谈论这两部作品都会谈及背后时代背景和社会现象, 这种现实的真实也是构成猎奇感的一部分. 但我一向是不愿意上升到宏大的方面去聊, 更想从个人的角度多谈一点. 就我自己而言, 个人特质也好, 社会影响也罢, 是多少能让我带入其中的角色的, 部分场景也能产生生活经历上的共鸣. 虽然不至于像作品中描述的如此极端, 我也时常在某些社交场合社恐闹出过不少笑话, 也会对一些事物痴迷到上瘾的程度而耽误计划, 过后又悔恨不已, 如此往复. 往往我在自嘲的同时也依然会萌生对角色的同情, 因此就会陷入一种边笑边哭的复杂情绪之中. 不过不同的是, 黑木智子似乎更多时候是处在某种负面情绪之中. 而尼古猫则是一个无可救药的乐天派, 我反而经常反问自己, 有没有可能尼古猫其实真的正过着自己想要的潇洒生活, 这确实能让我释怀一点点. </p><p>如果再仔细观察两作, 会发现他们剧作上的一些同构之处. 角色想要去做某事, 用自己不成熟的方法和手段, 最后不出意外地失败. 丧女里是不切实际的妄想, 而尼古猫则是对自身欲望的无底线纵容. 然而出人意料的是, 整个过程中, 世界的周遭都对角色保持开放和善意, 甚至可以说是被深深爱着的, 这种设定无疑使观者情感更加复杂. 比如丧女的父母和各种相遇的人们, 尼古猫的妹妹以及房东邻居的善待, 更别说尼古猫经常翘班, 也总能找到工作攒钱买烟, 这绝对称得上作者最后的善意. 只有如此, 这些角色才能一次次作死而没有任何反思, 这也是作品最不真实的一点, 不得不说这种既现实又童话的微妙感也是吸引我的原因之一. 对我而言, 我甚至会觉得这是这些作品成立的原因. 而反过来, 类似晚安布布, 章鱼噼的原罪这类作品对我就没那么地有吸引力.</p><p>我还想再多谈一点尼古猫的生活方式与成瘾行为. 前面提到, 这个温柔的世界是作者创造的乌托邦, 在这个世界里, 角色可以尽情地放纵自己, 而完全不考虑未来, 只追求当下享受. 尼古猫唯一需要付出的努力是打个零工弄到买烟钱, 还经常搞砸. 有烟时受本能支配无所事事, 没钱买烟时则又可以完全抛弃自己的尊严和主体性. 正如我们听闻的一些当代非常边缘的生活方式, 尼古猫对现代人的常识观念也完全没有认知, 什么干净, 整洁, 精致, 健康, 尊严, 效率, 目标. 当她说出常抱怨的那句「にゃーは悪いことしてないのに」时也<strong>不带有任何反思</strong>, 似乎就是在说「为什么不通过努力就没办法获得快乐呢？」是啊, 为什么呢? 如果从存在主义角度, 一个残酷的真相似乎是, 快乐确实就是需要挣来或者赢得的. 不止快乐, 一切人希望获得的东西与感受都需要你挣扎着做些什么. 如果你认为什么也做不到, 什么也不做, 人单单只能活着(在纸片人的世界里当然不会饿死啦), 还有令人难耐的虚无感. 而缓解这种虚无感最廉价最不需要努力的方式之一正是陷入某种成瘾行为. 我有时甚至会觉得这类行为本身并不真正给人带来快乐, 而只是麻痹了人对于时间的感知. 因为感到虚无时间变短了而获得的某种短暂舒缓的感觉. 它是一种自欺, 人无法在这种行为中保持基本的清醒, 也就无所谓抉不抉择. 当人在某个时刻不经意地意识到现实与自己的有限性时, 就会突然从这个迷梦中惊醒, 再次把我们拉回到那个避无可避的虚无问题面前. 然后呢? 是做些什么, 还是继续成瘾. 类似的成瘾行为其实非常广泛, 绝非局限于使用尼古丁这一类的生理成瘾的物质. 到这里不得不提一嘴<a href="https://movie.douban.com/subject/35332568/">奇巧计程车</a>「ODD TAXI」的神回ep.4田中革命, 也深刻探讨了这个心理过程. </p><p>尼古猫的活在当下和随性而为也是对我特别有吸引力的部分之一, 这也可以作为她被周围人善待的解释. 在现在优绩主义盛行与效率至上的时代里, 不知怎的, 就是会让我觉得这类特质闪闪发光. 我当然不会认为这种生活方式是一种解法, 那会让我们彻底失去对自我和未来的掌控感. 但她确实带来了反思并揭示了一种可能性, 一种我们有时忽视甚至不敢想象的可能性. 先发散聊这么多吧, 尼古喵喵就动画而言还有太多可以聊, 也有非常多让我有特别感受的场景, 细节与设定. 我看到过网上有一些考据, 确实能感受到制作组非常有想法, 这里就先不展开了.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;人懒起来真是太可怕了, 意识到时又大半年没更新了, にゃーは悪いことしてないのに…可恶, 没办法了, 这次就先搬个日记凑下数好了…&lt;/p&gt;
&lt;p&gt;就随便聊聊最近比较上头的一个动画, 上头到看了也买了原作漫画, 甚至不可思议地写了这篇日记来聊. 这是一个几乎没什么剧情可言的动</summary>
      
    
    
    
    <category term="ACGN" scheme="https://granvallen.github.io/categories/ACGN/"/>
    
    <category term="动画" scheme="https://granvallen.github.io/categories/ACGN/%E5%8A%A8%E7%94%BB/"/>
    
    
    <category term="动画" scheme="https://granvallen.github.io/tags/%E5%8A%A8%E7%94%BB/"/>
    
  </entry>
  
  <entry>
    <title>如何实现一个简单易用可扩展的导表工具</title>
    <link href="https://granvallen.github.io/konfi/"/>
    <id>https://granvallen.github.io/konfi/</id>
    <published>2025-10-01T14:14:00.000Z</published>
    <updated>2025-10-01T15:27:54.368Z</updated>
    
    <content type="html"><![CDATA[<p>米娜桑, 我又回来更新了. 这次回归正业, 聊聊几个月前写的一个简单的游戏配置导表工具 <a href="https://github.com/Granvallen/Konfi">konfi</a>. 起因是 side project 里手写的配置越来越多了, 在配置变得混乱前, 想着还是尽早转移到 excel 管理为好. 于是咱需要一个 excel 的导表工具, 这通常也是游戏项目的基础设施之一. </p><h1 id="为什么写了-konfi"><a href="#为什么写了-konfi" class="headerlink" title="为什么写了 konfi"></a>为什么写了 konfi</h1><p>本着找到能用的就绝不手写的原则, 一番检索后, 试了下被提到最多的 <a href="https://github.com/focus-creative-games/luban">Luban</a>. 毫无疑问, 这是一个强大且成熟的导表框架, 有着非常丰富的特性. 但我觉得它在易用性以及和我项目适配上并不太理想. 其配置语法在我看来也还是有点繁琐的. 并且导出的数据格式主要是基于 json, 然后可根据需要再转成其他语言的数据结构. 我个人认为用 json 去描述游戏配置信息并不够好, 比如 json 竟然不支持用整数做 key, 而用整数 id 做主键的配置表则非常普遍. 其实最理想还是直接把配置导出为所用编程语言的数据结构, 在保留信息的同时也方便读取. 另一方面, luban 对于我的项目而言实在有点过于庞大了, 有太多我几乎用不上的特性, 其源码都是我项目的好几倍了… 于是, 我又尝试了一些实现较简单的开源工具, 但大多又已年久失修. 最后想着这类工具反正是一劳永逸的, 需求也不会特别复杂, 不妨就自己实现一个算了.</p><p>konfi 的实现也参考了其他的导表工具的设计, 但总体原则是<strong>简单</strong>, <strong>易用</strong>, <strong>可扩展</strong>. 简单指核心实现不到千行, 只实现最常用的基础功能. 易用是希望无需额外配置, 直接建表即可运行导出, 配置语法简洁, 统一且符合直觉. 可扩展则是说可以方便地增加或修改导出流程和数据类型, 从而更好地适配项目. 接下来就简单聊聊如何使用以及如何扩展 konfi, 就当是 konfi 的使用文档了. 嘛, 话虽这么说, 其实也不算安利啦, 更多是分享一个思路, 毕竟 <strong>konfi 本身就是缺少测试的, 除非你觉得可以解决使用过程中出现的任何问题</strong>. 另外, 有些好用但我目前用不上的特性仍在斟酌中, 也暂时没有实现, 在后面会简单提一嘴.</p><h1 id="如何使用-konfi"><a href="#如何使用-konfi" class="headerlink" title="如何使用 konfi"></a>如何使用 konfi</h1><h2 id="使用篇"><a href="#使用篇" class="headerlink" title="使用篇"></a>使用篇</h2><p>有必要先说明一些基础设定. 一般比较常用的有三种配置表, 即<strong>枚举表</strong>, <strong>数据表</strong>, <strong>键值表</strong>. 枚举表用于配置项目中的枚举, 便于在整个项目中引用, 比如职业就可以是一个枚举类型, 有剑士, 小偷等职业. 有了枚举之后可以在项目或数据表中引用. 数据表通常包含了最主要的配置内容, 比如游戏中的角色, 物品信息等, 有非常多的属性数据, 角色此时也应当可以使用职业枚举来配置其职业属性. 键值表包含最简单的 key-value 结构, 用于简单的字典结构数据或杂项配置. 以上也可以看出枚举表应该比其他类型的表优先处理, 这样才能引用枚举表的数据.</p><p>在这个 <a href="https://github.com/Granvallen/Konfi">repo</a> 的 design 目录中有两个示例的 excel 表格, 演示了常用的一些配置方法. 枚举表因为需要优先导出故通常作为单独的 excel 文件. 为了导出 design 内的配置表, 可以直接运行 export_table.py 这个脚本, 配置将导出到 data 目录下.</p><p><strong>运行环境 Python 3.11+, 依赖包括 openpyxl(excel 文件读取) 与 black(Python格式输出).</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> konfi <span class="keyword">import</span> Exportor</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    table_dir = <span class="string">&quot;./design&quot;</span></span><br><span class="line">    data_dir = <span class="string">&quot;./data&quot;</span></span><br><span class="line">    writer_ext = <span class="string">&quot;py&quot;</span></span><br><span class="line">    enum_tables = &#123; <span class="string">&quot;枚举&quot;</span> &#125;</span><br><span class="line">    </span><br><span class="line">    exportor = Exportor(</span><br><span class="line">        table_dir = table_dir,</span><br><span class="line">        data_dir = data_dir,</span><br><span class="line">        writer_ext = writer_ext,</span><br><span class="line">        enum_tables = enum_tables,</span><br><span class="line">        is_inc = <span class="literal">False</span>,</span><br><span class="line">    )</span><br><span class="line">    exportor.run()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    main()</span><br></pre></td></tr></table></figure><h3 id="枚举表"><a href="#枚举表" class="headerlink" title="枚举表"></a>枚举表</h3><p>先看 枚举.xlsx 文件内容.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/konfi_1.png" style="zoom:60%;" class alt="枚举表" /></p><p>test_enum 这个 sheet 中配置了两个枚举类型, 导表过程会先读取一个目录内的所有 excel 文件, 然后逐文件, 逐 sheet 进行导表操作. 使用 _enum 作为后缀名的 sheet 会视为枚举表, 并用枚举表的导表规则进行导出. </p><p>konfi 要求表格内容划分为三个区域, 首先是保留的第一列, 主要用来对单行的内容做标记, 比如标记哪些行是配置行. 在上面的例子中, 使用 !var 标记了枚举表所需的一个配置行, 使用 # 标记了一个注释行. 然后是配置行, 配置行的每一列用于描述该列的数据是什么. 枚举表要求 !var 的配置行有 enum_cls, enum_cls_alias, enum_name, enum_alias, enum_val 这 5 列. 最后则是数据行, 即除第一列和配置行以外的右下区域.</p><p>以导出 python 数据结构为例, 这个枚举表导出的内容为<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 枚举.xlsx/test_enum</span></span><br><span class="line"><span class="comment"># export by konfi</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> enum <span class="keyword">import</span> Enum</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ClassisEnum</span>(<span class="title class_ inherited__">Enum</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot; 职业枚举</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    SWORDMAN = <span class="number">0</span> <span class="comment"># 剑士</span></span><br><span class="line">    BANDIT   = <span class="number">1</span> <span class="comment"># 山贼</span></span><br><span class="line">    ARCHER   = <span class="number">2</span> <span class="comment"># 弓手</span></span><br><span class="line">    THIEF    = <span class="number">3</span> <span class="comment"># 小偷</span></span><br><span class="line">    MAGE     = <span class="number">4</span> <span class="comment"># 魔法使</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">CombatAttrEnum</span>(<span class="title class_ inherited__">Enum</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot; 战斗属性枚举</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    ATK  = <span class="number">0</span> <span class="comment"># 物攻</span></span><br><span class="line">    MATK = <span class="number">1</span> <span class="comment"># 魔攻</span></span><br><span class="line">    AVO  = <span class="number">2</span> <span class="comment"># 回避</span></span><br><span class="line">    HIT  = <span class="number">3</span> <span class="comment"># 命中</span></span><br><span class="line">    CRIT = <span class="number">4</span> <span class="comment"># 暴击</span></span><br><span class="line">    BLK  = <span class="number">5</span> <span class="comment"># 格挡</span></span><br></pre></td></tr></table></figure></p><p>konfi 目前也支持导出 lua 与 json(通过修改 writer_ext 为 lua 或 json), 但我自己只使用 python, 故其他格式的导出支持可能没那么完善. 但理论上通过扩展, konfi 是可以导出任何格式的数据的.</p><p>关于枚举表的一些细节</p><ul><li>枚举值一列需配置整数, 也可以和 python 枚举一样使用 auto 让 konfi 自行决定枚举值.</li><li>枚举类型在其他配置表中被引用时, 可以使用 枚举名 或 枚举别名 进行配置, 这一点后面我们会看到例子.</li><li>没错, 使用 # 进行注释也借鉴自 python. 在 konfi 中, 这是一个统一的注释语法, 使用 # 不仅可以注释行, 写在配置行的里也可以注释列, 甚至可以写在 excel 或 sheet 名字前进行注释. 被注释的内容会跳过导出处理.</li></ul><h3 id="数据表"><a href="#数据表" class="headerlink" title="数据表"></a>数据表</h3><p>接着我们看内容最复杂的数据表, 测试.xlsx/test.test</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/konfi_2.png" style="zoom:80%;" class alt="数据表" /></p><p>上表导出的 python 数据结构为<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 测试.xlsx/test.test</span></span><br><span class="line"><span class="comment"># export by konfi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># id          : int id</span></span><br><span class="line"><span class="comment"># name        : string 名称</span></span><br><span class="line"><span class="comment"># class       : ClassisEnum? 职业</span></span><br><span class="line"><span class="comment"># level       : int? 等级</span></span><br><span class="line"><span class="comment"># range       : float 攻击距离</span></span><br><span class="line"><span class="comment"># combat_attr : dict_m[CombatAttrEnum,  int] 战斗属性</span></span><br><span class="line"><span class="comment"># combat_attr2: dict[CombatAttrEnum, int] 战斗属性2</span></span><br><span class="line"><span class="comment"># equip       : list_m[int] 装备</span></span><br><span class="line"><span class="comment"># equip2      : list[int] 装备2</span></span><br><span class="line"><span class="comment"># equip3      : set[int] 装备3</span></span><br><span class="line"><span class="keyword">from</span> ...enum <span class="keyword">import</span> ClassisEnum, CombatAttrEnum</span><br><span class="line"></span><br><span class="line">test = &#123;</span><br><span class="line">    <span class="number">1001</span>: &#123;</span><br><span class="line">        <span class="string">&quot;id&quot;</span>: <span class="number">1001</span>,</span><br><span class="line">        <span class="string">&quot;name&quot;</span>: <span class="string">&quot;皮皮洛&quot;</span>,</span><br><span class="line">        <span class="string">&quot;class&quot;</span>: ClassisEnum.MAGE,</span><br><span class="line">        <span class="string">&quot;level&quot;</span>: <span class="number">100</span>,</span><br><span class="line">        <span class="string">&quot;range&quot;</span>: <span class="number">1.2</span>,</span><br><span class="line">        <span class="string">&quot;combat_attr&quot;</span>: &#123;</span><br><span class="line">            CombatAttrEnum.ATK: <span class="number">1</span>,</span><br><span class="line">            CombatAttrEnum.MATK: <span class="number">50</span>,</span><br><span class="line">            CombatAttrEnum.AVO: <span class="number">10</span>,</span><br><span class="line">            CombatAttrEnum.HIT: <span class="number">100</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;combat_attr2&quot;</span>: &#123;</span><br><span class="line">            CombatAttrEnum.ATK: <span class="number">1</span>,</span><br><span class="line">            CombatAttrEnum.MATK: <span class="number">50</span>,</span><br><span class="line">            CombatAttrEnum.AVO: <span class="number">10</span>,</span><br><span class="line">            CombatAttrEnum.HIT: <span class="number">100</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;equip&quot;</span>: [<span class="number">12</span>, <span class="number">4</span>, <span class="number">0</span>],</span><br><span class="line">        <span class="string">&quot;equip2&quot;</span>: [<span class="number">12</span>, <span class="number">4</span>, <span class="number">0</span>],</span><br><span class="line">        <span class="string">&quot;equip3&quot;</span>: &#123;<span class="number">2</span>&#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="number">1002</span>: &#123;</span><br><span class="line">        <span class="string">&quot;id&quot;</span>: <span class="number">1002</span>,</span><br><span class="line">        <span class="string">&quot;name&quot;</span>: <span class="string">&quot;布库尔&quot;</span>,</span><br><span class="line">        <span class="string">&quot;class&quot;</span>: ClassisEnum.SWORDMAN,</span><br><span class="line">        <span class="string">&quot;level&quot;</span>: <span class="literal">None</span>,</span><br><span class="line">        <span class="string">&quot;range&quot;</span>: <span class="number">8.5</span>,</span><br><span class="line">        <span class="string">&quot;combat_attr&quot;</span>: &#123;</span><br><span class="line">            CombatAttrEnum.ATK: <span class="number">50</span>,</span><br><span class="line">            CombatAttrEnum.MATK: <span class="number">1</span>,</span><br><span class="line">            CombatAttrEnum.AVO: <span class="number">10</span>,</span><br><span class="line">            CombatAttrEnum.HIT: <span class="number">100</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;combat_attr2&quot;</span>: &#123;</span><br><span class="line">            CombatAttrEnum.ATK: <span class="number">50</span>,</span><br><span class="line">            CombatAttrEnum.MATK: <span class="number">1</span>,</span><br><span class="line">            CombatAttrEnum.AVO: <span class="number">10</span>,</span><br><span class="line">            CombatAttrEnum.HIT: <span class="number">100</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;equip&quot;</span>: [<span class="number">11</span>, <span class="number">2</span>, <span class="number">34</span>],</span><br><span class="line">        <span class="string">&quot;equip2&quot;</span>: [<span class="number">11</span>, <span class="number">2</span>, <span class="number">34</span>],</span><br><span class="line">        <span class="string">&quot;equip3&quot;</span>: &#123;<span class="number">2</span>, <span class="number">11</span>, <span class="number">34</span>&#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>注意到, 这里的 sheet 名是 test.test, 没有后缀说明是一个普通的数据表. 不过名字中使用了 . 作为分割, 点前面的部分作为这个表数据的<strong>模块名</strong>. 简单来说就是这个表导出的内容会放到 data/test 目录下. 如果没有模块名则会直接放到 data 目录下, 以便于数据管理. 通常同一个功能模块的配置表是单独的一个 excel 文件, 其中的 sheet 使用相同的模块名.</p><p>表的第一列出现了几个新的标记</p><ul><li>!var, 配置字段表示一列数据的变量名</li><li>!type, 配置字段表示一列数据的类型名</li><li>!lable, 配置字段表示一列数据的标签名, 用于增加额外说明</li><li>!param, 用于解析列时提供一些额外的用户设定信息, 比如修改数据类型的解析方式. 后面会解释效果.</li><li>使用 # 进行了行注释, 可以看到第一行也用 # 注释了 can_fly 这一列.</li></ul><p>另一个不同是, 数据表本质是一个字典结构, 所以会需要至少一个主键, 在 !var 配置行前加 * 表示主键, konfi 也支持多级主键.</p><h4 id="类型系统"><a href="#类型系统" class="headerlink" title="类型系统"></a>类型系统</h4><p><strong>类型系统</strong>是导表工具要解决的核心问题之一, 一个完善的类型系统会使许多功能的实现成为可能, 比如将数据转成任意语言的原生数据类型, 支持扩展配置的数据类型, 以及进行数据字段的校验等等. konfi 也实现了一个非常简单的类型系统去支持上述这些特性.</p><h5 id="类型支持"><a href="#类型支持" class="headerlink" title="类型支持"></a>类型支持</h5><p>之所以需要类型, 是因为我们需要告诉游戏程序如何去理解这些配置数据, 比较常见的有整型, 字符串, 布尔类型等等基础类型. 还有一类被称作泛型类型, 或容器类型, 比如列表, 集合, 字典等. 目前 konfi 内置支持的类型有</p><div class="table-container"><table><thead><tr><th>类型</th><th>配置名</th><th>默认值</th><th>说明</th></tr></thead><tbody><tr><td>整数</td><td>int</td><td>0</td><td></td></tr><tr><td>浮点数</td><td>float</td><td>0.0</td><td></td></tr><tr><td>布尔</td><td>bool</td><td>False</td><td>是/Y/1 为真, 否/N/0 为假</td></tr><tr><td>字符串</td><td>string</td><td>“”</td><td>不需要引号</td></tr><tr><td>列表(单格/多格)</td><td>list / list_m</td><td>[]</td><td>list[int], 默认使用 ‘,’ 分割项</td></tr><tr><td>集合(单格/多格)</td><td>set / set_m</td><td>set()</td><td>set[int], 默认使用 ‘,’ 分割项</td></tr><tr><td>字典(单格/多格)</td><td>dict / dict_m</td><td>{}</td><td>dict[int, int], 默认 ‘:’ 分割键值, ‘,’ 分割项</td></tr><tr><td>枚举</td><td>枚举类名</td><td></td><td>配置对应 枚举值 或 枚举别名</td></tr></tbody></table></div><p>可以看到这些类型名也都取自 python, 泛型类型的配置方式也类型 python 的类型注释.</p><h5 id="可空类型"><a href="#可空类型" class="headerlink" title="可空类型"></a>可空类型</h5><p>如果一个指定类型的表格没有配置会发生什么? 例如上面例子中 <strong>布库尔的等级</strong> 这一数据格就是空的. konfi 也尝试引入了 <strong>可空类型</strong>(nullable) 这一概念, 用以避免 默认值 与 空值 带来的混乱. 通过在任意类型后加 ? 标记一个类型为可空类型. 可空类型在解析一个空格时会作为空值处理, 在 python 中就是 None. 否则会解析为类型的默认值. 所以上面布库尔的等级其实是导出的 None.</p><h5 id="枚举类型"><a href="#枚举类型" class="headerlink" title="枚举类型"></a>枚举类型</h5><p>枚举类型是一个非常特别的类型, 因为其依赖枚举表导出的数据. 其使用的配置名即枚举表中的枚举类, 例如上面的 职业 这一列, 使用了枚举表中的 ClassisEnum. 而数据可以填 枚举值 或 枚举别名. 其他用法上与基础类型无异.</p><h5 id="泛型类型"><a href="#泛型类型" class="headerlink" title="泛型类型"></a>泛型类型</h5><p>konfi 支持的每种泛型类型都有两种配置版本, 单格 与 多格, 可以参考 combat_attr, combat_attr2,  equip, equip2 的配置方式. 前面提到 !param 这个标记, 对于<strong>单格泛型类型</strong>可以用这个标记的配置去修改默认的解析分隔符. 之所以有这个需求是因为如果容器里的类型配置格式比较复杂, 容器默认的分隔符可能被占用, 此时就需要换一个符号来分割. 而对于多格字典类型, 目前使用该标记可以指定配置的字典 key 值.</p><h5 id="扩展类型"><a href="#扩展类型" class="headerlink" title="扩展类型"></a>扩展类型</h5><p>konfi 支持自定义类型的扩展, 可以自行决定如何解析配置的数据, 不过会需要一些 python 的编码.</p><h4 id="分表"><a href="#分表" class="headerlink" title="分表"></a>分表</h4><p>另外 konfi 也实现了一个简单的分表功能, 可以把一张大表拆成多个 sheet 进行配置, 在导出时合并为一个整体. 需要保证合并的表的配置行一致. 在例子中有 测试.xlsx/#test.test@test2 这张表, 因为使用了 # 对 test.test@test2 进行了注释, 所以没有导出. 去掉 # 将其导出时, @ 之前的部分表示这个子表在导出时应该合并到 test.test 这个表的数据中.</p><p>分表的功能在大型表拆分时会有用, 我暂时是用不到这个功能了, 所以缺少测试.</p><h3 id="键值表"><a href="#键值表" class="headerlink" title="键值表"></a>键值表</h3><p>键值表是最简单的一种配置表, 通常只需要两列配置项 key 与 val, 例如 测试.xlsx/test_kv, 使用 _kv 后缀的即视为键值表.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/konfi_3.png" style="zoom:80%;" class alt="键值表" /></p><p>导出的 python 数据为<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 测试.xlsx/test_kv</span></span><br><span class="line"><span class="comment"># export by konfi</span></span><br><span class="line"><span class="comment"># key: string 键</span></span><br><span class="line"><span class="comment"># val: int 值</span></span><br><span class="line">test_kv = &#123;</span><br><span class="line">    <span class="string">&quot;移动距离&quot;</span>: <span class="number">5</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>关于键值表其他就没什么可说的了, 我个人是感觉这个键值表功能上还是有点简单了, val 这一列只能是一种类型, 这在作为杂项表时不太便利. 我在考虑是否应该支持更动态的类型, 比如直接支持配置 python 数据或 json 数据, 这在某些时候还是挺方便的.</p><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>以上就是 konfi 主要功能的使用方式了, 这里再补充一些细节和将来可能会考虑实现的特性(<del>请不用抱任何期待</del>)</p><h4 id="增量导出"><a href="#增量导出" class="headerlink" title="增量导出"></a>增量导出</h4><p>这个还是挺有用的, 特别是配置表比较多, 而改动比较少的时候. 姑且实现了一个简单的, 创建 Exportor 时传入 is_inc = True 即可开启增量导表. 开启后导表会在 data 目录生成一个记录每个 sheet 数据 md5 的 json 文件, 如果没有变化则会跳过导出.</p><h4 id="表格依赖"><a href="#表格依赖" class="headerlink" title="表格依赖"></a>表格依赖</h4><p>在有些情况下需要规定数据表的导出数据的顺序, 比如进行一些导出后校验等等, 也即此时表格之间是存在依赖关系的, 当前在 konfi 中并没有处理. 仅有的顺序也只是保证 枚举表 的优先导出. </p><h4 id="导表效率"><a href="#导表效率" class="headerlink" title="导表效率"></a>导表效率</h4><p>选择 python 实现在一定程度上就是用编码效率换运行效率, 尽管如此, 其实也是可以加入多进程导表, 优化导出流程等加速的方案, 但是目前看不到实现计划, 反……反正又不是不能用!</p><h4 id="通过配置生成类型"><a href="#通过配置生成类型" class="headerlink" title="通过配置生成类型"></a>通过配置生成类型</h4><p>我觉得这算一个比较进阶的功能了(luban 已有实现), 增强了配置表的功能, 也降低了扩展自定义类型的难度. 实现设计上需要非常小心, 感觉弄不好就非常鸡肋. 不过我暂时也用不上就是了.</p><h2 id="扩展篇"><a href="#扩展篇" class="headerlink" title="扩展篇"></a>扩展篇</h2><p>简单说下 konfi 实现和扩展的思路. 大体思路其实就是先把 sheet 转换为 python 特定的中间数据结构, 然后再把这个数据结构按导出语言进行重构, 写入到配置文件脚本.</p><p>konfi 的设计中划分了一些概念, 比如</p><ul><li>导出器(Exportor), 用于控制整个导表流程</li><li>解析器(Parser), 用于把一个 sheet 解析为 python 中间类型数据. 每个解析器可以自行决定如何匹配及处理 sheet.</li><li>类型(EType), 即中间类型, 用于描述 sheet 配置数据.</li><li>写入器(Writer), 用于把中间数据按特定语言写入到文件导出.</li></ul><p>其中的每个部分理论上都可进行扩展. 比如对于解析器, 首先需要继承 Parser 类, 然后有两种方式筛选该 Parser 期望处理的 sheet.</p><ol><li>类变量 match 可以直接列出匹配解析 sheet 的名字</li><li>实现 filter 函数, 参数为 openpyxl 的 Worksheet<br>接着需要实现 parse 方法, 确定 sheet 的导出规则. 可以参考已有的数据表解析器(CommonParser).</li></ol><p>类型扩展是更常用的, 比如下面这个用 python tuple 构造的位置类型.<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">EPos</span>(<span class="title class_ inherited__">EType</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot; 位置坐标</span></span><br><span class="line"><span class="string">        (x, y)</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    etype_tag = <span class="string">&quot;pos&quot;</span></span><br><span class="line">    default = (<span class="number">0</span>, <span class="number">0</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, val: <span class="type">Any</span>, nullable: <span class="built_in">bool</span> = <span class="literal">False</span></span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__(val, nullable)</span><br><span class="line">        self._convert()</span><br><span class="line">        </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">_convert</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">if</span> self.val <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">if</span> self.nullable:</span><br><span class="line">                self.py_val = <span class="literal">None</span></span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                self.py_val = self.__class__.default</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            self.py_val = literal_eval(self.val)</span><br><span class="line">        <span class="keyword">except</span> ValueError:</span><br><span class="line">            <span class="keyword">raise</span> ValueError(<span class="string">f&quot;无法将值 <span class="subst">&#123;self.val&#125;</span> 转换为 <span class="subst">&#123;<span class="built_in">type</span>(self).__name__&#125;</span>&quot;</span>) <span class="keyword">from</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><br>扩展类型需要继承自 EType, 确定类型名, 默认值, 以及是否支持多格. 然后实现类型的转换方法 _convert. 目前的这些类型实现得都还是比较简单的, 暂时也没有做校验什么的.</p><p>Enjoy.<br>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>終わり</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;米娜桑, 我又回来更新了. 这次回归正业, 聊聊几个月前写的一个简单的游戏配置导表工具 &lt;a href=&quot;https://github.com/Granvallen/Konfi&quot;&gt;konfi&lt;/a&gt;. 起因是 side project 里手写的配置越来越多了, 在配置变得混</summary>
      
    
    
    
    <category term="游戏开发" scheme="https://granvallen.github.io/categories/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="游戏开发" scheme="https://granvallen.github.io/tags/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
    
    <category term="Python" scheme="https://granvallen.github.io/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>それ町20周年与石黑正数的问答(译)</title>
    <link href="https://granvallen.github.io/soremachi20/"/>
    <id>https://granvallen.github.io/soremachi20/</id>
    <published>2025-05-11T11:10:00.000Z</published>
    <updated>2025-05-11T13:50:38.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/soremachi.png" style="zoom:80%;" class alt="杂志表纸" /></p><p>几天前看到石黑正数转了一个「それ町」20周年问答的推文, 是的, 2025年已经是连载开始的20周年了. 因为看起来有点意思, 自己业余翻译一下, 如有错误翻得不好请见谅. 这里只翻译了问答的部分, 共27问, 感兴趣可以直接去看原文. 话说原文的观感很好, 作者很用心地给每个提问的人配上了对应心情的步鸟截图…</p><ul><li>原文: <a href="https://soremachi.wordpress.com/2025/05/03/20th-soremachi-anniversary-qa-with-masakazu-ishiguro/">20th Soremachi Anniversary Q&amp;A With Masakazu Ishiguro!</a></li><li>原推: <a href="https://x.com/dailysoremachi/status/1918456235558572469">Daily Soremachi</a></li></ul><p>注: 文中「それ町」指「それでも町は廻っている」中文官方译名《女仆咖啡厅》. 鉴于官方译名太烂保留使用「それ町」.</p><span id="more"></span><p>这个海外的粉丝团体在去年也有过一个问答, 可以看<a href="https://soremachi.wordpress.com/2022/05/29/qa-with-masakazu-ishiguro-on-soremachi-tengoku-daimakyou-and-more/">Q&amp;A With Masakazu Ishiguro on Soremachi, Tengoku Daimakyou and more!</a>. 从这次的问答内容中可以看出, 如果有机会的话, 石黑还是很想画「それ町」后续的, 这一点真可以期待一下. 以及问到石黑创作过程的一些细节. 另外, 到底是谁问想知道步鸟闻起来味道的, 站出来一下, 保证不打死你(老父亲脸).</p><p>最后, 「それでも町は廻ている」也是我最喜欢的漫画之一, 再一次向大家安利这部似牛乳般浓郁纯粹的好作品!</p><h1 id="Q1-from-Internet-Lurker"><a href="#Q1-from-Internet-Lurker" class="headerlink" title="Q1 from Internet Lurker:"></a>Q1 from Internet Lurker:</h1><blockquote><p>Hello sensei. Soremachi is one of my favorite slice of life manga along with Yotsubato and Girls’ Last Tour. I always wonder if you ever read other fellow mangaka’s works? if so what are your favorites.</p><p>My first time experience with Soremachi was on Twitter, I randomly stumbled upon a fan account of your work which shocked me. The fact that there are people outside of 4chan who know of your work really was unexpected.  (The reasons for my surprised was that not many people in western sphere care much for obscure manga with exception being 4chan).</p><p>Anyway I was kinda curious if you ever look around internet community outside of Japan before? Sites like 4chan for example have dedicated people who love manga from many kind. You could see people talk about Yokohama Shopping Trip, Yotsubato, Sayonara Zetsubou Sensei to popular stuff like HxH, Berserk, Mushoku Tensei, Pretty series and Aikatsu and even old anime from 80s-90s!</p><p>Sorry for the long post though, I hope you have fun reading it.</p></blockquote><p>老师您好, 「それ町」是我最喜欢的日常漫画之一, 与《四叶妹妹》和《少女终末旅行》并列. 我一直想知道您是否读过其他漫画家的作品? 如果有, 您最喜欢的是哪些?</p><p>我第一次接触「それ町」是在推特上, 契机是偶然发现了一个您作品的粉丝账号, 这个作品让我感到很震惊. 同时让我没想到的是, 除了4chan之外还有其他人知道您的作品. (我吃惊的原因是, 在西方圈子里, 除了4chan外, 很少有人关注那些冷门漫画).</p><p>不管怎么说, 我还是相当好奇您是否曾经关注过日本之外的网络社群. 像4chan这样的社群, 有一群专门喜欢各种类型漫画的人, 你可以看到人们讨论《横滨购物纪行》, 《四叶妹妹》,《再见, 绝望老师》, 到人气漫画像是《全职猎人》, 《剑风传奇》, 《无职转生》, 《美妙系列》, 《偶像活动》甚至80, 90年代的老动画.</p><p>抱歉我的提问有点长, 希望你读起来会觉得有趣.</p><h2 id="石黑正数"><a href="#石黑正数" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>こんにちは。僕はあまり他の作家の漫画を読まない方でして、子供の頃に影響を受けた漫画を今も繰り返し読んでいます。その中で荒木飛呂彦先生のジョジョはずっと連載が続いているので今も単行本を追っています。  </p><p>インターネットコミュニティも、普段は海外どころか日本のもほとんど見ないです。見ないようにしていると言えます。  </p><p>しかし「天国大魔境」のアニメ放映時は海外の視聴者の反応が見られるコミュニティも見に行きました。</p></blockquote><p>你好, 我个人好像确实没有读太多其他漫画家的作品, 小时候受到影响的漫画现在也依然反复在读. 这其中荒木飞吕彦老师的jojo一直连载至今, 现在也会追单行本看.</p><p>网上社群也是, 我平时不太关注海外的社群, 甚至是日本国内社群也不怎么看. 倒不如说是故意不去看的.</p><p>但是《天国大魔境》动画播放时期, 倒是也去过能看到海外观众反应的社群.</p><h1 id="Q2-from-Lily-USA"><a href="#Q2-from-Lily-USA" class="headerlink" title="Q2 from Lily, USA:"></a>Q2 from Lily, USA:</h1><blockquote><p>Hello, Ishiguro-sensei. Thank you so much for creating Soremachi, which is one of my all-time favorite manga. I have to ask: it seems like there were a few moments where you teased a romantic attraction between Kon and Hotori (for example, the chapter where the latter walks in on the former sleeping and the paneling fakes you out into thinking she’ll try to kiss her). Did you ever think of taking this possibility more seriously? Or, underneath the gags, did you only ever conceive of them as having a deep, but platonic friendship?</p></blockquote><p>你好, 石黑老师. 非常感谢你创作了「それ町」, 这是我至今最喜欢的漫画. 我想要问: 在作品里似乎有一些像在是营造绀与步鸟之间恋爱氛围的情节. (例如有一话, 后者走进来时前者在睡觉, 格子的布局似乎让人误以为她想要亲吻她). 你是否有曾经更严肃地想过这个可能性? 又或者, 在这些玩笑背后, 是否只是把她们之间的关系视作一种深厚但纯洁的友谊.</p><h2 id="石黑正数-1"><a href="#石黑正数-1" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>現時点では友達以上の親密さはあるものの恋愛感情は無いと思います。将来的に状況によっては恋愛感情が芽生えるかもしれないです。<br>紺先輩→歩鳥はあり得そうですが、歩鳥→紺先輩が想像できないんですよね。</p></blockquote><p>目前她们之间有超越朋友的亲密关系, 但我认为并没有恋爱感情. 未来在某些情况下, 产生恋爱感情也说不定.<br>从绀学姐对步鸟的角度看, 似乎是有可能的, 但我无法想象步鸟会对绀学姐产生恋爱感情.</p><h1 id="Q3-from-Sarah-G-USA"><a href="#Q3-from-Sarah-G-USA" class="headerlink" title="Q3 from Sarah G, USA:"></a>Q3 from Sarah G, USA:</h1><blockquote><p>Thank you for responding to my previous question! It was really exciting to hear from an author that I like so much.</p><ol><li>Do you have a favorite chapter of Soremachi? How about a favorite episode of the anime?</li><li>I noticed that in your works you have a lot of very unique alien designs (which I like a lot). What is the process of designing something like a Hiruko?</li></ol></blockquote><p>感谢回复我上一次的提问! 收到喜欢作者的回复实在是让人激动万分.</p><ol><li>在「それ町」中, 您最喜欢的一话是什么? 动画里最喜欢的又是哪一话呢?</li><li>我注意到您的作品中有大量独特的外星人的设计(我非常喜欢). 你设计的过程是怎样的, 比如蛭子的设计?</li></ol><h2 id="石黑正数-2"><a href="#石黑正数-2" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>漫画の中で気に入っているのは15巻の「修学旅行」や歩鳥がオンラインゲームをする回などです。アニメだと「べちこ焼き」の回が好きです。</p><p>ヒルコのデザインは、古代の動物や微生物、海洋生物っぽく見えるようデザインしています。そういった図鑑を眺めて、気持ち悪いなぁと思う形をスケッチしてストックしています。それと縄文土器のデザインも参考にしています。</p></blockquote><p>漫画中最喜欢的是15卷毕业旅行(122话)与步鸟玩在线游戏(79话)的那几话. 动画的话, 喜欢「べちこ焼き」那一集.</p><p>至于蛭子的设计, 是为了看起来像古代的动物, 微生物, 海洋生物设计的. 我会翻看这类图鉴, 把那些有着让人感到不舒服形态的画下来并存下来. 此外, 也参考了绳文土器的设计.</p><h1 id="Q4-from-T-USA"><a href="#Q4-from-T-USA" class="headerlink" title="Q4 from T, USA"></a>Q4 from T, USA</h1><blockquote><p>I understand that the first epilogue chapter was more than a joke than anything, but hypothetically how accurate was it? Would a grown up Tatsuno actually get fat and marry Sanada?</p></blockquote><p>我知道第一个尾声篇(98话)更多是一个玩笑, 但假设一下, 这个结局的准确度有多高? 长大后的辰野真的会变胖一点并且和真田结婚吗?</p><h2 id="石黑正数-3"><a href="#石黑正数-3" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>歩鳥なりに本気で考えたリアルな未来なので半分くらいは実現するんじゃないでしょうか。辰野さんも本人が気を抜いたら太るのかもしれません。</p></blockquote><p>这是步鸟认真考虑后的真实未来, 大概有一半可能真的实现吧. 辰野如果本人松懈大意的话说不定真的会变胖.</p><h1 id="Q5-from-whonever-Chile"><a href="#Q5-from-whonever-Chile" class="headerlink" title="Q5 from whonever, Chile"></a>Q5 from whonever, Chile</h1><blockquote><p>It’s a pleasure to be able to ask you something!! Soremachi was an incredible journey, I recently decided to give it a try and it always made me smile, I’ve been wondering if Ishiguro-sensei has any moment that made him smile while working on Soremachi! THANK YOU! 🙏</p></blockquote><p>能向您直接发问真的非常高兴!!<br>阅读「それ町」是一段绝妙的旅程, 我最近又试着读, 它总能把我逗笑. 我一直想知道石黑老师在创作「それ町」时, 让您笑出来的瞬间! 谢谢! </p><h2 id="石黑正数-4"><a href="#石黑正数-4" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>描いているキャラクターと同じ表情筋を使ってしまうので、キャラが笑っている時は僕も笑っているし、怒っている時はしかめっ面で描いています。</p><p>漫画のネタに自分で笑う事は、あんまりないです。</p></blockquote><p>因为画的过程中总是和角色使用相同的面部表情, 所以角色笑的时候我也在笑, 生气的时候我也皱着眉头画.</p><p>对漫画中的梗自己笑出来则基本不太有.</p><h1 id="Q6-from-Alaino-Brazil"><a href="#Q6-from-Alaino-Brazil" class="headerlink" title="Q6 from Alaino, Brazil"></a>Q6 from Alaino, Brazil</h1><blockquote><p>Hello Ishiguro-sensei, I’m a big fan of your work<br>Was the “Masked Liger” show from Soremachi inspired by any specific rider show or is it just a general idea of one, I loved your suit designs so that always made me curious.</p></blockquote><p>您好, 石黑老师, 我是你作品的头号粉丝.<br>在「それ町」中出现的假面英雄是受哪个具体的假面骑士启发, 还是仅仅是一个泛泛的概念. 我喜欢你的战斗服设计, 所以一直好奇这一点.</p><h2 id="石黑正数-5"><a href="#石黑正数-5" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>「平成ライダー」と呼ばれる一連のシリーズに寄せて描いています。とくに「仮面ライダーカブト」のデザインに最も影響を受けていると思います。</p></blockquote><p>我是受所谓”平成骑士”系列的影响画这个角色的. 特别是受”假面骑士甲斗王”的设计影响最深.</p><h1 id="Q7-from-Boat-Argentina"><a href="#Q7-from-Boat-Argentina" class="headerlink" title="Q7 from Boat, Argentina:"></a>Q7 from Boat, Argentina:</h1><blockquote><p>Is the usage of teenagers over adults intentional in Heavenly Delusion despite being written in a seinen magazine?</p></blockquote><p>尽管天国大魔境在青年漫画杂志上连载, 但是依然使用青少年而非成人的角色设定, 这个是故意的吗?</p><h2 id="石黑正数-6"><a href="#石黑正数-6" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>あまり深い意味は無く、日本の漫画は主人公が15～18歳くらいなのが一番メジャーなのでそこに合わせたものです。</p><p>ただ、昔から「自分が主人公だったら、その年齢でその行動が可能か？」という事をずっと考えて生きて来たので、主人公の年齢に相応しい行動から逸脱しないよう常に気を配って描いています。</p></blockquote><p>这个设定并没有什么深刻的意味, 因为日本漫画主角15到18岁最为常见, 所以就照着这个设定来做了.</p><p>只不过, 从以前开始, 我就一直在思考”如果自己是主角的话, 在那个年龄能做出那种行为吗?”, 所以我总是画得很小心, 确保角色的行为不过于偏离与其年龄相符的范围.</p><h1 id="Q8-from-Shayb-USA"><a href="#Q8-from-Shayb-USA" class="headerlink" title="Q8 from Shayb, USA"></a>Q8 from Shayb, USA</h1><blockquote><p>Who’s your number 1 favorite character from both a series you are a fan of and your own series? And for the character in a series you’re a fan of specifically, did they influence your own original characters at all?</p></blockquote><p>在您喜欢的系列与自己创作的系列中, 谁是最喜欢的角色? 另外, 您喜欢作品的那个角色, 是否有影响到您自己原创角色的创作?</p><h2 id="石黑正数-7"><a href="#石黑正数-7" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>自分が作った中で一番好きなキャラクターは、時々変わるのですが今はキルコですかね。他の作品の好きなキャラクターは一番を決めるのが難しいくらい沢山います。ジョジョ4部の「仗助」、ルパン対複製人間の「マモー」、ＡＫＩＲＡの「金田」など。</p></blockquote><p>自己创作的作品中最喜欢的角色一直有在变, 当前的话应该是斩子. 其他人的作品喜欢的角色太多了所以很难决出最喜欢的. jojo第四部的仗助, 鲁邦三世鲁邦vs复制人中的马摩, 阿基拉的金田等等.</p><h1 id="Q9-from-J-USA"><a href="#Q9-from-J-USA" class="headerlink" title="Q9 from J, USA:"></a>Q9 from J, USA:</h1><blockquote><p>Ishiguro-sensei, I have heard that you are a fan of mystery novels. Have you read any works by the authors Seiryōin Ryūsui (清涼院 流水) or Maijō Ōtarō (舞城 王太郎)?</p><p>Do you think Hotori prefers Shimada Sōji (島田 荘司), or Ayatsuji Yukito (綾辻 行人)?</p></blockquote><p>石黑老师, 我听说您是推理小说迷. 您有读到过清凉院流水和舞城王太郎的作品吗?</p><p>您认为步鸟会更喜欢岛田庄司还是绫辻行人呢?</p><h2 id="石黑正数-8"><a href="#石黑正数-8" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>清涼院流水さんと舞城王太郎さんの作品は、全部ではないですが読んだ事があります。20年前はやたらと推理小説を読み漁っていたのですが今はそれほどでもないです。</p><p>歩鳥は島田荘司さんが好みだと思います。</p></blockquote><p>清凉院流水与舞城王太郎的作品, 我虽然没有全部, 但读过一些. 20年前我非常喜欢而且阅读了大量推理小说, 但现在没那么沉迷了.</p><p>至于步鸟, 我觉得她应该会喜欢岛田庄司.</p><h1 id="Q10-from-Poko-USA"><a href="#Q10-from-Poko-USA" class="headerlink" title="Q10 from Poko, USA"></a>Q10 from Poko, USA</h1><blockquote><p>Both Soremachi and Heavenly Delusion have a lot of complex story lines and details. How do you keep track of it all?</p><p>Also, we saw the beginning of Hotori’s writing career but what kind of career would Kon get into after graduating?</p></blockquote><p>无论是「それ町」还是《天国大魔境》都有着大量复杂的故事线和细节. 您是怎么把所有这一切梳理清楚的?</p><p>另外, 我们能看到步鸟写作事业的起步, 那么绀学姐毕业后会怎么发展职业呢?</p><h2 id="石黑正数-9"><a href="#石黑正数-9" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>どちらの作品も大まかなタイムテーブルを作って、細部の整合性は作品を作りつつ合わせています。</p><p>この方法は頭が混乱して大変なので解放されたいです。</p><p>绀先輩の進路は・・・大学に行った後はわかりません・・・。</p></blockquote><p>这两个作品我都会先制作一个大致的时间表, 细节上的一致性在创作过程中逐步调整.</p><p>这种方法会让人头脑混乱, 相当辛苦, 我还是希望能摆脱它.</p><p>绀学姐的未来去向…上了大学后就不知道了…</p><h1 id="Q11-from-Airship-Australia"><a href="#Q11-from-Airship-Australia" class="headerlink" title="Q11 from Airship, Australia"></a>Q11 from Airship, Australia</h1><blockquote><p>Hello Ishiguro-sensei! Congratulations on 20 years of Soremachi!</p><p>I really loved Murofushi Ryou appearance in Soremachi. She was a very chaotic character and added a different energy compared to Hotori, Tattsun, Kon and more.</p><p>What did you wanted to do with her addition to the story?</p><p>Were there more stories planned for her as she only appeared in a few chapters in the second half of the story?</p><p>What was the inspiration for her character?</p></blockquote><p>石黑老师您好, 「それ町」20周年恭喜!</p><p>我真的很喜欢「それ町」中室伏凉的登场, 她是一个非常混乱的角色, 与步鸟, 辰野, 绀等角色相比, 增添了一种非常不同活力.</p><p>您带着何种目的让她在故事中登场?</p><p>之后有计划创作更多关于她的故事吗? 因为目前她只在故事后半段的少量几话中登场.</p><p>这个角色的灵感来源是什么呢?</p><h2 id="石黑正数-10"><a href="#石黑正数-10" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>室伏涼は、端的に言えば将来的に歩鳥の敵になるかもしれないキャラクターとして登場させました。</p><p>ヒーローに対するヴィランです。</p><p>歩鳥と似て非なる、歩鳥がたじろいでしまうようなキャラにしたかったので、普段よりもキャラを立てることに尽力したような気がします。</p><p>「それ町」の作中での役割は「悪」を受け継ぐ事なので、それ以上登場予定はないですが、未来の話を描くならまた出てくるかもしれません。</p></blockquote><p>室伏凉, 简而言之, 是想作为将来可能成为步鸟敌人的角色登场的.</p><p>她是英雄对应的反派.</p><p>我想让室伏凉成为一个虽然与步鸟相似, 但又有所不同、能让步鸟感到畏惧的角色. 所以, 我觉得自己在塑造这个角色时比平时还要努力.</p><p>因为她在「それ町」中的角色定位是继承”恶”的使命, 所以除此之外没有登场的安排, 但如果会画将来的故事, 她可能会再次登场.</p><h1 id="Q12-from-zabuzabuzazaa-Australia"><a href="#Q12-from-zabuzabuzazaa-Australia" class="headerlink" title="Q12 from zabuzabuzazaa, Australia"></a>Q12 from zabuzabuzazaa, Australia</h1><blockquote><p>When was the last time you cried?</p></blockquote><p>您上一次流泪是什么时候?</p><h2 id="石黑正数-11"><a href="#石黑正数-11" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>落涙するほど泣いたのは子供が生まれた時だったかと思います。</p></blockquote><p>哭到流眼泪程度的话应该是孩子出生时候.</p><h1 id="Q13-from-Danny-Germany"><a href="#Q13-from-Danny-Germany" class="headerlink" title="Q13 from Danny, Germany"></a>Q13 from Danny, Germany</h1><blockquote><p>If you were to start Soremachi today, but with all of your current knowledge, would you change something?</p></blockquote><p>如果您今天才开始创作「それ町」, 但是拥有您当前所有的知识, 你会做改变吗?</p><h2 id="石黑正数-12"><a href="#石黑正数-12" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>最初の方のノリが変わると思います。もっと地味な始まり方をするかもしれません。</p><p>あと、室伏涼をもっと早い段階で登場させたいです。</p></blockquote><p>我可能会调整故事开始时的节奏. 可能会以更平淡的方式叙事.</p><p>另外, 也想让室伏凉在更早的阶段登场.</p><h1 id="Q14-from-Majin-Hotori-USA"><a href="#Q14-from-Majin-Hotori-USA" class="headerlink" title="Q14 from Majin Hotori, USA:"></a>Q14 from Majin Hotori, USA:</h1><blockquote><p>Hello, Ishiguro-sensei. Recently, I finished the Soremachi manga and absolutely adored it. I really appreciated the mid to late 2000s vibe the series has throughout, from bulky, portable DVD players to original DS systems. Did you ever struggle with accidentally putting anachronistic elements as time passed?</p><p>Additionally, thank you for creating Kon-senpai. She has some of my favorite expressions drawn to a manga.</p></blockquote><p>您好, 石黑老师. 我最近读完了「それ町」漫画, 非常喜欢. 我特别欣赏这作带有的贯穿2000年代中期到后期的氛围, 从笨重的便携DVD播放器到初版DS系统. 在创作过程中, 随着时间推移, 您有没有遇到不小心加入了时代不符元素的情况?</p><h2 id="石黑正数-13"><a href="#石黑正数-13" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>描き始めてから終わるまでに、世間ではブラウン管テレビが液晶になり、放送はデジタルに完全移行し、色々な変化がありました・・・。</p><p>基本的には思い出の中から話を作っているので時代錯誤な物を間違って描いたことは無いのですが、あえて作中の時代感を曖昧にするため作品後半ではスマホを登場させた事があります。</p></blockquote><p>从开始画到结束, 社会上发生了很多变化, 比如CRT电视变成了液晶电视, 广播完全转向了数字化, 还有各种各样其他的变化…</p><p>基本上, 我是从记忆中创造故事的, 所以基本没有错画时代不符的东西. 不过, 为了故意模糊作品中的时代感, 在作品后半部分我确实加入了智能手机的出现.</p><h1 id="Q15-from-Danny-Germany"><a href="#Q15-from-Danny-Germany" class="headerlink" title="Q15 from Danny, Germany"></a>Q15 from Danny, Germany</h1><blockquote><ol><li>What is your favourite character that only appeared for a single chapter?</li><li>Is there a character you wish you would have used more?</li><li>Have you ever abandoned a story line you planed on making?</li></ol></blockquote><ol><li>在单话登场的角色中, 您最喜欢的是哪位角色呢?</li><li>是否有一个角色您想让他再次登场?</li><li>您是否有过在创作过程中途放弃一条原本计划好的故事线</li></ol><h2 id="石黑正数-14"><a href="#石黑正数-14" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>1、「それ町」だと木星人、「天国大魔境」だと地獄夢(ヘルム)ですかね・・・。<br>2、「天国大魔境」のミチカです。<br>3、 あります。ネームまで描いて担当さんからＯＫと言われたのに捨てて描き直した事もあります。</p></blockquote><ol><li>「それ町」里的话是木星人, 《天国大魔境》的话则大概是地狱梦.</li><li>《天国大魔境》的美千花.</li><li>有过. 虽然我已经画到了分镜阶段并得到了编辑的认可, 但还是丢掉重新画过.</li></ol><h1 id="Q16-from-のつ-日本"><a href="#Q16-from-のつ-日本" class="headerlink" title="Q16 from のつ, 日本"></a>Q16 from のつ, 日本</h1><blockquote><p>歩鳥は作中でたまに小学生の頃に起きたことや、幼少期にシーサイドで起きた出来事などをわりと鮮明に思い出している(追体験している？)のですが、石黒先生もこういったことが出来る方なのでしょうか。</p><p>タケルのエピソードで描かれる男子小学生の様子がめちゃくちゃリアルで、自分では思い出せなかったけれど(確かにこんなことあったな)と共感出来る事ばかりなので面白いです。</p><p>女子と遊ぶやつは女だぞ！など、自分もやったわ確かに！という懐かしさがあります。</p></blockquote><p>步鸟在作品中有时会非常清晰地回忆起小学或幼年时期在Seaside发生的事(可说是追溯过去的体验?), 不知道石黑老师是不是也是能做到这类事的人呢?</p><p>小猛的几话里对于小学男生的表现刻画得十分真实, 我自己虽然记不起来那些事(好像确实有过这样的事啊)类似感同身受的时刻有好多, 这点也非常有趣.</p><p>和女生一起玩的人就是女生哦! 这种话, 我自己以前确实也说过! 这种怀念的感觉油然而生.</p><h2 id="石黑正数-15"><a href="#石黑正数-15" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>昔の事を思い出したり誰かに話すというのは普通の事だと思っていたのですが・・・。いや多分誰でもできると思います。<br>のつさんも普段必要がないから思い出さないだけで、切っ掛けや必要性があれば思い出すと思いますよ。</p></blockquote><p>我一直觉得回想起过去的事情和与别人谈论过去都是很普通的事情…不, 我感觉大概谁都能做到吧.<br>のつさん平时只是因为没有必要才没有去回忆, 如果有个契机或必要性, 我想您一定也是能回想起来的.</p><h1 id="Q17-from-Nichole-Hong-Kong"><a href="#Q17-from-Nichole-Hong-Kong" class="headerlink" title="Q17 from Nichole, Hong Kong"></a>Q17 from Nichole, Hong Kong</h1><blockquote><p>You are my favourite mangaka of all times! Soremachi ended around the time I was graduating and I still remember how I felt after reading Soremachi for so long. How did you decide that Soremachi was going to end and how did it feel when you finally finished the work? Do you think you will feel the same way for Tengoku Daimakyou?</p></blockquote><p>您一直是我最喜欢的漫画家! 「それ町」是在我毕业那段时间完结的, 但我一直记得读完「それ町」时的感受. 您是如何决定要完结「それ町」的? 当您完成这部作品时是什么感受? 您觉得对于《天国大魔境》也会有同样的感受吗?</p><h2 id="石黑正数-16"><a href="#石黑正数-16" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>ありがとうございます。</p><p>「それ町」を終わらせようと思ったのは、「それ町」におけるパフォーマンスのピークを迎えたと自分の中で思ったからのような気がします。</p><p>終わった時は感慨深さもありましたが、「この先の収入はどうしよう？」という不安が強かったです。</p><p>多分「天国大魔境」が終わらせた時も、その先の事で不安になるのが今から目に見えています。</p></blockquote><p>非常感谢.</p><p>我之所以想要完结「それ町」, 大概是因为在我心里, 这部作品的展现内容差不多到达了极限.</p><p>完结的时候虽然也有很深的感慨, 但”接下来的收入怎么办?”这样的不安感更加强烈.</p><p>大概《天国大魔境》完结的时候, 也会因为之后的事情而感到不安吧, 这一点现在已经可以预见了.</p><h1 id="Q18-from-Bacassu-Brazil"><a href="#Q18-from-Bacassu-Brazil" class="headerlink" title="Q18 from Bacassu, Brazil"></a>Q18 from Bacassu, Brazil</h1><blockquote><p>Thank you for creating manga, Soremachi is currently my favorite manga and possibly story ever, with Tengoku Daimakyou in second place. I have a few questions:</p><ol><li><p>Do you still plan to make more Soremachi content of any kind? If not, how do you see the relationship between Hotori and Kon evolving after the manga, could they really end up working together in a detective agency? What about the relationship between Hotori and Shizuka? I would love to see more content of the 3 of them, they’re my favorite characters.</p></li><li><p>I noticed that, in different chapters of the story, Kon picks up a stick from the ground and carries it with her, which I think is amazing because it’s a mundane detail that adds consistency and realism to the character. How did you keep track of these small details when building such a complex and rich story that took years to complete?</p></li><li><p>What is your favorite Soremachi chapter?</p></li><li><p>What are your favorite mystery and detective novels?</p></li><li><p>How do you feel knowing that, despite not being very famous, your story manages to deeply captivate and inspire readers even overseas? Did you expect something like that when you started creating? Were you at any moment surprised by the emotional depth of your own story?</p></li></ol></blockquote><p>感谢您创作了这部漫画, 「それ町」不只是我目前最喜欢的漫画, 也可能是我最喜欢的故事作品了, 《天国大魔境》排在第二. 我有几个问题想问:</p><ol><li>您是否还打算创作更多其他形式的「それ町」相关的内容? 如果没有, 您认为在漫画结束之后步鸟和绀学姐的关系会怎样发展, 她们真的会在一个侦探事务所里一起工作吗? 步鸟和静姐的关系会怎么发展呢? 我非常想看到更多关于她们三个的故事, 她们是我最爱的几个角色.</li><li>我注意到, 在故事的不同章节里, 绀都有从地上捡起一根棍子并随身带着的情节. 我觉得这非常了不起, 因为通过这样一个平凡的小细节, 让角色更具一致性和真实感. 在创作这样一个复杂丰富且花费多年才完成的故事时，您是如何保持关注这些小细节的呢？</li><li>在「それ町」中, 您最喜欢哪一话?</li><li>您最喜欢的推理小说是什么?</li><li>当得知尽管这部作品没有非常出名, 但依然能成功深深打动和激励国内外读者时, 您有什么感受? 在您刚开始创作时有预想到会这样吗? 您有没有曾经对自己创作的故事中蕴含的深厚情感感到惊讶？</li></ol><h2 id="石黑正数-17"><a href="#石黑正数-17" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>1、今の所「それ町」に新たなエピソードを加える予定は無いのですが、何か機会があれば描くかもしれません。しかし歩鳥は探偵にはならないと思います。</p><p>2、各キャラクターは自分自身が投影されていたり、実在の人物のように頭の中に存在していたので、その時々で各キャラクター本人らしい行動を取っているんだろうなぁ、と思っています。あまり自分で管理している気はしないです。</p><p>3、1番を決めるのは難しいのですが10巻の「歩鳥の戦争」ですかね。</p><p>4、島田荘司の「異邦の騎士」です。</p><p>5、海外の方々に読んで頂けるとは当初まったく考えていなかったので、楽しんで頂けたなら嬉しいです！</p></blockquote><ol><li>目前还没有计划为「それ町」增加新的剧情, 不过如果有什么机会的话也许会再画. 只是我想步鸟大概做不了侦探.</li><li>每个角色或多或少都有自己的投影, 在我的脑海中就像真实存在的人一样. 我觉得这样会让他们在不同的情境下做出符合自己性格的行为, 我倒并不觉得是有在刻意地控制这些角色.</li><li>要选出个第一是很难的, 大概是第十卷的”步鸟的战争”吧.</li><li>岛田庄司的《异邦骑士》.</li><li>起初完全没想过会有海外的读者阅读，如果大家喜欢的话，我很开心！</li></ol><h1 id="Q19-from-eita-日本"><a href="#Q19-from-eita-日本" class="headerlink" title="Q19 from eita, 日本"></a>Q19 from eita, 日本</h1><blockquote><p>Although a lot of eggplant food comes out in Soremachi. Does Mr. Ishiguro like eggplants?</p></blockquote><p>「それ町」中出现了大量的茄子料理. 石黑老师您喜欢茄子吗?</p><h2 id="石黑正数-18"><a href="#石黑正数-18" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>ナスは好きですが、とくに1番好きという事はなくて。</p><p>「それ町」にやたらとナスが出てくるのは、ジャガイモやコーンなみにナスが加工されて使われたら面白いのでは？思ったからです。</p></blockquote><p>虽然我喜欢茄子, 但也说不上特别喜欢.<br>「それ町」里茄子频繁登场, 是因为我在想, 如果茄子像土豆或玉米那样被各种加工料理的话, 会不会很有趣呢?</p><h1 id="Q20-from-whonever-Chile"><a href="#Q20-from-whonever-Chile" class="headerlink" title="Q20 from whonever, Chile"></a>Q20 from whonever, Chile</h1><blockquote><p>Hello, Ishiguro-sensei! Hotori sure prefer books, but do you think she’d also enjoy the internet if given proper tools and instructions? What kind of stuff would she search for? Thank you!</p></blockquote><p>石黑老师, 您好! 步鸟显然非常喜欢看书, 不过您觉得如果有人给她提供合适的工具和指导, 她也会喜欢上互联网吗? 她会上网查些什么呢? 谢谢!</p><h2 id="石黑正数-19"><a href="#石黑正数-19" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>「未解決事件」とか「都市伝説」とか「怖い話」、それとしょうもない雑学などを調べるのが目に浮かびます。</p></blockquote><p>我已经可以想象她会去查“未解之谜”, “都市传说”, “恐怖故事”, 还有一些没什么卵用的小知识之类的东西了.</p><h1 id="Q21-from-Twig-USA"><a href="#Q21-from-Twig-USA" class="headerlink" title="Q21 from Twig, USA"></a>Q21 from Twig, USA</h1><blockquote><p>Ishiguro-Sensei, what kind of job do you think Kon will end up getting in the future?</p></blockquote><p>石黑老师, 您觉得绀将来会做什么工作呢?</p><h2 id="石黑正数-20"><a href="#石黑正数-20" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>よく聞かれるんですが、思い付かないんです。服飾系の大学に行った事になっているので、上手くすればそっち方面の仕事に就くのではないかと。</p></blockquote><p>经常有人问我这个问题, 但我想不出来. 因为设定上是去读了服装设计相关的大学, 如果顺利的话, 大概会从事那方面的工作吧.</p><h1 id="Q22-from-gubber-Panama"><a href="#Q22-from-gubber-Panama" class="headerlink" title="Q22 from gubber, Panama"></a>Q22 from gubber, Panama</h1><blockquote><p>What does Hotori smell like?</p></blockquote><p>步鸟闻起来是什么味道的?</p><h2 id="石黑正数-21"><a href="#石黑正数-21" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>飾り気がない子なので、前の晩に使った石鹸やシャンプーの匂いがするんじゃないでしょうか。</p></blockquote><p>因为她是个不爱打扮的孩子, 所以可能会带着前一天晚上用的香皂或洗发水的香味吧.</p><h1 id="Q23-from-DonMayo-USA"><a href="#Q23-from-DonMayo-USA" class="headerlink" title="Q23 from DonMayo!, USA"></a>Q23 from DonMayo!, USA</h1><blockquote><p>I had a few questions regarding the anime production for the series! With the huge amount of staff behind the production, was there any conflicted/changes you told the head staff?</p><p>The anime opening movie had a ton of great key animators, like Nozomu Abe, was it a joy to have Yasuomi Umetsu direct the opening!?</p><p>Final question, what was your thoughts of the staff behind the anime Soremachi! You had the great people at studio shaft to create another banger! With my favorite being Ryo Imamura!</p></blockquote><p>我有几个关于这个系列动画的问题! 动画的制作背后涉及很多人, 您有和核心制作团队提出过一些异议或修改建议的地方吗?</p><p>这部动画的OP有非常多优秀的动画师参与, 像是阿部望, 这很让人兴奋吧? 梅津泰臣担任了OP导演!?</p><p>最后一个问题, 您对于「それ町」动画的幕后制作团队有什么想法! 这次也由出色的Shaft工作室负责制作, 又一部精彩作品! 我最喜欢的今村亮也参与了!</p><h2 id="石黑正数-22"><a href="#石黑正数-22" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>アニメはアニメの表現方法があるので基本的には現場にお任せする姿勢だったのと、「それ町」の時は結構な回数打ち合わせに参加したので方向性の食い違いというのは無かったです。</p><p>梅津泰臣さんは好きなアニメーターなのでＯＰを担当していただいたのは嬉しかったですし、素晴らしいＯＰだと思います。</p><p>アニメに関わっていただいたスタッフの皆様には感謝しかないです。</p></blockquote><p>因为动画有其独特的表现方式, 所以我基本是采取交由现场制作团队决定的态度. 而且在「それ町」的动画制作期间我也参加了不少次会议, 因此没有出现方向性的分歧.</p><p>梅津泰臣先生是我很喜欢的动画师, 能有幸请他来负责OP让我很开心, 我也觉得这是一个非常精彩的OP.</p><p>对参与动画制作的所有工作人员, 我只有满满的感激.</p><h1 id="Q24-from-Mayday-Malaysia"><a href="#Q24-from-Mayday-Malaysia" class="headerlink" title="Q24 from Mayday, Malaysia"></a>Q24 from Mayday, Malaysia</h1><blockquote><p>Congratulations on the 20th anniversary of Soremachi! You had mentioned that Soremachi was initially planned to run for just 4 volumes but it went on to go for 16 volumes.</p><p>When did you realize that Soremachi was going to be a successful series? Was it the anime announcement, the volume sales, more people recognizing you or something else?</p><p>And how did that change your approach to writing the rest of the Soremachi manga?</p></blockquote><p>「それ町」20周年恭喜! 您曾经提到「それ町」最开始只计划画4卷, 但是最后画了16卷之多.</p><p>您什么时候意识到「それ町」将会成为一个成功的系列? 是在动画化宣布的时候? 销量提高的时候? 还是有更多读者认识你的时候? 还是别的什么?</p><p>这些变化有改变您接下来创作「それ町」漫画剩下部分的方式吗?</p><h2 id="石黑正数-23"><a href="#石黑正数-23" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>「それ町」が連載として軌道に乗ったと思ったのは4巻くらいの財宝探しの話を描いた頃で、作品として上手くいっていると感じたのは7巻が出た頃ですね。</p><p>「それ町」の連載の継続には自分の精神的な余裕の有無が一番大きく関わっていた気がします。</p><p>そういった意味でアニメ化の企画は精神的余裕をもたらしてくれたと思います。</p></blockquote><p>我觉得「それ町」开始步入连载正轨, 是在画到第4卷寻宝故事那一话的时候. 而感受到作品风格逐渐完整成型, 大概是第7卷出版的时候.</p><p>关于「それ町」连载能否持续, 最关键的其实是我有没有精神上的余裕.</p><p>从这个意义上来说, 我认为动画化的企划确实带给我很大的精神支持.</p><h1 id="Q25-from-Ringo-Arashiyama-日本"><a href="#Q25-from-Ringo-Arashiyama-日本" class="headerlink" title="Q25 from Ringo Arashiyama, 日本"></a>Q25 from Ringo Arashiyama, 日本</h1><blockquote><ol><li>The chronology of each episode of SOREMACHI is backwards and forwards. Was that your intention from the beginning of the series?</li><li>How far in advance did you think about the story when you started serializing it?</li><li>Please choose your favorite episodes as if you were compiling a masterpiece selection.</li></ol></blockquote><ol><li>「それ町」各话时间顺序是前后跳跃的, 这是您在创作初期就有意识地安排的吗?</li><li>在开始连载这部作品的时候, 您提前构思了多少故事内容?</li><li>如果要您亲自编一套故事精选集的话, 您会挑哪些自己最喜欢的话数呢?</li></ol><h2 id="石黑正数-24"><a href="#石黑正数-24" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>1、時系列シャッフルは、「サザエさん」のようなループでもなく「あすまんが大王」のようなリアルタイムでもない方法としてうっすら構想はありましたが、ハッキリやれそうだと思い実行したのは1巻の途中だっと思います。<br>2、一応、歩鳥が天国から戻る話、放火魔の話、サイコロの絵関連の話、溝口ナメ子事件などは初期からありました。<br>3、その時々の気分で変わってくるので割愛しますが、旅行に行く系の話は好きですね。</p></blockquote><ol><li>关于打乱故事时间顺序的做法, 我一开始就隐约有这个构想. 我想尝试一种既不是像《海螺小姐》那样时间循环, 也不是像《阿兹漫画大王》那样按现实时间推进的方式. 不过, 真正明确地觉得”这种方式可以做”并开始付诸实践, 是在第1卷连载到一半的时候.</li><li>步鸟从天国回来的故事、纵火犯事件、骰子画相关的故事、沟口奈美子事件等等, 这些情节其实从创作初期就已经构思出来了.</li><li>喜欢的故事会随当时的心情而有所不同, 就不一一列举了, 不过我挺喜欢”去旅行”这一类的故事的.</li></ol><h1 id="Q26-from-Kirashura-USA"><a href="#Q26-from-Kirashura-USA" class="headerlink" title="Q26 from Kirashura, USA"></a>Q26 from Kirashura, USA</h1><blockquote><p>How do you combat lack of motivation and laziness? I imagine with manga being a job for you the obligation is a push but I’ve been quite curious how someone in the industry, especially one of my favorites, battles such.</p></blockquote><p>您是怎么对抗动力缺乏和懒惰的? 我能想到创作漫画作为你的职业, 本身的责任感或许是一个推动力. 但我还是很好奇像您这样的业内人士, 尤其是我很喜欢的创作者, 是怎么对抗创作中的惰性的？</p><h2 id="石黑正数-25"><a href="#石黑正数-25" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>「焦り」です。働いて家族を養う責任、描き続けないと保たれない社会的な立場などに駆り立てられて描いています。</p><p>それと、自分で納得のいくストーリーを思い付く事です。自分で面白いと思えない物を仕上げる事ほど苦痛な事はないので。ストーリーが気に入っていれば仕上げるモチベーションになります。</p></blockquote><p>是”焦虑”. 我是在用工作养家糊口的责任感, 以及”如果不继续创作就难以维持现在社会身份”的焦虑驱使下, 持续画下去的.</p><p>还有一点就是, 想创作出自己真正满意的故事. 如果要把一个连自己都觉得无趣的东西做完, 那是最痛苦的了. 只要自己喜欢这个故事, 它本身就能成为完成它的动力.</p><h1 id="Q27-from-poko-Singapore"><a href="#Q27-from-poko-Singapore" class="headerlink" title="Q27 from poko, Singapore"></a>Q27 from poko, Singapore</h1><blockquote><p>Hello Ishiguro-sensei, I’ve been reading Furutto recently and enjoy the simple but relaxing gag comedy of it. It made me realize that I would want a spinoff Soremachi 4koma manga with Josephine as the protagonist!</p><p>What do you think of the idea? How many volumes do you think you can write about it and was the idea of Furutto inspired by Josephine in some way? I always wondered what Josephine do when no one is at the Arashiyama household.</p></blockquote><p>石黑老师, 您好! 我最近在读《我家的街猫》, 很喜欢它那种简单又轻松的搞笑风格. 这让我想到, 如果有一部以「それ町」的乔瑟菲奴为主角的四格漫画衍生作品, 我一定会想读!</p><p>您觉得这个主意怎么样呢? 如果真要画的话, 您觉得能画几卷呢? 阿抖的灵感是否在某种程度上来自乔瑟菲奴呢? 我一直想知道乔瑟菲奴在岚山家没人的在做什么.</p><h2 id="石黑正数-26"><a href="#石黑正数-26" class="headerlink" title="石黑正数:"></a>石黑正数:</h2><blockquote><p>フルットを続けている限りはジョセフィーヌの漫画を自分で描くことは無いと思いますが、仮にフルットではなくジョセフィーヌが主人公だった場合、やはり15年くらい連載し続けるんじゃないかと思います。</p><p>ジョセフィーヌは家に誰もいない時、寝てると思います。子犬だったころはティッシュの箱を噛みちぎり中身をぶちまけたり、本をバラバラに破壊したと思います。うちの犬がそうでしたから。</p></blockquote><p>只要我还在连载《我家的街猫》, 我应该不会再自己去画乔瑟菲奴的漫画了. 但如果不是阿抖, 而是让乔瑟菲奴当主角的话, 恐怕也会像《我家的街猫》一样, 连载个15年左右吧.</p><p>我觉得乔瑟菲奴在家里没人时, 大概就是在睡觉吧. 不过还小的时候可能会咬烂纸巾盒, 把里面的纸撒得满地都是, 甚至还把书撕成碎片. 因为我们家的狗以前就是这样的.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.</p><p><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=1369727533&auto=1&height=66"></iframe><br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/soremachi.png&quot; style=&quot;zoom:80%;&quot; class alt=&quot;杂志表纸&quot; /&gt;&lt;/p&gt;
&lt;p&gt;几天前看到石黑正数转了一个「それ町」20周年问答的推文, 是的, 2025年已经是连载开始的20周年了. 因为看起来有点意思, 自己业余翻译一下, 如有错误翻得不好请见谅. 这里只翻译了问答的部分, 共27问, 感兴趣可以直接去看原文. 话说原文的观感很好, 作者很用心地给每个提问的人配上了对应心情的步鸟截图…&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原文: &lt;a href=&quot;https://soremachi.wordpress.com/2025/05/03/20th-soremachi-anniversary-qa-with-masakazu-ishiguro/&quot;&gt;20th Soremachi Anniversary Q&amp;amp;A With Masakazu Ishiguro!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;原推: &lt;a href=&quot;https://x.com/dailysoremachi/status/1918456235558572469&quot;&gt;Daily Soremachi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注: 文中「それ町」指「それでも町は廻っている」中文官方译名《女仆咖啡厅》. 鉴于官方译名太烂保留使用「それ町」.&lt;/p&gt;</summary>
    
    
    
    <category term="ACGN" scheme="https://granvallen.github.io/categories/ACGN/"/>
    
    <category term="漫画" scheme="https://granvallen.github.io/categories/ACGN/%E6%BC%AB%E7%94%BB/"/>
    
    
    <category term="Anli" scheme="https://granvallen.github.io/tags/Anli/"/>
    
    <category term="不务正业" scheme="https://granvallen.github.io/tags/%E4%B8%8D%E5%8A%A1%E6%AD%A3%E4%B8%9A/"/>
    
    <category term="漫画" scheme="https://granvallen.github.io/tags/%E6%BC%AB%E7%94%BB/"/>
    
    <category term="翻译" scheme="https://granvallen.github.io/tags/%E7%BF%BB%E8%AF%91/"/>
    
  </entry>
  
  <entry>
    <title>漫画推荐《路边的藤井》</title>
    <link href="https://granvallen.github.io/robonofujii/"/>
    <id>https://granvallen.github.io/robonofujii/</id>
    <published>2025-02-11T12:39:00.000Z</published>
    <updated>2025-02-11T13:44:57.000Z</updated>
    
    <content type="html"><![CDATA[<p>不务正业系列, 尝试机翻了一集聊漫画「路傍のフジイ」的日语播客, 出自<a href="https://www.youtube.com/watch?v=NiNxTihy7vM&amp;t=28s">YutaNのサブカル流し</a>, 因为意外地聊得很不错来安利下, 也欢迎订阅原播客谢谢喵. 翻译用时AI精翻5分钟, 人工粗校5小时. 说回来, 我个人还是很喜欢这作的, 有时间再写写关于这作的批评吧.</p><span id="more"></span><p>本作原名「路傍のフジイ〜偉大なる凡人からの便り〜」, 连载自小学馆「週刊ビッグコミックスピリッツ」的2023年24号, 目前隔周连载中, 作者为鍋倉夫(なべくらお), 日版单行本已出三卷, 2025年2月28日发售第四卷.</p><div style="position: relative; width: 100%; height: 0; padding-bottom: 50%;"><iframe src="//player.bilibili.com/player.html?autoplay=false&autoplay=false&bvid=BV13SNGedEaj&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position: absolute; width: 80%; height: 100%; Left: 10%; top: 0;"></iframe></div><p>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.</p><p><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=26470661&auto=0&height=66"></iframe><br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;不务正业系列, 尝试机翻了一集聊漫画「路傍のフジイ」的日语播客, 出自&lt;a href=&quot;https://www.youtube.com/watch?v=NiNxTihy7vM&amp;amp;t=28s&quot;&gt;YutaNのサブカル流し&lt;/a&gt;, 因为意外地聊得很不错来安利下, 也欢迎订阅原播客谢谢喵. 翻译用时AI精翻5分钟, 人工粗校5小时. 说回来, 我个人还是很喜欢这作的, 有时间再写写关于这作的批评吧.&lt;/p&gt;</summary>
    
    
    
    <category term="ACGN" scheme="https://granvallen.github.io/categories/ACGN/"/>
    
    <category term="漫画" scheme="https://granvallen.github.io/categories/ACGN/%E6%BC%AB%E7%94%BB/"/>
    
    
    <category term="Anli" scheme="https://granvallen.github.io/tags/Anli/"/>
    
    <category term="不务正业" scheme="https://granvallen.github.io/tags/%E4%B8%8D%E5%8A%A1%E6%AD%A3%E4%B8%9A/"/>
    
    <category term="漫画" scheme="https://granvallen.github.io/tags/%E6%BC%AB%E7%94%BB/"/>
    
  </entry>
  
  <entry>
    <title>复刻一个Pixel Art水体渲染实现</title>
    <link href="https://granvallen.github.io/pixelartwater/"/>
    <id>https://granvallen.github.io/pixelartwater/</id>
    <published>2024-12-21T09:20:00.000Z</published>
    <updated>2025-10-01T14:12:12.222Z</updated>
    
    <content type="html"><![CDATA[<p>jojo, 这是我今年最后的更新了! 咳咳, 这次继续来现学现卖, 分享下看到的 Pixel Art 相关的渲染技术. 前段时间油管上看到一个非常棒的 Pixel Art 水体渲染的分享, 是由游戏开发者 jess 制作的 <a href="https://www.youtube.com/watch?v=pGOLstWBCDA&amp;t=17s">How I Created 2D Pixel Art Water - Unity Shader Graph</a>. 这虽然是她的第一个视频, 但是质量高得可怕, 不仅制作过程思路清晰, 最后实现的效果也相当不错. 那话怎么说的来着, 新人都是怪物. 此后的几次分享同样好评如潮, 因为内容都是我感兴趣的领域, 所以我大概都会稍微研究一下. </p><span id="more"></span><p>回到正题, jess 在这个视频中分享了她如何通过 unity shader graph 实现 Pixel Art 风格的水体, 并且开源了<a href="https://github.com/jess-hammer/2d-pixel-water-shader/tree/main">示例工程</a>, 还非常”贴心”地更新了 godot 的实现(似乎 jess 也在之前的 unity 收费政策风波中打算把工程迁移至 godot). 如果你只关心实现, jess 的分享显然比这干巴巴的文字直观生动多了, 推荐直接去看.</p><p>而在这篇文章中, 我首先尝试用 unity 的 shaderlab 重写了 jess 的实现, 以便更容易地在项目中复用. 接着又试着实现一个2d水体倒影和水深效果的方案, 以期让水体与其他物体的交互看起来更生动. 事先申明, 这篇文章可能会涉及多个话题, 包括2d水体渲染、URP、shader、tilemap、渲染合批, 这每一个话题都是大坑, 也都值得单独拿出来讲. 如果你曾关注过这些关键词, 那么这个分享就不会有太多障碍, 可能还能些许帮到你. 同时必须说这些分享也还没有经过足够的实践, 仍需进一步迭代和验证. </p><h1 id="复刻2d水体渲染"><a href="#复刻2d水体渲染" class="headerlink" title="复刻2d水体渲染"></a>复刻2d水体渲染</h1><p>水体渲染算是图形学中比较基础且重要的部分, 有大量的研究和实践, 但通常是对于3d游戏而言. 不过近年来3d与2d实现方法结合的2d游戏似乎也越来越流行. 就我之前自己的调研来看, 传统2d, 特别是基于 tilemap 的游戏最常见的做法还是手绘水体 tile, 要更生动一点, 可以做一个 tile 的动画循环播放. 而有一些则在”2d游戏”里使用偏真实的水体渲染. 其实近年来也挺多的, 让我印象深刻的像是风来之国最后夜晚海面演出的那段. 再比如这个<a href="https://forum.gamemaker.io/index.php?threads/beast-socket-procedural-monsters.36711/">“2d游戏”</a>的水体看起来也非常 nice. jess 的方案也是类似, 通过把3d水体渲染方法进行简化和 Pixel Art 风格化来实现, 这种方法基于纯 shader 实现, 在复用性非常高的同时还让水体有丰富的变化. 而 2d 与 3d 不同的部分(比如没有体积与深度)则需要使用一些特殊方法, 这也是比较有趣的一点. 但反过来说, 这类的分享却非常少看到, 特别是像 jess 如此细腻2d水体的分享.</p><p>另一个我觉得需要说明的是水体是一个大类, 海洋, 湖泊, 河流, 瀑布中水的表现是存在差异的. 这也是我觉得这个标题有那么点名不副实的地方. jess 实现的水体其实更接近海洋与湖泊的水体, 特点是运动缓慢, 泛着波光. 而河水通常水流较急, 运动具有明显方向性, 波光也相对规律.</p><h2 id="水体的tile"><a href="#水体的tile" class="headerlink" title="水体的tile"></a>水体的tile</h2><p>在具体的 shader 实现前, 必须先提这个方案中水体材质的载体 tilemap. jess 的游戏是基于 tilemap 的, 她使用内部(水体部分)为黑色, 边缘为白色的图块作为水体的 tileset. 如图1是我自己做的水体 tileset. 这里同样可以做成 animate tile, 增加水体伸展的动画. 而 shader 则负责在黑色的部分画出水体效果. 这种制作方式同样有极高的复用性, 只需要更换材质, 不必重新制作 tileset 就可以做出不同的地形表现.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/water_tile_256x256.png" style="zoom:80%;" class alt="图1" /></p><h2 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h2><p>在这个 jess 的方案中, 考虑了水体表现的几个部分, 如图2,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/water_detail.png" style="zoom:80%;" class alt="图2" /></p><ul><li>海水近岸颜色渐变</li></ul><p>海水远离沙滩岸边, 随着海水变深, 岸底能见度下降, 海水颜色与沙滩颜色的混合渐变.<br>这是一个非常细节的表现, 其实很少在2d游戏中看到. 同样, 做法也非常地3d, 使用高度图或深度图进行颜色混合. jess 的游戏是一个程序生成地形的开放世界游戏, 所以本身就有这个高度信息. 我自己的实现中不存在这个地形高度, 所以这一条不考虑在内.</p><ul><li>波纹</li></ul><p>波纹的表现通常是在水体有相当透明度时出现, 有两个部分, 其一是使用散焦纹理(Caustic Texture)实现波纹底色的部分(图2位置1), 以及在底色基础上增加的高亮(图2位置2), 使水体看起来像是有深度且受光的表现.<br>同时这个波纹有扰动效果, 以及波纹的渐隐渐现, 体现水体的运动.</p><ul><li>波光</li></ul><p>即水面模拟受光镜面反射(Specular)的部分(图2位置3).</p><ul><li>泡沫</li></ul><p>泡沫(Foam)也是水体重要的部分. 其实严格来说也有好几种, 一种通常称为岸边泡沫, 是海岸与海水分界线上产生的细密泡沫(图2位置4). 另一种是物体在水中, 接触面上产生的交互泡沫. 在 jess 的另一个分享 <a href="https://www.youtube.com/watch?v=W4eVR_Fm5Gs&amp;t=205s">How I Made Pixel Art Water Trails - Godot Visual Shader</a> 中正分享了她是如何实现交互泡沫的.</p><p>大体上就是这些, 在 jess 的分享中似乎还不涉及光影的部分.</p><h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p> 完整的实现水体渲染 shader 如下, 部分函数定义在 pixel_art.hlsl 与 common.hlsl 见附录1.<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br></pre></td><td class="code"><pre><span class="line">Shader <span class="string">&quot;Custom/PixelWater3&quot;</span></span><br><span class="line">&#123;</span><br><span class="line">    Properties</span><br><span class="line">    &#123;</span><br><span class="line">        _MainTex(<span class="string">&quot;Texture&quot;</span>, <span class="number">2</span>D) = <span class="string">&quot;&quot;</span> &#123;&#125;</span><br><span class="line">        _Color(<span class="string">&quot;Color&quot;</span>, Color) = (<span class="number">0</span>, <span class="number">0.57</span>, <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">        _PPU(<span class="string">&quot;Pixels Per Unit&quot;</span>, Range(<span class="number">1</span>, <span class="number">100</span>)) = <span class="number">32</span></span><br><span class="line"></span><br><span class="line">        _CausticTex(<span class="string">&quot;Caustic Texture&quot;</span>, <span class="number">2</span>D) = <span class="string">&quot;&quot;</span> &#123;&#125;</span><br><span class="line">        _CausticColor(<span class="string">&quot;Caustic Color&quot;</span>, Color) = (<span class="number">0</span>, <span class="number">1</span>, <span class="number">0.98</span>, <span class="number">0.12</span>)</span><br><span class="line">        _CausticScale(<span class="string">&quot;Caustic Scale&quot;</span>, Range(<span class="number">0.01</span>, <span class="number">0.1</span>)) = <span class="number">0.08</span></span><br><span class="line">        _CausticSpeed(<span class="string">&quot;Caustic Speed&quot;</span>, Range(<span class="number">0</span>, <span class="number">2</span>)) = <span class="number">0.8</span></span><br><span class="line">        _CausticHighlightTex(<span class="string">&quot;Caustic Highlight Tex&quot;</span>, <span class="number">2</span>D) = <span class="string">&quot;&quot;</span> &#123;&#125;</span><br><span class="line">        _CausticHighlightColor(<span class="string">&quot;Caustic Highlight Color&quot;</span>, Color) = (<span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">0.67</span>)</span><br><span class="line">        _CausticNoiseScale(<span class="string">&quot;Caustic Noise Scale&quot;</span>, Range(<span class="number">0</span>, <span class="number">2</span>)) = <span class="number">1.63</span></span><br><span class="line">        _CausticNoiseBlendScale(<span class="string">&quot;Caustic Noise Blend Scale&quot;</span>, Range(<span class="number">0</span>, <span class="number">0.1</span>)) = <span class="number">0.018</span></span><br><span class="line">        _CausticSquash(<span class="string">&quot;Caustic Squash&quot;</span>, Range(<span class="number">0.1</span>, <span class="number">2</span>)) = <span class="number">1.3</span> <span class="comment">// 散焦 y 轴缩放</span></span><br><span class="line">        _CausticFadeNoiseScale(<span class="string">&quot;Caustic Fade Noise Scale&quot;</span>, Range(<span class="number">0</span>, <span class="number">1</span>)) = <span class="number">0.41</span></span><br><span class="line">        _CausticFadeMultiplier(<span class="string">&quot;Caustic Fade Multiplier&quot;</span>, Range(<span class="number">0</span>, <span class="number">1</span>)) = <span class="number">0.12</span></span><br><span class="line"></span><br><span class="line">        _SpecularSpeed(<span class="string">&quot;Specular Speed&quot;</span>, Range(<span class="number">0</span>, <span class="number">2</span>)) = <span class="number">0.3</span></span><br><span class="line">        _SpecularNoiseScale(<span class="string">&quot;Specular Noise Scale&quot;</span>, Range(<span class="number">0.1</span>, <span class="number">2</span>)) = <span class="number">0.83</span></span><br><span class="line">        _SpecularStaticScale(<span class="string">&quot;Specular Static Scale&quot;</span>, Range(<span class="number">0.1</span>, <span class="number">5</span>)) = <span class="number">3.68</span></span><br><span class="line">        _SpecularColor(<span class="string">&quot;Specular Color&quot;</span>, Color) = (<span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">0.87</span>)</span><br><span class="line">        _SpecularThreshold(<span class="string">&quot;Specular Threshold&quot;</span>, Range(<span class="number">-1</span>, <span class="number">1</span>)) = <span class="number">-0.65</span></span><br><span class="line"></span><br><span class="line">        _FoamTex(<span class="string">&quot;Foam Texture&quot;</span>, <span class="number">2</span>D) = <span class="string">&quot;&quot;</span> &#123;&#125;</span><br><span class="line">        _FoamColor(<span class="string">&quot;Foam Color&quot;</span>, Color) = (<span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">        _FoamScale(<span class="string">&quot;Foam Scale&quot;</span>, Range(<span class="number">0</span>, <span class="number">1</span>)) = <span class="number">0.5</span></span><br><span class="line">        _FoamBlurScale(<span class="string">&quot;Foam Blur Scale&quot;</span>, Range(<span class="number">0</span>, <span class="number">5</span>)) = <span class="number">3</span></span><br><span class="line">        _FoamTexelSize(<span class="string">&quot;Foam Texel Size&quot;</span>, Range(<span class="number">0</span>, <span class="number">0.01</span>)) = <span class="number">0.003</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 倒影扭曲</span></span><br><span class="line">        _ReflectionNoiseScale(<span class="string">&quot;Reflection Noise Scale&quot;</span>, Range(<span class="number">0</span>, <span class="number">0.1</span>)) = <span class="number">0.01</span></span><br><span class="line">        _ReflectionNoiseBlendScale(<span class="string">&quot;Reflection Noise Blend Scale&quot;</span>, Range(<span class="number">0</span>, <span class="number">1</span>)) = <span class="number">0.2</span></span><br><span class="line">        _ReflectionIntensity(<span class="string">&quot;Reflection Intensity&quot;</span>, Range(<span class="number">0</span>, <span class="number">1</span>)) = <span class="number">0.5</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    SubShader</span><br><span class="line">    &#123;</span><br><span class="line">        Tags</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="string">&quot;Queue&quot;</span> = <span class="string">&quot;Transparent&quot;</span></span><br><span class="line">            <span class="string">&quot;IgnoreProjector&quot;</span> = <span class="string">&quot;True&quot;</span></span><br><span class="line">            <span class="string">&quot;RenderType&quot;</span> = <span class="string">&quot;Transparent&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ZWrite Off</span><br><span class="line">        Blend SrcAlpha OneMinusSrcAlpha</span><br><span class="line">        Cull Off</span><br><span class="line"></span><br><span class="line">        Pass</span><br><span class="line">        &#123;</span><br><span class="line">            Name <span class="string">&quot;RenderWater&quot;</span></span><br><span class="line">            HLSLPROGRAM</span><br><span class="line">            <span class="meta">#<span class="keyword">pragma</span> vertex vert</span></span><br><span class="line">            <span class="meta">#<span class="keyword">pragma</span> fragment frag</span></span><br><span class="line">            <span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;</span></span></span><br><span class="line">            <span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Assets/AssetsPackage/Shader/Includes/pixel_art.hlsl&quot;</span></span></span><br><span class="line">            <span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Assets/AssetsPackage/Shader/Includes/common.hlsl&quot;</span></span></span><br><span class="line"></span><br><span class="line">            CBUFFER_START(UnityPerMaterial)</span><br><span class="line">            <span class="type">float</span> _PPU;</span><br><span class="line"></span><br><span class="line">            SamplerState linear_clamp_sampler;</span><br><span class="line">            SamplerState point_clamp_sampler;</span><br><span class="line">            SamplerState point_repeat_sampler;</span><br><span class="line">            SamplerState linear_repeat_sampler;</span><br><span class="line"></span><br><span class="line">            Texture2D _MainTex;</span><br><span class="line">            float4 _Color;</span><br><span class="line">            float4 _MainTex_TexelSize;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 散焦纹理</span></span><br><span class="line">            Texture2D _CausticTex;</span><br><span class="line">            Texture2D _CausticHighlightTex;</span><br><span class="line">            float4 _CausticColor;</span><br><span class="line">            float4 _CausticTex_TexelSize;</span><br><span class="line">            <span class="type">float</span> _CausticScale;</span><br><span class="line">            <span class="type">float</span> _CausticSpeed;</span><br><span class="line">            float4 _CausticHighlightColor;</span><br><span class="line">            <span class="type">float</span> _CausticSquash;</span><br><span class="line">            <span class="comment">// 噪声扰动</span></span><br><span class="line">            <span class="type">float</span> _CausticNoiseScale;</span><br><span class="line">            <span class="type">float</span> _CausticNoiseBlendScale;</span><br><span class="line">            <span class="comment">// 散焦渐隐</span></span><br><span class="line">            <span class="type">float</span> _CausticFadeNoiseScale;</span><br><span class="line">            <span class="type">float</span> _CausticFadeMultiplier;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 高光</span></span><br><span class="line">            <span class="type">float</span> _SpecularSpeed;</span><br><span class="line">            <span class="type">float</span> _SpecularNoiseScale;</span><br><span class="line">            <span class="type">float</span> _SpecularStaticScale;</span><br><span class="line">            float4 _SpecularColor;</span><br><span class="line">            <span class="type">float</span> _SpecularThreshold;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 边缘泡沫</span></span><br><span class="line">            Texture2D _FoamTex;</span><br><span class="line">            float4 _FoamColor;</span><br><span class="line">            <span class="type">float</span> _FoamScale;</span><br><span class="line">            <span class="type">float</span> _FoamBlurScale;</span><br><span class="line">            <span class="type">float</span> _FoamTexelSize;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 倒影</span></span><br><span class="line">            Texture2D _CameraSortingLayerTexture;</span><br><span class="line">            Texture2D _ReflectionTex;</span><br><span class="line">            Texture2D _UnderwaterTex;</span><br><span class="line">            <span class="type">float</span> _ReflectionNoiseScale;</span><br><span class="line">            <span class="type">float</span> _ReflectionNoiseBlendScale;</span><br><span class="line">            <span class="type">float</span> _ReflectionIntensity;</span><br><span class="line">            CBUFFER_END</span><br><span class="line"></span><br><span class="line">            <span class="class"><span class="keyword">struct</span> <span class="title">a2v</span></span></span><br><span class="line"><span class="class">            &#123;</span></span><br><span class="line">                float4 vertex: POSITION;</span><br><span class="line">                float2 texcoord: TEXCOORD0;</span><br><span class="line">                float4 color: COLOR;</span><br><span class="line">            &#125;;</span><br><span class="line"></span><br><span class="line">            <span class="class"><span class="keyword">struct</span> <span class="title">v2f</span></span></span><br><span class="line"><span class="class">            &#123;</span></span><br><span class="line">                float4 vertex: SV_POSITION;</span><br><span class="line">                float2 uv: TEXCOORD0;</span><br><span class="line">                float4 color: COLOR;</span><br><span class="line">                float3 world_pos: TEXCOORD1;</span><br><span class="line">                float4 screen_pos: TEXCOORD2;</span><br><span class="line">            &#125;;</span><br><span class="line"></span><br><span class="line">            v2f <span class="title function_">vert</span><span class="params">(a2v input)</span></span><br><span class="line">            &#123;</span><br><span class="line">                v2f output;</span><br><span class="line">                output.vertex = TransformObjectToHClip(input.vertex.xyz);</span><br><span class="line">                output.uv = input.texcoord;</span><br><span class="line">                output.color = input.color;</span><br><span class="line">                output.world_pos = mul(unity_ObjectToWorld, input.vertex).xyz;</span><br><span class="line">                output.screen_pos = ComputeScreenPos(output.vertex);</span><br><span class="line">                <span class="keyword">return</span> output;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            float4 <span class="title function_">frag</span><span class="params">(v2f input)</span> : SV_Target</span><br><span class="line">            &#123;</span><br><span class="line">                float3 world_pos = pixelate_world_pos(input.world_pos, _PPU);</span><br><span class="line">                float2 uv = input.uv;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 散焦纹理uv扰动</span></span><br><span class="line">                float2 squash_uv = world_pos.xy * float2(<span class="number">1</span>, _CausticSquash);</span><br><span class="line">                float2 caustic_uv = squash_uv * _CausticScale;</span><br><span class="line">                <span class="type">float</span> caustic_noise = gradient_noise(squash_uv + _Time.y * _CausticSpeed, _CausticNoiseScale);</span><br><span class="line">                float2 caustic_noise_uv = float2(caustic_noise, caustic_noise);</span><br><span class="line">                caustic_uv = blend_subtract(caustic_uv, caustic_noise_uv, _CausticNoiseBlendScale);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 倒影 / 水深</span></span><br><span class="line">                float2 screen_uv = input.screen_pos.xy / input.screen_pos.w;</span><br><span class="line">                screen_uv += caustic_noise_uv * _ReflectionNoiseScale / unity_OrthoParams.y;</span><br><span class="line">                <span class="comment">// float4 screen_col = _CameraSortingLayerTexture.Sample(point_clamp_sampler, clamp(screen_uv + caustic_noise_uv * _ReflectionNoiseScale, 0, 1));</span></span><br><span class="line">                float4 reflection_col = _ReflectionTex.Sample(point_clamp_sampler, screen_uv);</span><br><span class="line">                float4 underwater_col = _UnderwaterTex.Sample(point_clamp_sampler, screen_uv);</span><br><span class="line">                float4 screen_col = lerp(underwater_col, reflection_col, _ReflectionIntensity);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 散焦纹理</span></span><br><span class="line">                float4 caustic_col = _CausticTex.Sample(point_repeat_sampler, caustic_uv) * _CausticColor;</span><br><span class="line">                float4 highlight_col = _CausticHighlightTex.Sample(point_repeat_sampler, caustic_uv) * _CausticHighlightColor;</span><br><span class="line">                caustic_col = lerp(caustic_col, highlight_col, highlight_col.a);</span><br><span class="line">                <span class="comment">// 散焦渐隐</span></span><br><span class="line">                <span class="type">float</span> fade_noise = gradient_noise(input.world_pos.xy, _CausticFadeNoiseScale) * _CausticFadeMultiplier;</span><br><span class="line">                caustic_col.a = clamp(caustic_col.a - fade_noise, <span class="number">0</span>, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 高光</span></span><br><span class="line">                float2 delta = float2(_Time.y, <span class="number">0</span>) * _SpecularSpeed;</span><br><span class="line">                <span class="type">float</span> noise_left = gradient_noise(world_pos.xy + delta, _SpecularNoiseScale);</span><br><span class="line">                <span class="type">float</span> noise_right = gradient_noise(world_pos.xy - delta, _SpecularNoiseScale);</span><br><span class="line">                <span class="type">float</span> noise_static = gradient_noise(world_pos.xy, _SpecularStaticScale);</span><br><span class="line">                <span class="type">float</span> specular_noise = blend_overlay(noise_left, noise_right, <span class="number">1</span>);</span><br><span class="line">                specular_noise = blend_subtract(specular_noise, noise_static, <span class="number">1</span>);</span><br><span class="line">                float4 specular_col = step(specular_noise, _SpecularThreshold) * _SpecularColor;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 泡沫</span></span><br><span class="line">                float4 foam_col = _FoamTex.Sample(point_repeat_sampler, input.world_pos.xy * _FoamScale) * _FoamColor;</span><br><span class="line">                float4 blur = gaussian_blur_5x5(_MainTex, point_repeat_sampler, uv, float2(_FoamTexelSize, _FoamTexelSize)) * _FoamBlurScale;</span><br><span class="line">                foam_col.a *= blur.r;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 混合 主色, 散焦, 高光, 泡沫, 倒影</span></span><br><span class="line">                caustic_col.rgb = lerp(_Color.rgb, caustic_col.rgb, caustic_col.a);</span><br><span class="line">                caustic_col = lerp(caustic_col, specular_col, <span class="built_in">ceil</span>(caustic_col.a) * specular_col.a);</span><br><span class="line">                caustic_col = lerp(caustic_col, foam_col, foam_col.a);</span><br><span class="line">                caustic_col = lerp(caustic_col, screen_col, screen_col.a * _ReflectionNoiseBlendScale);</span><br><span class="line"></span><br><span class="line">                uv = pixelate_uv(uv, _MainTex_TexelSize);</span><br><span class="line">                float4 col = _MainTex.Sample(point_clamp_sampler, uv);</span><br><span class="line">                foam_col = _FoamColor * (col.r * col.a); <span class="comment">// 复用 foam_col 变量</span></span><br><span class="line">                col.rgb = lerp(caustic_col.rgb, foam_col.rgb, col.r * col.a);</span><br><span class="line">                col.a = col.a * _Color.a;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">return</span> col;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            ENDHLSL</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>分块简单解释一下</p><ul><li>产生散焦贴图uv扰动</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">float2 squash_uv = world_pos.xy * float2(<span class="number">1</span>, _CausticSquash);</span><br><span class="line">float2 caustic_uv = squash_uv * _CausticScale;</span><br><span class="line"><span class="type">float</span> caustic_noise = gradient_noise(squash_uv + _Time.y * _CausticSpeed, _CausticNoiseScale);</span><br><span class="line">float2 caustic_noise_uv = float2(caustic_noise, caustic_noise);</span><br><span class="line">caustic_uv = blend_subtract(caustic_uv, caustic_noise_uv, _CausticNoiseBlendScale);</span><br></pre></td></tr></table></figure><p>这里使用世界坐标加扰动来生成采样散焦纹理(<code>_CausticTex</code>)的uv坐标. 使用 <code>_CausticSquash</code> 对竖直方向进行缩放, 以用模拟斜视水面的效果. gradient_noise 用于产生扰动uv所需的柏林噪声(Perlin Noise), 其定义见附录1.</p><ul><li>倒影与水深</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 倒影 / 水深</span></span><br><span class="line">float2 screen_uv = input.screen_pos.xy / input.screen_pos.w;</span><br><span class="line">screen_uv += caustic_noise_uv * _ReflectionNoiseScale / unity_OrthoParams.y;</span><br><span class="line"><span class="comment">// float4 screen_col = _CameraSortingLayerTexture.Sample(point_clamp_sampler, clamp(screen_uv + caustic_noise_uv * _ReflectionNoiseScale, 0, 1));</span></span><br><span class="line">float4 reflection_col = _ReflectionTex.Sample(point_clamp_sampler, screen_uv);</span><br><span class="line">float4 underwater_col = _UnderwaterTex.Sample(point_clamp_sampler, screen_uv);</span><br><span class="line">float4 screen_col = lerp(underwater_col, reflection_col, _ReflectionIntensity);</span><br></pre></td></tr></table></figure><p>在这个实现方案中, 倒影(<code>_ReflectionTex</code>)与水深(<code>_UnderwaterTex</code>)有两个单独的渲染纹理(RT, Render Texture), 在渲染水体时进行采样, RT的生成参考倒影与水深实现方案一节. <code>_ReflectionIntensity</code> 用于控制两者混合的权重. 我感觉通常倒影和水深的效果只需取其一, 否则混合后重叠看着有点乱.</p><ul><li>散焦纹理采样</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 散焦纹理</span></span><br><span class="line">float4 caustic_col = _CausticTex.Sample(point_repeat_sampler, caustic_uv) * _CausticColor;</span><br><span class="line">float4 highlight_col = _CausticHighlightTex.Sample(point_repeat_sampler, caustic_uv) * _CausticHighlightColor;</span><br><span class="line">caustic_col = lerp(caustic_col, highlight_col, highlight_col.a);</span><br><span class="line"><span class="comment">// 散焦渐隐</span></span><br><span class="line"><span class="type">float</span> fade_noise = gradient_noise(input.world_pos.xy, _CausticFadeNoiseScale) * _CausticFadeMultiplier;</span><br><span class="line">caustic_col.a = clamp(caustic_col.a - fade_noise, <span class="number">0</span>, <span class="number">1</span>);</span><br></pre></td></tr></table></figure><p>这里有散焦底色的纹理(<code>_CausticTex</code>)与高亮的纹理(<code>_CausticHighlightTex</code>), 后者由前者图像处理腐蚀后生成, 保证了高亮的部分一定在底色的纹理之上, 看起来就像是部分波纹变成了高亮. 同时用柏林噪声增加了散焦纹理部分渐隐效果.</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 高光</span></span><br><span class="line">float2 delta = float2(_Time.y, <span class="number">0</span>) * _SpecularSpeed;</span><br><span class="line"><span class="type">float</span> noise_left = gradient_noise(world_pos.xy + delta, _SpecularNoiseScale);</span><br><span class="line"><span class="type">float</span> noise_right = gradient_noise(world_pos.xy - delta, _SpecularNoiseScale);</span><br><span class="line"><span class="type">float</span> noise_static = gradient_noise(world_pos.xy, _SpecularStaticScale);</span><br><span class="line"><span class="type">float</span> specular_noise = blend_overlay(noise_left, noise_right, <span class="number">1</span>);</span><br><span class="line">specular_noise = blend_subtract(specular_noise, noise_static, <span class="number">1</span>);</span><br><span class="line">float4 specular_col = step(specular_noise, _SpecularThreshold) * _SpecularColor;</span><br></pre></td></tr></table></figure><p>高光的部分使用2个反向运动的柏林噪声与一个静态的柏林噪声混合生成. </p><ul><li>泡沫</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 泡沫</span></span><br><span class="line">float4 foam_col = _FoamTex.Sample(point_repeat_sampler, input.world_pos.xy * _FoamScale) * _FoamColor;</span><br><span class="line">float4 blur = gaussian_blur_5x5(_MainTex, point_repeat_sampler, uv, float2(_FoamTexelSize, _FoamTexelSize)) * _FoamBlurScale;</span><br><span class="line">foam_col.a *= blur.r;</span><br></pre></td></tr></table></figure><p>这里 <code>_FoamTex</code> 泡沫贴图是散焦贴图缩放后得到的, 用于表现细小的岸边泡沫. 不同于3d中使用深度确定泡沫显示范围, jess 的方案使用高斯模糊使白色边缘具有渐变效果, 然后与泡沫纹理混合. <code>gaussian_blur_5x5</code> 的定义见附录1.</p><p>高斯模糊 + tilemap 会有一个隐含的坑点. 说来话长, 我把这个话题放到附录2中讨论. 其实可以预先在 tileset 中就处理好渐变, 这样可以省去高斯模糊这一步, 我看到 jess 后续的分享似乎也改变了泡沫的实现方法.</p><ul><li>混合以上所有颜色</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 混合 主色, 散焦, 高光, 泡沫, 倒影</span></span><br><span class="line">caustic_col.rgb = lerp(_Color.rgb, caustic_col.rgb, caustic_col.a);</span><br><span class="line">caustic_col = lerp(caustic_col, specular_col, <span class="built_in">ceil</span>(caustic_col.a) * specular_col.a);</span><br><span class="line">caustic_col = lerp(caustic_col, foam_col, foam_col.a);</span><br><span class="line">caustic_col = lerp(caustic_col, screen_col, screen_col.a * _ReflectionNoiseBlendScale);</span><br><span class="line"></span><br><span class="line">uv = pixelate_uv(uv, _MainTex_TexelSize);</span><br><span class="line">float4 col = _MainTex.Sample(point_clamp_sampler, uv);</span><br><span class="line">foam_col = _FoamColor * (col.r * col.a); <span class="comment">// 复用 foam_col 变量</span></span><br><span class="line">col.rgb = lerp(caustic_col.rgb, foam_col.rgb, col.r * col.a);</span><br><span class="line">col.a = col.a * _Color.a;</span><br></pre></td></tr></table></figure><p><code>pixelate_uv</code> 的定义见附录1, 原理可以参考这篇文章<a href="https://granvallen.github.io/PixelArtUpscaling/">浅谈Pixel Art缩放及抗锯齿问题 | Granvallen;Nest</a>.<br>以上从我自己测试来看, 相同参数下, 水体表现和 jess 的实现基本一致. shader 中所用到的3张贴图(<code>_CausticTex</code>, <code>_CausticHighlightTex</code> 和 <code>_FoamTex</code>)都来自 jess 的示例工程, 实现效果如图3,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_water_4.gif" style="zoom:80%;" class alt="图3" /></p><p>调整部分参数的效果变化如图4,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_water_3.gif" style="zoom:80%;" class alt="图4" /></p><h1 id="2d水体的倒影与水深"><a href="#2d水体的倒影与水深" class="headerlink" title="2d水体的倒影与水深"></a>2d水体的倒影与水深</h1><p>在我少量的调研中, 那些使用3d技术的2d游戏中, 为了表现水的真实, 大多都实现了这类效果, 特别是水深的效果. 而在纯2d游戏中也有少部分实现了简单的黑色模糊倒影效果, 水深则较为少见.</p><p>说是2d, 其实2d的游戏表现方式五花八门, 最常见的有横版平台跳跃, 类似 jess 这个游戏的正面(Top-down)视角, 以及2d等距视角(Isometric), 这里只讨论正面 Top-down 视角的情况. 关于这个视角, 突然想起王老菊有个视频很有意思<a href="https://www.bilibili.com/video/BV1ft411A74o/">未来科技开发日记#1</a>.</p><h2 id="思路-1"><a href="#思路-1" class="headerlink" title="思路"></a>思路</h2><p>尝试寻找解决方案之前, 先考虑下这个问题最复杂的情况, 如果能处理好复杂的情况, 那更通常的情况应该也是ok的. 比如一个复杂的情况是, 一个有一半没入水中的动态物体在水的边缘处. 这种情况会同时出现动态的倒影与水深, 以及倒影和水深在水不规则边缘的处理. </p><p>一种2d倒影简单且广泛采用的实现思路是物体的倒影是单独贴图, 事先做好, 控制渲染顺序, 先渲染出倒影, 然后渲染水体时抓取屏幕贴图, 对水体范围的颜色进行叠加和扰动. 比如这个分享中的做法 <a href="https://www.bilibili.com/video/BV1364y1X7NZ/">UnityShader实现2D水面及物体水面投影的渲染</a>.</p><p>显然这个思路是可行的, 但对于稍复杂的情况就暴露出一些问题, 需要进一步改进. 比如,</p><ul><li>因为是独立的贴图, 所以对于静态物体是比较好处理的, 但是如果贴图本身会动, 像是角色有序列帧动画, 就需要额外处理倒影贴图的变化, 这个就不太方便.</li><li>因为是先渲染的倒影, 渲染出来后屏幕才能抓到帧进行水体表现效果, 而如果物体在水边缘, 露出来的部分倒影就会穿帮. 期望应该是倒影只会在水体内渲染.</li><li>如果想处理地更精细一点, 单独控制倒影与水深的强度, 简单的抓帧显然是做不到的.</li></ul><p>我初期尝试对这种方案进行修补, 但最终放弃了. 这里 URP 2D 渲染管线 + 屏幕抓帧也有很多坑, 如果有人采用这个方案我简单提一嘴碰到的问题. URP 中无法使用 Build-in 管线中 GrabPass 进行屏幕抓帧, 而是需要在 Render Pipeline Asset 检视界面中勾选 Opaque Texture, 这样屏幕贴图会自动渲染到一个内置的全局RT <code>_CameraOpaqueTexture</code> 中, 在 shader 中采样这个RT即可. 而如果你使用 URP 2D 管线, 那么这个方法也是失效的, 你需要单独设置一个 layer 和在 Render Data 检视界面中新增 Render Object 来进行屏幕贴图的渲染, 然后使用 <code>_CameraSortingLayerTexture</code> 进行采样, 这种做法目前有很多局限, 可以参考这个讨论 <a href="https://discussions.unity.com/t/is-it-possible-to-have-transparents-using-the-2d-renderer-in-urp/772498/38">Is it possible to have transparents using the 2D renderer in URP? - Unity Engine - Unity Discussions</a>.</p><p>如果沿用这个屏幕空间反射的2d倒影实现思路, 对于上面要解决的问题可以简单总结为</p><ol><li>倒影/水深贴图怎么获取?</li><li>怎么只在水体范围内渲染这些贴图内容?</li></ol><p>对于第一个倒影贴图怎么来的问题, 如果要精细控制还是需要倒影与水深分开获取. 对于倒影, 需要能实时地渲染出物体的动态倒影, 这个也有几种方案, 像是屏幕空间上下倒转, 物体贴图的倒转. 水深与倒影的处理类似, 只是不需要进行倒转操作.</p><p>对于第二个问题, 倒影只出现在水体范围内, 大致有两种做法, 一是使用模板缓冲(Stencil Buffer), 水体渲染需要两个 pass, 第一个 pass 渲染水体时只写入模板缓存, 然后渲染倒影/水深时读取模板, 最后水体第二个 pass 读取倒影渲染结果再混合渲染水体颜色. 然而 URP 似乎不支持多 pass 的 shader, 只能作罢. 模板匹配的另一个麻烦之处在于水边缘处理, 如果边缘是不规则的, 就需要根据透明度的情况处理模板缓存.</p><p>另一种做法则更直接, 把倒影与水深渲染到单独RT上, 而主相机中不对其进行渲染, 然后水体渲染时读取RT进行混合. 这个方案使用自定义的 Renderer Featuer 可以很容易做到.</p><p>于是我想到一种简单可行, 且统一反射与水深的方案. 给每个入水物体节点再挂两个 Sprite 节点, 一个设置为倒影层用于渲染倒影, 一个设置为水深层渲染水深, 每帧把主 Sprite 的贴图更新到这两个子 Sprite 上, 而这两个 Sprite Renderer 各自使用单独的 shader 渲染自己的贴图, shader 的参数也可以直接在更新时赋值. 只需要这两个 Sprite 设置为不同的 Layer 比如反射层(Reflection) 与 水深层(Underwater), 借助自定义 Renderer Feature 就可以渲染出这两个单独的RT.</p><p>光这样还不够, 还需要实现一个特殊的 Sprite 分割 shader, 外部脚本可以传参数进去, 一个参数决定 Sprite 在 y 方向上显示的分割线, 另一个参数决定是分割线以上还是以下的部分显示, 这样就可以模拟出 Sprite 没入水中的效果.</p><h2 id="实现-1"><a href="#实现-1" class="headerlink" title="实现"></a>实现</h2><p>自定义的 Renderer Feature 与 Render Pass 代码如下,<br><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> UnityEngine.Rendering.Universal;</span><br><span class="line"><span class="keyword">using</span> UnityEngine;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">RenderLayerFeature</span> : <span class="title">ScriptableRendererFeature</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> RenderLayerPass _pass;</span><br><span class="line">    [<span class="meta">SerializeField</span>]</span><br><span class="line">    <span class="keyword">private</span> LayerMask layer_mask;</span><br><span class="line">    [<span class="meta">SerializeField</span>]</span><br><span class="line">    <span class="keyword">private</span> <span class="built_in">string</span> rt_name = <span class="string">&quot;_TempRenderLayer&quot;</span>;</span><br><span class="line">    [<span class="meta">SerializeField</span>]</span><br><span class="line">    <span class="keyword">private</span> RenderPassEvent pass_event = RenderPassEvent.BeforeRenderingOpaques;</span><br><span class="line">    [<span class="meta">SerializeField</span>]</span><br><span class="line">    <span class="keyword">private</span> TransparencySortMode sort_mode = TransparencySortMode.Default;</span><br><span class="line">    [<span class="meta">SerializeField</span>]</span><br><span class="line">    <span class="keyword">private</span> Vector3 custom_axis = Vector3.up;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">Create</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        _pass = <span class="keyword">new</span> RenderLayerPass(layer_mask, rt_name, pass_event, sort_mode, custom_axis);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">AddRenderPasses</span>(<span class="params">ScriptableRenderer renderer, <span class="keyword">ref</span> RenderingData rendering_data</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        _pass.Setup();</span><br><span class="line">        renderer.EnqueuePass(_pass);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> UnityEngine.Rendering.Universal;</span><br><span class="line"><span class="keyword">using</span> UnityEngine.Rendering;</span><br><span class="line"><span class="keyword">using</span> UnityEngine;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">RenderLayerPass</span> : <span class="title">ScriptableRenderPass</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> RenderTargetHandle rt_handle;</span><br><span class="line">    <span class="keyword">private</span> <span class="built_in">string</span> rt_name;</span><br><span class="line">    <span class="keyword">private</span> ShaderTagId shader_tag_id = <span class="keyword">new</span> ShaderTagId(<span class="string">&quot;SRPDefaultUnlit&quot;</span>);</span><br><span class="line">    <span class="keyword">private</span> FilteringSettings filtering_settings;</span><br><span class="line">    <span class="keyword">private</span> LayerMask layer_mask;</span><br><span class="line">    <span class="keyword">private</span> TransparencySortMode sort_mode;</span><br><span class="line">    <span class="keyword">private</span> Vector3 custom_axis;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">RenderLayerPass</span>(<span class="params">LayerMask layer_mask, <span class="built_in">string</span> rt_name, RenderPassEvent pass_event, TransparencySortMode sort_mode, Vector3 custom_axis</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">this</span>.layer_mask = layer_mask;</span><br><span class="line">        <span class="keyword">this</span>.rt_name = rt_name;</span><br><span class="line">        <span class="keyword">this</span>.sort_mode = sort_mode;</span><br><span class="line">        <span class="keyword">this</span>.custom_axis = custom_axis;</span><br><span class="line">        renderPassEvent = pass_event; <span class="comment">// 设置渲染时机</span></span><br><span class="line"></span><br><span class="line">        filtering_settings = <span class="keyword">new</span> FilteringSettings(RenderQueueRange.all, layer_mask);</span><br><span class="line">        rt_handle.Init(rt_name);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">Setup</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">Configure</span>(<span class="params">CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        RenderTextureDescriptor rt_desc = cameraTextureDescriptor; <span class="comment">// RenderTexture 描述符</span></span><br><span class="line"></span><br><span class="line">        cmd.GetTemporaryRT(rt_handle.id, rt_desc.width, rt_desc.height, <span class="number">0</span>, FilterMode.Bilinear, RenderTextureFormat.ARGBHalf); <span class="comment">// 临时 RT</span></span><br><span class="line">        cmd.SetGlobalTexture(rt_name, rt_handle.Identifier()); <span class="comment">// 设置全局纹理</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 配置渲染目标和清除设置</span></span><br><span class="line">        ConfigureTarget(rt_handle.Identifier());</span><br><span class="line">        ConfigureClear(ClearFlag.All, Color.clear);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">Execute</span>(<span class="params">ScriptableRenderContext context, <span class="keyword">ref</span> RenderingData rendering_data</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// 手动创建剔除参数</span></span><br><span class="line">        Camera camera = rendering_data.cameraData.camera;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 必须设置, 否则倒影渲染顺序会有问题</span></span><br><span class="line">        camera.transparencySortMode = sort_mode;</span><br><span class="line">        camera.transparencySortAxis = custom_axis;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (!camera.TryGetCullingParameters(<span class="keyword">out</span> ScriptableCullingParameters culling_params))</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置剔除遮罩</span></span><br><span class="line">        culling_params.cullingMask = (<span class="built_in">uint</span>)layer_mask.<span class="keyword">value</span>;</span><br><span class="line">        CullingResults culling_results = context.Cull(<span class="keyword">ref</span> culling_params);</span><br><span class="line">        DrawingSettings draw_settings = CreateDrawingSettings(</span><br><span class="line">            shader_tag_id, </span><br><span class="line">            <span class="keyword">ref</span> rendering_data, </span><br><span class="line">            SortingCriteria.SortingLayer | SortingCriteria.CommonTransparent</span><br><span class="line">        );</span><br><span class="line">        context.DrawRenderers(culling_results, <span class="keyword">ref</span> draw_settings, <span class="keyword">ref</span> filtering_settings);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">FrameCleanup</span>(<span class="params">CommandBuffer cmd</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        cmd.ReleaseTemporaryRT(rt_handle.id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个 RenderLayerFeature 实现的功能非常简单, 就是把特定 Layer 的内容渲染到一个自命名的全局RT里, 可以在 Render Data 资源检视界面里创建两个实例分别渲染物体的倒影层(<code>_ReflectionTex</code>)与水深层(<code>_UnderwaterTex</code>).</p><p>至于入水物体 shader 的实现, 这就不得不提到 Sprite Renderer 的一个坑点.</p><h2 id="水中物体处理与Sprite合批"><a href="#水中物体处理与Sprite合批" class="headerlink" title="水中物体处理与Sprite合批"></a>水中物体处理与Sprite合批</h2><p>在前面提到的实现思路中, 水中物体需要一个分割 Sprite 的 shader, 一种实现如下,<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">v2f <span class="title function_">vert</span><span class="params">(a2v input)</span></span><br><span class="line">&#123;</span><br><span class="line">v2f output;</span><br><span class="line">input.vertex.y -= _OffsetY; <span class="comment">// 竖直移动</span></span><br><span class="line">output.local_pos = input.vertex;</span><br><span class="line">output.vertex = TransformObjectToHClip(input.vertex.xyz);</span><br><span class="line">output.uv = input.texcoord;</span><br><span class="line">output.color = input.color * _Color;</span><br><span class="line"><span class="keyword">return</span> output;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">float4 <span class="title function_">frag</span><span class="params">(v2f input)</span> : SV_Target</span><br><span class="line">&#123;</span><br><span class="line"><span class="comment">// sprite 剔除</span></span><br><span class="line"><span class="type">float</span> is_discard = _Upper * input.local_pos.y + (<span class="number">1.0</span> / _PPU - input.local_pos.y) * (<span class="number">1</span> - _Upper);</span><br><span class="line">clip(is_discard);</span><br><span class="line"></span><br><span class="line">float2 uv = pixelate_uv(input.uv, _MainTex_TexelSize);</span><br><span class="line">float4 col = _MainTex.Sample(linear_clamp_sampler, uv);</span><br><span class="line"><span class="keyword">return</span> col * input.color;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><br>在顶点着色器中移动并缓存下物体偏移后的局部坐标, 然后在片元着色器中进行剔除.</p><p>当场景中该物体只有一个时表现符合期望, 但当场景中有两个以上的相同物体时, Sprite 的渲染就会出现问题, 看表现像是 shader 中的局部坐标不再可靠. 检索之后才知道 Sprite Renderer 会自带一个动态合批处理(与工程本身合批配置无关), 当场景中有多个相同 Sprite 且材质相同时, 就会触发这个幕后的动态合批, 进行网格合并操作, 以便同时绘制多个Sprite. 但这导致 shader 中上述局部空间坐标的失效, 参考,</p><ul><li><a href="https://discussions.unity.com/t/how-to-prevent-sprite-batching-or-display-sprites-without-using-sprite-renderer/805269/4">How to prevent sprite batching OR display sprites without using sprite renderer? - Unity Engine - Unity Discussions</a></li><li><a href="https://discussions.unity.com/t/how-to-get-an-absolute-object-space-position-when-draw-calls-are-batched/831888">How to get an absolute object-space position when draw calls are batched? - Unity Engine - Unity Discussions</a></li></ul><p>解决方法是在 shader 中加个禁止 batching 的 tag,<br><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">&quot;DisableBatching&quot;</span> = <span class="string">&quot;True&quot;</span></span><br></pre></td></tr></table></figure><br>这能解决表现的问题, 不过代价是所有物体都需要单独进行绘制. 进一步检索之后发现, Sprite Renderer 在合批流程中的支持一直不太好, 原生不支持常见的合批方法, 而其自带的动态合批在使用不同的 Sprite 时也没办法合批(似乎是Sprite Renderer 针对不同的 Sprite 会生成不同的网格). 参考</p><ul><li><a href="https://discussions.unity.com/t/batching-and-sprite-renderers/792099/8">Batching and Sprite Renderers - Unity Engine - Unity Discussions</a></li><li><a href="https://discussions.unity.com/t/srp-batcher-spriterenderer/918871">SRP Batcher &amp; SpriteRenderer - Unity Engine - Unity Discussions</a></li></ul><p>似乎 unity 2023 版本 Sprite Renderer 才支持了 SRP Batcher.</p><p>不过好在已经有人探索过相关的解决方案了, 比如</p><ul><li><a href="https://www.ownself.org/2022/unity-sprite-gpu-instancing.html">为Unity Sprite实现GPU Instancing</a></li><li><a href="https://github.com/ownself/UnitySpriteGPUInstancing">GitHub - ownself/UnitySpriteGPUInstancing: A Unity Sprite GPU Instancing Implementation Demo</a></li></ul><p>从这个分享中看, 我猜铃兰之剑似乎也使用了类似的方案. 思路是自己创建管理相同的渲染网格, 用 Mesh Renderer 代替 Sprite Renderer 进行渲染, 而这个流程支持 GPU Instancing, 可以说是一个相当优雅的解决方案了. 那么问题就由创建 Sprite, 变成创建 Mesh. 处理后我们可以在 frame debuger 中看到, 所有物体是由单个批次绘制的. 物体挂载脚本 MapObjectRenerer.cs 与物体 shader mapobject.shader 实现如下,</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System.Collections.Generic;</span><br><span class="line"><span class="keyword">using</span> UnityEngine;</span><br><span class="line"></span><br><span class="line">[<span class="meta">RequireComponent(typeof(SpriteRenderer))</span>]</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">MapObjectRenderer</span> : <span class="title">MonoBehaviour</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> SpriteRenderer _sprite_renderer;</span><br><span class="line">    <span class="keyword">private</span> MaterialPropertyBlock _prop_block;</span><br><span class="line">    <span class="keyword">private</span> <span class="built_in">int</span> _tex_index = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">private</span> Vector4 _pivot;</span><br><span class="line">    <span class="keyword">private</span> Vector4 _uv;</span><br><span class="line">    <span class="keyword">private</span> <span class="built_in">float</span> _ppu = <span class="number">32f</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">float</span> offset_y = <span class="number">0f</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// sprite mesh</span></span><br><span class="line">    <span class="keyword">private</span> GameObject _sprite_mesh_go;</span><br><span class="line">    <span class="keyword">private</span> MeshRenderer _sprite_mesh_renderer;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// reflection mesh</span></span><br><span class="line">    <span class="keyword">private</span> GameObject _reflection_mesh_go;</span><br><span class="line">    <span class="keyword">private</span> MeshRenderer _reflection_mesh_renderer;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// underwater mesh</span></span><br><span class="line">    <span class="keyword">private</span> GameObject _underwater_mesh_go;</span><br><span class="line">    <span class="keyword">private</span> MeshRenderer _underwater_mesh_renderer;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 静态变量</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Mesh _mesh;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Dictionary&lt;Texture2D, <span class="built_in">int</span>&gt; tex_indexes = <span class="keyword">new</span> Dictionary&lt;Texture2D, <span class="built_in">int</span>&gt;();</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> array_size = <span class="number">128</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Texture2DArray tex_array;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="built_in">int</span> tex_count = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">Awake</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        _sprite_renderer = GetComponent&lt;SpriteRenderer&gt;();</span><br><span class="line">        _sprite_renderer.enabled = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">        _ppu = _sprite_renderer.sprite.pixelsPerUnit;</span><br><span class="line">        Texture2D tex = _sprite_renderer.sprite.texture; <span class="comment">// tex 是 sprite sheet</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (tex_array == <span class="literal">null</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            tex_array = <span class="keyword">new</span> Texture2DArray(tex.width, tex.height, array_size, tex.format, <span class="literal">false</span>);</span><br><span class="line">            tex_count = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (_prop_block == <span class="literal">null</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            _prop_block = <span class="keyword">new</span> MaterialPropertyBlock();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (!tex_indexes.ContainsKey(tex))</span><br><span class="line">        &#123;</span><br><span class="line">            Graphics.CopyTexture(tex, <span class="number">0</span>, <span class="number">0</span>, tex_array, tex_count, <span class="number">0</span>);</span><br><span class="line">            _tex_index = tex_count;</span><br><span class="line">            tex_indexes[tex] = _tex_index;</span><br><span class="line">            tex_count++;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">        &#123;</span><br><span class="line">            _tex_index = tex_indexes[tex];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (_mesh == <span class="literal">null</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            _mesh = create_mesh();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        create_sprite_mesh();</span><br><span class="line">        create_reflection_mesh();</span><br><span class="line">        create_underwater_mesh();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">Update</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        update_mesh();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> Mesh <span class="title">create_mesh</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        Mesh mesh = <span class="keyword">new</span> Mesh();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 定义顶点，枢轴位于底部中心</span></span><br><span class="line">        Vector3[] vertices = <span class="keyword">new</span> Vector3[]</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">new</span> Vector3(<span class="number">-1f</span>, <span class="number">0f</span>, <span class="number">0</span>), <span class="comment">// 左下</span></span><br><span class="line">            <span class="keyword">new</span> Vector3(<span class="number">1f</span>, <span class="number">0f</span>, <span class="number">0</span>),  <span class="comment">// 右下</span></span><br><span class="line">            <span class="keyword">new</span> Vector3(<span class="number">-1f</span>, <span class="number">2f</span>, <span class="number">0</span>), <span class="comment">// 左上</span></span><br><span class="line">            <span class="keyword">new</span> Vector3(<span class="number">1f</span>, <span class="number">2f</span>, <span class="number">0</span>)   <span class="comment">// 右上</span></span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 定义 UV</span></span><br><span class="line">        Vector2[] uvs = <span class="keyword">new</span> Vector2[]</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">new</span> Vector2(<span class="number">0</span>, <span class="number">0</span>),</span><br><span class="line">            <span class="keyword">new</span> Vector2(<span class="number">1</span>, <span class="number">0</span>),</span><br><span class="line">            <span class="keyword">new</span> Vector2(<span class="number">0</span>, <span class="number">1</span>),</span><br><span class="line">            <span class="keyword">new</span> Vector2(<span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 定义三角形</span></span><br><span class="line">        <span class="built_in">int</span>[] triangles = <span class="keyword">new</span> <span class="built_in">int</span>[]</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="number">0</span>, <span class="number">2</span>, <span class="number">1</span>,</span><br><span class="line">            <span class="number">2</span>, <span class="number">3</span>, <span class="number">1</span></span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        mesh.vertices = vertices;</span><br><span class="line">        mesh.uv = uvs;</span><br><span class="line">        mesh.triangles = triangles;</span><br><span class="line">        mesh.RecalculateNormals();</span><br><span class="line">        mesh.RecalculateBounds();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> mesh;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 更新 pivot uv 变量</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">update_pivot_uv</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        Sprite sprite = _sprite_renderer.sprite;</span><br><span class="line"></span><br><span class="line">        _pivot.x = sprite.rect.width * <span class="number">0.5f</span> / sprite.pixelsPerUnit;</span><br><span class="line">        _pivot.y = sprite.rect.height * <span class="number">0.5f</span> / sprite.pixelsPerUnit;</span><br><span class="line">        _pivot.z = (sprite.rect.width * <span class="number">0.5f</span> - sprite.pivot.x) / sprite.pixelsPerUnit;</span><br><span class="line">        _pivot.w = sprite.pivot.y / sprite.pixelsPerUnit;</span><br><span class="line"></span><br><span class="line">        _uv.x = sprite.uv[<span class="number">1</span>].x - sprite.uv[<span class="number">0</span>].x;</span><br><span class="line">        _uv.y = sprite.uv[<span class="number">0</span>].y - sprite.uv[<span class="number">2</span>].y;</span><br><span class="line">        _uv.z = sprite.uv[<span class="number">2</span>].x;</span><br><span class="line">        _uv.w = sprite.uv[<span class="number">2</span>].y;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">create_sprite_mesh</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (_sprite_mesh_go != <span class="literal">null</span>)</span><br><span class="line">            DestroyImmediate(_sprite_mesh_go);</span><br><span class="line"></span><br><span class="line">        _sprite_mesh_go = <span class="keyword">new</span> GameObject(<span class="string">&quot;sprite_mesh&quot;</span>);</span><br><span class="line">        _sprite_mesh_go.layer = gameObject.layer;</span><br><span class="line">        _sprite_mesh_go.transform.SetParent(transform);</span><br><span class="line">        _sprite_mesh_go.transform.localPosition = Vector3.zero;</span><br><span class="line">        _sprite_mesh_go.transform.localRotation = Quaternion.identity;</span><br><span class="line">        _sprite_mesh_go.transform.localScale = Vector3.one;</span><br><span class="line"></span><br><span class="line">        MeshFilter mesh_filter = _sprite_mesh_go.AddComponent&lt;MeshFilter&gt;();</span><br><span class="line">        mesh_filter.sharedMesh = _mesh;</span><br><span class="line"></span><br><span class="line">        _sprite_mesh_renderer = _sprite_mesh_go.AddComponent&lt;MeshRenderer&gt;();</span><br><span class="line">        _sprite_mesh_renderer.enabled = <span class="literal">true</span>;</span><br><span class="line">        _sprite_mesh_renderer.sortingLayerID = _sprite_renderer.sortingLayerID;</span><br><span class="line">        _sprite_mesh_renderer.sortingOrder = _sprite_renderer.sortingOrder;</span><br><span class="line">        _sprite_mesh_renderer.sharedMaterial = ResMgr.instance.load_material(<span class="string">&quot;Material/mapobject.mat&quot;</span>);</span><br><span class="line">        _sprite_mesh_renderer.sharedMaterial.SetTexture(<span class="string">&quot;_Textures&quot;</span>, tex_array);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">create_reflection_mesh</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (_reflection_mesh_go != <span class="literal">null</span>)</span><br><span class="line">            DestroyImmediate(_reflection_mesh_go);</span><br><span class="line"></span><br><span class="line">        _reflection_mesh_go = <span class="keyword">new</span> GameObject(<span class="string">&quot;reflection_mesh&quot;</span>);</span><br><span class="line">        _reflection_mesh_go.layer = LayerMask.NameToLayer(<span class="string">&quot;Reflection&quot;</span>);</span><br><span class="line">        _reflection_mesh_go.transform.SetParent(transform);</span><br><span class="line">        _reflection_mesh_go.transform.localPosition = Vector3.zero;</span><br><span class="line">        _reflection_mesh_go.transform.localRotation = Quaternion.identity;</span><br><span class="line">        _reflection_mesh_go.transform.localScale = <span class="keyword">new</span> Vector3(<span class="number">1</span>, <span class="number">-1</span>, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">        MeshFilter mesh_filter = _reflection_mesh_go.AddComponent&lt;MeshFilter&gt;();</span><br><span class="line">        mesh_filter.sharedMesh = _mesh;</span><br><span class="line"></span><br><span class="line">        _reflection_mesh_renderer = _reflection_mesh_go.AddComponent&lt;MeshRenderer&gt;();</span><br><span class="line">        _reflection_mesh_renderer.enabled = <span class="literal">true</span>;</span><br><span class="line">        _reflection_mesh_renderer.sharedMaterial = ResMgr.instance.load_material(<span class="string">&quot;Material/mapobject.mat&quot;</span>);</span><br><span class="line">        _reflection_mesh_renderer.sharedMaterial.SetTexture(<span class="string">&quot;_Textures&quot;</span>, tex_array);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">create_underwater_mesh</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (_underwater_mesh_go != <span class="literal">null</span>)</span><br><span class="line">            DestroyImmediate(_underwater_mesh_go);</span><br><span class="line"></span><br><span class="line">        _underwater_mesh_go = <span class="keyword">new</span> GameObject(<span class="string">&quot;underwater_mesh&quot;</span>);</span><br><span class="line">        _underwater_mesh_go.layer = LayerMask.NameToLayer(<span class="string">&quot;Underwater&quot;</span>);</span><br><span class="line">        _underwater_mesh_go.transform.SetParent(transform);</span><br><span class="line">        _underwater_mesh_go.transform.localPosition = Vector3.zero;</span><br><span class="line">        _underwater_mesh_go.transform.localRotation = Quaternion.identity;</span><br><span class="line">        _underwater_mesh_go.transform.localScale = Vector3.one;</span><br><span class="line"></span><br><span class="line">        MeshFilter mesh_filter = _underwater_mesh_go.AddComponent&lt;MeshFilter&gt;();</span><br><span class="line">        mesh_filter.sharedMesh = _mesh;</span><br><span class="line"></span><br><span class="line">        _underwater_mesh_renderer = _underwater_mesh_go.AddComponent&lt;MeshRenderer&gt;();</span><br><span class="line">        _underwater_mesh_renderer.enabled = <span class="literal">true</span>;</span><br><span class="line">        _underwater_mesh_renderer.sharedMaterial = ResMgr.instance.load_material(<span class="string">&quot;Material/mapobject.mat&quot;</span>);</span><br><span class="line">        _underwater_mesh_renderer.sharedMaterial.SetTexture(<span class="string">&quot;_Textures&quot;</span>, tex_array);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">update_mesh</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        update_pivot_uv();</span><br><span class="line"></span><br><span class="line">        _sprite_mesh_renderer.GetPropertyBlock(_prop_block);</span><br><span class="line">        _prop_block.SetFloat(<span class="string">&quot;_PPU&quot;</span>, _ppu);</span><br><span class="line">        _prop_block.SetFloat(<span class="string">&quot;_TexIndex&quot;</span>, _tex_index);</span><br><span class="line">        _prop_block.SetVector(<span class="string">&quot;_Pivot&quot;</span>, _pivot);</span><br><span class="line">        _prop_block.SetVector(<span class="string">&quot;_UV&quot;</span>, _uv);</span><br><span class="line">        _prop_block.SetFloat(<span class="string">&quot;_Upper&quot;</span>, <span class="number">1</span>);</span><br><span class="line">        _prop_block.SetFloat(<span class="string">&quot;_OffsetY&quot;</span>, offset_y);</span><br><span class="line">        _sprite_mesh_renderer.SetPropertyBlock(_prop_block);</span><br><span class="line"></span><br><span class="line">        _reflection_mesh_renderer?.SetPropertyBlock(_prop_block);</span><br><span class="line"></span><br><span class="line">        _prop_block.SetFloat(<span class="string">&quot;_Upper&quot;</span>, <span class="number">0</span>);</span><br><span class="line">        _underwater_mesh_renderer?.SetPropertyBlock(_prop_block);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br></pre></td><td class="code"><pre><span class="line">Shader <span class="string">&quot;Custom/MapObject2&quot;</span></span><br><span class="line">&#123;</span><br><span class="line">    Properties</span><br><span class="line">    &#123;</span><br><span class="line">        _Color(<span class="string">&quot;Color&quot;</span>, Color) = (<span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>)</span><br><span class="line">        _Textures(<span class="string">&quot;Textures&quot;</span>, <span class="number">2</span>DArray) = <span class="string">&quot;&quot;</span> &#123;&#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    SubShader</span><br><span class="line">    &#123;</span><br><span class="line">        Tags</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="string">&quot;Queue&quot;</span> = <span class="string">&quot;Transparent&quot;</span></span><br><span class="line">            <span class="string">&quot;IgnoreProjector&quot;</span> = <span class="string">&quot;True&quot;</span></span><br><span class="line">            <span class="string">&quot;RenderType&quot;</span> = <span class="string">&quot;Opaque&quot;</span></span><br><span class="line">            <span class="string">&quot;PreviewType&quot;</span> = <span class="string">&quot;Plane&quot;</span></span><br><span class="line">            <span class="string">&quot;CanUseSpriteAtlas&quot;</span> = <span class="string">&quot;True&quot;</span></span><br><span class="line">            <span class="comment">// &quot;DisableBatching&quot;=&quot;True&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ZWrite Off</span><br><span class="line">        Lighting Off</span><br><span class="line">        Cull Off</span><br><span class="line">        Blend SrcAlpha OneMinusSrcAlpha</span><br><span class="line">        </span><br><span class="line">        Pass</span><br><span class="line">        &#123;</span><br><span class="line">            HLSLPROGRAM</span><br><span class="line">            <span class="meta">#<span class="keyword">pragma</span> vertex vert</span></span><br><span class="line">            <span class="meta">#<span class="keyword">pragma</span> fragment frag</span></span><br><span class="line">            <span class="meta">#<span class="keyword">pragma</span> require 2darray</span></span><br><span class="line">            <span class="meta">#<span class="keyword">pragma</span> multi_compile_instancing</span></span><br><span class="line">            <span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;</span></span></span><br><span class="line">            <span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Assets/AssetsPackage/Shader/Includes/pixel_art.hlsl&quot;</span></span></span><br><span class="line"></span><br><span class="line">            Texture2DArray _Textures;</span><br><span class="line">            SamplerState linear_clamp_sampler;</span><br><span class="line">            SamplerState point_clamp_sampler;</span><br><span class="line">            float4 _Textures_TexelSize;</span><br><span class="line">            float4 _Color;</span><br><span class="line"></span><br><span class="line">            UNITY_INSTANCING_BUFFER_START(Props)</span><br><span class="line">            UNITY_DEFINE_INSTANCED_PROP(<span class="type">float</span>, _PPU)</span><br><span class="line">            UNITY_DEFINE_INSTANCED_PROP(<span class="type">float</span>, _TexIndex)</span><br><span class="line">            UNITY_DEFINE_INSTANCED_PROP(half4, _Pivot)</span><br><span class="line">            UNITY_DEFINE_INSTANCED_PROP(half4, _UV)</span><br><span class="line">            UNITY_DEFINE_INSTANCED_PROP(<span class="type">float</span>, _Upper)</span><br><span class="line">            UNITY_DEFINE_INSTANCED_PROP(<span class="type">float</span>, _OffsetY)</span><br><span class="line">            UNITY_INSTANCING_BUFFER_END(Props)</span><br><span class="line"></span><br><span class="line">            <span class="class"><span class="keyword">struct</span> <span class="title">a2v</span></span></span><br><span class="line"><span class="class">            &#123;</span></span><br><span class="line">                float4 vertex: POSITION;</span><br><span class="line">                float2 texcoord: TEXCOORD0;</span><br><span class="line">                float4 color: COLOR;</span><br><span class="line">                UNITY_VERTEX_INPUT_INSTANCE_ID</span><br><span class="line">            &#125;;</span><br><span class="line"></span><br><span class="line">            <span class="class"><span class="keyword">struct</span> <span class="title">v2f</span></span></span><br><span class="line"><span class="class">            &#123;</span></span><br><span class="line">                float4 vertex: SV_POSITION;</span><br><span class="line">                float2 uv: TEXCOORD0;</span><br><span class="line">                float4 color: COLOR;</span><br><span class="line">                float4 local_pos: TEXCOORD1;</span><br><span class="line">                UNITY_VERTEX_INPUT_INSTANCE_ID</span><br><span class="line">            &#125;;</span><br><span class="line"></span><br><span class="line">            v2f <span class="title function_">vert</span><span class="params">(a2v input)</span></span><br><span class="line">            &#123;</span><br><span class="line">                UNITY_SETUP_INSTANCE_ID(input);</span><br><span class="line"></span><br><span class="line">                v2f output;</span><br><span class="line">                UNITY_TRANSFER_INSTANCE_ID(input, output);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// pivot</span></span><br><span class="line">                half4 pivot = UNITY_ACCESS_INSTANCED_PROP(Props, _Pivot);</span><br><span class="line">                half4x4 pivot_m = &#123;</span><br><span class="line">                    pivot.x, <span class="number">0</span>,       <span class="number">0</span>,      pivot.z,</span><br><span class="line">                    <span class="number">0</span>,       pivot.y, <span class="number">0</span>,      pivot.w,</span><br><span class="line">                    <span class="number">0</span>,       <span class="number">0</span>,       <span class="number">1</span>,      <span class="number">0</span>,</span><br><span class="line">                    <span class="number">0</span>,       <span class="number">0</span>,       <span class="number">0</span>,      <span class="number">1</span></span><br><span class="line">                &#125;;</span><br><span class="line">                <span class="type">float</span> offset_y = UNITY_ACCESS_INSTANCED_PROP(Props, _OffsetY);</span><br><span class="line">                input.vertex = mul(pivot_m, input.vertex);</span><br><span class="line">                input.vertex.y -= offset_y; <span class="comment">// 竖直移动</span></span><br><span class="line">                output.vertex = TransformObjectToHClip(input.vertex.xyz);</span><br><span class="line">                output.local_pos = input.vertex; <span class="comment">// 记录局部坐标用于剔除</span></span><br><span class="line"></span><br><span class="line">                <span class="comment">// uv</span></span><br><span class="line">                half4 uv = UNITY_ACCESS_INSTANCED_PROP(Props, _UV);</span><br><span class="line">                half3x3 uv_m = &#123;</span><br><span class="line">                    uv.x, <span class="number">0</span>,    uv.z,</span><br><span class="line">                    <span class="number">0</span>,    uv.y, uv.w,</span><br><span class="line">                    <span class="number">0</span>,    <span class="number">0</span>,    <span class="number">1</span></span><br><span class="line">                &#125;;</span><br><span class="line">                output.uv = mul(uv_m, half3(input.texcoord, <span class="number">1</span>)).xy;</span><br><span class="line"></span><br><span class="line">                output.color = input.color * _Color;</span><br><span class="line">                <span class="keyword">return</span> output;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            float4 <span class="title function_">frag</span><span class="params">(v2f input)</span> : SV_Target</span><br><span class="line">            &#123;</span><br><span class="line">                UNITY_SETUP_INSTANCE_ID(input);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// sprite 剔除</span></span><br><span class="line">                <span class="type">float</span> ppu = UNITY_ACCESS_INSTANCED_PROP(Props, _PPU);</span><br><span class="line">                <span class="type">float</span> upper = UNITY_ACCESS_INSTANCED_PROP(Props, _Upper);</span><br><span class="line">                <span class="type">float</span> is_discard = upper * input.local_pos.y + (<span class="number">1.0</span> / ppu - input.local_pos.y) * (<span class="number">1</span> - upper);</span><br><span class="line">                clip(is_discard);</span><br><span class="line"></span><br><span class="line">                float2 uv = input.uv;</span><br><span class="line">                uv = pixelate_uv(uv, _Textures_TexelSize);</span><br><span class="line">                <span class="type">int</span> tex_index = UNITY_ACCESS_INSTANCED_PROP(Props, _TexIndex);</span><br><span class="line">                float4 col = _Textures.Sample(linear_clamp_sampler, float3(uv, tex_index));</span><br><span class="line">                <span class="keyword">return</span> col * input.color;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            ENDHLSL</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>挂上这个脚本后, 原来的 Sprite Renderer 会停止渲染, 渲染的工作交给三个子节点上的 Mesh.<br>最终实现的效果如图5,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_water_2.gif" style="zoom:80%;" class alt="图5" /></p><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><h2 id="common-hlsl-与-pixel-art-hlsl"><a href="#common-hlsl-与-pixel-art-hlsl" class="headerlink" title="common.hlsl 与 pixel_art.hlsl"></a>common.hlsl 与 pixel_art.hlsl</h2><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#ifndef _INCLUDE_COMMON_HLSL_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> _INCLUDE_COMMON_HLSL_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#include &quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// ref: https://docs.unity3d.com/Packages/com.unity.shadergraph@12.1/manual/Blend-Node.html</span></span><br><span class="line"><span class="function"><span class="built_in">float</span> <span class="title">blend_overlay</span>(<span class="params"><span class="built_in">float</span> <span class="keyword">base</span>, <span class="built_in">float</span> blend, <span class="built_in">float</span> opacity</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">float</span> result1 = <span class="number">1.0</span> - <span class="number">2.0</span> * (<span class="number">1.0</span> - <span class="keyword">base</span>) * (<span class="number">1.0</span> - blend);</span><br><span class="line">    <span class="built_in">float</span> result2 = <span class="number">2.0</span> * <span class="keyword">base</span> * blend;</span><br><span class="line">    <span class="built_in">float</span> zero_or_one = step(<span class="keyword">base</span>, <span class="number">0.5</span>);</span><br><span class="line">    <span class="built_in">float</span> output = result2 * zero_or_one + (<span class="number">1</span> - zero_or_one) * result1;</span><br><span class="line">    <span class="keyword">return</span> lerp(<span class="keyword">base</span>, output, opacity);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="built_in">float</span> <span class="title">blend_subtract</span>(<span class="params"><span class="built_in">float</span> <span class="keyword">base</span>, <span class="built_in">float</span> blend, <span class="built_in">float</span> opacity</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> lerp(<span class="keyword">base</span>, <span class="keyword">base</span> - blend, opacity);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">float2 <span class="title">blend_subtract</span>(<span class="params">float2 <span class="keyword">base</span>, float2 blend, <span class="built_in">float</span> opacity</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> lerp(<span class="keyword">base</span>, <span class="keyword">base</span> - blend, opacity);</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// -------------------------------------- Gradient Noise --------------------------------------</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// gradient noise</span></span><br><span class="line"><span class="comment">// ref: https://docs.unity3d.com/Packages/com.unity.shadergraph@6.9/manual/Gradient-Noise-Node.html</span></span><br><span class="line"><span class="function">float2 <span class="title">gradient_noise_dir</span>(<span class="params">float2 p</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    p = p % <span class="number">289</span>;</span><br><span class="line">    <span class="built_in">float</span> x = (<span class="number">34</span> * p.x + <span class="number">1</span>) * p.x % <span class="number">289</span> + p.y;</span><br><span class="line">    x = (<span class="number">34</span> * x + <span class="number">1</span>) * x % <span class="number">289</span>;</span><br><span class="line">    x = frac(x / <span class="number">41</span>) * <span class="number">2</span> - <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">return</span> normalize(float2(x - floor(x + <span class="number">0.5</span>), abs(x) - <span class="number">0.5</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="built_in">float</span> <span class="title">gradient_noise</span>(<span class="params">float2 uv, <span class="built_in">float</span> scale</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    float2 p = uv * scale;</span><br><span class="line">    float2 ip = floor(p);</span><br><span class="line">    float2 fp = frac(p);</span><br><span class="line">    <span class="built_in">float</span> d00 = dot(gradient_noise_dir(ip), fp);</span><br><span class="line">    <span class="built_in">float</span> d01 = dot(gradient_noise_dir(ip + float2(<span class="number">0</span>, <span class="number">1</span>)), fp - float2(<span class="number">0</span>, <span class="number">1</span>));</span><br><span class="line">    <span class="built_in">float</span> d10 = dot(gradient_noise_dir(ip + float2(<span class="number">1</span>, <span class="number">0</span>)), fp - float2(<span class="number">1</span>, <span class="number">0</span>));</span><br><span class="line">    <span class="built_in">float</span> d11 = dot(gradient_noise_dir(ip + float2(<span class="number">1</span>, <span class="number">1</span>)), fp - float2(<span class="number">1</span>, <span class="number">1</span>));</span><br><span class="line">    fp = fp * fp * fp * (fp * (fp * <span class="number">6</span> - <span class="number">15</span>) + <span class="number">10</span>);</span><br><span class="line">    <span class="keyword">return</span> lerp(lerp(d00, d01, fp.y), lerp(d10, d11, fp.y), fp.x) + <span class="number">0.5</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// -------------------------------------- Gaussian Blur --------------------------------------</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> SAMPLE_KERNEL(i, x, y) \</span></span><br><span class="line">    color += kernel[i] * tex.Sample(ss, uv + texel_size.xy * float2(x, y));</span><br><span class="line"></span><br><span class="line"><span class="comment">// Gaussian Blur Function</span></span><br><span class="line"><span class="function">float4 <span class="title">gaussian_blur_3x3</span>(<span class="params">Texture2D tex, SamplerState ss, float2 uv, float2 texel_size</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// Gaussian kernel</span></span><br><span class="line">    <span class="built_in">float</span> kernel[<span class="number">9</span>] = &#123;</span><br><span class="line">        <span class="number">0.0625</span>, <span class="number">0.125</span>, <span class="number">0.0625</span>,</span><br><span class="line">        <span class="number">0.125</span>, <span class="number">0.25</span>, <span class="number">0.125</span>,</span><br><span class="line">        <span class="number">0.0625</span>, <span class="number">0.125</span>, <span class="number">0.0625</span>,</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Sample the texture</span></span><br><span class="line">    float4 color = float4(<span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">0.0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">0</span>, <span class="number">-1</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">1</span>,  <span class="number">0</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">2</span>,  <span class="number">1</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">3</span>, <span class="number">-1</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">4</span>,  <span class="number">0</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">5</span>,  <span class="number">1</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">6</span>, <span class="number">-1</span>,  <span class="number">1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">7</span>,  <span class="number">0</span>,  <span class="number">1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">8</span>,  <span class="number">1</span>,  <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> color;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">float4 <span class="title">gaussian_blur_5x5</span>(<span class="params">Texture2D tex, SamplerState ss, float2 uv, float2 texel_size</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// Gaussian kernel</span></span><br><span class="line">    <span class="built_in">float</span> kernel[<span class="number">25</span>] = &#123;</span><br><span class="line">        <span class="number">0.00390625</span>, <span class="number">0.015625</span>, <span class="number">0.0234375</span>, <span class="number">0.015625</span>, <span class="number">0.00390625</span>,</span><br><span class="line">        <span class="number">0.015625</span>, <span class="number">0.0625</span>, <span class="number">0.09375</span>, <span class="number">0.0625</span>, <span class="number">0.015625</span>,</span><br><span class="line">        <span class="number">0.0234375</span>, <span class="number">0.09375</span>, <span class="number">0.140625</span>, <span class="number">0.09375</span>, <span class="number">0.0234375</span>,</span><br><span class="line">        <span class="number">0.015625</span>, <span class="number">0.0625</span>, <span class="number">0.09375</span>, <span class="number">0.0625</span>, <span class="number">0.015625</span>,</span><br><span class="line">        <span class="number">0.00390625</span>, <span class="number">0.015625</span>, <span class="number">0.0234375</span>, <span class="number">0.015625</span>, <span class="number">0.00390625</span>,</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Sample the texture</span></span><br><span class="line">    float4 color = float4(<span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">0.0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">0</span>,  <span class="number">-2</span>, <span class="number">-2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">1</span>,  <span class="number">-1</span>, <span class="number">-2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">2</span>,   <span class="number">0</span>, <span class="number">-2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">3</span>,   <span class="number">1</span>, <span class="number">-2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">4</span>,   <span class="number">2</span>, <span class="number">-2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">5</span>,  <span class="number">-2</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">6</span>,  <span class="number">-1</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">7</span>,   <span class="number">0</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">8</span>,   <span class="number">1</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">9</span>,   <span class="number">2</span>, <span class="number">-1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">10</span>, <span class="number">-2</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">11</span>, <span class="number">-1</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">12</span>,  <span class="number">0</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">13</span>,  <span class="number">1</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">14</span>,  <span class="number">2</span>,  <span class="number">0</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">15</span>, <span class="number">-2</span>,  <span class="number">1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">16</span>, <span class="number">-1</span>,  <span class="number">1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">17</span>,  <span class="number">0</span>,  <span class="number">1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">18</span>,  <span class="number">1</span>,  <span class="number">1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">19</span>,  <span class="number">2</span>,  <span class="number">1</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">20</span>, <span class="number">-2</span>,  <span class="number">2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">21</span>, <span class="number">-1</span>,  <span class="number">2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">22</span>,  <span class="number">0</span>,  <span class="number">2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">23</span>,  <span class="number">1</span>,  <span class="number">2</span>);</span><br><span class="line">    SAMPLE_KERNEL(<span class="number">24</span>,  <span class="number">2</span>,  <span class="number">2</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> color;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">float4 <span class="title">gaussian_blur</span>(<span class="params">Texture2D tex, SamplerState ss, float2 uv, float2 texel_size, <span class="built_in">float</span> blur</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    float4 col = float4(<span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">0.0</span>, <span class="number">0.0</span>);</span><br><span class="line">    <span class="built_in">float</span> kernel_sum = <span class="number">0.0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">int</span> upper = (blur - <span class="number">1</span>) * <span class="number">0.5</span>;</span><br><span class="line">    <span class="built_in">int</span> lower = -upper;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">int</span> x = lower; x &lt;= upper; ++x)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="built_in">int</span> y = lower; y &lt;= upper; ++y)</span><br><span class="line">        &#123;</span><br><span class="line">            kernel_sum++;</span><br><span class="line">            float2 offset = float2(texel_size.x * x, texel_size.y * y);</span><br><span class="line">            col += tex.Sample(ss, uv + offset);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    col /= kernel_sum;</span><br><span class="line">    <span class="keyword">return</span> col;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">ifndef</span> _INCLUDE_PIXEL_ART_HLSL_</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> _INCLUDE_PIXEL_ART_HLSL_</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&quot;</span></span></span><br><span class="line"></span><br><span class="line">float2 <span class="title function_">pixelate_uv</span><span class="params">(float2 uv, float4 texel_size)</span></span><br><span class="line">&#123;</span><br><span class="line">    float2 tpp = clamp(fwidth(uv) * texel_size.zw, <span class="number">1e-5</span>, <span class="number">1</span>);</span><br><span class="line">    float2 tx = uv * texel_size.zw - <span class="number">0.5</span> * tpp;</span><br><span class="line">    float2 tx_offset = smoothstep(<span class="number">1</span> - tpp, <span class="number">1</span>, frac(tx)) + <span class="number">0.5</span>; <span class="comment">// saturate((frac(tx) + tpp - 1) / tpp) + 0.5;</span></span><br><span class="line">    uv = (<span class="built_in">floor</span>(tx) + tx_offset) * texel_size.xy;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> uv;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">float3 <span class="title function_">pixelate_world_pos</span><span class="params">(float3 world_pos, <span class="type">float</span> ppu)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">floor</span>(world_pos * ppu) / ppu;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><h2 id="Tilemap-的缝隙问题与解决方案"><a href="#Tilemap-的缝隙问题与解决方案" class="headerlink" title="Tilemap 的缝隙问题与解决方案"></a>Tilemap 的缝隙问题与解决方案</h2><p>正文中提到在 tilemap 中使用 高斯模糊 特定情况下可能会有问题, 如果模糊采样的范围(<code>_FoamTexelSize</code>)扩得太大, tile 的表现可能会异常. 具体到泡沫这里的例子, 随着 <code>_FoamTexelSize</code> 的增大, 水体内部的 tile 可能会出现异常的白色.</p><p>这个问题要从 tilemap 缝隙问题谈起, 这是一个几乎所有刚开始使用 tilemap 的人都会遇到的一个坑, 表现是 tilemap 在镜头移动过程中会出现缝隙, 即使所使用的 tileset 是严丝合缝的. 关于这个问题, 比较好的解释可以参考 <a href="https://tiled2unity.readthedocs.io/en/latest/fixing-seams/">Fixing Seams - Tiled2Unity</a>, 简单来说是 shader 在采样 tile 贴图时, 由于数值的精度问题会导致采样到贴图外面. 处理的方法也很多, 一种方式类似 jess 的 tileset 中的处理, 会发现每个 16x16 tile 的外面多一个像素宽度的颜色, 防止采样到 tile 外面后颜色不对导致缝隙. </p><p>我使用 <a href="https://github.com/Seanba/SuperTiled2Unity">Supertile2Unity</a> 这个项目, 把 <a href="https://www.mapeditor.org/">Tiled</a> 导出的 tilemap 导入到 unity. 在最新版本中, 这个插件会自动分割整张 tileset 纹理, 为了实现 seamless 的 tilemap, 可以手动创建一个 Sprite Atlas, 把这些分割的 sprite 做成一个图集, 此时这些 tile 的图像边缘会自动增加外扩的像素(在 Atlas 不勾选 Alpha Dilation 的情况情况下), 从而避免缝隙的产生. 只是打成图集后, 这些 tile 图片挨得非常近, 只有几个像素的间距(Atlas 检索面板中只能设置 2, 4, 8 个像素距离), 所以模糊采样时, 如果范围太大, 就会采到相邻图片的颜色. 一种解决办法是自定义图集的生成, 扩大图集中每张图片的间距. 这里也有一些坑, 不过和这次的主题差太远就先不提了.</p><h1 id="其他参考"><a href="#其他参考" class="headerlink" title="其他参考"></a>其他参考</h1><h2 id="2d游戏水体的参考"><a href="#2d游戏水体的参考" class="headerlink" title="2d游戏水体的参考"></a>2d游戏水体的参考</h2><ul><li><a href="https://www.youtube.com/watch?v=bJUnVU7eECc">Creating a 2D Water Shader in Unity - YouTube</a></li><li><a href="https://www.youtube.com/watch?v=YCFiCE1i-As">How to create a Water Tile Map Shader in Godot 4 - 2 minutes tutorial - YouTube</a></li><li><a href="https://mp.weixin.qq.com/s/zWvEh1Bn-D_wUuxnzY9G7Q">基于屏幕的2D实时反射</a></li><li><a href="https://www.youtube.com/watch?v=_iADGsjkLY8">Creating Animated Pixelart Water - Aseprite Tutorial - YouTube</a></li></ul><h2 id="3d游戏水体的参考"><a href="#3d游戏水体的参考" class="headerlink" title="3d游戏水体的参考"></a>3d游戏水体的参考</h2><p>有时可以从真实水体渲染方案中找找灵感</p><ul><li><a href="http://chenglixue.top/?p=45">Unity URP 风格化水 – 放課後ティータイム</a></li><li><a href="https://musoucrow.github.io/2020/09/06/lbbn_water/">在URP实现水面效果 | Musoucrow’ BLOG</a></li><li><a href="https://zhuanlan.zhihu.com/p/493766119">Unity 实现平面反射（基于 URP）</a></li><li><a href="https://www.cnblogs.com/uwatech/p/18413711">TA实践分享：材质与渲染——水体(Unity+UE） - UWATech</a></li><li><a href="https://www.patreon.com/posts/24192529">Making Interactive Water using RenderTexture | Patreon</a></li><li><a href="https://site-builder.wiki/posts/29659">水面シェーダーを作成する方法 [Unity] – Site-Builder.wiki</a></li><li><a href="https://www.reddit.com/r/Unity3D/comments/pbwvc0/are_water_shaders_still_popular_breakdown_video/">Are water shaders still popular? (breakdown video) : r/Unity3D</a></li><li><a href="https://halfpastyellow.com/blog/2020/10/01/Yet-Another-Stylised-Water-Shader.html">Yet Another Stylised Water Shader - Half Past Yellow | Blog</a><br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;jojo, 这是我今年最后的更新了! 咳咳, 这次继续来现学现卖, 分享下看到的 Pixel Art 相关的渲染技术. 前段时间油管上看到一个非常棒的 Pixel Art 水体渲染的分享, 是由游戏开发者 jess 制作的 &lt;a href=&quot;https://www.youtube.com/watch?v=pGOLstWBCDA&amp;amp;t=17s&quot;&gt;How I Created 2D Pixel Art Water - Unity Shader Graph&lt;/a&gt;. 这虽然是她的第一个视频, 但是质量高得可怕, 不仅制作过程思路清晰, 最后实现的效果也相当不错. 那话怎么说的来着, 新人都是怪物. 此后的几次分享同样好评如潮, 因为内容都是我感兴趣的领域, 所以我大概都会稍微研究一下. &lt;/p&gt;</summary>
    
    
    
    <category term="Lab" scheme="https://granvallen.github.io/categories/Lab/"/>
    
    <category term="计算机图形学" scheme="https://granvallen.github.io/categories/Lab/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
    
    <category term="游戏开发" scheme="https://granvallen.github.io/categories/Lab/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="计算机图形学" scheme="https://granvallen.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
    
    <category term="Unity" scheme="https://granvallen.github.io/tags/Unity/"/>
    
    <category term="PixelArt" scheme="https://granvallen.github.io/tags/PixelArt/"/>
    
    <category term="游戏开发" scheme="https://granvallen.github.io/tags/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>其实是一篇安利文...</title>
    <link href="https://granvallen.github.io/karakidakenokosyokurasi/"/>
    <id>https://granvallen.github.io/karakidakenokosyokurasi/</id>
    <published>2024-09-23T12:46:00.000Z</published>
    <updated>2024-09-23T13:09:08.000Z</updated>
    
    <content type="html"><![CDATA[<p>这段时间看了<a href="https://book.douban.com/subject/35776730/">百木田家の古書暮らし(百木田家的古书时光)</a>, 久违地又陷入一种很复杂的情绪之中, 实在是不吐不快, 我试图弄明白这种感觉是什么, 也许能更多窥见自己的内心.</p><span id="more"></span><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=29191028&auto=0&height=66"></iframe><p>这种感觉多出现于读完一个戳中我内心的好作品, 所谓贤者时间嘛. 好作品倒常见, 但真直达我心深处的却不那么多. 我似乎感觉到一种作者就是给像我一样的读者创作的感觉. 这种没有任何阻碍的信息传达, 每一个细节都精准击中的感觉, 实在是太舒服, 太让人回味, 以及太让人害怕失去了. 惊喜之后, 你就要付出面对终结后无尽苦闷的代价, 甚至是远没有到终结那天, 已经开始品尝终结的痛苦滋味. 这种痛苦在相遇之时就已注定, 无可避免. </p><p>前面我谈及了最表层的两种感觉, 已经略感纠结. 越是高看作品价值的人, 越是容易被这种感觉缠住无法自拔, 乃至不忍卒读. 我现在依然能回忆起的, 像是高分少女, 昨日之歌完结之时也都有类似症状. 如果再追问一步, 到底是什么放大了这种感觉, 可能就是对故事中的角色产生了相当的代入感, 也就是俗话说的入戏太深. 这其中大多是角色而不是故事在起作用. 不过话说回来, 哦, 原来你小子也是吃角色这一套的, 尽管在此之前我从不这么认为. 也许还可以看出, 这些多少都可以归到某种偏真实系的爱情剧, 这难道是我又一个软肋? 上面提到的作品, 可能远说不上伟大, 但我都愿意不吝称他们是好故事, 好角色, 情感真诚细腻且丰富, 确实很对我胃口. </p><p>回到前面说的直面这种苦闷, 何解? 有人说这种苦闷并不稀罕, 年轻人, 多读读经典的好作品, 你就发现等你还没来得及接受苦闷洗礼, 就会被下一个惊喜冲昏头脑了! 也有人说这种苦闷不值得, 年轻人, 多体验体验生活, 经历多了就看淡了. 长远来看是有些道理, 但对于此刻正处于苦闷的我来说如隔靴搔痒. 最后得出最可操作的做法是, 妈的, 得想个办法把进度推到最新, 然后熬着.</p><p>熬着, 我估摸着这应该也算不上是一种精神自虐吧. 想来这苦闷也不尽然全是坏处, 比如可以反思下自己的当下, 作者创作的”狡黠”, 最少也有一个念想. </p><p>说到创作者, 我这里不展开聊冬目景老师的事, 而是想说一类作品的共性. 高分少女和昨日之歌给我的感觉其实不太一样, 昨日之歌与百木田因为作者是同一人所以感觉确实是相近的, 而此之外, 石黑和小原漫画的感觉则和冬目景更近. 就是我想说这其中一定有某个东西贯穿了这些作者和作品, 同时不知怎得也把我连在其中, 让这些成了我最喜欢的一类作品. 我说不清这种感觉到底是什么, 思前想后, 有时在脑袋中会闪过这样一个具象画面, 是一个眯着眼睛笑着说话的角色!? 这些作品里确实也都有这样场景或角色. 更多地, 角色的说话方式, 剧情编排, 漫画的分格与分镜, 难道是某些共同的经验, 让我们对一个特定的漫画表现方式产生了特殊的感觉, 又或者说, 达成了某种对于漫画这一事物认知的共识? 这个话题我只能深入到这里. 也可能是我错了, 大错特错, 这些都是作者创作出的可恶的幻觉…</p><p>这苦闷之中似乎还有头绪, 我知道了我喜欢什么样的作品, 然后呢? 是的, 我满怀欣喜消费这些作品, 甚至恨不得直接把钱塞到创作者的钱袋子里, 我想是人都来者不拒的. 不可否认, 支持是重要的, 然而却并不总是稀有, 有时似乎创作者并不缺我这一份支持. 当然在创作者处境艰难时, 这个支持会格外有分量, 我也并不是说不缺支持就不支持, 只是这种分量, 你要说是某种爱也行, 仿佛被稀释了. 做一个只有10个粉丝的创作者的粉丝, 和一个百万粉的创作者的粉丝心态上会有不同吗. 当这种分量被削减后, 我又似乎希望通过其他方式进行补偿, 也就是, 该付出什么代价才能在心里弥补这个作品的价值, 又或者对于我们曾共同信仰的共识有几分贡献? 仿佛差额被抹平后, 才能和作者平等地对话.</p><p>好了, 以上都是我胡诌的, 但百木田家的古书时光是好作品不是. 我的贡献…我的贡献也就最多能拉个人头了, 本作双周刊グランドジャンプ好评连载中, 单行本已出版5卷, 台版东立出版1卷, 希望大家去看…<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这段时间看了&lt;a href=&quot;https://book.douban.com/subject/35776730/&quot;&gt;百木田家の古書暮らし(百木田家的古书时光)&lt;/a&gt;, 久违地又陷入一种很复杂的情绪之中, 实在是不吐不快, 我试图弄明白这种感觉是什么, 也许能更多窥见自己的内心.&lt;/p&gt;</summary>
    
    
    
    <category term="ACGN" scheme="https://granvallen.github.io/categories/ACGN/"/>
    
    <category term="漫画" scheme="https://granvallen.github.io/categories/ACGN/%E6%BC%AB%E7%94%BB/"/>
    
    
    <category term="读后感" scheme="https://granvallen.github.io/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/"/>
    
  </entry>
  
  <entry>
    <title>浅谈Pixel Art缩放及抗锯齿问题</title>
    <link href="https://granvallen.github.io/PixelArtUpscaling/"/>
    <id>https://granvallen.github.io/PixelArtUpscaling/</id>
    <published>2024-07-14T06:07:00.000Z</published>
    <updated>2025-10-01T14:11:59.739Z</updated>
    
    <content type="html"><![CDATA[<p>米娜桑, 我又回来更新了…. 嗯, 年更博主的地位保住了…</p><p>其实我在准备一个更长~篇的更新, 千真万确, 不过目前可公开的情报还不多. 那么在我们仍未知道的更新到来之前, 这次, 难得正经地, 来简单聊聊 Pixel Art 的缩放及抗锯齿问题吧…</p><span id="more"></span><h1 id="序"><a href="#序" class="headerlink" title="序"></a>序</h1><p>さて, 为什么会谈及这个问题? 我在处理像素艺术(Pixel Art)相关渲染的过程中, 遇到了这个问题. 具体可以描述为, 当<strong>像素风格纹理的精灵图(Sprite)在缩放或移动时, 会出现像素抖动与闪烁的现象</strong>, 看上去像是像素的波浪, 非常影响观感. 如图1左上角的例子.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_1.gif" style="zoom:80%;" class alt="图1" /></p><p>顺带一提, 精灵图上的角色是GBA游戏<a href="https://zh.wikipedia.org/wiki/%E8%81%96%E5%8A%8D%E5%90%8C%E7%9B%9F">《公主联盟》</a>中的<a href="https://lacrimacastle.net/index.php?game=Yggdra&amp;c=PSP&amp;sec=sprites">大天使マリエッタ</a>.</p><p>这个问题其实非常常见, 稍作检索就能找到原因与解决方法. 一个简单的规则是, <strong>像素图的放大需要是原尺寸的整数倍, 否则会出现畸变(Distorted)</strong>. 这种畸变是规律性的, 在放大或移动过程中就会出现有规律抖动和闪烁. 这里, 通常我们默认像素原图是比较小的, 所以在屏幕显示时主要是进行图像放大操作, 即Upscaling.</p><p>以上, 虽然是简单的理解, 但从某种角度说确实是最优解, 即不做任何的妥协, 让像素艺术分毫不损地显示到屏幕上吧! 在大部分时候, 上述方法并不会增加什么负担, 不过是限制游戏分辨率而已. 也可以参考 unity 官方的 <a href="https://github.com/Unity-Technologies/2d-pixel-perfect">2d-pixel-perfect: Pixel Perfect Camera</a>项目的方案.</p><p>但还是让人不禁想问, 是否有什么办法能让 Pixel Art 突破分辨率的桎梏, 同时不丢失太多表现力呢? 说到这里, 我回忆起之前3DS模拟器上玩到的结合3D场景与像素角色的游戏, 也有类似的观感.</p><p>相比之下, 尝试解决突破这个限制的分享和讨论则不是很多. 不过, 好在以我的检索能力依然能找到一些, 同时看到方法演进的思路. 我觉得更重要的是, 这些分享的内容都丰富且详尽, 是相当好的参考. 只是这样, 我也会认为这些技术值得分享.</p><p>这里自然不必把别人发明的东西再发明一遍, 我会在下面列出这些参考. 如果你也在解决类似问题, 可以参考看看(保佑你不是先看过了下面的内容后才看到这里).</p><ul><li><a href="https://csantosbh.wordpress.com/2014/01/25/manual-texture-filtering-for-pixelated-games-in-webgl/">Manual texture filtering for pixelated games in WebGL – Algorithmic Pensieve</a></li><li><a href="https://csantosbh.wordpress.com/2014/02/05/automatically-detecting-the-texture-filter-threshold-for-pixelated-magnifications/">Superior texture filter for dynamic pixel art upscaling</a></li><li><a href="https://colececil.io/blog/2017/scaling-pixel-art-without-destroying-it/">Scaling Pixel Art Without Destroying It</a></li><li><a href="https://www.youtube.com/watch?v=d6tp43wZqps">Crafting a Better Shader for Pixel Art Upscaling - YouTube</a></li><li><a href="https://www.patreon.com/posts/pixel-art-aa-83276362">Pixel Art AA Video Notes | Patreon</a></li></ul><p>上面的顺序正好是这几个作者相互引用的顺序, 其中第五个参考是 t3ssel8r 对于第四个参考(其所制作视频)的进一步解释. 关于 <a href="https://www.youtube.com/@t3ssel8r">t3ssel8r</a> 大佬还想多说几句, 很早之前看到过 t3ssel8r 关于3D像素艺术渲染的 Demo, 效果非常惊艳, 竟然能把像素画面调教地这么舒服. 至今还能在油管上看到其他人对 t3ssel8r demo 的 recreation. 不仅如此, t3ssel8r 其他游戏开发相关的分享, 质量也是高的可怕. 只是最近一年里频道貌似不怎么活动了, 不知进展是否顺利.</p><p>那么, 这篇文章到这里就结束了… 即使不理解原理, 只拿参考中简短的 shader 代码就能够得到图1下边一行的结果, 肉眼可见, 效果是相当明显的. 对了, 可能唯一容易漏掉的是, 如果使用Unity, <strong>记得把对应纹理的滤波模式(Filter Mode)从最近邻插值(Point)调整为双线性插值(Bilinear)</strong>.<br>.<br>.<br>.<br>.<br>.<br>虽然就想这样结束, 但出于笔记的完备, 我依然想稍微深入地谈谈对于这个问题的理解, 这些理解大体也不会超出参考的内容, 只算作是一个注解罢了.</p><h1 id="问题引入"><a href="#问题引入" class="headerlink" title="问题引入"></a>问题引入</h1><p>让我们试着重新理解为什么像素 Sprite 在放大后(非整数倍放大), 会出现像素畸变.</p><p>要理解这个问题, 需要先理解像素(pixel), 纹素(texel)与纹理采样(Texture Sampling)的概念. 像素通常意义下指的是屏幕显示的最小单元, 我们知道显示器分辨率即是用显示多少像素定义的. 而纹素指的是纹理贴图(Texture)最小色块单元. 在 Pixel Art 的语境下, 纹理贴图也通常带有明显的风格化特点. 纹理采样即是把贴图显示到屏幕上的过程. 某个像素的屏幕坐标$(x, y)$, 通过纹理坐标映射转换到归一化的纹理坐标$(u, v)$, 最终采样到纹理的颜色并显示在像素上.</p><p>如此我们可以知道, 在一定条件下, 像素与纹素是可以相同大小的, 此时像素与纹素将一一对应, 每个像素显示一个纹素的颜色. 在使用 Pixel Art 的纹理贴图时, 大部分情况下, 都需要对贴图进行放大显示, 因为原始像素素材通常是很小的, 而现代显示器的分辨率又辣么大. 一旦进行贴图的放大操作, 可以想见, 此时纹素色块也被扩大了, 会出现多个像素对应同一个纹素的情况.</p><p>事实上在 Unity 中, 当 Sprite 放大时, 会使用设定的 Filter Mode 对纹理进行插值操作. 插值是为了让像素在纹理放大后依然采样到期望的颜色. 就像上面提到的, 即使此时多个像素对应同一个纹素, 在不同应用场景下(比如期望像素颜色平滑过渡), 这些像素也并非就期望取对应纹素的颜色, 而是交由插值方法来最终决定的. 可以说, 不同的插值方法定义了像素纹理采样时参考纹素颜色的方式. </p><p>特别地, 对于像素艺术风格的纹理, 通常 Filter Mode 会选择设置为 Point, 即<a href="https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation">最近邻插值(Nearest Neighbor Interpolation)</a>. 简单说, 就是直接使用距离最近纹素颜色作为采样像素的颜色. 这种插值方式不会造成像素的模糊, 保持了像素色块间颜色的锐利变化, 也即保持了所谓的”像素感”. 可以说是像素艺术纹理理想的插值方法.</p><p>唯一的问题是, 当放大倍数为非整数倍时, 会有部分落在插值参考纹素边缘的采样像素, 这些像素无论最终选择插值为边界哪一边的颜色, 都不可避免地让色块组成的形状产生畸变. 这里引用下 Cole 在 <a href="https://colececil.io/blog/2017/scaling-pixel-art-without-destroying-it/">Scaling Pixel Art Without Destroying It</a> 一文中的图, 非常直观地说明了这一点. </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_2.png" style="zoom:80%;" class alt="图2" /></p><p>从图2可以看到, 下边的纹理在放大$\frac{7}{3}$倍后, 采样后的像素色块的形状发生了扭曲. 但是又如上边的例子所示, 却可以无损地放大2倍. 观察一下最近邻插值的过程, 纹理放大后, 每个像素的颜色都被定义为距离最近的放大后原纹素的颜色.</p><h1 id="解决思路"><a href="#解决思路" class="headerlink" title="解决思路"></a>解决思路</h1><p>上述提到的参考, 核心的解决思路其实是相同的. 如果牺牲在所难免, 那就只能尽可能减少牺牲. 事实上, 我们对于最近邻插值在靠近参考的原纹素中心附近的插值结果还是很满意的. 而落在那些参考纹素边缘的, 模棱两可的像素采样点(如图2中被虚线横穿的那些采样点)择需要被进一步调教. 具体来说, 我们希望这些像素的颜色是作为边缘两侧颜色的过渡(平滑). 这样就避免在放大或移动过程中, 因为边界线的移动, 导致这些像素颜色的跳变. 也就是类似图3(依然引自 Cole 文章)中的效果. </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_3.png" style="zoom:80%;" class alt="图3" /></p><p>毫无疑问, 这会损失一些”像素感”, 但好在这些平滑只发生在边界处, 通常占像素数量的比例很少. 那么接下来的问题就剩两个了, 如何找到这些边界处的采样像素, 以及如何确定这些采样点的颜色. 当然, 一个足够好的解决方法, 最好能同时解决这两个问题. </p><p>在上面两个问题中, 相对容易确定的是后者. 对于采样点颜色的平滑, 依然可以借助插值. 作为应用最广泛的<a href="https://en.wikipedia.org/wiki/Bilinear_interpolation">双线性插值(Bilinear Interpolation)</a>正满足这种需求. 二维平面上, 在给定4个参考点的情况下, 双线性插值对于任意一个参考点范围内的采样点进行两个维度上共三次线性插值. 双线性插值得到的采样是平滑的, 因而非常不适合 Pixel Art 纹理, 而对于绝大部分其他类型的纹理来说, 则是兼具性能与效果的选择. 其也作为基础插值方法, 内置于各大引擎. 因为不算很复杂, 我在附录1中也简单解释了其插值原理及性质, 算是对<a href="https://granvallen.github.io/sift/">SIFT特征提取算法理解与实现</a>中提到的三线性插值做一个补充.</p><p>解决第一个问题的思路最早在<a href="https://csantosbh.wordpress.com/2014/01/25/manual-texture-filtering-for-pixelated-games-in-webgl/">Manual texture filtering for pixelated games in WebGL – Algorithmic Pensieve</a>这篇博客中被分享. 其提出的解决问题的思路可用下图来说明,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_4.png" style="zoom:50%;" class alt="图4" /></p><p>我们首先定义一个 <strong>tpp(texel per pixel)</strong> 值, 这个值描述了像素与纹素的大小比例. 如 tpp = $\frac{1}{2}$, 表示一个纹素有两个像素长或宽. 当纹理放大时, 纹素对于像素就越大, tpp 的值就会相应变小. 接着, 我们可以把纹理上的纹素视作紧密排布的方形色块, 如图4中较大正方形所示. 同时像素也可以视为方形色块. 采样时像素方块映射到纹理坐标下, 如图四中较小正方形所示. 图四中 tpp 为 $\frac{1}{2}$, 此时可见, 像素是纹素方块的$\frac{1}{4}$大小. </p><p>图四上下两图中可以看到, 经过纹理映射的采样像素在水平方向上穿越一个纹素的过程. 当采样像素的区域完全落在同一纹素区域内时(如紫色方块之间的部分), 像素应该采样该纹素的颜色, 即之前认为的像素离这个纹素中心足够近的情况. 而采样区域如果落在多个纹素之间(如绿色方块附近), 此时就需要进行平滑插值计算采样值. 可以想象, 当区域落在4个纹素的交界中心时, 此时插值效果最平滑, 即插值需要平均地参考4个覆盖纹素的颜色.</p><p>如果这个纹理插值方法直接使用双线性插值, 而非最近邻插值, 那么此时在纹素内部采样得到的颜色是已经经过线性插值平滑的(如图四紫色采样区域, 虽然区域完全在单纹素区域内, 但采样结果也是经过附近纹素颜色线性插值过的). 只有当采样区域落在纹素正中心时, 采样值才与纹素颜色完全一致(线性插值完全参考某个参考值的情况).</p><p>这篇<a href="https://csantosbh.wordpress.com/2014/01/25/manual-texture-filtering-for-pixelated-games-in-webgl/">文章</a>中提出, 可以在纹理采样时手动调整纹理坐标$(u,v)$, 调整的结果是使得上图两紫色区域之间位置的纹理坐标的$u$修正为0.5, 即此时采样的是原本纹素颜色. 而其余位置的$u$则不做处理, 直接采样纹理平滑插值后的值. 同时在另一个维度$v$也进行同样的修正操作.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_5.png" style="zoom:50%;" class alt="图5" /></p><p>上述的$(u,v)$坐标的修正可以用图5所示的分段函数实现. 注意, 图5中横坐标为$lu$, 含义为采样点(采样区域中心)落在纹素上的局部$u$坐标(如右侧图示). 当$lu=0.5$, $lv=0.5$时, 采样点将位于纹素的中心. 我们可以通过微调$(lu,lv)$间接调整纹理坐标$(u,v)$. 可以发现, 图5中从$lu$调整为$lu’$的映射是一个分段函数, $lv$的映射也是类似.</p><p>关注下这个映射函数的分段点$\alpha$. 这个$\alpha$的位置即图4中上方一图的紫色采样区域, 此时正处于采样平滑插值与采样纹素中心颜色的分界点. 如果我们定义像素采样区域的宽高为$[pw, ph]$, 纹素宽高为$[tw,th]$, 那么有,</p><script type="math/tex; mode=display">tpp = \frac{pw}{tw}</script><script type="math/tex; mode=display">\alpha=\frac{1}{2}\frac{pw}{tw}=\frac{1}{2}tpp</script><p>可见这个$\alpha$与 tpp 的值有关, 也即与纹理放大倍数有关. 以上就是该方法的核心思路, 接下来是针对效果及实现上的优化, 在上述参考中也都有提及.</p><p>首先, 图5的分段映射函数存在两个问题. 其一是分段点的突变. 当采样点在边界采样经过临界点$\alpha$时, 该处的平滑插值与纹素颜色存在一个突变的色差. 为了有一个平滑过渡, 可以调整从0~$\alpha$的采样点分布, 如图6所示,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_6.png" style="zoom:50%;" class alt="图6" /></p><p>调整后的映射使得平滑插值处采样点向远离原点的方向偏移, 从而消除了颜色的突变.</p><p>第二个问题是如何在 shader 中实现这个分段函数, 为避免使用条件逻辑, <a href="https://csantosbh.wordpress.com/2014/01/25/manual-texture-filtering-for-pixelated-games-in-webgl/">文章</a> 中使用两个 clamp 函数组合的形式来实现. 如图7中红色与蓝色两个 clamp 函数所示, 可知两个函数之和即为上述分段函数.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_7.png" style="zoom:50%;" class alt="图7" /></p><p>这两个 clamp 和可以表达为,</p><script type="math/tex; mode=display">\begin{aligned}lu'&=clamp(\frac{lu}{2\alpha},\ 0,\ 0.5) + clamp(\frac{lu-1}{2\alpha}+0.5,\ 0,\ 0.5) \\    &=clamp(\frac{lu}{tpp},\ 0,\ 0.5)+clamp(\frac{lu-1}{tpp}+0.5,\ 0,\ 0.5)\end{aligned}</script><h1 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h1><p>Cole 在 <a href="https://colececil.io/blog/2017/scaling-pixel-art-without-destroying-it/">Scaling Pixel Art Without Destroying It</a> 中将上述思路在 unity 中进行了实现, 其核心的 shader 代码如下,</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">float</span> tpp;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">a2v</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    float4 vertex : POSITION;</span><br><span class="line">    float2 texcoord : TEXCOORD0;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">v2f</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    float4 vertex : SV_POSITION;</span><br><span class="line">    float2 texcoord : TEXCOORD0;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">v2f <span class="title function_">vert</span><span class="params">(a2v input)</span></span><br><span class="line">&#123;</span><br><span class="line">    v2f output;</span><br><span class="line">    output.vertex = UnityObjectToClipPos(input.vertex);</span><br><span class="line">    output.texcoord = input.texcoord * _MainTex_TexelSize.zw; <span class="comment">// 乘以纹理尺寸</span></span><br><span class="line">    <span class="keyword">return</span> output;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fixed4 <span class="title function_">frag</span><span class="params">(v2f input)</span> : SV_Target</span><br><span class="line">&#123;</span><br><span class="line">    float2 luv = frac(input.texcoord);</span><br><span class="line">    float2 txOffset = clamp(luv / tpp, <span class="number">0</span>, <span class="number">0.5</span>) + clamp((luv - <span class="number">1</span>) / tpp + <span class="number">0.5</span>, <span class="number">0</span>, <span class="number">0.5</span>);</span><br><span class="line">    float2 uv = (<span class="built_in">floor</span>(input.texcoord) + txOffset) * _MainTex_TexelSize.xy;</span><br><span class="line">    <span class="keyword">return</span> tex2D(_MainTex, uv);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="简化-clamp-计算"><a href="#简化-clamp-计算" class="headerlink" title="简化 clamp 计算"></a>简化 clamp 计算</h2><p>Cole 在评论中说明了关于 tpp 值的设定问题, 其做法是在相机的脚本中根据相机的 size 进行设置. 后续我们可以看到这个流程可以进一步优化. 不过在此之前, 我们先进一步简化上面的 clamp 计算.</p><p>上述讨论中, 我们默认把$(lu, lv)$设定为采样区域的中心. 为了简化, 我们也可以定义$(lu’’, lv’’)$设定为左上角, 用$lu’’$和$lv’’$来描述偏移. 此时上述分段映射函数可以调整为图8所示,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_8.png" style="zoom:50%;" class alt="图8" /></p><p>其中,</p><script type="math/tex; mode=display">lu''=lu-\alpha</script><p>此时映射函数可以用 saturate 函数来描述,</p><script type="math/tex; mode=display">\begin{aligned}lu'&=saturate(\frac{1}{2\alpha}lu''+1-\frac{1}{2\alpha})+\frac{1}{2} \\&=saturate(\frac{lu''+2\alpha-1}{2\alpha})+\frac{1}{2} \\&=saturate(\frac{lu''+tpp-1}{tpp})+\frac{1}{2}\end{aligned}</script><p>这里稍微再多解释下图8. 同样是为了保证平滑过渡, 当$lu’’$取1时, 需映射为1.5而不是1, 也即此时需采样右侧纹素的中心. 因为, 当采样点在这个临界值继续向右移动时, 将完全地进入右侧纹素.</p><p>由此, 片元着色器的代码可以进一步简化. 这也是 t3ssel8r 在视频 <a href="https://www.youtube.com/watch?v=d6tp43wZqps">Crafting a Better Shader for Pixel Art Upscaling - YouTube</a> 中所进行简化的由来.</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">fixed4 <span class="title function_">frag</span><span class="params">(v2f input)</span> : SV_Target</span><br><span class="line">&#123;</span><br><span class="line">    float2 tx = input.texcoord - <span class="number">0.5</span> * tpp;</span><br><span class="line">    float2 txOffset = saturate((frac(tx) + tpp - <span class="number">1</span>)) / tpp) + <span class="number">0.5</span>; <span class="comment">// luv = frac(tx)</span></span><br><span class="line">    float2 uv = (<span class="built_in">floor</span>(tx) + txOffset) * _MainTex_TexelSize.xy;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> tex2D(_MainTex, uv);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="优化-tpp-变量"><a href="#优化-tpp-变量" class="headerlink" title="优化 tpp 变量"></a>优化 tpp 变量</h2><p>前面提到, tpp(texel per pixel) 变量描述了纹素与像素的大小关系. Cole 曾提到可以通过相机参数计算该值并传入 shader. 我们希望能更合理地设置这个值, 以便在任何缩放尺度下达到最佳的效果. 相关技术最早在 <a href="https://csantosbh.wordpress.com/2014/02/05/automatically-detecting-the-texture-filter-threshold-for-pixelated-magnifications/">Superior texture filter for dynamic pixel art upscaling</a> 中被提及, t3ssel8r 在其视频中做了更深入的阐释.</p><p>在比较少见的 Sprite 三维旋转的情况下, 前述方法会出现新的问题, 如图9所示,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_9.gif" style="zoom:50%;" class alt="图9" /></p><p>图9左侧为不做任何处理的效果, 最右侧为使用前述方法和固定 tpp 后的效果, 中间的在右侧基础上使用动态计算的 tpp. 从上图可以看到, 使用固定 tpp 值在旋转角度较大时, 依然会出现像素抖动. </p><p>原因可以理解, 发生旋转后, 纹理上的纹素不再与像素轴对齐, 此时上述讨论方法就会失效. t3ssel8r 在视频中非常直观地展示了这个过程, 并重新对这个问题进行了建模. 其核心思路是<strong>用最小轴对称包围盒</strong>替代像素经过纹理映射后被旋转的采样区域进行采样, 如图10所示. </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_10.png" style="zoom:50%;" class alt="图10" /></p><p>重新回到纹理映射, 该过程把像素坐标$(x, y)$映射为归一化纹理坐标$(u,v)$, 可以用矩阵乘法进行描述,</p><script type="math/tex; mode=display">\pmatrix{u\\v}=\pmatrix{\frac{\partial u}{\partial x}&\frac{\partial u}{\partial y}\\\frac{\partial v}{\partial x}&\frac{\partial v}{\partial y}}\pmatrix{x\\y}=\pmatrix{\frac{\partial u}{\partial x}x+\frac{\partial u}{\partial y}y\\\frac{\partial v}{\partial x}x+\frac{\partial v}{\partial y}y}</script><p>代入归一化后的像素方格四个顶点坐标$(0,0)$, $(0,1)$, $(1, 0)$和$(1, 1)$, 得到经过对应纹理坐标为$(0, 0)$, $(\frac{\partial u}{\partial y}, \frac{\partial v}{\partial y})$, $(\frac{\partial u}{\partial x}, \frac{\partial v}{\partial x})$和$(\frac{\partial u}{\partial x}+\frac{\partial u}{\partial y}, \frac{\partial v}{\partial x}+\frac{\partial v}{\partial y})$. </p><p>分别取四个坐标$u$, $v$两方向上的最大值与最小值相减, 就得到了包围盒的长与宽.</p><script type="math/tex; mode=display">w \approx \left| \frac{\partial u}{\partial x} \right|+\left| \frac{\partial u}{\partial y} \right|</script><script type="math/tex; mode=display">h \approx \left| \frac{\partial v}{\partial x} \right|+\left| \frac{\partial v}{\partial y} \right|</script><script type="math/tex; mode=display">\boldsymbol{tpp}=\pmatrix{\frac{w}{tw}\\\frac{h}{th}}</script><p>上式中的 w 与 h 即前面讨论中使用的 pw 与 ph. 其值可借助 fwidth 函数计算得到. 于是我们可以调整 shader 为,</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">a2v</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    float4 vertex : POSITION;</span><br><span class="line">    float2 texcoord : TEXCOORD0;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">v2f</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    float4 vertex : SV_POSITION;</span><br><span class="line">    float2 uv : TEXCOORD0;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">v2f <span class="title function_">vert</span><span class="params">(a2v input)</span></span><br><span class="line">&#123;</span><br><span class="line">    v2f output;</span><br><span class="line">    output.vertex = UnityObjectToClipPos(input.vertex);</span><br><span class="line">    output.uv = input.texcoord;</span><br><span class="line">    <span class="keyword">return</span> output;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">fixed4 <span class="title function_">frag</span><span class="params">(v2f input)</span> : SV_Target</span><br><span class="line">&#123;</span><br><span class="line">    float2 tpp = clamp(fwidth(input.uv) * _MainTex_TexelSize.zw, <span class="number">1e-5</span>, <span class="number">1</span>);</span><br><span class="line">    float2 tx = input.uv * _MainTex_TexelSize.zw - <span class="number">0.5</span> * tpp;</span><br><span class="line">    float2 txOffset = saturate((frac(tx) + tpp - <span class="number">1</span>)) / tpp) + <span class="number">0.5</span>;</span><br><span class="line">    float2 uv = (<span class="built_in">floor</span>(tx) + txOffset) * _MainTex_TexelSize.xy;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> tex2D(_MainTex, uv);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以上就是关于这个话题我想说明的所有内容. 前文提到的参考中(如 t3ssel8r 的视频)解决了更多关联问题, 这里就不再展开了. 最终的效果即图1左下所示, 而右下为 Cole 的效果, tpp 根据相机参数设置.</p><h1 id="关于像素-Tilemap-中的抖动"><a href="#关于像素-Tilemap-中的抖动" class="headerlink" title="关于像素 Tilemap 中的抖动"></a>关于像素 Tilemap 中的抖动</h1><p>上述方法应用于 Tilemap 中也可以很大改善 像素Tile 抖动问题, 但是如果仔细观察, 会发现 Tile 的边缘依然会出现抖动现象. 这是因为 Tile 的纹理是从 Tileset 纹理中截取的, Tile 与 Tile 的边界并非通过直接插值得到, 可以看到这些边缘依然是锐利的. 我目前没看到有比较好处理这个问题的办法, 不过通过适当增加相机移动速度倒可以一定程度掩盖这个问题. 关于这个问题如果有后续进展, 会继续更新到这里.</p><h1 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h1><h2 id="1-关于双线性插值-Bilinear-Interpolation"><a href="#1-关于双线性插值-Bilinear-Interpolation" class="headerlink" title="1 关于双线性插值(Bilinear Interpolation)"></a>1 关于双线性插值(Bilinear Interpolation)</h2><p>让我们先从基础的一维线性插值开始. 线性插值简单来说, 是用直线连接两个参考点, 采样点居于参考点之间, 根据其与两个参考点距离的比例关系作为权重, 对两个参考值线性加权后计算得到采样值. 如下图,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_f1.png" style="zoom:50%;" /></p><p>其中, $x_0$与$x_1$为两个参考点, $x$为插值采样点, 目的是求得插值$f(x)$. 利用简单的相似三角形即可求得.</p><script type="math/tex; mode=display">\frac{x-x_0}{x_1-x_0} = \frac{f(x)-f(x_0)}{f(x_1)-f(x_0)}</script><script type="math/tex; mode=display">\begin{aligned}f(x)&=\frac{x-x_0}{x_1-x_0}\big[f(x_1) - f(x_0)\big]+f(x_0) \\    &=\frac{x_1-x}{x_1-x_0}f(x_0)+\frac{x-x_0}{x_1-x_0}f(x_1)\end{aligned}</script><p>如果把 $x$ 归一化到 $[0, 1]$, $x’=\frac{x-x_0}{x_1-x_0}$则有,</p><script type="math/tex; mode=display">f(x')=(1-x')f(x_0)+x'f(x_1)</script><p>线性插值中, $f(x_0)$与$f(x_1)$的权值即$x$分别到$x_0$与到$x_1$距离的反比. 也就是距离越远的参考点, 其权重就越小, 非常符合插值的直觉.</p><p>要把线性插值应用到二维平面会稍微麻烦些, 不过这些麻烦只是形式上的. 具体来说, 可以分维度依次进行线性插值, 如下图,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_f2.png" style="zoom:50%;" /></p><p>A, B, C, D为二维平面四个参考点, 其构成的四边形为轴对齐矩形. 先选择一个方向进行两次线性插值. 这里选择x还是y轴方向不影响最终结果, 如上图为先进行x轴方向插值, 得到$f(x, y_0)$与$f(x,y_1)$. 直接套用线性插值的结果有,</p><script type="math/tex; mode=display">f(x,y_0)=\frac{x_1-x}{x_1-x_0}f(x_0, y_0)+\frac{x-x_0}{x_1-x_0}f(x_1, y_0)</script><script type="math/tex; mode=display">f(x,y_1)=\frac{x_1-x}{x_1-x_0}f(x_0,y_1)+\frac{x-x_0}{x_1-x_0}f(x_1, y_1)</script><p>接着进行y方向线性插值,</p><script type="math/tex; mode=display">f(x, y) = \frac{y_1-y}{y_1-y_0}f(x, y_0)+\frac{y-y_0}{y_1-y_0}f(x,y_1)</script><p>代入前面的插值结果, 化简得,</p><script type="math/tex; mode=display">\begin{aligned}f(x,y)&=\frac{x_1-x}{x_1-x_0}\frac{y_1-y}{y_1-y_0}f(x_0,y_0)+\frac{x_1-x}{x_1-x_0}\frac{y-y_0}{y_1-y_0}f(x_0,y_1) \\&+\frac{x-x_0}{x_1-x_0}\frac{y_1-y}{y_1-y_0}f(x_1,y_0)+\frac{x-x_0}{x_1-x_0}\frac{y-y_0}{y_1-y_0}f(x_1,y_1)\end{aligned}</script><p>若对x, y进行归一化, 令$x’=\frac{x-x_0}{x_1-x_0}$, $y’=\frac{y-y_0}{y_1-y_0}$,</p><script type="math/tex; mode=display">f(x', y') = (1-x')(1-y')f(x_0,y_0)+(1-x')y'f(x_0, y_1)+x'(1-y')f(x_1,y_0)+x'y'f(x_1, y_1)</script><p>与线性插值时相同, 这里各参考值线性加权的权重比例按面积划分. 采样点越靠近参考点, 此时<strong>对角的参考点</strong>与采样点围成的面积越大, 即权重越大. 各面积区域作为权重与对应参考点关系如下图.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/pixel_aa_f3.png" style="zoom:50%;" /></p><p>同样原理, 可以类推<a href="https://en.wikipedia.org/wiki/Trilinear_interpolation">三线性插值(Trilinear interpolation)</a>, 其分别在三个维度方向上依次进行线性插值. 并且也具有体积比权重性质.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;米娜桑, 我又回来更新了…. 嗯, 年更博主的地位保住了…&lt;/p&gt;
&lt;p&gt;其实我在准备一个更长~篇的更新, 千真万确, 不过目前可公开的情报还不多. 那么在我们仍未知道的更新到来之前, 这次, 难得正经地, 来简单聊聊 Pixel Art 的缩放及抗锯齿问题吧…&lt;/p&gt;</summary>
    
    
    
    <category term="Lab" scheme="https://granvallen.github.io/categories/Lab/"/>
    
    <category term="计算机图形学" scheme="https://granvallen.github.io/categories/Lab/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
    
    <category term="游戏开发" scheme="https://granvallen.github.io/categories/Lab/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="计算机图形学" scheme="https://granvallen.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6/"/>
    
    <category term="Unity" scheme="https://granvallen.github.io/tags/Unity/"/>
    
    <category term="PixelArt" scheme="https://granvallen.github.io/tags/PixelArt/"/>
    
    <category term="游戏开发" scheme="https://granvallen.github.io/tags/%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>如何用Python开发Unity</title>
    <link href="https://granvallen.github.io/unitypython/"/>
    <id>https://granvallen.github.io/unitypython/</id>
    <published>2023-05-07T12:11:00.000Z</published>
    <updated>2023-05-07T12:43:23.332Z</updated>
    
    <content type="html"><![CDATA[<p>话说, 为什么想不开要用Python开发Unity, 快跑, 全是坑. 也许……你已经在坑里了?</p><p>这篇文章粗浅地介绍用Python开发Unity的一种可行的姿势, 以及可能会踩的一些坑.<br><span id="more"></span></p><p>背景是最近在尝试用Unity开发独立项目, 说实话, 一开始打算使用Python与Unity的组合确实没有料想到会有这么多问题, 毕竟一边是当前最主流的商业游戏引擎, 而另一边则是当前最主流的编程语言, 官方支持暂且不提, 各种应用一定很丰富吧. 但当我开始检索不得不认清事实, 相关资料少得可怜, 这个技术路线更多被应用于科研, 这倒也并非不可理解.</p><p>如前所述, 本文只是粗浅介绍一种用Python开发Unity的姿势, 除自己折腾外实则并不推荐. 显然就商业项目而言, Lua+Unity才是各种成本最小的选择. 简单一提, 因为热更新的需要, 通常把需要频繁更新的逻辑用解释型语言来编码. 除此之外, 在Unity中引入动态语言的好处还有不少, 比如相比原生C#有更高编码效率, 更容易上手, 改写动态语言编码的逻辑也不会触发Unity耗时的重编译. 至于为什么选择用Python更多是个人喜好, Python原生支持面向对象, 基础设施较为完备, 语法优雅, 美中不足是牺牲一些运行效率.</p><p>本文并不会涉及如何构建基于Unity的Python框架, 及优化Unity和Python调用性能等高级话题, 或许等到我有了些许心得的时候再聊.</p><p>当我们决定用Python来开发Unity时, 我们在想什么? 首先, 这从理论上可行性如何? 因为目前主流已有不少使用Lua开发Unity的方案, 所以应该可行, 需要了解下Python相关的支持如何. 也许可以试试Unity官方Python插件?</p><h1 id="Python-Scripting插件"><a href="#Python-Scripting插件" class="headerlink" title="Python Scripting插件"></a>Python Scripting插件</h1><p>没错, 实际上Unity官方确实提供了一个<a href="https://docs.unity3d.com/Packages/com.unity.scripting.python@7.0/manual/index.html">Python插件</a>, 利用该插件可以在Unity编辑器中执行Python脚本, 具体用法可参考官方文档与插件包内的例程. 安装可以参考这个讨论<a href="https://forum.unity.com/threads/python-for-unity-install.1112248/">Python for Unity Install - Unity Forum</a>. 截至目前, 这个插件竟然已迭代到了第七版(然而文档依然很简略), 安装插件会自带一个Python解释器. </p><p>然而经过简短的使用后发现一些问题, 首先这个Python解释器似乎是与Unity编辑器进程绑定的, 每次进入Play模式依然保留有上次运行的状态, 包括导入的module等, 更关键的是在代码中使用了Python插件相关接口Unity会无法build, 也就是无法利用这个插件开发独立的Unity应用. 也许是我看漏了, 我好像没有注意到官方文档有相关的说明, 直到我看到了这个讨论<a href="https://forum.unity.com/threads/python-for-unity-editor-only.914843/">Python for Unity Editor Only - Unity Forum</a>.</p><p>所以, 如果只是拿Unity和Python做个编辑器应用这个插件还是绰绰有余的, 但想更进一步就得自己动手了. 这个讨论也启示了一种使用Python开发Unity独立应用的方法, 最新的回帖坛友分享了他解决方案的一个例子<a href="https://forum.unity.com/threads/python-for-unity.1313007/">Showcase - Python for Unity - Unity Forum</a>.</p><p>启示, Python Scripting插件底层接口是依赖于一个叫<a href="http://pythonnet.github.io/">Python.NET</a>的开源项目, 借助这个开源项目能够实现Python与C#的相互调用, 这正是我们所需要的.</p><h1 id="Python-NET"><a href="#Python-NET" class="headerlink" title="Python.NET"></a>Python.NET</h1><p>因为Unity本身是基于.Net平台的, 可以把思路转换成Python在.Net环境下开发. 检索了一下, 目前比较主流有两种方案. 其一是前面提到的Python.NET, 另一个是同样开源的<a href="https://github.com/IronLanguages/ironpython3">IronPython</a>项目. IronPython是C#的Python实现, 有更好的C#支持, 而Python.Net支持的是CPython实现, Python这边的支持更好, 接口调用方式来看差异倒不是很大, 都值得尝试. 这里选择更新稍勤奋的Python.NET进行测试.</p><p>为了便于测试, 首先创建一个的Unity目录结构, 只列举必要的文件夹.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">Assets/</span><br><span class="line">    |- Plugins/               第三方插件dll</span><br><span class="line">    |    |- PythonRuntime/</span><br><span class="line">    |</span><br><span class="line">    |- PythonScripts/         Python代码</span><br><span class="line">    |    |- main.py</span><br><span class="line">    |    </span><br><span class="line">    |- Scripts/               C<span class="comment">#代码</span></span><br><span class="line">    |    |- Manager/</span><br><span class="line">    |    |    |- PythonMgr.cs</span><br><span class="line">    |    |- GameLauncher.cs</span><br><span class="line">    |</span><br><span class="line">    |- SteamingAssets/</span><br><span class="line">    |    |- Python/</span><br><span class="line">    |</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p>然后需要下载一个Python实现及Python.NET项目编译输出的dll, 分别放在Python与PythonRuntime文件夹内.</p><p>以Python3.10为例, 从<a href="https://www.python.org/downloads/windows/">Python Releases for Windows | Python.org</a>下载3.10版本的Windows embeddable package压缩包, 解压后放到Python目录即可, 这个package的python貌似是不带pip, 可能需要手动安装.</p><p>至于Python.NET编译的dll需要把项目源码从Github上下载下来, VS打开并编译一下Runtime这个子项目, 拿到Python.Runtime.dll并放入PythonRuntime目录. </p><p>Python.NET源码版本的选择这里有个问题, 即前面提到的, Unity编辑器进入Play模式运行Python脚本后, 再退出Play模式, Python运行时不会关闭, 依然会保留之前运行的状态(重新初始化也不会生效), 这自然对开发过程带来很大的不便. 而且可能因为Python侧引用失效, 存在第二次运行Play模式时崩溃的隐患. Python.NET在早前的一个版本中加入了SoftShutdown模式可用来处理上述情况, 但在最近的几次更新中把上述功能移除了(尽管不会再崩溃). 具体细节可参考以下资料</p><ul><li><a href="https://github.com/pythonnet/pythonnet/issues/957">Python can’t reliably re-initialize; new proposal for Unity’s domain reload · Issue #957 · pythonnet/pythonnet · GitHub</a></li><li><a href="https://github.com/pythonnet/pythonnet/pull/958">Add soft shutdown by amos402</a></li><li><a href="https://docs.google.com/document/d/1a9OLsdKHXJ6MxHjo0WlVRcfjAiaP5hONNKUKKH6Tr-o/edit">Python for NET domain reload</a></li><li><a href="https://github.com/pythonnet/pythonnet/pull/1638">Removed ShutdownMode. Now always behaves like original Reload by lostmsu · Pull Request #1638</a></li></ul><p>如果为了使用soft shutdown带来的便利, 我们可以暂时使用<a href="https://github.com/pythonnet/pythonnet/releases/tag/v3.0.0-a2">pythonnet 3.0.0 alpha-2版本</a>来编译.<br>关于Python.NET开发相关资料可参考:</p><ul><li><a href="http://pythonnet.github.io/">Python.NET</a></li><li><a href="https://github.com/pythonnet/pythonnet/wiki">Home · pythonnet/pythonnet Wiki · GitHub</a></li><li><a href="https://pythonnet.github.io/pythonnet/">Python.NET documentation</a></li></ul><h1 id="Python与C-互相调用"><a href="#Python与C-互相调用" class="headerlink" title="Python与C#互相调用"></a>Python与C#互相调用</h1><p>总算看到点希望了, 简单说下测试环境. Unity空场景中创建一个Game空物体, 给Game挂上一个GameLauncher.cs作为应用的启动脚本.<br><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// GameLauncher.cs</span></span><br><span class="line"><span class="keyword">using</span> UnityEngine;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">GameLauncher</span> : <span class="title">MonoSingleton</span>&lt;<span class="title">GameLauncher</span>&gt;</span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">Awake</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        PythonMgr.instance.InitEnv();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">Start</span>()</span> </span><br><span class="line">    &#123;</span><br><span class="line">        Debug.Log(<span class="string">&quot;[GameLauncher] EinS Start!&quot;</span>);</span><br><span class="line">        PythonMgr.instance.StartGame();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><p>PythonMgr类用于管理Python运行时, 内容如下</p><figure class="highlight c#"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// PythonMgr.cs</span></span><br><span class="line"><span class="keyword">using</span> UnityEngine;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> Python.Runtime;</span><br><span class="line"><span class="keyword">using</span> System.IO;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">PythonMgr</span> : <span class="title">Singleton</span>&lt;<span class="title">PythonMgr</span>&gt;</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> python_code_path;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">InitEnv</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        Runtime.PythonDLL = Path.Combine(Application.streamingAssetsPath, <span class="string">&quot;Python\\python310.dll&quot;</span>);</span><br><span class="line">        PythonEngine.Initialize(mode: ShutdownMode.Soft);</span><br><span class="line">        Debug.Log(<span class="string">&quot;[PythonMgr] python interpreter version: &quot;</span> + PythonEngine.Version);</span><br><span class="line"></span><br><span class="line">        python_code_path = Path.Combine(Application.dataPath, <span class="string">&quot;PythonScripts/&quot;</span>);</span><br><span class="line">        Debug.Log(<span class="string">&quot;[PythonMgr] python_path: &quot;</span> + python_code_path);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">StartGame</span>()</span></span><br><span class="line">    &#123;</span><br><span class="line">        Debug.Log(<span class="string">&quot;[PythonMgr] StartGame&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">using</span> (Py.GIL())</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="built_in">dynamic</span> sys = Py.Import(<span class="string">&quot;sys&quot;</span>);</span><br><span class="line">            sys.path.append(python_code_path);</span><br><span class="line"></span><br><span class="line">            <span class="built_in">dynamic</span> main = Py.Import(<span class="string">&quot;main&quot;</span>);</span><br><span class="line">            <span class="built_in">dynamic</span> res = main.StartGame();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        PythonEngine.Shutdown();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了通过Python.NET调用Python运行时需要用到Python.Runtime命名空间. 根据Python.Net的文档, 初始化PythonEngine前需要设置Runtime.PythonDLL到对应Python版本dll的路径, 或者设置PYTHONNET_PYDLL环境变量. 接着使用ShutdownMode.Soft进行初始化. StartGame中运行Python代码前需使用using(Py.GIL())获取到Python的GIL锁. 然后就可以使用Py.Import导入模块, 并调用模块方法了. 这里我们把PythonScripts路径加入sys.path, 然后加载Python脚本启动模块main, 并调用其StartGame方法.</p><p>注意要让untiy支持dynamic关键字, 需要在Unity project settings/player 中把api compatibility level 调整为 .Net Framework.</p><p>来到Python一侧, main.py内容如下<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># main.py</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># -*- coding: UTF-8 -*-</span></span><br><span class="line"><span class="keyword">from</span> clr <span class="keyword">import</span> GameLauncher</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> UnityEngine <span class="keyword">as</span> ue</span><br><span class="line"><span class="keyword">from</span> System.IO <span class="keyword">import</span> Path</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">StartGame</span>():</span><br><span class="line">    ue.Debug.Log(Path.Combine(ue.Application.dataPath, <span class="string">&quot;PythonScripts/main.py&quot;</span>))</span><br><span class="line">    </span><br><span class="line">    game = ue.GameObject.Find(<span class="string">&quot;Game&quot;</span>)</span><br><span class="line">    gameluncher = game.GetComponent(GameLauncher)</span><br><span class="line">    ue.Debug.Log(gameluncher.name)</span><br></pre></td></tr></table></figure></p><p>通过clr模块, 我们可以把自定义的类类型导入进来使用, 如果这些自定义类是在命名空间中, 则需要先导入命名空间. </p><p>关于这个话题的一些参考:</p><ul><li><a href="https://forum.unity.com/threads/python-for-unity-doesnt-support-custom-class.969111/">Python for Unity doesn’t support Custom class - Unity Forum</a></li><li><a href="https://github.com/pythonnet/pythonnet/wiki/How-to-call-a-dynamic-library">How to call a dynamic library · pythonnet/pythonnet Wiki · GitHub</a></li></ul><h1 id="开发与调试环境"><a href="#开发与调试环境" class="headerlink" title="开发与调试环境"></a>开发与调试环境</h1><p>最后再简单聊一下另一个比较关键的问题, 开发与调试环境. 本来可以期待着可以All In One, 使用VS code作为开发环境, 但是Unity官方最近放弃了继续支持VS Code的插件, 具体见<a href="https://forum.unity.com/threads/update-on-the-visual-studio-code-package.1302621/">Official - Update on the Visual Studio Code package - Unity Forum</a>. 所以对于C#的调试还是使用Visual Studio吧. 至于Python依然可以使用VS Code + Python插件来调试.</p><p>使用VS调试Unity的C#代码不用说是容易的, 而使用VS Code调试Python可以参考Python.NET的建议, 参考<a href="https://github.com/pythonnet/pythonnet/wiki/Various-debugging-scenarios-of-embedded-CPython">Various debugging scenarios of embedded CPython · pythonnet/pythonnet Wiki · GitHub</a>, 我们可以使用最简单的第三种远程调试方式, 其中<a href="https://github.com/microsoft/ptvsd">ptvsd</a>已被微软所废弃, 作为替代可以使用<a href="https://github.com/microsoft/debugpy/">debugpy</a>库. 原理就是VS Code开启一个调试服务, 执行的Python脚本使用debugpy.connect进行连接调试.</p><p>VS Code新建一个Python调试配置, 内容如下<br><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// launch.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.2.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;configurations&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Python debug&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;python&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;request&quot;</span><span class="punctuation">:</span> <span class="string">&quot;attach&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;justMyCode&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;listen&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">                <span class="attr">&quot;host&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.0.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;port&quot;</span><span class="punctuation">:</span> <span class="number">5678</span><span class="punctuation">,</span></span><br><span class="line">            <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;pathMappings&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">                <span class="punctuation">&#123;</span></span><br><span class="line">                    <span class="attr">&quot;localRoot&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;workspaceFolder&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">                    <span class="attr">&quot;remoteRoot&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;workspaceFolder&#125;&quot;</span></span><br><span class="line">                <span class="punctuation">&#125;</span></span><br><span class="line">            <span class="punctuation">]</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></p><p>然后在main.py中增加<br><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> debugpy</span><br><span class="line">debugpy.connect((<span class="string">&quot;localhost&quot;</span>, <span class="number">5678</span>))</span><br></pre></td></tr></table></figure></p><p>完成后VS Code先开启调试, Unity进入Play模式运行python脚本即可, 与调试普通Python脚本相同.</p><p>Enjoy.<br>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;话说, 为什么想不开要用Python开发Unity, 快跑, 全是坑. 也许……你已经在坑里了?&lt;/p&gt;
&lt;p&gt;这篇文章粗浅地介绍用Python开发Unity的一种可行的姿势, 以及可能会踩的一些坑.&lt;br&gt;</summary>
    
    
    
    <category term="Coding" scheme="https://granvallen.github.io/categories/Coding/"/>
    
    <category term="Unity" scheme="https://granvallen.github.io/categories/Coding/Unity/"/>
    
    <category term="Python" scheme="https://granvallen.github.io/categories/Coding/Unity/Python/"/>
    
    
    <category term="Unity" scheme="https://granvallen.github.io/tags/Unity/"/>
    
    <category term="Python" scheme="https://granvallen.github.io/tags/Python/"/>
    
    <category term="C#" scheme="https://granvallen.github.io/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>聊聊我对P5R的感受与观察</title>
    <link href="https://granvallen.github.io/persona5/"/>
    <id>https://granvallen.github.io/persona5/</id>
    <published>2021-02-17T06:33:00.000Z</published>
    <updated>2024-07-22T13:02:25.000Z</updated>
    
    <content type="html"><![CDATA[<p>我又回来水文章了…过去的一年于我而言也经历了很多事, 疫情之外, 其中像是虾米关闭一事对我影响还挺大, 毕竟是用了8年的老app了. 在我这称得上情怀的应用不多, 但虾米确实能算上一个, 关于这个话题也有一些我自己的观察, 这里就先按下不表. 说回正题, 就3天前Clear了女神异闻录5皇家版(P5R), 个人非常喜欢, 所以<del>怀着激动的心情来吹一吹</del>想分享一下我喜欢P5的一些地方(我看网上的舆论相较P5, 对于P5R可能争议会比较大, 这里并不会严格地去区分两者, 或者说都指的是P5R)以及我游玩过程中的一些思考(毕竟干聊P5也没什么意思). 由于我确实各方面经验有限(比如P系列除此之外只几年前玩过P4G还没通, 正传真女神转生系列更是没碰过), 有些内容大概会聊得比较浅, 只是点到为止, 如果之后有更深的体会再做补充. 总之, 本文还是会有比较强烈的个人喜好, 大部分属于想到什么写什么, 当然我其实也不满足于此, 会尽可能尝试从鉴赏的角度去看P5. 动手之前根本想象不到本文会写成什么样, 只能先把千头万绪一条条理出来, 无论如何, 我想这样也算表达对P5喜欢的方式之一吧.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/p5_2.jpg" style="zoom:50%" alt="" /></p><span id="more"></span><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=454224842&auto=0&height=66"></iframe><h1 id="P5华丽不失精致的外壳"><a href="#P5华丽不失精致的外壳" class="headerlink" title="P5华丽不失精致的外壳"></a>P5华丽不失精致的外壳</h1><p>当代电子游戏作为一种高度工业化的商品, 可以从很多视角去说, 像是美术, 配乐, 故事设定等. 初见P5, 上述三个方面是最直接能感受到制作组用心的地方, P5的这个组合更是有意思, 至少已经勾起了我浓厚的兴趣继续玩下去.</p><h2 id="美术"><a href="#美术" class="headerlink" title="美术"></a>美术</h2><p>P5的美术绝对值得单独拿出来提的. P5在色彩上为了和怪盗的设定契合大量使用了红黑的配色, 不得不说非常符合怪盗即优雅又神秘的气质. 在菜单画面的设计上也用了非常多的大色块, 使得很多画面非常鲜艳. 此外, 也许是为了让玩家能在眩目的菜单中抓到重点, 也用了很多动态的元素, 比如背景是大色块的缓慢变化, 而所选的选项则用不规则抖动的色块所包裹, 这也是我经验中很少看到的. 我在游戏里第一次看到约瑟交换花朵的菜单时被彻底惊艳到了.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/p5_1.jpg" style="zoom:50%" alt="" /></p><p>P5除了在菜单画面中用了非常多的动态元素, 另一个特别细节的设计是界面间的切换使用了大量转场动画(切换动画). 如三角键进入的菜单切换是joker各种连续改变姿势的动画(LoadData菜单还有几率刷出joker出糗的动画, 太细节了). 在游戏的战斗界面中这种转场更是无处不在, 选择一种操作就进行一次镜头切换, 人物的姿态也对应发生变化. 再比如制作潜入道具时把菜单像纸一样揉成团和展开的动画. 这些转场让菜单的操作也变得如此炫酷, 同时提升了流畅度. 看到网上调侃, P5是第一个用转场动画强行延长游戏时间的游戏, 当然, 我不清楚做这些动画是否会带来过高的成本. 上述提到的这些设计在P4中是几乎没有的.</p><p>聊到P5的人设反而没什么可聊的, P4P5都是中规中矩的日系人设. 反而人格面具的形象设计可能是P系列风格化的另外一点, 因为出乎意料地非常不符合日式审美. 精神象征人格面具这东西在P系列中也是玩法的核心之一(没错, 就是jojo的替身), 每个人格面具设计的出发点是世界各国文化与传说中的一些人神魔兽等, 还是相当考究的, 只是有些我觉得设计得非常放飞自我, 一些魔物的设计真是有点让我联想到剑风蚀之刻里的怪物(当然也没那么夸张, 剑风里的怪物设计真是狂掉san值).</p><p>游戏的剧情对话界面则和大部分日式游戏没有太大区别, 对话框用的抖动气泡, 比较特别的是为了增强表现力常常在对话中插入人物面部特写, 我个人是很喜欢这些漫画式的表现方式.</p><h2 id="配乐"><a href="#配乐" class="headerlink" title="配乐"></a>配乐</h2><p>除了画面之外, 另一个能直接让人喜欢上的就是P系列祖传的目黑的配乐了. 最开始吸引我接触女神异闻录系列就是P4G的配乐, 那时候就觉得P4G的配乐真是时髦度爆表, P4G的背景明明是乡下, 听着逛街曲《Heartbeat,Heartbreak》楞是感觉来到了灯红酒绿的大都会. 由于配乐而入了某个坑在我这也挺常见的, 奥日与大逆转也差不多是这种情况, 未来大概也会去试试尼尔吧.</p><p>单从风格上说, 目黑将司拿手的我大概知道是爵士和蓝调, 虽然在P4中略显不搭, 只能说配乐风格太强烈, 但到了P5里, 却和怪盗的主题意外地很相称. 说来也非常有意思, 爵士配黑帮怪盗其实很早就有这种印象, 但一直不知道缘由. 看网上有介绍说在jazz最为盛行的20世纪20年代, 当时出了几部比较有影响力的黑帮电影, 像是美国往事, 都大量采用了爵士配乐, 所以这种印象被保留下来, 并且被后世的作品不断强化. 就我自己接触到的像黑帮动画《永生之酒》与怪盗《鲁邦三世》也都大量采用了爵士配乐, 前者的《GUN’S &amp; ROSES》和后者大野雄二的《Theme From Lupin 3》可说是经典中的经典了. 但是也能明显感觉到, 虽说都是爵士, 目黑的爵士与前面提到的还是不太一样, 看网上评论说应该更接近酸爵士(Acid Jazz), 或称迷幻爵士, 属于爵士和Hip-hop的融合, 其实就不太有爵士味了, P5的曲子中有一首《Life Goes On》我觉得应该是非常标准的酸爵士. 顺着这条线, 我也非常幸运地找到了这一风格喜欢的音乐人, 比如加拿大的Four80East组合, 比较有代表性的作品是《Noodle Soup》和《Drive Time》.</p><p>说回P5, 当然目黑除了一些风格强烈的配乐外, 更多是比较常见的偏”洗脑”(重复旋律)的RPG配乐, 比如几个固定场景, 战斗和迷宫探索的配乐. 日式RPG游戏时长一般都比较长, 在跑图, 探索以及一些需要刷刷刷的部分会花很多时间, 自然需要一些可长时间循环且不会让玩家烦躁的配乐, 这部分我觉得P5做得也还不错. 就我自己来说, 这部分的配乐我会更关注也更喜欢, 因为每当听到这些配乐, 在游玩过程中经历的种种场景仿佛又重现于脑海. 如网上在这些曲子下的评论都是玩家的各种回忆, 如果是一些较难关卡的配乐, 甚至听到时会产生PTSD. P5的几个迷宫曲子都还挺”洗脑”的, 双叶殿堂的《母のいた日々》, 生存游戏商店的《Layer Cake》, 新岛殿堂的《The Whims of Fate》以及丸喜殿堂的《Gentle Madman》印象都挺深. 这些曲子如果单拿出来听感觉一般, 但是在重复了几十个小时后已经牢牢地和游戏体验绑定在一起, 或许这也是配乐或者说原声音乐(OST)的魅力所在吧.</p><p>P5配乐的另一个有意思的地方在于有相当一部分配乐是有英语人声的, 事实上整个P系列都一直如此, 现在这一类反传统的配乐也是出现得越来越多了. P5的几个主要战斗曲都有人声的版本, 只在战斗的最高潮时切入, 像是《Last Surprise》和《Rivers In the Desert》. 至于为什么用英语人声, 貌似并没有什么特别的理由, 我记得在一个什么目黑的访谈中提到只是因为日本人对于英文发音有种”虽然听不懂但是很厉害的样子”这种印象?</p><p>要从乐理层面聊配乐我是完全不够格的, 好在网上也能找到一些比较专业的评价, 如音乐人Alex做过的一个系列<a href="https://www.bilibili.com/video/BV1bp4y117nC">音乐制作人谈P5名曲《Last Surprise》好在哪？</a>, <a href="https://www.youtube.com/watch?v=Z5td7jZgaYs&amp;ab_channel=AlexMoukalaMusic">Ranking Persona 5’s Palace Themes (as a Music Producer)</a>.</p><h2 id="设定"><a href="#设定" class="headerlink" title="设定"></a>设定</h2><p>抛开没接触过的P3等前几作不谈, P系列的主要设定都是与人精神世界的冒险相关, 现实中人的认知与具现化精神世界(即殿堂)中的人(或者称为阴影)相联系, 而主角们正是进入精神世界展开各种行动, 最终达到改变现实中人认知的目的. 在殿堂中, 主角们自然也是使用精神世界的代表来进行战斗, 这就是人格面具(Persona). 这一设定在每作之间大体相同, 只是在细节上略有差异. 另一个则是每作与之联系的主题设定, 在P4中主角们是以事件调查团的身份行动, 目的是让现实中的人接受真实的自己, 而在P5中主角们化身怪盗团, 通过借由偷走殿堂中的秘宝来改变现实中人的认知并使其悔改. 我确实也更喜欢P5的设定, 一是感觉靠最后一通让人幡然悔悟的嘴炮, 不如像偷秘宝这样来得简单直接(不过设定的解释会是一个瑕疵), 整个行动过程也非常帅气优雅. 二是还有什么比作品里那些满是丑恶嘴脸的大人亲口忏悔自己的罪过来得更爽快的呢.</p><h1 id="简单聊聊P5的玩法"><a href="#简单聊聊P5的玩法" class="headerlink" title="简单聊聊P5的玩法"></a>简单聊聊P5的玩法</h1><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=454231893&auto=0&height=66"></iframe><p>如前所述, P5的主要玩法可分为现实世界与精神世界的体验两部分, 在现实世界的部分主要是日常生活的体验, 像是上学, 打工, 与某个社群(coop)的伙伴加深羁绊等, 这些活动的目地除了推进剧情, 更是为了解锁在探索精神世界时非常有用的技能(主要是通过提升coop等级解锁). 所谓精神世界的探索即是探索悔改目标的殿堂(设定上欲望扭曲者会拥有殿堂), 殿堂是一个有着特定主题, 并集战斗, 探索, 解谜一体的大型迷宫, 直至最后找到殿堂的秘宝, 战胜殿堂主盗走秘宝, 推动剧情进入下一章.</p><p>简单说一说其中我关注的一些设计. 我觉得P系列和其他jrpg很不一样的一个地方是用了替身战士即人格面具的设计, 也就是P系列的战斗不仅要关注角色的成长, 更要关注人格面具的培养, 这是两套相互联系却又很不同的系统. 在使用人格面具的战斗中, 敌人攻击的是玩家而不是人格面具, 因此在日常生活中的锻炼提升的是角色的5维属性, 但玩家又能获得所装备人格面具的抗性与弱点, 也可以使用人格面具进行魔法物理攻击, 反而角色则很少进行攻击行动(平A). 那么如何培养强大的人格面具呢? 只是通过传统的战斗是不行的, 虽然我不知道战斗的经验值是如何分配的, 就我自己的体验来看, 战斗也主要是提升角色等级, 而让人格面具升级提升5维属性是比较困难的, 一般需要在装备同一个人格面具的情况下进行大量战斗, 这是不现实的, 因为战斗中需要利用不同人格面具的抗性与技能, 总体玩下来在战斗中让主角人格面具升级的次数屈指可数. 当然这种设计也是有意为之的, 不然这么大本人格面具图鉴就白设计了. 游戏机制给出的引导是提升现实生活中coop的等级, 这样可以合成出5维属性更高, 抗性也更好的面具, 属性通常都会大大高于已经拥有的人格面具. 这种设计在我初次接触P系列还蛮不适应的, 因为我此前在玩其他的时候总想着主角可以不断地成长, 一些技能的搭配越来越完善, 逐渐形成一种套路化的战斗流程, 到了P系列这, 每次为了提升自己的实力, 都需要对已有的人格面具重新洗牌, 重新搭配技能, 才能确保自己有全属性的打击面. 所以到中期的时候我基本就放弃这种想法了, 只盯几个特殊的打击面, 其他有什么用什么. 不过到了后期…嗯…后期的人格面具培养是另外一种玩法了.</p><p>P系列另一个有特点的系统就是coop社群系统, 虽然P4也有, 但P5中才把这个系统做得更具价值. 要说明这点还是要先回过头去说P5的殿堂, 我觉得P5的殿堂体验真是太棒了.</p><p>P5中的殿堂迷宫总算是有迷宫的样子了, 除了一些常规的迷宫收集要素, 密道, 近道等设计, 每个迷宫都有别出心裁的主题, 并且围绕这个主题设计了一些剧情和谜题. 不过就解谜来说P5的设计还是相当简单, 都是一些非常经典谜题, 但聊胜于无. 而反面教材我觉得就可以拿P4举例, 体验基本上就是P5中印象空间的简化版. P5的迷宫相比P4有另外两个我觉得非常人性化的改进, 可能是P4的失望在先, 当我看到有安全小屋和加入隐匿移动时不由拍手叫好. 安全小屋让探索迷宫时去某个地方可以更方便地快速移动, 利用后者在迷宫拐角也不用一直先盯着小地图红点, 等着阴影转身时慌张地冲过去, 开房间门时也不用因为突然出现阴影而吓个半死. P5的迷宫还有一点也非常有意思, 隐藏, 跳跃, 使用钩爪都使用的是提示加弹出操作按钮的设计, 而非可自由地做这些动作(固定场景才可执行这些操作), 这不知道是成本上的限制, 还是觉得迷宫复杂容易让玩家摸不着头脑, 总之体验还算流畅, 游戏中的机制第三只眼可高亮这些场景. P5迷宫中的大头除了探索自然就是战斗, P系列的战斗由于人格面具加入还蛮有意思的, P5又通过增加属性和新的战斗要素(如枪械), 使回合制的战斗过程也充满了乐趣. 我个人最喜欢换手这个设计, 当打击弱点致使敌人倒地时会触发One more, 即可再行动一次, 此时可执行换手即把行动权让给队友, 换手后队友获得回血回蓝攻击力buff, 通常队友会再次击中敌人弱点(已倒地敌人不行), 从而换手的过程得到连锁, 我感觉这种不断换手打出高伤害的设计真是燃爆了, 当然特效也非常燃, 另外被换手的队员并不消耗他自己本回合的行动权. P5双人合体技Show time也依旧保持了无厘头搞笑的风格.</p><p>说回coop系统, 在P5中把战斗和探索殿堂各种方便的技能作为coop升级的奖励, 因此随着游戏的进行, 战斗与迷宫探索的丰富和便利程度也在上升, 如龙司coop后期可获得的对于等级低很多的阴影冲刺可瞬杀, 再比如东乡coop最后解锁的东乡系统, 可让伙伴自行交换上场也非常实用. P5战斗时通常是固定的4人上场, 前期只有joker行动时可以更换伙伴上场, 像这种后期体验的改善还有不少. P5游玩体验做到了即使到了中期依然不断地有新的战斗和探索的要素解锁, 从而能一直保持新鲜感. 然而这个设计有一个微妙的地方. 在P5中coop的提升意味着和特定伙伴羁绊的加深, 达到一定程度可发展为特殊关系, 没错, P5是有简单的恋人系统的(要不怎么说不少人就是拿P5当Galgame玩的). 但是即使和某人成为恋人关系后, 由于游戏机制, 为了提升coop解锁技能, 玩家依然需要和其他角色单独约会, 参与游戏设计好的逛街看电影等等. 当然, 在游戏中(或者任何其他文艺作品中)带入现实考量是否有必要这一话题也一直是存在争议的, 这一话题之后聊到jrpg时可能仍然会提起. 至于P5在这点上倒也没有做得特别过分就是了.</p><p>关于P5的战斗最后再提一点, 就像攻壳的聂俊(说好的P5鉴赏视频呢?)在<a href="https://www.bilibili.com/video/BV1yE411376q">P5S鉴赏</a>中提到的, 击中弱点(Weak)倒地触发One more的设计实际上降低了战斗的容错率, 使得战斗更加惊险刺激. 利用得当, 玩家可无伤解决战斗, 而一旦被敌人击中弱点也会被连续打击致使我方损失惨重. 不过由于这个机制过于强大, Boss战的设计就需要一些特别考虑. 我记得之前的感受就是打打小怪的时候各种利用弱点, 而到了Boss就特没劲, 为了突出难度通常就是全属性抗性, 只能硬着头皮打. 因此可以看到P5中的有些Boss有多个部分的设计, 各个部分抗性与弱点各不相同, 像是斑目Boss的设计. 另外一些Boss的设计就不是战斗这么简单了, 比如新岛Boss和双叶殿堂的Boss, 加入了新的互动要素和机制, 比如能够改变自身的抗性等.</p><p>顺便一说, P系列魔法的名字一直念起来很怪, 什么玛哈, 米吉多, 其实是有所考究的, 可参考这篇文章: <a href="https://www.gcores.com/articles/19888">闲聊女神转生及Persona系列技能命名方式</a></p><p>人格面具的合成是我到后期才发掘的乐趣, 本身还是达成某一种套路的成就感, 这种感觉类似于培养一支宝可梦对战的队伍. 人格面具也有特性与技能的搭配, 特性只能通过合成遗传, 技能可以通过遗传, 技能卡或者人格面具绞刑进行组合搭配, 5维可以通过人格面具的升级或监禁焚香培养. 各个人格面具的合成本身遵循特定的规律, 因此最后要合成并培养一个人格面具, 需要对整个流程进行计算和设计, 细节就不多说了, 我甚至在网上看到有用Dijkstra算法优化合成路径的<a href="https://www.bilibili.com/read/cv6565204/?from=readlist">文章</a>. 我自己也花了点时间合成了几个比较知名的p(网上一般把人格面具简称为p, xxp即是某些做好技能抗性搭配, 专门用于某种用途的面具), 比如物攻p易经, 即死p爱丽丝, 开场p湿婆, 魔法p兰达等. 不得不说P5R中的公开处刑机制(联机使用网络上的人格面具进行合成)大大降低了玩家自己合成一个理想面具的难度, 主要是很多吸收的抗性都可以很方便地获得. 一旦合成了这些最终人格面具, 战斗就变成了另一个画风, 不再需要小心翼翼地试探敌人弱点, 因为大多数时候都是一回合结束战斗, 不过用自己辛苦合成出来的面具战斗本身就是非常愉悦的.</p><p>最后再说说对P系列对日常生活模拟的观察(不一定是这么回事), 看似琐碎枯燥的日常生活也是P系列最具特色的玩法之一. 现代很多游戏的简化设计利用了玩家的生活经验和想象力, 这在P系列中也已经随处可见. 比较明显的几个例子, 比如P5中对看DVD与玩游戏的模拟, 镜头是对着joker看不到电视画面的, 只能听到散乱的剧情对话配音或者8bit游戏音乐. 再比游戏中角色说话不是全语音的, 而是只在适当的时间点, 比如表达惊讶, 说话转折时才有几个语气和单词的配音, 我记得早时候如果游戏不用配音还想说话有声音就一般用机械音代替, 像是逆转裁判这一类. 其他游戏如塞尔达旷野之息也是同样处理, 只有音调的配音. 另外比如动森里很多生活中有的道具都可简单的交互, 雷电做客天地无用播客聊动森时提到了他最爱的咖啡机, 互动就只有咕咚一下, 但就是能让人想象出使用咖啡机的场景. 我个人是能接受也挺喜欢这种简化的表现的, 制作也确实省下了成本.</p><p>关于玩家看不到的场景设计细节, 看到一个好玩的视频: <a href="https://www.bilibili.com/video/BV1ub41177nj">P5视角外的世界</a></p><h1 id="是时候聊聊P5的故事和表达了"><a href="#是时候聊聊P5的故事和表达了" class="headerlink" title="是时候聊聊P5的故事和表达了"></a>是时候聊聊P5的故事和表达了</h1><p>依然是剧透预警, 但这里不会聊太细节的剧情.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.<br>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=454231902&auto=0&height=66"></iframe><p>P5充满欢乐的日常故事和P4一样, 已经处理得足够好不必多谈, 只简单说说P5R主线故事的表达(P5R与P5剧情上存在差异, 这也是争议点之一). P5R的故事作为一个重故事的jrpg来讲已经很不错了, 相信是不会让大部分jrpg玩家失望的. 设定上虽然有不严谨的地方但总体上能自圆其说(或者说那些细节对于表达无伤大雅), 故事也是有曲折有高潮以及多次的反转, 也不像某作留下了一堆坑没填就匆匆结束. 当然我觉得最重要的还是把故事想要表达的东西清楚表达出来了, 并且这个东西能得到大家的认可.</p><p>P5R的故事感受下来, 算上皇家版追加的第三学期, 我觉得是要表达或者说探讨三个问题. 击败狮童与第三学期开始可以看作两个分界点. </p><p>第一段的故事主要就是心之怪盗团的成长, 不停地寻找悔改目标(其实都是目标自己撞上来的), 结识新伙伴, 提升怪盗团的社会影响力. 在怪盗团的努力下, 社会中的恶人一次次悔改并当众忏悔自己的罪行. 然而就在这个当下, 怪盗频道上抛出了一个问题: 你认为怪盗团是正义的吗? 嗯, 这恐怕不是一个可以简单回答的问题, 可以从很多的角度看这个问题, 比如是不是应该把强制执行的悔改看作一种手段不明的私刑. 或者从上帝视角, 那些恶人也是因为威胁到了怪盗团, 怪盗团才不得不对他们进行悔改. 又或者可以不用想那么多, 这些恶人的悔改自然促进了社会的正义, 而不用在乎使用的是什么手段, 有没有得到社会大众的认可.  这些角度的讨论在故事进行中是相互角逐的, 也体现在怪频上怪盗团的支持率来回摆动. 不由得想起死亡笔记中夜神月的理想与其所作所为, 不过在那个作品里, 月的正义和对其的探讨其实已经无关紧要, 月和L的斗智才更被人们所关心. 回到P5R, 击败了最后一个恶人狮童, 故事突然画风一转, 伪神圣杯出现, 怪盗团的任务突然变成了拯救世界拯救人类. 后半段故事的转向用到了前半段的几条暗线, 回过来看, 也不能说处理得太突兀, 只是前面提到的第一个问题因为这样的转向而忽略了. 这里当然不能说因为最后怪盗团拯救了人类所以取得了正义性, 这和前面的故事是两码事不是吗. 对于第一个问题, 虽然P5R没有给出正面的回答, 但我隐约觉得是想表达对于怪盗团来说, 被认为正义不正义不重要, 怪盗团自己行动的初心才更重要.</p><p>第二段与第三段的表达比较直白. 比如第二段我觉得表达的就是经常被提起的相信人类的可能性这一点, 这就非常适合用一个人类危机的环境来表达. 虽然略显中二, 但还是很感动, 现在脑海里还能回忆起打败伪神后的播片, Mona发着光离开时感慨: 世上并不存在”真实的世界”, 世界是由每个人的感受所形成的, 这便是世界的真相, 所以世界是无限的, 就算眼前一片漆黑, 只要握着别人的手, 用羁绊连结在一起的话, 世界就不会终结, 世界就在你们的心里. 回到家后, Mona? 你怎么诈尸了, 把我感动的眼泪还给我!</p><p>第三段我也挺喜欢, 是P5R中新加的故事. 丸喜可以说是P5R里非常不同的Boss了, 他继承了之前被怪盗团打败的伪神的力量(窃取了革命的果实), 获得了能够改写现实的能力. 虽然这个能力很强大, 但经历了失去至爱之痛的丸喜决心用这个能力让人类远离痛苦. 嗯, 确实到这里有不少设定上的瑕疵, 也把人类的痛苦说得过于轻巧了, 但要抛给玩家的问题我想已经表达出来了: 我们是否应该或者说有资格剥夺他人的痛苦. 这个问题有意思的地方在于不是剥夺快乐和美好, 而是负面情绪和苦痛. 丸喜给出的选择就是作为神一样的存在, 为自己看到的每一个人改写原本现实的不幸之处, 为此丸喜也甘愿承受无法被人理解的孤独. 丸喜无疑和本作中其他Boss一样有自己的欲望, 但是其欲望本身的内容却是剥夺他人痛苦. 在游戏中丸喜不断给joker时间, 让他好好感受身边伙伴是否因为新创造的现实而过得更快乐. joker可以选择在期限之日与丸喜战斗, 否定他的所作所为, 或者也可以选择接受. 这里选择抗争也是有理由的, 我觉得关键倒不是说丸喜创造的现实就是虚伪的, 而是人类的苦痛同样可以提取出人类完善自己的碎片, 放弃即意味着设限. 约定之日, 丸喜带着忧伤且失落的神情说道: 还以为是你的话能够理解我的理想…作为一款游戏遗憾之处也在于最终只能用战斗的方式来击碎丸喜傲慢的理想. 最后的最后, 在高楼上, 失去人格面具的两人因信念的不同肉身扭打在一起…</p><p>后来一想这意涵引的不就是《美丽新世界》嘛.</p><blockquote><p>“但我不要舒适。我要的是神，是诗，是真实的危险，是自由，是善良，是罪孽。”</p></blockquote><p>P5R中明智这个角色的塑造一直很有争议. 我觉得是前半段塑造得太好了, 确实有名侦探的样子, 与joker也建立了很深的羁绊(相爱相杀), 不是说不能接受后面明智因为立场出卖怪盗团或者有自己的野心, 而是在表现上把他往变态愉快犯的方向塑造(这类日系人设最近还挺流行的).</p><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>P系列是一个以现代生活为背景的jrpg, 以我有限的jrpg经验来说感受是非常特别的. P4G是现代的乡下生活, P5R则是现代的都市生活. 并且P系列尝试去关注和描绘现代生活中人们的精神状态, 我觉得挺好的. </p><p>本来只是想把玩P5R时所有想法和感想都写下来, 没想到篇幅已经这么长了, 还是有一些没有记下来, 比如关于丸喜我不知道怎么想到了漫画犬屋敷(鬼知道当时我是怎么联想的), 总之先到这里结束吧(摊手)…</p><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=454231899&auto=0&height=66"></iframe><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;我又回来水文章了…过去的一年于我而言也经历了很多事, 疫情之外, 其中像是虾米关闭一事对我影响还挺大, 毕竟是用了8年的老app了. 在我这称得上情怀的应用不多, 但虾米确实能算上一个, 关于这个话题也有一些我自己的观察, 这里就先按下不表. 说回正题, 就3天前Clear了女神异闻录5皇家版(P5R), 个人非常喜欢, 所以&lt;del&gt;怀着激动的心情来吹一吹&lt;/del&gt;想分享一下我喜欢P5的一些地方(我看网上的舆论相较P5, 对于P5R可能争议会比较大, 这里并不会严格地去区分两者, 或者说都指的是P5R)以及我游玩过程中的一些思考(毕竟干聊P5也没什么意思). 由于我确实各方面经验有限(比如P系列除此之外只几年前玩过P4G还没通, 正传真女神转生系列更是没碰过), 有些内容大概会聊得比较浅, 只是点到为止, 如果之后有更深的体会再做补充. 总之, 本文还是会有比较强烈的个人喜好, 大部分属于想到什么写什么, 当然我其实也不满足于此, 会尽可能尝试从鉴赏的角度去看P5. 动手之前根本想象不到本文会写成什么样, 只能先把千头万绪一条条理出来, 无论如何, 我想这样也算表达对P5喜欢的方式之一吧.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/p5_2.jpg&quot; style=&quot;zoom:50%&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="ACGN" scheme="https://granvallen.github.io/categories/ACGN/"/>
    
    <category term="游戏" scheme="https://granvallen.github.io/categories/ACGN/%E6%B8%B8%E6%88%8F/"/>
    
    
    <category term="游戏" scheme="https://granvallen.github.io/tags/%E6%B8%B8%E6%88%8F/"/>
    
    <category term="P5R" scheme="https://granvallen.github.io/tags/P5R/"/>
    
  </entry>
  
  <entry>
    <title>点灯游戏与数学之美</title>
    <link href="https://granvallen.github.io/lightoutgame/"/>
    <id>https://granvallen.github.io/lightoutgame/</id>
    <published>2020-02-20T06:33:00.000Z</published>
    <updated>2020-03-11T08:10:06.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近由于疫情隔离在家, 所以正好抽空刷一刷leetcode, 于是刷到了<a href="https://leetcode.com/problems/minimum-number-of-flips-to-convert-binary-matrix-to-zero-matrix/">Minimum Number of Flips to Convert Binary Matrix to Zero Matrix</a>这题, 题中讲的是一个将二元数组中的1根据特殊规则全消减成0的游戏. 看到规则, 虽然我记不得这游戏是叫什么名字, 但我确定曾经玩过几次这个游戏, 最近的一次是我拿Lue和Love游戏引擎写点格棋游戏的时候, 在社区里看到有人分享了用Love做的这个游戏. 查了一下, 原来叫灭灯游戏(<a href="https://en.wikipedia.org/wiki/Lights_Out_%28game%29">Lights Out</a>), 在国内貌似一般叫点灯游戏? 不过也没差, 这个点灯游戏规则很简单, 相信很多人都玩过, 长方形中划分网格, 每个网格是盏灯, 且有亮与暗两种状态, 游戏中唯一的操作是选择一盏灯改变状态, 同时这盏灯上下左右的灯(如果存在的话)状态也将同时改变, 游戏初始状态时可能有几盏灯已经是点亮的, 游戏的目的是经过一系列的开关灯的操作, 使网格中的灯全部点亮. 这里有个<a href="https://www.geogebra.org/m/JexnDJpt#material/YXt3VhPn">在线版本</a>可以重温一下.</p><p>本来感觉这个点灯游戏也没什么, 感觉还是有博弈的游戏像是<a href="https://en.wikipedia.org/wiki/Dots_and_Boxes">点格棋</a>更有意思一些, 直到我看到了知乎专栏的这篇文章<a href="https://zhuanlan.zhihu.com/p/21265602">点灯游戏: 简单的游戏能否很美?</a>, 说实话确实很美, 甚至感到有些震撼. 于是我打算稍微探索一下这个游戏, 所幸文章作者写得非常认真, 相关的结论和参考都有总结, 遗憾的是没有提到结论的证明. 虽然是已经被前人研究得比较透彻的问题, 但我还是想写点自己的理解, 于是就诞生了这篇不务正业的文章…</p><span id="more"></span><h1 id="搜索算法解"><a href="#搜索算法解" class="headerlink" title="搜索算法解"></a>搜索算法解</h1><p>要用搜索算法来解点灯问题就比较简单粗暴, 遍历所有点灯方案找出满足要求的方案即可(在leetcode中还需要得到操作最少的方案). 在解决问题之前首先必须对问题有足够的理解, 我们必须先说明下面虽明显但重要的两点, 即:</p><ul><li>操作的顺序无关性. 对于将所有灯从初始状态状态转换成全亮的点灯方案, 其一系列操作的顺序可以是任意的, 这很好理解, 因为对于点灯游戏, 灯的最终状态只取决于两点, 即初始状态与状态翻转的次数, 而与翻转的先后次序无关.</li><li>操作的非重复性. 这即是说如果我们需要找到一个操作最少的方案, 那么每盏灯被选择改变状态的次数是最多一次. 结合前面操作的顺序无关性很容易说明. 假设有这么一组操作方案是点灯游戏的一个解, 而这组操作中包含了多次选择同一盏灯进行翻转的操作, 我们可以利用顺序无关把这些操作集中到一起, 也就是连续对同样的灯进行翻转. 由于灯只有两种状态, 偶数次的相同操作相当于没操作, 奇数次的相同操作相当于操作了一次, 因此该方案都不可能是操作最少的方案.</li></ul><p>了解了上面两点我们才可以尝试着去解决问题, 其实这里我还尝试着考虑了下操作的唯一性, 但是无果. 有了更多的了解之后才意识到解确实不是唯一的.</p><p>算法的大致构思即测试所有点灯方案, 如满足要求记录下该方案. 剩下来的就是一些技术问题, 我们怎么存储点灯方案, 灯的当前状态又如何表示? 这里用二进制数来进行表示是一个比较好的想法, 其一是比较节省存储空间, 其二是状态的翻转可以用对相应位的异或来实现. 但是二进制矩阵不太容易实现, 对于小型规模的点灯游戏, 用c++内置类型即可, 如4字节int型可以用于存储32盏灯的状态, 这里涉及到的一个问题是两者从矩阵到数列坐标的转换. 对于测试的每一种点灯方案, 也可以用一个int来存储, 1的那些位表示直接选择翻转的灯. 当然别忘了选择翻转的灯的上下左右(如果存在的话)也需要进行翻转. 操作的次数只需要计算存储电灯方案int中二进制下1的个数即可. 下面是code.</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">minFlips</span><span class="params">(vector&lt;vector&lt;<span class="type">int</span>&gt;&gt;&amp; mat)</span> </span>&#123;</span><br><span class="line">        <span class="type">int</span> m = mat.<span class="built_in">size</span>(); <span class="comment">// 灯矩阵行数</span></span><br><span class="line">        <span class="type">int</span> n = mat[<span class="number">0</span>].<span class="built_in">size</span>(); <span class="comment">// 灯矩阵列数</span></span><br><span class="line">        <span class="type">int</span> stat = <span class="number">0</span>; <span class="comment">// 存储初始灯的状态</span></span><br><span class="line">        <span class="type">int</span> plan = <span class="number">0</span>; <span class="comment">// 当前应用的电灯方案</span></span><br><span class="line">        <span class="type">int</span> ans = <span class="number">-1</span>; <span class="comment">// 存储最少的操作次数 若不可解则返回-1</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; m * n; i++) <span class="comment">// 初始状态生成</span></span><br><span class="line">        &#123;</span><br><span class="line">            stat = stat &lt;&lt; <span class="number">1</span>;</span><br><span class="line">            stat += mat[i / n][i % n];</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (plan &lt; <span class="built_in">pow</span>(<span class="number">2</span>, m * n)) <span class="comment">// 遍历所有点灯方案 共 2^(mn) 种</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">int</span> cnt = <span class="number">0</span>; <span class="comment">// 存储当前点灯方案操作次数</span></span><br><span class="line">            <span class="type">int</span> num = plan;</span><br><span class="line">            <span class="keyword">while</span> (num)</span><br><span class="line">            &#123;</span><br><span class="line">                ++cnt;</span><br><span class="line">                num &amp;= (num - <span class="number">1</span>);</span><br><span class="line">            &#125;</span><br><span class="line"><span class="comment">// 如果该方案操作次数超过了之前解的操作次数 直接跳过该方案</span></span><br><span class="line">            <span class="keyword">if</span> (ans != <span class="number">-1</span> &amp;&amp; ans &lt;= cnt)</span><br><span class="line">            &#123;</span><br><span class="line">                plan += <span class="number">1</span>;</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            </span><br><span class="line">            <span class="type">int</span> temp = stat ^ plan; <span class="comment">// 将temp初始化为 初始状态下翻转该方案选择操作的那些灯 后的结果</span></span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; m * n; i++) <span class="comment">// 遍历plan</span></span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">if</span> (plan &amp; (<span class="number">1</span> &lt;&lt; m * n - i - <span class="number">1</span>)) <span class="comment">// 翻转选择操作的那些灯上下左右的灯</span></span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="keyword">if</span> (i / n - <span class="number">1</span> &gt;= <span class="number">0</span>)</span><br><span class="line">                        temp ^= (<span class="number">1</span> &lt;&lt; (m * n - (n*(i / n - <span class="number">1</span>) + i % n) - <span class="number">1</span>));</span><br><span class="line">                    <span class="keyword">if</span> (i / n + <span class="number">1</span> &lt; m)</span><br><span class="line">                        temp ^= (<span class="number">1</span> &lt;&lt; (m * n - (n*(i / n + <span class="number">1</span>) + i % n) - <span class="number">1</span>));</span><br><span class="line">                    <span class="keyword">if</span> (i % n - <span class="number">1</span> &gt;= <span class="number">0</span>)</span><br><span class="line">                        temp ^= (<span class="number">1</span> &lt;&lt; (m * n - (n*(i / n) + i % n - <span class="number">1</span>) - <span class="number">1</span>));</span><br><span class="line">                    <span class="keyword">if</span> (i % n + <span class="number">1</span> &lt; n)</span><br><span class="line">                        temp ^= (<span class="number">1</span> &lt;&lt; (m * n - (n*(i / n) + i % n + <span class="number">1</span>) - <span class="number">1</span>));</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (temp == <span class="number">0</span>) <span class="comment">// 当temp为0时 即灯全亮 是一个解</span></span><br><span class="line">                ans = cnt;</span><br><span class="line">            plan += <span class="number">1</span>; <span class="comment">// 处理下一个点灯方案</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> ans;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>上面的实现思路是比较简单的, 具体实现中其实也并没有遍历所有点灯方案的必要(在只要求找到最少操作方案的条件下), 因此当我们遍历到一个点灯方案操作灯的数量多于已经找到的解时, 将直接跳过这个方案的处理(无论这个方案是否为解). 上述搜索算法时间复杂度显见是$O(2^{mn}*mn)$的. 这意味着当灯的数量较少时才可能在比较短的时间内找到解, 以我现在手头上的i5surface laptop来说, 找到4*4规模初始状态为灯全灭的点灯游戏的最优解(操作最少的解)需0.007s, 5*5需要11.829s, 而6*6则已经在30分钟以上了. </p><h1 id="线性代数解"><a href="#线性代数解" class="headerlink" title="线性代数解"></a>线性代数解</h1><p>讨论完了暴力的搜索算法, 那么从数学的角度如何求解点灯问题呢? 根据<strong>每盏灯最终状态可由总翻转次数与初始状态确定</strong>这一点, 可以列线性方程来求解. 以下内容可参考Mathematica博客的这篇文章<a href="http://mathworld.wolfram.com/LightsOutPuzzle.html">Lights Out Puzzle</a>. </p><p>以3*3规模的点灯游戏为例, 我们首先需要得到一个9*9的二元对称矩阵$\boldsymbol{A}$,</p><script type="math/tex; mode=display">\boldsymbol{A}=\begin{pmatrix}1&1&0&1&0&0&0&0&0\\1&1&1&0&1&0&0&0&0\\0&1&1&0&0&1&0&0&0\\ 1&0&0&1&1&0&1&0&0\\0&1&0&1&1&1&0&1&0\\0&0&1&0&1&1&0&0&1\\0&0&0&1&0&0&1&1&0\\0&0&0&0&1&0&1&1&1\\0&0&0&0&0&1&0&1&1\\\end{pmatrix}</script><p>矩阵$\boldsymbol{A}$中只有元素0和1, 且是对称矩阵, 对于同样规模和规则的点灯游戏, 矩阵$\boldsymbol{A}$都是相同的. $\boldsymbol{A}$的每一行是这么来的, 依然把3*3的9盏灯按行展开排成一行, $\boldsymbol{A}$的第i行由将第i盏灯和其上下左右灯(如果存在的话)的位置置1其余置0得到. 如$\boldsymbol{A}$的第一行, 先把第一个位置自己置1, 然后在点灯游戏中第一盏灯在最左上角, 把其下与其右的灯的位置(2与4)也置1, 其余位置为0. 也可以说成$\boldsymbol{A}$的第i行相当于把所有影响第i盏灯状态的位置都置1, 其余置0. </p><p>一旦构成了$\boldsymbol{A}$, 那么顺理成章, 我们将点灯方案排成一列, 为9*1的列向量, 规定选择翻转的位置为1其余为0, 用$\boldsymbol{x}$表示. 同理我们可以假设$\boldsymbol{r}$为所有灯最终状态, 而$\boldsymbol{c}$表示所有灯初始状态, 那么上述向量之间的关系为,</p><script type="math/tex; mode=display">\boldsymbol{Ax}+\boldsymbol{c}=\boldsymbol{r}</script><p>注意上式我们只在GF(2)内进行讨论(关于伽罗华域顺便安利一下介绍纠错码的<a href="https://blog.openacid.com/storage/ec-2/">这篇文章</a>), 因为灯的状态只有0和1这两种. 将上式改成,</p><script type="math/tex; mode=display">\boldsymbol{Ax}=\boldsymbol{r}-\boldsymbol{c}=\boldsymbol{c'}</script><p>剩下来的问题就是解上面的非齐次线性方程组了. 可以用我们熟悉的高斯消元法(<a href="https://en.wikipedia.org/wiki/Gaussian_elimination">Gaussian elimination</a>)来解, 就是用初等变换将增广矩阵化为行阶梯型来求解, 解不唯一时需要先求对应齐次方程组通解与其本身一个特解, 然后组合通解与特解得到所有方程组的解. 由于方程组是定义在GF(2)上的, 解的每一位从0与1中取值, 因此化为阶梯型后对于有$d$个自由变量的方程组共有有$2^d$个解. 所以这个解的数目确实不是唯一的, 下图是2*3规模点灯游戏初始状态为全灭的4个解(黑色表示选择翻转的灯, 下同). 从图中可以看出解具有对称性.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/2_3.png" style="zoom:20%;" /></p><p>求解代码放在<a href="https://github.com/Granvallen/LightOutGame">这里</a>, 为了能够存储任意位数的二元矩阵使用了bitset类型, 其支持一些方便的位操作. 高斯消元法的时间复杂度为$O((mn)^3)$, 接着遍历找多解复杂度$O(2^d)$, 同样初始全灭的条件下, 解100*100规模点灯游戏找到所有的解需要15s, 相比搜索算法已经快了不少. 至于更快的方法主要是优化GF(2)下的方程组求解, 使用并行计算或改进高斯消元法. 下图为100*100的唯一解,</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/100_100.png" style="zoom:50%;" /></p><p>随便再放些不同规模解的图案吧, 可以发现有相当多的解是对称的, 特别是5倍数规模的解, 非常具有对称美. 以下为部分5倍数规模且解唯一的解图.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/5x.png" style="zoom:100%;" /></p><p>对于一些多解的情况也有不少是对称的, 下面是4*4, 5*5, 9*9, 16*16的多解图.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/4_4.gif" style="zoom:100%;" /></p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/5_5.gif" style="zoom:100%;" /></p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/9_9.gif" style="zoom:100%;" /></p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/16_16.gif" style="zoom:100%;" /></p><p>部分规模的解实在是太多了, 如30*30规模有$2^{20}$个解. 另外一个比较有意思的是对于有多解的情况, 存在着一种或多种最终不改变所有灯状态的点灯方案, 可由对应齐次方程组的解得到. 如下图是5*5不变的方案.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/5_5qp.png" style="zoom:10%;" /></p><hr><h1 id="番外-解存在性讨论"><a href="#番外-解存在性讨论" class="headerlink" title="番外 解存在性讨论"></a>番外 解存在性讨论</h1><p>事实上对于任意的m和n, 只要是满足自反(reflexive)与对称(symmetric)的点灯游戏, 初始状态为灯全灭的情况下, 可以证明游戏必定有解. 当然, 如果初始状态时已经有部分灯点亮则不一定有解. 证明之前先解释一下点灯游戏满足的自反与对称是什么.</p><p>自反即是说当选择一盏灯时, 除了翻转与这盏灯相邻灯的状态, 这盏灯本身也会状态翻转. 而对称是说相邻的两盏灯的状态相互影响, 当其中一盏翻转时, 另一盏灯的状态也会翻转. 其实点灯游戏有非常多的变种(就和魔方一样), 有些并不完全满足上面的两点, 比如<a href="https://en.wikipedia.org/wiki/Merlin_%28console%29">Merlin Magic square</a>游戏. </p><p>说明完毕, 推理证明如下.</p><p>使用反证, 假设存在着满足上述要求(自反, 对称, 初始状态为灯全灭)的点灯游戏无解, 且我们认为无解的情况中规模最小的有n盏灯, 此时n-1盏灯的点灯游戏则是可解的(如果不可解, 那么无解情况中规模最小的就应该是n-1盏灯了). 需要注意的是, 这里我们其实无视了灯的空间排列, 前面的讨论中, 点灯游戏普遍把所有灯排列成一个正方形或矩形, 这样只是为了让人容易看清(共享边的两盏灯是相互影响的), 但就本质上说, 我们完全可以用有向图(Directed Graphs)的结构来描述这个点灯游戏, 每盏灯是图中的一个结点, 由于游戏是对称的, 相互影响的灯的关系可以用双向的连线来表示, 因此这里不需要纠结n或者n-1盏灯能不能排列成矩形的空间结构的问题, 或者说能构成矩形空间结构的有向图在我们现在讨论的图的基础上需要更苛刻的条件.</p><p>如前所述, 那么我们必然有把这n盏灯中<strong>n-1盏灯点亮而只剩1盏灯灭掉</strong>的点灯方案(点灯方案指的就是n盏灯中选择哪几盏灯进行状态翻转), 这里的根据就是n-1盏灯的点灯游戏是可解的. 从n盏灯中选出n-1盏灯构成一个更小规模且可解的点灯游戏, 如果当这个小规模的点灯问题解决后(这n-1盏灯全点亮), 剩下来的那盏灯的状态是点亮的, 那么这个n盏灯的点灯游戏就也是可解的了, 这与假设矛盾, 因此孤立出来的那盏灯的状态必定是灭掉的. 对于总共n盏灯, 这n-1盏灯的选择显然有n种, 且这n种选择都能够完成上面所说的n-1盏灯亮1盏灯灭, <strong>如此便得到了相应的n种点灯方案</strong>.</p><p>将上面得到的n种点灯方案从灯全灭的初始状态开始连续施行一次, 我们发现当n为偶数时, 所有灯的状态正好翻转奇数(n-1)次, 如此一来最终n盏灯的状态就都是点亮的了! 因而n盏灯的点灯游戏就可解, 与假设再次矛盾, <strong>推出结论n只可能是奇数</strong>(当n为奇数时, 我们执行完上述n种点灯方案后, 所有灯翻转偶数次, 即状态不变, 全为灭).</p><p>我们继续讨论这n盏灯中是否存在着与偶数(包含0)盏其他灯相关联的灯(从有向图的观点来说, 就是这n个结点中是否存在着与偶数个其他结点双向连接的结点), 也就是这盏灯的状态翻转将影响其他奇数盏灯的状态翻转. 我们先假设存在这样满足条件的灯, 取其中的一盏来讨论, 把这盏灯和其关联的另外奇数盏灯单独划分为一组, 组内就有奇数盏灯. 我们从前面得到的n种点灯方案中挑选出<strong>以组内灯为最后灭掉的灯的那些方案</strong>, 将挑选出的这奇数个点灯方案同样从灯全灭的初始状态开始连续施行一次, 此时n盏灯中非组内灯的状态都改变了奇数次, 故都为点亮状态, 而组内灯状态则都改变了偶数次, 状态不变. 最后我们选择改变前面提到的满足条件的那盏灯, 如此组内灯的状态将再都改变一次, 即也都将被点亮. 此时我们发现所有灯都被点亮, 此n盏灯的点灯游戏可解, 再次与假设矛盾, 由此得到结论<strong>这n盏灯的每一盏灯都与其他奇数盏灯相关联, 而不可能是偶数盏</strong>.</p><p>推到这里, 其实我们已经找到了矛盾, 只是不是特别明显, 即<strong>点灯游戏的对称性</strong>与我们前面推出的<strong>有奇数盏灯且每盏灯都与其他奇数盏灯相关联</strong>之间存在矛盾. 点灯游戏的对称性也可以推出一个结论, 那就是<strong>当执行所有灯都选择点亮一次的点灯方案时, 不计算那些直接选择的灯的那次翻转, 只计算那些因其他灯选择翻转由于受关联而间接翻转的次数, 在这个意义下所有灯的累计翻转次数总是偶数次的</strong>. 道理也是显而易见的, 这里所谓游戏的对称性就是两盏相关联的灯是相互影响的, 选择一盏灯直接地状态翻转会间接地翻转相关联的另一盏灯, 而对那另一盏灯来说也同理. 当上述点灯方案都选择一次翻转后, 就这两盏相互关联的灯来说实际翻转了4次, 前面说这里我们不计算因直接选择的翻转, 所以计间接翻转的2次. 可见每一组相关联的灯都会计间接翻转的两次, 故最终所有灯累计的间接翻转次数必为2的倍数即偶数. 严谨起见, 我想还需要补充两点说明. 其一是这里计算的是因间接影响的翻转次数, 这仅仅是为了推理讨论的便利性(虽然其本身的理解增加了复杂性?). 其二是严格来说, 对于实际的一个点灯问题自然是不会出现<strong>孤立的灯</strong>的情况(这盏灯不与其他任何灯相关联), 但当存在孤立的灯时, 上述由对称性得到的结论仍然是正确的, 因为当选择点亮孤立的灯时, 由于没有与其相关联的灯, 所以计入的翻转次数为0. </p><p>但是如果点灯游戏中有奇数盏灯, 依然执行所有灯都选择翻转状态一遍的点灯方案, 为了能获得总共偶数次的间接翻转, 必然不可能每盏灯相关联的灯的数目都是奇数的, 或者说必定存在着与其他偶数盏灯(包括关联0盏的孤立的灯)相关联的灯, 这样我们就推得了最后一个矛盾, 从而否定了最开始无解的n盏灯的点灯游戏的存在性假设, 命题得证. </p><p>文字描述的证明有些地方确实啰嗦了点, 但我想还是尽可能留下更多的细节便于理解. 用线性代数的视角我们可以得到更简洁又严密的证明. 这里最后再说明一点, 在我参考的<a href="https://www.jaapsch.net/puzzles/lomath.htm#refs">The Mathematics of Lights Out</a>博客中, 作者对于最后一个矛盾的阐述举了一个更生动的例子, 但需要注意其与点灯游戏之间的差异. 这个例子是说<strong>有奇数个人参加聚会, 在聚会上自然少不了有些人会相互握手, 但是无论有多少人握手, 总存在和偶数个其他人握手的人(包括一次都没有和其他人握手的人)</strong>. 这个例子确实容易理解很多, 而这个例子与点灯游戏的差异在于握手是关联的双方同时进行的, 且每一次握手操作都只涉及握手的双方. 而对于点灯游戏, 每一次的操作是选择一盏灯进行状态翻转, 故灯之间的相互影响(间接翻转)是分开进行的, 且一次操作同时影响多盏其他的灯, 但我们依然可以理解这背后其实是一回事, 也就是对称性的体现. 另一个差异则是点灯游戏具有自反性, 握手则自然不可能自己与自己握手, 因此在上面讨论中我们忽略了直接选择时的状态翻转.</p><hr><p>同样引入线性代数能很大程度简化证明, 我们知道当非齐次线性方程组无解时有$\mathrm{rank}(\boldsymbol{A})&lt;\mathrm{rank}(\boldsymbol{A|\boldsymbol{c}’})\leqslant mn$. 那么必然存在着非$\boldsymbol{0}$列向量$\boldsymbol{v}$有,</p><script type="math/tex; mode=display">\boldsymbol{v}^T\boldsymbol{A}=\boldsymbol{0}^T</script><p>并且,</p><script type="math/tex; mode=display">\boldsymbol{v}^T\boldsymbol{c}'=1</script><p>这里稍微解释下, 若上式不成立则,</p><script type="math/tex; mode=display">\boldsymbol{v}^T\boldsymbol{A}\boldsymbol{x}=\boldsymbol{v}^T\boldsymbol{c}'=0</script><script type="math/tex; mode=display">\boldsymbol{0}^T\boldsymbol{x}=0</script><p>这样方程组便有解了. </p><p>由于点灯游戏的自反与对称性, $\boldsymbol{A}$是定义在GF(2)上的对称矩阵, 且对角线元素都为1, 因此有,</p><script type="math/tex; mode=display">\boldsymbol{v}^T\boldsymbol{A}\boldsymbol{v}=\boldsymbol{v}^T\boldsymbol{v}</script><p>这是因为左边展开后混合项都是成对出现, 故其系数皆为0, 只留下二次项. 结合最前面的式子便可得到,</p><script type="math/tex; mode=display">\boldsymbol{v}^T\boldsymbol{v}=0</script><p>于是,</p><script type="math/tex; mode=display">\boldsymbol{v}\boldsymbol{v}^T\boldsymbol{c}'=\boldsymbol{v}=\boldsymbol{0}</script><p>这与$\boldsymbol{v}$为非零向量矛盾, 假设不成立, 因此解必定存在.</p><p>其实在线性代数中有一个更强的定理, 对于定义在GF(2)上的对称矩阵$\boldsymbol{A}$, 存在$\boldsymbol{x}$满足$\boldsymbol{Ax}=\mathrm{Diag}(\boldsymbol{A})$, 翻译过来就是二元对称矩阵$\boldsymbol{A}$对角线元素构成的列向量必定在$\boldsymbol{A}$的列空间中, 详细证明见<a href="http://www.cs.toronto.edu/~yuvalf/Range.pdf">这里</a>. 如果知道这个定理对点灯游戏解的存在性甚至都不需要进一步说明, 因为在点灯游戏的设定下, 对称保证了$\boldsymbol{A}$是对称矩阵, 自反保证了$\boldsymbol{A}$对角线元素全为1, 而初始状态的$\boldsymbol{c}’$也总是全为1…</p><hr><p>最后的最后, 这点灯游戏竟然让我找回了小时候摆弄万花筒的感觉, 这大概也算是数学的魅力之一吧…</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><a href="https://en.wikipedia.org/wiki/Lights_Out_%28game%29">Lights Out (game) wiki</a></li><li><a href="https://www.jaapsch.net/puzzles/lomath.htm#refs">The Mathematics of Lights Out</a></li><li><a href="http://mathworld.wolfram.com/LightsOutPuzzle.html">Lights Out Puzzle</a></li><li><a href="https://www.geogebra.org/m/JexnDJpt#material/YXt3VhPn">5x5 Lights Out</a></li><li><a href="https://www.brand.site.co.il/riddles/201103a.html">Using your Head is Permitted March 2011 riddle</a></li><li><a href="https://www.jaapsch.net/puzzles/lights.htm#desc">Lights Out</a></li><li><a href="http://www.cs.toronto.edu/~yuvalf/Range.pdf">Range of Symmetric Matrices over GF(2)</a></li><li><a href="https://people.sc.fsu.edu/~jburkardt/classes/imps_2017/11_28/2690705.pdf">Turning Lights Out with Linear Algebra</a></li><li><a href="https://blog.openacid.com/storage/ec-2/">Erasure-Code-擦除码</a></li></ul><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=30051839&auto=0&height=66"></iframe><p>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近由于疫情隔离在家, 所以正好抽空刷一刷leetcode, 于是刷到了&lt;a href=&quot;https://leetcode.com/problems/minimum-number-of-flips-to-convert-binary-matrix-to-zero-matrix/&quot;&gt;Minimum Number of Flips to Convert Binary Matrix to Zero Matrix&lt;/a&gt;这题, 题中讲的是一个将二元数组中的1根据特殊规则全消减成0的游戏. 看到规则, 虽然我记不得这游戏是叫什么名字, 但我确定曾经玩过几次这个游戏, 最近的一次是我拿Lue和Love游戏引擎写点格棋游戏的时候, 在社区里看到有人分享了用Love做的这个游戏. 查了一下, 原来叫灭灯游戏(&lt;a href=&quot;https://en.wikipedia.org/wiki/Lights_Out_%28game%29&quot;&gt;Lights Out&lt;/a&gt;), 在国内貌似一般叫点灯游戏? 不过也没差, 这个点灯游戏规则很简单, 相信很多人都玩过, 长方形中划分网格, 每个网格是盏灯, 且有亮与暗两种状态, 游戏中唯一的操作是选择一盏灯改变状态, 同时这盏灯上下左右的灯(如果存在的话)状态也将同时改变, 游戏初始状态时可能有几盏灯已经是点亮的, 游戏的目的是经过一系列的开关灯的操作, 使网格中的灯全部点亮. 这里有个&lt;a href=&quot;https://www.geogebra.org/m/JexnDJpt#material/YXt3VhPn&quot;&gt;在线版本&lt;/a&gt;可以重温一下.&lt;/p&gt;
&lt;p&gt;本来感觉这个点灯游戏也没什么, 感觉还是有博弈的游戏像是&lt;a href=&quot;https://en.wikipedia.org/wiki/Dots_and_Boxes&quot;&gt;点格棋&lt;/a&gt;更有意思一些, 直到我看到了知乎专栏的这篇文章&lt;a href=&quot;https://zhuanlan.zhihu.com/p/21265602&quot;&gt;点灯游戏: 简单的游戏能否很美?&lt;/a&gt;, 说实话确实很美, 甚至感到有些震撼. 于是我打算稍微探索一下这个游戏, 所幸文章作者写得非常认真, 相关的结论和参考都有总结, 遗憾的是没有提到结论的证明. 虽然是已经被前人研究得比较透彻的问题, 但我还是想写点自己的理解, 于是就诞生了这篇不务正业的文章…&lt;/p&gt;</summary>
    
    
    
    <category term="Lab" scheme="https://granvallen.github.io/categories/Lab/"/>
    
    <category term="数学" scheme="https://granvallen.github.io/categories/Lab/%E6%95%B0%E5%AD%A6/"/>
    
    
    <category term="Mathematica" scheme="https://granvallen.github.io/tags/Mathematica/"/>
    
    <category term="C++" scheme="https://granvallen.github.io/tags/C/"/>
    
    <category term="oj" scheme="https://granvallen.github.io/tags/oj/"/>
    
  </entry>
  
  <entry>
    <title>CodeCraft 2019</title>
    <link href="https://granvallen.github.io/codecraft2019/"/>
    <id>https://granvallen.github.io/codecraft2019/</id>
    <published>2019-06-22T16:12:31.000Z</published>
    <updated>2021-02-24T06:23:10.930Z</updated>
    
    <content type="html"><![CDATA[<p>又是一篇拖了好长时间的文章…</p><p>CodeCraft是华为2015年起开始举办的编程挑战赛, 强调享受用coding解决问题的乐趣. 其实那段时间(两个月前)有好几个华为组织的比赛什么销售, 云计算等等. 我因为之前都没参加过, 加之感觉coding会好玩一些所以报名了CodeCraft. 虽然最后完成了程序但没及时上传（；´д｀）ゞ, 嗯, 从结果来说好像连炮灰也没当成?</p><span id="more"></span><p>题目在<a href="https://codecraft.huawei.com/">这里</a>, 简单来说就是车辆调度的问题, 官方给出car.txt, road.txt和cross.txt三个文件即车, 路和路口信息, 根据简化后的交通规则, 你的程序需要输出每辆车的上路时间以及行驶路线. 拿到题目首先就必须研究下这个设定的交通规则, 其中涉及到不少细节问题, 大体规则可以参考官方文档, 以下是一些当时有疑问的细节和官方解答.</p><h1 id="规则细节"><a href="#规则细节" class="headerlink" title="规则细节"></a>规则细节</h1><h2 id="关于车辆在路上行驶的规则"><a href="#关于车辆在路上行驶的规则" class="headerlink" title="关于车辆在路上行驶的规则"></a>关于车辆在路上行驶的规则</h2><blockquote><p>车辆如果行驶过程中, 发现前方有车辆阻挡, 且阻挡的车辆为终止车辆, 则该辆车也被标记为终止车辆. （与前方阻挡的车辆的距离记为s）则该车辆最大行驶速度为v = min(最高车速, 道路限速, s/t) 其中t=1, 该车辆最大可行驶距离为s. </p></blockquote><p>这里官方文档第二句话少了”否”字, 应为</p><blockquote><p>否则该车辆最大行驶速度为v = min(最高车速, 道路限速, s/t) 其中t=1, 该车辆最大可行驶距离为s. </p></blockquote><p>也就是说当有前车开得比较慢时, 该车也要降低速度. 而第一句是说如果前车停了, 则该车立即停止. <strong>因此, 每辆车的速度每次轮到时都要根据前车情况重新计算. </strong> 另外如果前车是等待调度状态, 那么该车也是等待状态.</p><p>下面是官方给出的例子:</p><blockquote><p>假定道路有如下车辆, 车AB车速为3, CD均为车速为1<br><strong>A空空B空空CD （路口）</strong></p><p>按步骤一后ABCD均为等待状态. </p><p>在处理待状态车辆D后, 假定D前进到其他道路后, 此刻道路状态变化为</p><p><strong>A空空B空空C空   （路口）</strong></p><p><strong>接下来因D车辆的前进后, 需要对该条道路该车道的所有车辆进行一次调度</strong></p><p>因此:</p><p>C的车速为1, 则C车前进1个距离, 且为终止状态. <strong>A空空B空空空C   （路口）</strong></p><p>B的车速为3, 则B车前进3个距离, 且为终止状态. <strong>A空空空空空BC   （路口）</strong></p><p>A的车速为3, 则A车前进3个距离, 且为终止状态. <strong>空空空A空空BC   （路口）</strong></p><p><strong>调度后道路车辆分布为: 空空空A空空BC   （路口）, 且ABC均为终止状态</strong></p></blockquote><h2 id="关于车辆在路口调度的规则"><a href="#关于车辆在路口调度的规则" class="headerlink" title="关于车辆在路口调度的规则"></a>关于车辆在路口调度的规则</h2><p>例子如下图:</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/cross.png" style="zoom:67%;" /></p><blockquote><p>在本路口调度道路的顺序为</p><p>5000、5001、5010、5018、5000、5001、5010、5018、5000、5001、5010、5018…5000、5001、5010、5018</p><p>整个系统调度按路口ID升序进行调度各个路口, 路口内各道路按道路ID升序进行调度. </p></blockquote><p>也就是说, <strong>路口调度顺序与各道路的调度顺序是固定的.</strong></p><blockquote><p>首先调度5000道路, 只有D100可以走, 因为D101左转与5010道路的D200冲突, 所以必须等待5010车道上没有直行冲突. </p></blockquote><p>这里的<code>D101</code>官方又写错了, 应该是<code>L101</code>, 该车是当前5000道路优先级最高的车, 但与5010最高优先级直行的车<code>D200</code>冲突(都要到5018道路), 直行的优先级更高, 故对5000道路的调度先搁下了(当道路最高优先级的车停下, 其他车也不能动). </p><p><strong>因此, 当对路口的一条道路调度时, 首先看这条道路优先级最高的车, 若不与其他道路最高优先级的车冲突, 则立即行驶, 然后看该道路第二优先级的车. 如果有冲突, 则调度下一条道路.</strong></p><blockquote><p>其次再调度5001道路, 因5001道路的L300与道路5018的D400冲突, 所以L300必须等待道路5018的D400先行后再调度行驶. </p><p>再次调度道路5010, D200、D201、D202不与其他道路的车辆冲突, 可以直接行驶. L203左转, 且道路5001无左转车辆与其冲突, 因此L203可以左转. L204也可以左转. L205为右转, 与左侧道路5000的车辆L101不冲突（只与道路5000的直行会发生冲突）, 且与道路5018的D400不发生冲突（只与道路5018的左转车辆发生冲突）, 因此车辆L205可以右转, 依次道路5010上的剩余车辆全部可以通过. </p></blockquote><p>在道路通畅的条件下(驶入道路没有限制), 路口调度应该是这样的:</p><ul><li>当调度一辆车是直行的时候一定可以立即直行</li><li>当调度一辆车左转时, 只需要看右边道路最高优先级的车是否直行</li><li>当调度一辆车右转时, 需要看左边道路优先级最高的车是否直行以及对面车道优先级最高的车是否左转</li></ul><p>在实际情况时还要考虑车驶入车道的情况, 比如驶入车道是拥堵的, 此时该车的调度也只能先搁下.</p><blockquote><p>接着调度5018道路上的车辆…..</p><p>再调度道路5000….</p><p>再调度道路5001…</p><p>再调度道路5000….</p><p>是否发生冲突, 只与相关道路的第一优先级车辆的行驶方向进行比较, 看是否发生冲突. </p><p>每次调度到一条道路, 直到该道路无车辆可调度, 或该条道路上车辆处于冲突状态. 也就是说尽可能多地让该道路行驶, 直到没有车辆或者车辆与其他车辆发生冲突不可行驶. </p></blockquote><h2 id="关于车辆从路口进入道路"><a href="#关于车辆从路口进入道路" class="headerlink" title="关于车辆从路口进入道路"></a>关于车辆从路口进入道路</h2><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/enterroad.png" style="zoom: 80%;" /></p><p>这里要注意的是<strong>发车顺序</strong>, 先是100-&gt;200-&gt;300, 然后101-&gt;201-&gt;301. 这里200比101更靠近路口, 尽管101在1号道, 200在2号道, 依然是200先行. 这一点在上面的路口冲突判断的图里也有体现, 当<code>D101</code>走后是判断<code>L101</code>, 而不是<code>L103</code></p><h2 id="关于死锁"><a href="#关于死锁" class="headerlink" title="关于死锁"></a>关于死锁</h2><blockquote><p>基于10, <strong>参赛选手需要注意</strong>, 一次调度能使所有车辆均到达各车辆的行驶速度行驶, 就得保证不能出现各车辆循环等待的情况, 否则该次调度就会<strong>锁死</strong>. 循环等待是指比如车辆A等待车辆B, 车辆B等待车辆C, 车辆C等待车辆D, 车辆D等待车辆E, 车辆E等待车辆F, 车辆F等待车辆A的情况. </p></blockquote><h2 id="Q-amp-A"><a href="#Q-amp-A" class="headerlink" title="Q&amp;A"></a>Q&amp;A</h2><p>问: 如果一个路口的所有道路的车都冲突不动了, 怎么办? 是直接结束该路口的调度? 这样的话如果下一路口调度了导致上一路口可以再进行调度, 还需要对上一路口进行调度吗? 如果不进行调度那么路口的调度顺序对结果影响很大?</p><p>答: 是的, 会对上一路口再次进行调度(会进行下一次路口的调度循环), 只要有一个路口还需要调度, 就会再进行一次调度所有路口的循环</p><p>问: 每辆车都有出发时间的限定, 但是规则中出现<code>实际出发时间</code>的说法, 那么是程序可以调节车的实际出发时间吗? 还是只能是因为道路堵塞推迟出发时间?</p><p>答: 车辆的实际出发时间由参赛选手自行决定, 但是不得早于车辆的计划出发时间</p><p>问: 起步车辆放置于车道的位置确定问题?</p><p>答: 进入道路依然按车道小的优先策略进行</p><p>问: 那么起步车辆与路口调度时的车辆比, 优先级如何?</p><p>答: 系统调度先调度在路上行驶的车辆进行行驶, 当道路上所有车辆全部不可再行驶后再调度等待上路行驶的车辆</p><p>问: 多辆车同地点同时发车?</p><p>答: 交通规则有说明, 同一时刻多辆车上路行驶, 按车辆编号升序进行优先上路</p><p>其他: </p><blockquote><p>整个系统限制参赛选手程序编译时间最大为60s, 程序运行生成answer.txt时间为300s. </p><p>车辆的长度为1(占一格), 行驶速度的含义是一个时间单位内前进的格数</p></blockquote><h1 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h1><p>整个系统要调度的只有车, 更确切点说是车的两个值: <strong>实际出发时间</strong>和<strong>车在每个路口选择的行驶方向</strong>, 除此之外都由规则定死了. 也就说只要确定了每辆车的这两个值, 那么最后的结果就是确定的, 不存在任何的随机性.</p><p>总的来说, 解决这个问题的想法大概有两个吧.</p><p>其一直接最优化, 系统根据输入和规则直接给出最优的调度方案. 因为本质上说这是一个最优化问题. 但我还是怀疑有没有这种解法, 题目设定的规则还是过于复杂, 于是没有做这方面的尝试, 而偏向于使用模拟或尝试性的解决办法.</p><p>退而求其次可以逐步最优, 系统首先对输入进行模拟, <strong>在每个时间片上(只考虑当前时刻)进行最优调度</strong>, 但每个时间片上的最优策略就结果来说并不能保证是最优的. 不过即使简化到这样可能对我来说实现还是比较困难. 不怕, 大不了在逐步最优的基础上, 再进行简化嘛, 比如变成在<strong>每个时间片上对每辆车单独进行最优调度</strong>. 还不够, 因为这个系统中的车的行驶过程不是独立的, 可能会相互影响, 因此即使单独考虑每一辆车的最优可能也是困难的, 所以只能是<strong>尽可能地靠近最优调度</strong>.</p><p>综上, 目前考虑的方法是建立在模拟运行的基础上, <strong>每一个时间片对每辆车单独地做出尽可能接近最优的调度</strong>. 这个方法可能比较好的一点是策略是动态的, 可以根据模拟运行时的情况对调度进行调整, 而这个调整(如何尽可能接近最优)也可以有发挥的余地.</p><p>以上讨论的都是车在路口的行驶方向的选择问题, 还有一部分是车实际出发时间的确定问题, 时间比较紧就没怎么考虑过. 总之现阶段考虑车能出发则出发, 不能出发则等到能出发为止. 当然一个理想的系统可能会故意延长车的实际出发时间(能出发不出发), 以达到系统更优.</p><p>要解决这个问题路径规划(Pathfinding)算法是必不可少的, 用于实际在每个路口确定行驶方向. 这方面优先考虑的是启发式路径搜索的A*, 在<a href="http://theory.stanford.edu/~amitp/GameProgramming/">Amit’s A* Pages</a>有比较详细的介绍和生动的例子, 同时也参考了部分将启发性路径搜索算法应用于实际导航路径规划的文献. 正当要动手开始写的时候发现了一个致命的问题—比赛给的地图不保证是物理可实现的. 启发式路径搜索不同于传统路径搜索的地方在于在计算路径cost的时候多了一个启发函数项(heuristic function), 因此路径搜索受启发函数影响优先往函数值小的方向搜索. 这类方法的主要问题在于如何选择这个启发函数, 选择的好坏最终将影响路径搜索的效率, 这也是为什么这个名字这么玄乎的原因. 通常采用欧式距离(平面坐标)和曼哈顿距离(网格坐标)作为启发函数, 这通常来说是合理的, 但都要求知道每个节点确切的坐标信息, 而赛题中只是知道节点与边的关系, 需要自己去构建一个位置关系, 由于是非实际可实现的道路, 先不说能不能构建或容不容易构建, 即使构建出来, 要设计一个符合的启发函数也不容易. というわけで, 最后还是采用了Dijkstra算法(摊手), 顺带稍稍优化了权重计算.</p><h1 id="程序大体框架"><a href="#程序大体框架" class="headerlink" title="程序大体框架"></a>程序大体框架</h1><ul><li>car.hpp实现了Car与CarList两个类, 用于封装车辆信息</li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Car</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">Car</span>() &#123;&#125; <span class="comment">// 为什么没有默认构造函数会报错</span></span><br><span class="line"><span class="built_in">Car</span>(<span class="type">long</span> id, <span class="type">long</span> from, <span class="type">long</span> to, <span class="type">int</span> speed, <span class="type">long</span> planTime):</span><br><span class="line"><span class="built_in">id</span>(id), <span class="built_in">from_id</span>(from), <span class="built_in">to_id</span>(to), <span class="built_in">max_speed</span>(speed), </span><br><span class="line"><span class="built_in">plan_time</span>(planTime), <span class="built_in">next_cross</span>(from), <span class="built_in">isSetoff</span>(<span class="literal">false</span>), <span class="built_in">isStoped</span>(<span class="literal">false</span>) &#123;&#125;</span><br><span class="line"><span class="built_in">Car</span>(<span class="type">long</span>* carinfo)</span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">if</span> (carinfo)</span><br><span class="line">&#123;</span><br><span class="line">id = carinfo[<span class="number">0</span>];</span><br><span class="line">from_id = next_cross = carinfo[<span class="number">1</span>];</span><br><span class="line">to_id = carinfo[<span class="number">2</span>];</span><br><span class="line">max_speed = <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(carinfo[<span class="number">3</span>]);</span><br><span class="line">plan_time = carinfo[<span class="number">4</span>];</span><br><span class="line">isSetoff = isStoped = <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">showRoute</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">friend</span> ostream &amp; <span class="keyword">operator</span>&lt;&lt;(ostream &amp;out, Car &amp;car);</span><br><span class="line"></span><br><span class="line"><span class="type">long</span> id; <span class="comment">// 车的id</span></span><br><span class="line"><span class="type">long</span> from_id; <span class="comment">// 起始路口id</span></span><br><span class="line"><span class="type">long</span> to_id; <span class="comment">// 终点路口id</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> max_speed; <span class="comment">// 最大车速</span></span><br><span class="line"><span class="type">int</span> curr_speed; <span class="comment">// 当前车速  这个变量到目前为止好像没起什么作用 可能到考虑实时路况时才能起参考</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 车在一条路上的坐标 如一条长为4的路坐标为 1 2 3 4</span></span><br><span class="line"><span class="comment">// 这个值在车进入道路时初始化 行进过程中更新</span></span><br><span class="line"><span class="type">int</span> pos;</span><br><span class="line"></span><br><span class="line"><span class="type">bool</span> isSetoff; <span class="comment">// 是否已出发</span></span><br><span class="line"><span class="type">long</span> plan_time; <span class="comment">// 原计划出发时间</span></span><br><span class="line"><span class="type">long</span> real_time; <span class="comment">// 实际出发时间</span></span><br><span class="line"><span class="type">long</span> dally_time; <span class="comment">// 已延迟的时间</span></span><br><span class="line"></span><br><span class="line">list&lt;<span class="type">long</span>&gt; route_plan; <span class="comment">// 计划路线经过的路口id 不包括已经走过的路口 虽然答案里要求是行驶道路的id</span></span><br><span class="line">list&lt;<span class="type">long</span>&gt; route_real; <span class="comment">// 实际行驶经过路口id</span></span><br><span class="line"><span class="type">int</span> next_turn; <span class="comment">// 下一个路口转弯方向 0-上 1-右 2-下 3-</span></span><br><span class="line"><span class="type">long</span> next_road; <span class="comment">// 下一条行驶的道路id 行驶到路口更新</span></span><br><span class="line"><span class="type">long</span> next_cross; <span class="comment">// 下一个路口id</span></span><br><span class="line"><span class="type">int</span> lane_num; <span class="comment">// 所行驶的车道号 车在上路时更新</span></span><br><span class="line"></span><br><span class="line"><span class="type">bool</span> isStoped; <span class="comment">// 是否停车</span></span><br><span class="line"><span class="type">bool</span> isScheduled; <span class="comment">// 是否在当前时间片段经过调度</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CarList</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">CarList</span>() &#123;&#125;</span><br><span class="line"><span class="built_in">CarList</span>(string carfile) &#123; <span class="built_in">initCarList</span>(carfile); &#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">initCarList</span><span class="params">(string carfile)</span></span>;</span><br><span class="line">Car&amp; <span class="keyword">operator</span>[](<span class="type">long</span> i) &#123; <span class="keyword">return</span> carlist[i]; &#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">add</span><span class="params">(Car&amp; car)</span> </span>&#123; carlist[car.id] = car; &#125; <span class="comment">// unordered_map这里[]实现深复制</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">remove</span><span class="params">(<span class="type">long</span> id)</span> </span>&#123; carlist.<span class="built_in">erase</span>(id); &#125;</span><br><span class="line"><span class="function"><span class="type">const</span> unordered_map&lt;<span class="type">long</span>, Car&gt;&amp; <span class="title">getList</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> carlist; &#125;</span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">empty</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> carlist.<span class="built_in">empty</span>(); &#125;</span><br><span class="line"><span class="function"><span class="type">long</span> <span class="title">size</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> carlist.<span class="built_in">size</span>(); &#125;</span><br><span class="line"></span><br><span class="line">unordered_map&lt;<span class="type">long</span>, Car&gt;::<span class="function">iterator <span class="title">begin</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> carlist.<span class="built_in">begin</span>(); &#125;</span><br><span class="line">unordered_map&lt;<span class="type">long</span>, Car&gt;::<span class="function">iterator <span class="title">end</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> carlist.<span class="built_in">end</span>(); &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"></span><br><span class="line">unordered_map&lt;<span class="type">long</span>, Car&gt; carlist;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>road.hpp实现了Road与RoadList两个类, 用于封装道路信息</li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Road</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">Road</span>() &#123;&#125;</span><br><span class="line"><span class="built_in">Road</span>(<span class="type">long</span>* roadinfo)</span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">if</span> (roadinfo)</span><br><span class="line">&#123;</span><br><span class="line">id = roadinfo[<span class="number">0</span>];</span><br><span class="line">length = <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(roadinfo[<span class="number">1</span>]);</span><br><span class="line">max_speed = <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(roadinfo[<span class="number">2</span>]);</span><br><span class="line">channel = <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(roadinfo[<span class="number">3</span>]);</span><br><span class="line">from_id = roadinfo[<span class="number">4</span>];</span><br><span class="line">to_id = roadinfo[<span class="number">5</span>];</span><br><span class="line">isDuplex = roadinfo[<span class="number">6</span>] &gt; <span class="number">0</span> ? <span class="literal">true</span> : <span class="literal">false</span>;</span><br><span class="line">road_from_to.<span class="built_in">resize</span>(channel);</span><br><span class="line"><span class="keyword">if</span> (isDuplex)</span><br><span class="line">road_to_from.<span class="built_in">resize</span>(channel);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">friend</span> ostream &amp; <span class="keyword">operator</span>&lt;&lt;(ostream &amp;out, Road &amp;road);</span><br><span class="line"></span><br><span class="line"><span class="type">long</span> id;</span><br><span class="line"><span class="type">int</span> length; <span class="comment">// 路长</span></span><br><span class="line"><span class="type">int</span> max_speed; <span class="comment">// 最大车速</span></span><br><span class="line"><span class="type">int</span> channel; <span class="comment">// 车道数</span></span><br><span class="line"><span class="type">long</span> from_id; <span class="comment">// 起始路口id</span></span><br><span class="line"><span class="type">long</span> to_id; <span class="comment">// 终点路口id</span></span><br><span class="line"><span class="type">bool</span> isDuplex; <span class="comment">// 是否双向</span></span><br><span class="line"></span><br><span class="line"><span class="type">bool</span> isBlock; <span class="comment">// 是否堵路</span></span><br><span class="line"></span><br><span class="line"><span class="function">vector&lt;Lane&gt;&amp; <span class="title">getRoadCars</span><span class="params">(<span class="type">long</span> to)</span></span>; <span class="comment">// 返回一个方向的所有车道</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">getCarsNum</span><span class="params">(<span class="type">long</span> crossid)</span></span>;</span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">isTo</span><span class="params">(<span class="type">long</span> crossid)</span> </span>&#123; <span class="keyword">return</span> crossid == to_id || (crossid == from_id &amp;&amp; isDuplex); &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">vector&lt;Lane&gt; road_from_to; <span class="comment">// 记录从from节点到to节点 各个channel行驶车的队列 存车的id</span></span><br><span class="line">vector&lt;Lane&gt; road_to_from; <span class="comment">// 如果是单行线的话这个vector是空的</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">RoadList</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">RoadList</span>() &#123;&#125;</span><br><span class="line"><span class="built_in">RoadList</span>(string roadfile) &#123; <span class="built_in">initRoadList</span>(roadfile); &#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">initRoadList</span><span class="params">(string roadfile)</span></span>;</span><br><span class="line">Road &amp; <span class="keyword">operator</span>[](<span class="type">long</span> i) &#123; <span class="keyword">return</span> roadlist[i]; &#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">add</span><span class="params">(Road&amp; road)</span> </span>&#123; roadlist[road.id] = road; &#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">remove</span><span class="params">(<span class="type">long</span> id)</span> </span>&#123; roadlist.<span class="built_in">erase</span>(id); &#125;</span><br><span class="line"><span class="function"><span class="type">const</span> map&lt;<span class="type">long</span>, Road&gt;&amp; <span class="title">getList</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> roadlist; &#125;</span><br><span class="line"></span><br><span class="line">map&lt;<span class="type">long</span>, Road&gt;::<span class="function">iterator <span class="title">begin</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> roadlist.<span class="built_in">begin</span>(); &#125;</span><br><span class="line">map&lt;<span class="type">long</span>, Road&gt;::<span class="function">iterator <span class="title">end</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> roadlist.<span class="built_in">end</span>(); &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"></span><br><span class="line">map&lt;<span class="type">long</span>, Road&gt; roadlist;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>cross.hpp实现了Cross与CrossList两个类, 用于封装路口信息</li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Cross</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">Cross</span>() &#123;&#125;</span><br><span class="line"><span class="comment">//Cross(long id, long road0, long road1, long road2, long road3) : id(id)</span></span><br><span class="line"><span class="comment">//&#123;</span></span><br><span class="line"><span class="comment">//road_id[road0] = 0; road_id[road1] = 1;</span></span><br><span class="line"><span class="comment">//road_id[road2] = 2; road_id[road3] = 3;</span></span><br><span class="line"><span class="comment">//&#125;</span></span><br><span class="line"><span class="built_in">Cross</span>(<span class="type">long</span>* crossinfo)</span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">if</span> (crossinfo)</span><br><span class="line">&#123;</span><br><span class="line">id = crossinfo[<span class="number">0</span>];</span><br><span class="line"><span class="comment">// 注意这里没路的 -1 也会放进去 把序号作为键 路id为值</span></span><br><span class="line">road_id[<span class="number">0</span>] = crossinfo[<span class="number">1</span>]; road_id[<span class="number">1</span>] = crossinfo[<span class="number">2</span>];</span><br><span class="line">road_id[<span class="number">2</span>] = crossinfo[<span class="number">3</span>]; road_id[<span class="number">3</span>] = crossinfo[<span class="number">4</span>];</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">friend</span> ostream &amp; <span class="keyword">operator</span>&lt;&lt;(ostream &amp;out, Cross &amp;cross);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 由路序号得到路id</span></span><br><span class="line"><span class="function"><span class="type">long</span> <span class="title">getRoadIdx</span><span class="params">(<span class="type">int</span> roadid)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">auto</span>&amp; i : road_id)</span><br><span class="line"><span class="keyword">if</span> (i.second == roadid)</span><br><span class="line"><span class="keyword">return</span> i.first;</span><br><span class="line"><span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">long</span> id; <span class="comment">// 路口id</span></span><br><span class="line">map&lt;<span class="type">long</span>, <span class="type">int</span>&gt; road_id; <span class="comment">// 四个连接道路的id 没路的为-1 用map可以实现路id升序</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//int road_num; // 连接道路数目</span></span><br><span class="line"><span class="type">bool</span> isBlock; <span class="comment">// 是否堵塞</span></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CrossList</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">CrossList</span>() &#123;&#125;</span><br><span class="line"><span class="built_in">CrossList</span>(string crossfile) &#123; <span class="built_in">initCrossList</span>(crossfile); &#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">initCrossList</span><span class="params">(string crossfile)</span></span>;</span><br><span class="line">Cross &amp; <span class="keyword">operator</span>[](<span class="type">long</span> i) &#123; <span class="keyword">return</span> crosslist[i]; &#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">add</span><span class="params">(Cross&amp; cross)</span> </span>&#123; crosslist[cross.id] = cross; &#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">remove</span><span class="params">(<span class="type">long</span> id)</span> </span>&#123; crosslist.<span class="built_in">erase</span>(id); &#125;</span><br><span class="line"><span class="function"><span class="type">const</span> map&lt;<span class="type">long</span>, Cross&gt;&amp; <span class="title">getList</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> crosslist; &#125;</span><br><span class="line"></span><br><span class="line">map&lt;<span class="type">long</span>, Cross&gt;::<span class="function">iterator <span class="title">begin</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> crosslist.<span class="built_in">begin</span>(); &#125;</span><br><span class="line">map&lt;<span class="type">long</span>, Cross&gt;::<span class="function">iterator <span class="title">end</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> crosslist.<span class="built_in">end</span>(); &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"></span><br><span class="line">map&lt;<span class="type">long</span>, Cross&gt; crosslist; <span class="comment">// 由于之后cross要按id从小到大遍历 所以用有序map</span></span><br><span class="line"></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>RoadGraph.hpp实现RoadGraph类, 负责整合道路与路口构建图网络, 并且实现路径规划算法</li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">RoadGraph</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">RoadGraph</span>() &#123;&#125;</span><br><span class="line"><span class="built_in">RoadGraph</span>(CrossList&amp; crosslist, RoadList&amp; roadlist) &#123; <span class="built_in">initGraph</span>(crosslist, roadlist); &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 初始化路线图 neighborsOf 与 distBetween 便于之后搜索路径</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">initGraph</span><span class="params">(CrossList&amp; crosslist, RoadList&amp; roadlist)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">findFastestRoute</span><span class="params">(CrossList&amp; crosslist, RoadList&amp; roadlist, Car&amp; car)</span></span>;</span><br><span class="line"></span><br><span class="line">unordered_map&lt;<span class="type">long</span>, list&lt;<span class="type">long</span>&gt; &gt; neighborsOf; <span class="comment">// 一个节点的相邻节点</span></span><br><span class="line">unordered_map&lt;<span class="type">long</span>, unordered_map&lt;<span class="type">long</span>, <span class="type">double</span>&gt; &gt; distBetween; <span class="comment">// 所有相邻节点的距离</span></span><br><span class="line">unordered_map&lt;<span class="type">long</span>, unordered_map&lt;<span class="type">long</span>, <span class="type">long</span>&gt; &gt; roadBetween; <span class="comment">// 所有相邻节点之间路的id</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">dijkstra_search</span><span class="params">(CrossList&amp; crosslist, RoadList&amp; roadlist, Car&amp; car)</span></span>; <span class="comment">// dijkstra路径搜索方法</span></span><br><span class="line"></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>Scheduler.hpp实现了一个调度器</li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Scheduler</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"><span class="built_in">Scheduler</span>(string cartxt, string roadtxt, string crosstxt): <span class="built_in">time</span>(<span class="number">0</span>), <span class="built_in">isDone</span>(<span class="literal">false</span>) </span><br><span class="line">&#123;</span><br><span class="line"><span class="built_in">initScheduler</span>(cartxt, roadtxt, crosstxt);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">long</span> time; <span class="comment">// 调度时间</span></span><br><span class="line"><span class="type">bool</span> isDone; <span class="comment">//是否调度完成</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">step</span><span class="params">()</span></span>; <span class="comment">// 走一个时间片段</span></span><br><span class="line"></span><br><span class="line">CarList carwaitgo; <span class="comment">// 等待上路的车</span></span><br><span class="line">CarList carsetoff; <span class="comment">// 在路上的车</span></span><br><span class="line">RoadList roadlist;</span><br><span class="line">CrossList crosslist;</span><br><span class="line">RoadGraph roadgraph;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">initScheduler</span><span class="params">(string cartxt, string roadtxt, string crosstxt)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">scheduleCarwaitgo</span><span class="params">()</span></span>; <span class="comment">// 调度等待上路的车</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">scheduleInit</span><span class="params">()</span></span>; <span class="comment">// 每次调度前准备</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">scheduleRoad</span><span class="params">()</span></span>; <span class="comment">// 调度每一条道路</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">scheduleLane</span><span class="params">(Road&amp; road, Lane&amp; lane)</span></span>; <span class="comment">// 调度路一个方向上的所有车道</span></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">scheduleCross</span><span class="params">()</span></span>; <span class="comment">// 调度所有路口</span></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">scheduleCrossRoad</span><span class="params">(Cross&amp; cross)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">enterRoad</span><span class="params">(Car&amp; car, Lane&amp; lane, <span class="type">int</span> lane_num, <span class="type">int</span> speed)</span></span>; <span class="comment">// 车进入道路</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">leaveRoad</span><span class="params">(Car&amp; car, Lane&amp; lane)</span></span>; <span class="comment">// 车离开道路</span></span><br><span class="line"><span class="function"><span class="type">long</span> <span class="title">getfirstCar</span><span class="params">(Road&amp; road, Cross&amp; to)</span></span>; <span class="comment">// 返回一个方向的车道中最先调度的车</span></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">tryEnterRoad</span><span class="params">(Cross&amp; cross, Road&amp; road, Car&amp; car)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">answer</span><span class="params">(Car&amp; car)</span></span>;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">showstatus</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="type">bool</span> flag1; <span class="comment">// 记录遍历 所有cross 的一个循环中 是否发生了调度 如果调度再进行一次这个循环</span></span><br><span class="line"><span class="type">bool</span> flag2; <span class="comment">// 记录遍历 一个cross所有路 的循环中 是否发生了调度</span></span><br><span class="line"><span class="type">bool</span> flag3; <span class="comment">// 记录遍历一个cross所有路的循环中 是否发生了调度 如果调度再进行一次这个循环 直到没有调度</span></span><br><span class="line"></span><br><span class="line"><span class="type">long</span> carnum = <span class="number">0</span>;</span><br><span class="line"><span class="type">long</span> donenum = <span class="number">0</span>;</span><br><span class="line"><span class="type">long</span> carwaitgonum = <span class="number">0</span>;</span><br><span class="line"><span class="type">long</span> carsetoffnum = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li>main</li></ul><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="function">Scheduler <span class="title">scheduler</span><span class="params">(<span class="string">&quot;car.txt&quot;</span>, <span class="string">&quot;road.txt&quot;</span>, <span class="string">&quot;cross.txt&quot;</span>)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (!scheduler.isDone)</span><br><span class="line">scheduler.<span class="built_in">step</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><ul><li>由于没有成功在赛方的服务器上跑过, 所以到底效果如何有没有其他问题都尚不明确, 虽然线下测试结果勉强还行, 也没碰到死锁的情况. 当然为了鲁棒性加个死锁判定还是有必要的.</li><li>由于比赛期间一直有其他事, 所以前后参赛了不到5天的时间. 后来看获奖者感言, 原来大家都一样, 都是在搬砖期间抽空做的, 白天搬砖, 晚上调程序…</li><li>全程玩得还是挺开心的, 嗯, 希望下次能做一个合格的炮灰(认真脸).</li></ul><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;又是一篇拖了好长时间的文章…&lt;/p&gt;
&lt;p&gt;CodeCraft是华为2015年起开始举办的编程挑战赛, 强调享受用coding解决问题的乐趣. 其实那段时间(两个月前)有好几个华为组织的比赛什么销售, 云计算等等. 我因为之前都没参加过, 加之感觉coding会好玩一些所以报名了CodeCraft. 虽然最后完成了程序但没及时上传（；´д｀）ゞ, 嗯, 从结果来说好像连炮灰也没当成?&lt;/p&gt;</summary>
    
    
    
    <category term="Coding" scheme="https://granvallen.github.io/categories/Coding/"/>
    
    <category term="C/C++" scheme="https://granvallen.github.io/categories/Coding/C-C/"/>
    
    
    <category term="C/C++" scheme="https://granvallen.github.io/tags/C-C/"/>
    
    <category term="CodeCraft" scheme="https://granvallen.github.io/tags/CodeCraft/"/>
    
  </entry>
  
  <entry>
    <title>从《星游记》出发聊聊所见所想</title>
    <link href="https://granvallen.github.io/xingyouji/"/>
    <id>https://granvallen.github.io/xingyouji/</id>
    <published>2019-06-14T16:13:00.000Z</published>
    <updated>2024-07-22T14:54:44.000Z</updated>
    
    <content type="html"><![CDATA[<p>怎么想起聊聊<a href="https://movie.douban.com/subject/6952623/">《星游记》</a>呢, 大概源于最近偶然看到的几个新闻吧. 一是米哈游开发的新游<a href="https://www.taptap.com/app/168332?utm_source=baidu&amp;utm_medium=sem&amp;utm_campaign=%E5%8E%9F%E7%A5%9E">《原神》</a>放出的pv由于过于像《塞尔达传说:旷野之息》被指抄袭引起网上热议; 二是<a href="https://movie.douban.com/subject/26709258/">《罗小黑战记》</a>的首部电影将在2019年7月上映. 由这两件事情我立马想起了星游记, 我觉得我想说些什么所以写了下来, 不过只是作为一个普通的吃瓜群众谈谈所见所想罢了.</p><span id="more"></span><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=730250&auto=0&height=66"></iframe><p>为什么会由原神想到了星游记? 因为星游记从开播起, 也一直以来就没有真正走出被指抄袭海贼王的阴影, 只要有其出现的地方就少不了这些争论. 星游记与海贼设定相似的确是无可争议的事实(导演刘北就是海贼的粉丝), 我个人其实无意参与这些争论, 是借鉴学习也好, 抄袭也罢, 这些还是交给法律与律师去界定吧. 只是, 我个人觉得作品本身的素质以及能否带给观众某种共鸣更重要一些(自然前提是合法的), 而不是它被贴上的各种标签. 说回这两件事它们其实还是有本质的不同, 原神pv明眼人一眼就能看出模仿的是塞尔达(各种剪辑分析的朋友可以歇一歇了), 就如网上所说很可能就是米哈游的营销手段, 这样的做法确实算不上光彩, 至于合法性, 偌大的公司一定有它的考量. 尽管如此, 刚看到原神pv时我依然抱有某种期待, 虽然是模仿但能达到什么样的精致程度? 又融入了制作者什么样的新想法? 此前任天堂公开的塞尔达制作的理念被原神消化了多少?(从目前流出来的内容来看定位依然是米哈游一贯的偏宅向)</p><p>为什么会由罗小黑战记想到了星游记? 因为他们都曾是我心目中最优秀的国产动画, 虽然出生迥异, 但某种程度上说也算殊途同归. 说到国产动画, 又是一个大而专的话题, 阅历有限只能谈谈个人的所见所想. 两部作品都是在2011年差不多的时间上线. 星游记的制作并不缺钱, 由北京卡酷, 优扬传媒出资, 电动画负责制作, 电动画创始人刘北(爱的战士)任导演与编剧, 动画在电视台播出. 原计划制作两季52集, 但电动画这边由于种种原因制作完26集第一季后停止了项目. 沉寂6年后, 全擎娱乐公司从卡酷优扬手里买下星游记版权(电动画只负责制作没有版权, 故没有话语权), 重新设立工作室重启了星游记项目, 但是以制作网络电影的形式放送. 而罗小黑的制作是坎坷的, 与当时的大多数缺钱的动画工作室一样, 一开始的定位就是5分钟长的网络动画, 即便如此, 更新的周期仍然很长而且不固定, 但是一直没有停止创作, 人气也不断积累. 然后在今年暑假, 罗小黑的电影也宣布上映了. 以上是我了解到的大概情况, 可能我了解得并不全面. 至于电动画为什么单方面停止了制作, 当时有个对刘北的采访说得很清楚(<a href="http://www.woko.cc/thread-21098-1-1.html">采访</a>), 简而言之就是说国内市场不成熟, 在探索回收方式上存在困难, 也就是没有商业市场. </p><p>.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/xyj2.jpg" style="zoom:60%" class alt="星游记" /></p><p>.</p><p>单从结果上说, 虽然星游记以这种方式重启了, 但我个人觉得电影各方面没有达到tv版应有的高度, 特别是制作组为了体现主角们的成长而更换画风, 事实上我觉得原作画风非常有特点已经非常令人满意了, 为了体现成长还是应该在剧情台词上下功夫. 另外在人设上也进行了改动, 也许是新制作组想脱离原作表现自己的想法吧, 因此从一开始就不是什么经典延续, 而是类似同人的二次创作. 我也非常理解这一点, 毕竟换了制作组, 也许制作上也还不够成熟. 其实很多粉丝期待的只是在原作的基础上给麦当的故事画上一个还算圆满的句号, 目前能期待做到这一点的还是刘北, 虽然刘北本人还没有放弃, 但希望是渺茫的. 罗小黑这边我比较乐观, 看pv还是原来的味道, 而且对于罗小黑这样单靠可爱卖萌就能吸粉的动画制作却也相当精良还是很难得. 我总感觉罗小黑战记所背负的压力和包袱(粉丝的期待)没有像星游记那么重, 这跟动画的定位也有关系.</p><p>.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/xyj1.jpg" style="zoom:40%" class alt="星游记" /></p><p>.</p><p>再回来聊聊星游记本身. 最近一些媒体对于作品的评价越来越谨慎, 如机核对游戏评论的<a href="https://www.gcores.com/radios/110358">讨论</a>很有意思. 媒体对作品的评论或许负有责任, 但个人评论则不需要考虑对他人的影响这一因素, 是非常主观的个人感受. 总体上, 我对星游记是喜欢的 , 那时我甚至惊讶于国产动画可以做得这么”精致”, 不是单元剧而是长篇, 不是什么生硬的3D建模, 不是只有几分钟的网络动画(当时这种形式很流行), 依然保持着较高的制作水准, 画风, 故事与人设都不错, 全剧从头到尾一直带给我惊喜. 硬要说不足的话大概有两点, 一是配乐相比之下有点逊色, 只有op与ed还算让人印象深刻, 二是放到现在来看清晰度不高, 作画略显简陋. 除此之外用今天的眼光来看依然丝毫不逊色, 当时我甚至觉得放在日本动画中也是远超及格线的存在(从讲故事的角度). 究其原因依然绕不过海贼王, 星游记的故事中同样有着海贼王中让人感动的那些要素, 梦想, 信念, 勇气与爱等等, 但最重要的是星游记做出了自己独特的味道, 这些故事很好地塑造了个性鲜明的角色. 而且我个人也觉得星游记达到了能对人产生影响的高度, 在网络上看到有因为星游记而看到国产希望毅然投入到动画产业中去的人(然而现实是残酷的), 对于他们来说, 星游记或许有更加特别的意义. </p><p>我一直想写一写那些影响过我的事物, 那些我经历中相见恨晚的作品, 属于我独一无二感受的作品, 没想到是用星游记开了头, 最后还是用我当时看时最喜欢的一幕最为结束吧…</p><blockquote><p>不管怎么为别人努力, 不变强的话, 不能打倒别人的话, 最后就只能被打倒跪在地上. 我们没必要为这种世界牺牲, 对吧? 因为我而跪下去的你, 我会替你站起来, 绝对不会让任何人再嘲笑你的名字. 记住了, 我的名字, 就! 是! 米! 龙!!!</p></blockquote><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=28267531&auto=0&height=66"></iframe><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;怎么想起聊聊&lt;a href=&quot;https://movie.douban.com/subject/6952623/&quot;&gt;《星游记》&lt;/a&gt;呢, 大概源于最近偶然看到的几个新闻吧. 一是米哈游开发的新游&lt;a href=&quot;https://www.taptap.com/app/168332?utm_source=baidu&amp;amp;utm_medium=sem&amp;amp;utm_campaign=%E5%8E%9F%E7%A5%9E&quot;&gt;《原神》&lt;/a&gt;放出的pv由于过于像《塞尔达传说:旷野之息》被指抄袭引起网上热议; 二是&lt;a href=&quot;https://movie.douban.com/subject/26709258/&quot;&gt;《罗小黑战记》&lt;/a&gt;的首部电影将在2019年7月上映. 由这两件事情我立马想起了星游记, 我觉得我想说些什么所以写了下来, 不过只是作为一个普通的吃瓜群众谈谈所见所想罢了.&lt;/p&gt;</summary>
    
    
    
    <category term="ACGN" scheme="https://granvallen.github.io/categories/ACGN/"/>
    
    <category term="动画" scheme="https://granvallen.github.io/categories/ACGN/%E5%8A%A8%E7%94%BB/"/>
    
    
    <category term="Anli" scheme="https://granvallen.github.io/tags/Anli/"/>
    
    <category term="动画" scheme="https://granvallen.github.io/tags/%E5%8A%A8%E7%94%BB/"/>
    
  </entry>
  
  <entry>
    <title>读《自新世界》</title>
    <link href="https://granvallen.github.io/zixinshijie/"/>
    <id>https://granvallen.github.io/zixinshijie/</id>
    <published>2019-04-24T11:31:00.000Z</published>
    <updated>2021-02-24T06:23:10.874Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/shinsekaiyori.jpg"  class alt="自新世界" /></p><p>由于每天坐公交来去实验室差不多要两小时, 所以在路上补完了<a href="https://book.douban.com/subject/1922216/">《全球通史》</a>, <a href="https://book.douban.com/subject/3646172/">《恶意》</a>, 前几周又补完了贵志祐介的<a href="https://book.douban.com/subject/25752955/">《自新世界》</a>, 实际上只花了不到一周的时间就看完了, 小说后半段情节非常紧凑, 高潮迭起, 几乎是一口气读完的, 真是久违了, 这种故事情节所展现出来的压迫感, 总觉得在早些时候也经历过, 一回想, 卧槽, 不就是<a href="https://book.douban.com/subject/26690465/">《恶之教典》</a>吗, 同样是贵志祐介的作品, 同样剧情让人窒息, 不过我看的不是原著, 而是漫画. 虽然已经过去了几个礼拜, 细节什么的也已经记不清了, 不过非常值得聊一聊观感的.</p><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=399367460&auto=0&height=66"></iframe><p>说起来, 这并不是我第一次接触《自新世界》, 没错, 第一次是其改编的<a href="https://movie.douban.com/subject/10527275/">动画</a>, 记得是12年的十月番. 遗憾的是因为种种原因没有追完, 当时给我的印象是画风, 世界观很独特, 恐怖和悬疑的氛围营造得很好, 隐约感觉后面会有神展开.</p><p>说回小说, 小说的设定真的很有意思, 大致是说人类社会由于爆发战争文明随之衰落, 回到了村落聚居的社会模式, 人类在那时开发了一种被称之为”咒力”的潜能, 其实简单理解就是超能力, 基本上可以凭借想象力做任何事, 理论上没有极限, 但实现的效果是根据想象到的细节多少决定的. 由于这种能力过于强大, 为了限制这种能力的滥用而建立了一套特殊的社会制度, 比如其中有教育委员会会严格监视孩子们的精神状况, 咒力的开发情况等, 另外还有利用心理暗示等手段实现的同类相残时触发的”愧死机制”. </p><p>此外贵志祐介还精心设计了好几种变异的生物, 其中有智力与人类相仿的化鼠, 益兽蓑白与传说中遇到就会被诅咒的拟蓑白(其实是傲娇萝卜子什么的我才不会乱说呢), 巨大的螃蟹什么的. 这其中与人关系最紧密的是化鼠, 由于人类有强大的咒力, 总体上化鼠作为人类奴隶差不多的存在, 化鼠把人类尊为神明, 在人类面前卑躬屈膝, 而人类也会命令化鼠做一些脏活. 化鼠的社会体系也是分不同族群, 人类为了便于区分管理, 会给予归顺的化鼠族群各种名号, 不同族群也有着不同的社会制度, 有化鼠女王统治的君主制, 以及废除女王设立议事机构的共和制. 这些细节的设定非常用心和巧妙.</p><p>主角是五人团(早季, 觉, 瞬, 真理亚, 守), 叙事视角是其中的早季, 采用倒叙. 故事主要讲述了他们成长经历, 冒险, 以及揭示这个社会和世界的真相. </p><p>介绍完毕下面随便吐槽一下吧, 以下内容依然包含剧透. . . </p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>自新世界的故事主要分为两个部分, 前一部分是主角一行年少时在学校以及毕业修行的所见所闻, 看完后面部分就会发现前面其实有好多伏笔, 其中最吸引人的疑问就是这个世界的真相以及化鼠的来源. 这一部分主要还是在完善小说的世界观(社会制度, 自然环境, 以及化鼠), 同时展现早季一行人的出格与特殊, 然后发生了人类社会弱点之一的”业魔”事件(可怜的瞬从此只能活在早季思念里了). 我个人也是很喜欢”业魔”设定, “咒力”是靠意识驱动的, 但是潜意识活动会在人不知觉的情况下使用”咒力”, 而所谓”业魔”就是在本人无知觉的情况下”咒力”大量外泄, 外泄的”咒力”会改变周围物体的构成从而引起变异或者破坏(增强版核泄漏). 瞬最后舍身救早季那段也很精彩, 可惜小说中并没有说清楚原因(能力太超出常人?遗传病?).</p><p>第二部分是小说的高潮, 果不其然, 化鼠背叛了他们的”神明”人类, 对人类展开了屠杀. 首先单看这个事件, 我的感受也像早季在最后的感受一样, 内心无比复杂. 化鼠作为人类的奴隶(或许连奴隶都算不上), 整个种族命运被人类随意玩弄于股掌(虽然也称不上人类暴政), 只能隐忍苟活. 然后一只叫斯奎拉的化鼠登上化鼠的历史舞台, 作为一个革命者改造了化鼠的社会制度, 提高化鼠科技水平(拟蓑白), 最后为了化鼠种族未来的自由向人类发出挑战. 斯奎拉很聪明, 不仅巧言善辩, 而且知道人类社会的弱点, 精心准备了一个屠杀计划, 并且计划惊人地顺利, 要不是早季与奇狼丸联手解决了人类天敌”恶鬼”, 斯奎拉将人类驱逐殆尽地梦想就实现了.</p><p>从某种意义上说斯奎拉也是小说角色塑造最成功的一个, 他有自己坚持的信念, 为了自己的种族愿意献出一切(敌对鼠窝的奇狼丸也同样), 尽管手段有些不光彩甚至残忍, 但不可否认他确实是最接近实现化鼠梦想的一个. 在本作的暗线中提到, 化鼠其实就是被有”咒力”的人类种群所改造的人类, 以便一劳永逸地解决两个人类种群之间的矛盾. 当斯奎拉最后在人类法庭上喊出”我们是人类!”, 怎么也没办法对这个角色产生恨意. 也许是出于敬意, 早季最后违反规定偷偷让斯奎拉安然死去.</p><p>话说必须要吐槽下小说里的人类战斗力也太弱了吧, 和化鼠开战后单方面被虐, 虽然你可以说斯奎拉的计划有多周密, 变种化鼠(奇行种)有多出奇不意, 但其实真正能对人类有威胁的就只有化鼠拉拢的”恶鬼”吧. 由于”恶鬼”以为自己是化鼠, 所以其”愧死机制”同类对象是化鼠, 这使得”恶鬼”能肆意使用咒力虐杀人类, 而其他人则由于”愧死机制”拿”恶鬼”没办法. 但是, “恶鬼”也只是一个孩子呀, 愧死机制虽然对咒力使用者造成痛苦但不至于完全不能用啊, 当传说认真起来可以把地球分成两半的镝木肆星先生(没错, 镝木大人的定语就是辣么长)前面刚用实力安慰众人不用担心, 下一秒就被”恶鬼”轻松虐杀, 反正我是接受不能. 但是最后早季反过来利用”愧死机制”制服”恶鬼”确实也很精彩(真理亚与守的孩子真是个悲剧, 小说中也没有道出她的过去). 失去”恶鬼”的化鼠又一下子被人类扭转了局势.</p><p>另外本文开头的曲子是我看自新世界时的BGM, 迷幻的曲风与小说世界观真实与虚幻并存非常应景, 另外女声也有一种经历了种种悲欢离合略显寂寞的感觉与早季的经历也很契合, 当然最关键的是歌词我很喜欢, 摘出来作为一个结束吧.</p><blockquote><p>明日で終わりかもしれないね. </p><p>今日はまだ 子どものままでいよう. </p><p>何もない僕らが 笑うことを</p><p>許すならば</p><p>あなたとゆめをみたい</p><p>明日は来ないかもしれないね. </p><p>何に笑い 何を歌える？</p><p>明日もまた、</p><p>あなたと笑っていた</p></blockquote><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>.</p><p>終わり</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/shinsekaiyori.jpg&quot;  class alt=&quot;自新世界&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于每</summary>
      
    
    
    
    <category term="ACGN" scheme="https://granvallen.github.io/categories/ACGN/"/>
    
    <category term="小说" scheme="https://granvallen.github.io/categories/ACGN/%E5%B0%8F%E8%AF%B4/"/>
    
    
    <category term="读后感" scheme="https://granvallen.github.io/tags/%E8%AF%BB%E5%90%8E%E6%84%9F/"/>
    
  </entry>
  
  <entry>
    <title>SIFT特征提取算法理解与实现</title>
    <link href="https://granvallen.github.io/sift/"/>
    <id>https://granvallen.github.io/sift/</id>
    <published>2019-01-20T12:27:00.000Z</published>
    <updated>2021-02-24T06:23:10.872Z</updated>
    
    <content type="html"><![CDATA[<p>我又回来了, 总算熬到放假了. . . 按照国际惯例先凑字数扯点近况.</p><span id="more"></span><p>没想到我托更的原因竟然不是懒, 而是事多. 最近几个礼拜真是累的够呛, 没想到研一课还挺多, 还都得做报告, 台下强制提问, 这感觉就有点过分了. 所以大家基本一份报告”吃遍天下”. 我还有几个报告在年后(1年3学期制), 所以有预感这个寒假也. . . </p><p>但总算是获得了一个喘息的机会, 然后平时娱乐也就打打麻将什么的, 也算正式入日麻坑了. 日麻这个事最开始的认识也是拜<a href="http://www.kylen314.com/">Vespa菊苣</a>所赐, 后来看<a href="https://book.douban.com/subject/3127875/">《赌博堕天录》</a>中社长和开司玩的麻将游戏—地雷游戏, 为了看懂了解了下日麻规则. 毕业闲下来的时候随便打打日麻, 只在天极牌和雀姬上玩过一段时间. 到了这学期, 需要频繁地在延长与宝山来回跑, 在路上就靠打牌打发时间了. 话说最近雀魂又莫名开始火起来了, 台游雀姬联合B站看准时机看来也要插一脚. 不过雀魂做得确实不错, 页游的形式很灵活方便.</p><p>另一个由于之前关注的几个漫画出了动画, 特别是《约定的梦幻岛》(在上一期漫画推荐中也有提到), 所以冲了个大会员来看, 顺便bilibili漫画与网易版权合作, 也能继续看漫画版了. 看来作者也要开始收笔完结了, 最近一话诺曼出场了/(ㄒoㄒ)/~~</p><p>另外之前七牛云终究还是崩了, 网站图片全挂(<a href="http://busuanzi.ibruce.info/">不蒜子</a>也被阴了一把), 于是干脆把图片也挂到Github上了, 虽然速度堪忧, 先勉强用用. 然后本站音乐外链也全换成了网易云, 虾米的外链太慢, 官方似乎也有意阉割外链功能.</p><hr><p>说回正题, 这个学期一直忙着上课(听报告╮(╯-╰)╭)和项目, 其实留给自己折腾的时间很少. 其中有一门CVPR的课, 一个课程项目是图像特征提取, 所以就去了解了下<a href="https://en.wikipedia.org/wiki/Scale-invariant_feature_transform">SIFT</a>. 其作者Lowe的论文对于算法具体实现叙述很少, 虽然CSDN上相关文章一大堆, 但真正搞懂且说明白的人也不多. 我在理解与实现中帮助比较大的是这两位博主<a href="http://aandds.com/blog/img-feature-detector.html#org9cc6832">cig01</a>和<a href="https://blog.csdn.net/zhaocj">zhaocj</a>, 博主zhaocj写了一篇33页的文章, 详尽讲解了SIFT原理与openCV的SIFT实现, 我在最后才看到这篇文章, 此前自己已经理解过openCV的源码, 但那篇文章还是在一些细节上给了我启发, 鉴于资源网上不太好找, 就分享个<a href="https://pan.baidu.com/s/1LytpQ81cp6nsv9zAuNiSXA">网盘</a>(m981)吧, 另外这是我做的<a href="https://pan.baidu.com/s/10b1o5Y_1H9tDyVKB4oxOsA">ppt</a>.</p><p>按理说已经有很多人写过SIFT, 就没我等渣渣什么事了, 但我确实在这上花了比较多的时间, 理解原理, 然后用Matlab与C++实现了SIFT, 过程中碰到的问题及处理细节还是印象深刻的. 所以这篇博文我主要还是会重点讨论其他博主所忽视的实现上的细节问题, 算作一个补充. 目测可能会比较长. . . </p><hr><h1 id="SIFT的理解"><a href="#SIFT的理解" class="headerlink" title="SIFT的理解"></a>SIFT的理解</h1><h2 id="关于SIFT"><a href="#关于SIFT" class="headerlink" title="关于SIFT"></a>关于SIFT</h2><p>SIFT即Scale Invariant Feature Transform, 由英国哥伦比亚大学计算机系教授Lowe在1999年首次提出, 并在04年完善后发表了成果<a href="http://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf">《Distinctive Image Features from Scale-Invariant Keypoints》</a>. </p><p>SIFT简单来说就是一种稳定、高效的图像关键点检测与描述子生成方法, 如同其名字中所描述的, SIFT特征具有对旋转、尺度缩放、光照变化的不变性, 同时对3D视角变化、仿射变换、加性噪声也保持一定程度的稳定性. 在SIFT之前的一些方法(各种角点检测(Corner detection)方法, 如Moravec角点检测, Harris角点检测等)是不具有尺度不变旋转不变的. 在SIFT之后的06年, Bay等在SIFT基础上改进, 提升其检测效率, 提出了SURF(Speeded Up Robust Features, 加速鲁棒性特征)算法. 09年G.Yu和 J.M. Morel在SIFT基础上提出<a href="http://www.cmap.polytechnique.fr/~yu/research/ASIFT/demo.html#SIFT%20and%20ASIFT%20---%20online%20demo%20:%20try%20if%20your%20images%20match!">Affine-SIFT (ASIFT)</a>以此改善SIFT特征的仿射不变性能. 应该来说, SIFT作为一种经典的关键点检测算法, 在计算机视觉领域动不动就是深度学习模型的当下仍然有着存在感, 或许SIFT本身所包含的尺度不变的思想仍有借鉴意义. 之前看到有基于深度学习的关键点检测方法LIFT出来, 从实验结果来看SIFT的性能依然不错.</p><p>另外Lowe为SIFT申请了专利, 在07年的时候提出了基于SIFT的图像拼接方法AutoStitch(Automatic Panoramic Image Stitching using Invariant Features), 这又是图像拼接领域的里程碑式的方法.</p><p>具体到SIFT主要的两部分即关键点检测与关键点描述. </p><h2 id="关键点检测"><a href="#关键点检测" class="headerlink" title="关键点检测"></a>关键点检测</h2><p>这一部分的目的就是检测关键点. 在SIFT中关键点来源于DoG图像的极值点, 然后经过筛选过程去除不稳定的极值点后剩下的就作为关键点了.</p><h3 id="建立图像金字塔"><a href="#建立图像金字塔" class="headerlink" title="建立图像金字塔"></a>建立图像金字塔</h3><p>图像金字塔是图像多分辨率分析或者说不同尺度空间分析的工具, 在SIFT中使用了图像金子塔, 这是其尺度不变性的一个重要来源. SIFT与其他关键点检测算法的不同之处就在于引入了尺度空间(Scale space), 在图像的行列坐标外, 还多了一个尺度空间的坐标$\sigma​$, 这个坐标来源于对图像做了方差为$\sigma​$的高斯滤波或$\sigma​$的图像尺度缩放.</p><p>为实现多尺度, 在SIFT中需要构建两个金字塔, <strong>高斯模糊金字塔与DoG金字塔</strong>. 这里是先构成高斯模糊金字塔, 在以此构建DoG金子塔的关系, 我们需要的极值就是在DoG金子塔中寻找.</p><p>那怎么构建高斯模糊金子塔呢? 为与论文叙述统一, 将金子塔的每一层称为octave, 而一层中的一张张图像称作layer, 构建金字塔的具体做法是对<strong>初始图像</strong>做不同尺度$\sigma$的高斯模糊生成第一个octave(底层), 下一个octave的第一张图像由上一个octave最后一张图像降采样产生(长宽减半, 具体方法的话在openCV源码中使用了最近邻), 接着对<strong>这第一张图像</strong>同样做不同尺度高斯模糊产生当前octave其余图像, 而下一octave重复进行以上操作. 至于DoG金子塔建立则更简单了, 其每个octave的图像即对高斯模糊金字塔对应octave内图像相邻图像两两做差得到(注意图像做差操作会使像素值变负, 在实现时转换为合适类型). 这样我们知道如果DoG金子塔一octave内有$s$张图像, 则高斯模糊金字塔octave有$s+1$张.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sift_dog.png" style="zoom:40%" class alt="构建高斯模糊金子塔与DoG金子塔" /></p><p>接下来我们就面临几个具体问题, 比如金子塔octave取多少? 金子塔octave内图像数$s$又取多少? 高斯模糊金字塔第一个octave的第一个图像的尺度空间坐标$\sigma_0$取多少呢?</p><p>对于octave可以取比较好的实验值, 事实上实验结果来看, 极值点主要出现在前几个octave中, 过大的octave没有意义. 在具体的实现中, 我有看到是用下面公式计算得到的</p><script type="math/tex; mode=display">Octave = log_2(min(w_0, h_0)) - log_2(min(w, h))+1</script><p>$w_0​$与$h_0​$为原图像宽高, $w​$与$h​$为塔顶图像宽高. 如对于一幅大小为16×16的图像, 当塔顶图像设定为4×4时, 金字塔层数octave = 3. 可以对其验证, 如前所述, 每一个octave的图像尺寸是前一个octave图像尺寸的一半, 于是16-&gt;8-&gt;4, 可见确实是三层的图像金字塔.</p><p>至于$s$的取值同样是实验值, Lowe的实验结果说明取3为优. 如前所述, $s$为DoG金子塔octave内的图像数目, 我们假设一octave图像在尺度空间上由$\sigma$均匀变化到$2\sigma$, 这样相邻两张图像尺度比值为一定值$k$</p><script type="math/tex; mode=display">k=2^{\frac{1}{s}}</script><p>如此一来, 实现了<strong>DoG图像在尺度空间坐标的连续性</strong>, 即当前octave最后一张DoG图像与下一个octave的第一张DoG图像的尺度比值也是$k$.</p><p>至于$\sigma_0$同样也是实验值, 在Lowe的实验中取1.6为优. 当$\sigma_0$与$k$都确定后, 金字塔中的每一张图像在尺度空间的坐标就都确定了.</p><p>另外为了获取更多的极值点, 通常会先对原图像做插值(论文中为双线性插值), 将图像的尺寸扩大一倍再作为初始图像进行后续处理.</p><h4 id="番外-关于LoG与DoG"><a href="#番外-关于LoG与DoG" class="headerlink" title="番外 关于LoG与DoG"></a>番外 关于LoG与DoG</h4><p>这一部分简单聊聊DoG. 在SIFT中关键点检出使用的是DoG(Difference of Gaussian), DoG被证明是LoG(Laplacian of Gaussian)的良好近似. 这部分在《数字图像处理 第三版》P.459关于图像边缘检测算子中有涉及到.</p><p>LoG是Marr和Hildreth在1980年提出的边缘检测算子, 其本质上是二阶导数的近似(边缘检测算子可用二阶导数), 另一方面LoG算子可以自由调整大小, 在任何图像尺度上都能取得比较好的检测效果. LoG形式上可对二维高斯函数的二阶导数进行采样得到. </p><script type="math/tex; mode=display">L(x,y,\sigma) = G(x,y,\sigma)\ast I(x,y)</script><script type="math/tex; mode=display">G(x,y, \sigma) = \frac{1}{2\pi\sigma^2}\exp\left(-\frac{(x^2+y^2)}{2\sigma^2}\right)</script><p>但LoG的缺点是为了求得零交叉点(Laplace算子通过对图像求取二阶导数的零交叉点(zero-cross)来进行边缘检测)需要经过比较麻烦的像素比较操作. 于是Marr和Hildreth在当时提出用DoG(高斯差分)近似LoG的运算. DoG函数定义如下</p><script type="math/tex; mode=display">D(x,y,\sigma) = L(x,y,k\sigma) - L(x,y,\sigma)</script><p>故在构建DoG金子塔时是对不同尺度高斯模糊后的图像做差来产生DoG图像.</p><h4 id="番外-高斯函数卷积性质"><a href="#番外-高斯函数卷积性质" class="headerlink" title="番外 高斯函数卷积性质"></a>番外 高斯函数卷积性质</h4><p>在构建高斯模糊金子塔时有一个细节, 比如一octave假设各图像尺度空间坐标为$\sigma_0, k\sigma_0$, 这意味着此octave中的图像是对<strong>初始图像</strong>分别做方差为$\sigma_0,k\sigma_0$的高斯模糊得到的, 在这种情况下, 我们是视初始图像的空间尺度坐标为0, 但尺度空间的建立不能从尺度为0开始(尺度坐标是成比例增长, 起始不能为0, 另一种解释是在尺度空间理论中视尺度坐标为0时图像是无限精细不可获得), 故在Lowe的论文中假设我们获得的初始图像是已经经过了方差为0.5的高斯模糊处理.</p><p>那么此时我们的问题变成如何从尺度为$\sigma_0$的图像得到尺度为$\sigma_1$与尺度为$\sigma_2$的图像, 用式子来描述就是</p><script type="math/tex; mode=display">I(x,y,\sigma_0)=I(x,y)*G(x,y,\sigma_0) \\I(x,y,k\sigma_0)=I(x,y)*G(x,y,k\sigma_0)</script><p>利用高斯函数的卷积性质</p><script type="math/tex; mode=display">G(x,y,k\sigma_0)=G(x,y,\sigma_0)*G(x,y,\sqrt{(k\sigma)^2-\sigma^2})</script><p>我们有</p><script type="math/tex; mode=display">I(x,y,k\sigma_0)=I(x,y,\sigma_0)*G(x,y,\sqrt{(k\sigma_0)^2-\sigma_0^2})</script><p>顺便一说, 这个性质可以使用傅里叶的卷积性质来证, 具体可参考<a href="http://www.tina-vision.net/docs/memos/2003-003.pdf">《Products and Convolutions of Gaussian Probability Density Functions》</a>, 证明过程也比较简洁这里就不重复写了.</p><h3 id="寻找局部极值点"><a href="#寻找局部极值点" class="headerlink" title="寻找局部极值点"></a>寻找局部极值点</h3><p>构造完DoG金字塔后, 就可以寻找极值点了. 具体来说, 把同一octave经过不同尺度高斯滤波后的图像按尺度大小堆成一叠(如下图), 若当前像素值(×)比邻近的26个位置(包括相邻上下两张图像)都大, 则作为一个局部极大值, 同样可以找到局部最小值.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sift_detection_maximum.png" style="zoom:40%" class alt="极值点检测" /></p><p>这里比较求局部极值点引出了另一个细节问题, 即一个octave中的第一张图像与最后一张图像中的像素无法进行3×3的比较得到极值, 如果是按之前说的高斯模糊金字塔每octave生成$(s+1)$张图像, 做差值后将生成$s$张DoG图像, 那么极值的寻找实际只在$(s-2)$个尺度上进行.</p><p>为了在每层中检测$s$个尺度的极值点, 简单粗暴的做法是再补上两张高斯模糊后的图像, 也就是高斯模糊金字塔一个octave生成$(s+3)$张图像, 然后在$s+1$张DoG图像的第2张到倒数第2张之间的图像像素中去寻找极值点. 如下图</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/add2.png" style="zoom:40%" class alt="增加模糊图像" /></p><p>同时为了保证之前DoG图像尺度坐标变化的连续性, 在降采样建立高斯模糊金字塔时, 每一个octave的第一张图像由前一个octave的<strong>倒数第三张</strong>图像降采样得到(原来是使用最后一张).</p><h4 id="番外-寻找极值点的实现细节"><a href="#番外-寻找极值点的实现细节" class="headerlink" title="番外 寻找极值点的实现细节"></a>番外 寻找极值点的实现细节</h4><p>在openCV的源码中, 判断极值点的第一步是看其是否高于一个阈值, 像素点过小的点会直接忽略. 在进行像素值比较时使用的是大于等于与小于等于, 这样能够检测到更多的极值点.</p><h3 id="局部极值点筛选"><a href="#局部极值点筛选" class="headerlink" title="局部极值点筛选"></a>局部极值点筛选</h3><p>这一步的主要目的是对之前得到的极值点进行测试, 看其是否足够稳定. 主要进行如下两步, 极值点精确定位与消除边缘效应.</p><h4 id="极值点精确定位"><a href="#极值点精确定位" class="headerlink" title="极值点精确定位"></a>极值点精确定位</h4><p>由于之前所找的极值是在离散空间中进行的, 所以找到的极值未必与真实极值位置相同. 论文提出可以通过对尺度空间DoG函数进行曲线拟合寻找极值点来减小这种误差. 利用已知的离散空间点插值得到的连续空间极值点的方法叫做子像素插值(Sub-pixel Interpolation).</p><p>这里有一个一元函数的例子说明求精确极值原理, 参考<a href="https://www.cnblogs.com/pakfahome/p/3598983.html">博文</a>.</p><p>方法中用到了泰勒(Taylor)展开, 一元函数形式在$x_{0}$的泰勒展开公式如下</p><script type="math/tex; mode=display">f(x) \approx f(x_0)+(x-x_0)f'(x_0)+\frac{1}{2!}(x-x_0)^2f''(x_0)</script><p>而对于二元函数, $f(x, y)$在$(x_{0}, y_{0})$处泰勒展开式为</p><script type="math/tex; mode=display">f(x,y)\approx f(x_0,y_0)+[(x-x_0)f'_x(x_0,y_0)+(y-y_0)f'_y(x_0,y_0)] \\+\frac1{2!}\left[(x-x_0)^2f''_{xx}(x_0,y_0)+(x-x_0)(y-y_0)f''_{xy}(x_0,y_0) \\+(x-x_0)(y-y_0)f''_{yx}(x_0,y_0)+(y-y_0)^2f''_{yy}(x_0,y_0)\right]\\</script><p>上式可写成向量形式, 注意$dx=x-x_0$, $dy=y-y_0$</p><script type="math/tex; mode=display">f(x, y)\approx f(x_0, y_0)+[dx\ \ dy]\left[ f_x(x_0,y_0) \atop f_y(x_0,y_0)\right] \\+\frac{1}{2!}[dx\ \ dy]\left[ f_{xx}(x_0,y_0)\ \ f_{xy}(x_0,y_0) \atop f_{yx}(x_0,y_0)\ \  f_{yy}(x_0,y_0)\right]\left[ dx \atop dy\right]</script><p>若令$\boldsymbol{x}=\left[x \atop y\right]$, 且$\boldsymbol{x}_0=\left[x_0 \atop y_0\right]=\boldsymbol{0}$, 有</p><script type="math/tex; mode=display">f(\boldsymbol{x})\approx f(\boldsymbol{x}_0)+\left.\frac{\partial f^{T}}{\partial\boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}\boldsymbol{x}+\frac{1}{2!}\boldsymbol{x}^{T}\left.\frac{\partial^{2} f}{\partial\boldsymbol{x}^2}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}\boldsymbol{x}</script><p>同样这里是对$D(x,y,\sigma)$进行泰勒展开, 如果写成向量形式则$\boldsymbol{x}$是三维列向量, 同样这里$\boldsymbol{x}_0=\boldsymbol{0}$</p><script type="math/tex; mode=display">D(\boldsymbol{x})\approx D(\boldsymbol{x}_0)+\left.\frac{\partial D^{T}}{\partial\boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}\boldsymbol{x}+\frac{1}{2!}\boldsymbol{x}^{T}\left.\frac{\partial^{2} D}{\partial\boldsymbol{x}^2}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}\boldsymbol{x}</script><script type="math/tex; mode=display">\frac {\partial D}{\partial \boldsymbol{x}} = \begin{bmatrix} \frac {\partial D}{\partial x} \\ \frac {\partial D}{\partial y} \\ \frac {\partial D}{\partial \sigma}\end{bmatrix} =\begin{bmatrix} \frac {D(x+1,y,\sigma) - D(x-1,y,\sigma)}{2} \\ \frac {D(x,y+1,\sigma)-D(x,y-1,\sigma)}{2} \\ \frac {D(x,y,\sigma+1) - D(x,y,\sigma-1)}{2} \end{bmatrix}</script><script type="math/tex; mode=display">\frac{\partial^2 D}{\partial \boldsymbol{x}^2} = \begin{bmatrix} D_{xx} & D_{xy} & D_{x\sigma} \\D_{yx} & D_{yy} & D_{y\sigma} \\D_{\sigma x} & D_{\sigma y} & D_{\sigma\sigma} \end{bmatrix}</script><p>其中</p><script type="math/tex; mode=display">D_{xx}=\frac { \frac{D(x+1,y,s)-D(x,y,s)}{1} - \frac{D(x,y,s)-D(x-1,y,s)}{1} }{1}</script><script type="math/tex; mode=display">D_{xy} = \frac { \frac{D(x+1,y+1,s)-D(x-1,y+1,s)}{2} - \frac{D(x+1,y-1,s)-D(x-1,y-1,s)}{2} }{2}</script><p>为求极值, 两边对$\boldsymbol{x}$求导, 并令其为0, 这部分需要用到<strong>数量函数对向量变量求导</strong>的知识(可参考博文<a href="https://www.cnblogs.com/hellcat/p/7216399.html">矩阵求导</a>与<a href="https://en.wikipedia.org/wiki/Matrix_calculus">Matrix calculus</a>)</p><script type="math/tex; mode=display">\begin{align}\frac{\partial D}{\partial\boldsymbol{x}}&=\left.\frac{\partial D^T}{\partial\boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}+\frac{1}{2}\left(\left.\frac{\partial^2 D}{\partial\boldsymbol{x}^2}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}+\left.\frac{\partial^2 D^T}{\partial\boldsymbol{x}^2}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}\right)\boldsymbol{x} \\&=\left.\frac{\partial D^T}{\partial\boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}+\left.\frac{\partial^2 D}{\partial\boldsymbol{x}^2}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}\boldsymbol{x}\\&=\boldsymbol{0}\end{align}</script><p>可解出精确极值点$\boldsymbol{x}$</p><script type="math/tex; mode=display">\hat{\boldsymbol{x}}=-\left.\frac{\partial^2 D^{-1}}{\partial\boldsymbol{x}^2}\frac{\partial D}{\partial\boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}</script><p>于是可求出精确极值$D(\hat{\boldsymbol{x}})$. </p><script type="math/tex; mode=display">D(\hat{\boldsymbol{x}})\approx D(\boldsymbol{x}_0)+\frac{1}{2}\left.\frac{\partial D^{T}}{\partial\boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{x}_0}\hat{\boldsymbol{x}}</script><p>这里再说明一下, 事实上这里的$\boldsymbol{x}$准确说应该是极值点$\boldsymbol{x}$与$\boldsymbol{x}_0$的差值, 即极值点的偏移量$\boldsymbol{x}-\boldsymbol{x}_0$, 但由于这里假设了$\boldsymbol{x}_0=\boldsymbol{0}​$所以才得到上述结果. </p><p>接下来就是根据计算出的精确极值筛选之前找到的极值点了, 主要基于两点: 一是之前找到的极值点偏离精确极值点的程度, 二是找到精确极值点极值的幅度大小.</p><p>对于第一点, 只要精确极值点在任意方向$(x, y,\sigma)$偏离找到的极值点某个阈值则剔除该极值点, 通常这个阈值在论文中取0.5; 对于第二点, 只要$|D(\hat{\boldsymbol{x}})|$小于某个阈值则剔除该极值点, 响应值过小易受噪声干扰, 视为不稳定点, 通常这个阈值在论文中取0.03(图像灰度归一化为[0,1]). 下面是经过这一步筛选前后的效果</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/check1.png" style="zoom:40%" class alt="精确定位筛选前后对比" /></p><h4 id="消除边缘效应"><a href="#消除边缘效应" class="headerlink" title="消除边缘效应"></a>消除边缘效应</h4><p>利用DoG得到的极值点还有另一个问题, 之前说LoG是对二阶导数的近似, 而DoG又是LoG的近似, DoG的处理会使得边缘像素点的响应很大(边缘效应), 接下来就是要尽可能地去除边缘关键点, 而保留下其他信息较大的点如角点. </p><p>为了找到和去除那些边缘响应过大的像素点, 需要利用那些像素点在DoG响应曲面(准确说是空间离散点集)上的特性. 比如曲面在该像素点处两主曲率(Principal curvature)的差值会很大(大的主曲率会很大, 小的主曲率会很小), 而对于角点的关键点两主曲率差值较小(大的主曲率与小的主曲率都会很大). </p><p>由于<strong>两主曲率与极值点处的Hessian矩阵特征值成比例</strong>(微分几何结论, 关于主曲率相关知识可参考<a href="http://www.cad.zju.edu.cn/home/vagwiki/index.php/Vag_guide_9">这个</a>), 同时我们将考察两主曲率的差值转化为考察比值, 这样可以利用矩阵特征值性质去求而避免了直接去求Hessian矩阵特征值. </p><p>Hessian矩阵定义: </p><script type="math/tex; mode=display">H(D(\boldsymbol{x}))\overset{def}{=}\frac{\partial^{2} D(\boldsymbol{x})}{\partial\boldsymbol{x}\partial\boldsymbol{x}^T}=\begin{bmatrix} D_{xx} & D_{xy} \\ D_{xy} & D_{yy} \end{bmatrix}</script><p>设Hessian矩阵的两特征值为$\alpha, \beta$, 且$\alpha&gt;\beta$, 令$\alpha=\gamma\beta$有</p><script type="math/tex; mode=display">\begin{aligned} \text{tr} (H) &= D_{xx} + D_{yy} = \alpha + \beta \\\det (H) &= D_{xx}D_{yy} - (D_{xy})^2 = \alpha \beta \end{aligned}</script><script type="math/tex; mode=display">\frac{\text{tr}(H)^2}{\det(H)} = \frac{(\alpha + \beta)^2}{\alpha \beta} = \frac{(\gamma \beta + \beta)^2 }{\gamma  \beta^2} = \frac{(\gamma+1)^2}{\gamma} = \gamma+\frac{1}{\gamma}+2</script><p>上式当两特征值相同时即$\gamma=1$时最小, 当两特征值差越大, 上式值也越大. 所以可以用$\gamma$再设置一个阈值, 间接地排除两主曲率相差悬殊的关键点(即边缘响应较大的关键点), $\gamma$这个阈值在Lowe论文中设置为了10.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/check2.png" style="zoom:40%" class alt="消除边缘效应前后对比" /></p><h4 id="番外-在极值点筛选过程实现上的细节"><a href="#番外-在极值点筛选过程实现上的细节" class="headerlink" title="番外 在极值点筛选过程实现上的细节"></a>番外 在极值点筛选过程实现上的细节</h4><p>这一步总得来说细节比较少, 首先需要注意的是一阶, 二阶, 二阶混合离散导数的计算, 特别注意这些导数的分母, 另外在进行阈值判断时需要注意是否需要将像素值归一到[0,1].</p><p>在openCV的实现源码中, 求精确极值时并不是偏移量超过阈值就之间去除, 而是会有5次调整机会, 即原坐标加上偏移后再按如上步骤计算偏移看是否这次满足阈值. 实现在一些阈值上也有些许改动.</p><h2 id="关键点描述"><a href="#关键点描述" class="headerlink" title="关键点描述"></a>关键点描述</h2><p>经过筛选考验的极值点就作为关键点保留下来了, 接着我们需要对每个关键点生成一个描述信息或称为描述子(Descriptor). SIFT作为一种局部特征, 即在计算描述子时需要用到关键点周围邻域的像素信息. 但在此之前需要给每个关键点分配一个主方向.</p><h3 id="关键点主方向"><a href="#关键点主方向" class="headerlink" title="关键点主方向"></a>关键点主方向</h3><p>给关键点分配主方向也是SIFT区别于其他特征的地方, 是SIFT特征拥有旋转不变性的原因之一. 关键点的主方向确定了在计算特征描述时使用的是关键点邻域内的哪些像素. 具体做法如下,</p><p>对于某一个关键点所在尺度$\sigma​$的高斯滤波后的图像可表达为</p><script type="math/tex; mode=display">L(x,y, \sigma) = G(x,y, \sigma) \ast I(x,y)</script><p>计算以关键点为中心, 以$3 \times \sqrt{2} \sigma​$(为了圆域包含内接正方形)为半径的区域(圆域)内的所有图像像素点的<strong>梯度幅角和幅值</strong>, 图像某点处梯度定义为</p><script type="math/tex; mode=display">grad\ L(x,y, \sigma)=\begin{bmatrix} \frac{\partial L}{\partial x}  \\ \frac{\partial L}{\partial y} \end{bmatrix}</script><p>梯度幅角与幅值计算公式如下</p><script type="math/tex; mode=display">\begin{aligned} m(x,y) & = \sqrt{(L(x+1,y) - L(x-1,y))^2 + (L(x, y+1) - L(x, y-1))^2} \\\theta(x,y) &= \arctan \left(\frac{L(x, y+1) - L(x, y-1)}{L(x+1,y) - L(x-1,y)} \right) \end{aligned}</script><p>完成关键点邻域内梯度幅角与幅值计算后, 将幅角的360°每10°为一个区间划分作为横轴, 共36个区间, 纵轴为在对应幅角区间内的像素点幅值累加, 可做出方向统计直方图.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/orientationhistogram1.png" style="zoom:40%" class alt="方向统计直方图" /></p><p>为改善特征仿射不变性的性能, 一般将累加的像素点幅值用高斯函数加权, 高斯加权函数的$\sigma​$取为关键点尺度的1.5倍. 经过高斯加权即让靠近关键点的像素幅值具有更大的权重(以此改善仿射不变性).</p><p>先对得到的直方图进行平滑(如加权滑动平均), 然后将直方图的峰值对应的幅角即作为该关键点的主方向(dominant orientation). 此时得到的主方向为一个10°区间的范围, 需要进行插值拟合(如抛物线拟合), 从离散值直方图得到较精确的关键点主方向.</p><p>如果像上图这样, 有另一个峰值超过最高峰值的80%, 那么这个方向应该作为一个参考的辅方向保留. <strong>在实际操作中, 就直接把关键点再复制一份(拥有与原关键点相同的尺度与位置), 新关键点的主方向取这个参考的辅方向.</strong> 一个关键点是可能有多个辅方向的.</p><h3 id="特征向量生成"><a href="#特征向量生成" class="headerlink" title="特征向量生成"></a>特征向量生成</h3><p>一个关键点点所包含的信息由特征描述子(Feature Descriptor)数值描述. 在SIFT中, 特征描述子是从关键点与其主方向确定的区域提取得到的一个128维特征向量. </p><p>为了确定最后提取特征向量需要的像素, 首先要确定提取特征向量的像素区域大小. 将以关键点为中心的邻域划分成 $4\times 4$个子区域, 每个子区域的尺寸为3σ(3$\sigma$原则)个像素, $\sigma$即关键点所在图像尺度空间坐标.</p><h4 id="确定特征提取像素区域"><a href="#确定特征提取像素区域" class="headerlink" title="确定特征提取像素区域"></a>确定特征提取像素区域</h4><p>确定了<strong>提取特征的像素区域</strong>(方域)大小后, 还要确定提取特征区域以关键点为中心的旋转角度. 为便于说明, 在提取特征向量的区域建立坐标轴, x轴正向朝右, y轴正向朝下, 而坐标原点位于关键点处. 在此坐标轴下, 比如计算特征向量像素区域(这是一个方域)的最左上角位置的坐标为$(x, y)$(<strong>注意这是以关键点为原点的相对坐标</strong>). 接着将计算特征向量像素区域顺时针旋转至该关键点的主方向(如图), 旋转后之前最左上角的像素坐标变为$(x’, y’)$. 这个过程可由坐标变换公式描述为</p><script type="math/tex; mode=display">\left[ \begin{matrix} x’\\ y’\end{matrix} \right] = \left[ \begin{matrix} cos\theta & -sin\theta \\ sin\theta & cos\theta \end{matrix} \right] \left[ \begin{matrix} x\\ y \end{matrix} \right]</script><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/rotateaxis.png" style="zoom:40%" class alt="旋转区域" /></p><p>用上式就可以通过遍历旋转前区域内像素坐标值, 经上式转化后, 即遍历的是区域旋转后的像素位置, 这样避免了直接去求旋转后区域内的像素坐标.</p><h4 id="计算特征向量"><a href="#计算特征向量" class="headerlink" title="计算特征向量"></a>计算特征向量</h4><p>特征向量的计算依旧需要用到统计梯度直方图. 具体来说, 统计$4\times4$个子区间内像素(16个像素)的梯度(幅值与幅角), 与之前相同, <strong>将幅角从360°每45°为一个区间划分</strong>作为横轴, 共8个区间, 纵轴为在对应幅角区间内的像素点幅值累加, 以此作出统计直方图. </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/orientationhistogram2.png" style="zoom:40%" class alt="方向直方图" /></p><p>这样每个子区域都包含8个柱状图的信息, 所以对于一个关键点可提取$4\times 4\times8$共128维的特征向量.</p><p>这里有比较多的细节问题, 首先注意计算主方向时的邻域范围与计算特征向量时是不同的, 至于具体怎么取取决于实现. </p><p>另外一个比较难理解的地方是直方图的插值问题. 此处我们有三个坐标, 即除图像行列外还有一个梯度幅角的坐标, 故使用三线性插值来处理. 这意味当一个像素点落在某个子区域时, 我们其实并不是直接将该像素点的梯度幅值加到所属子区域的方向直方图中, 而是通过某种方式得到权值, 通过权值贡献到离该像素点幅值最近的4个子区域的直方图, 这里<strong>计算权值的方式就是利用了三线性插值的性质</strong>. 不仅是贡献到4个子区域有权值, 而且离像素梯度幅角最近的两个角度区间也需要计算权值. 直观上来说的话, 类似于我们得到了立方体内一点的值, 将其值贡献到其8个顶点上去.</p><h5 id="番外-关于三线性插值"><a href="#番外-关于三线性插值" class="headerlink" title="番外 关于三线性插值"></a>番外 关于三线性插值</h5><p>一般意义上的<a href="https://en.wikipedia.org/wiki/Trilinear_interpolation">三线性插值(Trilinear interpolation)</a>指的是已知空间8个点的值, 去得到以8个点为顶点的长方体内任意一点的值, 本质上是连续进行7次线性插值得到内点的值.</p><p>在这里, 更像是三线性插值的逆用(或者有更专业的说法?), 已知空间一点的值得到包含该点立方体8个顶点的值. 利用三线性插值的性质, 可以求出分配到各顶点的权值.</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/trilinear.png" style="zoom:40%" class alt="三线性插值性质" /></p><p>如上图所示, 像素点作为内点, 其对邻近四子区域幅度权值为以内点到顶点连线为体对角线的长方体体积占8顶点构成长方体体积之比.</p><h4 id="特征向量的优化"><a href="#特征向量的优化" class="headerlink" title="特征向量的优化"></a>特征向量的优化</h4><p>为了改善特征受光照条件的影响, 需要对特征向量做优化. 对于线性光照改变, 由于SIFT使用的是DoG, 故特征对于对比度变化天生具有鲁棒性, 对于改善光照强度影响这里的处理是对特征向量进行归一化处理. 而对于非线性的光照改变, 只能通过抑制过大的特征向量的分量来改善, 使特征向量各分量差距不过于悬殊.</p><h1 id="SIFT的实现"><a href="#SIFT的实现" class="headerlink" title="SIFT的实现"></a>SIFT的实现</h1><p>我一开始是用Matlab初步实现了SIFT算法, 完成调试后又用C++进行重写, 尽可能地保留程序的易读性, 在部分具体实现中参考了openCV中对SIFT的优化改进方法, 包括阈值的设定, 一些特定变量的使用(如利用Mat进行矩阵运算). 但总得来说, 效率上依然不如openCV, 或许还有改进空间. 代码放在<a href="https://github.com/Granvallen/SIFT">GitHub</a>, 在openCV3.4环境下正常运行.</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ul><li><p>Lowe1999年论文<a href="http://www.cs.ubc.ca/~lowe/papers/iccv99.pdf">《Object Recognition from Local Scale-Invariant Features》</a></p></li><li><p>Lowe2004年完善SIFT后的论文<a href="http://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf">《Distinctive Image Features from Scale-Invariant Keypoints》</a></p></li><li><p><a href="https://en.wikipedia.org/wiki/Scale-invariant_feature_transform">SIFT wiki</a></p></li><li><p><a href="https://chtseng.wordpress.com/2017/05/06/%E5%9C%96%E5%83%8F%E7%89%B9%E5%BE%B5%E6%AF%94%E5%B0%8D%E4%B8%80-%E5%8F%96%E5%BE%97%E5%BD%B1%E5%83%8F%E7%9A%84%E7%89%B9%E5%BE%B5%E9%BB%9E/">图像特征比对(一)-取得影像的关键点</a> 关键点检测一些概念的理解</p></li><li><a href="http://aandds.com/blog/img-feature-detector.html#org9cc6832">Local Invariant Feature Detectors</a><br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;我又回来了, 总算熬到放假了. . . 按照国际惯例先凑字数扯点近况.&lt;/p&gt;</summary>
    
    
    
    <category term="Lab" scheme="https://granvallen.github.io/categories/Lab/"/>
    
    <category term="计算机视觉" scheme="https://granvallen.github.io/categories/Lab/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%A7%86%E8%A7%89/"/>
    
    
    <category term="C/C++" scheme="https://granvallen.github.io/tags/C-C/"/>
    
    <category term="Matlab" scheme="https://granvallen.github.io/tags/Matlab/"/>
    
    <category term="SIFT" scheme="https://granvallen.github.io/tags/SIFT/"/>
    
  </entry>
  
  <entry>
    <title>写在研究生生活之前</title>
    <link href="https://granvallen.github.io/nichijou180918/"/>
    <id>https://granvallen.github.io/nichijou180918/</id>
    <published>2018-09-18T15:12:00.000Z</published>
    <updated>2021-02-24T05:56:09.395Z</updated>
    
    <content type="html"><![CDATA[<p>额, 坦白说已经开学两个多星期了, 用这个标题真的没问题吗? 算了不管了.</p><span id="more"></span><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=468490581&auto=0&height=66"></iframe><p>总之, 研究生生活开始了. 作为一个渣渣, 我在开学后才确定导师(之前联系老师邮件基本不回), 上大这边的情况好像是大家都不太愿意去光纤组, 虽然光纤组很强老师又多, 在开学典礼上, 院长(光纤组组长)也强调了这个问题. 相反某些热门方向的老师不论资辈学生竞争激烈. 我个人是希望能做图像处理偏软件方向的, 好在天无绝人之路, 收到消息一个相关方向的老师还缺一个名额, 于是发了邮件, 过去聊了聊, 老师很爽快当场就定下了, 所以机遇这种事情吧, 就看自己有没有做好抓住机遇准备. 话说我在和老师聊的过程中陆续还有几个同学找导师过来, 当时我和老师已经谈妥并签下了双向协议, 老师只好当场一一回绝, 弄得很是尴尬. 总之各种机缘巧合, 终于能安下心来继续求学了.</p><p>我们这届被安排在延长, 自然各种不方便, 去食堂吃个饭加走路来回估计要三四十分钟, 去宝山的实验室公交车程要四五十分钟. 虽说也有校车, 但供不应求啊, 即使早到20分钟等车, 也面临着可能校车坐满先走白等的风险, 没有任何通知信息, 靠谱程度实在不如公交…记得一本校舍友的老师曾这么和他们这么说(那时通院的实验室还没有搬去宝山)</p><blockquote><p>以后等实验室搬到宝山你们就不用去延长这么麻烦了</p></blockquote><p>结果就是实验室搬到了宝山, 而我们被安排搬去延长住了…不过平心而论, 延长的住宿条件还是不错, 除了1到3层洗不上热水澡以及不怎么连得上的校园网. </p><p>实验室环境也还是不错的, 最近研三师姐们都忙着笔试面试找工作. 然后就是承担了一个偏工程的项目, 说白了就是用网页展现一个数据库的内容, 所以正在学习用Python写后端, 另外可能有机会玩一玩Xbox的Kinect.</p><p>暑假的时候摸出了<a href="https://github.com/Granvallen/bilibili-api">bilibili-api</a>和<a href="https://github.com/Granvallen/blockwar">Blockwar</a>这两个项目, 有空的话聊聊这段折腾的过程.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;额, 坦白说已经开学两个多星期了, 用这个标题真的没问题吗? 算了不管了.&lt;/p&gt;</summary>
    
    
    
    <category term="Nichijou" scheme="https://granvallen.github.io/categories/Nichijou/"/>
    
    <category term="杂文" scheme="https://granvallen.github.io/categories/Nichijou/%E6%9D%82%E6%96%87/"/>
    
    
  </entry>
  
  <entry>
    <title>浅谈线性回归</title>
    <link href="https://granvallen.github.io/linearregression/"/>
    <id>https://granvallen.github.io/linearregression/</id>
    <published>2018-04-30T07:30:00.000Z</published>
    <updated>2021-02-24T06:23:10.872Z</updated>
    
    <content type="html"><![CDATA[<p>之前学着用过线性回归的一些东西, 这里稍微整理下, 虽然是比较基础的内容, 希望能理出一个比较清晰的方法思路来, 如果能对到其他人理解这个问题有帮助那再好不过了:)</p><span id="more"></span><p>其实内容不多, 这里首先谈谈问题的背景引入回归这个概念, 以及他要解决的问题, 然后是今天的主角—<strong>线性回归</strong>方法, 从简单的一元函数开始说明, 再推广到实际应用上更一般的多元线性回归. <strong>最小二乘法</strong>是最常用来解决线性问题的方法之一, 说穿了其实是一种参数估计方法, 是估计线性方程中的未知权向量$\boldsymbol{\beta}$的一种方法, 我们还会给出最小二乘法的矩阵运算形式, 这将便于我们编程来实现算法.</p><p>理论方面的内容主要参考了陈希孺教授的<a href="https://book.douban.com/subject/2201479/">《概率论与数理统计》</a>, 这是一本值得安利的教材, 难能可贵的是这书不是公式和定义堆砌而成的”工具书”, 作者更想传授的是一种统计学特有的思考方式. 同时也有参考其他讨论这个问题的资料, 比如也引入了一些统计学习语境下的观点与术语, 顺便统一了一下符号的表示方法.</p><h1 id="回归分析"><a href="#回归分析" class="headerlink" title="回归分析"></a>回归分析</h1><p>回归分析这是统计学应用的一个分支, 下面是从wiki百科上摘下来的解释:</p><blockquote><p>回归分析(Regression Analysis)是一种统计学上分析数据的方法, 目的在于了解两个或多个变量间是否相关、相关方向与强度, 并建立数学模型以便观察特定变量来预测研究者感兴趣的变量. 更具体的来说, 回归分析可以帮助人们了解在只有一个自变量变化时因变量的变化量. 一般来说, 通过回归分析我们可以由给出的自变量估计因变量的条件期望. </p></blockquote><p>回归可以理解为对于有相关性的变量, 通过建立数学模型(函数)去对变量之间的关系进行刻画的过程. 比如人的年龄与身高可能与体重存在某种相关性, 抽象出一个数学模型, 记$x^{(1)}$为一个的年龄, $x^{(2)}$为这个人的身高(之所以不用$x_{1}$与$x_{2}$是为了避免与之后的样本的右下标号弄混), 而$y$是体重, 这三个变量之间的相关性可能存在一个函数关系即:</p><script type="math/tex; mode=display">y=f(x^{(1)},x^{(2)})+e</script><p>$e$是一种随机误差, 我们通常假设$e$是服从于均值为0, 方差为$\sigma^{2}$的正态分布.</p><p>回归分析的一个主要应用就是预测, 在这个例子里, 假如我们得到了这个$f$, 那么对于任何一个人, 只要报出他的年龄与身高就可以根据这个函数求出体重. 当然以上说的只是一种理想化, 事实上由于存在随机性与噪声, 我们永远也无法真正得到$y$, 再者与体重相关的变量因素可能不单单只有年龄与身高这么简单, 所以这种相关性的刻画是近似的, 而不是严格的. 我们只能估计得到$f$计算其函数值, 而希望$f$的值尽可能与$y$近似.</p><p>但是这个思路仍是有用的, 只是需要合理的处理变量与把误差尽可能限制在能够接受的范围内. 解决问题的关键往往不在消除误差, 而是使误差可控. 从上面的例子再抽象到一般, 假设我们认为有$p$个变量与$y$有关, 则可以表达为:</p><script type="math/tex; mode=display">y=f(x^{(1)},x^{(2)},\dots,x^{(p)})+e</script><p>其中$f$被称为”回归函数”, 统计回归的目的就是去估计这个函数. 由于回归常用于预测问题, 故这里的$x^{(1)},x^{(2)},\dots,x^{(p)}$也被称为<strong>预测因子或预测量</strong>(predictors), 这是统计学中术语, 在应用中也称为<strong>特征</strong>(features), 这在模式识别(PR, Pattern Recognition)问题中更为常用. </p><p>为了去估计$f$, 样本是必要的, 样本(sample)是采样得到的一组组数据, 或者说是观测值(observation) 每一个样本都包含了$p$个变量与$y$的值, 样本中蕴藏着这个模型(或者说函数$f$)的信息. 从统计机器学习的角度看回归是一种监督学习(supervised learning), 从样本信息中”学习”得到我们需要的模型.</p><p>以上我们一直在说模型, 函数, 那么我们对于$f$究竟有多少了解? 根据不同了解情况, 可以简单地把回归分成两类, 若对$f$没有任何的了解, 无法给出$f$数学形式的任何<strong>假定</strong>(assumption), 这种情况称”无参数回归”, 不做讨论; 另一种则更常用, 即先假定了$f$的数学形式, 只是这个函数或者说模型中存在一些尚未确定的参数, 这些参数影响着$f$的性态, 此时求解回归问题的更具体地说, 即是通过样本或者说观测值, 去估计这些未知参数, 即熟悉的参数估计问题(点估计或区间估计), 这也称为”参数回归”. 下面要说到的线性回归即是<strong>假定$f$为线性函数</strong>所进行的参数回归, 之所以重视这种情况, 是因为这是应用上最广泛, 理论发展最完善的特例之一.</p><p>既然是参数估计问题就离不开采样. 假设有$N$个样本, 记为${(\boldsymbol{x}_{1},y_{1}),(\boldsymbol{x}_{2},y_{2}),\dots,(\boldsymbol{x}_{N},y_{N})}$:</p><script type="math/tex; mode=display"> \boldsymbol{x_{i}}= \begin{bmatrix}  x_{i}^{(1)} \\  x_{i}^{(2)} \\  \vdots \\  x_{i}^{(p)} \\  \end{bmatrix}_{p\times1}</script><script type="math/tex; mode=display">y_{i}=f(\boldsymbol{x_{i}})+e_{i}\qquad i=1,2,\dots,N</script><p>可以用矩阵简洁地表达为:</p><script type="math/tex; mode=display">\boldsymbol{y}=f(\boldsymbol{X})+\boldsymbol{e}</script><p>其中:</p><script type="math/tex; mode=display">\boldsymbol{y}=\begin{bmatrix} y_{1} \\ y_{2} \\ \vdots \\ y_{N} \\ \end{bmatrix}_{N\times1}</script><script type="math/tex; mode=display">\boldsymbol{X}=\begin{bmatrix} \boldsymbol{x}_{1}^{T} \\ \boldsymbol{x}_{2}^{T} \\ \vdots \\ \boldsymbol{x}_{N}^{T} \\  \end{bmatrix}_{N\times p}</script><script type="math/tex; mode=display"> \boldsymbol{e}= \begin{bmatrix}  e_{1} \\  e_{2} \\  \vdots \\  e_{N} \\  \end{bmatrix}_{N\times1}</script><h1 id="线性回归"><a href="#线性回归" class="headerlink" title="线性回归"></a>线性回归</h1><p>就像之前说的, 线性回归是<strong>假定$f$为线性方程</strong>所进行的参数回归. 所谓”线性方程”, 是指$f$有如下形式:</p><script type="math/tex; mode=display">f(x^{(1)},x^{(2)},\dots,x^{(p)})=b_{0}+b_{1}x^{(1)}+b_{2}x^{(2)}+\dots+b_{p}x^{(p)}</script><p>然后采样$N$次, 对于第$i$次采样得到</p><script type="math/tex; mode=display">y_{i}=b_{0}+b_{1}x_{i}^{(1)}+b_{2}x_{i}^{(2)}+\dots+b_{p}x_{i}^{(p)}+e_{i}\qquad i=1,2,\dots,N</script><p>这里有一个问题需要先说明, 我们通常需要对采样得到的数据进行”中心化”, 具体操作为把上式改写为:</p><script type="math/tex; mode=display">y_{i}=\beta_{0}+\beta_{1}z_{i}^{(1)}+\beta_{2}z_{i}^{(2)}+\dots+\beta_{p}z_{i}^{(p)}+e_{i}\qquad i=1,2,\dots,N</script><p>可以求得:</p><script type="math/tex; mode=display">z^{(k)}_{i}=x_{i}^{(k)}-\bar{x}^{(k)}\qquad i=1,2,\dots,N;k=1,2,\dots,p</script><script type="math/tex; mode=display">\bar{x}^{(k)}=\frac{1}{N}\sum^{N}_{i=1}x^{(k)}_{i}\qquad k=1,2,\dots,p</script><script type="math/tex; mode=display">\beta_{k}=b_{k}\qquad k=1,2,\dots,p</script><script type="math/tex; mode=display">\beta_{0}=b_{0}+b_{1}\bar{x}^{(1)}+\dots+b_{p}\bar{x}^{(p)}</script><p>“中心化”后需要估计的参数由$b_{k}$转变为$\beta_{k}$, 这样处理地好处是相比估计得到的$\hat{b}_{k}$, 各$\hat{\beta}_{k}$之间是互不相关的. 这体现了”中心化”处理的优势.</p><p>另外我们注意到, “中心化”后:</p><script type="math/tex; mode=display">\sum^{N}_{i=1}z^{(k)}_{i}=\sum^{N}_{i=1}\left(x_{i}^{(k)}-\bar{x}^{(k)}\right)=\sum^{N}_{i=1}x_{i}^{(k)}-N\bar{x}^{(k)}=0</script><p>“中心化”或者说是模型的”中心化”, 实际上可以理解为是对样本的预处理过程, 我们一般假定这个预处理过程是事先完成的, $z^{(k)}_{i}$仍然用$x^{(k)}_{i}$来表示(但是知道此时的样本已经经过”中心化”处理了). 于是我们的方程描述成:</p><script type="math/tex; mode=display">y_{i}=\beta_{0}+\beta_{1}x_{i}^{(1)}+\beta_{2}x_{i}^{(2)}+\dots+\beta_{p}x_{i}^{(p)}+e_{i}\qquad i=1,2,\dots,N</script><p>同时有:</p><script type="math/tex; mode=display">\sum^{N}_{i=1}x^{(k)}_{i}=0\qquad k=1,2,\dots,p</script><p>可以看到$x^{(k)}_{i}$通过$\beta_{k}$进行线性加权. $\beta_{k}$即是每一个对应预测因子的权重. $\beta_{0}$这一项是孤立的常数项, 有时称这项为线性方程的截距(intercept). 下面依然试图用矩阵来简化线性方程的描述.</p><p>为了表示方便一般把上式写成两向量内积(inner product)的形式:</p><script type="math/tex; mode=display">y=\beta_{0}+\boldsymbol{x}^{T}\boldsymbol{\beta}+e</script><p>这里$\boldsymbol{\beta}$与$\boldsymbol{x}$都是列向量:</p><script type="math/tex; mode=display">\boldsymbol{\beta}=\begin{bmatrix} \beta_{1} \\ \beta_{2} \\ \vdots \\ \beta_{p} \\ \end{bmatrix}_{p\times1}</script><script type="math/tex; mode=display"> \boldsymbol{x}= \begin{bmatrix}  x^{(1)} \\  x^{(2)} \\  \vdots \\  x^{(p)} \\  \end{bmatrix}_{p\times1}</script><p>为了运算的便利, 有时也把$\boldsymbol{x}$扩一维, 添加$x^{(0)}=1$, 这样再把$\beta_{0}$加入$\boldsymbol{\beta}$, 就得到形式更为简洁的:</p><script type="math/tex; mode=display">y=\boldsymbol{x}^{T}\boldsymbol{\beta}+e</script><p>这里$\boldsymbol{x}$与$\boldsymbol{\beta}$列向量都是$p+1$维的, 称此线性回归问题为p元的的线性回归问题, 因为有p个预测因子. 向量$\boldsymbol{\beta}$也称之为<strong>权向量</strong>, 权向量的取值空间称为权空间(weight space)</p><p>然后$N$次采样后:</p><script type="math/tex; mode=display">\boldsymbol{y}=\boldsymbol{X}\boldsymbol{\beta}+\boldsymbol{e}</script><script type="math/tex; mode=display">\boldsymbol{X}=\begin{bmatrix} \boldsymbol{x}_{1}^{T} \\ \boldsymbol{x}_{2}^{T} \\ \vdots \\ \boldsymbol{x}_{N}^{T} \\  \end{bmatrix}_{N\times (p\times 1)}</script><h2 id="一元线性回归"><a href="#一元线性回归" class="headerlink" title="一元线性回归"></a>一元线性回归</h2><p>所有的准备都已完毕, 下面来解这个问题, 即如何估计得到权向量.</p><p>对于一元与二元线性回归我们有非常直观的解释, 下面讨论一元线性回归, 其模型假设为:</p><script type="math/tex; mode=display">y=\beta_{0}+\beta_{1}x+e</script><p>对于$N$次采样我们画出图来:</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/linearregression1.PNG"  style="zoom:40%" alt="" align=center /></p><p>图中的点即样本, 我们需要找到一种方法, 在某个标准下, 找到能比较准确描述输入输出关系的那条线, 比如图中的红线.</p><p>这条红线便是预测线, 他由如下线性方程描述:</p><script type="math/tex; mode=display">\hat{y}=\hat{\beta}_{0}+\hat{\beta}_{1}x</script><p>$\hat{\beta}_{0}$与$\hat{\beta}_{1}$是对$\beta_{0}$与$\beta_{1}$的估计, 如何获得这个估计?我们需要选择一个策略, 即一个衡量模型好坏的标准. 一个直观的办法就是看模型预测出的的$\hat{y}$在那些样本点上与采样值$y$的差距, 我们取:</p><script type="math/tex; mode=display">L(\hat{\beta}_{0}, \hat{\beta}_{1})=\sum^{N}_{i=1}(y_{i}-\hat{y}_{i})^{2}</script><p>上式直观的解释是: <strong>我们所寻找的预测线(模型), 应该使所有样本点到预测线竖直距离和最小</strong>. 这就是<strong>最小二乘法</strong>(Least Squares)在一元情况下遵循的策略. 同时也知道了, 最小二乘法属于一种点估计.</p><p>在统计学习中上式也被称为损失函数(loss function), 更确切地说是一种平方损失函数(quadratic loss function). 损失函数值越小(通过调节参数$\hat{\beta}_{0},\hat{\beta}_{1}$大小)模型越好.</p><p>在统计学中, 曾定义<strong>残差平方和</strong>(RSS,residual sum of squares), 其计算公式与$L(\hat{\beta}_{0}, \hat{\beta}_{1})$相同, 所以也可以认为最小二乘法是求得RSS最小的权向量估计. </p><p>利用多元函数求极值的方法, 我们很容易就可以求得这个问题的解析解.</p><script type="math/tex; mode=display">L(\hat{\beta}_{0}, \hat{\beta}_{1})=\sum^{N}_{i=1}(y_{i}-\hat{y}_{i})^{2}=\sum^{N}_{i=1}(y_{i}-\hat{\beta}_{0}- \hat{\beta}_{1}x_{i})^{2}</script><p>上式分别对$\hat{\beta}_{0}, \hat{\beta}_{1}$求偏导, 并令其为0. 得:</p><script type="math/tex; mode=display">\dfrac{\partial L}{\partial \hat{\beta}_{0}}=-2\sum^{N}_{i=1}[y_{i}-\hat{\beta}_{0}- \hat{\beta}_{1}x_{i}]=0</script><script type="math/tex; mode=display">\dfrac{\partial L}{\partial \hat{\beta}_{1}}=-2\sum^{N}_{i=1}x_{i}[y_{i}-\hat{\beta}_{0}- \hat{\beta}_{1}x_{i}]=0</script><p>由于经过”中心化”, 之前说过有结论:</p><script type="math/tex; mode=display">\sum^{N}_{i=1}x^{(k)}_{i}=0\qquad k=1,2,\dots,p</script><p>于是可解出$\hat{\beta}_{0},\hat{\beta}_{1}$:</p><script type="math/tex; mode=display">\hat{\beta}_{0}=\bar{y}=\frac{1}{N}\sum^{N}_{i=1}y_{i}</script><script type="math/tex; mode=display">\hat{\beta}_{1}=\frac{\sum\limits^{N}_{i=1}x_{i}y_{i}}{\sum\limits^{N}_{i=1}x^{2}_{i}}</script><p>$\hat{\beta}_{0},\hat{\beta}_{1}$本身有一些比较好的性质, 比如可以证明他们是$\beta_{0},\beta_{1}$的无偏估计, 并且他们是不相关的. 当假设随机误差$e$服从正态分布时, 他们还是独立的, 并且是$\beta_{0},\beta_{1}$的最小方差无偏估计, 证明可参考《概率论与数理统计》P.267~269内容.</p><h2 id="多元线性回归"><a href="#多元线性回归" class="headerlink" title="多元线性回归"></a>多元线性回归</h2><p>当$p=2$时即二元线性回归, 我们仍然可以直观地想象模型:</p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/linearregression3.PNG"  style="zoom:40%" alt="" align=center /></p><p>此时对于每个样本点有两个特征$\boldsymbol{x}_{i}=(x^{(1)}_{i},x^{(2)}_{i})$, 故模型是一个空间平面去拟合(fit)样本. 此时最小二乘法求解的原则是样本点到平面竖直距离和最小.</p><p>$p&gt;2$的多元线性回归就没有这么直观了, 但我们依然可以知道模型是使用超平面(hyperplane)去拟合样本.</p><p>为了能直接推导出最后实用的矩阵运算方法, 我们对$\boldsymbol{x}_{i}$升1维处理, 正如之前说过的, 令$x^{(0)}_{i}=1$, 于是:</p><script type="math/tex; mode=display">y_{i}=\hat{\beta}_{0}x^{(0)}_{i}+\hat{\beta}_{1}x_{i}^{(1)}+\hat{\beta}_{2}x_{i}^{(2)}+\dots+\hat{\beta}_{p}x_{i}^{(p)}\qquad i=1,2,\dots,N</script><p>推导权向量的估计与一元时类似, 损失函数为:</p><script type="math/tex; mode=display">L(\hat{\beta}_{0},\hat{\beta}_{1},\dots,\hat{\beta}_{p})=\sum^{N}_{i=1}(y_{i}-\hat{y_{i}})^{2}=\sum^{N}_{i=1}(y_{i}-\hat{\beta}_{0}x^{(0)}_{i}-\hat{\beta}_{1}x_{i}^{(1)}-\hat{\beta}_{2}x_{i}^{(2)}-\dots-\hat{\beta}_{p}x_{i}^{(p)})^{2}</script><p>对各权向量求导令其为0:</p><script type="math/tex; mode=display">\dfrac{\partial L}{\partial \hat{\beta}_{0}}=0,\quad \dfrac{\partial L}{\partial \hat{\beta}_{1}}=0,\quad \dfrac{\partial L}{\partial \hat{\beta}_{2}}=0,\quad\dots,\quad \dfrac{\partial L}{\partial \hat{\beta}_{p}}=0</script><p>由于样本经过”中心化”处理, 依然有:</p><script type="math/tex; mode=display">\sum^{N}_{i=1}x^{(k)}_{i}=0\qquad k=1,2,\dots,p</script><p>对于$\hat{\beta}_{0}$的偏导, 可以整理为:</p><script type="math/tex; mode=display">\dfrac{\partial L}{\partial \hat{\beta}_{0}}  = -2\sum^{N}_{i=1}\left( y_{i}-\hat{\beta}_{0}x^{(0)}_{i}-\hat{\beta}_{1}x^{(1)}_{i}-\dots-\hat{\beta}_{p}x^{(p)}_{i}\right)=0</script><script type="math/tex; mode=display">\sum^{N}_{i=1}x^{(0)}_{i}y_{i}=\hat{\beta}_{0}\sum^{N}_{i=1}x^{(0)}_{i}x^{(0)}_{i}+\hat{\beta}_{1}\sum^{N}_{i=1}x^{(0)}_{i}x^{(1)}_{i}+\dots+\hat{\beta}_{p}\sum^{N}_{i=1}x^{(0)}_{i}x^{(p)}_{i}</script><p>把剩余方程相同处理列在一起:</p><script type="math/tex; mode=display">\begin{cases} \hat{\beta}_{0}\sum^{N}_{i=1}x^{(0)}_{i}x^{(0)}_{i}+\hat{\beta}_{1}\sum^{N}_{i=1}x^{(0)}_{i}x^{(1)}_{i}+\dots+\hat{\beta}_{p}\sum^{N}_{i=1}x^{(0)}_{i}x^{(p)}_{i}=\sum^{N}_{i=1}x^{(0)}_{i}y_{i} \\ \hat{\beta}_{0}\sum^{N}_{i=1}x^{(1)}_{i}x^{(0)}_{i}+\hat{\beta}_{1}\sum^{N}_{i=1}x^{(1)}_{i}x^{(1)}_{i}+\dots+\hat{\beta}_{p}\sum^{N}_{i=1}x^{(1)}_{i}x^{(p)}_{i}=\sum^{N}_{i=1}x^{(1)}_{i}y_{i} \\ \vdots \\ \hat{\beta}_{0}\sum^{N}_{i=1}x^{(p)}_{i}x^{(0)}_{i}+\hat{\beta}_{1}\sum^{N}_{i=1}x^{(p)}_{i}x^{(1)}_{i}+\dots+\hat{\beta}_{p}\sum^{N}_{i=1}x^{(p)}_{i}x^{(p)}_{i}=\sum^{N}_{i=1}x^{(p)}_{i}y_{i}\end{cases}</script><p>下面用矩阵对上式进行形式上的简化, 令:</p><script type="math/tex; mode=display">\boldsymbol{X}=\begin{bmatrix} x^{(0)}_{1} & x^{(1)}_{1} & \ldots & x^{(p)}_{1} \\x^{(0)}_{2} & x^{(1)}_{2} & \ldots & x^{(p)}_{2} \\ \vdots & \vdots\ & \ddots & \vdots \\ x^{(0)}_{N} & x^{(1)}_{N} & \ldots & x^{(p)}_{N} \\ \end{bmatrix}=\begin{bmatrix} \boldsymbol{x}_{1}^{T} \\ \boldsymbol{x}_{2}^{T} \\ \vdots \\ \boldsymbol{x}_{N}^{T} \\ \end{bmatrix}_{N\times (p+1)}</script><script type="math/tex; mode=display">\boldsymbol{y}=\begin{bmatrix} y_{1} \\ y_{2} \\ \vdots \\ y_{N} \\ \end{bmatrix}_{N\times1}</script><script type="math/tex; mode=display">\boldsymbol{\hat{\beta}}=\begin{bmatrix} \beta_{0} \\ \beta_{1} \\ \beta_{2} \\ \vdots \\ \beta_{p} \\ \end{bmatrix}_{(p+1)\times1}</script><p>于是上式可简化为:</p><script type="math/tex; mode=display">\boldsymbol{X}^{T}\boldsymbol{X}\boldsymbol{\hat{\beta}}=\boldsymbol{X}^{T}\boldsymbol{y}</script><p>当$\boldsymbol{X}^{T}\boldsymbol{X}$可逆, 有:</p><script type="math/tex; mode=display">\boldsymbol{\hat{\beta}}=(\boldsymbol{X}^{T}\boldsymbol{X})^{-1}\boldsymbol{X}^{T}\boldsymbol{y}</script><h2 id="最小二乘法矩阵表示的简单推导"><a href="#最小二乘法矩阵表示的简单推导" class="headerlink" title="最小二乘法矩阵表示的简单推导"></a>最小二乘法矩阵表示的简单推导</h2><p>利用矩阵代数中的向量的数量函数对向量的导数的概念, 上述推导过程会更加简洁. 直接从损失函数出发:</p><script type="math/tex; mode=display">L(\hat{\boldsymbol{\beta}})=\sum^{N}_{i=1}(y_{i}-\boldsymbol{x}_{i}^{T}\hat{\boldsymbol{\beta}})^{2}=\begin{bmatrix} y_{1}-\boldsymbol{x}_{1}^{T}\boldsymbol{\hat{\beta}} \\ y_{2}-\boldsymbol{x}_{2}^{T}\boldsymbol{\hat{\beta}} \\ \vdots \\ y_{N}-\boldsymbol{x}_{N}^{T}\boldsymbol{\hat{\beta}} \\ \end{bmatrix}^{T}\begin{bmatrix}y_{1}-\boldsymbol{x}_{1}^{T}\boldsymbol{\hat{\beta}} \\ y_{2}-\boldsymbol{x}_{2}^{T}\boldsymbol{\hat{\beta}} \\ \vdots \\ y_{N}-\boldsymbol{x}_{N}^{T}\boldsymbol{\hat{\beta}} \\ \end{bmatrix}=(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{\hat{\beta}})^{T}(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{\hat{\beta}})</script><script type="math/tex; mode=display">\mathrm{d}L=-(\boldsymbol{X}\mathrm{d}\boldsymbol{\hat{\beta}})^{T}(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{\hat{\beta}})-(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{\hat{\beta}})^{T}\boldsymbol{X}\mathrm{d}\boldsymbol{\hat{\beta}}</script><p>再利用迹技巧(trace trick), 可以得到:</p><script type="math/tex; mode=display">\mathrm{d}L=-2(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{\hat{\beta}})^{T}\boldsymbol{X}\mathrm{d}\boldsymbol{\hat{\beta}}</script><p>故:</p><script type="math/tex; mode=display">\frac{\mathrm{d}L}{\mathrm{d}\boldsymbol{\hat{\beta}}}=-2(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{\hat{\beta}})^{T}\boldsymbol{X}=-2\boldsymbol{X}^{T}(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{\hat{\beta}})=0</script><script type="math/tex; mode=display">\boldsymbol{X}^{T}\boldsymbol{y}-\boldsymbol{X}^{T}\boldsymbol{X}\boldsymbol{\hat{\beta}}=0</script><p>同样解得:</p><script type="math/tex; mode=display">\boldsymbol{\hat{\beta}}=(\boldsymbol{X}^{T}\boldsymbol{X})^{-1}\boldsymbol{X}^{T}\boldsymbol{y}</script><p>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;之前学着用过线性回归的一些东西, 这里稍微整理下, 虽然是比较基础的内容, 希望能理出一个比较清晰的方法思路来, 如果能对到其他人理解这个问题有帮助那再好不过了:)&lt;/p&gt;</summary>
    
    
    
    <category term="Lab" scheme="https://granvallen.github.io/categories/Lab/"/>
    
    <category term="统计学习" scheme="https://granvallen.github.io/categories/Lab/%E7%BB%9F%E8%AE%A1%E5%AD%A6%E4%B9%A0/"/>
    
    
    <category term="数理统计" scheme="https://granvallen.github.io/tags/%E6%95%B0%E7%90%86%E7%BB%9F%E8%AE%A1/"/>
    
  </entry>
  
  <entry>
    <title>用Matlab处理Excel</title>
    <link href="https://granvallen.github.io/matlabexcel/"/>
    <id>https://granvallen.github.io/matlabexcel/</id>
    <published>2018-02-26T04:01:00.000Z</published>
    <updated>2021-02-24T06:23:10.872Z</updated>
    
    <content type="html"><![CDATA[<p>虽然最近比较多用Matlab做事, 但还没怎么正式地写过几篇讲讲Matlab. 嗯, 我是不是还忘了什么. . . （Mathematica QAQ）. </p><p>记起来之前写过一个Matlab与Excel交互处理表格的脚本, 于是又摸出了这篇博文. 事情是这样的, 作为一个苦逼的学习委员（基层干部）平时干得最多的事情当然是——打杂, 帮老师收个作业, 带个口信, 收收费什么的, 不过这些其中要说我比较讨厌的就是每次考试后的算绩点（好吧, 我承认我excel比较苦手）, 不知道其他学校怎么样, 我们是学委担此重任. 话说计算机的同学们, 你们都能拉出表格了不顺便算个GPA?其实我写了才知道最恶心的其实是处理表格的格式, 算绩点一句话就够了. . . </p><p>本文的脚本应该没有什么其他人能用得上, 因为各种格式都是按照表格和要求设计的, 不过其中的一些处理表格的方法还是值得做个笔记的. 受闪轨启发, 我把这个计划称为——学委解放战线</p><span id="more"></span><p>老师的要求也不复杂, 可以总结为两点: </p><ul><li>1.算出每个同学绩点与排名, 并且填入到姓名后新建的两列内, 并计算班级平均绩点. （挂科按60算）</li><li>2.筛选出低于60分不及格的成绩, 并用黄色填充. </li></ul><p>表格样本的原格式如下（我随便找的, 不算滥用职权吧）: </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sheet1.PNG"  style="zoom:40%" alt="" align=center /></p><p>姓名我用序号代替了. </p><p>要用matlab来处理做到这些, 则需要解决: </p><ul><li>1.表格里的信息都是文本格式, 需要把数据与字符进行区分. </li><li>2.有些课程是等级评分, 分优、良、及格等, 需要转换为对应百分制分值再计算. </li><li>3.表格中包含了某些修读双学位或重修的额外课程, 如电气X班的表里还有修读金融学的同学, 而该班其他同学并不修这些金融学课程, 原则上计算绩点时, 这些课程是不算在内的. </li><li>4.有些缓考的同学某课成绩标注的是’-‘, 需要区分出来, 并不计入绩点计算. </li><li>5.matlab对表格的格式控制, 包括插入数据列、改变表格背景色等. </li><li>6.matlab矩阵元素序号到excel表格序号的转换. </li><li>7.先计算绩点, 然后计算排名. </li></ul><p>主要思路是把处理表格格式和处理数据分开来做, 处理表格格式用<strong>actxserver(‘excel.application’)</strong>创建excel应用类来做, 操作方法与C#处理excel相同, 具体使用方法可以参考<a href="http://blog.csdn.net/north_easter/article/details/6861992">这里</a>或<a href="https://msdn.microsoft.com/en-us/vba/excel-vba/articles/application-object-excel">这里</a>；表格的数据用<strong>xlsread</strong>读入处理. </p><p>另外做了一些强化, 包括: </p><ul><li>1.打印处理表格的基本信息和没有计入绩点计算的课程的修读人数情况, 便于查错. </li><li>2.增设了几个可调变量, 包括判定为重修与双学位课程的修读人数阈值等. </li><li>3.程序自动扫描当前文件夹下所有excel文档, 批量处理, 处理后文档输出到新建的Output文件夹. </li></ul><p>下面是code</p><figure class="highlight matlab"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">% forfreedom.m</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">forfreedom</span></span></span><br><span class="line"></span><br><span class="line">    <span class="comment">%% 初始化 自定义参数设定</span></span><br><span class="line">    clc</span><br><span class="line">    clear</span><br><span class="line">    close all</span><br><span class="line">    <span class="comment">%======================== 一般只需要在画线区域修改 ============================</span></span><br><span class="line">    stu_ppt = <span class="number">0.2</span>; <span class="comment">% 当选课人数少于总人数的 stu_ppt * 100%, 认为该课为重修或双学位课程, 默认 20%</span></span><br><span class="line">    score_mdf = <span class="number">60</span>; <span class="comment">% 挂科的按 score_mdf 来算绩点, 默认以 60分来算</span></span><br><span class="line">    </span><br><span class="line">    </span><br><span class="line">    <span class="comment">%======================== 一般只需要在画线区域修改 ============================</span></span><br><span class="line">    <span class="built_in">disp</span>(<span class="string">&#x27;开始工作咯, 计时, 滴~&#x27;</span>);</span><br><span class="line">    tic</span><br><span class="line">    </span><br><span class="line">    <span class="comment">%% 读取excel文档</span></span><br><span class="line">    e = actxserver(<span class="string">&#x27;excel.application&#x27;</span>); <span class="comment">% 建立excel数据类</span></span><br><span class="line">    path = pwd;</span><br><span class="line">    xlsfiles = dir(fullfile(path, <span class="string">&#x27;*.xls&#x27;</span>));</span><br><span class="line">    <span class="built_in">disp</span>([<span class="string">&#x27;这个目录下有&#x27;</span> num2str(<span class="built_in">size</span>(xlsfiles, <span class="number">1</span>)) <span class="string">&#x27;张表格&#x27;</span>]);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> <span class="built_in">j</span> = <span class="number">1</span> : <span class="built_in">size</span>(xlsfiles)</span><br><span class="line">        <span class="built_in">disp</span>([<span class="string">&#x27;正在处理第&#x27;</span> num2str(<span class="built_in">j</span>) <span class="string">&#x27;张表格&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;!&#x27;</span>]);</span><br><span class="line">        xls_file_name = xlsfiles(<span class="built_in">j</span>).name;</span><br><span class="line">        WorkBook = e.Workbooks.Open([path <span class="string">&#x27;/&#x27;</span> xls_file_name]);</span><br><span class="line"></span><br><span class="line">        sheets = e.ActiveWorkBook.Sheets;</span><br><span class="line">        sheet1 = sheets.Item(<span class="number">1</span>); <span class="comment">% 获得sheet1</span></span><br><span class="line">    <span class="comment">%     e.Visible = 1;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">% 读入excel的数据</span></span><br><span class="line">        [~, xls] = xlsread([path <span class="string">&#x27;/&#x27;</span> xls_file_name], <span class="number">1</span>);</span><br><span class="line">        [xls_n, xls_p] = <span class="built_in">size</span>(xls);</span><br><span class="line"></span><br><span class="line">        <span class="comment">%% 预处理 1.数据分割, 参数计算    2.去除成绩中的字符     3.去除重修、双学位科目数据           </span></span><br><span class="line">        <span class="comment">% 数据分割 参数计算</span></span><br><span class="line">        xls_title = xls&#123;<span class="number">1</span>, <span class="number">1</span>&#125;;</span><br><span class="line">        stu_num = xls_n - <span class="number">5</span>; <span class="comment">% 学生人数</span></span><br><span class="line">        stu_nam = xls(<span class="number">6</span> : <span class="keyword">end</span>, <span class="number">4</span>); <span class="comment">%学生姓名</span></span><br><span class="line">        course_nam = xls(<span class="number">3</span>, <span class="number">5</span> : <span class="keyword">end</span> - <span class="number">3</span>); <span class="comment">% 课程名</span></span><br><span class="line">        course_cred = str2num(cell2mat(xls(<span class="number">4</span>, <span class="number">5</span> : <span class="keyword">end</span> - <span class="number">3</span>))); <span class="comment">%课程学分</span></span><br><span class="line">        score_matrix = xls(<span class="number">6</span> : <span class="keyword">end</span>, <span class="number">5</span> : <span class="keyword">end</span> - <span class="number">3</span>); <span class="comment">% 分数矩阵</span></span><br><span class="line">        [score_matrix_n, score_matrix_p] = <span class="built_in">size</span>(score_matrix);</span><br><span class="line">        appendx = xls(<span class="number">6</span> : <span class="keyword">end</span>, <span class="keyword">end</span> - <span class="number">2</span> : <span class="keyword">end</span>); <span class="comment">%最后三列</span></span><br><span class="line"></span><br><span class="line">        toc</span><br><span class="line">        fprintf(<span class="string">&#x27;表格基本信息: \n表格标题: %s\n班级人数: %d\n总课程数: %d\n&#x27;</span>, ...</span><br><span class="line">            xls_title, stu_num, <span class="built_in">length</span>(course_nam));</span><br><span class="line"></span><br><span class="line">        <span class="comment">% 分数矩阵处理</span></span><br><span class="line">        <span class="comment">% 非法字符处理</span></span><br><span class="line">        [score_matrix&#123;strncmp(&#123;<span class="string">&#x27;优&#x27;</span>&#125;, score_matrix, <span class="number">1</span>)&#125;] = deal(<span class="string">&#x27;95.0&#x27;</span>); <span class="comment">% 处理 优 良 及格 不及格</span></span><br><span class="line">        [score_matrix&#123;strncmp(&#123;<span class="string">&#x27;良&#x27;</span>&#125;, score_matrix, <span class="number">1</span>)&#125;] = deal(<span class="string">&#x27;85&#x27;</span>); </span><br><span class="line">        [score_matrix&#123;strncmp(&#123;<span class="string">&#x27;及格&#x27;</span>&#125;, score_matrix, <span class="number">1</span>)&#125;] = deal(<span class="string">&#x27;65.0&#x27;</span>); </span><br><span class="line">        [score_matrix&#123;strncmp(&#123;<span class="string">&#x27;不及格&#x27;</span>&#125;, score_matrix, <span class="number">1</span>)&#125;] = deal(<span class="string">&#x27;50&#x27;</span>); </span><br><span class="line">        [score_matrix&#123;strncmp(&#123;<span class="string">&#x27;-&#x27;</span>&#125;, score_matrix, <span class="number">1</span>)&#125;] = deal(<span class="string">&#x27;-1&#x27;</span>); <span class="comment">% 处理 &#x27;-&#x27;   </span></span><br><span class="line">        <span class="comment">% 元胞数组 转 数值矩阵</span></span><br><span class="line">        score_matrix_num = <span class="built_in">cellfun</span>(@str2num, score_matrix); <span class="comment">% 计算绩点用</span></span><br><span class="line">        score_matrix_mark = score_matrix_num; <span class="comment">%标注不及格成绩用</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">% 去除重修、双学位课程 规则: 有成绩的人数占班级总人数比例低于 stu_ppt * 100%</span></span><br><span class="line">        threshold = <span class="built_in">ceil</span>(stu_num * stu_ppt); <span class="comment">% 选课人数低于threshold认为要忽略课程</span></span><br><span class="line">        course_stu_num = sum(score_matrix_num ~= <span class="number">-1</span>); <span class="comment">% 选课人数</span></span><br><span class="line">        course_delete_mask = course_stu_num &lt;= threshold;<span class="comment">% 找到要删除的课程</span></span><br><span class="line">        course_delete_nam = course_nam(:, course_delete_mask); <span class="comment">% 要删除课程的名字</span></span><br><span class="line">        course_delete_stu_num = course_stu_num(course_delete_mask);</span><br><span class="line"></span><br><span class="line">        toc</span><br><span class="line">        <span class="keyword">if</span> (~<span class="built_in">isempty</span>(course_delete_mask))</span><br><span class="line">            fprintf(<span class="string">&#x27;忽略课程信息: \n有%d门课的成绩没有纳入计算绩点, 这些课可能是重修或双学位课程\n这些课程是:\n&#x27;</span>, ...</span><br><span class="line">                sum(course_delete_mask));</span><br><span class="line">            <span class="keyword">for</span> <span class="built_in">i</span> = <span class="number">1</span> : sum(course_delete_mask)</span><br><span class="line">                fprintf(<span class="string">&#x27;%-30s选课人数: %d\n&#x27;</span>, course_delete_nam&#123;<span class="built_in">i</span>&#125;, course_delete_stu_num(<span class="built_in">i</span>));</span><br><span class="line">            <span class="keyword">end</span></span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        score_matrix_num(:, course_delete_mask) = []; <span class="comment">% 删除课程分数数据</span></span><br><span class="line">        course_cred(:, course_delete_mask) = []; <span class="comment">% 删除学分数据</span></span><br><span class="line">        course_nam(:, course_delete_mask) = []; <span class="comment">% 删除课程名字</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">%% 计算绩点</span></span><br><span class="line">        score_mdf_mask = score_matrix_num &lt; <span class="number">60</span> &amp; score_matrix_num &gt;= <span class="number">0</span>; <span class="comment">% 选出挂科的成绩</span></span><br><span class="line">        score_matrix_num(score_mdf_mask) = score_mdf; <span class="comment">% 考试没过成绩修改</span></span><br><span class="line">        score_matrix_num(score_matrix_num &lt; <span class="number">0</span>) = <span class="number">50</span>; <span class="comment">% 没出成绩, 可能缺考, 先算50</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">% 计算 GPA</span></span><br><span class="line">        GPA = <span class="built_in">round</span>((score_matrix_num - <span class="number">50</span>) * course_cred&#x27; ./ (sum(course_cred) * <span class="number">10</span>), <span class="number">3</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">%% 修改表结构 + 填入绩点 + 填入排名</span></span><br><span class="line">        sheet1.Range([<span class="string">&#x27;F1:F&#x27;</span> num2str(xls_n)]).Insert; <span class="comment">% 绩点</span></span><br><span class="line">        sheet1.Range([<span class="string">&#x27;F1:F&#x27;</span> num2str(xls_n)]).Insert; <span class="comment">% 排名</span></span><br><span class="line">        sheet1.Range(<span class="string">&#x27;F5&#x27;</span>).Value = <span class="string">&#x27;绩点&#x27;</span>;</span><br><span class="line">        sheet1.Range(<span class="string">&#x27;G5&#x27;</span>).Value = <span class="string">&#x27;排名&#x27;</span>;</span><br><span class="line"></span><br><span class="line">        sheet1.Range([<span class="string">&#x27;F6:F&#x27;</span> num2str(<span class="number">6</span> + stu_num - <span class="number">1</span>)]).Value = GPA;</span><br><span class="line">        sheet1.Range([<span class="string">&#x27;F6:G&#x27;</span> num2str(<span class="number">6</span> + stu_num)]).HorizontalAlignment = <span class="number">-4108</span>; <span class="comment">%居中</span></span><br><span class="line">        sheet1.Range([<span class="string">&#x27;F&#x27;</span> num2str(<span class="number">6</span> + stu_num)]).Value = <span class="built_in">round</span>(<span class="built_in">mean</span>(GPA), <span class="number">3</span>);</span><br><span class="line"></span><br><span class="line">        [~, r] = <span class="built_in">sort</span>(GPA, <span class="string">&#x27;descend&#x27;</span>);</span><br><span class="line">        [~, r] = <span class="built_in">sort</span>(r);</span><br><span class="line">        sheet1.Range([<span class="string">&#x27;G6:G&#x27;</span> num2str(<span class="number">6</span> + stu_num - <span class="number">1</span>)]).Value = r;</span><br><span class="line"></span><br><span class="line">        <span class="comment">%% 将未及格的课程标为黄色</span></span><br><span class="line">        score_matrix_mark_mask = score_matrix_mark &lt; <span class="number">60</span> &amp; score_matrix_mark &gt;=<span class="number">0</span>;</span><br><span class="line">        [n, p] = <span class="built_in">find</span>(score_matrix_mark_mask == <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> <span class="built_in">i</span> = <span class="number">1</span> : sum(score_matrix_mark_mask(:))</span><br><span class="line">            sheet1.Range(mark_score_excel_coor(n(<span class="built_in">i</span>) + <span class="number">5</span>, p(<span class="built_in">i</span>)+ <span class="number">7</span>)).Interior.ColorIndex = <span class="number">6</span>;</span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">%% 保存excel</span></span><br><span class="line">        <span class="keyword">if</span> ~exist(<span class="string">&#x27;Output&#x27;</span>, <span class="string">&#x27;dir&#x27;</span>) </span><br><span class="line">            mkdir(<span class="string">&#x27;Output&#x27;</span>)</span><br><span class="line">        <span class="keyword">end</span> </span><br><span class="line">        WorkBook.SaveAs([path <span class="string">&#x27;/Output/&#x27;</span> xls_file_name]);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    e.Quit;</span><br><span class="line">    e.delete;</span><br><span class="line">    toc</span><br><span class="line">    <span class="built_in">disp</span>(<span class="string">&#x27;呼~处理完毕*^-^*&#x27;</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">%% 坐标转换</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">excel_coor</span> = <span class="title">mark_score_excel_coor</span><span class="params">(n, p)</span></span></span><br><span class="line"></span><br><span class="line">     excel_coor = [ex(p) num2str(n)];</span><br><span class="line"></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">excel_coor_p</span> = <span class="title">ex</span><span class="params">(p)</span></span></span><br><span class="line">    <span class="keyword">if</span> p &gt; <span class="number">26</span></span><br><span class="line">        s = ex(<span class="built_in">fix</span>((p<span class="number">-1</span>) / <span class="number">26</span>));</span><br><span class="line">        excel_coor_p = [s char(<span class="number">65</span> + <span class="built_in">mod</span>(p<span class="number">-1</span>, <span class="number">26</span>))];</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        excel_coor_p = char(<span class="number">65</span> + p - <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>处理结果显示如下: </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sheet2.PNG"  style="zoom:40%" alt="" align=center /></p><p>输出表格如下: </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sheet3.PNG"  style="zoom:40%" alt="" align=center /></p><p>其中可能值得一说的是matlab矩阵索引坐标转换为excel表格索引的处理, 比如matlab中矩阵坐标(1, 2), 对应excel坐标是(1, B). 查了一圈好像没什么人碰到这个问题就自己写了. 其实主要要解决的是列坐标的转换问题. excel的列坐标序号是, 从A到Z, 然后是AA到AZ, 之后BA…, 我想你看出来了, 这本质上就是26进制的计数系统, 我用了一个递归算法实现了他. </p><p>刚写完, 然而…今年不用算绩点了. . .<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;虽然最近比较多用Matlab做事, 但还没怎么正式地写过几篇讲讲Matlab. 嗯, 我是不是还忘了什么. . . （Mathematica QAQ）. &lt;/p&gt;
&lt;p&gt;记起来之前写过一个Matlab与Excel交互处理表格的脚本, 于是又摸出了这篇博文. 事情是这样的, 作为一个苦逼的学习委员（基层干部）平时干得最多的事情当然是——打杂, 帮老师收个作业, 带个口信, 收收费什么的, 不过这些其中要说我比较讨厌的就是每次考试后的算绩点（好吧, 我承认我excel比较苦手）, 不知道其他学校怎么样, 我们是学委担此重任. 话说计算机的同学们, 你们都能拉出表格了不顺便算个GPA?其实我写了才知道最恶心的其实是处理表格的格式, 算绩点一句话就够了. . . &lt;/p&gt;
&lt;p&gt;本文的脚本应该没有什么其他人能用得上, 因为各种格式都是按照表格和要求设计的, 不过其中的一些处理表格的方法还是值得做个笔记的. 受闪轨启发, 我把这个计划称为——学委解放战线&lt;/p&gt;</summary>
    
    
    
    <category term="Coding" scheme="https://granvallen.github.io/categories/Coding/"/>
    
    <category term="Matlab" scheme="https://granvallen.github.io/categories/Coding/Matlab/"/>
    
    
    <category term="不务正业" scheme="https://granvallen.github.io/tags/%E4%B8%8D%E5%8A%A1%E6%AD%A3%E4%B8%9A/"/>
    
    <category term="Matlab" scheme="https://granvallen.github.io/tags/Matlab/"/>
    
  </entry>
  
  <entry>
    <title>关于采样的总结</title>
    <link href="https://granvallen.github.io/sampling/"/>
    <id>https://granvallen.github.io/sampling/</id>
    <published>2018-02-15T15:02:00.000Z</published>
    <updated>2021-02-24T06:23:10.872Z</updated>
    
    <content type="html"><![CDATA[<p>最近又在复习信号处理的东西, 对于一些内容有了更深入的理解, 于是做一个关于采样的总结. 众所周知, 自然界大多数信号是模拟信号, 采样是数字系统处理的第一步, 主要由各种传感器配合A/D来实现. 在<a href="https://granvallen.github.io/fft/">FFT为什么那么快</a>一文中提到了“重采样”的概念, 在那里一笔带过了, 本文也算是一个补充. </p><span id="more"></span><p>既然是内容的梳理必然会牵涉到很多细节, 其中包括数学公式的推导, 这些在奥本海姆教授的两本著作<a href="https://book.douban.com/subject/21359219/">《信号与系统》</a>和<a href="https://book.douban.com/subject/26307919/">《离散时间信号处理》</a>都有详细推导过程. 本文的主要内容可以在《信号与系统》的第七章与《离散时间信号处理》的第四章中找到参考, 以下简写为《信-奥》与《离-奥》, 区别于参考郑君里教授的同名著作, 将简写作《信-郑》. 由于这部分采样内容在两本书中分布广泛、符号定义存在差异以及公式推导方式不一, 因此做一个总结是有必要的. 本文的一些内容会基于自己的理解, 可能错误在所难免, 不过书写是为了更好地理解. </p><p>在阅读这两本书的过程中, 令我十分敬佩的是作者清晰的写作思路. 如果把阅读一本书比作一次冒险旅行, 那么在旅途中作者不断地提醒你: 你现在身处何处？曾走过了哪些景点？经历了哪些岔路？我们这段路地主要景点是什么？我们旅行最终目的地又在何方？具体到某一知识点就是: 这个问题是怎么引入的？为什么要研究这个问题？这个问题在整个知识框架下是什么样的地位？搞清楚了这些再讲: 问题如何解决？解决方法怎么来的？为什么这么解决, 有什么好处？这个问题的解决会引入什么新问题？作者不厌其繁地讲这些也许会被一些人贴上“死于话多”的标签, 但我想对于初学者来说这总是好的, 国外的教材大抵有这样的特点. </p><p>扯远了, 这里也先给出本文的思路. 先梳理采样基础的内容, 包括对连续时间信号的采样和对离散时间信号（序列）的采样过程, 主要注意采样过程数学模型的建立. 接着由对离散时间信号的采样引出离散时间信号处理的两个操作—-抽取与内插, 再引出这两个操作的应用, 即实现重采样. </p><hr><h1 id="连续时间信号采样"><a href="#连续时间信号采样" class="headerlink" title="连续时间信号采样"></a>连续时间信号采样</h1><p>采样过程主要关注采样过程的数学表述以及采样过程在时域与频域的变化, 这部分内容可参考《信-郑》P.156或《信-奥》P.109. </p><p>讨论连续时间采样时, 我们总是采用“理想采样过程”的叙述, 即是说采用周期冲激串来简化模拟采样过程, 这意味着将忽略单次采样的时间, 或者认为ADC的转化过程是瞬间完成的, 这在数学上是一种简化. </p><p>周期为T的采样周期冲激串表达式为: </p><script type="math/tex; mode=display">P(t)=\sum^{\infty}_{n=-\infty}\delta(t-nT)</script><p>$x_{c}(t)$为被采样信号, $x_{s}(t)$为采样后信号, 整个采样过程由被采样信号与采样冲激串相乘来模拟, 即: </p><script type="math/tex; mode=display">x_{s}(t)=x_{c}(t)P(t)=\sum^{\infty}_{n=-\infty}x_{c}(t)\delta(t-nT)=\sum^{\infty}_{n=-\infty}x_{c}(nT)\delta(t-nT)</script><p>以上即是理想采样过程的时域表达, 频域的分析有两种推导方法, 一种是借由傅里叶级数, 把冲激串傅里叶级数展开, 然后对$x_{c}(t)P(t)$做傅里叶变换；另一种是先利用周期函数傅里叶变换求冲激串的傅里叶变换, 然后借助傅里叶变换的相乘性质去求. 这里用第一种方法说明. </p><p>将$P(t)$用复指数形式傅里叶级数展开: </p><script type="math/tex; mode=display">P(t)=\sum^{\infty}_{n=-\infty}\delta(t-nT)=\sum^{\infty}_{n=-\infty}a_{n}e^{jn\Omega_{s}t}</script><p>其中$a_{n}$为傅里叶级数系数, 而: </p><script type="math/tex; mode=display">\Omega_{s}=\frac{2\pi}{T}=2\pi f_{s}</script><p>$\Omega_{s}$是连续时间的采样角频率, 连续时间的角频率用大写的$\Omega$表示, 而离散时间归一化后的角频率用小写$\omega$表示. </p><p>下面计算系数$a_{n}$:</p><script type="math/tex; mode=display">\begin{align}a_{n} &= \frac{1}{T}\int^{T/2}_{-T/2}P(t)e^{-jn\Omega_{s}t}\mathrm{d}t \\ &= \frac{1}{T}\int^{T/2}_{-T/2}\left[\sum^{\infty}_{n=-\infty}\delta(t-nT)\right]e^{-jn\Omega_{s}t}\mathrm{d}t \\ &= \frac{1}{T}\int^{T/2}_{-T/2}\delta(t)e^{-j\cdot 0 \cdot \Omega_{s}t}\mathrm{d}t \\ &= \frac{1}{T}\end{align}</script><p>于是计算$x_{s}(t)$的傅里叶变换: </p><script type="math/tex; mode=display">\begin{align*}X_{s}(j\Omega) = \mathscr{F}\left[x_{s}(t)\right] &= \mathscr{F}\left[x_{c}(t)\cdot P(t)\right] \\&= \int^{+\infty}_{-\infty}x_{c}(t)P(t)e^{-j\Omega t}\mathrm{d}t \\&= \int^{+\infty}_{-\infty}x_{c}(t) \left[\sum^{\infty}_{n=-\infty}\frac{1}{T}e^{jn\Omega_{s}t}\right]e^{-j\Omega t}\mathrm{d}t \\&= \frac{1}{T}\sum^{\infty}_{n=-\infty}\int^{+\infty}_{-\infty}x_{c}(t)e^{-j(\Omega-n\Omega_{s})t}\mathrm{d}t \\&= \frac{1}{T}\sum^{\infty}_{n=-\infty}X_{c}(j(\Omega-n\Omega_{s}))\end{align*}</script><p>可见采样之后频域变化是把$x_{c}(t)$的频域图像以$\Omega_{s}$进行周期延拓, 并且在幅度上乘因子$\frac{1}{T}$. </p><p>最后的说明, 在连续时间信号采样的数学表达中最后产生的是一个序列$x[n]$, 与$x_{c}(t)$有关系: </p><script type="math/tex; mode=display">x[n]=x_{c}(nT)</script><p>实际上$x[n]$是$x_{s}(t)$把冲激替换为冲激能量的大小数值, 并在时间上做归一化处理得到的. 这部分的更详细的说明可参考《离-奥》P.109~110</p><p>也就是说整个采样过程应该是这样的: </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sampling.PNG" style="zoom:30%" alt="采样过程" align=center /></p><hr><h1 id="离散时间信号采样"><a href="#离散时间信号采样" class="headerlink" title="离散时间信号采样"></a>离散时间信号采样</h1><p>即对序列的采样过程, 参考《信-奥》的7.4节与《离-奥》P.127内容. </p><p>离散时间信号的采样过程的框图应该是这样的:<br><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sampling2.PNG" style="zoom:30%" alt="离散时间采样过程" align=center /></p><p>和连续时间采样一样, $x[n]$是待采样的序列, $P[n]$是用于采样的周期脉冲序列, $[n]$是一个周期脉冲序列, 周期为N（对应于连续时间的周期冲激串）, 而$x_{p}$是采样得到的序列, 有: </p><script type="math/tex; mode=display">x_{p}[n]=x[n]\cdot P[n]=\sum^{\infty}_{k=-\infty}x[kN]\delta[n-kN]</script><p>另外在时域上n为N的整数倍的点上$x[n]$与$x_{p}[n]$值相同: </p><script type="math/tex; mode=display">x_{p}[nN]=x[nN]</script><p>下面给出频域的分析, 由DTFT的相乘性质: </p><script type="math/tex; mode=display">X_{p}(e^{j\omega})=\frac{1}{2\pi}\int_{2\pi}P(e^{j\theta})X(e^{j(\omega-\theta)})\mathrm{d}\theta</script><p>时域乘积频域做周期卷积, 这一性质可参考《信-奥》P.247, DTFT的相乘性质. </p><p>我们已知周期脉冲序列$P[n]$的DTFT:</p><script type="math/tex; mode=display">P(e^{j\omega})=\frac{2\pi}{N}\sum^{\infty}_{k=-\infty}\delta(\omega-k\omega_{s}) \quad \quad\quad\omega_{s}=\frac{2\pi}{N}</script><p>上面的公式属于<strong>周期序列的DTFT</strong>, 所以引入了冲激函数, 这部分可参考《信-奥》P.236与《离-奥》P.443. 于是: </p><script type="math/tex; mode=display">\begin{align*}X_{p}(e^{j\omega}) &= \frac{1}{2\pi}\int_{2\pi}P(e^{j\theta})X(e^{j(\omega-\theta)})\mathrm{d}\theta \\&= \frac{1}{2\pi}\int_{2\pi} \left[ \frac{2\pi}{N}\sum^{\infty}_{k=-\infty}\delta(\theta-k\omega_{s})\right]X(e^{j(\omega-\theta)})\mathrm{d}\theta \\&= \frac{1}{N}\sum^{\infty}_{k=-\infty}X(e^{j(\omega-k\omega_{s})})\int_{2\pi}\delta(\theta-k\frac{2\pi}{N})\mathrm{d}\theta\end{align*}</script><p>这式子稍微有点难理解, 这里是无穷项求和, 每一项中k是确定的, 且都包含后面的积分项, 这个积分项先选定$2\pi$长的积分区间, 只有当$k\frac{2\pi}{N}$落在这个积分区间内时积分值为1, 否则积分值为0. 对于$2\pi$长的积分区间最多有N个k值下积分值不为0, 至于是哪N个k值关系不大, 因为$X(e^{j(\omega-k\omega_{s})})$对k来说是以N为周期的, 于是只需取N个连续k值即可, 比如从0到N-1, 即: </p><script type="math/tex; mode=display">X_{p}(e^{j\omega})=\frac{1}{N}\sum^{N-1}_{k=0}X(e^{j(\omega-k\omega_{s})}) \quad \quad\quad\omega_{s}=\frac{2\pi}{N}</script><p>如果把连续时间采样频域表达式拿过来做个对比: </p><script type="math/tex; mode=display">X_{s}(j\Omega)=\frac{1}{T}\sum^{\infty}_{k=-\infty}X_{c}(j(\Omega-k\Omega_{s}))\quad \quad\quad\Omega_{s}=\frac{2\pi}{T}</script><p>就会发现两式形式差别不大, 采样过程在频域都是对原信号频谱做周期搬移. 而且离散采样得到的就是序列, 不用像连续时间采样那样需要最后转换为离散序列. </p><hr><h1 id="离散时间信号抽取与内插"><a href="#离散时间信号抽取与内插" class="headerlink" title="离散时间信号抽取与内插"></a>离散时间信号抽取与内插</h1><p>在前一小节中得到的序列$x_{p}[n]$可以这样描述: <strong>在n为N的整数倍的点上的值与$x[n]$相同, 其他n值点都是0</strong>, 为了节省存储空间, 需要对$x_{p}[n]$进行压缩, 这里所进行的操作就是“抽取”, 而其逆过程则称为“内插”. </p><p>同样要弄清楚做这两种处理时域与频域的变化及混叠情况, 这部分参考《信-奥》P.353与《离-奥》P.127. </p><p>设对$x_{p}[n]$抽取后得到的序列为$x_{b}[n]$, 于是在时域上有$x_{b}[n]=x_{p}[nN]$（$x_{b}[n]$直接由$x_{p}[n]$N整数倍的点组成）, 又因为之前有$x_{p}[nN]=x[nN]$, 故: </p><script type="math/tex; mode=display">x_{b}[n]=x[nN]=x_{p}[nN]</script><p>然后寻找$x_{b}[n]$、$x_{p}[n]$与$x[n]$频域(DTFT)之间的关系, 推导过程可参考《信-奥》P.353~354. </p><p>首先$x_{b}[n]$的DTFT为: </p><script type="math/tex; mode=display">X_{b}(e^{j\omega})=\sum^{\infty}_{k=-\infty}x_{b}[k]e^{-j\omega k}</script><p>再利用$x_{b}[n]$与$x_{p}[n]$的关系: </p><script type="math/tex; mode=display">X_{b}(e^{j\omega})=\sum^{\infty}_{k=-\infty}x_{p}[kN]e^{-j\omega k}</script><p>做变量替换$n=kN$有$k=\frac{n}{N}$, 于是: </p><script type="math/tex; mode=display">X_{b}(e^{j\omega})=\sum_{n=N \text{的整数倍}}x_{p}[n]e^{-j\omega \frac{n}{N}}</script><p>因为n不为N的整数倍时$x_{p}[n]=0$,故可写为: </p><script type="math/tex; mode=display">X_{b}(e^{j\omega})=\sum^{\infty}_{n=-\infty}x_{p}[n]e^{-j\omega \frac{n}{N}}</script><p>把右边$\frac{n}{N}$看作整体, 右边即对$x_{p}[n]$的DTFT, 即: </p><script type="math/tex; mode=display">X_{p}(e^{j\frac{\omega}{N}})=\sum^{\infty}_{n=-\infty}x_{p}[n]e^{-j\omega \frac{n}{N}}</script><p>联立得到: </p><script type="math/tex; mode=display">X_{b}(e^{j\omega})=X_{p}(e^{j\frac{\omega}{N}})</script><p>之前由对序列采样, 我们已经得到了$x_{p}[n]$与$x[n]$的关系, 如下: </p><script type="math/tex; mode=display">X_{p}(e^{j\omega})=\frac{1}{N}\sum^{N-1}_{k=0}X(e^{j(\omega-k\omega_{s})}) \quad \quad\quad\omega_{s}=\frac{2\pi}{N}</script><p>至此就得到了这三个序列的频域联系, 可见抽取将导致频域扩展. 如果再把上面两式联立得到: </p><script type="math/tex; mode=display">X_{b}(e^{j\omega})=\frac{1}{N}\sum^{N-1}_{k=0}X(e^{j(\frac{\omega}{N}-k\omega_{s})}) \quad \quad\quad\omega_{s}=\frac{2\pi}{N}</script><p>上式后面会看到实际上就是采样率以N倍减小时得到的序列与原采样率下得到序列的频域关系. </p><p>下面是抽取在频域变化的例子:<br><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sampling4.PNG" style="zoom:50%" alt="抽取频域变化" align=center /></p><p>下面是内插的说明, 内插是抽取的逆过程, 可以猜想频域变化为收缩. 参考《离-奥》P.131. </p><p>这里依旧设原序列为$x[n]$, 内插后序列为$x_{e}[n]$, 假设两者时域关系为: </p><script type="math/tex; mode=display">x_{e}[n]= \begin{cases} x[\frac{n}{L}] & n=0,\pm L,\pm 2L,\dots\\ 0 & \text{others}.\\ \end{cases}</script><p>上式即以L为间隔的内插, 实际上就是在序列$x[n]$每两个值之间插入$(L-1)$个零值. 上式的一种等效写法为: </p><script type="math/tex; mode=display">x_{e}[n]=\sum^{\infty}_{k=-\infty}x[k]\delta[n-kL]</script><p>这个式子可以这样理解: $x_{e}[n]$由无穷项相加构成, 只有当$n=kL$时, 那一项才不为0, 而是$x[\frac{n}{L}]$, 与之前的公式描述相同. </p><p>对$x_{e}[n]$做DTFT: </p><script type="math/tex; mode=display">\begin{align*}X_{e}(e^{j\omega})&= \sum^{\infty}_{n=-\infty} \left( \sum^{\infty}_{k=-\infty}x[k]\delta[n-kL]\right)e^{-j\omega n} \\&= \sum^{\infty}_{k=-\infty}x[k] \left(\sum^{\infty}_{n=-\infty} \delta[n-kL]e^{-j\omega n} \right)\\&= \sum^{\infty}_{k=-\infty}x[k] e^{-j\omega Lk} \\&= X(e^{-j\omega L}) \end{align*}</script><p>由此便得到了$x_{e}[n]$与$x[n]$在频域的关系式.<br>上式推导过程中用到了: </p><script type="math/tex; mode=display">\sum^{\infty}_{n=-\infty} \delta[n-kL]e^{-j\omega n}=e^{-j\omega Lk}</script><p>以上讨论了抽取与内插过程, 最后实现抽取的系统称为压缩器, 而实现内插的系统称为扩展器. </p><hr><h1 id="利用离散时间信号处理实现重采样"><a href="#利用离散时间信号处理实现重采样" class="headerlink" title="利用离散时间信号处理实现重采样"></a>利用离散时间信号处理实现重采样</h1><p>这个问题可以看作是之前讨论的抽取与内插的应用, 说的是运用这两种技术实现对原连续时间信号的采样率, 称之为重采样. 需要注意的是<strong>改变的是对连续时间信号采样率</strong>. 这其中涉及用抽取实现的减采样与用内插实现的增采样两部分. </p><p>之前讨论过对连续信号$x_{c}(t)$以<strong>周期T</strong>采样最终将得到序列$x[n]$, 他们之间的关系为: </p><script type="math/tex; mode=display">x[n]=x_{c}(nT)</script><p>所谓“改变采样率”就是说换成<strong>周期为$T_{1}$</strong>去采样, 这样势必会得到一个新的采样序列记为$x_{1}[n]$, 有: </p><script type="math/tex; mode=display">x_{1}[n]=x_{c}(nT_{1})</script><p>现在的问题是$x_{c}[n]$是未知的, $x[n]$已知, 而去求另一个采样率下对$x_{c}(t)$采样得到的序列$x_{1}[n]$, 解决问题的办法通常有两种, 一是先由$x[n]$重构出$x_{c}(t)$, 再调整以周期$T_{1}$重新进行采样得到$x_{1}[n]$, 另一种方法则通过$x[n]$与$x_{1}[n]$的关系, 用离散时间信号处理的方式求得$x_{1}[n]$, 这是这里得重点. </p><p>下面先讨论减采样. 我们的目的还是推导出原采样率下得到的序列与降低采样率后采样得到的序列频域上的关系. </p><p>在降低采样率的情况下, 讨论重新设定采样率为原采样率$\frac{1}{M}$的这种情况. 比如新采样周期$T_{d}$是原周期T的M倍: </p><script type="math/tex; mode=display">T_{d}=MT</script><p>重采样得到的序列记作$x_{d}[n]$, 有: </p><script type="math/tex; mode=display">x_{d}[n]=x_{c}(nT_{d})=x_{c}(nMT)</script><p>利用$x[n]=x_{c}(nT)$, 于是找到两采样序列之间的关系: </p><script type="math/tex; mode=display">x_{d}[n]=x[nM]</script><p>这个时域关系之前刚讨论过, $x_{d}[n]$就是$x[n]$以M抽取得到的, 两者频域关系的表达式在抽取那里已经给出, 这里以M抽取, 故: </p><script type="math/tex; mode=display">X_{d}(e^{j\omega})=\frac{1}{M}\sum^{M-1}_{k=0}X(e^{j(\frac{\omega}{M}-k\frac{2\pi}{M})})</script><p>此式完整的推导过程可参考《离-奥》P.128. </p><hr><h2 id="一个补充"><a href="#一个补充" class="headerlink" title="一个补充"></a>一个补充</h2><p>这里有一个之前一直没想通的地方, 就是之前讨论是这样一个过程:<br><strong>$x[n]$是以周期为T对$x_{c}(t)$采样得到的, 然后是序列的采样过程, 假设采样周期为$N_{1}$, 得到$X_{p}[n]$. 最后对$x_{p}[n]$以比如$N_{2}$抽取, 得$x_{b}[n]$</strong>, 故这些序列在在频域得关系为: </p><script type="math/tex; mode=display">X_{p}(e^{j\frac{\omega}{N_{1}}})=\sum^{\infty}_{n=-\infty}x_{p}[n]e^{-j\omega \frac{n}{N_{1}}}</script><script type="math/tex; mode=display">X_{b}(e^{j\omega})=X_{p}(e^{j\frac{\omega}{N_{2}}})</script><p>联立得: </p><script type="math/tex; mode=display">X_{b}(e^{j\omega})=\frac{1}{N_{1}}\sum^{N_{1}-1}_{k=0}X(e^{j(\frac{\omega}{N_{2}}-k\frac{2\pi}{N_{1}})})</script><p>这与前面$X_{d}(e^{j\omega})$对比, 会发现如果有$N_{1}=N_{2}=M$时, 两者完全相等. 这个等式成立是合理的, 事实上在推导的时候也是默认成立的, 不然就失去了意义. </p><p>另一点要注意的是对$x[n]$以N抽取与对$x_{p}[n]$以N抽取得到的序列相同, 都是$x_{b}[n]$, 因为抽取操作只关心n为N整数倍的那些点. </p><p>以上所有讨论的序列关系如下图: </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sampling5.PNG" style="zoom:30%" alt="序列关系" align=center /></p><hr><p>接下来是减采样系统的构成: </p><p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sampling6.PNG" style="zoom:30%" alt="减采样系统" align=center /></p><p>可见系统除了一个实现抽取操作的压缩器外还有个抗混叠滤波器, 这个滤波器的主要作用是把$x[n]$的频域截断到$\frac{\pi}{M}$, 因为进行以M为周期的抽取等效于对频域扩展M倍, 要不发生混叠, 压缩器输入信号带宽最大就是$\frac{\pi}{M}$. </p><p>同样对于增采样过程, 若以L倍增加采样率, 实际上可以通过对$x[n]$以L内插得到采样序列$x_{e}[n]$. 之前得到他们在频域上: </p><script type="math/tex; mode=display">X_{e}(e^{j\omega})=X(e^{j\omega L})</script><p>而增采样系统一般为:<br><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/sampling7.PNG" style="zoom:23%" alt="减采样系统" align=center /></p><p>可以看到除了一个实现内插的扩展器外还有一个低通滤波器, 这个低通滤波器是必须的. </p><p>一方面, 增益为L进行幅度调整, 对于以周期为T进行采样得到的序列$x[n]$, 在频域幅度是被采样信号的$\frac{1}{T}$, 故若将将采样周期改变为$\frac{T}{L}$, 应把幅度也调整为$\frac{L}{T}$, 而之前减采样过程的频域表达式中本身就会乘以因子$\frac{1}{M}$, 将$\frac{1}{T}$调整为$\frac{1}{MT}$, 因此不需要滤波器再对幅度进行调整. </p><p>另一方面经过$\frac{\pi}{L}$的低通滤波器后, 能滤除频谱多余部分, 使剩下来的部分以$2\pi$为周期. </p><hr><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>本文首先回顾了连续时间信号的采样过程与数学表达, 然后将采样的概念引到序列处理, 讨论了序列采样过程. 由于离散时间信号存取与处理特点, 引出序列的抽取与插值操作, 最后讨论利用抽取与插值实现改变采样率这一应用. </p><p>这里所讨论的采样都是时域的采样, 实际上在采样这块另一个重要的内容是对傅里叶变换的采样（频域的采样）, 特别在离散时间信号处理中对DTFT、DFT的采样, 这些内容对于理解另一类按频率抽取的FFT算法是必要的.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近又在复习信号处理的东西, 对于一些内容有了更深入的理解, 于是做一个关于采样的总结. 众所周知, 自然界大多数信号是模拟信号, 采样是数字系统处理的第一步, 主要由各种传感器配合A/D来实现. 在&lt;a href=&quot;https://granvallen.github.io/fft/&quot;&gt;FFT为什么那么快&lt;/a&gt;一文中提到了“重采样”的概念, 在那里一笔带过了, 本文也算是一个补充. &lt;/p&gt;</summary>
    
    
    
    <category term="Lab" scheme="https://granvallen.github.io/categories/Lab/"/>
    
    <category term="信号处理" scheme="https://granvallen.github.io/categories/Lab/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    
    <category term="DSP" scheme="https://granvallen.github.io/categories/Lab/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/DSP/"/>
    
    
    <category term="DFT" scheme="https://granvallen.github.io/tags/DFT/"/>
    
    <category term="FFT" scheme="https://granvallen.github.io/tags/FFT/"/>
    
  </entry>
  
  <entry>
    <title>如何使用LaTeX绘图</title>
    <link href="https://granvallen.github.io/latextikz/"/>
    <id>https://granvallen.github.io/latextikz/</id>
    <published>2017-04-18T14:55:00.000Z</published>
    <updated>2021-02-24T06:23:10.872Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/fft_16.PNG"  style="zoom:60%" alt="" align=center /></p><p>之前有朋友问我如何使用$\LaTeX$绘图. </p><span id="more"></span><p>之前我写文章通常是使用绘图工具先保存为图片格式, 然后再插入文章的. 了解了下发现Latex可以使用TikZ宏包来进行绘制, 功能很强大, 函数图像、统计图、各类流程图都不在话下, 甚至是复杂信号流图如FFT的蝶形图也能简单地画出来, 这里放个绘制实例供参考. </p><p>TikZ一般与PGF宏包一起使用, 两者是上层封装与底层实现的关系. 学习的途径无疑是查阅参考文档与模仿例子, 在<a href="http://pgf.sourceforge.net/">这里</a>可以下载到TikZ与PGF完整参考文档, 而在<a href="http://www.texample.net/tikz/examples/">这里</a>能看到丰富的应用, 另外在一些问答社区如<a href="https://tex.stackexchange.com/search?q=tikz">这里</a>可以找到一些实用的使用技巧. </p><p>TikZ的参考文档虽然有1000多页, 但对于简单应用, 参看前面几个应用的Tutorial就够了. 但如果你是个完美主义者或是有强迫症, 那么这又会是一个天坑！TikZ是一个复杂的“所思即所得”的绘制系统, 相对于同样强大的Visio来说. </p><p>下面是一个如何绘制流程图的例子. 完成后大概是这个样子:<br><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/fftt.png"  style="zoom:50%" alt="" align=center /></p><p>首先需要加载TikZ宏包以及绘图需要用到的库.<br><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">%------------------------------------- TIKZ --------------------</span><br><span class="line">\usepackage&#123;tikz&#125;%画图</span><br><span class="line">    \usetikzlibrary&#123;graphs,arrows,shapes,chains,quotes&#125;</span><br><span class="line">%------------------------------------- TIKZ END-----------------</span><br></pre></td></tr></table></figure></p><p>然后实际的绘图是在<strong>tikzpicture</strong>环境中进行的, 就像figure环境一样. </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">\begin&#123;tikzpicture&#125;[环境及元素设定]</span><br><span class="line">...</span><br><span class="line">绘图语句</span><br><span class="line">...</span><br><span class="line">\end&#123;tikzpicture&#125;</span><br></pre></td></tr></table></figure><p>绘制流程图的过程大概是这样的: 首先你需要先设定一些元素及基本的环境, 比如矩形框、菱形框、各类符号等等；然后是在图中定位他们, 定位的方法有很多, 由于我们要化的图像比较规则, 这里使用<strong>\matrix</strong>的定位方法；最后是连接与标注, 同样有很多方法, 怎么方便怎么来就行了. </p><p>元素及环境的设定写在[]中实现起来大概是这样: </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">\begin&#123;tikzpicture&#125;[</span><br><span class="line">    %inner sep=2mm, %边框到形状内字符距离</span><br><span class="line">    &gt; = stealth, %箭头样式</span><br><span class="line">    -&gt; /.style=&#123;thick ,black&#125;,</span><br><span class="line">    -- /.style=&#123;thick ,black&#125;,</span><br><span class="line">    line width=1pt,%连接线宽</span><br><span class="line">    every edge quotes/.style=&#123;font=\tiny,auto&#125;, %标注样式</span><br><span class="line">    every node/.style=&#123;font=\tiny&#125;,</span><br><span class="line">    point/.style=&#123;inner sep=0pt&#125;, %point样式</span><br><span class="line">    terminal/.style=&#123;circle,draw,thin,inner sep=0pt&#125;, %terminal样式</span><br><span class="line">    rect/.style=&#123;rectangle,draw,thick,inner sep=0pt,minimum size=6mm,font=\itshape&#125;, %rect样式</span><br><span class="line">    plus/.style=&#123;circle,draw,thick,inner sep=-1pt&#125;, %plus样式</span><br><span class="line">    hv path/.style=&#123;to path=&#123;-| (\tikztotarget)&#125;&#125;, %直角折线样式</span><br><span class="line">    vh path/.style=&#123;to path=&#123;|- (\tikztotarget)&#125;&#125;</span><br><span class="line">]</span><br><span class="line">...</span><br><span class="line">元素定位语句</span><br><span class="line">连接与标注语句</span><br><span class="line">...</span><br><span class="line">\end&#123;tikzpicture&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>然后在“元素定位语句”处使用<strong>\matrix</strong>进行排布元素, 使用matrix的语法和\begin{aligned}类似: </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">%流程图骨架</span><br><span class="line">%       t1    p2    r1    r2    pl1    t2</span><br><span class="line">   %             r3                r4</span><br><span class="line">   %             p4    r5    r6    p5</span><br><span class="line"></span><br><span class="line">   \matrix[row sep=5mm,column sep=10mm]&#123;</span><br><span class="line">       \node (t1) [terminal] &#123;&#125;; &amp; </span><br><span class="line">       \node (p2) [point] &#123;&#125;; &amp; </span><br><span class="line">       \node (r1) [rect] &#123;$\downarrow 2$&#125;; &amp; &amp;</span><br><span class="line">       \node (r2) [rect] &#123;$\uparrow 2$&#125;; &amp; </span><br><span class="line">       \node (pl1) [plus] &#123;$+$&#125;; &amp;</span><br><span class="line">       \node (t2) [terminal] &#123;&#125;; &amp; \\</span><br><span class="line">       %------------------------------------------</span><br><span class="line">       &amp; \node (r3) [rect] &#123;左移1&#125;; &amp; &amp; &amp; &amp; \node (r4) [rect] &#123;右移1&#125;; \\</span><br><span class="line">       %------------------------------------------</span><br><span class="line">       &amp; \node (p4) [point] &#123;&#125;; &amp;</span><br><span class="line">         \node (r5) [rect] &#123;$\downarrow 2$&#125;; &amp; &amp;</span><br><span class="line">         \node (r6) [rect] &#123;$\uparrow 2$&#125;; &amp; </span><br><span class="line">         \node (p5) [point] &#123;&#125;; &amp; \\</span><br><span class="line">   &#125;;</span><br><span class="line"></span><br><span class="line">   连接与标注语句</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>此时看起来应该是这个效果:<br><img src="https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/tikz.PNG"  style="zoom:50%" alt="" align=center /></p><p>其中的\node是非常常用的命令, 可以用于放置元素与标签. ()中为node别名（在其他地方可以使用别名调用相应元素）；[]中为node属性, 可以设定之前定义的各种元素样式；{}中为node内部的字符串, 比如某个被定义为矩形框的node中的字符串, 支持使用Latex的数学公式的插入. 更详细的说明参考文档. </p><p>流程图中的连接与标注往往是同时进行的. 标注分为两种, 一种是在连接线上, 另一种是需要定位到某个固定的点. 连接这里使用<strong>\graph</strong>命令, 这里需要两条通路, 顺便可以在<strong>quotes</strong>库的帮助下完成线上标注. </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">\graph[use existing nodes]&#123;</span><br><span class="line">    t1  -&gt; </span><br><span class="line">    r1 -&gt;[&quot;$g[n]=x[2n]$&quot;,&quot;$G(e^&#123;j\omega&#125;)$&quot;&#x27;] </span><br><span class="line">    r2 -&gt; </span><br><span class="line">    pl1 -&gt; t2;</span><br><span class="line"></span><br><span class="line">    t1 -&gt;[hv path] </span><br><span class="line">    r3 -&gt;[vh path] </span><br><span class="line">    r5 -&gt;[&quot;$h[n]=x[2n+1]$&quot;,&quot;$H(e^&#123;j\omega&#125;)$&quot;&#x27;] </span><br><span class="line">    r6 -&gt;[hv path]</span><br><span class="line">    r4 -&gt;[&quot;$x[x-1]$&quot; right] pl1;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>特殊点的标注使用<code>\node</code>: </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">\node [above] at (t1) &#123;$x[n]$&#125;;</span><br><span class="line">\node [above] at (t2) &#123;$x[n]$&#125;;</span><br><span class="line">\node [below] at (p4) &#123;$x[n+1]$&#125;;</span><br><span class="line">\node [below] at (p5) &#123;$0\leqslant n\leqslant N-1$&#125;;</span><br></pre></td></tr></table></figure><p>呼, 这样就完成了. </p><p>在TizK的帮助下能轻松绘出FFT的蝶形流图, 我从这个<a href="http://www.texample.net/tikz/examples/radix2fft/">例子</a>中得到了帮助. 下面是完整16点FFT蝶形图的实现code. </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br></pre></td><td class="code"><pre><span class="line">\documentclass&#123;article&#125;</span><br><span class="line"></span><br><span class="line">\usepackage&#123;tikz&#125;</span><br><span class="line">\usetikzlibrary&#123;arrows,graphs,decorations.markings&#125;</span><br><span class="line"></span><br><span class="line">\begin&#123;document&#125;</span><br><span class="line">\pagestyle&#123;empty&#125;</span><br><span class="line"></span><br><span class="line">% 辅助计数器</span><br><span class="line">\newcounter&#123;x&#125;</span><br><span class="line">\newcounter&#123;y&#125;</span><br><span class="line"></span><br><span class="line">\begin&#123;tikzpicture&#125;[</span><br><span class="line">    yscale=.5, </span><br><span class="line">    xscale=1.5, </span><br><span class="line">    %node distance=1cm, </span><br><span class="line">    auto,</span><br><span class="line">    &gt; = stealth,</span><br><span class="line">    -&gt; /.style=&#123;thick, black&#125;,</span><br><span class="line">    -- /.style=&#123;thick, black&#125;,</span><br><span class="line">    -&gt;-/.style=&#123;decoration=&#123;</span><br><span class="line">  markings,</span><br><span class="line">  mark=at position #1 with &#123;\arrow&#123;&gt;&#125;&#125;&#125;,</span><br><span class="line">  postaction=&#123;decorate&#125;&#125;,</span><br><span class="line">  n/.style=&#123;</span><br><span class="line">  circle, </span><br><span class="line">  draw, </span><br><span class="line">  minimum size=2pt, </span><br><span class="line">  inner sep=0pt, </span><br><span class="line">  outer sep=0pt&#125;,</span><br><span class="line">    ]</span><br><span class="line">    % The strategy is to create nodes with names: N-column-row -&gt; N-列-行</span><br><span class="line"></span><br><span class="line">    %----------------------- 输入输出 -------------------------------------------------------</span><br><span class="line">    % 输入结点命名规则 N-0-0 ... N-0-15</span><br><span class="line">    % 输出结点命名规则 N-8-0 ... N-8-15</span><br><span class="line">    % Draw inputs</span><br><span class="line">    \foreach \y in &#123;0,...,15&#125;</span><br><span class="line">        \node[n,pin=&#123;[pin edge=&#123;&lt;-&#125;]left:$x[\y]$&#125;] </span><br><span class="line">            (N-0-\y) at (0,-\y) &#123;&#125;;</span><br><span class="line"></span><br><span class="line">    % Draw outputs \y计数  idx显示</span><br><span class="line">    \foreach \y / \idx in &#123;0/0,1/8,2/4,3/12,4/2,5/10,6,7/14,</span><br><span class="line">                           8/1,9,10/5,11/13,12/3,13/11,14/7,15&#125;</span><br><span class="line">        \node[n, pin=&#123;[pin edge=&#123;-&gt;&#125;]right:$X[\idx]$&#125;] (N-8-\y) at (6,-\y) &#123;&#125;;</span><br><span class="line"></span><br><span class="line">   %-------------------------- 画连接点 ---------------------------------------------------</span><br><span class="line">   %     0      1   2 3   4 5   6   7     8</span><br><span class="line">   %     in     o - o o - o o - o - o     out</span><br><span class="line"></span><br><span class="line">   % 7个中间连接点</span><br><span class="line">    \foreach \y in &#123;0,...,15&#125;</span><br><span class="line">        \foreach \x / \c in &#123;1/1, 1.5/2, 2.5/3, 3/4, 4/5, 4.5/6, 5.5/7&#125;</span><br><span class="line">            \node[n] (N-\c-\y) at (\x,-\y) &#123;&#125;;</span><br><span class="line"></span><br><span class="line">    %----------------------- 画连接线 -----------------------------------------------------</span><br><span class="line">    % 水平线连接</span><br><span class="line"></span><br><span class="line">    \foreach \y in &#123;0,...,15&#125;%行</span><br><span class="line">        \foreach \x in &#123;0,2,4,6&#125;%列</span><br><span class="line">        &#123;</span><br><span class="line">            \setcounter&#123;x&#125;&#123;\x&#125;\stepcounter&#123;x&#125;</span><br><span class="line">            \path (N-\x-\y) edge[-&gt;-=.87] (N-\arabic&#123;x&#125;-\y);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    \foreach \y in &#123;0,...,15&#125;%行</span><br><span class="line">        \foreach \x in &#123;1,3,5,7&#125;%列</span><br><span class="line">        &#123;</span><br><span class="line">            \setcounter&#123;x&#125;&#123;\x&#125;\stepcounter&#123;x&#125;</span><br><span class="line">            \path (N-\x-\y) edge[-&gt;-=.5] (N-\arabic&#123;x&#125;-\y);</span><br><span class="line">        &#125;    </span><br><span class="line"></span><br><span class="line">    % 斜线连接</span><br><span class="line">    \foreach \sourcey / \desty in &#123;0/8,1/9,2/10,3/11,</span><br><span class="line">                                   4/12,5/13,6/14,7/15,</span><br><span class="line">                                   8/0,9/1,10/2,11/3,</span><br><span class="line">                                   12/4,13/5,14/6,15/7&#125;</span><br><span class="line">       \path (N-0-\sourcey) edge[-&gt;-=.04] (N-1-\desty);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    \foreach \sourcey / \desty in &#123;0/4,1/5,2/6,3/7,</span><br><span class="line">                                   4/0,5/1,6/2,7/3,</span><br><span class="line">                                   8/12,9/13,10/14,11/15,</span><br><span class="line">                                   12/8,13/9,14/10,15/11&#125;</span><br><span class="line">        \path (N-2-\sourcey) edge[-&gt;-=.97] (N-3-\desty);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    \foreach \sourcey / \desty in &#123;0/2,1/3,2/0,3/1,</span><br><span class="line">                                   4/6,5/7,6/4,7/5,</span><br><span class="line">                                   8/10,9/11,10/8,11/9,</span><br><span class="line">                                   12/14,13/15,14/12,15/13&#125;</span><br><span class="line">        \path (N-4-\sourcey) edge[-&gt;-=.95] (N-5-\desty);</span><br><span class="line"></span><br><span class="line">    \foreach \sourcey / \desty in &#123;0/1,1/0,2/3,3/2,</span><br><span class="line">                                   4/5,5/4,6/7,7/6,</span><br><span class="line">                                   8/9,9/8,10/11,11/10,</span><br><span class="line">                                   12/13,13/12,14/15,15/14&#125;</span><br><span class="line">        \path (N-6-\sourcey) edge[-&gt;-=.3] (N-7-\desty);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    %----------------------- 标注旋转因子 -----------------------------------------------</span><br><span class="line"></span><br><span class="line">    % W_16</span><br><span class="line">    \setcounter&#123;y&#125;&#123;8&#125;</span><br><span class="line">    \foreach \i in &#123;0,...,7&#125;</span><br><span class="line">    &#123;</span><br><span class="line">        \path (N-1-\arabic&#123;y&#125;) edge[-&gt;-=.5] node &#123;\tiny $W^&#123;\i&#125;_&#123;16&#125;$&#125; (N-2-\arabic&#123;y&#125;);</span><br><span class="line">        \node[below] at (N-1-\arabic&#123;y&#125;.south) &#123;\tiny -1&#125;;</span><br><span class="line">        \stepcounter&#123;y&#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    % W_8</span><br><span class="line">    \setcounter&#123;y&#125;&#123;4&#125;</span><br><span class="line">    \foreach \j in &#123;1,2&#125;</span><br><span class="line">    &#123;</span><br><span class="line">    \foreach \i in &#123;0,2,4,6&#125;</span><br><span class="line">    &#123;</span><br><span class="line">        \path (N-3-\arabic&#123;y&#125;) edge[-&gt;-=.5] node &#123;\tiny $W^&#123;\i&#125;_&#123;16&#125;$&#125; (N-4-\arabic&#123;y&#125;);</span><br><span class="line">        \node[below] at (N-3-\arabic&#123;y&#125;.south) &#123;\tiny -1&#125;;</span><br><span class="line">        \stepcounter&#123;y&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    \addtocounter&#123;y&#125;&#123;4&#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    % W_4</span><br><span class="line">    \setcounter&#123;y&#125;&#123;2&#125;</span><br><span class="line">    \foreach \j in &#123;1,...,4&#125;</span><br><span class="line">    &#123;</span><br><span class="line">    \foreach \i in &#123;0,4&#125;</span><br><span class="line">    &#123;</span><br><span class="line">        \path (N-5-\arabic&#123;y&#125;) edge[-&gt;-=.5] node &#123;\tiny $W^&#123;\i&#125;_&#123;16&#125;$&#125; (N-6-\arabic&#123;y&#125;);</span><br><span class="line">        \node[below] at (N-5-\arabic&#123;y&#125;.south) &#123;\tiny -1&#125;;</span><br><span class="line">        \stepcounter&#123;y&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    \addtocounter&#123;y&#125;&#123;2&#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    % W_2</span><br><span class="line">    \setcounter&#123;y&#125;&#123;1&#125;</span><br><span class="line">    \foreach \j in &#123;1,...,8&#125;</span><br><span class="line">    &#123;</span><br><span class="line">    \foreach \i in &#123;0&#125;</span><br><span class="line">    &#123;</span><br><span class="line">        \path (N-7-\arabic&#123;y&#125;) edge[-&gt;-=.5] node &#123;\tiny $W^&#123;\i&#125;_&#123;16&#125;$&#125; (N-8-\arabic&#123;y&#125;);</span><br><span class="line">        \node[below] at (N-7-\arabic&#123;y&#125;.south) &#123;\tiny -1&#125;;</span><br><span class="line">        \stepcounter&#123;y&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    \addtocounter&#123;y&#125;&#123;1&#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">\end&#123;tikzpicture&#125;</span><br><span class="line">\end&#123;document&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Granvallen/granvallen.github.io/master/img/fft_16.PNG&quot;  style=&quot;zoom:60%&quot; alt=&quot;&quot; align=center /&gt;&lt;/p&gt;
&lt;p&gt;之前有朋友问我如何使用$\LaTeX$绘图. &lt;/p&gt;</summary>
    
    
    
    <category term="Note" scheme="https://granvallen.github.io/categories/Note/"/>
    
    <category term="LaTeX" scheme="https://granvallen.github.io/categories/Note/LaTeX/"/>
    
    
    <category term="LaTeX" scheme="https://granvallen.github.io/tags/LaTeX/"/>
    
    <category term="TikZ" scheme="https://granvallen.github.io/tags/TikZ/"/>
    
  </entry>
  
  <entry>
    <title>《语法俱乐部》笔记 3.高级篇</title>
    <link href="https://granvallen.github.io/grammarclub3/"/>
    <id>https://granvallen.github.io/grammarclub3/</id>
    <published>2017-03-11T22:32:00.000Z</published>
    <updated>2021-02-24T06:23:10.874Z</updated>
    
    <content type="html"><![CDATA[<p>这次总算把高级篇的内容整理出来了, 顺带重新整理了下另外几篇的排版. </p><p>在这本书的高级部分主要讨论了之前中级篇中三种复句的简化方法, 以及常用的特殊句型, 比如倒装句的用法. </p><span id="more"></span><h1 id="从句简化通则"><a href="#从句简化通则" class="headerlink" title="从句简化通则"></a>从句简化通则</h1><p>省略主语与be动词, 只留补语. </p><p>可能面临的问题</p><ul><li>1.若省略从属从句的主语会造成主语不清时该如何处理？</li><li>2.剩下补语部分如果词类与原来从属从句词类不同时怎么办？</li><li>3.连接词是否应一并省略？</li></ul><h2 id="为何省略从句主语？"><a href="#为何省略从句主语？" class="headerlink" title="为何省略从句主语？"></a>为何省略从句主语？</h2><p>如果从句主语是空的字眼(one,everybody,people等), 或者从句主语在主句中重复出现, 从修辞的角度, 有违精简的原则. 如: </p><ul><li>It is common courtesy (that one should wear black while one attends a funeral(葬礼).)<br>→It is common courtesy to wear black while attending a funeral.</li></ul><p>这里one should wear black→[ought] to wear black,去掉主语should变成了不定式. </p><h2 id="为何省略be动词？"><a href="#为何省略be动词？" class="headerlink" title="为何省略be动词？"></a>为何省略be动词？</h2><p>在简单句的5种基本句型中, 有4种都是通过动词来描述主语在干什么. 如</p><ul><li>Birds fly.→S+V</li><li>Birds eat worms.→S+V+O</li><li>Birds give us song.→S+V+O+O</li><li>Birds make the morning beautiful.→S+V+O+C</li></ul><p>至于S+V+C由于系动词（尤其是be动词）不能描述具体特性, 反而要靠后来的补语来做全部的叙述, 告诉别人主语怎么样. 而be动词只扮演串联主语与补语的角色, 故世称之为系动词. 如</p><ul><li>Birds are lovely.</li></ul><p>试想如果Birds are lovely是一个从句, Birds与主句主语重复, 那么只需留下lovely即可. 而重复的主语与be动词则是多余的. </p><p>因此为何省略be动词呢？简单来说就是因为没有具体的含义, 省略不会影响表达. </p><h2 id="没有be动词的时候怎么办？"><a href="#没有be动词的时候怎么办？" class="headerlink" title="没有be动词的时候怎么办？"></a>没有be动词的时候怎么办？</h2><p>如果没有be动词, 分两种情况处理: </p><p><strong>1.有语气助动词时可以变成不定式</strong></p><p>这是因为所有语气助动词（情态动词）都可以改写成<strong>be+不定式</strong>,如</p><ul><li><p>You must go at once.<br>→You are to go at once.</p></li><li><p>The train will leave in 10 minutes.<br>→The train is to leave in 10 minutes.</p></li><li><p>He should do as I say.<br>→He is to do as I say.</p></li></ul><p>助动词改写为be加不定式, 表达的意思可能不如原来精准. 这算是为简洁所做的牺牲. </p><ul><li>He studied hard so that he could get a scholarship.<br>could geta→was [able] to get→省略was→to get a scholarship</li><li>→He studied hard to get a scholarship.</li></ul><p>修饰以趋于原意</p><p>→He studied hard so as to get a scholarship.<br>→He studied hard in order to get a scholarship.</p><p><strong>2.没有助动词时, 变为ving</strong><br>如</p><ul><li>John remembers that he saw the lady before.<br>he was seeing the lady before.</li></ul><p>→John remembers seeing the lady before.</p><hr><p>下面分不同复句讨论如何简化的细节. </p><h1 id="关系从句的简化"><a href="#关系从句的简化" class="headerlink" title="关系从句的简化"></a>关系从句的简化</h1><p>在前面讨论关系从句的时候也简单讨论过他的简化方法, 即如果关系词充当从句的宾语部分, 则可省略关系词. </p><ul><li>1.<strong>The man</strong> is here.</li><li>2.You asked about <strong>him</strong>.</li></ul><p>→The man whom you asked about is here.<br>简化<br>→The man you asked about is here.</p><p>关系词的省略只能算是一般性的省略, 从句中仍有主语、动词, 所以不算是真正的简化从句. 如果主从句相同, 那么省略主语与be动词就是典型的形容词简化. </p><p>以下就简化之后所留下的不同补语分别讨论. </p><h2 id="补语为过去分词-ven"><a href="#补语为过去分词-ven" class="headerlink" title="补语为过去分词(ven)"></a>补语为过去分词(ven)</h2><p>如果关系从句中是被动态, 就会简化成为过去分词的补语部分. </p><p>如</p><ul><li>1.Beer is most delicious.</li><li>2.It is chilled to 6℃.</li></ul><p>→Beer <strong>which is chilled to 6℃</strong> is most delicious.<br>简化<br>→Beer <strong>chilled to 6℃</strong> is most delicious.<br>分词属于形容词类, 词类无冲突. </p><p>又如</p><ul><li>Your brothor John, who was wounded in war, will soon be sent home.</li></ul><p>→Your brothor John, wounded in war, will soon be sent home.</p><hr><h2 id="补语为现在分词-ving"><a href="#补语为现在分词-ving" class="headerlink" title="补语为现在分词(ving)"></a>补语为现在分词(ving)</h2><p>如果关系从句中动词是be+ving的形式, 只要省略主语与be动词即可. 如</p><ul><li>The ship <strong>which is coming to shore</strong> is from Gaoxiong.</li></ul><p>→The ship <strong>coming to shore</strong> is from Gaoxiong.</p><hr><h2 id="补语为to-do"><a href="#补语为to-do" class="headerlink" title="补语为to do"></a>补语为to do</h2><p>如果关系从句的动词有语气助动词存在, 就会成为不定词补语留下来.<br>如</p><ul><li>John is the one who should go this time.<br>→who is to go</li><li>→John is the one to go this time.</li></ul><p>简化时注意的问题:<br><strong>1.不定式的主动、被动区分清楚. </strong><br><strong>2.不定式有无宾语判断. </strong><br>如</p><ul><li>1.This is exactly the thing to do.</li><li>2.This is exactly the time to do it.</li></ul><p>第一句可视为是:<br>This is exactly <strong>the thing</strong>.<br>We should do <strong>the thing</strong>.<br>这两句话的复句简化</p><p>第二句可视为是:<br>This is exactly <strong>the time</strong>.<br>We should do it <strong>at this time</strong>.<br>复句的简化</p><p><strong>3.不定式后有无介词的判断. </strong><br>如</p><ul><li>1.He will be the toughest <strong>guy</strong>.</li><li>2.You must deal with <strong>the guy</strong>.</li></ul><p>→He will be the toughest guy [whom] you must deal with.<br>→He will be the toughest guy to deal with.</p><p><strong>4.从句主语省略意思是否改变</strong><br>如</p><ul><li>I have a job that your brother can do.</li></ul><p>若省略从句主语, 则变成 I have a job to do. 改变了原意.<br>这时可以用合适的介词短语插入主语, 如: </p><ul><li>I have a job for your brother to do.</li></ul><h2 id="补语为一般形容词"><a href="#补语为一般形容词" class="headerlink" title="补语为一般形容词"></a>补语为一般形容词</h2><p>若关系从句的动词是be动词, 后面是单纯的形容词类做补语, 可以直接简化主语和be动词.<br>如: </p><ul><li>Hilary Clinton, <strong>who is pretty and intelligent</strong>, is a popular First Lady.<br>→Hilary Clinton, <strong>pretty and intelligent</strong>, is a popular First Lady.</li></ul><p>形容词一般只有两种位置: 名词短语中及补语位置, 如果两位置都不是, 那么多半是简化形容词从句的残留补语. </p><hr><h2 id="补语为名词"><a href="#补语为名词" class="headerlink" title="补语为名词"></a>补语为名词</h2><p>依旧只需省略主语与be动词, 让补语名词做同位语. 如: </p><ul><li>Bill Clinton, who is President of the U.S., is a Baby Boomer.<br>→Bill Clinton, President of the U.S., is a Baby Boomer.</li></ul><hr><h1 id="名词从句的简化"><a href="#名词从句的简化" class="headerlink" title="名词从句的简化"></a>名词从句的简化</h1><p>名词从句与之前所讲的关系从句十分类似, 能够省去从句主语与be动词, 连接词that也可以一并省略. 若名词从句由疑问句演变而来, 以疑问词充当连接词, 那么疑问词需要保留, 因为是有明确意义的字眼. </p><p>名词从句简化之后, 剩下来的补语常见有ving与to do（动名词与不定式）皆可作为名词使用, 故词类无冲突. 而如果是动词分词, 就需要在词类上进一步改造. </p><h2 id="补语是ving的情况"><a href="#补语是ving的情况" class="headerlink" title="补语是ving的情况"></a>补语是ving的情况</h2><p>处理与关系从句完全一样. 如: </p><ul><li><strong>That I drink good wine with friends</strong> is my greatest enjoyment.<br>→<strong>Drinking good wine with friends</strong> is my greatest enjoyment.</li></ul><p>换个宾语的例子: </p><ul><li>Many husbands enjoy <strong>that they do the cooking</strong>.<br>→Many husbands enjoy <strong>doing the cooking</strong>.</li></ul><p>换个介词后宾语的例子: </p><ul><li>1.He got used to something.</li><li>2.He worked late into the night.</li></ul><p>第2句是第1句something的内容, 但由于语法中说that引导的从句不能作为介词宾语, 但可以简化之后再合在一起. </p><p>→He got used to working late into the night.</p><hr><p>前面说的是一般情况, 有下面几种常见的特殊情况: </p><h3 id="主语不能省略时"><a href="#主语不能省略时" class="headerlink" title="主语不能省略时"></a>主语不能省略时</h3><p>这种情况一般是主从句主语不一致引起的. </p><p><strong>1.试着将整个句子改写成S+V+O+C的句型. 此时需要名词从句在宾语位置, 且主句动词能适用该句型</strong>. 如</p><ul><li>I imagined <strong>that a beautiful girl was singing to me</strong>.</li></ul><p>主从句主语不同, 不能直接省略, 但还是可以省略be动词与that. </p><p>→I imagined a beautiful girl singsing to me.</p><hr><p><strong>2.使用所有格来处理</strong></p><ul><li><strong>That he calls my girlfriend every day</strong> is too much for me.</li></ul><p>这里he同样不能省略, 按照一般的简化流程, calling my grilfriend every day成为主句主语, 没有he的位置了, 此时可以把he变为所有格, 就能放在calling之前. </p><ul><li>His calling my girlfriend every day is too much for me.</li></ul><p><strong>3.加介词来处理</strong><br>只适合一些比较特殊的句型. </p><ul><li><p>I’m worried <strong>that my son lies all the time</strong>.<br>→I’m worried about my son’s lying all the time.<br>或</p></li><li><p>→I’m worried about my son lying all the time.</p></li></ul><h3 id="被动态的处理"><a href="#被动态的处理" class="headerlink" title="被动态的处理"></a>被动态的处理</h3><p>被动态中省略从句主语与be动词, 剩下动词被动态如改为过去分词是形容词类, 这里需要名词类, 只好另想办法. </p><ul><li>That anyone is called a liar(说谎者) is the greatest insult.</li></ul><p>如果按照之前所说的省略明显不对. 这里需要借用前面的be变成being,起名词的作用, 这里be纯粹只有词类变化的功能. </p><p>→Being called a liar is the greatest insult.</p><p>有时这里需要把被动动词改成相应动名词. </p><hr><h2 id="补语是to-do的情况"><a href="#补语是to-do的情况" class="headerlink" title="补语是to do的情况"></a>补语是to do的情况</h2><p>名词从句的简化, 若从句中有语气助动词, 就会成为不定式. 如: </p><ul><li>The children expect that they can get presents for Christmas.</li></ul><p>简化时先把助动词改写为be to形式. 成为</p><ul><li>The children expert to get presents for Chrismas.</li></ul><p>不定式不一定是什么词类, 可当名词、形容词、副词, 故可不必考虑词类是否符合的问题. 唯一需要注意的是, 不定式不合适放在介词后, 这时需要改为ving形式. </p><hr><p>这里同样也会有一些特殊情况. </p><h3 id="从句主语不能省略"><a href="#从句主语不能省略" class="headerlink" title="从句主语不能省略"></a>从句主语不能省略</h3><p>同样考虑可否变成S+V+O+C句型. 如</p><ul><li>1.I want that you should go.</li><li>→I want that you are to go.</li><li>→I want you to go.</li></ul><p>另外, 在大多情况可以添加介词, 将不能省略的从句主语放在其后. (P.347)</p><h3 id="疑问句改造而来的从句简化"><a href="#疑问句改造而来的从句简化" class="headerlink" title="疑问句改造而来的从句简化"></a>疑问句改造而来的从句简化</h3><p>其他类似, 只是充当引导词的疑问词不能省略. 如</p><ul><li>1.What should I do?</li><li>2.I don’t know the puestion.</li><li>→I don’t know <strong>what should I do</strong>.</li><li>→I don’t konw <strong>what to do</strong>.</li></ul><p>如果是一般疑问句, 如</p><ul><li>1.Should I vote for Mary?</li><li>2.I can’t decide the puestion.</li></ul><p>1句中没有疑问词, 需用whether引导. </p><ul><li>→I can’t decide whether I should vote for Mary [or not].</li><li>→I can’t decide whether to vote for Mary.</li></ul><hr><h1 id="插播-to-do与ving的再次比较"><a href="#插播-to-do与ving的再次比较" class="headerlink" title="插播 to do与ving的再次比较"></a>插播 to do与ving的再次比较</h1><p>前面在讲动状词的时候提到过, to do与ving都可做名词, 因此会用混, 也讲了一些他们的区别, 这里从简化从句的角度再谈谈他们的区别. </p><ul><li>He forgot <strong>that he should see his dentist</strong> that day.</li></ul><p>根据上面所说, 应简化为: </p><ul><li>→He forgot to see his dentist that day.</li></ul><p>而若原句为: </p><ul><li>He forgot that he saw the man before.</li></ul><p>没有be动词加ing, 简化为: </p><ul><li>He forgot seeing the man before.</li></ul><p>如果判断出名词从句中有不确定语气, 或者能看出原来应有助动词, 那么就能就能简化成不定式. </p><h1 id="副词从句的简化"><a href="#副词从句的简化" class="headerlink" title="副词从句的简化"></a>副词从句的简化</h1><p>同样, 先在此重复一下重要的观念: 所有从句简化的原则都一样, 即为求精简, 把从句中重复的主语与be动词省略, 只留下补语. 省略主语是为了避免重复, 如果省略会造成句意模糊错误, 主语就要另行处理；省略be动词是因为它本身没有任何意义. </p><h2 id="简化为ving补语时"><a href="#简化为ving补语时" class="headerlink" title="简化为ving补语时"></a>简化为ving补语时</h2><p>若从句是一般语法书中介绍的进行时, 那么省略主语与be动词后只剩ving补语. 若没有be动词, 也没有语气助动词可供改写, 那只能老办法, 先将从句改成进行时, 再进行简化. 如: </p><ul><li>While he was lying on the couch, the boy fell asleep.</li><li>→While lying on the couch, the boy fell asleep.</li></ul><h3 id="连接词是否需要省略"><a href="#连接词是否需要省略" class="headerlink" title="连接词是否需要省略"></a>连接词是否需要省略</h3><p>连接词有时不光起语法的作用, 还起语义的作用. 副词从句中连接词是否保留取决于修辞上是否清楚. </p><p>一般来说, while(包括when)表示“当…时”与because(包括so、since)表示“因为”时, 省略后不妨碍句子的清楚性, 但还是需要一个个句子去判断, 上例中的while便可省略. </p><p>知道从句简化, 便知道“分词构句”及传统语法中分词构句的由来. (P.361)<br>再举个需要改写的例子: </p><ul><li>Because we have nothing to do here, we might as well go home.</li></ul><p>首先观察从句中to do here其实是简化形容词从句来的, 原来是that we can do here修饰nothing. </p><ul><li>→Having nothing to do here, we might as well go home.</li></ul><p>注意, 这种修改并不是以变成进行时为最终目的, 而是为了进行词类变化, 使主语与be动词能够省略. </p><h3 id="关于连接词的省略再讨论"><a href="#关于连接词的省略再讨论" class="headerlink" title="关于连接词的省略再讨论"></a>关于连接词的省略再讨论</h3><p>这次看看不能省略的情况: </p><ul><li>Although we have nothing to do here, we can’t leave early.<br>→Although have nothing to do here, we can’t leave early.</li></ul><p>从句与主句有明显的“相反”的逻辑, 这需要Although才能表现清楚, 因此不宜省略, 这里保留Although仅仅是逻辑关系表达的需要, 当然也可以用其他词代替Although表达的逻辑. 再如: </p><ul><li>He raised his hand, as if he was trying to hit her.</li><li>→He raised his hand, as if trying to hit her.</li></ul><p>不宜省略as if. </p><h3 id="being的运用"><a href="#being的运用" class="headerlink" title="being的运用"></a>being的运用</h3><p>在某些需要的场合下, be需要以being的形式留下来. 如: </p><ul><li>As I am a student, I can’t afford to get married.</li></ul><p>按照上面全省略的话, 只剩a student, 读者无法判断这是个简化的副词从句而产生误解, 一个方法是保留连接词. 另一个方法是借用be动词改成being. </p><ul><li>→Being a student, I can’t afford to get married.</li></ul><p>因为有了Being, 能明显判断a student是补语. </p><h3 id="兼做介词的连接词before、after、since"><a href="#兼做介词的连接词before、after、since" class="headerlink" title="兼做介词的连接词before、after、since"></a>兼做介词的连接词before、after、since</h3><ul><li>Before he was in school, he used to be a naughty child.</li></ul><p>按以前说的省略后只剩in school,与原意相差很大, 原因在于省略了表示时间关系的before, 但若留下before变成为before in school也有问题, before在简化后会看成是介词, 后面只能接名词类, 因此使用being来进行词类变化, 即: </p><ul><li>Before being in school, he used to be a naughty child.</li></ul><p>before、after、since都兼有连接词与介词, 简化时会被视为介词, 以此进行词类判断. </p><h3 id="时态问题"><a href="#时态问题" class="headerlink" title="时态问题"></a>时态问题</h3><ul><li>After he wrote the letter, he put it to mail.<br>→After writing the letter, he put it to mail.</li></ul><p>这样简化是可以的, 如果省去After,读者会认为两个动作是同时发生的, 因为没有连接词, 会假设时间副词从句连接词是when. 所以如果要省略After,需要在时态上进行处理. </p><ul><li>→Having written the letter, he put it to mail.</li></ul><p>用完成式与简单式的对比交代写信在前, 其实该简单句也是下面这句的简化. </p><ul><li>When he had written the letter, he put it to mail.</li></ul><p>从中也可以看出, 连接词若是不能表达先后的when, 那么只能靠动词时态来表达. </p><h3 id="深入讨论从句主语的去留"><a href="#深入讨论从句主语的去留" class="headerlink" title="深入讨论从句主语的去留"></a>深入讨论从句主语的去留</h3><p>再讨论之前, 先谈谈副词从句简化与传统语法中“独立主格”的关系. </p><ul><li>The child already sleeping soundly in bed, her mother came to kiss her goodnight.</li></ul><p>传统语法中把这种保留从句主语的简化副词从句称为“独立主格”结构. 把already sleeping soundly in bed视为形容词短语看待, 修饰the child. 从句主语the child是非主句主语的独立存在, 故传统语法为了解决这个语法现象, 创造“独立主格”的说法. </p><p>另一方面, 如果从副词从句简化的角度看, 把从句主语留下来, 是为了保证句子的原意. </p><p>那么哪些情况应该保留主语呢？</p><p><strong>1.保留主语, 连接词必须要能省略</strong><br>简化从句一般是省略主语、be动词和连接词（视情况定）. 如果只是省略了be动词, 达不到简化的效果, 反而像是个病句. </p><p><strong>2.保留主语, 后面必须配合分词补语（现在分词/过去分词）, 如此才能看出是省略be动词的简化从句. </strong></p><p>如上一例The child sleeping soundly说明了the child是主语, sleeping soundly是补语, 省略了be动词与连接词, 形成简化副词从句. </p><h2 id="简化为ven时"><a href="#简化为ven时" class="headerlink" title="简化为ven时"></a>简化为ven时</h2><p>副词从句中原本是被动时态, 那么在省略后会剩下ven. 如: </p><ul><li>After he was shot in the knee, he couldn’t fight.<br>→Shot in the knee, he couldn’t fight.</li></ul><h3 id="老问题-连接词是否能够省略"><a href="#老问题-连接词是否能够省略" class="headerlink" title="老问题 连接词是否能够省略"></a>老问题 连接词是否能够省略</h3><p>上栗中的After可以省略, 原因在于shot是过去分词, 本身就表示“已经中枪”、“中枪之后”的完成时的暗示, 因而可以不要After一词. </p><p>但如果是Although带有“相反”的暗示, 省去后的意思会与原来有较大差别, 则要保留. </p><h3 id="如何应用having-been"><a href="#如何应用having-been" class="headerlink" title="如何应用having been"></a>如何应用having been</h3><ul><li>Because they had been warned, they proceeded carefully.</li></ul><p>这里动词had been warned有两种处理: </p><p>1.为了完整保留完成时“已经”的意味, 将其变为haing been<br>→Having been warned, they proceeded carefully.</p><p>2.另一种是省略had been, 只留warned.<br>→Warned, they proceeded carefully</p><p>因为warned是过去分词, 有完成的暗示, 省略had been影响不大.<br>总结起来就是, having been后面是动词过去分词, 就能进一步省略having been. </p><h3 id="从句主语去留"><a href="#从句主语去留" class="headerlink" title="从句主语去留"></a>从句主语去留</h3><p>副词从句简化为ven, 如果从句主语与主句主语不同, 就要把从句主语留下来. </p><ul><li>When the coffin had been interred, the minister said a few comforting words.<br>→The coffin [having been] interred, the minister said a few comforting words.</li></ul><h2 id="简化为to-do时"><a href="#简化为to-do时" class="headerlink" title="简化为to do时"></a>简化为to do时</h2><ul><li>He studied hard in order that he could get a scholarship.<br>→He studied hard in order to get a scholarship.</li></ul><h2 id="简化为单纯的be动词时"><a href="#简化为单纯的be动词时" class="headerlink" title="简化为单纯的be动词时"></a>简化为单纯的be动词时</h2><p><strong>be动词后是介词短语</strong></p><ul><li>When you are under attark, you must take cover immediately.<br>→When under attack, you must take cover immediately.</li></ul><p><strong>be动词后是形容词</strong></p><ul><li>While it is small in size, the company is very competitive.<br>→While small in size, the company is very competitive.</li></ul><p><strong>be动词后是名词</strong></p><ul><li>Although he was a doctor by training, Asimov became a writer.<br>→Although a doctor, Asimov become a writer.</li></ul><p>稍微总结下, 副词从句的连接词不同于名词从句或形容词从句, 是具有特定语义的连接词, 简化时常选择留下来, 那么它是由副词从句简化而成这一点就十分明显. </p><h2 id="再谈连接词的简化"><a href="#再谈连接词的简化" class="headerlink" title="再谈连接词的简化"></a>再谈连接词的简化</h2><p>副词从句的连接词有表达某种逻辑关系的意义. 简化时有一种特别的做法, 就是把连接词改为意义相近的介词, 整个从句简化为名词后作为介词宾语. </p><ul><li>Whe she arrived at the party, she found all the people gone.</li></ul><p>与when连接词接近的介词有on和upon. 上面的句子可改写为</p><ul><li>→Upon arriving at the party, she found all people gone.</li><li>→Upon her arrival at the party, she found all people gone.</li></ul><p>或是</p><ul><li>When she completed the project, she was promoted.<br>→Upon completing the project, she was promoted.</li></ul><p>但是这里把动词complete改为名词completion就会有问题, 原因在于这个动词有宾语, 改成名词后, 宾语没地方放了. 也有解决办法, 那就是再加个介词of处理. </p><ul><li>→Upon completion of the project, she was promoted.</li></ul><h1 id="倒装句"><a href="#倒装句" class="headerlink" title="倒装句"></a>倒装句</h1><p>这部分内容在书的423页, 主要介绍了倒装句这种特殊结构使用的情况. </p><p>倒装句是一种把动词（或助动词）移到主语前面的句型. 从这个定义看, 一般的疑问句都可以算是倒装句. </p><p>撇开疑问句这种具有语法功能的倒装句不谈, 比较值得研究的是具有修辞功能的倒装句. 恰当地使用倒装句, 可以加强语气, 增强清楚性与简洁性, 以及更流畅地衔接前后地句子. </p><p>以下是几种使用倒装句的条件. </p><h2 id="比较级的倒装"><a href="#比较级的倒装" class="headerlink" title="比较级的倒装"></a>比较级的倒装</h2><ul><li>1.Girls like cats more than boys.</li></ul><p>上面这句话有歧义, 可以有两种意思. </p><ul><li>2.Girls like cats more than boys do.</li><li>3.Girls like cats more than they like boys.</li></ul><p>这是省略不当造成的歧义, 如果想表达2句的意思, 句尾的do不可省. 不然无法判断boys是宾语还是从句主语, 是喜欢猫的人, 还是被喜欢的对象. </p><ul><li>Grils like cats more than boys, who as a rule are a cruel lot, do.</li></ul><p>do放在最后不好, 因为boys与do之间被关系从句阻隔, 而导致表达不清楚, 故使用倒装把do挪到boys之前. </p><ul><li>Girls like cats more than do boys, who as a rule are a cruel lot.</li></ul><p>总结一下, 比较级用倒装需要: </p><p>1.从句助动词或do动词不宜省略<br>2.主语后有较长修饰语</p><h2 id="关系从句的倒装"><a href="#关系从句的倒装" class="headerlink" title="关系从句的倒装"></a>关系从句的倒装</h2><ul><li>1.The Presidentg is man.</li><li>2.A heavy responsibility, whether he likes it or not, falls on him.<br>→The President is a man on whom a heavy responsibility, whether he likes it or not, falls.(不好)</li></ul><p>介词短语on whom因为内含关系词, 要移到句首位置. 然而一经移动, 产生了和上一例相同的问题. 用倒装处理把动词移到主语前. </p><ul><li>→The President is a man on whom <strong>falls a heavy responsibility</strong>, whether he likes it or not.</li></ul><p>如此一来, 关系词whom与先行词a man在一起, 在介词on whom与修饰的对象falls在一起, 而动词falls又与它的主语a heavy responsibility在一起, 解决了所有问题. 这便是倒装句的作用. 要注意的是, 关系词必须先向句首移动, 造成顺序反常, 才有倒装的可能. </p><p>举个不能倒装的例子: </p><ul><li>The President is a man who hears a lot of responsibility.</li></ul><p>这句话关系从句原来是 He bears a lot of responsibility, he改为who原本就在句首, 没有移动, 也就不能倒装. </p><h2 id="假设语气（虚拟语气）的倒装"><a href="#假设语气（虚拟语气）的倒装" class="headerlink" title="假设语气（虚拟语气）的倒装"></a>假设语气（虚拟语气）的倒装</h2><p>这种倒装比较单纯. 目的在于加强简洁性. </p><p>在假设语气中（往往是if引导的）, 如果有be动词或助动词, 就可以考虑倒装. 做法是把连接词（如if）省掉, be动词或主动词提前取代关系词. </p><ul><li>If I had been there, I could have done something to help.<br>_ →Had I been there, I could have done something to help.</li></ul><h2 id="引用句的倒装"><a href="#引用句的倒装" class="headerlink" title="引用句的倒装"></a>引用句的倒装</h2><p>在直接引用与间接引用中都可以选择使用倒装来突出引用的内容. 如</p><ul><li>The police said, “None was killed in the accident.”</li></ul><p>引用常出现在宾语位置, 为了突出说话内容使用倒装. </p><ul><li>→”None was killed in the accident.” said the police.</li></ul><p>句尾部分在直接引语时, 主语、动词也可以不倒装, 仍是the police said. </p><ul><li>The WHO warns that <strong>cholera is coming back</strong>.</li></ul><p>这句话除了可以把整个宾语从句移动到句首外, 还可以只把从句主语移到句首, 加强语气. </p><ul><li>→Cholera, warns the WHO, is coming back.</li></ul><h2 id="类似There-is-are的倒装"><a href="#类似There-is-are的倒装" class="headerlink" title="类似There is/are的倒装"></a>类似There is/are的倒装</h2><p>把地方副词挪到句首, 句型和there is/are很像, 目的在于强调语气, 衔接上下文. </p><ul><li>There goes the train! 你看, 火车开走了！</li><li>Here is your ticket for the opera!</li></ul><p>衔接例子: </p><ul><li>To the west of Taiwan lies Southern China.</li><li>To the east spreads the expanse of the Pacific.</li></ul><h2 id="否定副词句首的倒装"><a href="#否定副词句首的倒装" class="headerlink" title="否定副词句首的倒装"></a>否定副词句首的倒装</h2><p>如果把否定意味的副词(not、never、hardly)挪到句首来强调语气, 就得使用倒装句. </p><ul><li>We don’t have such luck every day.</li></ul><p>如果想要强调“不是每天”, 而把not every day放句首, 就要用倒装. </p><ul><li>→Not every day do we have such luck.</li></ul><p>又如: </p><ul><li>I will not stop waiting for you until you are married.</li></ul><p>同样如果把not until you are married移到句首强调语气, 就要把will倒装到主语前. </p><ul><li>Not until you are married will I shop waiting for you.</li></ul><p>另外一些表示否定的副词如hardly,barely等用法类似, 移到句首倒装. </p><ul><li><p>Hardly had I sat down to work when the phone rang.</p></li><li><p>Hardly anyone knew him.</p></li></ul><p>这句话不必倒装, 因为hardly修饰anyone, 句首是正常位置, 没有移到句首一说. </p><ul><li>Only I saw him yesterday.</li></ul><p>这句也不用倒装only修饰I, 放在I前是正常的. </p><hr><p>高级篇就先记到这里.<br>至此这三篇笔记也整理完毕. 可见整个语法体系中还有不少没有涵盖的内容, 至于要学会解构一个难句子, 唯有进行充分的练习才行, 而这本书更多的是告诉你一些结构的来龙去脉.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>.<br>終わり</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这次总算把高级篇的内容整理出来了, 顺带重新整理了下另外几篇的排版. &lt;/p&gt;
&lt;p&gt;在这本书的高级部分主要讨论了之前中级篇中三种复句的简化方法, 以及常用的特殊句型, 比如倒装句的用法. &lt;/p&gt;</summary>
    
    
    
    <category term="Note" scheme="https://granvallen.github.io/categories/Note/"/>
    
    <category term="English" scheme="https://granvallen.github.io/categories/Note/English/"/>
    
    
    <category term="语法俱乐部" scheme="https://granvallen.github.io/tags/%E8%AF%AD%E6%B3%95%E4%BF%B1%E4%B9%90%E9%83%A8/"/>
    
  </entry>
  
</feed>
