Javascript framework for 3D games:
3D game engines on the Web have been nothing for a long time, but now they are springing up.
- Unity (Unity has completely abandoned js and used C# since 2018.2)
- Three.js (relatively low-level framework, just a renderer, complex game interaction needs to find the right plug-in)
- PlayCanvas (Visual editor, Workflow for design)
- Babylon.js is a web side 3D engine developed and maintained by Microsoft.
- CopperCube (Visual editor type)
- 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
-
The basic elements and processes for creating a 3D scene are the same regardless of the framework or even 3D modeling software used:
-
Create canvas in HTML
<canvas id="renderCanvas"></canvas>
Copy the code
- 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
- scenario
scene = new BABYLON.Scene(engine);
Copy the code
- 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
- 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.
- Render loop
engine.runRenderLoop((a)= > {
scene.render()
})
Copy the code
This code ensures that the render of the scene is updated every frame
- 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
- 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
- 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.
- The parameters of the callback function are scene, mesh, particle, and skeleton
ImportMesh
The 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.Animation
Create an animated segment
B. Play in each framescene.onBeforeRenderObservable.add
Function 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 elementdome._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
- 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.
- 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.
- Babylonjs provides an off-the-shelf method
BABYLON.PhotoDome
const dome = new BABYLON.PhotoDome(
"testdome"."./textures/360photo.jpg",
{
resolution: 32.size: 1000
},
scene
)
Copy the code
The 360 figure of the demo
- 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.