本项目使用Webpack 5 + Typescript 4 + Threejs + Shader 基础模板 搭建
Three.js
经常会和WebGL
混淆, 但也并不总是,three.js
其实是使用WebGL
来绘制三维效果的。 WebGL
是一个只能画点、线和三角形的非常底层的系统. 想要用WebGL
来做一些实用的东西通常需要大量的代码, 这就是Three.js
的用武之地。它封装了诸如场景、灯光、阴影、材质、贴图、空间运算等一系列功能,让你不必要再从底层WebGL
开始写起。
一个最基础的Three.js程序包括渲染器(Renderer)、场景(Scene)、相机(Camera)、灯光(灯光),以及我们在场景中创建的物体(Earth)。
此次主要是项目实战,其他理论基础知识请前往官方文档
使用webpack
打包,src/index.html
是入口文件,关于webpack
的知识不多赘述。
html 体验AI代码助手复制代码加载资源中...
#loading
: 加载中的loading效果
#earth-canvas
:将canvas绘制到此dom下面
#html2canvas
:将html转换成图片,显示地球标点
webpack 会将此文件打包成js,放进 index.html 中
typescript 体验AI代码助手复制代码import World from './world/Word' // earth-canvas const dom: HTMLElement = document.querySelector('#earth-canvas') new World({ dom, })
new World()
将dom传进去。
new Basic(dom)
: 传入dom,创建出threejs场景、渲染器、相机和控制器。
typescript 体验AI代码助手复制代码this.basic = new Basic(option.dom) this.scene = this.basic.scene this.renderer = this.basic.renderer this.controls = this.basic.controls this.camera = this.basic.camera
new Sizes(dom)
:传入dom,主要进行dom尺寸计算和管理resize事件。
typescript 体验AI代码助手复制代码this.sizes = new Sizes({ dom: option.dom }) this.sizes.$on('resize', () => { this.renderer.setSize(Number(this.sizes.viewport.width), Number(this.sizes.viewport.height)) this.camera.aspect = Number(this.sizes.viewport.width) / Number(this.sizes.viewport.height) this.camera.updateProjectionMatrix() })
new Resources(function)
:传一个function,资源加载完成后会执行此function。
typescript 体验AI代码助手复制代码this.resources = new Resources(async () => { await this.createEarth() // 开始渲染 this.render() })
new Earth(options)
:地球相关配置
typescript 体验AI代码助手复制代码 type options = { data: { startArray: { name: string, E: number, // 经度 N: number, // 维度 }, endArray: { name: string, E: number, // 经度 N: number, // 维度 }[] }[] dom: HTMLElement, textures: Record, // 贴图 earth: { radius: number, // 地球半径 rotateSpeed: number, // 地球旋转速度 isRotation: boolean // 地球组是否自转 } satellite: { show: boolean, // 是否显示卫星 rotateSpeed: number, // 旋转速度 size: number, // 卫星大小 number: number, // 一个圆环几个球 }, punctuation: punctuation, flyLine: { color: number, // 飞线的颜色 speed: number, // 飞机拖尾线速度 flyLineColor: number // 飞行线的颜色 }, }
将earth中的group
添加到场景中
通过await init
创建地球及其相关内容,因为创建一些东西需要时间,所以返回一个Promise
地球创建完之后隐藏dom,添加一个事先定义好的类名,使用animation
渐渐隐藏掉dom
typescript 体验AI代码助手复制代码this.scene.add(this.earth.group) await this.earth.init() // 隐藏dom const loading = document.querySelector('#loading') loading.classList.add('out')
render()
:循环渲染
地球中需要若干个贴图,在创建地球前,先把贴图加载进来。
typescript 体验AI代码助手复制代码/** * 资源文件 * 把模型和图片分开进行加载 */ interface ITextures { name: string url: string } export interface IResources { textures?: ITextures[], } const filePath = './images/earth/' const fileSuffix = [ 'gradient', 'redCircle', "label", "aperture", 'earth_aperture', 'light_column', 'aircraft' ] const textures = fileSuffix.map(item => { return { name: item, url: filePath + item + '.png' } }) textures.push({ name: 'earth', url: filePath + 'earth.jpg' }) const resources: IResources = { textures } export { resources }
我们把需要加载的资源文件全部列在这里,然后导出去。
typescript 体验AI代码助手复制代码/** * 资源管理和加载 */ import { LoadingManager, Texture, TextureLoader } from 'three'; import { resources } from './Assets' export class Resources { private manager: LoadingManager private callback: () => void; private textureLoader!: InstanceType; public textures: Record; constructor(callback: () => void) { this.callback = callback // 资源加载完成的回调 this.textures = {} // 贴图对象 this.setLoadingManager() this.loadResources() } /** * 管理加载状态 */ private setLoadingManager() { this.manager = new LoadingManager() // 开始加载 this.manager.onStart = () => { console.log('开始加载资源文件') } // 加载完成 this.manager.onLoad = () => { this.callback() } // 正在进行中 this.manager.onProgress = (url) => { console.log(`正在加载:${url}`) } this.manager.onError = url => { console.log('加载失败:' + url) } } /** * 加载资源 */ private loadResources(): void { this.textureLoader = new TextureLoader(this.manager) resources.textures?.forEach((item) => { this.textureLoader.load(item.url, (t) => { this.textures[item.name] = t }) }) } }
通过使用threejs
提供的LoadingManager
方法,管理资源的加载进度,以及保存一个textures
对象,key为name,value为Texture对象。
接下来,我们要去创建我们的主角了~
earth
:创建一个地球mesh,并赋予ShaderMaterial材质和贴上地球贴图,之后可以通过着色器动画实现地球扫光效果。
points
:创建一个由points组成的包围球,放在外围。
typescript 体验AI代码助手复制代码const earth_geometry = new SphereBufferGeometry( this.options.earth.radius, 50, 50 ); const earth_border = new SphereBufferGeometry( this.options.earth.radius + 10, 60, 60 ); const pointMaterial = new PointsMaterial({ color: 0x81ffff, //设置颜色,默认 0xFFFFFF transparent: true, sizeAttenuation: true, opacity: 0.1, vertexColors: false, //定义材料是否使用顶点颜色,默认false ---如果该选项设置为true,则color属性失效 size: 0.01, //定义粒子的大小。默认为1.0 }) const points = new Points(earth_border, pointMaterial); //将模型添加到场景 this.earthGroup.add(points); this.options.textures.earth.wrapS = this.options.textures.earth.wrapT = RepeatWrapping; this.uniforms.map.value = this.options.textures.earth; const earth_material = new ShaderMaterial({ // wireframe:true, // 显示模型线条 uniforms: this.uniforms, vertexShader: earthVertex, fragmentShader: earthFragment, }); earth_material.needsUpdate = true; this.earth = new Mesh(earth_geometry, earth_material); this.earth.name = "earth"; this.earthGroup.add(this.earth);
typescript 体验AI代码助手复制代码 for (let i = 0; i < 500; i++) { const vertex = new Vector3(); vertex.x = 800 * Math.random() - 300; vertex.y = 800 * Math.random() - 300; vertex.z = 800 * Math.random() - 300; vertices.push(vertex.x, vertex.y, vertex.z); colors.push(new Color(1, 1, 1)); }
typescript 体验AI代码助手复制代码const aroundMaterial = new PointsMaterial({ size: 2, sizeAttenuation: true, // 尺寸衰减 color: 0x4d76cf, transparent: true, opacity: 1, map: this.options.textures.gradient, });
地球边缘发光的效果,创建一个比地球大一点点的精灵片,贴上下图,而且精灵片是一直朝向摄像机的。
高德地图取坐标点
我们需要将threejs的物体放置在地球上,就需要将经纬度转球面坐标,这是有详细转换文档
lon2xyz()
我们直接用这个方法会把经纬度转成为球面坐标,拿到坐标我们就可以在对应的位置上创建物体
typescript 体验AI代码助手复制代码/** * 经纬度坐标转球面坐标 * @param {地球半径} R * @param {经度(角度值)} longitude * @param {维度(角度值)} latitude */ export const lon2xyz = (R:number, longitude:number, latitude:number): Vector3 => { let lon = longitude * Math.PI / 180; // 转弧度值 const lat = latitude * Math.PI / 180; // 转弧度值 lon = -lon; // js坐标系z坐标轴对应经度-90度,而不是90度 // 经纬度坐标转球面坐标计算公式 const x = R * Math.cos(lat) * Math.cos(lon); const y = R * Math.sin(lat); const z = R * Math.cos(lat) * Math.sin(lon); // 返回球面坐标 return new Vector3(x, y, z); }
这个白色和红色的柱子其实是两个mesh相交,并贴上贴图,通过转换过来的坐标放置在地球上。
typescript 体验AI代码助手复制代码// render if (this.waveMeshArr.length) { this.waveMeshArr.forEach((mesh: Mesh) => { mesh.userData['scale'] += 0.007; mesh.scale.set( mesh.userData['size'] * mesh.userData['scale'], mesh.userData['size'] * mesh.userData['scale'], mesh.userData['size'] * mesh.userData['scale'] ); if (mesh.userData['scale'] 1.5 && mesh.userData['scale'] 循环waveMeshArr组,让里面的mesh变大并且渐渐消失,之后一直重复。添加城市标签 createSpriteLabel()使用 html2canvas 创建精灵片标签html 体验AI代码助手复制代码typescript 体验AI代码助手复制代码const opts = { backgroundColor: null, // 背景透明 scale: 6, dpi: window.devicePixelRatio, }; const canvas = await html2canvas(document.getElementById("html2canvas"), opts) const dataURL = canvas.toDataURL("image/png"); const map = new TextureLoader().load(dataURL); const material = new SpriteMaterial({ map: map, transparent: true, }); const sprite = new Sprite(material); const len = 5 + (e.name.length - 2) * 2; sprite.scale.set(len, 3, 1); sprite.position.set(p.x * 1.1, p.y * 1.1, p.z * 1.1); this.earth.add(sprite);地球上的标签是通过html2canvas插件将html转换成贴图,贴到精灵片上。创建环绕卫星 createAnimateCircle()getCirclePoints获取一个圆环坐标点列表createAnimateLine通过圆环点位创建一个圆环线,并clone出3条,加上若干个小卫星通过每帧循环让线条转圈,并带动小卫星转typescript 体验AI代码助手复制代码// render() this.circleLineList.forEach((e) => { e.rotateY(this.options.satellite.rotateSpeed); });End感谢观看,如果有什么意见或者疑问请随时联系~
有话要说...