微信小程序遇到AR,会擦出怎么样的火花期待激动......

通过该教程,可以从基础开始打造一个微信小程序的AR框架,所有代码开源,提供大家学习。

本课程需要一定的基础:微信开发者工具,JavaScript,Html,Css

第四章:基石-摄像头与Three.js结合


【前情提要】

  上一章,前面的两章内容,我们学习了基本的摄像头数据读取以及Three.js三维场景的创建。这两章内容学习之后,我们已经可以做很多更定制化的开发了。例如:

  1. 我们已经可以做基于摄像头图像的AR内容开发,(比如人脸识别,AR美妆涂口红,戴帽子,适眼镜等等)

  2. WebGL的三维游戏。

  这一章,既是基础也是升华,主要是探讨,如何在微信小程序中出现摄像头画面的背景,然后在背景之上渲染出WebGL的三维内容。

1. 实现在微信小程序中访问摄像头,并且可以实时的拿到每一帧画面的数据。
2. 实现在微信小程序中访问WebGL接口,实现绘制三维物体。该教程采用Three.js引擎
3. 实现在背景为摄像头实时画面的背景上显示WebGL的3D物体。
4. 整体框架搭建
5. 图像算法接入

【目的】

微信小程序中实现摄像头画面为背景,之上渲染WebGL内容

 [方案]

  在开始开发之前,我们想罗列一下各种可能的方案。

  要想在摄像头画面之上渲染出WebGL的内容,有一种方案比较容易想到:

方案一:双层Canvas结构

  如上图所示,我们可以构建两个画布,一个画布用来渲染摄像头的画面,另一个画布用来渲染WebGL的画面。WebGL的画面在上层,而摄像头的画面在下层,并且WebGL画布的背景要是透明的。

  这种方案,就要求我们的画布支持多层的结构,并且WebGL的背景支持透明。

方案二:单层Canvas,WebGL内平面贴图

  这种方案就采用纯粹的WebGL画布,在场景中建立一个竖直面向摄像头的平面,并将摄像头画面的每一帧图像作为贴图显示在平面上。

  当然两种方案各有各的优缺点,就目前而言:

  1. 方案一的优点是结构简单,不需要在场景中添加物体,再每一帧贴图。不过可能不同手机对于WebGL透明背景的支持并不好,另外在计算WebGL层上物体的位置的时候需将坐标转换到摄像头画面层上的坐标才可以显示正常。

  2. 方案二,结构相对复杂,不过可以适配性更强,全部采用WebGL硬件加速。三维物体和摄像头画面的坐标转换在同一个场景中完成。

【准备】

  下面需要搭建环境,做一些准备工作。

  首先,需要注册微信小程序开发者。注册地址=>

  注册成功之后,需要下载微信小程序开发工具。下载地址=>

  目前笔者的开发环境是:Windows 10

  下载的微信小程序版本为:RC v1.0.2.1909111

【创建工程】

  按照与上第二章同样的步骤,我们创建一个简单的基本工程。这里就不再赘述了。这里我们分别建立两个page:“scenario1”和“scenario2”,分别用来演示两种方案。同时也添加“libs”文件夹,并将第三章中修改过的three.js文件放入进去。建立好之后的项目目录如下:

【开发:方案一(scenario1文件夹下)】

  首先,我们在index.wxml文件中按顺序添加用于摄像头和用于WebGL的2个层:

<!--index.wxml-->
<view>
<!--WebGL层-->
<canvas
type="webgl"
id="webgl"
canvas-id="webgl"
style="position:fixed;top:0;width:{{canvasWidth}}px;height:{{canvasHeight}}px;z-index:1;">
</canvas>
<!--摄像头层-->
<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>

  需要注意的是,两个标签中的style属性,相比于之前的2各章节,都添加了position,top和z-index字段的设置,这样是为了让每个层都是从最手机屏幕最上方开始,并且保证WebGL层在上面。

  接下来,我们可以把上一章中关于three.js创建的旋转cube的代码复制过来。也就是把上一个章节中的index.js文件的内容复制过来。唯一需要修改的就是在创建WebGLRenderer渲染器的时候,指定渲染器的背景是透明的。具体代码如下:

