threejs实现星球

444 阅读3分钟

- 效果图

描述: 星球数据围绕着球心自转,空间内有多个星球,切换星球展示对应星球的数据并自转

image.png

- 代码实现

1.初始化场景

const init = () => {
  dom.value = document.getElementById("planetBox"); //获取dom
  width.value = dom.value.offsetWidth;
  height.value = dom.value.offsetHeight;
  // 场景
  scene = new THREE.Scene();
  // 创建透视相机(视角、长宽比、近面、远面)
  camera = new THREE.PerspectiveCamera(
    45,
    width.value / height.value,
    20,
    10000
  );
  camera.position.set(0, 600, 800); // 设置相机位置
  camera.lookAt(0, 0, 0);
  // 创建渲染器
  state.renderer = new THREE.WebGLRenderer({
    antialias: true, //抗锯齿
    alpha: true, //透明
  });
  state.renderer.setSize(width.value, height.value); // 设置渲染区域尺寸
  if (dom.value.children && dom.value.children.length) {
    dom.value.removeChild(dom.value.children[0]);
  }
  dom.value.appendChild(state.renderer.domElement); // 将渲染器添加到dom中形成canvas
  // 坐标系
  // const axesHelper = new THREE.AxesHelper(1000);
  // scene.add(axesHelper);
  createOrbitControls(); //创建鼠标控制器
  createLight(); //创建光源
  initSpheres(); // 初始化数据
  render(); //渲染
}

2.创建鼠标控制器

const createOrbitControls = () => {
  orbitControls = new OrbitControls(camera, state.renderer.domElement);
  orbitControls.enablePan = true; //右键平移拖拽
  orbitControls.enableZoom = true; //鼠标缩放
  orbitControls.enableDamping = true; //滑动阻尼
  orbitControls.dampingFactor = 0.05; //(默认.25)
  orbitControls.minDistance = 100; //相机距离目标最小距离
  orbitControls.maxDistance = 2700; //相机距离目标最大距离
  orbitControls.autoRotate = true; //自转(相机)
  orbitControls.autoRotateSpeed = 0; //自转速度
}

3.创建光源

const createLight = () => {
  let ambient = new THREE.AmbientLight(new THREE.Color(0xffffff)); //环境光
  scene.add(ambient);
  let pointLight = new THREE.PointLight(new THREE.Color(0xffff00), 2, 1, 0); //点光源
  pointLight.visible = true;
  pointLight.position.set(400, 200, 300); //点光源在原点充当太阳
  scene.add(pointLight); //点光源添加到场景中
}

4.初始化数据(涉及到的创建球心、创建球心上的文字及创建球体数据等方法在4.1、4.2、4.3)

const initSpheres = () => {
  resultList.value.forEach((item, index) => {
    let star = createStar(item);
    let container = getDataSphereRotation(star, item.data);
    container.userData.id = item.id; // 标识
    if (index === 0) {
      // 默展示第一个球
      let group =
        container &&
        container.children &&
        container.children.length &&
        container.children[1]; // 取出group
      showData(star, group, false);
    }
    // 当前球添加数据
    scene.add(container);
  });
}

4.1 利用canvas创建文字

const makeTextCanvas = (width, height, data, font, isLevel) => {
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  var ctx = canvas.getContext("2d");
  ctx.beginPath();
  ctx.translate(width / 2, height / 2);
  ctx.fillStyle = "#ffffff"; //文本填充颜色
  ctx.font = font + "px Arial"; //字体样式设置
  ctx.textBaseline = "middle"; //文本与 fillText 定义的纵坐标
  ctx.textAlign = "center"; //文本居中(以 fillText 定义的横坐标)
  ctx.fillText(data.name, 0, 0);
  // 排名标识
  if (isLevel) {
    ctx.beginPath();
    ctx.arc(0, -40, 2 * (10 - data.level), 0, 360, false);
    ctx.fillStyle = levelColor.value[data.level - 1]
      ? levelColor.value[data.level - 1]
      : "rgba(144, 78, 84)";
    ctx.fill(); //画实心圆
    ctx.closePath();
  }
  return canvas;
}

4.2 创建球心并将文字放到球心上

const createStar = (data) => {
  const geometry = new THREE.SphereGeometry(50, 45, 45);
  const material = new THREE.MeshBasicMaterial({
    color: "yellow",
    opacity: 0.2,
    transparent: true,
  });
  const sphere = new THREE.Mesh(geometry, material);
  sphere.position.set(data.position[0], data.position[1], data.position[2]);
  sphere.userData.isRotate = false; // 禁止自转
  sphere.userData.dataList = data.data; // 数据
  // 文字放到球心
  const canvasText = makeTextCanvas(80, 24, data, 18, false);
  const texture = new THREE.CanvasTexture(canvasText);
  texture.generateMipmaps = false;
  texture.minFilter = THREE.LinearFilter;
  texture.magFilter = THREE.LinearFilter;
  const sprite = new THREE.Sprite(
    new THREE.SpriteMaterial({
      map: texture,
    })
  );
  sprite.scale.set(80, 40, 1);
  sprite.position.set(0, -2, 0);
  sphere.add(sprite);
  return sphere;
}

