视角立方体

点击查看功能演示

功能概览:

  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;
        }
      }
    });
  }
};
相关推荐
不惑_3 天前
最佳ThreeJS实践 · 实现赛博朋克风格的三维图像气泡效果
javascript·node.js·webgl
小彭努力中4 天前
50. GLTF格式简介 (Web3D领域JPG)
前端·3d·webgl
小彭努力中5 天前
52. OrbitControls辅助设置相机参数
前端·3d·webgl
幻梦丶海炎5 天前
【Threejs进阶教程-着色器篇】8. Shadertoy如何使用到Threejs-基础版
webgl·threejs·着色器·glsl
小彭努力中6 天前
43. 创建纹理贴图
前端·3d·webgl·贴图
小彭努力中6 天前
45. 圆形平面设置纹理贴图
前端·3d·webgl·贴图
Ian10257 天前
webGL入门(五)绘制多边形
开发语言·前端·javascript·webgl
小彭努力中8 天前
49. 建模软件绘制3D场景(Blender)
前端·3d·blender·webgl
优雅永不过时·10 天前
使用three.js 实现着色器草地的效果
前端·javascript·智慧城市·webgl·three·着色器
baker_zhuang12 天前
Threejs创建胶囊体
webgl·threejs·web3d