Typc-C充电线中的电阻

最近发现手头的充电线都磨秃噜皮了,打算更新一波。正好去年PD 3.1这个新规范的发布,早就被amazon推荐了很多充电头,就研究了一番。可能是因为5年没有更新手机了,一直没有关注这个领域。没想到USB Type-C接口这几年进化出许多花头,不了解的话很容易花冤枉钱买到不能用的线材,甚至有可能损坏设备。

废话不多说,记录一下我了解到的关于线材的一些知识。不说数据传输速率,只说充电时候线材和接口上的电阻相关的事情。

TL;DR

  1. A2C线没有芯片,大致分带电阻和不带2类。带电阻有3种,10k和22k可能有烧电源风险,不合规。只有带56k欧上拉电阻的线材是正品,防止C口设备烧坏老电源。
  2. C2C线种类非常复杂:没有芯片的线材支持3A电流,兼容性好。带e-marker芯片的线材,支持5A电流,但它自带下拉电阻,和不规范的C口设备不兼容(初代树莓派4)。
  3. 在标准里C口线材(包括A2C)至少支持3A电流。所以标称60w或者3A线的C口线材是最普通的,应该很便宜即可买到,快速充电并不需要昂贵的线材。
  4. 只有用合规的C2C线连接的设备和电源,才可以根据PD充电协议协商,最多输出240w的功率。如果中间经过A口B口甚至pin不全的C口转接头,都会导致无法协商。目前60w以上的充电需求比较罕见,140w是家用电子产品最高的功率要求(苹果笔记本)。

首先是接口类型

最常见的USB Type-A口就是那个方口。其中2.0是4个pin,3.x是9个pin。但是充电也用不到那么多,所以在充电的场景中,可以认为2.0和3.x的A口是等价的,都只有2个pin生效,也就是电源正负极。

接下来是USB Type-C接口,这个就复杂了,它有24个pin。由于c口正反两面都可以接,在充电的场景中只有一半的pin在起作用。特例是两面各有一根的CC引脚,充电时候这两根会同时使用用来查询方向和电压等。

其他USB类型的口就不说了,市面上带C口的线基本就这两种:两头都Type-C的C2C线,或者一头A一头C简称A2C的线

先说说A2C的线

正如名字所写,它是有方向的线。必须从A到C,用来把C口出现之前的USB A口电源(包括充电器,电池,或者电脑等),接到带有C口的设备(例如手机,树莓派)上。在前些年C口刚出现的过渡时代,很多充电器都会送这种线。也有许多使用C口充电的小家电会送这种线,最著名的应该就是任天堂Switch游戏机了。

A2C的线内部结构很简单,就是A口到C口的映射。由于C口有24pin,充电只需要正负极,映射2根电源线绰绰有余。所以最直白的线材就是两根铜丝直接连过来

但是根据USB IF定义的C口规范,这样做是不够的。因为带有C口的设备可能需要各种各样的电压和电流,它需要了解电源能否提供这些不同的电压电流。设备怎样才能询问电源呢?一般来说,协商是通过C口新增加的CC信号线和各种芯片来实现。这就引出了两个问题:

1、CC信号线要怎么接?
2、这些芯片在哪里?

针对问题1,A2C的线材,在A口那一端是没有CC信号线的。问题2的芯片,是为了应付更复杂的情况,只有C2C的线材才可能有,A2C的线材中肯定没有芯片。啥都没有的话,设备如何通过A2C的线材询问电源规格呢?答案是使用上拉电阻

直白的说,就是在A2C线材的C口部分,把它的CC引脚和VBus引脚通过一个电阻连起来。加了这个电阻之后,A2C线材才符合USB IF的规范。因为电阻是从CC信号线接到VBus电源线的,因此被叫做上拉电阻。由于我的电子电路知识都还给老师了,简单认为接到电源的电阻叫上拉即可。

为什么一根A2C的供电线里面会有电阻呢?

之前没想明白的我发出了这样的疑问

答案也很简单,本来在C2C口的情况下,上拉电阻并不在线材里,而是做在C口电源的母座里。旧的A口电源当然不会有这个上拉电阻,更何况A口没有CC引脚,也无处可接。A2C线是为了让C口设备使用A口电源,所以它为了模拟“电源通过上拉电阻报告自己供电能力”的行为,把这个电阻做到了线材里。

当C口线材接到设备的C口之后,设备会给CC和VBus加电压,然后通过上面电流的大小来判断电用的供电能力。理论上A2C线材的上拉电阻应该是56k欧(规范强制)。因为56k的电阻在C口供电规范里表示默认功率,USB2.0的时候是5v*0.5A也就是2.5w功率,或者USB3.x的默认5v*0.9A也就是4.5w功率。最差的A口电源也可以输出这个功率,这就保证了双方和谐共处。

C口供电规范里,阻值还可以是22k欧,对应5v*1.5A也就是7.5w的功率。或者10k欧姆,对应5v*3A也就是15w的功率。无论哪种阻值,采用这种协商方式的电源电压都是5v,只是输出电流有变化。

怎样才是安全的

那A2C把阻值设成10k欧来模拟15w的电源可以吗?要知道这是线材在模拟电源的行为,A口那边的电源对此一无所知。而设备查询到电源的不同能力之后,可能会向电源请求更大的功率输出。确实一些A2C的线材会接10k欧的电阻(违反规范),一步到位模拟5v*3A的15w电源。但是15w电源在A口时代非常罕见,如果设备真的请求了这么大的电流,可能会烧坏电源(我猜不会烧坏设备,毕竟是它自己请求的,但谁知道呢……)

