SwitchingColor Shader

原文:SwitchingColor Shader
翻译:dreamana.com

你有看过这个 Nissan Juke 的网站专题吗?我在偶然在 Away3D showcase 里发现这个 microsite 的。很棒的美术,优雅的程序。自从我玩过许多 shader 之后,我立刻就对这个不可思议的 shader 作品产生兴趣。尤其这个“颜色切换”特效(当你点击汽车有色的区域并选择一个新颜色的时候)引起了我的注意。花了几个钟头的修修补补之后我做了一个 shader 出来,看起来很像。看!就点击方块的任意地方,却换到一个随机颜色。

Have you already seen the Nissan Juke webspecial? I came across this microsite on the Away3D showcase. Great artwork, neat programming. As I was playing a lot with shaders, I was immediately interested in the incredible shaderwork that was done here. Especially the “color switching” effect (when you click on a colored area of the car and select a new color), caught my attention. After some hours of tinkering I came up with a shader, which looks quite the same. Have a look! Just click on the block anywhere, to switch to a random color.

(flash)

那么背后的想法是怎样的呢?从变化的原点开始有两个事情发生了:

因为要改变颜色和形状,所以我们必须做一些顶点和片元的编程 (vertex and fragment programming)。颜色是通过片元着色器 (fragment shader) 改变的,而形状变形是通过顶点着色器 (vertex shader) 来完成的。 我们在 shader 类里面写一个 switchColor 函数,用于初始化整个流程。这应该是非常漂亮的自我解释:

So whats the idea behind it? Starting from the origin of the change there are two things happening:

As we have a change in color and shape, we have to do some vertex and fragment programming. The color is changed by the fragment shader, and the shape morphing is done from the vertex shader. We start with a switchColor function inside the shaderclass, which initiates the whole process. It should be pretty self explanatory:

public function switchColor(newc:Vector3D, neworigin:Vector3D):void
{
    // save starttime, to avoid subtraction to zero in renderEvent, decrease about 1
    startTime = getTimer() - 1;
    // set previous new color to the current one
    this.currentColor = this.newColor;
    // set new color and origin of transformation
    this.newColor = newc;
    this.origin = neworigin;
    // set colors and origin on the shader
    this.params.currentcolor.value[0] = currentColor.x;
    this.params.currentcolor.value[1] = currentColor.y;
    this.params.currentcolor.value[2] = currentColor.z;

    this.params.newcolor.value[0] = newc.x;
    this.params.newcolor.value[1] = newc.y;
    this.params.newcolor.value[2] = newc.z;

    this.params.origin.value[0] = neworigin.x;
    this.params.origin.value[1] = neworigin.y;
    this.params.origin.value[2] = neworigin.z;
}

The concentric expansion is dependent from the start time (t0) of the transition. So the more time elapsed since t0, the larger the distance from the origin. This has to be calculated only once every frame, so we do the calculation on the actionscript side in our SwitchingColorShader class. We simply subtract the afore saved startTime from the currentTimer. And we divide through a timeStretch value, to have some control over the speed of the change.

params.timedelta.value[0] = (getTimer() - startTime) / timeStretch;

Thats about it from the actionscript side. The rest is done inside the actual shader (FLSL). Let’s have a look at it:

/**
 * Flare3D Layer Shader Language v1.0.
 * @author Jonas Volger
 */

< namespace:"flare", name:"SwitchColorFilter" >

// current and new target color of object
public float4 currentcolor = float4(1,0,0,1);
public float4 newcolor = float4(0,0,1,1);
// the origin of the color change
public float4 origin = float4(0,0,0,1);
// time since change started
public float1 timedelta = float1(0.5);
// the width of the transition
public float1 spread = float1(6);
// view projection
public WORLD_VIEW_PROJ worldViewProj;
// world projection (transform local vertex coordinate into world space)
public WORLD world;
// position ob currently rendered vertex
input POSITION position;
// the normal of the current vertex/surface
input NORMAL normal;
// position in world of the current fragment
interpolated float4 iwPosition;
// factors/intensity of the colors at the current fragment
interpolated float4 factors;
private float4 vertexProgram()
{
    // take position of current vertex and multiply it with the world
    float4 iwposition = float4 (position,1) * world;
    // calculate distance between current position and distance to color change origin
    float1 distance = length(iwposition.xyz - origin.xyz);
    distance /= 3; // adjust it slightly, just for aesthetic reasons
    factors = float4(0,0,0,1); // init factors

    // calculate factors of current colors
    float1 f1 = saturate((distance - timedelta) / spread);
    float1 f2 = saturate(-((distance - timedelta) - spread) / spread);
    // save in interpolation variable
    factors.x = f1;
    factors.y = f2;
    // calculate modified surface normal, if its on the color transition, it gets translated outside
    // Optional: inverted "-sin" to "sin" to make it a dent!
    // if we are outside the transition, so either f1 or f2 is zero, the length becomes 0!
    float4 mynormal = float4 (normal * world,1) * -sin(f1 * f2 / 2);
    // add normal to world position!
    iwposition = float4 (position,1) + mynormal;
    // return position of vertex for final rendering
    return iwposition * worldViewProj;
    // delete all temporal variables!
    delete iwposition;
    delete distance;
    delete mynormal;
}

private float4 fragmentProgram()
{
    // multiply current color with its factor
    float4 color = factors.x * currentcolor;
    // add second color, according to its strength factor
    color += factors.y * newcolor;
    // and return it!
    return color;
    // as always, free the registers
    delete color;
}

technique "perVertex"
{
    vertex vertexProgram();
    fragment fragmentProgram();
}

Lets start in the vertex program. First, we calculate the world position of the current vertex and the distance to the origin to the origin of the color change:

float4 iwposition = float4 (position,1) * world;
float1 distance = length(iwposition.xyz - origin.xyz);

Then, we calculate the factors of the colors. We calculate the color factors for each vertex just once and pass the values into an interpolated variable.

float1 f1 = saturate((distance - timedelta) / spread);
float1 f2 = saturate(-((distance - timedelta) - spread) / spread);

This calculates the smooth transition between them. We saturateeach result, to have it in the range of 0 to 1, which corrensponds with 0% and 100%. I created a little graphic to explain everything a little better:

Now we have the color factors, which are passed into the fragment part of the shader.

As the second step, we utilize the color factors to calculate the bulge.

float4 mynormal = float4 (normal * world,1) * -sin(f1 * f2 / 2);

If you look at the graphic, you see the light blue curve. Its what you approximately get, if you multiply f1 and f2 and calculate the SIN of that. Well, you won’t get exactly that result, but something like it. At least a bulge. We now multiply our result with the normal of the surface and add the resulting vector to the world position.

This translates our surface position outside along the surface normal, so it forms a neat bulge at the transition area. Here is another graphic, to make this (hopefully) a little bit more clear:

The part in the fragment is quite unexcited. The interpolated color-factors from the vertex program are used to multiply each color with it and then the results are added up. Thats it. As all important calculations are done in the vertex program, I don’t get into this in detail.

For the result there a some things to keep in mind, before using it:

You can get the full code in the Sample Code Project. You find it in the Files section. Its also available on Github. So if you like to contribute, you are more than welcome!

If you have questions, I didn’t explain anything well enough, made a mistake or explained something wrong, please let me know and leave a comment!