When the WeChat applet encounters AR

Keywords: Mobile Javascript Windows Attribute

When the WeChat applet encounters AR, what kind of sparks will it wipe out?Expectation and excitement...

Through this tutorial, you can start from the foundation to create an AR framework for a WeChat applet, all of which are open source and available for everyone to learn.

This course requires some foundation: WeChat Developer Tools, JavaScript, Html, Css

Chapter IV: Foundation - Combination of Camera and Three.js

[Summary of Previous Situation]

In the previous two chapters, we learned the basic camera data reading and the creation of Three.js three-dimensional scenes.After learning these two chapters, we can do a lot of more customized development.For example:

1. We can already develop AR content based on camera images (e.g. face recognition, AR make-up, lipstick, hat, glasses, etc.)

2. WebGL's three-dimensional games.

This chapter is both a foundation and a sublimation, mainly discussing how to present the background of the camera picture in the WeChat applet, and then render the three-dimensional content of WebGL on the background.

1. To access the camera in the WeChat applet, and to get the data of each frame in real time.
2. Implement access to WebGL interface in the WeChat applet to draw three-dimensional objects.This tutorial uses the Three.js engine
3. Realize the display of 3D objects of WebGL on the background of the real-time picture of the camera.
4. Whole Frame Building
5. Image algorithm access

 

 

 

 

 

[Objective]

WebGL content is rendered on the background of the camera picture in the WeChat applet

[scenario]

Before we start development, we want to list the possible solutions.

To render WebGL content on top of a camera picture, one solution is easy to think of:

Option 1: Double-Layer Canvas Structure

As shown in the image above, we can build two canvas, one to render the camera's image and the other to render the WebGL's image.The WebGL picture is on the top, while the camera picture is on the bottom, and the background of the WebGL canvas is transparent.

This scenario requires that our canvas support multilayer structure and that the WebGL background support transparency.

Scenario 2: Single layer Canvas, WebGL interior plane map

 

This scenario uses a pure WebGL canvas to create a vertical plane facing the camera in the scene and display each frame of the camera picture as a map on the plane.

Of course, each has its own advantages and disadvantages, for the moment:

1. The advantage of Scheme 1 is that it is simple and does not require adding objects to the scene and mapping each frame.However, the WebGL transparent background may not be well supported by different mobile phones. In addition, when calculating the position of objects on the WebGL layer, coordinates on the camera screen layer need to be converted to display normally.

2. Scenario 2, the structure is relatively complex, but it can be more adaptable, all using WebGL hardware acceleration.The coordinate conversion between the three-dimensional object and the camera picture is done in the same scene.

 

[Prepare]

Next, we need to set up the environment and do some preparatory work.

First, you need to register the WeChat applet developer. Registration Address=>

After successful registration, you need to download the WeChat applet development tool. Download Address=>

The current development environment of the author is Windows 10

The downloaded version of the WeChat applet is RC v1.0.2.1909111

[Create a project]

Follow the same steps as in Chapter 2 above to create a simple basic project.That's not going to be repeated here.Here we set up two page s, scenario 1 and scenario 2, to demonstrate the two scenarios.Also add the "libs" folder and put in the 3.js file that was modified in Chapter 3.Once established, the project directory is as follows:

 

 

[Development: scenario one (under scenario 1 folder)]

First, in the index.wxml file, we sequentially add two layers for the camera and WebGL:

<!--index.wxml-->
<view>
  <!--WebGL layer-->
  <canvas 
    type="webgl" 
    id="webgl" 
    canvas-id="webgl" 
    style="position:fixed;top:0;width:{{canvasWidth}}px;height:{{canvasHeight}}px;z-index:1;">
  </canvas>
  <!--Camera Layer-->
  <camera 
    mode="normal" 
    device-position="back" 
    flash="auto" 
    frame-size="medium"
    style="position:fixed;top:0;width:100%;height:100%;z-index:0;">
  </camera>
</view>

Note that the style attribute in both tags adds position,top, and z-index field settings to the previous two chapters so that each layer starts at the top of the most mobile screen and ensures that the WebGL layer is on top.

Next, we can copy the code from the previous chapter about rotating cube created by 3.js.That is, copy the contents of the index.js file from the previous chapter.The only modification you need to make is to specify that the background of the WebGLRenderer renderer is transparent when you create it.The code is as follows:

//index.js

//Import three.js library
import * as THREE from '../../libs/three.js'

//Get Application Instances
const app = getApp();

