阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

Unity 构建与客户端安全

注:本文是博主在学生团队中所作的一次技术分享。博主本身并非从事构建或安全工作,也并未接触过许多相关工作的案例,许多内容存在推断和联想,其可行性还需在实际项目中进行验证工作,因此内容仅供读者参考。

在学生团队中制作独立游戏,日常是接触不到今天要分享的这个话题的。对于我们自己的打包需求而言,Unity 中的构建选项其实已经足够满足,并且操作起来很简单(除了需要精确对上小版本的 Android NDK 之类的坑)。而今天所讲的两个话题——Unity 构建与客户端安全,更多的应用场景会在大型游戏与网络游戏中。良好的构建系统可以提高制作团队的工作效率,而安全则用于维护网络游戏中的公平性。选择这一题目来分享,仅仅是为了开拓视野,并不需要大家真的在独立游戏中应用相关知识和技术,望各位知悉。

Unity 游戏构建

构建(build)是一个软件工程中的常见概念,通常指一个软件项目从源代码、原始资源进行编译、整合、打包,最终得到能够在目标平台上直接运行的文件的过程。例如,对于 C++ 项目来说,可以编写 cmake 文件来指定构建所需的信息和过程,利用它来进行编译,再将库文件、资源文件等放置在对应的目录结构中,就完成了一次构建。

构建管线与 CI/CD

在通常的 Web 应用项目中,构建通常和 CI/CD 的概念相连接。这里的 CI/CD 其实指三件事情:持续集成(continuous integration)、持续交付(continuous delivery)和持续部署(continuous deployment)。良好的 CI/CD 可以使开发、测试人员可以在各自的工作范围更多地集中在业务本身的工作中,而将检查、测试、构建、交付等工作自动化,并交由 CI/CD 管线执行,仅在流程出现问题时才通知相关人员介入处理。构建管线是 CI/CD 这一系统概念的其中一部分。

持续集成(CI)指的是对开发人员的自动化。这包括一系列的检查、测试和分支管理操作,例如编译检查、配置检查、单元测试、冒烟测试、分支的自动合并和冲突解决等。只有通过了这一系列流程的提交才会进入正式分支,从而保证正式分支的健康。

持续交付(CD)指的是开发人员的修改会被自动化构建为随时可以部署的应用,并对其进行测试,测试通过的版本将由管线统一管理,以便部署时使用。持续部署(另外一个 CD)指的是将已经构建完成的版本自动地部署在生产环境中,以供用户使用。这些自动化措施主要在解决大型项目的构建、部署上的复杂性,将这些复杂工序编写为构建、部署脚本,通过管线来管理复杂工作的配置、产物等,以节约开发人员的精力,提高工作效率,并协助实现敏捷开发。相比 CI 辅助的是开发时的工作,CD 辅助开发完成后的工作。

CI/CD 的核心概念是业务开发之外的复杂工作的封装和自动化,而构建管线在这其中起到了重要作用。构建管线承担着分支/版本管理、构建部署任务分发、构建产物管理、自动化测试等工作,在大型项目中,会有专门的开发和 QA 人员维护和监控构建管线的运行,以避免工作流程被管线故障阻塞。

在了解 Unity 项目的构建管线方案前,有必要先介绍 Unity 项目构建时将会进行的工作,这个话题可以从 Unity 游戏打包产物的结构开始。

Unity 游戏的文件结构

以团队项目 共脑 的一个 weekly 构建为例,游戏包含的文件如上所示。在这一级可以看到的文件有

  • LettersAdvProjectFiles.exe:游戏的启动器,Unity 自动生成的 exe
  • UnityCrashHandler64.exe:游戏 crash 时上报信息的工具
  • UnityPlayer.dll:Unity 引擎库
  • MonoBleedingEdge:Mono 运行所需的文件
  • LettersAdvProjectFiles_Data:游戏自身的资源和程序文件

在 LettersAdvProjectFiles_Data 文件夹下,还可以看到以下文件(截图只截取了部分)

  • Managed:C# 编写的代码和库,其中 Assembly-CSharp.dll 包含的就是游戏逻辑,其余为使用到的库
  • Plugins:native 库,如使用了 C++ 等直接编译到目标平台机器码的库,则会出现在这里
  • StreamingAssets:同项目目录下的同名文件夹
  • 其余文件为美术、音频等资源打包后的产物

除去一部分每个 Unity 游戏都有的文件(如启动器 exe、UnityPlayer.dll),其余部分大致可以分为代码和资源两部分。接下来将分别介绍代码和资源的构建过程。

C# 代码的构建

C# 代码如何运行

在了解 C# 代码的构建之前,需要介绍一下 C# 代码如何运行。C# 虽然也是一种编译型语言,但它并不直接编译到目标平台的机器码,而是需要类似 JVM 的运行时环境来运行。

C# 代码会首先被 C# 编译器编译到一种中间语言(IL,intermediate language),这种中间语言可以在实际运行时快速被转换为运行平台的机器码。中间语言表示的程序产物依然会使用 .exe、.dll 等扩展名,看起来像是正常的可执行程序或库,但要运行它们,需要运行时环境的辅助。

C# 属于 .NET 系列语言,使用 .NET 系列通用的运行时环境 CLR(Common Language Runtime)。CLR 实际上存在多种实现,微软官方的实现被叫做 .NET Framework,而 Unity 还使用了 Mono。这些运行时环境实际上还要进行一次 IL 到机器码的转换,这也是一种编译过程,因此被叫做 JIT 编译(Just-in-Time compilation)。

上图展示了一个 C# 编写的程序和库的实际运行过程。在运行的过程中,运行时会将 IL 编译为机器码,并将产物写入一块新的内存区域,再使 CPU 执行产物机器码。

Mono(JIT)

目前 Unity 使用的 C# 运行时环境为 Mono,正如上文所述,这是一种 JIT 编译的方式。Mono 是一个开源的、跨平台的 CLR 实现,在 Unity 开始使用 C# 作为脚本语言的时候,微软的 .NET Framework 尚未开源和实现跨平台,只能在 Windows 平台上运行,而为了 Unity 的多平台构建能力,Mono 被选做 C# 的 JIT 方式脚本后端。

如 .NET Framework 一样,Mono 也有自己的一套构建工具链,生成符合 ECMA 标准的 CIL 产物。此外,还可以使用 Mono.Cecil 等工具库来对 Mono CIL 产物进行分析,例如获取类型、函数信息,修改和生成代码等。

JIT 的 CIL 代码在运行时被转换成机器码,因此软件构建完成后,实际分发的是 CIL 二进制文件,这会给我们带来一些方便之处

  • 对于不同平台可以使用同一份 CIL 二进制,只需要平台具有对应的 CLR 即可运行
  • 多平台构建时,C# 可以只编译一次到 CIL,简单修改二进制文件结构即可生成不同平台的产物,节约构建时间
  • 相对机器码而言,CIL 更方便进行静态分析和代码修改、生成,可以简单地实现注入打点等

但这些方便也可能会带来不便,例如

  • 运行时将 CLR 编译为机器码需要消耗一定的 CPU 时间,可能导致性能低下
  • 分发的 CIL 文件很容易被用户逆向和修改,影响游戏正常运行,或容易被利用制作外挂等

由于性能和安全通常是高优先级的问题,Mono 在商业游戏中使用并不广泛,更多见于开发环境(开发构建)或小型独立游戏等。

IL2CPP(AOT)

除 Mono 外,Unity 还提供了一种 C# 脚本后端供选择,这就是 IL2CPP。这个名字包含了 IL 和 CPP 两个名字,从名字就可以猜测,它的工作方式是将 CIL 变为 C++ 代码,再直接编译到目标平台的机器码。

首先你可能会想这样一个问题,C# 代码要怎么变成 CIL?还记得我们的 Mono 吗,可以直接使用 Mono 工具链中的 C# 编译器将源码编译到 CIL。因此,IL2CPP 本身是一个 AOT 编译器(Ahead-of-Time compiler),它能够在实际运行之前将 CIL 变成机器码,只是中间的过程是先转换成 C++ 代码。IL2CPP 在构建过程中的位置可以从下图中理解

与 Mono 不同的是,构建出的产物不再包含任何 CIL 代码,而是直接编译到机器码。因此,游戏构建的 XXX_Data 文件夹下不再有 Managed 文件夹保存所有的 CIL .dll 文件,取而代之的是根目录下的 GameAssenbly.dll,这便是 CIL 转换成 C++ 后编译得到的机器码库文件。

然而,C# 仍需要运行时提供反射、GC 等功能,因此 IL2CPP 的产物中也需要包含一部分提供此类功能的方式,这是由 baselib.dll 和 XXX_Data/il2cpp_data 文件夹等共同完成的。

AOT 编译的好处与缺陷和 JIT 基本是对应的,它的好处包括

  • 最终分发的文件是目标平台的机器码二进制文件,可以直接运行,没有 JIT 编译导致的性能损失
  • native 的二进制文件逆向难度更大,且可以通过加壳等方式进一步处理,增强游戏的二进制安全性

而缺陷包括

  • 由于游戏逻辑也被编译到了目标平台的机器码,如果一个构建包需要在多种平台上运行,需要分别提供对应的二进制文件,这会是包体大小变大,这一点在构建 Android apk 文件时非常明显
  • 如果开发时就使用 IL2CPP 编译到机器码,再进行 C# 代码修改操作将变得困难;但这一点可以通过在构建过程中加入修改阶段,在 IL2CPP 阶段之前完成对 CIL 的注入,也可以实现相同的效果
  • 由于 IL2CPP 实际上是在 Mono 构建后新增了若干阶段来编译到机器码,这将使单次构建时间变长

美术、音效等资源的构建

Games are much more than source code,游戏项目的资源数量远远多于代码的数量,资源本身也存在复杂的类型、格式和构建过程,因此相比代码而言,资源的管理和构建更为复杂。资源的构建主要指资源的组织和打包方式,Unity 提供了多种加载资源的方式,它们的构建存在区别,因此将按 Unity 加载资源的方式介绍资源的构建。

通过 GUID 直接引用

在 Unity 的 Inspector 中,可以直接把一个资源文件拖进一个槽中,此时这个场景就建立了到一个资源文件的 GUID 引用。这种方式引用的资源会在场景打开时直接被加载进内存,因此,如果一个场景中所有的资源都是如此配置的,则打开场景会需要花费较长时间,并且全部资源都会在一开始加载,直到场景关闭才会卸载。

场景本身的信息会被打包成 levelX 文件,而场景引用的资源可能被打包在 resources.assets/sharedassetsX.assets 等文件中。

Resources 类

如果将资源放置在 Assets/Resources 文件夹中,可以通过 Resource.Load 方法动态地加载,这是 Unity 最简单的动态加载资源方式。这些资源保存在 resources.assets 文件中,因此一个项目的 resources.assets 文件可能很大。

上图展示了 resources.assets 文件的结构,它实际上是若干资源打成的包,文件中包含连续的资源块,文件头指示了每一个资源对应的块,因此可以通过文件路径索引到对应的块,再从块中读取资源到内存中。

这些资源的打包是在 Unity 构建过程中自动完成的,Unity 并未提供接口修改这一构建过程,因此难以从脚本层面控制资源构建。

Asset Bundle

Asset Bundle(以下简称 Asb)是 Unity 提供的另一种从文件中加载资源的方式。相比 Resources,Asb 最大的好处就是可控。在整理资源时,可以在 Inspector 中选择资源对应的 Asb。可以通过 BuildPipeline.BuildAssetBundles 从脚本中构建 Asb 文件。加载 Asb 时只需提供文件路径即可,可以将游戏本体和 Asb 分开构建,发行时将 Asb 放入 StreamingAssets 中,这样游戏本体就可以读取 Asb。另外,还可以直接通过网络分发 Asb,游戏本体下载到 StreamingAssets 中再读取。Asb 的加密也较好处理,Unity 提供了从流中加载 Asb 的接口,因此只需提供解密后的流,Unity 仍可照常加载 Asb。

Asb 具有和 resources.assets 类似的文件结构,但一个 Asb 包实际上包含多个文件,序列化数据和部分原始资源是分开的。Asb 也支持只加载其中的部分资源,它同样具有和 resources.assets 类似的文件头信息。

大多数大型游戏都会选择 Asb 这种资源的管理和构建方式,因为它可以和游戏本体分开构建,方便分发和更新,这样可以减少游戏的最小包大小,让更多资源可以通过网络分发,且玩家也可以选择下载的资源数量和质量。

数值、配置

数值、配置在兴趣项目开发时通常会直接配置在场景、Prefab 或 ScriptableObject 中,可以随 Unity 的引用或 Asb 等一并打包。

我们尝试在团队项目 Emoji 中引入了 Excel 表格管理的数值文件,并提供了从 Excel 表格导出数值、生成 Excel 行的数据类和序列化代码等工具。

这些自行实现的数值构建,可以在打包之前自行导出一遍配置,或自定义打包的流程,在打包前加入这一环节。数值配置的自行管理,在商业游戏中也方便进行数值 patch 等更新操作。另外,还可以在 Excel 表格或构建过程中加入数值校验环节,避免开发人员提交错误的数值配置,使项目无法正常运行。

Unity 游戏构建管线

Unity 提供了构建管线接口,即 BuildPipeline 类。除了在编辑器里打开构建窗口之外,还可以利用管线接口来定制构建脚本,并将构建配置独立成配置文件,可供切换。

自行实现构建脚本通常有以下分类和步骤

  • 构建游戏本体
    • 读取构建配置
    • 导出游戏本体所需的资源,如数值、最小包资源等
    • 代码混淆等
    • BuildPipeline.BuildPlayer
    • 二进制加壳保护等
    • 对游戏本体和最近的 Asb 构建运行冒烟测试
    • 如冒烟通过,上传构建至构建管理平台
    • 上传调试文件至构建管理平台
  • 构建 Asb
    • 读取构建配置
    • 导出 Asb 所需的资源
    • BuildPipeline.BuildAssetBundles
    • 如有自行设计的 Asb 加密或再分组策略,执行对应的打包流程
    • 对 Asb 构建和最近的游戏本体构建运行冒烟测试
    • 如冒烟通过,上传构建至构建管理平台

实现了构建脚本和构建配置后,便可利用 CI/CD 平台(如 Jenkins、TeamCity 等)配置构建管线。然而,与通常的应用 CI/CD 管线不同,游戏的一次构建花费的时间过长,大型项目通常要花费高达 10 小时甚至更长的时间才能完成一次全量构建。这使得游戏的 CI/CD 工作难以高频进行,自动化工作带来的效率提升效果大大减少。

游戏的 CI 方案通常包含较强的检查和较弱的测试,这是由于如数值检查、编译检查等工作可能可以在数分钟内完成,但启动游戏进行测试所需的时间可能较长,一次提交如果无法在短时间内通过 CI 流程进入项目,会大概率产生冲突,浪费开发者的时间在处理冲突上。一种解决的方案是,提交自动触发的管线任务仅包含检查,而自动化测试不作为提交的必要条件,而是当某一版本的测试不通过时再检查该版本的修改。管线的检查任务可以通过项目管理平台的提交触发器拉起,主流的版本管理系统(如 Git、Perforce 等)都具有提交前后触发脚本的功能。

尽管开发团队可以将游戏的构建过程进行修改和包装,变为便于使用的构建脚本,但单次构建的成本还是太高,无法持续地运行。此外,如果构建的目标是移动端设备,可能具有特殊的构建平台要求,如 iOS 应用需要在 Mac 上构建,这使得构建管线管理的机器数量和类型变多。这些复杂性使得游戏的构建最终只能手动触发,并在团队中限制使用,否则无论团队拥有多大规模的构建集群,也难以满足开发人员和需求对构建的需求。因此,相比于 CI 过程的自动触发和高频运行,游戏 CD 管线则因耗时原因,不得不选择手动触发和按需运行的策略。

在冰岩作坊这样的学生团队中,我们没有能力搭建一套这样健壮的构建管线。然而,a game that can’t build every day is in big trouble,我们仍然可以按照一定的频率手动进行构建,持续地打包、测试和提供给玩家试玩。这同样是对开发团队的鞭策,让我们能够在固定的时间间隔下生产和验收内容,保持项目的活力。

Unity 游戏客户端安全

客户端安全并不是只有游戏才会面临的问题,桌面应用、移动端应用同样面临着相同的问题。对于普通的应用而言,安全需求主要集中在反爬虫、反逆向、反隐私泄露等;而对于游戏而言,除了这些需求之外,安全更突出地反应在游戏体验问题上,外挂等针对游戏客户端的攻击工具严重破坏了玩家的游戏体验。试想,如果你是 战地1(Battlefield 1) 玩家,你正享受着游戏带来的乐趣,结果就被外挂狗骑脸输出了,导致你再也无法在这款游戏中获得乐趣,于是开始狂喷 EA 装死不处理外挂问题。

