<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
筆者利用業餘時間自學了three.js。為了更好的瞭解WebGL以及更熟練的使用three,想模仿原神中的小遊戲“七聖召喚”中的投擲骰子效果,作為首個練習專案~~ 這是堅持寫技術部落格的第二週,也是首篇在掘金寫的文章,人生路遠,仍需遠行。
直接貼程式碼~
/** * 建立場景物件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();
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; })();
至此基本物理世界場景就建立完成。接下來我們需要一個生成骰子的函數。
很簡單,直接使用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,表示以頂點資料為準。好像相差有點大。。不過我們還是得到了一個八面的骰子(沒有高清的元素圖示貼圖,只能勉強看看~)
根據上面弄好的骰子模型生成一個骰子的物理模型。
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~相當不錯
關於如何判斷骰子的頂面,翻遍了谷歌和百度,始終沒有好結果。
發一下牢騷,在網際網路上搜尋的幾乎全是不相關的內容。要麼就是一眾的採集站,要麼一樣的貼文大夥們反覆轉載反覆寫,甚至還有拿開源專案賣錢的。讓我體會了什麼叫“知識庫汙染”。
既然沒有現成的方案,那就只能自己想咯。我們知道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; },
在七聖召喚中每一次重隨都能鎖定骰子,被鎖定的骰子會移動到旁邊並且不會參與重隨。
//滑鼠選中模型 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七聖召喚擲骰子的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45