three.js簡(jiǎn)單實(shí)現(xiàn)類似七圣召喚的擲骰子
1基本工作
筆者利用業(yè)余時(shí)間自學(xué)了three.js。為了更好的了解WebGL以及更熟練的使用three,想模仿原神中的小游戲“七圣召喚”中的投擲骰子效果,作為首個(gè)練習(xí)項(xiàng)目~~ 這是堅(jiān)持寫技術(shù)博客的第二周,也是首篇在掘金寫的文章,人生路遠(yuǎn),仍需遠(yuǎn)行。
1.1 創(chuàng)建場(chǎng)景
直接貼代碼~
/**
* 創(chuàng)建場(chǎng)景對(duì)象Scene
*/
const scene = new THREE.Scene();
/**
* 創(chuàng)建網(wǎng)格模型
*/
const geometry = new THREE.BoxGeometry(300, 300, 5); //創(chuàng)建一個(gè)立方體幾何對(duì)象Geometry
const material = new THREE.MeshPhongMaterial({
color: 0x845EC2,
antialias: true,
alpha: true
}); //材質(zhì)對(duì)象Material
const desk = new THREE.Mesh(geometry, material); //網(wǎng)格模型對(duì)象Mesh
desk.receiveShadow = true;
desk.rotateX(Math.PI * 0.5)
scene.add(desk); //網(wǎng)格模型添加到場(chǎng)景中
//聚光燈
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); //點(diǎn)光源添加到場(chǎng)景中
//環(huán)境光
const ambient = new THREE.AmbientLight(0x666666);
scene.add(ambient);
// 相機(jī)設(shè)置
const width = window.innerWidth; //窗口寬度
const height = window.innerHeight; //窗口高度
const k = width / height; //窗口寬高比
const s = 70; //三維場(chǎng)景顯示范圍控制系數(shù),系數(shù)越大,顯示的范圍越大
//創(chuàng)建相機(jī)對(duì)象
const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(0, 200, 450); //設(shè)置相機(jī)位置
camera.lookAt(scene.position); //設(shè)置相機(jī)方向(指向的場(chǎng)景對(duì)象)
/**
* 創(chuàng)建渲染器對(duì)象
*/
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setSize(width, height);//設(shè)置渲染區(qū)域尺寸
renderer.setClearColor(0xb9d3ff, 1); //設(shè)置背景顏色
document.getElementById("app").appendChild(renderer.domElement) //插入canvas對(duì)象
//執(zhí)行渲染操作 指定場(chǎng)景、相機(jī)作為參數(shù)
function render() {
renderer.render(scene, camera);
}
render();
1.2 創(chuàng)建物理世界
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),
})
// 由于平面初始化是是豎立著的,所以需要將其旋轉(zhuǎn)至跟現(xiàn)實(shí)中的地板一樣 橫著
// 在cannon.js中,我們只能使用四元數(shù)(Quaternion)來(lái)旋轉(zhuǎn),可以通過(guò)setFromAxisAngle(…)方法,第一個(gè)參數(shù)是旋轉(zhuǎn)軸,第二個(gè)參數(shù)是角度
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;
})();
至此基本物理世界場(chǎng)景就創(chuàng)建完成。接下來(lái)我們需要一個(gè)生成骰子的函數(shù)。
2 骰子
2.1 骰子模型
很簡(jiǎn)單,直接使用new THREE.OctahedronGeometry(),這個(gè)構(gòu)造函數(shù)會(huì)返回一個(gè)八面立方體。
并且我們需要一個(gè)八面都是不同顏色的骰子。
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設(shè)為true,表示以頂點(diǎn)數(shù)據(jù)為準(zhǔn)。


