小程序关于AR 的开发实现 basic版(基于2.20.0版本,上)

894 阅读5分钟

ps.这一章主要是AR(2.20.0)的平面放置类型

打算把小程序上的AR(2.20.0),xr-frame(2.27.0 beta),three.js ,webgl 基础都学习一下

前言:增强现实脱离不了我们的摄像头这一个概念,主要的一个思想是将摄像头拍摄出来的视频,逐帧渲染到我们定义好的一个画布(canvas)上。这个时候相当于把拍摄的视频和我们想要实现的二维动画一起放到画布上渲染出来.

1. 先根据小程序上提供的代码将我们拍到视频渲染出来

1.1 首先初始化一个画布canvas,在wxml 中定义一个canvas


 // wxml
<view>
  <canvas type="webgl" id="webgl" style="width: {{width}}px; height: {{height}}px">
  </canvas>
</view>
// js
 onReady() {
    this.initCanvas();
  },
  
  initCanvas:function(){
    wx.createSelectorQuery()
    .select('#webgl')
    .node()
    .exec(res => {
      this.canvas = res[0].node
      const info = wx.getSystemInfoSync()
      const pixelRatio = info.pixelRatio
      const calcSize = (width, height) => {
        console.log(`canvas size2: width = ${width} , height = ${height}`)
        this.canvas.width = width * pixelRatio / 2
        this.canvas.height = height * pixelRatio / 2
        this.setData({
          width,
          height,
        })
      }
      calcSize(info.windowWidth, info.windowHeight * 0.6)

      this.initVK();
    })
  },
  
  

1.2 initVK 是VKSession的初始化,

它是实现AR 的很重要的一个API.它提供了以下几种模式,详细文档如下 developers.weixin.qq.com/miniprogram…

{
    version:"v1|v2",
    track:{
        plane:{mode:3},  //标明是平面 这里只是示例,具体应用看文档
        marker:true|false,  // marker 跟踪配置
        OSD:boolean  //OSD 跟踪配置 |
        face:{}  // 人脸识别
        OCR   // 
    },
    gl:WebGLRenderingContext  // 绑定的 WebGLRenderingContext 对象
}

其中有一个WebGLRenderingContext参数,是一个3D的对象,使用three.js 创建。所以初始化一个three构造函数。

这里面初始化包括了three 、camera、scene、 light、renderer 等对象.

此处使用 threejs-miniprogram 包,这是经过特殊封装以兼容小程序环境的 three.js 包,当然开发者们也可以替换成任意其它可以在小程序中跑的 WebGL 引擎,此处仅仅是以 three.js 来举例。registerGLTFLoader 则是用来加载 3D 模型。关于 three.js 的使用,这里只是给出了一个简单的 demo,有兴趣者可以查阅官方文档进行了解。 ——来源小程序官网[代码片段]

code.juejin.cn/pen/7166053… gltf-loader 文件过大 把它放在这个文件中的script中

import {
  createScopedThreejs
} from './threejs-miniprogram'

import {
  registerGLTFLoader
} from '../loaders/gltf-loader'

  // 初始化three.js 对象
  initThree:function(){
    const Three = this.Three = createScopedThreejs(this.canvas)
    registerGLTFLoader(Three)

    // 相机
    this.camera = new Three.Camera()

    // 场景
    const scene = this.scene = new Three.Scene()

    // 光源
    const light1 = new Three.HemisphereLight(0xffffff, 0x444444) // 半球光
    light1.position.set(0, 0.2, 0)
    scene.add(light1)
    const light2 = new Three.DirectionalLight(0xffffff) // 平行光
    light2.position.set(0, 0.2, 0.1)
    scene.add(light2)

    // 渲染层
    const renderer = this.renderer = new Three.WebGLRenderer({
        antialias: true,
        alpha: true
    })
    renderer.gammaOutput = true
    renderer.gammaFactor = 2.2
  },

1.3 在完成 WebGLRenderer 实例化之后

,我们需要对renderer 实例进行一些属性操作,比如说创建着色器WebGLShader gl.createShader(type);,创建缓冲区 vao gl.getExtension('OES_vertex_array_object').这些都是webgl 渲染的时候设置的属性(renderer)

