相关源码和模型的下载链接地址点击链接进行跳转
技术栈
- vite
- typescript
- threejs@0.152.2
- less
最近丫丫回国掀起一股熊猫热,本人也没闲着,在查资料时候无意间发现 熊猫世界 的3D首页,心一痒痒,就扒了一下数据,刚好有模型,那索性按照熊猫世界的首页复刻一下;
本文没什么高的技术含量,完全觉得这个页面比较好看,所以大概做了做,涉及到的api
有 光线投射Raycaster 、 CSS 2D渲染器(CSS2DRenderer)、 TextureLoader 等等;
扒下来的数据分几个部分 背景图、模型、贴图、icon、旗帜、等
素材
这是地图的分层贴图
这是一些图标的图片
场景基础要素
设置场景背景贴图
scene = new Scene();
const sceneTexture = await loadTexture('../src/assets/textures/bg.jpeg')
sceneTexture.wrapS = THREE.RepeatWrapping;
sceneTexture.wrapT = THREE.RepeatWrapping;
sceneTexture.repeat.set(1, 1);
scene.background = sceneTexture
贴图加载公共方法
loadTexture
是提取的一个公共方法,后续在加载earth
模型的时候也需要用到,下面是提取的方法,返回一个Promise
// 加载纹理
export function loadTexture(grid: string) {
return new Promise<Texture>((resolve, reject) => {
textureLoader.load(grid, (texture) => {
resolve(texture)
});
})
}
至于贴图的一些属性,我在之前的文章也有提到过,这里不赘述,可以看一下文档
渲染器
WebGLRenderer
renderer = new WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.needsUpdate = true;
CSS2DRenderer
2drenderer的作用是渲染国家旗帜用的,最后渲染出来的是一个element,在控制器渲染时,通过改变element的transform和position保证元素与3d向量对齐
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = "absolute";
labelRenderer.domElement.style.top = "0";
labelRenderer.domElement.style.pointerEvents = "none";
灯光
// 灯光
ambientLight = new AmbientLight(0xffffff, 2.2);
scene.add(ambientLight)
用普通的环境光,能照亮场景就可以了,如果需要做一些其他氛围渲染,可以选用平行光或者点光源
控制器
controls = new OrbitControls(camera, renderer.domElement)
使用控制器可以改变相机的位置,可以从各个角度观察场景
复刻渲染
加载模型
earthGroup = new THREE.Group()
定义一个地球的组,将地球模型add
进去,使用封装好的loadFbx
方法将地图模型加载到场景中
earth = await loadFbx('../src/assets/models/diqiu.fbx', async (event: ProgressEvent) => {
const { total, loaded } = event
const count = Math.min(Math.ceil(loaded / total * 100), 100)
rendeDom(count)
if (count === 100 && !scene) {
loading.style.display = "none"
}
})
方法支持两个属性,第一个属性为模型地址,字符串格式,第二个为加载中的回调,(event: ProgressEvent) => void
类型
loadFbx
// 加载FBX模型
export function loadFbx(modelUrl: string, cb?: (event: ProgressEvent) => void): Promise<Object3D> {
return new Promise((resolve, reject) => {
fbxLoader.load(modelUrl, function (loadedModel) {
resolve(loadedModel)
}, (a) => {
cb&&cb(a)
});
})
}
加载中的回调主要的作用就是加载进度条
地球贴图
// 地图贴图
const diqiuTexture = await loadTexture('../src/assets/textures/diqiu.jpeg')
earth.children[0].material.map = diqiuTexture
效果
渲染2d element
我在网站上没扒到国家位置的信息,而且在地球加载的过程中,原始网站提供的位置肯定跟咱们复刻的位置不会相同,所以另辟蹊径,从模型上下手
我的想法就是对照原始网站的大概位置,找到复刻的位置坐标,然后再渲染出2d元素,将位置设置到相应位置,利用鼠标点击射线,点击地图后,获取地图和鼠标的交叉点,说干就干~
提取坐标
let mouse = new THREE.Vector2(); //鼠标位置
var raycaster = new THREE.Raycaster();
window.addEventListener("click", (event) => {
mouse.x = (event.clientX / document.body.offsetWidth) * 2 - 1;
mouse.y = -(event.clientY / document.body.offsetHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
var raylist = raycaster.intersectObjects(earthGroup.children);
console.log(raylist)
});
Raycaster
射线方法在 官网 上也有一些案例,可以翻阅文档查看具体api
console.log(JSON.stringify({
vector: new Vector3(raylist[0].point.x,
raylist[0].point.y,
raylist[0].point.z,),
country: '日本',
mapUrl: '../',
iconUrl: '../'
}))
打印出组合的数据,这样就可以获取到具体的坐标,再组合一下其他数据,比如地图上的国家旗帜地址,列表的旗帜地址,或者直接存到localstore
缓存中,我这里用的是最笨的方法,一个一个复制到文件里
添加元素
// 渲染2d文字
function initThreeFloorName(d: any) {
var image = document.createElement("img");
image.className = "country-image";
image.setAttribute('src', d.mapUrl)
if(d.style) {
for(let key in d.style ) {
const s = d.style[key]
image.style[key] = s
}
}
var earthLabel = new CSS2DObject(image);
earthLabel.position.copy(d.vector);
group2D.add(earthLabel);
}
group2D
是存图标的组,每个元素都存在一个单独的组,后续好操作,
隐藏遮挡元素
从图中可以看到,地图正面背面的元素都展示在一起了,那是因为2d元素和3d元素不在同一个图层里,
这导致地图并不能遮挡到本应在地图后面的2d元素
那就需要在渲染的时候进行一些操作,来隐藏被遮挡的元素,还是利用射线;
从摄像机position
发送一条射线,到2d元素的position
,射线如果和地图及地图里的元素产生交叉,则隐藏当前2d元素,说干就干~
// 物体之间的射线
function rayMesh() {
group2D.traverse((text: any) => {
if (!text.isGroup) {
const opt = pointRay(camera.position, text.position, sphereGroup);
text.element.style.opacity = Number(!opt).toString();
}
});
}
pointRay 点到点射线交互
function pointRay(star, end, children) {
let nstar = star.clone(); // 克隆一个新的位置信息,这样不会影响传入的三维向量的值
let nend = end.clone().sub(nstar).normalize(); // 克隆一个新的位置信息,这样不会影响传入的三维向量的值
const raycaster = new THREE.Raycaster(nstar, nend); // 创建一个正向射线
const intersects = raycaster.intersectObjects(
children.children,
true
);
let jclang = 0
let textlang = 0
if (intersects.length != 0) {
// 计算起点到交互点的曼哈顿距离
jclang = star.distanceTo(intersects[0].point)
// 起点到结束点的曼哈顿距离
textlang = star.distanceTo(end)
}
return jclang < textlang;
}
一目了然,如果射线交叉点比终点的距离短,那视为隐藏 返回true 标记为有遮挡,
透明度降为0 text.element.style.opacity = Number(!opt).toString();
检测是否遮挡
// 渲染函数
function render() {
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
if (isUpdate) {
isUpdate = false
rayMesh()
let timeout: any = setTimeout(() => {
isUpdate = true
clearInterval(timeout)
timeout = null
}, 100)
}
}
在render
中调用rayMesh
方法,我这里写的是一个简易的防抖,并不代表本人观点,哈哈哈(努力撇清关系),那么遮挡效果就做好了,还有图标的点击跳转功能,留给大家发挥,可以用射线检测,也可以用link跳转,随意