SceneKit skeleton 3D animation - scenekit

i am implementing an app for iPhone6S that helps disabled people to gain back their limbs control by training using a virtual trainer.
This virtual trainer performs simple movements such as raising and lowering an arm, raising and lowering a leg, and then combines them in various rhythmic patterns.
Due to the target platform, I cannot make use of the powerful ARKit, and therefore I have to have to rely on SceneeKit alone.
My code is currently able to load a dae, and applying multiple actions to the skeleton. However I am facing problems in understanding the behavior of SCNAction.rotateTo and SCNAction.rotateBy
let DegreesPerRadians = Float(Double.pi/180)
func convertToRadians(angle:Float) -> Float {
return angle * DegreesPerRadians
}
func convertToRadians(angle:CGFloat) -> CGFloat {
return CGFloat(CGFloat(angle) * CGFloat(DegreesPerRadians))
}
var x45 = SCNAction.rotateBy( x: convertToRadians(angle: 45), y: 0, z: 0, duration: 0.2)
var x45r = SCNAction.rotateBy( x: convertToRadians(angle: -45), y: 0, z: 0, duration: 0.2)
func FOL()->SCNAction! {
let human = getHuman()
let bodyOfHuman = human?.childNode(withName: "Body", recursively: true)
let bodyRealSkeleton = bodyOfHuman?.skinner?.skeleton
let leftLegBone = bodyRealSkeleton?.childNode(withName: "mixamorig_LeftLeg", recursively: true)
let leftLegKick = SCNAction.customAction(duration: 0.1) { [self]
(node, elapsedTime) in leftLegBone?.runAction(x45)
}
return leftLegKick
}
func FOLrev()->SCNAction! {
let human = getHuman()
let bodyOfHuman = human?.childNode(withName: "Body", recursively: true)
let bodyRealSkeleton = bodyOfHuman?.skinner?.skeleton
let leftLegBone = bodyRealSkeleton?.childNode(withName: "mixamorig_LeftLeg", recursively: true)
let leftLegKick = SCNAction.customAction(duration: 0.1) { [self]
(node, elapsedTime) in leftLegBone?.runAction(x45r)
}
return leftLegKick
}
var actionSequence=[FOL(),FOLrev()]
The above code is then called with
_scene?.rootNode.runAction(SCNAction.sequence(actionSequence))
It results into the model properly bending the left leg and straightening it.
However when using instead "rotateTo" it goes into the opposite direction and does not "come back".
screenshot-rotateTo
I watched all DevCon SceneKit materials and sample code but could not find any explanation or guidance.
My real goal is to add more complex actions, such as clapping hands, and when the same approach is tried on those patterns (that require 3-4 rotations) then the results are less predictable.

The answer to this question is that when using rotateTo, in order to go "back" one should use the starting position of the specific angle.
In the specific example, using rotateTo means to use following code
var x45 = SCNAction.rotateTo( x: convertToRadians(angle: 45), y: 0, z: 0, duration: 0.2)
var x45r = SCNAction.rotateTo( x: 0, y: 0, z: 0, duration: 0.2)

Related

Analysing annual deforestation for multiple polygons in google earth engine

