SceneKit - render surface normal of a geometry

105 views Asked by At

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?

1

There are 1 answers

7
ZAY On BEST ANSWER

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.