//index.js

//导入three.js库
import * as THREE from '../../libs/three.js' //获取应用实例
const app = getApp(); Page({
data: {
canvasWidth: 0,
canvasHeight: 0
}, /**
* 页面加载回调函数
*/
onLoad: function () {
//初始化Canvas对象
this.initWebGLCanvas();
},
/**
* 初始化Canvas对象
*/
initWebGLCanvas: function () {
//获取页面上的标签id为webgl的对象,从而获取到canvas对象
var query = wx.createSelectorQuery();
query.select('#webgl').node().exec((res) => {
var canvas = res[0].node;
this._webGLCanvas = canvas;
//获取系统信息,包括屏幕分辨率,显示区域大小,像素比等
var info = wx.getSystemInfoSync();
this._sysInfo = info;
//设置canvas的大小,这里需要用到窗口大小与像素比乘积来定义
this._webGLCanvas.width = this._sysInfo.windowWidth * this._sysInfo.pixelRatio;
this._webGLCanvas.height = this._sysInfo.windowHeight * this._sysInfo.pixelRatio;
//设置canvas的样式
this._webGLCanvas.style = {};
this._webGLCanvas.style.width = this._webGLCanvas.width.width;
this._webGLCanvas.style.height = this._webGLCanvas.width.height;
//设置显示层canvas绑定的样式style数据,页面层则直接用窗口大小来定义
this.setData({
canvasWidth: this._sysInfo.windowWidth,
canvasHeight: this._sysInfo.windowHeight
});
this.initWebGLScene();
});
},
/**
* 初始化WebGL场景
*/
initWebGLScene: function () {
//创建摄像头
var camera = new THREE.PerspectiveCamera(60, this._webGLCanvas.width / this._webGLCanvas.height, 1, 1000);
this._camera = camera;
//创建场景
var scene = new THREE.Scene();
this._scene = scene; //创建Cube几何体
var cubeGeo = new THREE.CubeGeometry(30, 30, 30);
//创建材质,设置材质为基本材质(不会反射光线,设置材质颜色为绿色)
var mat = new THREE.MeshBasicMaterial({ color: 0x00FF00 });
//创建Cube的Mesh对象
var cube = new THREE.Mesh(cubeGeo, mat);
//设置Cube对象的位置
cube.position.set(0, 0, -100);
//将Cube加入到场景中
this._scene.add(cube); //创建渲染器,指定渲染器背景透明
var renderer = new THREE.WebGLRenderer({
canvas: this._webGLCanvas,
alpha:true
});
//设置渲染器大小
this._renderer = renderer;
this._renderer.setSize(this._webGLCanvas.width, this._webGLCanvas.height);
//记录当前时间
var lastTime = Date.now();
this._lastTime = lastTime;
//开始渲染
this.renderWebGL(cube);
},
/**
* 渲染函数
*/
renderWebGL: function (cube) {
//获取当前一帧的时间
var now = Date.now();
//计算时间间隔,由于Date对象返回的时间是毫秒,所以除以1000得到单位为秒的时间间隔
var duration = (now - this._lastTime) / 1000;
//打印帧率
console.log(1 / duration + 'FPS');
//重新赋值上一帧时间
this._lastTime = now;
//旋转Cube对象,这里希望每秒钟Cube对象沿着Y轴旋转180度(Three.js中用弧度标是,所以是Math.PI)
cube.rotation.y += duration * Math.PI; //渲染执行场景,指定摄像头看到的画面
this._renderer.render(this._scene, this._camera);
//设置帧回调函数,并且每一帧调用自定义的渲染函数
this._webGLCanvas.requestAnimationFrame(() => {
this.renderWebGL(cube);
});
}
})

  保存代码,编译运行,我们就可以看到在摄像头画面的背景之上,出现了我们的旋转Cube了(真机上测试同样效果),并且帧率也是维持在60FPS左右的。

