广告位联系
返回顶部
分享到

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

JavaScript 来源:互联网 作者:佚名 发布时间:2023-01-19 21:28:24 人浏览
摘要

1基本工作 笔者利用业余时间自学了three.js。为了更好的了解WebGL以及更熟练的使用three,想模仿原神中的小游戏七圣召唤中的投掷骰子效果,作为首个练习项目~~ 这是坚持写技术博客的

1基本工作

笔者利用业余时间自学了three.js。为了更好的了解WebGL以及更熟练的使用three,想模仿原神中的小游戏“七圣召唤”中的投掷骰子效果,作为首个练习项目~~ 这是坚持写技术博客的第二周,也是首篇在掘金写的文章,人生路远,仍需远行。

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

1.1 创建场景

直接贴代码~

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

/**

* 创建场景对象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 创建物理世界

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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(),这个构造函数会返回一个八面立方体。
并且我们需要一个八面都是不同颜色的骰子。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

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 骰子物理

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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物理模型了

1

2

3

4

const body = new CANNON.Body({

    mass: 10,

    shape,

  });

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

1

2

3

4

update: () => {

      mesh.position.copy(body.position);

      mesh.quaternion.copy(body.quaternion);

    },

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

1

2

3

4

5

6

7

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轴最大的定位点,也就是最高的那个,便是骰子的顶面。
缺点: 没想到,但应该比方案一好。

具体实现

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

1

2

3

4

5

6

7

8

9

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等等)用于标记点数,代表着游戏中的七大元素以及万能元素。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// 初始化点数位置

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;

};

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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 锁定骰子

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

//鼠标选中模型

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函数

1

2

3

4

5

6

7

8

9

// 移动骰子到相应位置

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;

        }

      }

    },

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

1

2

3

4

5

6

7

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));

        }

      }

    },

按照惯例测试一下。

基本上就差不多完工了,但是还有很多细节可以慢慢打磨


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。

您可能感兴趣的文章 :

原文链接 : https://juejin.cn/post/7188575871014273080
    Tag :
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计