dthgs
dthgs

Reputation: 81

Custom SceneKit character won't clone correctly

I created a fully custom rigged SCNNode programmatically. This node is complete with custom geometry, a skeletal rig as a hierarchy of child nodes and an SCNSkinner object to bind skeleton and geometry together. The problem is this: As soon as I clone my custom node, it looses the binding between geometry and skeleton.

To get to the root of this problem, I swapped my rather complex character node with the simplest geometry-rig combination I could think of: A geometry block of 12 vertices and a skeletal rig of 3 joints.

I'd have loved to share an image at this point, but can't do this as I don't have enough reputation, yet...

-(SCNNode *)createCustomRigBlock {

    // baseGeometry
    SCNVector3 positions[] = {
        SCNVector3Make(0, 0, 0),
        SCNVector3Make(0, 0, 1),
        SCNVector3Make(1, 0, 1),
        SCNVector3Make(1, 0, 0),
        SCNVector3Make(0, 1, 0),
        SCNVector3Make(0, 1, 1),
        SCNVector3Make(1, 1, 1),
        SCNVector3Make(1, 1, 0),
        SCNVector3Make(0, 2, 0),
        SCNVector3Make(0, 2, 1),
        SCNVector3Make(1, 2, 1),
        SCNVector3Make(1, 2, 0)
    };
    SCNGeometrySource * baseGeometrySource = [SCNGeometrySource geometrySourceWithVertices:positions count:12];

    typedef struct {
        uint16_t a, b, c;
    } Triangles;

    Triangles tVectors[20] = {
        0,1,2,
        0,2,3,
        0,1,5,
        0,4,5,
        4,5,9,
        4,8,9,
        1,2,6,
        1,5,6,
        5,6,10,
        5,9,10,
        2,3,7,
        2,6,7,
        6,7,11,
        6,10,11,
        3,0,4,
        3,4,7,
        7,4,8,
        7,8,11,
        8,9,10,
        8,10,11
    };

    NSData *triangleData = [NSData dataWithBytes:tVectors length:sizeof(tVectors)];
    SCNGeometryElement * baseGeometryElement = [SCNGeometryElement geometryElementWithData:triangleData primitiveType:SCNGeometryPrimitiveTypeTriangles primitiveCount:20 bytesPerIndex:sizeof(uint16_t)];
    SCNGeometry * baseGeometry = [SCNGeometry geometryWithSources:[NSArray arrayWithObject:baseGeometrySource] elements:[NSArray arrayWithObject:baseGeometryElement]];

    baseGeometry.firstMaterial.emission.contents = [UIColor greenColor];
    baseGeometry.firstMaterial.doubleSided = YES;
    baseGeometry.firstMaterial.transparency = 0.5;
    SCNNode *customBlock = [SCNNode nodeWithGeometry:baseGeometry];

    int stageSize = 30;
    customBlock.position = SCNVector3Make(stageSize/2, 0, stageSize/2-6);

    int vectorCount = (int)[(SCNGeometrySource *)[customBlock.geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticVertex].firstObject vectorCount];

    //bones ... the bones of the rig
    NSMutableArray * bonesArray = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        SCNNode * boneNode = [SCNNode new];
        boneNode.name = [NSString stringWithFormat:@"bone_%i",i];
        if (bonesArray.count > 0) {
            [bonesArray.lastObject addChildNode:boneNode];
        }
        boneNode.position = SCNVector3Make((i>0 ? 0 : 0.5), (i>0 ? 0.75 : 0.25), (i>0 ? 0 : 0.5));

        //add a sphere to each bone, to visually check its position etc.
        SCNSphere *boneSphereGeom = [SCNSphere sphereWithRadius:0.1];
        boneSphereGeom.firstMaterial.emission.contents = [UIColor redColor];
        SCNNode * boneSphere = [SCNNode nodeWithGeometry:boneSphereGeom];
        [boneNode addChildNode:boneSphere];

        [bonesArray addObject:boneNode];
    }

    [customBlock addChildNode:bonesArray.firstObject];

    //boneInverseBindTransforms  ... this defines the geometries transformation in the default pose!
    NSMutableArray * bibtArray = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        SCNMatrix4 initialPositionMatrix = SCNMatrix4MakeTranslation(0.5, (i*0.75)+0.25, 0.5);
        SCNMatrix4 inverseFinalMatrix = SCNMatrix4Invert(initialPositionMatrix);
        NSValue * bibtValue = [NSValue valueWithSCNMatrix4:inverseFinalMatrix];
        [bibtArray addObject:bibtValue];
    }

    //boneWeights ... the weights, at which each vertex is influenced by certain bones (which bones is defined by "boneIndices")
    typedef struct {
        float a, b, c;
    } WeightVectors;

    WeightVectors vectors[vectorCount];
    for (int i = 0; i < vectorCount; i++) {
        // set the same boneWeights for every vertex
        vectors[i].a = 1;
        vectors[i].b = 0;
        vectors[i].c = 0;
    }

    NSData *weightData = [NSData dataWithBytes:vectors length:sizeof(vectors)];
    SCNGeometrySource * boneWeightsGeometrySource = [SCNGeometrySource geometrySourceWithData:weightData
                                                                                     semantic:SCNGeometrySourceSemanticBoneWeights
                                                                                  vectorCount:vectorCount
                                                                              floatComponents:YES
                                                                          componentsPerVector:3
                                                                            bytesPerComponent:sizeof(float)
                                                                                   dataOffset:offsetof(WeightVectors, a)
                                                                                   dataStride:sizeof(WeightVectors)];

    //boneIndices
    typedef struct {
        short k, l, m;    // boneWeight
    } IndexVectors;

    IndexVectors iVectors[vectorCount];
    for (int i = 0; i < vectorCount; i++) {
        if (i > 7) {
            iVectors[i].k = 1;
            iVectors[i].l = 0;
            iVectors[i].m = 0;
        } else {
            iVectors[i].k = 0;
            iVectors[i].l = 0;
            iVectors[i].m = 0;
        }
    }

    NSData *indexData = [NSData dataWithBytes:iVectors length:sizeof(iVectors)];
    SCNGeometrySource * boneIndicesGeometrySource = [SCNGeometrySource geometrySourceWithData:indexData
                                                                                     semantic:SCNGeometrySourceSemanticBoneIndices
                                                                                  vectorCount:vectorCount
                                                                              floatComponents:NO
                                                                          componentsPerVector:3
                                                                            bytesPerComponent:sizeof(short)
                                                                                   dataOffset:offsetof(IndexVectors, k)
                                                                                   dataStride:sizeof(IndexVectors)];

    SCNSkinner * customBlockSkinner = [SCNSkinner skinnerWithBaseGeometry:baseGeometry
                                                                    bones:bonesArray
                                                boneInverseBindTransforms:bibtArray
                                                              boneWeights:boneWeightsGeometrySource
                                                              boneIndices:boneIndicesGeometrySource];

    customBlock.skinner = customBlockSkinner;
    [[bonesArray objectAtIndex:1] runAction:[SCNAction repeatActionForever:[SCNAction rotateByX:0 y:0 z:2 duration:2]]];

    return customBlock;
}

