Creating custom shapes from primitives - ios

I'm trying to create a custom Physics shape with combining primitive shapes. The goal is to create a rounded cube. The appropriate method seems to be init(shapes:transforms:) which I found here https://developer.apple.com/library/prerelease/ios/documentation/SceneKit/Reference/SCNPhysicsShape_Class/index.html#//apple_ref/occ/clm/SCNPhysicsShape/shapeWithShapes:transforms:
I'm thinking this could be done with 8 spheres, 12 cylinders and a box in the middle. Can anyone provide an example of doing that?

Yes, as you may have noticed, creating a physics body from an SCNBox with rounded corners ignores the chamfer radius. Actually, nearly all of the basic geometries (box, sphere, cylinder, pyramid, teapot, etc) generate physics shapes that are idealized forms rather than direct conversions of their vertex meshes to physics bodies.
Generally, this is a good thing. It's much faster to perform collision detection on an idealized sphere than on a mesh of eleventy-hundred triangles that approximates a sphere (is the point to test within radius distance of the sphere's center?). Ditto for an idealized box (convert point to box's local coordinate system, text for x/y/z within bounds).
The init(shapes:transforms:) initializer for SCNShape is a good way to build a complex shape from these idealized shapes. Actually, so is the init(node:options:) initializer: If you pass [SCNPhysicsShapeKeepAsCompoundKey: true] for the options parameter, you can pass an SCNNode that contains an hierarchy of child nodes whose geometries are primitive shapes, and SceneKit will convert each of those geometries to its idealized physics shape before creating a physics shape that's the union of all of them.
I'll show an example of each. But first, some shared context:
let side: CGFloat = 1 // one side of the cube
let radius: CGFloat = side / 4 // the corner radius
// the visual (but not physical) cube
let cube = SCNNode(geometry: SCNBox(width: side, height: side, length: side, chamferRadius: radius))
Here's a shot at making it with init(shapes:transforms:):
var compound: SCNPhysicsShape {
let sphereShape = SCNPhysicsShape(geometry: SCNSphere(radius: radius), options: nil)
let spheres = [SCNPhysicsShape](count: 8, repeatedValue: sphereShape)
let sphereTransforms = [
SCNMatrix4MakeTranslation( radius, radius, radius),
SCNMatrix4MakeTranslation(-radius, radius, radius),
SCNMatrix4MakeTranslation(-radius, -radius, radius),
SCNMatrix4MakeTranslation(-radius, -radius, -radius),
SCNMatrix4MakeTranslation( radius, -radius, -radius),
SCNMatrix4MakeTranslation( radius, radius, -radius),
SCNMatrix4MakeTranslation(-radius, radius, -radius),
SCNMatrix4MakeTranslation( radius, -radius, radius),
]
let transforms = sphereTransforms.map {
NSValue(SCNMatrix4: $0)
}
return SCNPhysicsShape(shapes: spheres, transforms: transforms)
}
cube.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: compound)
The dance you see in there with sphereTransforms and transforms is because SceneKit expects an ObjC NSArray for each of its parameters, and NSArrays can contain only ObjC objects... a transform is an SCNMatrix4, which is a struct, so we have to wrap it in an NSValue to store it in an NSArray. In Swift, it's convenient to work with an array of SCNMatrix4, then use map to get an array of NSValues wrapping each element. (And Swift automatically bridges to NSArray under the hood when we pass our [NSValue] to the SceneKit API.)
This creates a body that's just the rounded corners for the cube — there's empty space in between them. Depending on the situation where you need rounded-cube collisions, that may be enough. For example, if you just want to make rounded-cube dice roll on a floor, corner collisions are the only important ones, because the floor won't collide with the middle of a die without also contacting the corner spheres. If that's all you need, go for it — you get the best performance if your physics shapes are as simple as possible.
If you wanted to make a more accurate compound shape, with cylinders for the edges and either three boxes or six planes for the faces, you could extend the above example. Just make arrays of shapes transforms for each kind of shape, and concatenate the arrays before converting to [NSValue] and passing to SceneKit. (Note that the cylinders will need both rotation and translation transforms, so combine SCNMatrix4MakeTranslation with SCNMatrix4Rotate.)
Then again, all that math is getting hard to visualize. And nesting calls to SCNMatrix4Whatever to do that math isn't so fun. So you could do it with nodes instead:
var nodeCompound: SCNNode {
// a node to hold the compound geometry
let parent = SCNNode()
// one node with a sphere
let sphere = SCNNode(geometry: SCNSphere(radius: radius))
// inner func to clone the sphere to a specific position
func corner(x x: CGFloat, y: CGFloat, z: CGFloat) -> SCNNode {
let node = sphere.clone()
node.position = SCNVector3(x: x, y: y, z: z)
return node
}
// clone the sphere to each corner as child nodes
parent.addChildNode(corner(x: radius, y: radius, z: radius))
parent.addChildNode(corner(x: -radius, y: radius, z: radius))
parent.addChildNode(corner(x: -radius, y: -radius, z: radius))
parent.addChildNode(corner(x: -radius, y: -radius, z: -radius))
parent.addChildNode(corner(x: radius, y: -radius, z: -radius))
parent.addChildNode(corner(x: radius, y: radius, z: -radius))
parent.addChildNode(corner(x: -radius, y: radius, z: -radius))
parent.addChildNode(corner(x: radius, y: -radius, z: radius))
return parent
}
Put this node in a scene and you can visualize the results as you position your spheres (and cylinders, etc). Notice that this node doesn't have to actually be added to your scene, though (except when you're visualizing it for debugging purposes). Once you've got it how you want it, use it to create a physics shape, and assign that shape to the other node that you actually want to draw in your scene:
cube.physicsBody = SCNPhysicsBody(type: .Dynamic,
shape: SCNPhysicsShape(node: nodeCompound,
options: [SCNPhysicsShapeKeepAsCompoundKey: true]))
By the way, if you drop the keep-as-compound option here, you'll get a shape that's a convex hull mesh of your eight corner spheres (regardless of whether you also put edges and faces in, because those lie within the hull). That is, it gets you some approximation of a rounded cube... the corner radius will be less smooth than with the idealized geometry, but depending on what you need this collision body for, it might be all you need.

