Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom SceneKit Geometry in Swift on iOS not working but equivalent Objective C code does

I am keen to adopt the new Swift language as this seems to be the way forward with Apple development. I have also been impressed with the new SceneKit support in iOS 8. I would like to programatically create custom geometry at runtime but I am struggling to get the Swift code to work. Yet the equivalent code in Objective C works OK.

This could be a bug, or something I am doing wrong.

I am simply trying to create and render a single triangle. I will ignore the normals and textures etc at this point for simplicity. So I am only expecting to see a black triangle.

Swift Code (not working)

var verts = [SCNVector3(x: 0,y: 0,z: 0),SCNVector3(x: 1,y: 0,z: 0),SCNVector3(x: 0,y: 1,z: 0)]

let src = SCNGeometrySource(vertices: &verts, count: 3)
let indexes:Int[]=[0,1,2]
let dat  = NSData(bytes: indexes, length: sizeofValue(indexes))
let ele = SCNGeometryElement(data:dat, primitiveType: .Triangles, primitiveCount: 1, bytesPerIndex: sizeof(Int))
let geo = SCNGeometry(sources: [src], elements: [ele])

let nd = SCNNode(geometry: geo)
scene.rootNode.addChildNode(nd)

Objective C Code (which does work)

SCNVector3 verts[] = { SCNVector3Make(0, 0, 0), SCNVector3Make(1, 0, 0), SCNVector3Make(0, 1, 0) };
SCNGeometrySource *src = [SCNGeometrySource geometrySourceWithVertices:verts count:3];

int indexes[] = { 0, 1, 2 };
NSData *datIndexes = [NSData dataWithBytes:indexes length:sizeof(indexes)];
SCNGeometryElement *ele = [SCNGeometryElement geometryElementWithData:datIndexes primitiveType:SCNGeometryPrimitiveTypeTriangles primitiveCount:1 bytesPerIndex:sizeof(int)];
SCNGeometry *geo = [SCNGeometry geometryWithSources:@[src] elements:@[ele]];

SCNNode *nd = [SCNNode nodeWithGeometry:geo];
d
[scene.rootNode addChildNode:nd];

Any pointers would be appreciated.

like image 476
BassetMan Avatar asked Jun 30 '14 15:06

BassetMan


2 Answers

David's answer is pretty good, but to present a more complete picture...

  • A Swift array of structs packs its data together like a C array of structs, not an ObjC NSArray of NSValue-wrapped structs. And you can pass a Swift array to (Obj)C APIs that take a CMutablePointer or similar, so it's totally cool to construct an NSData from a Swift array.

  • The same goes for arrays of / structs of structs, as long as they all get down to scalar types in the end.

  • Even in ObjC, making an array of SCNVector3 can be problematic, because the element type of that struct changes between 32/64-bit architectures.

  • sizeOfValue doesn't seem to be working as expected for arrays in Beta 2, so I'd recommend filing a bug.

  • If you implement the ArrayLiteralConvertible protocol in your custom structs, you can declare arrays of nested struct values concisely.

If you're aware of these issues, you can use in Swift, with some variation, most of the same tricks for vertex/index buffer management that you would in (Obj)C. For example, here's a snippet that builds a custom geometry with interleaved vertex and normal data (which is good for performance on iOS device GPUs):

struct Float3 : ArrayLiteralConvertible {
    typealias Element = GLfloat
    var x, y, z: GLfloat
    init(arrayLiteral elements: Element...) {
        self.x = elements[0]
        self.y = elements[1]
        self.z = elements[2]
    }
}

struct Vertex: ArrayLiteralConvertible {
    typealias Element = Float3
    var position, normal: Float3
    init(arrayLiteral elements: Element...) {
        self.position = elements[0]
        self.normal = elements[1]
    }
}

// This must be a var, not a let, because it gets passed to a CMutablePointer
var vertices: Vertex[] = [
    [ [+0.5, -0.5, -0.5],      [+1.0, +0.0, +0.0] ],
    [ [+0.5, +0.5, -0.5],      [+1.0, +0.0, +0.0] ],
    // ... lots more vertices here!
]

let data = NSData(bytes: vertices, length: vertices.count * sizeof(Vertex))

let vertexSource = SCNGeometrySource(data: data,
               semantic: SCNGeometrySourceSemanticVertex,
            vectorCount: vertices.count,
        floatComponents: true,
    componentsPerVector: 3,
      bytesPerComponent: sizeof(GLfloat),
             dataOffset: 0,
             dataStride: sizeof(Vertex))

let normalSource = SCNGeometrySource(data: data,
               semantic: SCNGeometrySourceSemanticNormal,
            vectorCount: vertices.count,
        floatComponents: true,
    componentsPerVector: 3,
      bytesPerComponent: sizeof(GLfloat),
// no offsetof() in Swift, but Vertex has one Float3 before normal
             dataOffset: sizeof(Float3), 
             dataStride: sizeof(Vertex))

// use a Swift Range to quickly construct a sequential index buffer
let indexData = NSData(bytes: Array<UInt8>(0..<UInt8(vertices.count)), 
             length: vertices.count * sizeof(UInt8))

let element = SCNGeometryElement(data: indexData, 
              primitiveType: .Triangles, 
             primitiveCount: vertices.count / 3,
              bytesPerIndex: sizeof(UInt8))

let cube = SCNGeometry(sources: [vertexSource, normalSource], elements: [element])
like image 106
rickster Avatar answered Nov 06 '22 08:11

rickster


Well, the two pieces of code doesn't translate exactly to one another. The int in C is not the same as Int in Swift. It's actually called CInt in Swift:

/// The C 'int' type.
typealias CInt = Int32

If you change both occurrences to use CInt instead, the error message that you previously got goes away (at least for me in an OS X Playground. However, it still doesn't render anything for me.

I don't think sizeofValue is used to return the size of an array. It looks to me like it's returning the size of the pointer:

let indexes: CInt[] = [0, 1, 2]
sizeofValue(indexes)                   // is 8
sizeof(CInt)                           // is 4
sizeof(CInt) * countElements(indexes)  // is 12

// compare to other CInt[]
let empty: CInt[] = []
let large: CInt[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

sizeofValue(indexes)                   // is 8 (your array of indices again)
sizeofValue(empty)                     // is 8
sizeofValue(large)                     // is 8

So, for me the following code works (I've put the arguments on different lines to make it easier to point out my changes):

let src = SCNGeometrySource(vertices: &verts, count: 3)
let indexes: CInt[] = [0, 1, 2] // Changed to CInt

let dat  = NSData(
    bytes: indexes,
    length: sizeof(CInt) * countElements(indexes) // Changed to size of CInt * count
)
let ele = SCNGeometryElement(
    data: dat,
    primitiveType: .Triangles,
    primitiveCount: 1,
    bytesPerIndex: sizeof(CInt) // Changed to CInt
)
let geo = SCNGeometry(sources: [src], elements: [ele])

let nd = SCNNode(geometry: geo)
scene.rootNode.addChildNode(nd)

With this result:

enter image description here

like image 23
David Rönnqvist Avatar answered Nov 06 '22 07:11

David Rönnqvist