更新于 

Map 地图

mini map 场景小地图的添加

制作场景小地图,意味着除了渲染scene和主视角控制的camera外,
还会用到一个小地图专用的摄像机

这里的conrtols控制器使用 MapControls 会更好,

MapControls 控制摄像机的方式偏向 鸟瞰视角

1
2
3
4
5
6
7
8
9
10
11
import {MapControls} from "three/examples/jsm/controls/MapControls";
// ...
const controls = new MapControls(camera, renderer.domElement);
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.enableZoom = false

// 注意开启阻尼之后,需要再animate函数中update控制器
function animate(){
controls.update()
}

创建一个新的dom容器,用于渲染小地图:

1
2
3
4
5
6
7
8
9
10
11
  <style>
#mini-map{
position:absolute;
width:250px;
height:250px;
right:1rem;
bottom:1rem;
box-shadow:0px 0px 5px #000;
}
</style>
<div id="mini-map"></div>

为小地图准备专门的 渲染器摄像机

1
2
3
4
5
const miniMapDom = document.getElementById('mini-map')
const miniMapRenderer = new THREE.WebGLRenderer({antialias:true});
miniMapRenderer.setSize(miniMapDom.clientWidth, miniMapDom.clientHeight)
miniMapDom.appendChild(miniMapRenderer.domElement)
miniMapRenderer.setClearColor(0xffffff)
1
2
3
4
5
6
7
8
const miniMapCamera = new THREE.PerspectiveCamera(
45,
miniMapDom.clientWidth / miniMapDom.clientHeight,
0.1,
1000
)
miniMapCamera.position.set(0, 50, 0)
miniMapCamera.lookAt(0,0,0)
1
2
3
4
function miniMapAnimation(){
miniMapRenderer.render(scene, miniMapCamera)
}
miniMapRenderer.setAnimationLoop(miniMapAnimation)
视角联动

通过raycaster获取鼠标在小地图上的点击位置P,
将大地图的相机位置移动到P处,
注意相机位置移动后,还需要修改controls.target才能修改相机的朝向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mousePosition = new THREE.Vector2()
const raycaster = new THREE.Raycaster()
miniMapDom.onmousedown = e =>{
let rect = miniMapDom.getBoundingClientRect()
let x = e.offsetX
let y = e.offsetY
mousePosition.x = (x/miniMapDom.clientWidth*2) - 1
mousePosition.y = 1 - (y/miniMapDom.clientHeight * 2)
raycaster.setFromCamera(mousePosition, miniMapCamera)
let intersections = raycaster.intersectObject(model)
if(intersections[0]){
const point = intersections[0].point
camera.position.set(point.x, 8, point.z)
controls.target.set(point.x, 0, point.z)
}
}

texture + layer 控制实现小地图

小地图一般不需要过于精细的渲染,
因此为这个功能专门渲染一个模型,会消耗很多性能,
因此可以直接用一张texture纹理图片代替模型:

1
2
3
4
5
6
7
8
9
10
const textureLoader = new THREE.TextureLoader()
const mapTexture = textureLoader.load('assets/painting/MiniMap.png')
mapTexture.colorSpace = THREE.SRGBColorSpace
const planeGeo = new THREE.PlaneGeometry(41,41)
const planeMat = new THREE.MeshBasicMaterial({
map:mapTexture
})
const planeMesh = new THREE.Mesh(planeGeo, planeMat)
planeMesh.rotation.set(-Math.PI/2, 0, 0 )
scene.add(planeMesh)

通过图层控制的方式,将小地图用到的texture和实际的model分开到两个图层,
小地图渲染器只对texture图层进行渲染,
但是仍能保留raycaster交互事件:

1
2
3
4
5
miniMapCamera.layers.disableAll()
miniMapCamera.layers.enable(1)

planeMesh.layers.disable(0)
planeMesh.layers.enable(1)

在小地图中标注场景物体位置

在场景中动态添加建筑物,通过raycaster能够获得物体坐标,
在小地图的同坐标位置创建图标(贴图平面),
需要注意的是,小地图图标需要和小地图在同一个图层:

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
// 加载tower icon
const towerTexture= textureLoader.load('assets/painting/tower.png')
towerTexture.colorSpace = THREE.SRGBColorSpace
const towerMat = new THREE.MeshBasicMaterial({
map:towerTexture,
transparent:true
})

const towerGeo = new THREE.PlaneGeometry(3.5,3.5)
window.onkeydown = e =>{
if(e.key == 't' && tower){
raycaster.setFromCamera(mousePosition, camera)
const intersections = raycaster.intersectObject(model)
if(intersections[0]){
const point = intersections[0].point
let group = new THREE.Group()
scene.add(group)
// ... 添加塔部分省略
// 小地图图标
const towerMesh = new THREE.Mesh(towerGeo, towerMat)
group.add(towerMesh)
towerMesh.position.y = 0.1
towerMesh.rotation.set(-Math.PI/2, 0, 0)
towerMesh.layers.disable(0) // 修改图标所在图层
towerMesh.layers.enable(1)
}
}
}
DLC1:塔楼添加shader雷达效果

用shader和一张灰度转场图实现,
需要注意的是,叠加在同一y轴的物体会出现深度冲突的问题,
因此需要给y轴一定的偏移:

