3D 游戏的 javascript 框架:

在很久一段时间 web 端的 3D 游戏引擎一直是 nothing,但现在却如雨后春笋。

  1. Unity (Unity 2018.2 开始已经彻底弃用 js,使用 C#)
  2. Three.js(比较底层的框架,只是一个渲染器,复杂的游戏互动需要找合适的插件)
  3. PlayCanvas(可视化编辑器,走设计的 workflow)
  4. babylon.js (巴比伦 js,是微软开发和维护的 web 端 3D 引擎)
  5. CopperCube (可视化编辑器类型)
  6. A-frame (VR 开发专用,html 自定义 tag 形式编程)

本文介绍使用 babylon.js 的 3D 网页游戏开发流程。

1. Get Started

  1. 3D 场景基本概念
    创建一个 3D 场景,不论使用何种框架乃至 3D 建模软件,基本元素和流程都是一致的:

  2. html 中创建 canvas

<canvas id="renderCanvas"></canvas>
复制代码
  1. 初始化 3d 引擎
const canvas = document.getElementById('renderCanvas');
engine = new BABYLON.Engine(canvas, true); // 第二个选项是是否开启平滑(anti-alias)
engine.enableOfflineSupport = false; // 除非你想做离线体验,这里可以设为 false
复制代码
  1. 场景
scene = new BABYLON.Scene(engine);
复制代码
  1. 相机
// 最常用的是两种相机:
// 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)
复制代码
  1. 光源
  • 四种光类型

    // 点光源
    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)
    复制代码

可以自用使用多个光源达到复合效果,比如一个点光源加一个环境光就是不错的组合。

  1. 渲染 loop
engine.runRenderLoop(() => {scene.render()
})
复制代码

这段代码确保场景的每帧更新渲染

  1. 基本例子:
<!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)// 创建相机并添加到canvasconst 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()// loopengine.runRenderLoop(function () {scene.render()})// resizewindow.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>
复制代码
  1. npm 包使用 用webpack等打包工具的开发环境,可以使用npm包加载Babylonjs 主要有
    babylonjs - 主包
    babylonjs-loaders - 所有素材的加载loader
    babylonjs-gui - GUI 用户交互页面
    babylonjs-materials - 一些官方提供的材质
    还有
    babylonjs-post-process
    babylonjs-procedural-textures
    babylonjs-serializers
    babylonjs-viewer

加载方式以最常用的主体包和loader包为例:

npm i babylonjs babylonjs-loaders
复制代码
import * as BABYLON from 'babylonjs'
import 'babylonjs=loaders'BABYLON.SceneLoader.ImportMesh( ... )
复制代码
  1. React.js + Babylon.js
    详见官方详细guide, 或者将内容全写在 componentDidMount 就可以了。

2. 素材导入和使用

  • 素材获取
    除了粒子等少数元素,场景和物体(包含物体的动画)都是外部导入素材。目前最流行的素材统一格式是.gltf。 获取素材比较常用的网站是 sketchfab, Poly 和 Remix3d。三个都可以直接下载 .gltf 格式。

  • 素材处理
    下载的素材一般由 .gltf.bintextures (皮肤) 文件组成。个人喜欢 .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 => {...
    })
    复制代码

    AppendImportMesh 基本功能都是加载模型,然后渲染到场景 scene 中,不同在于:

    1. 回调函数的参数,前者是场景,后者是 mesh,粒子和骨架
    2. 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(); // 停止
    复制代码

    整个例子

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. 用户交互和事件触发

游戏最重要的互动部分,一般是由几组动画以及触发这些动画的用户交互组成的。

  • 交互方式
    可以是html原生的各种事件、React组件的onClick,Babylonjs也提供了自己的事件,使用observable监听。

  • 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.01ball.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,用于画出 Ray

    BABYLON.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
  1. 当你想为射击游戏创建一个枪管时,希望枪管一直不变的显示在屏幕右下方,如此demo
    这时候需要使用 parent 将枪管 mesh 的 parent 设置为 camera。
  2. parent还常用于寻找素材的主节点,以及将两个物体绑定。child 的 position、rotation、scaling 都会随着 parent 的变动而同步变动。
  3. 360 图 babylonjs 提供了现成方法 BABYLON.PhotoDome
const dome = new BABYLON.PhotoDome("testdome","./textures/360photo.jpg",{resolution: 32,size: 1000},scene
)
复制代码

360图的 demo

  1. 物体显示和隐藏

显示和隐藏一个物体时,需要注意物体是一个 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 = falsescene.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 网页游戏就成型了。

作者:FateRiddle
链接:https://juejin.im/post/5be9051751882516b9377792
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

