[Minecraft]你的皮肤是怎么显示出来的?皮肤加载与渲染详解

在MC中,只要是拥有正版的玩家在正版服务器中都能显示自己独特的皮肤。那么,这些皮肤是怎么加载与渲染的呢,这篇专栏将会对皮肤机制进行详解。
目录
加载皮肤
GameProfile/游戏档案
Yggdrasil API
应用皮肤
ClientboundPlayerInfoPacket
渲染
如何自定义你的皮肤
正版更换皮肤原理
代理Yggdrasil服务
使用URL定位你的皮肤
一.加载皮肤
1.GameProfile/游戏资料
在MC中,每个玩家都有GameProfile。这个游戏资料的内部存储了玩家的名称、UUID和皮肤信息,位于authlib库中。它的定义如下:
在PropertyMap中包含了我们的皮肤数据。皮肤数据是这个表中的一个Property,它是这样定义的:
在value中保存的就是我们的皮肤数据,是一个JSON。这个JSON是这样的:
可以看到,皮肤最后是引用到了一个URL上,这个URL通常是来源于http://textures.minecraft.net/textures/<哈希码值>
在了解这些后,我们看看GameProfile是如何被序列化的。
GameProfile也可以转变为JSON,它的JSON看起来像这样(也把PropertyMap的数据写入了)
玩家可以通过GameProfile得到皮肤的信息,那么MC是如何获得玩家的游戏数据的呢。接下来就是获得GameProfile的方法:Yggdrasil API
2.Yggdrsail API
MC采用了一套特殊的API用于玩家账户的验证和获取数据,这就是Yggdrasil API,位于authlib库中。这篇专栏主要讨论它加载皮肤的部分。
在玩家加入世界后,数据是不完全的,只含有玩家名称和UUID。这时系统就会调用fillGameProfile填充玩家的数据,补全成为上文那样。
Yggdrasil API在进行GameProfile数据填充时会向https://sessionserver.mojang.com/profile/<玩家UUID>发送一个GET请求,附加参数unsigned代表需不需要进行签名。在通常情况下,requireSecure都为true,也就是会请求签名。GET请求的结果是一个JSON文本,内容就是上文提到的GameProfile的序列化JSON。通过这个JSON,系统就能解析出玩家的资料。
获取皮肤使用了另一个方法:getTextures,它能通过玩家的资料提取出皮肤材质信息:
从这段代码中,我们可以看到皮肤的成功加载由下面几个条件:
1)这个皮肤数据必须是有签名的,否则不进行解析
2)找到文件签名后,就用Yggdrasil API的公钥进行解密。这个公钥在MC的文件中,位于/yggdrasil_session_pubkey.der。解密之后的文本应该与value相同,否则验证失败
3)进行解析,提取出URL。如果URL不属于白名单上的域名(这些在白名单的域名有".minecraft.net"和".mojang.com"),那么也不会通过验证
在三重验证之后,就得到了一个皮肤的映射,可以让客户端下载并使用。
但是,填充GameProfile是服务端的操作,而getTexture是客户端操作,客户端要怎么获取填充好的GameProfile呢?
二.应用皮肤
1.ClientboundPlayerInfoPacket
紧接着上文,客户端需要服务端的已经填充好的GameProfile,那么服务端就要发送一个数据包向客户端通知客户端获得数据,这个数据包就是ClientboundPlayerInfoPacket。
这个数据包一共有5种模式:ADD_PLAYER(玩家加入),UPDATE_GAME_MODE(改变游戏模式),UPDATE_LATENCY,UPDATE_DISPLAY_NAME(显示名称改变),REMOVE_PLAYER(玩家移除)。有关于GameProfile的传输是在ADD_PLAYER内部的。
在玩家加入服务器后,服务器会把所有玩家(包括自身)的GameProfile通过ADD_PLAYER模式发送到客户端。
在这个数据包传到客户端后,客户端的监听器将处理这个数据包:
GameProfile的数据会在这时传入PlayerInfo中,之后渲染系统会访问这个对象获取皮肤信息。先看看渲染系统是怎么编写的。
2.渲染
玩家的渲染在PlayerRenderer中,它通过AbstractClientPlayer的方法获取皮肤数据,下面是一个例子:
那么AbstractClientPlayer是哪里来的呢?答案还是在客户端数据包监听器内部。
在进入服务器时,服务器会把所有玩家GameProfile发送到客户端。而当其他玩家进入本玩家可视范围内时,服务端会再发送一个ClientboundAddPlayerPacket用于通知。通过这个数据包,客户端可以添加这个玩家对象。
PlayerInfo在这时就传入到了RemotePlayer(AbstractClientPlayer的子类)中。当渲染器渲染玩家时,调用getSkinTextureLocation,进而调用PlayerInfo的getSkinLocation。如果没有下载皮肤材质,调用皮肤下载器,使用GameProfile中的URL地址下载到皮肤。
如果皮肤材质加载失败了,那么系统会自动使用Steve/Alex皮肤。使用这两个默认皮肤不是随机的,它有关于玩家UUID的哈希码值。
至此,你的皮肤就展示在你的屏幕上了!
三.如何自定义你的皮肤
1.正版更换皮肤原理
官方启动器和很多第三方启动器都支持更换皮肤,它们是通过API进行换肤操作。