I want to analyse and extract the total annual deforestation between 2001-2021 using the Hansen forest change dataset in Google Earth Engine.
For that, I use a feature collection containing (let's say) sub-national boundaries. The code has referred to as Shape, which has about 8-9k polygons.
The code that analyses annual deforestation (area in km2) for each shape (subset of Feature Collection Shapes) and saves it as an array is pasted below (this part works fine):
var annualforestloss = function(ID)
{
// Get a feature collection with just the custom shapefile feature.
// Shape contains multiple features (polygons)
// Use any shapefile, if mine doesn't work
var geometry = Shape.filter(ee.Filter.eq('ID', ID));
//print(geometry);
Map.addLayer(geometry);
// Get the loss image.
// This dataset is updated yearly, so check for the latest version.
var gfc2021 = ee.Image('UMD/hansen/global_forest_change_2021_v1_9');
var lossImage = gfc2021.select(['loss']);
//Converting m2 to km2
var lossAreaImage = lossImage.multiply(ee.Image.pixelArea()).divide(1000000);
var lossYear = gfc2021.select(['lossyear']);
var lossByYear = lossAreaImage.addBands(lossYear).reduceRegion({
reducer: ee.Reducer.sum().group({
groupField: 1
}),
geometry: geometry,
scale: 30,
maxPixels: 1e9
});
//print(lossByYear);
var statsFormatted = ee.List(lossByYear.get('groups'))
.map(function(el) {
var d = ee.Dictionary(el);
return [ee.Number(d.get('group')).format("20%02d"), d.get('sum')];
});
var statsDictionary = ee.Dictionary(statsFormatted.flatten());
//print(statsFormatted)
//print(statsDictionary)
// Since there has been no forest loss for some years, the code above does not include those years.
// To remedy that, we use another dictionary with values as zero and later combine them without overlap
var dict1 = ee.Dictionary({
"2001": 0, "2002": 0, "2003": 0, "2004": 0, "2005": 0, "2006": 0, "2007": 0, "2008": 0,
"2009": 0, "2010": 0, "2011": 0, "2012": 0, "2013": 0, "2014": 0, "2015": 0, "2016": 0,
"2017": 0, "2018": 0, "2019": 0, "2020": 0, "2021": 0,});
statsDictionary = statsDictionary.combine(dict1, false);
//print(statsDictionary)
// Platting chart
/*
var chart = ui.Chart.array.values({
array: (statsDictionary.values()),
axis: 0,
xLabels: statsDictionary.keys()
}).setChartType('LineChart')
.setOptions({
title: 'Yearly Forest Loss (ID-'+ID+')',
hAxis: {title: 'Year', format: '####'},
vAxis: {title: 'Area (square kilometer)'},
legend: { position: "none" },
lineWidth: 1,
pointSize: 3
});
print(chart)
*/
// Adding 'ID' for seamless data analysis in python
return statsDictionary.values().add(ID)
};
However, the problem I am facing is: Looping through the features in Shapes: Although I know that 'for loops' are not recommended, I am limited by my ability to use map() function in GEE, which returns some client/server error.
// This function combines results from arrays in a loop
var combinearray = function(array1, array2)
{
var arraynew = (ee.Array.cat([array1, array2], 0))
return arraynew
}
// Size of the features (total shapes about 8664)
print(Shape.size())
//**Limited by skill to run it with 'map' function. ########## Please help!**!!!!!!!############
// Works fine for less loops. Hangs for double digit loops
var list = (annualforestloss(0))
for (var ID = 1; ID <= 6; ID++) {
var listnew = (annualforestloss(ID))
var list = (combinearray(list, listnew))
}
print(list)
//reshaping the array for export
var combinedarray = (list.reshape([7,22]))
print('--')
print(combinedarray)
Any help is greatly appreciated.
The following link can be used to see the code editor: https://code.earthengine.google.com/cd335cd1356dc69e3c9adc4ba9710bcb
I think using ReduceRegions() should do the trick. Here is a sample code (https://code.earthengine.google.com/?scriptPath=users%2Fdvps95%2Fdefault%3AHansen_Annual_ForestLoss)
The reduceRegion() maps the functions across multiple polygons. I hope this answers your question. I would suggest you follow the tutorials from "Spatial thoughts"- Ujwal gandhi does an excellent job in breaking down the concepts.
Good luck!

How to place SCNFloor on real floor?

I'd like to add an scnfloor where the real floor is, but it appears above and when adding a childNode, the object doesn't lay on the floor, but looks like flying.
When logging the position of the added node or plane anchor I always get y = 0, no matter if I'm scanning the floor or a table.
What am I missing? Or is it impossible to change the y of a scnfloor? But in the docs is written
"...of its local coordinate space"
, so I thought it was possible.
Thanks
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {return}
let anchorX = planeAnchor.center.x
let anchorY = planeAnchor.center.y
let anchorZ = planeAnchor.center.z
print("planeAnchor.center %#", planeAnchor.center) //y always 0
print("node.position %#", node.position) //y always 0
print("convert %#", node.convertPosition(SCNVector3(anchorX, anchorY, anchorZ), to: nil)) //y always 0
let floor = SCNFloor()
let floorNode = SCNNode(geometry: floor)
floorNode.position = SCNVector3(anchorX, anchorY, anchorZ)
let floorShape = SCNPhysicsShape(geometry: floor, options: nil)
floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: floorShape)
node.addChildNode(floorNode)
let childNode = createChildNode()
floorNode.addChildNode(childNode)
}
The center.y value of the anchor will always be zero, there is some more information here: https://developer.apple.com/documentation/arkit/arplaneanchor/2882056-center
Basically the anchors transform will give the correct position of the detected plane instead. Because of this you can just add your SCNFloor instance as a child of the passed in node and it will render in the correct position, there is no need to set the position explicitly as you are doing above.
You will need to update your geometry size as the ARPlaneAnchor's extent changes as ARKit refines the bounds of the plane. Some more information is here: https://developer.apple.com/documentation/arkit/tracking_and_visualizing_planes
For example, the code below works for me, rendering a SCNFloor at the initially detected size of the plane and renders a box that will sit exactly on top of it. The plane is accurately placed on the floor (or whatever horizontal surface you detect):
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {
return
}
let floor = SCNFloor()
let floorNode = SCNNode(geometry: floor)
floor.length = CGFloat(planeAnchor.extent.z)
floor.width = CGFloat(planeAnchor.extent.x)
node.addChildNode(floorNode)
let box = SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0)
box.materials[0].diffuse.contents = UIColor.red
let boxNode = SCNNode(geometry: box)
boxNode.position = SCNVector3(0, 0.025, 0)
node.addChildNode(boxNode)
}

