Kinect for Windows SDK v2.0 开发笔记 (十四) 高清面部帧(5) 面部模型 3D



(转载请注明出处)

使用SDK: Kinect for Windows SDK v2.0 public preview1409

同前面,因为SDK未完成,不附上函数/方法/接口的超链接。


这次让我们跨上3D吧。

所用接口是Direct3D11,还没学过OpenGL.

不过, D3D11的内容不再本节内,毕竟光D3D11就可以讲几十节。


在编码前,我们总结一下:


顶点, Kinect对于一位追踪对象,可以实时提供1347个顶点(由GetFaceModelVertexCount获取),那么,这1k+个顶点

的拓扑结构是什么?三角链(TRIANGLELIST)?还是三角带(TRIANGLESTRIP)?不,这些顶点均是独立的,每个点对应面部的某个

位置。所以需要索引。


索引, Kinect对每个面部模型的1347个顶点提供了对应的2630个三角形(由GetFaceModelTriangleCount获取)即

7890个索引(数字也够巧的),也就是说平均每个顶点使用了近6次。获取索引缓存的函数是GetFaceModelTriangles

第一个参数是索引的数量(3倍于三角面的数量),目前就是7890,第二个参数自然就是索引缓存(CPU)了.


导出,我们需要支持导出.obj文本文件,这个格式的具体格式这里不说明,不过请注意,obj文件中索引从1开始


询问策略, 这次我们使用D3D11,新开线程自然使用轮询模式,不过,我们键入S键保存数据,但是消息处理与渲染不在

同一线程,所以应对公共数据使用互斥锁保证安全.


渲染策略 D2D与D3D11在Win8上面可以完美交互,所以我们使用D2D渲染2D,使用D3D渲染3D,但是请注意

D2D与D3D的切换会造成性能问题,因为D2D必须保存并恢复D3D上下文的状态。所以原则上我们应该使用D3D11,正交渲染

彩色帧图像,但是请允许笔者偷懒: 使用D2D渲染背景彩色帧,再D3D渲染面部模型,最后,使用D2D渲染文本信息。


渲染模型 怎么渲染面部模型呢,因为面部模型使用了索引,那么法线交给CPU计算那是相当麻烦的。不使用法线,那

么没办法通过光照显示3D模型,那么模型就是一坨了,使用线框渲染就是一个不错的选择。不过为了好看,还是

多费点力,使用GPU计算法线吧。


D3D10新增的几何着色器就派上了用场,即时计算法线也是很简单(向量叉乘再单位化)。于是,着色器采用VS-GS-PS模式。

顶点着色器负责进行坐标转换,几何着色器负责计算法线,像素着色器负责计算光照,输出颜色。

那么,常量缓存策略:

VS: 3个转换矩阵,不夹杂其他的

GS: 理论应该带上一个世界转换矩阵,但是笔者偷懒,不对模型进行世界转换,所以就不带常量缓存了

PS: 简单光照模型而已,定式了,毕竟Phong模型简单(自己还是不懂)实用


编写完shader,利用VS自带的编译功能(右键, hlsl,选择模型等级5, 选择对应的着色器, 输出cso文件),使用时直接

载入即可,省去运行时编译时间,并且还在debug模式下保留调试信息方便图形调试(见笔者另一篇《Direct2D 1.1 开发笔记 特效篇(四) 图形调试》)


顶点输入布局就输入顶点,索引缓存就从GetFaceModelTriangles获取。


初始化初始化方面, Windows那里 创建窗口, Kinect 打开Kinect与彩色帧(用于显示背景)、骨骼帧(用于获取跟踪id)与高清面部帧(获取顶点)。

Kinect.Face.h文件中,提供的几个函数

HRESULT WINAPI CreateFaceFrameSource(_In_ IKinectSensor* sensor, _In_ const UINT64 initialTrackingId, _In_ const DWORD initialFaceFrameFeatures, _COM_Outptr_ IFaceFrameSource** ppSource);
HRESULT WINAPI CreateHighDefinitionFaceFrameSource(_In_ IKinectSensor* sensor, _COM_Outptr_ IHighDefinitionFaceFrameSource** hdFaceFrameSource);
HRESULT WINAPI CreateFaceAlignment(_COM_Outptr_ IFaceAlignment** ppFaceAlignment);
HRESULT WINAPI CreateFaceModel(float scale, UINT32 capacity, _In_reads_(capacity) float* pDeformations, _COM_Outptr_ IFaceModel** ppFaceModel);
HRESULT WINAPI GetFaceModelVertexCount(_Out_ UINT32* pVertexCount);
HRESULT WINAPI GetFaceModelTriangleCount(_Out_ UINT32* pTriangleCount);
HRESULT WINAPI GetFaceModelTriangles(UINT32 capacity, _Out_writes_all_(capacity) UINT32* triangeVertices);

除了第一个,因为不是高清面部帧,其余的均会用上,可见本节是"高清面部帧"的最后一节了,基本完成了.


创建设备无关对象,初始化D3D,初始化D2D。这部分就不说了。


