Skip to content

使用babylonjs画一个魔方

原创作者wenonly
发布时间
所属分类前端笔记
标签列表babylonjs, 3d

示例

基础模板

js
const canvas = document.querySelector("#canvas");
const engine = new Engine(canvas, true);
const scene = new Scene(engine);
// 添加灯光
// 添加一些其它逻辑
engine.runRenderLoop(() => {
  scene.render();
});
window.addEventListener("resize", () => {
  engine.resize();
});
  1. 添加摄像机

配置一个 ArcRotateCamera ,然后将视线固定到中心点,然后加上交互,就能围绕中心点拖动交互查看了。

ts
private createCamera() {
  const { centerPositions, cubeletSize } = this._options;
  const camera = new ArcRotateCamera(
    "camera1",
    Tools.ToRadians(-75),
    Tools.ToRadians(75),
    cubeletSize * 10,
    new Vector3(centerPositions[0], centerPositions[1], centerPositions[2]),
    this._scene
  );
  camera.attachControl();
  camera.setTarget(Vector3.Zero());
}
  1. 添加灯光

如果一个场景中没有灯光,则无法查看到效果。 在正方体的相对的两个角设置点光源,另外多设置一个直射光源加强光照。

ts
private createLight() {
  const { centerPositions, cubeletSize } = this._options;
  // 各个面都创建光源
  const lights = [
    [1, 1, 1],
    [-1, -1, -1],
  ];
  const lightDistance = cubeletSize * 1.5 + 2;
  lights.reverse().forEach((lt, index) => {
    const light = new PointLight(
      `light-${index}`,
      new Vector3(
        centerPositions[0] + lt[0] * lightDistance,
        centerPositions[1] + lt[1] * lightDistance,
        centerPositions[2] + lt[2] * lightDistance
      ),
      this._scene
    );
    light.intensity = 1;
    const dlight = new DirectionalLight(
      "d-light",
      new Vector3(lt[0], lt[1], lt[2]),
      this._scene
    );
    dlight.intensity = 0.5;
  });
}

创建模型

魔方其实就是一个包含 26 个正方体的集合,所以只需要按照一定方法创建 26 个正方体就行。 画了立方体后也需要确定每一个立方体的具体位置。 先确定一个中心,添加配置centerPositions: [0, 0, 0],表示魔方的中心就是 xyz 轴的中心。 让后使用-1,1,0排列表示每个魔方相对中心点的位置,这个位置需要乘以魔方每个小块的大小cubeletSize

  1. 计算绘制位置

魔方需要绘制 26 个小方块,这些方块的配置位置可以使用1,-1,0排列表示。

ts
function permute(arr: number[], stack: number[], result: number[][]) {
  if (stack.length === arr.length) {
    result.push(stack.slice());
    return;
  }
  for (let i = 0; i < arr.length; i++) {
    stack.push(arr[i]);
    permute(arr, stack, result);
    stack.pop();
  }
}
ts
const cubelets: number[][] = [];
// 全排列,构成26块位置
permute([-1, 1, 0], [], cubelets);
cubelets.pop(); // 不要0 0 0的项
查看计算结果
js
[[-1,-1,-1],[-1,-1,1],[-1,-1,0],[-1,1,-1],[-1,1,1],[-1,1,0],[-1,0,-1],[-1,0,1],[-1,0,0],[1,-1,-1],[1,-1,1],[1,-1,0],[1,1,-1],[1,1,1],[1,1,0],[1,0,-1],[1,0,1],[1,0,0],[0,-1,-1],[0,-1,1],[0,-1,0],[0,1,-1],[0,1,1],[0,1,0],[0,0,-1],[0,0,1]]
  1. 然后根据计算的相对位置数据,绘制魔方方块。

设置六个面的颜色。

