Search code examples
iosscenekitarkitmetal

Using SCNGeometryTessellator with a SCNGeometry that uses a custom SCNProgram?


I'm trying to add a SCNGeometryTessellator to some SceneKit geometry that uses a custom SCNProgram. My geometry renders fine normally, but as soon as I add the SCNGeometryTessellator, I see this error:

[SceneKit] Error: Compiler error while building render pipeline state for node <C3DNode:0x1053e9700 "(null)"
  geometry: <C3DParametricGeometry<Plane>:0x1053e9160 "(null)"
  mesh: <C3DMesh 0x282590620 "(null)"
  element0: <C3DMeshElement 0x2823922b0 type:triangles primCount:200 channels:1 indexBytes:2 offset:0 acmr:0.605000 inst:1 dataSize:1200 shared:0x0>
  source position (channel:0) : <C3DMeshSource 0x2837fbbf0(position) data:(0x281009ce0) mut:0 count:121 type:float3 divisor:0 mtl:0 offset:0 stride:32>
  source normal (channel:0) : <C3DMeshSource 0x2837fb480(normal) data:(0x281009ce0) mut:0 count:121 type:float3 divisor:0 mtl:0 offset:12 stride:32>
  source texcoord (channel:0) : <C3DMeshSource 0x2837fbb80(texcoord) data:(0x281009ce0) mut:0 count:121 type:float2 divisor:0 mtl:0 offset:24 stride:32>
  renderable element0: <C3DMeshElement 0x2823922b0 type:triangles primCount:200 channels:1 indexBytes:2 offset:0 acmr:0.605000 inst:1 dataSize:1200 shared:0x0>
  renderable source position: <C3DMeshSource 0x2837fbbf0(position) data:(0x281009ce0) mut:0 count:121 type:float3 divisor:0 mtl:0 offset:0 stride:32>
  renderable source normal: <C3DMeshSource 0x2837fb480(normal) data:(0x281009ce0) mut:0 count:121 type:float3 divisor:0 mtl:0 offset:12 stride:32>
  renderable source texcoord: <C3DMeshSource 0x2837fbb80(texcoord) data:(0x281009ce0) mut:0 count:121 type:float2 divisor:0 mtl:0 offset:24 stride:32>
>
  mat0: <C3DMaterial 0x2837159d0 : "(null)", custom <C3DFXTechnique>>
>
>:
Error Domain=AGXMetalA14 Code=3 "Attribute 0 incompatible with MTLStepFunctionPerPatchControlPoint." UserInfo={NSLocalizedDescription=Attribute 0 incompatible with MTLStepFunctionPerPatchControlPoint.}
[SceneKit] Error: _executeProgram - no pipeline state

Here's how I create my geometry:

let program = SCNProgram()
program.vertexFunctionName = "myVertexShader"
program.fragmentFunctionName = "myFragmentShader"

let mat = SCNMaterial()
mat.program = previewProgram

let plane = SCNPlane()
plane.widthSegmentCount = 1
plane.heightSegmentCount = 1
plane.firstMaterial = mat

let tessellator = SCNGeometryTessellator()
tessellator.edgeTessellationFactor = 10.0
tessellator.insideTessellationFactor = 10.0
tessellator.smoothingMode = .pnTriangles
plane.tessellator = tessellator

// add plane node to scene...

The tesselator actually works if I remove the custom material, but I need to use a SCNProgram.

What is causing this error and how can I use SCNGeometryTessellator?


Solution

  • Once you add a tessellator an SCNGeometry, your SCNProgram needs to use a post-tessellation vertex function instead of a standard vertex function. The only doc I've found that really covers this is Apple's metal tessellation programming guide

    While I'm not an expect on Metal tessellation, here's my understanding of the issue:

    Let's say your normal vertex function looks something like this:

    struct VertexInput {
        float3 position [[attribute(SCNVertexSemanticPosition)]];
        float3 normal [[attribute(SCNVertexSemanticNormal)]];
        float2 texCoords [[attribute(SCNVertexSemanticTexcoord0)]];
    };
    
    vertex ColorInOut myVertexShader(VertexInput in [[ stage_in ]], ...) {
        ...
    }
    

    The error about Attribute 0 incompatible with MTLStepFunctionPerPatchControlPoint is explaining that your input type (VertexInput) does not match the expected vertex input for the current render pipeline. This is because once you add tessellation, the render pipeline changes to use a special post tessellation vertex function instead of a standard vertex function.

    A post tessellation vertex function takes input from the tessellator instead of from your geometry. Here's the above vertex shader converted to a post tessellation vertex function:

    struct PatchIn {
        patch_control_point<VertexInput> control_points;
    };
    
    [[patch(triangle, 3)]]
    vertex ColorInOut myVertexShader(PatchIn patchIn [[stage_in]],
                                     float3 patch_coord [[ position_in_patch ]], ...)
    {
        // We have to compute the correct input positions from the tessellation data 
        // Note that there's probably a cleaner/better way to do this
    
        // Barycentric coordinates
        float u = patch_coord.x;
        float v = patch_coord.y;
        float w = patch_coord.z;
        
        // Convert to cartesian coordinates
        const float3 pos = float3(
                                  u * patchIn.control_points[0].position.x + v * patchIn.control_points[1].position.x + w * patchIn.control_points[2].position.x,
                                  u * patchIn.control_points[0].position.y + v * patchIn.control_points[1].position.y + w * patchIn.control_points[2].position.y,
                                  u * patchIn.control_points[0].position.z + v * patchIn.control_points[1].position.z + w * patchIn.control_points[2].position.z);
        
        ...
    }
    

    After rewriting the vertex function to a post tessellation vertex function, your geometry should render with tessellation and your custom SCNProgram