JavaScript Framework for 3D Games: The Web-side 3D game engine has been Nothing for a long time, but now it’s springing up.

Unity (Unity 2018.2 has completely abandoned JS, using C#) 3. JS (relatively low level framework, just a renderer, complex game interaction needs to find the appropriate plug-in) PlayCanvas (visual editor, Workflow Babylon.js (BabylonJS, A Web-side 3D engine developed and maintained by Microsoft) CopperCube (Visual Editor type) A-Frame (dedicated to VR development, HTML custom TAG form programming)

This article describes the development process of 3D web games using Babylon.js.

1. Get Started

  1. To create a 3D scene, the basic elements and process are the same regardless of the framework or 3D modeling software used:
  2. Create Canvas in HTML
<canvas id="renderCanvas"></canvas> copy code
  1. Initialize the 3D engine
const canvas = document.getElementById('renderCanvas'); engine = new BABYLON.Engine(canvas, true); / / the second option is whether smooth (anti - alias) engine. EnableOfflineSupport = false; // Set this to false unless you want an offline experience
  1. scenario
scene = new BABYLON.Scene(engine); Copy the code
  1. The camera
// There are two types of cameras most commonly used: // UniversalCamera, a camera that can move and turn freely, Const Camera = new Babylon. universalCamera (' fCamera ', new Babylon. Vector3(0, 0, 0), Scene camera. AttachControl (this.canvas, true) // and ArcroTateCamera, 360 degree "view" a scene using the camera // parameters alpha, beta, radius, Target and Scene Const Camera = new Babylon. ArcroTateCamera ("Camera", 0, 0, 10, new Babylon.Vector3(0, 0, 0), Scene) Camera. AttachControl (canvas, true) Copy the code
  1. The light source
  • Four types of light

    // 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), Spotlight (" Spotlight ", new Babylon.Vector3(0, 30, -10), New Babylon.Vector3(0, 30, -10)) -1, 0), Math.pi / 3, 2, Scene) // Ambient Light Const Light4 = New Babylon.HemisphericLight(" Hemilight ", new Babylon.Vector3(0, 1, 0), Scene) copy code

    A. The parameters of the spotlight are used to describe a tapered beam spotlight demo b. Ambient light simulation An ambient light demo that is illuminated everywhere

  • The colour of the light

    Diffuse = New Babylon.Color3(0, 0, 0); // Diffuse represents the main color of light; 1) Light. specular = new Babylon.Color3(1, 0, 0) // Light. groundColor = new Babylon.Color3(0, 1, 0) Copy the code

You can use multiple light sources to achieve the composite effect, such as a point light source and an ambient light is a good combination.

  1. Render loop
Engine. RunRenderLoop (() => {scene.render()}) copy code

This code ensures that every frame of the scene is rendered

  1. Basic examples:
<! 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 base </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 / * * * * * * * * * * * * * create scene/const createScene = function () {/ / instantiate the scene const scene = New Babylon. Scene(engine) // Create Camera and Add Camera to 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) // Add 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) A ball const sphere = BABYLON. MeshBuilder. CreateSphere (" sphere ", {diameter: 2}, Const scene = createScene() // Loop engine. RunRenderLoop (function () {const const scene = createScene(); scene.render() }) // resize window.addEventListener("resize", Function () {engine.resize()}) </script bb0 </body> </ HTML >

Note:

<! - based Babylonjs package -- -- > < script SRC = "https://cdn.babylonjs.com/babylon.js" > < / script > <! - loader used to load material -- -- > < script SRC = "https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js" > < / script > duplicate code
  1. The NPM package uses a development environment with packaging tools such as Webpack, You can use the NPM package to load BabylonJS. There are BabylonJS – main package BabylonJS -loaders – all material loader BabylonJS-GUI – user interaction page BabylonJS-materials – Post-process BabylonJS-Procedural Textures BabylonJS-Serializers BabylonJS-Viewer – Textures BabylonJS-Post-process BabylonJS-Procedural Textures BabylonJS-Serializers BabylonJS-Viewer

Loading takes the most common body and loader packages as an example:

NPM I babylonjs babylonjs-loaders copy the code
import * as BABYLON from 'babylonjs' import 'babylonjs=loaders' BABYLON.SceneLoader.ImportMesh( ... ) Copy the code
  1. Js + Babylon.js See the official detailed guide or write it all in componentDidMount.

2. Material import and use

  • Material gain

    With the exception of a few elements such as particles, scenes and objects (which contain animations of objects) are externally imported material. At present the most popular material unified format is.gltf. A common site for getting material issketchfab.Poly 和 Remix3d. All three can be downloaded directly.gltfFormat.
  • Material handling

    The downloaded material is usually from.gltf..bintextures(skin) file composition. People like to.gltf.glb, synthesize all the files into one.glb, more convenient to introduce. Online conversion siteglb-packer.glitch.me/
  • Material is introduced into

    //.gltf and other files are all in one folder, Such as/assets/apple BABYLON. SceneLoader. Append ("/assets/apple ", "apple. GLTF," scene, (newScene) = > {... }) / / individual. GLB file BABYLON. SceneLoader. ImportMesh (" ", ""," www.abc.com/apple.glb ", scene, (meshes, particleSystems. skeletons) => { ... }) / / promise version of BABYLON SceneLoader. AppendAsync ("/assets/apple ", "apple. GLTF", the scene). Then (newScene = > {... }) copy the code

    The basic function of Append and ImportMesh is to load the model and then render it to the scene. The differences are as follows:

    1. The parameters of the callback function are the scene and the mesh, particles, and skeleton
    2. ImportMeshThe first argument can be used to specify that a portion of the material should be imported, and an empty string should be imported altogether.
  • Select and process the material

    AppendExample:www.babylonjs-playground.com/#WGZLGJ

    ImportMeshExample:www.babylonjs-playground.com/#JUKXQD

    To grab a material that needs to operate parts and its own animation, you need to understand the composition of the material, the simplest way is to usesandbox. For example, download materials from SketchfabThe car, drag the entire folder into the sandbox to see the interface



    For example, to get the left front wheel:

// const wheel = newMeshes. Find (n => n.id === 'Cylinder.002_0'); // Hide wheel wheel.isVisible = false; // const car = newMeshes[0]; Const anime = scene.animationGroups[0]; // We can look for const anime = scene.animationGroups[0]; // Play and stop animation anime.start(); / / play anime. Stop (); // Stop copying code

The whole case

3. Create animations and control animations

  • Animation types

    There are two types of animation: a. PassBABYLON.AnimationCreate animation segments

    B. Play in each framescene.onBeforeRenderObservable.addVariation of the object parameters specified in the function per frame

A. Simple animations, such as objects moving, rotating, and zooming

Scene. OnBeforeRenderObservable. The add () {/ / the ball to the z axis movement ball. 0.01 per frame position. The z + = 0.01 / / rotating ball rotation. X + = 0.02 / / enlarge along the y axis Y += 0.01} copy the code

Use OnBeForeRenderObservable. Complex logical animations involving multiple objects and properties are also suitable for this approach, as any property under each frame can be retrieved for easy calculation.

B. The fragment Animation is created using 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, () = > {the console. The log (' end of the animation '); }); Copy the code

This animation moves and zooms in on the object. API specification:

/ / create a new Animation Animation (name, change the properties of the FPS, Animation variable data type, circulation pattern) / / use Animation scene. BeginDirectAnimation (target, animations, from which the frame, where the frame, Cycle? End, playback speed, the callback) / / control animation const myAnime = scene. BeginDirectAnimation (...). MyAnime. stop() myAnime.start() myAnime.pause() // MyAnime. restart() // Restart () myAnime. gotoFrame (60) // To a certain frame // Change to Promise  myAnime.waitAsync().then( ... ) Copy the code

Basic syntax as above, generally 60 frames (frame) is one second. Incidentally, the second category is the Animatable, which works for all of the animation operations described above. All of these animations can be read at scene.animationgroups.

4. User interaction and event triggering

The most important interactive part of a game is usually made up of several sets of animations and the user interactions that trigger those animations.

  • The interaction can be a variety of HTML native events, the React component’s onClick, BabylonJS also provides its own events, using Observable listening.
  • observable

    Babylon.js provides a set of observer observables that listen for events, One of the most commonly used is a. scene. OnBeforeRenderObservable per frame to monitor b. scene. OnPointerObservable click/drag/gestures/keyboard, etc

    scene.onKeyboardObservable.add(kbInfo => { switch (kbInfo.type) { case BABYLON.KeyboardEventTypes.KEYDOWN: Console. log(' Keys: ', kbinfo.event. key); break; Case BABYLON. KeyboardEventTypes. KEYUP: the console log (' raise keys: 'kbInfo. Event. KeyCode); break; }}); scene.onPointerObservable.add(pointerInfo => { switch (pointerInfo.type) { case BABYLON.PointerEventTypes.POINTERDOWN: The console. The log (' press '); break; Case BABYLON. PointerEventTypes. POINTERUP: the console log (' lift '); break; Case BABYLON. PointerEventTypes. POINTERMOVE: the console log (' mobile '); break; Case BABYLON. PointerEventTypes. POINTERWHEEL: the console log (' scroll '); break; Case BABYLON. PointerEventTypes. POINTERTAP: the console log (' click '); break; Case BABYLON. PointerEventTypes. POINTERDOUBLETAP: the console log (' double '); break; }}); Copy the code

    Add an Observable. Remove an Observable. Add Once to add an Observable. After one run, remove. hasObservers determines whether one of the Observables. Clear clears all Observables

  • Triggering of the first type of animation (animations executed in Gameloop)
scene.onBeforeRenderObservable.add() { gameloop() } function gameloop() { ... } Copy code

The rendering logic in GameLoop is executed once per frame, so it only takes a change to a Boolean variable to trigger the event

Let startGame = false // you can use native, The React in can directly use the onClick document. AddEventListener (' click ', () = > {the startGame = true}) / / can also use Babylonjs pointerObservable scene. OnPointerObservable. Add ((info) = > {the if (info. Type } function gameloop() {if(startGame){ball.rotation.x += 0.01 ball.position.y += 0.02}} Copy the code
  • Trigger of the second type of animation (animation segment)
/ / at this point cannot be directly the animation function in the gameloop moveBall () {scene. BeginDirectAnimation (...). } function gameloop() {if(startGame){moveBall()}} copy code

The above code will cause moveBall() to fire once every frame after the game starts, which is obviously not what we want.

If the trigger is mouse/keyboard, it is obviously available

Scene. OnPointerObservable. Add ((info) = > {the if (info. Type = = = 32) {moveBall ()}} to copy code

However, there are other trigger cases (such as camera proximity, property change, etc.). In this case, you can register an OnBeForeRenderObservable and execute the animation and remove the Observable when the trigger condition is met

const observer = scene.onBeforeRenderObservable.add(() => { if (scene.onBeforeRenderObservable.hasObservers && startGame) { scene.onBeforeRenderObservable.remove(observer); moveBall(); }}); Copy the code

5. How to select objects in 3D scene with the mouse?

  • And the common solution is rayCaster so given the starting point, the direction and the length, we can draw a line segment called ray

    // Start position const pos = new Babylon.Vector3(0, 0, 0); // const direction = new Babylon.Vector3(0, 1, 0); const ray = new BABYLON.Ray(pos, direction, 50); Copy the code

    BabylonJS provides a convenient API to verify whether a ray touches objects in the scene, as well as information about the objects it touches

    const hitInfo = scene.pickWithRay(ray); console.log(hitInfo); // {hit: true, pickedMesh: {mesh information}} copy code

    Since Ray is invisible and sometimes inconvenient to debug, provide RayHelper for drawing Ray

    BABYLON. RayHelper. CreateAndShow (ray, scene, new BABYLON. Color3 (1, 1, 0.1)); Copy the code
  • There are direct methods to determine whether the mouse has clicked on an object

    scene.onPointerObservable.add((info) => { if(info.pickInfo.hit === true) { console.log(info.pickInfo.pickedMesh) } } Copy the code
  • Only certain objects can be selected. Set the isPickable property of the unselected mesh to false. Note that some elements are not in the mesh themselves, such as the 360 diagram element required

    dome._mesh.isPickable = false; Copy the code
  • Only selected part of the object what to do

    This is often the case for materials that consist of multiple mesh. You need to identify and find the top parent node by name and id. The parent nodemesh.parent.

7. Particle effects

You need to write an introduction

8. Walking through some pits and exploring some solutions

  • How to ensure uniform animation speed:
// engine.getfps () const fpsFactor = 15 / engine.getfps (); object.rotation.y += fpsFactor / 5; Copy the code
  • Parent
  1. When you want to create a gun barrel for a shooter, you want the gun barrel to remain on the bottom right of the screen, so the demo needs to use Parent to set the parent of the gun barrel mesh to Camera.
  2. Parent is also used to find the primary node of the material and to bind two objects. Child’s position, rotation, and scaling will all change synchronously with parent’s change.
  3. 360 Figure BabylonJS provides a ready-made methodBABYLON.PhotoDome
Const dome = new Babylon. PhotoDome("testdome", "./texture /360photo.jpg", {resolution: 32, size: 1000}, scene) Copy the code

The 360 figure of the demo

  1. Object display and hiding

When displaying and hiding an object, you need to pay attention to whether the object is a TransformNode or a mesh. The incoming material will often use a TransformNode as the parent of a bunch of submesh. It is useless to use isVisible to display and hide an object.

// Hide Mesh. IsVisible = false // Show Mesh. IsVisible = true // Hide TransformNode.setEnabled (false) // Show the TransformNode.setEnabled (false) TransformNode.setEnabled (true) copies the code

9. Connecting projects

We talked about how to load material, animate and interact, how to finish a little game, and how it’s important to tie all the actions together.

Promise. All ([loadAsset1(), loadAsset2(), loadAsset2(), LoadAsset3 ()]).then(() => {Particles() // CreateParticles () // CreateMomeshes () // Creates other Mesh // SomeentryAnimation ().waitAsync(). Then (() = BB0 {// Start Game ()})}) // Game logic const Game = () => {// Animation will only be done once, And execute GameReady on completion to make sure that playAnimeOnTrigger(trigger, } const GameReady = () => {// Displays the start button, which can be an HTML button, It can also be BabylonJS GUI (I won't discuss this for now) showStartBTN ()... } // Click Start to start the game. // Every time the game executes const startGame = () => {const gameStarted = true RegisterBeforeRender and onBeforeRenderObservable. The add function of the same scene. RegisterBeforeRender (gameLoop) / / and time related game logic, such as time, Const interval = window.setInterval(gameLogic, 500) // The animation is performed once per game, PlayAnimeOnTrigger (trigger1, anime1) playAnimeOnTrigger(trigger2, anime2)} // Trigger logic, such as particle effects, can also be written outside, HitEffect () {if(Gamestarted) {ShowParticles ()}} const StopGame = () => {const Gamestarted = false scene.unregisterBeforeRender(gameLoop) window.clearInterval(interval) ... } // Common methods: Listen for variables, Execve the animation when a variable changes and end listening for const playAnimeOnTrigger = (trigger, anime) => { const observer = scene.onBeforeRenderObservable.add( () => { if (scene.onBeforeRenderObservable.hasObservers  && trigger) { scene.onBeforeRenderObservable.remove(observer) anime() } }) }

The summary is simply written like this. At this point, a simple 3D web game is in place.