using System.Collections.Generic;
using UnityEngine;
using Academy.HoloToolkit.Unity;
/// <summary>
/// Enumeration containing the surfaces on which a GameObject
/// can be placed. For simplicity of this sample, only one
/// surface type is allowed to be selected.
/// </summary>
/// 枚举包含了可以被放置的物体的表面。为了简化例子,只有一种surface类型可以被选择
public enum PlacementSurfaces
{
// Horizontal surface with an upward pointing normal. 有一个法线指向上的水平表面
Horizontal = 1,
// Vertical surface with a normal facing the user. 有一个法线指向使用者的垂直表面
Vertical = 2,
}
/// <summary>
/// The Placeable class implements the logic used to determine if a GameObject
/// can be placed on a target surface. Constraints for placement include:
/// * No part of the GameObject's box collider impacts with another object in the scene
/// * The object lays flat (within specified tolerances) against the surface
/// * The object would not fall off of the surface if gravity were enabled.
/// This class also provides the following visualizations.
/// * A transparent cube representing the object's box collider.
/// * Shadow on the target surface indicating whether or not placement is valid.
/// </summary>
/// Placeable 类执行这样的逻辑:决定一个物体是否能被放置在一个目标表面上。放置的限制包括:
/// 1 这个物体的box collider不会影响场景中的另一个物体
/// 2 这个物体必须躺在平坦的表面上
/// 3 如果有重力存在,那么这个物体不会掉下来
/// 这个类也提供了下面的可视化
/// 一个透明的正方形代表了这个目标物体的box collider
/// 在目标surface上的阴影代表了能不能被放置
public class Placeable : MonoBehaviour
{
[Tooltip("The base material used to render the bounds asset when placement is allowed.")]
public Material PlaceableBoundsMaterial = null;
[Tooltip("The base material used to render the bounds asset when placement is not allowed.")]
public Material NotPlaceableBoundsMaterial = null;
[Tooltip("The material used to render the placement shadow when placement it allowed.")]
public Material PlaceableShadowMaterial = null;
[Tooltip("The material used to render the placement shadow when placement it not allowed.")]
public Material NotPlaceableShadowMaterial = null;
//只有水平的surface可以被放置
[Tooltip("The type of surface on which the object can be placed.")]
public PlacementSurfaces PlacementSurface = PlacementSurfaces.Horizontal;
//再放置的过程中,这个物体的子物体将被隐藏起来
[Tooltip("The child object(s) to hide during placement.")]
public List<GameObject> ChildrenToHide = new List<GameObject>();
/// <summary>
/// Indicates if the object is in the process of being placed.
/// 布尔变量:这个目标物体是否正在被移动
/// </summary>
public bool IsPlacing { get; private set; }
// The most recent distance to the surface. This is used to
// locate the object when the user's gaze does not intersect
// with the Spatial Mapping mesh.
//离平面的最远距离。用来当用户的目光没有与空间网格进行交会时使用
private float lastDistance = 2.0f;
// The distance away from the target surface that the object should hover prior while being placed.
//目标物体应该放在目标平面前的距离
private float hoverDistance = 0.15f;
// Threshold (the closer to 0, the stricter the standard) used to determine if a surface is flat.
//阈值(接近0,更严格的标准)用于确定一个表面是平的。
private float distanceThreshold = 0.02f;
// Threshold (the closer to 1, the stricter the standard) used to determine if a surface is vertical.
//阈值(接近0,更严格的标准)用于确定一个表面是垂直的。
private float upNormalThreshold = 0.9f;
// Maximum distance, from the object, that placement is allowed.
// This is used when raycasting to see if the object is near a placeable surface.
//当光线投射时看是否这个目标物体接近一个可以被放置的表面,最大放置距离
private float maximumPlacementDistance = 5.0f;
// Speed (1.0 being fastest) at which the object settles to the surface upon placement.
//目标物体在位置上移动时的速度
private float placementVelocity = 0.06f;
// Indicates whether or not this script manages the object's box collider.
//判断这个脚本是不是去管理这个目标物体的box colloider
private bool managingBoxCollider = false;
// The box collider used to determine of the object will fit in the desired location.
// It is also used to size the bounding cube.
//去决定目标物体是否符合期待的位置的box collider
private BoxCollider boxCollider = null;
// Visible asset used to show the dimensions of the object. This asset is sized
// using the box collider's bounds.
//可视化的资源去展示目标物体的大小。这个资源使用box collider的边界去表示大小
private GameObject boundsAsset = null;
// Visible asset used to show the where the object is attempting to be placed.
// This asset is sized using the box collider's bounds.
//可视话的资源去展示目标物体是不是正在企图被放置。这个资源使用box collider的边界去表示大小
private GameObject shadowAsset = null;
// The location at which the object will be placed.
//目标物体将要被放置的位置
private Vector3 targetPosition;
/// <summary>
/// Called when the GameObject is created.
/// </summary>
private void Awake()
{
targetPosition = gameObject.transform.position;
// Get the object's collider. 没有box collider那就给他加一个box collider属性
boxCollider = gameObject.GetComponent<BoxCollider>();
if (boxCollider == null)
{
// The object does not have a collider, create one and remember that
// we are managing it.
managingBoxCollider = true;
boxCollider = gameObject.AddComponent<BoxCollider>();
boxCollider.enabled = false;
}
// Create the object that will be used to indicate the bounds of the GameObject.
//设置一个方框之后,把它设置为目标物体的子物体,之后并不激活它
boundsAsset = GameObject.CreatePrimitive(PrimitiveType.Cube);
boundsAsset.transform.parent = gameObject.transform;
boundsAsset.SetActive(false);
// Create a object that will be used as a shadow.
//弄一个方形的平板当作是目标物体的子物体(阴影),之后并不激活它
shadowAsset = GameObject.CreatePrimitive(PrimitiveType.Quad);
shadowAsset.transform.parent = gameObject.transform;
shadowAsset.SetActive(false);
}
/// <summary>
/// Called when our object is selected. Generally called by
/// a gesture management component.
/// </summary>
/// 当目标物体被选中时执行的函数
public void OnSelect()
{
/* TODO: 4.a CODE ALONG 4.a */
if (!IsPlacing)
{
OnPlacementStart();
}
else
{
OnPlacementStop();
}
}
/// <summary>
/// Called once per frame.
/// </summary>
private void Update()
{
/* TODO: 4.a CODE ALONG 4.a */
if (IsPlacing)
{
// Move the object.
Move();
// Set the visual elements.
Vector3 targetPosition;
Vector3 surfaceNormal;
bool canBePlaced = ValidatePlacement(out targetPosition, out surfaceNormal);
DisplayBounds(canBePlaced);
DisplayShadow(targetPosition, surfaceNormal, canBePlaced);
}
else
{
// Disable the visual elements.
boundsAsset.SetActive(false);
shadowAsset.SetActive(false);
// Gracefully place the object on the target surface.
float dist = (gameObject.transform.position - targetPosition).magnitude;
if (dist > 0)
{
gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, targetPosition, placementVelocity / dist);
}
else
{
// Unhide the child object(s) to make placement easier.
for (int i = 0; i < ChildrenToHide.Count; i++)
{
ChildrenToHide[i].SetActive(true);
}
}
}
}
/// <summary>
/// Verify whether or not the object can be placed.
/// </summary>
/// <param name="position">
/// The target position on the surface.
/// </param>
/// <param name="surfaceNormal">
/// The normal of the surface on which the object is to be placed.
/// </param>
/// <returns>
/// True if the target position is valid for placing the object, otherwise false.
/// </returns>
private bool ValidatePlacement(out Vector3 position, out Vector3 surfaceNormal)
{
Vector3 raycastDirection = gameObject.transform.forward;
if (PlacementSurface == PlacementSurfaces.Horizontal)
{
// Placing on horizontal surfaces.
// Raycast from the bottom face of the box collider.
raycastDirection = -(Vector3.up);
}
// Initialize out parameters.
position = Vector3.zero;
surfaceNormal = Vector3.zero;
Vector3[] facePoints = GetColliderFacePoints();
// The origin points we receive are in local space and we
// need to raycast in world space.
for (int i = 0; i < facePoints.Length; i++)
{
facePoints[i] = gameObject.transform.TransformVector(facePoints[i]) + gameObject.transform.position;
}
// Cast a ray from the center of the box collider face to the surface.
RaycastHit centerHit;
if (!Physics.Raycast(facePoints[0],
raycastDirection,
out centerHit,
maximumPlacementDistance,
SpatialMappingManager.Instance.LayerMask))
{
// If the ray failed to hit the surface, we are done.
return false;
}
// We have found a surface. Set position and surfaceNormal.
position = centerHit.point;
surfaceNormal = centerHit.normal;
// Cast a ray from the corners of the box collider face to the surface.
for (int i = 1; i < facePoints.Length; i++)
{
RaycastHit hitInfo;
if (Physics.Raycast(facePoints[i],
raycastDirection,
out hitInfo,
maximumPlacementDistance,
SpatialMappingManager.Instance.LayerMask))
{
// To be a valid placement location, each of the corners must have a similar
// enough distance to the surface as the center point
if (!IsEquivalentDistance(centerHit.distance, hitInfo.distance))
{
return false;
}
}
else
{
// The raycast failed to intersect with the target layer.
return false;
}
}
return true;
}
/// <summary>
/// Determine the coordinates, in local space, of the box collider face that
/// will be placed against the target surface.
/// </summary>
/// <returns>
/// Vector3 array with the center point of the face at index 0.
/// </returns>
private Vector3[] GetColliderFacePoints()
{
// Get the collider extents.
// The size values are twice the extents.
Vector3 extents = boxCollider.size / 2;
// Calculate the min and max values for each coordinate.
float minX = boxCollider.center.x - extents.x;
float maxX = boxCollider.center.x + extents.x;
float minY = boxCollider.center.y - extents.y;
float maxY = boxCollider.center.y + extents.y;
float minZ = boxCollider.center.z - extents.z;
float maxZ = boxCollider.center.z + extents.z;
Vector3 center;
Vector3 corner0;
Vector3 corner1;
Vector3 corner2;
Vector3 corner3;
if (PlacementSurface == PlacementSurfaces.Horizontal)
{
// Placing on horizontal surfaces.
center = new Vector3(boxCollider.center.x, minY, boxCollider.center.z);
corner0 = new Vector3(minX, minY, minZ);
corner1 = new Vector3(minX, minY, maxZ);
corner2 = new Vector3(maxX, minY, minZ);
corner3 = new Vector3(maxX, minY, maxZ);
}
else
{
// Placing on vertical surfaces.
center = new Vector3(boxCollider.center.x, boxCollider.center.y, maxZ);
corner0 = new Vector3(minX, minY, maxZ);
corner1 = new Vector3(minX, maxY, maxZ);
corner2 = new Vector3(maxX, minY, maxZ);
corner3 = new Vector3(maxX, maxY, maxZ);
}
return new Vector3[] { center, corner0, corner1, corner2, corner3 };
}
/// <summary>
/// Put the object into placement mode.
/// </summary>
public void OnPlacementStart()
{
// If we are managing the collider, enable it. 开始移动之后,那么boxcollider显示
if (managingBoxCollider)
{
boxCollider.enabled = true;
}
// Hide the child object(s) to make placement easier.隐藏子物体
for (int i = 0; i < ChildrenToHide.Count; i++)
{
ChildrenToHide[i].SetActive(false);
}
// Tell the gesture manager that it is to assume
// all input is to be given to this object.
GestureManager.Instance.OverrideFocusedObject = gameObject;
// Enter placement mode.表示被移动
IsPlacing = true;
}
/// <summary>
/// Take the object out of placement mode.
/// </summary>
/// <remarks>
/// This method will leave the object in placement mode if called while
/// the object is in an invalid location. To determine whether or not
/// the object has been placed, check the value of the IsPlacing property.
/// </remarks>
public void OnPlacementStop()
{
// ValidatePlacement requires a normal as an out parameter.
Vector3 position;
Vector3 surfaceNormal;
// Check to see if we can exit placement mode.
if (!ValidatePlacement(out position, out surfaceNormal))
{
return;
}
// The object is allowed to be placed.
// We are placing at a small buffer away from the surface.
targetPosition = position + (0.01f * surfaceNormal);
OrientObject(true, surfaceNormal);
// If we are managing the collider, disable it.
if (managingBoxCollider)
{
boxCollider.enabled = false;
}
// Tell the gesture manager that it is to resume
// its normal behavior.
GestureManager.Instance.OverrideFocusedObject = null;
// Exit placement mode.
IsPlacing = false;
}
/// <summary>
/// Positions the object along the surface toward which the user is gazing.
/// </summary>
/// <remarks>
/// If the user's gaze does not intersect with a surface, the object
/// will remain at the most recently calculated distance.
/// </remarks>
private void Move()
{
Vector3 moveTo = gameObject.transform.position;
Vector3 surfaceNormal = Vector3.zero;
RaycastHit hitInfo;
bool hit = Physics.Raycast(Camera.main.transform.position,
Camera.main.transform.forward,
out hitInfo,
20f,
SpatialMappingManager.Instance.LayerMask);
if (hit)
{
float offsetDistance = hoverDistance;
// Place the object a small distance away from the surface while keeping
// the object from going behind the user.
if (hitInfo.distance <= hoverDistance)
{
offsetDistance = 0f;
}
moveTo = hitInfo.point + (offsetDistance * hitInfo.normal);
lastDistance = hitInfo.distance;
surfaceNormal = hitInfo.normal;
}
else
{
// The raycast failed to hit a surface. In this case, keep the object at the distance of the last
// intersected surface.
moveTo = Camera.main.transform.position + (Camera.main.transform.forward * lastDistance);
}
// Follow the user's gaze.
float dist = Mathf.Abs((gameObject.transform.position - moveTo).magnitude);
gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, moveTo, placementVelocity / dist);
// Orient the object.
// We are using the return value from Physics.Raycast to instruct
// the OrientObject function to align to the vertical surface if appropriate.
OrientObject(hit, surfaceNormal);
}
/// <summary>
/// Orients the object so that it faces the user.
/// </summary>
/// <param name="alignToVerticalSurface">
/// If true and the object is to be placed on a vertical surface,
/// orient parallel to the target surface. If false, orient the object
/// to face the user.
/// </param>
/// <param name="surfaceNormal">
/// The target surface's normal vector.
/// </param>
/// <remarks>
/// The aligntoVerticalSurface parameter is ignored if the object
/// is to be placed on a horizontalSurface
/// </remarks>
private void OrientObject(bool alignToVerticalSurface, Vector3 surfaceNormal)
{
Quaternion rotation = Camera.main.transform.localRotation;
// If the user's gaze does not intersect with the Spatial Mapping mesh,
// orient the object towards the user.
if (alignToVerticalSurface && (PlacementSurface == PlacementSurfaces.Vertical))
{
// We are placing on a vertical surface.
// If the normal of the Spatial Mapping mesh indicates that the
// surface is vertical, orient parallel to the surface.
if (Mathf.Abs(surfaceNormal.y) <= (1 - upNormalThreshold))
{
rotation = Quaternion.LookRotation(-surfaceNormal, Vector3.up);
}
}
else
{
rotation.x = 0f;
rotation.z = 0f;
}
gameObject.transform.rotation = rotation;
}
/// <summary>
/// Displays the bounds asset.
/// </summary>
/// <param name="canBePlaced">
/// Specifies if the object is in a valid placement location.
/// </param>
private void DisplayBounds(bool canBePlaced)
{
// Ensure the bounds asset is sized and positioned correctly.
boundsAsset.transform.localPosition = boxCollider.center;
boundsAsset.transform.localScale = boxCollider.size;
boundsAsset.transform.rotation = gameObject.transform.rotation;
// Apply the appropriate material.
if (canBePlaced)
{
boundsAsset.GetComponent<Renderer>().sharedMaterial = PlaceableBoundsMaterial;
}
else
{
boundsAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableBoundsMaterial;
}
// Show the bounds asset.
boundsAsset.SetActive(true);
}
/// <summary>
/// Displays the placement shadow asset.
/// </summary>
/// <param name="position">
/// The position at which to place the shadow asset.
/// </param>
/// <param name="surfaceNormal">
/// The normal of the surface on which the asset will be placed
/// </param>
/// <param name="canBePlaced">
/// Specifies if the object is in a valid placement location.
/// </param>
private void DisplayShadow(Vector3 position,
Vector3 surfaceNormal,
bool canBePlaced)
{
// Rotate the shadow so that it is displayed on the correct surface and matches the object.
float rotationX = 0.0f;
if (PlacementSurface == PlacementSurfaces.Horizontal)
{
rotationX = 90.0f;
}
Quaternion rotation = Quaternion.Euler(rotationX, gameObject.transform.rotation.eulerAngles.y, 0);
shadowAsset.transform.localScale = boxCollider.size;
shadowAsset.transform.rotation = rotation;
// Apply the appropriate material.
if (canBePlaced)
{
shadowAsset.GetComponent<Renderer>().sharedMaterial = PlaceableShadowMaterial;
}
else
{
shadowAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableShadowMaterial;
}
// Show the shadow asset as appropriate.
if (position != Vector3.zero)
{
// Position the shadow a small distance from the target surface, along the normal.
shadowAsset.transform.position = position + (0.01f * surfaceNormal);
shadowAsset.SetActive(true);
}
else
{
shadowAsset.SetActive(false);
}
}
/// <summary>
/// Determines if two distance values should be considered equivalent.
/// </summary>
/// <param name="d1">
/// Distance to compare.
/// </param>
/// <param name="d2">
/// Distance to compare.
/// </param>
/// <returns>
/// True if the distances are within the desired tolerance, otherwise false.
/// </returns>
private bool IsEquivalentDistance(float d1, float d2)
{
float dist = Mathf.Abs(d1 - d2);
return (dist <= distanceThreshold);
}
/// <summary>
/// Called when the GameObject is unloaded.
/// </summary>
private void OnDestroy()
{
// Unload objects we have created.
Destroy(boundsAsset);
boundsAsset = null;
Destroy(shadowAsset);
shadowAsset = null;
}
}