I am attempting to create randomised terrain meshes as I have done so in the screenshot below:
However, the issue I am facing is when attempting to reduces the number of triangles and vertices (Level of Detail).
I understand that to do this I can just skip over vertices.
for example:
The above mesh is full detail in that the vertices are generated like so:
0->1->2->3->4->5->6->7->8->9->...
and to generate a lower level of detail i can skip vertices as long as the skipping of vertices does not exceed the length of vertices so i could do the following generation to lower detail:
0->2->4->6->8->10->12->14->16->...
or:
0->4->8->12->16->20->24->28->32->...
Using a 2D array and a nested loop makes this trivial as each iteration on 2D coordinates x/y can be incremented by the increment: 1, 2, 4, 8, however, i am dealing with 2D arrays in 1D format.
I have the following code which executes and almost correctly generates the above mesh in the screenshot above.
Unfortunately it does seem to be missing one line of of vertices on the top left (3d z axis) as seen below:
One caveat to the Execute(int, int) method below is that any access to the NativeArray which is not labeled [ReadOnly] will throw an exception if the array is accessing indexes outside of it's batch size.
public struct int6
{
public int a, b, c, d, e, f;
public int6(int a, int b, int c, int d, int e, int f) { this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; }
}
public class MeshGeneratorJob2
{
[ReadOnly] public static int width = 241;
[ReadOnly] public static int height = 241;
[ReadOnly] public static float topLeftX = (width - 1) / -2f;
[ReadOnly] public static float topLeftZ = (height - 1) / 2f;
[ReadOnly] public static NativeArray<float> heightMap = new NativeArray<float>(width * height, Allocator.TempJob);
public static NativeArray<float> heightCurveSamples;
public static NativeArray<float3> vertices = new NativeArray<float3>(width * height, Allocator.TempJob);
public static NativeArray<int6> triangles = new NativeArray<int6>((width - 1) * (height - 1), Allocator.TempJob);
public static NativeArray<float2> uvs = new NativeArray<float2>(width * height, Allocator.TempJob);
public void Execute()
{
for (int i = 0; i < vertices.Length; i += 5)
{
Execute(i, 5);
}
}
private void Execute(int startIndex, int count)
{
for (int vertexIndex = startIndex; vertexIndex < startIndex + count; vertexIndex++)
{
int x = vertexIndex % width;
int y = vertexIndex / width;
vertices[vertexIndex] = new float3(topLeftX + x, heightMap[vertexIndex] * 16.67f, topLeftZ - y);
uvs[vertexIndex] = new float2(x / (float)width, y / (float)height);
if (vertexIndex < triangles.Length && x < width - 1 && y < height - 1)
{
triangles[vertexIndex] = new int6(vertexIndex, vertexIndex + width + 1, vertexIndex + width,
vertexIndex + width + 1, vertexIndex, vertexIndex + 1);
}
}
}
}
I have come up with the following solution to this problem:
The first issue i solved was using a nested for loop y, x with y always starting at startIndex.
this, however, caused an issue as the vertexIndex could be higher than the length of the triangles length, so i calculated the current vertexIndex at the supplied startIndex as follows:
Here i introduced an incrementer value which increments both the x and y loops rather than y++, x++ however in this example incrementer is 1 which is essentially the same thing.
int vertexIndex = (int)(math.ceil((float)width / incrementer) * math.ceil((float)startIndex / incrementer));
however calculating the vertexIndex caused another issue which again caused out of bounds exceptions on setting the vertices.
This was due to the startIndex being incremented by count, where count was not the same as the incrementer.
To solve this I at the start of the method added the following code to round the startIndex up to the next incremental count if needed.
startIndex += startIndex % incrementer;
and altogether i then get the following code:
public struct int6
{
public int a, b, c, d, e, f;
public int6(int a, int b, int c, int d, int e, int f) { this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; }
}
public class MeshGeneratorJob2
{
public static int width = 241;
public static int height = 241;
public static float topLeftX = (width - 1) / -2f;
public static float topLeftZ = (height - 1) / 2f;
public static int increment = 1;
public static NativeArray<float> heightMap = new NativeArray<float>(width * height, Allocator.TempJob);
public static NativeArray<float> heightCurveSamples;
public static NativeArray<float3> vertices = new NativeArray<float3>(width * height, Allocator.TempJob);
public static NativeArray<int6> triangles = new NativeArray<int6>((width - 1) * (height - 1), Allocator.TempJob);
public static NativeArray<float2> uvs = new NativeArray<float2>(width * height, Allocator.TempJob);
public void Execute()
{
for (int i = 0; i < vertices.Length; i += 5)
{
Execute(i, 5);
}
}
private void Execute(int startIndex, int count)
{
startIndex += startIndex % increment;
int vertexIndex = (int)(math.ceil((float)width / increment) * math.ceil((float)startIndex / increment));
for (int y = startIndex; y < startIndex + count && y < height; y++)
{
for (int x = 0; x < width; x += increment)
{
vertices[vertexIndex] = new float3(topLeftX + x, heightMap[vertexIndex] * 16.67f, topLeftZ - y);
uvs[vertexIndex] = new float2(x / (float)width, y / (float)height);
if (vertexIndex < triangles.Length && x < width - 1 && y < height - 1)
{
triangles[vertexIndex] = new int6(vertexIndex, vertexIndex + width + 1, vertexIndex + width,
vertexIndex + width + 1, vertexIndex, vertexIndex + 1);
}
vertexIndex++;
}
}
}
}
Related
I have a piece of code in C# that populates an array with values. In the Awake method I set the length of the values array, yet when I try to access it in the setSample method it returns an IndexOutOfRangeException. The
public int gridSize = 32;
public int width;
public int height;
public int featureSize = 32;
public float[] values;
public void Awake () {
width = gridSize;
height = gridSize;
float[] values = new float[6 * width * height];
Debug.Log("Array length: " + values.Length);
for (int y = 0; y < height; y += featureSize) {
for (int x = 0; x < width; x += featureSize) {
setSample(x, y, Random.value);
}
}
}
public void setSample (int x, int y, float value) {
Debug.Log("Array length: " + values.Length);
values[((x & (width - 1)) + (y & (height - 1)) * gridSize)] = value;
}
I added the Debug.Log() lines, which gave me the following output:
Array length: 6144
Array length: 0
IndexOutOfRangeException: Array index is out of range.
Both the methods and the variables are public, so I don't see why there should be any access issues. Why does the array change after I declare it? Is it because it is full of null values?
Instead of declaring again you should use existing values, inside the awake method.
change
From
float[] values = new float[6 * width * height];
to
values = new float[6 * width * height];
About
I’m using WinForms. In my form, I have a picturebox. The picturebox size mode is set to zoom. I use the picturebox to view TIF images. The TIF images are grayscale (Black and White ONLY).
What My App Does
My application finds the first black pixel and the last black pixel in the document and draws a red rectangle around it. In hopes that it would draw the rectangle around the content of the image.
The Problem
Sometimes the TIF documents have spots/dots around the content of the image. This throws my application off. It doesn't know where the content of the image begins and ends. How can I find the content of the TIF documents and draw a rectangle around it if the document has spots/dots?
About the Document
Black and white only (1 bit depth)
TIF document
Document always has letters and numbers
The letters and numbers are always bigger than the spots/dots
There can be multiple spots all over the image even in the content
The background is always white
The content is always black
The spots are also black
Download Test Image Links:
• File Dropper: http://www.filedropper.com/test-tifs
• Rapid Share: https://ufile.io/2qiir
What I Found
Upon my research, I found AForge.Imaging library which has many imaging filters that may potentially help me achieve my goal. I'm thinking about removing the spots/dots using the median filter or use the other filters to achieve the desired result.
What I Tried
I tried applying the median filter from AForge library to get rid of the spots but that only got rid of some of the spots. I had to repeat replying the filter multiple times to get rid of MOST of the spots to find the content and it still had a hard time finding the content. That method didn't work too well for me.
Link to AForge Filters: http://www.aforgenet.com/framework/docs/html/cdf93487-0659-e371-fed9-3b216efb6954.htm
Code
private void btn_Draw_Click(object sender, EventArgs e)
{
// Wrap the creation of the OpenFileDialog instance in a using statement,
// rather than manually calling the Dispose method to ensure proper disposal
using (OpenFileDialog dlg = new OpenFileDialog())
{
if (dlg.ShowDialog() == DialogResult.OK)
{
pictureBox1.Image = new Bitmap(dlg.FileName);
int xMax = pictureBox1.Image.Width;
int yMax = pictureBox1.Image.Height;
startX = Int32.MaxValue;
startY = Int32.MaxValue;
endX = Int32.MinValue;
endY = Int32.MinValue;
using (Bitmap bmp = new Bitmap(pictureBox1.Image))
{
for (var y = 0; y < yMax; y+=3)
{
for (var x = 0; x < xMax; x+=3)
{
Color col = bmp.GetPixel(x, y);
if(col.ToArgb() == Color.Black.ToArgb())
{
// Finds first black pixel
if (x < startX)
startX = x;
if(y < startY)
startY = y;
// Finds last black pixel
if (x > endX)
endX = x;
if (y > endY)
endY = y;
}
}
}
int picWidth = pictureBox1.Size.Width;
int picHeight = pictureBox1.Size.Height;
float imageRatio = xMax / (float)yMax; // image W:H ratio
float containerRatio = picWidth / (float)picHeight; // container W:H ratio
if (imageRatio >= containerRatio)
{
// horizontal image
float scaleFactor = picWidth / (float)xMax;
float scaledHeight = yMax * scaleFactor;
// calculate gap between top of container and top of image
float filler = Math.Abs(picHeight - scaledHeight) / 2;
//float filler = 0;
startX = (int)(startX * scaleFactor);
endX = (int)(endX * scaleFactor);
startY = (int)((startY) * scaleFactor + filler);
endY = (int)((endY) * scaleFactor + filler);
}
else
{
// vertical image
float scaleFactor = picHeight / (float)yMax;
float scaledWidth = xMax * scaleFactor;
float filler = Math.Abs(picWidth - scaledWidth) / 2;
startX = (int)((startX) * scaleFactor + filler);
endX = (int)((endX) * scaleFactor + filler);
startY = (int)(startY * scaleFactor);
endY = (int)(endY * scaleFactor);
}
//var scaleX = picWidth / (float)xMax;
//var scaleY = picHeight / (float)yMax;
//startX = (int)Math.Round(startX * scaleX);
//startY = (int)Math.Round(startY * scaleY);
//endX = (int)Math.Round(endX * scaleX);
//endY = (int)Math.Round(endY * scaleY);
}
}
}
}
private bool _once = true;
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (_once)
{
//Rectangle ee = new Rectangle(35, 183, 405, 157);
Rectangle ee = new Rectangle(startX, startY, endX - startX, endY - startY);
System.Diagnostics.Debug.WriteLine(startX + ", " + startY + ", " + (endX - startX) + ", " + (endY - startY));
using (Pen pen = new Pen(Color.Red, 2))
{
e.Graphics.DrawRectangle(pen, ee);
}
//_once = false;
}
}
Tif document that DOES NOT HAVE any spots and dots around content
Tif document that HAS spots and dots around content
Example Image 1:
Example Image 2
:
Example Image 3
Following experiment seems to meet all your requirements.
I put following controls onto Form1
A MenuStrip: Docking=Top, with 2 MenuItems - one to open a file, second to run an algorithm
A progressbar: Docking=Top, to watch performance of loading and algorithm
A panel with Docking=Fill and AutoScroll=true
A picture into the panel, Point(0,0), the rest by default. SizeMode=Normal.
Update
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
// Opens an image file.
private void openToolStripMenuItem_Click(object sender, EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
this.image = Image.FromFile(dlg.FileName) as Bitmap;
this.pictureBox1.Image = image;
this.pictureBox1.Invalidate();
}
}
Bitmap image;
// finds top, left, right and bottom bounds of the content in TIFF file.
//
private void findBoundsToolStripMenuItem_Click(object sender, EventArgs e)
{
int contentSize = 70;
this.left = 0;
this.top = 0;
this.right = this.pictureBox1.Width - 1;
this.bottom = this.pictureBox1.Height - 1;
int h = image.Height;
int w = image.Width;
this.progressBar1.Value = 0;
this.progressBar1.Maximum = 4;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
if (this.image.GetPixel(x, y).ToArgb() == Black)
{
int size = this.image.GetBlackRegionSize(x, y);
if (this.image.GetBlackRegionSize(x, y) > contentSize)
{
this.top = y;
goto label10;
}
}
}
}
label10:
this.progressBar1.Increment(1);
for (int y = h - 1; y >= 0; y--)
{
for (int x = 0; x < w; x++)
{
if (this.image.GetPixel(x, y).ToArgb() == Black)
{
if (this.image.GetBlackRegionSize(x, y) > contentSize)
{
this.bottom = y;
goto label11;
}
}
}
}
label11:
this.progressBar1.Increment(1);
for (int x = 0; x < w; x++)
{
for (int y = 0; y < h; y++)
{
if (this.image.GetPixel(x, y).ToArgb() == Black)
{
if (this.image.GetBlackRegionSize(x, y) > contentSize)
{
this.left = x;
goto label12;
}
}
}
}
label12:
this.progressBar1.Increment(1);
for (int x = w - 1; x >= 0; x--)
{
for (int y = 0; y < h; y++)
{
if (this.image.GetPixel(x, y).ToArgb() == Black)
{
if (this.image.GetBlackRegionSize(x, y) > contentSize)
{
this.right = x;
goto label13;
}
}
}
}
label13:
this.progressBar1.Increment(1);
this.pictureBox1.Invalidate();
}
internal static readonly int Black = Color.Black.ToArgb();
internal static readonly int White = Color.White.ToArgb();
int top;
int bottom;
int left;
int right;
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (pictureBox1.Image == null)
{
return;
}
int xMax = pictureBox1.Image.Width;
int yMax = pictureBox1.Image.Height;
int startX = this.left;
int startY = this.top;
int endX = this.right;
int endY = this.bottom;
int picWidth = pictureBox1.Size.Width;
int picHeight = pictureBox1.Size.Height;
float imageRatio = xMax / (float)yMax; // image W:H ratio
float containerRatio = picWidth / (float)picHeight; // container W:H ratio
if (imageRatio >= containerRatio)
{
// horizontal image
float scaleFactor = picWidth / (float)xMax;
float scaledHeight = yMax * scaleFactor;
// calculate gap between top of container and top of image
float filler = Math.Abs(picHeight - scaledHeight) / 2;
//float filler = 0;
startX = (int)(startX * scaleFactor);
endX = (int)(endX * scaleFactor);
startY = (int)((startY) * scaleFactor + filler);
endY = (int)((endY) * scaleFactor + filler);
}
else
{
// vertical image
float scaleFactor = picHeight / (float)yMax;
float scaledWidth = xMax * scaleFactor;
float filler = Math.Abs(picWidth - scaledWidth) / 2;
startX = (int)((startX) * scaleFactor + filler);
endX = (int)((endX) * scaleFactor + filler);
startY = (int)(startY * scaleFactor);
endY = (int)(endY * scaleFactor);
}
//if (_once)
//Rectangle ee = new Rectangle(35, 183, 405, 157);
Rectangle ee = new Rectangle(startX, startY, endX - startX, endY - startY);
System.Diagnostics.Debug.WriteLine(startX + ", " + startY + ", " + (endX - startX) + ", " + (endY - startY));
using (Pen pen = new Pen(Color.Red, 2))
{
e.Graphics.DrawRectangle(pen, ee);
}
//_once = false;
}
}
static class BitmapHelper
{
internal static int GetBlackRegionSize(this Bitmap image, int x, int y)
{
int size = 0;
GetRegionSize(image, new List<Point>(), x, y, 0, ref size);
return size;
}
// this constant prevents StackOverFlow exception.
// also it has effect on performance.
// It's value must be greater than the value of contentSize defined in findBoundsToolStripMenuItem_Click(object sender, EventArgs e) method.
const int MAXLEVEL = 100;
static void GetRegionSize(this Bitmap image, List<Point> list, int x, int y, int level, ref int size)
{
if (x >= image.Width || x < 0 || y >= image.Height || y < 0 || list.Contains(x, y) || image.GetPixel(x, y).ToArgb() != Form1.Black || level > MAXLEVEL)
{
return;
}
if (size < level)
{
size = level;
}
list.Add(new Point(x, y));
image.GetRegionSize(list, x, y - 1, level + 1, ref size);
image.GetRegionSize(list, x, y + 1, level + 1, ref size);
image.GetRegionSize(list, x - 1, y, level + 1, ref size);
image.GetRegionSize(list, x + 1, y, level + 1, ref size);
}
static bool Contains(this List<Point> list, int x, int y)
{
foreach (Point point in list)
{
if (point.X == x && point.Y == y)
{
return true;
}
}
return false;
}
}
}
"this.pictureBox1.Size = image.Size;" has been removed. Paint event handler's code changed. PictureBox size mode can be set to Zoom now.
Update 2
I tried to simplify code and increase performance.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace WindowsFormsApplication3
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.pictureBox1.Paint += new PaintEventHandler(this.pictureBox1_Paint);
}
// Opens an image file
// and runs "FindBounds()" method
private void openToolStripMenuItem_Click(object sender, EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
this.image = Image.FromFile(dlg.FileName) as Bitmap;
FindBounds();
this.pictureBox1.Image = image;
this.pictureBox1.Invalidate();
}
}
Bitmap image;
// Possible maximum side of a spot or a dot in the image
int maxSpotOrDotSide = 7;
// Finds top, left, right and bottom bounds of the content in TIFF file.
private void FindBounds()
{
// Possible maximum area of a spot or a dot in the image
int maxSpotOrDotArea = maxSpotOrDotSide * maxSpotOrDotSide;
this.left = 0;
this.top = 0;
this.right = this.pictureBox1.Width - 1;
this.bottom = this.pictureBox1.Height - 1;
int h = image.Height;
int w = image.Width;
int num = w * h;
// Incrementers. I tested with greater values
// like "x = 2", "x = 5" and it increased performance.
// But we must be carefull as this may cause skipping content.
int dx = 1; // Incrementer for "x"
int dy = 1; // Incrementer for "y"
// Initialization of "progressBar1"
this.progressBar1.Value = 0;
this.progressBar1.Maximum = num;
// Content of the image
BlackContent imageContent = null;
// Here we will scan pixels of the image
// starting from top left corner and
// finishing at bottom right
for (int y = 0; y < h; y += dx)
{
for (int x = 0; x < w; x += dy)
{
int val = y * w + x;
this.progressBar1.Value = val > num ? num : val;
// This block skips scanning imageContent
// thus should increase performance.
if (imageContent != null && imageContent.Contains(x, y))
{
x = imageContent.Right;
continue;
}
// Interesting things begin to happen
// after we detect the first Black pixel
if (this.image.GetPixel(x, y).ToArgb() == Black)
{
BlackContent content = new BlackContent(x, y);
// Start Flood-Fill algorithm
content.FloodFill(this.image);
if (content.Area > maxSpotOrDotArea)
{
if (imageContent == null)
{
imageContent = content;
}
imageContent.Include(content.Right, content.Bottom);
imageContent.Include(content.Left, content.Top);
}
else
{
// Here it's better we increase values of the incrementers.
// Depending on size of spots/dots.
// It should increase performance.
if (dx < content.Width) dx = content.Width;
if (dy < content.Height) dy = content.Height;
}
}
}
}
// Everything is done.
this.progressBar1.Value = this.progressBar1.Maximum;
// If image content has been detected
// then we save the information
if (imageContent != null)
{
this.left = imageContent.Left;
this.top = imageContent.Top;
this.right = imageContent.Right;
this.bottom = imageContent.Bottom;
}
this.pictureBox1.Invalidate();
}
internal static readonly int Black = Color.Black.ToArgb();
internal static readonly int White = Color.White.ToArgb();
int top;
int bottom;
int left;
int right;
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (pictureBox1.Image == null)
{
return;
}
int xMax = pictureBox1.Image.Width;
int yMax = pictureBox1.Image.Height;
int startX = this.left;
int startY = this.top;
int endX = this.right;
int endY = this.bottom;
int picWidth = pictureBox1.Size.Width;
int picHeight = pictureBox1.Size.Height;
float imageRatio = xMax / (float)yMax; // image W:H ratio
float containerRatio = picWidth / (float)picHeight; // container W:H ratio
if (imageRatio >= containerRatio)
{
// horizontal image
float scaleFactor = picWidth / (float)xMax;
float scaledHeight = yMax * scaleFactor;
// calculate gap between top of container and top of image
float filler = Math.Abs(picHeight - scaledHeight) / 2;
//float filler = 0;
startX = (int)(startX * scaleFactor);
endX = (int)(endX * scaleFactor);
startY = (int)((startY) * scaleFactor + filler);
endY = (int)((endY) * scaleFactor + filler);
}
else
{
// vertical image
float scaleFactor = picHeight / (float)yMax;
float scaledWidth = xMax * scaleFactor;
float filler = Math.Abs(picWidth - scaledWidth) / 2;
startX = (int)((startX) * scaleFactor + filler);
endX = (int)((endX) * scaleFactor + filler);
startY = (int)(startY * scaleFactor);
endY = (int)(endY * scaleFactor);
}
//if (_once)
//Rectangle ee = new Rectangle(35, 183, 405, 157);
Rectangle ee = new Rectangle(startX, startY, endX - startX, endY - startY);
System.Diagnostics.Debug.WriteLine(startX + ", " + startY + ", " + (endX - startX) + ", " + (endY - startY));
using (Pen pen = new Pen(Color.Red, 2))
{
e.Graphics.DrawRectangle(pen, ee);
}
//_once = false;
}
}
// This class is similar to System.Drawing.Region class
// except that its only rectangular.
// Because all we need is to draw a rectagnle
// around the image this property must
// make it faster, at least I hope so.
class BlackContent
{
internal void FloodFill(Bitmap image)
{
FloodFillPrivate(image, this.left + 1, this.top, 0);
}
// Legendary Flood-Fill algorithm.
// Quite often it ends up with StackOverFlow exception.
// But this class and its rectangularity property
// must prevent this disaster.
// In my experiments I didn't encounter incidents.
void FloodFillPrivate(Bitmap image, int x, int y, int level)
{
if (x >= image.Width || x < 0 || y >= image.Height || y < 0 || this.Contains(x, y) || image.GetPixel(x, y).ToArgb() != Form1.Black)
{
return;
}
this.Include(x, y);
FloodFillPrivate(image, x, y - 1, level + 1);
FloodFillPrivate(image, x, y + 1, level + 1);
FloodFillPrivate(image, x - 1, y, level + 1);
FloodFillPrivate(image, x + 1, y, level + 1);
}
internal BlackContent(int x, int y)
{
this.left = x;
this.right = x;
this.top = y;
this.bottom = y;
}
internal void Include(int x, int y)
{
if (x < this.left)
{
this.left = x;
}
if (this.right < x)
{
this.right = x;
}
if (this.bottom < y)
{
this.bottom = y;
}
if (y < this.top)
{
this.top = y;
}
}
internal bool Contains(int x, int y)
{
return !(x < this.left || x > this.right || y < this.top || y > this.bottom);
}
int left;
internal int Left { get { return this.left; } }
int top;
internal int Top { get { return this.top; } }
int right;
internal int Right { get { return this.right; } }
int bottom;
internal int Bottom { get { return this.bottom; } }
internal int Area
{
get
{
return Width * Height;
}
}
internal int Width
{
get
{
return (this.right - this.left + 1);
}
}
internal int Height
{
get
{
return (this.bottom - this.top + 1);
}
}
}
}
I watched the performance with ProgressBar. This one's quite faster.
I must also mention that your images are too big.
A solution could be to find areas of black pixels. When a black pixel is found, check the colour of the neighbouring pixels and create an area of black pixels. When the area is big enough, it can be considered as content. The pseudo code below illustrates this. But it is a very resource intensive solution and should at least be optimized.
private List<List<Point>> areas = new List<List<Point>>();
public void PopulateAreas()
{
//Find all the areas
for (var y = 0; y < yMax; y += 3)
{
for (var x = 0; x < xMax; x += 3)
{
Color col = bmp.GetPixel(x, y);
if (col.ToArgb() == Color.Black.ToArgb())
{
//Found a black pixel, check surrounding area
var area = new List<Point>();
area.Add(new Point(x, y));
areas.Add(area);
AppendSurroundingPixelsToArea(area, x, y);
}
}
}
var startX = Int32.MaxValue;
var startY = Int32.MaxValue;
var endX = Int32.MinValue;
var endY = Int32.MinValue;
//Loop through list of areas.
foreach (var area in areas)
{
//Minimum size of area
if (area.Count > 5)
{
var minx = area.Min(p => p.X);
if (area.Min(p => p.X) < startX)
startX = minx;
//Do the same for the others...
}
}
}
public void AppendSurroundingPixelsToArea(List<Point> area, int startX, int startY)
{
for(var x = startX - 1; x <= startX + 1; x++)
for (var y = startY - 1; y <= startY + 1; y++)
{
if ((x != 0 || y != 0) && IsBlackPixel(bmp, x, y))
{
//Add to the area
if (PointDoesNotExistInArea(area, x, y))
{
area.Add(new Point(x, y));
AppendSurroundingPixelsToArea(area, x, y);
}
}
}
}
I have solution for this,
And I suggest "kodak imaging professional". This is the viewer to display multi-page tiff files. with many features like: Annotation, invert color, rotate image... etc., and these are inbuilt functionalities.
Edit: Rewrote my question after trying a few things and made it more specific.
Hi, so I'm creating a mobile RTS game with procedurally generated maps. I've worked out how to create a terrain with a basic perlin noise on it, and tried to integrate https://gamedev.stackexchange.com/questions/54276/a-simple-method-to-create-island-map-mask method to creating an island procedurally. This is the result so far:
The image below from http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/ shows the kind of terrain I'm after. The tutorial there is great but would be too intensive, thus the post.
I want the Random Shaped island with Perlin noise generated land mass, surrounded by water.
edit: Basic Perlin terrain gen working now =)
Here is my code. A script attached to a null with a button to activate Begin():
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
public class Gen_Perlin : MonoBehaviour {
public float Tiling = 0.5f;
private bool active = false;
public int mapHeight = 10;
public void Begin()
{
if (active == false) {
TerrainData terrainData = new TerrainData ();
const int size = 513;
terrainData.heightmapResolution = size;
terrainData.size = new Vector3 (2000, mapHeight, 2000);
terrainData.heightmapResolution = 513;
terrainData.baseMapResolution = 1024;
terrainData.SetDetailResolution (1024, 1024);
Terrain.CreateTerrainGameObject (terrainData);
GameObject obj = GameObject.Find ("Terrain");
obj.transform.parent = this.transform;
if (obj.GetComponent<Terrain> ()) {
GenerateHeights (obj.GetComponent<Terrain> (), Tiling);
}
} else {
GameObject obj = GameObject.Find ("Terrain");
if (obj.GetComponent<Terrain> ()) {
GenerateHeights (obj.GetComponent<Terrain> (), Tiling);
}
}
}
public void GenerateHeights(Terrain terrain, float tileSize)
{
Debug.Log ("Start_Height_Gen");
float[,] heights = new float[terrain.terrainData.heightmapWidth, terrain.terrainData.heightmapHeight];
for (int i = 0; i < terrain.terrainData.heightmapWidth; i++)
{
for (int k = 0; k < terrain.terrainData.heightmapHeight; k++)
{
heights[i, k] = 0.25f + Mathf.PerlinNoise(((float)i / (float)terrain.terrainData.heightmapWidth) * tileSize, ((float)k / (float)terrain.terrainData.heightmapHeight) * tileSize);
heights[i, k] *= makeMask( terrain.terrainData.heightmapWidth, terrain.terrainData.heightmapHeight, i, k, heights[i, k] );
}
}
terrain.terrainData.SetHeights(0, 0, heights);
}
public static float makeMask( int width, int height, int posX, int posY, float oldValue ) {
int minVal = ( ( ( height + width ) / 2 ) / 100 * 2 );
int maxVal = ( ( ( height + width ) / 2 ) / 100 * 10 );
if( getDistanceToEdge( posX, posY, width, height ) <= minVal ) {
return 0;
} else if( getDistanceToEdge( posX, posY, width, height ) >= maxVal ) {
return oldValue;
} else {
float factor = getFactor( getDistanceToEdge( posX, posY, width, height ), minVal, maxVal );
return oldValue * factor;
}
}
private static float getFactor( int val, int min, int max ) {
int full = max - min;
int part = val - min;
float factor = (float)part / (float)full;
return factor;
}
public static int getDistanceToEdge( int x, int y, int width, int height ) {
int[] distances = new int[]{ y, x, ( width - x ), ( height - y ) };
int min = distances[ 0 ];
foreach( var val in distances ) {
if( val < min ) {
min = val;
}
}
return min;
}
}
Yeah. The article in question is using a waaay complex method.
The best way of doing this is to take a function that represents the shape of your basic island, with height values between 0 and 1. For the type of island in the picture, you'd basically want something which smoothly rises from the edges, and smoothly dips back to zero where you want lakes.
Now you either add that surface to your basic fractal surface (if you want to preserve spikiness at low elevations) or you multiply it (if you want lower elevations to be smooth). Then you define a height, below which is water.
Here is my very quick go at doing this, rendered with Terragen:
I used a function that rises in a ring from the edge of the map to halfway to the middle, then drops again, to match a similar shape to the one from the article. In practice, you might only use this to get the shape of the island, and then carve the bit of terrain that matches the contour, and bury everything else.
I used my own fractal landscape generator as described here: https://fractal-landscapes.co.uk for the basic fractal.
Here is the C# code that modifies the landscape:
public void MakeRingIsland()
{
this.Normalize(32768);
var ld2 = (double) linearDimension / 2;
var ld4 = 4 / (double) linearDimension;
for (var y = 0u; y < linearDimension; y++)
{
var yMul = y * linearDimension;
for (var x = 0u; x < linearDimension; x++)
{
var yCoord = (y - ld2) * ld4;
var xCoord = (x - ld2) * ld4;
var dist = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
var htMul = dist > 2 ? 0 :
(dist < 1 ?
dist + dist - dist * dist :
1 - (dist - 1) * (dist - 1));
var height = samples[x + yMul];
samples[x + yMul] = (int) (height + htMul * 32768);
}
}
}
the image you are showing comes from article describing how to generate it
I was searching for a stretch like warp algorithm to apply in my Windows 8 store application.
I found this android (java) code below.
Most things I can port to C# but some android sdk specific things are blurry for me to port to .NET code.
For example the call too : canvas.drawBitmapMesh , is there some .NET counterpart for this?
The Matrix classes in .NET are also a bit different I think but I can figure that out I think.
Any tips on helping converting/porting the code below to .NET are very welcome.
public class BitmapMesh extends GraphicsActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new SampleView(this));
}
private static class SampleView extends View {
private static final int WIDTH = 20;
private static final int HEIGHT = 20;
private static final int COUNT = (WIDTH + 1) * (HEIGHT + 1);
private final Bitmap mBitmap;
private final float[] mVerts = new float[COUNT*2];
private final float[] mOrig = new float[COUNT*2];
private final Matrix mMatrix = new Matrix();
private final Matrix mInverse = new Matrix();
private static void setXY(float[] array, int index, float x, float y) {
array[index*2 + 0] = x;
array[index*2 + 1] = y;
}
public SampleView(Context context) {
super(context);
setFocusable(true);
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.beach);
float w = mBitmap.getWidth();
float h = mBitmap.getHeight();
// construct our mesh
int index = 0;
for (int y = 0; y <= HEIGHT; y++) {
float fy = h * y / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
float fx = w * x / WIDTH;
setXY(mVerts, index, fx, fy);
setXY(mOrig, index, fx, fy);
index += 1;
}
}
mMatrix.setTranslate(10, 10);
mMatrix.invert(mInverse);
}
#Override protected void onDraw(Canvas canvas) {
canvas.drawColor(0xFFCCCCCC);
canvas.concat(mMatrix);
canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, mVerts, 0,
null, 0, null);
}
private void warp(float cx, float cy) {
final float K = 10000;
float[] src = mOrig;
float[] dst = mVerts;
for (int i = 0; i < COUNT*2; i += 2) {
float x = src[i+0];
float y = src[i+1];
float dx = cx - x;
float dy = cy - y;
float dd = dx*dx + dy*dy;
float d = FloatMath.sqrt(dd);
float pull = K / (dd + 0.000001f);
pull /= (d + 0.000001f);
// android.util.Log.d("skia", "index " + i + " dist=" + d + " pull=" + pull);
if (pull >= 1) {
dst[i+0] = cx;
dst[i+1] = cy;
} else {
dst[i+0] = x + dx * pull;
dst[i+1] = y + dy * pull;
}
}
}
private int mLastWarpX = -9999; // don't match a touch coordinate
private int mLastWarpY;
#Override public boolean onTouchEvent(MotionEvent event) {
float[] pt = { event.getX(), event.getY() };
mInverse.mapPoints(pt);
int x = (int)pt[0];
int y = (int)pt[1];
if (mLastWarpX != x || mLastWarpY != y) {
mLastWarpX = x;
mLastWarpY = y;
warp(pt[0], pt[1]);
invalidate();
}
return true;
}
}
}
There is no direct equivalent in Windows Store apps to be able to draw a texture mapped mesh (and represent image warping and distortion that way). You will have to use Direct3D to draw the texture mapped mesh onto your page.
If you're reluctant to mix C++ and C# in your app, you have two choices.
SharpDX - these are .NET bindings to DirectX.
MonoGame - this is an XNA-compliant, cross-platform graphics interface that uses DirectX on WinRT.
Hope this helps!
I am currently dynamically creating a bitmap and using the graphics object from the bitmap to draw a string on it like so:
System.Drawing.Graphics graph = System.Drawing.Graphics.FromImage(bmp);
graph.DrawString(text, font, brush, new System.Drawing.Point(0, 0));
This returns a rectangular bitmap with the string written straight across from left to right.
I would like to also be able to draw the string in the shape of a rainbow.
How can I do this?
I recently had this problem (I was rendering text for printing onto CDs), so here's my solution:
private void DrawCurvedText(Graphics graphics, string text, Point centre, float distanceFromCentreToBaseOfText, float radiansToTextCentre, Font font, Brush brush)
{
// Circumference for use later
var circleCircumference = (float)(Math.PI * 2 * distanceFromCentreToBaseOfText);
// Get the width of each character
var characterWidths = GetCharacterWidths(graphics, text, font).ToArray();
// The overall height of the string
var characterHeight = graphics.MeasureString(text, font).Height;
var textLength = characterWidths.Sum();
// The string length above is the arc length we'll use for rendering the string. Work out the starting angle required to
// centre the text across the radiansToTextCentre.
float fractionOfCircumference = textLength / circleCircumference;
float currentCharacterRadians = radiansToTextCentre + (float)(Math.PI * fractionOfCircumference);
for (int characterIndex = 0; characterIndex < text.Length; characterIndex++)
{
char #char = text[characterIndex];
// Polar to cartesian
float x = (float)(distanceFromCentreToBaseOfText * Math.Sin(currentCharacterRadians));
float y = -(float)(distanceFromCentreToBaseOfText * Math.Cos(currentCharacterRadians));
using (GraphicsPath characterPath = new GraphicsPath())
{
characterPath.AddString(#char.ToString(), font.FontFamily, (int)font.Style, font.Size, Point.Empty,
StringFormat.GenericTypographic);
var pathBounds = characterPath.GetBounds();
// Transformation matrix to move the character to the correct location.
// Note that all actions on the Matrix class are prepended, so we apply them in reverse.
var transform = new Matrix();
// Translate to the final position
transform.Translate(centre.X + x, centre.Y + y);
// Rotate the character
var rotationAngleDegrees = currentCharacterRadians * 180F / (float)Math.PI - 180F;
transform.Rotate(rotationAngleDegrees);
// Translate the character so the centre of its base is over the origin
transform.Translate(-pathBounds.Width / 2F, -characterHeight);
characterPath.Transform(transform);
// Draw the character
graphics.FillPath(brush, characterPath);
}
if (characterIndex != text.Length - 1)
{
// Move "currentCharacterRadians" on to the next character
var distanceToNextChar = (characterWidths[characterIndex] + characterWidths[characterIndex + 1]) / 2F;
float charFractionOfCircumference = distanceToNextChar / circleCircumference;
currentCharacterRadians -= charFractionOfCircumference * (float)(2F * Math.PI);
}
}
}
private IEnumerable<float> GetCharacterWidths(Graphics graphics, string text, Font font)
{
// The length of a space. Necessary because a space measured using StringFormat.GenericTypographic has no width.
// We can't use StringFormat.GenericDefault for the characters themselves, as it adds unwanted spacing.
var spaceLength = graphics.MeasureString(" ", font, Point.Empty, StringFormat.GenericDefault).Width;
return text.Select(c => c == ' ' ? spaceLength : graphics.MeasureString(c.ToString(), font, Point.Empty, StringFormat.GenericTypographic).Width);
}
I needed to answer this question in raw C# and could not find any samples that do it so:
This solution requires a lot of Maths to solve. In summary it takes a set of points (2D vectors), aligns them to a baseline and then bends the points around a Spline. The code is fast enough to update in real time and handles loops, etc.
For ease the solution turns text into vectors using GraphicsPath.AddString and uses GraphicsPath.PathPoints/PathTypes for the points, however you can bend any shape or even bitmaps using the same functions. (I would not recommend doing 4k bitmaps in real time though).
The code has a simple Paint function followed by the Spline class. GraphicsPath is used in the Paint method to make the code hopefully easier to understand. ClickedPoints are the array of points you want the text bent around. I used a List as it was added to in Mouse events, use an array if you know the points beforehand.
public void Paint(System.Drawing.Graphics g)
{
List<System.Drawing.Point> clickedPoints = new List<System.Drawing.Point>();
Additional.Math.Beziers.BezierSplineCubic2D _Spline = new Additional.Math.Beziers.BezierSplineCubic2D();
// Create the spline, exit if no points to bend around.
System.Drawing.PointF[] cd = Additional.Math.Beziers.BezierSplineCubic2D.CreateCurve(ClickedPoints.ToArray(), 0, ClickedPoints.Count, 0);
_Spline = new Additional.Math.Beziers.BezierSplineCubic2D(cd);
if (_Spline.Beziers == null || _Spline.Length == 0) return;
// Start Optional: Remove if you only want the bent object
// Draw the spline curve the text will be put onto using inbuilt GDI+ calls
g.DrawCurve(System.Drawing.Pens.Blue, clickedPoints.ToArray());
// draw the control point data for the curve
for (int i = 0; i < cd.Length; i++)
{
g.DrawRectangle(System.Drawing.Pens.Green, cd[i].X - 2F, cd[i].Y - 2F, 4F, 4F);
}
// End Optional:
// Turn the text into points that can be bent - if no points then exit:
System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
path.AddString("Lorem ipsum dolor", new System.Drawing.FontFamily("Arial"), 0, 12.0F, new System.Drawing.Point(0, 0), new System.Drawing.StringFormat() { Alignment = System.Drawing.StringAlignment.Near });
textBounds = path.GetBounds();
curvedData = (System.Drawing.PointF[])path.PathPoints.Clone();
curvedTypes = (byte[])path.PathTypes.Clone();
dataLength = curvedData.Length;
if (dataLength == 0) return;
// Bend the shape(text) around the path (Spline)
_Spline.BendShapeToSpline(textBounds, dataLength, ref curvedData, ref curvedTypes);
// draw the transformed text path
System.Drawing.Drawing2D.GraphicsPath textPath = new System.Drawing.Drawing2D.GraphicsPath(curvedData, curvedTypes);
g.DrawPath(System.Drawing.Pens.Black, textPath);
}
And now for the spline class:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Additional.Math
{
namespace Vectors
{
public struct Vector2DFloat
{
public float X;
public float Y;
public void SetXY(float x, float y)
{
X = x;
Y = y;
}
public static Vector2DFloat Lerp(Vector2DFloat v0, Vector2DFloat v1, float t)
{
return v0 + (v1 - v0) * t;
}
public Vector2DFloat(Vector2DFloat value)
{
this.X = value.X;
this.Y = value.Y;
}
public Vector2DFloat(float x, float y)
{
this.X = x;
this.Y = y;
}
public Vector2DFloat Rotate90Degrees(bool positiveRotation)
{
return positiveRotation ? new Vector2DFloat(-Y, X) : new Vector2DFloat(Y, -X);
}
public Vector2DFloat Normalize()
{
float magnitude = (float)System.Math.Sqrt(X * X + Y * Y);
return new Vector2DFloat(X / magnitude, Y / magnitude);
}
public float Distance(Vector2DFloat target)
{
return (float)System.Math.Sqrt((X - target.X) * (X - target.X) + (Y - target.Y) * (Y - target.Y));
}
public float DistanceSquared(Vector2DFloat target)
{
return (X - target.X) * (X - target.X) + (Y - target.Y) * (Y - target.Y);
}
public double DistanceTo(Vector2DFloat target)
{
return System.Math.Sqrt(System.Math.Pow(target.X - X, 2F) + System.Math.Pow(target.Y - Y, 2F));
}
public System.Drawing.PointF ToPointF()
{
return new System.Drawing.PointF(X, Y);
}
public Vector2DFloat(System.Drawing.PointF value)
{
this.X = value.X;
this.Y = value.Y;
}
public static implicit operator Vector2DFloat(System.Drawing.PointF value)
{
return new Vector2DFloat(value);
}
public static Vector2DFloat operator +(Vector2DFloat first, Vector2DFloat second)
{
return new Vector2DFloat(first.X + second.X, first.Y + second.Y);
}
public static Vector2DFloat operator -(Vector2DFloat first, Vector2DFloat second)
{
return new Vector2DFloat(first.X - second.X, first.Y - second.Y);
}
public static Vector2DFloat operator *(Vector2DFloat first, float second)
{
return new Vector2DFloat(first.X * second, first.Y * second);
}
public static Vector2DFloat operator *(float first, Vector2DFloat second)
{
return new Vector2DFloat(second.X * first, second.Y * first);
}
public static Vector2DFloat operator *(Vector2DFloat first, int second)
{
return new Vector2DFloat(first.X * second, first.Y * second);
}
public static Vector2DFloat operator *(int first, Vector2DFloat second)
{
return new Vector2DFloat(second.X * first, second.Y * first);
}
public static Vector2DFloat operator *(Vector2DFloat first, double second)
{
return new Vector2DFloat((float)(first.X * second), (float)(first.Y * second));
}
public static Vector2DFloat operator *(double first, Vector2DFloat second)
{
return new Vector2DFloat((float)(second.X * first), (float)(second.Y * first));
}
public override bool Equals(object obj)
{
return this.Equals((Vector2DFloat)obj);
}
public bool Equals(Vector2DFloat p)
{
// If parameter is null, return false.
if (p == null)
{
return false;
}
// Optimization for a common success case.
if (this == p)
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode()
{
return (int)(System.Math.Round(X + Y, 4) * 10000);
}
public static bool operator ==(Vector2DFloat first, Vector2DFloat second)
{
// Check for null on left side.
if (first == null)
{
if (second == null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return first.Equals(second);
}
public static bool operator !=(Vector2DFloat first, Vector2DFloat second)
{
return !(first == second);
}
}
}
namespace Beziers
{
public struct BezierCubic2D
{
public Vectors.Vector2DFloat P0;
public Vectors.Vector2DFloat P1;
public Vectors.Vector2DFloat P2;
public Vectors.Vector2DFloat P3;
public int ArcLengthDivisionCount;
public List<float> ArcLengths { get { if (_ArcLengths.Count == 0) CalculateArcLength(); return _ArcLengths; } }
public float ArcLength { get { if (_ArcLength == 0.0F) CalculateArcLength(); return _ArcLength; } }
private Vectors.Vector2DFloat A;
private Vectors.Vector2DFloat B;
private Vectors.Vector2DFloat C;
private List<float> _ArcLengths;
private float _ArcLength;
public BezierCubic2D(Vectors.Vector2DFloat p0, Vectors.Vector2DFloat p1, Vectors.Vector2DFloat p2, Vectors.Vector2DFloat p3)
{
P0 = p0;
P1 = p1;
P2 = p2;
P3 = p3;
// vt = At^3 + Bt^2 + Ct + p0
A = P3 - 3 * P2 + 3 * P1 - P0;
B = 3 * P2 - 6 * P1 + 3 * P0;
C = 3 * P1 - 3 * P0;
ArcLengthDivisionCount = 100;
_ArcLengths = new List<float>();
_ArcLength = 0.0F;
}
public BezierCubic2D(System.Drawing.PointF p0, System.Drawing.PointF p1, System.Drawing.PointF p2, System.Drawing.PointF p3)
{
P0 = p0;
P1 = p1;
P2 = p2;
P3 = p3;
// vt = At^3 + Bt^2 + Ct + p0
A = P3 - 3 * P2 + 3 * P1 - P0;
B = 3 * P2 - 6 * P1 + 3 * P0;
C = 3 * P1 - 3 * P0;
ArcLengthDivisionCount = 100;
_ArcLengths = new List<float>();
_ArcLength = 0.0F;
}
public BezierCubic2D(float p0X, float p0Y, float p1X, float p1Y, float p2X, float p2Y, float p3X, float p3Y)
{
P0 = new Vectors.Vector2DFloat(p0X, p0Y);
P1 = new Vectors.Vector2DFloat(p1X, p1Y);
P2 = new Vectors.Vector2DFloat(p2X, p2Y);
P3 = new Vectors.Vector2DFloat(p3X, p3Y);
// vt = At^3 + Bt^2 + Ct + p0
A = P3 - 3 * P2 + 3 * P1 - P0;
B = 3 * P2 - 6 * P1 + 3 * P0;
C = 3 * P1 - 3 * P0;
ArcLengthDivisionCount = 100;
_ArcLengths = new List<float>();
_ArcLength = 0.0F;
}
public Vectors.Vector2DFloat PointOnCurve(float t)
{
return A * System.Math.Pow(t, 3) + B * System.Math.Pow(t, 2) + C * t + P0;
}
public Vectors.Vector2DFloat PointOnCurveGeometric(float t)
{
Vectors.Vector2DFloat p4 = Vectors.Vector2DFloat.Lerp(P0, P1, t);
Vectors.Vector2DFloat p5 = Vectors.Vector2DFloat.Lerp(P1, P2, t);
Vectors.Vector2DFloat p6 = Vectors.Vector2DFloat.Lerp(P2, P3, t);
Vectors.Vector2DFloat p7 = Vectors.Vector2DFloat.Lerp(p4, p5, t);
Vectors.Vector2DFloat p8 = Vectors.Vector2DFloat.Lerp(p5, p6, t);
return Vectors.Vector2DFloat.Lerp(p7, p8, t);
}
public Vectors.Vector2DFloat PointOnCurveTangent(float t)
{
return 3 * A * System.Math.Pow(t, 2) + 2 * B * t + C;
}
public Vectors.Vector2DFloat PointOnCurvePerpendicular(float t, bool positiveRotation)
{
return (3 * A * System.Math.Pow(t, 2) + 2 * B * t + C).Rotate90Degrees(positiveRotation).Normalize() * 10F + PointOnCurve(t);
}
public Vectors.Vector2DFloat PointOnCurvePerpendicular(float t, bool positiveRotation, float pointHeight)
{
return (3 * A * System.Math.Pow(t, 2) + 2 * B * t + C).Rotate90Degrees(positiveRotation).Normalize() * pointHeight + PointOnCurve(t);
}
public float FindTAtPointOnBezier(float u)
{
float t;
int index = _ArcLengths.BinarySearch(u);
if (index >= 0)
t = index / (float)(_ArcLengths.Count - 1);
else if (index * -1 >= _ArcLengths.Count)
t = 1;
else if (index == 0)
t = 0;
else
{
index *= -1;
float lengthBefore = _ArcLengths[index - 1];
float lengthAfter = _ArcLengths[index];
float segmentLength = lengthAfter - lengthBefore;
float segmentFraction = (u - lengthBefore) / segmentLength;
// add that fractional amount to t
t = (index + segmentFraction) / (float)(_ArcLengths.Count - 1);
}
return t;
}
private void CalculateArcLength()
{
// calculate Arc Length through successive approximation. Use the squared version as it is faster.
_ArcLength = 0.0F;
int arrayMax = ArcLengthDivisionCount + 1;
_ArcLengths = new List<float>(arrayMax)
{
0.0F
};
Vectors.Vector2DFloat prior = P0, current;
for (int i = 1; i < arrayMax; i++)
{
current = PointOnCurve(i / (float)ArcLengthDivisionCount);
_ArcLength += current.Distance(prior);
_ArcLengths.Add(_ArcLength);
prior = current;
}
}
public override bool Equals(object obj)
{
return this.Equals((BezierCubic2D)obj);
}
public bool Equals(BezierCubic2D p)
{
// If parameter is null, return false.
if (p == null)
{
return false;
}
// Optimization for a common success case.
if (this == p)
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (P0 == p.P0) && (P1 == p.P1) && (P2 == p.P2) && (P3 == p.P3);
}
public override int GetHashCode()
{
return P0.GetHashCode() + P1.GetHashCode() + P2.GetHashCode() + P3.GetHashCode() % int.MaxValue;
}
public static bool operator ==(BezierCubic2D first, BezierCubic2D second)
{
// Check for null on left side.
if (first == null)
{
if (second == null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return first.Equals(second);
}
public static bool operator !=(BezierCubic2D first, BezierCubic2D second)
{
return !(first == second);
}
}
public struct BezierSplineCubic2D
{
public BezierCubic2D[] Beziers;
public BezierCubic2D this[int index] { get { return Beziers[index]; } }
public int Length { get { return Beziers.Length; } }
public List<float> ArcLengths { get { if (_ArcLengths.Count == 0) CalculateArcLength(); return _ArcLengths; } }
public float ArcLength { get { if (_ArcLength == 0.0F) CalculateArcLength(); return _ArcLength; } }
private List<float> _ArcLengths;
private float _ArcLength;
public BezierSplineCubic2D(Vectors.Vector2DFloat[] source)
{
if (source == null || source.Length < 4 || (source.Length - 4) % 3 != 0) { Beziers = null; _ArcLength = 0.0F; _ArcLengths = new List<float>(); return; }
int length = ((source.Length - 4) / 3) + 1;
Beziers = new BezierCubic2D[length];
Beziers[0] = new BezierCubic2D(source[0], source[1], source[2], source[3]);
for (int i = 1; i < length; i++)
Beziers[i] = new BezierCubic2D(source[(i * 3)], source[(i * 3) + 1], source[(i * 3) + 2], source[(i * 3) + 3]);
_ArcLength = 0.0F;
_ArcLengths = new List<float>();
}
public BezierSplineCubic2D(System.Drawing.PointF[] source)
{
if (source == null || source.Length < 4 || (source.Length - 4) % 3 != 0) { Beziers = null; _ArcLength = 0.0F; _ArcLengths = new List<float>(); return; }
int length = ((source.Length - 4) / 3) + 1;
Beziers = new BezierCubic2D[length];
Beziers[0] = new BezierCubic2D(source[0], source[1], source[2], source[3]);
for (int i = 1; i < length; i++)
Beziers[i] = new BezierCubic2D(source[(i * 3)], source[(i * 3) + 1], source[(i * 3) + 2], source[(i * 3) + 3]);
_ArcLength = 0.0F;
_ArcLengths = new List<float>();
}
public BezierSplineCubic2D(System.Drawing.Point[] source)
{
if (source == null || source.Length < 4 || (source.Length - 4) % 3 != 0) { Beziers = null; _ArcLength = 0.0F; _ArcLengths = new List<float>(); return; }
int length = ((source.Length - 4) / 3) + 1;
Beziers = new BezierCubic2D[length];
Beziers[0] = new BezierCubic2D(source[0], source[1], source[2], source[3]);
for (int i = 1; i < length; i++)
Beziers[i] = new BezierCubic2D(source[(i * 3)], source[(i * 3) + 1], source[(i * 3) + 2], source[(i * 3) + 3]);
_ArcLength = 0.0F;
_ArcLengths = new List<float>();
}
public bool FindTAtPointOnSpline(float distanceAlongSpline, out BezierCubic2D bezier, out float t)
{
// to do: cache last distance and bezier. if new distance > old then start from old bezier.
if (distanceAlongSpline > ArcLength) { bezier = Beziers[Beziers.Length - 1]; t = distanceAlongSpline / ArcLength; return false; }
if (distanceAlongSpline <= 0.0F)
{
bezier = Beziers[0];
t = 0.0F;
return true;
}
for (int i = 0; i < Beziers.Length; i++)
{
float distanceRemainingBeyondCurrentBezier = distanceAlongSpline - Beziers[i].ArcLength;
if (distanceRemainingBeyondCurrentBezier < 0.0F)
{
// t is in current bezier.
bezier = Beziers[i];
t = bezier.FindTAtPointOnBezier(distanceAlongSpline);
return true;
}
else if (distanceRemainingBeyondCurrentBezier == 0.0F)
{
// t is 1.0F. Bezier is current one.
bezier = Beziers[i];
t = 1.0F;
return true;
}
// reduce the distance by the length of the bezier.
distanceAlongSpline -= Beziers[i].ArcLength;
}
// point is outside the spline.
bezier = new BezierCubic2D();
t = 0.0F;
return false;
}
public void BendShapeToSpline(System.Drawing.RectangleF bounds, int dataLength, ref System.Drawing.PointF[] data, ref byte[] dataTypes)
{
System.Drawing.PointF pt;
// move the origin for the data to 0,0
float left = bounds.Left, height = bounds.Y + bounds.Height;
for (int i = 0; i < dataLength; i++)
{
pt = data[i];
float textX = pt.X - left;
float textY = pt.Y - height;
if (FindTAtPointOnSpline(textX, out BezierCubic2D bezier, out float t))
{
data[i] = bezier.PointOnCurvePerpendicular(t, true, textY).ToPointF();
}
else
{
// roll back all points until we reach curvedTypes[i] == 0
for (int j = i - 1; j > -1; j--)
{
if ((dataTypes[j] & 0x80) == 0x80)
{
System.Drawing.PointF[] temp1 = new System.Drawing.PointF[j + 1];
Array.Copy(data, 0, temp1, 0, j + 1);
byte[] temp2 = new byte[j + 1];
Array.Copy(dataTypes, 0, temp2, 0, j + 1);
data = temp1;
dataTypes = temp2;
break;
}
}
break;
}
}
}
private void CalculateArcLength()
{
_ArcLength = 0.0F;
_ArcLengths = new List<float>(Beziers.Length);
for (int i = 0; i < Beziers.Length; i++)
{
_ArcLength += Beziers[i].ArcLength;
_ArcLengths.Add(_ArcLength);
}
}
internal static System.Drawing.PointF[] GetCurveTangents(System.Drawing.Point[] points, int count, float tension, int curveType)
{
if (points == null)
throw new ArgumentNullException("points");
System.Drawing.PointF[] pointfs = new System.Drawing.PointF[count];
for (int p = 0; p < count; p++)
{
pointfs[p] = new System.Drawing.PointF(points[p].X, points[p].Y);
}
return GetCurveTangents(pointfs, count, tension, curveType);
}
internal static System.Drawing.PointF[] GetCurveTangents(System.Drawing.PointF[] points, int count, float tension, int curveType)
{
float coefficient = tension / 3f;
System.Drawing.PointF[] tangents = new System.Drawing.PointF[count];
if (count < 2)
return tangents;
for (int i = 0; i < count; i++)
{
int r = i + 1;
int s = i - 1;
if (r >= count)
r = count - 1;
if (curveType == 0) // 0 == CurveType.Open
{
if (s < 0)
s = 0;
}
else // 1 == CurveType.Closed, end point jumps to start point
{
if (s < 0)
s += count;
}
tangents[i].X += (coefficient * (points[r].X - points[s].X));
tangents[i].Y += (coefficient * (points[r].Y - points[s].Y));
}
return tangents;
}
internal static System.Drawing.PointF[] CreateCurve(System.Drawing.Point[] points, int offset, int length, int curveType)
{
if (points == null)
throw new ArgumentNullException("points");
System.Drawing.PointF[] pointfs = new System.Drawing.PointF[length];
for (int p = 0; p < length; p++)
{
pointfs[p] = new System.Drawing.PointF(points[p].X, points[p].Y);
}
System.Drawing.PointF[] tangents = GetCurveTangents(pointfs, length, 0.5F, 0);
return CreateCurve(pointfs, tangents, offset, length, curveType);
}
internal static System.Drawing.PointF[] CreateCurve(System.Drawing.Point[] points, System.Drawing.PointF[] tangents, int offset, int length, int curveType)
{
if (points == null)
throw new ArgumentNullException("points");
System.Drawing.PointF[] pointfs = new System.Drawing.PointF[length];
for (int p = 0; p < length; p++)
{
pointfs[p] = new System.Drawing.PointF(points[p].X, points[p].Y);
}
return CreateCurve(pointfs, tangents, offset, length, curveType);
}
internal static System.Drawing.PointF[] CreateCurve(System.Drawing.PointF[] points, System.Drawing.PointF[] tangents, int offset, int length, int curveType)
{
List<System.Drawing.PointF> curve = new List<System.Drawing.PointF>();
int i;
Append(curve, points[offset].X, points[offset].Y, true);
for (i = offset; i < offset + length - 1; i++)
{
int j = i + 1;
float x1 = points[i].X + tangents[i].X;
float y1 = points[i].Y + tangents[i].Y;
float x2 = points[j].X - tangents[j].X;
float y2 = points[j].Y - tangents[j].Y;
float x3 = points[j].X;
float y3 = points[j].Y;
AppendBezier(curve, x1, y1, x2, y2, x3, y3, false);
}
return curve.ToArray<System.Drawing.PointF>();
}
internal static void Append(List<System.Drawing.PointF> points, float x, float y, bool compress)
{
System.Drawing.PointF pt = System.Drawing.PointF.Empty;
/* in some case we're allowed to compress identical points */
if (compress && (points.Count > 0))
{
/* points (X, Y) must be identical */
System.Drawing.PointF lastPoint = points[points.Count - 1];
if ((lastPoint.X == x) && (lastPoint.Y == y))
{
return;
}
}
pt.X = x;
pt.Y = y;
points.Add(pt);
}
internal static void AppendBezier(List<System.Drawing.PointF> points, float x1, float y1, float x2, float y2, float x3, float y3, bool isReverseWindingOnFill)
{
if (isReverseWindingOnFill)
{
Append(points, y1, x1, false);
Append(points, y2, x2, false);
Append(points, y3, x3, false);
}
else
{
Append(points, x1, y1, false);
Append(points, x2, y2, false);
Append(points, x3, y3, false);
}
}
}
}
}
I think the only way is to render each character individually and use the
Graphics.RotateTransform
to rotate the text. You'll need to work out the rotation angle and rendering offset yourself. You can use the
Graphics.MeasureCharacterRanges
to get the size of each character.
Unfortunatelly in GDI+ there is no way to attach Strings to a path (this is what you would be looking for).
So the only way to do this is doing it "by hand". That means splitting up the string into characters and placing them based on your own path calculations.
Unless you want to put a lot of work into this you should try to find a library (potentially complete GDI+ replacement) to do this or give up on your rainbow.
With WPF you can render text on a path (see link for a howto)