n阶魔方块数_n阶魔方阵

n阶魔方块数_n阶魔方阵相信很多同学都玩过魔方,那么你知道如何开发一个魔方的游戏么?今天这篇文章为大家带来使用 Oasis 快速实现 N 阶魔方的总结分享,感谢蚂蚁集团工程师箕宿的投稿和分享,也欢迎广大读者的交流反馈~

相信很多同学都玩过魔方,那么你知道如何开发一个魔方的游戏么?今天这篇文章为大家带来使用 Oasis 快速实现 N 阶魔方的总结分享,感谢蚂蚁集团工程师箕宿的投稿和分享,也欢迎广大读者的交流反馈~

Oasis 是一个移动优先的高性能引擎,支付宝里面的很多互动业务都是拿 Oasis 制作的。因此选择 Oasis 作为开发 N 阶魔方的引擎(本文使用引擎版本为 0.5.6)。

实体与组件

Oasis 有两个重要的概念

  1. Entity 每个场景下的都会有实体的树形,但是其本身不具备渲染等实际的功能
  2. Component 是具有实际功能的,可以被 Entity 添加

这种基于组件的功能扩展方式注重将程序按照功能独立封装,在使用的时候按照需要组合添加,非常有利于降低程序耦合度并提升代码复用率。

搭建项目

Oasis 自带一个脚手架可以直接用来搭建一个项目, npm init @oasisi-engine/oasis-app
n阶魔方块数_n阶魔方阵

添加摄像机控制

摄像头的控制器在 @oasis-engine/controls 这个包里

// init camera
const cameraEntity = rootEntity.createChild("camera");
const camera = cameraEntity.addComponent(Camera);
// add camera control
cameraEntity.addComponent(OrbitControl);
const pos = cameraEntity.transform.position;
pos.setValue(10, 10, 10);
cameraEntity.transform.position = pos;
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));

使用 bufferMesh 绘制魔方用的方块

bufferMesh 可以自由的控制顶点,法线和颜色, 绘制立方体的顶点。这里用 8 个顶角和 12 个棱以及 6 个面绘制魔方中的一个单独的方块, 通过顶点和棱绘制出边框的效果
1634474806(1).png

import {
  Buffer,
  BufferBindFlag,
  BufferMesh,
  BufferUsage,
  IndexFormat,
  VertexElement,
  Engine,
  Color,
  VertexElementFormat,
  MeshTopology,
} from "oasis-engine";

function hexToRgb(hex) {
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return new Color(
      parseInt(result[1], 16) / 255,
      parseInt(result[2], 16) / 255,
      parseInt(result[3], 16) / 255,
      1.0,
  );
}

const colorList = [
  hexToRgb('#C41E3A'),
  hexToRgb('#009E60'),
  hexToRgb('#0051BA'),
  hexToRgb('#FF5800'),
  hexToRgb('#FFD500'),
  hexToRgb('#FFFFFF')
];

