最近项目里有人脸捕捉的需求,刚开始时参考的下面这篇文章,使用官方发布的Facial AR Remote,需要我们自己构建IOS客户端,因此需要准备包括MacOS操作系统、Xcode等开发环境,在Unity构建出Xcode工程后,还要考虑开发许可证等问题,而且在尝试时,我使用的Xcode13版本,在编译上还有一些问题,比较麻烦。
https://www.163.com/dy/article/E70U8CLT0526E124.html
随后发现了另一个解决方案,即Live Capture,IOS客户端已经发布于App Store中,名称Unity Face Capture:
Live Capture在Package Manager中通过git url的方式进行添加,地址:
Live Capture官方手册地址:
https://docs.unity.cn/Packages/com.unity.live-capture@1.0/manual/index.html
PDF文档下载地址:
文档很详细,也可以参考下面的步骤:
1.新建空物体,添加Take Recoder组件,它依赖Playable Director组件,添加Take Recorder时自动添加Playable Director:
2.将制作好的包含人脸的模型拖入场景,挂载ARKit Face Actor组件,然后将其做成Prefab预制体。
3.在Take Recoder组件下方点击+按钮,添加ARKit Face Device,并将其Actor设为步骤2中挂载了ARKit Face Actor组件的人脸模型:
4.在Project窗口右键,Create / Live Capture / ARKit Face Capture / Mapper,创建一个Head Mapper资产,将其Rig Prefab设为步骤2中生成的Prefab,并设置人脸模型中对应的的Left Eye、RightEye、Head骨骼节点:
5.点击Head Mapper资产中的Add Renderer按钮,绑定BlendShape,名称如果与ARKit中要求一致则会自动绑定好,否则需要一一对应设置:
6.将编辑好的Head Mapper资产赋值给步骤2挂载的ARKit Face Actor组件中的Mapper:
7.Window / Live Capture / Connections 打开Connections窗口,创建服务器,点击Start即可启动:
8.启动后打开IOS客户端,点击Connect进行连接,若连接不上,检查一下手机和电脑是否处于同一网段,检查电脑防火墙,最好都关掉。
另外值得注意的是,服务端的启动是通过在Connections编辑器窗口中点击Start开启的,因此它的使用环境是在Unity编辑器环境中,如果想打包后运行时使用,需要将其从Package Manager中迁移到工程Assets目录下,并创建脚本编写启动方法:
创建LiveCaptureServer类,继承Server:
usingSystem; usingSystem.IO; usingSystem.Linq; usingSystem.Text; usingUnityEngine; usingSystem.Collections.Generic; usingUnity.LiveCapture.Networking; usingUnity.LiveCapture.Networking.Discovery; namespaceUnity.LiveCapture.CompanionApp{ [CreateAssetMenu(menuName="Live Capture/Server")] publicclassLiveCaptureServer : Server { constintk_DefaultPort=9000; /// <summary>/// The server executes this event when a client has connected./// </summary>publicstaticeventAction<ICompanionAppClient>ClientConnected=delegate { }; /// <summary>/// The server executes this event when a client has disconnected./// </summary>publicstaticeventAction<ICompanionAppClient>ClientDisconnected=delegate { }; structConnectHandler { publicstringName; publicDateTimeTime; publicFunc<ICompanionAppClient, bool>Handler; } readonlyDictionary<string, Type>s_TypeToClientType=newDictionary<string, Type>(); staticreadonlyList<ConnectHandler>s_ClientConnectHandlers=newList<ConnectHandler>(); /// <summary>/// Adds a callback used to take ownership of a client that has connected./// </summary>/// <param name="handler">The callback function. It must return true if it takes ownership of a client.</param>/// <param name="name">The name of the client to prefer. If set, this handler has priority over clients that have the given name.</param>/// <param name="time">The time used to determine the priority of handlers when many are listening for the same/// client <paramref name="name"/>. More recent values have higher priority.</param>/// <exception cref="ArgumentNullException">Thrown if <paramref name="handler"/> is null.</exception>publicstaticvoidRegisterClientConnectHandler(Func<ICompanionAppClient, bool>handler, stringname, DateTimetime) { if (handler==null) thrownewArgumentNullException(nameof(handler)); DeregisterClientConnectHandler(handler); s_ClientConnectHandlers.Add(newConnectHandler { Name=name, Time=time, Handler=handler, }); } /// <summary>/// Removes a client connection callback./// </summary>/// <param name="handler">The callback to remove.</param>>/// <exception cref="ArgumentNullException">Thrown if <paramref name="handler"/> is null.</exception>publicstaticvoidDeregisterClientConnectHandler(Func<ICompanionAppClient, bool>handler) { if (handler==null) thrownewArgumentNullException(nameof(handler)); for (vari=0; i<s_ClientConnectHandlers.Count; i++) { if (s_ClientConnectHandlers[i].Handler==handler) { s_ClientConnectHandlers.RemoveAt(i); } } } publicvoidInit() { foreach (var (type, attributes) inAttributeUtility.GetAllTypes<ClientAttribute>()) { if (!typeof(CompanionAppClient).IsAssignableFrom(type)) { Debug.LogError($"{type.FullName} must be assignable from {nameof(CompanionAppClient)} to use the {nameof(ClientAttribute)} attribute."); continue; } foreach (varattributeinattributes) { s_TypeToClientType[attribute.Type] =type; } } } [SerializeField, Tooltip("The TCP port on which the server will listen for incoming connections. Changes to the port only take effect after restarting the server.")] intm_Port=k_DefaultPort; [SerializeField, Tooltip("Start the server automatically after entering play mode.")] boolm_AutoStartOnPlay=true; readonlyDiscoveryServerm_Discovery=newDiscoveryServer(); readonlyNetworkServerm_Server=newNetworkServer(); readonlyDictionary<Remote, ICompanionAppClient>m_RemoteToClient=newDictionary<Remote, ICompanionAppClient>(); /// <summary>/// The TCP port on which the server will listen for incoming connections./// </summary>/// <remarks>/// Changes to the port only take effect after restarting the server./// </remarks>publicintPort { get=>m_Port; set { if (m_Port!=value) { m_Port=value; OnServerChanged(true); } } } /// <summary>/// Start the server automatically after entering play mode./// </summary>publicboolAutoStartOnPlay { get=>m_AutoStartOnPlay; set { if (m_AutoStartOnPlay!=value) { m_AutoStartOnPlay=value; OnServerChanged(true); } } } /// <summary>/// Are clients able to connect to the server./// </summary>publicboolIsRunning=>m_Server.IsRunning; /// <summary>/// The number of clients currently connected to the server./// </summary>publicintClientCount=>m_RemoteToClient.Count; /// <inheritdoc/>protectedoverridevoidOnEnable() { base.OnEnable(); m_Server.RemoteConnected+=OnClientConnected; m_Server.RemoteDisconnected+=OnClientDisconnected; } /// <inheritdoc/>protectedoverridevoidOnDisable() { base.OnDisable(); m_Discovery.Stop(); m_Server.Stop(); m_Server.RemoteConnected-=OnClientConnected; m_Server.RemoteDisconnected-=OnClientDisconnected; } /// <summary>/// Gets the currently connected clients./// </summary>/// <returns>A new collection containing the client handles.</returns>publicIEnumerable<ICompanionAppClient>GetClients() { returnm_RemoteToClient.Values; } /// <inheritdoc />publicoverridestringGetName() =>"Companion App Server"; /// <summary>/// Start listening for clients connections./// </summary>publicvoidStartServer() { if (!NetworkUtilities.IsPortAvailable(m_Port)) { Debug.LogError($"Unable to start server: Port {m_Port} is in use by another program! Close the other program, or assign a free port using the Live Capture Window."); return; } if (m_Server.StartServer(m_Port)) { // start server discoveryvarconfig=newServerData( "Live Capture", Environment.MachineName, m_Server.ID, PackageUtility.GetVersion(LiveCaptureInfo.Version) ); varendPoints=m_Server.EndPoints.ToArray(); m_Discovery.Start(config, endPoints); } OnServerChanged(false); } /// <summary>/// Disconnects all clients and stop listening for new connections./// </summary>publicvoidStopServer() { m_Server.Stop(); m_Discovery.Stop(); OnServerChanged(false); } /// <inheritdoc/>publicoverridevoidOnUpdate() { m_Server.Update(); m_Discovery.Update(); } voidOnClientConnected(Remoteremote) { m_Server.RegisterMessageHandler(remote, InitializeClient, false); } voidOnClientDisconnected(Remoteremote, DisconnectStatusstatus) { if (m_RemoteToClient.TryGetValue(remote, outvarclient)) { try { ClientDisconnected.Invoke(client); } catch (Exceptione) { Debug.LogError(e); } m_RemoteToClient.Remove(remote); OnServerChanged(false); } } voidInitializeClient(Messagemessage) { try { if (message.ChannelType!=ChannelType.ReliableOrdered) { return; } varstreamReader=newStreamReader(message.Data, Encoding.UTF8); varjson=streamReader.ReadToEnd(); vardata=default(ClientInitialization); try { data=JsonUtility.FromJson<ClientInitialization>(json); } catch (Exception) { Debug.LogError($"{nameof(CompanionAppServer)} failed to initialize client connection! Could not parse JSON: {json}"); return; } if (!s_TypeToClientType.TryGetValue(data.Type, outvarclientType)) { Debug.LogError($"Unknown client type \"{data.Type}\" connected to {nameof(CompanionAppServer)}!"); return; } varremote=message.Remote; varclient=Activator.CreateInstance(clientType, m_Server, remote, data) asCompanionAppClient; client.SendProtocol(); m_RemoteToClient.Add(remote, client); AssignOwner(client); ClientConnected.Invoke(client); OnServerChanged(false); } catch (Exceptione) { Debug.LogException(e); } finally { message.Dispose(); } } voidAssignOwner(ICompanionAppClientclient) { // connect to the registered handler that was most recently used with this client if possibleforeach (varhandlerins_ClientConnectHandlers.OrderByDescending(h=>h.Time.Ticks)) { try { if (handler.Name==client.Name) { if (handler.Handler(client)) return; } } catch (Exceptione) { Debug.LogException(e); } } // fall back to the first free device that is compatible with the clientforeach (varhandlerins_ClientConnectHandlers) { try { if (handler.Handler(client)) return; } catch (Exceptione) { Debug.LogException(e); } } } } }
更改CompanionAppDevice类:
usingSystem; usingUnityEngine; namespaceUnity.LiveCapture.CompanionApp{ /// <summary>/// A type of <see cref="LiveCaptureDevice"/> that uses a <see cref="ICompanionAppClient"/> for communication./// </summary>interfaceICompanionAppDevice { /// <summary>/// Clears the client assigned to this device./// </summary>voidClearClient(); } /// <summary>/// A type of <see cref="LiveCaptureDevice"/> that uses a <see cref="ICompanionAppClient"/> for communication./// </summary>/// <typeparam name="TClient">The type of client this device communicates with.</typeparam>publicabstractclassCompanionAppDevice<TClient> : LiveCaptureDevice, ICompanionAppDevicewhereTClient : class, ICompanionAppClient { boolm_ClientRegistered; boolm_Recording; TClientm_Client; readonlySlateChangeTrackerm_SlateChangeTracker=newSlateChangeTracker(); readonlyTakeNameFormatterm_TakeNameFormatter=newTakeNameFormatter(); stringm_LastAssetName; boolTryGetInternalClient(outICompanionAppClientInternalclient) { client=m_ClientasICompanionAppClientInternal; returnclient!=null; } /// <summary>/// This function is called when the object becomes enabled and active./// </summary>protectedvirtualvoidOnEnable() { CompanionAppServer.ClientDisconnected+=OnClientDisconnected; LiveCaptureServer.ClientDisconnected+=OnClientDisconnected; RegisterClient(); } /// <summary>/// This function is called when the behaviour becomes disabled./// </summary>/// <remaks>/// This is also called when the object is destroyed and can be used for any cleanup code./// When scripts are reloaded after compilation has finished, OnDisable will be called, followed by an OnEnable after the script has been loaded./// </remaks>protectedvirtualvoidOnDisable() { CompanionAppServer.ClientDisconnected-=OnClientDisconnected; CompanionAppServer.DeregisterClientConnectHandler(OnClientConnected); LiveCaptureServer.ClientConnected-=OnClientDisconnected; LiveCaptureServer.DeregisterClientConnectHandler(OnClientConnected); StopRecording(); UnregisterClient(); } /// <summary>/// This function is called when the behaviour gets destroyed./// </summary>protectedoverridevoidOnDestroy() { base.OnDestroy(); ClearClient(); } /// <inheritdoc/>publicoverrideboolIsReady() { returnm_Client!=null; } /// <inheritdoc/>publicoverrideboolIsRecording() { returnm_Recording; } /// <inheritdoc/>publicoverridevoidStartRecording() { if (!m_Recording) { m_Recording=true; OnRecordingChanged(); SendRecordingState(); } } /// <inheritdoc/>publicoverridevoidStopRecording() { if (m_Recording) { m_Recording=false; OnRecordingChanged(); SendRecordingState(); } } /// <summary>/// Gets the client currently assigned to this device./// </summary>/// <returns>The assigned client, or null if none is assigned.</returns>publicTClientGetClient() { returnm_Client; } /// <summary>/// Assigns a client to this device./// </summary>/// <param name="client">The client to assign, or null to clear the assigned client.</param>/// <param name="rememberAssignment">Try to auto-assign the client to this device when it reconnects in the future.</param>publicvoidSetClient(TClientclient, boolrememberAssignment) { if (m_Client!=client) { UnregisterClient(); if (m_Client!=null) { ClientMappingDatabase.DeregisterClientAssociation(this, m_Client, rememberAssignment); } m_Client=client; if (m_Client!=null) { // if any device is also using this client, we must clear the client from the previous device.if (ClientMappingDatabase.TryGetDevice(client, outvarpreviousDevice)) { previousDevice.ClearClient(); } ClientMappingDatabase.RegisterClientAssociation(this, m_Client, rememberAssignment); } RegisterClient(); } } voidRegisterClient() { if (!isActiveAndEnabled||m_ClientRegistered) { return; } LiveCaptureServer.DeregisterClientConnectHandler(OnClientConnected); CompanionAppServer.DeregisterClientConnectHandler(OnClientConnected); m_SlateChangeTracker.Reset(); if (TryGetInternalClient(outvarclient)) { client.SetDeviceMode+=ClientSetDeviceMode; client.StartRecording+=ClientStartRecording; client.StopRecording+=ClientStopRecording; client.StartPlayer+=ClientStartPlayer; client.StopPlayer+=ClientStopPlayer; client.PausePlayer+=ClientPausePlayer; client.SetPlayerTime+=ClientSetPlayerTime; client.SetSelectedTake+=ClientSetSelectedTake; client.SetTakeData+=ClientSetTakeData; client.DeleteTake+=ClientDeleteTake; client.SetIterationBase+=ClientSetIterationBase; client.ClearIterationBase+=ClientClearIterationBase; client.TexturePreviewRequested+=OnTexturePreviewRequested; OnClientAssigned(); client.SendInitialize(); UpdateClient(); m_ClientRegistered=true; } else { ClientMappingDatabase.TryGetClientAssignment(this, outvarclientName, outvartime); LiveCaptureServer.RegisterClientConnectHandler(OnClientConnected, clientName, time); CompanionAppServer.RegisterClientConnectHandler(OnClientConnected, clientName, time); } } voidUnregisterClient() { if (!m_ClientRegistered) { return; } if (TryGetInternalClient(outvarclient)) { OnClientUnassigned(); client.SendEndSession(); client.SetDeviceMode-=ClientSetDeviceMode; client.StartRecording-=ClientStartRecording; client.StopRecording-=ClientStopRecording; client.StartPlayer-=ClientStartPlayer; client.StopPlayer-=ClientStopPlayer; client.PausePlayer-=ClientPausePlayer; client.SetPlayerTime-=ClientSetPlayerTime; client.SetSelectedTake-=ClientSetSelectedTake; client.SetTakeData-=ClientSetTakeData; client.DeleteTake-=ClientDeleteTake; client.SetIterationBase-=ClientSetIterationBase; client.ClearIterationBase-=ClientClearIterationBase; client.TexturePreviewRequested-=OnTexturePreviewRequested; m_ClientRegistered=false; } } /// <inheritdoc />publicvoidClearClient() { SetClient(null, true); } /// <summary>/// Called to send the device state to the client./// </summary>publicvirtualvoidUpdateClient() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { SendDeviceState(takeRecorder.IsLive()); varslate=takeRecorder.GetActiveSlate(); varhasSlate=slate!=null; varslateChanged=m_SlateChangeTracker.Changed(slate); vartake=hasSlate?slate.Take : null; varassetName=GetAssetName(); varassetNameChanged=assetName!=m_LastAssetName; m_LastAssetName=assetName; if (TryGetInternalClient(outvarclient)) { client.SendFrameRate(takeRecorder.IsLive() ||take==null?takeRecorder.FrameRate : take.FrameRate); client.SendHasSlate(hasSlate); client.SendSlateDuration(hasSlate?slate.Duration : 0d); client.SendSlateIsPreviewing(takeRecorder.IsPreviewPlaying()); client.SendSlatePreviewTime(takeRecorder.GetPreviewTime()); if (slateChanged||assetNameChanged) { if (hasSlate) m_TakeNameFormatter.ConfigureTake(slate.SceneNumber, slate.ShotName, slate.TakeNumber); elsem_TakeNameFormatter.ConfigureTake(0, "Shot", 0); client.SendNextTakeName(m_TakeNameFormatter.GetTakeName()); client.SendNextAssetName(m_TakeNameFormatter.GetAssetName()); } } if (slateChanged) { SendSlateDescriptor(slate); } } SendRecordingState(); } /// <summary>/// Gets the name used for the take asset name./// </summary>/// <returns>The name of the asset.</returns>protectedvirtualstringGetAssetName() { returnname; } /// <summary>/// The device calls this method when a new client is assigned./// </summary>protectedvirtualvoidOnClientAssigned() {} /// <summary>/// The device calls this method when the client is unassigned./// </summary>protectedvirtualvoidOnClientUnassigned() {} /// <summary>/// The device calls this method when the recording state has changed./// </summary>protectedvirtualvoidOnRecordingChanged() {} /// <summary>/// The device calls this method when the slate has changed./// </summary>protectedvirtualvoidOnSlateChanged(ISlateslate) {} voidClientStartRecording() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { takeRecorder.StartRecording(); } Refresh(); } voidClientStopRecording() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { takeRecorder.StopRecording(); } Refresh(); } voidClientSetDeviceMode(DeviceModedeviceMode) { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { takeRecorder.SetLive(deviceMode==DeviceMode.LiveStream); SendDeviceState(takeRecorder.IsLive()); } } voidClientStartPlayer() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { takeRecorder.PlayPreview(); } Refresh(); } voidClientStopPlayer() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { takeRecorder.PausePreview(); takeRecorder.SetPreviewTime(0d); } Refresh(); } voidClientPausePlayer() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { takeRecorder.PausePreview(); } Refresh(); } voidClientSetPlayerTime(doubletime) { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { takeRecorder.SetPreviewTime(time); } Refresh(); } voidSendDeviceState() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { SendDeviceState(takeRecorder.IsLive()); } } voidSendDeviceState(boolisLive) { if (TryGetInternalClient(outvarclient)) { client.SendDeviceMode(isLive?DeviceMode.LiveStream : DeviceMode.Playback); } } voidSendRecordingState() { if (TryGetInternalClient(outvarclient)) { client.SendRecordingState(IsRecording()); } } voidSendSlateDescriptor() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { SendSlateDescriptor(takeRecorder.GetActiveSlate()); } } voidSendSlateDescriptor(ISlateslate) { if (TryGetInternalClient(outvarclient)) { client.SendSlateDescriptor(SlateDescriptor.Create(slate)); } OnSlateChanged(slate); } voidClientSetSelectedTake(Guidguid) { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { TakeManager.Default.SelectTake(takeRecorder.GetActiveSlate(), guid); SendSlateDescriptor(); Refresh(); } } voidClientSetTakeData(TakeDescriptordescriptor) { TakeManager.Default.SetTakeData(descriptor); SendSlateDescriptor(); Refresh(); } voidClientDeleteTake(Guidguid) { TakeManager.Default.DeleteTake(guid); SendSlateDescriptor(); Refresh(); } voidClientSetIterationBase(Guidguid) { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { varslate=takeRecorder.GetActiveSlate(); TakeManager.Default.SetIterationBase(slate, guid); SendSlateDescriptor(slate); Refresh(); } } voidClientClearIterationBase() { vartakeRecorder=GetTakeRecorder(); if (takeRecorder!=null) { varslate=takeRecorder.GetActiveSlate(); TakeManager.Default.ClearIterationBase(slate); SendSlateDescriptor(slate); Refresh(); } } voidOnTexturePreviewRequested(Guidguid) { vartexture=TakeManager.Default.GetAssetPreview<Texture2D>(guid); if (texture!=null&&TryGetInternalClient(outvarclient)) { client.SendTexturePreview(guid, texture); } } boolOnClientConnected(ICompanionAppClientclient) { if (m_Client==null&&clientisTClientc&& (!ClientMappingDatabase.TryGetClientAssignment(this, outvarclientName, out_) ||c.Name==clientName)) { SetClient(c, false); returntrue; } returnfalse; } voidOnClientDisconnected(ICompanionAppClientclient) { if (m_Client==client) { SetClient(null, false); } } } }
本人打包后成功运行,测试脚本:
usingUnityEngine; usingUnity.LiveCapture.CompanionApp; publicclassLiveCaptureExample : MonoBehaviour{ LiveCaptureServerserver; privatevoidAwake() { server=Resources.Load<LiveCaptureServer>("Live Capture Server"); server.Init(); server.StartServer(); } privatevoidUpdate() { server.OnUpdate(); } privatevoidOnDestroy() { server.StopServer(); } }