Como criar um objeto SceneKit SCNSkinner no código?

89

Eu tenho um aplicativo Swift usando SceneKit para iOS 8. Carrego uma cena de um arquivo .dae que contém uma malha controlada por um esqueleto. Em tempo de execução, preciso modificar as coordenadas de textura. Usar uma transformação não é uma opção - eu preciso calcular um UV diferente e completamente novo para cada vértice da malha.

Eu sei que a geometria é imutável no SceneKit, e li que a abordagem sugerida é fazer uma cópia manualmente. Estou tentando fazer isso, mas sempre acabo travando ao tentar recriar o SCNSkinnercódigo. O acidente é um EXC_BAD_ACCESSinterior C3DSourceAccessorGetMutableValuePtrAtIndex. Infelizmente, não há código-fonte para isso, então não sei exatamente por que está falhando. Eu o reduzi ao SCNSkinnerobjeto anexado ao nó da malha. Se eu não definir isso, não consigo travar e as coisas parecem estar funcionando.

EDIT: Aqui está uma pilha de chamadas mais completa da falha:

C3DSourceAccessorGetMutableValuePtrAtIndex
C3DSkinPrepareMeshForGPUIfNeeded
C3DSkinnerMakeCurrentMesh
C3DSkinnerUpdateCurrentMesh
__CFSetApplyFunction_block_invoke
CFBasicHashApply
CFSetApplyFunction
C3DAppleEngineRenderScene
...

Não encontrei nenhuma documentação ou código de exemplo sobre como criar um SCNSkinnerobjeto manualmente. Já que estou apenas criando com base em uma malha de trabalho anterior, não deve ser muito difícil. Estou criando o de SCNSkinneracordo com a documentação do Swift, passando todas as coisas corretas para o init. No entanto, há uma propriedade de esqueleto na SCNSkinnerque não tenho certeza de como definir. Eu o configurei para o esqueleto que estava no original SCNSkinnerda malha que estou copiando, que acho que deveria funcionar ... mas não funciona. Ao definir a propriedade esqueleto, ela não parece estar sendo atribuída. Verificar imediatamente após a atribuição mostra que ainda é nulo. Como teste, tentei definir a propriedade de esqueleto da malha original para outra coisa e, após a atribuição, ela também foi deixada intacta.

Alguém pode lançar alguma luz sobre o que está acontecendo? Ou como criar e configurar corretamente um SCNSkinnerobjeto manualmente?

Aqui está o código que estou usando para clonar manualmente uma malha e substituí-la por uma nova (não modifiquei nenhum dos dados de origem aqui - estou simplesmente tentando ter certeza de que posso criar uma cópia neste ponto) :

// 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
fonte
Meu plano atual para uma solução alternativa é simplesmente calcular as novas coordenadas de textura, sobrescrever o arquivo .dae, liberar a cena e recarregar o arquivo .dae. Isso deveria ser completamente desnecessário, mas pela documentação da Apple, não consigo descobrir qual é o problema. Estou fazendo algo incorretamente, deixando de fora alguma etapa crucial, ou o SceneKit usa algumas APIs privadas para configurar corretamente o SCNSkinner ao carregar do arquivo .dae. É interessante que a propriedade do esqueleto após o carregamento do SceneKit do .dae é um nó sem filhos , mas a animação do esqueleto funciona claramente.
jcr
Substituir o arquivo .dae não é uma opção, infelizmente. Há uma etapa de otimização e compactação que ocorre quando o Xcode copia seu arquivo .dae para o dispositivo. Salvar um novo arquivo .dae no dispositivo dentro do aplicativo não funciona porque o SceneKit no iOS não carregará um arquivo .dae que não foi executado por meio do script do Xcode.
jcr
Você conseguiu este trabalho?
μολὼν.λαβέ
Não, acabei escrevendo meu próprio motor e não usei o SceneKit desde então.
JCR
Impressionante. Você escolheu opengl ou metal?
μολὼν.λαβέ

Respostas:

1

Esses três métodos podem ajudá-lo a encontrar a solução:

  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. Veja este link também.

Kartik Raja S
fonte
0

Eu não sei especificamente o que causa o travamento do seu código, mas aqui está uma maneira de gerar uma malha, ossos e esfolar essa malha - tudo a partir do código. Swift4 e iOS 12.

No exemplo, há uma malha que representa a concatenação de dois cilindros, com um dos cilindros se ramificando em um ângulo de 45 graus, assim:

 \
 |

Os cilindros são apenas triângulos extrudados, ou seja, radialSegmentCount = 3. (Observe que existem 12 vértices, não 9, já que os dois cilindros não são realmente unidos. Os triângulos são ordenados assim:

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

Existem 3 ossos, correspondendo às cabeças e pés dos cilindros, onde o osso do meio corresponde à cabeça do cilindro inferior e simultaneamente ao pé do cilindro superior. Assim, por exemplo, os vértices v0, v2e v4correspondem a bone0; v1,v3 , v5Corresponder a bone1, e assim por diante. Isso explica porque boneIndices(veja abaixo) tem o valor que tem.

As posições de repouso dos ossos correspondem às posições de repouso dos cilindros na geometria ( bone2brota em um ângulo de 45 graus bone1, assim como a geometria do cilindro).

Com isso como contexto, o código a seguir cria tudo o que é necessário para revestir a geometria:

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

Observação: na maioria dos casos, você UInt16não deve usar UInt8.

Nick Kallen
fonte