export function createCube(engine: Engine, size: number, strokeSize: number) {
  const geometry = new BufferMesh(engine, "CustomCubeGeometry");
  const smallSize = size - strokeSize;

  // prettier-ignore
  // Create vertices data.
  const vertices: Float32Array = new Float32Array([
      // Up
      -smallSize, size, -smallSize, 0, 1, 0, smallSize, size, -smallSize, 0, 1, 0, smallSize, size, smallSize, 0, 1, 0, -smallSize, size, smallSize, 0, 1, 0,
      // Down
      -smallSize, -size, -smallSize, 0, -1, 0, smallSize, -size, -smallSize, 0, -1, 0, smallSize, -size, smallSize, 0, -1, 0, -smallSize, -size, smallSize, 0, -1, 0,
      // Left
      -size, smallSize, -smallSize, -1, 0, 0, -size, smallSize, smallSize, -1, 0, 0, -size, -smallSize, smallSize, -1, 0, 0, -size, -smallSize, -smallSize, -1, 0, 0,
      // Right
      size, smallSize, -smallSize, 1, 0, 0, size, smallSize, smallSize, 1, 0, 0, size, -smallSize, smallSize, 1, 0, 0, size, -smallSize, -smallSize, 1, 0, 0,
      // Front
      -smallSize, smallSize, size, 0, 0, 1, smallSize, smallSize, size, 0, 0, 1, smallSize, -smallSize, size, 0, 0, 1, -smallSize, -smallSize, size, 0, 0, 1,
      // Back
      -smallSize, smallSize, -size, 0, 0, -1, smallSize, smallSize, -size, 0, 0, -1, smallSize, -smallSize, -size, 0, 0, -1, -smallSize, -smallSize, -size, 0, 0, -1,
      // 八个角
      // up left back
      -smallSize, size, -smallSize, -1, 1, -1, -size, smallSize, -smallSize, -1, 1, -1, -smallSize, smallSize, -size, -1, 1, -1,
      // down left back
      -smallSize, -size, -smallSize, -1, -1, -1, -size, -smallSize, -smallSize, -1, -1, -1, -smallSize, -smallSize, -size, -1, -1, -1,
      // up right back 
      smallSize, size, -smallSize, 1, 1, -1, size, smallSize, -smallSize, 1, 1, -1, smallSize, smallSize, -size, 1, 1, -1,
      // down right back 
      smallSize, -size, -smallSize, 1, -1, -1, size, -smallSize, -smallSize, 1, -1, -1, smallSize, -smallSize, -size, 1, -1, -1,
      // up left front
      -smallSize, size, smallSize, -1, 1, 1, -size, smallSize, smallSize, -1, 1, 1, -smallSize, smallSize, size, -1, 1, 1,
      // down left front
      -smallSize, -size, smallSize, -1, -1, 1, -size, -smallSize, smallSize, -1, -1, 1, -smallSize, -smallSize, size, -1, -1, 1,
      // up right front 
      smallSize, size, smallSize, 1, 1, 1, size, smallSize, smallSize, 1, 1, 1, smallSize, smallSize, size, 1, 1, 1,
      // down right front 
      smallSize, -size, smallSize, 1, -1, 1, size, -smallSize, smallSize, 1, -1, 1, smallSize, -smallSize, size, 1, -1, 1,
      // 12个棱
      // up left
      -smallSize, size, -smallSize, -1, 1, 0, -smallSize, size, smallSize, -1, 1, 0, -size, smallSize, -smallSize, -1, 1, 0, -size, smallSize, smallSize, -1, 1, 0,
      // up right
      smallSize, size, -smallSize, 1, 1, 0, smallSize, size, smallSize, 1, 1, 0, size, smallSize, -smallSize, 1, 1, 0, size, smallSize, smallSize, 1, 1, 0,
      // up front
      -smallSize, size, smallSize, 0, 1, 1, smallSize, size, smallSize, 0, 1, 1, -smallSize, smallSize, size, 0, 1, 1, smallSize, smallSize, size, 0, 1, 1,
      // up back
      -smallSize, size, -smallSize, 0, 1, -1, smallSize, size, -smallSize, 0, 1, -1, -smallSize, smallSize, -size, 0, 1, -1, smallSize, smallSize, -size, 0, 1, -1,
      // down left
      -smallSize, -size, -smallSize, -1, -1, 0, -smallSize, -size, smallSize, -1, -1, 0, -size, -smallSize, -smallSize, -1, -1, 0, -size, -smallSize, smallSize, -1, -1, 0,
      // down right
      smallSize, -size, -smallSize, 1, -1, 0, smallSize, -size, smallSize, 1, -1, 0, size, -smallSize, -smallSize, 1, -1, 0, size, -smallSize, smallSize, 1, -1, 0,
      // down front
      -smallSize, -size, smallSize, 0, -1, 1, smallSize, -size, smallSize, 0, -1, 1, -smallSize, -smallSize, size, 0, -1, 1, smallSize, -smallSize, size, 0, -1, 1,
      // down back
      -smallSize, -size, -smallSize, 0, -1, -1, smallSize, -size, -smallSize, 0, -1, -1, -smallSize, -smallSize, -size, 0, -1, -1, smallSize, -smallSize, -size, 0, -1, -1,
      // left front
      -size, smallSize, smallSize, -1, 0, 1, -size, -smallSize, smallSize, -1, 0, 1, -smallSize, smallSize, size, -1, 0, 1, -smallSize, -smallSize, size, -1, 0, 1,
      // front right
      smallSize, smallSize, size, 1, 0, 1, smallSize, -smallSize, size, 1, 0, 1, size, smallSize, smallSize, 1, 0, 1, size, -smallSize, smallSize, 1, 0, 1,
      // right back
      size, smallSize, -smallSize, 1, 0, -1, size, -smallSize, -smallSize, 1, 0, -1, smallSize, smallSize, -size, 1, 0, -1, smallSize, -smallSize, -size, 1, 0, -1,
      // back left
      -smallSize, smallSize, -size, -1, 0, -1, -smallSize, -smallSize, -size, -1, 0, -1, -size, smallSize, -smallSize, -1, 0, -1, -size, -smallSize, -smallSize, -1, 0, -1,
  ]);

  // prettier-ignore
  // Create indices data.
  const indices: Uint16Array = new Uint16Array([
      // Up
      0, 2, 1, 2, 0, 3,
      // Down
      4, 6, 7, 6, 4, 5,
      // Left
      8, 10, 9, 10, 8, 11,
      // Right
      12, 14, 15, 14, 12, 13,
      // Front
      16, 18, 17, 18, 16, 19,
      // Back
      20, 22, 23, 22, 20, 21,
      // up left Back corner
      24, 26, 25,
      // down left back corner
      27, 28, 29,
      // up right back 
      30, 31, 32,
      // down right back 
      33, 35, 34,
      // up left front
      36, 37, 38,
      // down left front
      39, 41, 40,
      // up right front
      42, 44, 43,
      // down right front
      45, 46, 47,
      // Up left
      48, 50, 49, 50, 51, 49,
      // up right
      52, 53, 54, 55, 54, 53,
      // up front
      56, 58, 57, 58, 59, 57,
      // up back
      60, 61, 62, 62, 61, 63,
      // down left
      64, 65, 66, 66, 65, 67,
      // down right
      68, 70, 69, 71, 69, 70,
      // down front
      72, 73, 74, 75, 74, 73,
      // down back
      76, 78, 77, 79, 77, 78,
      // left front
      80, 81, 82, 83, 82, 81,
      // front right
      84, 85, 86, 87, 86, 85,
      // right back
      88, 89, 90, 91, 90, 89,
      // back left
      92, 93, 94, 95, 94, 93,
  ]);

  // Create vertices color and init by black.
  const colorData = new Float32Array(3 * 24 + 3 * 24 + 3 * 32 + 3 * 16);
  colorData.fill(0.0);
  for (let i = 0; i < 6; i++) {
      const color = colorList[i];
      const offset = i * 12;
      for (let i = 0; i < 4; i++) {
          colorData[offset + i * 3 + 0] = color.r;
          colorData[offset + i * 3 + 1] = color.g;
          colorData[offset + i * 3 + 2] = color.b;
      }
  }

  // Create gpu vertex buffer and index buffer.
  const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertices, BufferUsage.Static);
  const indexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, indices, BufferUsage.Static);
  const independentColorBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, colorData, BufferUsage.Dynamic);

  // Bind buffer
  geometry.setVertexBufferBinding(vertexBuffer, 24);
  geometry.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16);
  geometry.setVertexBufferBinding(independentColorBuffer, 12, 1);

  // Add vertexElement
  geometry.setVertexElements([
      new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0),
      new VertexElement("NORMAL", 12, VertexElementFormat.Vector3, 0),
      new VertexElement("COLOR_0", 0, VertexElementFormat.Vector3, 1)
  ]);

  // Add one sub geometry.
  geometry.addSubMesh(0, indices.length, MeshTopology.Triangles);
  return geometry;
}

