Javascript framework for 3D games:

3D game engines on the Web have been nothing for a long time, but now they are springing up.

  1. Unity (Unity has completely abandoned js and used C# since 2018.2)
  2. Three.js (relatively low-level framework, just a renderer, complex game interaction needs to find the right plug-in)
  3. PlayCanvas (Visual editor, Workflow for design)
  4. Babylon.js is a web side 3D engine developed and maintained by Microsoft.
  5. CopperCube (Visual editor type)
  6. A-frame (VR development special, HTML custom tag form programming)

This article describes the process of developing 3D web games using Babylu.js.

1. Get Started

  1. The basic elements and processes for creating a 3D scene are the same regardless of the framework or even 3D modeling software used:

  2. Create canvas in HTML

<canvas id="renderCanvas"></canvas>
Copy the code
  1. Initialize the 3D engine
const canvas = document.getElementById('renderCanvas');
engine = new BABYLON.Engine(canvas, true); // The second option is whether to enable smoothing (anti-alias).
engine.enableOfflineSupport = false; // Unless you want an offline experience, this can be set to false
Copy the code
  1. scenario
scene = new BABYLON.Scene(engine);
Copy the code
  1. The camera
// There are two types of camera:
// UniversalCamera, can freely move and turn, compatible with three terminals
const camera = new BABYLON.UniversalCamera(
  'FCamera'.new BABYLON.Vector3(0.0.0),
  scene
)
camera.attachControl(this.canvas, true)
// and ArcRotateCamera, a 360-degree camera for "viewing" a scene
// The parameters are 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
    / / the point light source
    const light1 = new BABYLON.PointLight("pointLight".new BABYLON.Vector3(1.10.1), scene)
    / / direction of the light
    const light2 = new BABYLON.DirectionalLight("DirectionalLight".new BABYLON.Vector3(0.- 1.0), scene)
    / / the spotlight
    const light3 = new BABYLON.SpotLight("spotLight".new BABYLON.Vector3(0.30.- 10), new BABYLON.Vector3(0.- 1.0), Math.PI / 3.2, scene)
    / / the ambient light
    const light4 = new BABYLON.HemisphericLight("HemiLight".new BABYLON.Vector3(0.1.0), scene)
    Copy the code

    A. The parameters of a spotlight are used to describe a conical beamThe spotlight demo

    B. Ambient light simulates an environment where light is everywhereThe ambient light demo

  • The colour of the light
    All light sources have Diffuse and specular
    // Diffuse represents the main color of light
    // specular represents the color of the highlighted part of the object
    light.diffuse = new BABYLON.Color3(0.0.1)
    light.specular = new BABYLON.Color3(1.0.0)
    // Only ambient light has groundColor, which represents the color of reflected light on the ground
    light.groundColor = new BABYLON.Color3(0.1.0)
    Copy the code

You can use multiple light sources to achieve compound effects, such as a point light source plus an ambient light is a good combination.

  1. Render loop
engine.runRenderLoop((a)= > {
    scene.render()
})
Copy the code

This code ensures that the render of the scene is updated every frame

  1. Basic examples:

      
<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 basis</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
    /******* Creation scenario ******/
    const createScene = function () {
      // Instantiate the scene
      const scene = new BABYLON.Scene(engine)
      // Create the camera and add it 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 the light
      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)
      // Create content, a ball
      const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene)
      return scene
    }
    /******* End Create scenario ******/
    const scene = createScene()
    // loop
    engine.runRenderLoop(function () {
      scene.render()
    })
    // resize
    window.addEventListener("resize".function () {
      engine.resize()
    })
  </script>
</body>

</html>
Copy the code

Note:

<! -- Base Babylonjs package -->
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<! --loader -->
<script src="https://preview.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
Copy the code
  1. NPM packages use a development environment with packaging tools such as WebPack, You can use NPM package to load Babylonjs. There are mainly Babylonjs – main package Babylonjs-loaders -loader for all materials. Babylonjs-gui-gui user interaction page Babylonjs-materials – Some official textures include Babylonjs-post-Process Babylonjs-procedural textures Babylonjs-serializers Babylonjs-viewer

The main package and Loader package are used 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. See the official guide for details, or write it all out at componentDidMount.