Math.random() * 0.01

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
<script type="x-vertex" id="v-shader">
varying vec2 vUv;
void main(){
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>

<script type="x-fragment" id="f-shader">
varying vec2 vUv;
uniform sampler2D u_transition;
uniform float u_time;

mat2 rotate(float angle){
return mat2(
cos(angle), -sin(angle),
sin(angle), cos(angle)
);
}

void main(){
vec2 vUv = vUv;
vUv -= 0.5;
vUv *= rotate(u_time);
vUv +=0.5;
vec4 texture = texture2D(u_transition, vUv);
gl_FragColor = vec4(
0.4, texture.g/1.3, texture.b/1.3, texture.r
);
}
</script>
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
// 渐变材质
const uniforms = {
u_transition:{
type:'t',
value:textureLoader.load('assets/transition/transition6.png')
},
u_time:{type:'f', value: 0.0}
}
const radarMat = new THREE.ShaderMaterial({
uniforms,
transparent:true,
vertexShader:document.getElementById('v-shader').textContent,
fragmentShader:document.getElementById('f-shader').textContent,
})
const radarGeo = new THREE.CircleGeometry(5,20)
window.onkeydown = e =>{
if(e.key == 't' && tower){
raycaster.setFromCamera(mousePosition, camera)
const intersections = raycaster.intersectObject(model)
if(intersections[0]){
const point = intersections[0].point
let group = new THREE.Group()
scene.add(group)
// 略....
// 底部扫描效果
const radarMesh = new THREE.Mesh(radarGeo, radarMat)
radarMesh.rotation.set(-Math.PI/2 , 0,0)
group.add(radarMesh)
radarMesh.position.y = Math.random() * 0.01
}
}
}
DLC2:CSS2DRender

引入CSS2DRender实现通过input:range控制扫描圈的大小

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
const labelRenderer = new CSS2DRenderer()
labelRenderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(labelRenderer.domElement)
labelRenderer.domElement.style.position = 'absolute'
labelRenderer.domElement.style.top = '0px'
labelRenderer.domElement.style.pointerEvents = 'none'
//...
window.onkeydown = e =>{
if(e.key == 't' && tower){
raycaster.setFromCamera(mousePosition, camera)
const intersections = raycaster.intersectObject(model)
if(intersections[0]){
const point = intersections[0].point
let group = new THREE.Group()
scene.add(group)
// ...
// 缩放滑动条
let slider = document.createElement('input')
slider.type = 'range'
slider.max = 3
slider.min = 1
slider.value = 2
slider.step = 0.1
slider.style.pointerEvents = 'all'
const sliderLabel = new CSS2DObject(slider)
sliderLabel.position.set(0,2,0)
// group.add(sliderLabel)
slider.addEventListener('input',e=>{
radarMesh.scale.set(slider.value, slider.value, slider.value)
})

}
}
}

小地图显示动态物体

小地图上物体图标的变化和实际物体的变化原理完全一致,

下面代码实现一台低空飞行的直升飞机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 加载直升飞机
let helicopte
let mixer
loader.load('assets/helicopter.glb',gltf=>{
helicopte = gltf.scene
helicopte.scale.set(0.2,0.2,0.2)
helicopte.position.set(0,1,0)
scene.add(helicopte)
mixer = new THREE.AnimationMixer(helicopte)
const action = mixer.clipAction(
THREE.AnimationClip.findByName(gltf.animations, 'Rotation')
)
action.play()
})

这辆直升飞机有固定的航迹,使用CatmullRomCurve3创建:

1
2
3
4
5
6
7
8
9
10
11
12
const points = [
new THREE.Vector3(-5,1,-5),
new THREE.Vector3(-5,3,3),
new THREE.Vector3(5,5,5),
new THREE.Vector3(10,3,-1),
new THREE.Vector3(3,2,-8),
]
const path = new THREE.CatmullRomCurve3(points,true)
const pathGeo = new THREE.BufferGeometry().setFromPoints(path.getPoints(50))
const pathMat = new THREE.LineBasicMaterial({color:0xffaa00})
const pathMesh = new THREE.Line(pathGeo, pathMat)
scene.add(pathMesh)

此外,还需要在小地图中创建一个直升飞机对应的icon:

1
2
3
4
5
6
7
8
9
10
11
12
13
const helicopterIconMat = new THREE.ShaderMaterial({
uniforms,
transparent:true,
vertexShader:document.getElementById('v-shader').textContent,
fragmentShader:document.getElementById('f-shader-2').textContent,
})
const helicopterIconGeo = new THREE.CircleGeometry(5,20)
const helicopterIconMesh = new THREE.Mesh(helicopterIconGeo, helicopterIconMat)
helicopterIconMesh.position.y = 1
helicopterIconMesh.rotation.x = -Math.PI/2
helicopterIconMesh.layers.disable(0)
helicopterIconMesh.layers.enable(1)
scene.add(helicopterIconMesh)

icon使用shaderMaterial自定义波纹效果:

1
2
3
4
5
6
7
8
9
10
11
<script type="x-fragment" id="f-shader-2">
varying vec2 vUv;
uniform float u_time;
void main(){
vec2 vUv = vUv;
float distance = distance(vec2(0.5), vUv);
// fract 取小数部分 10.0控制波纹密度 40.0控制波纹透明度
float color = fract(distance * 10.0 - u_time) / distance / 40.0;
gl_FragColor = vec4(vec3(1.0, 0.0, 0.0), color);
}
</script>

在动画帧中改变直升飞机和飞机图标的position和切向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const clock = new THREE.Clock()
function animate(time) {
if(mixer){
mixer.update(clock.getDelta())
const t = (time/2000 % 10) /10
const position = path.getPointAt(t)
const tangent = path.getTangentAt(t)
helicopte.position.copy(position)
helicopte.lookAt(
position.clone().add(tangent)
)
helicopterIconMesh.position.copy(position)
}
// ...
}