最终的绘制效果
image.png

绘制 3 阶魔方

从 xyz 轴 -1 0 1,三次 for 循环绘制 27 个小方块

for (let i = -1; i < 2; i++) {
  for (let j = -1; j < 2; j++) {
    for (let k = -1; k < 2; k++) {
      const cubeEntity = rootEntity.createChild("cube");
      const cubePos = cubeEntity.transform.position;
      cubePos.setValue(i * cubeSize, j * cubeSize, k * cubeSize);
      cubeEntity.transform.position = cubePos;
    
      // use to rayrast pick
      const boxCollider = cubeEntity.addComponent(BoxCollider);
      boxCollider.setBoxCenterSize(new Vector3(), new Vector3(cubeSize, cubeSize, cubeSize));
      const renderer = cubeEntity.addComponent(MeshRenderer);
      const mtl = new BlinnPhongMaterial(engine);
      renderer.mesh = createCube(engine, cubeSize/2, CUBE_STROKE);
      renderer.setMaterial(mtl);
    }
  }
}

1634475068(1).png

拾取鼠标点击的方块

通过摄像机的 raycast 和 collider 可以快速拾取 entity,同时可以获取到焦点。由于魔方只有 6 个面,那么肯定存在交点的 (x,y,z) 其中一个等于边界值的情况,根据这个边界值就可以判断点击的是哪个面。

