Coding4Fun tutorial: creating a 3D WebGL procedural QRCode maze with Babylon.js

In this tutorial, you should have a lot of fun. You’ll learn how to create a 3D Maze using Babylon.js/WebGL based on a dynamically generated QR Code! This is based on a demo I’ve written for our big annual French conference named Techdays 2014 and more specifically for our famous Coding4Fun session. The idea was to create a maze for the one you love during the Valentine’s Day. Indeed, isn’t it cool to create a 3D procedural maze in the name for your girlfriend/wife? Rire

Yes, this is our famous geeks’ touch in action my friend!

The name of this demo is “Le labyrinthe de l’amour”. (The love maze). I’ve kept it in French as we all know that Paris is the city of love!

Here is a video of the demo in action:

To launch & test the final result, click on the below image:

LLDLA 
Logo Credits: Michel Rousseau

Or simply click here: Launch Le labyrinthe de l’amour.

Then, enter the name of your girlfriend and click “Create”. You’ll be able to move into the maze dedicated to your beloved using the arrow keys & mouse. Press the “space” key to launch a specific camera’s animation, and you’ll be able to view the QRCode. Flash it using your smartphone and you should see “ [NameOfYourGirlFriend], I love you! ”. Your girlfriend should be impressed. ;-) It’s much better than flowers or chocolates to my point of view!

You’ll also notice also that if you click on the “isolated cube”, it will throw them into space. I’ve setup the physics engine on them to add a dramatic moment. Stay calm! Thanks to the 30% error correction algorithm, you’ll still be able to flash QR Code most of the time. ;-)

Pre-requisites

To be able to follow this tutorial, it’s better if you first read the following tutorials/articles/wiki:

- Download the last version Babylon.js: and cannon.js: https://github.com/BabylonJS/Babylon.js & https://schteppe.github.io/cannon.js/
- Learn about materials:  the StandardMaterial for your babylon.js game & 04 Materials
- Learn about lights: Using lights in your babylon.js game
- Learn about basic meshes like boxes: 02 Basic elements
- Learn about skyboxes: 15 Environment
- Learn about animations: 07-Animation
- See how to enable physics: Using webgl and a physics engine (babylon.js & cannon.js)

Even, if I’ll briefly re-explain some of the concepts.

To generate the QRCode, we’re going also to use an existing very cool library named qrcode.js created by Jerome Etienne and available on github: https://jeromeetienne.github.com/jquery-qrcode/

This tutorial is just a combination of all these resources! Clignement d'œil

Step 1: creating our playground

Create a web project using your favorite tool (mine is Visual Studio 2013) on your favorite web server.

Create a new “index.html” file at the root of the project and put this code inside it:

 <!DOCTYPE html>
<html>
<head>
    <title>Babylon.js - Coding4fun - 3D QRCode Maze</title>
    <link href="css/main.css" rel="stylesheet" />
    <script src="scripts/qrcode.js"></script>
    <script src="scripts/cannon.js"></script>
    <script src="scripts/babylon.js"></script>
    <script src="scripts/coding4fun.js"></script>
</head>
<body>
    <canvas id="canvas" class="offScreen"></canvas>
</body>
</html>

Create a “scripts” folder and put the 3 library you have downloaded in the pre-requisites: babylon.js, cannon.js & qrcode.js.

Create a “css” folder and put a “main.css” file into it with the following content:

 html, body, #canvas {
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0;
    overflow: hidden;
    touch-action: none;
}

Download the “textures” folder from here: textures.zip and add it to your web project resources. These are the assets we’re going to use for this tutorial.

Create a “coding4fun.js” file into “scripts” and put this base of code into it:

 /// <reference path="babylon.js" />

"use strict";

// Size of a cube/block
var BLOCK_SIZE = 8;
// Are we inside the maze or looking at the QR Code in bird view?
var QRCodeView = false;
var freeCamera, canvas, engine, lovescene;
var camPositionInLabyrinth, camRotationInLabyrinth;

