Here's actual code that creates vertex and fragment programs from a string; using it allows you to compile shaders at runtime (shown in the code following the shader string method).
To eliminate the need for the use of escape sequences (e.g., n...), I use the STRINGIFY macro. To workaround its limitation on the use of double quotes, I wrote a block that takes an array of header file names and creates import statements from them. It then inserts them into the shader at the appropriate place; I did the same for include statements. It simplifies and expedites the insertion of what are sometimes rather lengthy lists.
Incorporating this code will not only allow you to select a particular shader to use based on localization, but, if necessary, could also be used to update your app's shaders without having to update the app. You would simply create and ship a text file containing your shader code, which your app could be preprogrammed to reference as the shader source.
#if !defined(_STRINGIFY)
#define __STRINGIFY( _x ) # _x
#define _STRINGIFY( _x ) __STRINGIFY( _x )
#endif
typedef NSString *(^StringifyArrayOfIncludes)(NSArray <NSString *> *includes);
static NSString *(^stringifyHeaderFileNamesArray)(NSArray <NSString *> *) = ^(NSArray <NSString *> *includes) {
NSMutableString *importStatements = [NSMutableString new];
[includes enumerateObjectsUsingBlock:^(NSString * _Nonnull include, NSUInteger idx, BOOL * _Nonnull stop) {
[importStatements appendString:@"#include <"];
[importStatements appendString:include];
[importStatements appendString:@">\n"];
}];
return [NSString new];
};
typedef NSString *(^StringifyArrayOfHeaderFileNames)(NSArray <NSString *> *headerFileNames);
static NSString *(^stringifyIncludesArray)(NSArray *) = ^(NSArray *headerFileNames) {
NSMutableString *importStatements = [NSMutableString new];
[headerFileNames enumerateObjectsUsingBlock:^(NSString * _Nonnull headerFileName, NSUInteger idx, BOOL * _Nonnull stop) {
[importStatements appendString:@"#import "];
[importStatements appendString:@_STRINGIFY("")];
[importStatements appendString:headerFileName];
[importStatements appendString:@_STRINGIFY("")];
[importStatements appendString:@"\n"];
}];
return [NSString new];
};
- (NSString *)shader
{
NSString *includes = stringifyIncludesArray(@[@"metal_stdlib", @"simd/simd.h"]);
NSString *imports = stringifyHeaderFileNamesArray(@[@"ShaderTypes.h"]);
NSString *code = [NSString stringWithFormat:@"%s",
_STRINGIFY(
using namespace metal;
typedef struct {
float scale_factor;
float display_configuration;
} Uniforms;
typedef struct {
float4 renderedCoordinate [[position]];
float2 textureCoordinate;
} TextureMappingVertex;
vertex TextureMappingVertex mapTexture(unsigned int vertex_id [[ vertex_id ]],
constant Uniforms &uniform [[ buffer(1) ]])
{
float4x4 renderedCoordinates;
float4x2 textureCoordinates;
if (uniform.display_configuration == 0 ||
uniform.display_configuration == 2 ||
uniform.display_configuration == 4 ||
uniform.display_configuration == 6)
{
renderedCoordinates = float4x4(float4( -1.0, -1.0, 0.0, 1.0 ),
float4( 1.0, -1.0, 0.0, 1.0 ),
float4( -1.0, 1.0, 0.0, 1.0 ),
float4( 1.0, 1.0, 0.0, 1.0 ));
textureCoordinates = float4x2(float2( 0.0, 1.0 ),
float2( 2.0, 1.0 ),
float2( 0.0, 0.0 ),
float2( 2.0, 0.0 ));
} else if (uniform.display_configuration == 1 ||
uniform.display_configuration == 3 ||
uniform.display_configuration == 5 ||
uniform.display_configuration == 7)
{
renderedCoordinates = float4x4(float4( -1.0, -1.0, 0.0, 1.0 ),
float4( -1.0, 1.0, 0.0, 1.0 ),
float4( 1.0, -1.0, 0.0, 1.0 ),
float4( 1.0, 1.0, 0.0, 1.0 ));
if (uniform.display_configuration == 1 ||
uniform.display_configuration == 5)
{
textureCoordinates = float4x2(float2( 0.0, 1.0 ),
float2( 1.0, 1.0 ),
float2( 0.0, -1.0 ),
float2( 1.0, -1.0 ));
} else if (uniform.display_configuration == 3 ||
uniform.display_configuration == 7)
{
textureCoordinates = float4x2(float2( 0.0, 2.0 ),
float2( 1.0, 2.0 ),
float2( 0.0, 0.0 ),
float2( 1.0, 0.0 ));
}
}
TextureMappingVertex outVertex;
outVertex.renderedCoordinate = float4(uniform.scale_factor, uniform.scale_factor , 1.0f, 1.0f ) * renderedCoordinates[vertex_id];
outVertex.textureCoordinate = textureCoordinates[vertex_id];
return outVertex;
}
fragment half4 displayTexture(TextureMappingVertex mappingVertex [[ stage_in ]],
texture2d<float, access::sample> texture [[ texture(0) ]],
sampler samplr [[sampler(0)]],
constant Uniforms &uniform [[ buffer(1) ]]) {
if (uniform.display_configuration == 1 ||
uniform.display_configuration == 2 ||
uniform.display_configuration == 4 ||
uniform.display_configuration == 6 ||
uniform.display_configuration == 7)
{
mappingVertex.textureCoordinate.x = 1 - mappingVertex.textureCoordinate.x;
}
if (uniform.display_configuration == 2 ||
uniform.display_configuration == 6)
{
mappingVertex.textureCoordinate.y = 1 - mappingVertex.textureCoordinate.y;
}
if (uniform.scale_factor < 1.0)
{
mappingVertex.textureCoordinate.y += (texture.get_height(0) - (texture.get_height(0) * uniform.scale_factor));
}
half4 new_texture = half4(texture.sample(samplr, mappingVertex.textureCoordinate));
return new_texture;
}
)];
return [NSString stringWithFormat:@"%@\n%@", includes, imports, code];
}
/*
* Metal setup: Library
*/
__autoreleasing NSError *error = nil;
NSString* librarySrc = [self shader];
if(!librarySrc) {
[NSException raise:@"Failed to read shaders" format:@"%@", [error localizedDescription]];
}
_library = [_device newLibraryWithSource:librarySrc options:nil error:&error];
if(!_library) {
[NSException raise:@"Failed to compile shaders" format:@"%@", [error localizedDescription]];
}
id <MTLFunction> vertexProgram = [_library newFunctionWithName:@"mapTexture"];
id <MTLFunction> fragmentProgram = [_library newFunctionWithName:@"displayTexture"];
.
.
.