《Three.js 可操控3D魔方(键盘转动+打乱+复原)》

Three.js 可操控3D魔方(键盘转动+打乱+复原)

直接复制整段HTML打开即可运行,WASD/方向键控层旋转,一键打乱、一键复原

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Three.js 可玩3D魔方</title>
<style>
* {margin:0;padding:0;box-sizing:border-box;}
body {overflow:hidden;background:#121218;font-family:sans-serif;}
canvas {display:block;}
.btn-box{position:fixed;top:20px;left:20px;z-index:999;}
button{padding:8px 16px;margin:0 5px;border:none;border-radius:4px;cursor:pointer;background:#2563eb;color:#fff;}
</style>
</head>
<body>
<div class="btn-box">
    <button id="randomBtn">打乱魔方</button>
    <button id="resetBtn">一键复原</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/examples/js/controls/OrbitControls.js"></script>

<script>
// 场景初始化
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x121218);
const camera = new THREE.PerspectiveCamera(60,innerWidth/innerHeight,0.1,200);
camera.position.set(9,9,15);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight);
document.body.appendChild(renderer.domElement);

// 视角控制器
const controls = new THREE.OrbitControls(camera,renderer.domElement);
controls.enableDamping = true;

// 灯光
scene.add(new THREE.AmbientLight(0xffffff,0.5));
const dirLight = new THREE.DirectionalLight(0xffffff,0.7);
dirLight.position.set(15,20,10);
scene.add(dirLight);

// 魔方六色:前、后、左、右、上、下
const COLORS = [
    0xffffff, // 前白
    0xffff00, // 后黄
    0x0000ff, // 左蓝
    0xff0000, // 右红
    0x00ff00, // 上绿
    0xffa500  // 下橙
];
const OFFSET = 1.06;
let cubies = [];
let rubikRoot = new THREE.Group();

// 创建单个魔方小块
function createCubie(x,y,z){
    const geo = new THREE.BoxGeometry(1,1,1);
    const mats = COLORS.map(c=>new THREE.MeshLambertMaterial({color:c}));
    const cube = new THREE.Mesh(geo,mats);
    cube.userData = {ox:x,oy:y,oz:z};
    cube.position.set(x*OFFSET,y*OFFSET,z*OFFSET);
    return cube;
}

// 生成完整3阶魔方
function initRubik(){
    cubies = [];
    rubikRoot.clear();
    for(let x=-1;x<=1;x++){
        for(let y=-1;y<=1;y++){
            for(let z=-1;z<=1;z++){
                let c = createCubie(x,y,z);
                cubies.push(c);
                rubikRoot.add(c);
            }
        }
    }
    scene.add(rubikRoot);
}
initRubik();

// 旋转指定层
function rotateLayer(axis, idx, dir=1){
    let targetGroup = new THREE.Group();
    let targetList = [];
    cubies.forEach(c=>{
        let pos = c.position;
        let val;
        if(axis === 'x') val = Math.round(pos.x/OFFSET);
        if(axis === 'y') val = Math.round(pos.y/OFFSET);
        if(axis === 'z') val = Math.round(pos.z/OFFSET);
        if(val === idx){
            targetList.push(c);
            rubikRoot.remove(c);
            targetGroup.add(c);
        }
    });
    scene.add(targetGroup);
    let angle = Math.PI / 2 * dir;
    let rotObj = {v:0};
    function rotateAnimate(){
        if(rotObj.v >= angle){
            targetGroup.rotation[axis] = angle;
            targetList.forEach(item=>{
                let wp = new THREE.Vector3();
                item.getWorldPosition(wp);
                let q = new THREE.Quaternion();
                item.getWorldQuaternion(q);
                rubikRoot.attach(item);
                item.position.copy(wp);
                item.quaternion.copy(q);
            });
            scene.remove(targetGroup);
            return;
        }
        rotObj.v += 0.15;
        targetGroup.rotation[axis] = rotObj.v;
        requestAnimationFrame(rotateAnimate);
    }
    rotateAnimate();
}

// 键盘控制
window.addEventListener('keydown',e=>{
    switch(e.key.toLowerCase()){
        case 'w': rotateLayer('y',1,1);break;    // 上层右转
        case 's': rotateLayer('y',-1,-1);break;  // 下层左转
        case 'a': rotateLayer('x',-1,1);break;   // 左层顺转
        case 'd': rotateLayer('x',1,-1);break;   // 右层逆转
        case 'q': rotateLayer('z',1,1);break;    // 前层顺转
        case 'e': rotateLayer('z',-1,-1);break;  // 后层逆转
    }
});

// 打乱魔方
document.getElementById('randomBtn').onclick = ()=>{
    let ops = [
        ()=>rotateLayer('y',1,1),
        ()=>rotateLayer('y',-1,-1),
        ()=>rotateLayer('x',1,-1),
        ()=>rotateLayer('x',-1,1),
        ()=>rotateLayer('z',1,1),
        ()=>rotateLayer('z',-1,-1)
    ];
    let times = 20;
    let i=0;
    function randRun(){
        if(i>=times) return;
        let fn = ops[Math.floor(Math.random()*ops.length)];
        fn();
        i++;
        setTimeout(randRun,220);
    }
    randRun();
};

// 一键复原
document.getElementById('resetBtn').onclick = ()=>{
    initRubik();
};

// 窗口适配
window.onresize = ()=>{
    camera.aspect = innerWidth/innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth,innerHeight);
};

// 渲染循环
function animate(){
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene,camera);
}
animate();
</script>
</body>
</html>

操作说明

  1. 鼠标操作
  • 左键拖拽:旋转视角
  • 滚轮:缩放远近
  • 右键拖拽:平移画面
  1. 键盘按键控魔方转动
  • W:顶层向右旋转
  • S:底层向左旋转
  • A:左侧面顺时针转
  • D:右侧面逆时针转
  • Q:前面顺时针转
  • E:后面逆时针转
  1. 按钮功能
  • 打乱魔方:自动随机转动20步打乱
  • 一键复原:瞬间重置回初始完整状态

后续可加功能

  1. 加入魔方还原算法自动求解
  2. 加数字贴纸、边框黑线更逼真
  3. 手机触屏滑动转层
  4. 记录转动步数、计时闯关
相关推荐
FRing031 小时前
JavaScript入门到精通三十二(深拷贝、浅拷贝、递归函数)
开发语言·前端·javascript
鸿蒙开发2 小时前
Three.js 首帧卡顿排查:compileAsync 之后,为什么还会停一下?
react.js·three.js
小嘉丶学长2 小时前
Day14 · 字典与集合
java·开发语言·网络·python·学习·1024程序员节
小李不想当小白2 小时前
SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
兰亭妙微UI设计公司3 小时前
兰亭妙微ui设计公司分享:B端组件命名如何支持团队协作
前端·javascript·vue.js
小大宇4 小时前
Vue知识点-ElementPlus-03提示框+询问+通知
前端·javascript·vue.js
前端技术官5 小时前
前端的技术判断力是怎么练出来的?
javascript·web前端·localstorage·debounce·弱网处理
驱动小百科6 小时前
国产3A游戏《影之刃零》正式发售:售价及电脑配置要求介绍
游戏·影之刃零·影之刃零配置要求·影之刃零下载·影之刃零售价
adinnet20266 小时前
电商客服对话知识库的自学习闭环与质量演进机制
学习
程序员大阳6 小时前
副队长JavaScript教程(60)-BOM 初识:window 对象与浏览器模型
javascript·bom·window对象·浏览器模型