视角立方体

点击查看功能演示

功能概览:

  1. 单独通过threejs绘制的视角立方体,一共具有22个面。
  2. 视角立方体可以和三维空间里的任意一个模型进行绑定,也可以不和任意模型进行绑定。
  3. 视角立方体会和三维空间的相机进行同步,不管是转动视角立方体或者转动三维空间都会引起另一个旋转位置的变化。
  4. 点击视角立方体的任意一个面,视角立方体都会平滑的旋转过渡到另一个位置,使其被点的面正向朝向屏幕,三维空间也会进行平滑的旋转过渡。
  5. 提供与此功能相对应的一些api,比如销毁视角立方体,比如移动视角立方体在屏幕的二维空间的位置。

核心逻辑

立方体绘制

在3d建模软件maya绘制22面的立方体,并且导出obj文件,这个文件包含了点、面等信息

js 复制代码
const cubePoint = {
  vertices: [
    -0.33333, -0.5, 0.33333, -0.33333, -0.33333, 0.5, -0.5, -0.33333, 0.33333, 0.5, -0.33333, 0.33333, 0.33333,
    -0.33333, 0.5, 0.33333, -0.5, 0.33333, -0.5, 0.33333, 0.33333, -0.33333, 0.33333, 0.5, -0.33333, 0.5, 0.33333,
    0.33333, 0.5, 0.33333, 0.33333, 0.33333, 0.5, 0.5, 0.33333, 0.33333, -0.5, 0.33333, -0.33333, -0.33333, 0.5,
    -0.33333, -0.33333, 0.33333, -0.5, 0.33333, 0.33333, -0.5, 0.33333, 0.5, -0.33333, 0.5, 0.33333, -0.33333, -0.5,
    -0.33333, -0.33333, -0.33333, -0.33333, -0.5, -0.33333, -0.5, -0.33333, 0.33333, -0.5, -0.33333, 0.33333, -0.33333,
    -0.5, 0.5, -0.33333, -0.33333,
  ],
  faces: [
    [1, 3, 19, 21],
    [2, 1, 6, 5],
    [3, 2, 8, 7],
    [4, 6, 22, 24],
    [5, 4, 12, 11],
    [7, 9, 14, 13],
    [9, 8, 11, 10],
    [10, 12, 18, 17],
    [13, 15, 20, 19],
    [15, 14, 17, 16],
    [16, 18, 24, 23],
    [21, 20, 23, 22],
    [2, 5, 11, 8],
    [9, 10, 17, 14],
    [15, 16, 23, 20],
    [21, 22, 6, 1],
    [4, 24, 18, 12],
    [19, 3, 7, 13],
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
    [10, 11, 12],
    [13, 14, 15],
    [16, 17, 18],
    [19, 20, 21],
    [22, 23, 24],
  ],
};

通过threejs根据上述信息绘制立方体

js 复制代码
const { faces, vertices } = cubePoint;
faces.forEach((f, index) => {
  if (f.length > 3) {
    const vs: Array<THREE.Vector3> = [];
    f.forEach((v) => {
      const i = (v - 1) * 3;
      const vertex = new THREE.Vector3(vertices[i], vertices[i + 1], vertices[i + 2]);
      vs.push(vertex);
    });
    const meshGeometry = new ConvexGeometry(vs);
    const meshMaterial = new THREE.MeshLambertMaterial({
      color: 0xffffff,
      transparent: true,
      opacity: 0.7,
      side: THREE.FrontSide,
    });
    const mesh = new THREE.Mesh(meshGeometry, meshMaterial);
    group0.add(mesh);
    mesh.name = index + 'group0';
  } else {
    const geometry = new THREE.BufferGeometry();
    const vs: Array<number> = [];
    f.forEach((v) => {
      const i = (v - 1) * 3;
      vs.push(vertices[i], vertices[i + 1], vertices[i + 2]);
    });
    geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vs), 3));
    const material = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.7 });
    const mesh = new THREE.Mesh(geometry, material);
    group0.add(mesh);
    mesh.name = index + 'group0';
  }
});

给立方体的六个大面绘制贴图

