最近有新成员加入本团队,为了方便其开发HoloLens1 / HoloLens2,将不定时更新HoloLens相关开发相关内容。
软件需求:
HoloLens 1:VS2017 + Unity2017;HoloLens 2:VS2019 + Unity2019;
1.安装VS2017 / VS2019,HoloLens 1安装Win10 SDK 17134或者17763,HoloLens2要求至少18362;相关安装与配置请参考博文.
2.Unity2017 / Unity2019,安装UWP平台;
注:如果使用的软件为VS2019和Unity2019来开发HoloLens1,可参考HoloLens2的开发过程,修改MRTK的配置文件为HoloLens1即可!可参考博文1,博文2.
上一节介绍了HoloLens+Trilib插件综合开发-Part1,简单介绍了Trilib插件及示例工程Scene1,本节将开始Trilib插件在HoloLens端使用基础介绍。
首先拷贝Trilib插件的示例工程Scene1到自己新建的文件夹下,自定义重命名,eg:HoloTrilib.
打开场景HoloTrilib后看到左侧面板上如下图所示
主要功能脚本都在ForegroundCanvas下挂载:
1)AssetLoader下挂载AssetDownloader和AssetLoaderWindow组件。通过查看脚本内容可以看出,AssetDownloader组件为消息处理脚本;AssetLoaderWindow组件与AssetLoader下的界面UI绑定,主要为界面按钮得响应;
2)FileLoader下的FileOpenDialog为文件选取脚本,主要作用为调用系统的FileBrowser,显示文件目录提供给用户选取相应的fbx文件;
3)ErrorDialog如名字一般,其主要为错误反馈组件;
4)URIDialog没有用到,根据名字看是网络下载模型窗口;
5)LoadingTime显示加载模型用时;
我们在HoloLens上开发主要用到本地加载,因此着重于AssetLoader和FileLoader两个组件即可。
1.AssetDownloader不需要修改,后期直接挂在我们的场景中即可
2.AssetLoaderWindow中最重要的部分如下:
private void LoadLocalAssetButtonClick()
{
var fileOpenDialog = FileOpenDialog.Instance;
fileOpenDialog.Title = "Please select a File";
fileOpenDialog.Filter = AssetLoaderBase.GetSupportedFileExtensions() + "*.zip;";
#if !UNITY_EDITOR && UNITY_WINRT && (NET_4_6 || NETFX_CORE || NET_STANDARD_2_0) && !ENABLE_IL2CPP && !ENABLE_MONO
fileOpenDialog.ShowFileOpenDialog(delegate (byte[] fileBytes, string filename)
{
LoadInternal(filename, fileBytes);
#else
fileOpenDialog.ShowFileOpenDialog(delegate (string filename)
{
LoadInternal(filename);
#endif
}
);
}
private void LoadInternal(string filename, byte[] fileBytes = null)
{
_loadingTimer.Reset();
_loadingTimer.Start();
PreLoadSetup();
var assetLoaderOptions = GetAssetLoaderOptions();
if (!Async)
{
using (var assetLoader = new AssetLoader())
{
assetLoader.OnMetadataProcessed += AssetLoader_OnMetadataProcessed;
try
{
#if !UNITY_EDITOR && UNITY_WINRT && (NET_4_6 || NETFX_CORE || NET_STANDARD_2_0) && !ENABLE_IL2CPP && !ENABLE_MONO
var extension = FileUtils.GetFileExtension(filename);
_rootGameObject = assetLoader.LoadFromMemoryWithTextures(fileBytes, extension, assetLoaderOptions, _rootGameObject);
#else
if (fileBytes != null && fileBytes.Length > 0)
{
var extension = FileUtils.GetFileExtension(filename);
_rootGameObject = assetLoader.LoadFromMemoryWithTextures(fileBytes, extension, assetLoaderOptions, _rootGameObject);
}
else if (!string.IsNullOrEmpty(filename))
{
//获取obj
_rootGameObject = assetLoader.LoadFromFileWithTextures(filename, assetLoaderOptions);
}
else
{
throw new System.Exception("File not selected");
}
#endif
}
catch (Exception exception)
{
ErrorDialog.Instance.ShowDialog(exception.ToString());
}
}
if (_rootGameObject != null)
{
PostLoadSetup();
ShowLoadingTime();
}
else
{
HideLoadingTime();
}
}
else
{
using (var assetLoader = new AssetLoaderAsync())
{
assetLoader.OnMetadataProcessed += AssetLoader_OnMetadataProcessed;
try
{
if (fileBytes != null && fileBytes.Length > 0)
{
var extension = FileUtils.GetFileExtension(filename);
assetLoader.LoadFromMemoryWithTextures(fileBytes, extension, assetLoaderOptions, null, delegate (GameObject loadedGameObject)
{
_rootGameObject = loadedGameObject;
if (_rootGameObject != null)
{
PostLoadSetup();
ShowLoadingTime();
}
else
{
HideLoadingTime();
}
});
}
else if (!string.IsNullOrEmpty(filename))
{
assetLoader.LoadFromFileWithTextures(filename, assetLoaderOptions, null, delegate (GameObject loadedGameObject)
{
_rootGameObject = loadedGameObject;
if (_rootGameObject != null)
{
PostLoadSetup();
ShowLoadingTime();
}
else
{
HideLoadingTime();
}
});
}
else
{
throw new Exception("File not selected");
}
}
catch (Exception exception)
{
HideLoadingTime();
ErrorDialog.Instance.ShowDialog(exception.ToString());
}
}
}
}
这两个函数是底层实例化模型的接口函数,即用户在UI界面通过选择要加载的数模后,通过文件地址以及相关参数,Unity根据这两个函数实例化数模。
3.FileOpenDialog为文件选择脚本,我们将在HoloLens端开发使用时具体修改。
读者会注意到在LoadLocalAssetButtonClick函数中包含“#if…#else…#endif” 的结构,这是因为HoloLens所在的UWP平台和Unity所在的Unity在函数接口上的不一致,因此需要根据平台的不同,使用不同的接口用法。
经过以上介绍,我们在进行HoloLens+Trilib时,只需要保留需要的本地加载组件,同时还需要引入HoloLens交互组件,因此进行如下准备工作。
1.场景中,BackgroundCanvas删除,新建Canvas命名为“LoadCanvas”,取出ForegroundCanvas下的FileLoader组件,将其设为LoadCanvas的子物体,同时,把ForegroundCanvas剩余部分,如下所示
在LoadCanvas下新建UI-Button,并调整LoadCanvas的属性(Transform),将其调整到相机的可视范围内。这里为了后期上传HoloLens不需要再调整Canvas的比例,建议直接将LoadCanvas比例缩小,并保证相机可视。
2.为了方便HoloLens交互的舒适感,这里使用HoloToolKit的交互组件,因此需要导入HoloToolKit组件,如果苏读者有自己的交互设计,可忽略此步。
至此,准备工作完成,可以正式开始HoloLens+Trilib开发。
1.新建空物体,命名为Manager,挂载AssetDownloader脚本
2.Copy AssetLoaderWindow脚本至Scripts文件夹下,重命名,如myFBXLib,打开脚本修改脚本名,保持与文件名一致。
同时删除脚本中所有与UI有关的声明,以及所有URI加载数模的响应函数,最终如下
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
#if NETFX_CORE //UWP下编译
using Windows.Storage;
#endif
using Debug = UnityEngine.Debug;
namespace TriLib
{
namespace Samples
{
[RequireComponent(typeof(AssetDownloader))]
public class myFBXLib : MonoBehaviour
{
public static AssetLoaderWindow Instance { get; private set; }
public bool Async;
private GameObject _rootGameObject;
///
/// Handles "Load local asset button" click event and tries to load an asset at chosen path.
///
public void LoadLocalAssetButtonClick()
{
var fileOpenDialog = FileOpenDialog.Instance;
fileOpenDialog.Title = "Please select a File";
fileOpenDialog.Filter = AssetLoaderBase.GetSupportedFileExtensions() + "*.zip;";
#if !UNITY_EDITOR && UNITY_WINRT && (NET_4_6 || NETFX_CORE || NET_STANDARD_2_0) && !ENABLE_IL2CPP && !ENABLE_MONO
fileOpenDialog.ShowFileOpenDialog(delegate (byte[] fileBytes, string filename)
{
LoadInternal(filename, fileBytes);
#else
fileOpenDialog.ShowFileOpenDialog(delegate (string filename)
{
LoadInternal(filename);
#endif
}
);
}
///
/// Loads a model from the given filename or given file bytes.
///
/// Model filename.
/// Model file bytes.
private void LoadInternal(string filename, byte[] fileBytes = null)
{
PreLoadSetup();
var assetLoaderOptions = GetAssetLoaderOptions();
if (!Async)
{
using (var assetLoader = new AssetLoader())
{
assetLoader.OnMetadataProcessed += AssetLoader_OnMetadataProcessed;
try
{
#if !UNITY_EDITOR && UNITY_WINRT && (NET_4_6 || NETFX_CORE || NET_STANDARD_2_0) && !ENABLE_IL2CPP && !ENABLE_MONO
var extension = FileUtils.GetFileExtension(filename);
_rootGameObject = assetLoader.LoadFromMemoryWithTextures(fileBytes, extension, assetLoaderOptions, _rootGameObject);
#else
if (fileBytes != null && fileBytes.Length > 0)
{
var extension = FileUtils.GetFileExtension(filename);
_rootGameObject = assetLoader.LoadFromMemoryWithTextures(fileBytes, extension, assetLoaderOptions, _rootGameObject);
}
else if (!string.IsNullOrEmpty(filename))
{
//获取obj
_rootGameObject = assetLoader.LoadFromFileWithTextures(filename, assetLoaderOptions);
}
else
{
throw new System.Exception("File not selected");
}
#endif
}
catch (Exception exception)
{
ErrorDialog.Instance.ShowDialog(exception.ToString());
}
}
if (_rootGameObject != null)
{
PostLoadSetup();
}
}
else
{
using (var assetLoader = new AssetLoaderAsync())
{
assetLoader.OnMetadataProcessed += AssetLoader_OnMetadataProcessed;
try
{
if (fileBytes != null && fileBytes.Length > 0)
{
var extension = FileUtils.GetFileExtension(filename);
assetLoader.LoadFromMemoryWithTextures(fileBytes, extension, assetLoaderOptions, null, delegate (GameObject loadedGameObject)
{
_rootGameObject = loadedGameObject;
if (_rootGameObject != null)
{
PostLoadSetup();
}
});
}
else if (!string.IsNullOrEmpty(filename))
{
assetLoader.LoadFromFileWithTextures(filename, assetLoaderOptions, null, delegate (GameObject loadedGameObject)
{
_rootGameObject = loadedGameObject;
if (_rootGameObject != null)
{
PostLoadSetup();
}
});
}
else
{
throw new Exception("File not selected");
}
}
catch (Exception exception)
{
ErrorDialog.Instance.ShowDialog(exception.ToString());
}
}
}
}
///
/// Event assigned to FBX metadata loading. Editor debug purposes only.
///
/// Type of loaded metadata
/// Index of loaded metadata
/// Key of loaded metadata
/// Value of loaded metadata
private void AssetLoader_OnMetadataProcessed(AssimpMetadataType metadataType, uint metadataIndex, string metadataKey, object metadataValue)
{
Debug.Log("Found metadata of type [" + metadataType + "] at index [" + metadataIndex + "] and key [" + metadataKey + "] with value [" + metadataValue + "]");
}
///
/// Gets the asset loader options.
///
/// The asset loader options.
private AssetLoaderOptions GetAssetLoaderOptions()
{
var assetLoaderOptions = AssetLoaderOptions.CreateInstance();
assetLoaderOptions.DontLoadCameras = false;
assetLoaderOptions.DontLoadLights = false;
assetLoaderOptions.AddAssetUnloader = true;
return assetLoaderOptions;
}
///
/// Pre Load setup.
///
private void PreLoadSetup()
{
if (_rootGameObject != null)
{
Destroy(_rootGameObject);
_rootGameObject = null;
}
}
///
/// Post load setup.
///
private void PostLoadSetup()
{
_rootGameObject.transform.eulerAngles = new Vector3(0, 0, 0);
_rootGameObject.transform.position = new Vector3(0, 0, (float)0.5);
_rootGameObject.transform.localScale = new Vector3((float)0.008, (float)0.008, (float)0.008);
}
}
}
}
1.把myFBXLib脚本挂在Manager上.
2.选中之前加入的Button,将其响应函数设定为myFBXLib中的LoadLocalAssetButtonClick.
3.先在PC上尝试一下效果,运行如下
4.为了在HoloLens上运行,需要加上HoloToolKit的交互组件,搜索并打开示例场景——InteractiveButtonComponents,复制其中的如下三个组件至我们的场景中,同时把Main Camera取消勾选,并调整Canvas到合适位置.
5.其他相关上传HoloLens的设置如常即可,导出部署,将数模放在HoloLens的3D Object目录下,运行效果如下
需要注意的是,PostLoadSetup函数中localScale的大小需要根据模型的大小自己设定,否则会出现加载后的模型过大/过小的问题,影响效果。
根据以上设定,如此便实现了Trilib插件在HoloLens上动态加载数模的功能。
细心的读者会发现,当前的工程实现还存在一些问题,例如:
1.每次点击UI后,HoloLens主线程暂停,造成假死状态,且需要持续一段时间后才能正常打开FileBrowser,且每点一次按钮会有相同的情况,影响操作流畅感;
2.已加载模型后,再次点击按钮发现新的模型被加载,已加载模型消失;
3.对于已经加载的模型能否对其进行后续操作;
4.每次默认打开HoloLens的3D Object文件夹,能否选择其他文件夹内的数模;
5.单个按钮的UI过于单调,如何在复杂的UI场景中使用Trilib插件;
以上的问题的答案是肯定的,均可以通过修改相关脚本实现以上功能,笔者将在后续推出教程。
以上为HoloLens+Trilib插件综合开发的Part2,介绍了Trilib插件在HoloLens中的简单使用,但该工程相对简单,还不能满足我们在正常工程中的使用,笔者将在后续继续更新相关内容。欢迎批评指正!
风和日暖,令人愿意永远活下去 .HDarker