【开发:方案二(scenario2文件夹下)】

  按照之前对方案的描述,我们:

  1. 首先在场景中创建一个Plane平面的Geometry

  2. 接着我们在Camera的回调函数中更新一个贴图

  3. 最后在渲染器更新中将新的贴图应用到Plane平面上面

  首先我们来编写index.wxml文件,代码如下:

<!--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
mode="normal"
device-position="back"
flash="auto"
frame-size="medium"
style="position:fixed;top:-100%;width:100%;height:100%;">
</camera>
</view>

  在这个代码中我们不需要指定z-index的值,而是将摄像头Camera标签的style属性将top设置为了-100%,这样这一层就在屏幕的外面的,不会显示。因为后面我们会将摄像头显示的画面显示在WebGL中。

  接下来,我们就可以编写index.js文件了。

//index.js

//导入three.js库
import * as THREE from '../../libs/three.js' //获取应用实例
const app = getApp(); Page({
data: {
canvasWidth: 0,
canvasHeight: 0
}, /**
* 页面加载回调函数
*/
onLoad: function () {
//初始化Camera
this.initCamera();
//初始化Canvas对象
this.initWebGLCanvas();
},
/**
* 初始化Canvas对象
*/
initWebGLCanvas: function () {
//获取页面上的标签id为webgl的对象,从而获取到canvas对象
var query = wx.createSelectorQuery();
query.select('#webgl').node().exec((res) => {
var canvas = res[0].node;
this._webGLCanvas = canvas;
//获取系统信息,包括屏幕分辨率,显示区域大小,像素比等
var info = wx.getSystemInfoSync();
this._sysInfo = info;
//设置canvas的大小,这里需要用到窗口大小与像素比乘积来定义
this._webGLCanvas.width = this._sysInfo.windowWidth * this._sysInfo.pixelRatio;
this._webGLCanvas.height = this._sysInfo.windowHeight * this._sysInfo.pixelRatio;
//设置canvas的样式
this._webGLCanvas.style = {};
this._webGLCanvas.style.width = this._webGLCanvas.width.width;
this._webGLCanvas.style.height = this._webGLCanvas.width.height;
//设置显示层canvas绑定的样式style数据,页面层则直接用窗口大小来定义
this.setData({
canvasWidth: this._sysInfo.windowWidth,
canvasHeight: this._sysInfo.windowHeight
});
//初始化场景
this.initWebGLScene();
});
},
/**
* 初始化摄像头
*/
initCamera:function()
{
//获取Camera Coontext对象
const cContex = wx.createCameraContext();
//添加帧回调事件监听器
const listener = cContex.onCameraFrame((frame) => {
//在回调事件中,拿到每一帧的数据
var data = new Uint8Array(frame.data);
//通过RGBA的数据格式生成贴图
var tex = new THREE.DataTexture(data, frame.width, frame.height, THREE.RGBAFormat);
//清理次摄像头数据的贴图
if(this._tex != null)
{
this._tex.dispose();
}
//保留最新帧的贴图
this._tex = tex;
});
//启动监听
listener.start();
},
/**
* 初始化WebGL场景
*/
initWebGLScene: function () {
//创建摄像头
var camera = new THREE.PerspectiveCamera(60, this._webGLCanvas.width / this._webGLCanvas.height, 1, 1000);
this._camera = camera;
//创建场景
var scene = new THREE.Scene();
this._scene = scene; //创建Cube几何体
var cubeGeo = new THREE.CubeGeometry(30, 30, 30);
//创建材质,设置材质为基本材质(不会反射光线,设置材质颜色为绿色)
var mat = new THREE.MeshBasicMaterial({ color: 0x00FF00 });
//创建Cube的Mesh对象
var cube = new THREE.Mesh(cubeGeo, mat);
//设置Cube对象的位置
cube.position.set(0, 0, -100);
//将Cube加入到场景中
this._scene.add(cube); //创建平面几何
var planeGeo = new THREE.PlaneGeometry(100,100);
//创建平面的MEsh
var plane = new THREE.Mesh(planeGeo,new THREE.MeshBasicMaterial());
//设置平面的位置,为了不让平面挡住前面的Cube,所以将平面设置的更远了。
plane.position.set(0,0,-200);
//将平面加入到场景中
this._scene.add(plane); //创建渲染器,指定渲染器背景透明
var renderer = new THREE.WebGLRenderer({
canvas: this._webGLCanvas,
});
//设置渲染器大小
this._renderer = renderer;
this._renderer.setSize(this._webGLCanvas.width, this._webGLCanvas.height);
//记录当前时间
var lastTime = Date.now();
this._lastTime = lastTime;
//开始渲染
this.renderWebGL(cube,plane);
},
/**
* 渲染函数
*/
renderWebGL: function (cube,plane) {
//获取当前一帧的时间
var now = Date.now();
//计算时间间隔,由于Date对象返回的时间是毫秒,所以除以1000得到单位为秒的时间间隔
var duration = (now - this._lastTime) / 1000;
//打印帧率
//console.log(1 / duration + 'FPS');
//重新赋值上一帧时间
this._lastTime = now;
//旋转Cube对象,这里希望每秒钟Cube对象沿着Y轴旋转180度(Three.js中用弧度标是,所以是Math.PI)
cube.rotation.y += duration * Math.PI;
//设置plane的贴图
if(this._tex != null)
{
//当前摄像头贴图存在的时候
if(plane.material != null)
{
//清理上次帧的材质
plane.material.dispose();
}
//用新的贴图生成新的材质赋值给平面对象
plane.material = new THREE.MeshBasicMaterial({color: 0xFFFFFF, map: this._tex});
}
//渲染执行场景,指定摄像头看到的画面
this._renderer.render(this._scene, this._camera);
//设置帧回调函数,并且每一帧调用自定义的渲染函数
this._webGLCanvas.requestAnimationFrame(() => {
//启动下一帧渲染
this.renderWebGL(cube,plane);
});
}
})

  新的Js有几个地方的改动:

  1. 首先在OnLoad函数中添加了一个初始化Camera的自定义函数initCamera,这个函数中添加了帧事件监听器,并且用每一帧返回的数据生成了一个新的贴图存于this._tex对象上面。在创建贴图的地方,我们用到了Three.js的DataTexture贴图类型,它可以通过一个像素值数组来创建贴图。在第二章中我们已经知道手机相机的帧回调函数中返回的每一帧的数据是RGBA的形式,所以按照这个格式就可以正确的创建贴图了。

  2. 在场景初始化函数中,新创建了一个平面,这个平面放置到了距离Cube更远(相对于Camera)的距离上。这样不会挡住Cube。

  3.在渲染函数中,传入了平面对象plane,并且每一次渲染更新plane的贴图。

  保存,编译,我们就可以看到最后的效果了(真机测试同样有效):

  不过,我们会发现现在有一些问题,就是Plane上面摄像头的贴图画面是反的,左右上下都有颠倒。着说面摄像头的帧事件中传回来的每一帧画面的值里像素的排布顺序和three.js中贴图Texture里像素的排布顺序不一样。所以我们需要将plane旋转一下,才可以看到正确的结果。另外左右相反的问题,旋转之后就到了平面的背面,默认情况下,平面的背面是不会显示出来的。所以,我们需要将平面的材质设置为双面材质,这样才可以显示背面的内容。

