Skip to content

BlockArea 逆向笔记 ​

对 Phigros 4.0.1 客户端中 BlockArea(块)系统的逆向记录。

该系统对应谱面 JSON 的私有扩展字段 blockAreaList(与 judgeLineList 平级), 不属于通用谱面格式。它描述判定屏上若干带生命周期的矩形区域,可被触摸, 并会压低 BGM 低通滤波器。

文档 ​

按「数据结构 → 运行时行为 → 渲染表现」三层组织。三层各由一份文档负责,互不重复。

页面层次内容
data.md数据规格公共符号约定(τ / p / P / H / S)、Chart / BlockArea / 三类事件字段、驱动者 PreviewBlockControl 字段表、全部调参值
behavior.md运行时行为每帧执行顺序、坐标转换、缓动表与查表、变换、阶段判定、命中判定与触摸、音频
states.md状态与行为BlockPhase 五态(+Residual 过渡)、各状态的时间条件、位置/可见、layer、颜色、判定、销毁与一次性协程
composition.md叠加与样式普通/减块 × 六状态的 12 种组合、多块叠加(普通之间累加、普通⊕减块挖洞)、渲染差值与判定奇偶的区别、触摸悬停层
render.md渲染表现图层与相机、屏幕/分辨率影响因素、RT 管线、材质参数、遮罩布局、真实 GLSL 源码与关键算法、发光权重
materials.md材质细节各材质的 _ST(tiling)、贴图导入设置(Wrap/Filter/sRGB)、两套独立位移系统
tex/贴图资源4 张块系统贴图(Block / BlockNoise1 / PointNoise / FD_Noise,从 sharedassets12.assets 导出)
blockAreaList.json块数据样本c9s 剧情谱的 blockAreaList(48 块 / 4 减块)——本仓库唯一可从 APK 字节溯源的块语料
code/PreviewBlockControl.decompiled.cs等价 C#PreviewBlockControl(生命周期/阶段/几何/插值/变换/命中/协程)与 TouchBlockBehavior 的反汇编还原 C#
code/IL2CPP 类声明Il2CppDumper 导出的相关 C# 类(BlockRender / PreviewBlockControl / GameInformation.BlockArea / JudgeControl / TouchBlockBehavior 等),字段偏移与 VA 注释即各文档字段表的来源
code/BlockRender.decompiled.cs等价 C#由 ARM64 反汇编还原的渲染核心等价 C#(Start / Update / OnDestroy / CopyReadyTouchParamsToActive / RenderEffects / LateUpdate / GetGlowRingWeight / 低通 / SubtractBlockPostProcessor),未确定处均显式标注
code/PreviewElementUpdateControl.decompiled.cs等价 C#PreviewElementUpdateControl(Awake 视口 / CreateBlockRender / DestroyAndCreateAllBlocks / ClearAllBlocks / GetBlock)的反汇编还原
code/GameInformation_BlockArea.decompiled.cs等价 C#GameInformation.BlockArea.Mirror()(VA 0x1CA32A8,水平镜像语义)的反汇编还原
code/JudgeControl.decompiled.cs等价 C#JudgeControl 块命中部分(IsPositionInsideBlock VA 0x1D22560 / TryGetBlockingBlock VA 0x1D22010)

符号只定义一次

τ / p / P / H / S 的定义在 data.md 符号约定。 另两份文档不再重复定义,直接引用。

提取出的着色器位于 shaders/ —— 9 个 BlockArea 相关文件 / 13 个 program, Unity 编译后的 GLSL ES 3.00。

「124」与「9」的准确口径

  • 124:全游戏提取到的 shader program 数(来自 tools/_extract_all_shaders.py 的提取计数)。
  • 123:这些 program 落盘后的 .glsl 文件数——因存在同名 shader,落盘时相互覆盖。
  • 9 / 13:BlockArea 相关的文件数 / program 数。

