内容简介:在很久一段时间 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网页采集:解决网页分页与网址超链接问题
- 网页制作用什么软件?制作网页的常用软件工具分享
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Data Mining
Bing Liu / Springer / 2011-6-26 / CAD 61.50
Web mining aims to discover useful information and knowledge from Web hyperlinks, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is not purely an ......一起来看看 《Web Data Mining》 这本书的介绍吧!