// 这些都是微信小程序官网的代码
// 设置着色器 
initShader(){
    const gl = this.gl = this.renderer.getContext()
    const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM)
    const vs = `
      attribute vec2 a_position;
      attribute vec2 a_texCoord;
      uniform mat3 displayTransform;
      varying vec2 v_texCoord;
      void main() {
        vec3 p = displayTransform * vec3(a_position, 0);
        gl_Position = vec4(p, 1);
        v_texCoord = a_texCoord;
      }
    `
    const fs = `
      precision highp float;
      uniform sampler2D y_texture;
      uniform sampler2D uv_texture;
      varying vec2 v_texCoord;
      void main() {
        vec4 y_color = texture2D(y_texture, v_texCoord);
        vec4 uv_color = texture2D(uv_texture, v_texCoord);
        float Y, U, V;
        float R ,G, B;
        Y = y_color.r;
        U = uv_color.r - 0.5;
        V = uv_color.a - 0.5;
        
        R = Y + 1.402 * V;
        G = Y - 0.344 * U - 0.714 * V;
        B = Y + 1.772 * U;
        
        gl_FragColor = vec4(R, G, B, 1.0);
      }
    `
    const vertShader = gl.createShader(gl.VERTEX_SHADER)
    gl.shaderSource(vertShader, vs)
    gl.compileShader(vertShader)

    const fragShader = gl.createShader(gl.FRAGMENT_SHADER)
    gl.shaderSource(fragShader, fs)
    gl.compileShader(fragShader)

    // https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/createProgram
    const program = this._program = gl.createProgram()
    this._program.gl = gl
    gl.attachShader(program, vertShader)
    gl.attachShader(program, fragShader)
    gl.deleteShader(vertShader)
    gl.deleteShader(fragShader)
    gl.linkProgram(program)
    gl.useProgram(program)

    const uniformYTexture = gl.getUniformLocation(program, 'y_texture')
    gl.uniform1i(uniformYTexture, 5)
    const uniformUVTexture = gl.getUniformLocation(program, 'uv_texture')
    gl.uniform1i(uniformUVTexture, 6)

    this._dt = gl.getUniformLocation(program, 'displayTransform')
    gl.useProgram(currentProgram)
  },
  
  // 设置vao
  initVAO(){
    
    const gl = this.renderer.getContext()
    const ext = gl.getExtension('OES_vertex_array_object')
    this.ext = ext

    const currentVAO = gl.getParameter(gl.VERTEX_ARRAY_BINDING)
    const vao = ext.createVertexArrayOES()

    ext.bindVertexArrayOES(vao)

    const posAttr = gl.getAttribLocation(this._program, 'a_position')
    const pos = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, pos)
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW)
    gl.vertexAttribPointer(posAttr, 2, gl.FLOAT, false, 0, 0)
    gl.enableVertexAttribArray(posAttr)
    vao.posBuffer = pos

    const texcoordAttr = gl.getAttribLocation(this._program, 'a_texCoord')
    const texcoord = gl.createBuffer()
    gl.bindBuffer(gl.ARRAY_BUFFER, texcoord)
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, 0, 1, 1, 0, 0, 0]), gl.STATIC_DRAW)
    gl.vertexAttribPointer(texcoordAttr, 2, gl.FLOAT, false, 0, 0)
    gl.enableVertexAttribArray(texcoordAttr)
    vao.texcoordBuffer = texcoord

    ext.bindVertexArrayOES(currentVAO)
    this._vao = vao
  },
  // 我们把这两个方法放在一起调用
  initWebGL:function(){
    this.initShader();
    this.initVAO();
  },

1.4 在此,是把初始化gl实例的所有属性都设置好了

,接下来就是使用 wx.createVKSession,创建一个session 实例。在初始化session 之前,需要调用initThree,initWbgGL。不过initVK 是在获取canvas dom实例之后才调用

initVK:function(){
    this.initThree()
    console.log("this.Three",this.Three);
    this.initWebGL();
    console.log("this.gl",this.gl);
    if(this.initSession){
      this.initSession();
    }
  },
  