所以要修改一下,平面创建时候的代码:

 //创建平面几何
var planeGeo = new THREE.PlaneGeometry(100,100);
//创建平面的MEsh
var plane = new THREE.Mesh(planeGeo,new THREE.MeshBasicMaterial());
//设置平面的位置,为了不让平面挡住前面的Cube,所以将平面设置的更远了。
plane.position.set(0,0,-200);
//旋转平面的方向,正确的显示摄像头画面
plane.rotation.z = Math.PI;
plane.rotation.y = Math.PI;
//将平面加入到场景中
this._scene.add(plane);

  以及渲染函数中,创建材质的代码

//设置plane的贴图
if(this._tex != null)
{
//当前摄像头贴图存在的时候
if(plane.material != null)
{
//清理上次帧的材质
plane.material.dispose();
}
//用新的贴图生成新的材质赋值给平面对象,并设置为双面材质
plane.material = new THREE.MeshBasicMaterial({color: 0xFFFFFF, map: this._tex, side:THREE.DoubleSide});
}

  这样我们看到的画面就是正确的了。

  不过这时细心的朋友又会有新的疑问了。摄像头创建的贴图长宽是按照每一帧画面的长宽,即frame.height和frame.width得到的,但是我们将这个贴图贴在了一个正方形的平面上,由于贴图的尺寸不是正方形,所以会导致最后看到的画面被拉伸或者压缩了。另外整个平面也没全全屏占满整个屏幕。

  这些问题就需要了解Three.js的三维空间知识,利用屏幕的长宽比和透视矩阵计算得到正确的平面对象plane的长宽,这样才可以全屏的显示这个平面。

  又会有同学问,现在这个平面和Cube都是在三维场景中的,假如场景中的物体被放大了,或者位置接近平面的位置了,就会发生碰撞,导致显示出现bug。

  这个问题就是我们方案二存在的一个弊端。要修复它也可以有很多形式。比如多个摄像头分别渲染摄像头画面和三维场景再贴加起来,有点类似CSS里面多个Canvas的层次结构。也可以时刻改变plane的位置在最远处,等等。

  这些就需要在实际的应用场景中具体的结局了。

