Extracting vertices from scenekit
Asked Answered
T

9

21

I'm having a problem with understanding scenekit geometery.

I have the default cube from Blender, and I export as collada (DAE), and can bring it into scenekit.... all good.

Now I want to see the vertices for the cube. In the DAE I can see the following for the "Cube-mesh-positions-array",

"1 1 -1 1 -1 -1 -1 -0.9999998 -1 -0.9999997 1 -1 1 0.9999995 1 0.9999994 -1.000001 1 -1 -0.9999997 1 -1 1 1"

Now what I'd like to do in scenekit, is get the vertices back, using something like the following:

SCNGeometrySource *vertexBuffer = [[cubeNode.geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticVertex] objectAtIndex:0];

If I process the vertexBuffer (I've tried numerous methods of looking at the data), it doesn't seem correct.

Can somebody explain what "SCNGeometrySourceSemanticVertex" is giving me, and how to extract the vertex data properly? What I'd like to see is:

X = "float"
Y = "float"
Z = "float"

Also I was investigating the following class / methods, which looked promising (some good data values here), but the data from gmpe appears empty, is anybody able to explain what the data property of "SCNGeometryElement" contains?

SCNGeometryElement *gmpe = [theCurrentNode.geometry geometryElementAtIndex:0];

Thanks, assistance much appreciated,

D

Turves answered 22/6, 2013 at 12:2 Comment(1)
Hi Darren, I am wondering if you have the final code, perhaps in Swift, that simply fetch the point positions of the geometry, without extra data, etc. Perhaps in Swift? I am currently in need to be able to do this for Face AR Geometry buffer. I wish I know more code knowledge, but the idea is to fetch Face geo of iPhone X, 60 times per second, and somewhat save it into one big cache data that is efficient. In 3D it would be Alembic, but not sure with app. But anyhow, if you have the function that fetch the geometry point positions, I can continue from there. Cheeers.Watchful
D
29

The geometry source

When you call geometrySourcesForSemantic: you are given back an array of SCNGeometrySource objects with the given semantic in your case the sources for the vertex data).

This data could have been encoded in many different ways and a multiple sources can use the same data with a different stride and offset. The source itself has a bunch of properties for you to be able to decode the data like for example

  • dataStride
  • dataOffset
  • vectorCount
  • componentsPerVector
  • bytesPerComponent

You can use combinations of these to figure out which parts of the data to read and make vertices out of them.

Decoding

The stride tells you how many bytes you should step to get to the next vector and the offset tells you how many bytes offset from the start of that vector you should offset before getting to the relevant pars of the data for that vector. The number of bytes you should read for each vector is componentsPerVector * bytesPerComponent

Code to read out all the vertices for a single geometry source would look something like this

// Get the vertex sources
NSArray *vertexSources = [geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticVertex];

// Get the first source
SCNGeometrySource *vertexSource = vertexSources[0]; // TODO: Parse all the sources

NSInteger stride = vertexSource.dataStride; // in bytes
NSInteger offset = vertexSource.dataOffset; // in bytes

NSInteger componentsPerVector = vertexSource.componentsPerVector;
NSInteger bytesPerVector = componentsPerVector * vertexSource.bytesPerComponent;
NSInteger vectorCount = vertexSource.vectorCount;

SCNVector3 vertices[vectorCount]; // A new array for vertices

// for each vector, read the bytes
for (NSInteger i=0; i<vectorCount; i++) {

    // Assuming that bytes per component is 4 (a float)
    // If it was 8 then it would be a double (aka CGFloat)
    float vectorData[componentsPerVector];

    // The range of bytes for this vector
    NSRange byteRange = NSMakeRange(i*stride + offset, // Start at current stride + offset
                                    bytesPerVector);   // and read the lenght of one vector

    // Read into the vector data buffer
    [vertexSource.data getBytes:&vectorData range:byteRange];

    // At this point you can read the data from the float array
    float x = vectorData[0];
    float y = vectorData[1];
    float z = vectorData[2];

    // ... Maybe even save it as an SCNVector3 for later use ...
    vertices[i] = SCNVector3Make(x, y, z);

    // ... or just log it 
    NSLog(@"x:%f, y:%f, z:%f", x, y, z);
}

The geometry element

