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


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

差不多就是這樣,為了方便調試,我暫時把它渲染成了可見的。
判斷頂面,只需要獲取它們中最高的那一個即可
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();
//創(chuàng)建射線投射器對象
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); // 監(jiān)聽窗口鼠標單擊事件
move_dice函數(shù)
// 移動骰子到相應位置
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));
}
}
},
按照慣例測試一下。

結
基本上就差不多完工了,但是還有很多細節(jié)可以慢慢打磨,更多關于three.js七圣召喚擲骰子的資料請關注腳本之家其它相關文章!
相關文章
javascript 用記憶函數(shù)快速計算遞歸函數(shù)
摘自《JavaScript: The Good Parts》,作為讀書筆記備用。對于追求執(zhí)行效率的朋友可以參考下。2010-03-03
JavaScript數(shù)字數(shù)組的13個實用小技巧
數(shù)組是JS最常見的一種數(shù)據(jù)結構,咱們在開發(fā)中也經(jīng)常用到,在這篇文章中,提供一些小技巧,幫助咱們提高開發(fā)效率,這篇文章主要給大家分享介紹了關于JavaScript數(shù)字數(shù)組的13個實用小技巧,需要的朋友可以參考下2023-11-11
基于JavaScript實現(xiàn)百葉窗動畫效果不只單純flas可以實現(xiàn)
看到這種百葉窗效果的動畫,以為是用flash做的,下面通過本文給大家介紹基于JavaScript實現(xiàn)百葉窗動畫效果,需要的朋友參考下吧2016-02-02

