Summary
I found the solution. At the end it was my mistake. SpriteKit is definitely automatically falling back to openGL. The GLES shader language is less forgiving than Metal. This is where the problem came from. In openGL-shaders you MUST set the decimal point in each number. Unfortunately the shader compiler did't told me that after compiling. Another issue was that sometimes old shader builds stick with the bundle. Perform "clean" before testing a shader.
So this is how to deal with both kinds of shaders and detecting Metal / openGL:
Detect if Metal is available
This small helper can be placed anywhere in your code. It helps you to detect Metal at the first usage and gives you the opportunity to execute custom code depending on the configuration once.
Headers:
#import SpriteKit
#import Metal
Code:
/**
Detect Metal. Returns true if the device supports metal.
*/
var metalAvailable:Bool {
get {
struct Static {
static var metalAvailable : Bool = false
static var metalNeedsToBeTested : Bool = true
}
if Static.metalNeedsToBeTested {
Static.metalNeedsToBeTested = false
let device = MTLCreateSystemDefaultDevice()
Static.metalAvailable = (device != nil)
if Static.metalAvailable {
// Do sth. to init Metal code, if needed
} else {
// Do sth. to init openGL code, if needed
}
}
return Static.metalAvailable
}
}
Create shader in Sprite Kit
Create the shader as usual using sprite kit.
let shaderContainer = SKSpriteNode()
shaderContainer.position = CGPoint(x:self.frame.size.width/2, y:self.frame.size.height/2)
shaderContainer.size = CGSize(width:self.frame.size.width, height:self.frame.size.height)
self.backgroundNode.addChild(shaderContainer)
let bgShader:SKShader
// Test if metal is available
if self.metalAvailable {
bgShader = SKShader(fileNamed:"plasma.fsh")
} else {
NSLog("Falling back to openGL")
bgShader = SKShader(fileNamed:"plasmaGL.fsh")
}
// Add your uniforms. OpenGL needs the size of the frame to normalize
// The coordinates. This is why we always use the size uniform
bgShader.uniforms = [
SKUniform(name: "size", floatVector2:GLKVector2Make(1920.0, 1024.0))
]
shaderContainer.shader = bgShader
As you can see depending on the detected configuration another shader file is being loaded. The openGL shaders need an additional uniform for the size, because the symbol v_tex_coord is not available in openGL. If you don't use the size uniform in Metal, you can move the uniforms statement into the if block or just ignore it. Metal is not complaining if you don't use it.
Metal shader: plasma.fsh
#define M_PI 3.1415926535897932384626433832795
#define frequency 1 // Metal is less sensitive to number types.
#define colorDepth 2 // Numbers without decimal point make problems with openGL
void main(void) {
vec2 uv = v_tex_coord; // Normalized coordinates in Metal shaders
float red = ((sin((uv.x + u_time * 0.01) * M_PI * frequency) * cos((uv.y + u_time * 0.03) * M_PI * frequency) + 1) / colorDepth) + (colorDepth / 2.75) - (2 / 2.75);
gl_FragColor = vec4(red, uv.x, u_time, 1.0);
}
In Metal shaders you can simply read normalized coordinates. You can use the size to reconstruct the image coordinates if you like. However Metal is more forgiving with decimal points. As you can see, some numbers don't have decimal points here.
Open GL shader: plasmaGL.fsh
// OPEN GL shaders NEED the decimal point in numbers. so never use 1 but 1. or 1.0
#define M_PI 3.1415926535897932384626433832795
#define frequency 1.0 // This number must have a decimal point
#define colorDepth 2.0 // Same here.
void main(void) {
vec2 uv = gl_FragCoord.xy / size.xy; // Frame coordinates in openGL
// This formula is always using numbers with decimal points.
// Compare it to the metal shader. Two numbers of the metal
// have no decimal point. If you cut copy paste the metal shader
// formula to the GL shader it will not work!
float red = ((sin((uv.x + u_time * 0.01) * M_PI * frequency) * cos((uv.y + u_time * 0.03) * M_PI * frequency) + 1.0) / colorDepth) + (colorDepth / 2.75) - (2.0 / 2.75);
gl_FragColor = vec4(red, uv.x, u_time, 1.0);
}
Outlook
It is more work to test for both systems and creating two shaders. But as long as we are transitioning from GL to Metal this is a good method to test which kind of shader should be used. The iOS simulator doesn't support Metal, too. That means you can test the openGL behavior with the iOS and tvOS simulator.
If you develop for AppleTV then this approach is really handy, because the openGL shaders always work with Metal. You just need to replace gl_FragCoord.xy / size.xy with v_tex_coord. If you run the code on the simulator, you will see the openGL code, if you run it on the AppleTV target you'll see the smooth Metal shaders.
And another hint to all swift developers: Never ever forget the semicolon at the end of the line with shaders ;-)
Another trap is casting.
Metal:
int intVal = (int) uv.x;
float a = (float) intVal;
Open GL:
int intVal = int(uv.x);
float a = float(intVal);
I hope I could help anyone.
Cheers,
Jack