function createQRCodeMaze(nameOfYourGirlFriend) {
    //number of modules count or cube in width/height
    var mCount = 33;

    var scene = new BABYLON.Scene(engine);
    scene.gravity = new BABYLON.Vector3(0, -0.8, 0);
    scene.collisionsEnabled = true;

    freeCamera = new BABYLON.FreeCamera("free", new BABYLON.Vector3(0, 5, 0), scene);
    freeCamera.minZ = 1;
    freeCamera.checkCollisions = true;
    freeCamera.applyGravity = true;
    freeCamera.ellipsoid = new BABYLON.Vector3(1, 1, 1);

    // Ground
    var groundMaterial = new BABYLON.StandardMaterial("groundMat", scene);
    groundMaterial.emissiveTexture = new BABYLON.Texture("textures/arroway.de_tiles-35_d100.jpg", scene);
    groundMaterial.emissiveTexture.uScale = mCount;
    groundMaterial.emissiveTexture.vScale = mCount;
    groundMaterial.bumpTexture = new BABYLON.Texture("textures/arroway.de_tiles-35_b010.jpg", scene);
    groundMaterial.bumpTexture.uScale = mCount;
    groundMaterial.bumpTexture.vScale = mCount;
    groundMaterial.specularTexture = new BABYLON.Texture("textures/arroway.de_tiles-35_s100-g100-r100.jpg", scene);
    groundMaterial.specularTexture.uScale = mCount;
    groundMaterial.specularTexture.vScale = mCount;

    var ground = BABYLON.Mesh.CreateGround("ground", (mCount + 2) * BLOCK_SIZE, 
                                                     (mCount + 2) * BLOCK_SIZE, 
                                                      1, scene, false);
    ground.material = groundMaterial;
    ground.checkCollisions = true;

    //Skybox
    var skybox = BABYLON.Mesh.CreateBox("skyBox", 800.0, scene);
    var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
    skyboxMaterial.backFaceCulling = false;
    skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("textures/skybox", scene);
    skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
    skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
    skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
    skybox.material = skyboxMaterial;

    //At Last, add some lights to our scene
    var light0 = new BABYLON.PointLight("pointlight0", new BABYLON.Vector3(28, 78, 385), scene);
    light0.diffuse = new BABYLON.Color3(0.5137254901960784, 0.2117647058823529, 0.0941176470588235);
    light0.intensity = 0.2;

    var light1 = new BABYLON.PointLight("pointlight1", new BABYLON.Vector3(382, 96, 4), scene);
    light1.diffuse = new BABYLON.Color3(1, 0.7333333333333333, 0.3568627450980392);
    light1.intensity = 0.2;

    //TO DO: create the labyrinth

    return scene;
};

window.onload = function () {
    canvas = document.getElementById("canvas");

    if (!BABYLON.Engine.isSupported()) {
        window.alert('Browser not supported');
    } else {
        engine = new BABYLON.Engine(canvas, true);

        window.addEventListener("resize", function () {
            engine.resize();
        });

        lovescene = createQRCodeMaze("Jane Doe");
        // Enable keyboard/mouse controls on the scene (FPS like mode)
        lovescene.activeCamera.attachControl(canvas);

        engine.runRenderLoop(function () {
            lovescene.render();
        });
    }
};

Well, the code should be relatively straightforward and self-explicit. We’re creating a ground and enabling collisions & gravity on the free camera. You can then move on the ground without flying into the air or falling though the ground. We’re also creating a skybox to create our universe surrounding us as well as 2 lights with colors taken from the skybox.

You can test this first scene here in your WebGL browser: Love Maze - Step 1

It’s currently a simple & boring scene. Let’s add some fun to it.

Step 2: it’s all about cubes

Our maze is going to be built from a set of simple cubes. 1 cube will be associated to 1 dark pixel of the QR Code.

But let’s first create a unique simple cube. Add this code after the “TODO”:

 // The position of our cube:
var row = 15;
var col = 20;

var cubeWallMaterial = new BABYLON.StandardMaterial("cubeWalls", scene);
cubeWallMaterial.emissiveTexture = new BABYLON.Texture("textures/masonry-wall-texture.jpg", scene);
cubeWallMaterial.bumpTexture = new BABYLON.Texture("textures/masonry-wall-bump-map.jpg", scene);
cubeWallMaterial.specularTexture = new BABYLON.Texture("textures/masonry-wall-normal-map.jpg", scene);