模型方面

    // 面部模型
    class FaceModel{
    public:
        // 构造
        FaceModel();
        // 创造
        HRESULT Create(ID3D11Device* device);
        // 刷新常量缓存
        void RefreshCB(ID3D11DeviceContext* context);
        // 刷新顶点缓存
        void RefreshVB(ID3D11DeviceContext* context);
        // 析构
        ~FaceModel(){ 
            SafeRelease(m_pVertexBuffer);
            SafeRelease(m_pIndexBuffer);
            SafeRelease(m_pInputLayout);
            SafeRelease(m_pVSCBuffer);
            SafeRelease(m_pPSCBuffer);
            SafeRelease(m_pVertexShader);
            SafeRelease(m_pGeometryShader);
            SafeRelease(m_pPixelShader);
        }
        // 渲染
        void Render(ID3D11DeviceContext* context);
    private:
        // 模型顶点缓存
        ID3D11Buffer*           m_pVertexBuffer = nullptr;
        // 模型索引缓存
        ID3D11Buffer*           m_pIndexBuffer = nullptr;
        // 顶点输入布局
        ID3D11InputLayout*      m_pInputLayout = nullptr;
        // 顶点着色器 常量缓存
        ID3D11Buffer*           m_pVSCBuffer = nullptr;
        // 像素着色器 常量缓存
        ID3D11Buffer*           m_pPSCBuffer = nullptr;
        // 顶点着色器    
        ID3D11VertexShader*     m_pVertexShader = nullptr;
        // 几何着色器
        ID3D11GeometryShader*   m_pGeometryShader = nullptr;
        // 像素着色器
        ID3D11PixelShader*      m_pPixelShader = nullptr;
    public:
        // 顶点着色器常量缓存
        MatrixBufferType                    vscb;
        // 像素着色器常量缓存
        LightMaterialBufferType             pscb;
        // 顶点数量
        UINT                                vertex_count = 0;
        // 面部网格
        VertexNormal*                       face_mash_vertexs = nullptr;
        // 索引数量
        UINT32                              index_count = 0;
        // 索引
        UINT32*                             index = nullptr;
        // 收集状态
        FaceModelBuilderCollectionStatus    co_status = FaceModelBuilderCollectionStatus_MoreFramesNeeded;
        // 收集状态
        FaceModelBuilderCaptureStatus       ca_status = FaceModelBuilderCaptureStatus_GoodFrameCapture;
    };

创建顶点缓存(GPU)、索引缓存(GPU)、输入布局、VS,PS常量缓存、VS,PS,GS。


每帧渲染:

// SceneRenderer::FaceModel 渲染
void SceneRenderer::FaceModel::Render(ID3D11DeviceContext* context){
    //if (!context) return;
    // ---- 装配顶点
    // 设置顶点缓冲跨度和偏移.
    UINT offset = 0;
    UINT stride = sizeof(SceneRenderer::VertexNormal);
    // IA: 输入本实例的顶点缓存
    context->IASetVertexBuffers(0, 1, &m_pVertexBuffer, &stride, &offset);
    // IA : 设置索引缓存
    context->IASetIndexBuffer(m_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0);
    // 拓扑结构: 三角链
    context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    // ---- 装配着色器缓存
    // 设置顶点着色器常量缓存 写入常量缓存寄存器0 (b0)
    context->VSSetConstantBuffers(0, 1, &m_pVSCBuffer);
    // 设置像素着色器常量缓存 写入常量缓存寄存器0 (b0)
    context->PSSetConstantBuffers(0, 1, &m_pPSCBuffer);
    // 设置布局
    context->IASetInputLayout(m_pInputLayout);
    // 设置VS
    context->VSSetShader(m_pVertexShader, nullptr, 0);
    // 设置PS
    context->PSSetShader(m_pPixelShader, nullptr, 0);
    // ---- 装配着色器
    // 设置布局
    context->IASetInputLayout(m_pInputLayout);
    // 设置VS
    context->VSSetShader(m_pVertexShader, nullptr, 0);
    // 设置GS
    context->GSSetShader(m_pGeometryShader, nullptr, 0);
    // 设置PS
    context->PSSetShader(m_pPixelShader, nullptr, 0);
    // ---- 渲染
    context->DrawIndexed(index_count,0, 0);
}


每次获取了彩色帧数据,更新背景位图;




每次获取面部顶点,更新顶点缓存(CPU),再更新顶点缓存(GPU)。

    // 获取面部顶点
    if (SUCCEEDED(hr)){
        // 上锁
        m_muxFaceVertice.lock();
        // 获取数据
        hr = m_pFaceModel->CalculateVerticesForAlignment(m_pFaceAlignment, m_cFaceVerticeCount, m_pFaceVertices);
        // 解锁
        m_muxFaceVertice.unlock();
    }
    // 更新DX端数据
    if (SUCCEEDED(hr)){
        memcpy(
            m_SceneRenderer.face_model.face_mash_vertexs,
            m_pFaceVertices,
            sizeof(DirectX::XMFLOAT3) * m_cFaceVerticeCount
            );
        m_SceneRenderer.RefreshFaceData();
    }

