Babylon JS Day 5: Breathe

As I wrote about on Monday, the last day of each week in March I’m taking some time to work on a series of small projects built in Babylon JS. Today I got started with a simple breathing animation. I wanted something similar to the Breathe App on Apple Watch, but I wanted to control the timing animations and add my own sounds.

TL; DR: You can try out the scene on my WebXR Sandbox Site.

If you click the link above, you will see black scene with a sphere in the center. The sphere will grow and shrink over time to indicate the pace of the breathing exercise I’ve been learning. This is based on a pattern of breathing I read about in Breath: The New Science of a Lost Art by James Nestor. Breathe in for 5.5 seconds, breathe out for 5.5 seconds. Pretty simple.

Visuals

This project didn’t get much attention as far as graphics are concerned. I have a simple icosphere that is placed at the world origin. It has a material with an emissiveColor set to #8c1eff. The only light source is a hemisphere light with a diffuse value of #ff2975 and a specular value of #8c1eff. This combination of material and lighting gave me something that I liked so I moved on to the animations.

Expanded breathing sphere

Animations

In the case of this scene, the animations are broken into four segments (five if you count the starting point). These values are hardcoded for now, but in the future, I want to make these something that guests can modify.

0 (start)Base Size
1.5 secondsBase Size, play the wait sound
4 secondsExpanded Size, plan the breathe-in sound
1.5 secondsExpanded Size, play the wait sound
4 secondsBase Size, play the breathe-out sound
Animation sequencing used in the initial Breathe project.

Animations in Babylon JS can only affect a sole property, so I had to create several animations to scale the sphere on all three axes. I set the animation frame rate to 30 and created keyframe for each of the items in the table above. In each keyframe I multiplied the time in the sequence by the frame rate. Babylon JS has an interesting way to call events on a specified frame (maybe I could use this to clean up my redundant code?). I used these animation events to call the play() method on the sound objects. Finally, the animation is set to loop.

Recap

Overall, I’m happy with the result. I have a simple breathing animation that I can use to help myself learn this breathing pace, and I was able to use several things that I learned throughout the week. In the future I’ll expand this to include some other breathing patterns and add some customizable elements.

This weekend I’ll review my notes from this past week and plan for what I should learn next week. Every area that I covered in the Getting Started guide has an expanded section of documentation available, so I don’t think I’ll run out of things to learn anytime soon.

Here is a copy of the source code for the scene. I didn’t spend any time making this code pretty. I used the Babylon JS Playground Template as a starting point and did all the work in the createScene() function.

const canvas = document.getElementById("renderCanvas"); // Get the canvas element
const engine = new BABYLON.Engine(canvas, true); // Generate the BABYLON 3D engine

var shapeColor = "#8c1eff";

// Editable time sets for the animation segments
var set1 = 1.5;
var set2 = 4;
var set3 = 1.5;
var set4 = 4;

// Add scene code here
const createScene = () => {
  const scene = new BABYLON.Scene(engine);
  scene.clearColor = BABYLON.Color3.Black();

  const camera = new BABYLON.ArcRotateCamera("camera", 0, 0, 10, new BABYLON.Vector3(0, 0, 0));
  camera.setPosition(new BABYLON.Vector3(0, 0, -20));
  // camera.attachControl(canvas, true);

  const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, -0.5, 10));
  light.diffuse = new BABYLON.Color3.FromHexString("#ff2975");
  light.specular = new BABYLON.Color3.FromHexString("#8c1eff");

  const mat1 = new BABYLON.StandardMaterial("mat1", scene);
  mat1.emissiveColor = new BABYLON.Color3.FromHexString(shapeColor);

  // Create some sound objects
  const breathIn = new BABYLON.Sound("breathIn", "../assets/audio/BreathIn.mp3", scene);
  const breathOut = new BABYLON.Sound("breathOut", "../assets/audio/BreathOut.mp3", scene);
  const breathWait = new BABYLON.Sound("breathWait", "../assets/audio/BreathWait.mp3", scene);

  // Create a mesh
  let shape1 = BABYLON.MeshBuilder.CreateIcoSphere("shape1", { radius: 1, flat: false, subdivisions: 16 }, scene);
  shape1.material = mat1;

  // Setup for animations
  let frameRate = 30;

  const keyFrames = [];
  let seg1 = set1;
  let seg2 = set1 + set2;
  let seg3 = set1 + set2 + set3;
  let seg4 = set1 + set2 + set3 + set4;
  var totalTime = seg1 + seg2 + seg3 + seg4;

  keyFrames.push({
    frame: 0,
    value: 0.5
  });

  keyFrames.push({
    frame: seg1 * frameRate,
    value: 0.5
  });

  keyFrames.push({
    frame: seg2 * frameRate,
    value: 2.5
  });

  keyFrames.push({
    frame: seg3 * frameRate,
    value: 2.5
  });

  keyFrames.push({
    frame: seg4 * frameRate,
    value: 0.5
  });

  const scaleX = new BABYLON.Animation(
    "xSlide",
    "scaling.x",
    frameRate,
    BABYLON.Animation.ANIMATIONTYPE_FLOAT,
    BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
  );

  const playIn = new BABYLON.AnimationEvent(
    seg1 * frameRate,
    function () {
      breathIn.play();
    },
    false
  );
  const playWait = new BABYLON.AnimationEvent(
    (seg2 - 0.5) * frameRate,
    function () {
      breathWait.play();
    },
    false
  );
  const playOut = new BABYLON.AnimationEvent(
    seg3 * frameRate,
    function () {
      breathOut.play();
    },
    false
  );
  const playWait2 = new BABYLON.AnimationEvent(
    (seg4 - 0.5) * frameRate,
    function () {
      breathWait.play();
    },
    false
  );
  scaleX.addEvent(playIn);
  scaleX.addEvent(playWait);
  scaleX.addEvent(playOut);
  scaleX.addEvent(playWait2);

  scaleX.setKeys(keyFrames);
  const scaleY = new BABYLON.Animation(
    "xSlide",
    "scaling.y",
    frameRate,
    BABYLON.Animation.ANIMATIONTYPE_FLOAT,
    BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
  );
  scaleY.setKeys(keyFrames);
  const scaleZ = new BABYLON.Animation(
    "xSlide",
    "scaling.z",
    frameRate,
    BABYLON.Animation.ANIMATIONTYPE_FLOAT,
    BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
  );
  scaleZ.setKeys(keyFrames);

  shape1.animations.push(scaleX);
  shape1.animations.push(scaleY);
  shape1.animations.push(scaleZ);

  var easingFunction = new BABYLON.QuadraticEase();

  // For each easing function, you can choose beetween EASEIN (default), EASEOUT, EASEINOUT
  easingFunction.setEasingMode(BABYLON.EasingFunction.EASINGMODE_EASEINOUT);

  // Adding easing function to my animation
  scaleX.setEasingFunction(easingFunction);
  scaleY.setEasingFunction(easingFunction);
  scaleZ.setEasingFunction(easingFunction);

  console.log("total time:", totalTime);
  console.table(keyFrames);
  scene.beginAnimation(shape1, 0, totalTime * frameRate, true);

  return scene;
};

// Call the scene
const scene = createScene(); //Call the createScene function

// Register a render loop to repeatedly render the scene
engine.runRenderLoop(function () {
  scene.render();
});
// Watch for browser/canvas resize events
window.addEventListener("resize", function () {
  engine.resize();
});