var mainCube = BABYLON.Mesh.CreateBox("mainCube", BLOCK_SIZE, scene);
mainCube.material = cubeWallMaterial;
mainCube.checkCollisions = true;
mainCube.position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (mCount / 2)) * BLOCK_SIZE, BLOCK_SIZE / 2,
                                        BLOCK_SIZE / 2 + (col - (mCount / 2)) * BLOCK_SIZE);

You’ll see this cube and by moving the camera, you shouldn’t be able to go through thanks to the native collisions engine embedded into babylon.js:

image

But if you’re disabling the gravity on the freeCamera object and you’re moving the camera above the cube, you’ll see that its top doesn’t have the proper material set yet for our future QR Code recognition algorithm:

image

We need to have a different material for the top. For that, we’re going to use the multi-materials approach of babylon.js: Using multi-materials using this code:

 var row = 15;
var col = 20;

var cubeTopMaterial = new BABYLON.StandardMaterial("cubeTop", scene);
cubeTopMaterial.emissiveColor = new BABYLON.Color3(0.1, 0.1, 0.15);

var cubeWallMaterial = new BABYLON.StandardMaterial("cubeWalls", scene);
cubeWallMaterial.emissiveTexture = new BABYLON.Texture("textures/masonry-wall-texture.jpg", scene);
cubeWallMaterial.bumpTexture = new BABYLON.Texture("textures/masonry-wall-bump-map.jpg", scene);
cubeWallMaterial.specularTexture = new BABYLON.Texture("textures/masonry-wall-normal-map.jpg", scene);

var cubeMultiMat = new BABYLON.MultiMaterial("cubeMulti", scene);
cubeMultiMat.subMaterials.push(cubeTopMaterial);
cubeMultiMat.subMaterials.push(cubeWallMaterial);

var soloCube = BABYLON.Mesh.CreateBox("mainCube", BLOCK_SIZE, scene);
soloCube.subMeshes = [];
soloCube.subMeshes.push(new BABYLON.SubMesh(0, 0, 4, 0, 6, soloCube));
soloCube.subMeshes.push(new BABYLON.SubMesh(1, 4, 20, 6, 30, soloCube));

// same as soloCube.rotation.x = -Math.PI / 2; 
// but cannon.js needs rotation to be set via Quaternion
soloCube.rotationQuaternion = BABYLON.Quaternion.RotationYawPitchRoll(0, -Math.PI / 2, 0);
soloCube.material = cubeMultiMat;
soloCube.checkCollisions = true;
soloCube.position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (mCount / 2)) * BLOCK_SIZE, BLOCK_SIZE / 2,
                                        BLOCK_SIZE / 2 + (col - (mCount / 2)) * BLOCK_SIZE);

Note: as you can read, I’m turning the cube using Quaternion rather directly working on rotation.x. It’s because cannon.js needs to have the information set this way. If you’re setting it the other way, cannon.js will not take into account your rotation transformations. Oh yes, and I’m doing a rotation of the cube because otherwise the specific face with the top material associated is pointing down.

And here is the result:

image

Test the result of this step 2 here: Love Maze - Step 2

Step 3: creating the 3D QRCode maze

We’re now going to call the qrcode.js library to build the QR Code based on a specific text. This library normally needs a div to inject into. We’ll give it a fake div element. Then, it builds the QR Code and draw the output into the div. I was more interested in the 2-D array built inside it. This array represents the complete QR Code by a series of rather dark or white pixels.

In conclusion, replace in your code the mCount variable declaration by this:

 // It needs a HTML element to work with
var qrcode = new QRCode(document.createElement("div"), { width: 400, height: 400 });

qrcode.makeCode(nameOfYourGirlFriend + ", I love you!");

// needed to set the proper size of the playground
var mCount = qrcode._oQRCode.moduleCount;