基于babylon.js的3D网页游戏从零教程相关推荐

  1. 【中英双语】使用JavaScript 及Three.js开发3D网页游戏

    此教程共2.0小时,中英双语字幕,画质清晰无水印,源码附件全 课程英文名:3D Web Game Development with JavaScript and Three.js 下载地址 百度网盘地 ...

  2. html九图拼图游戏代码,基于Vue.js实现数字拼图游戏

    先来看看效果图: 功能分析 当然玩归玩,作为一名Vue爱好者,我们理应深入游戏内部,一探代码的实现.接下来我们就先来分析一下要完成这样的一个游戏,主要需要实现哪些功能.下面我就直接将此实例的功能点罗列 ...

  3. 如何实现3D网页游戏?

    现在目前国内的网页游戏大多采用平面的效果设计,人物看起来很不习惯,如何把网页游戏做成3D界面,或者是仿3D的界面也可以.希望高手来指导.

  4. Unity 3D网页游戏 Demo 展示

    原址:http://blog.csdn.net/trcj1/article/details/6187003 2011 年,网页 3D 这一网游开发新趋势逐渐浮出水面, Unity 作为浏览器及移动设备 ...

  5. 基于Unity3D开发的3D小游戏牧师与魔鬼

    3D小游戏牧师与魔鬼 作业要求 编程实践,3D小游戏:牧师与魔鬼 列出游戏中提及的事物(Objects) 用表格列出玩家动作表(规则表),注意,动作越少越好 请将游戏中对象做成预制,并在 GenGam ...

  6. 网页游戏开发入门教程

    Posted by Gamelook on 2009.09.06 一.简单的程序框架.webgame程序构成: 三大部分. 第一是数据流程.第二是程序.第三是美术. 其中,数据流程包括了功能.也只有在 ...

  7. 网页游戏开发入门教程二(游戏模式+系统)

    2019独角兽企业重金招聘Python工程师标准>>> 一.游戏模式 目前webgame游戏模式大体上可以分为以下四类: 1.玩家拥有一个城市,不断的升级城市内建筑,建筑可以自动获得 ...

  8. 游戏开发人员眼中的Unity 3D网页游戏測评报告

    眼下.能够实现3D页游的主流技术有Silverlight.XNA.Flash.HTML5和Unity3D. 当中.Unity3D作为一款专注于3D游戏的浏览器插件.最近在国内外页游产品线骚动异常:本人 ...

  9. 游戏开发者眼中的Unity 3D网页游戏测评报告

    目前,能够实现3D页游的主流技术有Silverlight.XNA.Flash.HTML5和Unity3D.其中,Unity3D作为一款专注于3D游戏的浏览器插件,近期在国内外页游产品线骚动异常:本人从 ...

最新文章

  1. std::string用法总结
  2. java lambda函数_最常用的 Java 8 中的 Lambda 函数(项目中实用笔记)
  3. C#实现局域网UDP广播--
  4. 核心编程之十一章的11-9
  5. 【戴嘉乐】(进阶)基于IPFS和Ngrok构建自维护资源网关
  6. Android Studio(6)---编写APP
  7. oracle导出表统计信息,Oracle统计信息的导出与导入
  8. QQ 新增“语音进度条”;滴滴顺风车负责人道歉;华为研发费将超 1000 亿美元 | 极客头条...
  9. 对象可以创建数组吗_电脑零基础可以学习创建网站吗?
  10. Kylin 对维度表的的要求
  11. 每日算法系列【LeetCode 376】摆动序列
  12. W,b的初始化和几种激活函数
  13. 搭建VSFTPD服务器 虚拟用户认证方式
  14. 正则修饰符、用敏感词库替换
  15. 编译安装vlmcsd
  16. latch: cache buffers chains问题分析
  17. prisma2.0和nexus搭建graphql后端(1)—prisma2.0
  18. 基于opencv第三方视觉库,通过内网IP调用手机摄像头,实现人脸识别与图形监测
  19. vue常用的时间、手机号等的格式化方法
  20. 在word2016中使用宏(VBA)来自动设置表格行高

热门文章

  1. NEON优化:性能优化常见问题QA
  2. java ppt转换为html5,Apache POI PPT - PPT转换为图片
  3. S19文件解析实现代码
  4. qml tableview
  5. GrayLog日志平台
  6. MYSQL 获取当前日期及日期格式,和常用时间转换函数
  7. 【数据分析之道】数据分析导读
  8. 【学习笔记】stm32+ESP8266+阿里云+云智能APP
  9. 二本计算机专业可以考电网,不能小瞧的二本大学,学生毕业后直接进入电网?不用羡慕公务员...
  10. swagger-ui导出word接口文档