ThreeJs 球面上加载多个柱子模型归一化(性能优化)

215 阅读4分钟

场景:

对于统计世界人口密度的时候,将每个国家的人口密度按照比例用柱子进行展示,世界国家数之多,如果每个国家一个柱子,那么在加载场景时,数据的渲染就会非常多,会创建非常之多的柱子模型,这样对于电脑的性能要求就会非常大,甚至有的电脑会加载不出来,直接出现器卡死白屏的现象。

解决方案

能否将所有国家的柱子模型归一为一个模型呢?当然是可以的!

面临的问题

因为是不同的国家,分布在地球的各个角落,那么每个国家对应的柱子都会朝向不同的角度,就会产生不同的柱子姿态问题。先看看以前单个柱子的是如何改变柱子姿态的。

// utils.js
const texture = new THREE.TextureLoader().load("./earth.jpg");
texture.encoding = THREE.sRGBEncoding;

// 创建一个地球
function createSphere(radius) {
  const geometry = new THREE.SphereGeometry(radius);
  const material = new THREE.MeshLambertMaterial({
    map: texture,
    transparent: true,
    opacity: 0.5,
  });
  const mesh = new THREE.Mesh(geometry, material);
  return mesh;
}

// 转换经纬度为球面坐标
function mathAngle(R, longitude, latitude) {
  // 将经纬度转为角度值
  let long = (longitude * Math.PI) / 180;
  let lat = (latitude * Math.PI) / 180;
  long = -long; // 这里因为默认地球的经度在z正半轴是-90度 所以要转换一个取反
  const y = R * Math.sin(lat); // 利用纬度计算出y坐标
  const x = R * Math.cos(lat) * Math.cos(long); //  R * Math.cos(lat)是在利用纬度算出在xoz平面留下的投影长度 再根据经度算出x z坐标
  const z = R * Math.cos(lat) * Math.sin(long);
  return {
    x,
    y,
    z,
  };
}

import * as THREE from "three";
import { createSphere, mathAngle } from "./utils.js";

const sumGroup = new THREE.Group();
const pointGroup = new THREE.Group();
sumGroup.add(pointGroup);
const R = 100;

const sphere = createSphere(R);
sumGroup.add(sphere);

// const pos = mathAngle(R, 110.35, 20.02);
// const loc = createCircleSphere(1, pos.x, pos.y, pos.z);
// pointGroup.add(loc);

const geometry = new THREE.BoxGeometry(10, 10, 30);
const material = new THREE.MeshLambertMaterial({
  color: 0x00ffff,
});

const angle = mathAngle(R, 0, 0);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(angle);
// 让物体不被遮挡在球体内
mesh.position.x += 5;
mesh.rotateY(Math.PI / 2);
mesh.translateZ(10);
const axeHelper = new THREE.AxesHelper(100);
mesh.add(axeHelper);
sumGroup.add(mesh);

export default sumGroup;

然后将该js文件导出组添加到场景中

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import sumGroup from "./modal.js";

const scene = new THREE.Scene();
scene.add(sumGroup);

const camera = new THREE.PerspectiveCamera(
  60,
  window.innerWidth / window.innerHeight,
  0.1,
  3000
);
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({
  antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild(renderer.domElement);

function animation() {
  renderer.render(scene, camera);
  requestAnimationFrame(animation);
}
animation();

window.onresize = () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
};

const controls = new OrbitControls(camera, renderer.domElement);

//导出方便在其他的Js文件中使用
export { scene, camera, renderer };

实现效果:

WechatIMG162.jpg

换一种方式去实现:

// modal.js
import * as THREE from "three";
import { createSphere, mathAngle } from "./utils.js";

const sumGroup = new THREE.Group();
const pointGroup = new THREE.Group();
sumGroup.add(pointGroup);
const R = 100;

const sphere = createSphere(R);
sumGroup.add(sphere);

// const pos = mathAngle(R, 110.35, 20.02);
// const loc = createCircleSphere(1, pos.x, pos.y, pos.z);
// pointGroup.add(loc);

const geometry = new THREE.BoxGeometry(10, 10, 30);
const material = new THREE.MeshLambertMaterial({
  color: 0x00ffff,
});

// 在进行姿态转换的时候还是建议使用geometry的方法去进行移动 不会改变世界坐标及坐标轴的位置
const angle = mathAngle(R, 0, 0);
const mesh = new THREE.Mesh(geometry, material);
// 相当于进行了旋转的操作
geometry.lookAt(new THREE.Vector3(angle.x, angle.y, angle.z));
// 归一化 朝着方向向量移动球面半径+自身高度一半的位移量
const translateV3 = new THREE.Vector3(angle.x, angle.y, angle.z)
  .normalize()
  .multiplyScalar(R + 15);
geometry.translate(translateV3.x, translateV3.y, translateV3.z);
const axeHelper = new THREE.AxesHelper(100);
mesh.add(axeHelper);
sumGroup.add(mesh);

export default sumGroup;

解决了柱子模型的姿态问题,那么接下来就要进行归一化操作,在场景加载渲染时只渲染一个柱子模型,将大大降低加载消耗的性能,加载效率提升。需要借助threeJs中辅助类中的mergeBufferGeometries方法(注意自己项目中使用的threeJs版本,有的BufferGeometryUtils.js文件中,导出的是一个类,我的这个版本导出的是类下的方法,如果导出的是类,则引用是先创建实例,在调用实例上的方法即可),看代码实现:

// modal.js
import * as THREE from "three";
import { createSphere, mathAngle } from "./utils.js";
import { mergeBufferGeometries } from "three/addons/utils/BufferGeometryUtils.js";

const sumGroup = new THREE.Group();
const R = 100;

const sphere = createSphere(R);
sumGroup.add(sphere);

const loader = new THREE.FileLoader();
loader.setResponseType("json");

// 人口密度文件,实际开发以后端接口返回的数据为准
loader.load("./人口密度.json", (data) => {
  // 做最大值的数据修正 避免差距过大出现插值是0的情况
  let max = data.population.map((it) => it[2]).sort((a, b) => b - a)[0] * 0.005;
  let geometryArr = [];
  data.population.map((poplat) => {
    const geometry = new THREE.BoxGeometry(0.5, 0.5, poplat[2] / max);
    const angle = mathAngle(R, poplat[0], poplat[1]);
    // 相当于进行了旋转的操作
    geometry.lookAt(new THREE.Vector3(angle.x, angle.y, angle.z));
    // 拿到单位向量乘以柱子的高度/2的长度避免柱子被球体遮挡住
    const v3 = new THREE.Vector3(angle.x, angle.y, angle.z)
      .normalize()
      .multiplyScalar(R + poplat[2] / max / 2);
    geometry.translate(v3.x, v3.y, v3.z);
    geometryArr.push(geometry);
  });
  const material = new THREE.MeshLambertMaterial({
    // color: 0x00ffff,
    vertexColors: true,
  });
  let mergeGeometry = null;
  if (geometryArr.length == 1) {
    mergeGeometry = geometryArr[0];
  } else {
    mergeGeometry = mergeBufferGeometries(geometryArr);
  }
  const mesh = new THREE.Mesh(mergeGeometry, material);
  sumGroup.add(mesh);
});

export default sumGroup;

结论

本文提供了一种新的方法是进行姿态的变化然后在进行归一化,当然原始的方法进行模型姿态的变化也是可以实现的。