1.天空盒
一个包含 3D 世界的正方体,一共六面,像一盒子一样把我们的场景包裹在内
创建一个数组,用来存储天空盒中正方体每一面的贴图材质。然后使用纹理加载器 Texture Loader
加载图像。最后,我们将创建一个 boxGeometry
几何体,并将其与我们之前创建的数组一起用于创建一个立方体。需要将材质的 side
属性设置为 THREE.backside
渲染天空盒的关键
scene.add(skybox);
animate();
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
2.OrbitControls 轨道控制摄像机
之前我们的相机是无法自己控制的,单使用这个之后则可以,
const controls = new OrbitControls(
(object: Camera),
(domElement: HTMLDOMElement)
);
实例:
const controls = new THREE.OrbitControls(camera); // 实例化一个 OrbitControls 对象
controls.addEventListener("change", renderer);
controls.minDistance = 100; // 设置控制的最小距离
controls.maxDistance = 3000; // 设置控制的最大距离
同时要记得引入orbit_controls.js文件,这个控件并不包括在three.js内
3.TextureLoader 纹理加载器
实例:
let materialArray = [];
const texture_ft = new THREE.TextureLoader().load("./images/afterrain_ft.jpg");
const texture_bk = new THREE.TextureLoader().load("./images/afterrain_bk.jpg");
const texture_up = new THREE.TextureLoader().load("./images/afterrain_up.jpg");
const texture_dn = new THREE.TextureLoader().load("./images/afterrain_dn.jpg");
const texture_rt = new THREE.TextureLoader().load("./images/afterrain_rt.jpg");
const texture_lf = new THREE.TextureLoader().load("./images/afterrain_lf.jpg");
materialArray.push(new THREE.MeshBasicMaterial({ map: texture_ft }));
materialArray.push(new THREE.MeshBasicMaterial({ map: texture_bk }));
materialArray.push(new THREE.MeshBasicMaterial({ map: texture_up }));
materialArray.push(new THREE.MeshBasicMaterial({ map: texture_dn }));
materialArray.push(new THREE.MeshBasicMaterial({ map: texture_rt }));
materialArray.push(new THREE.MeshBasicMaterial({ map: texture_lf }));
for (let i = 0; i < 6; i++) materialArray[i].side = THREE.BackSide;
let skyboxGeo = new THREE.BoxGeometry(10000, 10000, 10000);
let skybox = new THREE.Mesh(skyboxGeo, materialArray);
scene.add(skybox);
4.geoJson
geoJson
是与地理信息数据相关的 JSON 数据格式,它是基于 Javascript 对象表示法的地理空间信息数据交换格式,在three.js中,可以通过如下加载:
let loader = new THREE.FileLoader();
loader.load("getJSON.json", function (data) {
// todos
});
实例:
// 加载地图geoJson数据
loadMapGeoJson() {
let that = this;
let loader = new THREE.FileLoader();
loader.load('*****', function (data) {
let jsonData = JSON.parse(data);
that.initMap(jsonData);
})
}
5.墨卡托投影
假设地球被放置在空的圆柱里,其基准纬线(即:赤道线)和圆柱相切。然后,我们再想象地球中心有一盏射灯,把地球表面的图形(即:一个个国家、地区)投影到圆柱面上,当沿着地球中心旋转投影 360 度之后,圆柱的表面就会被投影上一块块连续的图形。接着,把圆柱面展开,展开后就能看到一幅以基准纬线为参照绘制出的地图。”等角“ 的用途就是按等角的方式将经纬网投影到圆柱面上,将圆柱面展开后,就在地图上形成了经纬线。
实例:地图就是一个 Object
,所以生成地图之前还是需要定义一个 Object3D
对象。接着,由于需要对地理空间信息数据的经纬度进行转换,我们还需要进行墨卡托投影,这里我们使用 d3 的 d3-geo
库实现。其次,每个部分都是一个 3D 模型对象,我们最终生成的地图就是由这些部分的 3D 图形组合而成。
// 初始化地图
initMap(geoJson) {
let that = this;
that.map = new THREE.Object3D();
// 墨卡托投影变换
const projection = d3.geoMercator().center([140.0, 37.5]).scale(80).translate([0, 0]);
geoJson.features.forEach(item => {
const province = new THREE.Object3D();
// 每个对象的坐标数组
const coordinates = item.geometry.coordinates;
// 循环坐标数组
coordinates.forEach(multiPolygon => {
multiPolygon.forEach(polygon => {
const shape = new THREE.Shape();
// 定义线的材质
const lineMaterial = new THREE.LineBasicMaterial({color: '#28b4e1'});
// 定义线几何体
const lineGeometry = new THREE.Geometry();
for (let i = 0; i < polygon.length; i++) {
// 将经纬度转为坐标值
const [x, y] = projection(polygon[i]);
if (i === 0) {
shape.moveTo(x, -y);
}
shape.lineTo(x, -y); // 使用这些坐标值合成路径THREE.Shape
lineGeometry.vertices.push(new THREE.Vector3(x, -y, 4.01));
}
const extrudeSettings = {
depth: 4,
bevelEnabled: false,
}
// 定义一个 ExtrudeGeometry 挤压几何体,生成一个有深度的地图形状
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
// 分别定义两种材质
const material1 = new THREE.MeshBasicMaterial({color: '#206199', transparent: true, opacity: 0.6});
const material2 = new THREE.MeshBasicMaterial({color: '#28b4e1', transparent: true, opacity: 0.5});
// 定义网格
const mesh = new THREE.Mesh(geometry, [material1, material2]);
// 定义地图的边界线条
const line = new THREE.Line(lineGeometry, lineMaterial);
province.add(mesh);
province.add(line);
})
})
province.properties = item.properties;
if (item.properties.contorid) {
const [x, y] = projection(item.properties.contorid);
province.properties._centroid = [x, y];
}
that.map.add(province);
})
that.scene.add(that.map);
}
鼠标悬浮的效果还需要学一学
首先,需要监听鼠标的 MouseMove
事件,其次,需要计算鼠标的 x 与 y 值,用于控制显示部分的名称信息框。那要如何让鼠标移动到部分的区块上面时能够拿到名称数据?THREE 提供的 Raycaster
(光射线投射),可以获取鼠标经过哪个物体上。使用 mousemove 监听鼠标的位置,然后在动画函数中计算经过哪些物体。
实例:
setMouseMove() {
let that = this;
that.raycaster = new THREE.Raycaster(); // 光射线投射
that.mouse = new THREE.Vector2();
that.eventOffset = {};
function onMouseMove(event) {
that.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
that.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
that.eventOffset.x = event.clientX;
that.eventOffset.y = event.clientY;
infoBox.style.left = that.eventOffset.x + 2 + 'px'; // 动态设置提示框的位置
infoBox.style.top = that.eventOffset.y + 2 + 'px'; // 动态设置提示框的位置
}
window.addEventListener('mousemove', onMouseMove, false);
}
animate() {
requestAnimationFrame(this.animate.bind(this));
this.raycaster.setFromCamera(this.mouse, this.camera); // 用新的原点和方向来更新射线
// 计算与拾取射线相交的对象
let intersects = this.raycaster.intersectObjects(this.scene.children, true); // 判断指定对象有没有被这束光线击中,返回被击中对象的信息,相交的结果会以一个数组的形式返回,其中的元素依照距离排序,越近的排在越前
if (this.activeInstersect && this.activeInstersect.length > 0) { // 将上一次选中的恢复颜色
this.activeInstersect.forEach(item => {
item.object.material[0].color.set('#206199');
item.object.material[1].color.set('#28b4e1');
});
}
this.activeInstersect = []; // 设置为空
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object.material && intersects[i].object.material.length === 2) {
this.activeInstersect.push(intersects[i]);
intersects[i].object.material[0].color.set(0xffe500);
intersects[i].object.material[1].color.set(0xffe500);
break; // 只取第一个
}
}
this.createProvinceInfo();
this.renderer.render(this.scene, this.camera);
}
createProvinceInfo() {
if (this.activeInstersect.length !== 0 && this.activeInstersect[0].object.parent.properties.name) {
let properties = this.activeInstersect[0].object.parent.properties;
// 设置信息框的名称内容
this.infoBox.textContent = properties.name;
// 显示信息框
this.infoBox.style.visibility = 'visible';
} else {
// 隐藏信息框
this.infoBox.style.visibility = 'hidden';
}
}
6.纹理
实例:
const earthMaterial = new THREE.MeshPhongMaterial({
map: new THREE.ImageUtils.loadTexture(
"*******"
),
color: 0xaaaaaa,
specular: 0x333333,
shininess: 25,
});
让物体自身产生旋转
const render = function () {
earth.rotation.y -= 0.0005;
renderer.render(scene, camera);
requestAnimationFrame(render);
};
render();
天空盒的另一种实现方式:用一个半径比现在大得多的实体覆盖现在的实体
//星际
const starGeometry = new THREE.SphereGeometry(1000, 50, 50);
const starMaterial = new THREE.MeshPhongMaterial({
map: new THREE.ImageUtils.loadTexture(
"*************"
),
side: THREE.DoubleSide,
shininess: 0,
});
const starField = new THREE.Mesh(starGeometry, starMaterial);
scene.add(starField);
动画其实就是让物体动起来,让摄像机旋转起来,这点在我们上面的代码中也有显现