Then, here is the loop creating the complete maze:

 for (var row = 0; row < mCount; row++) {
    for (var col = 0; col < mCount; col++) {
        if (qrcode._oQRCode.isDark(row, col)) {
            var soloCube = BABYLON.Mesh.CreateBox("mainCube", BLOCK_SIZE, scene);
            soloCube.subMeshes = [];
            soloCube.subMeshes.push(new BABYLON.SubMesh(0, 0, 4, 0, 6, soloCube));
            soloCube.subMeshes.push(new BABYLON.SubMesh(1, 4, 20, 6, 30, soloCube));
            // same as soloCube.rotation.x = -Math.PI / 2; 
            // but cannon.js needs rotation to be set via Quaternion
            soloCube.rotationQuaternion = BABYLON.Quaternion.RotationYawPitchRoll(0, -Math.PI / 2, 0);
            soloCube.material = cubeMultiMat;
            soloCube.checkCollisions = true;
            soloCube.position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (mCount / 2)) * BLOCK_SIZE,
                                                    BLOCK_SIZE / 2,
                                                    BLOCK_SIZE / 2 + (col - (mCount / 2)) * BLOCK_SIZE);
        }
    }
}

var x = BLOCK_SIZE / 2 + (7 - (mCount / 2)) * BLOCK_SIZE;
var y = BLOCK_SIZE / 2 + (1 - (mCount / 2)) * BLOCK_SIZE;
freeCamera.position = new BABYLON.Vector3(x, 5, y);

I’m setting the camera position to a specific place in the QR Code I know it will always be empty.

image

Test the result of this step 3 here: Love Maze - Step 3

Step 4: optimization with cloning

Currently, in our loop, we’re creating the very same object up to 500 times. A better approach is to create the geometry once and then clone it. It’s better for the memory consumption and for the performance. The CPU will send a unique geometry to the GPU. GPU will then clone this geometry as needed without asking more information from the CPU. It could be an important point especially on mobile devices. It could also have an impact on the rendering performance. In my case, my base geometry (a cube) is far too simple to have an immediate performance boost just thanks to cloning. But this doesn’t mean you shouldn’t do it every time you will duplicate the very same mesh.

More interestingly, during a game, it’s also much faster to instantiate a clone (of an enemy for instance) rather than creating it again from scratch. If you’re spawning a new enemy during your game by creating it without the cloning mechanism, you’ll probably have some fps drops. In conclusion, this is really a best practice to use cloning if you need to duplicate several times the very same mesh

Here is the code for that:

 var soloCube = BABYLON.Mesh.CreateBox("mainCube", BLOCK_SIZE, scene);
soloCube.subMeshes = [];
soloCube.subMeshes.push(new BABYLON.SubMesh(0, 0, 4, 0, 6, soloCube));
soloCube.subMeshes.push(new BABYLON.SubMesh(1, 4, 20, 6, 30, soloCube));
soloCube.rotationQuaternion = BABYLON.Quaternion.RotationYawPitchRoll(0, -Math.PI / 2, 0);
soloCube.material = cubeMultiMat;
soloCube.checkCollisions = true;
soloCube.setEnabled(false);

var cube;

for (var row = 0; row < mCount; row++) {
    for (var col = 0; col < mCount; col++) {
        if (qrcode._oQRCode.isDark(row, col)) {
            cube = soloCube.clone("ClonedCube" + row + col);
            cube.position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (mCount / 2)) * BLOCK_SIZE,
                                                BLOCK_SIZE / 2,
                                                BLOCK_SIZE / 2 + (col - (mCount / 2)) * BLOCK_SIZE);
        }
    }
}

Step 5: performance optimization by merging meshes

Maybe you’ll notice than even if we don’t have a lot of triangles currently being displayed, the performance are not stellar. This is because we have a lot of small objects and thus small operations associated to them. We’re then spending too much time between the CPU and the GPU. The CPU is doing a lot of roundtrips with the GPU to send the orders for the 500+ potential cubes to be displayed. It’s much more efficient to send a big mesh from the CPU to the GPU and then ask for specific operations on this big mesh (rotation, scaling, lights, etc.).

The idea is then to merge all the generated cubes into a big mesh. It will really enhance the global rendering performance.

Let’s launch the F12 UI Responsiveness tool of IE11 to check the current results before merging meshes:

image