This will give you all the vertices but won't tell you how they are used to construct the geometry. For that you need the geometry element that manages the indices for the vertices.

You can get the number of geometry elements for a piece of geometry from the geometryElementCount property. Then you can get the different elements using geometryElementAtIndex:.

The element can tell you if the vertices are used a individual triangles or a triangle strip. It also tells you the bytes per index (the indices may have been ints or shorts which will be necessary to decode its data.

Disinterested answered 22/6, 2013 at 12:45 Comment(7)
Hi, thanks for that, I'm using "SCNGeometrySourceSemanticVertex", so I would have thought I would have received "vertex" data only from this call to "geometrySourcesForSemantic". But I'm getting extra data (as far as I can tell). I've also used the following code to confirm the data types as you suggested: NSInteger vStride = [vertex dataStride]; NSInteger vOffset = [vertex dataOffset]; NSInteger vBytesPerComponent = [vertex bytesPerComponent]; bool areVertexFloats = [vertex floatComponents];Turves
@Turves I added code to parse out the vector data from the source. Hope it helps you read the necessary data from your file.Sirius
Hi thanks, this is good, can you explain why I have 36 vectors, where in the collada I have 24 vertices. I read in Apple Documentation that SCNGeometrySourceSemanticVertex returns a 3x3 vector. But don't quite understand the relationship between the two, re why there are more vertices in scenekit. ThanksTurves
Here's some additional information, if I do that same as the answer provided with Normals, and pack the data as a VBO (if that's the correct term). Then when I use the data elsewhere everything is messed up, I thought that could just be my packing code, so I took the code from above, i.e. vertexSources, and I moved that data elsewhere (iPhone app), and I enabled vertex and normals from the data and everything was fine (i.e. normals were working also). So that's telling me that the normal data has been included somehow (which is fine, solves my problem :)), but it would be good to understandTurves
And then there's also the texture coordinates. To me it looks like sometimes the data is all there packed correctly. At least the one I'm looking at right now is. Meaning that in memory it's x,y,z,w, nx,ny,nz, texcoordx,texcoordy. But then it's providing the stride and offset to correctly address each of those things when you specify each semantic. It's already interleaved. By doing the above code (times each semantic) you basically de-interleave it. Then you can repack it interleaved in whatever arrangement you need for your iOS app's 3d engine.Coprophilous
How to decode The geometry element, i don't have success to export mesh from a geometry.Catacomb
Since this is the accepted answer, perhaps it should be amended to reflect that SceneKit now lets you access vertices much faster as illustrated by other answers?Purr
E
8

Here is an extension method if the data isn't contiguous (the vector size isn't equal to the stride) which can be the case when the geometry is loaded from a DAE file. It also doesn't use copyByte function.

extension  SCNGeometry{
    
    /**
     Get the vertices (3d points coordinates) of the geometry.
 
     - returns: An array of SCNVector3 containing the vertices of the geometry.
     */
    func vertices() -> [SCNVector3]? {
        
        let sources = self.sources(for: .vertex)
        
        guard let source  = sources.first else{return nil}

        let stride = source.dataStride / source.bytesPerComponent
        let offset = source.dataOffset / source.bytesPerComponent
        let vectorCount = source.vectorCount
        
        return source.data.withUnsafeBytes { (buffer : UnsafePointer<Float>) -> [SCNVector3] in
            
            var result = Array<SCNVector3>()
            for i in 0...vectorCount - 1 {
                let start = i * stride + offset
                let x = buffer[start]
                let y = buffer[start + 1]
                let z = buffer[start + 2]
                result.append(SCNVector3(x, y, z))
            }
            return result
        }
    }
}

Enrichetta answered 10/7, 2018 at 8:12 Comment(2)
This works on Swift 5.5 iOS15Judsonjudus
But I dont understand why the geometry returned is always the same size, whatever size I give the shape, it returns 1x1 verion.Judsonjudus
B
5

Here's a Swift 5.3 version, based on the other answers, and that also supports a bytesPerComponent different from 4 (untested for size different from 4 though):