从开发团队的视角来看安全问题,更多地是一个需要衡量性价比的问题。例如,对于 PVP 很弱或基本不存在的单机游戏而言,安全需求主要是反破解/反盗版,而修改游戏数据类的外挂危害很小;但对于强竞技类的游戏,一款修改数据/辅助游戏的外挂会严重破坏游戏公平性,有必要在客户端中加入对于读取/修改内存的安全措施,但这种 PVP 游戏的反作弊也可以从服务器的角度入手。

本次分享中主要介绍若干客户端安全的策略/措施,其应用场景各不相同,具体的落地措施还需根据各项目的实际情况来决定。

反逆向:加壳

Unity 开发的游戏逻辑多采用 C# 编写,而 C# 的 CIL 或反射等机制容易为攻击者带来便利。因此,为了增强 Unity 游戏客户端的安全性,通常会采用 IL2CPP 脚本后端,并对二进制文件进行加壳。

加壳是指将原来的程序代码进行压缩、分段或加入难以阅读的或冗余的逻辑,使原来的程序代码能够正常运行的同时,反汇编得到的结果具有一定的迷惑性,增加逆向工作的难度。

VMP 壳(VMProtect)是一种将原有程序代码的运行转移到壳所实现的 VM 上的加壳保护方案。VMP 2 会将原本的 x86 汇编指令转换到壳 VM 所能运行的 RISC 指令集,并对指令的操作数进行加密,从而实现混淆原本汇编代码的目的。加壳后,逆向工作必须从 VM 本身开始寻找入口点,研究翻译后的 RISC 指令,而不是简单地对 x86 指令进行逆向。由于需要新增大量 VM 相关逻辑,VMP 壳会使二进制文件的大小膨胀许多倍,且会带来额外的运行开销。

UPX 壳(Ultimate Packer for eXecutables)是一种将原有程序代码压缩,并在程序中加入解压逻辑的加壳保护方案。UPX 加壳后,程序代码将被压缩算法压缩,在二进制层面和原本的内容产生较大区别。逆向工作必须先研究解压算法,将原本的程序代码解压后,才能分析程序。如果调整压缩/解压算法,使得此算法的逆向工作困难,则可以获得一定的安全性。UPX 壳会使二进制文件的大小缩小,但同样会带来额外的运行开销,使程序性能降低。

反逆向:混淆

IL2CPP 生成的二进制文件可以通过加壳保护,但 C# 的反射机制依赖的 metadata 也会给攻击者留下一个逆向的突破口。在构建过程中进行代码混淆则是一个解决方法。

在实际的逻辑中,反射所涉及的类和字段并不多,大多数的类名、变量名、函数名都可以被任意修改,因此可以进行混淆——即将原本具有含义的名称替换为不具有实际意义的随机字符串。混淆过程中,混淆器会维护一个名称映射表,逐一发现和替换代码中的名称,替换后的程序代码可读性大幅下降,metadata 中的名称也变得难以辨认,仅有部分反射用的名称保留了下来,增加了逆向难度。

反逆向:修改虚拟机

如 C#、Lua 等语言运行在虚拟机中,可以通过修改虚拟机的方式来使生成的 IL 与常规使用的不同,增加逆向难度。常见的修改方式是更改 IL 指令的 opcode,修改后攻击者只能通过特征猜测 opcode 与实际指令的对应关系,这一工作将耗费大量时间,提升逆向困难性。

反劫持:文件完整性检查

攻击者可以通过文件替换的方式,将游戏依赖运行的部分库文件替换成修改后的版本,从而在接口中增加攻击逻辑,劫持游戏逻辑或从游戏中获取资源数据等。

可以在游戏启动时增加文件完整性检查,分析将使用的文件是否被修改过,例如在分发时增加 checksum 检查,联网获取文件的 checksum,如文件被修改过则联网下载原始文件,再启动游戏。

反解包:自定义资源加密方式

在打包过程中加入自定义的资源打包和加密方式,而非直接使用通用或常见的加密方式,可以增加攻击者分析包体结构的难度,避免客户端资源包被轻易破解,原始资源泄露盗用。

矛盾的安全工作

游戏的安全工作存在着许多矛盾,例如二进制相关的安全措施对游戏软件的影响面过大、或影响游戏性能,明水印、盲水印等由于泄露图门锁拍照而难以识别,设计和验证加密方案周期很长、但攻击者破解加密方案却又可能较快(毕竟攻击者比打工人有时间太多了)等等。

安全措施的最终目的可能很难定为杜绝攻击的发生,对于游戏项目就更难做到如此。因此,安全的实际执行必须考虑性价比和可行性问题,例如资源上的水印足够防止大部分资源盗用、反逆向措施足够防止大部分玩家修改客户端,便可认为达到了安全方案的目的。

此外,安全方案并不是只在技术层面、在客户端侧进行。例如,对于联网的游戏客户端,还可以在服务端进行数据校验,或主动识别作弊玩家和被修改的客户端;对于需要保密的资源,和相关人员签订保密协议,明确保密义务,并在适当时间用法律手段解决问题。

结语

本次分享涉及了一些在日常进行兴趣项目游戏开发难以接触的话题,意在开阔视野,提供更多了解游戏开发工作的横向知识。本人并非从事这些工作的专家,对相关知识的了解也较为浅薄,如有错误或建议欢迎交流。

冰岩作坊游戏组正在进行的项目包括本文中提到的 共脑Emoji(代号)以及已经上架 TapTap 的 纽兰枢纽(参考资料中有链接!),欢迎大家支持我们的游戏项目。作者正在参与的 Emoji 是一款 3D 平台跳跃闯关游戏,目前正在原型期开发中,期待后续能够和各位见面。

参考资料

本文所使用部分材料和配图来源网络,感谢原作者的分享。

The post Unity 构建与客户端安全 first appeared on KSkun's Blog.
🔲 ☆

GAMES 202 笔记:Shadow Mapping

施工中……现在没有写的部分还有:

  • Convolution Soft Shadow Mapping
  • Moment Shadow Mapping
  • Mipmap 和 SAT 的原理

虽然没打算写,但有可能会加的部分有:

  • 这几种阴影、软阴影的 GLSL 实现(看另外一个 OpenGL 渲染器的坑)

注:头图来自 GAMES 202 作业 1 任务指导书,文中部分插图来自互联网资料,引用源均在参考资料中列出。

阴影贴图(Shadow Mapping)

阴影贴图是一种 2-pass 的实时阴影渲染技术。它在阴影 pass 中以光源视角绘制阴影贴图,再在着色 pass 中利用阴影贴图的信息计算直接光照,这个过程类似将阴影生成为贴图,并应用在阴影投射到的物体(receiver)上,故如此称呼。

原始论文:Casting curved shadows on curved surfaces,发布于 SIGGRAPH 1978,作者 Lance Williams (NYIT)。

流程

判断一点是否处于阴影中,即是判断该点和光源的连线上是否存在遮挡物。这可以简单地描述为:从光源的视角能否看到遮挡物,或光源沿此方向看到物体的深度是否小于该点。要得到光源看向场景的深度信息,可以直接使用绘制场景相同的流程,而将绘制结果写入一张深度图(depth map)中即可。在计算直接光时,将该点坐标变换到光源视角中,查询深度图做比较来判断是否被遮挡。因此可以将这一过程分为两个 pass 来实现:

  1. 深度 pass。设置相机参数在光源处看向场景,render target 为一张深度图,并选择恰当的相机、贴图参数使场景完全包含在内。提交场景后,得到光源视角看向场景的深度信息,还需保存下此 pass 的变换参数。
  2. 渲染 pass。在计算光照时需要着色点的世界坐标,将其变换到光源视角中,得到深度图纹理坐标(即光源视角下 NDC 坐标系的 x、y 坐标标准化值)。使用这个坐标查询深度图,如果深度与着色点到光源距离一致说明未被遮挡,如查询深度更小说明被遮挡。

以上为基本流程,但更好的效果需要加入一些修改,详见讨论部分。

讨论

光源变换矩阵的参数

如果使用一张 2D 贴图来保存阴影信息,需要选择合适的参数。点光源应选择透视投影,而平行光源应选择正交投影,这关系到投射出阴影的形状是否正确。视锥体应该包含全部场景,远、近平面、视锥边界参数应该取得足够大,否则可能导致深度图查询越界。如果光源没有办法从一个方向看到整个场景,应该考虑生成多张不同角度的深度图,或使用 Cubemap、球形贴图等保存。

此处再整理一下流程中涉及的不同坐标系。在查询深度图时,需要将着色点的世界坐标变换到光源视角的 NDC 坐标下,这通过乘光源的视角变换和投影变换矩阵(V、P 矩阵)实现。NDC 坐标通常在 [-1, 1] 范围内(取决于 API 定义),因此还需要进一步标准化到 [0, 1] 范围,得到深度图上的纹理 UV 坐标。

自遮挡:数值误差

深度图将标准化深度值保存在贴图像素上,每个像素的颜色值是固定位数的,因此能表示的精度也是有限的,这会为数值带来量化误差。此外,深度图代表遮挡物投影至平面上的结果,但遮挡物不一定与投影平面平行,深度图上的像素实际上代表了与投影平面平行的小面元,这也会带来离散化的误差。

Shadow Acne 现象
离散化误差

这些误差使得查询深度图的结果可能比真实遮挡物深度偏大或偏小,偏大不会造成严重后果,但偏小可能让遮挡物本身以为自己被遮挡,在没有阴影的地方形成奇怪的暗色斑点、纹路(被称为 Shadow Acne)。可以在判断被遮挡时,在着色点深度加上一个正偏差补偿数值误差,使数值误差不影响遮挡判断结果。

Peter Panning 现象

这种误差补偿是有局限性的。当遮挡平面与光源视线成较小角度时,离散化误差将变大,很难补偿这种情况的误差。而阴影遮挡物和被遮挡物离的很近时,被遮挡物的深度又被偏差补偿到小于遮挡物,使得一些阴影没有和遮挡物连起来,看起来好像遮挡物浮空了一样(被称为 Peter Panning)。可以在生成深度图时使用背面剔除,这样可以解决有厚度遮挡物的问题。

阴影的锯齿走样

由于深度图本身的尺寸有限,阴影投射后放大出现了锯齿状走样。这种走样和渲染中出现的锯齿现象原理类似,可以使用一些采样方法解决。一个方式是 PCF(百分比渐进滤波,Percentage Closer Filter)。着色点映射到深度图上后,不只查询一个像素,而是采样该像素附近一圈像素。采样的范围越大,阴影边缘的过渡部分越宽,锯齿现象越不明显,但阴影边界也会变模糊。

百分比渐进软阴影(PCSS,Percentage-Closer Soft Shadows)

Shadow Mapping 能为实时阴影计算提供遮挡信息,从而方便地实现点光源、平行光源的阴影。然而,光源常常并非二者之一,而是有一定面积的面光源。在面光源下,阴影分为本影(umbra)和半影(penumbra),本影是光源面积完全被遮挡的位置,亮度最暗,而半影是本影周围光源面积部分被遮挡的位置,形成了最暗到最亮的过渡带。半影形成的过渡带使阴影的边缘模糊,形成软阴影(soft shadow)的效果。

PCSS 受到 PCF 滤波效果的启发,通过指定阴影不同位置 PCF 滤波范围大小,来实现类似真实软阴影的效果。这种方法完全基于 Shadow Mapping 的结果,没有额外的前、后处理,也不需要更多信息。

原始论文:Percentage-Closer Soft Shadows,发布于 SIGGRAPH 2005,作者 Randima Fernando (Nvidia)。

流程

PCSS 通过控制 PCF 滤波范围大小来实现阴影不同程度的软化,因此需要确定着色点处需要选择的 PCF 滤波范围大小,这个大小与光源、遮挡物和被阴影投射物之间的集合关系决定。

上图所示的场景中,假设光源、遮挡物和投射物平面是平行关系。黄色的线段表示有面积的光源,其宽度为 $w_\text{Light}$,光源被遮挡物(绿色线段)左侧遮挡,其阴影投射在下方的投射物(蓝色线段上),遮挡物到光源距离 $d_\text{Blocker}$,投射物到光源距离 $d_\text{Receiver}$。从光源两端点经遮挡点向投射物连线(黄色虚线),投射物两连线的右方表示两点分别无法照亮的位置,即本影,宽度为 $B$;而两连线之间的部分表示光源被部分遮挡,即半影,宽度为 $w_\text{Penumbra}$。这样就建立了阴影和场景的几何关系。注意到存在一系列相似三角形关系,存在比例
$$ \dfrac{d_\text{Blocker}}{d_\text{Receiver}-d_\text{Blocker}} = \dfrac{w_\text{Light}}{w_\text{Penumbra}} $$
故半影宽度可以由
$$ w_\text{Penumbra} = \dfrac{d_\text{Receiver}-d_\text{Blocker}}{d_\text{Blocker}} \cdot w_\text{Light} $$
求得。另外,此处的两个 $d$ 值可以不是图中的垂线段长度,满足相似三角形的比例关系皆可。

经过上面的讨论,半影宽度与光源大小、遮挡物深度和着色点深度有关,后两个量可以分别从深度图与世界坐标得到,光源大小通常是预定义好的。PCSS 的工作过程可以描述为下面三步:

  1. 遮挡物搜索。由于光源有面积,能够遮挡着色点的遮挡物可能存在于一个区域中(即上图着色点和光源连线成的锥形区域),这片区域投影在深度图上也会占据一片区域。因此,要得到遮挡物的深度,需要查询所有可能遮挡到着色点的区域,求出其中遮挡物的平均深度值,这一步被称为遮挡物搜索。查询深度图的区域大小与着色点深度成正比,可定义一个比例系数作为参数。通常不会查询区域内的每个像素,而是采用采样方法估计搜索结果。
  2. 半影大小估计。上面的理论推导中得到了关系 $w_\text{Penumbra} = \dfrac{d_\text{Receiver}-d_\text{Blocker}}{d_\text{Blocker}} \cdot w_\text{Light}$。上一步中得到了 $d_\text{Blocker}$,而 $d_\text{Receiver}$ 可通过世界坐标中着色点到光源中心的距离估计,直接通过上面的关系计算半影大小即可。
  3. PCF 滤波。将半影大小作为 PCF 滤波范围大小,对深度图进行采样和滤波即可。PCF 滤波的过程可参考上文

讨论

PCSS 是一种基于 PCF 的很直接的思路,实现的难度也不高。然而由于遮挡物搜索和 PCF 两步中包含两次采样估计过程,PCSS 的效果通常包含许多噪声,需要提高采样数或应用低通滤波等方式改善渲染质量。此外,多次采样会带来大量的贴图查询,带来时间和带宽上的额外开销,效率上依然存在问题。随着时域、空域滤波的技术发展,现在有 TAA 等更好的方法改善 PCSS 的噪声和效率,因此 PCSS 仍然是一种广泛使用的方法。

方差软阴影(VSSM,Variance Soft Shadow Mapping)

PCSS 中需要范围查询深度图中的信息来完成遮挡物搜索和 PCF,而范围查询需要通过采样来完成,这会为渲染结果带来采样造成的随机噪声。为了避免这种噪声,有许多方法通过非采样的方法来估计遮挡物的深度。VSSM 是一种利用统计规律来估计遮挡物深度的方法,它基于 Chevbyshev 不等式和方差阴影贴图(VSM,Variance Shadow Mapping)方法,通过使用预计算的信息来避免采样。

原始论文:Variance Soft Shadow Mapping,发布于 PG 2010,作者杨宝光(Autodesk、浙江大学)。

流程

在 PCSS 中,遮挡物的平均深度通过对深度图的采样获得。从这个思路出发,设采样数为 $N$,其中有 $N_1$ 个样本为非遮挡物(深度不小于着色点)、$N_2$ 个样本为遮挡物(深度小于着色点)。记遮挡物的平均深度为 $z_\text{occ}$,非遮挡物的平均深度为 $z_\text{unocc}$,所有样本的平均深度为 $z_\text{Avg}$,则根据平均值的定义可得下面的关系
$$ \dfrac{N_1}{N} z_\text{unocc} + \dfrac{N_2}{N} z_\text{occ} = z_\text{Avg} $$
采样的频率会收敛到概率,故可以用概率来估计样本的频率,此外所有样本的平均深度也可近似为真实的平均深度,则记着色点深度为 $t$,采样的随机变量取值为 $x$,上式可化为
$$ P(x \ge t) z_\text{unocc} + [1 – P(x \ge t)] z_\text{occ} = z_\text{Avg} $$
那么遮挡物的平均深度便可通过下式求出
$$ z_\text{occ} = \dfrac{z_\text{Avg} – P(x \ge t) z_\text{unocc}}{1 – P(x \ge t)} $$
有了这个关系,还需要知道非遮挡物的平均深度 $z_\text{unocc}$,以及实际意义为深度图查询范围中不小于采样点深度的像素占比 $P(x \ge t)$。

