添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

不过魔方类的技术文章已经有很多实现了,如:

  • Github · I-cannot-deal-with-Rubiks-cube : 使用 Three.js+Vue 编写的 3D 前端魔方游戏
  • Github · Rubiks-Cube : Three.js 实现魔方小游戏
  • 知乎 · THREEJS 的三阶魔方
  • 知乎 · ThreeJS 四步制作一个简易魔方 魔方旋转
  • three.js 制作魔方 :了解 tween 的补间动画
  • 简化

    设想

    1. 【合集 8.21 已更新 93 话】Blender 2.9-3.4 黑铁骑士 Ⅱ 系统零基础入门教程(持续更新+中文字幕+普通话+不敷衍+义务教育+案例+学习)
    2. three.js 辉光的两种实现方法,及解决添加辉光后背景变成黑色的问题
    3. 思路

      首先,【枘凿六合】是个二阶魔方,八个方块位于空间直角坐标系的八个卦限中,投射面板位置固定且坐标系不会转动,绘制应该不成问题。

      然后是玩家操作上,仅可选择某一个面的方块,然后“左右”转动选择的方块。

      当同一方向上的方块和投射面满足消消乐条件时,即可得分。

      实现

      基础场景

      见文档: 创建一个场景(Creating a scene)

      绘制方块

      见文档: 如何绘制透明的物体

      可按照卦限给方块编号 1~8,以配置文件的方式设置方块的颜色及附加信息,便于调整方块的颜色,构造不同关卡。
      可附着一个 Box3 对象来标记选中时线框高亮。

      // 方块分类
      const ECubeType = Object.freeze({
        solid: {
          color: new THREE.Color().setHSL(0 / 8, 1, 0.5),
          value: 1,
        hyaline: {
          color: new THREE.Color().setHSL(3 / 8, 1, 0.8),
          value: 0,
      });
      // 方块配置
      const CubeCfg = {
        d: 0.8,
        size: 1.2,
        options: [
            name: "octant-1",
            x: 1,
            y: 1,
            z: 1,
            type: "hyaline",
          },...
      
      // 创建方块
      this.cubeGroup = this._createCubes(CubeCfg);
      this.scene.add(this.cubeGroup);

      选择共面

      如何选择共面呢?

      在崩铁中通过点击方向箭头选择共面,并添加选中效果(发光);这里直接搞六个选择共面的按钮和左右按钮,用相对定位糊上即可。

      如何确定选中哪些方块呢,毕竟方块是可以旋转走的?

    4. Box3Helper:设置六个 Box3Helper 并编号,每个 Box3Helper 都能包含四个位置;使用其 Box 对象的 distanceToPoint 方法来判断方块是否在选中的 Box3Helper 范围内,即方块是否选中。
    5. Raycaster:设置八条射线,可组合出六个面,通过射线相交来找选中的方块;且射线也可监测出投射面板,检查结果也更方便。
    6. * 创建射线组 _createRaycasters(d, size) { const raycasterOptions = [ * 投射面: XY平面 * 方向:Z负半轴 name: "raycaster-1", origin: new THREE.Vector3(d, d, d + size), direction: new THREE.Vector3(0, 0, -1), type: "solid", },... return raycasterOptions
      // 创建投射组
      this.raycasterOptions = this._createRaycasters(CubeCfg.d, CubeCfg.size);
      // 投射板
      this.boardGroup = this._addProjectionBoard(this.raycasterOptions, CubeCfg.d, CubeCfg.size);
      this.scene.add(this.boardGroup);
      // 划分选择面
      this.planeOptions = [
              name: "plane-1",
              raycasterIndexs: [0, 1], // xz,y>0
              rotateAxis: new THREE.Vector3(0, 1, 0),
              leftRad: Math.PI / 2,
              rightRad: -Math.PI / 2,
          },...
      

      旋转方块

      一开始调用 Object3D 的一些旋转 API(rotateOnAxis、rotateOnWorldAxis、rotateX…),结果是方块原地打转(方块各面纹理不同时可观察出来)。

      是 Object3D 本地坐标系的原因,后来参考知乎 · ThreeJS 四步制作一个简易魔方评论区方法,秒了。

      注意:这里的旋转仅仅是方块位置(看成中心点)的旋转,实际上方块旋转过程中还应该考虑自身的旋转,否则动画效果会比较怪异。

      * @descption 旋转动画 * @param {*} cubes 旋转的方块 * @param {*} rotateAxis 旋转轴 * @param {*} rad 旋转角度 _rotate(cubes, rotateAxis, rad) { // 更新起始数据 cubes.forEach((cube) => { cube.recoardStart = { position: cube.position.clone(), rotation: new THREE.Vector3(0, 0, 0), cube.rotation.x = 0; cube.rotation.y = 0; cube.rotation.z = 0; }); // 旋转动画 const LOCAL_AXIS = rotateAxis.x ? "x" : rotateAxis.y ? "y" : "z"; const TWEEN_DURATION = 1000; const tween = new TWEEN.Tween({ percentage: 0 }); tween.easing(TWEEN.Easing.Quartic.InOut); tween.onUpdate(({ percentage }) => { cubes.forEach((cube) => { // 旋转面 cube.rotation[LOCAL_AXIS] = cube.recoardStart.rotation[LOCAL_AXIS] + percentage * rad; const newPosition = cube.recoardStart.position.clone(); newPosition.applyAxisAngle(rotateAxis, percentage * rad); // 旋转位置 cube.position.set(newPosition.x, newPosition.y, newPosition.z); }); }); tween.onComplete(() => { /** 校验结果 */ this._check(); }); tween.to({ percentage: 1 }, TWEEN_DURATION); tween.start();

      胜利结算

      校验结果比较简单:

    7. 每条射线穿过 2 个方块和一个投射面,若满足消消乐规则,则给投射面一个高亮效果,代表成功;
    8. 若所有投射面都高亮,则表示关卡胜利。
    9. * 校验结果 _check() { this.raycasterOptions.forEach((e) => { const checkSet = new Set(); this.gzsRaycaster.set(e.origin, e.direction); const intersects = this.gzsRaycaster.intersectObjects( this.scene.children for (let i = 0; i < intersects.length; i++) { if (intersects[i].object.isMesh) { checkSet.add(intersects[i].object); const checkArr = Array.from(checkSet); checkArr[2].trigger( checkArr[0].rccs.value === checkArr[1].rccs.value && checkArr[1].rccs.value === checkArr[2].rccs.value }); let pass = this.boardGroup.children.every((e) => e.children[0].visible); setTimeout(() => { if (pass) { alert("出院!"); }, 100);

      优化

      UI太丑,魔方可用贴图或模型。下一步尝试 blender 建模。

      代码

      // 枘凿六合
      import * as THREE from "three";
      import * as TWEEN from "@tweenjs/tween.js";
      import ThreeMap from "./ThreeMap";
      const ECubeType = Object.freeze({
        solid: {
          color: new THREE.Color().setHSL(0 / 8, 1, 0.5),
          value: 1,
        hyaline: {
          color: new THREE.Color().setHSL(3 / 8, 1, 0.8),
          value: 0,
      });
      const CubeCfg = {
        d: 0.8,
        size: 1.2,
        options: [
            name: "octant-1",
            x: 1,
            y: 1,
            z: 1,
            type: "hyaline",
            name: "octant-2",
            x: -1,
            y: 1,
            z: 1,
            type
      
      
      
      
          
      : "solid",
            name: "octant-3",
            x: -1,
            y: -1,
            z: 1,
            type: "solid",
            name: "octant-4",
            x: 1,
            y: -1,
            z: 1,
            type: "hyaline",
            name: "octant-5",
            x: 1,
            y: 1,
            z: -1,
            type: "solid",
            name: "octant-6",
            x: -1,
            y: 1,
            z: -1,
            type: "hyaline",
            name: "octant-7",
            x: -1,
            y: -1,
            z: -1,
            type: "hyaline",
            name: "octant-8",
            x: 1,
            y: -1,
            z: -1,
            type: "solid",
      export default class GzsThreeMap extends ThreeMap {
        _initView() {
          this._createScene();
          this._addLight(-1, 2, 4);
          this._addLight(1, -1, -2);
          this._createCamera(this.options.camera.far);
          this._createRender(this.rootElement);
          this._createControls(this.camera, this.renderer.domElement);
          this._createAxesHelper();
          this.switchAnimate();
          // 相机归位
          this.camera.position.set(4.6, 3, 5.14);
          this.camera.up.set(0, -1, 0);
          this.camera.lookAt(0, 0, 0);
          // 创建方块
          this.cubeGroup = this._createCubes(CubeCfg);
          this.scene.add(this.cubeGroup);
          // 创建投射组
          this.raycasterOptions = this._createRaycasters(CubeCfg.d, CubeCfg.size);
          // 投射板
          this.boardGroup = this._addProjectionBoard(
            this.raycasterOptions,
            CubeCfg.d,
            CubeCfg.size
          this.scene.add(this.boardGroup);
          // 划分选择面
          this.planeOptions = [
              name: "plane-1",
              raycasterIndexs: [0, 1], // xz,y>0
              rotateAxis: new THREE.Vector3(0, 1, 0),
              leftRad: Math.PI / 2,
              rightRad: -Math.PI / 2,
              name: "plane-2",
              raycasterIndexs: [2, 3], // xz,y<0
              rotateAxis: new THREE.Vector3(0, 1, 0),
              leftRad: Math.PI / 2,
              rightRad: -Math.PI / 2,
              name: "plane-3",
              raycasterIndexs: [0, 3], // yz,x>0
              rotateAxis: new THREE.Vector3(1, 0, 0),
              leftRad: Math.PI / 2,
              rightRad: -Math.PI /
      
      
      
      
          
       2,
              name: "plane-4",
              raycasterIndexs: [1, 2], // yz,x<0
              rotateAxis: new THREE.Vector3(1, 0, 0),
              leftRad: Math.PI / 2,
              rightRad: -Math.PI / 2,
              name: "plane-5",
              raycasterIndexs: [4, 7], // xy,z>0
              rotateAxis: new THREE.Vector3(0, 0, 1),
              leftRad: Math.PI / 2,
              rightRad: -Math.PI / 2,
              name: "plane-6",
              raycasterIndexs: [5, 6], // yz,x<0
              rotateAxis: new THREE.Vector3(0, 0, 1),
              leftRad: Math.PI / 2,
              rightRad: -Math.PI / 2,
          // 投射
          this.gzsRaycaster = new THREE.Raycaster();
          this.gzsRaycaster.camera = this.camera;
          // 当前选中面
          this.currentPlane = undefined;
          this.currentMeshSet = new Set();
          this.scene.background = new THREE.Color("white");
         * 动画监听
        _animateListener() {
          TWEEN.update();
         * 添加光照
        _addLight(...pos) {
          const color = 0xffffff;
          const intensity = 1;
          const light = new THREE.DirectionalLight(color, intensity);
          light.position.set(-1, 2, 4);
          light.position.set(...pos);
          this.scene.add(light);
         * 新建立方体
         * @param {*} cubeCfg
         * @returns
        _createCubes(cubeCfg) {
          const { d, size, options } = cubeCfg;
          const geometry = new THREE.BoxGeometry(size, size, size);
          const cubeGroup = new THREE.Group();
          options.forEach((e) => {
            // 立方体
            const material = new THREE.MeshPhongMaterial({
              color: ECubeType[e.type].color,
              opacity: 0.5,
              transparent: true,
              side: THREE.DoubleSide,
            });
            const cube = new THREE.Mesh(geometry, material);
            cube.position.set(e.x * d, e.y * d, e.z * d);
            cube.name = e.name;
            cube.rccs = {
              value: ECubeType[e.type].value,
            // 选中效果
            const box = new THREE.Box3();
            box.setFromCenterAndSize(
              new THREE.Vector3(0, 0, 0),
              new THREE.Vector3(size, size, size)
            const helper = new THREE.Box3Helper(box, 0xffff00);
            helper.visible = false;
            helper.name = "box3Helper";
            cube.add(helper);
            // 触发选中效果
            cube.trigger = (visible) => {
              cube.children[0].visible = visible;
            cubeGroup.add(cube);
          });
          return cubeGroup;
         * 创建射线组
        _createRaycasters(d, size) {
          const raycasterOptions = [
             * 投射面: XY平面
             * 方向:Z负半轴
              name: "raycaster-1",
              origin: new THREE.Vector3(d, d,
      
      
      
      
          
       d + size),
              direction: new THREE.Vector3(0, 0, -1),
              type: "solid",
              name: "raycaster-2",
              origin: new THREE.Vector3(-d, d, d + size),
              direction: new THREE.Vector3(0, 0, -1),
              type: "solid",
              name: "raycaster-3",
              origin: new THREE.Vector3(-d, -d, d + size),
              direction: new THREE.Vector3(0, 0, -1),
              type: "hyaline",
              name: "raycaster-4",
              origin: new THREE.Vector3(d, -d, d + size),
              direction: new THREE.Vector3(0, 0, -1),
              type: "hyaline",
             * 投射面: YZ平面
             * 方向:X负半轴
              name: "raycaster-5",
              origin: new THREE.Vector3(d + size, d, d),
              direction: new THREE.Vector3(-1, 0, 0),
              type: "solid",
              name: "raycaster-6",
              origin: new THREE.Vector3(d + size, d, -d),
              direction: new THREE.Vector3(-1, 0, 0),
              type: "solid",
              name: "raycaster-7",
              origin: new THREE.Vector3(d + size, -d, -d),
              direction: new THREE.Vector3(-1, 0, 0),
              type: "hyaline",
              name: "raycaster-8",
              origin: new THREE.Vector3(d + size, -d, d),
              direction: new THREE.Vector3(-1, 0, 0),
              type: "hyaline",
          return raycasterOptions;
         * @descption 增加投影面
        _addProjectionBoard(raycasterOptions, d, size) {
          const boardGroup = new THREE.Group();
          raycasterOptions.forEach((e) => {
            const dx = e.direction.x === -1 ? 0.1 : size;
            const dy = size;
            const dz = e.direction.z === -1 ? 0.1 : size;
            const geometry = new THREE.BoxGeometry(
              e.direction.x === -1 ? 0.1 : size,
              size,
              e.direction.z === -1 ? 0.1 : size
            const material = new THREE.MeshPhongMaterial({
              color: ECubeType[e.type].color,
              opacity: 0.6,
              transparent: true,
              side: THREE.DoubleSide,
            });
            const board = new THREE.Mesh(geometry, material);
            board.position.set(
              e.direction.x === -1 ? -e.origin.x : e.origin.x,
              e.origin.y,
              e.direction.z === -1 ? -e.origin.z : e.origin.z
            board.rccs = {
              value: ECubeType[e.type].value,
            // 检查效果
            const box = new THREE.Box3();
            box.setFromCenterAndSize(
              new THREE.Vector3(0,
      
      
      
      
          
       0, 0),
              new THREE.Vector3(dx, dy, dz)
            const helper = new THREE.Box3Helper(box, 0xffff00);
            helper.visible = false;
            helper.name = "box3Helper";
            board.add(helper);
            // 触发通过效果
            board.trigger = (visible) => {
              board.children[0].visible = visible;
            boardGroup.add(board);
          });
          return boardGroup;
         * 按面选择
         * @param {*} name
        selectFrame(name) {
          const planeName = `plane-${name}`;
          this.currentPlane = this.planeOptions.find((e) => e.name === planeName);
          this.currentMeshSet.clear();
          this.cubeGroup.children.forEach((e) => {
            e.trigger(false);
          });
          this.currentPlane.raycasterIndexs.forEach((e) => {
            const { origin, direction } = this.raycasterOptions[e];
            this.gzsRaycaster.set(origin, direction);
            const intersects = this.gzsRaycaster.intersectObjects(
              this.cubeGroup.children
            for (let i = 0; i < intersects.length; i++) {
              if (intersects[i].object.isMesh) {
                this.currentMeshSet.add(intersects[i].object);
          });
          this.currentMeshSet.forEach((e) => {
            e.trigger(true);
          });
        rotateLeft() {
          this._rotate(
            this.currentMeshSet,
            this.currentPlane.rotateAxis,
            this.currentPlane.leftRad
        rotateRight() {
          this._rotate(
            this.currentMeshSet,
            this.currentPlane.rotateAxis,
            this.currentPlane.rightRad
         * @descption 旋转动画
         * @param {*} cubes 旋转的方块
         * @param {*} rotateAxis 旋转轴
         * @param {*} rad 旋转角度
        _rotate(cubes, rotateAxis, rad) {
          // 更新起始数据
          cubes.forEach((cube) => {
            cube.recoardStart = {
              position: cube.position.clone(),
              rotation: new THREE.Vector3(0, 0, 0),
            cube.rotation.x = 0;
            cube.rotation.y = 0;
            cube.rotation.z = 0;
          });
          // 旋转动画
          const LOCAL_AXIS = rotateAxis.x ? "x" : rotateAxis.y ? "y" : "z";
          const TWEEN_DURATION = 1000;
          const tween = new TWEEN.Tween({ percentage: 0 });
          tween.easing(TWEEN.Easing.Quartic.InOut);
          tween.onUpdate(({ percentage }) => {
            cubes.forEach((cube) => {
              // 旋转面
              cube.rotation[LOCAL_AXIS] =
                cube.recoardStart.rotation[LOCAL_AXIS] + percentage * rad;
              const newPosition = cube.recoardStart.position.clone();
              newPosition.applyAxisAngle(rotateAxis, percentage * rad);
              // 旋转位置
              cube.position.set(newPosition.x, newPosition.y, newPosition.z);
            });
          });
          tween.onComplete(() => {
            /** 校验结果 */
            this._check();
          });
          tween.to({ percentage: 1 }, TWEEN_DURATION);
          tween.start();
         * 校验结果
        _check() {
          this.raycasterOptions.forEach((e) => {
            const checkSet = new Set();
            this.gzsRaycaster.set(e.origin, e.direction);
            const intersects = this.gzsRaycaster.intersectObjects(
              this.scene.children
            for (let i = 0; i < intersects.length; i++) {
              if (intersects[i].object.isMesh) {
                checkSet.add(intersects[i].object);
            const checkArr = Array.from(checkSet);
            checkArr[2].trigger(
              checkArr[0].rccs.value === checkArr[1].rccs.value &&
                checkArr[1].rccs.value === checkArr[2].rccs.value
          });
          let pass = this.boardGroup.children.every((e) => e.children[0].visible);
          setTimeout(() => {
            if (pass) {
              alert("出院!");
          }, 100);