xcodescenekitscnnodescngeometry

SceneKit - render surface normal of a geometry


In Xcode SceneKit editor, its possible to visualise surface normals of a SCNGeoemtry

enter image description here

It seems that the editor uses SCNGeometrySource.Semantic to render the image: https://developer.apple.com/documentation/scenekit/scngeometrysource/semantic

How can I render the surface normals of a geometry to an image?


Solution

  • Let's give it a try. You can achieve a normal or normal like shading of your geometry by using a SCNProgram() for rendering. This replaces the entire rendering which is provided by Apple (like constant, phong, blinn, or physicallyBased)

    Start using for example with the default SceneKit Template that comes along with Xcode, the one that contains the rotating spaceship.

    First, create a Metal-File, and call it shaders.metal.

    Variation 1 (first Image below): Copy the following code into this file. (this code here will generate a World Space Normal Map)

    #include <metal_stdlib>
    using namespace metal;
    #include <SceneKit/scn_metal>
    
    // Nodebuffer
    struct MyNodeBuffer {
        // float4x4 modelTransform;
        // float4x4 inverseModelTransform;
        float4x4 modelViewTransform; // required
        // float4x4 inverseModelViewTransform;
        float4x4 normalTransform; // required
        // float4x4 modelViewProjectionTransform;
        // float4x4 inverseModelViewProjectionTransform;
    };
    
    // as is from StackOverflow
    typedef struct {
        float3 position  [[ attribute(SCNVertexSemanticPosition)  ]];
        float3 normal    [[ attribute(SCNVertexSemanticNormal)    ]];
        float4 color     [[ attribute(SCNVertexSemanticColor)     ]];
        float2 texCoords [[ attribute(SCNVertexSemanticTexcoord0) ]];
    } MyVertexInput;
    
    // MARK: - Structs filled by Vertex Shader
    struct SimpleVertexNormal
    {
        float4 position [[ position ]];
        float3 normal;
    };
    
    // MARK: - Normal Shader
    vertex SimpleVertexNormal myVertexNormal(MyVertexInput in [[stage_in]],
                                       constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                                       constant MyNodeBuffer& scn_node [[buffer(1)]])
    {
        float4 modelSpacePosition(in.position, 1.0f);
        float4 eyeSpacePosition(scn_node.modelViewTransform * modelSpacePosition);
        
        SimpleVertexNormal out;
        
        out.position                    = scn_frame.projectionTransform * eyeSpacePosition;
        out.normal                      = in.normal;
        
        return out;
    }
    
    fragment float4 myFragmentNormal(SimpleVertexNormal in [[stage_in]])
    
    {
        // Normal Color
        float3 normal           = normalize(in.normal);
        float3 normalColor      = float3(abs(normal));
        
        // Final Color
        float4 color            = float4(normalColor, 1.0);
        return color;
    }
    

    Variation 2 (second image below): Copy the following code into this file. (this code here will generate a Tangent Space Normal Map)

    #include <metal_stdlib>
    using namespace metal;
    #include <SceneKit/scn_metal>
    
    // Nodebuffer
    struct MyNodeBuffer {
        float4x4 modelViewTransform;
        float4x4 normalTransform;
    };
    
    // Vertex Input
    typedef struct {
        float3 position  [[ attribute(SCNVertexSemanticPosition)  ]];
        float3 normal    [[ attribute(SCNVertexSemanticNormal)    ]];
        float4 color     [[ attribute(SCNVertexSemanticColor)     ]];
        float2 texCoords [[ attribute(SCNVertexSemanticTexcoord0) ]];
    } MyVertexInput;
    
    // Vertex Output
    struct SimpleVertexNormal {
        float4 position [[ position ]];
        float3 normal;
        float3 tangent;
        float3 bitangent;
    };
    
    // Vertex Shader TANGENT SPACE NORMAL MAP
    vertex SimpleVertexNormal myVertexNormal(MyVertexInput in [[stage_in]],
                                            constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                                            constant MyNodeBuffer& scn_node [[buffer(1)]])
    {
        SimpleVertexNormal out;
    
        // Transform position to eye space
        float4 modelSpacePosition = float4(in.position, 1.0);
        float4 eyeSpacePosition = scn_node.modelViewTransform * modelSpacePosition;
    
        // Transform normal to world space
        float3 worldSpaceNormal = normalize((scn_node.normalTransform * float4(in.normal, 0.0)).xyz);
    
        // Use arbitrary vectors to generate the tangent and bitangent
        float3 arbitraryVector1 = float3(1, 0, 0);
    
        // Calculate tangent
        out.tangent = normalize(arbitraryVector1 - dot(arbitraryVector1, worldSpaceNormal) * worldSpaceNormal);
    
        // Calculate bitangent
        out.bitangent = cross(worldSpaceNormal, out.tangent);
    
        // Ensure that tangent and bitangent are orthogonal to the normal
        out.bitangent = normalize(out.bitangent - dot(out.bitangent, worldSpaceNormal) * worldSpaceNormal);
        out.tangent = cross(worldSpaceNormal, out.bitangent);
    
        // Transform normal, tangent, and bitangent to world space
        out.normal = normalize((scn_node.normalTransform * float4(in.normal, 0.0)).xyz);
        out.tangent = normalize((scn_node.normalTransform * float4(out.tangent, 0.0)).xyz);
        out.bitangent = normalize((scn_node.normalTransform * float4(out.bitangent, 0.0)).xyz);
    
        // Transform position to clip space
        out.position = scn_frame.projectionTransform * eyeSpacePosition;
    
        return out;
    }
    
    // Fragment Shader
    fragment float4 myFragmentNormal(SimpleVertexNormal in [[stage_in]])
    {
        float3 normalColor = 0.5 * (in.normal + 1.0); // Convert to [0, 1] range
        return float4(normalColor, 1.0);
    }
    

    In the the GameViewController insert the following function, that will attach this shader to a given Node (you can also define the SCNProgram globally and re-use it).

    func applyNormalShader(node: SCNNode) {
        let sceneProgramNormal = SCNProgram()      // Metal Shader Program
        sceneProgramNormal.fragmentFunctionName    = "myFragmentNormal"
        sceneProgramNormal.vertexFunctionName      = "myVertexNormal"
        
        node.geometry?.firstMaterial?.program = sceneProgramNormal
    }
    

    the last step is filing a Node to that shader. I'll use the default Spaceship here. (Make sure to file the exact node containing the geometry you want to shade with that custom shader program.)

    Modify Code like this: (here: in viewDidLoad)

    // retrieve the ship node
    let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
    
    // animate the 3d object
    ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
    
    // apply shader
    applyNormalShader(node: ship.childNodes.first!)
    

    World Space Normal Mapping

    Tangent Space Normal Mapping

    Hope this will help in some way.