【总结】

  通过这一章的学习,我们终于有了一些质的变化了。可以在摄像头背景下面显示三维物体。当然通过这一章的学习,我们也了解了如何将摄像头的画面变为贴图,应用到三维物体上。

  至此,AR的几个基本的要素我们都已经做出了技术实现,获取摄像头数据,显示三维物体,在摄像头画面前显示三维物体。这样我们已经可以在小程序中实现在第一章中介绍的现有的AR方案,也就是识别一个图片显示不带跟随信息的三维物体。

  不带有跟随信息也就是说不需要将三维物体的位置时刻根据背后摄像头画面的变化而变化。

  如果要做一个真正的AR,那就需要一个更完整的框架,以便应用不同的算法,实现不同的效果。所以下面的章节,将会做更深此次的和更结构化的讲解。从程序的框架入手,打开AR真正的大门。

【代码】

Github=>

当微信小程序遇到AR(四)的更多相关文章

  1. 当微信小程序遇到AR(二)

    当微信小程序遇到AR,会擦出怎么样的火花?期待与激动...... 通过该教程,可以从基础开始打造一个微信小程序的AR框架,所有代码开源,提供大家学习. 本课程需要一定的基础:微信开发者工具,JavaS ...

  2. 微信小程序把玩(四)应用生命周期

    原文:微信小程序把玩(四)应用生命周期 App() 函数用来注册一个小程序,注意必须在 app.js 中注册,且不能注册多个. 使用方式也跟Android中的Application中初始化一些全局信息 ...

  3. 微信小程序把玩(四十)animation API

    原文:微信小程序把玩(四十)animation API 动画水还是比较深的,这里只是简单介绍下小程序中动画的一些属性和注意事项,做动画前一定要整理好思路将动画一步步分解,再进行组合!这里只做引入. w ...

  4. 微信小程序把玩(四十一)canvas API

    原文:微信小程序把玩(四十一)canvas API 绘图是每个移动应用必备的技术,基本上和Android,IOS,等移动开发都是相同的,创建个上下文,给你个画布再上画,官网给的小例子都比较全了自己去看 ...

  5. 当微信小程序遇到AR(一)

    当微信小程序遇到AR,会擦出怎么样的火花?期待与激动...... 通过该教程,可以从基础开始打造一个微信小程序的AR框架,所有代码开源,提供大家学习. 本课程需要一定的基础:微信开发者工具,JavaS ...

  6. 当微信小程序遇到AR(三)

    当微信小程序遇到AR,会擦出怎么样的火花?期待与激动...... 通过该教程,可以从基础开始打造一个微信小程序的AR框架,所有代码开源,提供大家学习. 本课程需要一定的基础:微信开发者工具,JavaS ...

  7. 天河微信小程序入门《四》:融会贯通,form表单提交数据库

    天河在阔别了十几天之后终于又回来了.其实这篇文章里的demo是接着(天河微信小程序入门<三>)后面就做了的,但是因为最近在做别的项目,所以就偷懒没有发出来.放到今天来看,从前台提交数据到数 ...

  8. 微信小程序开发系列四:微信小程序之控制器的初始化逻辑

    微信小程序开发系列教程 微信小程序开发系列一:微信小程序的申请和开发环境的搭建 微信小程序开发系列二:微信小程序的视图设计 微信小程序开发系列三:微信小程序的调试方法 这个教程的前两篇文章,介绍了如何 ...

  9. 微信小程序开发(四)学习基本组件

    现在我们已经学会使用工具了,再来了解,测试一下微信小程序的常用组件,所谓组件,就是微信团队已经开发好的一些常用标签,我们只需要掌握用法就可以了,当然,以后学得深入了,也可以开发自己的组件,让其他人使用 ...