这个API的基础网址是https://api.mojang.com/user/profile/<玩家UUID>/skin,支持三种操作:上传皮肤,改变皮肤,重置皮肤。注意:修改皮肤只能在已登录条件下执行,因为这些操作都需要提供accessToken(访问令牌)
1)上传皮肤
请求为PUT,Headers需要包含“Authorization”一项,值为“Bearer <访问令牌>”
负载数据有两项:
"model"-值为slim或者为空字符串,slim代表Alex模型,空字符串代表Steve模型
"file"-负载数据为图片
2)改变皮肤
请求为POST,Headers要求与第一项相同
在URL上负载数据“model”和“url”,分别为模型和材质地址。材质地址只能属于第一节所述的几个网站,否则不会成功换肤
返回值正常为空负载
3)重置皮肤
请求为DELETE,Headers要求与第一项相同,无负载
2.代理Yggdrasil服务
对于没有正版账号的玩家来说,无法更换皮肤,但是如果将Yggdrasil服务换成自己的,不就能用自己的皮肤了吗?这就是“皮肤站”和“外置登录”的来源。

实现这个功能,就必须在启动MC时加入一个Java Agent进行代码修改,现在大家使用的注入器就是authlib-injector,它能修改Yggdrasil API的底层代码,使Yggdrasil服务定位到皮肤站的API上,这样就完成了代理。
(注:这种操作也让登录定位到了新的API上,所以MC系统会把你识别为“正版玩家”)
3.使用URL定位你的皮肤
根据Yggdrasil API的原理,可以看到我们可以只修改来源于服务端的GameProfile中的textures属性数据同时撤销客户端的Yggdrasil API的三重检查来达到重新定位皮肤的目的。
可是怎么修改呢?
首先是服务端,服务端在online-mode为false时会自动调用ServerLoginPacketListenerImpl#createFakeProfile生成一个“假的”游戏资料,,UUID的生成与名称有关:
这个对象中不包含textures属性。我们可以在这里插入代码使它加上数据,这样传送到客户端的数据就包含了skin属性。(注意,此处不要生成signature,因为没有私钥生成不了)
另一部分是客户端,我们只需要撤销掉三重检查就可以达到目的了。由于Mixin无法修改authlib库内部的类,所以我们可以采用继承+代理这个类更换掉Minecraft类中的对象。
这样,就可以自定义你的皮肤了!(对披风一样管用,只是多加了一点代码而已)

代码来源:1.18_experiment-snapshot-1,Mojang Mapping,使用Yarn Mapping的类名及方法名可能不同,但是逻辑一样
代码反混淆器及反编译器:MCDynamicExchanger beta8,bug正在修复
Mojang API的用法:https://c4k3.github.io/wiki.vg/Mojang_API.html