# 一、环境贴图

​ 贴图有两种分别为全景贴图天空盒贴图。

全景贴图免费获取:

HDRI: Indoor • Poly Haven

HDRi Haven • Free CC0 HDRi Maps for Everyone (hdri-haven.com)

全景转天空盒:

HDRI to CubeMap (matheowis.github.io)

exr 转其他图片类型:

Convertio — 文件转换器

  1. 天空盒

​ 天空盒需要有六张贴图,分别对应上下左右前后。

1
2
3
4
5
6
7
8
9
10
11
//天空盒
const loader = new THREE.CubeTextureLoader()
const texture = loader.load([
'./texture/px.png',
'./texture/nx.png',
'./texture/py.png',
'./texture/ny.png',
'./texture/pz.png',
'./texture/nz.png'
])
scene.background = texture //最后给场景加上贴图
  1. 全景贴图
1
2
3
4
5
6
7
8
9
10
11
12
13
//引入loader
const loader = new EXRLoader()
const pmremGenerator = new THREE.PMREMGenerator(renderer)
pmremGenerator.compileEquirectangularShader()
loader.load('./brown_photostudio_01_2k.exr', function (texture) {
// 通过PMREMGenerator处理texture生成环境贴图
const envMap = pmremGenerator.fromEquirectangular(texture).texture
scene.enviroment = envMap
scene.background = envMap
renderer.render(scene, camera)
//释放pmremGenerator资源
pmremGenerator.dispose()
})

​ 另外设置相机的可视角度时要小于 90,否则旋转相机时会发现周围的场景在扭曲。同时视点的位置 z 坐标如果设置为 0 的话就旋转不了了。

# 二、给物体标签

​ 给物体标签一般使用的是 sprite 精灵模型。该模型的好处在于永远面向摄像机。如果给一个物体设置标签,然后绕着这个物体旋转时,标签也会跟着摄像机一起旋转。不过这个是给场景增加贴图,给贴图增加标签,所以不会出现绕着标签旋转的情况,理论上不使用 sprite 也行。另外在设置摄像机的位置时,需将 z 值设置小一些比如 0.01 不然在摄像机旋转的时候标签也在大幅度的偏移场景。

1
2
3
4
5
6
7
8
9
10
//标签
const tipTexture = new THREE.TextureLoader().load('./R.png')
const spriteMaterial = new THREE.SpriteMaterial({
map:tipTexture,
color:'red'
})
const sprite = new THREE.Sprite(spriteMaterial)
sprite.scale.set(0.1,0.1,0.1)
sprite.position.set(2,1,1)
scene.add(sprite)

​ canvas 也可以作为纹理,如果要加入文字的话可以使用 canvas 写文字然后使用 canvasTexture 提前 canvas 作为纹理绘制在 sprite 中。

​ 批量给物体标签

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const spriteList=[
{
position:{
x: 0.18,
y: 0.02,
z: -0.98,
},
name:'镜子'
},
{
position:{
x: 0.65,
y: -0.13,
z: -0.74
},
name:'沙发'
},
{
position:{
x: 0.91,
y: 0.30,
z: 0.28
},
name:'窗户'
},
{
position:{
x: 0.31,
y: -0.46,
z: 0.82
},
name:'椅子'
}
]
const spriteListPromise = spriteList.map((item)=>{
const {position,name} = item
return new Promise((res,rej)=>{
res(createSprite(position,name))
})
})
function createSprite(position,name){
const {x,y,z} = position
const tipTexture = new THREE.TextureLoader().load('./R.png')
const spriteMaterial = new THREE.SpriteMaterial({
map:tipTexture,
color:'red'
})
const sprite = new THREE.Sprite(spriteMaterial)
sprite.scale.set(0.05,0.05,0.05)
sprite.position.set(x,y,z)
sprite.userData ={
name:name
}
scene.add(sprite)
}

# 三、拾取射线

​ 创建 raycaster,获取鼠标的位置将其归一化。通过 raycaster.setFromCamera (mouse,camera) 改变射线位置。使用 raycaster.intersectObjects (scene.children) 获取射线与场景内有接触的物体。判断接触的物体是否是 sprite,判断鼠标是否点到了 sprite。在点击 sprite 弹出描述框。

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
35
36
37
38
39
40
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
function popBox(){
//闭包,让物体的介绍框只有一个
let div
return function(e){
//将鼠标坐标转化为threejs上平面的归一化坐标
mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1
mouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1
//射线的发射点为鼠标的位置,方向为摄像机的方向
raycaster.setFromCamera( mouse, camera )
//获取摄像与创建中的物体的焦点
const intersects = raycaster.intersectObjects(scene.children)
for(let i=0;i<intersects.length;i++){
//当射线与sprite接触弹出框介绍物体如果没接触有框则去掉
if(intersects[i].object instanceof THREE.Sprite&&!div){
const name = intersects[i].object.userData.name
div = document.createElement('div')
div.innerHTML = `${name}`
div.style.position = 'absolute'
div.style.left = e.clientX + 20 + 'px'
div.style.top = e.clientY + 'px'
document.body.appendChild(div)
}else{
if(div){
document.body.removeChild(div)
div = null
}
}
}
//如果没有接触点时如果有框则取消
if(intersects.length==0){
if(div){
document.body.removeChild(div)
div = null
}
}
}
}
window.addEventListener('mousedown',popBox())