2. Import and use materials

  • Except for a few elements such as particles, scenes and objects (including animations of objects) are external imports. The most popular unified format for materials is.gltf. Some of the most popular sites for getting materials are Sketchfab, Poly, and Remix3d. All three can be downloaded directly in.gltf format.

  • Textures (textures) files. GLTF,.bin, and Textures (skins) files. Personally, I prefer.gltf to.glb, which combines all files into a.glb, which is more convenient to introduce. Glb-packer.glitch.me /

  • Material is introduced into

    //.gltf and other files in a folder, such as /assets/apple
    BABYLON.SceneLoader.Append("/assets/apple"."apple.gltf", scene, (newScene) => {
        ...
    })
    // a single.glb file
    BABYLON.SceneLoader.ImportMesh("".""."www.abc.com/apple.glb", scene, (meshes, particleSystems, skeletons) => {
        ...
    })
    // The promise version
    BABYLON.SceneLoader.AppendAsync("/assets/apple"."apple.gltf", scene).then(newScene= >{... })Copy the code

    The basic function of Append and ImportMesh is to load the model and then render it into the scene.

    1. The parameters of the callback function are scene, mesh, particle, and skeleton
    2. ImportMeshThe first argument can be used to specify that some of the material is introduced, and the empty string is introduced in full.
  • Select and process materials Append example: www.babylonjs-playground.com/#WGZLGJ ImportMesh Example: www.babylonjs-playground.com/#JUKXQD

    The sandbox is the easiest way to capture the parts of a material that need to be manipulated and animated, and to understand the composition of the material. For example, download sketchFab and drag the entire folder into the Sandbox to see the interface

    For example, to get the front left wheel:

    / / in the callback
    const wheel = newMeshes.find(n= > n.id === 'Cylinder.002_0');
    // Hide the wheel
    wheel.isVisible = false;
    // The whole material is
    const car = newMeshes[0];
    // Look for the animation in the scene
    const anime = scene.animationGroups[0];
    // Play and stop animations
    anime.start(); / / play
    anime.stop(); / / stop
    Copy the code

    The whole case

3. Create animation and control animation

  • Animation types

    There are two types of animation: a. PassBABYLON.AnimationCreate an animated segment

    B. Play in each framescene.onBeforeRenderObservable.addFunction to specify the per-frame variation of an object’s argument

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

scene.onBeforeRenderObservable.add() {
    // The ball moves 0.01 per frame towards the z axis
    ball.position.z += 0.01
    / / rotation
    ball.rotation.x += 0.02
    // Zoom in along the y axis
    ball.scaling.y += 0.01
}
Copy the code

Use onBeforeRenderObservable. Complex logical animations involving multiple objects and properties are also suitable because any property under each frame can be retrieved for easy calculation.

B. Snippet animations are 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, () = > {console.log('End of animation');
});
Copy the code

This animation moves and magnifies objects. API specification:

// Create an animation
newAnimation(name, changed properties, FPS, Animation variable data type, loop mode)// Use animationScene. BeginDirectAnimation (target, animations, from which the frame, where the frame, cycle? , playback speed, end callback)// Control animation
const myAnime = scene.beginDirectAnimation( ... )
myAnime.stop()
myAnime.start()
myAnime.pause() / / pause
myAnime.restart() / / to restart
myAnime.goToFrame(60) // To a frame
// Change to promise
myAnime.waitAsync().then( ... )
Copy the code

Basic syntax above, generally 60 frames is a second. By the way, the material’s own animations also fall into the second category, which are all Animatable and are applicable to all of the above animation operations. All such 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 HTML native events, the React component’s onClick, and Babylonjs provides its own events, which it listens to using Observables.

  • observable

    Babbling. Js provides a series of 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('Button:', kbInfo.event.key);
          break;
        case BABYLON.KeyboardEventTypes.KEYUP:
          console.log('Lift button:', kbInfo.event.keyCode);
          break; }}); scene.onPointerObservable.add(pointerInfo= > {
      switch (pointerInfo.type) {
        case BABYLON.PointerEventTypes.POINTERDOWN:
          console.log('press');
          break;
        case BABYLON.PointerEventTypes.POINTERUP:
          console.log('lift');
          break;
        case BABYLON.PointerEventTypes.POINTERMOVE:
          console.log('mobile');
          break;
        case BABYLON.PointerEventTypes.POINTERWHEEL:
          console.log('the wheel');
          break;
        case BABYLON.PointerEventTypes.POINTERTAP:
          console.log('click');
          break;
        case BABYLON.PointerEventTypes.POINTERDOUBLETAP:
          console.log('double');
          break; }});Copy the code

    Add adds an Observable. Remove deletes an Observable. AddOnce adds an Observable. Once implemented, remove. HasObservers determine whether any observable. Clear clears all observables

  • Triggering of type 1 animations (i.e. animations executed in GamelOOP)

scene.onBeforeRenderObservable.add() {
   gameloop()
}

function gameloop() {... }Copy the code

The rendering logic in Gameloop is executed once per frame, so you only need to change a Boolean variable to trigger the event

let startGame = false
// React can use onClick directly
document.addEventListener('click', () => {
    startGame = true
})
// You can also use The 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}}Copy the code
  • Trigger of the second type of animation (animation segment)
// Animation cannot be played directly in Gameloop at this point
function moveBall() {
    scene.beginDirectAnimation( ... )
}

