简单使用 ThreeJS
参考文章:
先来看一下一个简单的效果
ThreeJS
官方就是这么简单的一个介绍
"Javascript 3D library"
OpenGL 是一个跨平台 3D/2D 的绘图标准,WebGL 则是 openGL 在浏览器上的一个实现。web 前端开发人员可以直接用 WebGL 接口进行编程,但 WebGL 只是非常基础的绘图 API,需要编程人员有很多的数学知识、绘图知识才能完成 3D 编程任务,而且代码量巨大。Threejs 对 WebGL 进行了封装,让前端开发人员在不需要掌握很多数学知识和绘图知识的情况下,也能够轻松进行 web 3D 开发,降低了门槛,同时大大提升了效率。
ThreeJS 的几个要素
(以下具体 API 可以参照文档)
1. 场景
场景就像环境一样,用来放我们要添加的所有东西。
this.scene = new THREE.Scene()
2. 相机
相机可以当作我们的眼睛,没有相机,我们就看不到任何东西。相机有很多种。
camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
camera.position.set(0, -18, 15)
3. 灯光
就像我们没有灯光就看不见东西一样,我们需要灯光来照亮东西。光分很多种,有环境光,点光源,聚光灯,平行光等等,不同的光的照亮效果不一样。
// 环境光
ambientLight = new THREE.AmbientLight(0xbbbbbb)
scene.add(ambientLight)
// 平行光 (与点光源不同 是从一个方向来 不是从一个点)
directionalLight = new THREE.DirectionalLight(0x666666)
directionalLight.position.set(10, -50, 300)
scene.add(directionalLight)
4. 场景控制器
这里我们用它来控制场景里的所有东西,当我们拖动场景缩小放大的时候,实际上就是在控制相机移动。
controls = new OrbitControls(camera, renderer.domElement)
5. 渲染器
渲染器,把注册的所有东西渲染到场景中。
//注册渲染器
const canvas = document.querySelector('#map')
renderer = new THREE.WebGLRenderer({ canvas, alpha: true })
//渲染
renderer.render(this.scene, this.camera)
requestAnimationFrame(this.render)
绘制一个 3D 地图,并且带鼠标高亮
这里详细讲解一下如何画一个地图,并且做一个鼠标移动高亮的效果。
1.我们可以用 json 数据,收尾连线的方式,画出轮廓线和中国地图,再挤压画出的平面图形,让其成为有厚度的 3D 模型。
2.我们也可以直接引入成型的地图模型,渲染就完事了。不过对于这种简单的模型,建议使用 json 数据画出来,因为加载模型的话,模型大小是一个问题,网页打开时加载模型需要一些时间,影响体验,而且,你得有一个愿意配合你的小伙伴。
我们采用上面所说的第一种方式
简单讲解一下画模型的思路: 首先,模型由两点构成 几何体 和 网格,我们可以理解为,一个没穿衣服的光秃秃模型和它的衣服。所以显然易见,我们选择一个几何体(或者画出一个几何体),然后给他穿个衣服(给他捯饬捯饬,弄点颜色透、明度之类的属性,也可以贴图上去)
1. 画出轮廓线
/* group-组,将一个国家的轮廓线放在同一个 group 中(分组),这样可以在场景中进行整体控制
如果不分组,在复杂场景中过多的模型对象会很混乱,难以维护 */
var group = new THREE.Group() // 一个国家多个轮廓线条line的父对象
/* pointArr:行政区一个多边形轮廓边界坐标(2个元素为一组,分别表示一个顶点x、y值)
通过BufferGeometry构建一个几何体,传入顶点数据
通过Line模型渲染几何体,连点成线
LineLoop和Line功能一样,区别在于首尾顶点相连,轮廓闭合 */
// 创建一个Buffer类型几何体对象
var geometry = new THREE.BufferGeometry()
// 类型数组创建顶点数据
var vertices = new Float32Array(pointArr)
// 创建属性缓冲区对象
var attribue = new THREE.BufferAttribute(vertices, 3) // 3个为一组,表示一个顶点的xyz坐标
// 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue
// 材质对象
var material = new THREE.LineBasicMaterial({
color: 0x008bfb // 线条颜色
})
// var line = new THREE.Line(geometry, material);// 线条模型对象(轮廓不闭合)
var line = new THREE.LineLoop(geometry, material) // 首尾顶点连线,轮廓闭合
group.add(line)
2. 画出地图形状,并且挤压出厚度
var shapeArr = [] // 轮廓形状Shape集合
pointsArrs.forEach((pointsArr) => {
var vector2Arr = []
// 转化为Vector2构成的顶点数组
pointsArr[0].forEach((elem) => {
vector2Arr.push(new THREE.Vector2(elem[0] - this.offsetX, elem[1] - this.offsetY))
})
var shape = new THREE.Shape(vector2Arr)
shapeArr.push(shape)
})
// MeshBasicMaterial:不受光照影响
// MeshLambertMaterial:几何体表面和光线角度不同,明暗不同
var material1 = new THREE.MeshPhongMaterial({
color: this.bgColor,
specular: this.bgColor
})
var material2 = new THREE.MeshBasicMaterial({
color: 0x008bfb
})
// 拉伸造型
var geometry = new THREE.ExtrudeBufferGeometry(
shapeArr, // 多个多边形二维轮廓
// 拉伸参数
{
// depth:根据行政区尺寸范围设置,比如高度设置为尺寸范围的2%,过小感觉不到高度,过大太高了
depth: height, // 拉伸高度
bevelEnabled: false // 无倒角
}
)
var mesh = new THREE.Mesh(geometry, [material1, material2]) // 网格模型对象
3. 渲染模型
为什么要做响应式处理?
1.我们先来了解一下
requestAnimationFrame
[1] requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率
[2] 在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量
[3] requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销
2.即使用了 requestAnimationFrame,我们还要注意的是:
相机的会在每次
render()
时计算更新投影矩阵
,但是如果渲染区域没有变化的话,我们就没有必要一直计算更新,所以我们在每次执行渲染函数时,计算出渲染区域大小是否发生变化,如果没有,就不去更新它,以此节省性能
// 响应处理函数
resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement
const width = canvas.clientWidth
const height = canvas.clientHeight
// width和height是一开始记录的在网页种渲染区域的大小
const needResize = canvas.width !== width || canvas.height !== height
if (needResize) {
// 重新设置渲染器的渲染区域
renderer.setSize(width, height, false)
}
return needResize
}
//响应式渲染
render(){
if (this.resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement
// 重新计算相机的属性
camera.aspect = canvas.clientWidth / canvas.clientHeight
// 更新相机的投影矩阵
camera.updateProjectionMatrix()
}
controls.update()
renderer.render(scene, camera)
// 智能刷新
this.globalID = requestAnimationFrame(this.render)
}
4. 鼠标移动 区域高亮
实现方法:从一个地方发射一条射线,调用方法返回交叉点的数组,对穿过的模型进行操作
// 鼠标移动事件 高亮
handleMousemove(event) {
// event.preventDefault()
// 注册一个射线
this.raycaster = new THREE.Raycaster()
// 计算出位置坐标
let mouse = new THREE.Vector2(0, 0)
const canvas = document.querySelector('#map')
mouse.x = (event.offsetX / canvas.offsetWidth) * 2 - 1
mouse.y = -(event.offsetY / canvas.offsetHeight) * 2 + 1
// 用一个新的原点和方向向量来更新射线
this.raycaster.setFromCamera(mouse, camera)
// 检查射线和物体之间的所有交叉点(默认不包含后代) 返回的是对象数组
let intersects = this.raycaster.intersectObjects(meshGroup.children)
// 取第一个穿过的模型对象
this.previousObj.material[0].color = new THREE.Color(this.bgColor)
// 操作属性,此处为改变颜色,就达到了高亮的效果
if (intersects[0] && intersects[0].object) {
intersects[0].object.material[0].color = new THREE.Color(0xffaa00)
this.previousObj = intersects[0].object
}
},
补充
页面卸载的时候 一定要将 各种渲染器
卸载 否则再次显示组件的时候可能出现一些奇怪的 bug 比如多一个卡住的地图
省会柱子、柱子顶部label、光圈效果
简单来说:
1、找一个光圈的png图片作为纹理创建一个矩形平面,并随机设置初始size,然后在render函数中改变他的大小和透明度(借助requestAnimationFrame不停渲染)
2、找一个渐变的图片作为纹理创建柱状模型(注意设置柱体透明和顶部开口)
3、创建CSS2D 和js创建元素差不多,然后用labelRenderer渲染(注意创建坐标位置)
代码
(offsetX和offsetY是中国地图大致的中心坐标 之所以使用到相关坐标时要减去这两个值,是为了让地图的中心在坐标原点)
1.创建过程
this.cityData.forEach((item) => {
var pos = [item.longitude, item.latitude] // 每个省份行政中心位置经纬度
if (pos && pos.length > 0) {
// 添加圆圈
var geometry = new THREE.PlaneGeometry(1, 1) // 矩形平面
// TextureLoader创建一个纹理加载器对象,可以加载图片作为几何体纹理
var textureLoader = new THREE.TextureLoader()
// 执行load方法,加载纹理贴图成功后,返回一个纹理对象Texture
textureLoader.load(require('../../../assets/images/光圈贴图.png'), (texture) => {
var material = new THREE.MeshLambertMaterial({
color: 0xffffff,
// 设置颜色纹理贴图:Texture对象作为材质map属性的属性值
map: texture, // 设置颜色贴图属性值
transparent: true // 使用背景透明的png贴图,注意开启透明计算
}) // 材质对象Material
let mesh = new THREE.Mesh(geometry, material)
var size = Math.random() * 3 + 2 // 2~5之间随机,表示mesh.size缩放倍数
mesh.scale.set(size, size, size) // 设置mesh大小
mesh.position.set(pos[0] - this.offsetX, pos[1] - this.offsetY, 2.3) // 设置mesh位置
mesh._s = size // mesh自定义一个属性表征大小
cityPointGroup.add(mesh)
})
// 添加圆柱
let geometry2 = new THREE.CylinderGeometry(0.15, 0.15, 5, 32, 1, true)
geometry2.rotateX(Math.PI / 2)
geometry2.translate(0, 0, 1)
// geometry2.cityData = item
new THREE.TextureLoader().load(require('../../../assets/images/渐变.png'), (texture) => {
var material2 = new THREE.MeshBasicMaterial({ map: texture, transparent: true, blending: THREE.AdditiveBlending })
var mesh2 = new THREE.Mesh(geometry2, material2)
mesh2.position.set(pos[0] - this.offsetX, pos[1] - this.offsetY, 2.5) // 设置mesh位置
mesh2.cityData = item // 保存对应的城市数据
barGroup.add(mesh2)
})
// 创建css2d Label
this.labelRenderer = new CSS2DRenderer()
var div = document.createElement('div')
div.innerHTML = item.city + ': ' + item.count + '个'
div.style.display = 'block'
div.style.width = '8vw'
div.style.height = '4vh'
div.style.lineHeight = '4vh'
div.style.textAlign = 'center'
div.style.color = '#fff'
const img = require('@/assets/images/kuang.png')
div.style.background = 'url(' + img + ') no-repeat '
div.style.backgroundSize = '100% 100%'
div.style.fontSize = '0.6349vw'
div.style.position = 'absolute'
div.style.backgroundColor = 'rgba(25,25,25,0.5)'
div.style.borderRadius = '0.463vh'
var label = new CSS2DObject(div)
// 设置mesh位置
label.position.set(pos[0] - this.offsetX, pos[1] - this.offsetY, 5.5)
this.previousObj = label
scene.add(label)
this.labelRenderer.setSize(renderer.domElement.clientWidth, renderer.domElement.clientHeight)
this.labelRenderer.domElement.style.position = 'absolute'
// 避免renderer.domElement影响HTMl标签定位,设置top为0px
this.labelRenderer.domElement.style.top = '0px'
this.labelRenderer.domElement.style.left = '0px'
// 设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型
this.labelRenderer.domElement.style.pointerEvents = 'none'
document.querySelector('#box').appendChild(this.labelRenderer.domElement)
})
2.render中加入这一段
if (cityPointGroup.children.length) {
cityPointGroup.children.forEach(function (mesh) {
mesh._s += 0.02
mesh.scale.set(mesh._s, mesh._s, mesh._s)
if (mesh._s <= 2.6) {
mesh.material.opacity = (mesh._s - 2.0) * 1.67 // 1.67约等于1/(2.6-2.0),保证透明度在0~1之间变化
} else if (mesh._s > 2.6 && mesh._s <= 5) {
mesh.material.opacity = 1 - (mesh._s - 2) / 3 // 缩放5.0对应0 缩放2.0对应1
} else {
mesh._s = 2.0
}
})
}
总结
以上简单记录了如何画一个 3D 地图,其实总的来说,简单的模型没有什么难度,但是看下来大家也明白,写一套出来代码量也不少,优化的空间很大。
就像我们拆组件、写方法一样,我们可以将画轮廓、画地图的方法包装一下放到 JS 文件里导出,只要给 json 数据,就直接给他返回一个模型,然后再封装一下render()
函数,这样日后画一个地图就会很方便。还有鼠标高亮的效果,也可也封起来。