diff --git a/InBox/2026-05-12-structured-output-for-beginners-3.md b/InBox/2026-05-12-structured-output-for-beginners-3.md new file mode 100644 index 0000000..ca9e326 --- /dev/null +++ b/InBox/2026-05-12-structured-output-for-beginners-3.md @@ -0,0 +1,13 @@ +# Structured Output for Beginners (Part 3) + +**URL:** https://pocketflow.substack.com/p/structured-output-for-beginners-3 +**Source:** PocketFlow Substack newsletter +**Saved:** 2026-05-12 + +> ⚠️ 笔记无法直接抓取正文内容(Substack 从当前环境不可访问) + +## 关于该文章 + +这是 PocketFlow 的 "Structured Output for Beginners" 系列的第三部分。PocketFlow 是一个轻量级 AI 工作流框架,该系列文章主要讲解如何利用结构化输出(如 JSON Schema、Pydantic 模型等)来约束 LLM 输出格式,实现更可靠的 AI 应用。 + +请手动查看原文获取完整内容。 diff --git a/InBox/AI-First产研团队的交付路径.md b/InBox/AI-First产研团队的交付路径.md new file mode 100644 index 0000000..e26a2e5 --- /dev/null +++ b/InBox/AI-First产研团队的交付路径.md @@ -0,0 +1,87 @@ +# AI-First 产研团队的交付路径 + +**来源:** [微信公众号 - 昀启AI+](https://mp.weixin.qq.com/s/dNLJagjAoHtiUJj_bc8k0g) +**作者:** 昀启 +**日期:** 2026年4月21日 09:47 +**标签:** #AI-First #上下文工程 #Skill-as-Code #产研协作 #交付路径 + +--- + +## 核心框架 + +- **指标**:AI 初稿准确率 +- **底座**:上下文工程 (L0/L1/L2 三层收敛) +- **载体**:Skill-as-Code — 把团队规范和工作方法硬编码为可版本管理的工程资产 + +--- + +## 初稿准确率是 AI 协作的前置变量 + +> 为什么同样使用模型,不同团队的产出质量会出现显著差异? + +**差异的根源不在模型能力,而在于 AI 所获得的上下文质量。** 上下文充分且精准时,第一版初稿往往已接近可交付标准;上下文残缺或含混时,AI 的产出更像基于有限信息的推测,返工成本随之上升。 + +**核心度量:** AI 生成的第一版初稿,有多大比例可以进入审查流程,或仅经少量调整后即可交付。 + +这个指标直接关联 AI 协作的真实 ROI: +- 初稿准确率高 → 人的精力更多用在审查和决策上 +- 初稿准确率低 → 人的精力持续消耗在返工和修补上 + +很多团队引入 AI 后体感"快了但不稳",本质上就是这个指标波动过大。 + +--- + +## 六段式交付闭环:上下文逐级收敛与同步更新 + +六个阶段的核心逻辑不是线性流程编排,而是**上下文逐级收敛、阶段持续校验、结果最终回写**。 + +| 阶段 | 人的职责 | AI 的职责 | 显性资产(给人的) | 隐性上下文(给下阶段 AI 的) | +|------|----------|-----------|-------------------|---------------------------| +| 业务需求 | 定义目标、边界、优先级 | 起草需求内容 | 需求文档 | 业务意图、边界约束 | +| PRD 设计(write-prd Skill) | 澄清歧义、确认范围 | 生成结构化 PRD | AI 可执行的 PRD | 功能目标、业务规则、验收标准 | +| 技术方案设计(write-tech-design Skill) | 评审架构可行性 | 产出方案初稿 | 技术方案 + API 契约 | 模块边界、接口约定、架构约束 | +| 任务拆解(breakdown-tasks Skill) | 确认优先级与依赖 | 拆解为可执行单元 | WBS 任务列表 | 单任务输入/输出/依赖/验收标准 | +| Daily Coding Agent(编码/单测/CR Skill) | 审核关键逻辑与架构边界 | 编码→单测→CR→修复循环 | 经验证后可交付的代码 | 代码变更记录、新增接口与规则 | +| 上下文更新(update-context Skill) | 审检沉淀结果 | 回写知识与上下文 | 更新后的知识资产 | 下一轮迭代的全量上下文基线 | + +**关键洞察**:注意"隐性上下文"这一列——需求阶段提供业务意图,PRD 阶段将意图转为结构化约束,技术方案阶段将约束收敛为技术决策,任务拆解阶段再将决策压缩为最小执行单元。到 Daily Coding Agent 阶段,AI 拿到的已不是一个模糊需求,而是一份高度收敛的任务上下文。 + +最后一个阶段"上下文更新"负责闭合整个循环:代码变更后上下文同步刷新,确保下一轮迭代时 AI 的输入不会过时。 + +--- + +## 上下文工程:知识库与分层加载 + +### 系统知识库 + +| 业务视角 | 技术视角 | +|----------|----------| +| 功能清单、业务流程、领域知识、状态规则 | 架构文档、数据模型、API 契约、技术规范 | + +两者之间通过**共享术语表**打通,确保 AI 在生成内容时,业务概念与技术实现使用同一套语义。 + +> 知识库的完整度,直接决定了 AI 理解业务的深度,也决定了首稿在业务逻辑层面的可用程度。 + +### 三层加载模型 +- **L0(项目级)**:全局一致性 +- **L1(文件级)**:局部适配 +- **L2(任务级)**:单次任务准确率 + +核心原则:**只在需要的时候给需要的信息。** + +### 上下文工程为什么应被视为组织级投入 +1. 决定了 AI 投入能否产生实际回报 +2. 是团队能力的沉淀载体(人员流动冲击减小) +3. 具备复利效应——正向循环 vs 负向衰减 + +--- + +## Skill 体系:上下文工程的可复用封装 + +Skill-as-Code:把团队的工作方法与质量约束写成可版本管理的文本契约,而不是散落在聊天窗口里的一次性指令。 + +文章中提到的 Skill 包括: +- `write-prd`:生成结构化 PRD +- `write-tech-design`:产出技术方案初稿 +- `breakdown-tasks`:拆解为可执行单元 +- `update-context`:回写知识与上下文 diff --git a/InBox/AutoHarness-论文解读.md b/InBox/AutoHarness-论文解读.md new file mode 100644 index 0000000..856ef17 --- /dev/null +++ b/InBox/AutoHarness-论文解读.md @@ -0,0 +1,215 @@ +# AutoHarness 深度解读 + +> 来源:https://zhuanlan.zhihu.com/p/2016839356833341880 +> 收藏时间:2026-03-25 + +## 一句话总结 + +用小模型(Gemini-2.5-Flash)自动写一段"规则检查代码"包裹在 LLM 外面,让它不再犯"非法操作"的低级错误,结果小模型+代码 > 大模型裸跑。 + +--- + +## 这篇论文到底在解决什么问题? + +### LLM 做 Agent 时的尴尬现实 + +你可能已经知道,现在很多人在用 LLM 做 agent——让模型去完成一些需要"行动"的任务,而不是简单地回答问题。比如让 LLM 下棋、玩游戏、操控机器人等。 + +**问题来了:LLM 经常做出"非法操作"。** + +论文举了一个非常生动的例子:在 Kaggle GameArena 的国际象棋比赛中,Gemini-2.5-Flash 78% 的失败不是因为下棋策略差,而是因为走了不符合规则的棋。比如让马走直线、让兵倒着走之类的。 + +这就好比你请了一个非常聪明的人来帮你下棋,他对棋局分析得头头是道,但就是经常把棋子摆到不合法的位置上。 + +### 为什么会这样? + +LLM 本质上是一个文本生成模型。它"知道"国际象棋的规则(因为训练数据里有大量棋谱),但它没有一个硬编码的规则引擎来确保输出的每一步都合法。它的"知道"是概率性的、模糊的,不是精确的。 + +### 现有解决方案及其问题 + +| 方案 | 怎么做 | 问题 | +|------|--------|------| +| Fine-tuning | 用大量合法游戏轨迹去微调模型 | 成本极高;可能降低模型在其他任务上的能力 | +| 手写 Harness | 人工为每个游戏写一个规则检查器 | 费时费力,每换一个游戏就要重写 | + +**这篇论文的创新点是:让 LLM 自己写这个规则检查器。** + +--- + +## 核心概念:什么是 "Harness"? + +### Harness 的直觉理解 + +"Harness" 这个词在英文里是"挽具/马具"的意思,用来控制和约束马的行为方向。在这篇论文里,Harness 就是包裹在 LLM 外面的一层"安全壳",确保 LLM 输出的动作是合法的。 + +用工程的话说,这就是一个 wrapper / middleware / interceptor: + +``` +传统做法: +用户请求 → LLM → 输出动作(可能非法)→ 环境报错 + +加了 Harness 之后: +用户请求 → LLM → Harness检查 → 合法? → 执行 + ↓ 不合法 + 告诉LLM"这步不行" → LLM重新生成 → 再检查... +``` + +### 三种 Harness 变体 + +论文提出了三种不同"约束力度"的 harness: + +#### ① Harness-as-Action-Verifier(动作验证器)— 论文主要聚焦的方案 + +```python +while True: + action = LLM.generate(observation) # LLM 提出一个动作 + if code_harness.is_legal_action(obs, action): # 代码检查是否合法 + break # 合法就执行 + else: + # 不合法,告诉 LLM 这步不行,让它重新想 + observation += f"\n警告:{action} 是非法操作,请重新选择" +return action +``` + +类比:就像你写代码时 IDE 的实时语法检查——你写了不合法的代码,红线提示你改。 + +#### ② Harness-as-Action-Filter(动作过滤器) + +```python +legal_actions = code_harness.propose_action(obs) # 代码先列出所有合法动作 +best_action = LLM.rank(legal_actions) # LLM 从中选最好的 +return best_action +``` + +类比:就像下拉菜单——用户只能从合法选项中选,不可能输入非法值。 + +#### ③ Harness-as-Policy(代码即策略)— 最激进的方案 + +```python +action = code_harness.propose_action(obs) # 完全由代码决定动作 +return action # 根本不需要 LLM +``` + +类比:你直接写了一个规则引擎/算法来玩游戏,LLM 只在"开发阶段"用来写这个算法。运行时零 LLM 调用,零成本。 + +--- + +## 核心方法:怎么让 LLM 自动写出 Harness? + +这是论文最核心的技术贡献。如果你熟悉 GRPO,你可以把这个过程理解为:用环境反馈来优化代码,而不是优化模型权重。 + +### 整体流程 + +``` +1. 初始化:LLM 写一版 harness 代码(propose_action + is_legal_action) +2. 测试:用这个代码在游戏环境里跑 rollout +3. 评估:记录哪些动作被判为非法,收集错误信息 +4. 反馈:把错误信息喂给 LLM(Critic 模块整理错误) +5. 优化:LLM 基于错误反馈生成改进版代码(Refiner 模块) +6. 重复 2-5,直到合法率达到 100% 或超时 +``` + +### 和 GRPO 的对比 + +| 维度 | GRPO | AutoHarness | +|------|------|-------------| +| 优化的对象 | 模型权重 θ | 代码文本(程序) | +| 搜索空间 | 参数空间(连续) | 程序空间(离散) | +| 反馈信号 | reward(标量) | 执行反馈(错误日志+reward) | +| 优化方法 | 梯度下降 | LLM 当"突变算子"改代码 | +| 探索策略 | 采样多个 response 对比 | Thompson sampling 做 tree search | +| 类比 | 调参让模型更好 | 让 AI 写更好的代码 | + +**关键区别**:GRPO 改的是模型内部的权重,AutoHarness 改的是模型外部的代码。一个改"大脑",一个改"工具"。 + +### Tree Search + Thompson Sampling + +#### 为什么要用 Tree Search? + +简单的迭代优化(写代码 → 测试 → 改代码 → 测试…)有个问题:容易陷入局部最优。比如 LLM 沿着一个思路改了 5 版代码,发现这个方向走不通了,但已经回不去了。 + +Tree search 的思路是:同时维护多个版本的代码,像一棵树一样分叉发展。 + +``` +初始代码 v0 + / \ + v1a v1b ← 两个不同的改进方向 + / \ | +v2a v2b v2c ← 继续分叉 +| +v3a ← 这个方向成功了!合法率100% +``` + +#### Thompson Sampling 是什么? + +你面对这棵树上的多个节点,每次迭代应该选哪个节点来继续优化?这就是经典的 exploration-exploitation(探索-利用)问题: + +- **利用(Exploitation)**:选当前表现最好的代码版本继续改进 +- **探索(Exploration)**:试试那些还没被充分优化的代码版本,也许潜力更大 + +Thompson sampling 是一种概率性的选择策略: + +``` +对每个节点: +1. 根据它历史的"合法率"数据,建一个概率分布(Beta 分布) +2. 从这个分布中随机采样一个值 +3. 选采样值最高的节点来优化 + +效果:表现好的节点被选中的概率更高(利用), + 但表现差的节点也有机会被选中(探索) +``` + +工程类比:这和你做 A/B testing 时的 Multi-Armed Bandit 问题几乎一模一样。Thompson sampling 就是一种 bandit 算法。 + +### Critic 和 Refiner 的分工 + +**Critic(批评者)**: +- 输入:rollout 中失败的步骤(最多 5 个) +- 工作:整理和归纳各种错误类型 +- 输出:结构化的错误摘要 +- 类比:Code Review 时给你提 bug 的同事 + +**Refiner(优化者)**: +- 输入:当前代码 + Critic 的错误摘要 +- 工作:基于反馈生成改进版代码 +- 输出:新版本的 harness 代码 +- 类比:你根据 code review 意见改代码 + +一个关键细节:如果 `is_legal_action()` 返回 True 但环境说动作非法(漏判),则两个函数都要改;如果 `is_legal_action()` 返回 False 且动作确实非法(检查器工作正常,只是 `propose_action` 提出了错误动作),则只改 `propose_action()`。这个区分很重要,避免了"改了不该改的代码"。 + +--- + +## 实验结果解读 + +### 训练效率:多快能学会? + +- 平均 **14.5 次迭代**就能学会(即 LLM 改代码 14.5 次) +- **19/32 个游戏**不到 10 次就搞定了 +- 最难学的游戏:GermanWhist(43次)、Chess(64次)、Othello(62次) + +直觉理解:简单游戏(如猜数字、骰子)规则简单,几次就能写对检查器;复杂游戏(如国际象棋)规则多样(王车易位、吃过路兵等),需要更多轮迭代。 + +最终结果:**全部 145 个游戏都达到了 100% 合法动作率。** + +### 双人游戏:小模型+Harness vs 大模型 + +| 对阵 | 我们的方法胜率 | 对手胜率 | +|------|--------------|----------| +| Flash+Harness vs Gemini-2.5-Pro | **56.3%** | 38.2% | +| Flash+Harness vs Flash(原始) | **64.8%** | — | + +这意味着什么?**一个小模型(Flash)配上自动生成的规则检查代码,可以打败一个大几倍的模型(Pro)。** + +--- + +## 核心启示 + +1. **"代码即策略"可能是 LLM Agent 的终局形态** — 让 LLM 写代码,然后运行时零 LLM 调用 +2. **小模型+好代码 > 大模型裸跑** — 这打破了"模型越大越好"的迷信 +3. **Program Synthesis + RL 的结合** — 这可能是下一代 AI 系统的核心范式 + +--- + +## 标签 + +#论文解读 #AutoHarness #LLM #Agent #ProgramSynthesis #GRPO #强化学习 diff --git a/InBox/CLI-Switch-Agent调用ClaudeCode-Codex的正确姿势.md b/InBox/CLI-Switch-Agent调用ClaudeCode-Codex的正确姿势.md new file mode 100644 index 0000000..43c961c --- /dev/null +++ b/InBox/CLI-Switch-Agent调用ClaudeCode-Codex的正确姿势.md @@ -0,0 +1,91 @@ +# CLI-Switch:Agent 调用 Claude Code / Codex 的正确姿势 + +**来源:** [微信公众号 - WilleAi笔记](https://mp.weixin.qq.com/s/_lFBObJClMx74DuJVegAaw) +**作者:** 上心12138 +**日期:** 2026年5月13日 02:05 +**标签:** #cli-switch #ClaudeCode #Codex #Agent #AI #开发工具 + +--- + +## 从工具箱到手术刀 + +三个月前作者发过一篇文章介绍 cli-switch,定位是"Agent 的 CLI 工具箱"——支持多个编码 Agent,能调很多模型。跑了一段时间后发现:做得太多,反而没有一个做得够好。 + +Claude Code 和 Codex 在编码场景的领先优势越来越明显,其他工具逐渐边缘化。所以整个项目重构了。 + +**v1.0.0** 换成了 TypeScript / Node.js,npm 安装,聚焦只做一件事: +> **让 Agent 正确调用 Claude Code 和 Codex,并知道选对应的什么模型。** + +### 重构带来的关键变化 + +- **更干净**:砍掉了不必要的 Agent 支持,专注 Claude Code + Codex +- **环境隔离**:每次调用起独立子进程,不碰全局配置 +- **策略引擎**:4 种执行策略——single(单步)、write_review(写完审查)、write_test_fix(写完跑测试、不过就修)、high_quality(全程 premium 模型) +- **Skill 系统**:给 Agent 操作手册,自动识别 8 种能力(写代码、代码审查、重构、修 bug、分析、写测试、跑测试、解释代码) + +--- + +## 两个人的电脑(核心痛点) + +Claude Code 只有一份全局配置。Agent 没法单独指定模型,只能用全局配的那个。就像两个人共用一台电脑,谁改了设置另一个都受影响。 + +Codex 也一样。每次调 Codex 要手动设 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL`,用完还得改回来。 + +--- + +## 一行安装,一个 Key 搞定 + +```bash +npm install -g cli-switch +``` + +配一个环境变量: + +```bash +export SWITCH_API_KEY=your-gateway-key +export SWITCH_BASE_URL=https://your-relay.example.com/v1 +``` + +Agent 调 Claude Code 时自动注入为 `ANTHROPIC_API_KEY`,调 Codex 时注入为 `OPENAI_API_KEY`。**不碰全局配置**,通过子进程临时环境变量注入,用完即焚。 + +--- + +## 使用示例 + +**调 Claude Code:** +```bash +cli-switch run "给 src/auth.ts 补单元测试" --agent claude-code +``` + +**调 Codex:** +```bash +cli-switch run "重构 utils 模块" --agent codex +``` + +**写完自动跑测试,不过就修(最多循环 5 次):** +```bash +cli-switch run "实现登录功能" --strategy write_test_fix +``` + +**路由预览,不花 Token:** +```bash +cli-switch run "重构 auth 模块" --dry-run +``` + +--- + +## 对比表 + +| | 之前 | 之后 | +|---|---|---| +| 调 Claude Code | 手动设环境变量,担心改全局配置 | 一条命令,自动注入 | +| 调 Codex | 又得手动设一套 OpenAI 的变量 | 同一条命令,换 `--agent codex` | +| Key 管理 | 中转站 Key 要手动塞给每个工具 | 配一次,两个工具都能用 | +| 模型切换 | 改全局配置,影响自己正在用的 | Agent 用自己的,互不影响 | +| 执行隔离 | 直接在项目里改,改坏了就麻烦 | worktree 或临时副本,随便折腾 | + +--- + +## 与 Hermes Agent 的关联 + +这个工具直接解决了 Hermes Agent(以及类似 AI Agent)在调用 Claude Code / Codex 时的**环境变量冲突**和**配置隔离**问题。Hermes 的 `delegate_task` 工具配合 `acp_command` 参数可以调用 Claude Code 和 Codex,而 cli-switch 进一步简化了 Key 注入和模型选择。 diff --git a/InBox/CLIProxyAPI.md b/InBox/CLIProxyAPI.md new file mode 100644 index 0000000..8213c9f --- /dev/null +++ b/InBox/CLIProxyAPI.md @@ -0,0 +1,29 @@ +# CLIProxyAPI + +**GitHub**: https://github.com/router-for-me/CLIProxyAPI +**添加时间**: 2026-03-24 +**提醒时间**: 2026-03-25 10:00 +**标签**: #代理 #CLI #工具 + +--- + +## 项目简介 + +(待补充 - 明天落实时填写) + +## 主要功能 + +(待补充) + +## 安装使用 + +(待补充) + +## 备注 + +- 用户要求:无风险的情况下落实 +- 提醒已设置:明天(3月25日)10:00 + +--- + +**原始链接**: https://github.com/router-for-me/CLIProxyAPI diff --git a/InBox/Claude-Code-2-1-141-重磅发布-61项更新.md b/InBox/Claude-Code-2-1-141-重磅发布-61项更新.md new file mode 100644 index 0000000..1d31485 --- /dev/null +++ b/InBox/Claude-Code-2-1-141-重磅发布-61项更新.md @@ -0,0 +1,53 @@ +# Claude Code 2.1.141 重磅发布!61项更新,细节体验再次升级 + +> 来源:[微信公众平台](https://mp.weixin.qq.com/s/73PuGdfvSt7j2l-tTigd_g) +> 作者:晓明兄 +> 日期:2026年5月15日 08:04 + +## 本次更新三大亮点 + +### 1. Hook 通知能力大幅提升(最实用!) +- 新增 `terminalSequence` 字段到 Hook JSON 输出 +- 即使没有控制终端(non-TTY),Hook 也能直接发出: + - 桌面通知 + - 窗口标题变更 + - 终端铃声 +- 长任务跑完、重要事件发生时,Claude 终于能"主动叫你" +- 社区用户直呼这是 "sleeper hit"(潜伏杀手级改动),让 Agent 感觉更像真实同事 + +### 2. Rewind 菜单新增 "Summarize up to here" +- 长会话上下文爆炸时,不用手动重启丢失历史 +- 选中位置后一键压缩早期上下文,保留最近对话 +- 完美解决 token 限制痛点,同时不丢失"为什么之前方案失败"的重要信息 + +### 3. Agent View 与后台任务更稳定 +- `claude agents --cwd `:支持按目录过滤会话列表 +- 后台 Agent 完成工作但仍有 shell 时,自动移到 Completed 状态 +- 空闲后台会话 5 分钟后自动清理 +- 权限模式、模型切换等并发问题得到大量修复 + +## 其他重要新增与改进 + +**新增功能:** +- `CLAUDE_CODE_PLUGIN_PREFER_HTTPS`:无 SSH Key 环境也能轻松安装 GitHub Plugin +- `ANTHROPIC_WORKSPACE_ID`:支持 Workload Identity Federation,精确作用域 +- `CLAUDE_CODE_VOICE_FORWARD_INTERIMS_TYPED` 等语音相关环境变量 + +**体验与修复亮点:** +- 长思考 Spinner 10 秒后变 amber 色,提醒你 Claude 还在努力 +- 大量 Windows、插件、MCP、权限对话框、markdown 表格等修复 +- Auto Mode 权限提示更清晰,说明是由哪个 rule 触发 +- Prompt tokens 优化:tools 占比从 43.5% 提升至 47.3% + +## 开发者真实声音 +- "一个小改动,却让长跑 Agent 真正能‘喊’人。" +- "Rewind 压缩功能直接解决上下文爆炸问题,不用再手动重启丢失历史。" + +结合之前的 Agent View + /goal,Claude Code 正在成为一套真正可管、可控、可长时间运行的 AI 工程体系。 + +## 写在最后 +Claude Code 的迭代节奏越来越快。从 Agent View 的全局视野,到 /goal 的目标驱动,再到这次的 Hook 通知和上下文压缩,它正在一步步把"把任务扔给 AI"从理想变成稳定可靠的生产力工具。 + +--- + +#ClaudeCode #AI编程 #开发者工具 #AgenticAI diff --git a/InBox/Cursor 开源团队工作流 Team-kit.md b/InBox/Cursor 开源团队工作流 Team-kit.md new file mode 100644 index 0000000..85d0674 --- /dev/null +++ b/InBox/Cursor 开源团队工作流 Team-kit.md @@ -0,0 +1,108 @@ +# Cursor 开源团队工作流 Team-kit + +> 来源:[微信公众平台](https://mp.weixin.qq.com/s/YT-yD6LcYJhUm5aTQtCsKQ) +> 作者:VibeCoder · Vibe编码 +> 时间:2026年5月7日 07:56 + +--- + +Cursor 官方插件仓库发布了 `cursor-team-kit`(v1.1.0),将 Cursor 团队内部的 CI、Code Review、PR、测试、验证、代码清理、周报等工作流打包成可直接安装的插件。 + +**安装:** `/add-plugin cursor-team-kit` + +**内容构成:** 17 Skills + 1 Sub Agent + 2 Rules + +特点:不依赖 Linear、Jira、Slack、Notion 等第三方服务,靠 Git、GitHub CLI、本地测试、浏览器自动化、终端 harness 等基础能力工作。 + +--- + +## 它是什么 + +manifest 里写得直白:*Internal workflows used by Cursor developers*。 + +官方强调:**plug and play without requiring third-party service integrations**。价值不是把 SaaS 接进 IDE,而是把团队怎么收尾、验证、review 写成 agent 能读懂的操作手册。 + +--- + +## 技术原理 + +目录结构: +``` +cursor-team-kit/ +├── .cursor-plugin/plugin.json +├── agents/ci-watcher.md +├── rules/ +│ ├── no-inline-imports.mdc +│ └── typescript-exhaustive-switch.mdc +└── skills/ + ├── loop-on-ci/ + ├── verify-this/ + ├── control-cli/ + ├── control-ui/ + ├── pr-review-canvas/ + └── ... +``` + +设计哲学:Skills 很多,Sub Agent 很少。只有一个后台 agent `ci-watcher`(盯 PR checks)。其他复杂动作都拆成独立 skill,按需触发更像 checklist。 + +--- + +## verify-this(最有价值的 skill) + +硬性要求: +1. 把用户 claim 改写为**可证伪命题** +2. 采集 baseline 和 treatment 两组 artifact +3. 相同命令、相同数据、相同环境比较 +4. 只允许三种结论:**VERIFIED** | **NOT VERIFIED** | **INCONCLUSIVE** + +直击 agent 写代码最大的毛病——太容易凭感觉宣布完成。这个 skill 可迁移到 Claude Code、Codex、OpenCode 等任何本地 coding agent。 + +--- + +## control-cli & control-ui(执行层) + +- **control-cli**:给交互式 CLI/TUI 搭可重复 harness,用 tmux、PTY、Expect、Node inspector 驱动输入、捕获屏幕、记录 transcript +- **control-ui**:面向 Web/IDE/Electron,用 Playwright/CDP 连接真实页面,截图、读 accessibility tree、抓 console/network、做性能和内存分析 + +说明 Cursor 内部对 agent 的要求已不止读代码和写代码——代码写完后 agent 要真的去操作它。 + +--- + +## PR 被当成阅读体验来设计 + +PR 生命周期工具: +- `new-branch-and-pr` +- `review-and-ship` +- `make-pr-easy-to-review` — 整理 noisy history、改 PR 描述、补风险说明 +- `get-pr-comments` +- `pr-review-canvas` — 将 diff 渲染为交互式 HTML 走读页面,加伪代码、流程图、review checklist + +AI 让代码产出速度变快后,review 压力上升,审查代码也要做信息设计。 + +--- + +## 两条 Rules 暴露代码品味 + +只有两条 always-on rules: +1. **typescript-exhaustive-switch** — TypeScript union/enum switch 做穷尽处理(`never` 兜底) +2. **no-inline-imports** — import 放文件顶部 + +共同指向:代码要容易被静态分析,也要容易被人扫读。 + +--- + +## workflow-from-chats(元技能) + +从最近对话里提取团队偏好:触发条件、工作步骤、质量标准、停止条件、证据和置信度。判断该写成 skill、rule、workflow doc 还是不落地。 + +解决长期问题:人会不断纠正 agent 的行为习惯,但这些纠正如果不显式沉淀成 artifact,就不会累积为能力。 + +--- + +## 总结 + +- **verify-this** 解决了 agent 自证的问题 +- **control-cli/ui** 把 agent 从代码生成推进到操作验证 +- **pr-review-canvas** 承认审查代码需要信息设计 +- **两条 rules** 是品位种子 +- **workflow-from-chats** 解决团队知识沉淀 diff --git a/InBox/Cursor 持续改进智能体框架 - 2026-04-30.md b/InBox/Cursor 持续改进智能体框架 - 2026-04-30.md new file mode 100644 index 0000000..6f29eab --- /dev/null +++ b/InBox/Cursor 持续改进智能体框架 - 2026-04-30.md @@ -0,0 +1,72 @@ +# Cursor: 持续改进我们的智能体框架 (Continually Improving Agent Harness) + +> 原文: [cursor.com/cn/blog/continually-improving-agent-harness](https://cursor.com/cn/blog/continually-improving-agent-harness) +> 作者: Stefan Heule & Jediah Katz · 2026-04-30 + +## 核心思想 + +改进智能体框架 = 愿景驱动 → 提出假设 → 实验验证 → 定量/定性信号迭代。大多数改进不是跃迁式突破,而是执着地叠加一个个小优化。 + +--- + +## 1. 上下文窗口的演进 + +- **早期 (2024末):** 模型自行选择上下文能力弱 → 大量护栏:lint/类型错误反馈、改写文件读取请求、限制单轮工具调用数量、预填大量静态上下文(文件夹布局、语义匹配代码片段、用户附件压缩版) +- **现在:** 上述做法大多淡出。保留少量实用静态上下文(OS、git 状态、当前/最近查看文件)。转向**动态上下文**——模型在工作过程中按需拉取(过往对话、活跃终端会话、相关工具等) + +## 2. 评估框架变更 + +| 方式 | 说明 | +|------|------| +| **离线评估** | 自有评估套件 + 公开基准 [CursorBench](https://cursorbench.com);快速、标准化、可跨时间对比 | +| **在线 A/B 测试** | 同时部署两个+框架变体,在生产环境真实用户中测试 | + +**质量衡量指标:** +- **直接指标:** 延迟、token 效率、工具调用次数、缓存命中率 +- **保持率 (Keep Rate):** 智能体生成的代码变更在固定时间后仍保留在代码库中的比例 +- **语义满意度分析:** 用 LLM 读取用户对智能体输出的回应——用户进入下个功能=成功信号,用户粘贴堆栈追踪=失败信号 + +➤ 案例: 尝试用更贵模型做上下文摘要,改善微乎其微,不值得成本。 + +## 3. 跟踪并修复性能退化 + +**工具调用错误分类:** +- `InvalidArguments` / `UnexpectedEnvironment` — 模型出错、上下文矛盾 +- `ProviderError` — 外部工具服务中断(GenerateImage、WebSearch 等) +- `UserAborted` / `Timeout` — 用户中止或超时 + +**告警策略:** +- 未知错误=缺陷 → 超阈值即告警 +- 预期错误 → 异常检测告警(每个工具×每个模型分别计算基线),显著偏离基线时触发 +- 自动化工单: 每周运行一个特化智能体,搜索日志找出新增/激增问题,在 Linear 创建或更新工单 + +➤ 成果: 一次集中冲刺将意外工具调用错误降低一个数量级,所有工具可靠性达 99%+(很多 99.9%)。 + +## 4. 为不同模型定制框架 + +- 所有框架抽象不依赖具体模型,但可深度定制 +- **工具格式差异:** OpenAI 训练使用 patch 格式编辑文件;Anthropic 习惯字符串替换。用错格式会消耗更多 reasoning token 并产生错误 +- **提示定制:** OpenAI 遵循指令更字面/精确;Claude 更偏直觉,对不精确指令容忍度高 +- **Early Access 调优:** 从最接近的现有框架入手 → 离线评估找出易错点 → 团队成员实际使用反馈 → 迭代直到可发布 +- **"上下文焦虑"(context anxiety):** 一个模型在上下文窗口渐满时拒绝执行任务,通过调整提示缓解 + +## 5. 支持聊天中途切换模型 + +- 切换时自动切换到对应模型的框架(不同提示、不同工具接口) +- 添加自定义指令告诉模型它是"中途接手"对话 +- 缓存是 provider/model 特定的,切换导致缓存未命中 → 尝试用对话摘要缓解 +- 替代方案: 使用**子智能体**(从全新上下文窗口开始),最近支持用户指定模型运行子智能体 + +## 6. 框架与软件开发的未来 + +- **多智能体模式**是方向: 一个负责规划、一个负责快速编辑、一个负责调试,各司其职 +- **框架是关键:** 知道调度哪个智能体、如何描述任务、如何整合结果——这些编排能力体现在框架中,而非单个智能体身上 + +--- + +## 启发 + +> 与 Hermes Agent 的开发哲学高度一致——持续关注动态上下文、工具可靠性、评估方法论、以及为不同模型优化框架。 +--- + +原文链接: [https://cursor.com/cn/blog/continually-improving-agent-harness](https://cursor.com/cn/blog/continually-improving-agent-harness) diff --git a/InBox/GPU的BVH以及排序相关实现心得(史上最简洁GPU原理论述).md b/InBox/GPU的BVH以及排序相关实现心得(史上最简洁GPU原理论述).md new file mode 100644 index 0000000..332704c --- /dev/null +++ b/InBox/GPU的BVH以及排序相关实现心得(史上最简洁GPU原理论述).md @@ -0,0 +1,41 @@ +--- +title: GPU的BVH以及排序相关实现心得(史上最简洁GPU原理论述) +source: 游戏开发技术教程 (微信公众号) +url: https://mp.weixin.qq.com/s/FJxTDw9KdVwZx0RfojdYCg +date: 2026-05-06 +tags: [GPU, BVH, ComputeShader, 碰撞检测, GPU排序] +--- + +# GPU的BVH以及排序相关实现心得(史上最简洁GPU原理论述) + +## 核心内容 + +作者在Unity中实现了GPU版本的BVH(Bounding Volume Tree)碰撞检测,以及双调排序和radix-sort两种GPU排序算法。 + +## GPU原理简述 + +- **GPU架构**:grid → block → warp 三级结构 +- **SIMT**:单指令多线程,一个warp默认32线程 +- **分支发散(divergence)**:同一个warp内的线程必须执行相同指令,不同warp/block的线程可以独立执行不同逻辑分支 +- **Bank Conflict**:同一个warp的两个线程同时访问同一bank的不同地址时,访问会被串行化 +- **内存层次**:全局内存(RWStructuredBuffer)和共享内存(groupshared)— 共享内存访问速度更快,但只能在block内共享 + +## 关键见解 + +1. GPU完全可以做条件分支计算,前提是同一warp内的线程走相同分支 +2. Compute Shader本质上是低级的GPGPU编程语言,能力远不止"shader" +3. BVH每一帧都重新构造以处理运动物体并防止树木退化 +4. 双调排序实现简单,几行shader代码就能搞定 +5. Radix-sort效率更高,包含sweep up(reduce)和sweep down(前缀和)两个过程 + +## 相关链接 + +- 知乎GPU BVH系列:https://zhuanlan.zhihu.com/p/707169090 +- 作者的知乎专栏(游戏开发):https://zhuanlan.zhihu.com/column/c_1861690729694359552 + +## 技术细节 + +- 实现的是2D版本BVH,支持100万量级无明显卡顿 +- 使用instancing drawing渲染大量quad(Unity缺省渲染太卡) +- 作者显卡有19个SM,每个SM支持2048线程,最高并行约4万线程 +- 碰撞结果回传CPU方案:异步+分发器模式 diff --git a/InBox/Hermes Agent 架构分析.md b/InBox/Hermes Agent 架构分析.md new file mode 100644 index 0000000..c895439 --- /dev/null +++ b/InBox/Hermes Agent 架构分析.md @@ -0,0 +1,227 @@ +--- +source: 微信公众号 +url: https://mp.weixin.qq.com/s/Rao9okjfY7gzsHldjRKdDw +title: Hermes Agent 架构分析 +tags: [hermes-agent, architecture, AI-agent] +--- + +# Hermes Agent 架构分析 + +## 系统全景 +Hermes Agent 是一个**多平台、可扩展的 AI Agent 框架**,核心组件包括: +- **核心 Agent 引擎** — AIAgent 类(run_agent.py) +- **工具系统与注册中心** — registry 架构 +- **网关与多平台适配** — 20+ 平台适配器 +- **配置与状态管理** — 双轨配置 + SQLite 会话存储 +- **安全模型** — 多层审批 + 纵深防御 + +--- + +## 核心 Agent 引擎 + +### AIAgent 类设计 +采用**大类单文件设计**(run_agent.py),所有核心逻辑汇聚于一个文件中。 + +**60+ 构造器参数**按语义分区: +- 模型配置 +- 工具控制(enabled/disabled_toolsets) +- 行为控制(quiet_mode, save_trajectories) +- 上下文标识(platform, session_id) +- 回调注入(clarify, approval, sudo, progress callbacks) + +这是一种**配置注入(Configuration Injection)**模式。 + +### 双接口设计 +```python +def chat(self, message: str) -> str: + """简单接口 — 返回最终响应字符串""" + +def run_conversation(self, ...) -> dict: + """完整接口 — 返回字典""" +``` +Facade + Full API 双层设计。 + +### 核心循环 +``` +while api_call_count < max_iterations and budget.remaining > 0: + # 1. 预飞检查 — 上下文压缩 + # 2. API 调用 + # 3. 工具调用分支(支持并行执行) + # 4. 终止条件 +``` + +**关键设计决策:** +| 权衡 | 选择 | 理由 | +|------|------|------| +| 同步 vs 异步 | 同步循环 | 可预测性、调试简单 | +| 迭代控制 | 双重限制 | 线程安全预算管理 | +| 上下文压缩 | 触发式 | 仅在 token 上限时启动 | + +### IterationBudget — 线程安全预算管理 +使用 threading.Lock 实现,解决子代理委派场景下的预算共享问题。 + +### 上下文压缩(5 阶段管线) +1. **工具输出裁剪** — 截断超长返回值 +2. **头部保护** — 锁定系统提示 + 前 N 轮 +3. **Token 预算尾部定位** — 从尾部向前累积 +4. **结构化摘要** — LLM 摘要被截断的中间部分 +5. **工具对消毒** — 确保 tool_use/tool_result 成对出现 + +**防抖机制**:检测连续多次压缩则触发紧急降级。 + +### Prompt 缓存策略 +Anthropic system_and_3 策略:系统提示 + 最后 3 条消息设置 cache_control 断点。 + +### 多 API 模式适配 +支持 4 种 LLM API 协议:OpenAI、Anthropic、Google、OpenRouter,内部统一格式后在 API 调用前动态转换。 + +--- + +## 工具系统与注册中心 + +### Registry 架构(tools/registry.py) +```python +class ToolEntry: + __slots__ = ('name', 'toolset', 'schema', 'handler', 'check_fn', + 'requires_env', 'platform_filter', 'is_mcp') +``` + +**设计亮点:** +- **AST 自动发现** — 扫描 tools/ 目录的 registry.register() 调用,无需手动维护导入列表 +- **MCP 影子保护** — MCP 工具同名时优先内置版本 +- **__slots__ 优化** — 减少内存开销 + +### 工具分层(6 层) +从底层执行环境到顶层 Agent 循环,每一层单向依赖。 + +### 工具集定义 +_HERMES_CORE_TOOLS 列表,用户通过 enabled/disabled_toolsets 精确控制。 + +### 工具调用生命周期(8 步流程) +LLM → tool_calls → model_tools.handle_function_call() → registry.dispatch() → 审批检查 → handler → JSON result → tool_result message + +--- + +## 网关与多平台适配 + +### Gateway 架构 +异步事件循环,20+ 平台适配器并发运行,统一路由到 AIAgent。 + +### 平台适配器模式(Template Method + Strategy) +```python +class BasePlatformAdapter(ABC): + # 4 个必须实现的抽象方法 + async def send_text(self, chat_id, text): ... + async def send_typing(self, chat_id): ... + async def get_display_name(self, user_id): ... + async def start(self): ... + # 10+ 可选覆盖方法 +``` + +支持 20+ 适配器:Telegram、Discord、Slack、WhatsApp、QQ、Signal、HomeAssistant、WeChat(元宝)等。 + +### 会话管理 +- 双写持久化:SQLite(主)+ JSONL(向后兼容) +- 重置策略:idle(空闲超时)/ daily(每日定时) + +### Agent 缓存(LRU) +最大 128 个 Agent 实例的资源感知缓存。 + +--- + +## 数据流转 + +### 主流程(键盘 → 屏幕) +CLI 入口 → Agent 循环 → LLM API → 工具执行 → 流式渲染 → 会话持久化 + +### 工具调用流细节 +- **Agent 级拦截**:todo_tool、memory_tool 直接处理不进 registry +- **审批流程**:环境豁免 → YOLO 模式 → LLM 智能评估 → 人工审批 + +--- + +## 配置与状态管理 + +### 双轨配置系统 +``` +~/.hermes/ +├── config.yaml # 结构化配置 (YAML) +├── .env # 环境变量 (API keys) +├── skins/ # 自定义皮肤 +├── skills/ # 已安装技能 +└── sessions/ # 会话数据 (SQLite + FTS5) +``` + +### 三套配置加载器 +- load_cli_config() — CLI 交互模式 +- load_config() — 子命令 +- 直接 YAML 加载 — Gateway + +### SessionDB +SQLite + FTS5 全文搜索,WAL 模式读写并发,自动迁移,Jitter 写重试。 + +--- + +## 安全模型 + +### 多层审批体系 +1. 环境检测豁免 → 2. YOLO 模式 → 3. LLM 智能评估 → 4. 人工审批回调 + +### 危险模式检测(30+ 模式) +```python +DANGEROUS_PATTERNS = [ + r"rm\s+(-[rf]+\s+)?/", # 文件系统破坏 + r"curl.*\|\s*(bash|sh)", # 网络风险 + r"sudo\s+", # 权限提升 + # ... +] +``` +Unicode 规范化防绕过(NFKC + 零宽字符移除)。 + +### 凭证保护 +- 环境变量黑名单(工具执行时自动过滤) +- 敏感路径保护(~/.ssh/, ~/.gnupg/ 等) + +### SSRF 防护 +在平台适配器基类中验证 URL,阻止内网/回环地址访问。 + +### 安全设计哲学 +纵深防御(Defense in Depth):最小特权、分层检查、反绕过、环境隔离、凭证隔离。 + +--- + +## 可扩展性体系 + +### 工具扩展(最核心的扩展机制) +添加新工具仅需 2 个文件 + AST 自动发现。零配置。 + +### 技能系统(Skills) +纯文本能力增强(~/.hermes/skills/),通过自然语言描述注入系统提示。 + +### MCP 生态 +Hermes 同时作为 MCP Server 和 MCP Client。 + +### 皮肤系统 +纯数据扩展(YAML),运行时通过 /skin 命令即时切换。 + +--- + +## 设计哲学 + +### 核心原则 +- **实用主义胜于教条主义** — 大文件不如过度拆分 +- **回调注入实现界面无关** — 4 个入口共享同一个 AIAgent +- **分层而非分片** — 6 层清晰分层 +- **安全作为一等公民** — 深度嵌入架构 +- **约定优于配置** — 自动发现、自动加载、自动集成 + +### 主要权衡 +| 权衡 | 选择 | 收益 | +|------|------|------| +| 大文件 vs 小模块 | 大文件 | 核心逻辑内聚 | +| 同步 vs 异步 | 同步 | 可预测性 | +| 单进程 vs 微服务 | 单进程 | 部署简单 | +| AST 发现 vs 显式注册 | 动态发现 | 零配置 | + +### 一句话总结 +> Hermes 是一个以实用主义为导向、以可扩展性为骨架、以安全性为底线的工业级 AI Agent 框架。 diff --git a/InBox/Hermes编排Agent_Web可观测性方案.md b/InBox/Hermes编排Agent_Web可观测性方案.md new file mode 100644 index 0000000..823d9fb --- /dev/null +++ b/InBox/Hermes编排Agent_Web可观测性方案.md @@ -0,0 +1,66 @@ +--- +title: Hermes 编排 Agent + Web 可观测性方案 +date: 2026-04-29 +tags: [Hermes, Agent编排, ttyd, tmux, 可观测性] +--- + +# Hermes 编排 Agent + Web 可观测性方案 + +## 背景 + +Hermes 作为母 Agent 编排不同的子 Agent(Codex、Claude Code 等),但全部在后台运行,无法直观看到每个 Agent 的实时输出和进度。需要将后台 tmux session 暴露到 Web 前端观察。 + +## 方案:ttyd + tmux + +**ttyd**(GitHub 31k+ stars)可以把任意终端程序变成 Web 页面,通过 WebSocket 实时推流。 + +### 架构 + +``` +Hermes (编排大脑) + │ + ├─ tmux session codex1 ─── ttyd :8080 ──► http://localhost:8080 + ├─ tmux session claude1 ─── ttyd :8081 ──► http://localhost:8081 + └─ tmux session codex2 ─── ttyd :8082 ──► http://localhost:8082 +``` + +### 使用方式 + +```bash +# 启动 Agent 的 tmux session +tmux new-session -d -s codex1 -x 140 -y 40 +tmux new-session -d -s claude1 -x 140 -y 40 + +# ttyd 暴露每个 session 到 web +ttyd -p 8080 tmux attach -t codex1 & +ttyd -p 8081 tmux attach -t claude1 & + +# 浏览器打开 +open http://localhost:8080 # 看 Codex +open http://localhost:8081 # 看 Claude Code +``` + +### 类似方案 +- **Wetty / Gotty** — 与 ttyd 类似 +- **LangFuse / AgentOps** — 专业 Agent trace 平台,但偏事后日志分析,非实时终端画面 + +### 文件握手通信 + +Agent 之间通过文件交换上下文: + +``` +/tmp/handoff.json # Codex 输出 → Claude Code 读取 +/tmp/instruction.md # Claude Code 重注入指令 → 拉起新 Codex +/tmp/next_prompt.txt # 下一步指令 +``` + +## 关键结论 + +1. **不要** 让 Agent 之间直接互相调用(失控) +2. **要** 用 Hermes 作为中央编排大脑 +3. **用文件作为 Agent 间通信协议** +4. **ttyd 解决实时观察问题**,浏览器多标签同时查看所有 Agent + +--- + +*来源:Hermes Agent 对话 (2026-04-29)* diff --git a/InBox/OpenFlow-OpenSpec+Superpowers工作流编排器.md b/InBox/OpenFlow-OpenSpec+Superpowers工作流编排器.md new file mode 100644 index 0000000..12d1742 --- /dev/null +++ b/InBox/OpenFlow-OpenSpec+Superpowers工作流编排器.md @@ -0,0 +1,84 @@ +# OpenSpec + Superpowers workflow orchestrator(OpenFlow):连接需求与工程的工作流编排器 + +**来源:** [微信公众号 - 幽人](https://mp.weixin.qq.com/s/8PT7nlj-Jcu8Xa6lwyApYQ) +**作者:** 幽人 +**日期:** 2026年5月15日 11:12 +**标签:** #OpenFlow #OpenSpec #Superpowers #工作流编排 #AI编码 + +--- + +## 核心定位 + +> OpenSpec + Superpowers workflow orchestrator — bridging requirements specs and engineering execution, eliminating the format gap. + +**OpenFlow 是连接需求规格与工程执行的工作流编排器**,消除两者之间的格式鸿沟。它是一个 npm 全局包(`@lininn/openflow`),纯粹作为一个独立的**编排层**,不嵌入 OpenSpec 或 Superpowers 的代码。 + +GitHub: [lininn/openflow](https://github.com/lininn/openflow) + +### 解决的问题 +- **需求模糊**:用户说"做一个贪吃蛇游戏",AI 需要自行猜测技术栈、复杂度、边界条件 +- **规格缺失**:没有结构化的设计文档,代码写到一半发现需求变了 +- **进度不明**:做到哪里了?哪些功能已完成?哪些待验证? +- **验收困难**:如何确认代码实现了设计?设计变更是否同步到代码? + +--- + +## 核心架构 + +- **技术栈**:TypeScript(98.6%),npm 全局包 +- **核心依赖**:OpenSpec(结构化规格生成)+ Superpowers(实现规划与执行) +- **目录结构**:CLI 优先 + 模板驱动 + 平台兼容(.claude/ 技能 + .omc/ 会话存储) + +### 双层依赖检测机制 +**优雅降级策略** — 不强制依赖 OpenSpec 或 Superpowers: +- Init 时检测:检测缺失 → 引导安装,但技能文件仍然生成 +- 运行时检测:Build 阶段发现缺失时自动降级为手动步骤执行 + +核心理念:**让工具先能用,再逐步完善。** + +--- + +## 五阶段工作流 + +| 命令 | 阶段 | 描述 | +|------|------|------| +| `/openflow proposal` | proposal | 轻量需求捕获 — 3-5 个问题快速收敛需求 | +| `/openflow brainstorming` | brainstorming | 深度设计 — 多轮权衡探索 | +| `/openflow spec` | spec | 生成规格 + 自动翻译为 plan-ready.md | +| `/openflow build` | build | 执行实现(调用 Superpowers) | +| `/openflow close` | close | 验证一致性 + 归档 | + +### Proposal 阶段:需求的起点 +用最少的提问(3-5 个),把用户脑子里的需求变成可执行的变更描述: +1. **做什么** — 想实现什么功能/变更? +2. **为什么** — 解决什么问题?给谁用的? +3. **成功标准** — 怎样算做完了?验收条件? +4. **边界** — 什么不在范围内? +5. **现有约束** — 技术栈、兼容性、时间上的限制? + +输出格式:proposal.md + +### Spec 阶段:从 proposal 到可执行规格 +将 proposal 升级为完整的规格文档,包含详细的功能描述、数据结构、API 设计、组件树等。最终产出包括: +- `design.md` — 完整设计文档 +- `specs/` 目录 — 规格细节(包含详细的 spec 和 `plan-ready.md`) +- `tasks.md` — 任务清单(含依赖关系和验收标准) + +### Build 阶段:自动执行实现 +关键概念是**任务沙箱**(task sandbox): +1. 每个 task 从 tasks.md 中被抽取到一个独立的 `.snapshot/` 沙箱 +2. 沙箱内包含:任务描述、相关规格、类/方法骨架、依赖说明 +3. 为每个 task 生成独立的 context(LLM 上下文隔离,避免信息过载) + +### Close 阶段:验证与归档 +- `verify` 子命令:对照 tasks.md 验证每个任务的实现状态和格式一致性 +- `close` 归档:将 changes 目录中的内容归档到当前项目的 `changes/` + +--- + +## 编排 vs 工具绑定 + +OpenFlow 与其他方案的区别: +- **不是**将 Cursor/Claude Code/Codex 等工具与工作流深度绑定 +- **而是**在每个阶段生成描述性的 prompt 和产出,让各阶段的 AI Agent 可以看懂指令并产出对接产物 +- 靠**文件格式**(统一的 markdown 规范)打通各阶段,而不是靠 API 集成 diff --git a/InBox/OpenSpec落地实战-小米岗位内推.md b/InBox/OpenSpec落地实战-小米岗位内推.md new file mode 100644 index 0000000..ee8b965 --- /dev/null +++ b/InBox/OpenSpec落地实战-小米岗位内推.md @@ -0,0 +1,96 @@ +# AI编程新范式:规格驱动编程 OpenSpec 的落地实战 + +**来源:** [微信公众号 - AI软件产品经理](https://mp.weixin.qq.com/s/5CSEUj1R7q3KhaRBkJE-ZA) +**作者:** melong +**日期:** 2026年4月7日 07:00 +**标签:** #OpenSpec #规格驱动编程 #AI编程 #Cursor #落地实战 + +--- + +## OpenSpec 简介 + +> OpenSpec 是一个命令行工具,帮助我们和 AI 助手之间建立规范驱动(spec-driven)的开发流程,强调变更隔离、人类与 AI 的共识和审查闭环,本质上是在构建一种新的"人机协作语言"。 + +GitHub: [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec) + +### 核心思想 +将开发流程拆解为两个清晰的阶段: +1. **明确"要什么"** — 在 `openspec/specs/` 文件夹中定义当前系统的完整规范 +2. **管理"怎么改"** — 在 `openspec/changes/` 文件夹中存放所有变更提案 + +从"边写边改"到"规范先行",非常适合 **1→n 的项目迭代**。 + +### 亮点 +- 变更从提案到落地、全流程规范、闭环管理,每一步可溯源、复查与协同 +- 规范落地后,后续团队成员查阅 specs 文档即可快速了解业务历史和变更细节 + +--- + +## 初始化工作 + +### 1. 安装 OpenSpec +```bash +npm install -g @fission-ai/openspec@latest +# 需要 Node >= 20.19.0 +``` + +### 2. 项目初始化 +```bash +cd your_project +openspec init +``` +选择使用的工具(Cursor、Claude Code、Codex 等),会在项目根目录下生成对应的 skill 文件夹。 + +### 3. 填充项目上下文信息 +在 Cursor 对话框中输入: +``` +Please read openspec/project.md and help me fill it out with details about my project, tech stack, and conventions +``` + +`project.md` 结构包括:Purpose、Tech Stack、Project Conventions(Code Style / Architecture / Testing / Git Workflow)、Domain Context、Important Constraints、External Dependencies。 + +### 4. 生成变更提案 +在 Cursor 中使用 `/openspec-propose` 命令: +``` +/openspec-propose 需求文档地址为https://mi.feishu.cn/wiki/xxx +``` + +每次提案生成一个 Change ID,在 `changes/` 下创建目录,包含: +- **proposal.md** — 提案(Why / What Changes / Impact) +- **design.md** — 技术方案和架构决策 +- **tasks.md** — 任务清单(多阶段 checkbox 任务) +- **specs/xxx/spec.md** — 可测试的需求规格 + +### 5. 审查与验证 +- **重新提案**:不满意时可反复迭代,直到符合预期 +- **反复迭代**:直接在 tasks.md 上修改,而不是新建提案 + +--- + +## 执行落地 + +### 拆分任务为可用交付(walkthrough) +- 将 tasks.md 的每个阶段按最细粒度拆分 +- 让 AI 搞清楚当前阶段全部任务细节 +- 确认前后端各自的任务边界 + +### 增量编码模式 +- 每完成一个 phase,`/review` → `/fix` → `/test` +- 用自然语言描述期望,比问"有没有bug"更有效 +- tips:频繁 `/clear` 清空上下文,避免 token 老化 + +--- + +## 常见报错与解决 + +| 报错 | 原因 | 解决 | +|------|------|------| +| `openspec: command not found` | Node 版本不够或全局安装路径问题 | 用 `npx @fission-ai/openspec` 替代,或升级 Node | +| 图片/head 标签被截断 | Cursor 的响应长度限制 | 分阶段执行,或要求 AI 仅输出关键修改部分 | +| 上下文过长导致幻觉 | 长时间未 `/clear` | 每个 phase 完成后 `/clear`,重新加载上下文 | + +--- + +## 总结 + +> **OpenSpec 的核心价值不在于工具本身,而在于它强制执行的"先想清楚再动手"的工程纪律。** 在 AI 编码能力越来越强的今天,真正的瓶颈已经不是"写不出代码",而是"写不对代码"。OpenSpec 通过规范驱动的方式,把人的判断力放在设计阶段,把 AI 的执行力放在编码阶段,各取所长。 diff --git a/InBox/TMP字体Atlas内存优化实战.md b/InBox/TMP字体Atlas内存优化实战.md new file mode 100644 index 0000000..7e22192 --- /dev/null +++ b/InBox/TMP字体Atlas内存优化实战.md @@ -0,0 +1,51 @@ +--- +title: TMP字体Atlas内存优化实战 +source: https://mp.weixin.qq.com/s/iPSxQvg-riGh66efHlSyLA +author: 侑虎科技 +date: 2026-05-06 +tags: [Unity, TMP, 内存优化, Atlas, UWA] +--- + +# 【厚积薄发】从64MB降至5MB,TMP字体Atlas内存优化实战 + +> 来源:UWA公众号 - 第474篇UWA技术知识分享 + +## 实战案例一:字体Atlas双实例冗余 + RW动态图集内存过高 + +**问题:** GOT Online报告显示,项目游戏字体Atlas出现双实例冗余且开启RW动态图集内存占用过高。 + +**原因:** +- 字体Atlas实例数量为2,通常是因为该字体既在初始包中的初始场景中被引用,又在后续AssetBundle资源中重复引用 +- RW(Read/Write Enabled)开启导致CPU和GPU各存一份,内存翻倍 + +**解决方案:** +1. 将初始场景中使用的字符单独创建对应的小字体,避免初始场景中有大图集和大字体的引用 +2. 预先收集游戏的大部分字符集,将Atlas设置为**静态图集配置**,可减少16MB占用 +3. 不在静态图集中的字符,通过TMP的**Fallback机制**,设定一个小分辨率(如512×512)的动态Atlas进行字符补充 + +## 实战案例二:TMPAsset内嵌Atlas纹理无法压缩 + +**问题:** TMPAsset内嵌Atlas纹理属于子资源,无法单独进行纹理压缩参数配置。 + +**解决方案(Editor脚本改造):** +1. 通过Editor编辑器脚本对该纹理进行独立复制 +2. 将TMPAsset材质球关联替换为复制后的新纹理 +3. 移除原TMPAsset内的内嵌纹理 +4. 剥离后的独立纹理可自由配置压缩格式(ASTC6×6、ASTC8×8等),进一步降低内存开销 +5. 真机测试:压缩后文字美术表现无明显差异 + +## 优化前后对比 + +| 项目 | 内存 | +|------|------| +| 优化前 | **64MB** | +| 优化后合计 | **4.75MB** | +| 初始场景 512×512 静态图集 | 0.25MB | +| 4096×4096 静态图集(ASTC8×8) | 4MB | +| Fallback兜底纹理 512×512(Alpha8) | 0.5MB | + +## 相关资源 +- UWA社区:community.uwa4d.com +- UWA官网:www.uwa4d.com +- UWA学堂:edu.uwa4d.com +- QQ群:793972859 diff --git a/InBox/glue-coding-tmall-ai-practice-2026-03-27.md b/InBox/glue-coding-tmall-ai-practice-2026-03-27.md new file mode 100644 index 0000000..1268379 --- /dev/null +++ b/InBox/glue-coding-tmall-ai-practice-2026-03-27.md @@ -0,0 +1,62 @@ +# 97.9%采纳率,胶水编程:业务需求出码最佳实践【天猫AI Coding实践系列】 + +> 来源:[微信公众平台](https://mp.weixin.qq.com/s/G3aKbzdGUyD2h1aVjvbr2g) +> 作者:天猫品牌行业前端 / 大淘宝技术 +> 日期:2026年3月27日 + +## 摘要 + +本文分享了天猫团队在"胶水编程"场景下的最佳实践,即利用AI高效连接现有业务模块以快速响应需求,实现了高达97.9%的代码采纳率。文章指出,针对业务逻辑组装、接口对接及样板代码填充等"胶水"型任务,通过构建精准的上下文提示策略和标准化的开发流程,能极大发挥AI在理解业务意图和组合代码片段上的优势,显著缩短从需求到上线的周期。 + +--- + +## 核心认知:别让AI写代码,让它抄代码 + +试点业务域半年前采纳率 50%——不是AI写不出代码,而是写出来的代码不可控:组件乱用、规范不守、已知的坑反复踩。 + +**核心认知:** 把团队已有的开发规范、代码模式、领域知识喂给 Agent,让它组装而非创作——这套方法叫"**胶水编程**"。**SPEC 管意图,物料管执行**,两者叠加才是完整的可控编码。 + +## 核心理念:AI不应该"写(SPEC)"代码,而应该"抄(GLUE)"代码 + +中后台业务绝大部分需求以 CRUD 为基础——列表页、表单页、详情页、导入导出,场景高度相似。90%的代码本来就有现成的参照。 + +> **胶水编程不是在限制 AI,而是在顺应它的能力结构——让AI做拟合的事。能抄不写,能连不造,能复用不原创。** +> Agent 的工作不是从零创作,而是从内部物料中组装出新的交付,只在业务差异点写最少量的"胶水代码"——**90%抄,10%写**,胶水只在缝隙处。 + +## AI编码可控性的三个递进层次 + +1. **Vibe Coding** → 解决"能不能用AI写代码"。用自然语言描述需求,AI直接生成代码。产出完全不可控,不可能直接合入生产仓库。 +2. **SPEC Coding** → 解决"AI写的代码对不对"。用结构化的技术方案约束AI行为。但管不到具体实现——Agent知道要写列一页,但不知道团队列表页长什么样。 +3. **Glue Coding** → 解决"AI写的代码像不像我们的"。在SPEC基础上,给Agent三样东西:**开发规范(规矩)、代码模式(骨架)、领域知识(经验)**。产出的代码风格统一、组件正确、CR一次通过。 + +> **Vibe让AI能写代码,SPEC让AI写对代码,Glue让AI写出"你的"代码。** + +## 四层物料体系 + +Agent写代码时背后有四个彼此独立的决策,每一层物料恰好堵一个漏洞: + +| 物料层 | 加载方式 | 技术机制 | 触发时机 | +|--------|----------|----------|----------| +| **开发规范** | 静态注入 | 云端配置→AGENTS.md→注入system prompt | 打开仓库时自动加载 | +| **代码模式** | 静态注入 | 样板间代码文件通过提示词引用注入Agent上下文 | 打开仓库时自动加载 | +| **领域知识** | 动态检索 | Agent通过MCP协议调用Knowledge Server按需检索 | 编码过程中按需触发 | +| **任务规格** | 动态生成 | 开发者选择SPEC模板→填写→生成spec.md作为初始上下文 | 每次需求启动时生成 | + +**为什么开发规范必须静态加载?** 56%的场景中AI根本不会主动调用文档工具;将同样内容直接写入AGENTS.md静态加载后,通过率从53%提升到100%。**关键规则必须始终在场**,不能依赖AI的主动调用。 + +**为什么不能全量塞进去?** Anthropic上下文工程博客:"一个精准的300 token上下文,往往胜过一个混杂的113,000 token上下文。" 上下文越长,安全特性实现反而下降了47%。 + +## 这个转变意味着什么 + +- **投资方向变了**:核心投资不再是prompt工程或等待更强的模型,而是建设内部物料体系(让单次交付可控)和需求规格的持久化管理(让长期迭代可控)。 +- **质量标准变了**:好的AI编码不是"生成了多少代码",而是"原创了多少代码"——在可标准化的部分,**原创越少,说明物料体系越完善**,产出越可控。 +- **团队资产观变了**:已有项目代码是可复用的样板间资产,已完成的需求规格是后续需求的决策上下文——每一次交付都在积累物料和上下文,形成**复利效应**。 + +## 关键设计原则 + +- **开发规范**(AGENTS.md):仓库级别的编码约束,是写代码的底线。定义不可逾越的约束,而非可选的建议。 +- **代码模式**(样板间代码):具体的、经过评审的已有实现文件,作为Agent的"抄写范本"。 +- **领域知识**(Knowledge Server):通过MCP协议提供的内部组件坑点、接口注意事项等长效知识点。 +- **Task Spec**(任务规格):当前需求的完整上下文描述。 + +> 相关阅读:《Spec Coding 不是银弹》(内部文章)——SPEC编码的三个结构性局限:AI缺乏真正的理解能力、规范无法完整描述系统、规范比代码更难维护。 diff --git a/InBox/v2ex_1196441_大模型变笨讨论.md b/InBox/v2ex_1196441_大模型变笨讨论.md new file mode 100644 index 0000000..5003b86 --- /dev/null +++ b/InBox/v2ex_1196441_大模型变笨讨论.md @@ -0,0 +1,36 @@ +# 大家有没有感觉最近大模型变笨了 + +**来源**: https://www.v2ex.com/t/1196441#reply25 +**保存时间**: 2026-03-24 +**标签**: #AI #大模型 #讨论 + +--- + +## 帖子摘要 + +楼主提问:大家有没有感觉最近大模型变笨了? + +主要讨论点: +- 用户感觉 GPT-4、Claude 等大模型最近回复质量下降 +- 有人认为是心理作用或期望值提高 +- 也有人提到可能是模型更新导致的风格变化 +- 讨论涉及多个主流大模型:GPT-4、Claude、Gemini 等 + +--- + +## 关键回复观点 + +1. **心理作用论**: 用多了之后对模型能力边界更清楚,所以感觉"变笨" +2. **模型更新论**: OpenAI 等厂商确实会调整模型,可能影响某些任务的表现 +3. **任务复杂度**: 随着使用深入,提出的问题更难,模型显得力不从心 +4. **对比效应**: 新模型出来后,旧模型相对显得弱了 + +--- + +## 个人思考 + +(待补充) + +--- + +**原始链接**: https://www.v2ex.com/t/1196441#reply25 diff --git a/InBox/zhihu_arknights_endfield_cel_shading.md b/InBox/zhihu_arknights_endfield_cel_shading.md new file mode 100644 index 0000000..7fbf0d0 --- /dev/null +++ b/InBox/zhihu_arknights_endfield_cel_shading.md @@ -0,0 +1,1576 @@ +--- +title: 【unity urp】从零模仿复刻实现自己的终末地人物卡通渲染 - 知乎 +source: https://zhuanlan.zhihu.com/p/2028819446546932894 +created: 2026-05-09 15:44 +tags: [zhihu, article, unity, urp, rendering, cel-shading, anime-shader, arknights] +--- + +# 【unity urp】从零模仿复刻实现自己的终末地人物卡通渲染 - 知乎 + +**来源:** [https://zhuanlan.zhihu.com/p/2028819446546932894](https://zhuanlan.zhihu.com/p/2028819446546932894) + +--- + +闲话 + +瞅了一眼上一期文章的发布时间,没想到已经半年多没有发布新文章了 + +迫于毕业压力必须抽出大量精力和时间去做一些其他方面的研究,因此中途搁置了一段ta渲染的学习 + +好在最近总算是事情告一段落了,来把前面允诺的一些技术整理成教程继续给大伙们写一写 + +这一期是接着前面星铁米哈游卡渲的后续进阶文章,本来我的想法是把极度日式赛璐璐风格的米哈游风格卡渲讲完,紧接着引入少前的pbr风格卡渲效果里承上启下的。但是本人作为一个舟批,在终末地发售之后,第一时间也去拆解研究复刻了一下终末地的渲染。由于也是走的pbr卡渲的风格,不少地方和少前也是异曲同工之妙,于是决定把第二期卡渲换成了终末地渲染效果的复刻实现教程 + +刚好读者也可以把我这篇博客内容与蛋白胨大佬的少前渲染教程进行对比学习,可以在大脑里构建更加完善的人物卡渲方案系统,了解两种不同的pbr技术方案产生的直观画面效果 (但其实本质是少前的复刻有些基础性问题,文章写了大半了不想从头改了hhhh + +那么在具体讲解之前,还是照例说一下我的初衷,和之前写文章一样,我希望读者看了我的文章可以对整个实现有一个全面的了解,最好是可以从零开始跟着实现出一模一样的效果出来 + +因此我的文章会相对较长,并且尽我可能的让新入门的小白们也能对着我的技术文章一点一点的复刻出来,在这个过程学习和成长 + +同样的, 我也会在后面放出对应的代码仓库,但依旧不包含任何的美术资源,只有纯粹的shader,我希望你可以对着完整的shader去学习理解,当然,现在最好的做法可能就是把我的这篇博客和我开源的代码仓库直接扔给code agent帮你分析并且复现出来,体验极致的学习效率 + +本次终末地的卡通渲染复现,并没有参考过多的文章和他人实现,本质是一个我自己的逆向方案研究过程 + +结合某神秘截帧工具 + gemini的神力,尝试着去复现完整的游戏shader,也因此花费了不少时间 + +事先声明,文章里依旧会存在不少与原游戏效果偏差的地方,我也没有办法把所有地方全部研究清楚,在ai的辅助分析下,结合自己已有的经验做出来的复刻效果,最后成品如下,更加细致的展示可以去b站搜索我同名id查找: + + +00:29 + + + + +具体的技术方案总结目录会在下面章节说明,在这里提前说一下我没有去重点实现的一些地方,如果你后续能够在我的基础上把这些补上,那就太好了 +1. 羊毛绒裙边的渲染,尝试按照自己的理解和研究复刻了一下,但仍然没有抓到核心,效果差距较大,从视频里的裙边也可以看到,还有火焰呼吸变化的特效也没有去研究 + +2. 描边的着色逻辑,描边的外扩依旧是平滑,但是着色有一套自己的规律,我解读下来觉得就是正常的pbr卡渲逻辑,但最后出现的效果也和游戏里有差别,应该加了些trick设计 + +3. 多光源着色逻辑没有涉及,笔者也没有对游戏里的多光源效果进行观察分析 + +然后从效果展示里也可以观察到一些着色上和原游戏的差异,可能在着色代码的完整度,灯光配置,后处理选择上都会存在各种各样的区别 + +如果你觉得本篇文章复刻的不是很好,额,那我争取后面再研究的更透彻一点,卡渲目前就准备先研究到这里了 + +闲话就到此结束,最后放一下开源仓库链接,我们正式开讲 + +目录 + +还是老样子,先从目录开始写起,学习一门技术,对整体的把控是非常重要的 + +整篇 + +人物模型资源贴图分析与整理 +衣服 toon base shader +zmd的toon shader特点分析,存在明暗两种着色状态 +不同状态下的主光源变化 +基于三层色阶变化 + 背光补偿的toon漫反射 +基于相机方向的风格化pbr高光 +近似数学函数模拟的IBL高光 +基于菲涅尔的边缘光 + 基于光照的边缘光 +不同材质情况下的一些trick,透明材质和特殊物件材质 +皮肤 skin toon shader +toon base基础上的着色逻辑,去除IBL高光 +基于lut图的统一皮肤暗部颜色获取 +基于菲涅尔的sss透光效果 +脸部 face toon shader +依旧是基于sdf贴图变化的光照实现方式 +toon base基础上的着色逻辑,去除IBL高光,去除正常的边缘光 +表情贴图的trick,脸与脖子衔接处的平滑trick,边缘光定制化trick +眼睛 eye toon shader +基于眼睛特殊法向的漫反射 +基于matcap的眼睛高光 +其余部分采用一个toon base的漫反射着色 +头发 hair toon shader +各项异性的卡通高光实现,基于平滑法向和高光颜色贴图 +toon base基础上的着色逻辑,去除IBL高光,去除正常的高光 +描边 outline shader +基于平滑法向的外扩 +毛绒裙边 fur shader +多层pass实例渲染,基于形状贴图 +基于噪声贴图的火焰特效 +其余各种trick +刘海阴影,基于偏移的刘海模型 +眼部阴影 +后处理 +lut图调色 + bloom + 抗锯齿 + +整个技术架构就是上述目录的部分,后续我也会按照这个流程进行讲述 + +本文终究只是个人的复刻实现而已,很多地方纯属经验猜想,还请进行理解 + +资源分析与整理 + +资源的获取方式和处理流程我在这里不会讲解,还请读者自己解决资源的获取以及规范化模型与贴图资源 + +如果不清楚怎么做,可以翻阅我前面的卡渲或者查阅其他人的文章学习 + +这里我会讲述后续渲染流程中必须需要的一些美术资源以及对应的作用,然后再实现的过程里不会再提及这些贴图,默认你已经清楚存在这些贴图资源 + +首先toon base shader里,你需要保证有albedo,normal,以及一张材质属性贴图,以小羊的衣服部位举例 + +材质属性贴图范例 + +其中r通道是metallic,g是reflectivity,b是ao,w是smothness,粗糙度记得反向 + +记得确保albedo的w通道是存在的,关系到剔除和半透材质的实现 + +同时,每个部位都存在自己的ramp梯度图,小羊的人物着色中,衣服上存在三张不同部位下的ramp颜色图,皮肤和脸部共用一张ramp,头发一张,毛绒一张 + +并没有学米家少前将多张ramp拼接,然后使用id进行读取,因此需要我们手动确认ramp图是否全部获取 + +衣服上的三张ramp图示意 + +记得后续实现的时候ramp图不要给错就行,同时zmd的ramp图的rgb是颜色映射,w通道是NoL映射,得到定制化的光照明暗分布 + +ramp图的w通道 + +然后toon base存在自发光贴图 + +根据部位的不同,存在三张不同的自发光,用于特定部位下的美术效果补充,贴图比较有辨识度,全黑的中间存在颜色就是自发光贴图 + +环境贴图选一张cubemap环境贴图即可,也可以直接拿原游戏里的环境贴图使用,是一张被多次使用的环境贴图,在少前那里我也见到过 + +同时还需要有一张高光颜色映射贴图,与ramp的逻辑不一样,是IBL预积分贴图的采样逻辑,提前预积分好了一张高光brdf映射结果,用来调整特殊物件的F0颜色 + +然后是skin shader,需要有一张lut调色图的存在 + +face shader 上需要一张sdf魔法图和一张区域限制图,里面藏了一些trick设计 + +以及一张四格区分的表情贴图 + +eye shader上需要一张matcap,我觉得能看我这篇博客的读者应该是知道matcap的特点的,也不会漏掉,这里就不贴了 + +hair shader 上需要注意有一张高光颜色映射贴图 + +整个流程中比较特殊的贴图我应该都举例展示了,你在获取资源的时候遇到了这样的贴图记得存储和标记就行 + +还有有一些比如在毛绒shader里会遇到的一些噪声贴图我就不讲解了,比较常见且识别度很高,一眼就知道大概作用是做什么的 + +模型资源上你只需要注意存在一个平滑法向在uv中即可 + +有点类似把一个平滑法向贴图存到了uv上,这个逻辑是我从截帧数据中分析的,也许平滑逻辑并不是这么做的,但我这边测试下来确实是有用的 + +toon base shader + +确保模型贴图资源都大致获取后,就可以来开始正式的实现了 + +特点分析 + +在实现之前,先来对游戏里的效果进行一个分析,涉及到后面所有着色逻辑里的一个关键设置 + +来到武陵最佳光照位置,呆在原地看一会 + +可以看到环境中的主光太阳不是一直不变的,强度会随着时间变化,影响因素可能与天空中的云朵,当前天气等等有关 + +重点观察人物身上的着色变化,整体呈现了一个变暗的趋势,也许你会说,这种变暗效果直接动态调整主光强度不就行了吗? + +下面我们再在一些其他位置观察一下 + +其实实现上是完全可以的,根据天气情况和人物所处的位置去动态调整太阳光的强度,但是一些细节上会存在缺失 + +比如仍然会感觉到主光源方向的强烈分布影响,因为是整体的强度减弱,光照影响的分布范围还是保持一致的 + +但是人物处在一个墙角位置,比如第三张图的时候,理应这种强烈的主光方向的引导效果不会存在,因为人物更多是被周围的次级光源给照亮的 + +比如在这个时候,我们可以让与主光有关的边缘光消失,一些只有在主光直射找到才会产生的效果进行减弱或者去除,所以我们需要区分两个着色状态,决定太阳直射日光,或者说主光源对我们的直接影响 + +之后把这个称之日光直射强度(随便取得,能get到意思就行 + +从视频的展示里,你可以看到两个状态的切换,但一般不会直接拉到最低,根据情况进行选择 + +由于涉及到两个状态,因此我们后面的实现,都需要时刻注意到是否要对两个状态做不同的处理 + +比如toon base shader里的漫反射部分,我们在日光直射强度1的时候是这种计算,强度0的时候走另外一种计算逻辑,可能在这个逻辑里我们去除了一些需要主光源直射才会产生的一些影响,比如把NoL减弱 + +日光直射强度变化影响 + +在大世界里这种场景和光照变化复杂的地方,这种状态的设置应该会比实时调整光源的强度要更加准确,同时细节呈现的更加丰富 + +前置数据处理 + +现在我们来正式进入渲染的实现流程,如标题所示,本文全部实现均在unity urp上,且在蛋白胨大佬的框架下实现,这里首先要对蛋白胨大佬进行致谢,分享了一套优秀且成熟的卡渲管线 + +其中很多代码细节可能在讲解的过程中不会提及,读者最好是对着我分享的完成代码进行对比理解,但我仍然会按照从0到1的搭建实现过程慢慢讲解 + +// 基础贴图资源获取 +float4 mainTex_var = SAMPLE_TEXTURE2D(_AlbedoTex, sampler_AlbedoTex, i.uv0); +float4 ormTex_var = SAMPLE_TEXTURE2D(_OrmTex, sampler_OrmTex, i.uv0); +float4 normalTex_var = SAMPLE_TEXTURE2D(_NormalTex, sampler_NormalTex, i.uv0); +ormTex_var = lerp(float4(0,1,1,0),ormTex_var,_IsNeedOrmTex); + +// 光源属性获取 +float3 mainLightDir = _MainLightPosition; +// 获取xz平面光源dir +float3 mainLightDir_xz = normalize(float3(mainLightDir.x, 6.10351562e-05, mainLightDir.z)); + +// normal处理 +float3x3 tangentTransform = float3x3( i.tangenWS, i.bitangenWS, i.normalWS); // TBN矩阵 +float3 normalTex_processed = UnpackNormalFromTex(normalTex_var, _BumpScale); +float3 normalWS = lerp( i.normalWS ,normalize(mul(normalTex_processed, tangentTransform)), _IsNeedNormalMap); +// 面朝向 +float facing = isFrontFace ? 1.0 : -1.0; +normalWS = normalWS * facing; + +// view dir +float3 viewDir = normalize(_WorldSpaceCameraPos.xyz-i.posWS.xyz); +// 相机forward dir +float3 cameraForward = UNITY_MATRIX_V[2].xyz; +cameraForward = normalize(cameraForward); +viewDir = lerp(viewDir, cameraForward, _ForwardDirStrength); +viewDir = normalize(viewDir); + + +在实现之前,先把需要的一些基础属性全部计算获取,代码比较直观,就是常规的 light dir,normal,viewdir之类的 + +比较特殊的是需要额外获取一个camera forward dir,这个相机朝向会在后面多次利用,这个相机朝向你可以手动输出看一下,依旧和light dir这些dir是一样的,一定是从当前像素指向外边的,注意不是相机指向人物的那个朝向就行 + +光源计算 + +终末地的人物渲染中光源有一个特殊trick设计,我们可以认为他给每个人物的头顶额外设置了一盏顶光 + +我们把主光,太阳光定义为mainLight,然后这个顶光效果我们定义为otherLight + +如果你想在游戏里观察顶光,小羊正好是一个比较好观察的对象 + +可以看到游戏中存在一个明显的从上往下照才能出现的光照分布效果,但是你观察地上的阴影,你就会发觉主光一定是在人物前面左方的位置 + +主光mainLight的光照分布大致是这样 + +从分布上可以看到明显的区别,在明确光源方案后,我们来实现光源相关的计算出来 + +前面获取了mainLight的light dir,这里把强度和color,以及光照分布NoL获取一下 + +// 获取光源颜色 + float3 mainLightColor = _MainLightColor; + float mainLightIntensity = max(0.001, (0.299 * mainLightColor.r + 0.587 * mainLightColor.g + 0.114 * mainLightColor.b)); + mainLightColor = mainLightColor / mainLightIntensity; +float NoL = dot(normalWS, mainLightDir); + + +NoL大致效果如下 + +主光的属性部分这里计算完毕,剩下把顶光 otherLight获取一下 + +// 头顶补光光源计算 + float3 otherLightDir = float3(0,1,0); + float otherLightNoL = dot(otherLightDir, normalWS); + otherLightNoL = saturate(otherLightNoL + _OtherLightOffset); + otherLightNoL = otherLightNoL * _OtherLightStrength + _OtherLightStrength_Offset; +float3 otherLightColor = _OtherLightColor; +float3 otherLightResult = otherLightColor * otherLightNoL; + + +由于是顶光,dir就是0 1 0,然后由于是一个主观上的光源设计,因此我们需要对NoL的假如手动调节的效果,color颜色依旧是自定义的参数效果 + +得到的顶光NoL分布如下 + +这里我按照原游戏的效果调节了一下整体分布强度,比较偏向NoL*0.5+0.5的分布,二次元风格里不需要那么强硬的光照分布,因此这里需要手动调整一下,前面mainLight的NoL后面的计算过程中也会经过相似的处理 + +代码里最后把otherLight的result定义成 color x NoL,NoL这部分应该在着色渲染方程中出现,这里提前作用上去是因为mainLight后续的NoL部分需要经过ramp图的处理,会进行一个特殊映射,我会在下一小节讲解到这个otherLightResult为什么这么实现 + +前面我们提到了zmd的着色的一个特点,我们需要针对日光直射强度1和0设计两个不同的状态 + +其中光源上就要进行体现,首先日光直射强度0的时候,主光light部分应该去掉,这是合理的,主光light不产生影响,头顶的顶光区域会随着日光直射强度进行怎样的变化呢?首先1的时候不能太强,本质是一个补充作用,类似电影里的人物打光一样,多一些光的效果可以让人物更加立体好看 + +0的时候,此时主光影响几乎为0,但很显然不能然人物完全暗下去,一个环境光是不够的,我们也不走复杂的全局GI,不如就让顶光占据主体着色效果,最后我们的光源实现如下 + +// 计算日光强度两个临界情况的补光结果 + float3 otherLightResult_day1 = otherLightResult * _OtherLightResultStrength_day1; + float3 otherLightResult_day0 = otherLightResult * _OtherLightResultStrength_day0; +// 主光和补光相融合 得到最终主光源 + float3 mainLightColor_final = lerp( otherLightResult_day0, mainLightColor + otherLightResult_day1 ,_DayStrength); + + +我这里把光源强度分出来了,你把强度融入到color里也是可以的 + +从代码里可以看到,我们给顶光设计了两个状态的强度控制参数,用来自定义调节,然后在日光直射强度1的时候,主光 + 顶光补光的强度抑制结果,0的时候,去除主光,只要顶光的结果 + +也就是我们前面分析的想法,具体的光源效果我们在下一节漫反射里取观察 + +漫反射实现 + +zmd的漫反射实现本质和我前面几篇卡通渲染的文章里讲述的逻辑是一致的,本质是对渲染方程中的brdf进行修改,把NoL的光照分布项转变为颜色上的修改,而不是纯粹的强度变化 + +我们依旧按照传统的渲染方程来看我们的漫反射实现流程 + +light brdf NoL 其余项(shadow ao等) + +现在light部分我们前面计算了,还需要得到剩余三个部分,我们先把单独的shadow和ao计算一下 + +// shadow 获取 + float shadowAttenuation = 1; + float2 screenUV = GetNormalizedScreenSpaceUV(i.pos.xy); + shadowAttenuation = SAMPLE_TEXTURE2D(_ScreenSpaceShadowmapTexture, sampler_PointClamp, screenUV).x; + shadowAttenuation = min(shadowAttenuation, SamplePerObjectScreenSpaceShadowmap(screenUV)); + float shadowScene = (SigmoidSharp(shadowAttenuation, _ShadowCenter, _ShadowSmoothness) + _ShadowOffset) * _ShadowStrength ; + shadowScene = saturate(shadowScene); + + +shadow的计算方式按照读者你已有的shadow方案即可,但最好是逐人物阴影这种高精度的方式,然后做一个手动的sigmod明暗分界控制效果 + +// 初始化材质属性 + float roughness = 1 - ormTex_var.w; + float roughness2 = max(roughness * roughness, 0.0078); + float metallic = ormTex_var.r; + float reflectivity = ormTex_var.g; + float ao = ormTex_var.b; + ao = pow(ao,_AoStrength); + + +获取ao的同时,我们把其他pbr属性也一并获取一下,给ao一个pow的调整效果,用来自定义一下ao的强度 + +后续的一些效果展示里,我没有打开后处理调色,因此你如果直接对比游戏效果可能会有些区别,但从渲染逻辑上讲这样会更好一些 + +接下来我们来处理brdf和NoL部分,其实逻辑依旧是用NoL去采样ramp图,得到漫反射brdf的颜色映射变化 + +但是zmd在ramp图的漫反射brdf映射之上,还加入了传统的卡渲漫反射的方案,基于光照分布的明暗分层,手动给两个自定义的颜色来做明暗变化,可以参考我很久前面的日本toon shader那篇文章 + +我那篇老文章里的传统卡渲做法把物理的光照分布人为控制到非常硬的分层效果,有物理变成了风格化的光照分布。由于zmd的走的pbr结合的卡渲方案,因此分层不会做的很硬,走的真实物理光照分布,只是人为控制明暗上的颜色 + +在这种设计下,物理的光照分布+自定义颜色控制提供了主体的漫反射效果,ramp图更像是一种调色处理,来增加更多的细节质感 + +终末地选择漫反射上去用物理光照分布作为主体效果,所以你在zmd人物衣服身上不太能看到很明显的明暗分界线,显得更加真实 + +和少前的方案实现上是一致的,少前也是以物理光照分布作为主体影响,没有过度去影响整个分布,然后去采样ramp图,只是把ramp图的颜色效果作为主体,zmd里把ramp图的效果作为次要 + +至于是否依赖ramp图,可能与最初的美术方案设计有关,个人感觉zmd这一套在泛用性上会更好,ramp图可以脱离解耦开来,调整起来更方便,但实际上最终效果不会差别太大 + +扯远了,我们回到实现上,明确漫反射的brdf和NoL部分需要结合传统的卡渲分层颜色调整+ramp采样颜色映射 + +我们先来实现传统的卡渲分层颜色调整 + +既然要分层,那么要几层?zmd选择分了三层,那么我们需要三个颜色,分别是亮部,暗部,暗中暗 + +亮部颜色就是直接采样albedo贴图得到的颜色,暗部颜色在亮部颜色基础上给予一个强度衰减,zmd在实现上还给了一个饱和度衰减,暗中暗颜色在暗部颜色基础上再衰减到0.65即可 + +// albedo颜色 明暗处理 +float3 baseColor = mainTex_var.xyz * _BaseColor.xyz; +baseColor = pow(baseColor,_BaseColorPow); // 亮部颜色 + +float3 baseColor_dark = baseColor * _AlbedoDarkStrength; +float baseColor_dark_strength = MyCalculateLightStrength(baseColor_dark); +baseColor_dark = lerp(baseColor_dark_strength.xxx, baseColor_dark, _AlbedoDarkSaturation); // 暗部颜色 + +// 漫反射颜色获取 + float energyDistribution_metallic = 0.96 - 0.96 * metallic; + float3 mainDiffuseColor_Light = baseColor * energyDistribution_metallic; + float3 mainDiffuseColor_Dark = baseColor_dark * energyDistribution_metallic; + +float3 mainDiffuseColor_Dark_attention = mainDiffuseColor_Dark * 0.65 ;// 暗中暗颜色 + + +其中有一个饱和度调节,就是与黑白结果后的颜色做一个lerp + +然后读者回忆一下pbr的逻辑,应该会记得漫反射和高光有能量守恒的准则,因此我们需要根据金属度进行能量分配,得到漫反射color,也就是渲染方程中的brdf的部分 + +最后得到这样的三个颜色 + +三层的颜色有了,在计算每两层之间的权重分布之前,我们先去采样一下ramp图获取下颜色,在资源分析小节里,我有提到ramp图的w通道神秘作用,我们的NoL光照分布项也需要经过ramp图的调整,这样我们就不用学少前一些传统做法去通过sigmod smoothness去重映射范围,因此为了得到光照分布项NoL,我们必须先解决ramp的采样,这里的NoL均是主光的NoL分布,前面把顶光的NoL提前作用到光源上了,因此不需要考虑顶光NoL在brdf后续的影响作用 + +// 采样ramp的NoL计算 + float rampNoL = 0.5 - 0.5 * NoL * NoL; + float NoL_rampFinal = rampNoL * backLight + NoL; // 增加背光补偿 + NoL_rampFinal = max(NoL_rampFinal, -1); + NoL_rampFinal = min(NoL_rampFinal, 1); + NoL_rampFinal = NoL_rampFinal * 0.5 + 0.5; + // 采样ramp + float4 rampColor = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(NoL_rampFinal, 0.5)); + + +zmd的ramp采样就是直接拿物理光照分布采样即可,但是zmd在上面加了光照补偿,一个背光补偿项 + +目的是为了解决二次元角色在完全逆光时,受光面几乎消失、角色显得太暗、轮廓感支离破碎的问题。 + +可以认为是一个风格化的trick设计,在物理之上的风格插入,这是一个很好的选择 + +背光补偿的计算代码如下 + +// 背光判断计算 + float2 cameraForward_xz = normalize(cameraForward.xz); + float backLight = saturate(-dot(cameraForward_xz, mainLightDir_xz.xz)); + // y轴作用 + float backLight_y = saturate(-abs(cameraForward.y) + 0.75); + backLight_y = MySmoothstep(backLight_y); + backLight = backLight * backLight_y; + + +简单解释一下这段背光的逻辑,首先根据相机dir的xz和光源的xz去dot,从xz平面上去判断现在相机是正对光源还是背对光源,然后第二个y轴的作用是当相机俯视和仰视的时候,进行一个限制,比如极端视角下不需要这种补偿效果 + +具体的画面效果读者可以实现了在引擎里查看一下,大致效果如下 + +总结下来就是,先判断水平面是不是逆光,如果是,再看看相机的俯仰角有没有太高或太低。如果角度太极端就掐掉这个背光补偿,但在掐掉的过程中,用moothstep让过渡看起来平滑 + +采样ramp的结果如下,左边是颜色xyz,右边是重映射的w通道NoL + +回到我们的着色逻辑上,我们需要计算我们用来分层的光照分布,现在得到了这个分布NoL,并且在物理之上加入了人为映射修正 + +回到渲染方程 light brdf NoL 其他项(ao shadow),四个部分我们都已经全部先计算出来了 + +在之前的文章里我有提到如何在卡通渲染里理解brdf的特殊性,可以参考 + +这里我简短讲述一下,以漫反射为例,brdf其实就是一个颜色,并且无论光照条件怎么变化,材质的固有色都不会变化,也就是brdf 颜色本身不会变化,但是卡渲的特殊就在于,需要如同画画一样,在光照分布下,产生不同的颜色变化效果,因此brdf会随着光照分布产生变化 + +所以在卡渲的逻辑里,渲染方程中的四个项不再是相乘,NoL + 其他项会去影响brdf,最后得到的一个变化分布的brdf颜色即使后面三项的完整结果 + +我们即将做的三层漫反射着色也是这个原理,我们来得到三层变化下的漫反射brdf,再次之前我们给其他项里补充一个特殊trick,NoF变化 + +// 二次采样ramp 得到边缘明暗变化 + float NoF = dot(normalWS, cameraForward); + NoF = NoF * 0.5 + 0.5; + float rampColor_NoF = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(NoF, 0.5)).w; + + +得到的变化分布是这样的 + +比较接近一种在画画里ao的画法,在各种边缘位置进行一个涂黑过渡来增加立体感,这个部分主要用于暗中暗,具体为什么只用于暗中暗,应该是zmd美术们设计的结果 + +好,现在我们来利用 NoL ao shadow NoF来制作三层之间的分布,首先是明与暗,采用min的方式,我们可以得到边界最明显的分布效果 + +那么对于明与暗这种差距极大的两层,我们应当选用min的方式去组合强度分布 + +float ao_shadow = ao * shadowScene; + float min_shadowEffect = min(ao,shadowScene); + min_shadowEffect = min(min_shadowEffect,rampColor.w); + + +你得到的分布效果应该大致如下 + +然后暗部与暗中暗的强度分布我们采用如下计算方式 + +float ao_shadow_NoFRamp = ao * shadowScene * rampColor_NoF; +saturate(ao_shadow_NoFRamp + rampColor.w) + + +其他项我们选择乘法,然后我们加到NoL的主体上,分布效果大致如下 + +可以看到极大程度压缩了暗部的区域,本质上把暗中暗的部分计算了出来,因为暗中暗需要暗部里面去找到区域,从计算上可以这么理解,只有在NoL的暗部区域,且同时其他项均是暗部,才可以得到真正的暗中暗 + +并且大部分暗中暗都偏向于 ao NoF这种立体塑造(在日式绘画层面上?)因此暗部与暗中暗的分布zmd的实现如上 + +但其实这种两层之间的分布定义不是说一定要这样,只要你在合理的物理分布项上去进行有道理有逻辑的组合就可以了 + +最后来对三层颜色进行lerp混合,得到我们传统卡渲方案下的漫反射brdf结果 + +// 主光漫反射 + // 第一层lerp 暗部之中分出两层 + float3 mainDiffuseColor_Dark_lerp = lerp(mainDiffuseColor_Dark_attention, mainDiffuseColor_Dark , saturate(ao_shadow_NoFRamp + rampColor.w)); + // 第二层lerp 明暗区分 + float3 mainDiffuseBrdf = lerp(mainDiffuseColor_Dark_lerp, mainDiffuseColor_Light, min_shadowEffect); + + +然后我们需要把ramp图的颜色映射加上去,直接把rampColor乘上去即可,和正常的ramp使用方式是一样的 + +float3 mainDiffuseBrdf_rampColor = mainDiffuseBrdf * rampColor; + + +然而,我在研究的时候,发现zmd实际上没有这么做,在ramp color上额外做了一个纯色相影响的操作 + +需要修改一下代码 + +// rampColor xyz差异,得到一个饱和度分配结果 + float rampColor_max = max(max(rampColor.x, rampColor.y), rampColor.z); + float rampColor_min = min(min(rampColor.x, rampColor.y), rampColor.z); + float rampColor_xyzStrength = rampColor_max - rampColor_min; + +// 作用rampColor影响 + // xyz通道影响 + float3 rampColor_xyzEffect = rampColor * rampColor_xyzStrength + 1 - rampColor_xyzStrength; + float3 mainDiffuseBrdf_rampColor = mainDiffuseBrdf * rampColor_xyzEffect; + + +问了下ai,在色彩学中,Max(RGB) - Min(RGB)就是色度,它直接代表了这个颜色的饱和度。根据 Ramp 颜色的饱和度,在纯白(1,1,1)和 Ramp 原色之间进行插值 + +高饱和度 = 保留 Ramp 染色,高饱和度 = 保留 Ramp 染色 + +某种意义上这个设计应证了我前面说的,zmd里的ramp映射只是一个细节辅助作用,不是主体,这个设计就像是进一步减弱ramp的影响作用 + +或者这是zmd的美术在设计的时候提出的一个想法,希望有一个自适应的动态ramp调色效果,而不是我给什么颜色就是什么颜色,我希望更加方便,我给出的不同颜色会有一个不同的影响效果,看起来颜色过渡更加丝滑好看 + +背后的原因不得而知 + +最后重新得到的brdf结果,实际上大部分区域没有影响,变化明显的地方我标注了一下,可以观察之前的rampColor结果 + +如同前面光源计算一般,我们仍然需要针对日光直射强度去设计两个brdf状态 + +上面我们花费了大量功夫计算的brdf是日光直射强度1下的brdf状态,我们还需要一个0状态下的结果 + +思考一下这个情况下的特点是什么,首先主光也就是太阳光的影响应该消失不见,去除NoL的影响 + +此时回忆一下前面为什么需要把顶光的NoL先作用上去,这里就可以看出来了,为了和日光直射强度0状态下的一些计算进行配合 + +然后也不需要三层这种复杂的变化效果,2层应该就可以了,只需要明暗两层,ramp颜色也是不需要加的,因为这个是与主光NoL产生作用的设计。分布的话,我们采用最普通的相乘 + +float3 mainDiffuseBrdf_lowLight = lerp( mainDiffuseColor_Dark, mainDiffuseColor_Light, ao_shadow_NoFRamp); + +最后根据日光直射强度对两个brdf进行lerp即可 + +// 漫反射brdf根据日光强度lerp + float3 mainDiffuseBrdf_final = lerp(mainDiffuseBrdf_lowLight, mainDiffuseBrdf_rampColor * rampColor_control, _DayStrength); + +light brdf NoL 其他项我们都全部处理完了,直接的到最后的漫反射结果 + +// 漫反射结果计算 + float3 mainDiffuseResult = mainLightColor_final * mainDiffuseBrdf_final; + + +日光直射强度从高到低的变化结果,至此我们完成了toon base shader的漫反射部分 + +高光实现 + +高光没有像少前那样专门做了一个ramp图去调色,在brdf公式上还是按照pbr流程走的,但是在高光的分布计算上,进行了大改 + +我们先来得到一个特殊的light dir + +// 得到forward light dir + float forwardLightDir_y = lerp(0.5, mainLightDir.y, _DayStrength); + float3 forwardLightDir = normalize(float3(cameraForward.x, forwardLightDir_y, cameraForward.z)); + + +从计算上可以感觉出来,这是一个从相机打向人物的光 + +我们需要把这个虚拟光方向与主光的light dir进行混合,其中混合逻辑受到日光直射强度影响 + +// 得到forward light dir + float forwardLightDir_y = lerp(0.5, mainLightDir.y, _DayStrength); + float3 forwardLightDir = normalize(float3(cameraForward.x, forwardLightDir_y, cameraForward.z)); + float NoV = saturate(dot(normalWS, viewDir)); + float3 mainLightDir_new = mainLightDir * _DayStrength + 2*forwardLightDir; + float3 halfDir_new = viewDir * (2 + _DayStrength) + mainLightDir_new; + halfDir_new = normalize(halfDir_new); + float NoH = dot(normalWS, halfDir_new); + + +在zmd的高光里,这个forward light dir会占据高光计算的主体方向影响,如果日光强度1的状态下,mainLight dir占比会增大,0的时候会完全去除 + +可以说高光的形状分布是一个非常风格化的处理,我们通过NoH对比观察一下区别 + +左边是采用了forwardLightdir,右边正常的light dir,可以看到正常物理结果下,可以感觉到场景斜上方光源的方向,但是左边就明显没有那么强烈了 + +得到NoH后,我们按照pbr经典的一套高光brdf公式去计算高光结果,然后我们再对比观察一下 + +float roughness = 1 - ormTex_var.w; + float roughness2 = max(roughness * roughness, 0.0078); + float metallic = ormTex_var.r; + float reflectivity = ormTex_var.g; +float3 F0 = 0.04 * reflectivity.xxx + metallic * (baseColor - reflectivity.xxx * 0.04); +// 高光部分计算 + // D项 + float a2 = roughness2 * roughness2; + float specular_D = (NoH * a2 - NoH)*NoH + 1; + specular_D *= specular_D; + specular_D = a2 / specular_D; + + // V项 + float specular_V = NoV * 2 + roughness2 + 9.99999975e-05; + specular_V = 0.5/specular_V; + +// DVF结合 + float specular_DV = specular_D * specular_V - 6.10351562e-05; + specular_DV = max(0, specular_DV); + specular_DV = min(20, specular_DV); + float3 specular_brdf = specular_DV * F0; + // 计算高光结果 + float3 specularLight = mainLightColor_final * (ao_shadow_lowLight*0.5+0.5); + float3 mainLightSpeuclarResult = specularLight * specular_brdf; + mainLightSpeuclarResult *= _SpecularStrength; + + +其中我们同样需要作用 ao shadow这些其他项,zmd在之后的计算里,给这些其他项做了一个统一的日光直射强度变化,因为漫反射里我们在brdf计算的时候做了这个状态变化区别,高光的计算部分就直接用这个其他项变化+NoH变化来充当 0 1状态上的不同 + +float ao_shadow_lowLight = lerp(ao_shadow_NoFRamp, min_shadowEffect, _DayStrength); + + +这个就是纯粹的风格化的设计,你也可以依旧选择正常的NoH运算,我个人感觉高光上的这个trick主要是为了日光直射强度0的时候准备的,类似头顶光这个补光设计一样,当0的时候,总不能让顶光去产生高光吧,因此选择了一个相机方向的高光 + +当然,这个高光可能在美术上会更符合二次元直觉,但我在画画的时候,高光也都是从光源方向去思考出发的,则部分设计我也没办法完全理解 + +漫反射与高光的融合 + +漫反射与高光结合的时候,需要给漫反射结果作用一个albedo w通道的作用 + +// 漫反射 高光结合 + float diffuseSpecularBlend = lerp(1,mainTex_var.w,_DiffuseBlendEffect); + float3 mainLightResult = mainDiffuseResult * diffuseSpecularBlend + mainLightSpeuclarResult; + + +开一个参数来控制一下是否需要使用albedo贴图w通道 + +先来看一下w通道是什么 + +可以看到大部分都是1,在一些特殊材质区域上会有强度区分,这个本质上是一个sss材质上会出现的漫反射能量控制,因为次表面散射下,不能按照正常的漫反射能量分布去思考,因此这个部分可以理解是对sss材质brdf的一个拟合效果 + +后面讲到这些特殊材质的时候,会额外引入sss项,自发光项,这些项会分走漫反射的能量部分,因此这里的削弱从物理上分析是正常的 + +IBL高光 + +zmd采取了一个近似数学函数拟合,因此没什么可以讲的,这部分我也是拜托ai实现的,具体的数学公式应该是有图形学参考的,但笔者并没有花更多的时间去研究 + +让ai帮忙分析了一下出处,主要参考了NVIDIA NRD (NVIDIA Real-Time Denoisers) 库中的 DFG 拟合函数,然后引入了Imageworks 的 Kulla-Conty 多级弹射补偿 + +我的建议是,把这个方法对应的结果记到脑海里,下次需要使用IBL高光的时候,拿出来试一试效果,作为baseline去魔改的时候再细致研究也不急 + +// IBL高光计算 + float roughness4 = roughness2 * roughness2; + float roughness6 = roughness4 * roughness2; + float NoV2 = NoV * NoV; + float NoV3 = NoV2 * NoV; + float fit_A = 3.32707 * NoV + 0.0365463; + float fit_B = -9.04755 * NoV + 9.0632; + float IBLspecular_brdf1 = fit_A + fit_B * roughness2; + + float fitX = 3.59685 * NoV2 - 1.36772 * NoV3 + 1.0; + float fitY = 9.22949 * NoV3 - 16.3174 * NoV2 + 9.04401; + float fitZ = -20.2123 * NoV3 + 19.7886 * NoV2 + 5.56589; + float3 nvFactors = float3(fitX, fitY, fitZ); + float IBLspecular_brdf2 = dot(nvFactors, float3(1, roughness2, roughness6)); + + float IBLspecular_brdf = IBLspecular_brdf1/IBLspecular_brdf2; + + // IBL高光函数计算继续 拟合Lut图查找部分 DFG拟合板块 + float scale_fit_part1 = dot(float2(-1.28514, 1.0), float2(NoV, 0.990440011)); + float scale_fit_part2 = dot(float2(1.0, -0.75591), float2(1.29678, NoV)); + float env_scale = dot(float2(scale_fit_part1, scale_fit_part2), float2(1, roughness2)); + float bias_fit_x = dot(float3(2.92338, 59.4188, 1.0), float3(NoV, NoV3, 1.0)); + float bias_fit_y = dot(float3(1.0, -27.0302, 222.592), float3(20.3225, NoV, NoV3)); + float bias_fit_z = dot(float3(626.130, 316.627, 1.0), float3(NoV, NoV3, 121.563004)); + float bias_denominator = dot(float3(bias_fit_x, bias_fit_y, bias_fit_z), float3(1,roughness2,roughness6)); + float env_bias = env_scale / bias_denominator; + + float3 IBLspecular_brdf_final = IBLspecular_brdf * F0 + env_bias; + float IBLspecular_brdf_final_noF0 = IBLspecular_brdf + env_bias; + // 把IBL的传统预积分过程,拟合成了一个标准数学函数 + + + // IBL高光补充项 多级弹射补偿 前面是单次弹射的函数拟合 + float directionalAlbedo = IBLspecular_brdf_final_noF0; + // 计算多级弹射补偿因子 (Kulla-Conty Approximation) + // 目的:找回在微表面缝隙中经过多次弹射后才射出的光线 + float energyLossFactor = (1.0 - directionalAlbedo) / directionalAlbedo; + // 补偿颜色主要受 F0 影响 + float3 ms_compensation = F0 * energyLossFactor; + // 最终的 IBL BRDF = 基础项 + 补偿项 + // 这能显著提升粗糙材质在高光下的饱和度和亮度 + float3 final_ibl_brdf = IBLspecular_brdf_final * (1.0 + ms_compensation); + + // IBL高光 light部分获取 + float3 reflectDir = reflect(-viewDir, normalWS); + reflectDir.x = -reflectDir.x; + // reflectDir.y = -reflectDir.y; + reflectDir.z = -reflectDir.z; + + // 旋转环境贴图 + float angle = _EnvRotation * 0.0174532925; // 将角度转换为弧度 (PI/180) + float s, c; + sincos(angle, s, c); // 同时获得正弦和余弦 + + float3 rotatedDir; + rotatedDir.x = reflectDir.x * c - reflectDir.z * s; // 旋转 X + rotatedDir.y = reflectDir.y; // Y 保持不变 + rotatedDir.z = reflectDir.x * s + reflectDir.z * c; // 旋转 Z + + float envMap_level = log2(max(0.01, roughness)); + envMap_level = envMap_level * 1.2 + 5.0; + float3 envMap_color = SAMPLE_TEXTURECUBE_LOD(_EnvMap, sampler_LinearRepeat, rotatedDir, envMap_level); + envMap_color *= _EnvColor; + + // IBL高光结果获取 + float3 indirLightSpecular = envMap_color * final_ibl_brdf * _EnvLightStrength ; + + +看一下笔者的代码实现,然后原封不动复刻出来就好 + + + + +边缘光实现 + +边缘光zmd的实现分为两部分 + +首先是最常见的基于NoV的菲涅尔边缘光 + +// rimLIght + float rimStart = _RimLightArea * -0.6 + 0.8; + float rimEnd = _RimLightArea * -0.4 + 0.9; + float rimWidth = rimEnd - rimStart; + float rimt = ( (1.0 - NoV) - rimStart ) / rimWidth; + rimt = saturate(rimt); + float rimArea = rimt * rimt * (3.0 - 2.0 * rimt); // rim平滑处理 这里做了一个smoothstep处理 + float3 rimLight = rimArea * _RimLightColor * _RimLightStrength; + float3 rimLight_effectd = rimLight * min(ao,shadowScene); + // rimLight brdf + float3 rimLight_brdf = lerp(0.25, mainDiffuseColor_Light, _RimLightDiffuseColorEffect); + float3 rimLightResult = rimLight_brdf * rimLight_effectd; + + +分析一下这个NoV边缘光的实现,首先用smoothstep对1 - NoV做一个范围映射,决定边缘光的一个范围大小 + +然后作用color,作用ao shadow的影响,得到rimLight_effectd,最后看一下是否需要引入原漫反射颜色的影响 + +最后得到的结果如下 + +右边是完全引入了漫反射颜色的效果,左边是部引用 + +一言蔽之,1-NoV过一个自定义的范围映射+原固有色的影响控制 + +除了基于NoV的,还有基于光照的边缘光效果,这个在我之前的toon shader 2博客里提到过,蛋白胨大佬的少前博客里也提到过,是一个非常二次元的边缘光效果,只不过范围依旧是走的NoV,没有通过深度差去实现 + +// NoLxz rim + // 根据光源变化的rimLight + float3 rim_mainLight = lerp(1, mainLightColor * mainLightIntensity, _DayStrength); + float NoLxz = dot(normalWS, mainLightDir_xz); + float NoLxz_refine = 0.5 - (0.5*NoLxz - 1) * NoLxz; + NoLxz_refine *= _DayStrength; + float3 rim_mainLightResult = rim_mainLight * NoLxz_refine; + float NoV_mask = saturate(5.0 * (0.4-NoV)); // 0.2的width min是0 max是0.2 大概这样 + NoV_mask = MySmoothstep(NoV_mask); + rim_mainLightResult *= NoV_mask; + rim_mainLightResult *= min(ao,shadowScene); + rim_mainLightResult *= _RimLightNoLxzStrength; + + +来分析一下这个边缘光的试下逻辑,相比于NoV的要复杂一点,因为又涉及到了日光直射强度的状态变化 + +rimLight的color部分与日光强度有关,0的时候是1,1的时候直接是mainLight的color + +2. NoL进行特殊处理,不需要y轴的影响,只需要mainLightdir的xz部分,把人物当成了一个圆柱去照亮边缘光 + +3. 边缘光的强度受到日光强度影响,0的时候直接不要现在的NoLxz的边缘光影响 + +4. 依旧是1-NoV + 范围重映射去计算边缘光的范围,NoV_mask =saturate(5.0*(0.4-NoV))是我直接复现代码的结果,应该是类似smoothstep(0,0.2,1-NoV)这样的数值运算 + +5. 作用ao shadow,自定义强度控制的影响 + +最后结果如下 + +会受到明显的向光面影响,实际上也没有那么复杂,主要是涉及到一个状态变化的设计 + +整合最终结果 + +把前面部分加起来,得到最终结果 + +float3 resultColor = mainLightResult + indirLightSpecular + max(rimLight_finalResult,0); + +注意rimLight的部分做一个安全限制,实际上我目前实现的代码里还有不少安全控制没做,这种细节太多了,一个人复刻确实有点管理部过来 + +toon base shader最终结果 + +还没有加后处理调色,某些地方会有些过曝,后面对比度调整后就正常了 + +特殊材质变体 + +toon base shader基本上结束了,剩下需要针对一些特殊部位做变体补充 + +首先是衣服之外的两个小零件部位需要额外加入高光上的特殊处理 + +两个特殊部位需要特殊处理 + +加入变体的shader写法我这里不会讲解,我们定义一个_SPECULARREFINE_ON 变体 + +然后需要用到前面说的一张颜色映射图 + 自发光贴图 + +下面那张黑色的就是自发光贴图,复现的时候错误的认成了自定义高光,理解意思就行 + +在高光计算的过程中,对F0做一个修正 + +// F0 refine 过程 + #ifdef _SPECULARREFINE_ON + + float f0Refine_u = lerp(specular_D * roughness2, NoV * NoV, _RefineF0U_lerp); + float f0Refine_v = roughness * (1 - ao); + + float4 F0RefineTex_var = SAMPLE_TEXTURE2D(_SpecularRefineF0Tex, sampler_AlbedoTex, float2(f0Refine_u,1 - f0Refine_v)); + F0 *= F0RefineTex_var.xyz; + #endif + + +采样颜色映射图,逻辑就是类似IBL预积分lut图的采样方式,至于为什么是粗糙度和NoV,不要问,因为这是zmd预积分好的,我只是复刻出来而已 + +最后得到的修正F0如下 + +左边是修正后的,可以看到有些许颜色变化,这种纯金属特殊物体美术可能希望在手动添加一些风格化的高光特色,总之游戏里这么做了,我这里也复刻出来 + +// 自发光效果 + #ifdef _SPECULARREFINE_ON + + float4 specularRefineColorTex_var = SAMPLE_TEXTURE2D(_SpecularRefineColorTex, sampler_AlbedoTex, i.uv0); + float3 specularRefineResult = specularRefineColorTex_var.xyz * _SpecularRefineColor * _SpecularRefineColorStrength * diffuseSpecularBlend; + mainLightResult += specularRefineResult; + + #endif + + +然后在主光着色结果上,加一个自发光效果,主要体现在玩偶,还有缎带上,下面是specularRefineResult结果 + +然后自发光需要受到albedo贴图的w控制,diffuseSpecularBlend在前面漫反射高光融合那里可以找到定义 + +在复现研究的过程中,确实会感觉到zmd美术组对细节的各种巧思设计,大伙们确实是想着在游戏里完全复刻精致的美术插画原案效果,这部分trick我的感觉就是为了对其美术原画效果 + +下一个特殊材质变体是黑丝部分,只需要额外加入sss项 + +作用到最初的albedo获取那里,去改变固有色 + +// albedo颜色 明暗处理 + float3 baseColor = mainTex_var.xyz * _BaseColor.xyz; + baseColor = pow(baseColor,_BaseColorPow); + // 黑丝等带有sss效果的特殊材质 + float NoV_sss = saturate(dot(normalWS, viewDir)); + float sss_area = pow(1.05 - NoV_sss, 1 + mainTex_var.w * _SssPowStrength); + baseColor = lerp( baseColor, _SssColor, lerp(0, sss_area, _IsNeedSss)); + + +还是同样的用1-NoV去计算边缘区域,然后作为sss的分布权重,算是最简单的sss的实现方式 + +我这里甚至没加变体,因为没涉及到贴图采样,计算也不是特别复杂,就直接用lerp控制一下好了 + +左边是有sss,右边是没有sss影响albedo的结果 + +最后是透明材质,也就是小羊袖子这一块部位 + +定义_TRANSPARENT_ON变体,首先你需要记得把材质的blend和shader渲染顺序手动调节,然后在最后着色结果上,加上一个自发光部分即可 + +#ifdef _TRANSPARENT_ON + finalAlpha = mainTex_var.w * _CustomAlpha; + float3 emissionColor = SAMPLE_TEXTURE2D(_EmissionTex, sampler_LinearClamp, i.uv0 ).xyz; + + // 加入自发光颜色调整流程 + resultColor += _EmissionColor * _EmissionColorStrength * emissionColor * lerp(1,mainTex_var.w,0.8); + + #endif + + +需要注意的可能是,这个部位的ramp图是单独做的一个,记得给对,然后自发光贴图不要给错 + +同时需要调一调颜色,这与的话会更接近于游戏里的效果,不过我这边复刻的效果确实和游戏里的效果有些差距,就先这样吧 + +skin toon shader + +最难啃的大山已经结束了,有了toon base shader作为base,接下来的shader实现都会简单许多 + +之后的shader里,都会在base shader上的着色逻辑进行修改和添加,因此我会直接说这个shader需要toon base shader什么项,然后直接讲解其他全新的重点功能 + +在皮肤的实现上,首先不需要IBL的高光,至少我在游戏里没看到,其余漫反射高光和边缘光逻辑保持一致 + +然后我们同样需要加入sss效果 + +sss效果 +// sss效果实现 albedo处理 + float NoV = dot(normalWS, viewDir); + float sss_NoV = saturate(NoV); + sss_NoV = sss_NoV * 0.85 + 0.15; + float sss_area = saturate(_SSSArea * (1.0 - sss_NoV)); + float3 sssColorEffect = lerp(float3(1.0, 1.0, 1.0), _SSSColor, sss_area); + float3 albedo_sssRefine = mainTex_var.rgb * sssColorEffect; + + +对1-NoV的范围映射有些区别,然后需要对原有颜色进行作用,而不是之前黑丝材质上的lerp + +左边是sss作用后的albedo,右边是原albedo,效果也很明显,sssColor需要你手动调一下来接近游戏里的效果 + +基于Lut图的暗部albedo + +其余着色逻辑和toon base是一致的,但是有些许区别,首先皮肤没有材质属性贴图,需要我们默认把材质给上 + +// 材质属性初始化处理 + float metallic = 0; + float energyDistribution_metallic = 0.96 - 0.96 * metallic; + float3 mainDiffuseColor_Light = albedo_sssRefine * energyDistribution_metallic; + float reflectivity = 0.04 * _ReflectivityStrength; + float3 F0 = float3(1,1,1) * reflectivity; + float roughness2 = max(0.0078125, _Roughness * _Roughness); + + +把粗糙度和反射度开放成参数就可以了 + +然后是皮肤的一个特点,为了皮肤的统一复杂效果,设计了一个通用的颜色lut图来映射暗部,可以看出zmd美术对皮肤的用心程度,皮肤的明暗变化相比于ramp图,直接用了一套更复杂的颜色映射方式lut来实现,一般lut都是拿去给最终屏幕后处理调色用,在这里只是为了皮肤的细节变化 + +总之,我们toon base里的暗部颜色获取在这里需要通过贴图实现 + +我这边截取出来的结果是上下颠倒的,因此我会对后面的采样uv做一个反向 + +下面是基于这张帖图,来获取暗部颜色的方式,暗中暗依旧是在暗部上作用0.65 + +// 漫反射暗部颜色计算 + float3 albedo_sRGB = linear2sRGB(mainTex_var.brg); + float2 lut_uv = albedo_sRGB.xz * float2(31,0.96875); + float lut_uv_floorx = floor(lut_uv.x); + float2 lut_uv_yz = albedo_sRGB.yz * float2(0.0302734375,0.96875) + float2(0.00048828125,0.015625); + float lut_uv_x = lut_uv_floorx * 0.03125 + lut_uv_yz.x; + float2 lut_uv_final = float2(lut_uv_x, 1 - lut_uv_yz.y); + + float lutTex_lerp = albedo_sRGB.x * 31 - lut_uv_floorx; + float3 lutTex_var1 = SAMPLE_TEXTURE2D(_LutColorTex, sampler_LutColorTex, lut_uv_final).xyz; + float3 lutTex_var2 = SAMPLE_TEXTURE2D(_LutColorTex, sampler_LutColorTex, lut_uv_final + float2(0.03125,0.015625)).xyz; + + float3 albedo_lutRefine = lerp(lutTex_var1,lutTex_var2,lutTex_lerp); + float3 mainDiffuseColor_dark = albedo_lutRefine * energyDistribution_metallic * _AlbedoDarkStrength; + + // 计算暗中暗颜色 + float3 mainDiffuseColor_darkindark = mainDiffuseColor_dark * 0.65; + + +采样逻辑我推荐你去询问ai或者查阅资料,讲述lut图的采样方式对本文的主题意义不是很大,代码中的一些brg错位是因为这部分我依旧是拜托ai帮忙整理的 + +我们需要关注采样后的颜色结果就是暗部albedo,不需要像ramp那样还作用一下 + +因为lut图类似一个一对一的哈希表,这个颜色就是会直接变成另一个颜色 + +亮部,亮部颜色x0.5得到暗部颜色,基于lut的暗部颜色 + +可以看到区别很明显,基于lut图的暗部可能会更加符合皮肤质感,但我对真实人物的渲染研究还不够多,这里的原因仅仅只是推测而已 + +得到暗部颜色后,之后继续按照toon base的逻辑去着色即可 + +最后得到的皮肤结果,右边是边缘光增强的结果,最新的游戏版本里人物界面边缘光拉的挺高,一般不是特殊场景不会拉的很亮 + +face toon shader + +卡通渲染的脸部shader技术现在基本都是基于sdf的,zmd也不例外,重点就是如何在sdf的光照变化上加入各种细节trick + +在toon base shader的逻辑上,我们去除IBL高光,去除所有边缘光,只要漫反射高光着色逻辑,然后特点是获取基于sdf的光照变化分布NoL,来代替漫反射过程中的各种NoL分布 + +sss+lut暗部颜色 + +由于face也属于皮肤,所以皮肤的特性都存在,前面我们实现的sss效果+lut图暗部颜色获取在脸部依旧需要实现 + +代码我这边展示一下关键位置,sss上需要根据贴图做一些特殊处理 + +// sss范围限制 + float view_sssStrength = lerp( saturate(headFDotCamerF + 0.5), 1, sdfRefineTex_var.y) * sdfRefineTex_var.x; + +。。。 + +float sss_area = saturate(_SSSArea * view_sssStrength * (1.0 - sss_NoV)); +float3 sssColorEffect = lerp(float3(1.0, 1.0, 1.0), _SSSColor, sss_area); + float3 albedo_sssRefine = albedo * sssColorEffect; + +。。。 + +float3 albedo_lutRefine = lerp(lutTex_var1,lutTex_var2,lutTex_lerp); // 暗部颜色 + float3 mainDiffuseColor_dark = albedo_lutRefine * energyDistribution_metallic * _AlbedoDarkStrength; + + // 计算暗中暗颜色 + float3 mainDiffuseColor_darkindark = mainDiffuseColor_dark * 0.65; + + +这里的sdfRefineTex_var就是除了sdf魔法图的另一张范围限制图,其中x和y通道就是用来限制sss范围的,由于脸部的建模比较特殊,如果直接通过1-NoV得不到二次元日式的风格化效果,因此需要通过贴图进行限制 + +贴图的限制效果对比如下 + +左边是贴图限制后的结果,右边是没有限制的,是一目了然的 + +如果你后续遇到了脸部sss效果的实现,不妨也这么做,应该是一个成熟的做法 + +trick表情图的使用方式 + +贴图资源里存在一张用于表情的贴图,是一个四格存储的贴图,只需要按照采样逻辑去采样即可 + +得到的结果与固有色albedo做一个lerp + +// trick图获取 + float emojiType = _TrickType * 0.5; + float2 emojiUV = float2(frac(abs(emojiType)), floor(emojiType) * 0.5); + float4 trickTex_var = SAMPLE_TEXTURE2D(_TrickTex, sampler_TrickTex, i.uv0*0.5 + emojiUV); + float3 trickColor = trickTex_var.xyz ; + + albedo = lerp(albedo, trickColor, trickTex_var.w * _TrickStrength); + + +在获取albedo后,就可以接上表情的颜色影响 + +采样逻辑参考代码上的逻辑,很简单,后续我们都只采用无表情的模式,也就是第二个效果展示 + +最后一个表情应该还需要和五官配合一下,但我这边没有去额外加上 + +魔法图sdf + +如果你还记得米家星铁的脸部魔法图的采样方式,那你应该记得我们需要从外部传入面部的 forward dir,right dir,up dir三个方向 + +// face dir +float4 _FaceForward; +float4 _FaceRight; +float4 _FaceUp; + + +这三个方向的获取你可以问问ai或者查阅资料,这里我也不会讲述,比较读者再看这一节之前去回顾一下我之前米家星铁sdf贴图采样的过程 + +回顾一下这张sdf图采样需要注意的地方,首先我们需要判断光照在左还是在右,需要决定贴图采样的方向顺序 + +// 光照法向判断 用于采样sdf + float3 mainLightDir_xz_faceDir = normalize(float3( dot(mainLightDir, _FaceRight), 6.10351562e-05, dot(mainLightDir, _FaceForward))); + float sdf_uvFlag = step(0, mainLightDir_xz_faceDir.x); + + +通过mainLightDir和faceRight的dot来判断左右即可,如果读者不能直观理解的话,可以去引擎里输出看一下 + +根据光源所处的左右位置,来决定采样的uv处理 + +// 采样sdf + float sdf_u = sdf_uvFlag * (2 * i.uv0.x - 1) + 1 - i.uv0.x; + // lerp(1-x, x, flag) + float sdf_v = i.uv0.y; + float4 sdfTex_var = SAMPLE_TEXTURE2D(_SdfTex, sampler_SdfTex, float2(sdf_u, sdf_v)); + + +采样sdf魔法图后,我们需要根据当前光照方向来决定当前sdf应该是什么形状 + +用一个smoothstep来理解,smoothstep( x , y , sdf_var),我们通过调整x和y,来让sdf贴图的结果在脸部上呈现出丝滑的过渡变化效果,这个结果我们用代替物理的光照分布NoL + +现在我们的问题就是,x和y是什么,sdf var又是贴图的哪个通道? + +先说sdf_var的部分,是x通道+y通道的归一化,0.5 * (sdfTex_var.x + sdfTex_var.y) + +在脸上的效果大概是这样 + +不要问为什么这样做,我的分析是觉得也可以存到一个通道上的,但是zmd选择了分成两个通道去烘焙,可能涉及到一些优化上的处理,但我们的目的只是复刻出来 + +剩下x和y的部分,也就是min和max的部分,需要与光照方向产生联系,为了得到丝滑的脸部光照方向分布NoL,我们需要进行一些替换,在sdf的烘焙逻辑里,N就是一个固定的向前朝向,也就是_FaceForward + +通过dot faceForward和mainLightDir,我们可以得FoL分布 + +观察前面的sdf图,现在假设存在一个step(x, sdf_var),x从0变化到1,会得到什么效果呢? + +0的时候是全亮,0.5的时候是明暗sdf变化的一半,1的时候是全黑 + +0的时候应该是光照完全照亮脸部的时候,也就是FoL等于1的时候,反之1的时候就是FoL等于0的时候 + +因此,如果我们要用FoL来作为smoothstep的x部分,需要进行反向 + +float faceNoL = mainLightDir_xz_faceDir.z; + float halfFaceNoL = faceNoL * 0.5; + faceNoL = saturate(-faceNoL * 0.5 + 0.5); +float sdf_min = max(0, 2*faceNoL - 1); + + +现在用min代替step里的部分,可以得到这样的光照分布变化 + +过渡太硬了,因此我们需要加上一个width作为max的部分,max定义为min(1, 2*faceNoL),不需要对这个数值产生疑惑,你也可以修改这个max的部分,只需要知道目的是让过渡边界不这么生硬 + +// smoothstep部分特殊计算 + float sdf_min = max(0, 2*faceNoL - 1); + float sdf_width = min(1, 2*faceNoL) - sdf_min; + float sdf_smoothVar = saturate((0.5 * (sdfTex_var.x + sdfTex_var.y) - sdf_min) / sdf_width); +sdf_smoothVar = sdf_smoothVar * sdf_smoothVar * (3 - 2 * sdf_smoothVar); + + +上面这一块就是smoothstep(min, max, sdf var) + +这感觉一下子就上来了,不错,然后让我们来看一下zmd特殊处理 + +float faceNoL_compensation = -0.5 * mainLightDir_xz_faceDir.z * mainLightDir_xz_faceDir.z + 0.5; +// 背光判断 + float2 cameraForward_xz = normalize(cameraForward.xz); + float backLight = saturate(-dot(cameraForward_xz, mainLightDir_xz.xz)) * saturate(-mainLightDir_xz_faceDir.z); + float faceNoL = mainLightDir_xz_faceDir.z + backLight * faceNoL_compensation; + + +把我们toon base shader背光补偿逻辑加了上去,当人物背对光源,正对相机的时候,并不会让脸部全黑 + +我们只输出backLight * faceNoL_compensation观察一下 + +不会让这个情况下人物脸部陷入全黑,当作一个trick加上去即可 + +此时我们可以得到后续toon base shader着色逻辑里用到的NoL部分 + +float sdf_NoL = sdf_smoothVar * 2 - 1; + + +因为NoL的范围分布是-1 1,所以这里做一个x2-1的映射 + +脖子与脸衔接处处理 + +说起来,这个问题还曾经在面试的过程中被问到过,之前没有研究过,在这次复现的过程中反而体会到了这一点的重要性 + +// 脖子衔接处的NoL计算 + float NoL = dot(i.normalWS, mainLightDir); + float ramp_NoL = lerp( sdf_NoL, NoL, sdfRefineTex_var.y); + float ramp_u = ramp_NoL * 0.5 + 0.5; + float ramp_v = 0.5; + float4 rampTex_var = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(ramp_u, ramp_v)); + + +在漫反射着色阶段里的ramp采样之前,我们需要对NoL做一个lerp,其中sdfRefineTex_var.y很有意思,是一个区域mask图,0代表是脸部区域,1代表是靠近脖子区域,也就是说脖子上的NoL采用模型法向和光照方向的夹角即可 + +为什么需要单独处理脖子,核心就是为了靠近脖子的区域和脖子皮肤材质的平滑拼接 + +上面是我已经处理好的结果,但可以看到脸部和脖子的衔接区域仍然有一条裂缝一样的存在,如果不处理的话这一块区域会非常明显,因为你计算出来的光照分布和紧挨着的脖子(skin toon shader)的着色中的光照分布不一样,自然就会出现差异 + +定制化边缘光 + +我们去除了toon base shader里的边缘光逻辑,这里我们需要实现一个基于贴图的定制化边缘光效果 + +// sdf normal获取 + float sdf_z = lerp( -(sdfTex_var.z*2-1), sdfTex_var.z*2-1, sdf_uvFlag); + float3 sdfDir = float3(sdf_z, 6.10351562e-05, 1 - abs(sdf_z)); + sdfDir = normalize(sdfDir); + // 转换空间 + float3 faceSpace_x = float3(_FaceRight.x, _FaceUp.x, _FaceForward.x); + float3 faceSpace_y = float3(_FaceRight.y, _FaceUp.y, _FaceForward.y); + float3 faceSpace_z = float3(_FaceRight.z, _FaceUp.z, _FaceForward.z); + float3 sdfNormal = normalize(float3(dot(faceSpace_x, sdfDir), dot(faceSpace_y, sdfDir), dot(faceSpace_z, sdfDir))); + float3 normalWS = lerp(sdfNormal, i.normalWS, sdfRefineTex_var.y); + + +还记得前面sss的实现上出现的问题吗?直接1-NoV是行不通的,sss通过贴图去限制 + +边缘光上的实现决定通过重新算一个normal + 基于sdfRefineTex_var的w通道限制区域 + +左边是重新计算的normal,右边是脸部模型normal + +在这个特殊处理后的normal下,重新计算NoV的菲涅尔边缘光 + +// rimLIght + float rim_NoV = dot(normalWS, viewDir); + float rimStart = _RimLightArea * -0.6 + 0.8; + float rimEnd = _RimLightArea * -0.4 + 0.9; + float rimWidth = rimEnd - rimStart; + float rimt = ( (1.0 - rim_NoV ) - rimStart ) / rimWidth; + rimt = saturate(rimt); + float rimArea = rimt * rimt * (3.0 - 2.0 * rimt); // rim平滑处理 + + +逻辑依旧是对1-NoV过一个自定义控制的smoothstep重映射 + +看起来还可以,但是脸部还需要根据sdfRefineTex_var.w定制化一下 + +// 使用基于 物理的rim 还是 基于mask的rim + float rimArea_mask = sdfRefineTex_var.w; + float rimArea_final = lerp( rimArea, rimArea_mask, _RimMaskStrength); + + +这个笔尖边缘光效果确实不错,游戏里也是可以观察到的,可以确认肯定是使用了的 + +这两个边缘光的效果,直接相加的话效果不太对,应该还是做一个lerp比较好,这里是我的处理,我也没研究清楚游戏里的是怎么实现的 + +然后边缘光默认是会对称出现,但是游戏里不是这样的效果,二次元日式风格里,一般都是只有一边的效果,所以我们还需要对出现时机进行一个限制 + +// 限制一半的区域 + float rimHalfArea = saturate(dot(cameraRight, normalWS)); + + +最后给边缘光作用上ao shadow的影响,得到最终的边缘光结果 + +float3 rimLight_effectd = rimLight * min(ao,shadowScene); + +eye toon shader + +这部分难度不大,首先我们只要toon base shader的漫反射部分,高光走一个matcap实现,如果你在看后续过程中对某些地方上下文理解出现了问题,一定要参考我分享的代码仓库,对照着完整shader去理解 + +眼睛区域划分 +// 获取 眼睛中心mask区域 + float2 fracUV = frac(i.uv0); + float2 eyeCenterAreaUV = fracUV - float2(0.5, 0.5); + float eyeCenterArea = step(0.25, dot(eyeCenterAreaUV, eyeCenterAreaUV)); + + +基于uv,直接计算出一个眼睛的中心区域,后面会多次用到,日式二次元画风中的特殊高光区域 + +只需要注意眼睛内部区域,其余部分不会这么处理 + +基于特殊法向的着色 + +toon base shader的着色逻辑没有太多变化,但是法向不能直接用模型法向 + + // 计算特殊法向 + float2 centeredUV = eyeCenterAreaUV * 2.0; + float uvSq = dot(centeredUV, centeredUV); + float zHemi = sqrt(max(0.0, 1.0 - min(1.0, uvSq))); // max和min用来防溢出报错 + zHemi = max(1e-16, zHemi); // 防止出现完美的 0 导致后续归一化出 NaN + + float2 corneaTS_xy = 0.125 * _CorneaBumpStrength * centeredUV; + float3 corneaNormalTS = float3(corneaTS_xy, zHemi); + + corneaNormalTS = lerp(corneaNormalTS, float3(0, 0, 1), eyeCenterArea); + // 特殊位置需要0 0 1,法向不变 + corneaNormalTS.x = -corneaNormalTS.x; + + float3 customNormalWS = corneaNormalTS.x * tangentWS + + corneaNormalTS.y * bitangentWS + + corneaNormalTS.z * normalWS; + // TBN变换 切线空间到世界空间 + float3 corneaNormalWS = normalize(customNormalWS); + + +左边是计算后的特殊法向,右边是模型法向 + +特殊法向的逻辑很有意思,通过uv,根据距离,得到了一个平滑过渡的切线法向,可以把uv看作一个二位函数场,然后我们求取梯度,得到了一个平滑变化的法向效果 + +由于眼球的uv特殊性,刚好可以让uv的中心在眼球正中心,所以可以采用这种方法,这种眼睛的特殊技巧我个人觉得是可以留一个心眼的,也许在其他的一些设计上也可以采用同样的方案,产生意想不到的收获 + +把这个特殊法向作用到后续的漫反射着色过程中即可 + +自定义区域发光trick + +在漫反射的双层lerp过程中,需要做如下修改 + +// 自定义颜色调节 + float3 specularTrickColor = lerp(1, _SpecularTrickColor * 2.5, eyeCenterArea); + float3 eyeInTrickColor = lerp(1, _EyeInTrickColor * 2.5, mainTex_var.w); + float3 trickAlbedo = specularTrickColor * eyeInTrickColor; + +。。。 + +// 第一层lerp 暗部之中分出两层 暗中暗和暗部的区分 + float3 mainDiffuseColor_Dark_lerp = lerp(mainDiffuseColor_Dark_final, mainDiffuseColor_dark , saturate(rampTex_NoF + rampNoL)); + // 第二层lerp 明暗区分 + float3 mainDiffuseBrdf = lerp(mainDiffuseColor_Dark_lerp, mainDiffuseColor_Light * trickAlbedo, rampNoL); + + +在亮部albedo上,需要作用一个颜色修正,其中eyeCenterArea前面已经看到了,是指眼睛的日式高光区域,然后mainTex_var.w比较特殊,也是一个日式二次元画风中比较重要的区域 + +这两个区域都需要单独给上一个颜色作用,颜色确定可以对照游戏里的效果去设定 + +基于matcap的高光 +// 日光变化的 暗部影响因素 + float DayDarkEffect = lerp(rampTex_NoF, rampNoL, _DayStrength); + + // matcap采样 view空间下的normal + float3 corneaNormalVS = mul((float3x3)UNITY_MATRIX_V, corneaNormalWS); + corneaNormalVS = normalize(corneaNormalVS); + float2 matcapUV = corneaNormalVS.xy * 0.5 + 0.5; + float4 specularMatcapVar = SAMPLE_TEXTURE2D(_SpecularMatcap, sampler_SpecularMatcap, matcapUV); + float3 specularColor = specularMatcapVar.xyz; // 高光 RGB 颜色 + float specularW = specularMatcapVar.w; // 高光 Alpha (通常用于控制高光的范围或与底色Alpha叠加) + + // specular brdf 获取 + float3 specularBrdf = specularColor * _SpecularStrength + _SpecularColor * specularW; + float specularDarkEffect = DayDarkEffect * 0.5 + 0.5; + specularDarkEffect *= lerp(_SelfAoShadowStrength, 1, DayDarkEffect); + // 高光结果 + mainSpecularResult = mainLightColor_final * specularBrdf * specularDarkEffect; + + +就是正常的matcap采样逻辑,需要得到view空间下的normal,然后用xy去采样 + +其中matcap的w通道给你划分了一个高光区域的mask,你可以在matcap的基础上,加上一个自定义的高光颜色 + +matcap w通道 + +最后作用shadow ao,同时注意日光直射强度的状态影响,得到最后的高光结果 + +非常好看的一个高光效果 + +其余眉毛等部分 + +不需要加入高光,不需要计算新的normal,直接走toon base shader的漫反射逻辑即可 + +hair toon shader + +在toon base shader的基础上,去除IBL高光+主光源高光部分 + +头发的着色逻辑也算是比较特殊的一个,需要做不少处理 + +获取平滑法向 + +你可以在游戏里观察到人物的高光都是一个整体的排列,非常的日式二次元,为了做到这种整齐的效果,我们必须得到平滑法向 + +但是这个法向我们只用于高光,漫反射效果我们仍然需要保留模型法向的物理效果去着色 + +可以在模型里存储平滑法向,也是通用的做法,但是头发的平滑法向是通过贴图完成的 + +在你获取的头发的法向贴图的zw通道上,你可以发现和前面的法向贴图不用,这两个通道是有值的 + +把这两个通道按照法相贴图的逻辑作用到模型法向上,你就可以得到一个平滑法向结果 + +// HNormal处理 + float3 normalTex_Hprocessed = UnpackNormalFromTex(normalTex_var.zwzw, _BumpScale); + float3 HNormalWS = lerp( i.normalWS ,normalize(mul(normalTex_Hprocessed, tangentTransform)), _IsNeedNormalMap); + +左边是法向贴图zw通道平滑后的结果,右边是模型法向 + +但我在使用的过程中,这个平滑的效果还不够,可能是我对贴图的使用方式有问题,我这里选择自己找一个取巧的替代方案 + +// 计算平滑效果 基于center + float3 sphereNormal = normalize(i.posWS.xyz - _FaceCenter.xyz); + + HNormalWS = lerp(sphereNormal, HNormalWS, ormTex_var.x); + + +确定一个中心点,然后直接做一个球体法向效果,缺点就是实现过于简单暴力,效果与原游戏仍有不少差距 + +应该是需要定制一套平滑法向才行,这样才可以保证高光的稳定形状,但是我这边确实没有办法完美复现基于贴图的平滑法向,就先这样替代一下 + +伪平滑法向 + +读者只需要知道这里需要给头发计算出一套平滑法向用于后续的高光即可,只要怎么得到这个平滑方式很多 + +笔者这里选择的是最暴力的方式,没必要学我这么做 + +二次元高光实现 + +虽然说是二次元高光,但实际上依旧参考了头发的各向异性高光实现, 基于最通用的Kajiya-Kay Shading + +把模型看成一根根圆柱组成,高光是在这些圆柱上生成的,这些圆柱的特殊法向可以通过发丝方向切线去求取,经过公式整合后,可以直接把高光结果与切线联系起来,这个技术可以参考的文章很多,下面我罗列一篇 + +也因此高光的形状会依赖于一个定制化的uv,因为uv与切线息息相关,但这种事情美术已经帮我们处理好了 + +最后我们计算高光的公式就是sqrt( 1- pow(dot(T,H) ) ),然后对这个结果x做一个pow的操作来限制高光的大小形状,pow(x , _specularPowStrength) + +H的获取是非常简单的,就是viewdir + mainLightdir + +需要思考发丝方向切线怎么获取,这个方向你需要有一个可视化的印象,沿着头发丝向上走的这么一个方向 + +因为我们不用模型方向,因此unity求取的切线是不能用的,我们需要手动重新求取 + +float dotRight = dot(HNormalWS, cameraRight); + float3 cylinderNormal = normalize(HNormalWS - dotRight * cameraRight); + float3 flatHNormal = normalize(lerp(HNormalWS, cylinderNormal, _SpecularTrick_Flatten)); + float3 fakeTangent = normalize(cross(float3(0,1,0), flatHNormal)); + float3 hairBiNormal = normalize(cross(flatHNormal, fakeTangent)); + + +无视掉前三行,先看后两行,fakeTangent =normalize(cross(float3(0,1,0), flatHNormal))计算了一个假切线,然后我们基于这个切线与平滑normal叉乘,得到了发丝方向,可以简单可视化几何画一画图就知道了 + +然后再看前三行,我为了实现完全平行整齐排列的高光效果,做了一个trick设计,把法向上与cameraRight重合的部分去除,这样法向在任何地方都是只朝着正前方的,是存在forward和up上的变化,不会存在左右上的变化 + +效果大概如下 + +之所以加入这么个trick,主要是我调整光源方向,一直没能做到游戏里的整齐高光效果,于是出了这么一个下策 + +然后在前面给view dir加入一个偏移 + +float3 hairViewDir = normalize(viewDir + float3(0, _ViewDirYOffset*(1 - ormTex_var.x), 0)); + + +这里对view dir的y进行偏移,可以让最后的高光产生上下位置的移动,用来后续调整 + +最后用这个发丝方向,带入Kajiya-Kay Shading公式,我们可以得到如下结果 + +float ToH_lut = dot(hairHalfDir, hairBiNormal_flatten); + float lutUV_u = 1 - ToH_lut * ToH_lut; + + +高光位置拿到了,剩下就是怎么进行着色,这里需要用到美术准备好的颜色贴图 + +我们需要基于算出来的高光位置分布去采样这张颜色贴图 + +从我的变量名可以看出,我们已经拿到的u上的结果,还差v,由于这依旧是预计算处理好的图,因此你不需要由于v的计算由来,只需要直接使用我复刻的结果即可 + +// v计算 + float2 viewDirProj = float2(dot(viewDir, cameraRight), dot(viewDir, cameraForward)); + float2 HNormalProj = float2(dot(HNormalWS, cameraRight), dot(HNormalWS, cameraForward)); + float VoHN_horizontal = saturate(dot(viewDirProj, HNormalProj)); + VoHN_horizontal = pow(VoHN_horizontal, _LutVPowStrength); + float directionMask = step(0.0, ToH_lut); + float lutUV_v = VoHN_horizontal * VoHN_horizontal * directionMask; + + +最后采样的结果如下 + +用这个颜色修正F0,然后执行一个简化版的高光渲染,不走pbr的逻辑 + +float3 lutF0 = SpecularRefineF0Tex_var.xyz * F0; +float3 backF0 = _SpecularBackF0 * ormTex_var.w * pow(sqrt(1 - ToH_lut * ToH_lut), trunc(200 * _SpecularBackF0_ToHPowStrength)); +float3 finalF0 = lutF0 * 7 + backF0; +// 日光变化下的 暗部因素 + float ao_shadow_lowLight = lerp(ao_shadow_NoFRamp, min_shadowEffect, _DayStrength); + float selfAoShadowEffect = lerp(_SelfAoShadowStrength, 1, ao_shadow_lowLight); + + // 高光结果 + float3 mainLightSpecularResult = mainLightColor_final * selfAoShadowEffect * finalF0; + + +同样引入了日光直射强度的变化,读者记得复刻前面一些内容的时候,需要时刻注意是否考虑两个状态的实现 + +还加入了一个trick涉及,在我们算好的高光结果上,又计算了一次Kajiya-Kay Shading,算了一个高光光晕的效果,也就是backF0 + +最后组合到一起,得到最终的高光结果 + +之后的漫反射着色和边缘光,参考toon base shader逻辑实现出来即可 + +outline shader + +如果读者你一边学习复刻看到了这里,作为笔者的我还是很感激的 + +主体渲染部分我们已经全部结束了,剩下的部分就是去整理各种细枝末节的地方,但对于美术而言,细节这个东西是没有上限的,可以无限打磨,而且区别90和100分的差距也都在上面 + +所以在稍微坚持一会 + +描边的着色逻辑我这边直接用的toon base shader漫反射,很暴力 + +你不需要这么做,根据光源color 光源dir,固有色之类的自己设计一个着色方案即可 + +我们只讲外扩的实现 + +法线外扩 +// 平滑法向过程 + float weightN = sqrt(saturate(1.0 - dot(v.uv1.xy, v.uv1.xy))); + float3 smoothNormalOS = v.uv1.x * v.tangent.xyz + v.uv1.y * biTangentOS + weightN * v.normal; + + + + // 将平滑法线转换到观察空间 (View Space) + float3 smoothNormalVS = mul((float3x3)UNITY_MATRIX_IT_MV, smoothNormalOS); + + // 将 View 空间的法线投影到 Clip 空间 (只取 XY,这就是屏幕上的 2D 延伸方向) + float2 clipNormal2D = mul((float2x3)UNITY_MATRIX_P, smoothNormalVS); + // 归一化,保证基础方向长度为 1 + float2 normal2D = normalize(clipNormal2D); + normal2D.x /= ScaleX; + + // 深度修正 + float depthScale = o.positionHCS.w; + float outlineDepthClamp = min(depthScale, 20.0); + + // 距离自定义修正 + float depthRefine = lerp( 1, _ZMinRefine, smoothstep( 1, 12, depthScale)); + + // 计算最终的 2D 偏移向量 + float2 offset = normal2D * (_OutlineWidth * outlineDepthClamp * 0.01 * depthRefine); + + o.positionHCS.xy += offset; + + +注意基于平滑uv的法向计算,以及根据相机距离去修正描边的宽度,最后需要根据屏幕空间去修正外扩大小 + +左边是平滑后的结果 + +fur shader + +如何执行多层pass,我这里不讲述了,然后fur shader拥有toon base shader的所有着色逻辑,漫反射,高光,IBL,边缘光加上去 + +我们来重点讲解一下形状贴图的使用 + +// Voronoi 细胞噪声实时计算 +float2 scaledUV = i.uv * _ClumpScale; float2 cellID = floor(scaledUV * 2.0); +float4 hashInput = float4(123.0, 123.0, 123.1, 123.1) + cellID.xyxy; + +// 随机角度 +float rand1 = frac(sin(dot(hashInput.xy, float2(127.1, 311.7))) * 43758.5469); +float angle = rand1 * 6.28318548; // * 2π + +// 随机半径 +float rand2 = frac(sin(dot(hashInput.zw, float2(127.1, 311.7))) * 43758.5469); +float radius = sqrt(rand2); + +// 特征点偏移与距离 +float2 randomOffset = float2(cos(angle), sin(angle)) * radius * 0.25; +float2 cellCenter = cellID * 0.5 + float2(0.25, 0.25); +float2 featurePoint = cellCenter + randomOffset; + +float2 diff = scaledUV - featurePoint; // 对应 r14.zwfloat dist = length(diff); // 对应 r1.w (Voronoi 距离) + +float4 flowMap_var = SAMPLE_TEXTURE2D(_NoiseTex, sampler_LinearRepeat, i.uv * _NoiseTex_ST.xy); +float3 flowMap = flowMap_var.xyz; +float2 flowDir = flowMap.xy * 2.0 - 1.0; +flowDir *= _FlowStrength; + +// 基于层级高度的随机毛躁 Hash (对应 Bug 修复的 w1.xx)float jitter = frac(sin(dot(float2(i.h, i.h), float2(12.9898, 78.233))) * 43758.5469); +jitter = jitter * 2.0 - 1.0; +jitter = jitter * _JitterStrength * 0.05; + +// 合并基础偏移 +float2 totalDistortion = flowDir * 0.005 + float2(jitter, jitter); + +// 强行拉向细胞中心 (成簇偏移) +totalDistortion = i.h * diff + totalDistortion; + +// 计算最终用于采样的弯曲 UV (虽然形状图是1不需要采样,但这里还原完整逻辑) +float2 shellUV = -totalDistortion * i.h + i.uv; + +float thicknessThreshold = lerp(baseThickness, sqrt(baseThickness), _TaperCurve); + +// 视角边缘补偿 (掠射角增厚防穿帮) float3 viewDirWS = GetWorldSpaceNormalizeViewDir(i.positionWS); +float NoV = saturate(dot(i.normalWS, viewDirWS)); + +float heightCubedInv = 1.0 - (i.h * i.h * i.h); +float viewComp = -_ViewCompThreshold + NoV; +float grazingMask = viewComp + heightCubedInv; + +// 细胞距离成簇衰减 +float clumpDist = min(1.0, dist * 2.8284); +float heightFactor = saturate(i.h * 2.0); // 只有上半段毛发强烈收缩 +float clumpAttenuation = heightFactor * (clumpDist - 1.0) + 1.0; + +// 合并形状 +float rawAlpha = clumpAttenuation * shapeTexMask; + +// 构建 Smoothstep 过渡区间 [-0.25, 0.25]float2 bounds = float2(-0.25, 0.25) + thicknessThreshold; +float lowerBound = max(0.0, bounds.x); +float upperBound = min(1.0, bounds.y); + +// 执行平滑阶跃,生成极其丝滑的毛发边缘 +float smoothAlpha = smoothstep(lowerBound, upperBound, rawAlpha); + +// 保证紧贴皮肤的最底层绝对不透明 +float isRoot = (i.h <= 0.01) ? 1.0 : 0.0; +float finalAlpha = lerp(smoothAlpha, 1.0, isRoot); + +// 乘以边缘补偿遮罩 +finalAlpha = saturate(grazingMask * finalAlpha); + +clip(finalAlpha - 0.003); + + +比较复杂,你要学习理解的话喂给ai比较好,我这里简单梳理一下 + +通过Voronoi噪声去形成簇状结构,出现毛发一簇一簇的集中效果 +基于形状贴图的xy方向产生毛发偏移效果(实验中没什么直接效果 +基于形状贴图的z通道决定毛发细节形状效果 +最后过一个smoothstep来控制分布平滑 + +但这只是我复刻的效果,和游戏里最终效果差距较大,我也不建议你学习我的做法,试试自己研究研究能不能做出更好的效果,笔者捣鼓了一段时间没能完全复刻出来 + +至于火焰特效,这个不在笔者的重点研究对象上,我就随便实现了一下,呼吸变化效果也没有加 + +直接采样特效噪声图,做一个缩放,给一个颜色,因此和原游戏效果差距很大 + +哦对,你如果不进行剔除和透明效果,单层的话大概是这样的效果,用来给读者你复刻学习的时候对照使用 + +当作正常的toon base shader先去实现即可 + +其余各种trick shader + +应该是小羊全身都剖析的差不多了,剩下的部分就是一些小trick的shader,我挑了我觉得必须要实现的 + +刘海阴影 + +最喜欢的一集,实现起来最轻松的一次 + +在你截帧获取资源的时候,应该可以拿到这么一个特殊的刘海模型,还帮你错位好了,直接做一个模板渲染即可 + +着色方式按照喜欢的来,根据日光直射强度产生变化,根据光照方向产生偏移,根据光照color产生色相影响 + +等等都可以 + +我这边只加了一个日光直射强度变化,在0的时候把刘海阴影去除,没有主光直射的话,应该是不会产生这么明显的阴影效果 + +眼部阴影 + +眼部阴影效果跟大部分游戏一样,会给好一个面片然后做透明渲染 + +实现的时候用到了一张权重贴图 + +读取贴图,做一个强度渐变效果,甚至不用去做数学函数拟合,很方便 + +后处理 + +三个通用后处理,lut图调色 + bloom + 抗锯齿 + +后面两个按照读者自己的实现来就可以 + +lut图调色需要你去游戏里获取这么一张lut图资源 + +同时不要用urp自带的lut图volume,直接用现成的feature就行,在后处理之前执行调色 + +左边是调色前,右边是调色后,差异还是挺大的,可以根据需求去调整lut图影响强度 + +尾声 + +这篇的长度好像和上一篇flux water差不多了,还是感谢能看到这里的各位读者 + +希望这篇文章可以给你起到帮助,也希望你不要因为过长而感到厌烦,我确实比较喜欢写这种长文章,全面系统一点更适合他人学习,觉得太长了就交给ai工具吧 + +距离上次间隔了半年多,中途虽然比较忙,但也看了不了玩了不少,终末地还是台服和我胃口了,美术,玩法我都很喜欢,剧情暂且先不谈,主打一个相信,就是我连续5次大保是不是不太对啊 + +总之按照惯例,最后我想安利的作品是传颂之物即将推出的最新作 传颂之物 循白之证 + +作为传颂老粉,看到这一作标榜着系列ip的终结,心里的想法难以言表,传颂的手游也走到了尽头,可惜藤原叔走的太早了,种田嗓子也遭遇了不幸,好在手游里给小白等人物都算是给了一个幸福的结局 + +最后也想趁这个机会宣传一下传颂ip,本质可以看成以文字剧情形式呈现十分出彩的jrpg,并且是古早的那一套日式jrpg味道。但传颂的核心魅力应该在于所处的科技背景并非现代人类科技都市,经过了一次世界危机倒退回了农耕时代,但又遗留着过去的核心科技这种科幻背景。在里面和美少女冒险的同时会给一种莫名的放松休闲感,你会感觉这个社会恰好处于一个压迫又没那么压迫的氛围(二次元滤镜)。总之求求你了,玩一玩传颂吧hhhh + +有一说一,上哪里找又喜欢传颂,还玩方舟的人。 + +想说的就这些,下一篇文章应该是关于体积云和大气天空相关的,大海蓝天,实在是太美了,怎么都得全研究一下 + +那么祝各位读者学业事业双双顺利,咋们下次再见咯 diff --git a/InBox/zhihu_article_2028819446546932894.md b/InBox/zhihu_article_2028819446546932894.md new file mode 100644 index 0000000..ec37b86 --- /dev/null +++ b/InBox/zhihu_article_2028819446546932894.md @@ -0,0 +1,17 @@ +--- +title: 知乎文章 - 2028819446546932894 +source: https://zhuanlan.zhihu.com/p/2028819446546932894 +created: 2026-05-09 15:02 +tags: [zhihu, article, saved] +--- + +# 知乎文章 + +**URL:** https://zhuanlan.zhihu.com/p/2028819446546932894 + +**说明:** 此 URL 由 Hermes 保存,文章内容因知乎反爬虫机制(zse_ck)暂无法自动抓取。 + +> 请手动访问该链接查看完整文章内容,然后将正文粘贴到此处。 + +--- + diff --git a/InBox/zhihu_deterministic_build.md b/InBox/zhihu_deterministic_build.md new file mode 100644 index 0000000..ded239b --- /dev/null +++ b/InBox/zhihu_deterministic_build.md @@ -0,0 +1,377 @@ +--- +title: Unity 热更新的隐形命门:一篇讲透"确定性构建管线" - 知乎 +source: https://zhuanlan.zhihu.com/p/2028488903561163102 +created: 2026-05-09 15:35 +tags: [zhihu, article, unity, assetbundle, deterministic-build] +--- + +# Unity 热更新的隐形命门:一篇讲透"确定性构建管线" - 知乎 + +**来源:** [https://zhuanlan.zhihu.com/p/2028488903561163102](https://zhuanlan.zhihu.com/p/2028488903561163102) + +--- + +写在前面 + +文章是由AI生成的,最开始是为了当个人笔记,让AI补全一下相关内容 + +核心观点是确定性构建管线,这个在6000.x的官方文档里已经有明确说明 + +Deterministic builds + +如发现错误,以官方文档为准 + +起因:看到 Unity Partner Engineer 发的一篇帖子 Understanding Old Dependency Reuse in Unity AssetBundles,通篇在反复强调一个短语——deterministic pipeline。这个概念在中文社区里几乎没人讲透,但它恰恰是 AssetBundle 热更新体系的根基。本文尝试把它讲清楚。 +一、一个看似简单的问题,藏着一个深坑 + +先抛一个场景给你,你试着回答一下: + +线上已经下发了 Bundle A 给玩家。现在你新打了一个 Bundle B,B 依赖 A。你想让玩家继续用本地那个旧的 A,不要重新下载,可以吗? + +凭直觉回答,多数人会说”当然可以,A 没改嘛”。 + +但真相是:在”确定性构建管线”下可以,在”非确定性构建管线”下不行。 + +Unity 官方的说法原话是: + +It might work, but do not rely on it. (可能能用,但别指望它。) + +这个”能不能依赖”的分界线,就是Deterministic Build Pipeline。 + +二、到底什么是”确定性构建”? + +一句话定义: + +给定完全相同的输入(资源 + 代码 + 配置 + 工具链 + 环境),无论构建多少次、在哪台机器、谁来构建,输出的二进制文件字节级完全一致。 + +写成公式: + +same input + same pipeline = same output (byte-identical) + +初学者常常会有这样一种错觉: + +“我重新打了一次包,文件哈希没变,那我的管线肯定是确定性的。” + +这个理解方向对,但不够精准。我们需要区分两个概念: + +概念 性质 描述 +“输出未变” 结果现象 两次构建 AB 的 MD5/SHA 相同 +“确定性管线” 管线属性 只要输入不变,就必然输出不变的能力 + +换句话说——“输出未变”只是一次幸运的观测;“确定性”是一种架构保证。后者才是工程意义上的可依赖性。 + +三、为什么它是热更新的命门? + +让我们把场景画出来: + +上线日: Bundle A (已推送给 1000 万玩家) +次月更新: Bundle B (依赖 A),本次不想动 A + +你希望玩家的流量账单是这样: + +下载 B + 复用本地 A = 省下一整个 A 的流量 + +要做到这点,你必须回答一个问题:B 里记录的那个”指向 A 的引用”,今天还指得到吗? + +这就要讲 AB 的内部机制了。 + +AB 是怎么记录跨包引用的 + +每个 AssetBundle 内部是一个 SerializedFile,开头有一张 External References 表: + +External References +path(1): "Library/unity default resources" +path(2): "Resources/unity_builtin_extra" +path(3): "archive:/CAB-35fce856128a6714740898681ea54bbe/CAB-35fce856128a6714740898681ea54bbe" + +然后对象内部的引用长这样: + +m_Material PPtr + m_FileID : 3 ← 指向上面 path(3) + m_PathID : 6 ← 目标文件里的第 6 号对象 + +所以 B 里的一个引用本质是一个二元组: + +(file, object) = (CAB 路径, PathID) + +关键点:CAB 路径里那个 hash,是由”构建出的 SerializedFile“决定的,不是抽象的”Bundle A”。 + +现在你就能看懂这个”坑”了 + +如果管线是确定性的: + +A 没改 → 重新打也一定得到字节完全一致的 A → CAB 路径不变 → PathID 不变 +B 里的 (CAB-xxx, PathID=6) 仍然能 100% 命中旧 A +玩家本地的旧 A 可以安全复用,不用重新下载 + +如果管线是非确定性的: + +A 没改,但重新打出来的 A’ 可能CAB 哈希变了、或者对象顺序变了、或者PathID 漂移了 +B 里记的引用指的是”新 A’“的结构 +你让玩家复用旧 A → 可能看起来正常,也可能随机 crash + +这就是为什么 Unity 官方只敢说 “might work, but do not rely on it”。 + +构建管线的确定性,决定了你整个热更体系的可靠上限。 + +四、哪些行为会破坏确定性?踩雷清单 + +下面我把最容易踩的坑按四个层级整理出来。每个坑都用 “现象 → 原因 → 怎么修” 的格式来讲,方便直接对照排查。 + +A. 构建管线层 —— 你选错了工具或用错了姿势 +1. 🔥 搞混了几种 hash,把”输入 hash”当成了”内容指纹” + +现象:明明包内容变了,你的版本对比系统却说没变;或者反过来,内容没变,hash 却变了。 + +原因:Unity 内置 BuildPipeline.BuildAssetBundles 生成的 hash 是基于输入估算的,不是基于最终产物的内容。里面其实混着好几种 hash: + +hash 类型 它代表什么 +IncrementalBuildHash 用来判断”要不要重新打这个包”的输入指纹 +TypeTreeHash 用来判断”类型结构有没有变” +缓存 version hash 给 Caching 系统用的版本标识符 +最终二进制内容 包文件实际的字节 + +这四个东西完全不是一回事。如果你把前面某个 hash 直接当成 CDN 版本号,或者当成”两个包一不一样”的判断依据,就会出错。 + +Unity 6.6 手册也明确提醒了: + +The AssetBundle input hash isn’t an ideal value for tracking file versions. + +怎么修: + +如果你的发布系统需要精确判断”包内容到底变没变”,优先考虑 SBP 或 Addressables — 它们的 hash 是基于构建输出算的,更准确 +如果继续用内置 API,就别拿它的 hash 直接当内容校验,自己对产物算一遍 SHA256 更靠谱 +2. 以为开了 DeterministicAssetBundle 就万事大吉 + +现象:明明开了这个 flag,不同机器打出来的包还是不一样。 + +原因:BuildAssetBundleOptions.DeterministicAssetBundle 是老时代的知识。它只解决 PathID 分配的确定性问题。但在现代 Unity(6.x)里,破坏确定性的主要原因早已不是这个 flag,而是: + +收集资源时没排序 +构建脚本里塞了时间戳、随机数 +不同机器的导入缓存不一致 +构建环境(OS、Unity 版本)不一样 + +怎么修:不要只盯这一个开关。真正要盯的是整个构建流程的输入稳定性。 + +3. 打包前忘了切 Build Target + +现象:CI 上打 Android 包,但编辑器的 Active Target 还停留在 iOS。打出来的包和手动打的不一致。 + +原因:如果你调用 BuildPipeline.BuildAssetBundles 时指定的平台和当前 Active Target 不一样,Unity 会悄悄触发资源重导入和脚本重编译。更坑的是,正在跑的编辑器脚本(比如你的打包工具、构建回调)是按旧 Target 编译的,可能出现平台条件判断错乱。 + +怎么修:CI 脚本里,打包之前先显式切换 Active Target: + +EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android); +B. 资源数据层 —— 你的资源本身就不稳定 +4. AB 名字里带了日期或构建编号 + +现象:每天打包,即使资源没变,包名都在变,hash 自然跟着变。 + +坏例子 vs 好例子: + +❌ ui_mainmenu_20260417.bundle ← 每天都变 +❌ ui_mainmenu_build_2841.bundle ← CI 编号会变 +✅ ui_mainmenu ← 稳定标识 +✅ ui_mainmenu_v1.2.3 ← 语义化版本 + +Unity 6.3⁄6.6 文档的原话: + +Avoid timestamps or build-date suffixes in AssetBundle names and use static identifiers or semantic versions. +5. ⭐ 收集资源时没排序(最常见的坑!) + +现象:同一台机器连续打两次,包的 hash 居然不一样。更常见的场景是 A 电脑打出来的包和 B 电脑的不一样。 + +原因:AssetDatabase.FindAssets() 和 Directory.GetFiles() 返回的顺序不保证一致。不同机器、不同文件系统、甚至同一台机器在不同时间调用,顺序都可能飘。一旦资源添加顺序飘了,序列化数据的排列就飘了,hash 自然就变了。 + +坏写法: + +// ❌ 直接遍历,顺序不可控 +var guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/UI" }); +foreach (var guid in guids) +{ + group.AddAsset(guid); +} + +好写法: + +// ✅ 加一行排序,问题消失 +var guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/UI" }) + .OrderBy(x => x, StringComparer.Ordinal) + .ToArray(); + +记住这条规则:凡是涉及 FindAssets、GetFiles、EnumerateFiles 的地方,必须手动排序。 + +6. 往 ScriptableObject 里塞数据时,塞入顺序不稳定 + +现象:和上面一样,不同机器或不同时间构建出的包不一致。 + +原因:如果你在构建前用 IPreprocessBuildWithReport 之类的回调,动态往 SO 的 List 字段里塞东西,塞的顺序不稳定,序列化出来的字节就不稳定。 + +Unity 6.6 文档说得很明白: + +If the order of objects in that array isn’t explicitly sorted, the serialized data might differ between machines or consecutive builds. + +怎么修:喂给 SO 的数据,在写入之前先排好序。 + +7. 资源里写入了随机值、时间戳、机器名 + +现象:每次构建,某些 SO 文件的内容必定不同。 + +坏写法: + +// ❌ 每次构建都不一样 +so.buildGuid = Guid.NewGuid().ToString(); +so.buildTime = DateTime.UtcNow.Ticks; +so.machine = Environment.MachineName; + +这类字段一旦进了序列化数据,确定性就直接被你自己亲手打穿了。 + +C. 环境与工具链层 —— 你以为机器都一样,其实不一样 +8. Unity 版本不一致 + +一句话:哪怕只差一个小版本号,序列化格式、导入器行为、Shader 编译都可能有细微差异。 + +怎么修:CI 锁死 Unity 版本,精确到补丁号(比如 6000.0.32f1),团队所有人用同一个版本。 + +9. 操作系统或 CPU 架构不一致 + +现象:Windows 的 CI 和 Mac 的 CI 打出来的包不一样,尤其是包含动画、粒子的包。 + +原因:浮点数运算在不同 CPU 架构上的舍入行为不同。Unity 6.6 文档原话: + +Floating-point rounding behavior differs between architectures, which can change serialized float values especially in AnimationClips or imported Mesh data. + +怎么修:所有 CI 构建机用同一种 OS + 同一种 CPU 架构。 + +10. Git 自动换行没关掉 + +现象:Windows 开发者提交的文本文件是 CRLF,Mac 开发者的是 LF。文本资源的 hash 莫名其妙对不上。 + +怎么修: + +git config --global core.autocrlf false + +再在 .gitattributes 里统一声明所有文本文件用 LF。 + +11. 不同机器的 Library 缓存状态不同 + +现象:同样的项目,A 机器是全新 clone(Library 是新生成的),B 机器已经用了很久(Library 有大量缓存)。两台机器打出来的包不一样。 + +原因:Unity 的资源导入结果会受 Library 缓存状态影响。Unity 曾修过一个著名 bug(UUM-114616):Shader AB 在”干净 Library”和”有缓存 Library”下产出不同。 + +怎么修:用 Unity Accelerator 让所有构建机共享同一份导入结果。但注意:Accelerator 只缓存导入产物(imported assets),不缓存打出来的 AB 包本身。它帮你统一了导入阶段,但不是万能保险。 + +D. 脚本与代码层 —— 改了代码,资源也跟着动 +12. 脚本改动的涟漪效应 + +现象:明明没动资源,一堆 AB 的 hash 都变了。 + +原因:脚本的某些改动会导致 MonoScript 身份信息变化,进而影响引用了这些脚本的 AB。但具体是哪些改动有影响,需要搞清楚: + +会影响 AB 的脚本改动 不会影响 AB 的脚本改动 +脚本移到另一个 asmdef 下 只改注释 +asmdef 改名 只改方法内部实现(不涉及序列化字段) +改类名或命名空间 只改 private 字段(非序列化的) +改 [SerializeField] 字段的类型/名称 +丢失 .meta 文件后重新生成 +13. 构建回调里偷偷写入了变化数据 + +现象:每次构建的 Scene AB 都不一样,但场景文件明明没动。 + +原因:你(或某个插件)在 IProcessSceneWithReport 之类的回调里写了类似这样的代码: + +// ❌ 每天构建出来的 name 都不一样 +public void OnProcessScene(Scene scene, BuildReport report) +{ + foreach (var go in scene.GetRootGameObjects()) + { + go.name = $"Built_{DateTime.Now:yyyyMMdd}"; + } +} + +怎么修:构建回调里严禁使用 DateTime.Now、Guid.NewGuid()、Environment.MachineName 等非确定性 API。 + +14. Shader 变体集合不稳定 + +现象:不同机器或不同时间打的包里,Shader 变体数量不一样。 + +原因:如果你的 Shader Variant Collection (SVC) 是通过运行时录制收集的,那它完全取决于”测试的人跑了哪些路径”。不同人、不同时间跑出来的变体集合自然不同。 + +怎么修:大型项目应该用离线枚举 + 手动维护的方式管理 SVC,而不是依赖”录制式收集”。 + +五、如何保障确定性:一份能落地的清单 +工程约定 +[ ] 所有自动化脚本收集到的资源集合,统一做稳定排序 +[ ] 禁止把时间戳、构建编号、机器名写进 AB 名称或序列化资源 +[ ] 明确区分输入 hash、缓存 version hash、内容校验 hash,避免混用 +[ ] Git 关闭自动换行转换,并用 .gitattributes 固定文本换行策略 +[ ] 构建回调中禁止使用 DateTime.Now、Guid.NewGuid 等非确定性来源 +[ ] 若项目强依赖内容级哈希语义,优先评估 SBP 或 Addressables +[ ] Addressables 项目里,catalog 是依赖身份的权威来源,不要手工替换 bundle +CI/CD 约定 +[ ] 锁定 Unity Editor 版本,精确到补丁号 +[ ] 固定构建机的 OS 与 CPU 架构 +[ ] 构建前对齐 active target 或 build profile +[ ] 使用 Unity Accelerator 统一 imported assets +[ ] 发布构建尽量采用 clean build +[ ] ProjectSettings、构建配置全部纳入版本控制 +Code Review Checklist +所有 FindAssets、GetFiles、EnumerateFiles 的结果是否稳定排序 +所有写入 SO 的数组、列表、映射是否有稳定顺序 +是否写入了时间、随机数、机器相关元数据 +是否有 build callback 在构建时改写资源 +是否把某种 hash 误当成了内容校验或发布版本号 +六、最后的防线:双跑比对 + +在 CI 里做双跑比对: + +不修改任何资源 +在相同参数下连续构建两次 +对每个 AB 文件计算 SHA256 +任何哈希不一致,立刻告警并定位差异 + +定位阶段,用 UnityDataTools 的 textdumper 把两个版本的 SerializedFile 导出成可读文本后直接 diff。 + +七、一些延伸思考 + +写到这里,回头看 Unity 那篇原帖里一段意味深长的话: + +The reason I initially missed the point is that this question is only interesting when that assumption breaks. (我一开始没 get 到这个问题的点,因为这个问题只有在”确定性假设被打破时”才有意义。) + +这句话其实在说一个很工程哲学的事情: + +构建的确定性是一种”隐形的契约”。它平时不存在、不被感知,但一旦破裂,会带着你整个热更体系一起塌方。 + +很多商业项目上线多年都没踩过这个坑,不是因为管线真的确定性,而是因为: + +每次版本更新都全量重推所有依赖包(成本掩盖了问题) +或者依赖关系极简(运气好到没引用跨包的老资源) + +但一旦你想要做差分热更、按需下载、跨版本依赖复用这些更精细的运营动作,确定性就是你绕不开的地基。 + +八、结语 + +一篇帖子引出的概念,最后会延展成一整套工程规范。回到最初的问题: + +打包出来的 AB 完全没变,就是确定性管线吗? + +答案是: + +“输出没变”是确定性管线的一种观测现象;”确定性管线”是只要输入不变就保证输出不变的架构能力。在热更场景下,后者才是你能信赖的东西。 + +如果你负责的是一个需要长期运营的 Unity 项目,强烈建议今天就做两件事: + +在 CI 上加一次”双跑比对”,看看你们的管线现在到底是什么状态 +把本文的”踩雷清单”打印出来,贴在构建平台的 PR 模板里 + +地基打牢之前,所有的热更优化都是在流沙上盖楼。 + +参考资料 +Unity 官方讨论:Understanding Old Dependency Reuse in Unity AssetBundles +Unity 6.3 LTS 手册:AssetBundle and Addressables determinism +Unity 6.6 手册:Deterministic builds introduction +工具:UnityDataTools +包:Scriptable Build Pipeline + +如果这篇内容对你有帮助,欢迎点赞/收藏。 diff --git a/InBox/zhihu_replay_system.md b/InBox/zhihu_replay_system.md new file mode 100644 index 0000000..d3bb48c --- /dev/null +++ b/InBox/zhihu_replay_system.md @@ -0,0 +1,100 @@ +--- +title: 回放系统让你的游戏时光倒流 - 知乎 +source: https://zhuanlan.zhihu.com/p/2031265691286947108 +created: 2026-05-09 17:43 +tags: [zhihu, article, game-dev, replay-system, sync-mechanism] +--- + +# 回放系统让你的游戏时光倒流 - 知乎 + +**来源:** [https://zhuanlan.zhihu.com/p/2031265691286947108](https://zhuanlan.zhihu.com/p/2031265691286947108) + +--- + +打一盘很爽快的游戏或者遇到了恼人的bug,此时你一定想要一个“月光宝盒”,能回放你的游戏过程。然而在很多游戏的初始开发阶段,回放系统的优先级都是比较低的,等到真正要去实现回放系统时发现现有的一些架构会带来很多困难,不免感叹“如果当初...”。 + +我们不妨假设拥有了“月光宝盒”,回到游戏最初开发的阶段,看下怎么设计好一个回放系统。 + +回放深度绑定同步机制 + +回放的前提是录制游戏运行数据,然后根据数据重新执行游戏流程。录制时的性能消耗、数据的存储大小以及重新运行数据的复杂程度都是必须考虑的因素。再考虑到为了复现bug的目的,回放时要尽可能走正常游戏时一样的数据处理逻辑。稍加思考就能发现,不同的游戏同步机制下,这些问题的解决方案会大相径庭。所以,我们也必须区分讨论。 + +状态同步下的回放方案 + +采用状态同步机制的游戏,客户端表现主要是通过与服务器的交互状态数据包驱动。那么,直接将客户端收发的数据按照时间顺序保存下来,然后重新创建一场游戏去加载这些数据包,能实现回放吗?很遗憾,这种方案是行不通的。 + +1P困境 + +因为1P玩家的往来数据包并不能完全还原当时的操作,比如最基础的位置信息,客户端通常是先依据操作计算出位置,然后将位置发送给服务器,服务器进行一定的校验后如果通过则不会返回任何数据给客户端,可以看出1P玩家的位置完全是由本地事件驱动的(先不考虑弱网情况)。 + +为了极致的手感,很多操作都是1P本地先行触发,然后上报服务器进行校验和广播。既然只保存1P的交互协议数据是不完整的,那能不能同时记录本地操作的事件呢?只能说原理上可行,但实现上很难。 + +游戏中有很多的操作输入口,你需要重构这些输入层统一汇总到一个管线,然后在管线中执行录制操作,录制的信息格式要能反映出多种类型的输入,后续的响应逻辑也要按照约定的参数进行设计。想一想就知道,涉及面很广,涉及的人很多,如果版本迭代多年后才要开发回放系统,这种情况下很难推动方案的落地。况且,你也不想Bug缠身天天加班不是?最后,如果游戏逻辑中事件响应还有随机逻辑,你必将面临随机数的确定性问题,估计又要打一个很大的补丁。 + +换个观察者视角 + +揉一揉疲惫的脑袋,想一下游戏比赛中转播的画面是怎么做的?主持人选择一个目标角色,画面就能切换到这个玩家的视角。实际上,服务器会为主持人创建一个observer,设定其观战某个target玩家,所有要转发给target的数据包,都会转发给observer。 + +假如给开启回放的玩家创建一个隐藏的observer,然后把target设置为自己,可行么?这绝对是可行的。客户端只要保存observer相关的数据包就能用来回放。 + +数据包回放 + +保存数据包时同时记录其时间,再回放时可以设定一定的帧率(比如30帧)驱动客户端Update。网络层并不发起真正的socket连接,而是直接设定为已连接状态,网络模块Update改为从回放数据包中取数据。依照时间取出所有33ms内的数据包,数据包处理走正常3P一样的处理逻辑。 + +在移动设备上,如果每条数据包都立刻落盘到文件,累积会有很大的IO消耗。可以采用批量写入的方案,积累到一定条数的数据包以后,统一写入文件。写入文件的操作,可以放到单独的线程中执行。 + +性能“打脸” + +当你将功能自测完毕,交由性能测试时,无情的报告单显示"数据流量增长、发热出现增长"。是啊,多出一个observer的数据发送,必然会导致这样的结果。 + +你揪着越来越少的头发,漫无目的地对比1P普通情况下和开启replay情况下的数据包日志文件。突然发现,observer的数据包和1P数据包绝大部分都是重合的,只是多了些1P没有必要的数据包。你突然有了精神,能不能使用1P的数据包伪造出observer的数据包呢! + +基于差分的缝合怪 + +经过一番代码排查,最终确认只要为1P补发一些observer初始化数据,以及几大类1P操作对应的广播数据包就行。那些广播数据包,通常都有集中的逻辑流管线,处理起来并不麻烦。 + +此方案完美地解决了流量增长和发热增加的性能问题。 + +功能交付QA测试后,又报过来一个新的问题,总是感觉角色移动和一些关键操作慢了一拍,这是为什么呢?问题在于observer下发数据包自带的延迟(网络延迟+处理延迟)! + +如果简单地为这些关键数据包统一做延迟修正,能有所改善,但是延时设置为多少是个问题。设置过大可能会早于实际的操作发生时间,就会与其他正常的数据包时间错位,产生“穿帮”,设想下被击中者还没有走到位置,你的击中操作已经显示了,这是无法接受的。 + +此时就要采用非常规的手段了,能不能将这些关键的数据包对应的IP发送包C2S包处理成S2C包呢?这样时间肯定是正确的,避免强行修正延迟导致的数据包之间的时间错乱问题。数据包格式的转换也并不复杂,大部分字段都是复用的,仅需要做简单的头部调整即可。 + +快进和回退的难处 + +在回放时,快进和回退是必不可少的功能。要实现快进,只要在极短的时间里快速处理跳过时间段里的所有数据包即可。很显然,如果是小时间段的快进,处理会很顺畅,但是如果跳过的时间段非常大,那么快进消耗的时间就很明显了,必须考虑做遮罩处理避免看起来画面卡住。 + +虽然通过一定时间间隔保存全量快照的方式可以缓解长时间段快进的耗时(直接调到最近的快照点),但这会给录制带来计算和存储压力,不一定划算。 + +回退的问题更大,因为你无法从当前状态通过向后回退数据包而把场景也回退了。唯一的方法只能是从头重新播放,然后以快进的方式调到回退点,让玩家看起来在回退。 + +帧同步就比较简单了 + +因为帧同步天然的“只同步输入,不同步状态”,只要输入一致,客户端演算的结果就一致,所以录像就比较简单了,只要全部录制发送和接收的操作指令即可。而且,因为输入指令的数据量很小,所以整场比赛的录像数据也很小,存储和IO压力可以忽略不计。 + +至于快进和回放面临的问题与状态同步下的问题本质一样,这里不再赘述。不过,采用帧同步的游戏,场景实体数量通常较少,所以可以更多地考虑关键帧快照的优化方式。 + +更酷的死亡回放 + +战斗中被人击杀了,你一定很想立刻知道“TM谁杀了我!”。死亡回放可以让你查看死亡前几秒内发生的事情,一定对你的“复仇大业”很有帮助。 + +死亡回放的关键是要服务器时刻为每个玩家保存一段可回放数据包,因为你不知道自己会被谁杀死,只有在被击杀的时候你才能确定要回放的“凶手”视角。毫无疑问,直接为所有玩家保存回放数据包,服务器的压力会很大。好在通常也只需要保存不到10s的时间段,所以还能接受。显然,这种不完整的录像数据段必须配合全量快照才能工作。 + +以10s为例,要设计一套双缓冲存储机制。假设命名为P缓冲和N缓冲。当P写满后,开始写入N,如果N写满后,再次写入P,如此往复。每次写入新的缓冲时,都生成服务器全量快照。当死亡点发生再N中时( N_d ),则从P的全量快照开始恢复游戏场景,然后播放录像到N_d。 + +关键是断线重连 + +对于有复活机制的玩法,播放死亡回放时玩家还在游戏对局中,那么服务器怎么看待这个玩家的状态呢?如果当作在线的玩家,一些广播的数据包就还要通知客户端,这必然导致和正在播放的死亡回放场景冲突。另外,玩家死亡回放播放结束后,还要回到正在进行的游戏场景中。 + +这段时间在服务器看来如同客户端发生了断线重连一样。是的,关键就是断线重连!依靠断线重连,如同为死亡回放在客户端构建了一个平行宇宙,完全不会对服务器架构和逻辑产生过多的影响。而且,重连机制通常是游戏的基础设施之一,直接拿来用“何乐而不为”。 + +当然,死亡回放是模拟断线重连的表现,但是并不能真的完全断线,一些重要的即时数据还是要接收的,比如被复活的消息。可以制定协议白名单,处理这些特殊的逻辑,既保证不影响死亡回放的场景,又能让玩家不漏掉重要信息。 + +清理场景的脏活儿 + +为了死亡回放而清理场景中的实体以及HUD资源并非想象中容易,对于迭代了几年的游戏来说,并不一定有统一的卸载逻辑收口,难免需要人工排查清理,还有一些全局的数据状态都要特别处理,后续添加的新逻辑也都要考虑死亡回放的情况。因进出死亡回放引发的bug也经常发生,此为头痛之事,尚没有好的解决办法。 + +性能优化的好帮手 + +性能优化的一大难点是如何验证优化是有效的,可重现的基准场景能提供极大的帮助,而回放系统就能提供可重现的基准场景。以同样的录像数据包,在优化前和优化后的客户端中播放录像,对比性能数据能有力的验证优化的效果,因为能排除玩家数量、操作行为、时序等变量的干扰。所以,早日实现回放系统能给你带来很多意想不到的好处。 diff --git a/InBox/从需求到交付-一套基于AI辅助的高质量代码生产实践.md b/InBox/从需求到交付-一套基于AI辅助的高质量代码生产实践.md new file mode 100644 index 0000000..e79a30a --- /dev/null +++ b/InBox/从需求到交付-一套基于AI辅助的高质量代码生产实践.md @@ -0,0 +1,42 @@ +# 从需求到交付:一套基于 AI 辅助的高质量代码生产实践 + +**来源:** [微信公众号 - OLDLIE](https://mp.weixin.qq.com/s/V8AR1ooSgrafZgH6KgEEOw) +**作者:** oldlie +**日期:** 2026年4月11日 19:32 +**标签:** #AI编码 #代码质量 #交付流程 #OpenSpec #Superpowers + +--- + +## 核心流程 + +### 1. 需求分析与关键点识别 +使用 `openspec explore` 指令向 AI 提出概要需求。核心目的是借助 AI 的信息处理能力,快速识别需求中的关键点、潜在风险和边界条件。 + +### 2. 制定分阶段实现路线图 +利用 `superpower` 技能,要求 AI 将需求转化为清晰、可执行的路线图(Roadmap),将整个项目拆解为多个可管理的阶段。 + +### 3. 前置条件确认与详细设计 +进入每个新阶段前,让 AI 确认所有前置条件是否满足。确认后协作进行详细设计,要求 AI 输出包含后台代码详细设计和关键过程时序图的设计文档,并严格参考既定的代码规范。 + +### 4. 设计审查与规范对齐 +设计文档完成后进行严格审查,确保实现方案完全符合代码规范。**"先设计,后编码"** 的模式让问题在早期被发现和解决,效率远高于在代码写完后再去分散阅读源文件。 + +### 5. 代码实现与自动化审查 +AI 完成代码编写后立即执行 `/review` 指令。`/review` 模式关注的维度(安全性、健壮性)与默认编码模式不同,要求更高,能发现单元测试难以覆盖的逻辑问题。 + +此外,AI 有时倾向于使用最简单而非最高效的方式实现功能,或在处理长上下文时出现"偷懒"现象——人工触发的全面审查必不可少。 + +### 6. 查漏补缺与迭代循环 +`/review` 之后让 AI 根据路线图再次检查当前阶段是否存在遗漏,确认无遗漏且满足进入下一阶段的条件后,再开启新一轮循环。 + +--- + +## 流程设计的深层思考 + +| 设计要素 | 价值 | +|---------|------| +| **路线图** | 将宏大目标拆解为具体步骤,减少单次交互的上下文信息量,确保整体目标不偏离 | +| **详细设计文档** | 集中审核设计逻辑比分散阅读代码更高效,更容易发现深层次问题 | +| **分步设计** | 管理上下文窗口长度,确保 AI 在每个环节保持高效和精准 | + +**核心理念:** 通过精心设计的步骤、指令和审查机制,引导 AI 成为"结对编程"伙伴,共同交付高质量的代码。 diff --git a/InBox/基于Harness-SDD-多仓管理的AI全栈开发实践.md b/InBox/基于Harness-SDD-多仓管理的AI全栈开发实践.md new file mode 100644 index 0000000..0bed0d8 --- /dev/null +++ b/InBox/基于Harness-SDD-多仓管理的AI全栈开发实践.md @@ -0,0 +1,80 @@ +--- +title: 基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践 +source: https://mp.weixin.qq.com/s/ygQGSH5c7GHYDvkqWoQTXQ +author: 盖伦 / 得物技术 +date: 2026-05-06 +tags: [AI开发, 全栈, SDD, Harness, Cursor, Claude Code] +--- + +# 基于 Harness + SDD + 多仓管理模式的 AI 全栈开发实践|得物技术 + +## 一、核心理念:Harness 思维 — 让 AI 模仿,而不是凭空创造 + +### 全栈AI开发最容易踩的坑 +让AI从零开始写代码产生"外星代码":风格不一致、复用率低、采纳率低。AI生成了代码,但Review成本和返工成本反而更高了。 + +### Harness 思维的核心:给 AI 一个"模仿对象" +给AI一个已有的实现作为参照,让它照着复刻一份,而不是凭空创造。 + +**四条原则:** + +| 原则 | 说明 | 举例 | +|------|------|------| +| 找相似实现 | 在代码库中找到功能最相似的已有实现作为参照 | "结束语"参照"场景化欢迎语" | +| 复用优先 | 能复用的组件、接口封装、数据结构直接复用 | 复用greetingExtendInfo数据结构 | +| 模仿着复制 | "抄一份改一改"比用新方式好 | Controller/Service/Repository按已有模仿 | +| 约束生成范围 | 提示词中明确指定参考文件/参考接口 | 前端修改入口@FeatureTable/index.tsx:53-58 | + +### 提示词体现 Harness +- ❌ 不推荐:`请实现一个结束语管理的 CRUD 接口` +- ✅ 推荐:明确指定参考文件、数据结构和接口路径,如"参照场景欢迎语功能(后端/api/v1/feature/list,前端FeatureTable/index.tsx:53-58)实现" + +## 二、全栈工作区搭建与 Codebase Indexing + +将前后端代码放在同一个工作区下的三个核心价值: +1. **Codebase Indexing**:Cursor对工作区内所有代码进行向量化嵌入建立语义索引,AI能跨仓库理解代码关系 +2. **上下文完整**:AI同时能看到前后端代码,接口字段、命名风格自然对齐 +3. **SDD文档集中管理**:前后端SDD文档在同一工作区,便于接口契约对齐 + +### Cursor vs Claude Code 实测对比 + +| 功能维度 | Cursor | Claude Code | +|---------|--------|-------------| +| 代码库语义索引 | 支持grep+语义检索,速度快 | 仅支持grep,依赖模型能力 | +| 代码生成速度 | 极速,平均1-3分钟 | 中速,平均3-30分钟 | +| 代码采纳率 | 两者相当 | 两者相当 | +| 文件/代码段引用 | 快捷键、拖拽即可引用 | 需手动@文件路径,无法引用代码段 | +| 多Agent | 默认开启(多Tab并行) | 需手动注册子Agent | +| 费率模型 | 失败任务不收费 | 失败任务耗时长,容易浪费Token | +| 历史会话恢复 | 仅能查看当前项目会话记录 | 可查看全局会话记录 | +| 综合评价 | 快速迭代首选,推荐Composer2模式 | 长链路复杂任务可用 | + +## 三、SDD 驱动的全栈代码生成流程 + +- 全栈SDD需同时覆盖前后端 +- 提示词编写范式:明确需求、参考实现、数据结构、接口契约 +- 前后端需求点清单分工示例 +- SDD文档产出与指令使用说明 + +## 四、多 Agent 协作:前后端并行开发 + +- Cursor中使用多Tab并行(默认开启) +- Claude Code中使用Subagent能力(需手动注册) +- 建议:前端Agent专注UI/交互,后端Agent专注API/数据 + +## 五、前后端联调:Mock 数据与分阶段验证 + +- 三阶段验证策略 +- Mock数据编写要点 +- 后端独立构建验证 +- 前后端联调步骤 + +## 六、警惕 SDD 陷阱:测试如何介入全栈研发 + +- SDD不等于需求文档 +- 关注隐性功能(异常处理、边界情况、性能要求) +- 测试应尽早介入 + +## 七、综合效益与总结 + +核心公式:**Harness(约束) + SDD(规格) + 多仓(上下文) = 高质量AI全栈代码** diff --git a/InBox/天猫新品团队AI编码实战指南(下).md b/InBox/天猫新品团队AI编码实战指南(下).md new file mode 100644 index 0000000..3292457 --- /dev/null +++ b/InBox/天猫新品团队AI编码实战指南(下).md @@ -0,0 +1,78 @@ +# 天猫新品团队AI编码实战指南(下) + +**来源:** [微信公众号 - 大淘宝技术](https://mp.weixin.qq.com/s/iRkxznDYhE-kXjbIHlrnNA) +**作者:** 天猫新品营销技术 +**日期:** 2026年5月8日 16:07 +**标签:** #AI编码 #天猫 #全栈化 #知识库 #实战指南 + +--- + +## 背景 + +团队开始了后端向前,前端向后的全栈化转型运动。后端承担小二工作台与研发工具开发(需求驱动型),前端承担 C 端 solution 编写。 + +**核心思想:** 作为业务团队,重点应该是沉淀自己团队的工作流与 AI 资产——通过**最大化复用**来提高整体效能(代码、知识、工作流、工具的复用)。 + +### 场景分类 + +| 严苛程度 | 典型场景 | 错误容忍 | 交互问题容忍 | 代码要求 | 视觉还原要求 | +|----------|---------|---------|-------------|---------|-------------| +| 最为严苛 | C 端频道 | 0 容忍 | 明显则 0 容忍 | 高 | 高 | +| 较为严苛 | B 端商家平台 | 0 容忍 | 特别明显则 0 容忍 | 中 | 中 | +| **交付分割线** | | | | | | +| 普通严苛 | 小二端工作台 | 0 容忍 | 影响主链路则 0 容忍 | 低 | 低 | +| 低严苛 | 研发自用工具 | 一定程度容忍 | 无 hack 绕过则 0 容忍 | 无 | 无 | +| 不严苛 | 研发 DEMO | 能意会则容忍 | 怎么都能容忍 | 无 | 无 | + +--- + +## 小二端 - AI 主导的对话生码 + +特点:无视觉还原要求,实现形式自由,页面独立性强,适合 AI 编码完成全部需求。 + +### 初期 - 统一生成方案 +- 提供标准化的代码规范与视觉规范文档 +- 提供高度封装的代码模版(一行代码唤起页面组件) + +### 中期 - 辅助补齐前端经验短板 +- **需求描述不准确**:搭建「AI案例实践中心」,标准化 prompt 模板;提供 MCP 速查工具「天猫新品业务编码助手」 +- **垂类场景无经验**:将 AI 生码省下的时间用于生码沉淀,详细记录实现过程与踩坑心得 + +### 后期 - 更简单、无感、一致的方案 +- 开发轻量级团队知识库,以类 Skill 形式封装开发规范与代码模板 +- 知识库用 git 仓库管理,npm 包作为资源承载与版本管理工具 + +--- + +## C 端 - 交付质量要求的 AI 提效 + +C 端场景复用机制复用分为五个级别: +1. **代码复用**:对组件、布局、逻辑进行封装(RCG——布局/组件/脚手架粒度递进) +2. **知识复用**:团队知识库 + 逐层加载 +3. **工作流复用**:固定 AI 工作流 + 兜底方案 +4. **工具复用**:通过 MCP 协议 + 内部工具获取业务数据 +5. **人机分工**:固定重复工作交 AI,关键环节人把控 + +### 视图分离方案 + +**核心思想:** 把一份 PRD 分成"给 AI 的结构化描述"和"给人看的说明文档",各自优化。 + +- 在 C 端,过复杂的 prompt 和过长的上下文都会带来性能问题 +- 提出了结构化的 prompt 写法,包含场景分析、关键数据、交互与动效说明 + +### 知识库建设 + +对标 OpenAI 的 `prompt.md` + `--preamble` 的标准化管理: +1. 定义优先级规则 +2. 使用 script 进行文件注入(含自动 chunk、优先命中机制) +3. 统一文件结构和索引 + +--- + +## 实用技巧集锦 + +- **UI 重构**:利用 prompt 对图片转结构化描述 +- **复杂 Prompt 构建**:结构化多段式 +- **多方案选优**:让 AI 给出多个方案并对比 +- **文档生成**:代码完成后自动生成说明文档 +- **严厉语气 + 合理质疑**:实验表明严厉语气能提升准确率 diff --git a/InBox/微信文章_c460ef2f9e36ffbdc1083dd36c2595b6.md b/InBox/微信文章_c460ef2f9e36ffbdc1083dd36c2595b6.md new file mode 100644 index 0000000..4f32847 --- /dev/null +++ b/InBox/微信文章_c460ef2f9e36ffbdc1083dd36c2595b6.md @@ -0,0 +1,13 @@ +# 微信文章(未抓取到内容 - 触发验证码) + +**来源:** [微信公众号文章 (未知公众号)](https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650543356&idx=1&sn=c460ef2f9e36ffbdc1083dd36c2595b6) +**日期:** 2026年(未知具体日期) +**标签:** #微信文章 + +--- + +> ⚠️ 该文章在抓取时触发了微信的 CAPTCHA 验证码保护,未能获取到内容和标题。 +> +> 原始 URL 参数:__biz=MzAxNDEwNjk5OQ==, mid=2650543356, idx=1, sn=c460ef2f9e36ffbdc1083dd36c2595b6 +> +> 建议在微信客户端内手动打开,或使用 WeChat 官方 API 获取。 diff --git a/InBox/测试笔记_共享转移_20250320.md b/InBox/测试笔记_共享转移_20250320.md new file mode 100644 index 0000000..1ce9e69 --- /dev/null +++ b/InBox/测试笔记_共享转移_20250320.md @@ -0,0 +1,20 @@ +--- +title: 测试笔记 - 共享目录转移 +date: 2026-03-20 +tags: [测试, 共享目录] +--- + +# 测试笔记 + +这是一篇测试笔记,用于验证共享目录转移流程。 + +## 创建信息 +- 创建时间: 2026-03-20 03:01 +- 来源: Mac mini 共享目录 +- 目标: zanepc Obsidian InBox + +## 测试内容 +如果这篇笔记能成功转移到 zanepc 的 Obsidian 中,说明流程配置正确。 + +--- +*自动创建用于测试共享目录转移* diff --git a/InBox/淘天营销中后台生码工作流最佳实践.md b/InBox/淘天营销中后台生码工作流最佳实践.md new file mode 100644 index 0000000..d0c4718 --- /dev/null +++ b/InBox/淘天营销中后台生码工作流最佳实践.md @@ -0,0 +1,86 @@ +# 淘天营销中后台生码工作流最佳实践 + +**来源:** [微信公众号 - 大淘宝技术](https://mp.weixin.qq.com/s/VjTyHFr17l_bObG6x8-Mmw) +**作者:** 营销前台技术团队(大淘宝技术) +**日期:** 2026年4月27日 16:16 +**标签:** #淘天 #营销中后台 #AI生码 #云端托管 #工作流 #大淘宝技术 + +--- + +## 升级路径总览 + +核心决策:从"本地+云端双路径" → **统一收敛至云端托管生码**(基于 AoneSuper) + +**三个核心工程:** +1. 跨仓库工作区(git submodule + turborepo) +2. 可编排场景化工作流 +3. 知识自动沉淀(功能树 + 领域 Skill) + +**核心方法论:** +- 给恰好够用的精确知识 +- 确定性逻辑交工程 +- 知识建正向循环 + +--- + +## 为什么弃用本地研发 + +| 问题 | 具体表现 | +|------|---------| +| 环境配置难统一 | 系统版本、Node 版本、网络代理差异巨大,排查困难 | +| AK 管理困难 | 生态用工需明文存储 AK 在个人设备,分发/轮换/回收无管控 | +| 执行易中断 | 电脑息屏/网络断开导致长任务中断,需手动续跑 | + +## 为什么选 AoneSuper 而非自建 + +S1 自建了基于 LangGraph 的多 Agent 架构,但发现: +- 基建维护成本高 +- CodeAgent CLI 社区生态(Skills、MCP、SubAgent)迅速成熟 +- 自建 LangGraph 方案边际收益递减 + +**结论:** 接入 AoneSuper,投入重心从基建打磨转向业务效果优化。 + +--- + +## 跨仓库工作区 + +### 设计思路 + +1. **聚合需求仓库**:创建"需求工作区"文件夹,聚合所有相关仓库 +2. **引入 git submodule**:外层文件夹作为独立 git 空间,用于存放 Agent 配置(Skills、MCP、SubAgent)和中间产物(需求理解、方案设计、任务列表等) +3. **消除副作用**:通过脚本自动化 submodule 操作,降低同学理解成本 + +### 研发调试优化 + +利用 **turborepo** 的 monorepo 构建能力,实现: +- 一键启动所有需求子仓库服务 +- 自动配置子仓库间的依赖 link +- 解决多层依赖(基础组件 → 业务组件 → 前端业务)的调试问题 + +--- + +## 可编排场景化工作流 + +### 两种场景的差异化策略 + +**场景一:迁移/重构(高确定性)** +- 已有明确的"A 迁移到 B"的确定性逻辑 +- 架构说明文档 + 领域 Skill 固化规则 +- 将迁移/重构的逻辑转化为可复用的领域能力 + +**场景二:日常迭代(低确定性)** +- 需求边界模糊,需要大量上下文 +- 引入**功能树**实现精准查表式知识供给 +- 辅助 D2C(Design-to-Code)/ API 还原优化 + +### 功能树 + +核心思想:将一个项目的结构化知识(路由、组件、数据流、API)提取为树形索引,Agent 在接到需求时可以快速"查表"定位到代码位置,而不是大海捞针。 + +### 知识自动沉淀 + +通过持续积累,形成**提效飞轮**: +1. 生码过程中发现知识盲区 +2. 补充功能树 / 领域 Skill +3. 下次生码质量提升 +4. 释放人力持续补充更多知识 diff --git a/InBox/清华大学 驾驭工程 Harness Engineering 研究报告.md b/InBox/清华大学 驾驭工程 Harness Engineering 研究报告.md new file mode 100644 index 0000000..d8d01e2 --- /dev/null +++ b/InBox/清华大学 驾驭工程 Harness Engineering 研究报告.md @@ -0,0 +1,24 @@ +# 清华大学:驾驭工程 (Harness Engineering) 研究报告 + +> 来源:GIS极客 · 微信公众号 +> 日期:2026年4月10日 +> 原文:https://mp.weixin.qq.com/s/EdVjZuBVcXjd30TpxyLsXQ +> 完整报告下载:关注公众号 **GIS极客**,后台回复 **"清华HarnessEngineering"** 获取下载链接 + +--- + +## 核心观点 + +驾驭工程 (Harness Engineering) 的核心是**围绕高自治、长时程 AI 构建可治理的操作系统层**,将提示词、上下文、智能体等能力制度化为机械可验证的契约、状态恢复与审计体系,从而从 **"让AI听懂"** 升级为 **"让AI系统可信、可控、可持续运行"**。 + +--- + +## 报告概述 + +本文为清华大学发布的研究报告,主要内容以图片形式呈现(报告正文截图)。需要阅读完整版请按上述方式获取 PDF。 + +报告关注的关键问题: +- 高自治 AI 系统的治理与可控性 +- 长时程 AI 运行的操作系统层设计 +- 提示词、上下文、智能体的制度化 +- 机械可验证的契约、状态恢复与审计体系 diff --git a/InBox/知乎文章_2033520572144030920.md b/InBox/知乎文章_2033520572144030920.md new file mode 100644 index 0000000..50a32f4 --- /dev/null +++ b/InBox/知乎文章_2033520572144030920.md @@ -0,0 +1,12 @@ +--- +source: 知乎专栏 +url: https://zhuanlan.zhihu.com/p/2033520572144030920 +date_saved: 2026-05-02 +tags: [zhihu, inbox] +--- + +# 知乎专栏文章:p/2033520572144030920 + +**链接:** [https://zhuanlan.zhihu.com/p/2033520572144030920](https://zhuanlan.zhihu.com/p/2033520572144030920?utm_psn=2033977981434114693) + +> 待阅读。文章内容因知乎反爬保护未能自动抓取,请手动打开链接查看。