extension SCNGeometrySource {
    var vertices: [SCNVector3] {
        let stride = self.dataStride
        let offset = self.dataOffset
        let componentsPerVector = self.componentsPerVector
        let bytesPerVector = componentsPerVector * self.bytesPerComponent

        func vectorFromData<FloatingPoint: BinaryFloatingPoint>(_ float: FloatingPoint.Type, index: Int) -> SCNVector3 {
            assert(bytesPerComponent == MemoryLayout<FloatingPoint>.size)
            let vectorData = UnsafeMutablePointer<FloatingPoint>.allocate(capacity: componentsPerVector)
            defer {
                vectorData.deallocate()
            }
            let buffer = UnsafeMutableBufferPointer(start: vectorData, count: componentsPerVector)
            let rangeStart = index * stride + offset
            self.data.copyBytes(to: buffer, from: rangeStart..<(rangeStart + bytesPerVector))
            return SCNVector3(
                CGFloat.NativeType(vectorData[0]),
                CGFloat.NativeType(vectorData[1]),
                CGFloat.NativeType(vectorData[2])
            )
        }

        let vectors = [SCNVector3](repeating: SCNVector3Zero, count: self.vectorCount)
        return vectors.indices.map { index -> SCNVector3 in
            switch bytesPerComponent {
            case 4:
                return vectorFromData(Float32.self, index: index)
            case 8:
                return vectorFromData(Float64.self, index: index)
            case 16:
                return vectorFromData(Float80.self, index: index)
            default:
                return SCNVector3Zero
            }
        }
    }
}
Bridgettebridgewater answered 22/3, 2021 at 15:14 Comment(4)
This is great! Really good, and up to date answer. If you want to get the UVMap, just swap CGPoint and SCNVector3, and ignore the last vectorData[2]Allay
Unfortunately in current implementation it's memory leaking at ".allocate"Tarrsus
@АндрейПервушин Indeed, whoops! I've updated the code to properly deallocate the buffer; thanks for letting me know!Guerdon
Great answer. Remark: Float80 is only available on OSX, not on iOS, see here.Heartworm
E
4

The Swift Version

The Objective-C version and this are essentially identical.

let planeSources = _planeNode?.geometry?.geometrySourcesForSemantic(SCNGeometrySourceSemanticVertex)
if let planeSource = planeSources?.first {
    let stride = planeSource.dataStride
    let offset = planeSource.dataOffset
    let componentsPerVector = planeSource.componentsPerVector
    let bytesPerVector = componentsPerVector * planeSource.bytesPerComponent

    let vectors = [SCNVector3](count: planeSource.vectorCount, repeatedValue: SCNVector3Zero)
    let vertices = vectors.enumerate().map({
        (index: Int, element: SCNVector3) -> SCNVector3 in
        var vectorData = [Float](count: componentsPerVector, repeatedValue: 0)
        let byteRange = NSMakeRange(index * stride + offset, bytesPerVector)
        planeSource.data.getBytes(&vectorData, range: byteRange)
        return SCNVector3Make(vectorData[0], vectorData[1], vectorData[2])
    })

    // You have your vertices, now what?
}
Eagre answered 30/5, 2016 at 10:16 Comment(0)
C
2

With swift 3.1 you can extract vertices from SCNGeometry in a much faster and shorter way:

func vertices(node:SCNNode) -> [SCNVector3] {
    let vertexSources = node.geometry?.getGeometrySources(for: SCNGeometrySource.Semantic.vertex)
    if let vertexSource = vertexSources?.first {
        let count = vertexSource.data.count / MemoryLayout<SCNVector3>.size
        return vertexSource.data.withUnsafeBytes {
            [SCNVector3](UnsafeBufferPointer<SCNVector3>(start: $0, count: count))
        }
    }
    return []
}

... Today i've noted that on osx this not going to work correct. This happens because on iOS SCNVector3 build with Float and on osx CGFloat (only apple good do smth simple so suffering). So I had to tweak the code for osx but this not gonna work as fast as on iOS.