Related

How to ensure SceneKit camera always points at specific position

I'm writing a game in Swift 5 (but had the same problem with Swift 4 which i recently updated by game from), with all my SCNNode's centered around SCNVector3Zero. In other word's (0,0,0) is at the center of my play area.
The SCNCamera is attached to a SNCNode and positioned just outside the limits of the play area, looking at (0,0,0).
cameraNode.look(at: SCNVector3Zero)
If I place 5 nodes in the scene, all on the y=0 plane, (0,0,0), (-1,0,-1), (1,0,1), (-1,0,1) and (1,0,-1) like the face of a dice showing five, it all works great. I can rotate around the scene with the node at (0,0,0) staying centered with no movement.
If I add a 2nd row, so the first row moves to y=1 and the second row gets nodes with y=-1 the scene wobbles a little when rotating.
The further a node is moved from the center, the more exaggerated this wobble becomes.
Here's the code setting up the scene (this example has three rows and looks like a three dimensional "five" face of a dice, point in the center at (0,0,0) and the other dots at each corner of a cube) ...
addSphere(
x: 0.0,
y: 0.0,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: -2.0 * spacing,
y: -2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: 2.0 * spacing,
y: 2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: -2.0 * spacing,
y: 2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "granite")
addSphere(
x: 2.0 * spacing,
y: -2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "granite")
addSphere(
x: 0.0,
y: -2.0 * spacing,
z: -2.0 * spacing,
radius: radius,
textureName: "slime")
addSphere(
x: 0.0,
y: 2.0 * spacing,
z: 2.0 * spacing,
radius: radius,
textureName: "slime")
addSphere(
x: 0.0,
y: 2.0 * spacing,
z: -2.0 * spacing,
radius: radius,
textureName: "wood")
addSphere(
x: 0.0,
y: -2.0 * spacing,
z: 2.0 * spacing,
radius: radius,
textureName: "wood")
This code behaves, everything is symmetrical around (0,0,0) with the node at (0,0,0) staying exactly where it should.
If I introduce this node into the scene, it all goes badly wrong ...
addSphere(
x: 6.0,
y: 6.0,
z: 6.0,
radius: radius,
textureName: "earth")
I've tried adding a fixed physics body to each node that has a mass of zero to no avail. It's like the camera is no longer looking directly at (0,0,0) but is influenced but the nodes in the scene.
I've tried all sorts of permutations of adding nodes, and on some tests it appears adding anything with value for the z-axis caused problems
This is the addSphere method ...
internal func addSphere(x: Float, y: Float, z: Float, radius: Float, color: UIColor, textureName: String) -> SCNNode {
let sphereGeometry = SCNSphere(radius: CGFloat(radius))
sphereGeometry.firstMaterial?.diffuse.contents = UIImage(imageLiteralResourceName: textureName)
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.name = "dot"
sphereNode.position = SCNVector3(x: x, y: y, z: z)
sphereNode.lines = [];
sphereNode.ignoreTaps = false
sphereNode.categoryBitMask = NodeBitMasks.dot
//sphereNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
self.rootNode.addChildNode(sphereNode)
return sphereNode
}
There are no errors and as you might expect, any point positioned at (0,0,0) should remain static when rotating about while "looking at" this point.
Any pointers would be really appreciated as I can't make heads or tails of why it's behaving like this.
I assume you use the SCNView.allowsCameraControl = true to move the camera around. This built-in setting of SceneKit is actually only for debug purposes of your scene. It is not suited for anything when you want to have dedicated control over your camera movement.
You should instead try to implement a camera orbit, see https://stackoverflow.com/a/25674762/3358138.
I could reproduce your problem with the "wobbling" center of your scene, see Playground Gist https://gist.github.com/dirkolbrich/e2c247619b28a287c464abbc0595e23c.
A camera orbit solves this "wobbling" and let’s the camera stay on center, see Playground Gist https://gist.github.com/dirkolbrich/9e4dffb3026d0540d6edf6877f27d1e4.

