精华内容
下载资源
问答
  • 魔方模拟器

    2018-07-18 06:57:03
    一个模拟魔方的小程序源代码,编译环境VS2008,其中3D效果函数为自己手写,封装在静态库Standard3D中。运行时左右键转动当前面。用贪心算法写了个简单的最少步数还原策略,但没能完全解开。
  • MATLAB工具箱大全- 魔方模拟器与规划求解
  • 【手把手制作三阶魔方模拟器】用MATLAB绘制一个一阶魔方绘制三阶魔方每个块的中心为每个块六面上色# 其他 by 今天不飞了 有一个酷爱魔方的朋友,托我给他定制一个专门用于“训练魔方观察和预判能力”的程序。听完...

    【手把手制作三阶魔方模拟器】用MATLAB绘制一个一阶魔方

    by 今天不飞了

    有一个酷爱魔方的朋友,托我给他定制一个专门用于“训练魔方观察和预判能力”的程序。听完需求之后觉得很有趣,就答应了,并决定把整个制作过程公开。

    今天不飞了,一起写代码吧!

    上次画了一个一阶魔方,这次就把它怼成三阶。话不多说,直接上代码。


    绘制三阶魔方每个块的中心

    以魔方中心轴为原点,魔方每个块尺寸为222单位, 则整个魔方尺寸为666,那么每个块的中心坐标易推导,我直接上代码,你们自个推导

    %% 块定义
    % 块八顶点定义(魔方每一个小块上的8个顶点的相对坐标)
    blockVertices = [ 1,-1,-1; 1, 1,-1;-1, 1,-1;-1,-1,-1;...
                      1,-1, 1; 1, 1, 1;-1, 1, 1;-1,-1, 1];
    % 块六面顶点索引(在上次的基础上优化了一下,用索引表示)
    blockFace{1} = [1,2,3,4];
    blockFace{2} = [1,2,6,5];
    blockFace{3} = [2,3,7,6];
    blockFace{4} = [3,4,8,7];
    blockFace{5} = [4,1,5,8];
    blockFace{6} = [5,6,7,8];
    
    %% 26块中心定义(先用半径1写,完事儿*2,简单快捷,以后想变大直接改)
    % 6轴
    axisPosList = [0, 0,-1; 1, 0, 0; 0, 1, 0; -1, 0, 0; 0,-1, 0; 0, 0, 1]*2;
    % 8角
    cornerPosList = [1,-1,-1; 1, 1,-1; -1, 1,-1; -1,-1,-1;...
        1,-1, 1; 1, 1, 1; -1, 1, 1; -1,-1, 1]*2;
    % 12棱
    edgePosList = [1, 0,-1; 0, 1,-1;-1, 0,-1; 0,-1,-1;...
        1, 1, 0;-1, 1, 0;-1,-1, 0; 1,-1, 0;
        1, 0, 1; 0, 1, 1;-1, 0, 1; 0,-1, 1]*2;
    
    %% 显示一下
    figure
    plot3(axisPosList(:,1),axisPosList(:,2),axisPosList(:,3),'K*'),hold on
    plot3(edgePosList(:,1),edgePosList(:,2),edgePosList(:,3),'bo')
    plot3(cornerPosList(:,1),cornerPosList(:,2),cornerPosList(:,3),'r<')
    axis equal
    axis([-1,1,-1,1,-1,1]*4)
    grid on
    view([2,1,1])
    

    结果如下,静止图不明显,自己运行的时候旋转一下,没毛病

    在这里插入图片描述


    为每个块六面上色

    配色与编号
    在这里插入图片描述

    各块编号如下图

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    %% 颜色与不透明度  白 蓝 红 绿 橙 黄
    color = {[1,1,1],[0,0.2,0.8],[0.8,0,0],[0,0.8,0.2],...
    [1,0.4,0],[1,1,0],[0.1,0.1,0.1]};
    colorAlpha = [1,1,1,1,1,1,0.3];
    
    %% 266面上色
    % 轴块属性初始化
    axisBlock = cell(6,1);
    for n = 1:6
        axisBlock{n}.position = axisPosList(n,:);
        axisBlock{n}.color = ones(1,6)*7;
        axisBlock{n}.color(n) = n;
    end
    
    % 角块属性初始化
    cornerBlock = cell(8,1);
    for n = 1:8
        cornerBlock{n}.position = cornerPosList(n,:);
        cornerBlock{n}.color = ones(1,6)*7;
    end
    cornerBlock{1}.color([1,2,5]) = [1,2,5];
    cornerBlock{2}.color([1,2,3]) = [1,2,3];
    cornerBlock{3}.color([1,3,4]) = [1,3,4];
    cornerBlock{4}.color([1,4,5]) = [1,4,5];
    cornerBlock{5}.color([6,2,5]) = [6,2,5];
    cornerBlock{6}.color([6,2,3]) = [6,2,3];
    cornerBlock{7}.color([6,3,4]) = [6,3,4];
    cornerBlock{8}.color([6,4,5]) = [6,4,5];
    
    % 棱块属性初始化
    edgeBlock = cell(12,1);
    for n = 1:12
        edgeBlock{n}.position = edgePosList(n,:);
        edgeBlock{n}.color = ones(1,6)*7;
    end
    edgeBlock{1}.color([1,2]) = [1,2];
    edgeBlock{2}.color([1,3]) = [1,3];
    edgeBlock{3}.color([1,4]) = [1,4];
    edgeBlock{4}.color([1,5]) = [1,5];
    edgeBlock{5}.color([2,3]) = [2,3];
    edgeBlock{6}.color([3,4]) = [3,4];
    edgeBlock{7}.color([4,5]) = [4,5];
    edgeBlock{8}.color([5,2]) = [5,2];
    edgeBlock{9}.color([6,2]) = [6,2];
    edgeBlock{10}.color([6,3]) = [6,3];
    edgeBlock{11}.color([6,4]) = [6,4];
    edgeBlock{12}.color([6,5]) = [6,5];
    
    %% 显示一下(利用循环全部绘制)
    %% 显示
    
    % 轴块
    for n = 1:6    
        for f = 1:6
    	    V = axisBlock{n}.position+blockVertices(blockFace{f},:);
    	    C = color{axisBlock{n}.color(f)};
    	    A = colorAlpha(axisBlock{n}.color(f));
    	    h = patch('Faces',[1 2 3 4],'Vertices',V,'FaceColor',C,...
    	        'FaceAlpha',A);
        end
    end
    %for n = 1:12    
        for f = 1:6
    	    V = edgeBlock{n}.position+blockVertices(blockFace{f},:);
    	    C = color{edgeBlock{n}.color(f)};
    	    A = colorAlpha(edgeBlock{n}.color(f));
    	    h = patch('Faces',[1 2 3 4],'Vertices',V,'FaceColor',C,...
    	        'FaceAlpha',A);
        end
    end
    % 角块
    for n = 1:8    
        for f = 1:6        
    	    V = cornerBlock{n}.position+blockVertices(blockFace{f},:);
    	    C = color{cornerBlock{n}.color(f)};
    	    A = colorAlpha(cornerBlock{n}.color(f));
    	    h = patch('Faces',[1 2 3 4],'Vertices',V,'FaceColor',C,...
    	        'FaceAlpha',A);
        end
    end
    

    直接这么绘制,太紧了,受不了。 所以我们把这个小块半径压缩压缩,留点缝,如下

    % 块八顶点定义(魔方每一个小块上的8个顶点的相对坐标)
    blockVertices = [ 1,-1,-1; 1, 1,-1;-1, 1,-1;-1,-1,-1;...
                      1,-1, 1; 1, 1, 1;-1, 1, 1;-1,-1, 1]*0.95; % *0.95 
    

    上效果图

    在这里插入图片描述

    # 其他

    1. 持续更新
    2. B站录播视频:【三阶魔方】手把手从零实现,魔方观察训练程序(一)我们来画个魔方
    展开全文
  • 魔方模拟器 软件

    2013-03-24 22:35:38
    3D模拟用于新手练习魔方,高手研究魔方必备工具。
  • 有一个酷爱魔方的朋友,托我给他定制一个专门用于“训练魔方观察和预判能力”的程序。听完需求之后觉得很有趣,就答应了,并决定把整个制作过程公开。上次已经画好三阶魔方,是时候让它动起来了。话不多说,直接上...

    by 今天不飞了

    有一个酷爱魔方的朋友,托我给他定制一个专门用于“训练魔方观察和预判能力”的程序。听完需求之后觉得很有趣,就答应了,并决定把整个制作过程公开。

    今天不飞了,一起写代码吧!

    上次已经画好三阶魔方,是时候让它动起来了。话不多说,直接上代码。


    1 定义魔方初始状态

    function [axisBlock,cornerBlock, edgeBlock] = init(axisPosList,cornerPosList,edgePosList)
    
    % 轴块属性初始化
    axisBlock = cell(6,1);
    for n = 1:6
        axisBlock{n}.RotateMatrix = [1,0,0;0,1,0;0,0,1];
        axisBlock{n}.position = axisPosList(n,:);
        axisBlock{n}.color = ones(1,6)*7;
        axisBlock{n}.color(n) = n;
    end
    % 角块属性初始化
    cornerBlock = cell(8,1);
    for n = 1:8
        cornerBlock{n}.RotateMatrix = [1,0,0;0,1,0;0,0,1];
        cornerBlock{n}.position = cornerPosList(n,:);
        cornerBlock{n}.color = ones(1,6)*7;
    end
    cornerBlock{1}.color([1,2,5]) = [1,2,5];
    cornerBlock{2}.color([1,2,3]) = [1,2,3];
    cornerBlock{3}.color([1,3,4]) = [1,3,4];
    cornerBlock{4}.color([1,4,5]) = [1,4,5];
    cornerBlock{5}.color([6,2,5]) = [6,2,5];
    cornerBlock{6}.color([6,2,3]) = [6,2,3];
    cornerBlock{7}.color([6,3,4]) = [6,3,4];
    cornerBlock{8}.color([6,4,5]) = [6,4,5];
    % 棱块属性初始化
    edgeBlock = cell(12,1);
    for n = 1:12
        edgeBlock{n}.RotateMatrix = [1,0,0;0,1,0;0,0,1];
        edgeBlock{n}.position = edgePosList(n,:);
        edgeBlock{n}.color = ones(1,6)*7;
    end
    edgeBlock{1}.color([1,2]) = [1,2];
    edgeBlock{2}.color([1,3]) = [1,3];
    edgeBlock{3}.color([1,4]) = [1,4];
    edgeBlock{4}.color([1,5]) = [1,5];
    edgeBlock{5}.color([2,3]) = [2,3];
    edgeBlock{6}.color([3,4]) = [3,4];
    edgeBlock{7}.color([4,5]) = [4,5];
    edgeBlock{8}.color([5,2]) = [5,2];
    edgeBlock{9}.color([6,2]) = [6,2];
    edgeBlock{10}.color([6,3]) = [6,3];
    edgeBlock{11}.color([6,4]) = [6,4];
    edgeBlock{12}.color([6,5]) = [6,5];
    
    end
    

    2 操作模块

    2.1 全操作定义

    就是所谓的“U U’ R R’ f r M x” 等等,直接上成品,对推导过程感兴趣的,可以上B站看录播视频(见文末)

    function [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(opt, ...
        blockIdx, axisBlock, cornerBlock, edgeBlock)
    
    switch opt
        
        case 1 % F
            R = getRotateMatrix(1,0,0);
            
            for n = blockIdx.cornerIdx([1,2,6,5])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([1,5,9,8])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([1,2,6,5]) = blockIdx.cornerIdx([2,6,5,1]) ;
            blockIdx.edgeIdx([1,5,9,8]) = blockIdx.edgeIdx([5,9,8,1]);
            
        case -1 % F'
            R = getRotateMatrix(-1,0,0);
            
            for n = blockIdx.cornerIdx([1,2,6,5])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([1,5,9,8])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([1,2,6,5]) = blockIdx.cornerIdx([5,1,2,6]) ;
            blockIdx.edgeIdx([1,5,9,8]) = blockIdx.edgeIdx([8,1,5,9]);
            
        case 2 % R
            R = getRotateMatrix(0,1,0);
            
            for n = blockIdx.cornerIdx([2,3,7,6])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([2,6,10,5])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([2,3,7,6]) = blockIdx.cornerIdx([3,7,6,2]) ;
            blockIdx.edgeIdx([2,6,10,5]) = blockIdx.edgeIdx([6,10,5,2]);
            
        case -2 % R'
            R = getRotateMatrix(0,-1,0);
            
            for n = blockIdx.cornerIdx([2,3,7,6])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([2,6,10,5])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([2,3,7,6]) = blockIdx.cornerIdx([6,2,3,7]) ;
            blockIdx.edgeIdx([2,6,10,5]) = blockIdx.edgeIdx([5,2,6,10]);
            
        case 3 % U
            R = getRotateMatrix(0,0,1);
            
            for n = blockIdx.cornerIdx([5,6,7,8])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([9,10,11,12])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([5,6,7,8]) = blockIdx.cornerIdx([6,7,8,5]) ;
            blockIdx.edgeIdx([9,10,11,12]) = blockIdx.edgeIdx([10,11,12,9]);
            
        case -3 % U'
            R = getRotateMatrix(0,0,-1);
            
            for n = blockIdx.cornerIdx([5,6,7,8])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([9,10,11,12])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([5,6,7,8]) = blockIdx.cornerIdx([8,5,6,7]) ;
            blockIdx.edgeIdx([9,10,11,12]) = blockIdx.edgeIdx([12,9,10,11]);
        case 4 % B
            R = getRotateMatrix(-1,0,0);
            
            for n = blockIdx.cornerIdx([3,4,8,7])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([3,7,11,6])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([3,4,8,7]) = blockIdx.cornerIdx([4,8,7,3]) ;
            blockIdx.edgeIdx([3,7,11,6]) = blockIdx.edgeIdx([7,11,6,3]);
            
        case -4 % B'
            R = getRotateMatrix(1,0,0);
            
            for n = blockIdx.cornerIdx([3,4,8,7])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([3,7,11,6])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([3,4,8,7]) = blockIdx.cornerIdx([7,3,4,8]) ;
            blockIdx.edgeIdx([3,7,11,6]) = blockIdx.edgeIdx([6,3,7,11]);
            
        case 5 % L
            R = getRotateMatrix(0,-1,0);
            
            for n = blockIdx.cornerIdx([1,5,8,4])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([4,8,12,7])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([1,5,8,4]) = blockIdx.cornerIdx([5,8,4,1]) ;
            blockIdx.edgeIdx([4,8,12,7]) = blockIdx.edgeIdx([8,12,7,4]);
            
        case -5 % L'
            R = getRotateMatrix(0,1,0);
            
            for n = blockIdx.cornerIdx([1,5,8,4])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([4,8,12,7])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([1,5,8,4]) = blockIdx.cornerIdx([4,1,5,8]) ;
            blockIdx.edgeIdx([4,8,12,7]) = blockIdx.edgeIdx([7,4,8,12]);
            
            
            
        case 6 % D
            R = getRotateMatrix(0,0,-1);
            
            for n = blockIdx.cornerIdx([1,2,3,4])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([1,2,3,4])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([1,2,3,4]) = blockIdx.cornerIdx([4,1,2,3]) ;
            blockIdx.edgeIdx([1,2,3,4]) = blockIdx.edgeIdx([4,1,2,3]);
            
        case -6 % D'
            R = getRotateMatrix(0,0,1);
            
            for n = blockIdx.cornerIdx([1,2,3,4])
                cornerBlock{n}.RotateMatrix = cornerBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([1,2,3,4])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.cornerIdx([1,2,3,4]) = blockIdx.cornerIdx([2,3,4,1]) ;
            blockIdx.edgeIdx([1,2,3,4]) = blockIdx.edgeIdx([2,3,4,1]);
            
        case 13 % S
            R = getRotateMatrix(1,0,0);
            
            for n = blockIdx.axisIdx([1,3,6,5])
                axisBlock{n}.RotateMatrix = axisBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([2,10,12,4])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.axisIdx([1,3,6,5]) = blockIdx.axisIdx([3,6,5,1]) ;
            blockIdx.edgeIdx([2,10,12,4]) = blockIdx.edgeIdx([10,12,4,2]);
        case -13 % S'
            R = getRotateMatrix(-1,0,0);
            
            for n = blockIdx.axisIdx([1,3,6,5])
                axisBlock{n}.RotateMatrix = axisBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([2,10,12,4])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.axisIdx([1,3,6,5]) = blockIdx.axisIdx([5,1,3,6]) ;
            blockIdx.edgeIdx([2,10,12,4]) = blockIdx.edgeIdx([4,2,10,12]);
            
        case 14 % M
            R = getRotateMatrix(0,-1,0);
            
            for n = blockIdx.axisIdx([1,4,6,2])
                axisBlock{n}.RotateMatrix = axisBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([1,3,11,9])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.axisIdx([1,4,6,2]) = blockIdx.axisIdx([2,1,4,6]) ;
            blockIdx.edgeIdx([1,3,11,9]) = blockIdx.edgeIdx([9,1,3,11]);
        case -14 % M'
            R = getRotateMatrix(0,1,0);
            
            for n = blockIdx.axisIdx([1,4,6,2])
                axisBlock{n}.RotateMatrix = axisBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([1,3,11,9])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.axisIdx([1,4,6,2]) = blockIdx.axisIdx([4,6,2,1]) ;
            blockIdx.edgeIdx([1,3,11,9]) = blockIdx.edgeIdx([3,11,9,1]);
            
        case 15 % E
            R = getRotateMatrix(0,0,-1);
            
            for n = blockIdx.axisIdx([2,3,4,5])
                axisBlock{n}.RotateMatrix = axisBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([5,6,7,8])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.axisIdx([2,3,4,5]) = blockIdx.axisIdx([5,2,3,4]) ;
            blockIdx.edgeIdx([5,6,7,8]) = blockIdx.edgeIdx([8,5,6,7]);
        case -15 % E'
            R = getRotateMatrix(0,0,1);
            
            for n = blockIdx.axisIdx([2,3,4,5])
                axisBlock{n}.RotateMatrix = axisBlock{n}.RotateMatrix*R;
            end
            for n = blockIdx.edgeIdx([5,6,7,8])
                edgeBlock{n}.RotateMatrix = edgeBlock{n}.RotateMatrix*R;
            end
            blockIdx.axisIdx([2,3,4,5]) = blockIdx.axisIdx([3,4,5,2]) ;
            blockIdx.edgeIdx([5,6,7,8]) = blockIdx.edgeIdx([6,7,8,5]);
            
        case 7 % f
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(1, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % F
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(13, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % S
            
        case -7 % f'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-1, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % F'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-13, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % S'
            
        case 8 % r
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(2, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % R
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-14, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % M'
            
        case -8 % r'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-2, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % R'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(14, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % M
        case 9 % u
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(3, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % U
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-15, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % E'
            
        case -9 % u'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-3, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % U'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(15, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % E
            
        case 10 % b
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-4, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % B
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-13, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % S'
            
        case -10 % b'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-4, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % B'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(13, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % S
            
        case 11 % l
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(5, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % L
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(14, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % M
            
        case -11 % l'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-5, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % L'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-14, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % M'
        case 12 % d
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(6, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % D
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(15, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % E
            
        case -12 % d'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-6, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % D'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-15, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % E'
            
        case 16 % x (y)
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(8, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % r
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-5, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % L'
            
        case -16 % x'(y')
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-8, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % r'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(5, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % L
            
        case 17 % y (z)
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(9, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % u
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-6, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % D'
            
        case -17 % y'(z')
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-9, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % u'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(6, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % D
            
        case 18 % z (x)
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(7, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % f
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-4, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % B'
            
        case -18 % z'(x')
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(-7, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % f'
            [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(4, ...
                blockIdx, axisBlock, cornerBlock, edgeBlock); % B
            
    end
    
    end
    

    2.2 旋转公式小函数

    
    function R = getRotateMatrix(alpha,beta,gamma)
    % alpha,beta,gamma 为1-1,分别表示顺时针转和逆时针转
    
    alpha = -pi/2*alpha;
    beta = -pi/2*beta;
    gamma = -pi/2*gamma;
    
    Rx = [1,0,0;0,cos(alpha),-sin(alpha);0,sin(alpha),cos(alpha)];
    Ry = [cos(beta),0,sin(beta);0,1,0;-sin(beta),0,cos(beta)];
    Rz = [cos(gamma),-sin(gamma),0;sin(gamma),cos(gamma),0;0,0,1];
    R = (Rx*Ry*Rz)';
    
    end

    2.3操作识别

    咱们直接用魔方手法代号,让它自动转为上面的编号把
    注:由于字符串里面不能再写 这个符号,所以我们用代替。比如RUR’U’ ,代码里用RUR,U,

    function [optIdx,optNum] = getOptIdx(optList)
    
    optListNum = length(optList);
    optNum = 0;
    optIdx = zeros(optListNum,1);
    for k = 1:optListNum
        tmp = strfind('FRUBLDfrubldSMExyz',optList(k));
        if ~isempty(tmp)
            optNum = optNum+1;
            optIdx(optNum) = tmp;
        else
            if optNum >0
                optIdx(optNum) = -optIdx(optNum);
            end
        end
    end
    optIdx(optNum+1:end) = [];
    
    end

    3 显示模块

    3.1 初始化

    画一个未打乱的魔方

    function H = initshowcube(axisBlock, cornerBlock, edgeBlock, ...
        blockVertices, blockFace, color, colorAlpha)
    
    % 轴块
    for n = 1:6
        for f = 1:6
            V = (axisBlock{n}.position+blockVertices(blockFace{f},:))*axisBlock{n}.RotateMatrix;
            C = color{axisBlock{n}.color(f)};
            A = colorAlpha(axisBlock{n}.color(f));
            H.axis{n}{f} = patch('Faces',[1 2 3 4],'Vertices',V,'FaceColor',C,...
                'FaceAlpha',A);
        end
    end
    
    
    %for n = 1:12
        for f = 1:6
            V = (edgeBlock{n}.position+blockVertices(blockFace{f},:))*edgeBlock{n}.RotateMatrix;
            C = color{edgeBlock{n}.color(f)};
            A = colorAlpha(edgeBlock{n}.color(f));
            H.edge{n}{f} = patch('Faces',[1 2 3 4],'Vertices',V,'FaceColor',C,...
                'FaceAlpha',A);
        end
    end
    
    % 角块
    for n = 1:8
        for f = 1:6
            
            V = (cornerBlock{n}.position+blockVertices(blockFace{f},:))*cornerBlock{n}.RotateMatrix;
            C = color{cornerBlock{n}.color(f)};
            A = colorAlpha(cornerBlock{n}.color(f));
            H.corner{n}{f} = patch('Faces',[1 2 3 4],'Vertices',V,'FaceColor',C,...
                'FaceAlpha',A);
        end
    end
    
    end

    3.2 显示操作后的魔方

    function showcube(H, axisBlock, cornerBlock, edgeBlock, ...
        blockVertices, blockFace, color, colorAlpha)
    
    % 轴块
    for n = 1:6
        for f = 1:6
            V = (axisBlock{n}.position+blockVertices(blockFace{f},:))*axisBlock{n}.RotateMatrix;
            C = color{axisBlock{n}.color(f)};
            A = colorAlpha(axisBlock{n}.color(f));
            H.axis{n}{f}.Vertices = V;
            H.axis{n}{f}.FaceColor = C;
            H.axis{n}{f}.FaceAlpha = A;
        end
    end
    
    %for n = 1:12
        for f = 1:6
            V = (edgeBlock{n}.position+blockVertices(blockFace{f},:))*edgeBlock{n}.RotateMatrix;
            C = color{edgeBlock{n}.color(f)};
            A = colorAlpha(edgeBlock{n}.color(f));
            H.edge{n}{f}.Vertices = V;
            H.edge{n}{f}.FaceColor = C;
            H.edge{n}{f}.FaceAlpha = A;
        end
    end
    
    % 角块
    for n = 1:8
        for f = 1:6
            V = (cornerBlock{n}.position+blockVertices(blockFace{f},:))*cornerBlock{n}.RotateMatrix;
            C = color{cornerBlock{n}.color(f)};
            A = colorAlpha(cornerBlock{n}.color(f));
            H.corner{n}{f}.Vertices = V;
            H.corner{n}{f}.FaceColor = C;
            H.corner{n}{f}.FaceAlpha = A;
        end
    end
    
    end

    4 测试

    简单测试

    clear; close all; clc
    
    %% 基本定义(魔方几何信息) 尺寸颜色透明度之类的可以根据需要自己修改
    blockVertices = [ 1,-1,-1; 1, 1,-1;-1, 1,-1;-1,-1,-1;...
        1,-1, 1; 1, 1, 1;-1, 1, 1;-1,-1, 1]*0.95;
    blockFace{1} = [1,2,3,4];
    blockFace{2} = [1,2,6,5];
    blockFace{3} = [2,3,7,6];
    blockFace{4} = [3,4,8,7];
    blockFace{5} = [4,1,5,8];
    blockFace{6} = [5,6,7,8];
    
    % 6轴
    axisPosList = [0, 0,-1; 1, 0, 0; 0, 1, 0; -1, 0, 0; 0,-1, 0; 0, 0, 1]*2;
    % 8角
    cornerPosList = [1,-1,-1; 1, 1,-1; -1, 1,-1; -1,-1,-1;...
        1,-1, 1; 1, 1, 1; -1, 1, 1; -1,-1, 1]*2;
    % 12棱
    edgePosList = [1, 0,-1; 0, 1,-1;-1, 0,-1; 0,-1,-1;...
        1, 1, 0;-1, 1, 0;-1,-1, 0; 1,-1, 0;
        1, 0, 1; 0, 1, 1;-1, 0, 1; 0,-1, 1]*2;
    
    % 颜色定义 白 蓝 红 绿 橙 黄
    color = {[1,1,1],[0,0.2,0.8],[0.8,0,0],[0,0.8,0.2],[1,0.4,0],[1,1,0],[0.1,0.1,0.1]};
    colorAlpha = [1,1,1,1,1,1,0.9];
    
    % 初始槽位
    blockIdx.axisIdx = [1,2,3,4,5,6];
    blockIdx.cornerIdx = [1,2,3,4,5,6,7,8];
    blockIdx.edgeIdx = [1,2,3,4,5,6,7,8,9,10,11,12];
    
    %% 初始化
    [axisBlock,cornerBlock,edgeBlock] = init(axisPosList,cornerPosList,edgePosList);
    
    % 显示
    figure('Position',[100,100,600,700])
    axis off
    axis equal
    axis([-1,1,-1,1,-1,1]*4)
    xlabel('x')
    ylabel('y')
    zlabel('z')
    view([5,2,2])
    
    H = initshowcube(axisBlock, cornerBlock, edgeBlock, ...
        blockVertices, blockFace, color, colorAlpha);
    
    %% 操作
    % 设置操作
    optList = 'RUUR,U,RUR,U,RU,R,'; 
    % 转为编号
    [optIdx,optNum] = getOptIdx(optList);
    % 循环转动
    for k = 1:optNum
    	% 执行
        [blockIdx, axisBlock, cornerBlock, edgeBlock] = operation(optIdx(k), blockIdx, axisBlock, cornerBlock, edgeBlock);
        % 显示
        showcube(H, axisBlock, cornerBlock, edgeBlock, ...
            blockVertices, blockFace, color, colorAlpha)
        pause(0.1)
    end
    

    效果图

    在这里插入图片描述
    大家肯定发现了,这样实现的话缺少“动感”,给人感觉就是颜色在变,没有转动的感觉。
    没错,下一期就一起来实现“动态旋转”。

    其他

    1. 程序有bug,或有更好的提议,欢迎留言告诉我
    2. 代码录播视频见B站【三阶魔方】手把手从零实现,魔方观察训练程序(二)让魔方动起来
    展开全文
  • 点击上方“蓝字”,一起来涨知识吧~冬瓜魔方计时器电脑端使用教程“魔域杯”2020HCUCU夏季线上魔方联赛马上就要到来了,各位同学们都准备的怎么样了?本次比赛是联盟首次在线上办赛,使用微信小程序中的冬瓜魔方计时...
    点击上方“蓝字”,一起来涨知识吧~0c2aa794c97e2515ab472d5ec2a9a4b6.gif

    冬瓜魔方计时器电脑端使用教程

    0c2aa794c97e2515ab472d5ec2a9a4b6.gif

    “魔域杯”2020HCUCU夏季线上魔方联赛马上就要到来了,各位同学们都准备的怎么样了?

    本次比赛是联盟首次在线上办赛,使用微信小程序中的冬瓜魔方计时器进行比赛。

    由于比赛规则要求选手的比赛过程需要全程录像,这可难倒了家人都上班,独自一人在家比赛的魔友了(比如笔者)。

    (有多台手机的壕魔友可以忽略)

    由于冬瓜魔方计时器小程序因为某种未知原因,所以电脑端的PC版以及Win10版都不能正常地打开小程序,尤其是“线上比赛”界面无法进入。

    下面笔者搜集了多方的资源,并自身测试正确性之后,就与大家及时来分享用电脑如何正常进入冬瓜魔方计时器程序的方法了。

    第一步:下载雷电模拟器

    雷电模拟器是一款在电脑上模拟安卓系统环境的软件。

    下载链接:https://www.ldmnq.com/

    点击以上链接,下载雷电模拟器(4.0.26版本)。

    b71f1157d3f6793c0fbe915bac3f03ca.png

    第二步:安装并打开雷电模拟器

    在搜索栏搜索“微信”,将其下载后打开微信并登录。

    13894bd06d1fe9c9b6cbac89f6b19736.png

    第三步:打开河南高校魔方联盟微信公众号

    点击赛事活动目录→线上比赛。

    163ee30abd1154efcf48099139f3775e.png

    第四步:进入冬瓜魔方计时器小程序

    点击“魔域杯”2020HCUCU夏季线上魔方联赛,进入公众号文章。

    4a928090aba42fba4697c32e4655a33a.png

    点击小程序链接,进入冬瓜魔方计时器小程序界面

    3a5b9f2b7466d9077dfe794b1fa0d10a.png

    这时候我们就已经成功打开冬瓜魔方计时器

    第五步:开始激烈的魔方比赛/练习吧!

    点击线上比赛,输入赛事群公布的房间号即可参与比赛。

    2782972eac0feb3e6c640db728eff484.png

    至此电脑端的比赛流程已经介绍完毕,众所周知部分魔友不习惯鼠标的操作方式,微信小程序在电脑上只能用鼠标操作,对喜欢用电脑的计时的同学较不友好。下面我们介绍一款小工具,使电脑端的冬瓜魔方计时器能像CSTimer一样方便快捷地,用空格来开始计时与结束计时。

    感谢魔友@三维坐标系提供了一种用空格操作冬瓜魔方计时器的方法,如下图所示,是不是非常简(ma)单(fan)?如果有更好的方法欢迎评论区留言。

    (喜欢鼠标操作计时器的魔友请忽略以下教程)

    第六步:下载SharpKeys小工具

    SharpKeys是一款在GitHub平台上的鼠标模拟器(用键盘控制鼠标)。

    下载链接:                        https://github.com/randyrants/sharpkeys/releases/tag/v3.9/

    点击以上链接,下载SharpKeys

    76022b2da663531820ee951ba5675b8f.png

    解压压缩文件后,点击上图打开SharpKeys。

    第七步:打开并设置SharpKeys

    c0525dbaa765496b6b6f1e53300e8390.png

    点击窗口左下角的Add

    2fa4c77d8f60d914890e03fb5305b579.png

    左边From选项选择Special:Space(00_39)

    右边To选项选择Num:5(00_4C),点击右下角OK关闭界面。

    点击退出后选择是以保存设置。

    第八步:启用鼠标键

    f739c13a24f8af54c18392eeebf08ab1.png

    按左Shift+左Alt+NumLock,启用鼠标键。如上图,点击“是”。

    第九步:打开冬瓜魔方计时器开始比赛/练习

    3e36b7ebfbbdd971ee7867c6f103447b.png

    打开冬瓜魔方计时器,切记一定要把鼠标放在计时器的操作区(计时器显示数字周围),然后按空格就可以开始计时以及停止计时了。

                  文案 | 钱治宇、张业鑫 

    图片 | 钱治宇

    编辑 | 王俊清

    审核 | 钱治宇

    3f3d8343cc69b6cf1fc233ae3d2653cb.png

    河南高校魔方联盟

    微信号:HCUCU520

    QQ官方号:3364537308

    展开全文
  • 2-20阶魔方模拟器

    2011-04-08 16:00:35
    能模拟2阶到20阶魔方操作就不用讲了,画面是很好
  • 魔方是个结构简单而变化无穷的神奇玩具。那么如何在万能的浏览器里模拟出魔方的无尽变换,又如何将其还原呢?下面让我们一步步地来一探究竟吧。 魔方的抽象 拆解过魔方的同学可能知道,现实中魔方的内部结构包含了...

    魔方是个结构简单而变化无穷的神奇玩具。那么如何在万能的浏览器里模拟出魔方的无尽变换,又如何将其还原呢?下面让我们一步步地来一探究竟吧。

    魔方的抽象

    拆解过魔方的同学可能知道,现实中魔方的内部结构包含了中轴、弹簧、螺丝等机械装置。但当我们只是想要「模拟」它的时候,我们只需抓住它最显著的性质即可——3x3x3 的一组立方体:

    基本概念

    上图演示了魔方最基本的思维模型。但光有这样的感性认识还不够:组成魔方的每个块并非随意安置,它们之间有着细微的区别:

    • 位于魔方各角的块称为角块,每个角块均具有 3 个颜色。一个立方体有 8 个角,故而一个魔方也具有 8 个角块。
    • 位于魔方各棱上的块称为棱块,每个棱块均具有 2 个颜色。一个立方体有 12 条棱,故而一个魔方也具有 12 个棱块。
    • 位于魔方各面中心的块称为中心块,每个中心块仅有 1 个颜色。一个立方体有 6 个面,故而一个魔方也具有 6 个中心块。
    • 位于整个魔方中心的块没有颜色,在渲染和还原的过程中也不起到什么实际的用处,我们可以忽略这个块。

    将以上四种块的数量相加,正好是 3^3 = 27 块。对这些块,你所能使用的唯一操作(或者说变换)方式,就是在不同面上的旋转。那么,我们该如何标识出一次旋转操作呢?

    设想你的手里「端正地」拿着一个魔方,我们将此时面对你的那一面定义为 Front,背对的一面定义为 Back。类似地,我们有了 Left / Right / Upper / Down 来标识其余各面。当你旋转某一面时,我们用这一面的简写(F / B / L / R / U / D)来标识在这一面上的一次顺时针 90 度旋转。对于一次逆时针的旋转,我们则用 F' / U' 这样带 ' 的记号来表达。如果你旋转了 180 度,那么可以用形如 R2 / U2 的方式表示。如下图的 5 次操作,如果我们约定蓝色一面为 Front,其旋转序列就是 F' R' L' B' F'

    关于魔方的基础结构和变换方式,知道这些就足够了。下面我们需要考虑这个问题:如何设计一个数据结构来保存的魔方状态,并使用编程语言来实现某个旋转变换呢?

    数据结构

    喜欢基于「面向对象」抽象的同学可能很快就能想到,我们可以为每个块设计一个 Block 基类,然后用形如 CornerBlockEdgeBlock 的类来抽象棱块和角块,在每个角块实例中还可以保存这个角块到它相邻三个棱块的引用……这样一个魔方的 Cube 对象只需持有对中心块的引用,就可以基于各块实例的邻接属性保存整个魔方了。

    上面这种实现很类似于链表,它可以 O(1) 地实现「给定某个块,查找其邻接块」的操作,但不难发现,它需要 O(N) 的复杂度来实现形如「某个位置的块在哪里」这样的查找操作,基于它的旋转操作也并不十分符合直觉。相对地,另一种显得「过于暴力」的方式反而相当实用:直接开辟一个长度为 27 的数组,在其中存储每一块的颜色信息即可。

    为什么可以这样呢?我们知道,数组在基于下标访问时,具有 O(1) 的时间复杂度。而如果我们在一个三维坐标系中定位魔方的每一个块,那么每个块的空间坐标都可以唯一地映射到数组的下标上。更进一步地,我们可以令 x, y, z 分别取 -1, 0, 1 这三个值来表达一个块在其方向上可能的位置,这时,例如前面所定义的一次 U 旋转,刚好就是对所有 y 轴坐标值为 1 的块的旋转。这个良好的性质很有利于实现对魔方的变换操作。

    旋转变换

    在约定好数据结构之后,我们如何实现对魔方的一次旋转变换呢?可能有些同学会直接将这个操作与三维空间中的四阶变换矩阵联系起来。但只要注意到一次旋转的角度都是 90 度的整数倍,我们可以利用数学性质极大地简化这一操作:

    在旋转 90 度时,旋转面上每个角块都旋转到了该面上的「下一个」角块的位置上,棱块也是这样。故而,我们只需要循环交替地在每个块的「下一个」位置赋值,就能轻松地将块「移动」到其新位置上。但这还不够:每个新位置上的块,还需要对其自身六个面的颜色做一次「自旋」,才能将它的朝向指向正确的位置。这也是一次交替的赋值操作。从而,一次三维空间绕某个面中心的旋转操作,就被我们分解为了一次平移操作和一次绕各块中心的旋转操作。只需要 30 余行代码,我们就能实现这一魔方最核心的变换机制:

    rotate (center, clockwise = true) {
      const axis = center.indexOf(1) + center.indexOf(-1) + 1
      // Fix y direction in right-handed coordinate system.
      clockwise = center[1] !== 0 ? !clockwise : clockwise
      // Fix directions whose faces are opposite to axis.
      clockwise = center[axis] === 1 ? clockwise : !clockwise
    
      let cs = [[1, 1], [1, -1], [-1, -1], [-1, 1]] // corner coords
      let es = [[0, 1], [1, 0], [0, -1], [-1, 0]] // edge coords
      const prepareCoord = coord => coord.splice(axis, 0, center[axis])
      cs.forEach(prepareCoord); es.forEach(prepareCoord)
      if (!clockwise) { cs = cs.reverse(); es = es.reverse() }
    
      // 移动每个块到其新位置
      const rotateBlocks = ([a, b, c, d]) => {
        const set = (a, b) => { for (let i = 0; i < 6; i++) a[i] = b[i] }
        const tmp = []; set(tmp, a); set(a, d); set(d, c); set(c, b); set(b, tmp)
      }
      const colorsAt = coord => this.getBlock(coord).colors
      rotateBlocks(cs.map(colorsAt)); rotateBlocks(es.map(colorsAt))
    
      // 调整每个块的自旋朝向
      const swap = [
        [[F, U, B, D], [L, F, R, B], [L, U, R, D]],
        [[F, D, B, U], [F, L, B, R], [D, R, U, L]]
      ][clockwise ? 0 : 1][axis]
      const rotateFaces = coord => {
        const block = colorsAt(coord)
        ;[block[swap[1]], block[swap[2]], block[swap[3]], block[swap[0]]] =
        [block[swap[0]], block[swap[1]], block[swap[2]], block[swap[3]]]
      }
      cs.forEach(rotateFaces); es.forEach(rotateFaces)
      return this
    }
    复制代码

    这个实现的效率应该不差:在笔者的浏览器里,上面的代码可以支持每秒 30 万次的旋转变换。为什么在这里我们需要在意性能呢?在魔方的场景下,有一个非常不同的地方,即状态的有效性与校验

    熟悉魔方的同学应该知道,并不是随便给每块涂上不同颜色的魔方都是可以还原的。在普通的业务开发领域,数据的有效性和校验常常可以通过类型系统来保证。但对于一个打乱的魔方,保证它的可解性则是一个困难的数学问题。故而我们在保存魔方状态时,只有保存从六面同色的初始状态到当前状态下的所有变换步骤,才能保证这个状态一定是可解的。这样一来,反序列化一个魔方状态的开销就与操作步骤数量之间有了 O(N) 的关联。好在一个实际把玩中的魔方状态一般只会在 100 步之内,故而上面以牺牲时间复杂度换取数据有效性的代价应当是值得的。另外,这个方式可以非常简单地实现魔方任意状态之间的时间旅行:从初始状态走到任意一步的历史状态,都只需要叠加上它们之间一系列的旋转 diff 操作即可。这是一个很可靠的思维模型。

    上面的实现中有一个特别之处:当坐标轴是 y 轴时,我们为旋转方向进行了一次取反操作。这初看起来并不符合直觉,但其背后却是坐标系定义的问题:如果你推导过每个块在顺时针变换时所处的下一个位置,那么在高中教科书和 WebGL 所用的右手坐标系中,绕 y 轴旋转时各个块的下一个位置,其交换顺序与 x 轴和 z 轴是相反的。反而在 DirectX 的左手坐标系中,旋转操作的正负能完全和坐标系的朝向一致。笔者作为区区码农,并不了解这背后的对称性是否蕴含了什么深刻的数学原理,希望数学大佬们解惑。

    到此为止,我们已经基本完成了对魔方状态的抽象和变换算法的设计了。但相信很多同学可能更好奇的是这个问题:在浏览器环境下,我们该如何渲染出魔方呢?让我们来看看吧。

    魔方的渲染

    在浏览器这个以无数的二维矩形作为排版原语的世界里,要想渲染魔方这样的三维物体并不是件查个文档写几行胶水代码就可以搞定的事情。好在我们有 WebGL 这样的三维图形库可供差遣(当然了,相信熟悉样式的同学应该是可以使用 CSS 来渲染魔方的,可惜笔者的 CSS 水平很菜)。

    WebGL 渲染基础

    由于魔方思维模型的简单性,要渲染它并不需要使用图形学中纹理、光照和阴影等高级特性,只需要最基本的几何图形绘制特性就足够了。正因为如此,笔者在这里只使用了完全原生的 WebGL API 来绘制魔方。笼统地说,渲染魔方这样的一组立方体,所需要的步骤大致如下:

    1. 初始化着色器(编译供 GPU 执行的程序)
    2. 向缓冲区中传递顶点和颜色数据(操作显存)
    3. 设置用于观察的透视矩阵和模-视变换矩阵(传递变量给 GPU)
    4. 调用 drawElementsdrawArray 渲染一帧

    在前文中,我们设计的数据结构使用了长度为 27 的数组来存储 [-1, -1, -1][1, 1, 1] 的一系列块。在一个三重的 for 循环里,逐个将这些块绘制到屏幕上的逻辑大概就像前面看到的这张图:

    需要注意的是,并不是越接近底层的代码就一定越快。例如在最早的实现中,笔者直接通过循环调用来自(或者说抄自)MDN 的 3D 立方体例程来完成 27 个小块的渲染。这时对于 27 个立方体区区不足千个顶点,60 帧绘制动画时的 CPU 占用率都可能跑满。经过定位,发现重复的 CPU 与 GPU 交互是一个大忌:从 CPU 向 GPU 传递数据,以及最终对 GPU 绘图 API 的调用,都具有较大的固定开销。一般我们需要将一帧中 Draw Call 的数量控制在 20 个以内,对于 27 个立方体就使用 27 次 Draw Call 的做法显然是个反模式。在将代码改造为一次批量传入全部顶点并调用一次 drawElements 后,即可实现流畅的 60 帧动画了 :)

    旋转动画实现

    在实现基本的渲染机制后,魔方整体的旋转效果可以通过对模-视矩阵做矩阵乘法来实现。模-视矩阵会在顶点着色器由 GPU 中对每一个顶点并行地计算,得到顶点变换后的 gl_Position 位置。但对于单个面的旋转,我们选择了先在 CPU 中计算好顶点位置,再将其传入顶点缓冲区。这和魔方旋转动画的实现原理直接相关:

    • 在一次某个面的旋转过程中,魔方的数据模型不发生改变,仅改变受影响的顶点所在位置。
    • 在旋转结束时,我们调用上文中实现的 rotate API 来「瞬间旋转好」魔方的数据模型,而后再多绘制一帧。

    我们首先需要设计用于渲染一帧的 render API。考虑到魔方在绘制时可能存在对某个面一定程度的旋转,这个无状态的渲染 API 接口形如:

    render (rX = 0, rY = 0, moveFace = null, moveAngle = 0) {
      if (!this.gl) throw new Error('Missing WebGL context!')
      this.buffer = getBuffer(this.gl, this.blocks, moveFace, moveAngle)
      renderFrame(this.gl, this.programInfo, this.buffer, rX, rY)
    }
    复制代码

    而对单个面的旋转过程中,我们可以使用浏览器的 requestAnimationFrame API 来实现基本的时序控制。一次调用 animate 的旋转返回一个在单次旋转结束时 resolve 的 Promise,其实现如下:

    animate (move = null, duration = 500) {
      if (move && move.length === 0) return Promise.resolve()
      if (!move || this.__ANIMATING) throw new Error('Unable to animate!')
    
      this.__ANIMATING = true
      let k = move.includes("'") ? 1 : -1
      if (/B|D|L/.test(move)) k = k * -1
      const beginTime = +new Date()
      return new Promise((resolve, reject) => {
        const tick = () => {
          const diff = +new Date() - beginTime
          const percentage = diff / duration
          const face = move.replace("'", '')
          if (percentage < 1) {
            this.render(this.rX, this.rY, face, 90 * percentage * k)
            window.requestAnimationFrame(tick)
          } else {
            this.move(move)
            this.render(this.rX, this.rY, null, 0)
            this.__ANIMATING = false
            resolve()
          }
        }
        window.requestAnimationFrame(tick)
      })
    }
    复制代码

    连续旋转实现

    在实现了单次旋转后,如何支持连续的多次旋转呢?本着能偷懒就偷懒的想法,笔者对上面的函数进行了不改动已有逻辑的递归化改造,只需要在原函数入口处加入如下几行,就可以使支持传入数组为参数来递归调用自身,并在传入的连续动画数组长度为 1 时作为递归的出口,从而轻松地实现连续的动画效果:

    if (Array.isArray(move) && move.length > 1) {
      const lastMove = move.pop()
      // 返回递归得到的 Promise
      return this.animate(move).then(() => this.animate(lastMove))
    } else if (move.length === 1) move = move[0] // 继续已有逻辑
    复制代码

    到这里,一个可以供人体验的魔方基本就可以在浏览器里跑起来了。但这还不是我们最终的目标:我们该怎么自动还原一个魔方呢?

    魔方的还原

    魔方的还原算法在学术界已有很深入的研究,计算机在 20 步之内可以解出任意状态的魔方,也有成熟的轮子可以直接调用。但作为一个(高中时)曾经的魔方业余爱好者,笔者这里更关注的是「如何模拟出我自己还原魔方的操作」,故而在这里我们要介绍的是简单易懂的 CFOP 层先算法。

    在开始前,有必要强调一个前文中一笔带过的概念:在旋转时,魔方中心块之间的相对位置始终不会发生变化。如下图:

    因此,在魔方旋转时,我们只需关注角块和棱块是否归位即可。在 CFOP 层先法中,归位全部角块和棱块的步骤,被分为了逐次递进的四步:

    1. 还原底部四个棱块,构建出「十字」。
    2. 分组还原底层和第二层的所有角块和棱块。
    3. 调整顶层块朝向,保证顶面同色。
    4. 调整顶层块顺序,完成整个求解。

    让我们依次来看看每一步都发生了什么吧。

    底层十字

    这一步可以说是最简单也最难的,在此我们的目标是还原四个底部棱块,像这样:

    对一个完全打乱的魔方,每个目标棱块都可能以两种不同的朝向出现在任意一个棱块的位置上。为什么有两种朝向呢?请看下图:

    这是最简单的一种情形,此时直接做一次 R2 旋转即可使红白棱块归位。但下面这种情况也是完全合法的:

    这时由于棱块的朝向不同,所需的步骤就完全不同了。但总的来说,构成十字所需的棱块可能出现的位置总是有限的。拆解分类出所有可能的情形后,我们不难使用贪心策略来匹配:

    1. 每次找到一个构成十字所需的棱块,求出它到目标位置的一串移动步骤。
    2. 在不影响其他十字棱块的前提下将其归位,而后寻找下一个棱块。

    这个最简单的策略很接近语法分析中向前看符号数量为 1 时的算法,不过这里不需要回溯。实现机制可以抽象如下:

    solveCross () {
      const clonedCube = new Cube(null, this.cube.moves)
      const moveSteps = []
      while (true) {
        const lostEdgeCoords = findCrossCoords(clonedCube)
        if (!lostEdgeCoords.length) break
        moveSteps.push(solveCrossEdge(clonedCube, lostEdgeCoords[0]))
      }
      return moveSteps
    }
    复制代码

    这个实现原理并不复杂,其代价就是过小的局部最优造成了较多的冗余步骤。如果同时观察 2 个甚至更多的棱块状态并将其一并归位,其效率显然能得到提升(这时的实现难度也是水涨船高)。作为对比,一流的魔方玩家可以在 7 步内完成十字,但这个算法实现却需要 20 步左右——不过这里意思已经到了,各位看官就先不要太苛刻啦。

    底部两层

    这里的目标是在底部十字完成的基础上,完成底部两层所有块的归位。我们的目标是实现这样的状态:

    这个步骤中,我们以 Slot 和 Pair 的概念作为还原的基本元素。相邻的十字之间所间隔的一个棱和一个角,构成了一个 Slot,而它们所对应的两个目标块则称为一个 Pair。故而这个步骤中,我们只需要重复四次将 Pair 放入 Slot 中的操作即可。一次最简单的操作大概是这样的:

    上图将顶层的一对 Pair 放入了蓝红相间的 Slot 中。类似于之前解十字时的情形,这一步中的每个棱块和角块也有不同的位置和朝向。如果它们都在顶层,那么我们可以通过已有的匹配规则来实现匹配;如果它们在其它的 Slot 中,那么我们就递归地执行「将 Pair 从其它 Slot 中旋出」的算法,直到这组 Pair 都位于顶层为止。

    这一步的还原算法与下面的步骤相当接近,稍后一并介绍。

    顶层同色与顶层顺序

    完成了前两层的还原后,我们最后所需要处理的就是顶层的 8 个棱块与角块了。首先是顶面同色的步骤,将各块调整到正确的朝向,实现顶面同色(一般采用白色作为底面,此时按照约定,黄色为顶面):

    而后是顶层顺序的调整。这一步在不改变棱与角朝向的前提下,改变它们的排列顺序,最终完成整个魔方的还原:

    从前两层的还原到顶层的还原步骤中,都有大量的魔方公式规则可供匹配使用。如何将这些现成的规则应用到还原算法中呢?我们可以使用规则驱动的方式来使用它们。

    规则驱动设计

    了解编译过程的同学应该知道,语法分析的过程可以通过编写一系列的语法规则来实现。而在魔方还原时,我们也有大量的规则可供使用。一条规则的匹配部分大概是这样的:

    在顶面同色过程中,满足上述 "pattern" 的顶面,可以通过 U L U' R' U L' U' R 的步骤来还原。类似地,在还原顶层顺序时,规则的匹配方式形如这样:

    满足这条规则的顶层状态可以通过该规则所定义的步骤求解:R2 U' R' U' R U R U R U' R。这样一来,只需要实现对规则的匹配和执行操作,规则的逻辑就可以完全与代码逻辑解耦,变为可配置的 JSON 格式数据。用于还原前两层的一条规则格式形如:

    {
      match: { [E]: topEdge(COLOR_F, E), [SE]: SE_D_AS_F },
      moves: "U (R U' R')"
    }
    复制代码

    顶层同色的规则格式形如:

    {
      match: { [NW]: L, [NE]: R, [SE]: R, [SW]: L },
      moves: "R U R' U R U' R' U R U U R'"
    }
    复制代码

    顶面顺序的规则格式形如:

    {
      match: { [N]: W, [W]: [E], [E]: N },
      moves: "R R U' R' U' R U R U R U' R"
    }
    复制代码

    这里的 NW / E / SE 是笔者的实现中基于九宫格东西南北方向定位的简写。在实现了对规则的自动匹配和应用之后,CFOP 中后面三步的实现方式可以说大同小异,主要的工作集中在一些与旋转相关的 mapping 处理。

    规则的自测试

    在整个还原过程中,一共有上百条规则需要匹配。对于这么多的规则,该如何保证它们的正确性呢?在 TDD 测试驱动开发的理念中,开发者需要通过编写各种繁冗的测试用例来实现对代码逻辑的覆盖。但在魔方领域,笔者发现了一种优雅得多的性质:任何一条规则本身,就是自己的测试用例!如这条规则:

    {
      match: { [N]: W, [W]: [E], [E]: N },
      moves: "R R U' R' U' R U R U R U' R"
    }
    复制代码

    我们只需要将 moves 中的每一步顺序颠倒地输入初始状态的魔方,就可以用这个状态来验证规则是否能够匹配,以及魔方是否能基于该规则还原了。这个性质使得我很容易地编写了下面这样简单的代码,自动验证每条输入规则的正确性:

    const flip = moves => moves.map(x => x.length > 1 ? x[0] : x + "'").reverse()
    
    OLL.forEach(rule => {
      const rMoves = flip(rule.moves)
      const cube = new Cube(null, rMoves)
      if (
        matchOrientationRule(cube, rule) &&
        isOrientationSolved(cube.move(rule.moves))
      ) {
        console.log('OLL test pass', rule.id)
      } else console.error('Error OLL rule match', rule.id)
    })
    复制代码

    在这个支持自测试的规则匹配算法基础上,求解魔方的全部步骤就这样计算出来了 :)

    成果与后记

    经过半个多月业余时间的折腾,笔者实现了一个非常小巧的魔方求解模拟器 Freecube。它支持三阶魔方状态的渲染和逐步求解,还提供了旋转与求解的 API 可供复用。由于它没有使用任何第三方依赖并使用了各种力求精简的「技巧」,因而它的体积被控制在了压缩后 10KB 内。欢迎移步 GitHub 观光 XD

    Freecube 是笔者在很多地方忙里偷闲地实现的:咖啡厅、动车、公交车甚至饭桌上……即便写不了代码的场合,也可以拿平板写写画画来设计它。它的灵感来自于 @youngdro 神奇的吉他和弦算法博文,另外感谢大佬的指点和某人对 README 文档的审校 XD

    展开全文
  • 有一个酷爱魔方的朋友,托我给他定制一个专门用于“训练魔方观察和预判能力”的程序。听完需求之后觉得很有趣,就答应了,并决定把整个制作过程公开。
  • 大家好,今天同大家分享的是我制作的二十二面恐龙魔方。录了一小段带讲解的展示视频,和文字内容有重叠。1P是介绍,2P是一次打乱和还原。顾名思义,这个魔方是恐龙魔方的一个形状变形。制作这个魔方完全是为了展示这...
  • 多维数据集模拟器和求解器 该程序模拟,操纵和求解所有魔方。包括网络摄像头识别。 运行:>> digrub 该程序允许您生成任意尺寸的随机加扰多维数据集,然后可以手动对其进行操作或由计算机求解。您...
  • 魔方是个结构简单而变化无穷的神奇玩具。那么如何在万能的浏览器里模拟出魔方的无尽变换,又如何将其还原呢?下面让我们一步步地来一探究竟吧。魔方的抽象拆解过魔方的同学可能知道,现实中魔方的内部结构包含了中轴...
  • 本文使用 Zhihu On VSCode 创作并发布写完文章后才发现这属于循环群,那本文就当作是循环群的通俗解释吧.这是那天我和同学推出来的结论:对任意封闭体系内的...或许更好的阅读体验:由魔方问题引出的思考 | Xecad...
  • 本程序能够产生一个任意尺寸的随机打乱的立方体,可以通过手动操作或者由计算机求解进行魔方复原。 This program allows you to generate arandomly scramble cube of arbitrary dimension which can then be ...
  • 和常规的笔记本玩法一样,可以安装WPS,游戏平台,模拟器游戏,PS软件等各类应用,满足我们的日常所需。 在续航表现上来说,酷比魔方KNote X拥有7.6V 5500mAh大容量电池,可以在屏幕亮度50%的前提下,使用超过8小时...
  • 任务由两个智能体程序(分别是:可访问模拟器状态的、基于状态的智能体程序,以及使用原始像素观测值的、基于视觉的智能体程序)同时进行训练,由前者为后者提供用于强化学习的数据。 上图:在真实和模拟域随机环境中...
  • RubiksCube:魔方游戏(3D模拟器+解算器)
  • 3d魔方游戏,unity

    2020-05-28 09:35:49
    体验了几个魔方的APP软件,都没有镜子效果(看到背面情况),于是决定自己制作一个魔方模拟器。 成品体验(含windows版、安卓版): 链接:https://pan.baidu.com/s/18wr8C5Jnub48vmLfuhHfYg 提取码:aqoz 代码...
  • 这个「魔方模拟器」由 Gitee 用户华哲辰发布,小编实际体验后发现它并不像看上去那么简单,一起来和小编看看吧! 项目名称:cuber 项目作者:华哲辰 开源许可证:MIT 技术栈 typescript webpack ...

空空如也

空空如也

1 2 3
收藏数 52
精华内容 20
关键字:

魔方模拟器