¿Cómo crear un objeto SceneKit SCNSkinner en código?

89

Tengo una aplicación Swift que usa SceneKit para iOS 8. Carga una escena de un archivo .dae que contiene una malla controlada por un esqueleto. En tiempo de ejecución, necesito modificar las coordenadas de textura. Usar una transformación no es una opción: necesito calcular un UV diferente y completamente nuevo para cada vértice de la malla.

Sé que la geometría es inmutable en SceneKit y leí que el enfoque sugerido es hacer una copia manualmente. Estoy tratando de hacer eso, pero siempre termino fallando cuando intento volver a crear el SCNSkinnercódigo en. El choque es un EXC_BAD_ACCESSinterior C3DSourceAccessorGetMutableValuePtrAtIndex. Desafortunadamente, no hay un código fuente para esto, por lo que no estoy seguro de por qué se bloquea exactamente. Lo he reducido al SCNSkinnerobjeto adjunto al nodo de malla. Si no configuro eso, no obtengo un bloqueo y las cosas parecen estar funcionando.

EDITAR: Aquí hay una pila de llamadas más completa del bloqueo:

C3DSourceAccessorGetMutableValuePtrAtIndex
C3DSkinPrepareMeshForGPUIfNeeded
C3DSkinnerMakeCurrentMesh
C3DSkinnerUpdateCurrentMesh
__CFSetApplyFunction_block_invoke
CFBasicHashApply
CFSetApplyFunction
C3DAppleEngineRenderScene
...

No he encontrado ninguna documentación o código de ejemplo sobre cómo crear un SCNSkinnerobjeto manualmente. Como lo estoy creando basándome en una malla que funcionaba anteriormente, no debería ser demasiado difícil. Estoy creando el de SCNSkinneracuerdo con la documentación de Swift, pasando todas las cosas correctas al init. Sin embargo, hay una propiedad de esqueleto en el SCNSkinnerque no estoy seguro de cómo configurar. Lo configuré en el esqueleto que estaba en el original SCNSkinnerde la malla que estoy copiando, que creo que debería funcionar ... pero no funciona. Al configurar la propiedad del esqueleto, no parece estar asignando. Verificarlo inmediatamente después de la asignación muestra que todavía es nulo. Como prueba, intenté establecer la propiedad de esqueleto de la malla original en otra cosa, y después de la asignación tampoco se modificó.

¿Alguien puede arrojar algo de luz sobre lo que está sucediendo? ¿O cómo crear y configurar correctamente un SCNSkinnerobjeto manualmente?

Aquí está el código que estoy usando para clonar manualmente una malla y reemplazarla por una nueva (no he modificado ninguno de los datos de origen aquí; simplemente estoy tratando de asegurarme de poder crear una copia en este momento) :

// This is at the start of the app, just so you can see how the scene is set up.
// I add the .dae contents into its own node in the scene. This seems to be the
// standard way to put multiple .dae models into the same scene. This doesn't seem to
// have any impact on the problem I'm having -- I've tried without this indirection
// and the same problem exists.
let scene = SCNScene()

let modelNode = SCNNode()
modelNode.name = "ModelNode"

scene.rootNode.addChildNode(modelNode)

let modelScene = SCNScene(named: "model.dae")

if modelScene != nil {
    if let childNodes = modelScene?.rootNode.childNodes {
        for childNode in childNodes {
            modelNode.addChildNode(childNode as SCNNode)
        }
    }
}


// This happens later in the app after a tap from the user.

let modelNode = scnView.scene!.rootNode.childNodeWithName("ModelNode", recursively: true)

let modelMesh = modelNode?.childNodeWithName("MeshName", recursively: true)

let verts = modelMesh?.geometry!.geometrySourcesForSemantic(SCNGeometrySourceSemanticVertex)
let normals = modelMesh?.geometry!.geometrySourcesForSemantic(SCNGeometrySourceSemanticNormal)
let texcoords = modelMesh?.geometry!.geometrySourcesForSemantic(SCNGeometrySourceSemanticTexcoord)
let boneWeights = modelMesh?.geometry!.geometrySourcesForSemantic(SCNGeometrySourceSemanticBoneWeights)
let boneIndices = modelMesh?.geometry!.geometrySourcesForSemantic(SCNGeometrySourceSemanticBoneIndices)
let geometry = modelMesh?.geometry!.geometryElementAtIndex(0)

// Note: the vertex and normal data is shared.
let vertsData = NSData(data: verts![0].data)
let texcoordsData = NSData(data: texcoords![0].data)
let boneWeightsData = NSData(data: boneWeights![0].data)
let boneIndicesData = NSData(data: boneIndices![0].data)
let geometryData = NSData(data: geometry!.data!)

let newVerts = SCNGeometrySource(data: vertsData, semantic: SCNGeometrySourceSemanticVertex, vectorCount: verts![0].vectorCount, floatComponents: verts![0].floatComponents, componentsPerVector: verts![0].componentsPerVector, bytesPerComponent: verts![0].bytesPerComponent, dataOffset: verts![0].dataOffset, dataStride: verts![0].dataStride)