4.3 创建星球上的数据,并将数据和球心放到容器group中

const getDataSphereRotation = (sphere, data) => {
  let group = new THREE.Group();
  const len = data && data.length;
  data.forEach((item, i) => {
    const phi = Math.acos(-1 + (2 * i) / len);
    const theta = Math.sqrt(len * Math.PI) * phi;
    let text = makeTextCanvas(160, 120, item, 30, true);
    var texture = new THREE.CanvasTexture(text);
    texture.generateMipmaps = false;
    texture.minFilter = THREE.LinearFilter;
    texture.magFilter = THREE.LinearFilter;
    let pinMaterial = new THREE.SpriteMaterial({
      map: texture,
    });
    let sprite = new THREE.Sprite(pinMaterial);
    sprite.scale.set(120, 80, 2);
    sprite.position.setFromSphericalCoords(250, phi, theta);
    sprite.visible = false; // 初始化隐藏数据
    sprite.userData = item;
    group.add(sprite);
  });
  let container = new THREE.Object3D();
  container.add(sphere);
  container.add(group);
  return container;
}

5.渲染

const render = () => {
  //请求动画帧,屏幕每刷新一次调用一次,绑定屏幕刷新频率
  requestAnimationFrame(render);
  orbitControls.update(); //鼠标控件实时更新
  state.renderer.render(scene, camera);
  TWEEN.update();
  // 当前group自转
  if (state.curSphere && state.curSphere.userData.isRotate) {
    curGroup.rotation.y += 0.001;
    curGroup.rotation.x += 0.001;
  }
}

6.切换球体(点击事件在7)

const switchToCurrentSphere = (clickSphere) => {
  // 隐藏当前球体的数据 && 更改当前球体数据
  hiddenData();
  // 切换动画
  startRotationAndZoom(clickSphere);
  // 展示点击球体的数据
  showData(clickSphere, null, true);
}

6.1 隐藏当前球体的数据 && 更改当前球体数据

const hiddenData = () => {
  // 更改数据
  state.curSphere.userData.isRotate = false;
  // 隐藏数据
  const curGroupChildren = curGroup && curGroup.children;
  if (curGroupChildren && curGroupChildren.length) {
    curGroupChildren.forEach((i) => {
      i.visible = false;
    });
  }
}

6.2 缓动动画,将相机拉近点击的球体

const startRotationAndZoom = (targetSphere) => {
  // 点击球体的位置
  const targetPosition = targetSphere.position.clone();
  new TWEEN.Tween(scene.position)
    .to(targetPosition.negate(), 2000)
    .easing(TWEEN.Easing.Quadratic.Out)
    .start();
}

6.3 展示点击球体的数据

const showData = (clickSphere, group, isMove) => {
  clickSphere.userData.isRotate = true;
  state.curSphere = clickSphere;
  let clickSphereGroup = group
    ? group
    : clickSphere.parent &&
      clickSphere.parent.children &&
      clickSphere.parent.children.length &&
      clickSphere.parent.children[1];
  curGroup = clickSphereGroup;
  if (
    clickSphereGroup &&
    clickSphereGroup.type === "Group" &&
    clickSphereGroup.children &&
    clickSphereGroup.children.length
  ) {
    const clickSprites = clickSphereGroup.children;
    clickSprites.forEach((i) => {
      i.visible = true;
    });
    // 移动group
    if (isMove) {
      curGroup.position.set(
        clickSphere.position.x,
        clickSphere.position.y,
        clickSphere.position.z
      );
    }
  }
}

7. 球体上数据的点击事件(因为全局监听的点击事件,在里面区分了点击的对象是球体上的数据,还是点击的球心进行星球切换)

// 监听点击事件
dom.value.addEventListener("click", moduleEvent)

const moduleEvent = (e) => {
  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();
  mouse.x = (e.offsetX / width.value) * 2 - 1;
  mouse.y = -(e.offsetY / height.value) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  let intersectsMesh = raycaster.intersectObjects(scene.children, true); // 点击球体
  let intersectsSprite = raycaster.intersectObjects(curGroup.children, true); // 点击姓名
  // 优先判断点击的是不是人名 => 点击的是不是球体
  if (intersectsSprite && intersectsSprite.length) {
    state.curSprite =
      intersectsSprite[0] && intersectsSprite[0].object.userData;
  } else if (intersectsMesh && intersectsMesh.length) {
    // 点击球体
    intersectsMesh.forEach((item) => {
      const object = item.object;
      if (
        object.userData &&
        !object.userData.isRotate &&
        object.type === "Mesh"
      ) {
        switchToCurrentSphere(object);
      }
    });
  }
}

ending!!!