内容简介:在很久一段时间 web 端的 3D 游戏引擎一直是 nothing,但现在却如雨后春笋。本文介绍使用 babylon.js 的 3D 网页游戏开发流程。可以自用使用多个光源达到复合效果,比如一个点光源加一个环境光就是不错的组合。
在很久一段时间 web 端的 3D 游戏引擎一直是 nothing,但现在却如雨后春笋。
- Unity (Unity 2018.2 开始已经彻底弃用 js,使用 C#)
- Three.js(比较底层的框架,只是一个渲染器,复杂的游戏互动需要找合适的插件)
- PlayCanvas(可视化编辑器,走设计的 workflow)
- babylon.js (巴比伦 js,是微软开发和维护的 web 端 3D 引擎)
- CopperCube (可视化编辑器类型)
- A-frame (VR 开发专用,html 自定义 tag 形式编程)
本文介绍使用 babylon.js 的 3D 网页游戏开发流程。
1. Get Started
-
3D 场景基本概念
创建一个 3D 场景,不论使用何种框架乃至 3D 建模软件,基本元素和流程都是一致的:
-
html 中创建 canvas
<canvas id="renderCanvas"></canvas> 复制代码
- 初始化 3d 引擎
const canvas = document.getElementById('renderCanvas'); engine = new BABYLON.Engine(canvas, true); // 第二个选项是是否开启平滑(anti-alias) engine.enableOfflineSupport = false; // 除非你想做离线体验,这里可以设为 false 复制代码
- 场景
scene = new BABYLON.Scene(engine); 复制代码
- 相机
// 最常用的是两种相机: // UniversalCamera, 可以自由移动和转向的相机,兼容三端 const camera = new BABYLON.UniversalCamera( 'FCamera', new BABYLON.Vector3(0, 0, 0), scene ) camera.attachControl(this.canvas, true) // 以及ArcRotateCamera, 360度“围观”一个场景用的相机 // 参数分别是alpha, beta, radius, target 和 scene const camera = new BABYLON.ArcRotateCamera("Camera", 0, 0, 10, new BABYLON.Vector3(0, 0, 0), scene) camera.attachControl(canvas, true) 复制代码
- 光源
- 四种光类型
// 点光源 const light1 = new BABYLON.PointLight("pointLight", new BABYLON.Vector3(1, 10, 1), scene) // 方向光 const light2 = new BABYLON.DirectionalLight("DirectionalLight", new BABYLON.Vector3(0, -1, 0), scene) // 聚光灯 const light3 = new BABYLON.SpotLight("spotLight", new BABYLON.Vector3(0, 30, -10), new BABYLON.Vector3(0, -1, 0), Math.PI / 3, 2, scene) // 环境光 const light4 = new BABYLON.HemisphericLight("HemiLight", new BABYLON.Vector3(0, 1, 0), scene) 复制代码
a. 聚光灯的参数用于描述一个锥形的光束聚光灯demo
b. 环境光模拟一种四处都被光照射到的环境环境光demo - 光的色彩
// 所有光源都有 diffuse 和 specular // diffuse 代表光的主体颜色 // specular 代表照在物体上高亮部分的颜色 light.diffuse = new BABYLON.Color3(0, 0, 1) light.specular = new BABYLON.Color3(1, 0, 0) // 只有环境光有groundColor,代表地上反射光的颜色 light.groundColor = new BABYLON.Color3(0, 1, 0) 复制代码
可以自用使用多个光源达到复合效果,比如一个点光源加一个环境光就是不错的组合。
- 渲染 loop
engine.runRenderLoop(() => { scene.render() }) 复制代码
这段代码确保场景的每帧更新渲染
- 基本例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Babylonjs 基础</title> <style> html, body { overflow: hidden; width: 100%; height: 100%; margin: 0; padding: 0; } #renderCanvas { width: 100%; height: 100%; touch-action: none; } </style> <script src="https://cdn.babylonjs.com/babylon.js"></script> <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> </head> <body> <canvas id="renderCanvas"></canvas> <script> const canvas = document.getElementById("renderCanvas") const engine = new BABYLON.Engine(canvas, true) engine.enableOfflineSupport = false /******* 创建场景 ******/ const createScene = function () { // 实例化场景 const scene = new BABYLON.Scene(engine) // 创建相机并添加到canvas const camera = new BABYLON.ArcRotateCamera("Camera", Math.PI / 2, Math.PI / 2, 2, new BABYLON.Vector3(0, 0, 5), scene) camera.attachControl(canvas, true) // 添加光 const light1 = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(1, 1, 0), scene) const light2 = new BABYLON.PointLight("light2", new BABYLON.Vector3(0, 1, -1), scene) // 创建内容,一个球 const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene) return scene } /******* 结束创建场景 ******/ const scene = createScene() // loop engine.runRenderLoop(function () { scene.render() }) // resize window.addEventListener("resize", function () { engine.resize() }) </script> </body> </html> 复制代码
注:
<!--基础Babylonjs包--> <script src="https://cdn.babylonjs.com/babylon.js"></script> <!--loader, 用于加载素材--> <script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> 复制代码
-
npm 包使用 用webpack等打包 工具 的开发环境,可以使用npm包加载Babylonjs 主要有
babylonjs - 主包
babylonjs-loaders - 所有素材的加载loader
babylonjs-gui - GUI 用户交互页面
babylonjs-materials - 一些官方提供的材质
还有
babylonjs-viewer
加载方式以最常用的主体包和loader包为例:
npm i babylonjs babylonjs-loaders 复制代码
import * as BABYLON from 'babylonjs' import 'babylonjs=loaders' BABYLON.SceneLoader.ImportMesh( ... ) 复制代码
-
React.js + Babylon.js
详见官方详细guide, 或者将内容全写在 componentDidMount 就可以了。
2. 素材导入和使用
-
素材获取
除了粒子等少数元素,场景和物体(包含物体的动画)都是外部导入素材。目前最流行的素材统一格式是
.gltf
。 获取素材比较常用的网站是sketchfab,Poly 和Remix3d。三个都可以直接下载.gltf
格式。 -
素材处理
下载的素材一般由
.gltf
,.bin
和textures
(皮肤) 文件组成。个人喜欢.gltf
转.glb
,将所有文件合成一个.glb
, 更方便引入。线上转换网址 glb-packer.glitch.me/ -
素材引入
// .gltf 等文件全放在一个文件夹,比如 /assets/apple BABYLON.SceneLoader.Append("/assets/apple", "apple.gltf", scene, (newScene) => { ... }) // 单个 .glb 文件 BABYLON.SceneLoader.ImportMesh("", "", "www.abc.com/apple.glb", scene, (meshes, particleSystems, skeletons) => { ... }) // promise 版本的 BABYLON.SceneLoader.AppendAsync("/assets/apple", "apple.gltf", scene).then(newScene => { ... }) 复制代码
Append
和 ImportMesh
基本功能都是加载模型,然后渲染到场景 scene 中,不同在于:
ImportMesh
-
选中和处理素材
Append
例子: www.babylonjs-playground.com/#WGZLGJImportMesh
例子: www.babylonjs-playground.com/#JUKXQD
- 要抓取一个素材需要操作的部分和自带动画,需要了解素材的构成,最简单的方式是使用sandbox。比如从 sketchfab 下载素材赛车,解压后将整个文件夹拖入 sandbox,可看到界面 比如要获得左前轮:
// 在callback里 const wheel = newMeshes.find(n => n.id === 'Cylinder.002_0'); // 隐藏轮子 wheel.isVisible = false; // 一般整个素材是 const car = newMeshes[0]; // 可以在scene里寻找动画 const anime = scene.animationGroups[0]; // 播放和停止动画 anime.start(); // 播放 anime.stop(); // 停止 复制代码
3. 创建动画,控制动画
-
动画种类
一共有两类动画: a. 通过
BABYLON.Animation
创建的动画片段b. 在每帧播放的
scene.onBeforeRenderObservable.add
函数中指定个物体参数的每帧的变化
a. 简单的动画,比如物体不停移动
scene.onBeforeRenderObservable.add() { // 球向z轴每帧0.01移动 ball.position.z += 0.01 // 旋转 ball.rotation.x += 0.02 // 沿y轴放大 ball.scaling.y += 0.01 } 复制代码
使用 onBeforeRenderObservable
即可。 涉及多个物体和属性的复杂逻辑动画也适合用此方法,因为可获取每帧下任何属性进行方便计算。
b. 片段形的动画使用 BABYLON.Animation
创建
const ballGrow = new BABYLON.Animation( 'ballGrow', 'scaling', 30, BABYLON.Animation.ANIMATIONTYPE_VECTOR3, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT ); const ballMove = new BABYLON.Animation( 'ballMove', 'position', 30, BABYLON.Animation.ANIMATIONTYPE_VECTOR3, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT ); ballGrow.setKeys([ { frame: 0, value: new BABYLON.Vector3(0.12, 0.12, 0.12) }, { frame: 60, value: new BABYLON.Vector3(3, 3, 3) }, { frame: 120, value: new BABYLON.Vector3(100, 100, 100) }, ]); ballMove.setKeys([ { frame: 0, value: new BABYLON.Vector3(0.5, 0.6, 0) }, { frame: 60, value: new BABYLON.Vector3(0, 0, 0) }, ]); scene.beginDirectAnimation(dome, [ballGrow, ballMove], 0, 120, false, 1, () => { console.log('动画结束'); }); 复制代码
此动画移动并放大物体。API 说明:
// 创建动画 new Animation(名称, 变化的属性, fps, 动画变量数据类型, 循环模式) // 使用动画 scene.beginDirectAnimation(target, animations, 从哪帧, 到哪帧, 循环否?, 播放速度, 结束callback) // 控制动画 const myAnime = scene.beginDirectAnimation( ... ) myAnime.stop() myAnime.start() myAnime.pause() // 暂停 myAnime.restart() // 重开 myAnime.goToFrame(60) // 到某一帧 // 转变成promise myAnime.waitAsync().then( ... ) 复制代码
基本语法如上,一般 60 帧(frame)是一秒。顺带一提,素材自带动画也属于第二类,都是 Animatable,适用一切上述动画操作。所有此类动画可在 scene.animationGroups
读到。
4. 用户交互和事件触发
游戏最重要的互动部分,一般是由几组动画以及触发这些动画的用户交互组成的。
-
交互方式
-
Babylon.js 提供了一系列观察者 observable,用于监听事件,其中最常用的是
a.
scene.onBeforeRenderObservable
每帧监听b.
scene.onPointerObservable
监听点击/拖拽/手势/键盘等scene.onKeyboardObservable.add(kbInfo => { switch (kbInfo.type) { case BABYLON.KeyboardEventTypes.KEYDOWN: console.log('按键: ', kbInfo.event.key); break; case BABYLON.KeyboardEventTypes.KEYUP: console.log('抬起按键: ', kbInfo.event.keyCode); break; } }); scene.onPointerObservable.add(pointerInfo => { switch (pointerInfo.type) { case BABYLON.PointerEventTypes.POINTERDOWN: console.log('按下'); break; case BABYLON.PointerEventTypes.POINTERUP: console.log('抬起'); break; case BABYLON.PointerEventTypes.POINTERMOVE: console.log('移动'); break; case BABYLON.PointerEventTypes.POINTERWHEEL: console.log('滚轮'); break; case BABYLON.PointerEventTypes.POINTERTAP: console.log('点击'); break; case BABYLON.PointerEventTypes.POINTERDOUBLETAP: console.log('双击'); break; } }); 复制代码
observable 实例有以下方法
.add
添加一个 observable.remove
删除一个 observable.addOnce
添加一个 observable, 并在执行一次后 remove.hasObservers
判断是否有某个 observable.clear
清除所有的 observable -
第一类动画的触发(即在 gameloop 里执行的动画)
scene.onBeforeRenderObservable.add() { gameloop() } function gameloop() { ... } 复制代码
gameloop 中的渲染逻辑会在每一帧执行一次,所以只需要通过对一个 boolean 变量的改变就能完成触发事件
let startGame = false // 可以使用原生的,React里可以直接用onClick document.addEventListener('click', () => { startGame = true }) // 也可以使用Babylonjs 的pointerObservable scene.onPointerObservable.add((info) => { if(info.type === 32) { startGame = true } } function gameloop() { if(startGame){ ball.rotation.x += 0.01 ball.position.y += 0.02 } } 复制代码
- 第二类动画的触发 (动画片段)
// 此时不能在 gameloop 里直接播放动画 function moveBall() { scene.beginDirectAnimation( ... ) } function gameloop() { if(startGame){ moveBall() } } 复制代码
上面的代码会造成游戏开始后每帧都触发一遍 moveBall()
, 这显然不是我们希望的。
如果触发是鼠标/键盘,显然可以使用
scene.onPointerObservable.add((info) => { if(info.type === 32) { moveBall() } } 复制代码
但也有别的触发情况(比如相机靠近,属性变化等),此时可以注册一个 onBeforeRenderObservable
并在触发条件达成时执行 animation 并 remove observable
const observer = scene.onBeforeRenderObservable.add(() => { if (scene.onBeforeRenderObservable.hasObservers && startGame) { scene.onBeforeRenderObservable.remove(observer); moveBall(); } }); 复制代码
5. 如何用鼠标选取 3D 场景物体?
- 普适的解决方式是rayCaster 给定起始点,方向和长度,我们能画一条线段,称之为 ray
// 起始位置 const pos = new BABYLON.Vector3(0, 0, 0); // 方向 const direction = new BABYLON.Vector3(0, 1, 0); const ray = new BABYLON.Ray(pos, direction, 50); 复制代码
Babylonjs 提供了方便的 api,检验一条 ray 是否触碰到场景中的物体,以及触碰到的物体信息const hitInfo = scene.pickWithRay(ray); console.log(hitInfo); // {hit: true, pickedMesh: { mesh信息 }} 复制代码
由于 ray 是不可见的,有时候不方便调试, 提供 RayHelper,用于画出 RayBABYLON.RayHelper.CreateAndShow(ray, scene, new BABYLON.Color3(1, 1, 0.1)); 复制代码
- 判断鼠标是否点击到物体,有直接方法
scene.onPointerObservable.add((info) => { if(info.pickInfo.hit === true) { console.log(info.pickInfo.pickedMesh) } } 复制代码
- 只有特定物体能被选中
将不能选中的 mesh 的 isPickable 属性设置为 false 即可。注意某些元素本身不是 mesh,如 360 图元素需要dome._mesh.isPickable = false; 复制代码
- 只选中了部分物体咋办
对于由多个 mesh 组成的素材,这是常常发生的事。需要用名称、id 判断并寻找到最上层的父节点。父节点mesh.parent
。
7. 粒子效果
需要专门写一篇介绍
8. 走过的一些坑和探索的一些解决
- 如何确保动画匀速:
// engine.getFps() 获得当前帧数 const fpsFactor = 15 / engine.getFps(); object.rotation.y += fpsFactor / 5; 复制代码
- Parent
- 当你想为射击游戏创建一个枪管时,希望枪管一直不变的显示在屏幕右下方,如此demo
这时候需要使用 parent 将枪管 mesh 的 parent 设置为 camera。 - parent还常用于寻找素材的主节点,以及将两个物体绑定。child 的 position、rotation、scaling 都会随着 parent 的变动而同步变动。
- 360 图 babylonjs 提供了现成方法
BABYLON.PhotoDome
const dome = new BABYLON.PhotoDome( "testdome", "./textures/360photo.jpg", { resolution: 32, size: 1000 }, scene ) 复制代码
- 物体显示和隐藏
显示和隐藏一个物体时,需要注意物体是一个 transformNode
还是 mesh
, 引入的素材往往会用一个 transformNode
作为一堆子 mesh
的 parent,此时使用 isVisible
来显隐是无用的。
// 隐藏 mesh.isVisible = false // 显示 mesh.isVisible = true // 隐藏 transformNode.setEnabled(false) // 显示 transformNode.setEnabled(true) 复制代码
9. 项目串联
讨论了如何加载素材,动画和交互,完成一个小游戏,如何将所有行为有机串联起来至关重要。
// 使用Promise.all 和 ImportMeshAsync 加载所有素材 Promise.all([loadAsset1(), loadAsset2(), loadAsset3()]).then(() => { createParticles() // 创建粒子 createSomeMeshes() // 创建其他mesh // 进场动画 SomeEntryAnimation().waitAsync().then(() => { // 开始游戏 game() }) }) // 游戏逻辑 const game = () => { // 只执行一遍的动画, 并在完成时执行gameReady, 确定可以开始 playAnimeOnTrigger(trigger, () => anime(gameReady)) // 其他只执行一次的流程 } const gameReady = () => { // 显示开始按钮,可以是html的button,也可以是Babylonjs的GUI(暂不讨论) showStartBtn() ... } // 点击start,开始游戏,每次游戏执行 const startGame = () => { const gameStarted = true // 一类动画全写在gameLoop, registerBeforeRender 和 onBeforeRenderObservable.add 作用相同 scene.registerBeforeRender(gameLoop) // 和时间相关的游戏逻辑,比如计时,定时播放的动画 const interval = window.setInterval(gameLogic, 500) // 每次游戏执行一遍的动画,动画本身可以是循环和串联 playAnimeOnTrigger(trigger1, anime1) playAnimeOnTrigger(trigger2, anime2) } // 触发逻辑, 比如粒子效果,也可以写在外面,通过 gameStarted 变量判断 hitEffect() { if(gameStarted) { showParticles() } } const stopGame = () => { const gameStarted = false scene.unregisterBeforeRender(gameLoop) window.clearInterval(interval) ... } // 常用方法:监听变量,变量变化时执行动画并结束监听 const playAnimeOnTrigger = (trigger, anime) => { const observer = scene.onBeforeRenderObservable.add( () => { if (scene.onBeforeRenderObservable.hasObservers && trigger) { scene.onBeforeRenderObservable.remove(observer) anime() } }) } 复制代码
个人总结的简单写法大致如此。至此,一个简单的 3D 网页游戏就成型了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- [译] 详细教程:如何使用代理服务器进行网页抓取?
- 用CSS实现分页符,控制Web网页打印时自动强制分页:page-break-after教程
- 防止网页被其他网页iframe嵌套的思考与实现
- 响应式网页设计–css设置网页字体大小自适应
- R网页采集:解决网页分页与网址超链接问题
- 网页制作用什么软件?制作网页的常用软件工具分享
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
算法艺术与信息学竞赛
刘汝佳 / 清华大学出版社 / 2004-1 / 45.00元
《算法艺术与信息学竞赛》较为系统和全面地介绍了算法学最基本的知识。这些知识和技巧既是高等院校“算法与数据结构”课程的主要内容,也是国际青少年信息学奥林匹克(IOI)竞赛和ACM/ICPC国际大学生程序设计竞赛中所需要的。书中分析了相当数量的问题。 本书共3章。第1章介绍算法与数据结构;第2章介绍数学知识和方法;第3章介绍计算机几何。全书内容丰富,分析透彻,启发性强,既适合读者自学,也适合于课......一起来看看 《算法艺术与信息学竞赛》 这本书的介绍吧!