tools/_catalog_shaders.py 统计的是落盘文件(123)与其中的 #version 块数, 不是 124;需要 124 这个数请看提取脚本的输出。

置信度标记 ​

本文档的结论分三级,每条结论都应能归入其中之一:

标记含义判定标准
✅ 已验证直接读出有 VA 地址、字段偏移、.rodata 常量值或序列化路径可核验
🔶 推断由证据推导结论合理但依赖对反汇编/渲染语义的解读,无直接字节依据
❓ 未确认存疑待查已知有疑问,或仅有间接线索

各文档中大量结论属于 🔶,尤其在缺少谱面语料与 IL2CPP 交叉验证时 (见下方「本仓库的可验证性限制」)。

已撤回的结论(曾写入文档,后经复核推翻):

曾经的结论实际情况发现方式
着色器源码不可得错误。源码经 LZ4 压缩存于 Shader.m_CompressedBlob,已提取实测解压
BlockArea 时间单位为 T错误。单位为秒;judgeLineList 的事件才用整数 T统计 1884 个块时间值
旋转为顺时针错误。反汇编中 sin/cos 寄存器对应关系被读反,实为标准逆时针手工核算 δ=90° 变换
SubtractBlockBlender 输出 [−1,1]错误。st ∈ {0,1},实际范围 [0,2]推导 v 值域
Ready 阶段为「扫光带」错误。实为 sin 呼吸脉冲读 GLSL
发光权重 Σ = 223.1错误。实为 251.5922实算 Σ i^2.65
PreviewBlockControl 是纯编辑器脚本错误。虽在 ProjectEditor 命名空间下,但实例在关卡场景中且启用场景 sharedassets12.assets pathID 374
ScaleEvent 字段为 stepX/stepY错误。序列化字段名是 scale,stepX/stepY 是形参名dump.cs 偏移 0x24
发光权重的两个 epsilon 取值不明错误。即具名字段 glowWeightFalloff / glowPassWeightThresholdBlockRender 字段表
块的数据不在谱面 JSON 内错误。它在 JSON 顶层 blockAreaList 字段里;准确说法是「不属于通用谱面格式规范」直接读谱面 JSON
相机掩码十六进制值错误。4 个掩码十进制对、十六进制错(如 40960 误写为 0x0000C000),与同表 layer 列矛盾逐位解码核对
_DilateTexelSize 的 .zw 供像素比较错误。.zw 在 9 个 GLSL 中从未被读取;半径靠迭代轮数而非单次 UV 偏移grep 全部 22 处引用
easeType 12 越界读「无影响」错误。越界在构建期写入 E[12][47..49],钳制只作用于运行期查表,这三点会被采样复核 GetEaseWithProgress 钳制作用域
遮罩 .y 范围 0~1错误。实际写入 t.y * v * 10,最大可达 20读 GLSL 写入式
_Shine* 有三个材质共用错误。只有 2 个材质(pathID 5、11)保存了 _Shine*block-params.json
发光权重表中间列错误。(6−p)^2.65 有 4 个值算错(Σ 与权重列正确)实算比对

本仓库的可验证性限制

本仓库只附带 9 个 .glsl、一个 block-params.json、4 张块贴图(tex/)、 一份 APK 可溯源的块样本(blockAreaList.json)与相关类声明 (code/),不含 1058 文件的谱面语料与 libil2cpp.so。因此:

  • 涉及 VA 地址、.rodata 常量、指令序列的结论,只能核对内部一致性,无法独立复现;
  • 统计类结论(1058 / 31 / 26 / 17830 / 879)依赖 Phigros_Extractor 解出的语料, 本文档不附带该语料,也不附带分析脚本,故无法直接复算。

分析脚本未附带

本文档不包含任何分析脚本(原脚本内含作者本机绝对路径,且依赖私有语料)。 文档中出现的 tools/_xxx.py 仅是分析过程的引用,不随本文档提供。