正因为如此,任天堂在官方的指南中写着请使用带有56k电阻的线材。可是它没有明确说这是A2C的线材的需求,让许多玩家非常困惑。贩售线材的商家也不一定知道这么细节的规定,毕竟焊接的电阻存在于A2C的C口内部,肉也无法分辨。所以我们甚至能看到有些C2C的线号称自己带56k上拉电阻,这就完全是扯淡了。

56k电阻的A2C线材虽然安全,但是充电速度就慢了。强调下不是因为电阻大哦,要是供电线路里加了这么大56k的电阻,电线还能通电吗?这个电阻是在线材C口那端,把自己的CC引脚和VBus引脚连接了电阻,并没有放在供电引脚上,所以不会参加供电的过程,只是用来模拟握手协议。

我手头的大部分A口电源都能输出2A,10k电阻肯定是过头了有风险,理论上22k或许也能用?然而想要随便一根A2C线,搭配任何A口充电器,还是得56k电阻的线材。USB规范上写了,需要更高的电流可以通过其他协议再商量:

“The value of Rp shall indicate an advertisement of Default USB Power (See Table 4-24), even though the cable itself can carry 3 A. This is because the cable has no knowledge of the capabilities of the power source, and any higher current is negotiated via USB BC 1.2.”

(正常的A口电源应该只有5V的规格,所以A2C线也不需要模拟其他规格的电源电压输出,上拉电阻就够了。那种什么电阻都没加的直连线材,是不符合规范的。我猜设备遇到这样的线材,应该只会请求最低的功率。那它是否等价于56k电阻的线材我就不清楚了,也许有的设备检测到cc没有上拉电阻,压根就不工作。这就是为什么某些A2C线有时候无法给C口设备充电?此部分纯瞎猜的)

但无论如何,A2C的供电部分双方都是直连的。具体会请求多少电力完全看设备了。比如有些带C口供电的台灯,我猜里面没有什么协议啊握手之类的芯片,放什么电阻都一样。单纯就是要5V电压,功率全看灯泡的瓦数了。

Google工程师烧了笔记本?

我在收集信息的时候看到有人提到了错误电阻规格的线会烧设备,证据就是这个谷歌工程师烧掉笔记本的故事,说得有眉有眼。实际上这也是个myth,Google工程师烧笔记本确有其事,但和上拉电阻规格无关。

这位工程师因为此事出名,后来一直在review各种C口的线材。可惜他发的东西都在Google+上面,已经随着Google+关站一起灰飞烟灭了。具体到烧笔记本的事件,可以看看这两个连接:PCMag的新闻报道,以及他当时发的贴子的快照。

很多报道都没提到细节,这是一根USB A2C的线,只有PCMag的编辑详细引用了这段话,表明这线根本就是搭错了引脚,A口的地线接到了C口的电源,而A口的电源接到了C口的地线,完全反了过来。估计是电源反接导致设备某些器件被击穿,和上拉电阻没有任何关系。

“I directly analyzed the Surjtech cable using a Type-C breakout board and a multimeter, and it appears that they completely miswired the cable. The GND pin on the Type-A plug is tied to the Vbus pins on the Type-C plug. The Vbus pin on the Type-A plug is tied to GND on the Type-C plug,” Leung described.

反接5v会导致的后果ChatGPT有不错的描述

USB电源线的5V电源如果反接,即正负极颠倒连接到设备上,理论上确实有可能导致设备损坏。USB接口设计有标准的针脚布局,其中包括为设备提供电源的Vbus(正电压)和GND(地线)针脚。如果这些电源线被错误地接反,就可能在设备内部产生不正确的电流流向,导致以下几种情况:

即刻损坏:电源反接可能会立即损坏设备内部的电子组件,因为这些组件设计时预期电流将以特定方向流动。反向电流可能会导致电子元件过热、短路或永久损坏。

保护电路触发:一些设计良好的设备和系统可能包含保护电路,如反向极性保护、过流保护和短路保护等,用于防止因错误连接而引起的损坏。如果存在这样的保护机制,设备可能会在电源反接时关闭或断开电源,从而避免损坏。

无反应:在某些情况下,如果设备具有非常强大的保护电路,可能会在没有任何损坏的情况下简单地不响应反接的电源。

绝大多数现代电子设备都设计有一定程度的保护措施来防止因电源反接而导致的损坏,但这并不意味着所有设备都能在电源反接的情况下安然无恙。依赖设备内部的保护措施并非一个好的做法,因为不是所有设备都能够在这种情况下保护自己。

最佳做法是始终确保正确连接USB电源和设备,遵循设备制造商的指南和推荐,以避免因错误连接而导致的损坏。如果不确定,最好是检查设备文档或咨询制造商,以确认设备是否具有防止反接导致损坏的保护功能。

然而不合规格的上拉电阻确实很不安全,根据充电头网的这篇文章

“由于转接线和充电头可能不是配套的。如果充电头端的USB Type-A Receptacle只能提供5V@900mA,那么即使转接线Type-C Plug端Rp声明为 5V@3A,也没法给手机最大提供5V@3A的充电功率,还有可能损坏充电头。”

根据这个日本评测网站的说法,也确实有部分A2C线材会真的连接10k或22k的电阻,只能希望电源和设备都给点力,保护好自己了。

总结一下

https://www.chromium.org/chromium-os/cable-and-adapter-tips-and-tricks/

从C口诞生开始就有非常多的错误信息和myth流传在互联网上。甚至Chromium都有一个专门的页面来澄清,但是格式错乱了。我把它翻成中文留在这里。名词定义:

Resistor Rp:(上拉电阻Rp,接在VBus和CC引脚之间,就是接着电源的电阻)

Resistor Rd:(下拉电阻Rd,接在GND和CC引脚之间,就是接着地的电阻)

ID 引脚 :(只有micro-B口里面有,用于OTG连接)