func vertices() -> [SCNVector3] {

        let vertexSources = sources(for: SCNGeometrySource.Semantic.vertex)


        if let vertexSource = vertexSources.first {

        let count = vertexSource.vectorCount * 3
        let values = vertexSource.data.withUnsafeBytes {
            [Float](UnsafeBufferPointer<Float>(start: $0, count: count))
        }

        var vectors = [SCNVector3]()
        for i in 0..<vertexSource.vectorCount {
            let offset = i * 3
            vectors.append(SCNVector3Make(
                CGFloat(values[offset]),
                CGFloat(values[offset + 1]),
                CGFloat(values[offset + 2])
            ))
        }

        return vectors
    }
    return []
}
Chifforobe answered 6/6, 2017 at 13:37 Comment(4)
This is seems to be working simplest, but from my geometry being sourced, seems like there are some extra data, not just the vertices. Is there an additional thing I need to do? Thanks!Watchful
Well the code expect that your data is a float stream... check what you actually have in itTarrsus
Ok, somewhat I got source code of this app that does this "vertex position fetching" for the Face AR that I am doing. Maybe kind of related, but anyhow: github.com/elishahung/FaceCaptureX?files=1Watchful
This doesn't work for all geometries, it doesn't seem to take into account stride and offset of vertices. So the byte size of vertices may not be uniform in the geometry.Elroyels
G
2

// call this function _ = vertices(node: mySceneView.scene!.rootNode) // I have get the volume in Swift 4.2 :--- this function

func vertices(node:SCNNode) -> [SCNVector3] {

    let planeSources1 = node.childNodes.first?.geometry

    let planeSources = planeSources1?.sources(for: SCNGeometrySource.Semantic.vertex)
    if let planeSource = planeSources?.first {

        let stride = planeSource.dataStride
        let offset = planeSource.dataOffset
        let componentsPerVector = planeSource.componentsPerVector
        let bytesPerVector = componentsPerVector * planeSource.bytesPerComponent

        let vectors = [SCNVector3](repeating: SCNVector3Zero, count: planeSource.vectorCount)
        let vertices = vectors.enumerated().map({
            (index: Int, element: SCNVector3) -> SCNVector3 in
            let vectorData = UnsafeMutablePointer<Float>.allocate(capacity: componentsPerVector)
            let nsByteRange = NSMakeRange(index * stride + offset, bytesPerVector)
            let byteRange = Range(nsByteRange)

            let buffer = UnsafeMutableBufferPointer(start: vectorData, count: componentsPerVector)
            planeSource.data.copyBytes(to: buffer, from: byteRange)
            return SCNVector3Make(buffer[0], buffer[1], buffer[2])
        })
        var totalVolume = Float()
        var x1 = Float(),x2 = Float(),x3 = Float(),y1 = Float(),y2 = Float(),y3 = Float(),z1 = Float(),z2 = Float(),z3 = Float()
        var i = 0
        while i < vertices.count{

                x1 = vertices[i].x;
                y1 = vertices[i].y;
                z1 = vertices[i].z;

                x2 = vertices[i + 1].x;
                y2 = vertices[i + 1].y;
                z2 = vertices[i + 1].z;

                x3 = vertices[i + 2].x;
                y3 = vertices[i + 2].y;
                z3 = vertices[i + 2].z;

                totalVolume +=
                    (-x3 * y2 * z1 +
                        x2 * y3 * z1 +
                        x3 * y1 * z2 -
                        x1 * y3 * z2 -
                        x2 * y1 * z3 +
                        x1 * y2 * z3);

                        i = i + 3
        }
        totalVolume = totalVolume / 6;
        volume = "\(totalVolume)"
        print("Volume Volume Volume Volume Volume Volume Volume :\(totalVolume)")
        lbl_valume.text = "\(clean(String(totalVolume))) cubic mm"
    }
    return[]
}
Gottfried answered 1/2, 2019 at 8:16 Comment(0)
J
2

OK, here is another Swift 5.5 version based on Oliver's answer.

extension  SCNGeometry{

    /**
     Get the vertices (3d points coordinates) of the geometry.

     - returns: An array of SCNVector3 containing the vertices of the geometry.
     */
    func vertices() -> [SCNVector3]? {

        let sources = self.sources(for: .vertex)

        guard let source  = sources.first else{return nil}

        let stride = source.dataStride / source.bytesPerComponent
        let offset = source.dataOffset / source.bytesPerComponent
        let vectorCount = source.vectorCount

        return source.data.withUnsafeBytes { dataBytes in
                    let buffer: UnsafePointer<Float> = dataBytes.baseAddress!.assumingMemoryBound(to: Float.self)
            var result = Array<SCNVector3>()
            for i in 0...vectorCount - 1 {
                let start = i * stride + offset
                let x = buffer[start]
                let y = buffer[start + 1]
                let z = buffer[start + 2]
                result.append(SCNVector3(x, y, z))
            }
            return result
        }
    }
}