js 复制代码
for (let index = 0; index < 6; index++) {
  const geometry = new THREE.PlaneGeometry(0.67, 0.67);
  const material = new THREE.MeshBasicMaterial({
    map: getTexture(index),
    side: THREE.FrontSide,
    opacity: 0.5,
    transparent: true,
  });
  const mesh = new THREE.Mesh(geometry, material);
  const plane = textConfig[index].plane;
  mesh.translateOnAxis(plane.normal, plane.constant);
  const quaternion = new THREE.Quaternion();
  quaternion.setFromUnitVectors(new Vector3(0, 0, 1), plane.normal);
  mesh.applyQuaternion(quaternion);
  mesh.name = index + 'group1';
  group1.add(mesh);
}
js 复制代码
const textConfig: { [key: string]: { text: string; view: string; plane: Plane } } = {
  0: { text: '前', view: 'homeView', plane: new Plane(new Vector3(0, 0, 1), len) },
  1: { text: '后', view: 'backView', plane: new Plane(new Vector3(0, 0, -1), len) },
  2: { text: '左', view: 'leftView', plane: new Plane(new Vector3(-1, 0, 0), len) },
  3: { text: '右', view: 'rightView', plane: new Plane(new Vector3(1, 0, 0), len) },
  4: { text: '下', view: 'bottomView', plane: new Plane(new Vector3(0, -1, 0), len) },
  5: { text: '上', view: 'topView', plane: new Plane(new Vector3(0, 1, 0), len) },
};

const getTexture = (index: number) => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d')!;
  const width = 40;
  const height = 40;
  canvas.width = width;
  canvas.height = height;
  const scale = 2;
  ctx.scale(scale, scale);
  ctx.fillStyle = 'transparent';
  ctx.fillRect(0, 0, 40 / scale, 40 / scale);
  ctx.fillStyle = '#0F0F0F';
  ctx.textAlign = 'center';
  ctx.translate(0, (40 - 40 / scale) / (2 * scale) - scale);
  ctx.fillText(textConfig[index].text, 20 / 2, 20 / 2);
  const url = canvas.toDataURL('image/png');
  const texture = new THREE.TextureLoader().load(url);
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
  return texture;
};

三轴箭头绘制

js 复制代码
const createAxesGroup = () => {
const axesGroup = new Group();
const axesY = new Group();
const axesX = new Group();
const axesZ = new Group();
// 坐标轴:圆柱体和圆锥组成一条轴
const cylinder = new CylinderGeometry(0.015, 0.015, 0.7, 16);
const arrow = new CylinderGeometry(0, 0.035, 0.1);
const sphere = new SphereGeometry(0.03);
const d = 0.4;
// 材质:RGB对应XYZ
const red = new MeshStandardMaterial({ color: 0xff0000, transparent: true, opacity: 0.6 });
const green = new MeshStandardMaterial({ color: 0x00ff00, transparent: true, opacity: 0.6 });
const blue = new MeshStandardMaterial({ color: 0x0000ff, transparent: true, opacity: 0.6 });
const gold = new MeshStandardMaterial({ color: 'gold', transparent: true, opacity: 0.6 });

const origin = new Mesh(sphere, gold);
origin.position.set(-d, -d, -d);

const y_line = new Mesh(cylinder, green);
const y_arrow = new Mesh(arrow, green);
y_arrow.position.y += 0.7;
y_line.position.y += 0.35;
axesY.position.set(-d, -d, -d);
axesY.add(y_arrow, y_line);

const x_line = new Mesh(cylinder, red);
const x_arrow = new Mesh(arrow, red);
x_arrow.position.y += 0.7;
x_line.position.y += 0.35;
axesX.add(x_line, x_arrow);
axesX.position.set(-d, -d, -d);
axesX.rotation.z = -Math.PI / 2;

const z_line = new Mesh(cylinder, blue);
const z_arrow = new Mesh(arrow, blue);
z_arrow.position.y += 0.7;
z_line.position.y += 0.35;

axesZ.add(z_line, z_arrow);
axesZ.position.set(-d, -d, -d);
axesZ.rotation.x = Math.PI / 2;

axesGroup.add(axesY, axesX, axesZ, origin);

return {
  axesGroup,
};
};

事件处理

点击立方体任意一个面的时候触发的事件

