PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛
前言
新春佳节将至,为了让大家学(摸)习(鱼)学习, 本期将通过一个简单的烟花案例来介绍一个3d的游戏引擎,预祝大家新年快乐。点击体验。大致效果如下:
babylon
babylon是由微软出品的3d的js游戏引擎。
官网的介绍:
我们的使命是让大家用尽可能简单的方法,创建好玩、好看的全平台运行游戏,Babylon.js建立在javascript和Web标准之上,消除了跨平台的复杂性,使你可以专注于真正重要的事情:那就是为了网络游戏玩家们,创造出令人叹为观止的好玩游戏。
听起来就很牛批
其实跟three.js差不多,都是一个3d的js库。three.js跟专注于渲染,而babylon.js更偏向于做用。babylon.js相比three.js提供了更多应用的api,比如动画渲染(three.js通常需要用tween或requestanimationframe)、碰撞检测等能力。
总结就是同样一个复杂的应用,babylon.js用能比three.js用更少的代码写出来(两者专注点不一样,并不是说babylon就更优秀)。
我们可以用它来实现很复杂的应用,这里分享一下蔬菜土豆泥大佬的作品LocalWar, 这是一款仿cs的web射击游戏,该demo也在babylon官网上有展示。
安装
// 先创建个空项目,个人习惯用vite,也可以选用其他方式
yarn create vite babylon-test
// 安装依赖
npm install @babylonjs/core
hellow babylon
接下来实现做个babylon版的hellow world
html
<body>
<canvas id="canvas" style="width: 100%; height:100%;"/>
<script type="module" src="/main.js"></script>
</body>
main.js
import * as BABYLON from "@babylonjs/core"
const canvas = document.getElementById('canvas')
const engine = new Engine(canvas)
const createScene = () => {
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2.5, 3, new BABYLON.Vector3(0, 0, 0));
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0));
const box = BABYLON.MeshBuilder.CreateBox("box", {});
return scene;
}
const scene = createScene()
engine.runRenderLoop(() => {
scene.render();
})
运行后可以看见一个立方体,也可以使用在线的demo查看
坐标
与three.js不同,babylon.js默认采用的是左手坐标系。
还是使用上面的代码,尝试坐标来查看位置
...
const box = BABYLON.MeshBuilder.CreateBox("box", {});
box.position.set(0,1,0);
...
position对应的三个参数是x、y、z。可以看到当x上调之后,立方体被上移动了
再来修改z试试
...
const box = BABYLON.MeshBuilder.CreateBox("box", {});
box.position.set(0,1,0);
...
从图中可以看出立方体向屏幕内移动了,有变小的感觉。
粒子系统
最简单的粒子
可以直接打开官网的粒子demo, 在原点(0,0,0)位置创建一个发射2000个粒子效果的,并且加上贴图。
const createScene = function () {
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera("ArcRotateCamera", -Math.PI / 2, Math.PI / 2.2, 10, new BABYLON.Vector3(0, 0, 0), scene);
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
// 创建粒子
const particleSystem = new BABYLON.ParticleSystem("particles", 2000);
// 粒子贴图
particleSystem.particleTexture = new BABYLON.Texture("textures/flare.png");
// 发射位置
particleSystem.emitter = new BABYLON.Vector3(0,0, 0);
particleSystem.start();
return scene;
}
粒子的图片尽量选用对称的
以下是我在尝试跑酷中使用福字做粒子特效,由于福字并不是堆成的图案,展现出来的脚印粒子效果就比较糟糕。
新春烟花
初始化界面
简单构造了一个class。
为了方便不在每个项目html里去写canvas,提供了一个创建canvas的方法,_createCanvas,并在构造函数中初始化界面,得到一个夜晚的背景。
class App {
_scene
_canvas
_engine
constructor() {
this._canvas = this._createCanvas();
this._engine = new Engine(this._canvas, true);
this._scene = new Scene(this._engine);
this._scene.clearColor = Color3.Black;
const camera = new ArcRotateCamera("ArcRotateCamera", -1, 1, 100, new Vector3(0, 0, 0), this._scene);
camera.attachControl(this._canvas, true);
this._engine.runRenderLoop(() => {
this._scene.render();
})
}
_createCanvas(id = 'babylon') {
document.documentElement.style["overflow"] = "hidden";
document.documentElement.style.overflow = "hidden";
document.documentElement.style.width = "100%";
document.documentElement.style.height = "100%";
document.documentElement.style.margin = "0";
document.documentElement.style.padding = "0";
document.body.style.overflow = "hidden";
document.body.style.width = "100%";
document.body.style.height = "100%";
document.body.style.margin = "0";
document.body.style.padding = "0";
this._canvas = document.createElement("canvas");
this._canvas.style.width = "100%";
this._canvas.style.height = "100%";
this._canvas.id = id;
document.body.appendChild(this._canvas);
return this._canvas;
}
}
烟花升起
构建方向朝上的粒子系统
import { Mesh, Vector3, Color4, ParticleSystem, Texture, VertexBuffer } from "@babylonjs/core"
export class artifice
{
constructor(scene)
{
this.scene = scene
this.isTop = false;
this.timer = 0;
this.isFired = false;
this.timer1 = 0;
this.textureFirework = "textures/flare.png";
this.posX = 0
this.posY = 0
this.posZ = 0
}
shoot(posX = 0, posY = -20, posZ = 0)
{
let startSphere = new Mesh.CreateSphere("Shoot", 4, 1, this.scene);
startSphere.position = new Vector3(posX, posY, posZ);
startSphere.isVisible = false;
let particleSystem = new ParticleSystem("particles", 350, this.scene);
particleSystem.particleTexture = new Texture(this.textureFirework, this.scene);
particleSystem.emitter = startSphere;
particleSystem.minEmitBox = new Vector3(0, 0, 0);
particleSystem.maxEmitBox = new Vector3(0, 0, 0);
particleSystem.color1 = new Color4(1, 0.8, 1.0, 1.0);
particleSystem.color2 = new Color4(1, 0.5, 1.0, 1.0);
particleSystem.colorDead = new Color4(0, 0, 0.2, 0.5);
particleSystem.minSize = 1;
particleSystem.maxSize = 1;
particleSystem.minLifeTime = 0.5;
particleSystem.maxLifeTime = .5;
particleSystem.emitRate = 350;
particleSystem.blendMode = ParticleSystem.BLENDMODE_ONEONE;
particleSystem.direction1 = new Vector3(0, -2, 0);
particleSystem.direction2 = new Vector3(0, -2, 0);
particleSystem.minEmitPower = 1;
particleSystem.maxEmitPower = 1;
particleSystem.updateSpeed = 0.005;
let bigEnough = false;
let updateFunction = function(particles) {
for (let index = 0; index < particles.length; index++) {
let particle = particles[index];
particle.age += this._scaledUpdateSpeed;
if (particle.age >= particle.lifeTime) {
this.recycleParticle(particle);
index--;
continue;
} else {
if(!bigEnough){
particle.size -= .01;
}
particle.direction.scaleToRef(particleSystem._scaledUpdateSpeed, particleSystem._scaledDirection);
particle.position.addInPlace(particleSystem._scaledDirection);
particleSystem.gravity.scaleToRef(particleSystem._scaledUpdateSpeed, particleSystem._scaledGravity);
particle.direction.addInPlace(particleSystem._scaledGravity);
}
}
};
particleSystem.updateFunction = updateFunction;
particleSystem.start();
this.scene.registerBeforeRender(() => {
if(!this.isFired){
if(!this.isTop){
startSphere.position.y += .5;
if(startSphere.position.y > 30){
this.isTop = !this.isTop;
if (this.isTop ) {
this.posX = startSphere.position.x
this.posY = startSphere.position.y
this.posZ = startSphere.position.z
}
particleSystem.stop();
startSphere.position.x -= .5;
}
} else {
this.timer +=5;
if(this.timer == 125){
for(let i = 0; i < 2; i++){
this.firework();
}
this.isFired = !this.isFired;
}
}
}
});
}
getRandomBetween(Min, Max){
let Range = Max - Min;
let Rand = Math.random();
let num = Min + Math.round(Rand * Range);
return num;
}
...
}
烟花绽放
class artifice{
...
firework()
{
let fountain = new Mesh.CreateSphere("explosion", 4, 1, this.scene);
fountain.isVisible = false;
fountain.position.x = this.posX
fountain.position.y = this.posY
fountain.position.z = this.posZ
let perticleFromVerticesEmitter = fountain;
perticleFromVerticesEmitter.useVertexColors = true;
let verticesPositions = perticleFromVerticesEmitter.getVerticesData(VertexBuffer.PositionKind);
let verticesNormals = perticleFromVerticesEmitter.getVerticesData(VertexBuffer.NormalKind);
let verticesColor = [];
for (let i = 0; i < verticesPositions.length; i += 3){
let vertexPosition = new Vector3(
verticesPositions[i],
verticesPositions[i + 1],
verticesPositions[i + 2]
);
let vertexNormal = new Vector3(
verticesNormals[i],
verticesNormals[i + 1],
verticesNormals[i + 2]
);
let r = Math.random();
let g = Math.random();
let b = Math.random();
let alpha = 1.0;
let color = new Color4(r, g, b, alpha);
verticesColor.push(r);
verticesColor.push(g);
verticesColor.push(b);
verticesColor.push(alpha);
let gizmo = Mesh.CreateBox('gizmo', 0.001, this.scene);
gizmo.position = vertexPosition;
gizmo.parent = perticleFromVerticesEmitter;
this.createParticleSystem(gizmo, vertexNormal.normalize().scale(1), color);
}
perticleFromVerticesEmitter.setVerticesData(VertexBuffer.ColorKind, verticesColor);
}
createParticleSystem(emitter, direction, color)
{
let bigEnough = false;
let particleSystem1 = new ParticleSystem("particles", 500, this.scene);
let updateFunction = function(particles) {
for (let index = 0; index < particles.length; index++) {
let particle = particles[index];
particle.age += this._scaledUpdateSpeed;
if (particle.age >= particle.lifeTime) {
this.recycleParticle(particle);
index--;
continue;
} else {
if(!bigEnough){
particle.size = particle.size +.005;
if(particle.size >= .162){
bigEnough = !bigEnough;
}
}
particle.direction.scaleToRef(particleSystem1._scaledUpdateSpeed, particleSystem1._scaledDirection);
particle.position.addInPlace(particleSystem1._scaledDirection);
particleSystem1.gravity.scaleToRef(particleSystem1._scaledUpdateSpeed, particleSystem1._scaledGravity);
particle.direction.addInPlace(particleSystem1._scaledGravity);
}
}
};
particleSystem1.updateFunction = updateFunction;
particleSystem1.domeRadius = 10;
particleSystem1.particleTexture = new Texture(this.textureFirework, this.scene);
particleSystem1.emitter = emitter; // the starting object, the emitter
particleSystem1.minEmitBox = new Vector3(1, 0, 0); // Starting all from
particleSystem1.maxEmitBox = new Vector3(1, 0, 0); // To...
particleSystem1.color1 = color;
particleSystem1.color2 = color;
particleSystem1.colorDead = new Color4(0, 0, 0, 0.0);
particleSystem1.minSize = .1;
particleSystem1.maxSize = .1;
particleSystem1.minLifeTime = 1;
particleSystem1.maxLifeTime = 2;
particleSystem1.emitRate = 500;
particleSystem1.blendMode = ParticleSystem.BLENDMODE_ONEONE;
particleSystem1.gravity = new Vector3(0, -9.81, 0);
particleSystem1.direction1 = direction;
particleSystem1.direction2 = direction;
particleSystem1.minEmitPower = 10;
particleSystem1.maxEmitPower = 13;
particleSystem1.updateSpeed = 0.01;
particleSystem1.start();
this.scene.registerBeforeRender(() =>{
if(this.timer1 < 300){
this.timer1 += 0.15;
} else {
particleSystem1.stop();
}
});
}
...
}
添加按钮
class App {
...
_createPlayButton() {
const advancedTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI");
const button = Button.CreateSimpleButton("playButton", "发射");
button.width = "150px"
button.height = "40px";
button.color = "white";
button.posi
button.cornerRadius = 20;
button.background = "red";
button.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
button.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
button.onPointerUpObservable.add(()=> {
const x = getRandomBetween(-20,20)
this.play(x)
});
advancedTexture.addControl(button);
}
play(x) {
const firework = new artifice(this._scene)
firework.shoot(x)
}
....
}
点击右下角的发射按钮即可发射烟花
最后
点击本期GIT代码,这次主要是分享的新春烟花的案例,并没有很系统的去讲解babylon,感兴趣的同学可以到babylon官网学习。今年的支付宝五福活动增加了AR看福的玩法,是用阿里自研oasis实现的,编辑器暂时没有对外开放,目前相对来说使用babylon开发更友好一些,babylon也能轻松的实现此类玩法。