In Xcode SceneKit editor, its possible to visualise surface normals of a SCNGeoemtry
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?
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!)
Hope this will help in some way.