目录
- 最终效果
- 代码实现
- 创建项目
- DigitalMapView.vue的核心代码
最终效果
最近事情比较多,今晚难得有空,就抽空完成了一个使用Threejs实现地图雷达扫描效果的程序,下面说下代码实现的原理及核心代码,老规矩,先看下效果图
# 实现原理 通过加载模型文件,实现模型的加载,这里使用的是FBX模型,通过Threejs提供的FBXLoader来加载fbx模型,加载完成后,通过traverse方法遍历模型,并对该模型的子类进行不同的颜色设置,这里主要是对建筑的颜色定义和对地面的颜色定义;然后,通过使用threejs提供的CircleGeometry创建几何体,并设置其材质;最后,通过使用着色器对雷达效果进行渲染,通过调用THREE.Clock()创建一个计时器实现雷达的扫描动画
代码实现
创建项目
使用vite构建工具创建一个vue项目,具体如何创建就不在赘述了,如果你还不知道如何创建项目,建议你先不要看下面的内容了,先看下基础的vue3框架再来阅读下面的内容 创建项目后,删除vite构建工具为我们创建的HelloWord.vue文件和style.css中的样式,删除App.vue中的样式,并在components文件夹下新建DigitalMapView.vue文件 创建完成后的基础代码如下 APP.vue代码
<template> | |
<DigitalMapView></DigitalMapView> | |
</template> | |
<script setup> | |
import DigitalMapView from './components/DigitalMapView.vue'; | |
</script> | |
<style scoped> | |
</style> |
style.css中的样式代码如下: *{ margin: 0; padding: 0; list-style: none; }
DigitalMapView.vue的核心代码
- 在DigitalMapView.vue的template模板中添加一个div,id设置为scene,作为承载Threejs的容器; template模板中代码如下:
<template> | |
<div id="scene"></div> | |
</template> |
- 在script标签中引入threejs、OrbitControls 、FBXLoader 及vue中的onMounted 生命周期函数
import * as THREE from 'three' | |
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' | |
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' | |
import { onMounted } from 'vue'; |
- 创建场景、相机、灯光、渲染器、控制器等Threejs用到的各个要素
const initScene = () => { | |
scene = new THREE.Scene() | |
scene.background = new THREE.Color(0xcccccc) | |
scene.environment = new THREE.Color(0xcccccc) | |
} | |
const initCamera = () => { | |
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000) | |
camera.position.set(600, 750, -1221) | |
scene.add(camera) | |
} | |
// 添加灯光 | |
const initLight = () => { | |
const light = new THREE.AmbientLight(0xadadad); // 环境光,soft white light | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); // 方向光 | |
directionalLight.position.set(100, 100, 0); | |
scene.add(light); | |
scene.add(directionalLight); | |
} | |
const initRenderer = () => { | |
renderer = new THREE.WebGLRenderer({ antialias: true }) | |
renderer.setSize(window.innerWidth, window.innerHeight) | |
document.getElementById('scene').appendChild(renderer.domElement) | |
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
renderer.setClearColor(new THREE.Color('#32373E'), 1); | |
} | |
// 添加控制器 | |
const initControl = () => { | |
controls = new OrbitControls(camera, renderer.domElement) | |
controls.enableDamping = true | |
controls.dampingFactor = 0.25 | |
controls.enableZoom = true | |
} | |
// 渲染循环 | |
const playAnimate = () => { | |
const animate = function () { | |
requestAnimationFrame(animate) | |
renderer.render(scene, camera) | |
} | |
animate() | |
} |
添加FBX模型 使用FBXLoader加载fbx模型
loader = new FBXLoader() | |
loader.load('public/data/shanghai.fbx', (object) => { | |
model = object | |
model.traverse((child) => { | |
// 设置城市建筑(mesh物体),材质基本颜色 | |
if (child.name == 'CITY_UNTRIANGULATED') { | |
const materials = Array.isArray(child.material) ? child.material : [child.material] | |
materials.forEach((material) => { | |
// material.opacity = 0.6; | |
material.transparent = true; | |
material.color.setStyle("#9370DB"); | |
}) | |
} | |
// 设置城市地面(mesh物体),材质基本颜色 | |
if (child.name == 'LANDMASS') { | |
const materials = Array.isArray(child.material) ? child.material : [child.material] | |
materials.forEach((material) => { | |
// material.opacity = 0.6; | |
material.transparent = true; | |
material.color.setStyle("#030912"); | |
}) | |
} | |
}) | |
scene.add(model) | |
}) |
刷新浏览器,可以看到此时模型已经加载到页面
- 雷达效果实现 首先在场景中绘制一个圆
const radarData = { | |
position: { | |
x: 0, | |
y: 20, | |
z: 0 | |
}, | |
radius: 240, | |
color: '#f005f0', | |
opacity: 0.5, | |
speed: 300, | |
followWidth: 220, | |
} | |
// 创建几何体 | |
const circleGeometry = new THREE.CircleGeometry(radarData.radius, 1000) | |
const rotateMatrix = new THREE.Matrix4().makeRotationX(-Math.PI / 180 * 90) | |
circleGeometry.applyMatrix4(rotateMatrix) | |
// 创建材质 | |
const material = new THREE.MeshPhongMaterial({ | |
color: radarData.color, | |
opacity: radarData.opacity, | |
transparent: true, | |
}) | |
const radar = new THREE.Mesh(circleGeometry, material) |
使用着色器渲染雷达效果
const vertex = ` | |
varying vec3 vPosition; | |
void main() { | |
vPosition = position; | |
` | |
shader.vertexShader = shader.vertexShader.replace('void main() {', vertex) | |
const fragment = ` | |
uniform float uRadius; | |
uniform float uTime; | |
uniform float uSpeed; | |
uniform float uFollowWidth; | |
varying vec3 vPosition; | |
float calcAngle(vec3 oFrag){ | |
float fragAngle; | |
const vec3 ox = vec3(1,0,0); | |
float dianji = oFrag.x * ox.x + oFrag.z*ox.z; | |
float oFrag_length = length(oFrag); | |
float ox_length = length(ox); | |
float yuxian = dianji / (oFrag_length * ox_length); | |
fragAngle = acos(yuxian); | |
fragAngle = degrees(fragAngle); | |
if(oFrag.z > 0.0) { | |
fragAngle = -fragAngle + 360.0; | |
} | |
float scanAngle = uTime * uSpeed - floor(uTime * uSpeed / 360.0) * 360.0; | |
float angle = scanAngle - fragAngle; | |
if(angle < 0.0){ | |
angle = angle + 360.0; | |
} | |
return angle; | |
} | |
void main() { | |
` | |
const fragementColor = ` | |
if(length(vPosition) == 0.0 || length(vPosition) > uRadius-2.0){ | |
gl_FragColor = vec4( outgoingLight, diffuseColor.a ); | |
} else { | |
float angle = calcAngle(vPosition); | |
if(angle < uFollowWidth){ | |
float opacity = 1.0 - angle / uFollowWidth; | |
gl_FragColor = vec4( outgoingLight, diffuseColor.a * opacity ); | |
} else { | |
gl_FragColor = vec4( outgoingLight, 0.0 ); | |
} | |
} | |
` | |
shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragment) | |
shader.fragmentShader = shader.fragmentShader.replace('gl_FragColor = vec4( outgoingLight, diffuseColor.a );', fragementColor) | |
} |
最后,通过定义事件来实现雷达扫描的动画效果
const dt = clock.getDelta(); | |
time.value += dt; | |
startTime.value += dt; | |
if (startTime.value >= startLength.value) { | |
startTime.value = startLength.value; | |
} |
好了,今天就先写到这里,有问题评论区留言,喜欢的小伙伴点赞关注+收藏哦!