【three+mediapipe】纯前端实现手势控制3d物体

1,772 阅读4分钟

大家好,我是Electrolux。这个文章讲一下纯前端实现手势控制3d物体。主要用到的技术是threejs 和 mediapipe。我们实现两个效果,第一个是手势控制模型上下移动,第二个是控制模型缩放

代码链接:gitee.com/Electrolux/…

视频链接:www.bilibili.com/video/BV1om…

mediapipe

先从mediapipe 讲起吧,这玩意是老朋友了,我上一篇文章也有讲到,简单来说,你可以理解mediapipe是一个很好用的姿势识别模型。你只要输入带有人物图像,你就可以获得人物的关节点数据

mediapipe 可以参考我这篇文章 juejin.cn/post/723232…

threejs

接下来我来简单介绍一下threejs

我们要构建threejs 基本的项目,除了引入包之外。我们最好还要了解他一些基本的概念

threejs 由 Scene camera 和 render 渲染函数 构成。其中 scene 里面 可以放我们的模型 灯光 等asset ,camera 和 render 无需介绍,就是我们的字面意思

现在让我们来封装一个threejs 初始化的模块吧

初始化模块

class threeInit {
    constructor(param) {
        // 当前模型位置的 信息
        this.param = {
            x: 0,
            y: 0,
            z: 0
        }
        this.initScene()
        this.initLight()
        this.initObject()
        this.initCamera()
    }
    // 初始化物体
    initObject() {
    }
    initScene() {      
    }
    initLight() {   
    }
    initCamera() {
    }
    initGui() {
    }       
    initRender() {
    }
}
​

初始化scene

initScene() {
    this.scene = new THREE.Scene()
}

初始化light 并添加到scene

initLight() {
    let pointLight = new THREE.PointLight(0xffffff); //创建一个白色的点光源
    pointLight.position.set(0, 0, 150);
    this.scene.add(pointLight);
    let ambient = new THREE.AmbientLight(0xffffff, 1);
    this.scene.add(ambient);
}

初始化object 并添加到scene

这里我们新建了一个圆柱体并且添加到scene。还记得我们一开始的目标吗。第一个是手势控制模型上下移动,第二个是控制模型缩放。在我们的mesh return 回来的变量中,在一个position 属性里面的我们通过控制 里面的 y变量就可以实现模型的上下移动,控制模型缩放我们也可以通过控制这个mesh 的 scale 来实现

// 初始化物体
initObject() {
    //材质
    let material = new THREE.MeshLambertMaterial({
        color: 0x05c5cf4
    });
    //创建一个立方体几何对象Geometry
    let geometry = new THREE.BoxGeometry(10, 10, 10); 
    this.mesh = new THREE.Mesh(geometry, material); 
    this.mesh.position.set(this.param.x, this.param.y, this.param.z)
    this.mesh.scale.set(0.6,0.6,0.6)
    this.scene.add(this.mesh)
}

初始化camema

initCamera() {
    this.width = 1680; //canvas画布宽度
    this.height = 1200; //canvas画布高度
    let k = this.width / this.height; //canvas画布宽高比
    //三维场景显示范围控制系数,系数越大,显示的范围越大
    //创建相机对象
    this.camera = new THREE.PerspectiveCamera(30, k, 20, 1000);
    //设置相机位置
    this.camera.position.set(300, 600, 300); 
    let v1 = new THREE.Vector3(0, 0, 0)
    //设置相机方向(指向的场景对象)
    this.camera.lookAt(v1); 
}

初始化render函数