ts
private getColors() {
  return [
    new Color4(1, 1, 1, 1), // 白色
    new Color4(1, 1, 0, 1), // 黄色
    new Color4(0, 0, 1, 1), // 蓝色
    new Color4(0, 1, 0, 1), // 绿色
    new Color4(1, 0.5, 0, 1), // 橙色
    new Color4(1, 0, 0, 1), // 红色
  ];
}

绘制方块,每个方块的各个面分别都设置有颜色。

ts
cubelets.forEach((pos) => {
  const cubeletBox = MeshBuilder.CreateBox(
    `cubelet-${this._id}-${pos[0]}-${pos[1]}-${pos[2]}`,
    {
      width: this._cubeletSize,
      height: this._cubeletSize,
      depth: this._cubeletSize,
      faceColors: colors,
    },
    this._scene
  );
  cubeletBox.scaling = new Vector3(0.98, 0.98, 0.98);
  cubeletBox.metadata = {
    originPos: pos.slice(), // 纪录排列位置
    currentPos: pos.slice(),
  };
  this.calcRealPosition(cubeletBox);
  this._cubelets.push(cubeletBox);
});

根据参数计算方块实际位置。

ts
private calcRealPosition(cubelet: Mesh) {
  const currentPos = cubelet.metadata.currentPos.slice();
  const x = currentPos[0] * this._cubeletSize + this._centerPosition.x;
  const y = currentPos[1] * this._cubeletSize + this._centerPosition.y;
  const z = currentPos[2] * this._cubeletSize + this._centerPosition.z;
  cubelet.position = new Vector3(x, y, z);
}

旋转动画

旋转的时候需要同时旋转一个面的所有方块,为了方便计算,创建一个空的节点,将需要旋转的方块全部绑定到这个空的节点上,然后旋转这个节点,就能实现旋转。

ts
public rotateCustomFace(
  faceCubelets: Mesh[],
  rotationQuaternion: Quaternion
) {
  // 定义一个空节点,用于旋转
  const axisNode = new TransformNode("axis", this._scene);
  faceCubelets.forEach((item) => {
    item.parent = axisNode;
  });
  const frameRate = 60;
  // 定义绕世界Y轴旋转的动画
  const rotationAnimation = new Animation(
    "rotationAnimation",
    "rotationQuaternion",
    frameRate,
    Animation.ANIMATIONTYPE_QUATERNION,
    Animation.ANIMATIONLOOPMODE_CONSTANT
  );
  const keys = [
    { frame: 0, value: Quaternion.Identity() },
    { frame: frameRate, value: rotationQuaternion },
  ];
  rotationAnimation.setKeys(keys);
  axisNode.animations = [rotationAnimation];

  return new Promise((resolve, reject) => {
    if (!this._scene) {
      reject(new Error("cannot find scene!"));
      return;
    }
    try {
      // 开始动画
      this._scene.beginAnimation(axisNode, 0, frameRate, false, 1, () => {
        faceCubelets.forEach((item) => {
          // 解绑和重新计算模块位置
          this.calcCurrentPos(item);
          this.calcRealPosition(item);
          item.parent = null;
          item.rotationQuaternion = rotationQuaternion.multiply(
            item.rotationQuaternion ?? Quaternion.Identity()
          );
        });
        axisNode.dispose();
        resolve(true);
      });
    } catch (error) {
      reject(error);
    }
  });
}

旋转后因为会将空的节点删除掉,所以旋转后的节点为了保持位置还需重新计算。

ts
faceCubelets.forEach((item) => {
  // 解绑和重新计算模块位置
  this.calcCurrentPos(item);
  this.calcRealPosition(item);
  item.parent = null;
  item.rotationQuaternion = rotationQuaternion.multiply(
    item.rotationQuaternion ?? Quaternion.Identity()
  );
});
axisNode.dispose();

拖动交互

为了实现拖动旋转的功能,需要全局监听拖动事件,纪录下拖动方块的法线和旋转方向。 在鼠标点击的时候纪录点击的方块坐标和法线等信息,移动的时候纪录下在相同方块移动的最后位置,最后根据坐标和法线计算旋转角度,并旋转。