const ray = new Ray();
const ratio = window.devicePixelRatio;
// 鼠标点击触发拾取
function handleMouseDown(e) {
  let x = e.offsetX;
  let y = e.offsetY;
  if (e.touches && e.touches.length > 0) {
    x = e.touches[0].pageX;
    y = e.touches[0].pageY;
  }
  camera.screenPointToRay(new Vector2(x, y).scale(ratio), ray);
  const hit = new HitResult();
  const result = engine.physicsManager.raycast(ray, Number.MAX_VALUE,
                                               Layer.Everything, hit);
  if (result) {
    console.info('点击的entity',hit.collider.entity)
    console.info('点击的相交点',hit.point)
  }
}

document.getElementById("canvas").addEventListener("mousedown", handleMouseDown);
document.getElementById("canvas").addEventListener("touchstart", handleMouseDown);

判断是哪个面

// 拾取面 上下左右前后
export enum DIR {
    UP = 'UP',
    DOWN = 'DOWN',
    LEFT = 'LEFT',
    RIGHT = 'RIGHT',
    FRONT = 'FRONT',
    BACK = 'BACK',
}
// 根据点判断是哪个面
function getDir(point:Vector3) {
  const maxDist = CUBE_LEVEL * CUBE_SIZE / 2;
  const delta = 0.001;
  const dx = Math.abs(Math.abs(point.x) - maxDist);
  const dy = Math.abs(Math.abs(point.y) - maxDist);
  const dz = Math.abs(Math.abs(point.z) - maxDist);
  if (dx < delta) {
    if (point.x > 0) {
      return DIR.RIGHT;
    } else {
      return DIR.LEFT;
    }
  } else if (dy < delta) {
    if (point.y > 0) {
      return DIR.UP;
    } else {
      return DIR.DOWN;
    }
  } else if (dz < delta) {
    if (point.z > 0) {
      return DIR.BACK;
    }
  }
  return DIR.FRONT;
}

判定旋转方向