initRender(status) {
    let v1 = new THREE.Vector3(0, 0, 0)
    // let renderer = new THREE.WebGLRenderer();
    let renderer = new THREE.WebGLRenderer({ alpha: true });
    //设置渲染区域尺寸
    renderer.setSize(this.width, this.height);
    // 添加控制器
    let control = new OrbitControls(this.camera, renderer.domElement);
    //设置相机方向(指向的场景对象)
    control.target = v1 
    control.update();
​
    // 渲染函数
    let render = () => {
        // console.log(actions)
        let status = this.status
        // actions[status]?.play();
        const delta = clock.getDelta();
        if (mixer) mixer.update(delta);
        control.update();
        // this.mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
        //执行渲染操作
        renderer.render(this.scene, this.camera);
        // mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
        //请求再次执行渲染函数render,渲染下一帧
        requestAnimationFrame(render);
    }
    render(status);
    return renderer
}

封装结束

好了,我们现在就封装完了。我们如何调用呢,可以参考下面的例子

let three = new threeInit()
document.querySelector('.canvas').appendChild(three.initRender().domElement);

结合mediapipe 和 threejs

我们要实现手势控制3d 物体,我们需要对手部的关节点进行计算

函数定义

 let loganHandle = {
     prev: {
         x: null,
         y: null
     },
     next: {
         x: null,
         y: null
     }
 }
 let distance = {
     x_distance: null,
     y_distance: null,
     allDistance: null
 }
​
 // 求x y 移动的距离
 let loganHandleFn = (val) => {
     val.x = val.x*100
     val.y = val.y*100
     if (!loganHandle.prev.x && !loganHandle.next.x) {
         loganHandle.prev = JSON.parse(JSON.stringify(val))
         return
     }
     if (loganHandle.prev.x && !loganHandle.next.x) {
         loganHandle.next = JSON.parse(JSON.stringify(loganHandle.prev))
         loganHandle.prev = JSON.parse(JSON.stringify(val))
     }
     // 更新数据
     if (loganHandle.prev.x && loganHandle.next.x) {
         loganHandle.prev = JSON.parse(JSON.stringify(loganHandle.next))
         loganHandle.next = JSON.parse(JSON.stringify(val))
     }
     if (loganHandle.prev.x && loganHandle.next.x) {
         let x_distance = Math.pow((loganHandle.prev.x - loganHandle.next.x), 2)
         let y_distance = Math.pow((loganHandle.prev.y - loganHandle.next.y), 2)
         // console.log("笛卡尔距离:",x_distance,y_distance,Math.log2(x_distance+y_distance))
         if (Math.log2(x_distance + y_distance)) {
             // console.log("笛卡尔距离:",x_distance,y_distance,Math.log2(x_distance+y_distance))
             distance.allDistance = Math.log2(x_distance + y_distance)
​
         }
         distance.x_distance = Math.log2(x_distance)
         distance.y_distance = Math.log2(y_distance)
     }
 }

实现手势控制 模型 上下移动

loganHandleFn({
    // 传入 拇指的x y 轴距离,这里的thumb.x 从mediapipe中 拿到
    x:thumb.x,
    y:thumb.y
})
​
运行 后 帧之间的x y 距离就能 得到计算
// 假如输出 distance.x_distance 就是 手指 x轴移动的距离
// 假如输出 distance.y_distance 就是 手指 y轴移动的距离// 那么我们 在 调用 这个函数 之后 可以 用
model.position.y = model.position.y + (loganHandle.prev.y - loganHandle.next.y) *4   进行控制 模型上下移动

实现手势控制 模型 缩放

简单计算一个 3维点 的 笛卡尔距离就好了

 let thumb = results?.leftHandLandmarks[4]
 let secondThumb = results?.leftHandLandmarks[8]
 let distance_tempX = Math.pow((thumb.x - secondThumb.x), 2)
 let distance_tempY = Math.pow((thumb.y - secondThumb.y), 2)
 let distance_tempZ = Math.pow((thumb.z - secondThumb.z), 2)
 let distance_thumb= Math.pow((distance_tempX + distance_tempY + distance_tempZ), 0.5)
 // console.log(distance)
​
​
let scale  = distance_thumb
// 实现缩放
model.scale.set(scale*6,scale*6, scale*6);

代码链接:gitee.com/Electrolux/…

视频链接:www.bilibili.com/video/BV1om…

好了,大概就这样吧