好像相差有點(diǎn)大。。不過(guò)我們還是得到了一個(gè)八面的骰子(沒(méi)有高清的元素圖標(biāo)貼圖,只能勉強(qiáng)看看~)
2.2 骰子物理
根據(jù)上面弄好的骰子模型生成一個(gè)骰子的物理模型。
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 = [];
// 轉(zhuǎn)換成cannon需要的頂點(diǎn)和面
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我們就可以創(chuàng)建一個(gè)body物理模型了
const body = new CANNON.Body({
mass: 10,
shape,
});
將渲染模型和物理模型綁定起來(lái):
update: () => {
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
},
設(shè)置body參數(shù)的函數(shù),來(lái)讓我們可以投擲骰子:
init_body: (position) => {
body.position = position;
// 設(shè)置加速度和向下的速度
body.angularVelocity.set(Math.random(), Math.random(), Math.random());
body.velocity.set(0, -80, 0);
body.sleepState = 0; //將sleepState設(shè)為0 不然重置后不會(huì)運(yùn)動(dòng)
},

fine~相當(dāng)不錯(cuò)
2.3 判斷骰子的頂面
關(guān)于如何判斷骰子的頂面,翻遍了谷歌和百度,始終沒(méi)有好結(jié)果。
發(fā)一下牢騷,在互聯(lián)網(wǎng)上搜索的幾乎全是不相關(guān)的內(nèi)容。要么就是一眾的采集站,要么一樣的帖子大伙們反復(fù)轉(zhuǎn)載反復(fù)寫,甚至還有拿開(kāi)源項(xiàng)目賣錢的。讓我體會(huì)了什么叫“知識(shí)庫(kù)污染”。
既然沒(méi)有現(xiàn)成的方案,那就只能自己想咯。我們知道three有個(gè)Group類,他用于將多個(gè)模型組合成一個(gè)組一起運(yùn)動(dòng)。由此想到兩個(gè)相對(duì)可行的方案:(有沒(méi)有大佬分享更好的辦法啊~
方案一
骰子每個(gè)面弄成多個(gè)mesh組合成一個(gè)THREE.Group(),在骰子停止時(shí)獲取所有骰子的位置,THREE.Raycaster()在每個(gè)骰子的上面生成射線并朝向骰子,此時(shí)相交的第一個(gè)模型就是骰子的頂面。
缺點(diǎn): 太復(fù)雜,物理模型不好弄,pass掉~
方案二
骰子還是那個(gè)骰子,但是在每個(gè)面上創(chuàng)建一個(gè)不可見(jiàn)的模型,并用THREE.Group()綁定到一塊兒,隨著骰子一起運(yùn)動(dòng),停下時(shí),獲取每個(gè)骰子y軸最大的定位點(diǎn),也就是最高的那個(gè),便是骰子的頂面。
缺點(diǎn): 沒(méi)想到,但應(yīng)該比方案一好。
具體實(shí)現(xiàn)
首先創(chuàng)建一個(gè)函數(shù),它用于在骰子相應(yīng)的地方創(chuàng)建一個(gè)不可見(jiàn)的模型。
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; //標(biāo)記面的點(diǎn)數(shù)
return mesh;
};
將其包裝成一個(gè)組,其中頂點(diǎn)位置后的參數(shù)(grass等等)用于標(biāo)記點(diǎn)數(shù),代表著游戲中的七大元素以及萬(wàn)能元素。
// 初始化點(diǎn)數(shù)位置
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;
};

差不多就是這樣,為了方便調(diào)試,我暫時(shí)把它渲染成了可見(jiàn)的。
判斷頂面,只需要獲取它們中最高的那一個(gè)即可
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 鎖定骰子
在七圣召喚中每一次重隨都能鎖定骰子,被鎖定的骰子會(huì)移動(dòng)到旁邊并且不會(huì)參與重隨。
//鼠標(biāo)選中模型
const choose = (event) => {
let mouseX = event.clientX;//鼠標(biāo)單擊位置橫坐標(biāo)
let mouseY = event.clientY;//鼠標(biāo)單擊位置縱坐標(biāo)
//屏幕坐標(biāo)轉(zhuǎn)標(biāo)準(zhǔn)設(shè)備坐標(biāo)
const x = (mouseX / window.innerWidth) * 2 - 1;
const y = - (mouseY / window.innerHeight) * 2 + 1;
let standardVector = new THREE.Vector3(x, y);//標(biāo)準(zhǔn)設(shè)備坐標(biāo)
//標(biāo)準(zhǔn)設(shè)備坐標(biāo)轉(zhuǎn)世界坐標(biāo)
let worldVector = standardVector.unproject(camera);
//射線投射方向單位向量(worldVector坐標(biāo)減相機(jī)位置坐標(biāo))
let ray = worldVector.sub(camera.position).normalize();
//創(chuàng)建射線投射器對(duì)象
let raycaster = new THREE.Raycaster(camera.position, ray);
raycaster.camera = camera//設(shè)置一下相機(jī)
let intersects = raycaster.intersectObjects(dice_meshs);
//長(zhǎng)度大于0說(shuō)明選中了骰子
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))) //移動(dòng)骰子
}
}
addEventListener('click', choose); // 監(jiān)聽(tīng)窗口鼠標(biāo)單擊事件
move_dice函數(shù)
// 移動(dòng)骰子到相應(yīng)位置
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;
}
}
},
重隨時(shí)需要判斷被鎖定的骰子。
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));
}
}
},
按照慣例測(cè)試一下。

