I've made a Perlin Noise filter for CoreImage using Metal Shader Language. My first version of it works fine, and allows me to specify 2 colors to use as the low
and high
. My next step is to allow it to accept a color map such that it can produce multi-color output. The color map version compiles fine, but when my CIFilter subclass tries to create a CIColorKernel from it using the init(functionName:fromMetalLibraryData:)
method, I get an EXEC_BAD_ACCESS
with code=1
. Any idea what about my color map implementation might be causing this?
Here's the simple, 2-color version:
#include <CoreImage/CoreImage.h>
using namespace metal;
constant int p[512] = {151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180};
float fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }
float grad(int hash, float x, float y, float z) {
int h = hash & 15;
float u = h < 8 ? x : y;
float v = h < 4 ? y : h == 12 || h == 14 ? x : z;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
float lerp(float t, float x, float y) { return x + t * (y - x); }
float noise(float x, float y, float z) {
int X = (int)floor(x) & 255;
int Y = (int)floor(y) & 255;
int Z = (int)floor(z) & 255;
x -= floor(x);
y -= floor(y);
z -= floor(z);
float u = fade(x);
float v = fade(y);
float w = fade(z);
int A = p[X ] + Y;
int AA = p[A ] + Z;
int AB = p[A + 1] + Z;
int B = p[X + 1] + Y;
int BA = p[B ] + Z;
int BB = p[B + 1] + Z;
float result = lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD
grad(p[BA ], x-1, y , z )), // BLENDED
lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS
grad(p[BB ], x-1, y-1, z ))),// FROM 8
lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS
grad(p[BA+1], x-1, y , z-1 )), // OF CUBE
lerp(u, grad(p[AB+1], x , y-1, z-1 ),
grad(p[BB+1], x-1, y-1, z-1 ))));
return (result + 1.0) / 2.0;
}
extern "C" float4 PerlinNoise (float4 lowColor, float4 highColor, float offsetX, float offsetY, float offsetZ, float scale, float contrast, coreimage::destination dest)
{
float val = noise(dest.coord().x * scale + offsetX, dest.coord().y * scale + offsetY, offsetZ);
return mix(lowColor, highColor, pow(val, contrast));
}
And here is the color map version:
#include <CoreImage/CoreImage.h>
using namespace metal;
constant uint8_t p[512] = {151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180};
float fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }
float grad(uint8_t hash, float x, float y, float z) {
uint8_t h = hash & 15;
float u = h < 8 ? x : y;
float v = h < 4 ? y : h == 12 || h == 14 ? x : z;
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
float lerp(float t, float x, float y) { return x + t * (y - x); }
float noise(float x, float y, float z) {
uint8_t X = (uint8_t)floor(x) & 255;
uint8_t Y = (uint8_t)floor(y) & 255;
uint8_t Z = (uint8_t)floor(z) & 255;
x -= floor(x);
y -= floor(y);
z -= floor(z);
float u = fade(x);
float v = fade(y);
float w = fade(z);
uint8_t A = p[X ] + Y;
uint8_t AA = p[A ] + Z;
uint8_t AB = p[A + 1] + Z;
uint8_t B = p[X + 1] + Y;
uint8_t BA = p[B ] + Z;
uint8_t BB = p[B + 1] + Z;
float result = lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD
grad(p[BA ], x-1, y , z )), // BLENDED
lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS
grad(p[BB ], x-1, y-1, z ))),// FROM 8
lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS
grad(p[BA+1], x-1, y , z-1 )), // OF CUBE
lerp(u, grad(p[AB+1], x , y-1, z-1 ),
grad(p[BB+1], x-1, y-1, z-1 ))));
return (result + 1.0) / 2.0;
}
float4 colormapLookup(float value, size_t count, float indices[], float4 colormap[]) {
// If the value is at the outside boundary, just return the boundary color.
if (value < indices[0]) {
return colormap[0];
} else if (value >= indices[count - 1]) {
return colormap[count - 1];
}
// Find which 2 indices the value falls between.
size_t index = 0;
while (value < indices[index] && index < count - 1) {
index++;
}
float startIndex = indices[index];
float endIndex = indices[index+1];
// Calculate the normalized offset between those indies.
float offset = (value - startIndex) / (endIndex - startIndex);
// Return the blended color for that offest.
return mix(colormap[index], colormap[index+1], offset);
}
extern "C" float4 PerlinNoise (size_t count, float indices[], float4 colormap[], float offsetX, float offsetY, float offsetZ, float scale, float contrast, coreimage::destination dest)
{
float val = noise(dest.coord().x * scale + offsetX, dest.coord().y * scale + offsetY, offsetZ);
return colormapLookup(pow(val, contrast), count, indices, colormap);
}
Here's the method where I attempt to instantiate the CIColorKernel. The EXEC_BAD_ACCESS
happens on the line where the CIColorKernel init function is called:
static var kernel: CIColorKernel? = {
guard let url = Bundle.main.url(forResource: "PerlinNoiseGenerator", withExtension: "ci.metallib") else { return nil }
do {
let data = try Data(contentsOf: url)
return try CIColorKernel(functionName: "PerlinNoise", fromMetalLibraryData: data)
} catch {
print("[ERROR] Failed to create CIColorKernel: \(error)")
}
return nil
}()
Edit: Added the Swift code where the CIColorKernel is being instantiated.
Based on Frank's comment, I reverted my Perlin Noise filter to its original, functional state and instead applied the color map as a following step using this improved version of the CIColorMap
filter which can generate its own gradient image based on a [CGFloat: CIColor] dictionary.
import CoreImage
class ImprovedColorMap: CIFilter {
enum ColorMapError: Error {
case unableToCreateCGContext
case unableToCreateCGGradient
}
private var _colorMap: [CGFloat: CIColor] = [
0.0: .black,
1.0: .white,
]
private var gradientImage: CIImage = CIImage()
private var mapFilter: CIFilter? = CIFilter(name: "CIColorMap")
var inputImage: CIImage?
var inputColorMap: [CGFloat: CIColor] {
get { _colorMap }
set {
guard newValue.keys.count >= 2 else {
print("ERROR: Color map must have at least 2 entries. Change will be ignored.")
return
}
_colorMap = newValue
generateGradient()
}
}
override var outputImage: CIImage? {
guard let filter = mapFilter else {
return nil
}
filter.setValue(gradientImage, forKey: kCIInputGradientImageKey)
filter.setValue(inputImage, forKey: kCIInputImageKey)
return filter.outputImage
}
init(colorMap: [CGFloat: CIColor]? = nil) {
if let colorMap = colorMap {
_colorMap = colorMap
}
super.init()
generateGradient()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func generateGradient() {
DispatchQueue.global(qos: .default).async {
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(data: nil, width: 512, height: 16, bitsPerComponent: 8, bytesPerRow: 512 * 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
print("ERROR: Could not create CGContext.")
return
}
var locations: [CGFloat] = []
var components: [CGFloat] = []
for (location, color) in self._colorMap {
locations.append(location)
components.append(contentsOf: color.floatComponents)
}
guard let gradient = CGGradient(colorSpace: colorSpace, colorComponents: components, locations: locations, count: locations.count) else {
print("ERROR: Could not create CGGradient.")
return
}
context.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: 512.0, y: 0.0), options: [])
guard let image = context.makeImage() else {
print("ERROR: Failed to create image from context.")
return
}
DispatchQueue.main.async {
self.gradientImage = CIImage(cgImage: image)
}
}
}
}
That works well, except now I realize that traditional Perlin Noise is too smooth for realistic-looking terrain, so I'm moving on to creating a Simplex Noise metal kernel next.