Skip to content

Custom Metal Shaders

christinakelly edited this page Sep 30, 2014 · 3 revisions

Custom Metal Shaders:

(See also: Metal Rendering for Cocos2D.)

The Metal shading language is more powerful than GLSL, but also more complicated. You will want to be comfortable with GLSL first. Next you'll want to read the Metal Shading Language Guide. (The rest of this document assumes that you've read it!)

One thing to keep in mind is that currently hardware support for Metal is relatively low. It's unlikely that you'll want to write a game that supports Metal only. As far as I can tell, it's currently not possible to specify that your app requires Metal support when submitting to the app store anyway. (See the bad reviews of Epic's Zen Garden demo crashing on the iPhone 5 for instance.) So if you want to support Metal, expect that you'll also need to support GL devices as well. Only use Metal for enhanced effects or performance -- do not require it.

Metal Fragment Shaders:

Let's start with an example:

struct MyStruct {
	float4x4 colorMatrix;
	float2 textureOffset;
};

fragment half4 MyFunShaderFS(
	const CCFragData in [[stage_in]],
	const device CCGlobalUniforms *cc_GlobalUniforms [[buffer(0)]],
	const device struct MyStruct *myStruct [[buffer(1)]],
	const device float4 *additiveColor [[buffer(2)]],
	texture2d<half> cc_MainTexture [[texture(0)]],
	sampler cc_MainTextureSampler [[sampler(0)]]
){
	half4 sample = cc_MainTexture.sample(cc_MainTextureSampler, in.texCoord1 + myStruct->textureOffset);
	return half4(myStruct->colorMatrix*float4(in.color*sample) + *additiveColor);
}

This shader doesn't really do anything useful, but it does do many things you will want to do in your own shaders.

  • CCFragData is the struct type output by the standard vertex function. Unless you are writing your own vertex shader, you'll want to use this type.
  • If you define a cc_GlobalUniforms argument with the type CCGlobalUniforms then Cocos2D will pass its built-in global variables to your shader.
  • You can define and pass your own struct types as well. It's recommended to put them in a header file so you can use them in both your Metal and ObjC code.
  • You can also pass "plain" float, vector or matrix values to a shader without wrapping them in a struct, but it's more efficient to pass your custom variables in a single struct argument.
  • cc_MainTexture is the name of the main texture passed by most Cocos2D node types.
  • Cocos2D textures create a sampler which you can access by appending "Sampler" to the end of a texture argument name.
  • Sampler arguments are optional, although there is currently not yet a way to pass samplers that aren't associated with a texture.
  • It's not yet possible to pass your own buffers to Metal shaders.
  • Cocos2D passes shader values by name and does not reserve specific argument indexes.

NOTE: Be careful when laying out structs you want to share with Metal since the alignment matters. Using GLKMath types helps with this since they share common alignments with Metal types.

Passing Shader Values From Objective-C:

Passing shader values to Metal works very similarly to GL using the CCDirector.globalShaderUniforms and CCNode.shaderUniforms dictionaries. There are couple of differences though. Firstly, Metal doesn't use "uniforms" exactly. Instead you pass addresses of memory buffers to your shaders and access your shader values that through the buffers. Cocos2D takes care of the hard parts, packing and synchronizing buffers, all you need to do is make sure the types match. Secondly, unlike with the GL renderer where you pass many individual values, with Metal you are encouraged to bundle many into a single struct and pass them all as a single argument.

Here is an example to pass values to MyFunShaderFS() above:

struct MyStruct {
	GLKMatrix4 colorMatrix;
	GLKVector2 textureOffset;
};

struct MyStruct myStruct = {
	GLKMatrix4MakeScale(1, 2, 3),
	GLKVector2Make(4, 5),
};

// Non-texture types are always passed using NSValue objects. (CCColor, NSNumber, etc are not supported)
node.shaderUniforms[@"myStruct"] = [NSValue valueWithBytes:&myStruct objCType:@encode(struct MyStruct)];

// CCColor objects are not supported by the Metal renderer and must be wrapped as vector in an NSValue.
// This will also work with the GL renderer.
CCColor *color = nil;
node.shaderUniforms[@"additiveColor"] = [NSValue valueWithGLKVector4:color.glkVector4];

// Textures are passed using CCTexture objects.
CCTexture *texture = ...;
node.shaderUniforms[@"myTexture"] = texture;

NOTE: With the GL renderer if a uniform is not set using either CCDirector.globalShaderUniforms or CCNode.shaderUniforms, then it will pass zeroed values. With the Metal renderer, you MUST pass a value using one or the other or the result is undefined.

Metal Vertex Shaders:

Cocos2D does not use a vertex descriptor for Metal, and this simplifies Cocos2D and the vertex shaders a little bit. Metal vertex shaders are a little different from vertex shaders for GL.

vertex CCFragData CCVertexFunctionDefault(
	const device CCVertex *cc_VertexAttributes [[buffer(0)]],
	unsigned int vid [[vertex_id]]
){
	CCFragData out;
	
	out.position = cc_VertexAttributes[vid].position;
	out.texCoord1 = cc_VertexAttributes[vid].texCoord1;
	out.texCoord2 = cc_VertexAttributes[vid].texCoord2;
	out.color = saturate(half4(cc_VertexAttributes[vid].color));
	
	return out;
}

Some things to note:

  • If you define a cc_GlobalUniforms argument with the type CCGlobalUniforms then Cocos2D will pass the array of vertexes to your shader.
  • You need to define an argument bound to [[vertex_id]] to access the current vertex.
  • It uses a CCFragData struct for the return type, but when writing custom vertex shaders you probably want to define your own struct.
  • Like the GL version of the standard vertex shader, it really just copies data from the vertex attributes to the fragment shader input.

Compute Shaders:

Cocos2D does not have any specific support for compute shaders yet, but you can experiment with them if you import the CCMetalSupport_Private.h header. Using the [CCMetalContext currentContext] method, you can get access to the command queue and create a command buffer with compute commands in it. Since these are private APIs, they may change in the future, but suggestions on an official way to expose compute shaders would be welcome.