當微信小程序遇到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真正的大門。
【代碼】