let newNormals = SCNGeometrySource(data: vertsData, semantic: SCNGeometrySourceSemanticNormal, vectorCount: normals![0].vectorCount, floatComponents: normals![0].floatComponents, componentsPerVector: normals![0].componentsPerVector, bytesPerComponent: normals![0].bytesPerComponent, dataOffset: normals![0].dataOffset, dataStride: normals![0].dataStride)

let newTexcoords = SCNGeometrySource(data: texcoordsData, semantic: SCNGeometrySourceSemanticTexcoord, vectorCount: texcoords![0].vectorCount, floatComponents: texcoords![0].floatComponents, componentsPerVector: texcoords![0].componentsPerVector, bytesPerComponent: texcoords![0].bytesPerComponent, dataOffset: texcoords![0].dataOffset, dataStride: texcoords![0].dataStride)

let newBoneWeights = SCNGeometrySource(data: boneWeightsData, semantic: SCNGeometrySourceSemanticBoneWeights, vectorCount: boneWeights![0].vectorCount, floatComponents: boneWeights![0].floatComponents, componentsPerVector: boneWeights![0].componentsPerVector, bytesPerComponent: boneWeights![0].bytesPerComponent, dataOffset: boneWeights![0].dataOffset, dataStride: boneWeights![0].dataStride)

let newBoneIndices = SCNGeometrySource(data: boneIndicesData, semantic: SCNGeometrySourceSemanticBoneIndices, vectorCount: boneIndices![0].vectorCount, floatComponents: boneIndices![0].floatComponents, componentsPerVector: boneIndices![0].componentsPerVector, bytesPerComponent: boneIndices![0].bytesPerComponent, dataOffset: boneIndices![0].dataOffset, dataStride: boneIndices![0].dataStride)

let newGeometry = SCNGeometryElement(data: geometryData, primitiveType: geometry!.primitiveType, primitiveCount: geometry!.primitiveCount, bytesPerIndex: geometry!.bytesPerIndex)

let newMeshGeometry = SCNGeometry(sources: [newVerts, newNormals, newTexcoords, newBoneWeights, newBoneIndices], elements: [newGeometry])

newMeshGeometry.firstMaterial = modelMesh?.geometry!.firstMaterial

let newModelMesh = SCNNode(geometry: newMeshGeometry)

let bones = modelMesh?.skinner?.bones
let boneInverseBindTransforms = modelMesh?.skinner?.boneInverseBindTransforms
let skeleton = modelMesh!.skinner!.skeleton!
let baseGeometryBindTransform = modelMesh!.skinner!.baseGeometryBindTransform

newModelMesh.skinner = SCNSkinner(baseGeometry: newMeshGeometry, bones: bones, boneInverseBindTransforms: boneInverseBindTransforms, boneWeights: newBoneWeights, boneIndices: newBoneIndices)

newModelMesh.skinner?.baseGeometryBindTransform = baseGeometryBindTransform

// Before this assignment, newModelMesh.skinner?.skeleton is nil.
newModelMesh.skinner?.skeleton = skeleton
// After, it is still nil... however, skeleton itself is completely valid.

modelMesh?.removeFromParentNode()

newModelMesh.name = "MeshName"

let meshParentNode = modelNode?.childNodeWithName("MeshParentNode", recursively: true)

meshParentNode?.addChildNode(newModelMesh)
jcr
fuente
Mi plan actual para una solución alternativa es simplemente calcular las nuevas coordenadas de textura, sobrescribir el archivo .dae, limpiar la escena y volver a cargar el archivo .dae. Esto debería ser completamente innecesario, pero a partir de la documentación de Apple, no puedo averiguar cuál es el problema. O estoy haciendo algo incorrectamente, omitiendo algún paso crucial, o SceneKit usa algunas API privadas para configurar correctamente SCNSkinner cuando se carga desde el archivo .dae. Es interesante que la propiedad del esqueleto después de que SceneKit se cargue desde el .dae sea un nodo sin hijos , pero la animación del esqueleto claramente funciona.
jcr
Lamentablemente, sobrescribir el archivo .dae no es una opción. Hay un paso de optimización y compresión que ocurre cuando Xcode copia su archivo .dae al dispositivo. Guardar un nuevo archivo .dae en el dispositivo dentro de la aplicación no funciona porque SceneKit en iOS no cargará un archivo .dae que no se haya ejecutado a través del script de Xcode.
jcr
¿Conseguiste este trabajo?
μολὼν.λαβέ
No, terminé escribiendo mi propio motor y no he usado SceneKit desde entonces.
jcr
Impresionante. ¿Fuiste con opengl o metal?
μολὼν.λαβέ

Respuestas:

1

Estos tres métodos pueden ayudarlo a encontrar la solución:

  1. SCNNode *hero = [SCNScene sceneNamed:@"Hero"].rootNode;
    SCNNode *hat = [SCNScene sceneNamed:@"FancyFedora"].rootNode;
    hat.skinner.skeleton = hero.skinner.skeleton;
  2. [Export ("initWithFrame:")]
    public UIView (System.Drawing.RectangleF frame) : base (NSObjectFlag.Empty)
    {
    // Invoke the init method now.
        var initWithFrame = new Selector ("initWithFrame:").Handle;
        if (IsDirectBinding)
            Handle = ObjCRuntime.Messaging.IntPtr_objc_msgSend_RectangleF (this.Handle, initWithFrame, frame);
        else
            Handle = ObjCRuntime.Messaging.IntPtr_objc_msgSendSuper_RectangleF (this.SuperHandle, initWithFrame, frame);
    }
  3. Vea este enlace también.