This custom SCNNode works perfectly, as long as I add it to my scene directly. Now, in my current application I need to add this node to the scene several times. That's why I need to clone the node and add the resulting clone to the scene instead. But something is very wrong with the result of cloning the custom node. As I mentioned above, the binding between geometry and skeleton appears to be lost. The skeleton in the cloned node can still be positioned in my scene, but the geometry doesn't follow the skeletons position or transformation anymore.

What I tried already:

1.) I created a fresh node and added a copy of the original geometry to it. Then I cloned the skeleton root node and added this to the fresh node as well. And finally I set the originals skinner as the fresh nodes skinner. Unfortunately to no avail. The result behaves exactly the same as a directly cloned node.

SCNNode * newNode = [SCNNode node];
newNode.geometry = [node.geometry copy];
for (SCNNode * childNode in node.childNodes) {
    [newNode addChildNode:[childNode clone]];
}
newNode.skinner = node.skinner;

2.) In the attempt to recreate the binding between the geometry and the skeleton I created a deep copy of the SCNSkinner and made it the skinner of the fresh node. But this only caused a crash in C3DSourceAccessorGetMutableValuePtrAtIndex, which I couldn't fix.

NSArray * newBones = [newNode childNodesPassingTest:^(SCNNode *child, BOOL *stop)
                      {
                          if (![child.name isEqualToString:@"manipulator"] ) {
                              return YES;
                          }
                          return NO;
                      }];
NSArray * newBoneInverseBindTransforms = [node.skinner.boneInverseBindTransforms copy];
SCNGeometrySource * newBoneWeights = [SCNGeometrySource geometrySourceWithData:[node.skinner.boneWeights.data copy]
                                                                      semantic:SCNGeometrySourceSemanticBoneWeights
                                                                   vectorCount:node.skinner.boneWeights.vectorCount
                                                               floatComponents:node.skinner.boneWeights.floatComponents
                                                           componentsPerVector:node.skinner.boneWeights.componentsPerVector
                                                             bytesPerComponent:node.skinner.boneWeights.bytesPerComponent
                                                                    dataOffset:node.skinner.boneWeights.dataOffset
                                                                    dataStride:node.skinner.boneWeights.dataStride];

SCNGeometrySource * newBoneIndices = [SCNGeometrySource geometrySourceWithData:[node.skinner.boneIndices.data copy]
                                                                      semantic:SCNGeometrySourceSemanticBoneIndices
                                                                   vectorCount:node.skinner.boneIndices.vectorCount
                                                               floatComponents:node.skinner.boneIndices.floatComponents
                                                           componentsPerVector:node.skinner.boneIndices.componentsPerVector
                                                             bytesPerComponent:node.skinner.boneIndices.bytesPerComponent
                                                                    dataOffset:node.skinner.boneIndices.dataOffset
                                                                    dataStride:node.skinner.boneIndices.dataStride];

SCNSkinner * newSkinner = [SCNSkinner skinnerWithBaseGeometry:newNode.geometry
                                                bones:newBones
                            boneInverseBindTransforms:newBoneInverseBindTransforms
                                          boneWeights:newBoneWeights
                                          boneIndices:newBoneIndices];
newNode.skinner = newSkinner;

Upvotes: 1

Views: 1166

Answers (1)

Alec Firtulescu
Alec Firtulescu

Reputation: 553

The answer is in apple SCNNode documentation. Try using flattenedClone() method instead of clone(). And one more thing, it's not needed any for statement. Here is a little example in Swift:

nodeB = nodeA.flattenedClone()

Now, nodeB contains all children nodes of nodeA

Upvotes: 0

Related Questions