随机推荐

  1. Python3发送webservice请求

    Python3使用suds-jurko库来发送webservice接口请求 导入请求webservice接口需要用到的包 pip install suds-jurko 第一步:导入所需要的包 from ...

  2. linux 批量创建用户

    user 模块添加用户 python -c 'from passlib.hash import sha512_crypt; import getpass; print (sha512_crypt.en ...

  3. SIGAI机器学习第八集 数据降维1

    讲授数据降维原理,PCA的核心思想,计算投影矩阵,投影算法的完整流程,非线性降维技术,流行学习的概念,局部线性嵌入,拉普拉斯特征映射,局部保持投影,等距映射,实际应用 大纲: 数据降维问题PCA的思想 ...

  4. 无缓存I/O操作和标准I/O文件操作区别

    本文转载于:http://www.360doc.com/content/11/0521/11/5455634_118306098.shtml 首先,先稍微了解系统调用的概念:       系统调用,英 ...

  5. 解决Virtualbox的根分区容量不够用问题

    现在Virtualbox新建一块磁盘.容量一定要比原来的大.然后执行克隆命令. 把原来的磁盘内容克隆到新磁盘上.然后重新启动电脑. 运行相关扩容命令即可. #克隆磁盘 cd C:\Program Fi ...

  6. windows游戏编程X86 (内存)寄存器相关的基本概念

    本系列文章由jadeshu编写,转载请注明出处.http://blog.csdn.net/jadeshu/article/details/22446971 作者:jadeshu   邮箱: jades ...

  7. tarjan——校园网(缩点,再构图)

    P2746 [USACO5.3]校园网Network of Schools 任务一:求缩完点后入度为0的点的个数(有向边) 任务二:求缩完点后入度为0和出度为0的最大值(要把图构造成强连通分量) 注意 ...

  8. Js中Array常用方法小结

    说起Array的方法,不免让人皱一下眉头,下面我们从增删改查角度依次来总结. 1.增 push: 将传入的参数 ,插入数组的尾部,并返回新数组的长度.不管传入参数为一个值还是一个数组,都作为插入数组的 ...

  9. 感知机和BP神经网络

    一.感知机 1.感知机的概念 感知机是用于二分类的线性分类模型,其输入是实例的特征向量,输出是实例的类别,类别取+1和-1二个值,+1代表正类,-1代表负类.感知机对应于输入空间(特征空间)中将实例分 ...

  10. 利用csv文件批量编辑更新sql

    历史表(popularity_ranking)数据中只存了用户手机号,业务需求中需要新增用户昵称字段, 这里我们用户表和popularity_ranking表在不同数据库中,有两种方法:1.编写后台服 ...