Everybody loves a lens flare, don't they? If you want to give your iOS game or app that distinctive JJ Abrams look, you may be a tad disappointed that Core Image doesn't come bundled with a nice lens flare filter and be scratching your head wondering how to create one - especially one with some nice hexagonal artefacts.
Well stop scratching and start flaring! My Core Image lens flare filter may just be the solution.
Lens Flare Basics
Lens flare is caused by internal reflections between the lens elements of a camera lens. Often the flares appear as polygonal shapes caused by the shape of the iris. My filter is a simple implementation where the flare artefacts are all the same colour and are all hexagonal.The line of flares begins at a light source, passes through the centre of the image and ends at the opposite point from the origin around the centre. The filter displays eight reflection artefacts - one at the furthest point and the remaining seven positioned anywhere along that line. The size of each is also user defined.
To render the light source (at the origin), I use a Core Image Sunbeams generator. To render the hexagonal artefacts I use a little bit of Core Image Kernel Language (CIKL) magic:
Rendering Hexagonal Reflection Artefacts
A Core Image color kernel function is invoked for every pixel in a destination image. To render a hexagon, the kernel function needs to ask the question, "are the coordinates of the pixel currently being computed within a hexagon of some size at some point?". The kernel function returns a colour based on the answer to that question.Luckily, I'm not the first person to ask this question and there's a fantastic solution at playchilla.com.
You may notice that the hexagons in the image above aren't a solid colour: they're brighter towards the edges. To achieve this, I took the playchilla.com code and tweaked the return value to be a function of the distance from the hexagon centre.
My final CIKL function, brightnessWithinHexagon(), accepts the co-ordinates of the current pixel, the co-ordinates of the centre of a hexagon and the size of a hexagon. It returns a normalised value that is the brightness the current pixel should be:
float brightnessWithinHexagon(vec2 coord, vec2 center, float v)
{
float h = v * sqrt(3.0);
float x = abs(coord.x - center.x);
float y = abs(coord.y - center.y);
float brightness = (x > h || y > v * 2.0) ?
0.0 :
smoothstep(0.5, 1.0, (distance(destCoord(), center) / (v * 2.0)));
return ((2.0 * v * h - v * x - h * y) >= 0.0) ? brightness : 0.0;
}
The main kernel function itself accepts the coordinates and sizes of eight hexagons, and the colour and brightness to render the hexagons. With those values, it progressively checks whether the current pixel is in any of the eight hexagons, adding the values with each step:
kernel vec4 lensFlare(vec2 center0, vec2 center1, vec2 center2, vec2 center3, vec2 center4, vec2 center5, vec2 center6, vec2 center7,
float size0, float size1, float size2, float size3, float size4, float size5, float size6, float size7,
vec3 color, float reflectionBrightness)
{
float reflectionO = brightnessWithinHexagon(destCoord(), center0, size0);
float reflection1 = reflectionO + brightnessWithinHexagon(destCoord(), center1, size1);
float reflection2 = reflection1 + brightnessWithinHexagon(destCoord(), center2, size2);
float reflection3 = reflection2 + brightnessWithinHexagon(destCoord(), center3, size3);
float reflection4 = reflection3 + brightnessWithinHexagon(destCoord(), center4, size4);
float reflection5 = reflection4 + brightnessWithinHexagon(destCoord(), center5, size5);
float reflection6 = reflection5 + brightnessWithinHexagon(destCoord(), center6, size6);
float reflection7 = reflection6 + brightnessWithinHexagon(destCoord(), center7, size7);
return vec4(color * reflection7 * reflectionBrightness, reflection7);
}
Implementing as a Core Image Filter
With the CIKL written, the next step is to wrap the kernel is a Core Image filter. The filter has a lot of attributes to control the position and colours of each reflection:
var inputOrigin = CIVector(x: 150, y: 150)
var inputSize = CIVector(x: 640, y: 640)
var inputColor = CIVector(x: 0.5, y: 0.2, z: 0.3)
var inputReflectionBrightness: CGFloat = 0.25
var inputPositionOne: CGFloat = 0.15
var inputPositionTwo: CGFloat = 0.3
var inputPositionThree: CGFloat = 0.4
var inputPositionFour: CGFloat = 0.45
var inputPositionFive: CGFloat = 0.6
var inputPositionSix: CGFloat = 0.75
var inputPositionSeven: CGFloat = 0.8
var inputReflectionSizeZero: CGFloat = 20
var inputReflectionSizeOne: CGFloat = 25
var inputReflectionSizeTwo: CGFloat = 12.5
var inputReflectionSizeThree: CGFloat = 5
var inputReflectionSizeFour: CGFloat = 20
var inputReflectionSizeFive: CGFloat = 35
var inputReflectionSizeSix: CGFloat = 40
var inputReflectionSizeSeven: CGFloat = 20
The filter's overridden outputImage getter calculates the position of reflectionZero to be opposite the inputOrigin:
let center = CIVector(x: inputSize.X / 2, y: inputSize.Y / 2)
let localOrigin = CIVector(x: center.X - inputOrigin.X, y: center.Y - inputOrigin.Y)
let reflectionZero = CIVector(x: center.X + localOrigin.X, y: center.Y + localOrigin.Y)
The remaining seven reflections simply interpolate between the origin and reflectionZero:
let reflectionOne = inputOrigin.interpolateTo(reflectionZero, value: inputPositionOne)
let reflectionTwo = inputOrigin.interpolateTo(reflectionZero, value: inputPositionTwo)
[...]
let reflectionSeven = inputOrigin.interpolateTo(reflectionZero, value: inputPositionSeven)
The interpolateTo(_:value) is a simple extension to CIVector:
func interpolateTo(target: CIVector, value: CGFloat) -> CIVector
{
return CIVector(
x: self.X + ((target.X - self.X) * value),
y: self.Y + ((target.Y - self.Y) * value))
}
With the reflection centres defined, the kernel containing the CIKL can be executed and a little bit of blur applied:
let arguments = [
reflectionZero, reflectionOne, reflectionTwo, reflectionThree, reflectionFour, reflectionFive, reflectionSix, reflectionSeven,
inputReflectionSizeZero, inputReflectionSizeOne, inputReflectionSizeTwo, inputReflectionSizeThree, inputReflectionSizeFour,
inputReflectionSizeFive, inputReflectionSizeSix, inputReflectionSizeSeven,
inputColor, inputReflectionBrightness]
let lensFlareImage = colorKernel.applyWithExtent(
extent,
arguments: arguments)?.imageByApplyingFilter("CIGaussianBlur", withInputParameters: [kCIInputRadiusKey: 2])
This lens flare image is composited over the sunbeams image generated by a CISunbeamsGenerator.
Conclusion
Yet again, the power of custom kernels shows that almost anything is possible with Core Image. Once I'd found the playchilla.com code, writing the filter took no more than an hour or so.The image above shows the output from my lens flare filter composited over a photograph of a sunset using CIAdditionCompositing. Since both SceneKit and SpriteKit support Core Image filters, this effect can easily be integrated into projects using either framework.
Of course, this filter has been added to Filterpedia:
If you'd like to learn more about writing custom Core Image kernels or the framework in general, may I recommend my book, Core Image for Swift.
Core Image for Swift is available from both Apple's iBooks Store or, as a PDF, from Gumroad. IMHO, the iBooks version is better, especially as it contains video assets which the PDF version doesn't.
Add a comment