Page({
  data: {
    canvasWidth: 0,
    canvasHeight: 0
  },

  /**
   * Page Load Callback Function
   */
  onLoad: function () {
    //Initialization Canvas object
    this.initWebGLCanvas();
  },
  /**
   * Initialize Canvas object
   */
  initWebGLCanvas: function () {
    //Get the labels on the page id by webgl Object to get the canvas object
    var query = wx.createSelectorQuery();
    query.select('#webgl').node().exec((res) => {
      var canvas = res[0].node;
      this._webGLCanvas = canvas;
      //Get system information, including screen resolution, display area size, pixel ratio, etc.
      var info = wx.getSystemInfoSync();
      this._sysInfo = info;
      //Set up canvas Size, which is defined by multiplying the window size by the pixel ratio
      this._webGLCanvas.width = this._sysInfo.windowWidth * this._sysInfo.pixelRatio;
      this._webGLCanvas.height = this._sysInfo.windowHeight * this._sysInfo.pixelRatio;
      //Set up canvas Style
      this._webGLCanvas.style = {};
      this._webGLCanvas.style.width = this._webGLCanvas.width.width;
      this._webGLCanvas.style.height = this._webGLCanvas.width.height;
      //Set Display Layer canvas Binding Style style Data, the page layer is defined directly by the window size
      this.setData({
        canvasWidth: this._sysInfo.windowWidth,
        canvasHeight: this._sysInfo.windowHeight
      });
      this.initWebGLScene();
    });
  },
  /**
   * Initialize WebGL Scenarios
   */
  initWebGLScene: function () {
    //Create Camera
    var camera = new THREE.PerspectiveCamera(60, this._webGLCanvas.width / this._webGLCanvas.height, 1, 1000);
    this._camera = camera;
    //Create Scene
    var scene = new THREE.Scene();
    this._scene = scene;

    //Establish Cube Geometry
    var cubeGeo = new THREE.CubeGeometry(30, 30, 30);
    //Create materials and set them as basic materials (no light will be reflected, set the material color to green)
    var mat = new THREE.MeshBasicMaterial({ color: 0x00FF00 });
    //Establish Cube Of Mesh object
    var cube = new THREE.Mesh(cubeGeo, mat);
    //Set up Cube Position of object
    cube.position.set(0, 0, -100);
    //take Cube Join the scene
    this._scene.add(cube);

    //Create Renderer,Specify renderer background transparency
    var renderer = new THREE.WebGLRenderer({
      canvas: this._webGLCanvas,
      alpha:true
    });
    //Set Renderer Size
    this._renderer = renderer;
    this._renderer.setSize(this._webGLCanvas.width, this._webGLCanvas.height);
    //Record the current time
    var lastTime = Date.now();
    this._lastTime = lastTime;
    //Start Rendering
    this.renderWebGL(cube);
  },
  /**
   * renderer
   */
  renderWebGL: function (cube) {
    //Get the time of the current frame
    var now = Date.now();
    //Calculate time interval,Because Date Object returns in milliseconds, so divide by 1000 to get the time interval in seconds
    var duration = (now - this._lastTime) / 1000;
    //Print frame rate
    console.log(1 / duration + 'FPS');
    //Re-assign last frame time
    this._lastTime = now;
    //rotate Cube Object, where you want every second Cube Object follows Y Rotate axis 180 degrees ( Three.js Medium radian is, so is Math.PI)
    cube.rotation.y += duration * Math.PI;

    //Render the execution scene, specifying what the camera sees
    this._renderer.render(this._scene, this._camera);
    //Set the frame callback function and call the custom rendering function for each frame
    this._webGLCanvas.requestAnimationFrame(() => {
      this.renderWebGL(cube);
    });
  }
})

Save the code, compile and run, and we can see that our rotating Cube appears on the background of the camera picture (the same effect is tested on the real machine), and the frame rate is maintained around 60FPS.

 

[Development: Scenario 2 (under scenario 2 folder)]

Following the previous description of the scenario, we:

1. First create a Geometry with a Plane plane in the scene

2. Next we update a map in Camera's callback function

3. Finally apply the new map to the Plane plane in the Renderer Update

First, let's write the index.wxml file with the following code:

<!--pages/scenario2/index.wxml-->
<view>
<canvas 
    type="webgl" 
    id="webgl" 
    canvas-id="webgl" 
    style="position:fixed;top:0;width:{{canvasWidth}}px;height:{{canvasHeight}}px;">
  </canvas>
  <!--Camera Layer-->
  <camera 
    mode="normal" 
    device-position="back" 
    flash="auto" 
    frame-size="medium"
    style="position:fixed;top:-100%;width:100%;height:100%;">
  </camera>
</view>

In this code, instead of specifying a z-index value, set the top property of the camera Camera tag to -100%, so that this layer is outside the screen and will not be displayed.Because we will show the webcam display in WebGL later.

Next, we can write the index.js file.

//index.js

//Import three.js library
import * as THREE from '../../libs/three.js'