ts
private attachDrag() {
  let pickedMeshNormal: Vector3 | undefined;
  let pickedMesh: Mesh | undefined;
  let pickedStartPoint: Vector3 | undefined;
  let pickedEndPoint: Vector3 | undefined;
  let moving = false; // 动画正在运行
  this._scene?.onPointerObservable.add((pointerInfo) => {
    if (moving) return;
    if (pointerInfo.type === PointerEventTypes.POINTERUP) {
      this._scene?.activeCamera?.attachControl();
      if (
        !pickedMesh ||
        !pickedStartPoint ||
        !pickedEndPoint ||
        !pickedMeshNormal
      ) {
        pickedMeshNormal = undefined;
        pickedMesh = undefined;
        pickedStartPoint = undefined;
        pickedEndPoint = undefined;
        return;
      }
      const moveVecotor = pickedEndPoint.subtract(pickedStartPoint);
      const moveDirection = this.moveVectorToAxis(moveVecotor);
      // 需要旋转的cubelets
      const cubelets = this.getFaceCubeletsByNormalAndDirect(
        pickedMesh,
        pickedMeshNormal,
        moveDirection
      );
      const rotationQueration = this.getRotationQueration(
        pickedMeshNormal,
        moveDirection
      );
      moving = true;
      this.rotateCustomFace(cubelets, rotationQueration).finally(
        () => (moving = false)
      );
      pickedMeshNormal = undefined;
      pickedMesh = undefined;
      pickedStartPoint = undefined;
      pickedEndPoint = undefined;
    }
    if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
      if (pointerInfo.pickInfo) {
        const pickResult = this._scene?.pickWithRay(
          pointerInfo.pickInfo.ray!,
          (m) => {
            return m.metadata?.originPos;
          }
        );
        if (
          pickResult?.pickedPoint &&
          pickResult.pickedMesh === pickedMesh &&
          pickedMeshNormal
        ) {
          const normal = pickResult.getNormal(true);
          if (normal) {
            const realNormal = new Vector3(
              Math.round(normal.x),
              Math.round(normal.y),
              Math.round(normal.z)
            ).normalize();
            if (realNormal.equals(pickedMeshNormal)) {
              // 更新end信息
              pickedEndPoint = pickResult.pickedPoint;
            }
          }
        }
      }
    }
    if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
      if (
        !pointerInfo.pickInfo?.hit ||
        !pointerInfo.pickInfo?.pickedMesh?.metadata.originPos
      )
        return;
      this._scene?.activeCamera?.detachControl();
      // 使用场景的pick方法来获取被点击的对象
      const normal = pointerInfo.pickInfo.getNormal(true);
      // 说明不是魔方的方块
      if (!normal) return;
      if (!pointerInfo.pickInfo.pickedPoint) return;
      const realNormal = new Vector3(
        Math.round(normal.x),
        Math.round(normal.y),
        Math.round(normal.z)
      ).normalize();
      pickedMeshNormal = realNormal;
      pickedMesh = pointerInfo.pickInfo.pickedMesh as Mesh;
      pickedStartPoint = pointerInfo.pickInfo.pickedPoint;
    }
  });
}

根据法线和移动方向还有点击的方块信息,能计算出最终需要旋转的所有方块。 有法线和移动方向,能很方便的通过叉积计算旋转轴。 然后根据之间纪录的在节点 metadata 中的 currentPos 信息,就能过滤出所有需要旋转的方块。