iOS revert camera projection

I'm trying to estimate my device position related to a QR code in space. I'm using ARKit and the Vision framework, both introduced in iOS11, but the answer to this question probably doesn't depend on them.
With the Vision framework, I'm able to get the rectangle that bounds a QR code in the camera frame. I'd like to match this rectangle to the device translation and rotation necessary to transform the QR code from a standard position.
For instance if I observe the frame:
* *
B
C
A
D
* *
while if I was 1m away from the QR code, centered on it, and assuming the QR code has a side of 10cm I'd see:
* *
A0 B0
D0 C0
* *
what has been my device transformation between those two frames? I understand that an exact result might not be possible, because maybe the observed QR code is slightly non planar and we're trying to estimate an affine transform on something that is not one perfectly.
I guess the sceneView.pointOfView?.camera?.projectionTransform is more helpful than the sceneView.pointOfView?.camera?.projectionTransform?.camera.projectionMatrix since the later already takes into account transform inferred from the ARKit that I'm not interested into for this problem.
How would I fill
func get transform(
qrCodeRectangle: VNBarcodeObservation,
cameraTransform: SCNMatrix4) {
// qrCodeRectangle.topLeft etc is the position in [0, 1] * [0, 1] of A0
// expected real world position of the QR code in a referential coordinate system
let a0 = SCNVector3(x: -0.05, y: 0.05, z: 1)
let b0 = SCNVector3(x: 0.05, y: 0.05, z: 1)
let c0 = SCNVector3(x: 0.05, y: -0.05, z: 1)
let d0 = SCNVector3(x: -0.05, y: -0.05, z: 1)
let A0, B0, C0, D0 = ?? // CGPoints representing position in
// camera frame for camera in 0, 0, 0 facing Z+
// then get transform from 0, 0, 0 to current position/rotation that sees
// a0, b0, c0, d0 through the camera as qrCodeRectangle
}
====Edit====
After trying number of things, I ended up going for camera pose estimation using openCV projection and perspective solver, solvePnP This gives me a rotation and translation that should represent the camera pose in the QR code referential. However when using those values and placing objects corresponding to the inverse transformation, where the QR code should be in the camera space, I get inaccurate shifted values, and I'm not able to get the rotation to work:
// some flavor of pseudo code below
func renderer(_ sender: SCNSceneRenderer, updateAtTime time: TimeInterval) {
guard let currentFrame = sceneView.session.currentFrame, let pov = sceneView.pointOfView else { return }
let intrisics = currentFrame.camera.intrinsics
let QRCornerCoordinatesInQRRef = [(-0.05, -0.05, 0), (0.05, -0.05, 0), (-0.05, 0.05, 0), (0.05, 0.05, 0)]
// uses VNDetectBarcodesRequest to find a QR code and returns a bounding rectangle
guard let qr = findQRCode(in: currentFrame) else { return }
let imageSize = CGSize(
width: CVPixelBufferGetWidth(currentFrame.capturedImage),
height: CVPixelBufferGetHeight(currentFrame.capturedImage)
)
let observations = [
qr.bottomLeft,
qr.bottomRight,
qr.topLeft,
qr.topRight,
].map({ (imageSize.height * (1 - $0.y), imageSize.width * $0.x) })
// image and SceneKit coordinated are not the same
// replacing this by:
// (imageSize.height * (1.35 - $0.y), imageSize.width * ($0.x - 0.2))
// weirdly fixes an issue, see below
let rotation, translation = openCV.solvePnP(QRCornerCoordinatesInQRRef, observations, intrisics)
// calls openCV solvePnP and get the results
let positionInCameraRef = -rotation.inverted * translation
let node = SCNNode(geometry: someGeometry)
pov.addChildNode(node)
node.position = translation
node.orientation = rotation.asQuaternion
}
Here is the output:
where A, B, C, D are the QR code corners in the order they are passed to the program.
The predicted origin stays in place when the phone rotates, but it's shifted from where it should be. Surprisingly, if I shift the observations values, I'm able to correct this:
// (imageSize.height * (1 - $0.y), imageSize.width * $0.x)
// replaced by:
(imageSize.height * (1.35 - $0.y), imageSize.width * ($0.x - 0.2))
and now the predicted origin stays robustly in place. However I don't understand where the shift values come from.
Finally, I've tried to get an orientation fixed relatively to the QR code referential:
var n = SCNNode(geometry: redGeometry)
node.addChildNode(n)
n.position = SCNVector3(0.1, 0, 0)
n = SCNNode(geometry: blueGeometry)
node.addChildNode(n)
n.position = SCNVector3(0, 0.1, 0)
n = SCNNode(geometry: greenGeometry)
node.addChildNode(n)
n.position = SCNVector3(0, 0, 0.1)
The orientation is fine when I look at the QR code straight, but then it shifts by something that seems to be related to the phone rotation:
Outstanding questions I have are:
How do I solve the rotation?
where do the position shift values come from?
What simple relationship do rotation, translation, QRCornerCoordinatesInQRRef, observations, intrisics verify? Is it O ~ K^-1 * (R_3x2 | T) Q ? Because if so that's off by a few order of magnitude.
If that's helpful, here are a few numerical values:
Intrisics matrix
Mat 3x3
1090.318, 0.000, 618.661
0.000, 1090.318, 359.616
0.000, 0.000, 1.000
imageSize
1280.0, 720.0
screenSize
414.0, 736.0
==== Edit2 ====
I've noticed that the rotation works fine when the phone stays horizontally parallel to the QR code (ie the rotation matrix is [[a, 0, b], [0, 1, 0], [c, 0, d]]), no matter what the actual QR code orientation is:
Other rotation don't work.
Coordinate systems' correspondence
Take into consideration that Vision/CoreML coordinate system doesn't correspond to ARKit/SceneKit coordinate system. For details look at this post.
Rotation's direction
I suppose the problem is not in matrix. It's in vertices placement. For tracking 2D images you need to place ABCD vertices counter-clockwise (the starting point is A vertex located in imaginary origin x:0, y:0). I think Apple Documentation on VNRectangleObservation class (info about projected rectangular regions detected by an image analysis request) is vague. You placed your vertices in the same order as is in official documentation:
var bottomLeft: CGPoint
var bottomRight: CGPoint
var topLeft: CGPoint
var topRight: CGPoint
But they need to be placed the same way like positive rotation direction (about Z axis) occurs in Cartesian coordinates system:
World Coordinate Space in ARKit (as well as in SceneKit and Vision) always follows a right-handed convention (the positive Y axis points upward, the positive Z axis points toward the viewer and the positive X axis points toward the viewer's right), but is oriented based on your session's configuration. Camera works in Local Coordinate Space.
Rotation direction about any axis is positive (Counter-Clockwise) and negative (Clockwise). For tracking in ARKit and Vision it's critically important.
The order of rotation also makes sense. ARKit, as well as SceneKit, applies rotation relative to the node’s pivot property in the reverse order of the components: first roll (about Z axis), then yaw (about Y axis), then pitch (about X axis). So the rotation order is ZYX.
Math (Trig.):
Notes: the bottom is l (the QR code length), the left angle is k, and the top angle is i (the camera)

Make an object orbit another in SceneKit

Say I have 2 nodes in my SceneKit scene. I want one to rotate around or orbit (like a planet orbiting a star), the other node once in a certain time interval. I know I can set up animations like so:
let anim = CABasicAnimation(keyPath: "rotation")
anim.fromValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: 0))
anim.toValue = NSValue(scnVector4: SCNVector4(x: 0, y: 1, z: 0, w: Float(2 * Double.pi)))
anim.duration = 60
anim.repeatCount = .infinity
parentNode.addAnimation(aim, forKey: "spin around")
Is there an animation for "orbiting", and a way to specify the target node?
The way to do this is by using an additional (helper) SCNNode. You'll use the fact that it adds its own coordinate system and that all of its Child Nodes will move together with that (helper) coordinate system. The child nodes that are off-centre will effectively be orbiting if you view them from the world coordinate system.
You add the HelperNode at the centre of your FixedPlanetNode (orbited planet), perhaps as its child, but definitely at the same position
You add your OrbitingPlanetNode as a child to the HelperNode, but with an offset on one of the axes, e.g. 10 points on the X axis
You start the HelperNode rotating (together with its coordinate system) around a different axis, e.g. the Y axis
This will result in the OrbitingPlanetNode orbiting around the Y axis of HelperNode with an orbit radius of 10 points.
EXAMPLE
earthNode - fixed orbited planet
moonNode - orbiting planet
helperNode - helper node added to provide coordinate system
// assuming all planet geometry is at the centre of corresponding nodes
// also helperNode.position is set to (0, 0, 0)
[earthNode addChildNode:helperNode];
moonNode.position = SCNVector3Make(10, 0, 0);
[helperNode addChildNode:moonNode];
// set helperNode to rotate forever
SCNAction * rotation = [SCNAction rotateByX:0 y:3 z:0];
SCNAction * infiniteRotation = [SCNAction repeatActionForever:rotation];
[helperNode runAction:infiniteRotation];
I used actions and objective-c as this is what I am familiar with, but should be perfectly doable in Swift and with animations.

