WinUI(WASDK)使用HelixToolkit載入3D模型並進行專案實踐

2023-06-07 06:00:19

前言

本人之前開發了一個叫電子腦殼的上位機應用,給稚暉君ElectronBot開源機器人提供一些功能,但是由於是結合硬體才能使用的軟體,如果擁有硬體的人員太少,就會導致我的軟體沒什麼人用,於是我就想著能不能將機器人硬體的模型載入到軟體裡,這樣使用者就可以不使用硬體也可以使用我的軟體了。於是就有了在WinUI(WASDK)裡使用3D模型的需求。

先來個B站復刻機器人的開箱視訊吧。(如果感覺無聊可以直接拖到程式碼講解部分)

庫選擇的糾結過程

在選擇庫的過程中其實並不是一帆風順,因為WinUI(WASDK)是個比較新的框架,框架本身也沒有提供3D模型載入的功能,於是我就在想到底選擇什麼樣的辦法載入,之前有看到一些UWP載入模型的demo,但是基本上就是封裝的c++的庫,對於我這c++垃圾的人來說還是很痛苦的,我的要求就是不能有c++程式碼。

要滿足模型的載入,模型的旋轉變換平移,模型材質的更換,在經過篩選以後,備選方案有three.js,HelixToolkit。

  • three.js

    GitHub Copilot: three.js 是一個基於 JavaScript 的 3D 圖形庫,它使用 WebGL 技術在瀏覽器中渲染 3D 圖形。它提供了一組易於使用的 API,使使用者可以建立高度可客製化的 3D 場景,包括模型、紋理、光源和相機等部分。此外,three.js 還支援動畫、物理模擬和粒子系統等功能,使使用者可以建立逼真的 3D 動畫效果。three.js 可以在多種瀏覽器和裝置上執行,並且有一個龐大的社群支援和貢獻。

    由於要和前端框架進行互操作,所以這個我也不是優先考慮了。

  • HelixToolkit(基於SharpDX再次封裝的)

所用框架和庫介紹

1. WASDK

這個框架是微軟最新的UI框架,我主要是用來開發程式的主體,做一些互動和功能的承載,本質上和wpf,uwp這類程式沒什麼太大的區別,區別就是一些工具鏈的不同。

2. HelixToolkit

GitHub Copilot: HelixToolkit是一個面向.NET開發人員的3D圖形庫。它提供了一組控制元件和輔助類,用於在WPF和WinForms應用程式中建立和渲染3D場景。HelixToolkit支援多種3D檔案格式,包括OBJ、STL和FBX,幷包括照明、材質、紋理和動畫等功能。它還支援高階渲染技術,如陰影、反射和透明度。HelixToolkit是開源的,可以從GitHub下載。

程式碼講解

1. Demo專案介紹

demo專案地址

專案結構如下圖:

處理過程

=>載入器載入模型=>將模型載入到模型組=>初始化相機=>進行資料到控制元件的繫結

2. 核心程式碼講解

預備知識

  • SharpDX的座標系是左手座標系

    GitHub Copilot: DirectX使用左手座標系來描述3D空間中的物件位置和方向。在左手座標系中,x軸向右,y軸向上,z軸向外(螢幕外)。這與右手座標系的z軸方向相反。左手座標系在計算機圖學中廣泛使用,因為它與人眼的觀察方式相符合。

  • 3D場景下的模型變換(平移 旋轉 縮放)

模型載入完成以後,就需要讓模型動起來,這樣才能算是完整的過程了。

核心旋轉程式碼如下:

場景是將模型繞手臂點進行旋轉,需要將手臂模型組移動到原點,旋轉完成之後再移動到原來的位置。

3. 實際專案使用

下載電子腦殼原始碼