复现顺序 ​

若目标是重新实现该机制,建议按以下顺序推进,每步都可独立验证:

  1. 数据层 — 按 data.md 解析 blockAreaList。 验证:31 张含块谱面能全部读出,字段与本文档一致。 不需要渲染。

  2. 时间轴与阶段 — 实现 behavior.md 生命周期与阶段 的分支,输出 BlockPhase。 验证:对 Petrichor.voidMournfinale 逐帧打印阶段,应与音轨上块的起止吻合。

  3. 几何 — 实现 behavior.md 坐标转换与变换。 验证:transform.localPosition / localScale 与预期矩形一致;隐藏态应落在 x = 1000。

  4. 缓动 — 按 behavior.md 缓动 生成 15×101 表(APK 事实)。 唯一死表是 13(恒 0),14 恒 1;3 / 6 / 9 / 12 是复合表 (降采样 + 压半,含 50…57 断点),其 47~49 三点因构建期越界读无法复现。 ⚠️ 但本仓库预览/复现按社区对称 in-out 实现 3/6/9/12(见 §2.1.1 取舍),不构建该复合表。 验证:easeType 0 与 14 在 ハテ.rNFrums.AT 中的表现应与原版一致。

  5. 触摸 — 实现 behavior.md 命中判定与触摸,注意普通块外扩、减块内缩。 验证:Petrichor.voidMournfinale.IN 的 427 个块逐个验证命中区。

  6. 音频 — BGM 低通 22000 → 1500 Hz,0.25 s 扫频。 验证:按住任一 Active 块时能听到压低。

  7. 渲染 — 最后再按 render.md 搭建 7 相机 / 13 RT 的管线。

    最小链会丢掉位移扰动

    BlockCompose 的第一个 fragment program(读 _NormalBlockRT / _SubtractBlockRT, 带 _DisplaceMap 双 tap 与 _Time.x * _DisplaceSpeed)是 normalBlockRT / subtractBlockRT 的唯一消费者。若只跑 BlockSprite → EdgeMask → GlowMask → BlockCompose → SubtractBlockBlender 这条链而只取 BlockCompose 的第二个 program,画面会完全没有液态扰动效果。

    建议顺序:先跑通第二个 program 确认遮罩合成正确,再补第一个 program 的扰动, 最后处理 ActiveBlock(25 KB,已逐段解析,见 render.md)。

前置条件 ​

  • 目标渲染器需支持 GLES 3.0(shaders/ 下为 GLSL ES 3.00)。 Unity 无法直接加载 GLSL,迁回 ShaderLab 需按 render.md 复现建议 的四条注意事项改写。

    9 个文件里有 4 个不能直接编译

    EdgeMask / GlowMask / BlockCompose / SubtractBlockBlender 在两个 program 之间嵌了二进制记录头,且一个文件含 2 个 program(共 13 个 program)。 使用前需按 program 边界手工切分,详见 render.md 着色器清单。

  • 阶段 1–6 不需要渲染器和 shaders/,纯逻辑即可完成。

数据来源 ​

项值
逆向对象libil2cpp.so (ARM64, IL2CPP metadata v31)
引擎版本Unity 2022.3.62f2(由 UnityFS 头泄漏)
工具Il2CppDumper 6.x(dump.cs)、capstone、UnityPy 1.25.3(必须,见 render.md 警告)
场景assets/bin/Data/data.unity3d → level12 / sharedassets12.assets
谱面 JSON🔶 1058 个难度文件(外部 Phigros_Extractor 语料,未附带;本机 APK 解出的 1032 个 textassets 谱面均不含 blockAreaList)
含 blockAreaList 键🔶 31 个难度文件(其中 26 个非空、5 个是 []),涉及 8 首歌(同上,未附带)
块条目总数🔶 17830(其中减块 879)(同上,未附带)
可从 APK 溯源的块样本blockAreaList.json —— 加密包 b855456f… 解出的 c9s 剧情谱,48 块 / 4 减块
提取着色器全包 124 个 program(落盘 123 个文件),其中块相关 9 文件 / 13 个 program

