Related
Matrix operations performed on the GPU can be pretty hard to debug because GPU operations don't really allow for console logs.
I've written one designed for a real time 2D rendering engine based on a very simple form of I guess what could be called ray casting and am having trouble figuring out what's wrong with it (it's outputting [0,0,0,255,0,0,0,255,...] instead of populating colors).
this.thread.x is the index of the current unit (color channel) in the matrix being operated on.
scene is a buffer made up of 6-unit clumps, each value containing, in order:
The type of entity, always 1 for "sprite" in this case.
The sprite ID, corresponding the the index in this.constants.textures containing the buffer for the entity's sprite.
X offset, the left edge of the sprite
Y offset, the top edge of the sprite
width of the sprite
height of the sprite
bufferWidth is the width of the render area multiplied by 4 channels.
this.constants.textures is an array containing buffers of each sprite which the sprite IDs from the scene refer to.
Note: For those curious, this is being done with GPU.js, a JavaScript lib that converts a JS func into GLSL code to be run via WebGL.
function(scene, sceneLength, bufferWidth) {
var channel = this.thread.x % 4;
if (channel === 3) {
return 255;
}
var x = this.thread.x % bufferWidth;
var y = Math.floor(this.thread.x / bufferWidth);
for (let i1 = 0; i1 < sceneLength; i1 += 6) {
var id = scene[i1 + 1];
var x1 = scene[i1 + 2];
var y1 = scene[i1 + 3];
var w1 = scene[i1 + 4];
var h1 = scene[i1 + 5];
var r1 = scene[i1 + 6];
var offsetX1 = x1 - x;
if (offsetX1 > 0 && offsetX1 < w1) {
var offsetY1 = y1 - y;
if (offsetY1 > 0 && offsetY1 < h1) {
var c1 = offsetY1 * w1 * 4 + offsetX1 * 4;
var c1R = c1 - (c1 % 4);
var c1A = c1R + 3;
if (this.constants.textures[id][c1A] != 0) {
return this.constants.textures[id][c1];
}
}
}
}
return 0;
}
Explanation for the concept I'm trying to implement:
With a matrix operation, when you want to draw a sprite if you were to perform a pass on the entire render area, you'd be doing far more work than necessary. If you break the rendering area down into chunks and only update the sections involved in the sprite being drawn, that would be a fairly decent way to do it. It would certainly be good enough for real time game rendering. This would be a multi-pass approach, where sprites are rendered one at a time.
Alternatively, for what seems to me to be the most optimal approach possible, instead of that, we can use a single-pass approach that performs a single matrix operation for the entire rendering area, evaluating for each color channel what should be there based on doing a very basic form of collision detection with each sprite in the scene and the relevant pixel in that sprite.
You're calculating your sprite offsets backwards, the calculations should be:
var offsetX1 = x - x1;
and
var offsetY1 = y - y1;
The offsets should increase as x and y increase (assuming the sprite co-ordinates have the same co-ordinate system as the screen co-ordinates), so you shouldn't be subtracting x and y.
I´d like to get the tilt of the device, so i can use this to mesure the tilt os some surface, laying down the device over the surface.
Right now i am using Device Motion Plugin for xamarin forms from here https://github.com/rdelrosario/xamarin-plugins
and the code below:
CrossDeviceMotion.Current.Start(MotionSensorType.Accelerometer);
CrossDeviceMotion.Current.SensorValueChanged += (s, a) =>
{
switch (a.SensorType)
{
case MotionSensorType.Accelerometer:
{
Debug.WriteLine("A: {0},{1},{2}", ((MotionVector)a.Value).X, ((MotionVector)a.Value).Y,
((MotionVector)a.Value).Z);
Exposicao.Inclinacao = ((MotionVector)a.Value).Z;
break;
}
case MotionSensorType.Compass:
{
// Debug.WriteLine("H: {0}", a.Value);
Exposicao.Bussola = (double)a.Value.Value;
break;
}
}
};
The compass part is ok, the accelerometer part is working but there are some but´s.
If i am not wrong, i get the tilt in Z axis, so z.Value.Value.
This value is diferent for android and ios, lets focus in android.
z values are from 10 when device is laying down on flat surface, to 0 if device is stand up, lets focus only in just one quadrant.
I am doing something wrong to achieve what i explained?
How can i convert those values to a Angle between 0 and 90? It seems not linear, so the 5 does not seem 45 degrees.
Thanks
I'd probably roll out my own platform implementation for the feature you're looking for. The DeviceMotion library looks a bit simple for your purposes, as can be seen from the answer below. I'm pretty sure you can use it as a good starting point but it needs to be extended a little.
Android
On Android, you should use the Rotation Vector Sensor which uses a Kalman filter (with accelerometer, magnetometer and gyroscope) to get accurate measurements of the device's rotation:
The rotation vector represents the orientation of the device as a combination of an angle and an axis, in which the device has rotated through an angle θ around an axis (x, y, or z).
Image from the official Android documentation
iOS:
For iOS, you have to do a bit more work yourself. The key is to make use of CMAttitude, which describes the attitude of the device relative to an initial attitude. I found a snippet I've saved to my collection from an unknown source (can't credit the original author) here:
public void CalculateLeanAngle ()
{
motionManager = new CMMotionManager ();
motionManager.DeviceMotionUpdateInterval = 0.02;
if (motionManager.DeviceMotionAvailable) {
motionManager.StartDeviceMotionUpdates(CMAttitudeReferenceFrame.XArbitraryZVertical, NSOperationQueue.CurrentQueue, (data, error) => {
CMQuaternion quat = motionManager.DeviceMotion.Attitude.Quaternion;
double x = quat.x;
double y = quat.y;
double w = quat.w;
double z = quat.z;
double degrees = 0.0;
//Roll
double roll = Math.Atan2 (2 * y * w - 2 * x * z, 1 - 2 * y * y - 2 * z * z);
degrees = Math.Round (-applyKalmanFiltering (roll) * 180.0 / Constants.M_PI);
});
}
public double applyKalmanFiltering (double yaw)
{
if (motionLastYaw == 0)
motionLastYaw = yaw;
float q = 0.1f; // process noise
float r = 0.1f; // sensor noise
float p = 0.1f; // estimated error
float k = 0.5f; // kalman filter gain
double x = motionLastYaw;
p = p + q;
k = p / (p + r);
x = x + k * (yaw - x);
p = (1 - k) * p;
motionLastYaw = x;
return motionLastYaw;
}
Image from the official Xamarin documentation
I'll try to look for the original source when I have more time but I'm pretty sure this will work out of the box for your purposes.
Given two rgb colors and a rectangle, I'm able to create a basic linear gradient. This blog post gives very good explanation on how to create it. But I want to add one more variable to this algorithm, angle. I want to create linear gradient where I can specified the angle of the color.
For example, I have a rectangle (400x100). From color is red (255, 0, 0) and to color is green (0, 255, 0) and angle is 0°, so I will have the following color gradient.
Given I have the same rectangle, from color and to color. But this time I change angle to 45°. So I should have the following color gradient.
Your question actually consists of two parts:
How to generate a smooth color gradient between two colors.
How to render a gradient on an angle.
The intensity of the gradient must be constant in a perceptual color space or it will look unnaturally dark or light at points in the gradient. You can see this easily in a gradient based on simple interpolation of the sRGB values, particularly the red-green gradient is too dark in the middle. Using interpolation on linear values rather than gamma-corrected values makes the red-green gradient better, but at the expense of the back-white gradient. By separating the light intensities from the color you can get the best of both worlds.
Often when a perceptual color space is required, the Lab color space will be proposed. I think sometimes it goes too far, because it tries to accommodate the perception that blue is darker than an equivalent intensity of other colors such as yellow. This is true, but we are used to seeing this effect in our natural environment and in a gradient you end up with an overcompensation.
A power-law function of 0.43 was experimentally determined by researchers to be the best fit for relating gray light intensity to perceived brightness.
I have taken here the wonderful samples prepared by Ian Boyd and added my own proposed method at the end. I hope you'll agree that this new method is superior in all cases.
Algorithm MarkMix
Input:
color1: Color, (rgb) The first color to mix
color2: Color, (rgb) The second color to mix
mix: Number, (0..1) The mix ratio. 0 ==> pure Color1, 1 ==> pure Color2
Output:
color: Color, (rgb) The mixed color
//Convert each color component from 0..255 to 0..1
r1, g1, b1 ← Normalize(color1)
r2, g2, b2 ← Normalize(color1)
//Apply inverse sRGB companding to convert each channel into linear light
r1, g1, b1 ← sRGBInverseCompanding(r1, g1, b1)
r2, g2, b2 ← sRGBInverseCompanding(r2, g2, b2)
//Linearly interpolate r, g, b values using mix (0..1)
r ← LinearInterpolation(r1, r2, mix)
g ← LinearInterpolation(g1, g2, mix)
b ← LinearInterpolation(b1, b2, mix)
//Compute a measure of brightness of the two colors using empirically determined gamma
gamma ← 0.43
brightness1 ← Pow(r1+g1+b1, gamma)
brightness2 ← Pow(r2+g2+b2, gamma)
//Interpolate a new brightness value, and convert back to linear light
brightness ← LinearInterpolation(brightness1, brightness2, mix)
intensity ← Pow(brightness, 1/gamma)
//Apply adjustment factor to each rgb value based
if ((r+g+b) != 0) then
factor ← (intensity / (r+g+b))
r ← r * factor
g ← g * factor
b ← b * factor
end if
//Apply sRGB companding to convert from linear to perceptual light
r, g, b ← sRGBCompanding(r, g, b)
//Convert color components from 0..1 to 0..255
Result ← MakeColor(r, g, b)
End Algorithm MarkMix
Here's the code in Python:
def all_channels(func):
def wrapper(channel, *args, **kwargs):
try:
return func(channel, *args, **kwargs)
except TypeError:
return tuple(func(c, *args, **kwargs) for c in channel)
return wrapper
#all_channels
def to_sRGB_f(x):
''' Returns a sRGB value in the range [0,1]
for linear input in [0,1].
'''
return 12.92*x if x <= 0.0031308 else (1.055 * (x ** (1/2.4))) - 0.055
#all_channels
def to_sRGB(x):
''' Returns a sRGB value in the range [0,255]
for linear input in [0,1]
'''
return int(255.9999 * to_sRGB_f(x))
#all_channels
def from_sRGB(x):
''' Returns a linear value in the range [0,1]
for sRGB input in [0,255].
'''
x /= 255.0
if x <= 0.04045:
y = x / 12.92
else:
y = ((x + 0.055) / 1.055) ** 2.4
return y
def all_channels2(func):
def wrapper(channel1, channel2, *args, **kwargs):
try:
return func(channel1, channel2, *args, **kwargs)
except TypeError:
return tuple(func(c1, c2, *args, **kwargs) for c1,c2 in zip(channel1, channel2))
return wrapper
#all_channels2
def lerp(color1, color2, frac):
return color1 * (1 - frac) + color2 * frac
def perceptual_steps(color1, color2, steps):
gamma = .43
color1_lin = from_sRGB(color1)
bright1 = sum(color1_lin)**gamma
color2_lin = from_sRGB(color2)
bright2 = sum(color2_lin)**gamma
for step in range(steps):
intensity = lerp(bright1, bright2, step, steps) ** (1/gamma)
color = lerp(color1_lin, color2_lin, step, steps)
if sum(color) != 0:
color = [c * intensity / sum(color) for c in color]
color = to_sRGB(color)
yield color
Now for part 2 of your question. You need an equation to define the line that represents the midpoint of the gradient, and a distance from the line that corresponds to the endpoint colors of the gradient. It would be natural to put the endpoints at the farthest corners of the rectangle, but judging by your example in the question that is not what you did. I picked a distance of 71 pixels to approximate the example.
The code to generate the gradient needs to change slightly from what's shown above, to be a little more flexible. Instead of breaking the gradient into a fixed number of steps, it is calculated on a continuum based on the parameter t which ranges between 0.0 and 1.0.
class Line:
''' Defines a line of the form ax + by + c = 0 '''
def __init__(self, a, b, c=None):
if c is None:
x1,y1 = a
x2,y2 = b
a = y2 - y1
b = x1 - x2
c = x2*y1 - y2*x1
self.a = a
self.b = b
self.c = c
self.distance_multiplier = 1.0 / sqrt(a*a + b*b)
def distance(self, x, y):
''' Using the equation from
https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
modified so that the distance can be positive or negative depending
on which side of the line it's on.
'''
return (self.a * x + self.b * y + self.c) * self.distance_multiplier
class PerceptualGradient:
GAMMA = .43
def __init__(self, color1, color2):
self.color1_lin = from_sRGB(color1)
self.bright1 = sum(self.color1_lin)**self.GAMMA
self.color2_lin = from_sRGB(color2)
self.bright2 = sum(self.color2_lin)**self.GAMMA
def color(self, t):
''' Return the gradient color for a parameter in the range [0.0, 1.0].
'''
intensity = lerp(self.bright1, self.bright2, t) ** (1/self.GAMMA)
col = lerp(self.color1_lin, self.color2_lin, t)
total = sum(col)
if total != 0:
col = [c * intensity / total for c in col]
col = to_sRGB(col)
return col
def fill_gradient(im, gradient_color, line_distance=None, max_distance=None):
w, h = im.size
if line_distance is None:
def line_distance(x, y):
return x - ((w-1) / 2.0) # vertical line through the middle
ul = line_distance(0, 0)
ur = line_distance(w-1, 0)
ll = line_distance(0, h-1)
lr = line_distance(w-1, h-1)
if max_distance is None:
low = min([ul, ur, ll, lr])
high = max([ul, ur, ll, lr])
max_distance = min(abs(low), abs(high))
pix = im.load()
for y in range(h):
for x in range(w):
dist = line_distance(x, y)
ratio = 0.5 + 0.5 * dist / max_distance
ratio = max(0.0, min(1.0, ratio))
if ul > ur: ratio = 1.0 - ratio
pix[x, y] = gradient_color(ratio)
>>> w, h = 406, 101
>>> im = Image.new('RGB', [w, h])
>>> line = Line([w/2 - h/2, 0], [w/2 + h/2, h-1])
>>> grad = PerceptualGradient([252, 13, 27], [41, 253, 46])
>>> fill_gradient(im, grad.color, line.distance, 71)
And here's the result of the above:
I wanted to point out the common mistake that happens in color mixing when people try average the r, g, and b components:
R = (R1 + R2) / 2;
G = (G1 + G2) / 2;
B = (B1 + B2) / 2;
You can watch the excellent 4 Minute Physics video on the subject:
Computer Color is Broken
The short version is that trying to niavely mixing two colors by averaging the components is wrong:
R = R1*(1-mix) + R2*mix;
G = G1*(1-mix) + G2*mix;
B = B1*(1-mix) + B2*mix;
The problem is that RGB colors on computers are in the sRGB color space. And those numerical values have a gamma of approx 2.4 applied. In order to mix the colors correctly you must first undo this gamma adjustment:
undo the gamma adjustment
apply your r,g,b mixing algorithm above
reapply the gamma
Without applying the inverse gamma, the mixed colors are darker than they're supposed to be. This can be seen in a side-by-side color gradient experiment.
Top (wrong): without accounting for sRGB gamma
Bottom (right): with accounting for sRGB gamma
The algorithm
Rather than the naive:
//This is the wrong algorithm. Don't do this
Color ColorMixWrong(Color c1, Color c2, Single mix)
{
//Mix [0..1]
// 0 --> all c1
// 0.5 --> equal mix of c1 and c2
// 1 --> all c2
Color result;
result.r = c1.r*(1-mix) + c2.r*(mix);
result.g = c1.g*(1-mix) + c2.g*(mix);
result.b = c1.b*(1-mix) + c2.b*(mix);
return result;
}
The correct form is:
//This is the wrong algorithm. Don't do this
Color ColorMix(Color c1, Color c2, Single mix)
{
//Mix [0..1]
// 0 --> all c1
// 0.5 --> equal mix of c1 and c2
// 1 --> all c2
//Invert sRGB gamma compression
c1 = InverseSrgbCompanding(c1);
c2 = InverseSrgbCompanding(c2);
result.r = c1.r*(1-mix) + c2.r*(mix);
result.g = c1.g*(1-mix) + c2.g*(mix);
result.b = c1.b*(1-mix) + c2.b*(mix);
//Reapply sRGB gamma compression
result = SrgbCompanding(result);
return result;
}
The gamma adjustment of sRGB isn't quite just 2.4. They actually have a linear section near black - so it's a piecewise function.
Color InverseSrgbCompanding(Color c)
{
//Convert color from 0..255 to 0..1
Single r = c.r / 255;
Single g = c.g / 255;
Single b = c.b / 255;
//Inverse Red, Green, and Blue
if (r > 0.04045) r = Power((r+0.055)/1.055, 2.4) else r = r / 12.92;
if (g > 0.04045) g = Power((g+0.055)/1.055, 2.4) else g = g / 12.92;
if (b > 0.04045) b = Power((b+0.055)/1.055, 2.4) else b = b / 12.92;
//return new color. Convert 0..1 back into 0..255
Color result;
result.r = r*255;
result.g = g*255;
result.b = b*255;
return result;
}
And you re-apply the companding as:
Color SrgbCompanding(Color c)
{
//Convert color from 0..255 to 0..1
Single r = c.r / 255;
Single g = c.g / 255;
Single b = c.b / 255;
//Apply companding to Red, Green, and Blue
if (r > 0.0031308) r = 1.055*Power(r, 1/2.4)-0.055 else r = r * 12.92;
if (g > 0.0031308) g = 1.055*Power(g, 1/2.4)-0.055 else g = g * 12.92;
if (b > 0.0031308) b = 1.055*Power(b, 1/2.4)-0.055 else b = b * 12.92;
//return new color. Convert 0..1 back into 0..255
Color result;
result.r = r*255;
result.g = g*255;
result.b = b*255;
return result;
}
Update: Mark's right
I tested #MarkRansom comment that the color blending in linear RGB space is good when colors are equal RGB total value; but the linear blending scale does not seem linear - especially for the black-white case.
So i tried mixing in Lab color space, as my intuition suggested (as well as this photography stackexchange answer):
Mark's algorithm sometimes falls over
That's quite simple. Besides angle, you would actually need one more parameter, i.e. how tight/wide the gradient should be. Let's instead just work with two points:
__D
__--
__--
__--
__--
M
Where M is the middle point of the gradient (between red and green) and D shows the direction and distance. Therefore, the gradient becomes:
M'
| __D
| __--
| __--
| __--
| __--
M
__-- |
__-- |
__-- |
__-- |
D'-- |
M"
Which means, along the vector D'D, you change from red to green, linearly as you already know. Along the vector M'M", you keep the color constant.
That was the theory. Now implementation depends on how you actually draw the pixels. Let's assume nothing and say you want to decide the color pixel by pixel (so you can draw in any pixel order.)
That's simple! Let's take a point:
M'
| SA __D
__--| __--
P-- |__ A __--
| -- /| \ __--
| -- | |_--
| --M
|__-- |
__--CA |
__-- |
__-- |
D'-- |
M"
Point P, has angle A with the coordinate system defined by M and D. We know that along the vector M'M", the color doesn't change, so sin(A) doesn't have any significance. Instead, cos(A) shows relatively how far towards D or D' the pixels color should go to. The point CA shows |PM|cos(A) which means the mapping of P over the line defined by M and D, or in details the length of the line PM multiplied by cos(A).
So the algorithm becomes as follows
For every pixel
Calculate CA
If farther than D, definitely green. If before D', definitely red.
Else find the color from red to green based on the ratio of |D'CA|/|D'D|
Based on your comments, if you want to determine the wideness from the canvas size, you can easily calculate D based on your input angle and canvas size, although I personally advise using a separate parameter.
The way I solved this is first by being able to calculate L (lightness) for an RGB color: calculate only the Y (luminance) of CIE XYZ and use that to get L.
static private float rgbToL (float r, float g, float b) {
float Y = 0.21263900587151f * r + 0.71516867876775f * g + 0.072192315360733f * b;
return Y <= 0.0088564516f ? Y * 9.032962962f : 1.16f * (float)Math.pow(Y, 1 / 3f) - 0.16f;
}
That gives L as 0-1 for any RGB. Then to lerp RGB: first interpolate linear RGB, then fix lightness by lerping the start/end L and scale the RGB by targetL / resultL. I posted an Rgb class that does this.
The same library also has an Hsl class which stores a color as HSLuv. It does interpolation by converting to linear RGB, interpolating, converting back to HSLuv and then fixing the brightness by interpolating L from the start/end HSLuv colors.
The comment of #user2799037 is totally correct:
each line is moved by some pixels to the right compared to the previous one.
The actual constant can be computed as the tangent of the angle you specified.
Is there a simple complete code example using any gui toolkit (that will work in both Linux and Windows) of opening multiple opengl windows simultaneously? And how to handle their events and such separately of course. I tried it naively and it crashes.
I received a full working source code example from someone outside of stackoverflow. I'm pasting it here for all to benefit.
module Main where
import Graphics.UI.GLUT
import System.Exit (exitWith, ExitCode(ExitSuccess))
reshape :: ReshapeCallback
reshape size = do
viewport $= (Position 0 0, size)
matrixMode $= Projection
loadIdentity
frustum (-1) 1 (-1) 1 1.5 20
matrixMode $= Modelview 0
keyboard :: KeyboardMouseCallback
keyboard (Char '\27') Down _ _ = exitWith ExitSuccess
keyboard _ _ _ _ = return ()
renderCube :: Color3 GLfloat -> IO ()
renderCube c = do
clear [ ColorBuffer ]
let color3f = color :: Color3 GLfloat -> IO ()
scalef = scale :: GLfloat -> GLfloat -> GLfloat -> IO ()
color3f c
loadIdentity
lookAt (Vertex3 0 0 5) (Vertex3 0 0 0) (Vector3 0 1 0)
scalef 1 2 1
renderObject Wireframe (Cube 1)
flush
displayR :: DisplayCallback
displayR = renderCube (Color3 1 0 0)
displayB :: DisplayCallback
displayB = renderCube (Color3 0 0 1)
createWindowWithDisplayFunc :: String -> Position -> DisplayCallback -> IO Window
createWindowWithDisplayFunc name pos display = do
win <- createWindow name
windowPosition $= pos
clearColor $= Color4 0 0 0 0
shadeModel $= Flat
displayCallback $= display
reshapeCallback $= Just reshape
keyboardMouseCallback $= Just keyboard
return win
main = do
getArgsAndInitialize
initialDisplayMode $= [ SingleBuffered, RGBMode ]
initialWindowSize $= Size 100 100
initialWindowPosition $= Position 100 100
createWindowWithDisplayFunc "R" (Position 10 10) displayR
createWindowWithDisplayFunc "B" (Position 110 10) displayB
mainLoop
GLUT, of course.
The GLUT homepage states
The toolkit supports:
- Multiple windows for OpenGL rendering
- Callback driven event processing
- Sophisticated input devices
- An 'idle' routine and timers
- A simple, cascading pop-up menu facility
- Utility routines to generate various solid and wire frame objects
- Support for bitmap and stroke fonts
- Miscellaneous window management functions
Hence you can use GLUT for managing multiple windows (I had used once). Here is a tutorial for what you need.
I've also found this article which you may look a little, since it's Haskell specific.
OpenGL support in wxWidgets uses the WxGLCanvas class, which is in wxcore as GLCanvas. Unfortunately, it doesn't seem to exist in the wx package. You can probably implement your own control for GLCanvas without too much difficulty, using the other controls in the wx package and C++ usage examples as a reference.
I'm currently writing a program to generate really enormous (65536x65536 pixels and above) Mandelbrot images, and I'd like to devise a spectrum and coloring scheme that does them justice. The wikipedia featured mandelbrot image seems like an excellent example, especially how the palette remains varied at all zoom levels of the sequence. I'm not sure if it's rotating the palette or doing some other trick to achieve this, though.
I'm familiar with the smooth coloring algorithm for the mandelbrot set, so I can avoid banding, but I still need a way to assign colors to output values from this algorithm.
The images I'm generating are pyramidal (eg, a series of images, each of which has half the dimensions of the previous one), so I can use a rotating palette of some sort, as long as the change in the palette between subsequent zoom levels isn't too obvious.
This is the smooth color algorithm:
Lets say you start with the complex number z0 and iterate n times until it escapes. Let the end point be zn.
A smooth value would be
nsmooth := n + 1 - Math.log(Math.log(zn.abs()))/Math.log(2)
This only works for mandelbrot, if you want to compute a smooth function for julia sets, then use
Complex z = new Complex(x,y);
double smoothcolor = Math.exp(-z.abs());
for(i=0;i<max_iter && z.abs() < 30;i++) {
z = f(z);
smoothcolor += Math.exp(-z.abs());
}
Then smoothcolor is in the interval (0,max_iter).
Divide smoothcolor with max_iter to get a value between 0 and 1.
To get a smooth color from the value:
This can be called, for example (in Java):
Color.HSBtoRGB(0.95f + 10 * smoothcolor ,0.6f,1.0f);
since the first value in HSB color parameters is used to define the color from the color circle.
Use the smooth coloring algorithm to calculate all of the values within the viewport, then map your palette from the lowest to highest value. Thus, as you zoom in and the higher values are no longer visible, the palette will scale down as well. With the same constants for n and B you will end up with a range of 0.0 to 1.0 for a fully zoomed out set, but at deeper zooms the dynamic range will shrink, to say 0.0 to 0.1 at 200% zoom, 0.0 to 0.0001 at 20000% zoom, etc.
Here is a typical inner loop for a naive Mandelbrot generator. To get a smooth colour you want to pass in the real and complex "lengths" and the iteration you bailed out at. I've included the Mandelbrot code so you can see which vars to use to calculate the colour.
for (ix = 0; ix < panelMain.Width; ix++)
{
cx = cxMin + (double )ix * pixelWidth;
// init this go
zx = 0.0;
zy = 0.0;
zx2 = 0.0;
zy2 = 0.0;
for (i = 0; i < iterationMax && ((zx2 + zy2) < er2); i++)
{
zy = zx * zy * 2.0 + cy;
zx = zx2 - zy2 + cx;
zx2 = zx * zx;
zy2 = zy * zy;
}
if (i == iterationMax)
{
// interior, part of set, black
// set colour to black
g.FillRectangle(sbBlack, ix, iy, 1, 1);
}
else
{
// outside, set colour proportional to time/distance it took to converge
// set colour not black
SolidBrush sbNeato = new SolidBrush(MapColor(i, zx2, zy2));
g.FillRectangle(sbNeato, ix, iy, 1, 1);
}
and MapColor below: (see this link to get the ColorFromHSV function)
private Color MapColor(int i, double r, double c)
{
double di=(double )i;
double zn;
double hue;
zn = Math.Sqrt(r + c);
hue = di + 1.0 - Math.Log(Math.Log(Math.Abs(zn))) / Math.Log(2.0); // 2 is escape radius
hue = 0.95 + 20.0 * hue; // adjust to make it prettier
// the hsv function expects values from 0 to 360
while (hue > 360.0)
hue -= 360.0;
while (hue < 0.0)
hue += 360.0;
return ColorFromHSV(hue, 0.8, 1.0);
}
MapColour is "smoothing" the bailout values from 0 to 1 which then can be used to map a colour without horrible banding. Playing with MapColour and/or the hsv function lets you alter what colours are used.
Seems simple to do by trial and error. Assume you can define HSV1 and HSV2 (hue, saturation, value) of the endpoint colors you wish to use (black and white; blue and yellow; dark red and light green; etc.), and assume you have an algorithm to assign a value P between 0.0 and 1.0 to each of your pixels. Then that pixel's color becomes
(H2 - H1) * P + H1 = HP
(S2 - S1) * P + S1 = SP
(V2 - V1) * P + V1 = VP
With that done, just observe the results and see how you like them. If the algorithm to assign P is continuous, then the gradient should be smooth as well.
My eventual solution was to create a nice looking (and fairly large) palette and store it as a constant array in the source, then interpolate between indexes in it using the smooth coloring algorithm. The palette wraps (and is designed to be continuous), but this doesn't appear to matter much.
What's going on with the color mapping in that image is that it's using a 'log transfer function' on the index (according to documentation). How exactly it's doing it I still haven't figured out yet. The program that produced it uses a palette of 400 colors, so index ranges [0,399), wrapping around if needed. I've managed to get pretty close to matching it's behavior. I use an index range of [0,1) and map it like so:
double value = Math.log(0.021 * (iteration + delta + 60)) + 0.72;
value = value - Math.floor(value);
It's kind of odd that I have to use these special constants in there to get my results to match, since I doubt they do any of that. But whatever works in the end, right?
here you can find a version with javascript
usage :
var rgbcol = [] ;
var rgbcol = MapColor ( Iteration , Zy2,Zx2 ) ;
point ( ctx , iX, iY ,rgbcol[0],rgbcol[1],rgbcol[2] );
function
/*
* The Mandelbrot Set, in HTML5 canvas and javascript.
* https://github.com/cslarsen/mandelbrot-js
*
* Copyright (C) 2012 Christian Stigen Larsen
*/
/*
* Convert hue-saturation-value/luminosity to RGB.
*
* Input ranges:
* H = [0, 360] (integer degrees)
* S = [0.0, 1.0] (float)
* V = [0.0, 1.0] (float)
*/
function hsv_to_rgb(h, s, v)
{
if ( v > 1.0 ) v = 1.0;
var hp = h/60.0;
var c = v * s;
var x = c*(1 - Math.abs((hp % 2) - 1));
var rgb = [0,0,0];
if ( 0<=hp && hp<1 ) rgb = [c, x, 0];
if ( 1<=hp && hp<2 ) rgb = [x, c, 0];
if ( 2<=hp && hp<3 ) rgb = [0, c, x];
if ( 3<=hp && hp<4 ) rgb = [0, x, c];
if ( 4<=hp && hp<5 ) rgb = [x, 0, c];
if ( 5<=hp && hp<6 ) rgb = [c, 0, x];
var m = v - c;
rgb[0] += m;
rgb[1] += m;
rgb[2] += m;
rgb[0] *= 255;
rgb[1] *= 255;
rgb[2] *= 255;
rgb[0] = parseInt ( rgb[0] );
rgb[1] = parseInt ( rgb[1] );
rgb[2] = parseInt ( rgb[2] );
return rgb;
}
// http://stackoverflow.com/questions/369438/smooth-spectrum-for-mandelbrot-set-rendering
// alex russel : http://stackoverflow.com/users/2146829/alex-russell
function MapColor(i,r,c)
{
var di= i;
var zn;
var hue;
zn = Math.sqrt(r + c);
hue = di + 1.0 - Math.log(Math.log(Math.abs(zn))) / Math.log(2.0); // 2 is escape radius
hue = 0.95 + 20.0 * hue; // adjust to make it prettier
// the hsv function expects values from 0 to 360
while (hue > 360.0)
hue -= 360.0;
while (hue < 0.0)
hue += 360.0;
return hsv_to_rgb(hue, 0.8, 1.0);
}