//Get Application Instances
const app = getApp();

Page({
  data: {
    canvasWidth: 0,
    canvasHeight: 0
  },

  /**
   * Page Load Callback Function
   */
  onLoad: function () {
    //Initialization Camera
    this.initCamera();
    //Initialization Canvas object
    this.initWebGLCanvas();
  },
  /**
   * Initialize Canvas object
   */
  initWebGLCanvas: function () {
    //Get the labels on the page id by webgl Object to get the canvas object
    var query = wx.createSelectorQuery();
    query.select('#webgl').node().exec((res) => {
      var canvas = res[0].node;
      this._webGLCanvas = canvas;
      //Get system information, including screen resolution, display area size, pixel ratio, etc.
      var info = wx.getSystemInfoSync();
      this._sysInfo = info;
      //Set up canvas Size, which is defined by multiplying the window size by the pixel ratio
      this._webGLCanvas.width = this._sysInfo.windowWidth * this._sysInfo.pixelRatio;
      this._webGLCanvas.height = this._sysInfo.windowHeight * this._sysInfo.pixelRatio;
      //Set up canvas Style
      this._webGLCanvas.style = {};
      this._webGLCanvas.style.width = this._webGLCanvas.width.width;
      this._webGLCanvas.style.height = this._webGLCanvas.width.height;
      //Set Display Layer canvas Binding Style style Data, the page layer is defined directly by the window size
      this.setData({
        canvasWidth: this._sysInfo.windowWidth,
        canvasHeight: this._sysInfo.windowHeight
      });
      //Initialize scene
      this.initWebGLScene();
    });
  },
  /**
   * Initialize camera
   */
  initCamera:function()
  {
    //Obtain Camera Coontext object
    const cContex = wx.createCameraContext();
    //Add Frame Callback Event Listener
    const listener = cContex.onCameraFrame((frame) => {
      //In the callback event, get the data for each frame
      var data = new Uint8Array(frame.data);
      //adopt RGBA Data format generation map
      var tex = new THREE.DataTexture(data, frame.width, frame.height, THREE.RGBAFormat);
      //Clean up the map of secondary camera data
      if(this._tex != null)
      {
        this._tex.dispose();
      }
      //Keep the map of the latest frame
      this._tex = tex;
    });
    //lsnrctl start
    listener.start();
  },
  /**
   * Initialize WebGL Scenarios
   */
  initWebGLScene: function () {
    //Create Camera
    var camera = new THREE.PerspectiveCamera(60, this._webGLCanvas.width / this._webGLCanvas.height, 1, 1000);
    this._camera = camera;
    //Create Scene
    var scene = new THREE.Scene();
    this._scene = scene;

    //Establish Cube Geometry
    var cubeGeo = new THREE.CubeGeometry(30, 30, 30);
    //Create materials and set them as basic materials (no light will be reflected, set the material color to green)
    var mat = new THREE.MeshBasicMaterial({ color: 0x00FF00 });
    //Establish Cube Of Mesh object
    var cube = new THREE.Mesh(cubeGeo, mat);
    //Set up Cube Position of object
    cube.position.set(0, 0, -100);
    //take Cube Join the scene
    this._scene.add(cube);

    //Create Planar Geometry
    var planeGeo = new THREE.PlaneGeometry(100,100);
    //Creating planar MEsh
    var plane = new THREE.Mesh(planeGeo,new THREE.MeshBasicMaterial());
    //Set the position of the plane so that it does not block the front Cube,So set the plane farther away.
    plane.position.set(0,0,-200);
    //Add a plane to the scene
    this._scene.add(plane);

    //Create Renderer,Specify renderer background transparency
    var renderer = new THREE.WebGLRenderer({
      canvas: this._webGLCanvas,
    });
    //Set Renderer Size
    this._renderer = renderer;
    this._renderer.setSize(this._webGLCanvas.width, this._webGLCanvas.height);
    //Record the current time
    var lastTime = Date.now();
    this._lastTime = lastTime;
    //Start Rendering
    this.renderWebGL(cube,plane);
  },
  /**
   * renderer
   */
  renderWebGL: function (cube,plane) {
    //Get the time of the current frame
    var now = Date.now();
    //Calculate time interval,Because Date Object returns in milliseconds, so divide by 1000 to get the time interval in seconds
    var duration = (now - this._lastTime) / 1000;
    //Print frame rate
    //console.log(1 / duration + 'FPS');
    //Re-assign last frame time
    this._lastTime = now;
    //rotate Cube Object, where you want every second Cube Object follows Y Rotate axis 180 degrees ( Three.js Medium radian is, so is Math.PI)
    cube.rotation.y += duration * Math.PI;
    //Set up plane Mapping of
    if(this._tex != null)
    {
      //When the current camera map exists
      if(plane.material != null)
      {
        //Clean up the material of the last frame
        plane.material.dispose();
      }
      //Generate new materials with new maps and assign them to flat objects
      plane.material = new THREE.MeshBasicMaterial({color: 0xFFFFFF, map: this._tex});
    }
    //Render the execution scene, specifying what the camera sees
    this._renderer.render(this._scene, this._camera);
    //Set the frame callback function and call the custom rendering function for each frame
    this._webGLCanvas.requestAnimationFrame(() => {
      //Start next frame rendering
      this.renderWebGL(cube,plane);
    });
  }
})

