问题
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?
回答1:
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 NSArray
s 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 NSValue
s 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.
来源:https://stackoverflow.com/questions/31656591/creating-custom-shapes-from-primitives