3d photo gallery surround
Description
In this lesson, we are..
- Adding an invisible node at the centre of the scene (world origin)
- Creating 7 rows of 2d planes spaced at regular intervals
- Applying a look at constraint to all the 2d planes to point towards the central invisible node
- Calling the Unsplash API in a separate thread as not to affect the main UI thread
- Updating the 2d planes with images returned from the Unsplash API call
Video
Code
using ARKit;
using Foundation;
using Newtonsoft.Json;
using SceneKit;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using UIKit;
namespace XamarinArkitSample
{
public partial class ViewController : UIViewController
{
private readonly ARSCNView sceneView;
public string unsplashAccessKey = "<INSERT_YOUR_OWN_ACCESSKEY_HERE>";
List<ImagePlaneNode> imagePlaneNodes = new List<ImagePlaneNode>();
ConcurrentBag<UIImage> uiImages = new ConcurrentBag<UIImage>();
SCNNode centerNode;
float imageWidth = 0.3f;
float imageHeight = 0.2f;
float verticalMargin = 0.01f;
string searchTerm = "beach";
public ViewController(IntPtr handle) : base(handle)
{
this.sceneView = new ARSCNView
{
AutoenablesDefaultLighting = true
};
this.View.AddSubview(this.sceneView);
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
this.sceneView.Frame = this.View.Frame;
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
this.sceneView.Session.Run(new ARWorldTrackingConfiguration
{
AutoFocusEnabled = true,
PlaneDetection = ARPlaneDetection.Horizontal,
LightEstimationEnabled = true,
WorldAlignment = ARWorldAlignment.GravityAndHeading,
FrameSemantics = ARFrameSemantics.PersonSegmentationWithDepth
}, ARSessionRunOptions.ResetTracking | ARSessionRunOptions.RemoveExistingAnchors);
var radius = 1.25f; // 1.25m away from world origin
var sides = 21; // images per row
var rows = 7;
centerNode = new SCNNode();
centerNode.Position = new SCNVector3(0, 0, 0);
this.sceneView.Scene.RootNode.AddChildNode(centerNode);
AddBlankRow((imageHeight * 3) + (verticalMargin * 3), radius-0.15f, sides);
AddBlankRow((imageHeight * 2) + (verticalMargin * 2), radius-0.075f, sides);
AddBlankRow(imageHeight + verticalMargin, radius, sides);
AddBlankRow(0, radius, sides);
AddBlankRow(0 - imageHeight - verticalMargin, radius, sides);
AddBlankRow(0 - (imageHeight * 2) - (verticalMargin * 2), radius-0.075f, sides);
AddBlankRow(0 - (imageHeight * 3) - (verticalMargin * 3), radius-0.15f, sides);
Task.Run(async () =>
{
var imageUrls = await GetUrlsFromUnSplashApi(searchTerm, sides * rows);
int x = 0;
foreach(var imageUrl in imageUrls)
{
var taskA = LoadImage(imageUrl);
await taskA.ContinueWith(cw =>
{
var image = cw.Result;
uiImages.Add(image);
BeginInvokeOnMainThread(() =>
{
if (x < (sides*rows))
{
imagePlaneNodes[x].UpdateImage(image);
x++;
}
});
});
}
});
}
private void AddBlankRow(float y, float radius, int sides)
{
for (int i = 0; i < sides; i++)
{
var imagePlaneNode = new ImagePlaneNode(imageWidth, imageHeight);
float x = (float)(radius * Math.Cos(2 * Math.PI * i / sides));
var z = (float)(radius * Math.Sin(2 * Math.PI * i / sides));
imagePlaneNode.Position = new SCNVector3(x, y, z);
var lookConstraint = SCNLookAtConstraint.Create(centerNode);
lookConstraint.GimbalLockEnabled = true;
imagePlaneNode.Constraints = new SCNConstraint[] { lookConstraint };
imagePlaneNodes.Add(imagePlaneNode);
this.sceneView.Scene.RootNode.AddChildNode(imagePlaneNode);
}
}
private async Task<string[]> GetUrlsFromUnSplashApi(string searchTerm, int perPage)
{
var urls = new List<string>();
var client = new WebClient();
for (int page = 1; page <= 5; page++)
{
var url = $"https://api.unsplash.com/search/photos?client_id={unsplashAccessKey}&page={page}&per_page={perPage}&orientation=landscape&query={searchTerm}";
var response = await client.DownloadStringTaskAsync(url);
dynamic array = JsonConvert.DeserializeObject(response);
foreach (var result in array["results"])
{
urls.Add(result.urls.small.ToString());
}
}
return urls.ToArray();
}
public override void ViewDidDisappear(bool animated)
{
base.ViewDidDisappear(animated);
this.sceneView.Session.Pause();
}
public override void DidReceiveMemoryWarning()
{
base.DidReceiveMemoryWarning();
}
public async Task<UIImage> LoadImage(string url)
{
var httpClient = new WebClient();
Task<byte[]> contentsTask = httpClient.DownloadDataTaskAsync(url);
var contents = await contentsTask;
return UIImage.LoadFromData(NSData.FromArray(contents));
}
}
public class ImagePlaneNode : SCNNode
{
public ImagePlaneNode(float width, float height)
{
Geometry = CreateGeometry(width, height);
Opacity = 0.2f;
}
private static SCNGeometry CreateGeometry(float width, float height)
{
var material = new SCNMaterial();
material.Diffuse.Contents = UIColor.White;
material.DoubleSided = true;
var geometry = SCNPlane.Create(width, height);
geometry.Materials = new[] { material };
return geometry;
}
internal void UpdateImage(UIImage uIImage)
{
Geometry.FirstMaterial.Diffuse.Contents = uIImage;
this.RunAction(SCNAction.FadeIn(1));
}
}
}
Next Step : Place webview in scene
After you have mastered this you should try Place webview in scene