ts
private getFaceCubeletsByNormalAndDirect(
  pointedMesh: Mesh,
  normal: Vector3,
  direct: Vector3
) {
  // 计算两个向量的叉积(得到旋转轴)
  const rotationAxis = normal.cross(direct).normalize();
  const axis = rotationAxis.x !== 0 ? "x" : rotationAxis.y !== 0 ? "y" : "z";
  return this._cubelets.filter((item) => {
    const currentPos: number[] = item.metadata.currentPos;
    const pointCurrentPos: number[] = pointedMesh.metadata.currentPos;
    if (axis === "x") {
      return currentPos[0] === pointCurrentPos[0];
    } else if (axis === "y") {
      return currentPos[1] === pointCurrentPos[1];
    } else if (axis === "z") {
      return currentPos[2] === pointCurrentPos[2];
    }
    return false;
  });
}

根据法线和鼠标移动方向也能很好的计算出旋转方向。

ts
// 根据法线和移动方向计算旋转量
private getRotationQueration(normal: Vector3, direct: Vector3) {
  // 计算两个向量的点积
  const dotProduct = normal.dot(direct);
  // 计算两个向量的叉积(得到旋转轴)
  const rotationAxis = normal.cross(direct);
  // 计算夹角(弧度制)
  let angle = Math.acos(dotProduct / (normal.length() * direct.length()));
  // 如果向量A和向量B方向相反,需要特殊处理
  if (dotProduct < 0) {
    rotationAxis.scaleInPlace(-1);
    angle = Math.PI - angle;
  }
  // 构造旋转四元数
  const rotationQuaternion = Quaternion.RotationAxis(rotationAxis, angle);
  return rotationQuaternion;
}

旋转角度使用Quaternion四元数是为了更精准的表示旋转方向,能够明确的表示最后的旋转状态。

在旋转魔方的时候也需要关闭摄像机的旋转交互,不然摄像机会跟着旋转。

ts
this._scene?.activeCamera?.detachControl();

旋转完成再次开启摄像机交互。

ts
this._scene?.activeCamera?.attachControl();

源码

ts
import { onMounted } from "vue";
import { renderCube } from "@/examples/html/3D/魔方/cubeBoxSpace.ts";

onMounted(() => {
  renderCube("#canvas1");
});
ts
import {
  ArcRotateCamera,
  AxesViewer,
  DirectionalLight,
  Engine,
  PointLight,
  Scene,
  Tools,
  Vector3,
} from "@babylonjs/core";
import { CubeBox, CubeFace } from "./cubeBox";

interface CubeBoxSpaceOptions {
  cubeletSize: number; // 每一小块的大小
  centerPositions: [number, number, number]; // xyz
}

const defaultOptions: CubeBoxSpaceOptions = {
  cubeletSize: 1,
  centerPositions: [0, 0, 0],
};

