babylon游戏引擎入门 - 新春烟花

1,858 阅读4分钟

PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛

前言

新春佳节将至,为了让大家学(摸)习(鱼)学习, 本期将通过一个简单的烟花案例来介绍一个3d的游戏引擎,预祝大家新年快乐。点击体验。大致效果如下:

image.png

babylon

babylon是由微软出品的3d的js游戏引擎。

官网的介绍:

我们的使命是让大家用尽可能简单的方法,创建好玩、好看的全平台运行游戏,Babylon.js建立在javascript和Web标准之上,消除了跨平台的复杂性,使你可以专注于真正重要的事情:那就是为了网络游戏玩家们,创造出令人叹为观止的好玩游戏。

image.png

听起来就很牛批

image.png

其实跟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官网上有展示。

image.png

安装

// 先创建个空项目,个人习惯用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查看

image.png

坐标

与three.js不同,babylon.js默认采用的是左手坐标系。 image.png

还是使用上面的代码,尝试坐标来查看位置

 ...
 const box = BABYLON.MeshBuilder.CreateBox("box", {});
 box.position.set(0,1,0);
 ...
 

position对应的三个参数是x、y、z。可以看到当x上调之后,立方体被上移动了 image.png

再来修改z试试

 ...
 const box = BABYLON.MeshBuilder.CreateBox("box", {});
 box.position.set(0,1,0);
 ...
 

从图中可以看出立方体向屏幕内移动了,有变小的感觉。 image.png

粒子系统

最简单的粒子

可以直接打开官网的粒子demo, 在原点(0,0,0)位置创建一个发射2000个粒子效果的,并且加上贴图。

image.png

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;
}

image.png

粒子的图片尽量选用对称的

以下是我在尝试跑酷中使用福字做粒子特效,由于福字并不是堆成的图案,展现出来的脚印粒子效果就比较糟糕。

final.gif

新春烟花

初始化界面

简单构造了一个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;
    }
}

image.png

烟花升起

构建方向朝上的粒子系统

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;
    }

    ...
}

image.png

烟花绽放

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();
			}
		});
	}
    ...
}

image.png

添加按钮


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)
    }
    ....
}

点击右下角的发射按钮即可发射烟花 image.png

最后

点击本期GIT代码,这次主要是分享的新春烟花的案例,并没有很系统的去讲解babylon,感兴趣的同学可以到babylon官网学习。今年的支付宝五福活动增加了AR看福的玩法,是用阿里自研oasis实现的,编辑器暂时没有对外开放,目前相对来说使用babylon开发更友好一些,babylon也能轻松的实现此类玩法。