您的位置:首页 > 教程 > JavaScript > three.js简单实现类似七圣召唤的掷骰子

three.js简单实现类似七圣召唤的掷骰子

2023-01-18 05:43:42 来源:易采站长站 作者:

目录
1基本工作1.1 创建场景1.2 创建物理世界2 骰子2.1 骰子模型2.2 骰子物理2.3 判断骰子的顶面方案一方案二具体实现2.4 锁定骰子

1基本工作

笔者利用业余时间自学了three.js。为了更好的了解WebGL以及更熟练的使用three,想模仿原神中的小游戏“七圣召唤”中的投掷骰子效果,作为首个练习项目~~>

    为了方便直接用vite创建了vue项目npm下载three.js和cannon-es,最重要的两个库~

    1.1>

    直接贴代码~

    /**
    * 创建场景对象Scene
     */
    const scene = new THREE.Scene();
    /**
     * 创建网格模型
     */
    const geometry = new THREE.BoxGeometry(300, 300, 5); //创建一个立方体几何对象Geometry
    const material = new THREE.MeshPhongMaterial({
      color: 0x845EC2,
      antialias: true,
      alpha: true
    }); //材质对象Material
    const desk = new THREE.Mesh(geometry, material); //网格模型对象Mesh
    desk.receiveShadow = true;
    desk.rotateX(Math.PI * 0.5)
    scene.add(desk); //网格模型添加到场景中
    //聚光灯
    const light = new THREE.SpotLight(0xffffff);
    light.position.set(20, 220, 100); //光源位置
    light.castShadow = true;
    light.shadow.mapSize.width = 2048;
    light.shadow.mapSize.height = 2048;
    scene.add(light); //点光源添加到场景中
    //环境光
    const ambient = new THREE.AmbientLight(0x666666);
    scene.add(ambient);
    // 相机设置
    const width = window.innerWidth; //窗口宽度
    const height = window.innerHeight; //窗口高度
    const k = width / height; //窗口宽高比
    const s = 70; //三维场景显示范围控制系数,系数越大,显示的范围越大
    //创建相机对象
    const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    camera.position.set(0, 200, 450); //设置相机位置
    camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
    /**
     * 创建渲染器对象
     */
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setSize(width, height);//设置渲染区域尺寸
    renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
    document.getElementById("app").appendChild(renderer.domElement) //插入canvas对象
    //执行渲染操作   指定场景、相机作为参数
    function render() {
      renderer.render(scene, camera);
    }
    render();
    

    1.2>
    const world = new CANNON.World();
    world.gravity.set(0, -9.82, 0);
    world.allowSleep = true;
    const floorBody = new CANNON.Body({
      mass: 0,
      shape: new CANNON.Plane(),
      position: new CANNON.Vec3(0, 3, 0),
    })
    // 由于平面初始化是是竖立着的,所以需要将其旋转至跟现实中的地板一样 横着
    // 在cannon.js中,我们只能使用四元数(Quaternion)来旋转,可以通过setFromAxisAngle(…)方法,第一个参数是旋转轴,第二个参数是角度
    floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5)
    world.addBody(floorBody)
    const fixedTimeStep = 1.0 / 60.0; // seconds
    const maxSubSteps = 3;
    // loop
    let lastTime;
    (function animate(time) {
      requestAnimationFrame(animate);
      if (lastTime !== undefined) {
        var dt = (time - lastTime) / 500;
        world.step(fixedTimeStep, dt, maxSubSteps);
      }
      dice_manager.update_all();
      render();
      lastTime = time;
    })();
    

    至此基本物理世界场景就创建完成。接下来我们需要一个生成骰子的函数。

    2>

    2.1>

    很简单,直接使用new THREE.OctahedronGeometry(),这个构造函数会返回一个八面立方体。
    并且我们需要一个八面都是不同颜色的骰子。

      const rgb_arr = [
        [161, 178, 74],
        [255, 150, 75],
        [176, 103, 208],
        [219, 168, 79],
        [20, 204, 238],
        [109, 210, 192],
        [166, 228, 241],
        [255, 255, 255],
      ];
      const color_arr = [];
      rgb_arr.map((val_arr) => {
        for (let i = 0; i < 3; i++) {
          val_arr.map((val) => {
            color_arr.push(val / 255);
          });
        }
      });
      const color = new Float32Array(color_arr);
      geometry.attributes.color = new THREE.BufferAttribute(color, 3);
      const material = new THREE.MeshLambertMaterial({
        vertexColors: true, 
        side: THREE.DoubleSide,
      });
      const polyhedron_mesh = new THREE.Mesh(geometry, material);
    
      THREE.BufferAttribute接收的rbg的值为0~1,所以还需要将原始的rbg值除以255。vertexColors设为true,表示以顶点数据为准。

      好像相差有点大。。不过我们还是得到了一个八面的骰子(没有高清的元素图标贴图,只能勉强看看~)

      2.2>

      根据上面弄好的骰子模型生成一个骰子的物理模型。

      const create_dice_shape = (mesh) => {
        let geometry = new THREE.BufferGeometry();
        geometry.setAttribute("position", mesh.geometry.getAttribute("position"));
        geometry = mergeVertices(geometry);
        const position = geometry.attributes.position.array;
        const index = geometry.index.array;
        const vertices = [];
        // 转换成cannon需要的顶点和面
        for (let i = 0, len = position.length; i < len; i += 3) {
          vertices.push(
            new CANNON.Vec3(position[i], position[i + 1], position[i + 2])
          );
        }
        const faces = [];
        for (let i = 0, len = index.length; i < len; i += 3) {
          faces.push([index[i], index[i + 1], index[i + 2]]);
        }
        // 生成cannon凸多面体
        return new CANNON.ConvexPolyhedron({ vertices, faces });
      };
      

      有了ConvexPolyhedron我们就可以创建一个body物理模型了

      const body = new CANNON.Body({
          mass: 10,
          shape,
        });
      

      将渲染模型和物理模型绑定起来:

      update: () => {
            mesh.position.copy(body.position);
            mesh.quaternion.copy(body.quaternion);
          },
      

      设置body参数的函数,来让我们可以投掷骰子:

      init_body: (position) => {
            body.position = position;
            // 设置加速度和向下的速度
            body.angularVelocity.set(Math.random(), Math.random(), Math.random());
            body.velocity.set(0, -80, 0);
            body.sleepState = 0; //将sleepState设为0 不然重置后不会运动
          },
      

      fine~相当不错

      2.3>

      关于如何判断骰子的顶面,翻遍了谷歌和百度,始终没有好结果。

      发一下牢骚,在互联网上搜索的几乎全是不相关的内容。要么就是一众的采集站,要么一样的帖子大伙们反复转载反复写,甚至还有拿开源项目卖钱的。让我体会了什么叫“知识库污染”。

      既然没有现成的方案,那就只能自己想咯。我们知道three有个Group类,他用于将多个模型组合成一个组一起运动。由此想到两个相对可行的方案:(有没有大佬分享更好的办法啊~

      方案一

      骰子每个面弄成多个mesh组合成一个THREE.Group(),在骰子停止时获取所有骰子的位置,THREE.Raycaster()在每个骰子的上面生成射线并朝向骰子,此时相交的第一个模型就是骰子的顶面。
      缺点: 太复杂,物理模型不好弄,pass掉~

      方案二

      骰子还是那个骰子,但是在每个面上创建一个不可见的模型,并用THREE.Group()绑定到一块儿,随着骰子一起运动,停下时,获取每个骰子y轴最大的定位点,也就是最高的那个,便是骰子的顶面。
      缺点: 没想到,但应该比方案一好。

      具体实现

      首先创建一个函数,它用于在骰子相应的地方创建一个不可见的模型。

      const create_basic_mesh = (position, name) => {
        const geometry = new THREE.BufferGeometry();
        const vertices = new Float32Array([0, 0, 0]);
        geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
        const mesh = new THREE.Mesh(geometry);
        [mesh.position.y, mesh.position.x, mesh.position.z] = position;
        mesh.name = name; //标记面的点数
        return mesh;
      };
      

      将其包装成一个组,其中顶点位置后的参数(grass等等)用于标记点数,代表着游戏中的七大元素以及万能元素。

      // 初始化点数位置
      const init_points = (mesh) => {
        const group = new THREE.Group();
        group.add(mesh);
        group.name = "dice";
        group.add(create_basic_mesh([5, 5, 5], "grass"));
        group.add(create_basic_mesh([5, -5, 5], "universal"));
        group.add(create_basic_mesh([5, -5, -5], "water"));
        group.add(create_basic_mesh([5, 5, -5], "rock"));
        group.add(create_basic_mesh([-5, 5, 5], "fire"));
        group.add(create_basic_mesh([-5, -5, 5], "ice"));
        group.add(create_basic_mesh([-5, -5, -5], "wind"));
        group.add(create_basic_mesh([-5, 5, -5], "thunder"));
        return group;
      };
      

      差不多就是这样,为了方便调试,我暂时把它渲染成了可见的。

      判断顶面,只需要获取它们中最高的那一个即可

      get_top: () => {
            let top_face,
              max = 0;
            mesh.children.map((val, index) => {
              if (index == 0) return;
              val.updateMatrixWorld(); //更新模型的世界矩阵
              let worldPosition = new THREE.Vector3();
              val.getWorldPosition(worldPosition); //获取模型在世界中的位置
              if (max < worldPosition.y) {
                max = worldPosition.y;
                top_face = val.name;
              }
            });
            return top_face;
          },
      

      2.4>

      在七圣召唤中每一次重随都能锁定骰子,被锁定的骰子会移动到旁边并且不会参与重随。

      //鼠标选中模型
      const choose = (event) => {
        let mouseX = event.clientX;//鼠标单击位置横坐标
        let mouseY = event.clientY;//鼠标单击位置纵坐标 
        //屏幕坐标转标准设备坐标
        const x = (mouseX / window.innerWidth) * 2 - 1;
        const y = - (mouseY / window.innerHeight) * 2 + 1;
        let standardVector = new THREE.Vector3(x, y);//标准设备坐标
        //标准设备坐标转世界坐标
        let worldVector = standardVector.unproject(camera);
        //射线投射方向单位向量(worldVector坐标减相机位置坐标)
        let ray = worldVector.sub(camera.position).normalize();
        //创建射线投射器对象 
        let raycaster = new THREE.Raycaster(camera.position, ray);
        raycaster.camera = camera//设置一下相机
        let intersects = raycaster.intersectObjects(dice_meshs);
        //长度大于0说明选中了骰子
        if (intersects.length > 0) {
          let dice_name = intersects[0]?.object.parent.name;
          locked_dice.push(dice_name);
          dice_manager.move_dice(dice_name, new CANNON.Vec3(135, 10, (-100 + locked_dice.length * 20))) //移动骰子
        }
      }
      addEventListener('click', choose); // 监听窗口鼠标单击事件
      

      move_dice函数

      // 移动骰子到相应位置
      move_dice: (name, position) => {
            for (let i = 0; i < dice_arr.length; i++) {
              if (name == dice_arr[i].mesh.name) {
                dice_arr[i].body.position = position;
                break;
              }
            }
          },
      

      重随时需要判断被锁定的骰子。

      init_dice: (exclude_dices) => {
            for (let i = 0; i < dice_arr.length ; i++) {
              if(!exclude_dices.includes(dice_arr[i].mesh.name)){
                dice_arr[i].init_body(new CANNON.Vec3(-(i % 4) * 21, 100, i * 6));
              }
            }
          },
      

      按照惯例测试一下。

      基本上就差不多完工了,但是还有很多细节可以慢慢打磨,更多关于three.js七圣召唤掷骰子的资料请关注易采站长站其它相关文章!

      如有侵权,请发邮件到 [email protected]

相关文章

  • vue跳转页面的几种方法(推荐)

    vue跳转页面的几种方法(推荐)

    vue跳转不同页面的多种方法 1:router-link跳转 !-- 直接跳转 --router-link to='/testDemo' button点击跳转2/button/router-link !-- 带参数跳转 --router-link :to="{path:'testDemo',query:{setid:123456}}" button点击跳转
    2020-03-26
  • Vue+elementUI实现多图片上传与回显功能(含回显后继续上传或删除

    Vue+elementUI实现多图片上传与回显功能(含回显后继续上传或删除

    最近有使用vue+elementUI实现多图片上传的需求,遂做此纪录。 本次主要写一下前端的实现细节,至于后台以Multipart[ ]数组接收即可,不再赘述,网上一搜大把文章可供参考。 本次使用
    2020-03-23
  • 微信小程序搜索框样式并实现跳转到搜索页面(小程序搜索功能

    微信小程序搜索框样式并实现跳转到搜索页面(小程序搜索功能

    上效果图: 一:搜索框功能实现 1.在首页做一个搜索框的样式并实现跳转到搜索页面 view class='page_row' bindtap="suo" view class="search" view class="df search_arr" icon class="searchcion" size='20' type='search'/
    2020-03-10
  • 微信小程序实现canvas分享朋友圈海报

    微信小程序实现canvas分享朋友圈海报

    本文实例为大家分享了微信小程序分享朋友圈海报的具体代码,供大家参考,具体内容如下 思路:生成朋友圈海报放在公共文件,首先需要绘制canvas,点击分享朋友圈按钮,在手机屏幕
    2020-06-21
  • 详解ES6 Modules

    详解ES6 Modules

    当下, 我们几乎所有的项目都是基于 webpack、rollup 等构建工具进行开发的,模块化已经是常态。 我们对它并不陌生,今天,我们就再系统的回顾一下ES6的模块机制, 并总结下常用的操
    2020-07-04
  • Vue-router 报错NavigationDuplicated的解决方法

    Vue-router 报错NavigationDuplicated的解决方法

    版本:3.1.x 报错原因: 使用push()、replace()进行导航时,不能重复导航到当前路由。 解决办法: 方法1:在定义路由的文件中router/index.js const originalPush = VueRouter.prototype.pushVueRouter.protot
    2020-03-31
  • vue使用better-scroll实现滑动以及左右联动

    vue使用better-scroll实现滑动以及左右联动

    本文实例为大家分享了vue实现滑动以及左右联动效果的具体代码,供大家参考,具体内容如下 一、首先需要在项目中引入better-scroll 1. 在package.json 直接写入 "better-scroll":"^1.15.1" 版本以
    2020-06-30
  • Vue如何提升首屏加载速度实例解析

    Vue如何提升首屏加载速度实例解析

    在Vue项目中,引入到工程中的所有js、css文件,编译时都会被打包进vendor.js,浏览器在加载该文件之后才能开始显示首屏。若是引入的库众多,那么vendor.js文件体积将会相当的大,影响
    2020-06-25