class CubeBoxSpace {
  private _canvas: HTMLCanvasElement | undefined;
  private _engine: Engine | undefined;
  private _scene: Scene | undefined;
  private _options: CubeBoxSpaceOptions;
  private _cube: CubeBox | undefined;
  constructor(options?: CubeBoxSpaceOptions) {
    this._options = {
      ...defaultOptions,
      ...options,
    };
  }
  // #region light
  private createLight() {
    const { centerPositions, cubeletSize } = this._options;
    // 各个面都创建光源
    const lights = [
      [1, 1, 1],
      [-1, -1, -1],
    ];
    const lightDistance = cubeletSize * 1.5 + 2;
    lights.reverse().forEach((lt, index) => {
      const light = new PointLight(
        `light-${index}`,
        new Vector3(
          centerPositions[0] + lt[0] * lightDistance,
          centerPositions[1] + lt[1] * lightDistance,
          centerPositions[2] + lt[2] * lightDistance
        ),
        this._scene
      );
      light.intensity = 1;
      const dlight = new DirectionalLight(
        "d-light",
        new Vector3(lt[0], lt[1], lt[2]),
        this._scene
      );
      dlight.intensity = 0.5;
    });
  }
  // #endregion light
  // #region camera
  private createCamera() {
    const { centerPositions, cubeletSize } = this._options;
    const camera = new ArcRotateCamera(
      "camera1",
      Tools.ToRadians(-75),
      Tools.ToRadians(75),
      cubeletSize * 10,
      new Vector3(centerPositions[0], centerPositions[1], centerPositions[2]),
      this._scene
    );
    camera.attachControl();
    camera.setTarget(Vector3.Zero());
  }
  // #endregion camera
  private createScene(engine: Engine) {
    const scene = new Scene(engine);
    this._scene = scene;
  }
  public mount(canvasDom: HTMLCanvasElement | string) {
    const { centerPositions, cubeletSize } = this._options;
    if (!canvasDom) throw new Error("canvas is required");
    if (typeof canvasDom === "string") {
      const canvas = document.querySelector(canvasDom);
      if (!canvas) throw new Error("canvas is not found");
      this._canvas = canvas as HTMLCanvasElement;
    } else {
      this._canvas = canvasDom;
    }
    this._engine = new Engine(this._canvas, true);
    this.createScene(this._engine);
    this.createCamera();
    this.createLight();
    this._cube = new CubeBox(
      cubeletSize,
      new Vector3(centerPositions[0], centerPositions[1], centerPositions[2]),
      this._scene
    );
    // 添加坐标轴
    // X 轴指向正方向(+X)通常是红色。
    // Y 轴指向正方向(+Y)通常是绿色。
    // Z 轴指向正方向(+Z)通常是蓝色。
    new AxesViewer(this._scene);
    const scene = this._scene!;
    this._engine.runRenderLoop(() => {
      scene.render();
    });
    window.addEventListener("resize", () => {
      this._engine?.resize();
    });
    setTimeout(async () => {
      // await this._cube?.rotate(CubeFace.Up);
      // await this._cube?.rotate(CubeFace.Down);
      // await this._cube?.rotate(CubeFace.Right);
      // await this._cube?.rotate(CubeFace.Left);
      // await this._cube?.rotate(CubeFace.Front);
      // await this._cube?.rotate(CubeFace.Back);
      // await this._cube?.rotate(CubeFace.Up, false);
      // await this._cube?.rotate(CubeFace.Down, false);
      // await this._cube?.rotate(CubeFace.Right, false);
      // await this._cube?.rotate(CubeFace.Left, false);
      // await this._cube?.rotate(CubeFace.Front, false);
      // await this._cube?.rotate(CubeFace.Back, false);
    }, 1000);
  }
}

export function renderCube(canvas: string) {
  const cubeBoxSpace = new CubeBoxSpace();
  cubeBoxSpace.mount(canvas);
}
ts
import {
  Animation,
  Axis,
  Color4,
  EngineStore,
  Mesh,
  MeshBuilder,
  Nullable,
  PointerEventTypes,
  Quaternion,
  Scene,
  Tools,
  TransformNode,
  Vector3,
} from "@babylonjs/core";

// 排列
// #region permute
function permute(arr: number[], stack: number[], result: number[][]) {
  if (stack.length === arr.length) {
    result.push(stack.slice());
    return;
  }
  for (let i = 0; i < arr.length; i++) {
    stack.push(arr[i]);
    permute(arr, stack, result);
    stack.pop();
  }
}
// #endregion permute

export enum CubeFace {
  Front = "F",
  Back = "B",
  Up = "U",
  Down = "D",
  Left = "L",
  Right = "R",
}

const faceGetters = {
  [CubeFace.Front]: (pos: number[]) => pos[2] === -1,
  [CubeFace.Back]: (pos: number[]) => pos[2] === 1,
  [CubeFace.Up]: (pos: number[]) => pos[1] === 1,
  [CubeFace.Down]: (pos: number[]) => pos[1] === -1,
  [CubeFace.Left]: (pos: number[]) => pos[0] === -1,
  [CubeFace.Right]: (pos: number[]) => pos[0] === 1,
};

let id = 0;