function gameloop() {
    if(startGame){
        moveBall()
    }
}
Copy the code

The above code causes moveBall() to be triggered every frame after the game starts, which is obviously not desirable.

If the trigger is mouse/keyboard, obviously it works

scene.onPointerObservable.add((info) = > {
    if(info.type === 32) {
        moveBall()
    }
}
Copy the code

However, there are other triggers (such as camera approach, property change, etc.). In this case, you can register an onBeforeRenderObservable and execute animation and Remove Observable when the trigger condition is reached

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

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

  • The universal solution israyCasterGiven a starting point, direction, and length, we can draw a line segment called ray
    // Start position
    const pos = new BABYLON.Vector3(0.0.0);
    / / direction
    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 an object in the scene, as well as information about the object touched

    const hitInfo = scene.pickWithRay(ray);
    console.log(hitInfo); // {hit: true, pickedMesh: {info}}
    Copy the code

    Since Ray is invisible, it is sometimes inconvenient to debug. RayHelper is provided for drawing Ray

    BABYLON.RayHelper.CreateAndShow(ray, scene, new BABYLON.Color3(1.1.0.1));
    Copy the code
  • There is a direct way to determine whether the mouse has been 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 mesh themselves, such as the 360 diagram element
    dome._mesh.isPickable = false;
    Copy the code
  • What if only some objects are selected

    This is often the case with footage consisting of multiple meshes. You need to identify and find the topmost parent node by name and ID. The parent nodemesh.parent.

Particle effects

You need to write a special introduction

8. Some potholes walked through and some solutions explored

  • How to ensure constant animation speed:
// engine.getfps () gets the current frame number
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 always be in the bottom right of the screen, so you need to use parent to set the parent of the gun barrel mesh to camera.
  2. Parent is also used to find the main node of the material and to bind two objects together. The child’s position, rotation, and scaling all change simultaneously with the parent’s changes.
  3. Babylonjs provides an off-the-shelf methodBABYLON.PhotoDome
const dome = new BABYLON.PhotoDome(
    "testdome"."./textures/360photo.jpg",
    {
        resolution: 32.size: 1000
    },
    scene
)
Copy the code

The 360 figure of the demo

  1. Objects show and hide

When displaying and hiding an object, you need to pay attention to whether the object is a transformNode or a mesh. The imported material usually uses one transformNode as the parent of a group of sub-mesh. In this case, using isVisible is useless.

/ / hide
mesh.isVisible = false
/ / show
mesh.isVisible = true
/ / hide
transformNode.setEnabled(false)
/ / show
transformNode.setEnabled(true)
Copy the code

9. Series of projects

We discussed how to load material, animate and interact, how to make a small game, how to connect all the actions together.

// Use promise. all and ImportMeshAsync to load all the stories
Promise.all([loadAsset1(), loadAsset2(), loadAsset3()]).then((a)= > {
    createParticles() // Create a particle
    createSomeMeshes() // Create additional mesh
    // Enter animation
    SomeEntryAnimation().waitAsync().then((a)= > {
      // Start the game
      game()
    })
})

// Game logic
const game = (a)= > {
    // Perform the animation only once, and gameReady is executed when it is finished to make sure it is ready to start
    playAnimeOnTrigger(trigger, () => anime(gameReady))
    // Other processes that only execute once
}

const gameReady = (a)= > {
    // Display the start button, either as an HTML button or as a Babylonjs GUI (not discussed yet)
    showStartBtn()
    ...
}

// Click start to start the game every time the game executes
const startGame = (a)= > {
    const gameStarted = true
    / / class of animation all is written in the gameLoop registerBeforeRender and onBeforeRenderObservable. Add the same role
    scene.registerBeforeRender(gameLoop)
    // Time related game logic, such as timing, timed animation
    const interval = window.setInterval(gameLogic, 500)
    // The animation is performed once per game. The animation itself can be looping and concatenating
    playAnimeOnTrigger(trigger1, anime1)
    playAnimeOnTrigger(trigger2, anime2)
}

// Trigger logic, such as particle effects, can also be written outside, judged by the gameStarted variable
hitEffect() {
    if(gameStarted) {
        showParticles()
    }
}

const stopGame = (a)= > {
    const gameStarted = false
    scene.unregisterBeforeRender(gameLoop)
    window.clearInterval(interval)
    ...
}

// Execute animation when variable changes and end listener
const playAnimeOnTrigger = (trigger, anime) = > {
    const observer = scene.onBeforeRenderObservable.add( (a)= > {
        if (scene.onBeforeRenderObservable.hasObservers && trigger) {
            scene.onBeforeRenderObservable.remove(observer)
            anime()
        }
    })
}
Copy the code

The simple way to write a personal summary is something like this. At this point, a simple 3D web game takes shape.