The average fps is around 30 fps. And the CPU usage reaches 100% which tends to prove that the CPU is doing more work than expected.

Insert the merging function into your code by copy/pasting it from our wiki: How to merge meshes.

And now use this code to generate the optimized maze:

 var topCube = BABYLON.Mesh.CreatePlane("ground", BLOCK_SIZE, scene, false);
topCube.material = cubeTopMaterial;
topCube.rotation.x = Math.PI / 2;
topCube.setEnabled(false);

var cube, top;
var cubesCollection = [];
var cubesTopCollection = [];

for (var row = 0; row < mCount; row++) {
    for (var col = 0; col < mCount; col++) {
        if (qrcode._oQRCode.isDark(row, col)) {
            cube = soloCube.clone("ClonedCube" + row + col);
            cube.position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (mCount / 2)) * BLOCK_SIZE,
                                                BLOCK_SIZE / 2,
                                                BLOCK_SIZE / 2 + (col - (mCount / 2)) * BLOCK_SIZE);

            top = topCube.clone("TopClonedCube" + row + col);
            top.position = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (mCount / 2)) * BLOCK_SIZE,
                                                BLOCK_SIZE + 0.05,
                                                BLOCK_SIZE / 2 + (col - (mCount / 2)) * BLOCK_SIZE);

            cubesCollection.push(cube);
            cubesTopCollection.push(top);
        }
    }
}

var maze = mergeMeshes("maze", cubesCollection, scene);
maze.checkCollisions = true;
maze.material = cubeWallMaterial;

var mazeTop = mergeMeshes("mazeTop", cubesTopCollection, scene);
mazeTop.material = cubeTopMaterial;

Note: as you can see, I’m creating a special merged mesh with some plane elements to put just above the merged cubes. It’s to have a simple solution to handle multi-materials in this case.

Now, let’s analyze the gain with IE11 F12:

image

You’ll immediately see that the CPU usage has drop dramatically and we now have an average fps around 60 fps.

Test the result of this step 5 here: Love Maze - Step 5

You can verify the performance boost in your browser/machine with the previous version in step 3.

Step 6: animating the camera

To be able to view the QR Code from the space and flash it with your smartphone, we need to change the current camera position and rotation to a specific place. Rather than jumping directly to this specific position, let’s animate the camera from its current place in the maze to the best position in the space for flashing.

For that, let’s add this simple function:

 var animateCameraPositionAndRotation = function (freeCamera, fromPosition, toPosition,
                                                 fromRotation, toRotation) {

    var animCamPosition = new BABYLON.Animation("animCam", "position", 30,
                              BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
                              BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);

    var keysPosition = [];
    keysPosition.push({
        frame: 0,
        value: fromPosition
    });
    keysPosition.push({
        frame: 100,
        value: toPosition
    });

    animCamPosition.setKeys(keysPosition);

    var animCamRotation = new BABYLON.Animation("animCam", "rotation", 30,
                              BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
                              BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);

    var keysRotation = [];
    keysRotation.push({
        frame: 0,
        value: fromRotation
    });
    keysRotation.push({
        frame: 100,
        value: toRotation
    });

    animCamRotation.setKeys(keysRotation);
    freeCamera.animations.push(animCamPosition);
    freeCamera.animations.push(animCamRotation);

    scene.beginAnimation(freeCamera, 0, 100, false);
};

Let’s now call this function to animate our camera when the user will press the “space” key:

 window.addEventListener("keydown", function (event) {
    if (event.keyCode === 32) {
        if (!QRCodeView) {
            QRCodeView = true;
            // Saving current position & rotation in the maze
            camPositionInLabyrinth = freeCamera.position;
            camRotationInLabyrinth = freeCamera.rotation;
            animateCameraPositionAndRotation(freeCamera, freeCamera.position,
                new BABYLON.Vector3(16, 400, 15),
                freeCamera.rotation,
                new BABYLON.Vector3(1.4912565104551518, -1.5709696842019767,freeCamera.rotation.z));
        }
        else {
            QRCodeView = false;
            animateCameraPositionAndRotation(freeCamera, freeCamera.position,
                camPositionInLabyrinth, freeCamera.rotation, camRotationInLabyrinth);
        }
        freeCamera.applyGravity = !QRCodeView;
    }
}, false);