There are several changes to the new Js:

1. First, a custom function initCamera to initialize Camera is added to the OnLoad function, which adds a frame event listener and generates a new map on this._tex object with the data returned from each frame.Where we created the map, we used the DataTexture map type of Three.js, which creates the map from an array of pixel values.In Chapter 2, we already know that the data returned from the frame callback function of a mobile phone camera is in the form of RGBA, so you can create the map correctly in this format.

2. In the scene initialization function, a new plane is created, which is placed farther away from Cube than Camera.This will not block Cube.

3. In the rendering function, a plane object is passed in, and the map of the plane is updated with each rendering.

Save, compile, and we'll see the final result (live testing works as well):

 

However, we will find that there are some problems now, that is, the map of the camera on Plane is reversed, with the left and right side upside down.In other words, the order of pixels in the value of each frame returned from the frame event of the face camera is different from that in the Texture of the map in 3.js.So we need to rotate the planes to see the correct results.On the other hand, the opposite problem is left and right. After rotation, it reaches the back of the plane. By default, the back of the plane will not be displayed.So we need to set the flat material to be double-sided so that we can show the content on the back.

So to modify it, the code at the time the plane was created:

//Create Planar Geometry
    var planeGeo = new THREE.PlaneGeometry(100,100);
    //Creating planar MEsh
    var plane = new THREE.Mesh(planeGeo,new THREE.MeshBasicMaterial());
    //Set the position of the plane so that it does not block the front Cube,So set the plane farther away.
    plane.position.set(0,0,-200);
    //Rotate the direction of the plane to display the camera picture correctly
    plane.rotation.z = Math.PI;
    plane.rotation.y = Math.PI;
    //Add a plane to the scene
    this._scene.add(plane);

And code for creating materials in rendering functions

//Set up plane Mapping of
    if(this._tex != null)
    {
      //When the current camera map exists
      if(plane.material != null)
      {
        //Clean up the material of the last frame
        plane.material.dispose();
      }
      //Generate new textures with new maps to assign to flat objects and set to double-sided textures
      plane.material = new THREE.MeshBasicMaterial({color: 0xFFFFFF, map: this._tex, side:THREE.DoubleSide});
    }

This gives us the right picture.

But now careful friends will have new questions.The length and width of the map created by the camera is based on the length and width of each frame, i.e. frame.height s and frame.width, but we have pasted this map on a square plane. Because the size of the map is not square, it will stretch or compress the last view.In addition, the whole plane does not occupy the whole screen.

These problems require understanding the three-dimensional spatial knowledge of Three.js, and using the aspect ratio and perspective matrix of the screen to calculate the correct plane width of the plane object, so that the plane can be displayed in full screen.

You will be asked again that the plane and Cube are now in a three-dimensional scene. If the object in the scene is enlarged or the position is close to the plane, collisions will occur, resulting in display bug s.

This is one drawback of our scheme two.It can also take many forms to repair.For example, multiple cameras render camera images and three-dimensional scenes and then paste them together, which is a bit like the hierarchy of multiple Canvas in CSS.You can also change the position of the plane s at the farthest point all the time, and so on.

These require specific outcomes in the actual application scenario.

[Summary]

Through this chapter, we have finally made some qualitative changes.Three-dimensional objects can be displayed against the background of the camera.Of course, through this chapter, we also learned how to turn the camera picture into a map and apply it to three-dimensional objects.

So far, several basic elements of AR have been technically implemented to acquire camera data, display three-dimensional objects, and display three-dimensional objects in front of camera images.This allows us to implement the existing AR scheme described in Chapter 1 in our applet, which identifies a picture showing a three-dimensional object without following information.

There is no follow information, that is, there is no need to change the position of a three-dimensional object at any time according to the change of the camera picture behind it.

If you want to make a real AR, you need a more complete framework for applying different algorithms to achieve different effects.So the following sections will provide a deeper and more structured explanation.Start with the framework of the program to open the real door of AR.

[Code]

Github=>

Posted by 555sandy on Wed, 06 May 2020 21:34:06 -0700