拾取的魔方中的一个方块的其中一个面,那么旋转只存在两种情况,一种是横着旋转,一种是竖着旋转。如果是垂直于 x 轴的面的话,那么只存在绕着 y 轴或者绕着 z 轴两种情况。
1634481230(1).png
通过 rayrast 可以获取到一个从屏幕点击点发出的射线,在鼠标按下和鼠标抬起的分别取射线的 direction
n阶魔方块数_n阶魔方阵
判断两次 direction 移动的在 xyz 的方向最大的一个就可以拿到用户要进行的旋转方向。

整个魔方只有6个旋转方向

class ROTATION {
  	// x轴顺时针
    static X = new Vector3(1, 0, 0);
  	// x轴逆时针
    static DX = new Vector3(-1, 0, 0);
  	// y轴顺时针
    static Y = new Vector3(0, 1, 0);
    // y轴逆时针
    static DY = new Vector3(0, -1, 0);
  	// z轴顺时针
    static Z = new Vector3(0, 0, 1);
  	// z轴逆时针
    static DZ = new Vector3(0, 0, -1);
}

根据面和 direction 获取要进行的旋转轴

function getRotation(startDirection: Vector3, endDirection: Vector3): Vector3 {
  const { x, y, z } = startDirection.clone().subtract(endDirection);
  switch (this.dir) {
    case DIR.UP:
        if (Math.abs(x) > Math.abs(z)) {
            if (x > 0) {
                return ROTATION.DZ;
            } else {
                return ROTATION.Z;
            }
        } else {
            if (z > 0) {
                return ROTATION.X;
            } else {
                return ROTATION.DX;
            }
        }
    case DIR.DOWN:
        if (Math.abs(x) > Math.abs(z)) {
            if (x > 0) {
                return ROTATION.Z;
            } else {
                return ROTATION.DZ;
            }
        } else {
            if (z > 0) {
                return ROTATION.DX;
            } else {
                return ROTATION.X;
            }
        }
    case DIR.LEFT:
        if (Math.abs(y) > Math.abs(z)) {
            if (y > 0) {
                return ROTATION.DZ;
            } else {
                return ROTATION.Z;
            }
        } else {
            if (z > 0) {
                return ROTATION.Y;
            } else {
                return ROTATION.DY;
            }
        }
    case DIR.RIGHT:
        if (Math.abs(y) > Math.abs(z)) {
            if (y > 0) {
                return ROTATION.Z;
            } else {
                return ROTATION.DZ;
            }
        } else {
            if (z > 0) {
                return ROTATION.DY;
            } else {
                return ROTATION.Y;
            }
        }
    case DIR.FRONT:
        if (Math.abs(x) > Math.abs(y)) {
            if (x > 0) {
                return ROTATION.DY;
            } else {
                return ROTATION.Y;
            }
        } else {
            if (y > 0) {
                return ROTATION.X;
            } else {
                return ROTATION.DX;
            }
        }
    case DIR.BACK:
        if (Math.abs(x) > Math.abs(y)) {
            if (x > 0) {
                return ROTATION.Y;
            } else {
                return ROTATION.DY;
            }
        } else {
            if (y > 0) {
                return ROTATION.DX;
            } else {
                return ROTATION.X;
            }
        }
  }
}

旋转

点击的方块和旋转轴,就可以旋转了,每次旋转都是旋转在同一个面的方块。
获取要旋转的方块。

function near(a: number, b: number): boolean {
    return Math.abs(a - b) < 0.0001;
}

const cubes = cubes.filter(v => {
  if (this.rotation === ROTATION.X || this.rotation === ROTATION.DX) {
    return near(v.transform.position.x, entity.transform.position.x);
  } else if (this.rotation === ROTATION.Y || this.rotation === ROTATION.DY) {
    return near(v.transform.position.y, entity.transform.position.y);
  }
  return near(v.transform.position.z, entity.transform.position.z);
});