参考资料:Raycaster – three.js docs (threejs.org)

# 四、界面 ui 以及切换场景

​ 可以直接使用 html 设计界面 ui,为了不影响 canvas 的位置,将 position 为 absolute。直接使用 html 设计界面,ui 就不会顺着相机的改变而改变了,而是固定在用户的屏幕上。同时将 canvas 的 z-index 设置成 - 1,让设置的 html 得以显示。

​ 场景切换:

​ 在界面 ui 上加上按钮

1
2
3
4
<div id="objectList">
<button class="button">房间</button>
<button class="button">照相馆</button>
</div>

​ 给按钮点击事件,当用户点击后,如果点击的房间与现在的房间不符就切换。将当前的房间的 sprite 模型去除,加载纹理贴图,给背景加上纹理贴图。

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
35
36
37
38
39
40
41
42
buttonList.addEventListener('click',(e)=>{
if(e.target.innerHTML===room) return
room = e.target.innerHTML
changeScene(room) //改变场景
})
//加载纹理
function loading (room){
if(room==='照相馆'){
texturePhoto = photoLoader.load('./brown_photostudio_01_2k.exr')
}else{
textureRoom = roomLoader.load([
'./texture/px.png',
'./texture/nx.png',
'./texture/py.png',
'./texture/ny.png',
'./texture/pz.png',
'./texture/nz.png'
])
}
}
//给背景切换纹理
function changeSceneTexture(room){
if(room==='照相馆'){
const envMap = pmremGenerator.fromEquirectangular(texturePhoto).texture
scene.enviroment = envMap
scene.background = envMap
renderer.render(scene, camera)
//释放pmremGenerator资源
pmremGenerator.dispose()
}else{
scene.background = textureRoom //最后给场景加上贴图
}
}
//给loader加上进程管理
const loadingeManager = new THREE.LoadingManager()
const photoLoader = new EXRLoader(loadingeManager)
const roomLoader = new THREE.CubeTextureLoader(loadingeManager)
//当纹理加载完毕后就给场景加上纹理和创建sprite模型
loadingeManager.onLoad=async()=>{
changeSceneTexture(room)
createSpriteList(room)
}

# 五、载入动画

​ 在部署上线后,发现初次进入画面很慢,大概是加载 5mb 的纹理慢了,然后将纹理的分辨率改成 1k,1mb 还是慢。于是就想要不转成天空盒,并行加载六张图片会快不少。然后将 exr 转成 jpg,再转成六张图片。但是初次加载要一些时间,为了提高这段时间的感受便想着加入一段载入动画。因为是加载场景有六张图片可以在这里 loadingeManager 的 onProgress 事件监听加载贴图。设计一个进度条。简单的讲就是设计一个矩形,border-radius 设计的大一些就会是一个进度条。随着加载进度条的宽度也会增大并加上背景色就有进度条的前进的感觉了。在此基础上加入一张沙漏转动的 gif。

html,css 部分:

1
2
3
4
5
6
7
<div id="loading"></div>

#loading{
margin-left: 50px;
height: 20px;
border-radius: 10px;
}
1
2
3
4
5
6
loadingeManager.onProgress=(item, loaded, total)=>{
const loading = document.getElementById('loading')
const percent = loaded / total
loading.style.width = 540*percent+'px'
loading.style.backgroundColor = '#aacdf0'
}

​ 在加载完成后就将其隐藏并将进度条的长度设置成 0,在切换场景的时候再显示,同时进度条从 0 开始前进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
loadingeManager.onLoad=()=>{
const div = document.getElementById('fullScene')
const loading = document.getElementById('loading')
loading.style.width = 0+'px'
div.style.display = 'none'
changeSceneTexture(room)
createSpriteList(room)
}

buttonList.addEventListener('click',(e)=>{
if(e.target.innerHTML===room) return
room = e.target.innerHTML
changeScene(room)
//加入加载动画
const div = document.getElementById('fullScene')
div.style.display = 'block'
})

在线预览:watchhouse.pages.dev