Swift: how to use the SCNGeometrySources from one node, to create another independent copy of that node?

101 Views Asked by At

I am trying to change the color of selected vertices of an imported .usdz model. My (naive?) approach is to make a copy of the geometry of the original model by extracting the SCNGeometrySources for the .vertex, .normal and .color SCNGeometrySource.Sematic. Then change the color source and re-assemble the geometry. It sometimes works, but not in all cases. For example the robot_walk_idle.usdz model from Apple's gallery, cannot be re-constructed by this approach. What am I missing?

Below is a SwiftUI view that displays the original robot, a straightforward copy and a re-constructed copy. I only show the first node that has a geometry attached. I haven't modified the color source, so the re-constructed copy should show equal to the middle scene. I cannot get rid of the strange appearance of the re-constructed copy, even after assigning various materials (which I do not show here)

I did notice that the geometry.elements.first.debugDescription shows Optional(<SCNGeometryElement: 0x600001ad6760 | 10576 x triangle, ***4 channels***, int indices>). Does that have any impact?

Here's the output:

unsuccesful SCNGeometry copy

Here's the code:

import SwiftUI
import SceneKit

struct CompareGeometries: View {
    
    let original = SCNScene(named: "SceneKit Asset Catalog.scnassets/robot_walk_idle.usdz")!
    
    var copy: SCNScene {
        var geometryCopy = SCNGeometry()
        original.rootNode.enumerateHierarchy { child, stop in
            if let geometry = child.geometry {
                stop.pointee = true
                geometryCopy = geometry.copy() as! SCNGeometry
            }
        }
        let node = SCNNode(geometry: geometryCopy)
        let scene = SCNScene()
        scene.rootNode.addChildNode(node)
        return scene
    }
    
    var reconstructed: SCNScene {
        var geometryReconstruction = SCNGeometry()
        original.rootNode.enumerateHierarchy { child, stop in
            if let geometry = child.geometry {
                stop.pointee = true
                let _ = print(geometry.sources)
                let vertexSources = geometry.sources(for: .vertex)
                let normalSources = geometry.sources(for: .normal)
                let colorSources = geometry.sources(for: .color)
  
                geometryReconstruction = SCNGeometry(
                    sources: vertexSources + normalSources + colorSources,
                    elements: geometry.elements)
            }
        }
        let node = SCNNode(geometry: geometryReconstruction)
        let scene = SCNScene()
        scene.rootNode.addChildNode(node)
        return scene
    }
    
    var body: some View {
        HStack {
            SceneView(scene: original, options: [.autoenablesDefaultLighting])
                .frame(width: 500, height: 500)
                .border(.black)
            SceneView(scene: copy, options: [.autoenablesDefaultLighting])
                .frame(width: 500, height: 500)
                .border(.black)
            SceneView(scene: reconstructed, options: [.autoenablesDefaultLighting])
                .frame(width: 500, height: 500)
                .border(.black)
        }
    }
}

Can this be done within SceneKit or do I need ModelIO ? Any help appreciated! Thanks.

1

There are 1 best solutions below

0
On

I think I've found a solution using ModelIO, and I believe the problem with the previous approach was due to the .usdz model using interleaved channels. Creating an MDLAsset from the scene dis-interleaves the geometry. At least that what I understand :)

The code below works fine:

    var thisIsTheWay: SCNScene {
        // Get the geometry of the first mesh in the scene.
        let mdlAsset = MDLAsset(scnScene: original)
        guard let firstMesh = mdlAsset.childObjects(of: MDLMesh.self).first as? MDLMesh else {
            return SCNScene()
        }
        let geometry = SCNGeometry(mdlMesh: firstMesh)

        let vertexSources = geometry.sources(for: .vertex)
        let normalSources = geometry.sources(for: .normal)
        let colorSources = geometry.sources(for: .color)
        
        let geometryReconstruction = SCNGeometry(
            sources: vertexSources + normalSources + colorSources,
            elements: geometry.elements)
        
        geometryReconstruction.firstMaterial?.lightingModel = .physicallyBased
        
        let node = SCNNode(geometry: geometryReconstruction)
        let scene = SCNScene()
        scene.rootNode.addChildNode(node)
        return scene
    }