SKSpriteNode will not center

I have this inside my GameScene which is called in the didMove()
for i in 1...5 {
// path to create the circle
let path = UIBezierPath(arcCenter: CGPoint(x: center.x, y: center.y), radius: CGFloat(((43 * i) + 140)), startAngle: CGFloat(GLKMathDegreesToRadians(-50)), endAngle: CGFloat(M_PI * 2), clockwise: false)
// the inside edge of the circle used for creating its physics body
let innerPath = UIBezierPath(arcCenter: CGPoint(x: center.x, y: center.y), radius: CGFloat(((43 * i) + 130)), startAngle: CGFloat(GLKMathDegreesToRadians(-50)), endAngle: CGFloat(M_PI * 2), clockwise: false)
// create a shape from the path and customize it
let shape = SKShapeNode(path: path.cgPath)
shape.lineWidth = 20
shape.strokeColor = UIColor(red:0.98, green:0.99, blue:0.99, alpha:1.00)
// create a texture and apply it to the sprite
let trackViewTexture = self.view!.texture(from: shape)
let trackViewSprite = SKSpriteNode(texture: trackViewTexture)
trackViewSprite.physicsBody = SKPhysicsBody(edgeChainFrom: innerPath.cgPath)
self.addChild(trackViewSprite)
}
It uses UIBezierPaths to make a few circles. It converts the path into a SKShapeNode then a SKTexture and then applies it to the final SKSpriteNode.
When I do this, the SKSpriteNode is not where it should be, it is a few to the right:
But when I add the SKShapeNode I created, it is set perfectly fine to where it should be:
Even doing this does not center it!
trackViewSprite.position = CGPoint(x: 0, y: 0)
No matter what I try it just will not center.
Why is this happening? Some sort of bug when converting to a texture?
P.S - This has something to do with this also Keep relative positions of SKSpriteNode from SKShapeNode from CGPath
But there is also no response :(
Edit, When I run this:
let testSprite = SKSpriteNode(color: UIColor.yellow, size: trackViewSprite.size)
self.addChild(testSprite)
It shows it has the same frame also:
After a long discussion, we determined that the problem is due to the frame size not being the expected size of the shape.
To combat this, the OP created an outer path of his original path, and calculated the frame that would surround this. Now this approach may not work for everybody.
If anybody else comes across this issue, they will need to do these things:
1) Check the frame of the SKShapeNode to make sure that it is correct
2) Determine what method is best to calculate the correct desired frame
3) Use this new frame when getting textureFromNode to extract only the desired texture size