export class CubeBox {
  private _cubelets: Mesh[] = [];
  private _cubeletSize;
  private _centerPosition: Vector3;
  private _scene: Nullable<Scene>;
  private _id: number;
  constructor(cubeletSize: number, centerPosition: Vector3, scene?: Scene) {
    this._cubeletSize = cubeletSize;
    this._centerPosition = centerPosition;
    this._scene = scene ?? EngineStore.LastCreatedScene;
    this._id = id++;
    this.createCube();
    this.attachDrag();
  }
  // 计算移动的方向
  private moveVectorToAxis(moveVecotor: Vector3) {
    // 计算向量在各个坐标轴上的分量
    var xComponent = moveVecotor.x;
    var yComponent = moveVecotor.y;
    var zComponent = moveVecotor.z;

    // 找出绝对值最大的分量
    var absComponents = [
      Math.abs(xComponent),
      Math.abs(yComponent),
      Math.abs(zComponent),
    ];
    var maxIndex = absComponents.indexOf(Math.max(...absComponents));

    // 构建与坐标轴平行的新向量
    switch (maxIndex) {
      case 0:
        return new Vector3(Math.sign(xComponent), 0, 0);
      case 1:
        return new Vector3(0, Math.sign(yComponent), 0);
      case 2:
      default:
        return new Vector3(0, 0, Math.sign(zComponent));
    }
  }
  // #region getRotationQueration
  // 根据法线和移动方向计算旋转量
  private getRotationQueration(normal: Vector3, direct: Vector3) {
    // 计算两个向量的点积
    const dotProduct = normal.dot(direct);
    // 计算两个向量的叉积(得到旋转轴)
    const rotationAxis = normal.cross(direct);
    // 计算夹角(弧度制)
    let angle = Math.acos(dotProduct / (normal.length() * direct.length()));
    // 如果向量A和向量B方向相反,需要特殊处理
    if (dotProduct < 0) {
      rotationAxis.scaleInPlace(-1);
      angle = Math.PI - angle;
    }
    // 构造旋转四元数
    const rotationQuaternion = Quaternion.RotationAxis(rotationAxis, angle);
    return rotationQuaternion;
  }
  // #endregion getRotationQueration
  // 根据法线和旋转方向,获取一面的方块节点
  // #region getFaceCubeletsByNormalAndDirect
  private getFaceCubeletsByNormalAndDirect(
    pointedMesh: Mesh,
    normal: Vector3,
    direct: Vector3
  ) {
    // 计算两个向量的叉积(得到旋转轴)
    const rotationAxis = normal.cross(direct).normalize();
    const axis = rotationAxis.x !== 0 ? "x" : rotationAxis.y !== 0 ? "y" : "z";
    return this._cubelets.filter((item) => {
      const currentPos: number[] = item.metadata.currentPos;
      const pointCurrentPos: number[] = pointedMesh.metadata.currentPos;
      if (axis === "x") {
        return currentPos[0] === pointCurrentPos[0];
      } else if (axis === "y") {
        return currentPos[1] === pointCurrentPos[1];
      } else if (axis === "z") {
        return currentPos[2] === pointCurrentPos[2];
      }
      return false;
    });
  }
  // #endregion getFaceCubeletsByNormalAndDirect
  // 拖动旋转的时候调用
  // #region rotateCustomFace
  public rotateCustomFace(
    faceCubelets: Mesh[],
    rotationQuaternion: Quaternion
  ) {
    // 定义一个空节点,用于旋转
    const axisNode = new TransformNode("axis", this._scene);
    faceCubelets.forEach((item) => {
      item.parent = axisNode;
    });
    const frameRate = 60;
    // 定义绕世界Y轴旋转的动画
    const rotationAnimation = new Animation(
      "rotationAnimation",
      "rotationQuaternion",
      frameRate,
      Animation.ANIMATIONTYPE_QUATERNION,
      Animation.ANIMATIONLOOPMODE_CONSTANT
    );
    const keys = [
      { frame: 0, value: Quaternion.Identity() },
      { frame: frameRate, value: rotationQuaternion },
    ];
    rotationAnimation.setKeys(keys);
    axisNode.animations = [rotationAnimation];

    return new Promise((resolve, reject) => {
      if (!this._scene) {
        reject(new Error("cannot find scene!"));
        return;
      }
      try {
        // 开始动画
        this._scene.beginAnimation(axisNode, 0, frameRate, false, 1, () => {
          // #region recalc
          faceCubelets.forEach((item) => {
            // 解绑和重新计算模块位置
            this.calcCurrentPos(item);
            this.calcRealPosition(item);
            item.parent = null;
            item.rotationQuaternion = rotationQuaternion.multiply(
              item.rotationQuaternion ?? Quaternion.Identity()
            );
          });
          axisNode.dispose();
          // #endregion recalc
          resolve(true);
        });
      } catch (error) {
        reject(error);
      }
    });
  }
  // #endregion rotateCustomFace
  // 开启拖动魔方
  // #region attachDrag
  private attachDrag() {
    let pickedMeshNormal: Vector3 | undefined;
    let pickedMesh: Mesh | undefined;
    let pickedStartPoint: Vector3 | undefined;
    let pickedEndPoint: Vector3 | undefined;
    let moving = false; // 动画正在运行
    this._scene?.onPointerObservable.add((pointerInfo) => {
      if (moving) return;
      if (pointerInfo.type === PointerEventTypes.POINTERUP) {
        this._scene?.activeCamera?.attachControl();
        if (
          !pickedMesh ||
          !pickedStartPoint ||
          !pickedEndPoint ||
          !pickedMeshNormal
        ) {
          pickedMeshNormal = undefined;
          pickedMesh = undefined;
          pickedStartPoint = undefined;
          pickedEndPoint = undefined;
          return;
        }
        const moveVecotor = pickedEndPoint.subtract(pickedStartPoint);
        const moveDirection = this.moveVectorToAxis(moveVecotor);
        // 需要旋转的cubelets
        const cubelets = this.getFaceCubeletsByNormalAndDirect(
          pickedMesh,
          pickedMeshNormal,
          moveDirection
        );
        const rotationQueration = this.getRotationQueration(
          pickedMeshNormal,
          moveDirection
        );
        moving = true;
        this.rotateCustomFace(cubelets, rotationQueration).finally(
          () => (moving = false)
        );
        pickedMeshNormal = undefined;
        pickedMesh = undefined;
        pickedStartPoint = undefined;
        pickedEndPoint = undefined;
      }
      if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
        if (pointerInfo.pickInfo) {
          const pickResult = this._scene?.pickWithRay(
            pointerInfo.pickInfo.ray!,
            (m) => {
              return m.metadata?.originPos;
            }
          );
          if (
            pickResult?.pickedPoint &&
            pickResult.pickedMesh === pickedMesh &&
            pickedMeshNormal
          ) {
            const normal = pickResult.getNormal(true);
            if (normal) {
              const realNormal = new Vector3(
                Math.round(normal.x),
                Math.round(normal.y),
                Math.round(normal.z)
              ).normalize();
              if (realNormal.equals(pickedMeshNormal)) {
                // 更新end信息
                pickedEndPoint = pickResult.pickedPoint;
              }
            }
          }
        }
      }
      if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
        if (
          !pointerInfo.pickInfo?.hit ||
          !pointerInfo.pickInfo?.pickedMesh?.metadata.originPos
        )
          return;
        this._scene?.activeCamera?.detachControl();
        // 使用场景的pick方法来获取被点击的对象
        const normal = pointerInfo.pickInfo.getNormal(true);
        // 说明不是魔方的方块
        if (!normal) return;
        if (!pointerInfo.pickInfo.pickedPoint) return;
        const realNormal = new Vector3(
          Math.round(normal.x),
          Math.round(normal.y),
          Math.round(normal.z)
        ).normalize();
        pickedMeshNormal = realNormal;
        pickedMesh = pointerInfo.pickInfo.pickedMesh as Mesh;
        pickedStartPoint = pointerInfo.pickInfo.pickedPoint;
      }
    });
  }
  // #endregion attachDrag
  // 获取一个面的方块
  private getFaceCublets(faceDirection: CubeFace) {
    return this._cubelets.filter((item) =>
      faceGetters[faceDirection](item.metadata.currentPos)
    );
  }
  // 可以通过CubeFace类型字段控制旋转哪一个面
  public rotate(faceDirection: CubeFace, clockwise = true) {
    const faceCublets = this.getFaceCublets(faceDirection);
    const axis =
      faceDirection === CubeFace.Up || faceDirection === CubeFace.Down
        ? Axis.Y
        : faceDirection === CubeFace.Left || faceDirection === CubeFace.Right
        ? Axis.X
        : Axis.Z;

    const angle = !(
      Number(clockwise) ^
      Number(
        faceDirection === CubeFace.Up ||
          faceDirection === CubeFace.Right ||
          faceDirection === CubeFace.Back
      )
    )
      ? Tools.ToRadians(90)
      : Tools.ToRadians(-90);
    const rotationQuaternion = Quaternion.RotationAxis(axis, angle);
    return this.rotateCustomFace(faceCublets, rotationQuaternion);
  }
  // #region color
  private getColors() {
    return [
      new Color4(1, 1, 1, 1), // 白色
      new Color4(1, 1, 0, 1), // 黄色
      new Color4(0, 0, 1, 1), // 蓝色
      new Color4(0, 1, 0, 1), // 绿色
      new Color4(1, 0.5, 0, 1), // 橙色
      new Color4(1, 0, 0, 1), // 红色
    ];
  }
  // #endregion color
  private createCube() {
    // #region permute-use
    const cubelets: number[][] = [];
    // 全排列,构成26块位置
    permute([-1, 1, 0], [], cubelets);
    cubelets.pop(); // 不要0 0 0的项
    // #endregion permute-use
    this._cubelets = [];
    const colors = this.getColors();
    // #region createCubelets
    cubelets.forEach((pos) => {
      const cubeletBox = MeshBuilder.CreateBox(
        `cubelet-${this._id}-${pos[0]}-${pos[1]}-${pos[2]}`,
        {
          width: this._cubeletSize,
          height: this._cubeletSize,
          depth: this._cubeletSize,
          faceColors: colors,
        },
        this._scene
      );
      cubeletBox.scaling = new Vector3(0.98, 0.98, 0.98);
      cubeletBox.metadata = {
        originPos: pos.slice(), // 纪录排列位置
        currentPos: pos.slice(),
      };
      this.calcRealPosition(cubeletBox);
      this._cubelets.push(cubeletBox);
    });
    // #endregion createCubelets
  }
  // #region calcRealPosition
  private calcRealPosition(cubelet: Mesh) {
    const currentPos = cubelet.metadata.currentPos.slice();
    const x = currentPos[0] * this._cubeletSize + this._centerPosition.x;
    const y = currentPos[1] * this._cubeletSize + this._centerPosition.y;
    const z = currentPos[2] * this._cubeletSize + this._centerPosition.z;
    cubelet.position = new Vector3(x, y, z);
  }
  // #endregion calcRealPosition
  private calcCurrentPos(cubelet: Mesh) {
    const newPos = [
      Math.round(
        (cubelet.getAbsolutePosition().x - this._centerPosition.x) /
          this._cubeletSize
      ),
      Math.round(
        (cubelet.getAbsolutePosition().y - this._centerPosition.y) /
          this._cubeletSize
      ),
      Math.round(
        (cubelet.getAbsolutePosition().z - this._centerPosition.z) /
          this._cubeletSize
      ),
    ];
    cubelet.metadata.currentPos = newPos;
  }
}