To use it you simply create a standard shape from which you can extract the vertex and rebuild the index.

    let g = SCNSphere(radius: 1)

    let newNode = SCNNode(geometry: g)

    let vectors = newNode.geometry?.vertices()
    var indices:[Int32] = []
    
    for i in stride(from: 0, to: vectors!.count, by: 1) {
        indices.append(Int32(i))
        indices.append(Int32(i+1))
    }
    
    return self.createGeometry(
            vertices:vectors!, indices: indices,
                primitiveType: SCNGeometryPrimitiveType.line)

The createGeometry extension can be found here

It draws this...

enter image description here

Judsonjudus answered 10/5, 2022 at 16:19 Comment(0)
Q
1

For someone like me want to extract data of face from SCNGeometryElement.

Notice I only consider primtive type is triangle and index size is 2 or 4.

void extractInfoFromGeoElement(NSString* scenePath){
    NSURL *url = [NSURL fileURLWithPath:scenePath];
    SCNScene *scene = [SCNScene sceneWithURL:url options:nil error:nil];
    SCNGeometry *geo = scene.rootNode.childNodes.firstObject.geometry;
    SCNGeometryElement *elem = geo.geometryElements.firstObject;
    NSInteger componentOfPrimitive = (elem.primitiveType == SCNGeometryPrimitiveTypeTriangles) ? 3 : 0;
    if (!componentOfPrimitive) {//TODO: Code deals with triangle primitive only
        return;
    }
    for (int i=0; i<elem.primitiveCount; i++) {
        void *idxsPtr = NULL;
        int stride = 3*i;
        if (elem.bytesPerIndex == 2) {
            short *idxsShort = malloc(sizeof(short)*3);
            idxsPtr = idxsShort;
        }else if (elem.bytesPerIndex == 4){
            int *idxsInt = malloc(sizeof(int)*3);
            idxsPtr = idxsInt;
        }else{
            NSLog(@"unknow index type");
            return;
        }
        [elem.data getBytes:idxsPtr range:NSMakeRange(stride*elem.bytesPerIndex, elem.bytesPerIndex*3)];
        if (elem.bytesPerIndex == 2) {
            NSLog(@"triangle %d : %d, %d, %d\n",i,*(short*)idxsPtr,*((short*)idxsPtr+1),*((short*)idxsPtr+2));
        }else{
            NSLog(@"triangle %d : %d, %d, %d\n",i,*(int*)idxsPtr,*((int*)idxsPtr+1),*((int*)idxsPtr+2));
        }
        //Free
        free(idxsPtr);
    }
}
Quetzalcoatl answered 29/6, 2018 at 10:22 Comment(0)
M
1

The Swift 3 version:

    // `plane` is some kind of `SCNGeometry`

    let planeSources = plane.geometry.sources(for: SCNGeometrySource.Semantic.vertex)
    if let planeSource = planeSources.first {
        let stride = planeSource.dataStride
        let offset = planeSource.dataOffset
        let componentsPerVector = planeSource.componentsPerVector
        let bytesPerVector = componentsPerVector * planeSource.bytesPerComponent

        let vectors = [SCNVector3](repeating: SCNVector3Zero, count: planeSource.vectorCount)
        let vertices = vectors.enumerated().map({
            (index: Int, element: SCNVector3) -> SCNVector3 in

            let vectorData = UnsafeMutablePointer<Float>.allocate(capacity: componentsPerVector)
            let nsByteRange = NSMakeRange(index * stride + offset, bytesPerVector)
            let byteRange = Range(nsByteRange)

            let buffer = UnsafeMutableBufferPointer(start: vectorData, count: componentsPerVector)
                planeSource.data.copyBytes(to: buffer, from: byteRange)
            let vector = SCNVector3Make(buffer[0], buffer[1], buffer[2])
        })

        // Use `vertices` here: vertices[0].x, vertices[0].y, vertices[0].z
    }
Mccarver answered 21/10, 2018 at 18:32 Comment(1)
This works and is a universal approach as it takes into account stride and offset. Some other answers here don't, so they might work in some cases, not in others.Elroyels

© 2022 - 2024 — McMap. All rights reserved.