SceneKit how to draw a sphere showing mesh like surface instead of smooth?

I am drawing a sphere in Scene Kit, and it all works ok. I am drawing it like so:
...
let g = SCNSphere(radius: radius)
geometria.firstMaterial?.diffuse.contents = myColor
let node = SCNNode(geometry: g)
node.position = SCNVector3(x: x, y: y, z: z)
scene.rootNode.addChildNode(node)
This draws the sphere with a smooth surface (see image).
I would
What I am trying to accomplish is to have the sphere not rendered "smooth" like in the photo but I want to be able to have it so it shows the skeleton... so maybe control how many triangles it uses to draw the surface of the sphere but the triangles need to be empty, so I would just see the sides of the triangles...
Any suggestion?
So here's an image of what zI am trying to make the sphere look like:
Your wish #1: "not smooth" sphere
Your wish #2: wireframe
Past this into Xcode playground:
import Cocoa
import SceneKit
import QuartzCore
import XCPlayground
// create a scene
var sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
var scene = SCNScene()
sceneView.scene = scene
XCPShowView("The Scene View", sceneView)
sceneView.autoenablesDefaultLighting = false
// create sphere
let g = SCNSphere(radius: 100)
g.firstMaterial?.diffuse.contents = NSColor.greenColor()
// WISH #1
g.segmentCount = 12
let node = SCNNode(geometry: g)
node.position = SCNVector3(x: 10, y: 10, z: 10)
scene.rootNode.addChildNode(node)
// WISH #2
glPolygonMode(GLenum(GL_FRONT), GLenum(GL_LINE));
glPolygonMode(GLenum(GL_BACK), GLenum(GL_LINE));
// animate
var spin = CABasicAnimation(keyPath: "rotation")
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 1, y: 1, z: 0, w: CGFloat(2.0*M_PI)))
spin.duration = 3
spin.repeatCount = HUGE // for infinity
node.addAnimation(spin, forKey: "spin around")
Taken from the possible duplicate question Render an SCNGeometry as a wireframe, I'm going to copy it here for posterity:
it's possible to set the fillMode of the Material to just lines, which gives the desired effect:
g.firstMaterial?.fillMode = .lines
// (or possibly geometria.firstMaterial?.fillMode, not clear from your example)
you can then change the 'resolution' of the wireframe using:
g.segmentCount = 12
// 48 is the default, lower is 'coarser', less than 3
// is undefined and therefore unsupported
and you can probably also want to set:
g.isGeodesic = true
... which will give you triangular 'tiles' on your wireframe rather than the default rectangles.

Resources