// 记录下旋转开始前的方块的position和rotation
const rotationQuaternions = cubes.map(v => v.transform.rotationQuaternion.clone());
const positions = cubes.map(v => v.transform.position.clone());

旋转的动画

const angle = 0;
let start = true;
function update() {
  if(!start) return;
	angle += ROTATION_SPEED;
  if (angle > 90) {
    angle = 90;
    start = false;
  }
  cubes.forEach((cube, index) => {
    const matrix = new Matrix();
    const position = positions[index].clone();
    const quaternion = rotationQuaternions[index].clone();

    const { x, y, z } = position;
    matrix.translate(new Vector3(-x, -y, -z))
    matrix.rotateAxisAngle(this.rotation, this.radians);
    matrix.translate(new Vector3(x, y, z));
    // 更新position
    cube.transform.position = position.transformNormal(matrix);

    const q = new Quaternion;
    q.rotateAxisAngle(this.rotation, this.radians);
    // 更新旋转
    cube.transform.rotationQuaternion = q.multiply(quaternion);
  });
}

旋转的动画还有一种优化方案,把所有要旋转的方块当一个整体,绕某个轴旋转,这样只需要更新下每个方块的世界矩阵即可,具体代码如下

const angle = 0;
let start = true;
const tempMat1 = new Matrix();
const tempMat2 = new Matrix();
function update() {
  if(!start) return;
	angle += ROTATION_SPEED;
  let delta = ROTATION_SPEED;
  if (angle > 90) {
    delta = 90 - (angle - ROTATION_SPEED);
    angle = 90;
    start = false;
  }
  tempMat1.identity().rotateAxisAngle(this.rotation, delta * Math.PI / 180 );
  cubes.forEach((cube, index) => {
    // 更新世界矩阵
    Matrix.multiply(tempMat1, cube.transform.worldMatrix, tempMat2);
    cube.transform.worldMatrix = tempMat2;
  });
}

扩展到 N 阶魔方

上述算法与魔方的阶数没有太大关联,只要在创建的时候指定好魔方块的位置就可以实现 N 阶段魔方。
奇数魔方和偶数魔方要注意整体相对于(0,0,0)对称摆放

const CUBE_SIZE = 1.0;
const CUBE_LEVEL = 10;

function createRuickCube(rootEntity: Entity, engine: Engine) {
	const isEven = CUBE_LEVEL % 2 === 0;
	let start = - Math.floor(CUBE_LEVEL / 2);
	let end = Math.ceil(CUBE_LEVEL / 2);
	let offset = 0;
	if (isEven) {
		start += 1;
		end += 1;
		offset = -0.5;
	}
	for (let i = start; i < end; i++) {
		for (let j = start; j < end; j++) {
			for (let k = start; k < end; k++) {
				const cubeEntity = rootEntity.createChild("cube");
				const cubePos = cubeEntity.transform.position;
				cubePos.setValue((i + offset) * CUBE_SIZE, (j + offset) * CUBE_SIZE, (k + offset) * CUBE_SIZE);
				cubeEntity.transform.position = cubePos;

				const boxCollider = cubeEntity.addComponent(BoxCollider);
				boxCollider.setBoxCenterSize(new Vector3(), new Vector3(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE));
				const renderer = cubeEntity.addComponent(MeshRenderer);
				const mtl = new BlinnPhongMaterial(engine);
				renderer.mesh = createCube(engine, CUBE_SIZE / 2, CUBE_STROKE);
				renderer.setMaterial(mtl);
			}
		}
	}
}

效果
image.png

源码

github.com/ilimei/rubi…

演示地址

ilimei.github.io/rubikcube

如何进一步了解我们

Oasis 开源社区群 (钉钉):

JPG.png

Oasis 开源社区群管理员 (微信):

image.png

网站

官网地址
oasisengine.cn
Engine 源码地址
github.com/oasis-engin…
Engine Toolkit 源码地址
github.com/oasis-engin…

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13090.html

(0)

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注