This is part nine of a nine-part series that explores how to create a Unity application for a mobile device, using Augmented Reality and Azure Spatial Anchors
- In Part 1 we introduce the project, requirements, technologies and various AR concepts.
- In Part 2 we follow the steps in the project QuickStart guide.
- In Part 3 we create a basic Unity ARFoundation application.
- In Part 4 we look at one of the building blocks of AR - the “Placement Cursor”.
- In Part 5 we explore how the UI for the project works.
- In Part 6 we learn how Azure Spatial Anchors can be used in our project.
- In Part 7 we step up our game by looking at how we can add and move virtual scenery.
- In Part 8 we develop functionality that saves and restores virtual scenery to the cloud.
- In Part 9 we learn about the Azure Functions backend for the solution.
The entire project sourcecode that accompanies this series can be found at:
In this final article of the series, we’ll be looking at the project code that represents our Azure Functions cloud-hosted backend.
Applicable code in this article can be found in:
\src\BackendFunctions\BackendFunctions\
Tip: It’s recommended that you use your IDE to just open the
BackendFunctions.sln
solution file and explore the code from there.
Let’s understand what our backend service needs to do
Our demonstration solution implements HTTP APIs to address two key requirements:
Spatial Anchor ID API - an API to store and retrieve the most recent ID value. This is a simple unformatted GUID.
- this API accepts both GET and POST methods for saving or returning the ID.
- in the demo, the URL for this is:
https://<yourfunctionhost>.azurewebsites.net/api/spatialanchor?code=<your_function_key>
Scenery Layout API - an API to store and retrieve the most recent layout description, which is a JSON document that contains information such as coordinates and rotations.
- this API also accepts both GET and POST methods for saving or returning the JSON document.
- in the demo, the Url for this is:
https://<yourfunctionhost>.azurewebsites.net/api/scenery?code=<your_function_key>
Tip: in this demo, we are using an Azure Function “Authorization Key”. We will need to include this key in the Url when we make any request. You can read more about this:
To persist the data, we have no need for any database:
- we only need to store a small string value, for each of the two use cases.
- we store only the most recent value(s) - we do not need to store any historic records.
As such, we will be using an Azure Storage account to save two blob files (text files):
- the content of the blob(s) will be overwritten, as needed, with replacement data.
We have no need to parse or process any data - the code simply needs to:
- POST - relay data from the Unity client (running on the mobile device) and saved to blob storage using a fixed filename which is defined in app-configuration.
- GET retrieve string data from a blob and return it to the Unity client.
Let’s look at the code
The code works like this:
- the model class
Models/ProjectSettings.cs
is used to represent application configuration within the project.
public class ProjectSettings
{
public string AzureStorageAccountConnectionString { get; set; }
public string AzureStorageAccountContainerName { get; set; }
public string CurrentAnchorIdBlobName { get; set; }
public string CurrentSceneryDefinitionBlobName { get; set; }
}
- the functions startup class
/Startup.cs
is used to create and populate the configuration model.- we use Azure Functions built-in dependency injection service to register the configuration object as a singleton, which will then be available to the rest of the application.
[assembly: FunctionsStartup(typeof(Startup))]
namespace BackendFunctions
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
var config = new ConfigurationBuilder()
.SetBasePath(Environment.CurrentDirectory)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
ProjectSettings projectSettings = new ProjectSettings
{
AzureStorageAccountConnectionString = Environment.GetEnvironmentVariable("AzureStorageAccountConnectionString"),
AzureStorageAccountContainerName = Environment.GetEnvironmentVariable("AzureStorageAccountContainerName"),
CurrentAnchorIdBlobName = Environment.GetEnvironmentVariable("CurrentAnchorIdBlobName"),
CurrentSceneryDefinitionBlobName = Environment.GetEnvironmentVariable("CurrentSceneryDefinitionBlobName"),
};
builder.Services.AddSingleton(projectSettings);
}
}
}
- we have created a factory service
/Services/StorageService.cs
which is responsible for:- using the constructor-injected application configuration to obtain the appropriate:
- connection string,
- storage container-name
- storage blob-name(s).
- creating, configuring and returning a
Microsoft.WindowsAzure.Storage.Blob.CloudBlockBlob
object, which ultimately manages the connection to the Azure Storage Account.
- using the constructor-injected application configuration to obtain the appropriate:
public class StorageService : IStorageService
{
private ProjectSettings _projectSettings;
public StorageService(ProjectSettings projectSettings)
{
_projectSettings = projectSettings;
}
public async Task<CloudBlockBlob> SetupStorageBlob_CurrentCloudAnchorId()
{
CloudBlobContainer blobContainer = await SetupBlobContainer();
CloudBlockBlob blob = blobContainer.GetBlockBlobReference(_projectSettings.CurrentAnchorIdBlobName);
blob.Properties.ContentType = "text/plain";
return blob;
}
public async Task<CloudBlockBlob> SetupStorageBlob_CurrentSceneryDefinition()
{
CloudBlobContainer blobContainer = await SetupBlobContainer();
CloudBlockBlob blob = blobContainer.GetBlockBlobReference(_projectSettings.CurrentSceneryDefinitionBlobName);
blob.Properties.ContentType = "application/json";
return blob;
}
private async Task<CloudBlobContainer> SetupBlobContainer()
{
var containerName = _projectSettings.AzureStorageAccountContainerName;
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_projectSettings.AzureStorageAccountConnectionString);
CloudBlobClient client = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = client.GetContainerReference(containerName);
await container.CreateIfNotExistsAsync();
await container.SetPermissionsAsync(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Blob });
return container;
}
}
- also in the functions startup class
/Startup.cs
, we use the built-in dependency injection service to register a Singleton instance of theStorageService
builder.Services.AddSingleton<IStorageService, StorageService>();
- there are two HTTP-Triggered Azure Functions:
/SceneryFunction.cs
and/SpatialAnchorFunction.cs
.- The service
StorageService
is injected into both functions, using constructor-injection. - Depending on whether the REST method call is a GET or POST, the function method(s) call either:
CloudBlockBlob.UploadTextAsync()
to add data to Azure Storage, orCloudBlockBlob.DownloadTextAsync()
to retrieve data from storage
- in either case, an appropriate
ActionResult
is returned.
- The service
public class SpatialAnchorFunction
{
private readonly IStorageService _storageHandler;
public SpatialAnchorFunction(IStorageService storageHandler)
{
_storageHandler = storageHandler ?? throw new ArgumentException();
}
[FunctionName("SpatialAnchor")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
{
CloudBlockBlob cloudBlockBlob = await _storageHandler.SetupStorageBlob_CurrentCloudAnchorId();
if (req.Method == "POST")
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
await cloudBlockBlob.UploadTextAsync(requestBody);
return (ActionResult)new OkObjectResult("");
}
// otherwise, drop through to a default "GET" behaviour
return (ActionResult)new OkObjectResult(await cloudBlockBlob.DownloadTextAsync());
}
}
Note: In our sample code, we have used a single Azure Function to handle both GET and POST requests. There is no reason why we couldn’t use an entirely separate function to handle both versions. This may be a better choice if you have more complex code in our function.
Wrapping up
We’ve reached the end of the series with a complete project - that I’d be absolutely delighted for you to take away, play with and make your own.
Augmented Reality is a relatively young technology and we may have seen that various elements, that have been used in this project, are still somewhat immature.
However, it’s a sector that is not just “gaining traction” but, according to IDC : Worldwide Spending on Augmented and Virtual Reality, is actively growing at a rapid pace.
Currently, the skill-set needed to work with this technology is largely the same as those expected of video game developers.
As the drive to create AR solutions for other business sectors continues to grow, we are likely to see a shift away from predominantly “entertainment” uses. This will drive the growth of an entirely new category of skilled developers.
Such a role could be the equivalent of today’s “full stack developer” - except we substitute web-technologies, with game-engine skills. As before, the role would still require a strong awareness of how to create and use cloud technologies. Maybe now is the time to get ahead of that demand curve!
I really hope that you can see that amazing potential of what this could all mature into!
Disclosure
I always disclose my position, association or bias at the time of writing; Specifically neither Microsoft nor Unity have compensated me or otherwise endorsed me for my promotion of their services. I have no bias to recommend Microsofts’ or Unity’s services.
Unrelated to the actual written content of this article series, Twilio Inc generously sponsored me to progress this open-source project and associated documentation. This sponsorship was originally related to a separate, speculative, project related to the field of AR that was never progressed due to the Covid pandemic. This blog series was supported by Twilio being honourable - thank you so much!