Finding a "good" place for the bin

As discussed earlier, our goal here is to provide a way for the user to manually place the bin; our approach is to attach a bin to the user's gaze (as implemented earlier) and follow it around, updating its position and orientation when in a valid position. Once happy, the user can place the bin using the tap gesture. In this section, we will implement this functionality; in the next section, we will implement the functionality used to detect the tap gesture to place the bin.

Before writing any code, let's quickly discuss how we will implement this. As the app transitions from scanning to placing, we will instantiate the Bin prefab and attach a component to it that will be responsible for making the bin follow the user's gaze. As we want our bin to obey the laws of physics, we will try to ensure that the bin is positioned on the ground rather than floating in the air or climbing up walls. To achieve this, we will do the following:

  • Project the bin away from a wall when the user is gazing at a vertical surface
  • Check that a valid surface is under the bin and, if so, update its position; valid here means a surface that exists and is reasonably flat

Let's start by creating the component responsible for following the gaze around and positioning the bin on the ground.

Click on the Create button on the Project panel and select C# Script to create a new script; name it Placeable and double-click to open it in Visual Studio.

Within the Placeable script, add the following variables and properties:

    public LayerMask RaycastLayers = (1 << 31); // eq.  
LayerMask.GetMask("SpatialSurface"); public float AngleThresholdToApplyOffset = 20f; public float RaycastMaxDistance = 15f; public float SurfaceAngleThreshold = 15f; public float CornerAngleDifferenceThreshold = 10f; public bool ValidPosition { get; private set; } public bool Placed { get; private set; }

These variables describe the constraints we want the placement to adhere to; RaycastLayers determines the layer(s) the bin can rest on. AngleThresholdToApplyOffset is the angle threshold that determines whether we push the bin away from the wall or not. SurfaceAngleThreshold is the maximum angle of the surface the bin will rest on. CornerAngleDifferenceThreshold is used to determine whether the surface is flat or not, where each corner is compared against the other. Finally, we have the ValidPosition flag, indicating whether the bin is in a valid position as well as whether the bin has been placed.

Our final variables are used to animate the bin toward a target position and orientation; add the following to the Placeable script:

    public float HoverDistance = 0.01f; 
 
    public float PlacementSpeed = 10f; 
 
    Vector3 targetPosition; 
 
    Vector3 targetNormal; 
 
    MeshFilter meshFilter; 
 

HoverDistance, like our Cursor component, is an offset applied to the bin to avoid it hugging too tightly to a surface (avoiding possible Z-fighting); targetPosition and targetNormal are the current, valid, position, and orientation of our bin with the PlacementSpeed determining how quickly the bin transitions toward these values. Finally, meshFilter holds the reference to the GameObject's MeshFilter, a component that contains information about the mesh that we will use to get the bounds to determine where the corners are.

Add the following code in the Start method; this just gets a reference to the MeshFilter GameObject, and sets some default values for targetPosition and targetNormal:

    void Start () 
{ meshFilter = GetComponentInChildren<MeshFilter>(); targetPosition = transform.position; targetNormal = transform.up; }

For the rest, let's build it from the bottom up, starting with the GetRaycastOrigins method, a utility method that will return the center and corner positions of our bin, given a bounding box and offset:

    Vector3[] GetRaycastOrigins(Bounds boundingBox, Vector3 originPosition) 
    {         
        float minX = originPosition.x + boundingBox.center.x - boundingBox.extents.x; 
        float maxX = originPosition.x + boundingBox.center.x + boundingBox.extents.x; 
        float minY = originPosition.y + boundingBox.center.y - boundingBox.extents.y; 
        float maxY = originPosition.y + boundingBox.center.y + boundingBox.extents.y; 
        float minZ = originPosition.z + boundingBox.center.z - boundingBox.extents.z; 
        float maxZ = originPosition.z + boundingBox.center.z + boundingBox.extents.z; 
 
        return new Vector3[] 
        { 
            new Vector3(originPosition.x + boundingBox.center.x, minY, originPosition.z + boundingBox.center.z), // centre 
            new Vector3(minX, minY, minZ), // back left  
            new Vector3(minX, minY, maxZ), // front left  
            new Vector3(maxX, minY, maxZ), // front right  
            new Vector3(maxX, minY, minZ) // back right  
        }; 
    } 

We use this mainly to determine whether the surface is even or not. With that in mind, let's implement the method responsible for determining whether the surface is flat. Add the following method to your Placeable script:

    bool IsSurfaceApproximatelyEven(Vector3[] raycastPositions) 
    {  
        float previousAngle = Vector3.Angle(raycastPositions[0], raycastPositions[1]); 
 
        for (int i=2; i<raycastPositions.Length; i++) 
        { 
            float angle = Vector3.Angle(raycastPositions[0], raycastPositions[i]); 
            if(Mathf.Abs(previousAngle - angle) > CornerAngleDifferenceThreshold) 
            { 
                return false; 
            } 
            previousAngle = angle; 
        } 
 
        return true;  
    } 

This method iterates through all corners, calculating the angle for each with regard to the center, and returns false if any angle differences are larger than CornerAngleDifferenceThreshold.

The next method is the workhorse for this component; let's build this up piece by piece. Start by adding the method signature to the Placeable class:

    bool GetValidatedTargetPositionAndNormalFromGaze(out Vector3 position, out Vector3 normal) 
    { 
        position = Vector3.zero; 
        normal = Vector3.zero; 
 
        return true; 
    } 

