11

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.

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
BassetMan
  • 461
  • 6
  • 13

3 Answers3

9

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

David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • Hi David, thanks for the 'Swift' response. You are absolutely correct, strange as at one point I was using CShort and hard coded the NSData length to 6. But even that appears to work now. I think I might have changed the only Light from a Omni to an Ambient, which didn't help. – BassetMan Jun 30 '14 at 17:33
  • Hey David, I posted an interesting question (with bounty) about vertices and ARKit. Would you mind take a look? https://stackoverflow.com/questions/63662318/arkit-apply-cifilter-to-a-specific-vertices-of-arfaceanchor We can't seem to find a solution ah – Roi Mulia Sep 10 '20 at 12:32
8

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])
0nelight
  • 65
  • 7
rickster
  • 124,678
  • 26
  • 272
  • 326
  • The sizeofvalue has been fixed in iOS 8 beta 3. – BassetMan Jul 14 '14 at 19:01
  • And the array syntax and semantics have changed a bit, too -- I'll update this answer when I get some time to check the code. – rickster Jul 14 '14 at 19:04
  • For `let element = SCNGeometryElement(data: indexData, ..`, if the index data is just [0,1,2,3,4,etc], you can say `let element = SCNGeometryElement(data: nil, ..`. I have to assume that `bytesPerIndex` is ignored in this usage. – Ramsay Consulting Jun 13 '17 at 22:13
  • @rickster But how would you go about making INTERLEAVED indexData (as output by Collada)? As far as I can see in SceneKit, it's impossible. – Geoff H May 12 '18 at 11:13
0

I don't know a lot about swift but I would expect your versions of "verts" and "indexes" to be completely different in swift and ObjC (actually C).

In your C version you get a C array of C structures. In swift I assume you get a swift "array" (equivalent to NSArray) of SCNVector3 wrapped into NSValues. so the NSData you build with the "indexes" bytes doesn't contain a flat buffer of float values like in C. Same for the "verts" array.

Looks like such C and swift interoperability issues are discussed here: Swift use c struct

Community
  • 1
  • 1
Toyos
  • 4,024
  • 15
  • 16