結(jié)
基本上就差不多完工了,但是還有很多細(xì)節(jié)可以慢慢打磨,更多關(guān)于three.js七圣召喚擲骰子的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 詳解Three.js?場(chǎng)景中如何徹底刪除模型和性能優(yōu)化
- Three.js添加陰影和簡(jiǎn)單后期處理實(shí)現(xiàn)示例詳解
- Three.js GLTF模型加載實(shí)現(xiàn)示例詳解
- three.js?Mool3D模型類的使用
- threeJs實(shí)現(xiàn)波紋擴(kuò)散及光標(biāo)浮動(dòng)效果詳解
- three.js-結(jié)合dat.gui實(shí)現(xiàn)界面可視化修改及調(diào)試詳解
- Three.js?Interpolant實(shí)現(xiàn)動(dòng)畫插值
- Three.js中實(shí)現(xiàn)Bloom效果及完整示例
相關(guān)文章
JavaScript處理解析JSON數(shù)據(jù)過(guò)程詳解
JSON 是 JavaScript 原生格式,也就是說(shuō)在 JavaScript 中處理 JSON 數(shù)據(jù)不需要任何特殊的 API 或工具包。接下來(lái),本文給大家介紹JavaScript處理解析JSON數(shù)據(jù)過(guò)程詳解,感興趣的朋友快來(lái)了解了解吧2015-09-09
JS實(shí)現(xiàn)Excel導(dǎo)出功能以及導(dǎo)出亂碼問(wèn)題解決詳解
這篇文章主要為大家詳細(xì)介紹了JavaScript如何調(diào)用后臺(tái)接口實(shí)現(xiàn)Excel導(dǎo)出功能以及導(dǎo)出亂碼問(wèn)題的解決辦法,需要的小伙伴可以參考一下2023-07-07
js實(shí)現(xiàn)前端分頁(yè)頁(yè)碼管理
本文主要介紹了js實(shí)現(xiàn)前端分頁(yè)頁(yè)碼管理的具體方法。具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01
js開(kāi)發(fā)一個(gè)類似ChatGPT的AI應(yīng)用助手
一位創(chuàng)業(yè)朋友想做一個(gè)垂直領(lǐng)域的AI助手,經(jīng)過(guò)一個(gè)月的開(kāi)發(fā)迭代,我們成功上線了第一個(gè)版本,這篇文章分享了開(kāi)發(fā)一個(gè)類似ChatGPT的應(yīng)用的過(guò)程,包括技術(shù)選型、架構(gòu)設(shè)計(jì)和實(shí)戰(zhàn)經(jīng)驗(yàn),實(shí)現(xiàn)了流式響應(yīng)、上下文記憶系統(tǒng)和優(yōu)化提示詞,應(yīng)用得到了用戶的高度評(píng)價(jià)2024-12-12
echarts數(shù)據(jù)可視化實(shí)現(xiàn)多個(gè)柱狀堆疊圖頂部顯示總數(shù)示例
這篇文章主要為大家介紹了echarts實(shí)現(xiàn)多個(gè)柱狀堆疊圖頂部顯示總數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
淺談JS 數(shù)字和字符串之間相互轉(zhuǎn)化的糾紛
下面小編就為大家?guī)?lái)一篇淺談JS 數(shù)字和字符串之間相互轉(zhuǎn)化的糾紛。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10