initSession:function(){
    console.log('this.gl', this.gl)
    const session =this.session= wx.createVKSession({
      track: {
        plane: {
          mode: 3,
        },
        marker:true,
      },
      version:'v1',
      gl: this.gl
    })
    session.start(err => {
      console.log("session.start");
      if (err) return console.error('VK error: ', err);
      
      const canvas = this.canvas
      console.log("session.end");
      console.log(`canvas size: width = ${canvas.width} , height = ${canvas.height}`)
      const onFrame = tiemstame=>{
        const frame = session.getVKFrame(canvas.width,canvas.height);
        console.log("frame",frame);
        if(frame){
          console.log("frame ing",frame);
            this.renderFrame(frame)
        }
        // 帧渲染
        session.requestAnimationFrame(onFrame)
      };
      session.requestAnimationFrame(onFrame);
    })
  },

1.5 初始化session 之后,就可以渲染了

renderFrame:function(frame){
    this.renderGL(frame);
},

// 这些详细代码都在小程序官网,不甚理解 书读百遍,其义自现
renderGL:function(frame){
    const gl = this.renderer.getContext()
    gl.disable(gl.DEPTH_TEST)
    const {
        yTexture,
        uvTexture
    } = frame.getCameraTexture(gl, 'yuv')
    const displayTransform = frame.getDisplayTransform()
    if (yTexture && uvTexture) {
        const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM)
        const currentActiveTexture = gl.getParameter(gl.ACTIVE_TEXTURE)
        const currentVAO = gl.getParameter(gl.VERTEX_ARRAY_BINDING)

        gl.useProgram(this._program)
        this.ext.bindVertexArrayOES(this._vao)

        gl.uniformMatrix3fv(this._dt, false, displayTransform)
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)

        gl.activeTexture(gl.TEXTURE0 + 5)
        const bindingTexture5 = gl.getParameter(gl.TEXTURE_BINDING_2D)
        gl.bindTexture(gl.TEXTURE_2D, yTexture)

        gl.activeTexture(gl.TEXTURE0 + 6)
        const bindingTexture6 = gl.getParameter(gl.TEXTURE_BINDING_2D)
        gl.bindTexture(gl.TEXTURE_2D, uvTexture)

        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)

        gl.bindTexture(gl.TEXTURE_2D, bindingTexture6)
        gl.activeTexture(gl.TEXTURE0 + 5)
        gl.bindTexture(gl.TEXTURE_2D, bindingTexture5)

        gl.useProgram(currentProgram)
        gl.activeTexture(currentActiveTexture)
        this.ext.bindVertexArrayOES(currentVAO)
    }
  },

到此为止就可以让相机拍摄的视频渲染到我们的canvas 上了。

2. 接下来就是将一些3d文件放置在我们的canvas.

2.1 点击方式一个机器人

在canvas上绑定一个触摸事件 bindtouchend

<canvas type="webgl" id="webgl" style="width: {{width}}px; height: {{height}}px" bindtouchend="onTouchEnd">
    </canvas>

2.2编写 触摸事件的方法

VKSession 的 hitTest 接口。这个接口的主要是为了将 2D 坐标转成 3D 世界坐标,即 (x, y) 转成 (x, y, z)。通俗来说就是画面上显示的桌子,在屏幕上它是 2D 的,当我们手指触摸屏幕时拿到的坐标是 2D 坐标,也就是 (x, y);hitTest 接口可以将其转换成 3D 世界坐标 (x, y, z),而 3D 世界坐标系的原点则是相机打开瞬间其所在的点

// 触摸屏幕放置机器人
  onTouchEnd(evt) {
    // 点击位置放一个机器人
    const touches = evt.changedTouches.length ? evt.changedTouches : evt.touches
    if (touches.length === 1) {
      const touch = touches[0]
      if (this.session && this.scene && this.model) {
        const hitTestRes = this.session.hitTest(touch.x / this.data.width, touch.y / this.data.height, this.resetPanel)
        this.resetPanel = false
        if (hitTestRes.length) {
          const model = this.getRobot()
          model.matrixAutoUpdate = false
          model.matrix.fromArray(hitTestRes[0].transform)
          this.scene.add(model)
        }
      }
    }
  },