Now, launch your code and press the “space” bar, you should obtain this view:

image

Using my Windows Phone native QR Code app, it works! Rire

wp_ss_20140218_0004

Step 7: enabling physics on isolated cubes

In this last part of the tutorial, we’re going to see how to enable some physics on isolated cubes (cubes without direct neighbors on their left/right/up/down direction). Then, we’ll see how to select a mesh by clicking it and apply some impulse force to throw these isolated cubes in the space.

First, you need to enable physics on the scene:

 scene.enablePhysics(new BABYLON.Vector3(0, 0, 0));

In my case, I’m setting a zero gravity as we’re supposed to be in space. Then, you need to define the various impostor for the ground and the isolated boxes.

Enabling physics on our ground:

 ground.setPhysicsState({ impostor: BABYLON.PhysicsEngine.PlaneImpostor, mass: 0, 
                         friction: 0.5, restitution: 0.7 });

And here is the new loop checking if the cube is isolated and setting up physics properties on it:

 var mainCube = BABYLON.Mesh.CreateBox("mainCube", BLOCK_SIZE, scene);
mainCube.material = cubeWallMaterial;
mainCube.checkCollisions = true;
mainCube.setEnabled(false);

var cube, top;
var cubesCollection = [];
var cubesTopCollection = [];
var cubeOnLeft, cubeOnRight, cubeOnUp, cubeOnDown;

for (var row = 0; row < mCount; row++) {
    for (var col = 0; col < mCount; col++) {
        if (qrcode._oQRCode.isDark(row, col)) {
            var cubePosition = new BABYLON.Vector3(BLOCK_SIZE / 2 + (row - (mCount / 2)) * BLOCK_SIZE,
                                                    BLOCK_SIZE / 2,
                                                    BLOCK_SIZE / 2 + (col - (mCount / 2)) * BLOCK_SIZE);

            cubeOnLeft = cubeOnRight = cubeOnUp = cubeOnDown = false;
            if (col > 0) {
                cubeOnLeft = qrcode._oQRCode.isDark(row, col - 1)
            }
            if (col < mCount - 1) {
                cubeOnRight = qrcode._oQRCode.isDark(row, col + 1)
            }
            if (row > 0) {
                cubeOnUp = qrcode._oQRCode.isDark(row - 1, col)
            }
            if (row < mCount - 1) {
                cubeOnDown = qrcode._oQRCode.isDark(row + 1, col)
            }

            if (cubeOnLeft || cubeOnRight || cubeOnUp || cubeOnDown) {
                cube = mainCube.clone("Cube" + row + col);
                cube.position = cubePosition.clone();
                top = topCube.clone("TopCube" + row + col);
                top.position = cubePosition.clone();
                top.position.y = BLOCK_SIZE + 0.05;
                cubesCollection.push(cube);
                cubesTopCollection.push(top);
            }
            else {
                cube = soloCube.clone("SoloCube" + row + col);
                cube.position = cubePosition.clone();
                cube.setPhysicsState({ impostor: BABYLON.PhysicsEngine.BoxImpostor, mass: 2, 
                                       friction: 0.4, restitution: 0.3 });
            }
        }
    }
}

Finally, we need to send a ray against the scene targeted by the mouse pointer during the mouse down event. If a mesh is selected, we’re applying a force impulse:

 canvas.addEventListener("mousedown", function (evt) {
    var pickResult = scene.pick(evt.clientX, evt.clientY);
    if (pickResult.hit) {
        var dir = pickResult.pickedPoint.subtract(scene.activeCamera.position);
        dir.normalize();
        pickResult.pickedMesh.applyImpulse(dir.scale(50), pickResult.pickedPoint);
    }
});

If it’s an isolated cube, it will fly into the space forever! Some cubes could interact between each other’s also.

image

You’ll still be able to flash the QR Code however as I’ve used the default encoding using 30% error correction. Even with some missing flying isolated cube, you can still impressed your girlfriend. Clignement d'œil

That’s all folks!

You can download the complete source code contained in this ZIP archive: finalcode.zip

Follow the author @davrous