视角立方体

点击查看功能演示

功能概览:

  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;
        }
      }
    });
  }
};
相关推荐
GISer_Jing6 小时前
WebGL跨端兼容实战:移动端适配全攻略
前端·aigc·webgl
Aurora@Hui3 天前
WebGL & Three.js
webgl
CC码码5 天前
基于WebGPU实现canvas高级滤镜
前端·javascript·webgl·fabric
ct9785 天前
WebGL 图像处理核心API
图像处理·webgl
ct9787 天前
Cesium 矩阵系统详解
前端·线性代数·矩阵·gis·webgl
ct97810 天前
WebGL Shader性能优化
性能优化·webgl
棋鬼王10 天前
Cesium(一) 动态立体墙电子围栏,Wall墙体瀑布滚动高亮动效,基于Vue3
3d·信息可视化·智慧城市·webgl
Longyugxq13 天前
Untiy的Webgl端网页端视频播放,又不想直接mp4格式等格式的。
unity·音视频·webgl
花姐夫Jun13 天前
cesium基础学习-坐标系统相互转换及相应的场景
学习·webgl
ct97814 天前
WebGL开发
前端·gis·webgl