The method is responsible for setting the position and normal values of the passed in parameters and returning a flag indicating whether a valid bin position resides where the user is currently gazing:

        Bounds boundingBox = meshFilter.mesh.bounds; 
        Vector3 originPosition = GazeController.SharedInstance.GazeHitPosition; 

Next, we get the bounds of the mesh; these are in local space, so we need to use originPosition to transform them into world space:

        if (Vector3.Angle(Vector3.up, GazeController.SharedInstance.GazeHitNormal) >= AngleThresholdToApplyOffset) 
        { 
            originPosition += GazeController.SharedInstance.GazeHitNormal * boundingBox.extents.z * 2.2f;  
        }         
 
        var raycastPositions = GetRaycastOrigins(boundingBox, originPosition); 

Before calling GetRaycastOrigins, we check to see whether the user is gazing at a vertical surface such as a wall; if so, we push the originPosition out from the wall to allow us to project down to the floor. This is done by comparing the angle between the normal surface the user is gazing at and the constant up vector with the AngleThresholdToApplyOffset threshold.

Next, we project a ray from the center of the bin to check whether a surface exists and to verify that the surface is reasonably flat; continue updating the GetValidatedTargetPositionAndNormalFromGaze method of Placeable with the following code:

        RaycastHit hit; 
        if (!Physics.Raycast(raycastPositions[0], -Vector3.up, out hit, RaycastMaxDistance, RaycastLayers)) 
        { 
            return false;                          
        } 
 
        position = hit.point + (HoverDistance * hit.normal); 
        normal = hit.normal; 
 
        if(Vector3.Angle(Vector3.up, normal) > SurfaceAngleThreshold) 
        { 
            return false;  
        } 

If we collide with a surface, we set the position and normal parameters and verify that the surface is not too steep when tested against the SurfaceAngleThreshold variable we declared. If it is considered too steep, we return false.

Our final check is to see whether the surface is flat and if so, use our IsSurfaceApproximatelyEven method that we implemented. Add the following code to perform this final check:

        if (!IsSurfaceApproximatelyEven(raycastPositions)){ 
            return false;  
        } 

That's it for our GetValidatedTargetPositionAndNormalFromGaze method; the final piece of code is simply a question of calling this method and updating the target position and orientation when finding a valid position. Add the following code to wrap up this section:

   void Update () {         
        Vector3 position; 
        Vector3 normal;  
        if(GetValidatedTargetPositionAndNormalFromGaze(out position,  
out normal)) { ValidPosition = true; targetPosition = position; targetNormal = normal; } transform.position = Vector3.Lerp(transform.position,
targetPosition, Time.deltaTime * PlacementSpeed); transform.up = Vector3.Lerp(transform.up, targetNormal,
Time.deltaTime * PlacementSpeed); }

In each frame, we are checking for a new valid position and orientation; if they're found, we update our targetPosition and targetNormal variables and animate toward these values until updated.

We are almost there; check your progress by building and deploying to your device or emulator to see how the bin is dragged around by your gaze.

Where's our bin? You will have noted that our bin doesn't make an appearance in our scene. The reason for this is simply that we're not instantiating it when the state transitions from scanning to placing.

Jump back into the SceneController script and make the following amendments.

Add the BinPrefab instance variable; as the name suggests, this will hold the reference to the Bin prefab, which will be instantiated when we transition to the placing state, and the Bin property to hold the reference to the bin instance:

    public GameObject BinPrefab;  
 
    public GameObject Bin 
    { 
        get; 
        private set;  
    } 

Now, update the PlacingStateRoutine method with the following changes highlighted:

    IEnumerator PlacingStateRoutine() 
    { 
        SpatialMappingManager.SharedInstance.IsObserving = false; 
        SpatialMappingManager.SharedInstance.SurfacesVisible = false; 
 
        Bin = GameObject.Instantiate( 
        BinPrefab,  
        Camera.main.transform.position +  
(Camera.main.transform.forward * 2f) - (Vector3.up * 0.25f), Quaternion.identity); var placebale = Bin.AddComponent<Placeable>(); while (!placebale.Placed) { yield return null; } GameObject.Destroy(placebale); Bin.AddComponent<WorldAnchor>(); CurrentState = State.Playing; }

After transitioning into the placing state, we instantiate an instance of BinPrefab and assign it to our Bin property, positioning it slightly in front and below the user. We then add the Placeable component to the Bin, which takes care of placing the bin. Once placed, we remove the Placeable component and attach a WorldAnchor before transitioning to the playing state.

To avoid drift and increase stability, world anchors can be attached to your stationary GameObjects. Once attached, HoloLens ensures that it maintains its position and rotation relative to the frame of reference. World anchors can also be persisted and shared with other devices, providing means that are efficiently built-- shared holographic spaces, something we will explore in later chapters.

With that set up, jump back into the editor to give our SceneController a reference to the Bin prefab. Select the Controllers GameObject from the Hierarchy panel, and then locate and drag the Bin prefab from the imported assets onto the SceneController Bin prefab variable in the Inspector panel.

Once again, build and deploy to your device or emulator to see your code in action; if everything is working, you should see something similar to the following:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.30.162