在好久一段时间 web 端的 3D 游戏引擎一直是 nothing,但如今却如雨后春笋。javascript
本文介绍使用 babylon.js 的 3D 网页游戏开发流程。css
3D 场景基本概念
建立一个 3D 场景,不论使用何种框架乃至 3D 建模软件,基本元素和流程都是一致的: html
html 中建立 canvasjava
<canvas id="renderCanvas"></canvas>
复制代码
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// 全部光源都有 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)
复制代码
能够自用使用多个光源达到复合效果,好比一个点光源加一个环境光就是不错的组合。react
engine.runRenderLoop(() => {
scene.render()
})
复制代码
这段代码确保场景的每帧更新渲染webpack
<!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>
复制代码
注:web
<!--基础Babylonjs包-->
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<!--loader, 用于加载素材-->
<script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
复制代码
加载方式以最经常使用的主体包和loader包为例:npm
npm i babylonjs babylonjs-loaders
复制代码
import * as BABYLON from 'babylonjs'
import 'babylonjs=loaders'
BABYLON.SceneLoader.ImportMesh( ... )
复制代码
素材获取
除了粒子等少数元素,场景和物体(包含物体的动画)都是外部导入素材。目前最流行的素材统一格式是.gltf
。 获取素材比较经常使用的网站是 sketchfab, Poly 和 Remix3d。三个均可以直接下载 .gltf
格式。编程
素材处理
下载的素材通常由 .gltf
,.bin
和 textures
(皮肤) 文件组成。我的喜欢 .gltf
转 .glb
,将全部文件合成一个 .glb
, 更方便引入。线上转换网址 glb-packer.glitch.me/canvas
素材引入
// .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/#WGZLGJ
ImportMesh
例子: 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(); // 中止
复制代码
BABYLON.Animation
建立的动画片断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
读到。
游戏最重要的互动部分,通常是由几组动画以及触发这些动画的用户交互组成的。
交互方式
能够是html原生的各类事件、React组件的onClick,Babylonjs也提供了本身的事件,使用observable监听。
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();
}
});
复制代码
// 起始位置
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)
}
}
复制代码
dome._mesh.isPickable = false;
复制代码
mesh.parent
。须要专门写一篇介绍
// engine.getFps() 得到当前帧数
const fpsFactor = 15 / engine.getFps();
object.rotation.y += fpsFactor / 5;
复制代码
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)
复制代码
讨论了如何加载素材,动画和交互,完成一个小游戏,如何将全部行为有机串联起来相当重要。
// 使用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 网页游戏就成型了。