js 复制代码
self.current.pointerClick = (e: MouseEvent) => {
  if (self.current.mouseIsMove) return;
  const mouse = new THREE.Vector2();
  mouse.x = ((e.clientX - self.current.lefts) / width) * 2 - 1;
  mouse.y = -((e.clientY - self.current.tops) / height) * 2 + 1;
  const raycaster = new THREE.Raycaster();
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children);
  if (intersects && intersects[0]) {
    const name = intersects[0]?.object?.name;
    let viewType = '';
    if (name.includes('group1')) {
      viewType = textConfig[name.split('group1')[0]].view;
    } else if (name.includes('group0')) {
      viewType = 'ISO' + name.split('group0')[0];
    }
    if (viewType && viewTypes[viewType]) {
      const { heading, pitch } = viewTypes[viewType];
      const x = self.current.rotateX;
      const y = self.current.rotateY;
      const startQuaternion = new THREE.Quaternion().setFromEuler(
        new THREE.Euler(cesiumMath.toRadians(-x), cesiumMath.toRadians(y), 0, 'XYZ')
      );
      const endQuaternion = new THREE.Quaternion().setFromEuler(
        new THREE.Euler(cesiumMath.toRadians(-pitch), cesiumMath.toRadians(heading), 0, 'XYZ')
      );

      const startCameraQuaternion = Quaternion.fromHeadingPitchRoll(
        new HeadingPitchRoll(viewer.camera.heading, viewer.camera.pitch, viewer.camera.roll)
      );
      const endHeadingPitchRoll = new HeadingPitchRoll(
        cesiumMath.toRadians(heading),
        cesiumMath.toRadians(pitch == -87 ? -88.5 : pitch),
        0
      );
      const endCameraQuaternion = Quaternion.fromHeadingPitchRoll(endHeadingPitchRoll);
      self.current.temporaryCameraCenterPosition = self.current.isCameraCenterPosition
        ? self.current.cameraCenterPosition
        : getCameraCenterPosition();
      moveAnimation({
        moveFn: ({ currentValues }) => {
          const quaternion = new THREE.Quaternion().slerpQuaternions(
            startQuaternion,
            endQuaternion,
            currentValues[0]
          );
          const headingPitchRoll = HeadingPitchRoll.fromQuaternion(
            Quaternion.slerp(startCameraQuaternion, endCameraQuaternion, currentValues[0], new Quaternion())
          );
          self.current.group0!.setRotationFromQuaternion(quaternion);
          self.current.group1!.setRotationFromQuaternion(quaternion);
          viewer.camera.lookAt(
            self.current.temporaryCameraCenterPosition!,
            new HeadingPitchRange(headingPitchRoll.heading, headingPitchRoll.pitch, self.current.scalar)
          );
        },
        animationTime: 0.5,
        moveKey: 'cubeAnimation',
        easingType: 'easeInOutQuad',
        changeValues: [1],
        callback: () => {
          self.current.rotateY = heading;
          self.current.rotateX = pitch;
          viewer.camera.lookAt(
            self.current.temporaryCameraCenterPosition!,
            new HeadingPitchRange(endHeadingPitchRoll.heading, endHeadingPitchRoll.pitch, self.current.scalar)
          );
          self.current.group0!.setRotationFromQuaternion(endQuaternion);
          self.current.group1!.setRotationFromQuaternion(endQuaternion);
          viewer.camera.lookAtTransform(Matrix4.IDENTITY);
        },
      });
    }
  }
};

点击后能自然平滑的旋转到指定角度的核心方法是调用了Quaternion.slerp进行了四元数插值算法,计算了开始的旋转姿态startQuaternionstartCameraQuaternion,结束的旋转姿态endQuaternionendCameraQuaternion,再通过自己封装的moveAnimation动画函数,一边设置立方体的姿态一边设置相机的姿态。

硬编码的每一个面的欧拉角的姿态表示

