Wed, 10 Dec 2008 03:25:45 GMT

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 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!


Trackbacks

Use the following link to trackback from your own site:
http://blog.pioneeringsoftware.co.uk/trackbacks?article_id=14

Comments

  • Benoit Cerrina says

    Hello,

    Very nice sample, I am trying to use something like this to replicate the look of the SMS conversation bubbles (like iChat) on the iphone, I know there will be things to change to improve the portability (due to the lack of NSColor on the iphone but this is not what I want to talk about.

    There are a couple things missing:

    First the gradient should not be: glossy gradient - caustic gradient. There should really be an area in the middle where the non caustic color is applied without a gradient. For this purpose I have modified the shading function to be:

    void RRGlossCausticShaderEvaluate(void *info, const CGFloat *in, CGFloat *out)
    {
    #define INFO(x) (((struct RRGlossCausticShaderInfo *)info)->x)
        CGFloat x = *in;
        if (x < 0.2f)
        {
            int i;
            // 0<=x<0.5
            // 0<=2x<1
            CGFloat f = RRExponentialFunctionEvaluate(&INFO(exponentialFunction), 5*x)*INFO(gloss.whiteExtent) + INFO(gloss.whiteOrigin);
            CGFloat g = 1 - f;
            // f: 0 --> 1
            // g: 1 --> 0
            for (i = 0; i < 4; i++)
            {
                out[i] = INFO(noncausticRGBA)[i]*g + f;
            }
        }
        else if (x >0.8)
        {
            int i;
            // 0.5<=x<1
            // 0<=2(x-0.5)<1
            CGFloat f = RRExponentialFunctionEvaluate(&INFO(exponentialFunction), 5*(x - 0.8f));
            CGFloat g = 1 - f;
            // f: 0 --> 1
            // g: 1 --> 0
            for (i = 0; i < 4; i++)
            {
                out[i] = INFO(noncausticRGBA)[i]*g + INFO(causticRGBA)[i]*f;
            }
        }
        else
        {
            for (int i = 0; i < 4; i++)
                out [i]= INFO(noncausticRGBA)[i];
        }
    }
    

    Now my remaining problems are: 1) at the top there is an area which is actually darker than the base color before the gloss gradient should be added. I am going to see if I can reproduce that. Second the corners have some kind of gradient too which does not appear to be either linear nor radial to my poor untrained eyes and I have no idea how I’ll replicate this.

    Best regards
    Benoit

  • Pioneer! says

    Dear Benoit,

    Most interesting! I like your idea. iPhone’s and iChat’s glossy speech bubble is a nice effect. It would be handy to have some way to reproduce it. Or, at least, something similar. I’m interested to see how you get on. Please keep me posted. In fact, you’ve got me thinking about it too!

    Kind regards,
    Roy

    P.S.
    If you want to check out the version-controlled sources and clone your own, they live here at github.

  • Benoit Cerrina says

    too bad I hadn’t seen the reply yet. I have just downloaded the git hub. As I was in a hurry to get those bubble ready and as in my app for now they are static, I stopped working on this and ended up doing them with illustrator (and I had to learn the basics of that tool… it may have been faster to code in the end). Since I sent my new version to apple I am now back on the problem and will tell you the progess. I think my time with illustrator gave me a better understanding of what will be needed. so it was not lost. I’ll keep you posted

  • L|M|TER says

    First, thanks for this great example.Second, is there a way to draw the gloss into an NSBezierPath ? .. maybe add a drawShadingIntoPath: function to the shader. Thanks !

  • L|M|TER says

    Nevermind, I found out how to do it..

  • Ole Begemann says

    Great post, thank you very much! I just ported your code to the iPhone and added an iPhone sample app. See my blog post if you are interested.

  • Kenneth Lewis says

    Hello Pioneer, could this be modified to use this piece of code to draw the rounded glossy icons you see in Settings?

    Thanks,

    Kenneth

  • pioneer says

    Dear Kenneth, Indeed you can. You only need to set up clipping for the Core Graphics context. See the Quartz 2D Programming Guide for the details. At the online iPhone library, you can find the document here.

    But in summary, before sending -drawShadingFromPoint:toPoint:inContext: to the shader you need to clip the context. You can also do this using CGLayer masking as per the iPhone sample code, or via the CGContext graphics API or using NSBezierPath on the Mac, e.g.

    - (void)drawRect:(NSRect)rect
    {
        // Shade from the top-left corner of the bounds to the bottom-left.
        NSRect bounds = [self bounds];
        [[NSBezierPath bezierPathWithRoundedRect:bounds xRadius:10.0 yRadius:10.0] setClip];
        [shader drawShadingFromPoint:CGPointMake(NSMinX(bounds), NSMaxY(bounds))
                             toPoint:CGPointMake(NSMinX(bounds), NSMinY(bounds))
                           inContext:[[NSGraphicsContext currentContext] graphicsPort]];
    }
    
  • Jeff Jones says

    Thank you so much. You are a rock star. (And so is Matt G.)

Leave a comment

(never displayed)

Markdown enabled