Plug:公头,就是凸出来的那种,很多地方也称为male,插头。

Receptacle:母座,就是凹进去的插槽,很多地方也叫female,母头。

Open:就是开路,表示这个引脚悬空什么也没连接。

以下表格描述的都是一边C口一头其他口的Legacy Cable and Connector,其C口内部的电阻情况。

线材类型C口电阻ID引脚USB-C规范
C公头 <-> USB 3.x A公头(9pin)56 kΩ RpN/ASection 3.5.1
C公头 <-> USB 2.0 A公头(4pin)56 kΩ Rp N/ASection 3.5.2
C公头 <-> USB 3.x A 母座(9pin)5.1k Ω RdN/ASection 3.6.1
C母座 <-> 各种A/B公头规格不允许Section 2.2
C公头 <-> micro-B 公头 5.1k Ω RdOpenSection 3.5.7
C公头 <-> micro-B 母座56 kΩ RpOpenSection 3.6.2

虽然不合规,实际上C母座转A或者B公头也可以买到。使用上要注意风险,因为接入A/B母座转出来的C母座并不是真的C母座(想也不可能啊,pin数量都差好多)。我买来只用于转接5v充电的A2C,或者USB 3.x的5Gbps数据传输的C2C。相当于把A2C线变成了的A2B线,或者把C2C线变成了A2C线,算是线材降级?

Type-C之前的时代,USB线只要接上总是可以用的,线材上没有花活儿只有电缆,复杂的东西都在设备上。这个时代已经过去了,使用带Type-C口的线,或者接入C口的母座都得注意。

得空再写C2C线的各种情况。

TBC……

MAUI还没入门就放弃

之前听说.NET终于把名字统一了,可喜可贺,我终于不用再纠结.NET Core和.NET Framework哪个新哪个旧了。之后MS在统一的路上继续狂奔,决定把UI也统一了,说是出了个叫做MAUI的框架。

论UI开发框架,MS的开发部门真是研发了好几辈子。之前有WinFrom还有WPF,古早的MFC之类的玩意儿,都是长江后浪推前浪,一代不如一代浪。现在谁不用javascript+html写UI谁就是boomer,手机呼机商务通都可以用的东西各家都在搞,MS家的东西已经不耐看了。但MS野心显然不止于此,大概是因为移动平台的地位失去了,想找回场子,就弄了这么个框架。老调重弹,还是号称一次写码到处编译(亦读作修锅),架构图也是老套路,顶层一大块MAUI包圆了,下面各种模块支撑,叠了三四层最后是各种平台。

我简单理解了下,它是把以前的Xamarin收编了,然后在各个平台上实现了.NET的这套runtime,就能把App跑在各种地方了。布局和MVVM这类东西,还是原来WPF的那套,写起来也主要靠XAML(写到这里,我的小拇指已经因为不停的按Shift书写各种全大写的缩写名称搞得开始酸痛了)。

这听起来没什么新意嘛,可能是我已经老了或者没搞懂?之前也知道Xamarin,但这个名字我一直不喜欢,主要是不知道该怎么念。.NET就已经够奇葩了,Xamarin又是怎么起出来的?无论如何,新的MAUI来了,该怎么念呢?猫艾?Whatever了……

打开tutorial,过了一遍,总之先装VS2022,再装MAUI开发工具集。不管我愿不愿意,20多个G就没了,Android和iOS的一大坨东西被塞进了c盘。要知道我本地已经装了好几个Android SDK,它就不能先别装让我选个本地的版本吗?还有iOS又是什么鬼?已经能在Win上面交叉编译iOS的程序了?我不懂,但我大受震憾。

好在装起来虽然慢但也简单,等进度条跑完,打开IDE创建一个HelloWorld项目,我也来尝尝鲜!MAUI,启动…………哎不对报错了。再启动………………哎呀说要先打开Windows10的开发者模式。什么我的PC还支持这种模式?我平时各种写代码debug爬数据跑ai注入dll原来都不算开发者,只有MAUI才让我明白自己一直用的是门外汉模式。

行吧,开了之后咱重新来过。就假装我的3900x+4090RTX是个平板电脑好了:开发者模式MAUI,启动!!!!

然后就继续报错了,这次报错连推荐解决方案都没有,出现了微软的传统艺能:0x8adsaf123DSAF123文件找不到错误,总之就是看不明白,中英文夹杂的报错信息显得异常结棍。我不服又能怎样?ChatGPT根本不懂这新玩意儿,还得Google来帮忙。