js 复制代码
const viewTypes: { [key: string]: { heading: number; pitch: number } } = {
  // 主视图
  homeView: {
    heading: 0,
    pitch: 0,
  },
  // 俯视图
  topView: {
    heading: 0,
    pitch: -87,
  },
  // 左视图
  leftView: {
    heading: 90,
    pitch: 0,
  },
  // 右视图
  rightView: {
    heading: 270,
    pitch: 0,
  },
  // 仰视图
  bottomView: {
    heading: 0,
    pitch: 87,
  },
  // 后视图
  backView: {
    heading: 180,
    pitch: 0,
  },

  // 上侧三角iso视图
  ISO21: {
    heading: 315,
    pitch: -45,
  },
  ISO20: {
    heading: 45,
    pitch: -45,
  },
  ISO22: {
    heading: 135,
    pitch: -45,
  },
  ISO23: {
    heading: 225,
    pitch: -45,
  },

  // 下侧三角iso视图
  ISO19: {
    heading: 315,
    pitch: 45,
  },
  ISO18: {
    heading: 45,
    pitch: 45,
  },
  ISO24: {
    heading: 135,
    pitch: 45,
  },
  ISO25: {
    heading: 225,
    pitch: 45,
  },

  // 上侧横条iso视图
  ISO6: {
    heading: 0,
    pitch: -45,
  },
  ISO5: {
    heading: 90,
    pitch: -45,
  },
  ISO9: {
    heading: 180,
    pitch: -45,
  },
  ISO7: {
    heading: 270,
    pitch: -45,
  },

  // 中侧横条iso视图
  ISO4: {
    heading: 315,
    pitch: 0,
  },
  ISO2: {
    heading: 45,
    pitch: 0,
  },
  ISO8: {
    heading: 135,
    pitch: 0,
  },
  ISO10: {
    heading: 225,
    pitch: 0,
  },

  // 下侧横条iso视图
  ISO1: {
    heading: 0,
    pitch: 45,
  },
  ISO0: {
    heading: 90,
    pitch: 45,
  },
  ISO11: {
    heading: 180,
    pitch: 45,
  },
  ISO3: {
    heading: 270,
    pitch: 45,
  },
};

移动立方体触发的事件

js 复制代码
self.current.pointermove = (e: MouseEvent) => {
  const mouse = new THREE.Vector2();
  mouse.x = ((e.clientX - self.current.lefts) / width) * 2 - 1;
  mouse.y = -((e.clientY - self.current.tops) / height) * 2 + 1;
  const raycaster = new THREE.Raycaster();
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children);
  if (intersects && intersects[0]) {
    const name = intersects[0]?.object?.name;
    let meshNames = '';
    if (name.includes('group1')) {
      meshNames = group1ToGroup0[name.split('group1')[0]] + '';
    } else if (name.includes('group0')) {
      meshNames = name.split('group0')[0];
    }
    if (meshNames) {
      if (!changeMesh[meshNames]) {
        const mesh = group0.getObjectByName(meshNames + 'group0') as THREE.Mesh;
        if (mesh) {
          (mesh.material as THREE.MeshLambertMaterial).color = new THREE.Color(0x4d79ff);
          changeMesh[meshNames] = true;
        }
      }
      forEach(changeMesh, (value, meshName) => {
        if (value && meshName != meshNames) {
          const mesh = group0.getObjectByName(meshName + 'group0') as THREE.Mesh;
          if (mesh) {
            (mesh.material as THREE.MeshLambertMaterial).color = new THREE.Color(0xffffff);
            changeMesh[meshName] = false;
          }
        }
      });
    }
  } else {
    forEach(changeMesh, (value, meshName) => {
      if (value) {
        const mesh = group0.getObjectByName(meshName + 'group0') as THREE.Mesh;
        if (mesh) {
          (mesh.material as THREE.MeshLambertMaterial).color = new THREE.Color(0xffffff);
          changeMesh[meshName] = false;
        }
      }
    });
  }
};
相关推荐
刘一说6 小时前
腾讯位置服务JavaScript API GL地图组件库深度解析:Vue生态中的地理空间可视化利器
javascript·vue.js·信息可视化·webgl·webgis
烛阴16 小时前
从“无”到“有”:手动实现一个 3D 渲染循环全过程
前端·webgl·three.js
WebGISer_白茶乌龙桃1 天前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
烛阴2 天前
拒绝配置地狱!5 分钟搭建 Three.js + Parcel 完美开发环境
前端·webgl·three.js
WebGISer_白茶乌龙桃2 天前
Vue3 + Mapbox 加载 SHP 转换的矢量瓦片 (Vector Tiles)
javascript·vue.js·arcgis·webgl
ThreePointsHeat6 天前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
林枫依依8 天前
电脑配置流程(WebGL项目)
webgl
冥界摄政王9 天前
CesiumJS学习第四章 替换指定3D建筑模型
3d·vue·html·webgl·js·cesium
温宇飞11 天前
高效的线性采样高斯模糊
javascript·webgl
冥界摄政王12 天前
Cesium学习第一章 安装下载 基于vue3引入Cesium项目开发
vue·vue3·html5·webgl·cesium