「31」是难度文件数,不是歌曲数

🔶 外部语料称含块的只有 8 首歌。完整统计与可信度说明见 data.md 官方谱面实测统计。

场景 MonoBehaviour typetree 已被剥离

本构建中 MonoBehaviour 只保留 m_Enabled / m_Name,组件字段无法从场景序列化读出, 必须靠 dump.cs 的 IL2CPP 声明 + ARM64 反汇编。因此各文档中的字段表均标注了偏移量, 且调参值的来源需区分「场景序列化」与「反汇编 + .rodata 默认值」两类 (见 data.md 调参)。

已知未解问题 ​

评审中提出的清单已全部查证闭合(结论见下表)。与 APK 仍有的客观差距只剩: 块语料 blockAreaList(1058 文件)未随文档提供,见上文「数据来源」。

已解决(评审问题 → 结论) ​

评审问题结论
easeType 12 的完整数值部分解决:15 个采样点已重算吻合,但 47~49 因构建期越界读不可复现,见 behavior.md 缓动表采样点对照
GetGlowRingWeight 的 epsilon不是具名字段:实测为 .rodata 常量 0.001(0xC26530) 与 1e-6(0xC26384);glowWeightFalloff 只是作为 falloff 参数传入,glowPassWeightThreshold 用在 RenderEffects 跳过低权轮次。材质 _PassWeight = 0.0249 仅交叉验证公式形式
7 台相机的 layer mask已取得全部实测值(以十进制为准,见 render.md 图层与相机)
材质常量已读出 8 个材质的保存参数(BlockRender 持 7 个 + subtractBlockMaterial 挂在 3 个后处理上),见 block-params.json
RT 的 ping-pong 规则已证实:RenderEffects 每轮用 stp/ext 交换 pingA(0x110)/pingB(0x118),见 render.md LateUpdate
各 RT 的分辨率已解:sceneColorRT = Screen/6,块遮罩 = Screen/8,effectRT/ping = Screen/4(BlockRender.Start VA 0x1D1BC4C),见 render.md RT 尺寸
低通滤波器 Q 值已解:set_Q 从未被调用,恒为 Unity 默认 1.0;按下/放开目标分别为 lowPassCutoffFrequency 与 22000,见 behavior.md 音频
SubtractBlockPostProcessor已解:if (cam.targetTexture == null) Blit(src,dest); else Blit(src,dest,material,targetPass),见 render.md
disabledBlockReadyDuration 用途已解:它是 Ready 窗口宽度(enableTime − 0x5C ≤ τ < enableTime),并非未使用;disabledBlockShowDuration(0x58) 才是 DisabledBlockShow 的颜色淡入时长。见 behavior.md 生命周期
阶段切换实现已解:UpdateBlockActivation 无 if(!visible) return,只在 notInWindow 切 disabledLayer;readyLayer/enabledLayer 由 DisabledBlockReady 协程设置
事件缓动字段已解:MoveEvent/ScaleEvent 有 easeTypeX(0x1C) 与 easeTypeY(0x20) 双缓动,分量各自插值;RotateEvent 用 easeType(0x1C)+rotation(0x20)
GetBlockGeometry 返回(size, center, anchorWorld)(已按元组构造寄存器确认),见 code/PreviewBlockControl.decompiled.cs
膨胀半径如何实现_DilateTexelSize = (1/w, 1/h, w, h) 取自 effectRT,但只用 .xy 做 1-texel 偏移,半径靠迭代轮数;见 render.md
RT ↔ sampler 映射见 render.md RT 字段 ↔ 着色器 sampler 对照
事件起点 / 时间基准p = (progressControl.nowTime − 本事件 time) / (下一事件 time − 本事件 time),easeType 取自本事件,见 behavior.md 查表与进度
每帧执行顺序UpdateBlocksTransform → UpdateBlockActivation → UpdateBlockAnimations → DestroyAfterInterval
isDragging 的作用为真时变换更新整段跳过,块保持编辑器拖好的位置
PreviewBlockControl 是否运行时是。实例在 sharedassets12.assets pathID 374,与 BlockRender 同场景且启用
音频滤波器挂载位置挂在 ProgressControl(字段 0xB8 AudioLowPassFilter,早期误写 0xC0),配 LerpLowPassFilter 协程;既非 AudioSource 也非 AudioMixer 组
预备态 RT 为何无人读取已解释:196/197 各渲染两个 layer,预备态与禁用态合并进同一张 RT
减块如何吃音符已澄清:不吃判定,只是视觉扣除 + alpha 0.1
wasVisible / idlePosition 语义已澄清,见 behavior.md 命中判定与触摸
提取了多少 shader124 个程序;因重名落盘为 123 个文件
事件索引选取规则已解:FindCurrentEventIndex 返回 [-1, Count-2],严格大于比较(相等时继续前移),见 behavior.md 事件插值
位移/缩放/旋转如何套到几何已解:UpdateMovement(originalCenter=geo.center, currentCenter=旋转后中心),返回 旋转后中心 + (插值目标 − 原始几何中心)(+= 增量语义);缩放/旋转经栈上 AnimState 绕事件锚点就地累积;详见 behavior.md 变换
SafeDiv 调用点已解:UpdateScale 中 SafeDiv(next.scale, cur.scale) 求相邻事件的缩放比
UpdateScale / UpdateRotation已解:逐事件把 center 绕锚点按比率缩放/旋转,size = 插值scale × originalSize、rotation = 插值rotation,见 code/PreviewBlockControl.decompiled.cs
RotateAroundAnchor 守卫常量已解:s3 = Mathf.Epsilon(1.401298E-45),阈值 `max(
*ReadyBlockRT 的消费者已解:字段名带 Ready 的 RT 通过 _DisabledNormalBlockRT / _DisabledSubtractBlockRT sampler 被 activeBlockMaterial / blockReadyMaterial 采样(初版「无 GLSL 采样」错误——只是 RT 字段名与 sampler 名不同)
renderer.color 的 RGB已解:全代码仅两处写 renderer.color——UpdateBlockActivation 只改 .a(减块 0.1),DisabledBlockShow 用 (1,1,1,*);故 RGB 恒为 (1,1,1)
块相机投影已解:7 台全 正交、orthographicSize = 5.0(sharedassets12 pathID 195–201)、localScale=1;PreviewElementUpdateControl.Awake 定 screenHeight=2·orthoSize、screenWidth=·aspect ⇒ screenWidth/Height 是世界单位(≈17.78×10)非像素;初版「5.0 必被覆盖 / 1 单位=1 像素」均错误
formatVersion / offset 与块时间轴已解:DestroyAndCreateAllBlocks 把 chart.blockAreaList[i] 直接赋给 blockInfo(不加 offset、不看 formatVersion);块时间与 progressControl.nowTime 比较,而 nowTime = audioTime − levelInformation.offset(ProgressControl 0x90)。故只有关卡级 offset 经时钟整体平移,formatVersion 与块无关
触摸按住的块被销毁时的路径已解:无专门路径。DestroyAfterInterval(0x1D70B10) 仅在 now > max(disableTime, disappearTime) + destroyInterval(5.0) 时 Object.Destroy(gameObject);届时块早已 IsActive=false、不会被 TryGetBlockingBlock 命中。触摸悬停槽由 fingerId 驱动、经 EndTouchBlockFrame 释放,与块生命周期无关;BlockRender.OnDestroy(0x1D1DB54) 移除 CameraEvent.BeforeImageEffects 命令缓冲、cmd.Release()、清 4 台相机 targetTexture(0x20/0x28/0x30/0x50;0x38/0x40/0x48 未清,照抄)、Release 并 Clear totalRT,不碰触摸槽位
BlockRender.Update / PreviewBlockControl.Start已解:Update(0x1D1CEBC) 仅尾调用 UpdateTouchPos();Start(0x1D7060C) 只在 isSubtract==true 时设置 enabledLayer/disabledLayer/readyLayer 三个 layer 名(静态字符串常量,字面量未解出)
GameInformation.BlockArea.Mirror()已解(VA 0x1CA32A8,只翻 x):角点 x 交叉取补(tr.x←1−bl.x、bl.x←1−tr.x,y 不变);rotateEvents 的 anchor.x 自身取补且 rotation→−rotation;moveEvents.endPosition.x/scaleEvents.anchor.x 自身取补。闭合此前「未文档化」缺口,见 data.md
PreviewBlockControl.UpdateBlockInfo()已解(VA 0x1D71B40,编辑器回写):由 transform.localPosition/localScale 反算 topRightPercentage/bottomLeftPercentage,再 List.set_Item(index, blockInfo) 写回 levelControl(+0x140 → chart)(+0x20 → blockAreaList)
块的触摸命中测试已解(JudgeControl,code/JudgeControl.decompiled.cs):IsPositionInsideBlock(0x1D22560) 是裸半边 0.5 的局部 AABB(无 inset,lossyScale ≤1e-4 失败);TryGetBlockingBlock(0x1D22010) 按减块命中数奇偶抵消普通块。订正此前文档的「普通外扩 / 减块内缩 inset」——APK 里不存在
DisabledBlockReady / DisabledBlockShow 协程已解:Ready 先切 readyLayer → WaitForSeconds(disabledBlockReadyDuration) → 切 enabledLayer;Show 按 disabledBlockShowDuration 线性淡入颜色(普通 (1,1,1,0)→(1,1,1,1),减块 (1,0,1,0.1)→(1,1,1,0.1))
TouchBlockBehavior.Animation已解:Vector3.Lerp(start,end,Clamp01(t/animationDuration));缩放起/终点 = Vector3.zero(静态单例 0x413DAF0 → 元数据槽 Vector3_TypeInfo 首字段);Hide 收尾时把 position 归到 idlePosition
缓动表是否与游戏一致已由 GetEase.Instantiation(VA 0x1CAE478)反汇编复核:E[t]=u^n、E[t+1]=1-(1-u)^n;t∈{1,4,7,10}、n=t/3+2(2/3/4/5);t+2(即 3/6/9/12)= 复合(降采样+压半)、13=0、14=1。初版误把 3/6/9 当死表。⚠️ 实现按社区对称 in-out(见 §2.1.1 取舍)
Unlit/ActiveBlock 完整解析已逐段读出,见 render.md ActiveBlock:入口水平 discard、_ReadyComposeRT 预备亮度、火花 HSV 色相偏移、触摸 SDF 层、glowAdj / alpha 公式
材质 _ST / 贴图导入设置已读出,见 materials.md。全部 Point 过滤;_ST 各向异性(x ≠ y);active _DisplaceMap (0.8,0.3)、_SparkMap (3.0,1.2)、_NoiseMap (1.5,1.46),compose _DisplaceMap (2.13,1.02);FD_Noise/BlockNoise1 为 Mirror,仅 PointNoise 为 Repeat

一句话结论 ​

  • 行为逻辑:阶段 1–6 已还原并可复现。
  • 画面:着色器源码已获取,遮罩与合成结构完整。
  • 仍缺:Start 三个 layer 名的字符串字面量内容(.so 无 RELA/Android-reloc 未解,字段名已知);其余评审清单已闭合。

提取出的资产版权归 南京鸽游网络有限公司(Pigeon Games)所有,仅供技术研究。