Sign in to follow this  
simotix

Signed distance field rendering (Blending question)

Recommended Posts

I have been working on implementing the Valve technique for rendering decals, although I am doing it for text. I have seen some workings on it while searching this forum, but I am confused about how it should be rendered.

For testing, I am rendering my image on quad (although I will split it up and batch the letters later). It is just a quad with a Vertex/Texture Coordinate signature. I am confused about two things for how it should be rendered.

1) Blending: From what I read, it should enable blending, and have D3D11_BLEND_SRC_ALPHA with D3D11_BLEND_INV_SRC_ALPHA. Although, I am not exactly aure. This is how I generate my blend state, is this the correct blend state?


D3D11_BLEND_DESC blendDesc;
blendDesc.AlphaToCoverageEnable = false;
blendDesc.IndependentBlendEnable = false;
for (UINT i = 0; i < 8; ++i)
{
blendDesc.RenderTarget[i].BlendEnable = true;
blendDesc.RenderTarget[i].BlendOp = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[i].BlendOpAlpha = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[i].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
blendDesc.RenderTarget[i].DestBlendAlpha = D3D11_BLEND_ONE;
blendDesc.RenderTarget[i].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
blendDesc.RenderTarget[i].SrcBlend = D3D11_BLEND_SRC_ALPHA;
blendDesc.RenderTarget[i].SrcBlendAlpha = D3D11_BLEND_ONE;
}
HRESULT hResult = m_pD3D11Device->CreateBlendState(&blendDesc, &m_pAlphaBlendState);



2) Texture Filtering. I create a basic Linear, with Wrapping sampler state. Is this would I should be creating?


ID3D11SamplerState *pSamplerLinear;
D3D11_SAMPLER_DESC sampDesc;
ZeroMemory( &sampDesc, sizeof(sampDesc) );
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HRESULT hResult = m_pD3D11Device->CreateSamplerState( &sampDesc, &pSamplerLinear );

Share this post


Link to post
Share on other sites
I suppose that I read something wrong, I was under the impression that I needed to blend.

I took your advice and I am now using clip(), is there any sort of state that I should be setting? Currently all I do is the following in the pixel shader.


float4 color = txDiffuse.Sample(samLinear, input.Tex);
clip(color.a - 0.5);
return color;



Also, I suppose my linear sampler state is fine?

Share this post


Link to post
Share on other sites
Unfortunately the text I am trying to render really does not look great. I am rendering a decal that I created using signed distance fields at several distances and anything with a curved surface does not look very good. I was wondering if anyone has ever experienced this? I am still unsure of my texture filtering is correct, so maybe that is the problem.

This is when I created it with a block size of 32. I tried 16 and 64, but I will still get bad curves (or even worse).

Share this post


Link to post
Share on other sites
For hard (aliased) edged objects, you can use clip.

If you want soft edges, then turn on blending like you were originally... but you've got to calculate a sensible alpha value in your shader. Something like:
color.a = smoothstep(0.48,0.52,color.a);

Share this post


Link to post
Share on other sites
Quote:
Original post by Hodgman
If you want soft edges, then turn on blending like you were originally... but you've got to calculate a sensible alpha value in your shader. Something like:
color.a = smoothstep(0.48,0.52,color.a);


That did improve the look of the letters a lot, but they still look very rough at a distance and letters like X look very jagged. Do you (or anyone else) have any suggestions to what I should do? I tried to increase my scan size (it was at 100, I tried it at 200) but that did not improve anything.

Share this post


Link to post
Share on other sites
The code used to generate the distance fields was done in C#, it will save a .png then I will render that .png in DirectX11.

Create signed distance bitmap