2.3 这个this.model 是由一个glb文件转换来的,

我们来看一下this.getRobot的方法

getRobot() {
    const Three = this.Three
    const model = new Three.Object3D()
    model.add(this.copyRobot())
    this._insertModels = this._insertModels || []
    this._insertModels.push(model)
    if (this._insertModels.length > 5) {
      const needRemove = this._insertModels.splice(0, this._insertModels.length - 5)
      needRemove.forEach(item => {
        if (item._mixer) {
          const mixer = item._mixer
          this.mixers.splice(this.mixers.indexOf(mixer), 1)
          mixer.uncacheRoot(mixer.getRoot())
        }
        if (item.parent) item.parent.remove(item)
      })
    }
    return model
  },

2.4 model是 通过我们之前

初始化好的Three中的Object3D 生成的,下面的这段逻辑还是小程序官网的示例代码,生成了5个机器人,放在一个数组缓存中,return model的这个model 是新生成的,是在点击屏幕的时候,就能生成。放在数组缓存中的永远是最新的5个,model.matrix.fromArray(hitTestRes[0].transform) 这段代码也是把这个model 的位置给记录了下来。

copyRobot() {
    const Three = this.Three
    const {
      scene,
      animations
    } = cloneGltf(this.model, Three)
    scene.scale.set(0.05, 0.05, 0.05)
    // scene.rotate.set(0,8,1)

    // 动画混合器
    const mixer = new Three.AnimationMixer(scene)
    for (let i = 0; i < animations.length; i++) {
      const clip = animations[i]
      if (clip.name === 'Dance') {
        const action = mixer.clipAction(clip)
        action.play()
      }
    }
    this.mixers = this.mixers || []
    this.mixers.push(mixer)
    scene._mixer = mixer
    return scene
  },

2.5 这里面有一个this.model

,它是由 three中的一个loader方法将我们的glb文件转成model,这个是初始化了一个model模型 ,上面的是给这个模型添加动画 现在来初始化一个model

initModal(){
    const Three = this.Three;
    const loader = new Three.GLTFLoader()
          const mkUrl = "https://aod.cos.tx.xmcdn.com/storages/c7d4-audiofreehighqps/41/E3/GKwRIDoHSSl-ABAAAAHL1Sqc.glb"
          const logoUrl = "https://aod.cos.tx.xmcdn.com/storages/0206-audiofreehighqps/75/AB/GKwRIUEHQiThAAI_gAHHeVIW.glb"
          const rabotUrl = "https://aod.cos.tx.xmcdn.com/storages/3c67-audiofreehighqps/72/0A/GKwRIDoHSSrtAAcUdAHL1ilu.glb"
          loader.load(rabotUrl, gltf => {
            this.model = {
              scene: gltf.scene,
              animations: gltf.animations,
            }
          })
    this.clock = new Three.Clock()
  },

2.6 上文中我们提到 renderGl 是将我们摄像头拍摄的视频渲染出来了,现在是将 我们在this.scene中的动画渲染出来

renderFrame(frame){
    this.renderGL(frame)
    const camera = frame.camera

    const dt = this.clock.getDelta()
    if (this.mixers) {
        this.mixers.forEach(mixer => mixer.update(dt))
    }

    // 相机
    if (camera) {
        this.camera.matrixAutoUpdate = false
        this.camera.matrixWorldInverse.fromArray(camera.viewMatrix)
        this.camera.matrixWorld.getInverse(this.camera.matrixWorldInverse)

        const projectionMatrix = camera.getProjectionMatrix(NEAR, FAR)
        this.camera.projectionMatrix.fromArray(projectionMatrix)
        this.camera.projectionMatrixInverse.getInverse(this.camera.projectionMatrix)
    }

    this.renderer.autoClearColor = false
    this.renderer.render(this.scene, this.camera)
    this.renderer.state.setCullFace(this.Three.CullFaceNone)
  },

这样应该就大功告成了