動作序列以及表情幀資料處理實際程式碼如下:

 private void Instance_ModelActionFrame(object? sender, Verdure.ElectronBot.Core.Models.ModelActionFrame e)
    {
        BodyModel.HxTransform3D = _bodyMt * Matrix.RotationY(MathUtil.DegreesToRadians((e.J6)));

        Material = new DiffuseMaterial()
        {
            EnableUnLit = false,
            DiffuseMap = LoadTextureByStream(e.FrameStream)
        };


        var nodeList = HeadModel.GroupNode;

        foreach (var itemMode in nodeList.Items)
        {
            if (itemMode.Name == "Head3.obj")
            {
                foreach (var node in itemMode.Traverse())
                {
                    if (node is MeshNode meshNode)
                    {
                        meshNode.Material = Material;
                    }
                }
            }
        }

        var rightList = RightShoulderBoundingBox.GetCorners();

        var rightAverage = new SharpDX.Vector3(
            (rightList[1].X + rightList[5].X) / 2f,
            ((rightList[1].Y + rightList[5].Y) / 2f) - 8f,
            (rightList[1].Z + rightList[5].Z) / 2f);

        var leftList = LeftShoulderBoundingBox.GetCorners();

        var leftAverage = new SharpDX.Vector3(
            (leftList[0].X + leftList[4].X) / 2f,
            ((leftList[0].Y + leftList[4].Y) / 2f) - 8f,
            (leftList[0].Z + leftList[4].Z) / 2f);

        var translationMatrix = Matrix.Translation(-rightAverage.X, -rightAverage.Y, -rightAverage.Z);

        var tr2 = _rightArmMt * translationMatrix;

        var tr3 = tr2 * Matrix.RotationZ(MathUtil.DegreesToRadians(-(e.J2)));
        var tr4 = tr3 * Matrix.RotationX(MathUtil.DegreesToRadians(-(e.J3)));

        var tr5 = tr4 * Matrix.Translation(rightAverage.X, rightAverage.Y, rightAverage.Z);


        var tr6 = tr5 * Matrix.RotationY(MathUtil.DegreesToRadians((e.J6)));

        RightArmModel.HxTransform3D = tr6;


        var leftMatrix = Matrix.Translation(-leftAverage.X, -leftAverage.Y, -leftAverage.Z);

        var leftTr2 = _leftArmMt * leftMatrix;

        var leftTr3 = leftTr2 * Matrix.RotationZ(MathUtil.DegreesToRadians((e.J4)));
        var leftTr4 = leftTr3 * Matrix.RotationX(MathUtil.DegreesToRadians(-(e.J5)));

        var leftTr5 = leftTr4 * Matrix.Translation(leftAverage.X, leftAverage.Y, leftAverage.Z);


        var leftTr6 = leftTr5 * Matrix.RotationY(MathUtil.DegreesToRadians((e.J6)));

        LeftArmModel.HxTransform3D = leftTr6;

        var headMatrix = Matrix.Translation(-HeadModelCentroidPoint.X, -HeadModelCentroidPoint.Y, -HeadModelCentroidPoint.Z);

        var headTr2 = _headMt * headMatrix;

        var headTr3 = headTr2 * Matrix.RotationX(MathUtil.DegreesToRadians(-(e.J1)));

        var headTr4 = headTr3 * Matrix.Translation(HeadModelCentroidPoint.X, HeadModelCentroidPoint.Y, HeadModelCentroidPoint.Z);


        var headTr5 = headTr4 * Matrix.RotationY(MathUtil.DegreesToRadians((e.J6)));

        HeadModel.HxTransform3D = headTr5;
    }

最終效果如下:

個人感悟

通過模型載入的這個過程的學習,最大的感悟就是程式設計開發其實只是運用工具實現自己的想法,這個過程中我們對於工具的使用可能比較熟悉了,但是如果我們的領域知識不夠豐富,那基本上也是做不了什麼的,所以學習一些東西的時候我們的相關知識也要進行學習才好。

我也是在這個過程中才學習了什麼是左手座標系,以及4x4矩陣的變換和相機視角。這些東西都是和框架無關的知識,假如我們都學精通了,用什麼框架實現我們的想法其實都不是問題了。

總結,還是要努力學習呀。

參考推薦檔案專案如下

demo地址

電子腦殼

WASDK檔案地址

ElectronBot

HelixToolkit