// 刷新顶点缓存
void SceneRenderer::FaceModel::RefreshVB(ID3D11DeviceContext* context){

    D3D11_MAPPED_SUBRESOURCE mapped_subsource;
    // 更新顶点缓存
    HRESULT hr = context->Map(m_pVertexBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_subsource);

    // 写入数据
    if (SUCCEEDED(hr)){
        // 写入数据
        memcpy(mapped_subsource.pData, face_mash_vertexs, sizeof(VertexNormal)*this->vertex_count);
        // 结束映射
        context->Unmap(m_pVertexBuffer, 0);
    }
}


高清面部帧没有跟踪对象就尝试从骨骼帧中找一个合适的跟踪id:


// 检查骨骼帧
void ThisApp::check_body_frame(){
    // 骨骼帧
    IBodyFrame* pBodyFrame = nullptr;
    HRESULT hr = m_pBodyFrameReader->AcquireLatestFrame(&pBodyFrame);
    if (!pBodyFrame) return;
    // 获取骨骼数据
    if (SUCCEEDED(hr)) {
        hr = pBodyFrame->GetAndRefreshBodyData(BODY_COUNT, m_apBody);
    }
    // 高清面部帧源未被跟踪时尝试更换ID
    BOOLEAN tracked = FALSE;
    // 检查是否未被跟踪
    if (SUCCEEDED(hr)){
        hr = m_pHDFaceFrameSource->get_IsTrackingIdValid(&tracked);
    }
    // 高清面部帧源未被跟踪时尝试更换ID
    if (SUCCEEDED(hr) && !tracked){
        for (int i = 0; i < BODY_COUNT; ++i) {
            hr = m_apBody[i]->get_IsTracked(&tracked);
            if (SUCCEEDED(hr) && tracked){
                UINT64 id = 0;
                if (FAILED(m_apBody[i]->get_TrackingId(&id))) continue;
#ifdef _DEBUG
                _cwprintf(L"更换ID: %08X-%08X\n", HIDWORD(id), LODWORD(id));
#endif
                m_pHDFaceFrameSource->put_TrackingId(id);
                break;
            }
        }

    }
    SafeRelease(pBodyFrame);
}


注:

在打开彩色帧后,之后随即获取彩色帧帧形容,用来获取视角弧度(get_VerticalFieldOfView)

因为笔者想要渲染的面部模型与彩色帧中图像中脸一般大。

    // 获取彩色帧帧形容
    if(SUCCEEDED(hr)){
        hr = pColorFrameSource->get_FrameDescription(&pColorFrameDescription);
        // 设置D3D 相机视野 与Kinect 彩色摄像机一致
        if (pColorFrameDescription){
            int width = 0, height = 0;
            float angle_y=0.f;
            pColorFrameDescription->get_Height(&height);
            pColorFrameDescription->get_Width(&width);
            pColorFrameDescription->get_VerticalFieldOfView(&angle_y);
            // 计算透视转换
            auto projection = DirectX::XMMatrixPerspectiveFovLH(
                DirectX::XM_PI * angle_y / 180.f,
                static_cast<float>(WNDWIDTH) / (static_cast<float>(WNDHEIGHT)),
                SCREEN_NEAR_Z,
                SCREEN_FAR_Z
                );
            // 透视
            DirectX::XMStoreFloat4x4(
                &m_SceneRenderer.face_model.vscb.projection,
                projection
                );
        }
    }

所以随便窗口也设为16:9.


透视转换也就如此,世界转换前面说了,不进行变化,至于视角转换,那就是从原点朝前面看。


关于坐标系 Kinect的相机坐标系是右手坐标系,上面的代码是XMMatrixPerspectiveFovLH,即笔者这里是

使用了左手坐标系,这是因为笔者希望窗口是面"镜子",可以从彩色帧看出,彩色帧默认是镜像的.



这次需要为CPU端分配3块内存, 1 是储存相机坐标系 顶点数据, 2 是储存 准备送入GPU 的顶点数据,

3是索引缓存。理论上1、2应该合并,但是为了数据的灵活性,分开比较好。

3块内存可以一次性分配,提高效率。


再仔细想想,好像除了导出obj文件,就与上节没什么不同了。


那么这就是成果图:

这次没有错字啦!但是

还....是...有...问..题!我又忘了!



还有就是没有对齐,可见彩色帧坐标与相机坐标不是对齐的状态,毕竟彩色摄像头不与红外摄像头同一位置。

希望对齐的话就使用坐标映射对象,或者简单调整一下视角,只要不是离得太远用户感受不到,不需要精确

到像素点.



我们这次能够导出obj文件,那么,我们随便找个3D建模软件,打开试试:



天哪!我的亲妈都认不出是自己啊,,希望微软官方提供皮肤纹理采集。

1k+个顶点,2k+个三角面感觉还是挺粗糙啊


下载地址:点击这里







展开阅读全文

没有更多推荐了,返回首页