8 min read

Gloss Caustic Shading

Matt Gallagher recently wrote a very useful article about drawing gloss gradients using Core Graphics. In his article, Matt describes how to reproduce the oft-seen glossy gradient effect. Thanks Matt! It’s a nice article. “Cocoa with Love” lovingly provides the working source code. This little article aims to complement Matt’s work.

I’ve also re-factored the software and packaged the result within an Objective-C class called RRGlossCausticShader. This packaging automatically adds support for key-value coding and observing. Bindings then let you easily wrap the class within a little application able to adjust the many parameters interactively.

Show me the money

Before going any further, you might first want to see the results. I know I would! Download the project to play with the shader. Compile and run the application using Xcode. The main window appears as follows. Inspector panels also appear from where you can interactively adjust the many shading parameters.

Gloss Caustic Shader Window
Gloss Caustic Shader Window

Gloss above, caustic below. Those are the two halves of the shading. Gloss appears in the top half, caustic in the bottom half. Within each half, an exponential function blends a given non-caustic base colour with whiteness above and with matching caustic colour below. The main window displays these basic elements of the shading process: the shading itself (left), non-caustic base colour (top right) and coefficient of the exponent function (bottom right).

Re-factoring

My version of Matt’s work includes some re-factoring. Rather than just the one functional interface, the version that you’ll find within the project involves three modular utility classes. - RRGlossCausticShader encapsulates shading - RRCausticColorMatcher finds caustic matches for given colours - RRExponentialFunction wraps the exponential maths - RRLuminanceFromRGBComponents returns luminance from RGB triples

This re-factoring has some benefits. First, it promotes re-use. Other requirements may call for RGB-to-luminance conversion, or a generic exponential function for example; or you might want to sub-class the caustic colour matcher and add some customisation. Separating out the individual sub-requirements adds some extra scope for growth. I think it also makes the source code somewhat easier to follow and therefore comprehend. But that might just be my own personal opinion. Your mileage may vary, as they say!

Anyway, the purpose of this article is to outline some of the differences in implementation and the underlying thinking. Comments welcome, of course.

Twinkle, twinkle

The gloss effect on the upper half of the gradient derives from the base colour’s luminosity. So obtaining luminance from a given Red, Green and Blue is part of the glossing requirement.

As you can see below, I replace Matt’s conversion with the one described at OpenGL. The difference is small, almost non-existent. Only, the comment that “Haeberli notes that [the YIQ colour conversion values used by Matt] are incorrect in a linear RGB colour space” convinced me to make the small change. Just call me fussy.

CGFloat RRLuminanceFromRGBComponents(const CGFloat *rgb)
{
    // 0.3086 + 0.6094 + 0.0820 = 1.0
    return 0.3086f*rgb[0] + 0.6094f*rgb[1] + 0.0820f*rgb[2];
}

Notice a few other things. I’m using CGFloat type for floats because this corresponds exactly with Apple’s usage for colour components. Can we always assume that CGFloat equals float? No. In fact, CGFloat is sometimes double! This happens when 64-bit compiling for instance. Argument const-correctness is another feature.

Caustic nature

The lower half of the gradient blends the base colour into shades of yellow (yellow is the default caustic hue). The lower end of the gradient starts to look more-and-more tinted towards the yellow. However, the precise colour depends upon the non-caustic base colour. For blue colours, for example, the default caustic switches to magenta. Blue looks better fading toward magenta, rather than yellow. In all cases though, the amount of caustic to non-caustic blending varies according to the cosine of the distance between the caustic and non-caustic hues.

So rather than having a function handle the caustic colour matching and implementing the adjustable parameters as manifest constants, the new version implements a “colour matching” class. Interface listed below. The class implements adjustable parameters as read-write class properties with all the necessary setters and getters. This approach buys some advantages. It automatically adds support for key-value coding and observing. In practice, it means that user-interface controls can bind to these properties using Cocoa Bindings.

@interface RRCausticColorMatcher : NSObject
{
    CGFloat causticHue;
        // Yellow by default.
    CGFloat graySaturationThreshold;
        // Saturation level at which colours appear grey. Below this level,
        // matcher response snaps to pure caustic.
    CGFloat causticSaturationForGrays;
        // Defines the caustic saturation for grey colours. Grey colours fall
        // below the grey saturation threshold. When saturation drops too low,
        // everything looks grey.
    CGFloat redHueThreshold;
        // Colours at this threshold and above match to default caustics rather
        // than default magenta for blues.
    CGFloat blueHueThreshold;
        // Triggers a switch to magenta caustics. Hues at blue and beyond
        // display magenta-modulated caustics by default.
    CGFloat blueCausticHue;
        // Magenta by default. Magenta caustics for blue colours.
    CGFloat causticFractionDomainFactor;
        // Expands or contracts the caustic fraction's domain. With factor equal
        // to 1, non-caustic and caustic hues blend according to the cosine of
        // their difference. Smaller the difference, greater the amount of
        // caustic hue. Defaults to 1.4 meaning that the point of absolutely no
        // caustic blending occurs at 1/1.4 difference from caustic hue. Try
        // plotting cos(x*pi*1.4) in the -1,1 interval.
    CGFloat causticFractionRangeFactor;
        // Scales the caustic fraction which without a factor outputs a blending
        // fraction between 0 and 1 in favour of the caustic blend. Defaults to
        // 0.6 which scales down the amount of caustic hue-and-brightness by
        // that amount.
}