ARSCNView prepare crash when SCNMaterialProperty is SpriteKit scene

I'm experiencing an ARKit/SceneKit crash.
It happens if I do the following:-
Create SCNPlane and set the SCNMaterialProperty to a SpriteKit
scene
Create SCNNode with SCNPlane
Call prepare on ARSCNView and add SCNNode.
It crashes when I call prepare. However, it does not crash if I add the SCNNode without calling prepare or if the SCNMaterialProperty is not a SpriteKit scene.
See code below:-
var exhibitScene = SKScene(fileNamed: spriteName.lowercased())
exhibitScene?.view?.preferredFramesPerSecond = 60
exhibitScene?.view?.ignoresSiblingOrder = true
exhibitScene?.scaleMode = .aspectFit
exhibitScene?.isPaused = false
let exhibitSize = CGSize.init(width: (exhibitScene?.frame.size.width)! / PIXEL_TO_CENTIMETRE, height: (exhibitScene?.frame.size.height)! / PIXEL_TO_CENTIMETRE)
let exhibitPlane = SCNPlane(width: exhibitSize.width/100, height: exhibitSize.height/100)
exhibitPlane.firstMaterial?.diffuse.contents = exhibitScene
exhibitPlane.firstMaterial?.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(1, -1, 1), 0, 1, 0)
let exhibitNode = SCNNode(geometry: exhibitPlane)
exhibitNode.name = name
exhibitNode.geometry?.firstMaterial?.isDoubleSided = false
exhibitNode.position = SCNVector3(x/100, (y + Float(exhibitSize.height * 0.5))/100, z/100)
self.sceneView.prepare([exhibitNode], completionHandler: { (Bool) in
self.sceneView.scene.rootNode.addChildNode(exhibitNode)
})
Not sure how to fix this.
Do I have to set a parameter on the ARSCNView for it to prepare a SCNNode when the SCNMaterialProperty is set to use a SpriteKit scene?
Not sure about your case, but I had a similar crash when the size of SKScene (and consequently SCNPlane) was more than some limit. The actual reason for the crash was that SCNMaterialProperty.contents may not be bigger than some limit (I believe it was about 4000px or something).

Accessing the number of elements in an array and applying gravity behaviour

