In my previous blog post I discussed how to interact with nodes in a scene, using touch. This involved creating gesture recognisers and adding them to the ARSCNView
instance with the AddGestureRecognizer
method.
In this blog post I’ll examine animating a node in a scene. I originally wanted to animate a sphere, to make it rotate. However, it can be difficult to observe a sphere with a diffuse colour rotating. Therefore, I switched to rotating a cube.
The sample this code comes from can be found on GitHub.
Animate a node
In order to add a cube to the scene, I created a CubeNode
type that derives from SCNNode
:
using SceneKit;
using UIKit;
namespace ARKitFun.Nodes
{
public class CubeNode : SCNNode
{
public CubeNode(float size, UIColor color)
{
SCNMaterial material = new SCNMaterial();
material.Diffuse.Contents = color;
SCNBox geometry = SCNBox.Create(size, size, size, 0);
geometry.Materials = new[] { material };
SCNNode node = new SCNNode
{
Geometry = geometry
};
AddChildNode(node);
}
}
}
The CubeNode
constructor takes a float
argument that represents the size of each side of the cube, and a UIColor
argument that represents the colour of the cube. The constructor creates the materials and geometry for the cube, before creating a SCNNode
and assigning the geometry to its Geometry
property, and adds the node as a child node to the SCNNode
.
Nodes can be animated with the SCNAction
type, which represents a reusable animation that changes attributes of any node you attach it to. SCNAction
objects are created with specific class methods, and are executed by calling a node object’s RunAction
method, passing the action object as an argument.
For example, the following code creates a rotate action and applies it to a CubeNode
:
SCNAction rotateAction = SCNAction.RotateBy(0, (float)Math.PI, 0, 5); // X,Y,Z,secs
CubeNode cubeNode = new CubeNode(0.1f, UIColor.Blue);
cubeNode.RunAction(rotateAction);
sceneView.Scene.RootNode.AddChildNode(cubeNode);
In this example, the CubeNode
is rotated 360 degrees on the Y axis over 5 seconds. To rotate the cube indefinitely, use the following code:
SCNAction rotateAction = SCNAction.RotateBy(0, (float)Math.PI, 0, 5);
SCNAction indefiniteRotation = SCNAction.RepeatActionForever(rotateAction);
CubeNode cubeNode = new CubeNode(0.1f, UIColor.Blue);
cubeNode.RunAction(indefiniteRotation);
sceneView.Scene.RootNode.AddChildNode(cubeNode);
In this example, the CubeNode
is rotated 360 degrees on the Y axisover 5 seconds. That is, it takes 5 seconds to complete a full 360 degree rotation. Then the animation is looped.
This code can be generalised into an extension method that can be called on any SCNNode
type:
using System;
using SceneKit;
namespace ARKitFun.Extensions
{
public static class SCNNodeExtensions
{
public static void AddRotationAction(this SCNNode node, SCNActionTimingMode mode, double secs, bool loop = false)
{
SCNAction rotateAction = SCNAction.RotateBy(0, (float)Math.PI, 0, secs);
rotateAction.TimingMode = mode;
if (loop)
{
SCNAction indefiniteRotation = SCNAction.RepeatActionForever(rotateAction);
node.RunAction(indefiniteRotation, "rotation");
}
else
node.RunAction(rotateAction, "rotation");
}
}
}
The AddRotationAction
extension method adds a rotate animation to the specified SCNNode
. The SCNActionTimingMode
argument defines the easing function for the animation. The secs
argument defines the number of seconds to complete a full rotation of the node, and the loop
argument defines whether to animate the node indefinitely. The RunAction
method calls both specify a string key
argument. This enables the animation to be stopped programmatically by specifying the key
as an argument to the RemoveAction
method.
The ViewDidAppear
method in the ViewController
class can then be modified to add a CubeNode
to the scene, and animate it:
using System;
using System.Linq;
using ARKit;
using ARKitFun.Extensions;
using ARKitFun.Nodes;
using CoreGraphics;
using SceneKit;
using UIKit;
namespace ARKitFun
{
public partial class ViewController : UIViewController
{
readonly ARSCNView sceneView;
const float size = 0.1f;
const float zPosition = -0.5f;
bool isAnimating;
...
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
sceneView.Session.Run(new ARWorldTrackingConfiguration
{
AutoFocusEnabled = true,
LightEstimationEnabled = true,
PlaneDetection = ARPlaneDetection.Horizontal,
WorldAlignment = ARWorldAlignment.Gravity
}, ARSessionRunOptions.ResetTracking | ARSessionRunOptions.RemoveExistingAnchors);
CubeNode cubeNode = new CubeNode(size, UIColor.Blue);
cubeNode.Position = new SCNVector3(0, 0, zPosition);
sceneView.Scene.RootNode.AddChildNode(cubeNode);
UITapGestureRecognizer tapGestureRecognizer = new UITapGestureRecognizer(HandleTapGesture);
sceneView.AddGestureRecognizer(tapGestureRecognizer);
...
}
...
In this example, a blue CubeNode
is created and positioned in the scene at (0,0,-0.5). In addition, a UITapGestureRecognizer
is added to the scene.
The following code example shows the HandleTapGesture
method:
void HandleTapGesture(UITapGestureRecognizer sender)
{
SCNView areaPanned = sender.View as SCNView;
CGPoint point = sender.LocationInView(areaPanned);
SCNHitTestResult[] hitResults = areaPanned.HitTest(point, new SCNHitTestOptions());
SCNHitTestResult hit = hitResults.FirstOrDefault();
if (hit != null)
{
SCNNode node = hit.Node;
if (node != null)
{
if (!isAnimating)
{
node.AddRotationAction(SCNActionTimingMode.Linear, 3, true);
isAnimating = true;
}
else
{
node.RemoveAction("rotation");
isAnimating = false;
}
}
}
}
In this example, the node on which the tap gesture was detected is determined. If the node isn’t being animated, an indefinite rotation SCNAction
is added to the node, which fully rotates the node every 3 seconds. Then, provided that the node is being animated, when it’s tapped again the animation ceases by calling the RemoveAction
method, specifying the key value for the action.
The overall effect is that when the app runs, tapping on the node animates it. When animated, tapping on the node stops the animation. Then a new animation will begin on the subsequent tap:
As I mentioned at the beginning of this blog post, I originally wanted to rotate a sphere, with a view to creating a rotating earth. However, it can be difficult to see a sphere with a diffuse colour rotating.
In my next blog post I’ll discuss rotating a sphere to create a rotating earth.
No comments:
Post a Comment