Creating custom shapes from primitives

I am trying to create a custom physics shape with a union of primitive shapes. The goal is to create a rounded cube. The relevant method seems to be init (forms: 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 think it can be done with 8 spheres, 12 cylinders and a box in the middle. Can anyone provide an example of this?

+3


source to share


1 answer


Yes, as you may have noticed, creating a physical solid from SCNBox

with rounded corners ignores the chamfer radius. In fact, almost all basic geometries (box, sphere, cylinder, pyramid, kettle , etc.) generate physical shapes that are idealized shapes rather than direct transformations of their vertex meshes into physical bodies.

This is generally good. It is much faster to perform collision detection on an idealized sphere than on a grid of eleven hundred triangles that approximate the sphere (is this a point to check for the radius of the sphere's center radius?). Ditto for a perfect box (convert point to box's local coordinate system, text for x / y / z within bounds).

The initializer init(shapes:transforms:)

for SCNShape

is a good way to build a complex shape from these idealized shapes. Actually the same initializer init(node:options:)

: if you pass [SCNPhysicsShapeKeepAsCompoundKey: true]

for a parameter options

, you can pass SCNNode

that contains a hierarchy of child nodes whose geometries are primitive shapes, and SceneKit converts each of these geometries to its idealized physics form, before creating a physical shape that combines all of them.

I'll show an example for each. But first, some general 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 an attempt at doing 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 there with sphereTransforms

and transforms

is because SceneKit expects ObjC NSArray

for each of its parameters, and NSArray

can only contain ObjC objects ... the transformation is that SCNMatrix4

, which is a struct, so we have to wrap it in NSValue

to store it in NSArray

... In Swift it is convenient to work with an array SCNMatrix4

and then use map

it to get an array NSValue

that wraps each element. (And Swift automatically connects to NSArray

under the hood when we pass ours [NSValue]

to the SceneKit API.)



This creates a solid that has only rounded corners for the cube - there's empty space in between. Depending on the situation where you need collisions with a rounded cube, this may be sufficient. For example, if you just want to make a cube with dice cubes across the floor, corner collisions are the only important ones because the floor will not hit the middle of the cube without touching the corners. If that's all you need, go for it - you'll get maximum performance if your fitness is as simple as possible.

If you want to make a more precise composite shape, with cylinders for the edges and three drawers or six planes for faces, you can expand on the example above. Just create arrays of shapes for each type of shape and merge the arrays before going to [NSValue]

and move on to SceneKit. (Note that cylinders will need a rotation and translation transformation, so combine SCNMatrix4MakeTranslation

with SCNMatrix4Rotate

.)

Again, all that mathematics becomes difficult to visualize. And nested calls SCNMatrix4Whatever

to do this math are not that fun. So you can do it with nodes instead of:

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
}

      

Place this node in the scene and you can render the results as you position your spheres (and cylinders, etc.). Note that this node does not actually need to be added to your scene (except when you render it for debugging purposes). Once you get it how you want, use it to create a physics shape and assign this shape to another 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 end up with a shape that is the convex hull of the hull from your eight corner spheres (regardless of whether you inserted the edges and faces as well, because they lie inside the hull). That is, you get some approximation of the rounded cube ... the corner radius will be less smooth than with idealized geometry, but depending on what you want this collision for, maybe that's all you want.

+6


source







All Articles