- (NSColor *)matchForColor:(NSColor *)aColor;
    // Matches the given colour. Answers a matching caustic colour. The result
    // shifts hue and brightness towards yellow. Saturation remains unchanged.
- (void)matchForHSB:(const CGFloat *)hsb caustic:(CGFloat *)outHSB;
    // Does the work.

@property(assign) CGFloat causticHue;
@property(assign) CGFloat graySaturationThreshold;
@property(assign) CGFloat causticSaturationForGrays;
@property(assign) CGFloat redHueThreshold;
@property(assign) CGFloat blueHueThreshold;
@property(assign) CGFloat blueCausticHue;
@property(assign) CGFloat causticFractionDomainFactor;
@property(assign) CGFloat causticFractionRangeFactor;

@end

Exponentially

Shading employs an exponential function. As the gradient progresses from 0 to 0.5, gloss white blending increases exponentially in the 0,1 interval. Progressing through the bottom half of the gradient from 0.5 to 1, caustic blending increases exponentially. The implementation re-factors the exponential function. Interface listing below. It becomes a C-style object class and thereby minimises its dependencies: just plain C types and standard library math.h and hence very suitable for repeated invocations from the bowels of a shading function.

// Encapsulates an optimising generic Exponential Function where
//      y=1-(exp(x*-c)-exp(-c))/(1-exp(-c))
// and where 0<c is a general coefficient describing the exponential
// curvature. The function's input domain lies within the 0..1 interval, its
// output range likewise. The implementation optimises by pre-computing those
// constant terms depending only on the coefficient whenever the coefficient
// changes. Repeated evaluation takes less computing time thereafter.
struct RRExponentialFunction
{
    float coefficient;
    float exponentOfMinusCoefficient;
    float oneOverOneMinusExponentOfMinusCoefficient;
};

typedef struct RRExponentialFunction RRExponentialFunction;

void RRExponentialFunctionSetCoefficient(RRExponentialFunction *f, float c);
float RRExponentialFunctionEvaluate(RRExponentialFunction *f, float x);

Shading

And finally, the “gloss-caustic shader” class brings all the disparate pieces together. The design aims for reusability. Idea is that you instantiate a shader, set up the parameters such as non-caustic colour or any other necessary adjustments to defaults, then re-use it over-and-over whenever you need to draw the shading.

@interface RRGlossCausticShader : NSObject
{
    struct RRGlossCausticShaderInfo *info;
    RRCausticColorMatcher *matcher;
}

- (void)drawShadingFromPoint:(NSPoint)startingPoint toPoint:(NSPoint)endingPoint inContext:(CGContextRef)aContext;

- (void)update;
    // Send -update after changing one or more parameters. Setters do not
    // automatically update the shader. This is by design. It applies to the
    // caustic colour matcher too. Change anything? Send an update. Otherwise,
    // if the setters automatically update, multiple changes trigger unnecessary
    // multiple updates. It's a small optimisation.
    // Updating follows the dependency chain. Caustic colour depends on
    // non-caustic colour along with all the caustic colour matcher's tuneable
    // configuration settings. Updating also re-computes the gloss. Gloss
    // derives from non-caustic colour luminance, among other things.

//---------------------------------------------------------------------- setters

- (void)setExponentialCoefficient:(float)c;
- (void)setNoncausticColor:(NSColor *)aColor;
    // Converts aColor to device RGB colour space. The resulting colour
    // components become the new non-caustic colour. This setter, like all
    // others, does not automatically readjust the dependencies. Invoke -update
    // after adjusting one or more settings.
- (void)setGlossReflectionPower:(CGFloat)powerLevel;
    // Assigns a new power level to the gloss reflection.
- (void)setGlossStartingWhite:(CGFloat)whiteLevel;
    // White levels range between 0 and 1 inclusive. Gloss starting white levels
    // typically have higher values compared to ending white level.
- (void)setGlossEndingWhite:(CGFloat)whiteLevel;

//---------------------------------------------------------------------- getters

- (float)exponentialCoefficient;
- (NSColor *)noncausticColor;
    // Returns the non-caustic colour.
- (CGFloat)glossReflectionPower;
- (CGFloat)glossStartingWhite;
- (CGFloat)glossEndingWhite;

// Key-value coding automatically gives access to colour matching for caustic
// colours. Special note though, changing caustic matcher thresholds does not
// (repeat not) automatically adjust the shader's caustic colour. You need to
// update the shader when ready.
// Currently, the matcher property offers read-only access. You cannot set the
// matcher! However, perhaps future versions will allow setting in order to
// override the default caustic matching behaviour. Developers might want to
// customise the colour matching algorithmically as well as by tweaking
// parameters.
@property(readonly) RRCausticColorMatcher *matcher;

@end

The sample project explains how to use the shader. I hope it’s clear enough. The sources have an MIT license. I haven’t tried it out on iPhone or pre-Leopard Mac, so can’t make any comments about portability except to say that porting should be fairly straightforward. But no doubt you’ve heard that before!