I'm having issues with getting ALL elements of an array to fall using the Gravity module. I have managed to get the LAST element in the array to fall and then the remaining elements just stay at the top of the screen during testing. Upon debugging
I am using UIKit and want to understand this language thoroughly before using other various engines such as SpriteKit and GameplayKit.
func mainGame()
{
let cars = ["car5", "car1", "car6", "car3", "car2", "car4"]
var random2 = Int(arc4random_uniform(UInt32(cars.count))) + 1
for i in 1...random2
{
let image = UIImage(named: cars[i - 1])
let carView = UIImageView(image: image!)
carView.frame = CGRect(x:i * 52, y:0 , width: 40, height: 50)
view.addSubview(carView)
dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
gravityBehavior = UIDynamicItemBehavior(items: [carView]) //cars falling
dynamicAnimator.addBehavior(gravityBehavior)
collisionBehavior = UICollisionBehavior(items: [carView, mainCar]) //collide
collisionBehavior.translatesReferenceBoundsIntoBoundary = false
gravityBehavior.addLinearVelocity(CGPoint(x: 0, y: 200), for: carView)
dynamicAnimator.addBehavior(collisionBehavior)
}
collisionBehavior.addBoundary(withIdentifier: "Barrier" as NSCopying, for: UIBezierPath(rect: mainCar.frame))
collisionBehavior.removeAllBoundaries()
}
With the game so far the last car in the array falls and the main player car that I control has collision behaviour, which is a big step for me!
You are creating a new UIDynamicAnimator with each iteration of the loop and assigning it to dynamicAnimator. That is why only the last element is working, because it is the last one assigned to that variable.
To fix it, just move this line to somewhere that would only be called once.
dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
viewDidLoad is a possible place that should work.
UIKitDynamics is backwards of most similar frameworks. You don't animate the object. You have an animator and attach objects to it. As Clever Error notes, you only want one animator in this case.
The key point is that you don't attach gravity to cars; you attach cars to behaviors (gravity), and then behaviors to the animator. Yes, that's bizarre and backwards.
I haven't tested this, but the correct code would be closer to this:
func mainGame()
{
let cars = ["car5", "car1", "car6", "car3", "car2", "car4"]
var random2 = Int(arc4random_uniform(UInt32(cars.count))) + 1
var carViews: [UIImageView] = []
dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
// First create all the views
for i in 1...random2
{
let image = UIImage(named: cars[i - 1])
let carView = UIImageView(image: image!)
carView.frame = CGRect(x:i * 52, y:0 , width: 40, height: 50)
view.addSubview(carView)
carViews.append(carView)
}
// and then attach those to behaviors:
gravityBehavior = UIGravityBehavior(items: carViews) //cars falling
dynamicAnimator.addBehavior(gravityBehavior)
collisionBehavior = UICollisionBehavior(items: carView + mainCar) //collide
collisionBehavior.translatesReferenceBoundsIntoBoundary = false
dynamicAnimator.addBehavior(collisionBehavior)
collisionBehavior.addBoundary(withIdentifier: "Barrier" as NSCopying, for: UIBezierPath(rect: mainCar.frame))
collisionBehavior.removeAllBoundaries()
// You don't need this; it's built into Gravity
// gravityBehavior.addLinearVelocity(CGPoint(x: 0, y: 200), for: carView)
}
The main way that UIKitDynamics is different than most animation frameworks is that things that are animated don't know they're being animated. You can't ask a car what behaviors it has, because it doesn't have any. A UIDynamicAnimator basically is a timing loop that updates the center and transform of its targets. There's really not anything fancy about it (in contrast to something like Core Animation which has many fancy things going on). With a little iOS experience, you could probably implement all of UIKitDynamics by hand with a single GCD queue (it probably doesn't even need that, since it runs everything on main....)

Swift - Create circle and add to array

I've been looking for a little while on how to create a circle in Swift, but it seems quite complicated. I want to create a circle at run time, with a specified x and y, and width and height. I then want to add this circle to an array where I can create more circles and add to it.
How do I do this?
Edit: This is what I've tried so far:
var center : CGPoint = touchLocation
var myContext : CGContextRef = UIGraphicsGetCurrentContext()
let color : [CGFloat] = [0, 0, 1, 0.5]
CGContextSetStrokeColor (myContext, color);
CGContextStrokeEllipseInRect (myContext, CGRectMake(touchLocation.x, touchLocation.y, 20, 20));
touchLocation is the location of the users finger. This crashes on execution on this line:
var myContext : CGContextRef = UIGraphicsGetCurrentContext()
The error says "Unexpectedly found nil while unwrapping an Optional value
Also, this doesn't allow me to add the circle to an array, because I don't know what variable type it is.
There are many ways to draw a circle, here is a snippet that I have been hacking with:
func circleWithCenter(c:CGPoint, radius r:CGFloat,
strokeColor sc: UIColor = UIColor.blackColor(),
fillColor fc: UIColor = UIColor.clearColor()) -> CAShapeLayer {
var circle = CAShapeLayer()
circle.frame = CGRect(center:c, size:CGSize(width:2*r, height:2*r))
circle.path = UIBezierPath(ovalInRect:circle.bounds).CGPath
circle.fillColor = fc.CGColor
circle.strokeColor = sc.CGColor
circle.fillColor = fc == UIColor.clearColor() ? nil : fc.CGColor
return circle
}
Note that I extended CGRect (using Swift specific features) to add a initializer that takes a center, but that is not material to your question.
Your code is not "creating" a circle as an object, it is attempting to draw one in the graphics context. What you need to do is to create a bezier path object, draw into that and save the path in your array. Something like:
var circleArray = [CGMutablePathRef]()
// ...
let path: CGMutablePathRef = CGPathCreateMutable()
CGPathAddArc(path, nil, touchLocation.x, touchLocation.y, radius, 0, M_PI * 2, true)
circleArray += [path]
You then need to stroke the path(s) in your draw routine.

Resources