一个好习惯,先给结论
在线体验地址:点我预览
代码地址:点我github
本文首发于:blog.gis1024.com/three_news_…
这里才是引言
本来预计第二章是写地球的创建的,但是在这之前还得写一下资源的预加载和三维场景的初始化,地球的创建只能放到第三章去啦。
资源预加载
预加载逻辑放到 preload.js
文件中,通过对three.js
自带的加载回调函数进行封装,改造成promise
形式。等所有promise
都完成后,再继续后续的操作。
我们将用到的红黄蓝色带、地球贴图、地球云层贴图、字体全都进行预加载。
代码如下:
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
目前还是实验性质,属于比较新的特性,在 webpack
、 vite
等构建工具中都要进行设置才能正确构建。
vite
中需要这样设置,代表构建成下一代js:
build: {
target: 'esnext'
}
这样可以打包,在最新chrome上也可以正确打开,但当我分享到iPhone手机上时,却死活打不开,整个裂开。
一番折腾后发现是手机safari浏览器不支持最新的js顶层await特性,裂开。
于是找了一个vite插件, vite-plugin-top-level-await
,按照其配置,可以将顶层await打包成常规js。其原理是将所有用到的顶层 await
进行一层封装,封装成 Promise.all
,再进行后续的操作,这里不赘述。有了这个插件,上面的build target配置也可以删除了。
三维初始化
在html中我们有
`<div id="three"></div>`
我们在 initThree.js
进行three.js场景的初始化,也是各种水文(包括本文😁)中常提的三大件: scene
(场景) 、 camera
(相机) 、 renderer
(渲染器)。
额外的,还需要搞一个 control
(控制器,旋转视角方便调试), EffectComposer
(场景后处理,方便进行特效处理), TWEEN
(补间库,插入到render函数中,后面的动画才能动起来) 。
代码如下:
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 };