🌏 把 MIUI 12 的超级火星搬到 Web 上
当初 MIUI 12 发布时,超级壁纸功能着实看起来特别的有感觉,之前已经申请开发版内测,第一时间体验下来整个桌面到息屏的动画流畅,最后手机息屏之后一个火星在屏幕上还是挺震撼的。
这几天瞥到宣传图突发奇想,不就是个3D球吗?WEB 上好像也能实现,要不试试?
预览可戳(16MB 左右,爪机慎点,源码在最后): 超级火星
整个内容包括俯瞰的一个视角、一个侧面部分视角、一个转场到目标位置的视角。
全部都是 3D 的内容,所以这块一定要用 Three.js 了,能把整个实现难度降低很多。
第一和第二个视角只需要将贴图蒙在球上,设置灯光、旋转和角度应该就能实现整体效果,在调整相机和灯光的位置,能够实现初步的转场。
第三个视角应该是独立的场景,需要一个地形结构和贴图,配合视角变化的转场效果。
网上直接找资源,不过先从官方安装包拆开看看,传说根据 NASA 数据生成的火星数据有没有在里面。
下载 apk 包,然后 apktool 反编译,拿到基本的目录,大概翻翻各个文件夹,主要目的是看看这个火星数据在哪?在
Mars_Bright\assets\bin\Data
目录下找到了点东西:看到有 unity 关键字,所以应该是 unity 技术实现,简单搜索下知道这个文件夹就是资源文件,AssetStudio 能够查看这些资源文件,在 Windows 电脑上下载运行,然后加载文件夹,看到全部的资源列表。
按照大小排序后发现了 Mars8k 这个文件,整整 50MB 的一张 8k × 4k 的图片,放大之后清晰度也很 ok。除此之外还能看到其他的资源贴图。
导出全部资源,能找到地标的 obj 和 fbx 形状文件、贴图文件等,这样就好办多了。不过好像文件都太大了,动辄几十兆,这个后面优化。
- Mars8k.png 星球贴图
- mars1/mars1.fbx 地面一个点的地形
- mars1/landscape6.png 地形的贴图
- mars1/landscape6_n.png 夜间地形贴图
- mars1/landsmid11.png 放大转场的效果图
星球贴图能够直接贴在球上,然后 fbx 格式的模型可以被 three.js 直接加载,加上贴图就行,开搞。
这一步只需要 Mars8k.png 这个文件
用 react 做简单的 UI,然后搭建一个基本的代码框架,以下代码仅展示逻辑,文章最后有源码地址:
以下部分都可以在 three.js 网站的 在线编辑器 上可视化搭建。
import {Group,Mesh,MeshLambertMaterial,PerspectiveCamera,PointLight,Scene,SphereGeometry,WebGLRenderer,Vector3,} from "three";// 初始化渲染器const renderer = new WebGLRenderer({alpha: true,antialias: true,canvas: canvasElement,});// 添加各个部件const camera = new PerspectiveCamera(); // 相机const sunOrbit = new Group(); // 太阳轨道const globe = new Group(); // 地球const globeSphere = new Mesh(); // 球体const scene = new Scene(); // 场景// 加个环境光用来测试scene.add(new AmbientLight("white", 5));// 合体scene.add(sunOrbit);globe.add(globeSphere);scene.add(camera);scene.add(globe);// 渲染部分const render = (): void => {renderer.render(scene, camera);// TODO: 星球和太阳的运行// TODO: 相机的转场更新}// 循环器const animate = (): void => {render();requestAnimationFrame(animate);}// 监听执行 resize 更新视角大小,这里略const viewPortChangeHandler = (): void => {renderer.setSize(window.innerWidth, window.innerHeight);camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();}window.addEventListener('resize', viewPortChangeHandler)viewPortChangeHandler()
以上逻辑很简单。
// 测试用camera.position.set(300 * 4, 0, 0);camera.lookAt(0, 0, 0);camera.fov = 45; // 视角广度camera.far = 300 * 10; // 视角远点camera.near = 0.01; // 视角近点
// 设置形状和顶点数globeSphere.geometry = new SphereGeometry(300, 100, 100);// 添加材质和贴图globeSphere.material = new MeshLambertMaterial({map: new TextureLoader(manager).load('textures/Mars8K_web_low.jpg')});
自转部分在 render 函数中调用:
// 每次渲染的时候绕 y 轴旋转 1/24°globeSphere.rotateY(Math.PI / 180 / 24);
太阳是个点光源,轨道可以使用一个 Group,让太阳作为子组件,然后轨道绕 x 轴旋转即可,不需要太复杂的轨道控制
// 在太阳的轨道上添加光源,这样只需要轨道旋转即可const sunLight = new PointLight("white"); // 太阳sunOrbit.add(sunLight)// 这样轨道在 x 正上方,光源不会被遮挡,旋转可以直接绕 x 轴sunOrbit.position.setX(300 * 2);// 设置太阳的相对位置sunLight.position.set(0, 300 * 3, 300 * 3);// 设置强度sunLight.intensity = 3;
轨道运行可以在 render 函数中调用:
// 每次渲染的时候绕 x 旋转 1/24°sunOrbit.rotateX(Math.PI / 180 / 24)
转场就是把相机视角移动到星球左上角,然后将太阳光移动到视角上方,所以过程就是两个物体的两个状态切换。
其中用到 TWEEN,用来做动画过程中值的过渡:
import TWEEN from "@tweenjs/tween.js";// 保存过渡前后的太阳角度const lastSunOrbitRotate = 0// 太阳部分 ↓lastSunOrbitRotate = sunOrbit.rotation.x;const sunOrbitTween = new TWEEN.Tween({ x: lastSunOrbitRotate });// 更新过程中把中间值添加到物体上sunOrbitTween.onUpdate((o: {x: number}) => {sunOrbit.setRotationFromAxisAngle(new Vector3(1, 0, 0), o.x);});// 过渡到 0 角度sunOrbitTween.to({ x: 0 }, 3000).start();// 相机部分需要考虑位置可看的方向 ↓const before = {pos: { x: 300 * 4, y: 0, z: 0 },look: { x: 0, y: 0, z: 0 },}const after = {pos: { x: 300 * 2.5, y: 300 / 8, z: 300 / 2 },look: { x: 300, y: 300 / 8, z: 300 / 2 },}const cameraPosAtTween = new TWEEN.Tween({ ...before.pos });cameraPosAtTween.onUpdate((o: XYZ) => camera.position.set(o.x, o.y, o.z))cameraPosAtTween.to({ ...after.pos }, 300).start();const cameraLookAtTween = new TWEEN.Tween({ ...before.look });cameraLookAtTween.onUpdate((o: XYZ) => camera.lookAt(o.x, o.y, o.z));cameraLookAtTween.to({ ...after.look }, 300).start();
至此,能实现火星阳光的照射效果,然后添加火星自转、添加太阳绕着火星旋转,然后将视角和太阳角度在两个状态之间切换,也就能实现最基本的功能了,再加上亿点细节,能够基本实现超级壁纸的效果。
实现了星球的效果,那么降落到具体的地方呢?
这两个场景首先做在同一个场景中有点困难,毕竟这个地点如果实际放到星球的某个位置,让相机去放大,先不说效果怎么样,光线的部分不太好安排,加上整个尺度和单位都不太好整,所以并没有这么做,不过技术上应该是可以实现的。
这个项目用到的方法是把地面的模型放到了另一个场景中,然后从 dom 上来处理切换过程。
下面先实现第三个场景。
从原来的资源中导出来的是 fbx 的格式,Three.js 能够直接加载,
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";/*fbx 里面包含贴图的资源引用,所以必须将textures/landscape6.pngtextures/landscape6_n.pngtextures/landsmid11.pngtextures/Default-ParticleSystem.png这几个文件也放在同一级目录,不然会报 404,并且没有贴图*/new FBXLoader().load('textures/mars1.fbx', (model: Group) => {scene.add(model)})// 加个环境光用来测试scene.add(new AmbientLight("white", 5));
视角落下、偏移、旋转的部分,和之前相似,设置好参数和过渡即可,这里不再啰嗦,详细可以查看源码。
实现效果之后,如果要放到 web 上,还有一个很严重的问题就是资源大小。一个星球图 50MB 在本地调试可以,但是放到线上是一定不可能接受的。
- png 贴图转为 jpg(或者更好的 basis 格式),然后品质设为 60% 或者能接受的程度,只看效果的话,50MB 压缩到 4.94MB 最后,并没有很影响观感。
- 另一方面是裁剪不需要的模型和贴图,例如其中一些模型的贴图并不需要,那么可以从模型中直接删除。
- 最后是在 three.js/editor 中做一点简单的压缩和顶点处理。
fbx 实现之后,效果还可以,但是贴图一部分区域出现闪烁的情况,可能是因为后续变更材质的原因,总之效果很差,最后使用 three.js/editor 转换成了 glTF 模型,能够很好的实现效果,甚至将贴图直接嵌入模型文件中实现更小的二进制文件。
转成 glTF 模型之后,闪烁问题解决了,但是颜色差别特别大,以为是材质问题、光线问题等,调试很久之后才发现 官网文档 glTF 说的颜色空间问题。
renderer.outputEncoding = THREE.sRGBEncoding;
最后的源码包含了很多的尝试和处理,比文章这一点代码有很多不同:
感谢您的阅读,本文由 Ubug 版权所有。如若转载,请注明出处:Ubug(https://ubug.io/blog/move-miui-12-super-wallpaper-to-web)