Kartik Raja S
fuente
0

No sé específicamente qué causa que su código se bloquee, pero aquí hay una forma de generar una malla, huesos y pelar esa malla, todo desde el código. Swift4 y iOS 12.

En el ejemplo, hay una malla que representa la concatenación de dos cilindros, con uno de los cilindros ramificándose en un ángulo de 45 grados, así:

 \
 |

Los cilindros son solo triángulos extruidos, es decir, radialSegmentCount = 3.(tenga en cuenta que hay 12 vértices, no 9, ya que los dos cilindros no están realmente unidos. Los triángulos están ordenados así:

      v5
      ^
v3 /__|__\ v1
   |  |  |
   |  v4 |
v2 |/___\| v0

Hay 3 huesos, correspondientes a las cabezas y pies de los cilindros, donde el hueso del medio corresponde a la cabeza del cilindro inferior y simultáneamente al pie del cilindro superior. Así, por ejemplo, los vértices v0, v2y v4corresponden a bone0; v1, v3, v5Corresponden a bone1, y así sucesivamente. Eso explica por qué boneIndices(ver más abajo) tiene el valor que tiene.

Las posiciones de reposo de los huesos corresponden a las posiciones de reposo de los cilindros en la geometría ( bone2brota en un ángulo de 45 grados bone1, al igual que la geometría del cilindro).

Con eso como contexto, el siguiente código crea todo lo necesario para revestir la geometría:

let vertices = [float3(0.17841241, 0.0, 0.0), float3(0.17841241, 1.0, 0.0), float3(-0.089206174, 0.0, 0.1545097), float3(-0.089206174, 1.0, 0.1545097), float3(-0.089206256, 0.0, -0.15450965), float3(-0.089206256, 1.0, -0.15450965), float3(0.12615661, 1.1261566, 0.0), float3(-0.58094996, 1.8332633, 0.0), float3(-0.063078284, 0.9369217, 0.1545097), float3(-0.7701849, 1.6440284, 0.1545097), float3(-0.063078344, 0.93692166, -0.15450965), float3(-0.77018493, 1.6440284, -0.15450965)]
let indices: [UInt8] = [0, 1, 2, 3, 4, 5, 0, 1, 1, 6, 6, 7, 8, 9, 10, 11, 6, 7]
let geometrySource = SCNGeometrySource(vertices: vertices.map { SCNVector3($0) })
let geometryElement = SCNGeometryElement(indices: indices, primitiveType: .triangleStrip)
let geometry = SCNGeometry(sources: [geometrySource], elements: [geometryElement])

let bone0 = SCNNode()
bone0.simdPosition = float3(0,0,0)
let bone1 = SCNNode()
bone1.simdPosition = float3(0,1,0)
let bone2 = SCNNode()
bone2.simdPosition = float3(0,1,0) + normalize(float3(-1,1,0))
let bones = [bone0, bone1, bone2]

let boneInverseBindTransforms: [NSValue]? = bones.map { NSValue(scnMatrix4: SCNMatrix4Invert($0.transform)) }
var boneWeights: [Float] = vertices.map { _ in 1.0 }
var boneIndices: [UInt8] = [
    0, 1, 0, 1, 0, 1,
    1, 2, 1, 2, 1, 2,
]

let boneWeightsData = Data(bytesNoCopy: &boneWeights, count: boneWeights.count * MemoryLayout<Float>.size, deallocator: .none)
let boneIndicesData = Data(bytesNoCopy: &boneIndices, count: boneWeights.count * MemoryLayout<UInt8>.size, deallocator: .none)

let boneWeightsGeometrySource = SCNGeometrySource(data: boneWeightsData, semantic: .boneWeights, vectorCount: boneWeights.count, usesFloatComponents: true, componentsPerVector: 1, bytesPerComponent: MemoryLayout<Float>.size, dataOffset: 0, dataStride: MemoryLayout<Float>.size)
let boneIndicesGeometrySource = SCNGeometrySource(data: boneIndicesData, semantic: .boneIndices, vectorCount: boneIndices.count, usesFloatComponents: false, componentsPerVector: 1, bytesPerComponent: MemoryLayout<UInt8>.size, dataOffset: 0, dataStride: MemoryLayout<UInt8>.size)

let skinner = SCNSkinner(baseGeometry: geometry, bones: bones, boneInverseBindTransforms: boneInverseBindTransforms, boneWeights: boneWeightsGeometrySource, boneIndices: boneIndicesGeometrySource)

let node = SCNNode(geometry: geometry)
node.skinner = skinner

Nota: en la mayoría de los casos, debe utilizar UInt16 no UInt8.

Nick Kallen
fuente