原来报错是因为我的项目放在subst虚拟出来的盘符上,可能deploy的过程需要管理员权限,管理员看不到我的个人用户subst出来的东西,就挂了。行吧可以理解(理解个屁啊!编译EXE也要用到高权限了吗?但无论如何,换到普通盘符下面打开项目,终于可以MAUI启动了。

一番折腾,出现了蓝紫色的画面,除了启动过程略长以外,中规中矩的平板App风格。我跑到build目录一看,好家伙,各个平台每种一个目录。打开Windows的目录,里三层外三层,居然生产了250多个文件将近300m的各种不明就里的dll和exe。

我的天哪!!!

我的天哪!!!

我的天哪!!!

比electron这坨abomination还要吓人呢。我真的找不出合适的中文词汇形容我的感觉了,只有abomination这个长且发音简单的词汇可以表达我内心的震撼。我第一次听到还是十多年前玩龙腾世纪的时候,每次出现一大坨肉队友就会大喊abomination。恶臭,臃肿,2023年了怎么会有这样的东西!

然后我找了下编译出来的exe,大概几百k。双击一下这个文件,居然无法启动我的程序。果然,dotnet core熟悉的感觉出来了,程序编出来不知道怎么启动!明明在visual studio里面可以启动的呀。我索性再编了个release版,文件数没少但是容量减少到了100m,算是小号的abomination吧。

还是无法双击exe启动,满头问号的我再次Google,果然许多人在Github上询问这个事情。在翻了好多微软公司人废话连篇云里雾里的兜圈子回答之后,我终于明白了,这个MAUI开发出来的应用需要publish,变成特殊的MSIX格式,然后安装到PC上才可以脱离Visual Studio启动。

什么?这套流程不是xcode开发mac程序时候的恶臭步骤吗?还得先打包成Archive,再签名,再搞来搞去,最后安装。有种“我自己写的代码不是我自己的,我自己的电脑也不是我自己的,我自己为什么要做这种自我怀疑的事情”的感觉。

试了一下果然,publish过程中问要不要签名,要不要这要不要那,最后产出一坨MSIX。等等,微软又发明新的安装包了?不是MSI是MSIX?怎么现在这些有钱公司的开发者都喜欢X啊……X来X去还是100多M,然后点击安装,然后我的程序终于可以直接跑了。

经过MAUI这么一套组合拳,我感觉自己被赋能,整个人都是MAUI的形状了。硬要我评价的话,就是主打一个“干”字。我想对开发这些东西的人说:还我的1个小时和20G硬盘空间好不好啊?MAUI你们自己留着,我滚回去随便用什么框架了,哪怕electron我也不敢再骂了。

谢谢微软,谢谢MAUI,我手头的其他框架看起来都变得美好了。

可能这就是生命的意义吧。

尝试了一下现代技术

感受了JS前端+全套解决方案开发手机App的效率。

我选择了Tailwind CSS做布局,很容易就能写出现代风格的软件界面。然后React作为逻辑层,数据直接用各种封装好的key-value数据库。然后各种异步直接在js里面写await/async,根本就没有脑力负担。

从web变成手机App的话,就转为React Native,配合Expo的环境,连自己编译都不用搞了,纯粹的写写网页就能变成全平台的App,打包发布一应俱全,这个门槛不能再低了。

实际在开发的时候,各种状态管理都有现成的解决方案,我全是跟ChatGPT问出来的。不会的API或者typescript语法就跟它现学,说得非常清楚,都很容易掌握。比如atom+immer用来做全局的状态储存,swr做异步的数据获取和更新,然后各种hook用于各种不同的场景。

总而言之,就是App的开发生态完全和几年前不一样。那会儿React刚出来的时候,难用得不行,想做稍微复杂的东西就得搞什么redux这种我看着就难受,完全不想搞明白的玩意儿。

当然我相信这不是最优的解决方案,只是对于我这种熟悉WPF和JS还有React的人来说,真是愉悦的开发体验,尤其是不用搭建build环境,告别了和傻缺build engineer脑波同步的过程,幸福生活从云打包开始。

不过前端社区进化太快,我玩的这套也是“过时”技术栈。好在我也不是专业开发,就是花了2周从0开始到做完一个可用的App体验了一把。我不喜欢追新潮的技术,我就喜欢这种“成熟”或者说过时的技术,所有的坑都有人踩过,行不行一眼就能看到,不用自己琢磨太多,反正开发App又不是什么高大上的活儿,自己做着玩刚刚好。

顺便鄙视google出的什么Flutter+Dart,现成的语言不用非要搞全新的东西,拜拜了您的,没心情和任何Google的技术栈产生过深的交集,离丫一段距离比较安全。

Unity的Canvas坐标系统

Unity在游戏中为Scene和UI提供了不同的渲染组件。Scene的Mesh/Sprite的渲染采用了MeshRenderer/SpriteRenderer,而UI的2D组件则通过CanvasRenderer渲染。

我个人不喜欢这套组件的命名方式。本质上所有的Renderer都并非渲染操作的执行者,而是采集渲染数据的容器组件。最终实际的渲染工作由Engine中的渲染代码完成。不过名字只是个代号,知道背后的原理就好。

对于基本的渲染流程来说,被渲染的数据来源都是相同的。Mesh负责提供顶点和颜色数据,Material提供Shader和它的各种参数,再加上用于提供位置信息的Transform Matrix,被渲染物体就算描述清楚了。

在Scene里面所有的物体,无论是2D的Sprite或者3D的Cube,它们在World中的位置都是通过Transform来描述的,Mesh的顶点数据是在Object Local坐标系。只有遇到需要合并批次的情况才会被转换为World坐标系。单独渲染的情况下,在RenderDoc中可以看到Vertex数据和Mesh中的数据完全一致。整个流程就是教科书上的标准流程,Local->World->View->Clip(Projection)->NDC。

在Unity Engine内部会做一个简单的优化。为了减少渲染批次,会把Material相同的Mesh组合成同一个批次。大概过程就是先将这些Mesh的顶点坐标乘以Transform Matrix,转化为个世界坐标系中的位置,然后放在同一个batch中一口气画出来。合批之后,RenderDoc中看到的World Matrix就变成了标准矩阵,因为每个Mesh已经乘上了各自的World Matrix。

通过CanvasRender组织的数据,则稍微有些不同。

Unity里面大部分UI是对各种运行时的UI元件抽象的描述,没有预先制作好的Mesh,这种情况下Mesh会在运行时直接生成。以最简单的RawImage为例,需要通过4个顶点2个三角形组成的矩形Mesh来描述。只要知道UI也是Mesh组成的,想要自定义UI的形状,就可以通过自己创建Mesh来实现,具体可以参考Graphic.cs等UI的代码。

// 自定义的情况下,更新mesh后调用SetVerticesDirty();
// 然后override UnityEngine.UI.Graphic.UpdateGeometry传入自定义的mesh。
protected override void UpdateGeometry()
{
    canvasRenderer.SetMesh(_custom_mesh); 
}

// 或者在Dirty之后,通过OnPopulateMesh()中直接生成顶点数据
// 基类会把这些顶点放进Mesh里,省去自己创建mesh对象。
protected override void OnPopulateMesh(VertexHelper vh)
{
  vh.AddVert(v0);
  vh.AddVert(v1);
  vh.AddVert(v2);
  vh.AddTriangle(0, 1, 2);
  ...
}

如何生成UI Mesh的顶点,就需要牵扯到RectTransform。它是Transfrom在UI组件中的特化,这个概念我不太喜欢。一般情况下,Transform只应该负责“位置和变换”相关的信息,但RectTransform同时定义了“形状”,因此它包含Width/Height属性。更加混乱的是,它还囊括了Anchors和Pivot的概念。Anchor会和UI组件上层的UI组件产生关联,以此修改自身的位置和形状。而Pivot则定义了Mesh中顶点的Local坐标系原点。我之所以讨厌这个组件,就是因为它把太多的东西杂糅到了一起,牵一发而动全身。

以RawImage为例,本来可以简单的定义中心作为原点,通过Width/Height即可生成Mesh需要的4个顶点在Local坐标系的位置。但是在Unity中,UI对象的Local坐标原点是通过Pivot来定义的。因此Pivot改变之后,Mesh的顶点数据也需要重新计算。

知道Pivot和Width/Height,生成了Mesh之后还不算完,这些数据并不会直接提交到渲染的流水线中。可能Unity假定UI基本上拥有相同的Material,所以UIMesh都会被强制做合并批次处理。即使只有1个UI组件,它也会被合并批次。

因此在RenderDoc中看到的顶点数据,都不是UI的Mesh中储存的原始数据。这些顶点会被转化到“Canvas Local坐标系”中,可以说UI的World就是包含它的Canvas。这也是为什么Unity需要把UI对象要放到Canvas的树型结构中,才能将其绘制出来。

对于UI组件来说,它的World Matrix是“自身的RectTransform”和“它所有Parent的RectTransform”相乘的结果:

// 合并批次之后的顶点数据:
// mul( GetCanvasTransform(this.RectTransform) , Mesh顶点 )
Matrix4x4 GetCanvasTransform(Transform t)
{
	Matrix4x4 current = Matrix4x4.identity;
	while (true)
	{
		var canvas = t.gameObject.GetComponent<canvas>();
		if (canvas != null &amp;&amp; canvas.isRootCanvas)
			return current;

		var m = Matrix4x4.TRS(t.localPosition, t.localRotation, t.localScale) * current;
		if (t.parent == null)
			return m;

		current = m;
		t = t.parent;
	}
}

UI的顶点变换相当于(Local->CanvasLocal)->CanvasWorld->View->Clip(Projection)->NDC,前两步对应UI Mesh合并批次的结果。

对于World,View和Projection三个变换矩阵,Scene里面的对象很直接,World是对象各自的Transform,View和Projection通过Camera上的设置生成。UI和Scene并没有本质区别,因为在shader中数据的处理方式都是相同的,主要区别在于送入渲染管线之前如何得到这些矩阵。

具体计算方法和Canvas的Render Mode相关,总共3种模式:

1、Screen Space – Overlay模式:此模式将在屏幕最上方直接渲染UI,忽略任何Camera。在Editor中这个模式的表现很奇怪,编辑器中的UI不能和当前场景的对象对齐,可能强制设定了一套坐标,我一般避免使用它,但官方范例爱用这个模式,即使游戏中只有一个Camera。

Canvas Local坐标系相当于View坐标系的数据。在RenderDoc中可以看到,World和View矩阵都被强制设为单位矩阵,而Projection为全屏范围的正交投影,和任何Camera都无关。在这个模式下,UI会在所有Camera渲染完毕之后,单独被放在在“UGUI.Rendering.RenderOverlays”批次中渲染。

如果设置了自动缩放的CanvasScaler,只有Projection矩阵会根据缩放比例产生变化,World/View依然是单位矩阵。因为CanvasScaler的代码只修改Canvas.scaleFactor,这个属性只会影响Projection矩阵。

2、Screen Space – Camera模式:此模式将在Camera正对的平面上渲染UI,Canvas的范围是该Camera的Viewport,相当于在billboards上渲染UI。在编辑器中可以看到,Canvas会跟着Camera旋转,始终正对Camera。

World矩阵用于把Canvas缩放旋转之后摆在World里正对Camera的位置。接下来View和Projection矩阵和Scene中其他物体的情况相同,按照同样的计算方式,最终得到Clip坐标系中的顶点。

值得注意的是,RenderDoc中可能看不到View矩阵的正确值。unity_MatrixV始终显示为单位矩阵,但unity_MatrixVP的结果是Projection*View,和渲染Scene中其他物体时候的MatrixVP保持一致。

3、World Space – Camera模式:此模式把Canvas当作World中的一个平面来渲染。在编辑器中可以看到Canvas的位置不会随着Camera转动,因此可以渲染出带有3D空间感觉的UI。

和前一个模式唯一的区别在于,World矩阵始终为单位矩阵。因为Canvas Loca坐标系被当作World坐标系来对待,所以Canvas的Width/Height不再是Pixel Size,而是World坐标系的单位。

============================

上述3种模式中,前两种模式中Canvas的RectTransform的各项数值由Canvas自己设定,只有第3种可以自己设定。RectTransform再次承担了特殊功能。我猜测引擎内部根据模式和RectTransform的数值,计算View Matrix,以及UIMesh合批的时候转换到Canvas Local坐标系的计算。

总结一下就是:

1、无论何种模式,传入渲染管线的定点数据坐标系没有变化,均为Canvas Local坐标系。这也是Canvas渲染模式切换不影响material的缘故。

2、根据Canvas渲染模式的不同,View和Projection会被分别设置。

3、除了World Space – Camera模式,所有Canvas树形结构中的对象,其Transform均不受Canvas层以上的父节点影响。这和普通的Scene对象有区别。

4、如果希望在shader中获取UI Mesh的Local坐标,可以通过传入GetCanvasTransform()计算结果的逆矩阵,然后和顶点坐标相乘来获得。

// material.SetMatrix("canvas_transform_inverse", GetCanvasTransform(this.rectTransform).inverse);
float2 local = mul( canvas_transform_inverse, float4(vertex, 0.0, 1.0) );

以上均为个人对Canvas坐标系统的理解,如果以后发现错误,再来订正。

针对RectTransform的吐槽就不再重复了,最后想说得是,Unity完全没有必要设计这些东西,单独为UI开一条渲染路径。所有的UI都应该并入Scene中,和其他对象一样进行渲染。或者反过来说,针对UI的这些“优化”和“坐标转换”,都应该允许场景中其他对象参与。否则Graphic相关的实现,都得单独 给UI做一套,例如particle,effect,animation。这又是何苦呢。

Xbox One X硬盘更换

之前想给X1X换个SSD,一直没有找到机会下手。趁着最近正在整理硬盘文件,腾出一张256G的SSD,就想给X1X换上,还能拿到1T的旧盘用来当移动硬盘。

本来以为X1X更换硬盘和PS4差不多,找到硬盘插上去然后重新执行升级文件,完事儿!网上查了下确实也有X1X的升级文件,于是就按照教程把硬盘拆下来。此处省略1000字的吐槽,X1X拆硬盘的难度比PS4高很多,主要是需要把机器翻个底朝天才能拿到最里面的盘。这时候我就有了不详的预感,果然装好SSD之后,升级文件点了就报错E101。

接下来上网翻帖子,才发现X1X的硬盘更换是很麻烦的。坑非常多,但好处是可以直接硬盘对拷,不用像PS4一样备份后再导入。前提是知道怎么操作,网上专门搞这个的家伙是个废话特别多的主儿,youtube视频又臭又长,平均40分钟起。

其实说白了就是:X1X硬盘只有500G/1T/2T三种标准容量。根据当前你机器的硬盘型号,如果直接换同样大小的硬盘,1:1对拷之后就可以解决问题。否则就需要分成两种情况:

1、新的硬盘也是标准容量的情况下,如果不想保留旧数据,插上去直接用离线升级文件OSU1就可以安装系统,不需要分区,新硬盘直接可装。

2、如果要保留旧数据,或者新硬盘不是标准容量(例如我的情况256G,或者1.5T什么的),不能直接对拷也不能使用OSU1离线安装系统。

在第二种情况中,需要手动把硬盘分区。按照X1X的规格,硬盘需要四个NTFS分区。其中3个是固定大小的系统分区,1个是用户文件分区,可以根据自己的硬盘容量把剩余的空间都分进去。分区之后,就可以直接把旧硬盘对应分区的数据,拷贝到新硬盘。

最重要的一步,也是最后一步,要把这些分区的GUID修改成指定的值。具体修改成哪个GUID,是按照机器旧的硬盘的容量来的。500G/1T/2T各有一套GUID,如果GUID不对硬盘放进去也启动不了。虽然可以用OSU1升级,但是校验之后就会报E101的错误。

所以我的情况,是1T的X1X,更换为256G的SSD。需要确认旧的硬盘可以正常进入系统,然后断电关机,把新的SSD分区,再拷贝数据,再修改为1T对应的那套GUID。这样的顺序操作才能让新的SSD直接在X1X中启动。

分区和GUID的修改可以用任何软件进行,拷贝也可以直接用文件管理器。但是要操作很多步骤,手动做很麻烦。网上那位XFiX小哥已经做好了一套脚本,本质上是一堆分区工具+Bat批处理/shell脚本,可以在windows或者linux里,直接自动分区和修改GUID。在Windows上操作唯一要注意的就是,不能同时挂载具有相同GUID的硬盘。新旧硬盘需要修改为相同的GUID,所以要先拷贝数据,拔下旧硬盘之后再修改GUID。

脚本可以在这里下载:https://gbatemp.net/download/xbox-one-windows-and-linux-internal-hard-drive-partitioning-script.34239/

有兴趣研究的也可以看小哥充满各种废话的教程视频:https://www.youtube.com/watch?v=gPJ-sBAWeL0

当然,吐槽归吐槽,前人栽树后人乘凉,小哥的脚本还是很好用的。要怪就怪X1X的硬盘升级步骤太繁琐了。毕竟人家是安全系数极高的console,从来没有被攻破,可不是得高点事情嘛~

最后吐槽:X1X不能安装大于2T的硬盘,装上去也用不了,可能是里面虚拟内核或者加密问题?还有就是离线安装文件居然叫OSU1,而且还存在OSU2和OSU3,但是OSU1才是最新的升级包,不知道起名字的人是什么脑回路,可能是给X1X和XSX以及XSS起名的那位大神……

Unity的“新”feature

加引号是因为我从2018升级,很多feature都出来好久了,只是对我来说“新”一些。而且很多features其他引擎早就有了,在Unity的范畴内算是“新”的。

以下记录一些自己觉得有用的新玩意儿。

Unity 2019:

GC终于会分散在不同的frame里面处理了。https://blogs.unity3d.com/2018/11/26/feature-preview-incremental-garbage-collection/

使用[GenerateAuthoringComponent]自动生成对应的property代码,更好的支持面向数据的程序设计。

序列化终于支持多态,可以正确的处理null和子类。需要显示标记[SerializeReference]。

可以配置进入Playe Mode不需要重新加载代码。

Unity 2020:

独立的Profiler,可以通过Flow Event分析jobs,避免Profiler自身影响分析结果。

显示使用debug和play两种运行模式。

通过-deterministic,避免重复编译没有变化的代码(以前居然不是这样?)

更加稳定的Time.deltaTime(需要DX11+)

导入出现错误,可以先在安全模式中修正代码然后再继续导入。

Unity 2021:

号称production-ready的整套2d-pipeline,目测加了骨骼/光照/特效/复杂地形?

很多profiler的改进,看来是考虑到许多Unity游戏性能惨不忍睹。

支持了Incremental build分布式编译,真的有需要这么夸张吗?

支持IL2CPP生成更小的版本,但是性能可能受损,不知道具体差异有多大。

Addressables的接口终于有同步加载的API了(以前居然没有)

记录了一些我个人比较关注的新改进。当然,新版本肯定有新Bug,这个是逃不了了。总体来看这么多年过去还是有很多值得一试的东西,升还是得升级的,2018实在太老了。

说说《三角战略》的投票系统

SE上个月发售了《Triangle Strategy》,这个游戏被我视作《皇家骑士团》的精神续作,自然是第一时间入手了。然而因为老头环的存在,没有第一时间开始投入的玩。直到白金了老头环之后我才捡起来。

打老头环的时候,和老婆一起断断续续玩到第七章,对剧情的感觉是还行,没有特别的逻辑崩坏,人物智商也都在线。没想到就从第七章开始悲剧了……

这游戏除去战略关卡,比较新鲜的就是投票系统了。具体来说,就是战略的下一步走向不是简单的由主角自己决定,而是要靠7位性格各异的伙伴投票。主角只能尝试说服他们,但对方不一定听主角的话。

第一次看到这个系统是在第3章,觉得很有意思。7这个数字不能被3整除,正好代表游戏标题的三种不同的价值观。2:2:2然后还有骑墙派,主角只需要说服骑墙派就可以左右信念的天枰。没想到开始投票才发现,方向只有2个。而且主角似乎很容易就把大家都说服了,全民投票通过,团结的不得了。

然后到了第7章,终于出现了稍微严肃一点的选择。这个时候违和感就出现了,因为我发现无论怎么S/L,7个小伙伴总有4个人投出和我想要的方向不一致的的结果。仔细研究了这个游戏的数值系统之后,我和老婆才发现这游戏并不是自由选择走向的游戏,而是个非常简单的二极管数值系统,并且参杂了策划自己的强制路线,顿时就让剧情选择的体验降成了0。

具体的吐槽我列这么几点,尽量不剧透,防止有玩家搜到这篇blog……

1、价值观投票完全由数值决定

本来游戏设计了三个不同的价值观,分为“自由Liberty”,“道德Morality”,还有“功利Utility”。从名称上来看,三角策略嘛,也就是三条路你自己选。当然,由于路线是同伴决定的,同伴的价值观实际上左右了投票的结果。

如果不被剧透,玩家会觉得自己可以自由的展现自己的价值观,然后导向不同的结局。如果同伴和自己产生分歧,只要尝试去说服即可。但这个说服系统居然不是按照收集的线索或者说服选项来定的,完全是按照“主角”自身的三项数值是否简单的满足策划设定的说服条件。

也就是说,如果主角的“道德”水平足够高,那么主角就可以说服一个“功利”的角色去做出“道德”的选择。这听起来就很不可思议了,为什么主角我自己道德水平高会影响功利角色的道德抉择呢?而且如果道德值不够,即使主角已经可以在逻辑上说服功利角色,“道德”选择是最有利的,功利角色自己也“陷入沉思”……最终投票的时候还是会义无反顾的投下功利票。

具体判断流程是,首先策划给每个角色安排一种投票方向,基本上3个人向左,3个人向右,1个人骑墙,实际骑墙角色也有策划固定的走向。接下来判断主角的三个隐藏价值观,小于被说服角色的“阈值”,直接显示“似乎有些冷淡”。这听起来有机会其实是中文翻译的太含蓄了,英文直接是“talking to a wall”,数值不够说啥都白瞎。数值够了的情况下,如果高出非常多,选啥都可以说服。如果不高不低,就看你选项有没有道理,有道理就成功说服了。

所以系统策划才是上帝,是策划写死的剧情走向,是策划设计了多周目数值继承。只要玩多周目,不管你的说服选项多么无厘头,只要积累的数值够高,总是可以说服各种价值观的角色同意自己的观点。知道这点之后,我和老婆就对游戏的“自由度”系统丧失了全部兴趣。

2、剧情对话是三元,但投票几乎都是二元

这个游戏设计了3个价值观对应的结局,无可厚非。但是还保留了隐藏结局,比普通结局多出好几章游戏内容。从剧情走向来看,大概作者自己想表达“共存”是可能的,三个价值观并非一条路走到黑,如果共存就能导向圆满的隐藏结局。

总结一下就是,想要达成圆满结局,当遇到关键的选择时,1个选择需要功利,1选择需要道德,1个选择需要自由。然而每次都是二元选择,其中一项数值无法发挥作用。

这个时候无力感就出现了,凭什么所有的选项都是合理的,探索也都是完成的,偏偏因为该项数值不够高就不能继续呢?策划按着玩家的头,让玩家吃屎。

3、都2222年了游戏还有死档

不得不说,剧本策划对自由派的心态拿捏还是比较准的。打到第7章,按照喜好选择的方向,基本都是朝着“自由”方向的抉择。单说这一点,我还是比较满意的。但是道德和功利的选项就很模糊,也可能是三观不合导致我不太能分辨。

然而到第7章出现的第一次投票,居然完全没有自由任何事情。其他两项数值非常低的主角就这样被锁死了选项,这条路线策划安排是“功利”,投票结果就是功利。所谓的自由度在这一刻碎成了渣渣,策划甚至不用特别麻烦的写剧情,只要操作骑墙派角色选择功利,就可以轻易导向功利的结果。

游戏不同于现实,在游戏玩家遇到两难选择的时候,因为游戏中的道德行为成本很低,大部分人应该是倾向于道德路线的。策划在这里强行喂屎,抛开剧情矛盾,自然会激起玩家的讨论。所以redit上充满了各种询问,究竟怎样S/L才能作道德选择呢?答案是你的存档已经被锁死了,想选就下周目吧。诛心的说,难道是为了话题性和营销,脸都不要了?

针对这一点,老婆表示,系统策划你已经失败了,想改进就等下辈子吧。

一开始就让玩家在三观上就有优劣之分,要么是做太多路线钱不够,要么就是剧本向着数值妥协了,总之这游戏给皇骑提鞋都不配,可惜了这套系统和美术。

这个SE的工作室的游戏都得拉黑了,制作人本质是个刷子爱好者,从《勇气默示录》到《八方旅人》,透露着一股所有东西都得给我刷级刷钱刷周目的设计让路的态度,残念啊……

Culture Shock

又被各民族迥异的文化震惊了,困扰我好几个月的神秘的bug。

起因是玩家报告说游戏启动之后画面特别的小,所有游戏场景都微缩在窗口正中,看不清也点不到,还有截图。并且有人说了之后,还有其他人附和。

我起初没特别在意,因为报这个错的人非常少,而且发帖的人不是English Native Speaker,甚至回帖的人都直接用德语在聊天。我还以为是来自欧洲的troll,想展示一下自己电脑分辨率特别高。但看起来又不像,毕竟他贴出来的分辨率还没有我的高……

因为没有头绪就不了了之了,直到几个月之后,有中文玩家开始报告同样的错误。这次就比较警觉了,我猜测是控制屏幕比例的代码有问题,读了好几遍觉得没啥逻辑错误。于是加了很多log,搞了个特殊版本,拜托玩家试试看。

结果虽然报bug的有那么几个,但是肯帮忙测还发log出来的就没有那么多了,毕竟不是人家的义务,我也只能等。过了好几天,终于有个热心玩家回馈了log文件,我看了好几遍,突然发现输入的“设计分辨率”参数居然变成了10倍!根据我设计的缩放逻辑,实际画面就被缩小到了1/10!

这下就搞了,为什么会出现这种问题?设计分辨率是储存在配置文件里的字符串啊……等等,我去翻了下配置,发现里面是这样写的:

[1920.0, 1080.0]

而log里面打印出来是这样的:

[19200,0, 10800,0]

这么多逗号是什么鬼?我突然想到了几个月前德国人发的帖子……等一下,莫非德国用的小数点是逗号?然后我就打开了c# fiddle,试了下这样的代码:

var cur = new CultureInfo("de");
Console.WriteLine("Hello World: " + float.Parse("1920.0",cur));

果然,1920.0被解析成了19200,整整翻了10倍……如果有人好奇的话,数字中的dot似乎被简单的忽略了。所以这个bug的成因是德语区的玩家因为本地的CultureInfo不同,解析字符串的时候会出错。

而之前的游戏没有错,是因为我储存了[1920,1080]这样的整数,后来编辑器修改,这部分以浮点数的形式输出了,就多了个小数点。看来信玩家的时间,应该也不是在中国,猜测是在欧洲生活的玩家就是了。

我也不知道为何Unity要去自动适应这种东西,可能是.NET的原因吧。目前的修复方法,强制在游戏的Start代码中加入

CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

这样至少保证主线程不适用本地的Culture,各种float.Parse和ToString就不用一个个去修改了。不幸中的万幸是,还好分辨率这关把玩家直接卡在了门外,要不然各种数值逻辑非得爆炸不可。

顺便记录下测试方法:需要在Windows10上修改显示语言,Setting/Language选项,添加新的语言,比如德语。然后再把显示语言修改为德语即可。不需要修改那个Unicode的东西,只要显示变了就会出bug,只能说.NET太商务了。

嗯嗯,奇怪的知识又增加了。

翻到十年前的贴

因为某些原因翻到了blog第一页,看到了十年前Google离开中国的时候,我发出过的感慨。那会儿我称Google是“伟大的公司”,对它的离开悲痛万分。这么多年过去了,作为深度Google用户经历了一些事情之后,就再也不好意思这么吹了。也可能是我变了,当然互联网肯定是变了。

和老婆讨论现在的生态,就说到当年原始人靠天吃饭,春天播种秋天收获。如果遇到天灾只能饿肚子,甚至有生命危险。然而老天为何发怒,降下灾厄,原始人无法解释,因此就诞生了各种天神崇拜。宗教固然无法解释根本原因,可是如果手法恰好有效,信仰就会得到加强。如果和事实相悖,大家定是不敢质疑天神,只能从自身寻找原因。

找不到原因只能乱来,比如杀个人祭天,穷尽用各种手法试图和混沌建立交流。然而天神自然不会响应,原始人也就只能靠结果来证实或证伪自己的猜测。还好随着文明的进步,人类逐渐可以控制和分析天象,神秘力量才渐渐边缘化。可能从杀人祭天变成杀羊,信仰也就不那么纯粹了。

我在想现如今互联网上也有神的存在,普通网民无法和神直接对话,只能自我审查。曾经作为先锋的Tech现在变成了Big Tech,人格逐渐丧失,神格倒是逐年增加了。这些万亿大厂就是赛博生态里的老大哥,掌管着网民的方方面面。普通人还有逃脱的机会吗?

我不打算爱老大哥,我想保留自己的独立人格。