Threejs 中秋佳节感受闽南名俗 | 中秋博饼🥮
点击关注公众号,回复”福利”即可参与文末抽奖
中秋博饼是流行于福建省厦门市的传统民俗,国家级非物质文化遗产之一。
明月洒光,洒出人间无限浪漫;桂花飘香,飘出大地五彩缤纷;月饼圆圆,圆出家人欢声笑语;提前祝大伙祝你中秋快乐,永远平安。
今天让我们走进闽南的乡俗,一起使用Threejs实现 中秋博饼 游戏。
实现步骤 实现主要有以下几个步骤:
等待骰子、盆子模型加载物理引擎场景模拟3D场景物体与引擎物体 "绑定"游戏规则模型加载3D 模型网站有很多,其中较为知名的是 Sketchfab ,其中有丰富的免费模型作为使用,并且有很多模型格式提供下载,如fbx、obj、gltf、glb等。
glTF与glb模型格式gltf是JSON文件,里面包括了所有场景的信息,如scene、textures等,是常见的三维模型标准文件格式。gltf的贴图、几何等资源可以直接通过base64的格式内联,也可指向外部的资源。所以gltf通常以文件夹的方式传输,里面含有需要外联的资源文件,
image.pngglb格式则是gltf的二进制版本,由于base64的编解码需要时间与空间,所以引用了glb格式文件。相同文件下,glb格式文件会比gltf文件更小一些,本文使用到的正是glb格式。
Threejs模型加载Threejs包中 提供了 FBXLoader、OBJLoader、GLTFLoader等方式进行模型的加载,其中GLTFLoader可加载gltf与glb两种格式。用法非常简单,直接贴代码。
/** * 加载GLTF */
let loadGLTF = async (url): PromiseTHREE.Group { return new Promise(resolve = { const loader = new GLTFLoader(); loader.load(url, object = { resolve(object as THREE.Group); }); });};
/** * 初始化模型 */
let loadModel = async () = { let basinModel = await loadGLTF("/basin2.glb"); let diceModel = await loadGLTF("/dice.glb");
// 加载色子 dice = diceModel.scene.children[0]; dice.scale.set(0.1, 0.1, 0.1); dice.traverse(function (child) { if (child.isMesh) { child.castShadow = true; child.material.metalness = 1; child.material.emissive = child.material.color; child.material.emissiveMap = child.material.map; } });
// 加载盆子 basin = basinModel.scene.children[0]; basin.scale.set(18, 18, 18); basin.position.y = 0.7; basin.position.x = 0; scene.add(basin);};现在我们可以看到一个盆子在屏幕中
物理引擎场景模拟Threejs的物理引擎我选择使用 cannon-es,主要是使用其中的碰撞和重力模拟。
文档的首页示例是偏简单的,我们首先需要初始化一个世界,然后设置他的重力方向与大小,并设置在物体静止的时候是睡眠状态的,减少计算
let world = new CANNON.World();world.gravity.set(0, -9.82, 0);world.allowSleep = true;然后我们进行物体的模拟,骰子我们可以使用引擎中提供的 Box 类来进行生成。
/** * 创建色子物理引擎函数 */
const generateDiceBody = () = { const size = 0.1; const halfExtents = new CANNON.Vec3(size, size, size);
const dice = new CANNON.Body({ mass: 0.1, material: new CANNON.Material({ friction: 0.1, restitution: 0.7 }), shape: new CANNON.Box(halfExtents) });
dice.sleepSpeedLimit = 1.0; world.addBody(dice);
return dice;};
我们使用了Body类生成World需要的主体,mass是物体质量,material是材质,由Material类生成,friction是摩擦力,restitution是弹性系数,可通过不断调整来达到最好的效果。
sleepSpeedLimit 属性是刚体进入SLEEP状态的判断条件,当速度此值则进入睡眠状态。
盆子模拟cannon-es文档中提供了以下方式生成形状
BoxConvexPolyhedronParticlePlaneSphereHeightfieldTrimeshBox、Sphere、Plane都是 threejs 熟悉的几何体无法生成盆子这种凹面体,ConvexPolyhedron是凸多面体,Particle是粒子,显然都无法实现。
只剩下Trimesh修剪网格,用法是用顶点数据进行生成,模型中正好有这些信息。
正当我写完以后以为可以大功告成时,运行一看却发现碰撞效果失效,骰子直接从盆子传过去。百思不得其解之时,在github提的issue中看到以下回答
image.png原来修建网格不支持碰撞!!!!
好好好,你这样整是吧,行行行。
现在只剩下Heightfield高度场了,高度场是通过提供的高度数据来生成形状。我们通过勾股定理判断X、Z是否在圆内,如果在则高度为-1.7,否则为0,这样我们通过Heightfield类来生成凹面体的形状。
/** * 初始化盆子刚体 */
const initBasin = () = { const numRows = 60; const numCols = 60; let heights: number[][] = []; for (let i = 0; i numRows; i++) { const row: number[] = []; for (let j = 0; j numCols; j++) { const x = (j / (numCols - 1) - 0.5) * 20; const z = (i / (numRows - 1) - 0.5) * 20; const radius = 4.2; const height = Math.sqrt(x * x + z * z) = radius ? -1.7 : 0; row.push(height); } heights.push(row); }
const heightfieldShape = new CANNON.Heightfield(heights, { elementSize: 0.2 }); const heightfieldBody = new CANNON.Body({ mass: 0, material: new CANNON.Material({ friction: 0.1, restitution: 0.7 }) }); heightfieldBody.addShape(heightfieldShape); heightfieldBody.position.set(-5.9, 1.3, 5.9); heightfieldBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); world.addBody(heightfieldBody);};在写文章的时候突然想到其实并不需要借助物理引擎,碰撞可以通过八叉树Octree进行检测,重力可以自己模拟,有兴趣的小伙伴可以试试看
这里也不得不抱怨一下cannon-es,首先没有中文文档,二来是使用人数也相对较少,遇到问题解决需要花很长的时间来找答案。
3D场景物体与引擎物体 "绑定"我们定义一个对象dices保存骰子的引擎刚体和threejs物体的信息。
let dices: { tDice?: THREE.Object3D; cDice?: CANNON.Body }[] = [];
/** * 初始化色子位置 */
let initDice = () = { for (let i = 1; i i++) { let cDice = generateDiceBody(); cDice.quaternion.setFromEuler(Math.PI / 2, 0, 0); let tDice = dice.clone(); scene.add(tDice); dices.push({ tDice: tDice, cDice: cDice }); }};然后将引擎刚体的旋转位移赋值给threejs物体
dices.forEach(({ cDice, tDice }) = { if (cDice && tDice) { tDice.position.copy(cDice.position); tDice.quaternion.copy(cDice.quaternion); } });
至此,3D场景就已经搭建好了,接下来就到了游戏逻辑的处理。
游戏规则游戏规则如下图
image.png要想知道博的什么名首先需要获得每个骰子的点数,我们可以通过每个方向的旋转角度与朝上初始面来得出。
/** * 获取色子点数 */
let getDicePoints = () = { let points: number[] = []; dices.forEach(({ cDice, tDice }) = { let xAngle = Math.round((tDice!.rotation.x / Math.PI) * 180); let zAngle = Math.round((tDice!.rotation.z / Math.PI) * 180);
let point;
if (xAngle == -90) { point = 1; } else if (xAngle == 90) { point = 5; } else if (xAngle + zAngle == -90 || xAngle + zAngle == 270) { point = 4; } else if (xAngle + zAngle == 0 || xAngle + zAngle == 360) { point = 3; } else if (xAngle + zAngle == 180 || xAngle + zAngle == -180) { point = 6; } else { point = 2; }
points.push(point); });
return points;};得到每个骰子的点数后,我们需要根据规则来看博饼结果。
const getName = () = { let obj = {};
result.value.forEach(index = { if (obj[index]) { obj[index] += 1; } else { obj[index] = 1; } });
if (obj[4] == 1) { if (Object.keys(obj).length === 6) { return "对堂"; } else { return "一秀"; } } else if (obj[4] == 2) { return "二举"; } else if (obj[4] == 3) { return "三红"; } else if (obj[4] == 4) { if (obj[2] == 2) { return "状元插金花"; } else { return "状元"; } } else if (obj[4] == 5) { return "五王"; } else if (obj[4] == 6) { return "六捧红"; } else if (Object.keys(obj).some((key)= obj[key] == 4)) { return "四进"; } else if (obj[6] == 5) { return "五子登科"; } else if (obj[6] == 6) { return "手捧黑"; }
return "再接再厉";};
result 是一个数字元组,保存每个骰子的点数
let result: Refnumber[] = ref([]);我们需要每个点 与 每个点出现的次数,通过上面的函数得到以下数据结构
{1:1,2:1,3:4} // key = 点数, value = 出现次数然后就可以通过规则来得出博饼结果。
以上就是这个小游戏的核心,完结啦!
最终效果:
2.gif是不是非常nice ~
最后 我们通过本文学习到了以下知识
模型知识、下载地址、gltf与glb的区别等物理引擎的使用与简单形状和复杂形状的刚体模拟 (高度场)博饼游戏规则。相信大家在看完这篇文章后,能够对博饼、Threejs 都有了深刻的理解,也相信你们能够实现更棒的效果。
点击小卡片,参与粉丝专属福利!!
如果文章对你有帮助的话欢迎「关注+点赞+收藏」
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线