界标
界标是诸如点、边或多边形之类的空间数据,这些数据会将现实世界的数据分解为有用的部分以锚定或对齐虚拟内容。例如,在面部,眼睛、嘴巴和耳朵都是界标,而检测到的 AR 平面具有多边形表面和边界矩形。在这两种情况下,您都希望将虚拟内容连接到界标,而不是连接到检测到的面部或平面的中心点。
向 Unity MARS 对象添加界标
应使用操作 (Action) 来添加界标。可以将界标添加到面部、平面和其他几种类型的数据。
平面上的界标
可以将平面界标添加到具有平面条件(例如,IsPlaneCondition
或 PlaneSizeCondition
)的代理。为此,在代理的 Inspector 窗口中,单击 Add MARS Component 按钮,然后选择 Action > Plane Landmark (1)。
在该操作的 Inspector 中,单击 Add Landmark 按钮 (2),然后从下拉菜单选择一个选项。根据您选择的选项,这将创建一个新的游戏对象来作为当前所选代理的子项。选择新建的界标,然后在 Inspector 中对其进行编辑。
如果您想了解有关如何在 MARS 中使用界标的更多信息,请参阅相应包的 UseCaseContent/Test Scenes/PlaneLandmarkTest
文件夹中的 PlaneLandmarkTest 场景。该场景提供一些平面界标示例,例如“平面上离用户最近的点”和“成对表面上的最近点”。
面部界标
对于面部跟踪界标,可以从已经设置界标的面部预制件开始(位于 MARS/Runtime/Prefabs/FaceMask.prefab
中)。右键单击该预制件,将其解压缩(从上下文菜单中选择 Unpack prefab completely)。将您的内容附加到每个面部特征的子变换组件。然后,可以删除您的应用程序未使用的界标,或复制现有界标并在 Inspector 中更改设置。
可以将面部界标添加到代理,而不使用该预制件。首先,将 Is Face 特征添加到代理 (1),然后添加 Face Landmark 操作 (2),接着单击 Add Landmark 按钮 (3),如以下截屏中所示。
有关面部界标在若干代理上的运行方式的示例,请参阅相应包的 UseCaseContent/Test Scenes/FaceLandmarksTest
文件夹中的 FaceLandmarksTest 场景。
其他类型的界标
还可以在其他类型的情况下使用界标。要添加其他类型的界标,请向游戏对象(新的或现有的游戏对象)添加界标控制器 (Landmark Controller) 组件,并将 Source 属性设置为一个实现 ICalculateLandmarks
的组件(例如 PlaneLandmarksAction
、FaceLandmarksAction
或 LandmarkOutputPolygon
)。
注意:Plane Landmarks 和 Face Landmarks 操作仅在被直接置于代理对象上或代理的某个子对象上的情况下,才会获得数据。
多个界标可以引用一个操作并共享源数据。在这种情况下,可以将界标控制器放置在层级视图中的任何位置,只要它引用该操作即可。
界标脚本
以下脚本用于处理界标:LandmarkController
、LandmarkOutput
、LandmarkSettings
和 LandmarkSource
。根据您的需求,可以将全部或部分脚本附加到游戏对象。
LandmarkController
LandmarkController
脚本引用其他组件(源、设置、输出),并控制何时(而非如何)更新界标。
LandmarkOutput
LandmarkOutput
脚本包含生成的界标数据,并使用这些数据来影响场景。标准界标输出类型(例如点、边等)修改变换组件,绘制辅助图标,并具有可通过脚本访问的公共属性。
每种类型的界标输出均会实现 ILandmarkOutput
并添加所需的特定数据。从 LandmarkController
可以访问 .output
属性,然后将这个属性强制转换为所需的特定输出类型。如果属性的类型错误,则强制转换将失败并返回 null。
public class MyScript : MonoBehaviour {
public LandmarkController closestEdgeLandmark; // 对界标的引用
void Update() {
var edgeOutput = closestEdgeLandmark.output as LandmarkOutputEdge;
if (edgeOutput == null)
{
// 分配的界标未被设置为“边”类型
}
else
{
// 对 edgeOutput 中的数据执行某种操作,例如
Debug.Log(edgeOutput.startPoint + “ -> “ + edgeOutput.endPoint);
}
}
}
LandmarkSettings
某些界标类型需要额外的输入数据,因为不能仅通过源来计算它们。例如,平面的最近边取决于另一个位置,以便 MARS 对其进行计算。如果无需任何设置,则 LandmarkController
的 .settings
字段可以为 null。否则,它会引用一个包含所需额外属性的组件。每个设置组件都会实现接口 ILandmarkSettings
,并可以在需要时触发对界标的重新计算。
LandmarkSource
LandmarkSource
脚本用于定义界标的计算方式。每个界标源均包含名称、设置组件(如果需要)及其支持的输出类型。它还提供用于计算界标的方法。源可以是实现 ICalculateLandmarks
接口的任何类。
Plane Landmarks 和 Face Landmarks 操作是适用于特定 MARS 游戏对象的界标源 (Landmark Source) 的示例。它们是操作组件,会使用来自代理的 MatchAcquire
事件(在同一游戏对象上或父游戏对象上),并从 MARS 获取一些世界数据来计算界标。有关更多信息,请参阅术语表中的代理和匹配事件条目。
所有这些组件(而不仅一个组件)存在的原因是,界标系统旨在让您轻松地复用和扩展代码。将此行为拆分为不同组件可更改某些部分,而不必复制所有代码。
LandmarkController
脚本是可复用的框架,用于可能需要计算的不同设置和输出。在所有界标之间可以共享自定义 Inspector,即使它们使用的是完全不同类型的设置和输出也是如此。
从技术角度,这也是必要的,因为组件必须准确知道字段被序列化时将是什么数据类型。同样,为了使不同界标针对其设置和输出具有不同数据类型,必须将它们序列化为不同组件类型。
定义自定义界标
要定义自定义界标,请按以下步骤操作。
创建界标源
要创建自定义界标,必须创建一个界标源来定义它。为此,需要创建一个组件来实现 ICalculateLandmarks
接口。
此接口需要 AvailableLandmarkDefinitions
属性,其中的源返回它可以计算的所有定义。该定义包含名称、输出类型以及可选的设置类型。
static readonly List<LandmarkDefinition> k_Definitions = new List<LandmarkDefinition>
{
new LandmarkDefinition(“Center”, new []{typeof(LandmarkOutputPoint)}),
new LandmarkDefinition(“Closest”,
new []{typeof(LandmarkOutputPoint), typeof(LandmarkOutputEdge)},
typeof(ClosestLandmarkSettings))
};
public override List<LandmarkDefinition> AvailableLandmarkDefinitions { get { return k_Definitions; } }
在以上示例中,类返回一个包含两个定义的列表:
- 第一个定义名为“Center”,是未采用任何设置的“点”。
- 第二个定义名为“Closest”,可以是点或边。它需要一个设置组件来指定目标。
在 GetLandmarkCalculation()
方法中,源必须为给定界标的定义提供 Action<ILandmarkController>
(这个方法返回 void 并以界标为唯一参数)。该方法可以接受界标的 .settings
和 .output
,将它们强制转换为对定义所期望的类型,然后相应修改输出。
public override Action<ILandmarkController> GetLandmarkCalculation(LandmarkDefinition definition)
{
if (definition.name == “Closest”)
return CalculateClosestLandmark;
// else if
// ...检查其他定义
else
Debug.LogError("Invalid landmark definition");
return null;
}
void CalculateClosestLandmark(ILandmarkController landmark)
{
var settings = landmark.settings as ClosestLandmarkSettings;
if (settings == null)
{
//平面最近的界标缺少有效设置
return;
}
// 在此处进行计算
var landmarkPoint = landmark.output as LandmarkOutputPoint;
if (landmarkPoint != null)
{
landmarkPoint.position = closestPoint;
}
var landmarkEdge = landmark.output as LandmarkOutputEdge;
if (landmarkEdge != null)
{
landmarkEdge.startPoint = edgeVertA;
landmarkEdge.endPoint = edgeVertB;
}
}
该接口具有一个 dataChanged
事件,界标控制器将监听该事件并选择是否更新。对于 MARS 代理,MatchUpdate
事件会触发该事件。
创建界标输出
如果您要定义的界标无法输出为提供的类型之一(输出、点、姿态或边),可以创建一个自定义类型。例如,如果界标将其输出类型定义为旋转,可添加以下脚本:
public class LandmarkOutputRotation : MonoBehaviour, ILandmarkOutput
{
// 这将通过界标源计算方法来予以修改
public Quaternion rotation { get; set; }
public override void UpdateOutput()
{
// 对旋转进行某种操作
}
void OnDrawGizmosSelected()
{
// 绘制一些辅助图标来使旋转可视化
}
}
创建界标设置
如果要计算的界标需要额外的自定义输入数据,您可以使用设置组件字段。例如,平面的最近点或边具有目标参考。相应的设置组件如下所示:
public class ClosestLandmarkSettings : MonoBehaviour, ILandmarkSettings
{
[SerializeField]
Component m_Target;
[SerializeField]
bool m_UpdateIfTargetMoves = true;
[SerializeField]
float m_UpdateInterval = 0.03f;
Vector3 m_PreviousPosition;
Quaternion m_PreviousRotation;
float m_TimeSinceLastCheck;
public Component target { get { return m_Target; } set { m_Target = value; } }
public event Action<ILandmarkSettings> dataChanged;
void Update()
{
CheckIfMoved();
}
void CheckIfMoved()
{
if (!m_UpdateIfTargetMoves || dataChanged == null || m_Target == null)
return;
m_TimeSinceLastCheck += Time.unscaledDeltaTime;
if (m_TimeSinceLastCheck < m_UpdateInterval)
return;
m_TimeSinceLastCheck = 0f;
var targetTransform = m_Target.transform;
if (targetTransform.position != m_PreviousPosition ||
targetTransform.rotation != m_PreviousRotation)
{
dataChanged(this);
m_PreviousPosition = targetTransform.position;
m_PreviousRotation = targetTransform.rotation;
}
}
}
就像源一样,设置组件具有一个 dataChanged
事件;可以调用该事件来指示某些内容已发生更改,并且界标需要对此更改做出反应。在以上示例中,如果目标移动,设置将调用该事件,以便界标可以重新计算最近点。