public static Bitmap CreateSignedDistanceBitmap(Bitmap bitmap, int scanSizeWidth, int scanSizeHeight, int blockSize)
{
int textureOutWidth = bitmap.Width / blockSize;
int textureOutHeight = bitmap.Height / blockSize;

Bitmap signedDistanceBitmap = new Bitmap(textureOutWidth, textureOutHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

int blockWidth = blockSize;
int blockHeight = blockSize;

float[,] signedDistances = new float[signedDistanceBitmap.Width, signedDistanceBitmap.Height];

float max = 0, min = 0;
for (int x = 0; x < signedDistanceBitmap.Width; ++x)
{
for (int y = 0; y < signedDistanceBitmap.Height; ++y)
{
float signedDistance = CalculateSignedDistance(bitmap,
(x * blockWidth) + (blockWidth / 2),
(y * blockHeight) + (blockHeight / 2),
scanSizeWidth, scanSizeHeight);

signedDistances[x, y] = signedDistance;
if (signedDistance != float.MaxValue && signedDistance > max)
max = signedDistance;
else if (signedDistance != float.MinValue && signedDistance < min)
min = signedDistance;
}

}

float scale = Math.Max(Math.Abs(min), Math.Abs(max));
for (int x = 0; x < textureOutWidth; ++x)
{
for (int y = 0; y < textureOutHeight; ++y)
{
float signedDistance = signedDistances[x, y];

if (signedDistance == float.MaxValue)
{
signedDistance = 1.0f;
}
else if (signedDistance == float.MinValue)
{
signedDistance = 0.0f;
}
else
{
signedDistance /= scale;
signedDistance /= 2;
signedDistance += 0.5f;
}

signedDistances[x, y] = signedDistance;

// Set the signed distance into the alpha channel
signedDistanceBitmap.SetPixel(x, y, Color.FromArgb((int)Math.Round(signedDistance * 255), 255, 255, 255));
}
}

return signedDistanceBitmap;
}



Calculate signed distance

public static float CalculateSignedDistance(Bitmap image, int imageX, int imageY, int scanWidth, int scanHeight)
{
Color baseColor = image.GetPixel(imageX, imageY);

// Check to see if it is a solid color, if it is a solid color then that means there is no texture on the texel
bool solidColor = baseColor.R > 0;

float closestDistance = float.MaxValue;
bool closestValid = false;

int startX = imageX - (scanWidth / 2);
int endX = startX + scanWidth;
int startY = imageY - (scanHeight / 2);
int endY = startY + scanHeight;

// No need in searching past the images bounds
if (startX < 0)
startX = 0;

if (endX >= image.Width)
endX = image.Width;

if (startY < 0)
startY = 0;

if (endY >= image.Height)
endY = image.Height;

for (int x = startX; x < endX; ++x)
{
for (int y = startY; y < endY; ++y)
{
Color texelColor = image.GetPixel(x, y);

if (solidColor)
{
if (texelColor.R == 0)
{
float dist = Separation(imageX, imageY, x, y);
if (dist < closestDistance)
{
closestDistance = dist;
closestValid = true;
}
}
}
else
{
if (texelColor.R > 0)
{
float dist = Separation(imageX, imageY, x, y);
if (dist < closestDistance)
{
closestDistance = dist;
closestValid = true;
}
}
}
}
}

if (solidColor)
{
if (closestValid)
return closestDistance;
else
return float.MaxValue;
}
else
{
if (closestValid)
return -closestDistance;
else
return float.MinValue;
}
}



Seperation

private static float Separation(float startX, float startY, float endX, float endY)
{
float x = startX - endX;
float y = startY - endY;

return (float)Math.Sqrt(x * x + y * y);
}

Share this post


Link to post
Share on other sites
Your blockSize stuff looks a bit suspect - I'd say that's whats causing the loss of detail from your high-res inputs. Did you get this concept of calculating the field in blocks from somewhere, or was it an optimisation you came up with?

In my tool, I generate the distance field with a blockSize of 1 (i.e. the distance field is calculated at high-resolution).
Then after you've got the high-res distance field, you shrink the image using a bilinear/box filter.

Share this post


Link to post
Share on other sites
I recall reading about the block size from a source that I can't seem to find at the moment, I did not think of that optimization my self.

How did you learn to create the distance fields? Currently with my method, if I run a block size of 1 (with a scan size of 200) it will seemingly never finish. I have had it running for six hours now ...

Share this post


Link to post
Share on other sites
Yeah it's a pretty intense process if you don't optimise the hell out of it.

There's not much to learn, it's a fairly straightforward idea - for each pixel, find the distance to the closest pixel with a different value.
for each pixel
closest = max number
for each other pixel
if pixel contents != other pixel contents
D = distance(this pixel, other pixel)
if D < closest
closest = D
The problem is that this is an O(n^2) algorithm -- that is, as you increase the number of pixels linearly, the workload increases exponentially.

By using a limited search area, it's O(n*m), which is still pretty bad.

Some other things you can try:
* Get rid of the square-root operation from the loop. You can perform the search using squared-distances, and then when you've got the final results, go through and perform a square-root for each distance value.

* Get rid of as many ifs as possible. Actually, get rid of as much of the code inside the inner loop (i.e. CalculateSignedDistance). You can simplify it quite a bit, though it'll still probably be unbearably slow...
        float CalculateDistance(Bitmap image, int imageX, int imageY, int scanSize)
{
Color baseColor = image.GetPixel(imageX, imageY);

float closestDistance = float.MaxValue;

int startX = imageX - halfScanSize;
int endX = startX + scanWidth;
int startY = imageY - halfScanSize;
int endY = startY + scanHeight;

// No need in searching past the images bounds
startX = max(0, startX);
startY = max(0, startX);
endX = min(image.Width, endX);
endY = min(image.Height, endY);

for (int x = startX; x < endX; ++x)
{
for (int y = startY; y < endY; ++y)
{
Color texelColor = image.GetPixel(x, y);
if (baseColor.R != texelColor.R)
{
float dx = imageX - x;
float dy = imageY - y;
float dist = dx*dx+dy*dy;
closestDistance = min(dist, closestDistance);
}
}
}
return closestDistance;
}

....
....

for (int x = 0; x < signedDistanceBitmap.Width; ++x)
{
for (int y = 0; y < signedDistanceBitmap.Height; ++y)
{
float distance = CalculateDistance(bitmap, x, y, halfScanSize);

signedDistances[x, y] = distance;
}
}
for (int x = 0; x < textureOutWidth; ++x)
{
for (int y = 0; y < textureOutHeight; ++y)
{
float distance = distances[x, y];

if (distance == float.MaxValue)
distance = 1.0f;
else
{
distance = Math.Sqrt(distance);
distance /= halfScanSize;
}

if(bitmap.GetPixel(x, y).R == 0)
distance = -distance;

distance /= 2;
distance += 0.5f;

// Set the signed distance into the alpha channel
signedDistanceBitmap.SetPixel(x, y, Color.FromArgb((int)Math.Round(distance * 255), 255, 255, 255));
}
}

* Another optimisation I did was to make an array that was a fraction of the size of the original, e.g. [Bitmap.Width/16, Bitmap.Height/16].
In this smaller array, I stored a value indicating whether each 16x16 block of pixels was all solid, all empty, or a mixture of both. Then I could use this information to do much less calculations on the blank areas of my font textures.

...but after doing all this, and getting my tool to run in a few minutes instead of hours, I just use Photoshop these days, which does it instantly ;/

Share this post


Link to post
Share on other sites
Yeah, the naive approach is indeed painful. Though probably not as fast as the photoshop/image processing hack, I found this quite useful:
The dead reckoning signed distance transform(pdf)
Since you are using C# - and the source code link in that paper's dead anyway - here's my implementation. I hope I successfully stripped it from my lib stuff.

    public class DistanceField2D
{
public DistanceField2D(int width, int height)
{
Width = width;
Height = height;
bitmap = new bool[width, height];
initDist = new float[width, height];
d = new float[width, height];
p = new Point[width, height];
Reset();
}
public static DistanceField2D FromImage(Bitmap bitmap)
{
return FromImage(bitmap, 128);
}
public static DistanceField2D FromImage(Bitmap bitmap, byte threshold)
{
DistanceField2D result = new DistanceField2D(bitmap.Width, bitmap.Height);
float scale = 1f / 255;
for (int y = 0; y < result.Width; y++)
{
for (int x = 0; x < result.Height; x++)
{
int colorValue = bitmap.GetPixel(x, y).R;
result.bitmap[x, y] = colorValue > threshold;
result.initDist[x, y] = colorValue * scale;
}
}
return result;
}

void Reset()
{
for (int y = 0; y < Height; y++)
{
for (int x = 0; x < Width; x++)
{
d[x, y] = float.MaxValue;
p[x, y] = new Point(-1, -1);
}
}
}
void DeadReckoningPropagateSafe(int x, int y, int dx, int dy, float distance)
{
if (Between(x + dx, 0, Width - 1) && Between(y + dy, 0, Height - 1))
DeadReckoningPropagate(x, y, dx, dy, distance);
}

void DeadReckoningPropagate(int x, int y, int dx, int dy, float distance)
{
if (d[x + dx, y + dy] + distance < d[x, y])
{
Point pp = p[x + dx, y + dy];
p[x, y] = pp;
d[x, y] = Distance(x - pp.X, y - pp.Y);
}
}

public void DeadReckoning()
{
Reset();
float d1 = 1f;
float d2 = Distance(1, 1);

// init
for (int y = 1; y < Height - 1; y++)
{
for (int x = 1; x < Width - 1; x++)
{
if (
bitmap[x, y] != bitmap[x + 1, y] ||
bitmap[x, y] != bitmap[x - 1, y] ||
bitmap[x, y] != bitmap[x, y - 1] ||
bitmap[x, y] != bitmap[x, y + 1]
)
{
d[x, y] = 0;
p[x, y] = new Point(x, y);
}
}
}

// forward pass
for (int y = 0; y < Height; y++)
{
for (int x = 01; x < Width; x++)
{
DeadReckoningPropagateSafe(x, y, -1, -1, d2);
DeadReckoningPropagateSafe(x, y, 0, -1, d1);
DeadReckoningPropagateSafe(x, y, +1, -1, d2);
DeadReckoningPropagateSafe(x, y, -1, 0, d1);
}
}

// backward pass
for (int y = Height - 2; y >= 0; y--)
{
for (int x = Width - 2; x > 0; x--)
{
DeadReckoningPropagate(x, y, +1, 0, d1);
DeadReckoningPropagate(x, y, -1, +1, d2);
DeadReckoningPropagate(x, y, 0, +1, d1);
DeadReckoningPropagate(x, y, +1, +1, d2);
}
}

// final pass, detect interiour/exterior
for (int y = 1; y < Height - 1; y++)
{
for (int x = 1; x < Width - 1; x++)
{
float v = d[x, y];
if (!bitmap[x, y])
{
v = -v;
}
d[x, y] = v;
}
}
}

public Bitmap ToImage()
{
return ToImage(1f / Width);
}

public Bitmap ToImage(float scale)
{
Bitmap result = new Bitmap(Width, Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
Graphics g = Graphics.FromImage(result);
g.Clear(Color.Black);
g.Dispose();
for (int y = 0; y < Height; y++)
{
for (int x = 0; x < Width; x++)
{
if (x == 0 || y == 0 || x == Width - 1 || y == Height - 1)
continue;
float v = d[x, y];
if (System.Math.Abs(v) < float.MaxValue)
{
int c = (int)(255f * System.Math.Max(0, System.Math.Min(1, v * scale + 0.5f)));
Color color = Color.FromArgb(c,c,c);
result.SetPixel(x, y, color);
}
}
}
return result;
}

bool Between(int value, int min, int max)
{
return value >= min && value <= max;
}
float Distance(int dx, int dy)
{
return (float)System.Math.Sqrt(dx * dx + dy * dy);
}

public bool[,] bitmap;
public float[,] initDist;
public float[,] d;
public Point[,] p;

public int Width;
public int Height;
}

And here's an example how to use:

	int size = 1024;
var original = new Bitmap(size, size);
var graphics = Graphics.FromImage(original);
System.Drawing.Font font = new System.Drawing.Font("Times New Roman", size * 0.4f);
string text = "X";
var f = graphics.MeasureString(text, font);
var p = new PointF();
p.X = (size - f.Width) * 0.5f;
p.Y = (size - f.Height) * 0.5f;
graphics.Clear(Color.Black);
graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixelGridFit;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
graphics.DrawString(text, font, Brushes.White, p);
graphics.Dispose();

var field = DistanceField2D.FromImage(original);
field.DeadReckoning();
var image = field.ToImage(4f / size); // <------ play with this value!!!
image.Save("big.png");

int smallSize = 64;
Bitmap small = new Bitmap(smallSize, smallSize);
graphics = Graphics.FromImage(small);
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half;
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
RectangleF destRectangle = new RectangleF(0, 0, smallSize, smallSize);
RectangleF sourceRectangle = new RectangleF(0, 0, image.Width, image.Height);
graphics.DrawImage(image, destRectangle, sourceRectangle, GraphicsUnit.Pixel);
graphics.Dispose();

small.Save("small.png");

This 1024x1024 image takes about 5 secs on my Intel DualCore 2GHz. Could probably optimized further if the need arises. Hodgman's last idea with the smaller array sounds interesting :)

Make sure to play around with the scale parameter of ToImage (or even the 0.5f offset in said function). I realized when playing with anti-alias/outline/shadow variants of the shader, sometimes I re-introduced jaggies.

Share this post


Link to post
Share on other sites
I am looking into both suggestions you gave, there is certaintly a lot of information to read.

Something I noticed is that there is an issue with my blending. I was moving the camera around and I noticed the images are over lapping the previous image. Does anyone know what could be causing this?

Share this post


Link to post
Share on other sites
That was exactly the issue, I figured that since these were going to be decals in the world and not GUI items that depth testing could still be on. Does depth testing need to be disabled for all objects with alpha?

Share this post


Link to post
Share on other sites
Has anyone ever created a full length character sheet with using any signed distance field? I am interested to know how big they made their initial image. I am doing a 4096x4096 which can take a little bit of time with any size. I am doing it so I may fit all 256 characters on it.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this