对于非遮挡物的深度,一般来说着色点附近的非遮挡物是着色点所在的平面,因此可以近似为着色点的深度 $t$。而遮挡物的占比则可以借助 VSM 的思路解决,对深度图查询范围进行采样,其对应的随机变量应是范围内像素构成的离散型随机变量,对随机变量成立单边 Chevbyshev 不等式如下(证明见附录
$$ P(x \ge t) \le \frac{\sigma^2}{\sigma^2+(t-\mu)^2}, \forall t > \mu $$
一个会让上式的不等号取等的情况是查询范围内的深度值都一样,而实际情况中深度值常常相差不大,使用上界值估计实际情况不会有很大误差,此处取该上界的值作为 $P(x \ge t)$ 的估计。要计算这个上界,需要得到查询范围内深度的平均值 $\mu$ 和方差 $\sigma^2$,平均值可以通过 Mipmap 或 SAT 等方式得到,而方差可以通过关系 $\sigma^2 = E(x^2) – \mu^2$ 得到,因此只需要另外计算一张深度平方的深度图和它的 Mipmap/SAT 即可。

上面的理论推导得到了重要的结论,即仅需要保存深度图、深度平方图和它们的 Mipmap/SAT,就可以通过单边 Chevbyshev 不等式的上界估计平均遮挡物深度,避免了 PCSS 中遮挡物搜索的采样操作。而 PCSS 中 PCF 滤波的采样操作,则可以通过对估计出的滤波范围大小再次进行非遮挡物占比(即 $P(x \ge t)$)的估计,将其作为可见性的值,同样也可以实现无采样的滤波效果。故 VSSM 的整体流程为

  1. 遮挡物平均深度估计。使用与 PCSS 同样的方法得出遮挡物搜索范围的大小,在这个范围中查询 Mipmap/SAT 得到均值 $\mu$ 和平方均值 $E(x^2)$,从而计算出方差 $\sigma^2 = E(x^2) – \mu^2$,进而估计出范围内非遮挡物占比 $P(x \ge t) = \dfrac{\sigma^2}{\sigma^2+(t-\mu)^2}$,再根据 $z_\text{occ} = \dfrac{\mu – P(x \ge t) \cdot t}{1 – P(x \ge t)}$ 求出遮挡物平均深度的估计。
  2. 半影大小估计。这一步与 PCSS 一致。
  3. 可见性估计。将上一步求出的半影大小作为查询范围大小,同样地计算 $P(x \ge t)$,该值就是可见性值的估计。

讨论

VSSM 利用了单边 Chevbyshev 不等式作为平均遮挡物深度 $z_\text{occ}$ 的估计,然而不等式需要满足单边条件 $t > \mu = z_\text{Avg}$,通常在着色点附近是平面时这个条件能够被满足。当着色点附近的点和着色点不在一个平面上,尤其是远离光源方向时,实际的 $z_\text{Avg}$ 可能比着色点深度 $t$ 更大,此时使用原来的估计会使 $P(x \ge t)$ 的值小,从而使 $z_\text{occ}$ 的估计可能大于 $t$,一些本应在阴影中的像素被判断成不在阴影中,如下图所示。

引起问题的原因是着色点附近不是平面,如果把查询范围划分成更小的格子,小格子中的几何会更接近平面,更可能满足不等式的条件。对于划分后仍然不满足条件的小格子,可以简单认为不被遮挡,或使用常规采样方法进行 PCF。

划分后,每个满足不等式条件格子 $w_{cj}$ 的 $E(x)$ 和 $E(x^2)$ 可以通过 SAT/Mipmap 直接查出,记格子的大小为 $T_{cj}$,则可以通过每个格子的信息合并得整体的 $\mu, \sigma^2$,如下所示

$$ \begin{aligned} \mu &= \dfrac{\sum_j E(x)_{cj} T_{cj}}{\sum_j T_{cj}}, \\ \sigma^2 &= \dfrac{\sum_j E(x^2)_{cj} T_{cj}}{\sum_j T_{cj}} – \mu^2 \end{aligned} $$

满足条件格子的平均遮挡物深度 $d_1$ 可以通过 $\mu, \sigma^2$ 和不等式估计得到。而不满足条件的格子则使用上文提到的采样方法,得到其平均遮挡物深度 $d_2$。根据两类格子占比对 $d_1, d_2$ 加权得到最终的平均遮挡物深度 $d$,这样一来就解决了非平面的问题。

此外,方差 $\sigma^2$ 具有的统计意义可以反映随机变量分布的集中特性,如果方差很小且 $z_\text{Avg}$ 大于着色点深度 $t$,说明着色点附近基本都是更深的点,此时可以直接认为着色点未被遮挡以减少计算。

参考资料

附录

单边 Chevbyshev 不等式的证明

参考资料:https://zhuanlan.zhihu.com/p/111329527

Markov 不等式

设 $X$ 是取非负值的随机变量,则对于任何常数 $a>0$,有
$$ P(X \ge a) \le \frac{E(X)}{a} $$

证明 对于 $a>0$ 令随机变量 $I = \begin{cases} 1, & X \le a \\ 0, & X < a \end{cases}$,由于 $X>0$ 有 $I \le \dfrac{X}{a}$,两边求期望得 $E(I) = P(X \ge a) \le \dfrac{E(X)}{a}$。

单边 Chevbyshev 不等式

设 $X$ 具有 0 均值和有限方差 $\sigma^2$,则对任意 $a>0$ 有
$$ P(X \ge a) \le \frac{\sigma^2}{\sigma^2+a^2} $$

证明 引入常数 $b>0$,则有 $X \ge a \Rightarrow X+b \ge a+b \Rightarrow (X+b)^2 \ge (a+b)^2$,因此有
$$ P(X \ge a) = P(X+b \ge a+b) \le P[(X+b)^2 \ge (a+b)^2] $$
由 Markov 不等式可进一步推得
$$ P(X \ge a) \ge P[(X+b)^2 \ge (a+b)^2] \le \frac{E[(X+b)^2]}{(a+b)^2} = \frac{\sigma^2+b^2}{(a+b)^2} $$
既然上式对任意常数 $b>0$ 都成立,取右式关于 $b$ 的极小值时 $b = \dfrac{\sigma^2}{a}$,得到
$$ P(X \ge a) \le \frac{\sigma^2}{\sigma^2+a^2} $$

推论 设 $X$ 均值为 $\mu$,则上述结论式可变为对任意 $a>\mu$ 有
$$ P(X \ge a) \le \frac{\sigma^2}{\sigma^2+(a-\mu)^2} $$

取等条件 其中一个充分条件为 $P(X=0)=1$。

The post GAMES 202 笔记:Shadow Mapping first appeared on KSkun's Blog.
🔲 ⭐

2021~2022 年丢失博文的备份

博客挂的这几个月 这篇文章发出第二天,就有一位友人联系过来,提供了巧合下存档的本博客 2021~2022 年间的博文内容。这些内容仅包含文字,其中部分排版格式和图片已经丢失了,如果这段时间的博文对你有所帮助,可以在附件中找到对应的 Markdown 文档查阅。

其实,我想这更多是留给我自己一次回忆的机会吧 233,毕竟博文确实没什么营养,不会有人看的。

The post 2021~2022 年丢失博文的备份 first appeared on KSkun's Blog.
🔲 ☆

博客挂的这几个月

8 月底的时候发现博客站挂了,9 月底结束实习回到学校以后,我才有机会从外置硬盘里找到博客的备份,恢复站点的服务。然而遗憾的是,上次备份数据还是 2021 年 4 月,近一年来写的博文,包括 MIT 6.837 的学习笔记、2021 年终总结等等,这些内容都随着旧 VPS 实例的删除而丢失了。于是这次恢复服务后,想着借这个机会补一篇随笔记录一下这段事情,也记录一些今年的经历,权当是 2021 年终总结遗憾的一点微不足道的补偿了。

博客的消失

2022 年 8 月 20 日晚,刚下班回家,突然发现踢紫上不去了,遂开始 debug。尝试访问 HTTP 服务、SSH 登服务器未果,于是上 VPS 控制台查看服务状态,竟发现 VPS 实例从列表中消失了,只有工单处能发现实例处于 Cancelled 状态。于是提工单质问技术支持。

用蹩脚的英语与技术支持来回问了几次,才明白 VPS 实例是真的被移除了,原因是不知何时启动了 Disable Renew 功能。此功能启用时后台不会自动生成续费账单,到期后会自动销毁实例,且没有恢复数据的机会。

正当时我身处上海,而服务器的备份数据在离开学校时转移到了外置硬盘中,硬盘仍在武汉的行李之中,没有办法立即拿到这些备份。我除了无能狂怒并埋怨自己的误操作之外,什么都做不到。总之先通知此 VPS 实例上各种服务的相关方,并开始规划恢复服务。

最先恢复的一定是踢紫,可以说在当下的网络接入环境中,踢紫是生产力的重要来源。趁此机会,我购入了更划算的 VPS 订阅,重新写了一份配置调通踢紫,同步到各种设备上。除此之外,由于拿不到服务器备份,只好先把 DNS 记录改对,其余服务则必须得等到回学校再说。

9 月底结束实习回到学校,一切安排妥当后便着手开始恢复 Web 服务。首先从找到备份存档开始,而当我翻出来备份的时候又陷入了自闭之中——上次备份已经是 2021 年 4 月了,如果只能恢复这一版本,一年多来的博文内容就全部丢失了。事实证明我也没有更好的办法了,于是只能接受这个事实。

在恢复的过程中踩了不少坑,包括 WordPress 早期版本不支持 PHP 8.1、UNMP 本身的配置、想给站点根目录换个位置而 Wordfence 挂了于是只能搬回原来的位置等等,最终是用了一下午恢复了完整功能,顺便恢复了友人托管在我这里的站点。

连同这个博客一同丢失的还有友人站点上一年来的内容,为此我也没有更好的办法,只得以「红豆泥私密马赛」对应,并承诺以后会每月备份一次内容同步给友人做存档。这同时也是对我的警醒,之前太相信自己不会犯错误,于是备份存档、检查续费等等工作都懈怠了,想着每个月只需要支付一下续费账单即可,而没考虑到 Disable Renew 这样功能的存在而带来的风险。

读书,还是当社畜?

这是贯穿了我 2022 年整一年的话题,至少目前,我已经能给出一个绝对绝对不会后悔的答案——当社畜。

6 月的时候,我的想法还是「两手抓,两手都要硬」。一方面准备暑假前往上海参加实习,另一方面广泛地发邮件套磁,寻求可能的机会。这些邮件普遍被老师无事,有回复也没有直接回答招生相关的问题,所以其实没有什么价值。和大佬组队参加的 Jittor 比赛最终也因为大家的时间问题不了了之,以至于在申请时一点相关的研究经历都没法写入材料中。这个时候看起来,对升学的准备远没有工作的准备有把握,但咨询的前辈们也安慰我说,工业界经历和技术能力也会是老师看重的点,这份履历还是存在竞争力的。想着莽一把浙大预推免,而没有准备任何夏令营。

7 月伴随着上海零星的疫情入职了,各种培训、Mini Project,整个人都忙了起来。这样的忙碌直到 8 月,我们拿着做出来的小东西参加试玩会、末期评审,准备自己的个人评审,迎接最后的分组结果。幸运的是,我们辛苦的付出还是得到了相应的回报的,大伟哥对我们的游戏印象深刻,我在其中投入的精力也换得了较好的评价,进入了理想的项目组。

9 月,给出最后的答案。其实这个答案早在 7 月就已经想好了,只是 9 月能够把工牌和《放弃保研承诺书》一起拍一张照片,发在空间和朋友圈里炫耀一番决心。由于这个决定实际上很早就定下了,签署弃保承诺书的时候心里毫无波澜,觉得这不过是人生中的又一次岔路而已。不过发出去以后,还是有许多亲友来关心我的情况,大家也都能理解这个选择,这些问候让我欣慰。9 月也忙碌于项目组里的工作,最终无论是取得转正还是交付工作,都还比较顺利,算是平稳地度过了「读书,还是当社畜?」这个问题的困难期。

工作的压力

无论是在公司参加 Mini Project,还是进项目组后承担实际的工作,都是一件压力很大的事情。工作不仅会造成生理上的劳累,更在一段时间内给了我过大的压力,甚至导致了回家以后连游戏都不想打开,陷入一种「电子 ED」的状态。

工作的压力来源有很多。MP 时最主要的压力有两类,一是排期实在太地狱,以至于觉得多少时间给我都不够按计划做完要做的事情;二是其他人的期望很高,我很担心自己的工作能否满足合作者的预期。排期紧张和期望高都是无可奈何的,只能苦苦熬过去,辅以加班加点的工作来缓解。

实际工作的压力也存在两类,一是没有经验导致的规划少了工时,实际的工作时间因为需要边熟悉工作边完成需求而延长;二是对自己能力的担心,生怕自己搞出了什么问题阻断了其他人的工作,或者做出了不符合预期要求的结果导致返工。比我入职早的同事觉得,在项目呆的时间久了、熟悉工作了之后这些担忧会自然减少,但实习期只有这么长,这些压力也就无可奈何了,只得继续熬过去。

归根结底,工作带来的压力还是在于工作环境与学校环境的不同。我作为学生时时常单打独斗地完成作业,很少进行团队协作,也不需要对已经完成的作业做后续的修补维护工作,不需要考虑作业的可扩展性、通用性、健壮性等问题。工作则恰恰相反,你需要和很多人合作完成工作,需要尽量高质量地完成工作以免为自己和别人挖坑,需要关注工作的 ddl 以免拖延后续工作的进行。我直到这次才真正参与到一个庞大项目的协作之中,真正面对这些问题,而这个起点又是如此之高,以至于菜菜如我难以在短时间内适应这种要求,便难免需要背负许多压力。

好在,最后还是熬过了这近 3 个月的实习。

未来的一年、三年甚至更久……

以后要干些什么呢?

刚回学校的我忙于收拾行李、整理房间,这些清理工作花不了三天时间,闲下来的我没能立即为上面这个问题找到一个答案,于是又花了一周时间思考这个问题。

未来的一年做点什么呢?暑假抢到了 JLPT 的考位,所以得学一下日语了,还有 C1 驾照可能也得安排上以备工作后的不时之需。在上海的时候经常和同事唱 K,也对声乐起了兴趣,有必要报一个班专门训练一下吗?在学校很不方便做一些工程上的尝试,而理论方面会容易很多,所以想借着空闲来补一补没学完的 GAMES 202、104 等等。毕业前想和同学们搓个自己的游戏,尽量搓个大的、好玩的,以后可能没有这种机会了。以及最最最不能忘记的,一定要打游戏打个爽,首先就得把崩 3 原神没通的主线给打穿。

再往后想,入职后的生活会是什么样的呢?首先得摸清租房、通勤、户口等等这些门路,安顿下来生活。实习时和同事的这些社交,工作以后应该有机会继续下去,这样也挺开心的。工作之余一定得安排健身,至少是规律性的运动,身体状况已经有点危险了。但有一件事还不确定,工作以后还能剩下多少空闲时间呢?如果不知道这件事,就也没法安排兴趣向的活动了。

我真的成为了以前想成为的那种人吗?或许吧,至少现在方向挺对的。无论是从事游戏行业,还是进入 mhy,甚至误打误撞进入了前辈所在的项目,这些都是以前梦寐以求的结果。但作为学生时的那些幻想终究没法覆盖到方方面面,就连工作以后的生活是什么样的也无法想象到,这些内容只有亲身了解过——也就是实习后的现在,才有机会向后构思了。对工作后生活的幻想,也会是未来一年内要做的很重要的事呢。

至少,博客回来了!从现在开始,慢慢地补上一些知识性质的博文,如果有心情也时常更新一些生活和想法好了。

The post 博客挂的这几个月 first appeared on KSkun's Blog.
🔲 ⭐

2021 路在脚下(备份补档)

这是一位朋友巧合存档下的内容,其中的排版、图片等丢失了不少,我只打算从存档的博文中恢复这一篇最有价值的,其余存档还请移步存档汇总查阅。

按照惯例,在落笔写下这篇文章的第一句话前,是要回头看看去年的年终总结的。2020 的年终总结如此写到:

另外就是,看了一遍去年的总结。怎么说呢,在往回看自己的时候,总会觉得以前的自己 too young,很多时候确实存在偏执和激进的成分,包括这篇总结内提到的许多观点。我一直不希望向别人输出自己的观念,因此各位在阅读的时候也就图一乐就好,千万别当真。

2020, FUCKOFF

今年再来看这些想法和事情,觉得也并不是那么不成熟了。这是否说明个人的观念已经在收敛了,或者已经成长到自己认为成熟的一个阶段了呢?我不知道,但这样的观察会一直继续下去。

2021 年是疫情后的一年,大家都渐渐熟悉了后疫情时代的相处方式,习惯了戴着口罩勒疼耳根,也会下意识扫商店门口的二维码登记信息。唯独我,虽然还是跟着照做,但始终还是不习惯这样的生活,更不喜欢这个后疫情时代;如果有可能,我现在 100% 同意能让新冠疫情从没有在这个世界上发生过;当然这不可能,所以我现在希望能立即将新冠从这个世界上移除。总之,我现在变成了一个,有点怀念过去大家不用戴着口罩逛 gai,也不用带着立场和偏见互相攻击的过去,这样的「精神旧时代人」。

时代在走下行,但看起来我的人生还在上行中。2021 年内发生的种种波折:反反复复的零星疫情、并不能迅速恢复的经济状况、市场和政策的松松紧紧,一切都不容易看清楚,也曾动摇过我对未来状况的预期。另一方面,从结果上看,在这样的时代背景下,我依然做到了许多事情,也见证了很多积极和乐观的现状和预期。我依然愿意在这不太晴朗的现状中期待明天的好天气,以及期望着更接近理想中的未来。

在阅读往年的年终总结时,觉得还是过于意识流 + 记录思考了一些。流水账虽然不太有意思,但作为不同的体验,还是有记下来的价值的,不然就和「人生的意义在于体验」的想法相违背了。因此,本文将分为以时间顺序回顾的「大事记」、记录思考与观点的「意识流」、记录娱乐和感受的「阿宅记」。 ( 起名真难呀,在这三个标题上卡了一小会)

谨以此文献给这个世界。

大事记

走马观「花」

2021 年的开头,有两次亲近自然的体验。

一次发生在寒假结束回学校前,和母上一起爬山。倒是没有发生什么特别的事,但在快一年没怎么着家的状态下看到当时留下的照片,会突然产生一点想家的念头。在武汉上学的这段时间其实一点都不想家,因为首先宿舍是足够舒适的地方,生活本身也没有遇到很大的困难,另外家离的也不远,愿意的话买张车票 2 小时高铁就回了。大概实在是太久没有回家,最近也挺累的,再微小的情绪也会积累到能够产生情感波动的程度吧。

2021/2/21 摄于十堰四方山

另一次是在武大看樱花,趁着樱花还没全盛人不多的时候溜进了校园,赶了这趟早班车。这是在我高中时就有所耳闻,且心心念念一定要亲眼看一次的景观,由于时间不合适以及疫情原因拖到了 2021 年才真正看到了。武大和樱花是真的漂亮,奈何笔者没什么文采,就不弄巧成拙瞎描写了。

2021/3/4 摄于武汉大学2021/3/4 摄于武汉大学

一段很奇葩的情感经历

对我来说没有太大的意义,但很奇葩的一段故事。

被以前认识的一个 20 级学妹莫名其妙暗恋了,还表白了。多亏有消息灵通的友人告诉了我一些事情,让我得以在事情进展前摸清对方的动向,心里多少有点底。本来想着当个老好人演演戏,平静地拒掉,再随意聊点别的就能让事情结束。没想到对方不是很愿意,而且还闹上了,遂使用不予理睬对应,卓有成效。

总之就是非常奇葩,而且我怀疑我不知道怎么和妹子相处,现在对这件事也是一头雾水。

希望她那天半夜闹别把自己整感冒了。

软挑 & 与 wls 相处

春季学期的上半部分和 wls 及 dtm 组了个队打华为的软挑,还请下了长假脱产打比赛。想着好像有段时间没有这么拼一把了,觉得很有趣,就应了邀。

比赛本身没什么太大意思,觉得比赛题目有问题,哪有换数据集测的,自己对着样例鼓捣过拟合是必然的吧!但是打比赛的这个过程还挺有意义的,第一次不是单打独斗地脱产打比赛,还是与非常牛逼的两位队友组队,时刻都在想着我该怎么不拖大家的后腿。

借着比赛的契机,和 wls 及 dtm 相处了一段时间,他们都是很友善、好相处的前辈,平时的交流时没有感受到任何障碍,分锅干脆利落,讨论时(虽然不怎么能提供思路)氛围也很不错,也有幸以码力提供了重构框架以及实现工具的贡献。在比赛外的时间也会一起吃吃饭,聊聊天,总的来说相处的很开心。

大佬可以是很好相处的,也可以是共事起来大家都很平等坦诚的;心里那一份对大佬的仰慕容易引起一些不必要的紧张,相处了一段时间之后自然也会放下一点。

2021/4/11 摄于华为武汉研究所

广州之旅

因家事回了趟广东,办完之后就顺路在广州玩了两三天。住在北京路南边的天字码头附近,距离商圈特别近,于是就在周围的商业区和景区玩了下。本还想逛黄埔军校的,到地方发现要预约,但错过了最晚预约时间,于是在遗憾中作罢。

广州真是个好地方,好吃的又多,价格也便宜。有机会还想去。

2021/5/19 摄于广州南越王宫博物馆潮汕牛肉火锅 2021/5/19 摄于广州某八合里广东版 nginx 2021/5/20 摄于广州某工地

这趟回来之后发现自己对沙示汽水还挺有好感的,遂网购喝了个爽。

第一次隔离

广州之旅约二十几天后,由于广州新冠疫情,在临近考试周被要求隔离,心态爆炸。

隔离点的环境不能算好,饭菜虽然味道还行但很贵。在隔离点自然很难复习进去考试,于是直接进行了一个烂的摆,每天最大的乐趣是用花露水杀蚊子,前前后后能弄死了几十只。

2021/6/17 摄于华中科技大学隔离点

南京之旅

趁着出差讲课的机会在南京玩了一天,吃到了盐水鸭之类的,去总统府逛了一圈。

2021/7/4 摄于南京总统府

打工之前

在 4 月拿下了天美的实习 offer,7 月要去深圳打工。打工之前要搬宿舍,以及初到深圳的一些新鲜见闻。

拿下 offer 的过程还是比较曲折的。在朋友的煽动下随便写了点简历,找了好几位前辈征求意见,然后小心翼翼地投出去。先后参加了腾讯、网易雷火的游戏客户端实习生、米哈游的游戏服务器实习生招聘,笔试倒是过得比较顺利,主要是面试普遍被一面问到。腾讯更是撑到三面后被大人物问到觉得自己啥都不会,但居然被放进了 HR 面,聊聊天后也就顺利拿到 offer 了。

搬宿舍是这个过程中做的最糟糕的事情,因为一个错误的预期:一天内搬完整个宿舍是可能的,因此在时间计划上没有留出任何余地。那天起得很晚,中午吃完饭后去新宿舍办手续,随后发现房间卫生状况糟糕,于是开始打扫。打扫结束就临近傍晚了,于是速度开始搬运东西,凭着小电瓶车和从顺丰买来的无敌可靠大纸箱一趟趟地搬,从傍晚忙到了深夜 2 点,连宿管大叔都有点可怜我。

带着睡眠不足上了高铁,一路到了深圳。初到的观感其实特别新鲜,因为城市设计比较新潮。随后便是入住公司的中转酒店,四处玩玩,租下来房子,入职学习干活,后面就是另一个故事了。至少在入职之前,玩的这一段时间还是蛮开心的。

2021/7/7 摄于深圳北站

打工之中

毕竟是第一次的打工,还是在腾讯这样的大厂,很难说没有抱着一些幻想或者期待去投入。就像其他的活动那样,刚开始的时候因为有幻想和新鲜感在,不会有太差的体验;反而是久而久之,新鲜感褪去,一些不尽人意的地方的折磨也积累了起来,渐渐地就开始难受;在腾讯的实习便是如此的一段经历。

入职礼包 2021/7/9 摄于酒店

base 在科兴科学园,初见逼格十足,楼上都是玻璃幕墙,而楼下小吃、餐馆众多,看起来生活质量会很高。但是接触之后才知道科兴的办公环境比滨海还是差了一些,而且餐馆的消费普遍很高,我一开始还不是很能接受那样的消费水平(当然后来都习惯了)。

在腾讯的部门和人都很不错,mentor 和 leader 都是业界的前辈,很好相处。

虽然是实习,也并没有从很 common 的学习阶段开始,而是直接从具体的需求开始试着做。我接到的需求大多是不紧急、但很有研究价值的探索性课题,一方面能够从这些课题接触到对应方向的基础要件,也是一种很好的学习;而没有 ddl 就不至于被做不出来的压力压垮,可以慢慢入手;最后,如果这些课题的探索能够成功,无论是落地项目带来的成就感,或作为一段经历写入简历,都是很有重量的收获。除去自己太菜、做不出东西的痛苦之外,我还是很喜欢这样风格的培养方式。

在这个过程中,mentor 和 leader 也给了很多的指导,也会经常在一起交流感想。记得在中段的时候 mentor 问了我一次对什么方向感兴趣,说如果想接触其他的方向,也可以让对应的同事安排。当时的想法其实是想接触一下实际的 gameplay,不过后来还是选择了一个更大的课题继续做下去。大家对新人真的很友好。

腾讯科兴 base 2021/7/24 摄于科兴科学园

说完了要吹的部分,就该说说不吹的部分了,不过这里大部分因素其实在自己身上。

房子租在南油,因为是不同公司的同学合租,我是 base 离房子最远的那个,每天早上要 8 点起床赶班车,下班也要坐 9 点的班车回家。工作时间倒是不算长,但是把通勤算上,每天相当于有 13 个小时在外面,所以其实相对比较累。

暑假其实实习不是唯一要做的事,当时天真的以为自己能顾得过来,三开了冰岩的活和 GAMES101。最开始的时候,是晚上吃完晚饭后分一点时间出来做这些事情,以及下班回家之后做一会,但这些时间效率都不高,导致了晚睡。叠加上前面提到的通勤问题,其实后期长期在睡眠不足的状态下。

另外一个压力来源就是做不出来。实习后期的一个活做了一个 demo 来测试,demo 本身倒是写的很顺利,但测试出来的结果怪怪的,自己也分析不懂,于是就在反反复复的调整和尝试中耗费了大量时间。因为特别想亲手把这个结果落地到项目里,在离职前留下点自己的痕迹,做不出来的时候其实是比较着急的。

此外,一个中学时结识的朋友,仰慕的人,也是游戏开发的引路人,不幸发生了意外。这件事情其实是更早时知道的,但当时不知道应该怎么处理,于是拖到了暑假才和其他的友人一起开始处理。每每想起这件事都会有很多思绪泛起,更在当时推动了勉强自己的想法。

8 月份的时候,冰岩的 ddl 几乎没有一个赶上了,而因为睡眠不足又导致了感冒之类的毛病,最严重的一次整个人生理上地难受。

整个人处于一种,没什么大病,但是头疼、没精神、吃不下饭、躺着也睡不着的状态,以及肩关节依然酸着

8/25 的说说

后来和 zcy 聊了许多,把沉浸在痛苦里的自己拉了出来。虽然很不甘心承认我没办法兼顾那么多事情,但是生活总是要过下去的,效率也总是要提高的。为了现在觉得更重要的事,放下一些可能是更好的选择。

展开的话放在后面吧。

打工之外

实习的期间其实也没少出去玩,这里列举一下在深圳去过的地方。

2021/7/10 摄于南方科技大学

刚到深圳时去友人的学校窜门,南科是真滴有钱,楼都修的巨漂亮,宿舍条件也很好(除了一些很迷惑的设计)。友人在宿舍楼下的公共面积里搞了个学生酒吧&咖啡吧,简单喝了点东西。

2021/7/17 摄于腾讯滨海大厦2021/7/17 摄于腾讯滨海大厦

这是部门团建去了滨海的运动区域体验攀岩。滨海的环境真的好牛逼啊,连攀岩设施都有。暑假的时候身体虽然废柴,不过撑一撑最后还是爬上去了,好耶。

2021/8/1 摄于深圳湾公园

深圳的城市规划真的很不错。工业区和生活区基本上是交错排列的,通勤不会太长,绿地公园的开发也比较多。8/1 这一天来深圳湾公园夜里散散心,大概走了一半的步道。如果在白天,这里看看海还是很惬意的,晚上的话稍微无聊一些,但大热天吹吹海风也挺舒畅。

2021/9/4 摄于莲花山公园2021/9/5 摄于梧桐山景区

临走之前在深圳各个地方逛一逛。其中爬梧桐山的时候,因为身体状况实在是太差了,上一小段台阶心率就会飙到 140,只能走走停停地追着 csy,还是挺丢人的。那时下定决心说回学校以后一定要把日常锻炼提上日程,倒是坚持了那么一小段时间,后来又鸽了。 人 类的本质!

消费升级

要说今年买的最值的几样东西是什么,一定是刚开学的这几样:

  • SSD 和内存
  • 人体工学椅

刚回学校的时候趁着手里有点钱,入了 16GB 内存和 AX210 网卡给电脑升到顶配。后来机械硬盘寄了,于是申请了点援助经费把 SATA 位换了一块 1TB SSD。吃了同学的安利买了西昊 M18,自坐上之后腰再也没疼过。

这些东西都是实打实提升生活幸福感的内容,电脑的性能提升了以后开几个 JetBrains 的 IDE 不会再担心性能问题,而从高中困扰到现在的腰痛终于被解决,也是真的很爽。

追梦成功

话越短,事越大。

9 月拿到了米忽悠的实习 offer。

米忽悠的校招礼盒 2022/1/8 摄于宿舍

内部转组

经过暑假的影响后,我把冰岩的锅推掉了,在程序组招新结束之后就转到了游戏组。其实之前差不多也都互相认识过,转过来以后也相处的很愉快。但是自己的经历还是有点不对,没有办法直接转化成即插即用的生产力,这一点上面还是感到有点不安的。

游戏组联合主办的纸上游戏工坊活动 2021/10/30 摄于启明学院

CCSP

12 月的时候去了一趟深圳参加 CCSP,有幸打了块金,但由于路程匆忙没留下什么照片记录。

这块金其实挺意料之外的,因为算下来已经是退役的第 4 年了,很难说自己还剩下什么经验或者直觉去解决算法题。也因此把 T1 签上到之后,T2 只打了个暴力,后面也没再怎么碰这道题。

优势更多是在偏实现的 T3~T5 创造的,尤其是 T5 给了我巨大的帮助。T3 由于确实不怎么能看懂,看懂也不怎么想的出来怎么实现看起来很麻烦的量子操作,尽最大努力打了看懂了的部分,但是好像被卡了常数。T4 开的比较早,打了个 LRU 上去签上到,很快就被卷下去了,不过由于中段主要在看 T5 暂时放了放,最后卷回来的时候是在调 LRU-K 的参数 K。T5 本身还是比较有想法的,因为之前简单看了下 Redis 的可持久化原理,按部就班地先用 unordered_map 把基础要求打出来,然后用文件 IO 持久化指令,再加入分段快照,没怎么调试就写完了,实现了一血 + 常驻榜首。后来思考了一下为啥能榜首,可能是因为实现比较 C 风格,文件也基本上用二进制 IO,可以省掉流/格式化 IO 的一些常数。但也有点问题,比如 buffer 之间的复制次数还可以继续优化,以及合理使用 reinteprete_cast。

寒假讲课

被邀请去给高一的 OIer 讲组合数学,因此寒假离校后借住 qsf 家里以便线下讲课 顺 便撸猫

qsf 家里的布偶 2022/1/21 摄于 qsf 家里

讲课倒是很通常的事,不寻常的是,在备课的过程中找回了一些学 OI 时没能发现的乐趣。

学 OI 时因为着急点技能树,只记结论而不深究原理,后期靠题海战术强行点熟练度,导致整个学习的过程其实挺无聊的,也有很多原理是在退役后想明白的。

在备课的时候,因为之前试讲发现学生的水平能够接受原理性的知识,于是萌生了扩展范围的想法,自己也开始看离散教材和《具体数学》来挑选适合放进去的内容。在这个过程中重新学了一遍生成函数,并且手推了挺多的式子,感觉这种把单个的函数与无穷数列联系起来的处理方式非常精妙,而且和信号里时域-频域的关系还有点像。

学无止境,虽然好像现在学这些也没太大作用,只是希望能给学生带来一些不一样的东西,顺便如果能安利他们以后走上数学或者 TCS 的道路也挺好的。

摘自南京大学《操作系统:设计与实现》课程

意识流

这里主要记录今年的一些看法,类似去年的年终总结的风格。

开头也提到了,今年再来回头看去年的年终总结,那种 too young 的感觉有所减少,也说明自己的思想在往收敛的方向发展,不知道这是否是一件好事。

内卷的未来

内卷是什么呢?2021 年,我又把这个问题拿出来思考了一遍。

狭义的内卷,一般指的是需求太大而资源太少,导致竞争升级的现象。而由于资源的供给不由参与竞争的这些人提供,而是由上层建筑来决定,所谓的这个上层建筑又由于发展阶段的原因并不能真正满足每个人的资源需求。

如果这样来解释内卷,其实作为内卷参与者的我们是无力去改变现状的,因为它的原因是深刻而复杂的,是短时间内不能很好解决的难题。而且内卷带来的结果也不一定是负面的,因为竞争本身也是一种筛选的过程,卷赢的人或许在统计学上更适合得到这份资源。

那么为什么我们要反对内卷呢?

因 为作为内卷的参与者,我们被升级的竞争折磨得死去活来,竞争的压力实在太大,它压垮了我们的自信,压坏了我们的情绪。我们在参与竞争的时候总是希望自己是无情的学习机器,能够以 7×24h 的可用性全功率地参与竞争,因为这样才能卷赢。但在实际做这件事情的时候,感性会告诉自己这是坏的,人有社会化的需求,有与他们社交、共情的需求,看着自己一步步向讨厌的方向堕落,迟早会开始崩溃。

因 为内卷的局面不总是公平的,就算是公平的,也会存在囚徒困境和边界效应这些讨厌的情形。参与竞争的我们最相信的事情是:只要付出努力,那么无差别的努力就可以转化成取得的成果。公平性是这一命题成立的关键,如果有人破坏了公平性,相当于所有人付出的努力被否定。其次,不合理的规则会导致囚徒困境,竞争时的我们都可以当做理性的博弈者,如果规则中存在囚徒困境,所有人会让彼此陷入互相伤害的局面,而事后又会开始憧憬所有人在互相受益的局面的可能性。再次,边界效应是升级的竞争中很容易遇到的问题,付出的努力无比巨大,但在最后的一段时间里却没能获得显著成果,尽管努力转化成果是存在的,但容易使努力的积极性受损。

也许前面的两个原因是共性的,这一条可能有我自己的个性。 因 为内卷让我被迫面对那些讨厌的事情,让我在短期内离追梦更远。例如我的专业和发展方向是有偏差的,但为了提升学历,我必须投入较大的精力到专业课程里,而其中有一些课程是我不太学的明白的,支撑我学习的动力会随着时间推移减弱。虽然嘴上说着热爱学习未知,也很想对每一份未知都保持着好奇心,焦虑多少搞的自己还是有点过度在乎那些功利的考量。

这些是自己在面对内卷遇到的困难。而如何解决这些困难呢?这可以从这一年的经历中提取出一些想法,但并不能完全地解决这些困扰,毕竟,现在的我仍然面对着其中的一些困扰。

找 一个信得过的朋友,坦诚布公地聊聊天。这是在我身上最有用的一种方法。和你的朋友完整地描述你现在遇到的困境,你被卡在了什么地方,你有哪些选择,你的想法是什么。和他辩论为什么自己做不出决定,别的选择有什么好处,让他说服你选择你下不了决心的某一项。反正在我这里,我一般都能被说服,并且大胆地尝试改变。

娱 乐。这是一种我经常用,但是效果不好,却又不能没有的方法。娱乐一定是生活中不可或缺的要件,生活总有不如意的地方,长期以来就会积累出所谓的磨损,而娱乐可以在有限地减少一些磨损。具体来说的话,我喜欢吃甜食、吃大餐、去 KTV、去自然风光好的地方游览、看番和打游戏。不过这个有一个挺大的缺点,大部分项目还是有点费钱。

转 移注意力。由于我长期处于多开模式,这种方法也很常用。分一点时间出来,做一些更有意思但当下不是那么着急的事情,比如复习备考之余去写点代码,学点图形之类的。

好像扯得有点远了,这个部分的主题是「内卷的未来」,也就是想要探讨这个内卷的时代的未来会发生什么。那么开门见山,我认为 未 来不会发生什么。

内卷其实只是起源于类似 meme 的表达,大家觉得这样来描述竞争很有趣,就这样传开了。内卷背后的那些事情,资源的不足、压力的上升、焦虑的气氛、不良的环境,这些都早已存在,只是在这个时代里我们用内卷称呼他们,并且这个时代的环境稍微更加恶化了一些而已。

既然如此,平常心看待这个现象便可。以前的前辈们如何面对竞争,如何探索自己的人生,我们依然可以效仿着在自己身上实践。

效率与时间

很多 ddl 没赶上的时候,我都会因为这个问题而自责。定下了一个比较激进的 timeline 以后,下决心说要逼自己一把,或是有许多机会舍不得放弃,而决定多开来兼顾两方。这样的计划就要求自己在一定的时间内创造比悠闲时更多的成果,也就是说要提高效率。而大部分情况下,这种尝试都会以失败而告终。

这里列举若干个年内发生了的事情。例如实习时希望多开冰岩的项目框架与 GAMES101,分配了晚上和下班后的时间,但实际上效率低下,只顾得上实习本身的事情了。而后来联系到浙大的老师之后希望在大三上多开必修课和 MIT 6.837,最后也以牺牲 6.837 的产出而告终。

存在提高效率的方法吗?一个人的效率是否有上限,又是否与悠闲时的效率等同?

一如那些讨论成功学的书的结论,提高效率的方法是当然存在的。比如可以通过日计划、周计划来精确地分配自己的时间,比如将需要完成的事情列成清单并标注 ddl,以便随时查阅来安排短期规划。这些方法都有过尝试,但最后也都放弃了,最后,研究这些方法反而显得很没有意义。

我自认为我是一个可以为了有趣而加班加点肝的人,而对于那些不感兴趣的事情,则会一拖再拖。而在这个拖延的过程中,用来打发时间的办法却又是一些水群、睡觉、出去玩一类的短平快娱乐活动,在娱乐中对时间流逝的感知不强,从而忘记了自己原先的规划。

一个在自己身上实践成功了的方法是: 立 即去做。有意识地在 todo list 里挑选一个事情,马上去做,试着专注在这件事情上。如果能够实现这种专注,那么专注带来的惯性和事情有进展的反馈会推动自己一直做到结束,甚至进入一种心流状态。

然而很多时候,问题也并不出于自己的拖延上,而是看起来时间很多,却有大量时间被打碎,不足以形成专注或干不完就会被中断。如何利用碎片时间也是一个我暂时没有找到答案的问题,目前而言,我对手机上的信息流已经感到厌倦(除了水群),且正在尝试随身带一本不烧脑子的书阅读,看起来还是比较有效果的。

在这一年内,我的消费观念也发生了些许变化。首先是被深圳的消费水平彻底打碎了原有的价格认知,并且在拿到实习工资时甚至觉得自己又消费得起了,不知不觉变得更愿意消费了一些。在这之外,我也建立起一种认识, 如 果能用接受范围内的钱来换取方便或者节省时间,一定是赚的买卖,该打车的时候打车,该购买服务就购买服务。至少这样的消费能切实感受到生活变得容易了一些,一些琐事从 todo list 里需要分出精力应对的项目变成了一笔交易,在有钱的时候真的很爽。

在游戏或影视中很容易建立的心流,放在其他的事务上变得困难无比。尽管如此,人为地引导心流的尝试也有少数几次成功,希望在来年能够掌握这种方法,毕竟还有很多想做的事情需要「逼一逼自己」。

人生的走向

2021 年里,我做出了一个仅次于人生走向选择的决定: 放 弃一些虽然想做下去,但长远来看对发展无益,精力上也顾不过来的事务。这条原则源于 8 月在深圳过的很难过的那段时间,在冰岩的后端 ddl 始终没法达到,而且也分不出精力为招新准备材料。和 zcy 夜聊交流了很多想法以后,觉得已经到不得不把手上的事情交出去的地步了,于是就去和当时的主管谈心,把项目和组长都交了出去,剩下的只有一个招新的事情。幸运的是,在事情结束的后来,我得以转入游戏组,也和游戏组的大家相处得很愉快,这样的结局比切断一切联系要稍好一点。

另一方面,2021 年我这样一句话有了更深的感触:

一个人的命运啊,当然要靠自我奋斗,但是也要考虑到历史的进程。

引自某人语录

2021 年,反垄断、市场监管、文化准入等等铁拳向互联网行业砸下,与之同时,版号、未成年人限制、媒体的又一次污名化也影响了游戏行业。

记得是在一次上班等电梯的时候看到的有关游戏行业的信息,当时就在想,从禁令解除这么长时间以来,游戏行业发展到今天这一步已经很不容易了,这样做到底是图个什么呢。我自己当然想不出什么答案,但多少有些担忧自己的未来,2 年后毕业的自己会不会刚好踩进行业低谷的坑中。

在 2020 年定下未来的路线以后,我一直抱着一种「现在先把自己的技能树点满,到真正参与工作的时候,只要足够强就能站着把钱挣了」的心态来预想未来,也因此一直保持着对未来乐观的心态。然而,看到大环境的这种波动时,很难说不会开始一些担忧,而担忧又伴随着「就算变成无敌强人,局势也不是我能扭转的」这样的无力感,或许存在不小的可能性,未来真的站起来也没法把钱挣了。

再来谈谈今年发生态度转变的契机吧。实习的后段其实整个人都非常低沉,原因有很多,例如手里的工作进展不佳,例如计划中的多开最后都没顾上,更重要的是这样一个消息:一个仰慕已久的前辈遭遇了意外。意外的到来都是很难接受的,因为它没有什么理由,也没有什么预兆,任何一个人都很难接受一个人突然从你的面前消失。

今天要讲一个故事,一个关于少年和一位 mad scientist 的故事。 少年还是孩子的时候,认识了一个比他大 5 岁的中学生。是因为打电动而认识的,他们都是宅,而且也都爱打电动,所以熟悉的很快。 中学生是一位科幻爱好者,很喜欢 Half-Life,他在另一款游戏里重现出了 HL。虽然少年没有玩过 HL,但是这个重现版本也让他觉得中学生很厉害。 中学生发起了一个社团,作为主催经营着社团的活动,上面的这个重现也是社团的作品之一。少年在这个社团里,还有一些朋友在这个社团里。大家都很喜欢打电动。

有一天,一个新的点子在社团里激起了水花,在游戏里复刻科幻动漫难道不是一件很炫酷的事情吗?少年和中学生都喜欢看这部动漫,抱着很大的兴趣,和朋友们一起启动了这个项目。 少年一直觉得中学生很厉害,也想学着在游戏里做一些炫酷的事情,因此他去学习了 Java。这次,是他第一次能在作品里帮上忙—-> 虽然是一些无关紧要的小东西。他尝试看过那些中学生写的代码,但是一知半解,没有深究下去。 中学生变成了大学生,他的实力只会更强。看到作品一天天趋于完善,甚至有着出色的效果和质量,少年真的很佩服他! 他曾经说过,他想看到完全潜行技术变成现实,想制作出这样的游戏。所以,复刻已经无法满足他了,制作独立游戏是他的下一个目标,于是他又开始了新的征途。 这个时候的少年在做什么呢?少年是中学生,而中学生必须要通掉高考的副本才能变成大学生。少年

在苦苦地打本升级,虽然他也尝试过有些不同的道路,但少年做不到他那样与众不同。少年想,等我成为大学生以后,我也想变成能做出炫酷东西的人。少年和他不再有以前那么紧密的联系了,只是默默地刷等级,默默地仰慕他和他的作品。

少年终于变成了大学生,和他是同一个学校,可是早在进入大学之前,仰慕的那个人已经毕了业,进入了一家最接近他愿望的公司。少年也想进入这家公司,或许是因为出品的游戏好玩,或许是因为少年一直在追寻他的身影。

少年的专业不是计算机,也和游戏开发不沾边。少年加入了他曾经在的团队,也不是做游戏开发想换的事情。少年觉得这样也没什么不好,便顺其自然地生活了下去,少年知道他仍然在继续他的征途,但是已经完全不知道他在哪里,他的征途又长什么样了。少年只是觉得现状很舒服。

心里的一丝念头让少年走进了一些他本不应进入的课堂,比如《计算机图形学》,又或《游戏学导论》,少年发现,虽然自己的专业也很好玩,但这些课程让他沉迷其中,少年觉得自己丢掉了初心,应该做点什么弥补一下。少年也许回到了若干年前的心态,但现在,他终于明白了那位日日夜夜奋战于兴趣作品的前辈,是什么样的东西给他做得更好的力量。这真的很有意思。

少年迈出了第一步,那是一门网课,他看了一半,作业也做了一半,却因为不知道什么原因断掉了。少年的第二步是学习基础知识,但这一步甚至从来都没能迈出去。至于阴差阳错跨出了一大步的第三步,少年得到了一份offer,岗位和他的偶像相同,只是不是一家公司。当然,少年显然会尝试他的那家,因为资历和经验实在太白,少年显然四处碰壁。

少年在定下目标后,变得更忙了。是因为需要完成目标?不,是因为课更难了,事更多了,团队也有需要出力的地方。少年在他的目标上没有成果,但是他已经被各种事情耗尽了电池,却又缺乏加快充电的方法。少年觉得自己很废,觉得自己做不到偶像那样的万能而强大。

少年撑到了入职的那天,他觉得这是自己离偶像最近的时候。但是,少年实在对这个业界了解不多,他从事的工作和偶像并不一样。这不是因为少年选错了职位,而是因为这个工作真的有很多方向,这是少年第一次对这个业界形成了最真实的认识。在若干天学习、工作后,少年觉得自己成长了许多。”也许这样,以后我真的能获得与他共事的机会,也能帮得上他的忙了。”少年在辛苦但收获颇丰的工作生活中偶尔勉励自己。

但是少年有了一件不愿意相信的事情。不知何时不知何地,也不知为什么,偶像永远地离开了这个世界。这个消息实在是过于震惊,也没有能确认的渠道来证实,少年始终不愿意相信。接到offer的时候,少年第一时间告诉了他,这是在两个月前。为什么才过去了这么短的时间,事情变化会这么大呢?可望而不可及的那个身影,突然消失了。少年很懵逼。

重拾起那些有他的身影的回忆,少年的成长里到处可见对他的崇拜,少年追随了他很久,尽管迷茫过,最终也回到了他走过的道路上。他是少年的引路人,是影响少年人生的重要之人,是会被少年永远记住的人。 少年心中的那位已经离开了。少年要怎么做?他是一个 mad scientist,曾经有着中二的想法,最终竟真的走向了那个想法。他是对游戏很认真的一个人,为了有趣的点子,他能点出所有需要的技能树。他喜欢思考,他热爱生活,他有文学和艺术的兴趣,他依然喜欢纸片人,甚至还偶尔临摹一些。少年想继续成为像他一样的,能做出很酷的东西的人。 少年觉得自己可能还是太废了。但是少年会尽量努力,尝试变得不那么废。

2021/7/30 的说说

回想起记忆中模糊的片段,其实从事什么样的工作,未来接触什么样的事情,自己早就有过一个答案。只是在刚进入更广大的世界中,因为选项太多而误打误撞走上了别的路线,又带着一份随遇而安的无所谓心态就这么走了下去,也没有过多的考虑。这件事让我回想起了中学时单纯的想法,此时我这样的状态是不是也是一种遗失了初心的表现呢?在这样的不安中,我决定来一次急刹车,把方向强行掰过去。

阿宅记

2021 年中,我总有一种浓度在慢慢流失的感觉,因为看的番越来越少,游戏也很难再有动力打开。仔细回想起来,其实是分了太多的精力在焦虑和内耗之上。每时每刻都在思考 ddl 的优先级顺序,在纠结如何为接下来的一片时间创造意义,并为刚刚浪费的一片时间感到懊悔。

娱乐是生活的必要组成部分,影视、读书、游戏,乃至运动,都可以成为组成娱乐的项目。问题在于,冗杂事务将时间打碎成一片片的零散片段,依赖沉浸体验的娱乐项目很难进行下去,一段时间离开了环境很有可能就回不去那种状态了。因此, 有 必要在未来规划一些专门用于娱乐的大块时间。

番剧

轻音少女

早就听说一个高中同学非常喜欢这一部作品,也是古早时代京阿尼的著名作,抱着仰慕的心情看了下去,因为太符合个人兴趣而立即被圈粉。

轻音的日常剧幽默得很自然,经常会笑出声音而不自知,感情戏则细腻而动情,强烈的感情会被掺杂在一些反而并不是很强烈的行动、对话中表现出来。毕业时阿梓喵的一句「请不要毕业」,随后奏响的《相遇天使》,在这样的氛围中很难让人绷得住。

音乐是轻音中给我留下最深影响的元素。很早以前就听过 ED 之一的《Don’t say “lazy”》,当时就树立起了澪帅气的形象。真正看完了整部番之后才体会到其音乐完全没有局限于 ED 这样的风格,OP 类似唯的轻快少女风格,插入曲中也有不同风格的作品,让人很难忍住不跟着唱。本身个人比较喜欢日式流行音乐,受轻音音乐和唯学琴经历的影响,更是有点想去接触一下吉他了。

当然啦,就我这样三分钟热度的学法,肯定不会有什么结果。预想到这一切的我最终还是放弃了这个念头。

编舟记

相当小众的一个题材,讲述一位对文字词语天生具有兴趣的编辑参与词典编撰工作的故事。剧情平平淡淡,感觉重点更像是把一些常人并不知道,也不会刻意关注的幕后故事放在台前讲述,用于支持这些带有一定社会公益性质的事业发展,同时也能满足观众的猎奇心理。

番剧之外,更多的是让我想起了初中中二时期的一些「文学创作」。当时还会刻意地阅读和模仿一些近现代作家的风格,并且认为文化素养是有必要积累的东西。而今已经完全变成了没有任何文化素养、铁板一块的工科男,笑死。

魔女之旅

伊蕾娜是真的可爱!

观看的体验感觉非常新鲜,观感类似于公路片,主人公伊蕾娜是能力很强、能解决各种问题的理想人设,但她并不会主动参与到各种事情中去,而只是旁观着事情发生。因此,作为主人公的伊蕾娜更像是一位讲述者,虽然她有自己的想法,但是并不会像爽文那样,利用自己的能力来改变那些不美好。这种新鲜的观感,加上非常漂亮的人设,以及还算不错的剧情,创造了非常好的观看体验,也从此开始喜欢上了单元剧的格式。

但是全剧结束之后,反而让我自己产生了一种落差感。在观看过程中,我倾向于与伊蕾娜产生共情,在她的视角参与到事情中,理解她的心情,从而体会到了一种「自信爆棚、随心所欲、个性张扬」的感觉。伊蕾娜被刻画成相当理想的形象,出身于学者世家,靠着惊人的天赋成为了魔女,几乎没有遇到什么挫折。而反观自己的经历,每进入一个更大的圈子中,都会感叹于别人的牛逼,而自己的能力有限(最近甚至总感觉已经顶到了这层上限),追赶不及。从代入理想角色切回现实视角的时候,难免会产生如此的落差感,而感受到一种模糊的伤感。

总的来说,还是很棒的观看体验。

死后文

自看完《魔女之旅》以后就对单元剧方式叙事的作品有兴趣,进而开始看大河内一楼的一些作品。死后文便是其一,在这一世界观里,死者可以从死后世界向现实世界中的人寄一封信,由邮递员负责将信交给收件人。故事从邮递员文伽的角度展开,她只负责派送信件,而几乎不参与围绕信件的故事。而剧情后半,文伽也被卷入有关死后文的故事,向观众解释了她的背景故事。作品的设定与剧情表现不错,以死后文的设定引起有关道德伦理的思考,题材新颖深刻。

公主代理人(Princess Principal)

相当不错的一部作品,将爽文发挥到了极致。同样是大河内一楼编剧的一部单元剧作品,更是引入了乱序叙事,体验非常新鲜。

世界观建立在中世纪+未来科技之上,少女们承担着「间谍」的沉重责任,古老且有些脏的环境风格与科技的发达形成对比,同时少女们漂亮的身姿与其职业、格斗技艺也形成对比。实际上,少女们只是政治博弈的工具,但她们对此没有怨言,而公主和间谍之间还发展出了一条暗线,这两条线在故事中相辅相成,朝着积极的结局发展着。

整部作品的各种表现都围绕着爽这一个字来设计,强烈的设定对比,打戏中的激烈对抗表现,看似柔弱的少女总能化解危机,在紧要关头实现漂亮的反击,可以说从头上瘾到尾,非常值得观看!

赛马娘 第二季

第一季是有关特别周和无声铃鹿的故事,从观感上来看更像是为了给游戏做广告。而第二季中,我感受到了对东海帝王这一名马的厨力放送。

东海帝王的三次骨折,一次终结了「三冠」,一次打破了「不败」,最后一次夺走了她登上赛场的机会。她有过失落不振,有过摆烂养老,曾放下了所有的追求,彻底躺平。但是最后一次,承载着众人的愿望,她还是走上了赛场,甚至以第一的成绩完赛。大起大落的经历,加上编剧有心的铺垫与氛围营造,给当时的我狠打了一针鸡血。

现在回想起来,有马纪念那一集喜极而泣的心情依然能够震撼人心。Cygames 是神!

Love Live 系列

补完了 LL、LLSS 剧场版以及虹团和星团的 TV,算是怀旧一下吧。LL 系列作品本身倒是表现普通,但这个符号对应的那个时代,和在那个时代发生的种种故事,却是人生中深刻的回忆。

NOI 失利后,尽管心情很差,还是去参加了当年的文艺汇演,约到了一些不认识的 LL 众在 NOI 现场唱了《愛してるばんざーい!》。已经记不清当年约到的人分别是谁了,只有一位现在还在保持联系,LL 这个符号和 OI 生涯的回忆通过这段经历联系起来了,因此当补到 LL 系列,尤其是初代,总能回忆起当时的情景。

游戏

节奏医生(Rhythm Doctor)

以简单的单键节奏游戏为核心的一款独立游戏。但其质量主要体现在交互设计和演出上,Boss 关中多轨混合以及窗口的画面效果、移动效果体验都很出色。同样地,音乐也很好听!

海沙风云

味道很正很正的粤语文字冒险!好久没有玩过文字冒险之后,体验了一把非恋爱题材+粤配的这个作品,各种方面让人耳目一新。网上看了一些关于世界观和暗线的解释,文案设定老师在这块是下了功夫的,非常推荐。

莱莎的炼金工房 2

通过 BGM 先认识到的游戏,本身是典型 JRPG,玩法核心在采集和合成系统上,通过这个解锁更多的道具、工具、药品之类。画面和模型品质一般,BGM 出色,跑图的感觉也不错,解谜属于偏简单的难度。玩这款游戏更多地是因为莱莎可爱以及个人就是比较喜欢 JRPG。

只狼(SEKIRO)

第一次体验高难度动作游戏,在精英怪处死了几十次,和训练怪又练了几十次,看了下攻略视频,才过去的。一直以来游戏战斗喜欢用很保守的策略去放技能之类,但是在只狼这里更看重时机和节奏感,也就是所谓的手感,因此刚开始上手时会很不适应。此外,手柄默认绑定的按键是 LB/RB 这点也不是很适应。

对我来说这类游戏是第一次体验,找手感-训练-实战攻破的过程还是很有趣的,只是可能不是很有时间来反复练习。

原神

其实没有什么好说的,只是想晒一下(手动滑稽)。

阅读

游戏改变世界(Reality is Broken)

这本书中围绕着一些已经发生过的、具体的游戏实例,来向读者展示游戏为我们带来的美好前景,以及游戏可以带给我们的超能力。阅读这本书本身是一件非常上瘾的事情,因为自己也是游戏玩家,相信着游戏能够为我们带来美好的未来,又配合书中的讲述,在脑内构建出了一个相当理想的世界。

可惜的是,这本书的成书较早,而在今天的角度来看待当时提出的这些愿景,只能说终究是错付了。或许比起合作和社交,我们更喜欢攀比与竞争吧,毕竟这是一个内卷的时代。

但是我还是很推荐你阅读这本书!我觉得我对于一些问题的看法,因为阅读了这本书,并接受了其美好的愿景后,变得积极了一些。我愿意为创造这样的美好未来付出一些努力。

游戏人工智能

一本在策划角度深入理解游戏中的人工智能的书,还在看。一个最深刻的观点是,在游戏中应用人工智能,其目的是利用技术手段给玩家带来一种体验,因此人工智能可以不那么智能,可以是决策树或状态机一类简单的模型。优秀的玩法设计、战斗设计才是体现出这些技术在游戏中的价值的关键点。

结语

不出意外,这一篇年终总结又鸽了好久,最终完成于开学后的 3 月 10 日凌晨。此时的我已经记不起创建这篇文章时的心情了,那时有着许多想表达的内容,以及对过去一年的复杂想法。因此,只能草草收尾,补全一些当时中断未写的内容,趁着记忆还在赶紧留下这些记录。

2021 年真的发生了很多,第一次发现自己的看法在收敛,第一次觉得自己的能力顶到了上限,第一次向行业迈出一步。最重要的是,一个曾经遥不可及的,可能能被称作梦的目标,在这一年终于达成了。现在回忆起这段经历,依然感觉到如梦似幻,有些不太真实。

想用积极的态度为 2021 年结尾。在大三这一年肯定会发生更多能够决定人生走向的事情,我正在为此做好一切能够做到的准备,等待时间为这些事情埋下种子,萌发新芽,最终得到结果。我愿意相信努力能够收获回报,未来能够被我掌握,目标能够通过进步来达成。希望在 2022 年再撰写年终总结时,能等待现在埋下的种子产生结果,骄傲地向世界展示这一切。

May all the beauty be blessed.

KSkun 2022 年 3 月 10 日 于武汉

The post 2021 路在脚下(备份补档) first appeared on KSkun's Blog.
🔲 ☆

VoIP 中的话音失真问题分析

By KSkun, 2021/2

前言

2020 年的疫情使得我经历了一整个学期的网课。课程资源的电子化有方便保存和分享的有点,但是网络状况等问题依然使线上教学成为一件非常折磨人的事情。我依然记得,在这半年里我经历了无数次的自己掉线、老师掉线、声音卡顿、视频变静止或者变糊等一系列问题。有时声音还会变的不自然,有一种电音的感觉(导致了某同学被称为电音女王)

为什么网络状况不佳会导致声音的卡顿、不自然和出现电音一样的失真呢?这便是本文将要探讨的问题。

VoIP 是什么?

既然需要研究在线语音通话,就必须先了解其工作原理。

面对面说话时,声音作为机械波通过空气等传播;打电话时,声音被转换为电信号,并通过电话网络传播到对方电话,再将电信号转换回声音;线上语音通话时,声音通过麦克风被设备采集为数字信号,再通过 IP 网络传输到对方设备上,这便基于 VoIP 技术。

图 1 VoIP 的系统结构[1]

VoIP(Voice over IP,基于 IP 的语音传输)是指利用 IP 网络与相关编码、协议、算法,对语音进行采集、处理、传输与还原的一种语音通话技术。[2]根据描述,我们可以把 VoIP 的工作流程整理如下:

  1. 输入:声音从麦克风输入设备,设备将其转换为数字信号;
  2. 编码:将一定时长的声音信号按指定规则编码;
  3. 传输:使用 RTP(Realtime Transport Protocol,实时传输协议)协议在 IP 网络上传输声音数据;
  4. 处理:利用一些规则与算法减少网络抖动的影响、提高声音质量与抑制回声等;
  5. 输出:通过音频设备输出处理后的声音。

网络不佳主要影响第 3 步的传输过程。为了提供更好的实时性,RTP 协议工作在 UDP 协议之上,而 UDP 协议提供无保证的传输,因此容易发生丢包、乱序等问题。RTP 协议中,一个数据包包含一些必要的信息与一小段声音数据,因此丢包造成的影响直接体现为缺失某一段声音。[3]

话音失真现象

我们已经知道使用在线语音通话时,声音失真的问题应该是由丢包导致的,但丢包如何导致我们观察到的这些现象呢?接下来我们就将研究这一问题。

请注意:以下内容使用的示例可能会让您血压升高。

语音卡顿

由于 VoIP 基于 UDP 协议传输数据,而 UDP 是不可靠的,导致丢包时有发生。且因为通话的实时性,我们也无法重传丢失的数据,因此如何填充音频流中的空缺部分成为一个问题。

一个选择是直接填充空白,这可能导致说话时,音节中出现不自然的空白。这种情况听起来一卡一卡的,即卡顿现象。下面是一段在 10% 丢包率下,以空白填充缺失部分的语音示例:[4]

10pct_rand_silence.wav *请在参考资料中获取音频

通过这个示例,我们发现,如果空缺部分覆盖了语音的辅音,很容易造成语音难以辨认。且空白段的存在造成了部分爆破音与不自然的听感,听起来非常难受。

「电音」

在空缺部分填充空白的效果不佳,因此需要采用其他方法填充,这便是 PLC(Packet Loss Concealment,丢包隐藏)算法。一种常用的方法是重复输出最后收到的一小段,这在丢包率较低的时候效果良好,但当丢包率升高时,则容易出现类似合成声音的机械音(robotic)效果,也就是所谓的电音。下面是一段在 40% 丢包率下,以重复播放最后一段填充的语音示例:[5]

40pct_rand_plc.wav *请在参考资料中获取音频

接下来的问题是,为什么这种方法会使声音听起来像合成声音。不妨换个角度,先来看看如何制造听起来很机械的合成声音。Valve 公司开发的 Portal 系列游戏中有一个人工智能角色 GLaDOS,其语音就具有这样的特点,以下是 Portal 2 游戏中的一个片段:[6]

GLaDOS_voice.mp4 *请在参考资料中获取音频

Valve Developer Community 中给出了一种将正常语音处理出此效果的方法:固定声调、抑制声调变化[7],这可以通过某些声音处理软件实现。接下来,我们通过 Melodyne 软件来对一段正常的语音进行以上处理,以制造类似电音的效果。

【Melodyne 使用】 *请自行搜索相关示例

通过这个例子,我们知道,如果声调变化较小,声音听起来就像合成声音。而如果我们连续重复播放最后一段,由于传输时音频切分的较短,会使填充的部分声调单一化,出现类似以上处理的效果。这也说明,通过重复最后一段进行 PLC 处理的做法不总是能得到好的效果,我们需要改进。

ITU-T G.711 附录 I

国际电信联盟在文档 ITU-T G.711 附录 I 中提供了一种比较好的 PLC 算法。该算法工作在 PCM(Pulse Code Modulation,脉冲编码调制)编码下,采样频率为 8kHz,且一帧为 10ms(80 个样本),流程如下:[8]

  1. 在正常接收数据时,保存最后的 48.75ms 数据且延迟 3.75ms 输出;
  2. 遇到第一个丢帧时,进行如下工作:
    • 估计声音的周期:用最后的 20ms 声音向前计算相关性数值,取相关性最强的位置计算周期;
    • 填充第一个丢帧:使用计算出的最后一个周期重复来填充第一个丢帧,且前后各取 1/4 周期做平滑过渡的处理;
    • 将生成的一帧保存下来;
  3. 如果第一个丢帧之后还有丢帧,此时继续重复周期将可能生成不自然的声音,因此要引入变化与衰减来调整声音;
  4. 与之后正常数据的衔接处也需要平滑过渡处理。

这种处理方式引入了周期的估计、过渡平滑与引入衰减等处理,相比机械地重复最后一帧效果有极大提升。下面是一段 40% 丢帧率下,使用此方法填充丢帧的示例[9]

male1_3_itut_20ms_40.wav *请在参考资料中获取音频

可以观察到,此方法填充后效果良好。但与机械重复相比,此方法必须进行大量数学运算且必须引入延迟,可能影响通话效果。

结语

本文中,我们简单介绍了 VoIP,并研究了 VoIP 中影响通话质量的问题表现、原因,也讨论了几种对于此问题的解决方法。语音通话是一项要求强实时性的业务,用户可以忍受一定程度上的丢帧,我们必须在延迟和丢帧影响上做平衡。因此,也许我们可以使用更好的方法处理丢帧问题,但由于会引入延迟、消耗算力,实际应用中有时不会采用这些方法。

本文并未深入介绍 VoIP 的相关原理,也只提到了几种解决丢帧问题的方法。在这些方法之外,还有其他从信号处理或人工智能角度解决问题的方法,有兴趣的读者可以自行了解。

参考资料

The post VoIP 中的话音失真问题分析 first appeared on KSkun's Blog.
🔲 ⭐

2020, FUCKOFF

2020 年真的是在进入成年这个阶段以来过的最复杂也最简单的一年,以一种没有任何准备的状态经历了数不清的事情。1 月 1 日的凌晨,我以复杂的心情写下这篇文章,来记录与总结 KSkun 这个人一年中的各种经历和感受,同时作为 KSkun 在这个世界上存在过的痕迹之一。

又由于人类本质原因,这篇文章鸽到了 2021 年 2 月 12 日(农历正月初一)的凌晨才完成,加入了寒假后的部分内容。

谨以此文献给这个世界。

前言/碎碎念

动笔之前,其实发现了自己的一个小习惯,手机里无意识拍下的照片可以组合出彼时彼刻的生活轨迹,因此说不定照片是比这样的文章更好的记录方式。当然,光是照片也是没有用的,还是需要做回忆,并且把彼时彼刻的感受想办法用文字记录下来。

另外就是,看了一遍去年的总结。怎么说呢,在往回看自己的时候,总会觉得以前的自己 too young,很多时候确实存在偏执和激进的成分,包括这篇总结内提到的许多观点。我一直不希望向别人输出自己的观念,因此各位在阅读的时候也就图一乐就好,千万别当真。

尝试着用不流水账的方式写这篇东西,希望效果还行吧。

疫情

其实最开始听到一些风声的时候完全没有放在心上,甚至还大大咧咧地去看了京紫外传的点映场。一个同学非常紧张,打算购入一盒 KN95 口罩,问我要不要合购,于是我顺手分了 5 个,由于买的比较早还没涨价实在是非常幸运,这一批口罩在我从武汉回家的路上提供了保障。

回家之后,生活其实还是照常进行的,购物也好,年饭也好,只是感觉街上的人变少了些。真正觉得事情不对劲还得从封城说起,首先是武汉封城,紧接着家里也不让出门了,所有人都不允许出小区,最多可以下楼。

仍然记得因为老旧小区人手不够,志愿者放我出去采购物品的时候,亲眼看到街上空无一人的样子,真的是非常不可想象的。虽然说是冒着风险出门采购,总比闷在家里看着确认人数单增要稍微轻松一点,这也算是疫情初期紧张情绪的一种调剂吧。

空无一人的街道

在这期间,采购主要是通过网上订购后统一配送到小区或自提点的形式进行的,也有通过志愿者或者政府采购的渠道,因此没法像逛超市那样挑着购买,也因此误打误撞买到了一些平常没有注意的好物。在物资如此受限的情况下搞到了什么好吃的东西带来的快乐甚至是平时的好多倍,有种回到了物资贫乏的时代的味道。也因为这个契机开始跟着家长在家做饭了,做饭还是非常快乐的,只是讨厌洗碗。

疫情在家的这么长一段时间,包括后续可以出门之后的一段时间,真的是非常宝贵与难得的一段时间。也许这是上学以来第一次长时间地在家和家长一起生活,还有机会经常去祖辈串门。现在越来越觉得,时间是非常难得的,能分这么大比例的时间和家人在一起更是非常难得的。说一个人在世界上活着,有一部分是因为他维持着一些特定的人际关系,在其他人的记忆里留下了痕迹;自己的家人与朋友也许会随着时间而不得不减少联系,在关系淡化的情况下,如果记忆里也没有留下痕迹,就非常不妙了。另一方面是,家长确实很希望我长期留在家里与他们一起生活,这也算是满足了他们的一个愿望。

在线函授,成人教育

和疫情伴随的,便是这一个完全在线上开课的学期了。当然了,经过了一个假期,在不在学校的前提下,早起和早睡都是不太可能的事情,于是自然而然地就开始翘早 8,后来扩展到了所有不签到的课。至于学习怎么办,那就随便把作业对着答案搞一下,找个时间把视频挂完算了。一句话总结便是,除了要签到和答题的电路理论课之外,我屁都没学到。

更致命的是,屁都没学到就算了,还习惯了阴间作息和颓废。这种颓废并不是开开心心地打一天游戏,而是在打游戏或者水群的同时还焦虑着、不安着,明明抱有干正事的想法,却不断地逃避着现实。这也许有疫情带来的恐慌的因素,但也同时存在对自己的期望很高与家庭经济问题带来的巨大压力。在一整个线上学期之内,我的表现和对自己的要求存在巨大的落差,且这一点一直都没有被我接受。

后来呢,这种落差开始被我慢慢地接受了,这不是因为我恢复了作息和表现,而是因为我降低了对自己的要求,开始认同自己是一个废物的事实。当然,所谓降低也不是倒向了另一端的降低,而是稍微降低了一点,让自己感觉好受一点,本质上也只是在逃避而已。这个过程中我挣扎过好几次,例如某一次我熬到第二天清晨并且直接当早起,结果快中午就没撑住去睡觉了,又例如之前写的一篇博文,总之最后是以失败告终的。

这一系列的事情让我意识到,我确实是一个没有办法自己激励自己去卷的人,尽管不讨厌目前的课程,但是也没有到能让我自己驱动自己去深入学习的程度。不过,如果要达到对自己的高期望,目前这种状态肯定是不对的,但是至今我也没有找到比较好的办法解决。

总之,这个学期就在各种焦虑和应付中过去了,一事无成

从沉沦中走了出来

从学期开始直至学期结束,我整个人一直都处于上面所说的这种,焦虑着、不安着却没有动力改变现状的状态,但暑假里的各种经历确实短暂地让我从这种状态中走了出来。

第一件事是和 panda 去了一趟成都,在当地群友的带领下广泛地尝试了各种当地特色食物,体验极佳。后来又去了一趟武汉,算是亲眼看到了疫情之后的武汉的样子。由于自己也是湖北人,在山里疫情都有如此严重的程度,其实很难想象武汉的实际情况。但 7 月再去的时候,武汉看起来缓了过来,看上去街上的人都在正常生活,除了随处可见的关张的门面提示着经济的崩溃。这次旅行和朋友同行,一路确实很开心,缓解了一些疫情期间积累起来的压力。

第二件事是冰岩作坊的夏令营,我也是第一次参与这种形式的活动,参与指导了两位同学,并且参与了具体任务的规划。由于夏令营的终极任务我自己也没写过,正好也跟着做了一遍恢复下手感。对于我来说,写代码还是比复习要上头很多,连续写大几个小时也不会分心,很好地说明了写代码是一项能让我有动力驱动自己的事情。同时从长期以来一事无成的失落中走了出来,因为觉得最后写出来的东西还可以。

第三件事是延迟到下学期的期末考试真的要来了,加上家里没有空调太热了,于是被逼着跑去麦当劳自习。刚好那两天可以从麦当劳白嫖一杯中雪碧,再自费 5 元买一杯咖啡麦炫酷,便可以在糖和咖啡因的驱动下去复习期末的 4 门课程,从结果上看成效的确不错,在下学期给了我一点点初始的动力。

从这些事情里找规律的话,可以发现有反馈的事情需要的动力会少一些,例如复习的时候进度如果稳定向前推动就会感觉自己确实有在做事情。总之,如果事情动起来了,坚持下去会稍微轻松一些。

专业选择不慎

下半年的这个学期是以期末考试开始的,归功于暑假高强度补课一个月,总算是平稳度过去了,并且在心态上起了一点正面作用,提供了一点初始动力。因此,最开始的一段时间我依然全勤了所有课。

后来便是一样的套路,开始渐渐变得颓废,失去动力,又沉入了不安地摸鱼的这层困境之中,被 ddl 推着做事。由于这学期所有的课程都对齐到学期初开课,在中期的时候大量结课让 ddl 堆了起来,一度心态爆炸到只想放弃所有的事情,也因此推掉了冰岩的一个锅,也丢掉了不少平时分。又由于许多课程突然签了到,又被一些老师找了麻烦,才又补充了一点动力恢复作息和去上课。由此看来,缺乏动力这个问题真的是很难解决的。

不过在浑浑噩噩的生活里,也有一些好事发生。这学期里也吃了不少好吃的东西,并且尝试着开始在空间里连载美食博主 KS 系列说说。认识了一个在做游戏的学长,并且在学长的建议下蹭到了软件学院万老师的图形学课程,发现自己对图形学的兴趣很大,也因此确定了未来发展的方向,同时也增加了一些压力,因为可能需要读一个硕士研究生。另外就是抽空去考取了业余无线电操作证书,开始了作为 HAM 的活动,包括接收 ISS 下发的 SSTV 图片和借助卫星远距离通信。我觉得这些兴趣与技能的发现确实比较突兀,不知道为什么就去接触了这些事情,觉得很好玩于是就做了下去。此外,这学期对信件和无线电通信的兴趣也说明我比较喜欢仪式感,信件上的邮戳和获得许可、进行合法的通信都可以看成是仪式感的来源。

另外一件事就是,过了一年之后重新来审视转专业和内卷这件事,也有了更多的看法。当初转专业单纯是因为光电和自己的方向不合,想换到一个稍微近一点的,结果就来了电信。其实在体验了三学期的课程之后发现电信和计算机(华科的专业设置)的区别还是相当大的,当初的一种「用电信作为计算机的替代品」的想法是相当不成熟的。作为电信来说,不讨论教学质量的前提下,其课程设计还是比较具有专业色彩的,但是这种专业色彩和计算机专业的也有很大区别。如果希望借助电信作为进入相关行业的替代品的话,也许不能指望从学校的课程中获得什么技能,而是需要自己寻找资源和自主学习吧。好在很早之前就对相关技能有了初步的了解,也通过冰岩作坊等平台走上了正确的方向。

此外,本学期中特别关注的一点事是自己飞速增长的物质欲与消费欲。我不清楚是否是疫情期间许多愿望无法满足的原因,还是获得了一些可支配收入的原因,在整个学期中各种广告与念头对我的吸引力大幅提升了,也因此冲动消费了好几次。不过好在我还是以比较理性的状态面对这件事情的,不至于刷爆花呗到还不起的地步,但也同时需要做其他的努力来填补冲动消费带来的空缺了。目前看来,还好我对自己还是有一点 B 树的,消费都在可以承受的范围内,且欲望没有无限膨胀。

关于未来

在这一年里发生的最重要的事情是一些关于未来的思考与决定。第一件事是跟着学长的推荐去蹭了软工的选修课「计算机图形学」,并且认识了人很好的万琳老师。通过蹭上的几节课,我确认了自己对计算机图形学的兴趣,并且把关于未来的目标确定到了计算机图形学的这个方向上

为什么是图形学?其实回忆我的游戏玩家史,图形学出现的次数不算少。Minecraft 中的 Shader’s Mod(光影 Mod)是一个最直观的例子,它通过自定义着色器极大地提升了 MC 本身的画面质量,也是童年时希望有好显卡这一愿望的来源。后来又参与了 AcademyCraft 的制作,彼时的偶像承担了其大部分的开发工作,其中渲染部分是我完全不理解的高端领域,通过看不懂的复杂操作能够实现超能力设定中的各种酷炫特效。后来那位大佬去了华科,毕业后入职了米哈游,而米哈游的游戏《崩坏 3》与《原神》更是我有限游戏体验中印象非常好的两作。因此图形学即是小时候的愿望,也是追随那位大佬的身影,又是被优秀作品吸引而做出的选择,加上自己确实有兴趣,我认为这是非常充分的理由了。

在有了目标之后,就需要思考如何达到这一目标。目前我的专业和图形学交集甚少,我能接触到的资源主要是冰岩的游戏组、认识的一些在做游戏的同学,与蹭课接触到的万琳老师。又由于本专业本科培养计划颇为丰富,可能需要通过读研来延长时间来深入了解这方面的内容,因此大概是得投身内卷的。至于冰岩作坊,Web 相关的工作也许是分得出来精力完成的,这件事情目前还没有做出决定。另外,寒假计划分一些精力出来学习在线课程 GAMES101 来补充一些基础知识,随后视情况可能会加入万琳老师的组参与一些实际科研或项目的工作吧。

做出读研这一决定,在现在来说似乎有点晚。这主要基于以下几方面的考虑:首先,从目前的成绩看,我似乎还能卷出来一些东西;之后,本科阶段可能确实分不出什么精力完整地补充关于计科、图形与游戏相关的知识,而延长作为学生的时间可以用来做这些工作;再次,其实对参与工作还是有一些恐惧的,毕竟刚经历过上面所描述的那些失落情绪。

依然颓废的寒假

按照寒假前定下的计划,寒假需要分出一部分精力来学习在线课程 GAMES101 与日语,但回家之后这些事情就被我立即抛到了脑后。回家后,作息以极高的速度变得阴间,GAMES101 也渐渐开始看不进去,报名了一个春运志愿者的活动并在这上面花了大量时间,而日语则什么都没动。在寒假这种时期、家里这种环境下,上文提到的动力缺乏问题更显严重。寒假中,我唯一能抱着热情完成的工作只有一些短期的工作,比如作为阅读作为睡前读物的《Head First 设计模式》,以及研究一个关于在线语音声音失真的问题(博文稍后发布)。是否有一种方法来补充确实的动力呢,我希望有一天我能找到。

现在是正月初一,还有半个月左右的寒假,大概是没有办法做更多的事情了。希望能够快乐地过完剩下的寒假,以及按时开学吧。

后记

这篇文章的前半部分是在 1 月 1 日写下的,彼时我还没有太多关于 2020 年的感想,也只是在以记流水账的方式记下了一些事情。那时,我还在为期末考试的科目焦头烂额,更因比较大的压力与关注到自己的问题而缺乏灵感,因此仅仅因为太困这一原因写一半弃坑了。

现在是正月初一的凌晨,在今年拜年纪的单品里挑着看的时候刷到了歌曲《时光盲盒》,引起了一些回忆与感触,因此趁着动力还在赶紧续上了这个大坑。歌词里有这么一段真的很让我共情。

辛苦了
可以哭了 可以笑着说结束了
丢下所有规则 忘记所有挫折
敬自己一杯 因为值得

《时光盲盒》(2020 哔哩哔哩拜年纪单品)

在 2020 年里,先是经历了疫情期间情绪与认识的大起大落,又是在后一个学期里压力过大、被焦虑笼罩,真的发生了太多的事情。虽然也许这些事情都是在自己折磨自己,但是我依然偏执地在意这些,舍不得放下。《原神》里钟离(岩王)选择了放弃神位,是因长年以来的责任过重,而现在他只想放下一切,对自己说一句可以下班了。在这首歌里,又听到了如上的段落与温暖的音乐,更让我与之共情。

尽量有很多事情很重要无法逃避,现在我只想放下这些一会,哭一会。未来会变好的。

KSkun
2021 年 2 月 12 日 于十堰

The post 2020, FUCKOFF first appeared on KSkun's Blog.
🔲 ☆

随机数生成算法与其图形应用

By KSkun, 2020/12

注:由于本文章面向非专业读者,其中的描述可能不够准确。需要获取准确的说明请阅读参考资料等。

什么是随机数

随机数是一种在各种行业中被广泛应用的工具。在密码学中,我们利用随机数生成随机密钥;在图形学中,我们利用随机数进行蒙特卡洛积分,计算渲染的结果;在统计学中,我们利用随机数进行抽样调查,减小统计的工作量。

对于随机数的不同用途,我们对随机数的要求不一,因此随机数也存在着多种定义[1]

  • 统计学伪随机数:指生成的随机数大致均匀分布在其取值空间中。 例如,对于统计学伪随机数比特流而言,其均匀分布即样本中的 1 与 0 的数量大致相同;对于在二维有限空间中生成的统计学伪随机数点而言,图 1-1 是较均匀的分布。
  • 密码学安全伪随机数:指取得随机数样本的一部分,不能轻易计算出样本的剩余部分。 在密码学中,随机数通常用于生成成对或成组的密钥,如果随机数样本可以预测,则获得其中的一部分密钥,其余密钥也有被破解的风险。
  • 随机数:随机样本不可重现。 由量子力学可知,在自然界中存在具有真随机性的现象,但我们很难采集这些随机性;常用的随机性通过物理噪音采集,但也只是接近理想的真随机数。
图 1.1 利用 Sobol 序列生成的二维随机数点[2]

以上三个随机数的条件是逐渐增强的,获得它们的难度也是逐渐增加的。因此我们需要面向随机数的使用场景选择合适的随机数产生方式,以实现安全性与性能的最佳匹配。

噪声也很有用:真随机数的获取与使用

真随机数的获取机理

真随机数生成器(True Random Number Generator,TRNG)用于生成真正的随机数,即不可预测、不可重现的随机数。目前应用中的真随机数大多来自于物理定律保证的随机性,根据原理可以分为量子和经典两种类型。接下来分别简单介绍两种随机性的采集。[3]

量子随机性

量子随机性的来源主要可以分为两类:原子或更小尺度的量子现象,如原子的衰变;或热噪声,如气体分子的碰撞。以下是一些可以被采集的量子随机性源:

  • 散粒噪声(Shot Noise):观测微观粒子时,样本数量足够小时,可以观测到数据产生涨落变化,这种涨落便是散粒噪声。如可以用光电二极管采集光源的光子,由于不确定性原理,光子作用在二极管上会在电路中产生噪声;
  • 衰变辐射源:原子的衰变是随机过程。可以用盖革计数器采集这种衰变的随机性;
  • 晶体管实现的放大电路:在使用晶体管放大信号时,发射极富含电子,这些电子偶尔会穿越势垒从基极离开,可以使用放大电路放大这一随机性并采集。

经典随机性

经典随机性通常来源于热现象,但由于温度越高热现象越剧烈,降低温度可能会减少热随机性。以下是一些可以被采集的热随机性源:

  • 热噪声(Johnson-Nyquist Noise):导体内部的电荷在热运动时产生的电噪声,可以通过放大电路放大并采集;[4]
  • 二极管的击穿:齐纳二极管通常工作在反向击穿区起稳压作用,击穿时也会产生噪声;
  • 大气噪声:环境中存在形式为电磁波的噪声,可以通过一个无线电接收器采集。

Linux 中的真随机数:/dev/random

Linux 中存在两个与随机数相关的虚拟设备:/dev/random/dev/urandom。这两个设备可以输出随机比特流,用户也可以通过输入数据增加其熵池中的熵。两个设备的区别是,当熵池中的熵低到一定程度时,前者会阻塞并等待熵增加,后者则不会阻塞,但可能导致输出的随机性较差。

Linux 的这些设备可以认为是真随机数生成器。其随机性来源自计算机系统运行中的噪声,具体而言,是 IO 操作的时间戳。磁盘、网络以及键盘、鼠标等设备的输入时间戳会被捕捉,并截取其毫秒或微秒部分的数值,这一部分的数值通常具有随机性。[5]

采集到随机性后,系统将其与熵池中的现有熵进行一系列数学组合,增加熵池中的熵。在生成随机数时,系统使用 SHA-1 对整个熵池计算散列值,这个值便是随机数的输出。

统计学工具:准随机数生成器

蒙特卡洛方法往往需要均匀分布在一定空间中的随机数,准随机数生成器(Quasi-Random Number Generator,QRNG)便是用于生成这样一系列随机数的工具。具体而言,常用的准随机数序列包括:Halton 序列、Sobol 序列等。[6][7]这些序列在选取不同参数时,呈现出低相关性,因此可以用于生成随机数。

Van der Corput 序列

我们先给出 Van der Corput 序列的定义:给定一个正整数 $b$ 作为基,对于一个整数 $i$,其可表示为 $b$ 进制数 $i=\sum a_l(i) b^l$,则 Van der Corput 序列可以由正整数序列通过下列变换获得

$$ \mathbf{\Phi}_b(i) = (b^{-1} \ b^{-2} \ \cdots \ b^{-M}) \cdot (a_0(i) \ a_1(i) \ \cdots \ a_{M-1}(i))^{\mathbf{T}} = \sum a_l(b)\cdot b^{-(l+1)} $$

这可以看做将一个数字的 ​ 进制表示镜像翻转到小数点右侧,如下图所示是一个以 2 为基的 Van der Corput 序列的一部分:

图 3-1 部分以 2 为基的 Van der Corput 序列[7]

容易发现,这个序列的每一个点都是取目前最长的未覆盖区域的中点,因此具有平均分布的特性。

Halton 序列与 Hammersley 点集

Halton 序列通过下式生成:

$$ \boldsymbol{X}_i = (\mathbf{\Phi}_{b_1}(i) \ \mathbf{\Phi}_{b_2}(i) \ \cdots \ \mathbf{\Phi}_{b_n}(i)) $$

其中,$b_1, b_2, \dots, b_n$ 取一些互质的质数。由于每一维都是一个 Van der Corput 序列,如此得到的 $n$ 维空间中的一些点在每一维上都是均匀分布的,因此其也具有均匀分布的性质。

而 Hammersley 点集通过下式生成:

$$ \boldsymbol{X}_i = \left(\dfrac{i}{N} \ \mathbf{\Phi}_{b_1}(i) \ \mathbf{\Phi}_{b_2}(i) \ \cdots \ \mathbf{\Phi}_{b_{n-1}}(i)\right) $$

它和 Halton 序列的区别只有第一维是 ​ 上均匀分布的,由于这一区别,其平均分布的性质较 Halton 序列更好。此外,生成 Hammersley 点集需要知道样本点的总数量。下图展示了数量为 100 的二维 Halton 序列和 Hammersley 点集的分布状况:

图 3-2 数量为 100 的二维 Halton 序列(左)[7]
图 3-3 数量为 100 的二维 Hammersley 点集(右)[7]

这些序列的缺点是,当基数选取的较大时,生成的前一些点容易呈线性相关性,如基为 17、19 的 Halton 序列的前 16 项为 $(1/17, 1/19), (2/17, 2/19), \dots, (16/17, 16/19)$。为了避免此问题,通常可以丢弃生成的前一些点,或使用一些手段打乱序列的顺序。打乱顺序并不会影响序列的平均分布性质和无关性。[8]

Sobol 序列

让我们修改一下 Van der Corput 序列的生成方式,在翻转之前先乘以一个生成矩阵 $\mathbf{C}$,即:

$$ \mathbf{\Phi}_{b,\mathbf{C}}(i) = (b^{-1} \ b^{-2} \ \cdots \ b^{-M}) \cdot [\mathbf{C} \cdot (a_0(i) \ a_1(i) \ \cdots \ a_{M-1}(i))^{\mathbf{T}}] $$

而 Sobol 序列则可以通过下式生成:

$$ \boldsymbol{X}_i = (\mathbf{\Phi}_{2,\mathbf{C}_1}(i) \ \mathbf{\Phi}_{2,\mathbf{C}_2}(i) \ \cdots \ \mathbf{\Phi}_{2,\mathbf{C}_n}(i)) $$

即,Sobol 序列的每一维都是以 2 为基的,带不同生成矩阵的类似 Van der Corput 序列。它也具有均匀分布性质,且是对于每一维的 2 的幂次等分区域都会恰好有一个样本点。为了得到分布良好的数列,且避免类似 Halton 序列前几个点出现的线性相关性,生成矩阵需要精心设计,可以在相关资料中获取设计好的生成矩阵。

很快啊,啪一下就生成了:伪随机数生成器

真随机数的获得需要采集物理现象,准随机数的获得可能需要大量运算,生成这些随机数的成本都较高。为了适应需要快速获得随机数的场景,我们可以降低对随机数性质的要求,则伪随机数生成器(Pseudo Random Number Generator,PRNG)就被提了出来。它用于生成近似具有随机数分布的特性,但可能可以通过分析预测的伪随机数,出于性能考量,其算法具有快速计算的特征。[9]

线性同余生成器

线性同余生成器(Linear Congruential Generator,LCG)生成随机数的原理基于一个迭代公式:

$$ X_{n+1}=(aX_n+c) \bmod m $$

这种算法在早期是最常用的随机数生成算法,因为其计算简单,且当时并没有发现更好的方法。例如,C 中的 rand() 函数便是以这种方法实现的。[10]

这一方法产生的随机数周期与参数 $a, c, m$ 值的选取有关,根据取模运算的性质,这一算法生成随机数的周期最多为 $m$,因此存在周期较小的问题。[11]

梅森旋转算法(梅森转转转)

梅森旋转算法(Mersenne Twister,MT)是一种于 1997 年新发明的伪随机数生成算法。该算法的运算非常适合计算,且周期达到了 $2^{19937}-1$ 规模,这个数字被称作梅森质数(Mersenne Prime),这也是该算法名的由来。[13]

为了清晰地分析 MT 算法的流程,我们先给出一个伪随机数生成器的抽象工作流程:

图 4-1 伪随机数生成器的抽象工作流程[12]

如图所示,我们先用种子(seed)初始化一个初始状态,此时可以通过一个生成函数从状态中生成随机数,生成后通过一个转换函数将当前状态转换成下一个状态。在 MT 中,生成随机数的函数被称为 temper,转换状态的函数被称为 twist。接下来我们按照工作流的先后顺序解释算法各部分的流程[13][15],下面给出了 MT 的全流程示意图,读者也可以结合该图理解。

图 4-2 梅森旋转算法的全流程示意图[14]

初始化

MT 算法的工作区包含 $n$ 个 $w$ 位整数组成的数组 $x_0, x_1, \dots, x_{n-1}$,初始化的工作即是通过种子计算出这些整数的值。下式用于迭代地计算这些整数的值:

$$ x_i=f \cdot {x_{i-1} \oplus [x_{i-1} \gg (w-2)]} + i $$

其中,$\oplus$ 表示按位异或,$\gg$ 表示二进制右移,$\cdot$ 表示常规的整数乘法。通过上式即可获得初始状态的数值,但初始状态不能直接用于生成随机数,必须要经过一次 twist 转换到下一状态。

twist

twist 是 MT 的状态转换函数,其通过下式迭代地计算下一状态的值:

$$ x_{k+n} = x_{k+m} \oplus [(\operatorname{upperbits}x_k || \operatorname{lowerbits}x_{k+1}) \cdot \mathbf{A}] \ (\text{for } k: \ 0\rightarrow n-1)$$

其中,$\operatorname{upperbits}$ 代表该整数的高 $w-r$ 位(低位填 0),$\operatorname{lowerbits}$ 代表该整数的低 $r$ 位(高位填 0),$||$ 代表二进制或,在这里的作用是将高位和低位组合起来,其他记号同上。通过上式计算出的 $x_n, x_{n+1}, \dots, x_{2n-1}$ 即为下一状态的值。

这里的 $\mathbf{A}$ 是一个矩阵,如果将一个 $w$ 位的整数看成是 $w$ 维的向量,则上式中则是令此向量乘上矩阵 $\mathbf{A}$。该矩阵定义如下:

$$ \mathbf{A} = \begin{pmatrix} 0 & \mathbf{I}_{w-1} \\ a_{w-1} & (a_{w-2}, \dots, a_0) \end{pmatrix} $$

则乘上该矩阵的作用等效为下式:

$$ x\mathbf{A} = \begin{cases} x \gg 1, & x_0=0, \\ (x \gg 1) \oplus a, & x_0=1. \end{cases} $$

这一操作的设计将在之后提到。

temper

容易发现,MT 的一个状态包括了 $n$ 个整数 $x_0, x_1, \dots, x_{n-1}$,其中每一个整数都可以用于产生一个随机数,因此一个状态共可以产生 $n$ 个随机数。产生随机数时,我们取出下一个未使用的数字 $x_m$,并进行如下操作:

$$ \begin{cases} y=x\oplus[(x\gg u)\&d] \\ y=y\oplus[(y\ll s)\&b] \\ y=y\oplus [(y\ll t)\& c] \\ z=y\oplus (y\gg l) \end{cases} $$

其中 $u, s, t, b, c, d$ 都是参数,$\ll$ 和 $\gg$ 分别代表二进制左移和右移,$\&$ 代表按位与。操作后产生的 $z$ 即为此次生成的随机数。

线性反馈移位寄存器与 twist

为了解释 MT 算法的核心操作 twist,我们首先要引入一个概念:线性反馈移位寄存器(Linear Feedback Shifting Register,LFSR)

学过数电的同学对移位寄存器的概念并不陌生,该寄存器支持存储几个位,并支持将存储的位进行左移或右移,空出来的一位通过外部输入的信号指定。而 LFSR 则是将空出来的一位通过一个反馈函数进行迭代异或来指定的,因此被称作线性反馈。

对于一个 4 位的 LFSR,有反馈函数 $f(x)=x^4+x^2+x+1$ ,则反馈应该通过高 4、2、1 位迭代异或后生成,即 $a_{\text{new}}=a_3\oplus a_1\oplus a_0$。令其初始状态为 1000,则迭代进行向右移位,其状态变化:1000→1100→1110→0111→0011→0001→1000。容易发现此处形成了一个长为 6 的状态环。

对于一个 $w$ 位的 LFSR,其最多有 $2^w$ 种可能的状态,其中全 0 是无效状态,因此有 $2^w-1$ 种有效状态。当反馈函数 $f(x)$ 满足某些条件时,可以让 LFSR 的状态环长度达到最大值 $2^w-1$。

图 4-3 MT 看做 LSFR 的示意图[15]

让我们回头来看 twist 的流程,它包含一个迭代进行的递推式,如果将 $x_{k+n}$ 看做 $x_{k-1}$,则可以认为 $x_{k+n}$ 便是其中的反馈位,该反馈位通过 $x_k$ 的高位和 $x_{k+1}$ 的低位拼接,再与 $x_{k+m}$ 进行迭代异或得到。因此 MT 可以看做一个 $nw-r$ 位的线性反馈移位寄存器,一次 twist 本质上在做 $w$ 次原子化的反馈移位。根据上面我们得到的结论,MT 的周期最大可达 $2^{nw-r}-1$,一组精心设计的参数可以令其达到梅森质数的周期大小,作为参考,MT19937 的参数为:

图 4-4 MT19937 的参数[13]

基于上述过程,容易发现,MT 的运算大多为简单的加、乘与位运算,对于 CPU 而言这些运算比取模、浮点数与矩阵更快,因此 MT 是高效率的。MT 的周期高达 $2^{19937}-1$,在一般应用中可以不用考虑其周期问题,因此 MT 是性质好的。这就是为什么现在主流的随机数算法都采用了 MT19937,其中包括 C++11 中引入的 std::mt19937

一个 MT19937 的 C++ 实现可以参见参考资料的 [16]。

Xorshift 随机数生成器

2003 年发明的 Xorshift 随机数生成器系列也基于 LSFR,但并没有 MT 中那么复杂的规则。它通过多次异或移位后的状态来生成随机数,移位的规则需要精心构造才能保证随机数的性质。

例如,下面是一个最简单的 32 位 Xorshift 随机数生成器的 C 源代码[17]

#include <stdint.h>
​
struct xorshift32_state {
  uint32_t a;
};
​
/* The state word must be initialized to non-zero */
uint32_t xorshift32(struct xorshift32_state *state)
{
    /* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */
    uint32_t x = state->a;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    return state->a = x;
}

图形学中的随机数:全局光照渲染

随机数的一个应用便是在图形学中广泛存在的蒙特卡洛方法。蒙特卡洛方法通过随机采样对函数的积分值进行估计,从而快速计算出一些不便计算的积分值,且估计的过程可以并行化,便于在 GPU 上计算。

在这一节中将介绍全局光照渲染的光线追踪方法,来展现随机数与蒙特卡洛方法在图形学中的应用。[18][19]

蒙特卡洛方法

蒙特卡洛方法(Monte Carlo Method)是一种通过随机采样进行大量实验,根据实验结果来计算不易计算的积分结构或得到概率分布等。根据大数定理,蒙特卡洛方法在采样数越大的时候,估计结果越接近真实值。

下面以一个例子来理解蒙特卡洛方法。利用蒙特卡洛方法可以求圆周率 π,我们画一个半径为 1、圆心在原点的圆,并取其右上角在 (1, 1) 的外切正方形,接下来在 $(-1,1)^2$ 空间中随机取一些样本点,利用点到圆心的距离判断是否在圆内。假设总样本数为 $n$,在圆内的样本数为 $m$,则圆与外切正方形的面积之比为 $\dfrac{m}{n}$,而 π 可以根据下式求出:

$$ \pi = \dfrac{m}{n} \cdot 2^2 $$

图 5-1 利用蒙特卡洛方法求 π 的值[18]

全局光照渲染原理

在本例中,我们只介绍较为简单的模型。

图 5-2 环境光渲染示意图[19]

如上图所示,我们想求出观测者观测到给定点 $p$ 的亮度值 $L_o(p, \omega_0)$,其中 $\omega_0$ 为给定点 $p$ 出射到观测者的光线角度。考虑光线入射到 $p$ 点后发生反射,再出射到观测者眼中,则我们可以反着找出哪些入射光可以反射到观测者眼中即可。

给定一个入射角度 $\omega_i$,要找出入射光强 $L_i(p, \omega_i)$ 是简单的,根据光沿直线传播的原理,可以考虑反着求出该光的传播路径,即反着射出一束光,如果路径与某一光源相交,则该光的入射光强可通过光源参数求出。

由于 $p$ 点的反射性质存在漫反射成分,入射角度具有连续的取值区间。假设 $p$ 点自身不发光,则下式可以表示 $p$ 点出射的亮度:

$$ L_o(p,\omega_0) = \int L_i(p,\omega_i)f_r(p,\omega_i,\omega_o) \mathrm{d}S$$

此积分值往往较复杂,在实时渲染中通常采用蒙特卡洛方法快速计算出积分的值。即,从取值区间中取出随机样本,对于每一个角度进行光线追踪,并对所有追踪得到的亮度值求平均叠加。在实际应用中,样本数越多,则积分精度越高,渲染效果越接近理想的物理效果。

GPU 在计算上述流程时,可以并行地生成多组相关度低的随机数作为样本,之后并行地对大量样本进行追踪和计算,随机数生成与蒙特卡洛方法的并行性和高效性在此充分展现出来。

结语

本文的灵感来源为 NVIDIA Developer 上的一篇文章《Efficient Random Number Generation and Application Using CUDA》(参考资料 [18]),该文章启发我学习与研究各种随机数生成算法及其特征,以及文章本身提到的 GPU 上的随机数生成与随机数在图形学中的应用。

随机数是生活中常见的概念,但生活中的随机概念一般基于我们的定性认识。本文尝试从数学与计算机科学的角度研究随机数与其应用,大概介绍了常用的各类随机数与一个随机数在图形学中的应用实例,以期读者能建立对随机数的系统认知。

本文参考了大量网络与文献资料,在此对这些资料的贡献者表示感谢。

参考资料

The post 随机数生成算法与其图形应用 first appeared on KSkun's Blog.
❌