一个好习惯,先给结论

最终效果如下:

在线体验地址:点我预览

代码地址:点我github

本文首发于:https://blog.gis1024.com/three_news_op_2.html

这里才是引言

本来预计第二章是写地球的创建的,但是在这之前还得写一下资源的预加载和三维场景的初始化,地球的创建只能放到第三章去啦。

资源预加载

预加载逻辑放到 preload.js 文件中,通过对three.js自带的加载回调函数进行封装,改造成promise形式。等所有promise都完成后,再继续后续的操作。

我们将用到的红黄蓝色带、地球贴图、地球云层贴图、字体全都进行预加载。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import * as THREE from "three";
import { TTFLoader } from "three/examples/jsm/loaders/TTFLoader.js";
import { Font } from "three/examples/jsm/loaders/FontLoader";

// 预加载图片
const images = [
"/assets/rgb-r-small.png",
"/assets/rgb-g-small.png",
"/assets/rgb-b-small.png",
];
const list = images.map((item) => {
return loadImage(item);
});
const rgb = await Promise.all(list);

// 预加载贴图
const textures = ["/assets/earthmap2k.jpg", "/assets/earthCloud.png"];
const textureList = textures.map((item) => {
return loadTexture(item);
});
const [earthTexture, cloudTexture] = await Promise.all(textureList);

// 预加载字体
const font = await loadTTF("/assets/minTFF/FangZhengHeiTiJianTi-1.ttf");

export { rgb, earthTexture, cloudTexture, font };

// 预加载图片
function loadImage(url) {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}

// 预加载贴图
function loadTexture(url) {
const loader = new THREE.TextureLoader();
return new Promise((resolve, reject) => {
loader.load(
url,
(texture) => {
resolve(texture);
},
(progress) => {},
(e) => reject(e)
);
});
}

// 预加载字体
function loadTTF(url) {
const loader = new TTFLoader();
return new Promise((resolve, reject) => {
loader.load(
url,
(json) => {
const font = new Font(json);
resolve(font);
},
(progress) => {},
(e) => reject(e)
);
});
}

有一点需要注意, preload.js 中用到了顶层 await 。在 dev 环境的时候是没事的,一切都正常,但是等到 build 时会报错,提示不能使用顶层 await

这是因为顶层 await 目前还是实验性质,属于比较新的特性,在 webpackvite 等构建工具中都要进行设置才能正确构建。

vite中需要这样设置,代表构建成下一代js:

1
2
3
build: {
target: 'esnext'
}

这样可以打包,在最新chrome上也可以正确打开,但当我分享到iPhone手机上时,却死活打不开,整个裂开。

一番折腾后发现是手机safari浏览器不支持最新的js顶层await特性,裂开。

于是找了一个vite插件, vite-plugin-top-level-await ,按照其配置,可以将顶层await打包成常规js。其原理是将所有用到的顶层 await 进行一层封装,封装成 Promise.all,再进行后续的操作,这里不赘述。有了这个插件,上面的build target配置也可以删除了。

三维初始化

在html中我们有

1
`<div id="three"></div>`

我们在 initThree.js 进行three.js场景的初始化,也是各种水文(包括本文😁)中常提的三大件: scene(场景) 、 camera(相机) 、 renderer(渲染器)。

额外的,还需要搞一个 control(控制器,旋转视角方便调试), EffectComposer (场景后处理,方便进行特效处理), TWEEN(补间库,插入到render函数中,后面的动画才能动起来) 。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js";
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";

const scene = new THREE.Scene();

const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});

const dom = document.getElementById("three");
const domW = dom.offsetWidth;
const domH = dom.offsetHeight;

// 屏幕物理像素和px比率
renderer.setPixelRatio(window.devicePixelRatio);
// three.js 的色彩空间渲染方式
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.textureEncoding = THREE.sRGBEncoding;
// 设置canvas宽高
renderer.setSize(domW, domH);
// 将renderer 加到dom元素上
dom.appendChild(renderer.domElement);

const camera = new THREE.PerspectiveCamera(45, domW / domH, 1, 1000);
camera.position.set(0, 0, 35);

const controls = new OrbitControls(camera, renderer.domElement);
controls.maxDistance = 800;
controls.autoRotateSpeed = 1.7;
if (!import.meta.env.DEV) controls.enabled = false;

const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms["resolution"].value.set(
1 / window.innerWidth,
1 / window.innerHeight
);
composer.addPass(effectFXAA);

function animate() {
controls.update();
requestAnimationFrame(animate);
composer.render();
TWEEN.update();
}
animate();

window.addEventListener("resize", () => {
const dom = document.getElementById("three");
const domW = dom.offsetWidth;
const domH = dom.offsetHeight;
renderer.setSize(domW, domH);
renderer.setPixelRatio(window.devicePixelRatio);
composer.setSize(domW, domH);
composer.setPixelRatio(window.devicePixelRatio);
camera.aspect = domW / domH;
camera.updateProjectionMatrix();
});

// 如果是开发环境添加坐标轴,方便调试
if (import.meta.env.DEV) {
const helper = new THREE.AxesHelper(200);
scene.add(helper);
}

const light = new THREE.AmbientLight("#ffffff", 0.8);
scene.add(light);

const light2 = new THREE.DirectionalLight("#ffffff", 0.5);
light2.position.set(1, 1, 1).normalize();
scene.add(light2);

window.app = { scene, camera, renderer, controls, composer };

export { scene, camera, renderer, controls, composer };

本文首发于:https://blog.gis1024.com/three_news_op_2.html