Related
I'm trying to draw a 3d image that displays a ripple:
function myFunc(x, y) {
let zRipple =
Math.pow(2, -0.005 * (Math.abs(x) + Math.abs(y))) *
Math.cos(((x * x + y * y) * 2 * pi) / 180 / width) *
height;
return zRipple;
}
width and height here are constants that define a drawing area and are equal to 200 in my tests.
My approach is based on what I recall from an article that I read 30 years ago and trying to recall now.
The idea is to:
split the whole drawing board into the 10-pixel grid
for each 'cell' of the grid, draw a line to the nearest cell along the Y- and the X-axis' (step=10, ds=0.0
for (let x3 = width; x3 >= - width; x3 -= step) {
for (let y3 = -height; y3 <= height; y3 += step) {
for (let s = 0; s < step; s += ds) {
let x = x3 + s;
if (x < width) {
let z3 = myFunc(x, y3);
drawPixel3d(x, y3, z3);
}
}
for (let s = 0; s < step; s += ds) {
let y = y3 + s;
if (y < height) {
let z3 = myFunc(x3, y);
drawPixel3d(x3, y, z3);
}
}
}
}
}
Here is how I convert 3d coordinates to 2d:
function drawPixel3d(x3, y3, z3) {
let x2 = (x3 + y3) * Math.sin((60 * pi) / 180);
let y2 = z3 - ((x3 - y3) * Math.sin((30 * pi) / 180)) / 4;
drawPixel(x2, y2);
}
As you see from the image below, I get a decent graphic, but there is a problem: I draw ALL dots, not only those, that are VISIBLE.
Question: How do I check if any pixel needs to be displayed or not?
From what I can recall in that article, we should follow the approach:
start drawing from the front part of the scene (which I believe I do, the closest to the viewer or screen if dot with coordinates (width, -height)
for each pixel column - remember the 'Z' coordinate and only draw the new pixel if its Z-coordinate is bigger than the last recorded one
To achieve this I've modified my 'drawPixel3d' method:
function drawPixel3d(x3, y3, z3) {
let x2 = (x3 + y3) * Math.sin((60 * pi) / 180);
let y2 = z3 - ((x3 - y3) * Math.sin((30 * pi) / 180)) / 4;
let n = Math.round(x2);
let visible = false;
if (zs[n] === undefined) {
zs[n] = z3;
visible = true;
} else {
if (z3 > zs[n]) {
visible = z3 > zs[n];
zs[n] = z3;
}
}
if (visible) drawPixel(x2, y2);
}
But the result is not expected:
What do I do wrong? Or an alternative question: how to draw a simple 3d graphic?
Thanks!
P.S. The last piece of the program (that illustrates inversion of Y-coordinate during actual drawing):
function drawPixel(x: number, y: number) {
ctx.fillRect(cX + x, cY - y, 1, 1); // TS-way to draw pixel on canvas is to draw a rectangle
} // cX and cY are coordinates of the center of the drawing canvas
P.P.S. I have an idea of the algorithmic solution, so added an 'algorithm' tag: maybe someone from this community can help?
Your surface is concave which means you can not use simple methods based on dot product between face normal and camera view direction.
You got 3 obvious options for this.
use ray tracing
as you got analytical equation of the surface this might be even better way
use depth buffering to mask out the invisible stuff
As you render wireframe then you need to do this in 2 passes:
render invisible filled surface (fill just depth buffer not the screen)
render wireframe
your depth buffer condition must contain also equal values so either z<=depth[y][x] or z>=depth[y][x]
However you need to use face rendering (triangles or quads ...) and I assume this is software rendering so if you not familiar on such stuff see:
how to rasterize rotated rectangle (in 2d by setpixel)
Algorithm to fill triangle
use depth sorting by exploiting topology
If you do not have view transform so your x,y,z coordinates are directly corresponding to camera space coordinates then you can render the grid in back to front order simply by ordering the for loops and direction of iteration (its common in isometric views). This does not need depth buffering however you need to render filled QUADS in order to obtain correct output (border is set to the plot color and the inside is filled with background color).
I did go for the #2 approach. When I ported the last link into 3D I got this (C++ code):
//---------------------------------------------------------------------------
const int col_transparent=-1; // transparent color
class gfx_main
{
public:
Graphics::TBitmap *bmp; // VCL bitmap for win32 rendering
int **scr,**zed,xs,ys; // screen,depth buffers and resolution
struct pbuf // convex polygon rasterization line buffer
{
int x,z; // values to interpolate during rendering
pbuf() {}
pbuf(pbuf& a) { *this=a; }
~pbuf() {}
pbuf* operator = (const pbuf *a) { *this=*a; return this; }
//pbuf* operator = (const pbuf &a) { ...copy... return this; }
} *pl,*pr; // left,right buffers
gfx_main();
gfx_main(gfx_main& a) { *this=a; }
~gfx_main();
gfx_main* operator = (const gfx_main *a) { *this=*a; return this; }
//gfx_main* operator = (const gfx_main &a) { ...copy... return this; }
void resize(int _xs=-1,int _ys=-1);
void clear(int z,int col); // clear buffers
void pixel(int x,int y,int z,int col); // render 3D point
void line(int x0,int y0,int z0,int x1,int y1,int z1,int col); // render 3D line
void triangle(int x0,int y0,int z0,int x1,int y1,int z1,int x2,int y2,int z2,int col); // render 3D triangle
void _triangle_line(int x0,int y0,int z0,int x1,int y1,int z1); // this is just subroutine
};
//---------------------------------------------------------------------------
gfx_main::gfx_main()
{
bmp=new Graphics::TBitmap;
scr=NULL;
zed=NULL;
pl =NULL;
pr =NULL;
xs=0; ys=0;
resize(1,1);
}
//---------------------------------------------------------------------------
gfx_main::~gfx_main()
{
if (bmp) delete bmp;
if (scr) delete[] scr;
if (zed)
{
if (zed[0]) delete[] zed[0];
delete[] zed;
}
if (pl) delete[] pl;
if (pr) delete[] pr;
}
//---------------------------------------------------------------------------
void gfx_main::resize(int _xs,int _ys)
{
// release buffers
if (scr) delete[] scr;
if (zed)
{
if (zed[0]) delete[] zed[0];
delete[] zed;
}
if (pl) delete[] pl;
if (pr) delete[] pr;
// set new resolution and pixelformat
if ((_xs>0)&&(_ys>0)) bmp->SetSize(_xs,_ys);
xs=bmp->Width;
ys=bmp->Height;
bmp->HandleType=bmDIB;
bmp->PixelFormat=pf32bit;
// allocate buffers
scr=new int*[ys];
zed=new int*[ys];
zed[0]=new int[xs*ys]; // allocate depth buffer as single block
for (int y=0;y<ys;y++)
{
scr[y]=(int*)bmp->ScanLine[y]; // screen buffer point directly to VCL bitmap (back buffer)
zed[y]=zed[0]+(y*xs); // just set pointers for each depth line instead of allocating it
}
pl=new pbuf[ys];
pr=new pbuf[ys];
}
//---------------------------------------------------------------------------
int rgb2bgr(int col) // just support function reversing RGB order as VCL/GDI and its direct pixel access are not the same pixelformat
{
union
{
BYTE db[4];
int dd;
} c;
BYTE q;
c.dd=col;
q=c.db[0]; c.db[0]=c.db[2]; c.db[2]=q;
return c.dd;
}
//---------------------------------------------------------------------------
void gfx_main::clear(int z,int col)
{
// clear buffers
int x,y;
col=rgb2bgr(col);
for (y=0;y<ys;y++)
for (x=0;x<xs;x++)
{
scr[y][x]= 0x00000000; // black
zed[y][x]=-0x7FFFFFFF; // as far as posible
}
}
//---------------------------------------------------------------------------
void gfx_main::pixel(int x,int y,int z,int col)
{
col=rgb2bgr(col);
if ((x>=0)&&(x<xs)&&(y>=0)&&(y<ys)) // inside screen
if (zed[y][x]<=z) // not after something already rendered (GL_LEQUAL)
{
zed[y][x]=z; // update depth
if (col!=col_transparent) scr[y][x]=col;// update color
}
}
//---------------------------------------------------------------------------
void gfx_main::line(int x0,int y0,int z0,int x1,int y1,int z1,int col)
{
int i,n,x,y,z,kx,ky,kz,dx,dy,dz,cx,cy,cz;
// DDA variables (d)abs delta,(k)step direction
kx=0; dx=x1-x0; if (dx>0) kx=+1; if (dx<0) { kx=-1; dx=-dx; }
ky=0; dy=y1-y0; if (dy>0) ky=+1; if (dy<0) { ky=-1; dy=-dy; }
kz=0; dz=z1-z0; if (dz>0) kz=+1; if (dz<0) { kz=-1; dz=-dz; }
n=dx; if (n<dy) n=dy; if (n<dz) n=dz; if (!n) n=1;
// integer DDA
for (x=x0,y=y0,z=z0,cx=cy=cz=n,i=0;i<n;i++)
{
pixel(x,y,z,col);
cx-=dx; if (cx<=0){ cx+=n; x+=kx; }
cy-=dy; if (cy<=0){ cy+=n; y+=ky; }
cz-=dz; if (cz<=0){ cz+=n; z+=kz; }
}
}
//---------------------------------------------------------------------------
void gfx_main::triangle(int x0,int y0,int z0,int x1,int y1,int z1,int x2,int y2,int z2,int col)
{
int x,xx0,xx1,y,yy0,yy1,z,zz0,zz1,dz,dx,kz,cz;
// boundary line coordinates to buffers
_triangle_line(x0,y0,z0,x1,y1,z1);
_triangle_line(x1,y1,z1,x2,y2,z2);
_triangle_line(x2,y2,z2,x0,y0,z0);
// y range
yy0=y0; if (yy0>y1) yy0=y1; if (yy0>y2) yy0=y2;
yy1=y0; if (yy1<y1) yy1=y1; if (yy1<y2) yy1=y2;
// fill with horizontal lines
for (y=yy0;y<=yy1;y++)
if ((y>=0)&&(y<ys))
{
if (pl[y].x<pr[y].x){ xx0=pl[y].x; zz0=pl[y].z; xx1=pr[y].x; zz1=pr[y].z; }
else { xx1=pl[y].x; zz1=pl[y].z; xx0=pr[y].x; zz0=pr[y].z; }
dx=xx1-xx0;
kz=0; dz=zz1-zz0; if (dz>0) kz=+1; if (dz<0) { kz=-1; dz=-dz; }
for (cz=dx,x=xx0,z=zz0;x<=xx1;x++)
{
pixel(x,y,z,col);
cz-=dz; if (cz<=0){ cz+=dx; z+=kz; }
}
}
}
//---------------------------------------------------------------------------
void gfx_main::_triangle_line(int x0,int y0,int z0,int x1,int y1,int z1)
{
pbuf *pp;
int i,n,x,y,z,kx,ky,kz,dx,dy,dz,cx,cy,cz;
// DDA variables (d)abs delta,(k)step direction
kx=0; dx=x1-x0; if (dx>0) kx=+1; if (dx<0) { kx=-1; dx=-dx; }
ky=0; dy=y1-y0; if (dy>0) ky=+1; if (dy<0) { ky=-1; dy=-dy; }
kz=0; dz=z1-z0; if (dz>0) kz=+1; if (dz<0) { kz=-1; dz=-dz; }
n=dx; if (n<dy) n=dy; if (n<dz) n=dz; if (!n) n=1;
// target buffer according to ky direction
if (ky>0) pp=pl; else pp=pr;
// integer DDA line start point
x=x0; y=y0;
// fix endpoints just to be sure (wrong division constants by +/-1 can cause that last point is missing)
if ((y0>=0)&&(y0<ys)){ pp[y0].x=x0; pp[y0].z=z0; }
if ((y1>=0)&&(y1<ys)){ pp[y1].x=x1; pp[y1].z=z1; }
// integer DDA (into pbuf)
for (x=x0,y=y0,z=z0,cx=cy=cz=n,i=0;i<n;i++)
{
if ((y>=0)&&(y<ys))
{
pp[y].x=x;
pp[y].z=z;
}
cx-=dx; if (cx<=0){ cx+=n; x+=kx; }
cy-=dy; if (cy<=0){ cy+=n; y+=ky; }
cz-=dz; if (cz<=0){ cz+=n; z+=kz; }
}
}
//---------------------------------------------------------------------------
Just ignore/port the VCL stuff. I just added z coordinate to interpolation and rendering and also depth buffer. The rendering code looks like this:
//---------------------------------------------------------------------------
gfx_main gfx;
//---------------------------------------------------------------------------
float myFunc(float x,float y)
{
float z;
x-=gfx.xs/2;
y-=gfx.ys/2;
z=sqrt(((x*x)+(y*y))/((gfx.xs*gfx.xs)+(gfx.ys*gfx.ys))); // normalized distance from center
z=((0.25*cos(z*8.0*M_PI)*(1.0-z))+0.5)*gfx.ys;
return z;
}
//---------------------------------------------------------------------------
void view3d(int &x,int &y,int &z) // 3D -> 2D view (projection)
{
int zz=z;
z=y;
x=x +(y/2)-(gfx.xs>>2);
y=zz+(y/2)-(gfx.ys>>2);
}
//---------------------------------------------------------------------------
void draw()
{
int i,x,y,z,ds,x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3,col;
gfx.clear(-0x7FFFFFFF,0x00000000);
// render
ds=gfx.xs/50;
for (i=0;i<2;i++) // 2 passes
for (y=ds;y<gfx.ys;y+=ds)
for (x=ds;x<gfx.xs;x+=ds)
{
// 4 vertexes of a quad face
x0=x-ds; y0=y-ds; z0=myFunc(x0,y0);
x1=x; y1=y0; z1=myFunc(x1,y1);
x2=x; y2=y; z2=myFunc(x2,y2);
x3=x0; y3=y; z3=myFunc(x3,y3);
// camera transform
view3d(x0,y0,z0);
view3d(x1,y1,z1);
view3d(x2,y2,z2);
view3d(x3,y3,z3);
if (i==0) // first pass
{
// render (just to depth)
col=col_transparent;
gfx.triangle(x0,y0,z0,x1,y1,z1,x2,y2,z2,col);
gfx.triangle(x0,y0,z0,x2,y2,z2,x3,y3,z3,col);
}
if (i==1) // second pass
{
// render wireframe
col=0x00FF0000;
gfx.line(x0,y0,z0,x1,y1,z1,col);
gfx.line(x1,y1,z1,x2,y2,z2,col);
gfx.line(x2,y2,z2,x3,y3,z3,col);
gfx.line(x3,y3,z3,x0,y0,z0,col);
}
}
// here gfx.scr holds your rendered image
//---------------------------------------------------------------------------
Do not forget to call gfx.resize(xs,ys) with resolution of your view before rendering. As you can see I used different function (does not matter) here the output:
And here the same without depth condition in pixel(x,y,z,col)
The pbuf structure holds all the stuff that will be interpolated in the last rendering interpolation of the horizontal lines. So if you want gourard, textures or whatever you just add the variable to this structure and add the interpolation to the code (mimic the pbuf[].z interpolation code)
However this approach has one drawback. Your current approach interpolates one axis pixel by pixel and the other is stepping by grid size. This one is stepping both axises by grid size. So if you want to have the same behavior you might to do the first pass with 1 x 1 quads instead of ds x ds and then do the lines as you do now. In case 1 in your view is corresponding to pixel then you can do this on pixels alone without the face rendering however you risk holes in the output.
I got the idea of the solution: start drawing from the point nearest to the observer but for every combination of x2 and y2 coordinates draw the pixel only once and only when it is visible (never draw points behind others)... The only problem is that I don't draw EVERY point of the surface, I only draw a surface grid with 10 points step. As a result, part of the surface will be visible in 'between' the grid cells.
Another idea is to calculate distance from every drawing point of the surface to the observer and make sure to draw only that point that is visible of the surface that is CLOSEST to the observer... but how?
I need to develop an algorithm that connects points in a non-linear way, that is, with smooth curves, as in the image below:
The problem is that I can not find the best solution, either using Bezier Curves, Polimonial Interpolation, Curve Adjustment, among others.
In short, I need a formula that interpolates the points according to the figure above, generating N intermediate points between one coordinate and another.
In the image above, the first coordinate (c1) is (x = 1, y = 220) and the second (c2) is (x = 2, y = 40).
So if I want to create for example 4 intermediate coordinates between c1 and c2 I will have to get an array (x, y) of 4 elements something like this:
[1.2, 180], [1.4, 140], [1.6, 120], [1.8, 80]
Would anyone have any ideas?
I think any Piecewise curve interpolation should do it. Here small C++ example:
//---------------------------------------------------------------------------
const int n=7; // points
const int n2=n+n;
float pnt[n2]= // points x,y ...
{
1.0, 220.0,
2.0, 40.0,
3.0,-130.0,
4.0,-170.0,
5.0,- 40.0,
6.0, 90.0,
7.0, 110.0,
};
//---------------------------------------------------------------------------
void getpnt(float *p,float t) // t = <0,n-1>
{
int i,ii;
float *p0,*p1,*p2,*p3,a0,a1,a2,a3,d1,d2,tt,ttt;
// handle t out of range
if (t<= 0.0f){ p[0]=pnt[0]; p[1]=pnt[1]; return; }
if (t>=float(n-1)){ p[0]=pnt[n2-2]; p[1]=pnt[n2-1]; return; }
// select patch
i=floor(t); // start point of patch
t-=i; // parameter <0,1>
i<<=1; tt=t*t; ttt=tt*t;
// control points
ii=i-2; if (ii<0) ii=0; if (ii>=n2) ii=n2-2; p0=pnt+ii;
ii=i ; if (ii<0) ii=0; if (ii>=n2) ii=n2-2; p1=pnt+ii;
ii=i+2; if (ii<0) ii=0; if (ii>=n2) ii=n2-2; p2=pnt+ii;
ii=i+4; if (ii<0) ii=0; if (ii>=n2) ii=n2-2; p3=pnt+ii;
// loop all dimensions
for (i=0;i<2;i++)
{
// compute polynomial coeficients
d1=0.5*(p2[i]-p0[i]);
d2=0.5*(p3[i]-p1[i]);
a0=p1[i];
a1=d1;
a2=(3.0*(p2[i]-p1[i]))-(2.0*d1)-d2;
a3=d1+d2+(2.0*(-p2[i]+p1[i]));
// compute point coordinate
p[i]=a0+(a1*t)+(a2*tt)+(a3*ttt);
}
}
//---------------------------------------------------------------------------
void gl_draw()
{
glClearColor(1.0,1.0,1.0,1.0);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
glDisable(GL_TEXTURE_2D);
// set 2D view
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glScalef(1.0/5.0,1.0/500.0,1.0);
glTranslatef(-4.0,0.0,0.0);
// render lines
glColor3f(1.0,0.0,0.0);
glBegin(GL_LINE_STRIP);
float p[2],t;
for (t=0.0;t<=float(n-1);t+=0.1f)
{
getpnt(p,t);
glVertex2fv(p);
}
glEnd();
// render points
glPointSize(4.0);
glColor3f(0.0,0.0,1.0);
glBegin(GL_POINTS);
for (int i=0;i<n2;i+=2) glVertex2fv(pnt+i);
glEnd();
glPointSize(1.0);
glFinish();
SwapBuffers(hdc);
}
//---------------------------------------------------------------------------
Here preview:
As you can see it is simple you just need n control points pnt (I extracted from your graph) and just interpolate ... The getpnt functions will compute any point on the curve addressed by parameter t=<0,n-1>. Internally it just select which cubic patch to use and compute as single cubic curve. In gl_draw you can see how to use it to obtain the points in between.
As your control points are uniformly distributed on the x axis:
x = <1,7>
t = <0,6>
I can write:
x = t+1
t = x-1
so you can compute any point for any x too...
The shape does not match your graph perfectly because the selected control points are not the correct ones. Any local minimum/maximum should be a control point and sometimes is safer to use also inflex points too. The starting and ending shape of the curve suggest hidden starting and ending control point which is not showed on the graph. You can use any number of points you need but beware if you break the x uniform distribution then you lose the ability to compute t from x directly!
As we do not know how the graph was created we can only guess ...
I am trying to create a shape similar to this, hexagons with 12 pentagons, at an arbitrary size.
(Image Source)
The only thing is, I have absolutely no idea what kind of code would be needed to generate it!
The goal is to be able to take a point in 3D space and convert it to a position coordinate on the grid, or vice versa and take a grid position and get the relevant vertices for drawing the mesh.
I don't even know how one would store the grid positions for this. Does each "triagle section" between 3 pentagons get their own set of 2D coordinates?
I will most likely be using C# for this, but I am more interested in which algorithms to use for this and an explanation of how they would work, rather than someone just giving me a piece of code.
The shape you have is one of so called "Goldberg polyhedra", is also a geodesic polyhedra.
The (rather elegant) algorithm to generate this (and many many more) can be succinctly encoded in something called a Conway Polyhedron Notation.
The construction is easy to follow step by step, you can click the images below to get a live preview.
The polyhedron you are looking for can be generated from an icosahedron -- Initialise a mesh with an icosahedron.
We apply a "Truncate" operation (Conway notation t) to the mesh (the sperical mapping of this one is a football).
We apply the "Dual" operator (Conway notation d).
We apply a "Truncate" operation again. At this point the recipe is tdtI (read from right!). You can already see where this is going.
Apply steps 3 & 4 repeatedly until you are satisfied.
For example below is the mesh for dtdtdtdtI.
This is quite easy to implement. I would suggest using a datastructure that makes it easy to traverse the neighbourhood give a vertex, edge etc. such as winged-edge or half-edge datastructures for your mesh. You only need to implement truncate and dual operators for the shape you are looking for.
First some analysis of the image in the question: the spherical triangle spanned by neighbouring pentagon centers seems to be equilateral. When five equilateral triangles meet in one corner and cover the whole sphere, this can only be the configuration induced by a icosahedron. So there are 12 pentagons and 20 patches of a triangular cutout of a hexongal mesh mapped to the sphere.
So this is a way to construct such a hexagonal grid on the sphere:
Create triangular cutout of hexagonal grid: a fixed triangle (I chose (-0.5,0),(0.5,0),(0,sqrt(3)/2) ) gets superimposed a hexagonal grid with desired resolution n s.t. the triangle corners coincide with hexagon centers, see the examples for n = 0,1,2,20:
Compute corners of icosahedron and define the 20 triangular faces of it (see code below). The corners of the icosahedron define the centers of the pentagons, the faces of the icosahedron define the patches of the mapped hexagonal grids. (The icosahedron gives the finest regular division of the sphere surface into triangles, i.e. a division into congruent equilateral triangles. Other such divisions can be derived from a tetrahedron or an octahedron; then at the corners of the triangles one will have triangles or squares, resp. Furthermore the fewer and bigger triangles would make the inevitable distortion in any mapping of a planar mesh onto a curved surface more visible. So choosing the icosahedron as a basis for the triangular patches helps minimizing the distortion of the hexagons.)
Map triangular cutout of hexagonal grid to spherical triangles corresponding to icosaeder faces: a double-slerp based on barycentric coordinates does the trick. Below is an illustration of the mapping of a triangular cutout of a hexagonal grid with resolution n = 10 onto one spherical triangle (defined by one face of an icosaeder), and an illustration of mapping the grid onto all these spherical triangles covering the whole sphere (different colors for different mappings):
Here is Python code to generate the corners (coordinates) and triangles (point indices) of an icosahedron:
from math import sin,cos,acos,sqrt,pi
s,c = 2/sqrt(5),1/sqrt(5)
topPoints = [(0,0,1)] + [(s*cos(i*2*pi/5.), s*sin(i*2*pi/5.), c) for i in range(5)]
bottomPoints = [(-x,y,-z) for (x,y,z) in topPoints]
icoPoints = topPoints + bottomPoints
icoTriangs = [(0,i+1,(i+1)%5+1) for i in range(5)] +\
[(6,i+7,(i+1)%5+7) for i in range(5)] +\
[(i+1,(i+1)%5+1,(7-i)%5+7) for i in range(5)] +\
[(i+1,(7-i)%5+7,(8-i)%5+7) for i in range(5)]
And here is the Python code to map (points of) the fixed triangle to a spherical triangle using a double slerp:
# barycentric coords for triangle (-0.5,0),(0.5,0),(0,sqrt(3)/2)
def barycentricCoords(p):
x,y = p
# l3*sqrt(3)/2 = y
l3 = y*2./sqrt(3.)
# l1 + l2 + l3 = 1
# 0.5*(l2 - l1) = x
l2 = x + 0.5*(1 - l3)
l1 = 1 - l2 - l3
return l1,l2,l3
from math import atan2
def scalProd(p1,p2):
return sum([p1[i]*p2[i] for i in range(len(p1))])
# uniform interpolation of arc defined by p0, p1 (around origin)
# t=0 -> p0, t=1 -> p1
def slerp(p0,p1,t):
assert abs(scalProd(p0,p0) - scalProd(p1,p1)) < 1e-7
ang0Cos = scalProd(p0,p1)/scalProd(p0,p0)
ang0Sin = sqrt(1 - ang0Cos*ang0Cos)
ang0 = atan2(ang0Sin,ang0Cos)
l0 = sin((1-t)*ang0)
l1 = sin(t *ang0)
return tuple([(l0*p0[i] + l1*p1[i])/ang0Sin for i in range(len(p0))])
# map 2D point p to spherical triangle s1,s2,s3 (3D vectors of equal length)
def mapGridpoint2Sphere(p,s1,s2,s3):
l1,l2,l3 = barycentricCoords(p)
if abs(l3-1) < 1e-10: return s3
l2s = l2/(l1+l2)
p12 = slerp(s1,s2,l2s)
return slerp(p12,s3,l3)
[Complete re-edit 18.10.2017]
the geometry storage is on you. Either you store it in some kind of Mesh or you generate it on the fly. I prefer to store it. In form of 2 tables. One holding all the vertexes (no duplicates) and the other holding 6 indexes of used points per each hex you got and some aditional info like spherical position to ease up the post processing.
Now how to generate this:
create hex triangle
the size should be radius of your sphere. do not include the corner hexess and also skip last line of the triangle (on both radial and axial so there is 1 hex gap between neighbor triangles on sphere) as that would overlap when joining out triangle segments.
convert 60deg hexagon triangle to 72deg pie
so simply convert to polar coordiantes (radius,angle), center triangle around 0 deg. Then multiply radius by cos(angle)/cos(30); which will convert triangle into Pie. And then rescale angle with ratio 72/60. That will make our triangle joinable...
copy&rotate triangle to fill 5 segments of pentagon
easy just rotate the points of first triangle and store as new one.
compute z
based on this Hexagonal tilling of hemi-sphere you can convert distance in 2D map into arc-length to limit the distortions as much a s possible.
However when I tried it (example below) the hexagons are a bit distorted so the depth and scaling needs some tweaking. Or post processing latter.
copy the half sphere to form a sphere
simply copy the points/hexes and negate z axis (or rotate by 180 deg if you want to preserve winding).
add equator and all of the missing pentagons and hexes
You should use the coordinates of the neighboring hexes so no more distortion and overlaps are added to the grid. Here preview:
Blue is starting triangle. Darker blue are its copies. Red are pole pentagons. Dark green is the equator, Lighter green are the join lines between triangles. In Yellowish are the missing equator hexagons near Dark Orange pentagons.
Here simple C++ OpenGL example (made from the linked answer in #4):
//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#pragma hdrstop
#include "win_main.h"
#include "gl/OpenGL3D_double.cpp"
#include "PolyLine.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMain *Main;
OpenGLscreen scr;
bool _redraw=true;
double animx= 0.0,danimx=0.0;
double animy= 0.0,danimy=0.0;
//---------------------------------------------------------------------------
PointTab pnt; // (x,y,z)
struct _hexagon
{
int ix[6]; // index of 6 points, last point duplicate for pentagon
int a,b; // spherical coordinate
DWORD col; // color
// inline
_hexagon() {}
_hexagon(_hexagon& a) { *this=a; }
~_hexagon() {}
_hexagon* operator = (const _hexagon *a) { *this=*a; return this; }
//_hexagon* operator = (const _hexagon &a) { ...copy... return this; }
};
List<_hexagon> hex;
//---------------------------------------------------------------------------
// https://stackoverflow.com/a/46787885/2521214
//---------------------------------------------------------------------------
void hex_sphere(int N,double R)
{
const double c=cos(60.0*deg);
const double s=sin(60.0*deg);
const double sy= R/(N+N-2);
const double sz=sy/s;
const double sx=sz*c;
const double sz2=0.5*sz;
const int na=5*(N-2);
const int nb= N;
const int b0= N;
double *q,p[3],ang,len,l,l0,ll;
int i,j,n,a,b,ix;
_hexagon h,*ph;
hex.allocate(na*nb);
hex.num=0;
pnt.reset3D(N*N);
b=0; a=0; ix=0;
// generate triangle hex grid
h.col=0x00804000;
for (b=1;b<N-1;b++) // skip first line b=0
for (a=1;a<b;a++) // skip first and last line
{
p[0]=double(a )*(sx+sz);
p[1]=double(b-(a>>1))*(sy*2.0);
p[2]=0.0;
if (int(a&1)!=0) p[1]-=sy;
ix=pnt.add(p[0]+sz2+sx,p[1] ,p[2]); h.ix[0]=ix; // 2 1
ix=pnt.add(p[0]+sz2 ,p[1]+sy,p[2]); h.ix[1]=ix; // 3 0
ix=pnt.add(p[0]-sz2 ,p[1]+sy,p[2]); h.ix[2]=ix; // 4 5
ix=pnt.add(p[0]-sz2-sx,p[1] ,p[2]); h.ix[3]=ix;
ix=pnt.add(p[0]-sz2 ,p[1]-sy,p[2]); h.ix[4]=ix;
ix=pnt.add(p[0]+sz2 ,p[1]-sy,p[2]); h.ix[5]=ix;
h.a=a;
h.b=N-1-b;
hex.add(h);
} n=hex.num; // remember number of hexs for the first triangle
// distort points to match area
for (ix=0;ix<pnt.nn;ix+=3)
{
// point pointer
q=pnt.pnt.dat+ix;
// convert to polar coordinates
ang=atan2(q[1],q[0]);
len=vector_len(q);
// match area of pentagon (72deg) triangle as we got hexagon (60deg) triangle
ang-=60.0*deg; // rotate so center of generated triangle is angle 0deg
while (ang>+60.0*deg) ang-=pi2;
while (ang<-60.0*deg) ang+=pi2;
len*=cos(ang)/cos(30.0*deg); // scale radius so triangle converts to pie
ang*=72.0/60.0; // scale up angle so rotated triangles merge
// convert back to cartesian
q[0]=len*cos(ang);
q[1]=len*sin(ang);
}
// copy and rotate the triangle to cover pentagon
h.col=0x00404000;
for (ang=72.0*deg,a=1;a<5;a++,ang+=72.0*deg)
for (ph=hex.dat,i=0;i<n;i++,ph++)
{
for (j=0;j<6;j++)
{
vector_copy(p,pnt.pnt.dat+ph->ix[j]);
rotate2d(-ang,p[0],p[1]);
h.ix[j]=pnt.add(p[0],p[1],p[2]);
}
h.a=ph->a+(a*(N-2));
h.b=ph->b;
hex.add(h);
}
// compute z
for (q=pnt.pnt.dat,ix=0;ix<pnt.nn;ix+=pnt.dn,q+=pnt.dn)
{
q[2]=0.0;
ang=vector_len(q)*0.5*pi/R;
q[2]=R*cos(ang);
ll=fabs(R*sin(ang)/sqrt((q[0]*q[0])+(q[1]*q[1])));
q[0]*=ll;
q[1]*=ll;
}
// copy and mirror the other half-sphere
n=hex.num;
for (ph=hex.dat,i=0;i<n;i++,ph++)
{
for (j=0;j<6;j++)
{
vector_copy(p,pnt.pnt.dat+ph->ix[j]);
p[2]=-p[2];
h.ix[j]=pnt.add(p[0],p[1],p[2]);
}
h.a= ph->a;
h.b=-ph->b;
hex.add(h);
}
// create index search table
int i0,i1,j0,j1,a0,a1,ii[5];
int **ab=new int*[na];
for (a=0;a<na;a++)
{
ab[a]=new int[nb+nb+1];
for (b=-nb;b<=nb;b++) ab[a][b0+b]=-1;
}
n=hex.num;
for (ph=hex.dat,i=0;i<n;i++,ph++) ab[ph->a][b0+ph->b]=i;
// add join ring
h.col=0x00408000;
for (a=0;a<na;a++)
{
h.a=a;
h.b=0;
a0=a;
a1=a+1; if (a1>=na) a1-=na;
i0=ab[a0][b0+1];
i1=ab[a1][b0+1];
j0=ab[a0][b0-1];
j1=ab[a1][b0-1];
if ((i0>=0)&&(i1>=0))
if ((j0>=0)&&(j1>=0))
{
h.ix[0]=hex[i1].ix[1];
h.ix[1]=hex[i0].ix[0];
h.ix[2]=hex[i0].ix[1];
h.ix[3]=hex[j0].ix[1];
h.ix[4]=hex[j0].ix[0];
h.ix[5]=hex[j1].ix[1];
hex.add(h);
ab[h.a][b0+h.b]=hex.num-1;
}
}
// add 2x5 join lines
h.col=0x00008040;
for (a=0;a<na;a+=N-2)
for (b=1;b<N-3;b++)
{
// +b hemisphere
h.a= a;
h.b=+b;
a0=a-b; if (a0< 0) a0+=na; i0=ab[a0][b0+b+0];
a0--; if (a0< 0) a0+=na; i1=ab[a0][b0+b+1];
a1=a+1; if (a1>=na) a1-=na; j0=ab[a1][b0+b+0];
j1=ab[a1][b0+b+1];
if ((i0>=0)&&(i1>=0))
if ((j0>=0)&&(j1>=0))
{
h.ix[0]=hex[i0].ix[5];
h.ix[1]=hex[i0].ix[4];
h.ix[2]=hex[i1].ix[5];
h.ix[3]=hex[j1].ix[3];
h.ix[4]=hex[j0].ix[4];
h.ix[5]=hex[j0].ix[3];
hex.add(h);
}
// -b hemisphere
h.a= a;
h.b=-b;
a0=a-b; if (a0< 0) a0+=na; i0=ab[a0][b0-b+0];
a0--; if (a0< 0) a0+=na; i1=ab[a0][b0-b-1];
a1=a+1; if (a1>=na) a1-=na; j0=ab[a1][b0-b+0];
j1=ab[a1][b0-b-1];
if ((i0>=0)&&(i1>=0))
if ((j0>=0)&&(j1>=0))
{
h.ix[0]=hex[i0].ix[5];
h.ix[1]=hex[i0].ix[4];
h.ix[2]=hex[i1].ix[5];
h.ix[3]=hex[j1].ix[3];
h.ix[4]=hex[j0].ix[4];
h.ix[5]=hex[j0].ix[3];
hex.add(h);
}
}
// add pentagons at poles
_hexagon h0,h1;
h0.col=0x00000080;
h0.a=0; h0.b=N-1; h1=h0; h1.b=-h1.b;
p[2]=sqrt((R*R)-(sz*sz));
for (ang=0.0,a=0;a<5;a++,ang+=72.0*deg)
{
p[0]=2.0*sz*cos(ang);
p[1]=2.0*sz*sin(ang);
h0.ix[a]=pnt.add(p[0],p[1],+p[2]);
h1.ix[a]=pnt.add(p[0],p[1],-p[2]);
}
h0.ix[5]=h0.ix[4]; hex.add(h0);
h1.ix[5]=h1.ix[4]; hex.add(h1);
// add 5 missing hexagons at poles
h.col=0x00600060;
for (ph=&h0,b=N-3,h.b=N-2,i=0;i<2;i++,b=-b,ph=&h1,h.b=-h.b)
{
a = 1; if (a>=na) a-=na; ii[0]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[1]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[2]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[3]=ab[a][b0+b];
a+=N-2; if (a>=na) a-=na; ii[4]=ab[a][b0+b];
for (j=0;j<5;j++)
{
h.a=((4+j)%5)*(N-2)+1;
h.ix[0]=ph->ix[ (5-j)%5 ];
h.ix[1]=ph->ix[ (6-j)%5 ];
h.ix[2]=hex[ii[(j+4)%5]].ix[4];
h.ix[3]=hex[ii[(j+4)%5]].ix[5];
h.ix[4]=hex[ii[ j ]].ix[3];
h.ix[5]=hex[ii[ j ]].ix[4];
hex.add(h);
}
}
// add 2*5 pentagons and 2*5 missing hexagons at equator
h0.a=0; h0.b=N-1; h1=h0; h1.b=-h1.b;
for (ang=36.0*deg,a=0;a<na;a+=N-2,ang-=72.0*deg)
{
p[0]=R*cos(ang);
p[1]=R*sin(ang);
p[2]=sz;
i0=pnt.add(p[0],p[1],+p[2]);
i1=pnt.add(p[0],p[1],-p[2]);
a0=a-1;if (a0< 0) a0+=na;
a1=a+1;if (a1>=na) a1-=na;
ii[0]=ab[a0][b0-1]; ii[2]=ab[a1][b0-1];
ii[1]=ab[a0][b0+1]; ii[3]=ab[a1][b0+1];
// hexagons
h.col=0x00008080;
h.a=a; h.b=0;
h.ix[0]=hex[ii[0]].ix[0];
h.ix[1]=hex[ii[0]].ix[1];
h.ix[2]=hex[ii[1]].ix[1];
h.ix[3]=hex[ii[1]].ix[0];
h.ix[4]=i0;
h.ix[5]=i1;
hex.add(h);
h.a=a; h.b=0;
h.ix[0]=hex[ii[2]].ix[2];
h.ix[1]=hex[ii[2]].ix[1];
h.ix[2]=hex[ii[3]].ix[1];
h.ix[3]=hex[ii[3]].ix[2];
h.ix[4]=i0;
h.ix[5]=i1;
hex.add(h);
// pentagons
h.col=0x000040A0;
h.a=a; h.b=0;
h.ix[0]=hex[ii[0]].ix[0];
h.ix[1]=hex[ii[0]].ix[5];
h.ix[2]=hex[ii[2]].ix[3];
h.ix[3]=hex[ii[2]].ix[2];
h.ix[4]=i1;
h.ix[5]=i1;
hex.add(h);
h.a=a; h.b=0;
h.ix[0]=hex[ii[1]].ix[0];
h.ix[1]=hex[ii[1]].ix[5];
h.ix[2]=hex[ii[3]].ix[3];
h.ix[3]=hex[ii[3]].ix[2];
h.ix[4]=i0;
h.ix[5]=i0;
hex.add(h);
}
// release index search table
for (a=0;a<na;a++) delete[] ab[a];
delete[] ab;
}
//---------------------------------------------------------------------------
void hex_draw(GLuint style) // draw hex
{
int i,j;
_hexagon *h;
for (h=hex.dat,i=0;i<hex.num;i++,h++)
{
if (style==GL_POLYGON) glColor4ubv((BYTE*)&h->col);
glBegin(style);
for (j=0;j<6;j++) glVertex3dv(pnt.pnt.dat+h->ix[j]);
glEnd();
}
if (0)
if (style==GL_POLYGON)
{
scr.text_init_pixel(0.1,-0.2);
glColor3f(1.0,1.0,1.0);
for (h=hex.dat,i=0;i<hex.num;i++,h++)
if (abs(h->b)<2)
{
double p[3];
vector_ld(p,0.0,0.0,0.0);
for (j=0;j<6;j++)
vector_add(p,p,pnt.pnt.dat+h->ix[j]);
vector_mul(p,p,1.0/6.0);
scr.text(p[0],p[1],p[2],AnsiString().sprintf("%i,%i",h->a,h->b));
}
scr.text_exit_pixel();
}
}
//---------------------------------------------------------------------------
void TMain::draw()
{
scr.cls();
int x,y;
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0,0.0,-5.0);
glRotated(animx,1.0,0.0,0.0);
glRotated(animy,0.0,1.0,0.0);
hex_draw(GL_POLYGON);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0,0.0,-5.0+0.01);
glRotated(animx,1.0,0.0,0.0);
glRotated(animy,0.0,1.0,0.0);
glColor3f(1.0,1.0,1.0);
glLineWidth(2);
hex_draw(GL_LINE_LOOP);
glCirclexy(0.0,0.0,0.0,1.5);
glLineWidth(1);
scr.exe();
scr.rfs();
}
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner)
{
scr.init(this);
hex_sphere(10,1.5);
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender)
{
scr.exit();
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormPaint(TObject *Sender)
{
_redraw=true;
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender)
{
scr.resize();
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60,float(scr.xs)/float(scr.ys),0.1,100.0);
_redraw=true;
}
//-----------------------------------------------------------------------
void __fastcall TMain::Timer1Timer(TObject *Sender)
{
animx+=danimx; if (animx>=360.0) animx-=360.0; _redraw=true;
animy+=danimy; if (animy>=360.0) animy-=360.0; _redraw=true;
if (_redraw) { draw(); _redraw=false; }
}
//---------------------------------------------------------------------------
void __fastcall TMain::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift)
{
Caption=Key;
if (Key==40){ animx+=2.0; _redraw=true; }
if (Key==38){ animx-=2.0; _redraw=true; }
if (Key==39){ animy+=2.0; _redraw=true; }
if (Key==37){ animy-=2.0; _redraw=true; }
}
//---------------------------------------------------------------------------
I know it is a bit of a index mess and also winding rule is not guaranteed as I was too lazy to made uniform indexing. Beware the a indexes of each hex are not linear and if you want to use them to map to 2D map you would need to recompute it using atan2 on x,y of its center point position.
Here previews:
Still some distortions are present. They are caused by fact that we using 5 triangles to connect at equator (so connection is guaranteed). That means the circumference is 5*R instead of 6.28*R. How ever this can be still improved by a field simulation. Just take all the points and add retractive forces based on their distance and bound to sphere surface. Run simulation and when the oscillations lower below threshold you got your sphere grid ...
Another option would be find out some equation to remap the grid points (similarly what I done for triangle to pie conversion) that would have better results.
I am currently work some sort of map generation algorithm for my game. I have a basic understanding on what I want it to do and how it would generate the map.
I want to use the Polar Coordinate system. I want a circular graph so that each player would spawn on the edge of the circle, evenly spread out.
The algorithm should generate "cities" spread out from across the circle (but only inside the circle). Each city should be connected some form of way.
The size of the circle should depends on the number of players.
Everything should be random, meaning if I run
GenerateMap()
two times, it should not give the same results.
Here is a picture showing what I want: img
The red arrows are pointing to the cities and the lines are the connections between the cities.
How would I go about creating an algorithm based on the above?
Update: Sorry the link was broken. Fixed it.
I see the cities like this:
compute sizes and constants from N
as your cities should have constant average density then the radius can be computed from it directly. as it scales linearly with average or min city distance.
loop N (cities) times
generate random (x,y) with uniform distribution
throw away iterations where (x,y) is outside circle
throw away iterations where (x,y) is too near to already generated city
The paths are similar just generate all possible paths (non random) and throw away:
paths much longer then average or min distance between cities (connects jutst neighbors)
paths that intersect already generated path
In C++ code it could look like this:
//---------------------------------------------------------------------------
// some globals first
const int _max=128; // just max limit for cities and paths
const int R0=10; // city radius
const int RR=R0*R0*9; // min distance^2 between cities
int N=20; // number of cities
int R1=100; // map radius
struct _city { int x,y; }; // all the data you need for city
_city city[_max]; // list of cities
struct _path { int i0,i1; };// index of cities to join with path
_path path[_max]; // list of paths
int M=0; // number of paths in the list
//---------------------------------------------------------------------------
bool LinesIntersect(float X1,float Y1,float X2,float Y2,float X3,float Y3,float X4,float Y4)
{
if (fabs(X2-X3)+fabs(Y2-Y3)<1e-3) return false;
if (fabs(X1-X4)+fabs(Y1-Y4)<1e-3) return false;
float dx1,dy1,dx2,dy2;
dx1 = X2 - X1;
dy1 = Y2 - Y1;
dx2 = X4 - X3;
dy2 = Y4 - Y3;
float s,t,ax,ay,b;
ax=X1-X3;
ay=Y1-Y3;
b=(-(dx2*dy1)+(dx1*dy2)); if (fabs(b)>1e-3) b=1.0/b; else b=0.0;
s = (-(dy1*ax)+(dx1*ay))*b;
t = ( (dx2*ay)-(dy2*ax))*b;
if ((s>=0)&&(s<=1)&&(t>=0)&&(t<=1)) return true;
return false; // No collision
}
//---------------------------------------------------------------------------
// here generate n cities into circle at x0,y0
// compute R1,N from R0,RR,n
void genere(int x0,int y0,int n)
{
_city c;
_path p,*q;
int i,j,cnt,x,y,rr;
Randomize(); // init pseudo random generator
// [generate cities]
R1=(sqrt(RR*n)*8)/10;
rr=R1-R0; rr*=rr;
for (cnt=50*n,i=0;i<n;) // loop until all cities are generated
{
// watchdog
cnt--; if (cnt<=0) break;
// pseudo random position
c.x=Random(R1+R1)-R1; // <-r,+r>
c.y=Random(R1+R1)-R1; // <-r,+r>
// ignore cities outside R1 radius
if ((c.x*c.x)+(c.y*c.y)>rr) continue;
c.x+=x0; // position to center
c.y+=y0;
// ignore city if closer to any other then RR
for (j=0;j<i;j++)
{
x=c.x-city[j].x;
y=c.y-city[j].y;
if ((x*x)+(y*y)<=RR) { j=-1; break; }
}
if (j<0) continue;
// add new city to list
city[i]=c; i++;
}
N=i; // just in case watch dog kiks in
// [generate paths]
for (M=0,p.i0=0;p.i0<N;p.i0++)
for (p.i1=p.i0+1;p.i1<N;p.i1++)
{
// ignore too long path
x=city[p.i1].x-city[p.i0].x;
y=city[p.i1].y-city[p.i0].y;
if ((x*x)+(y*y)>5*RR) continue; // this constant determine the path density per city
// ignore intersecting path
for (q=path,i=0;i<M;i++,q++)
if ((q->i0!=p.i0)&&(q->i0!=p.i1)&&(q->i1!=p.i0)&&(q->i1!=p.i1))
if (LinesIntersect(
city[p.i0].x,city[p.i0].y,city[p.i1].x,city[p.i1].y,
city[q->i0].x,city[q->i0].y,city[q->i1].x,city[q->i1].y
)) { i=-1; break; }
if (i<0) continue;
// add path to list
if (M>=_max) break;
path[M]=p; M++;
}
}
//---------------------------------------------------------------------------
Here overview of generated layout:
And Growth of N:
The blue circles are the cities, the gray area is the target circle and Lines are the paths. The cnt is just watch dog to avoid infinite loop if constants are wrong. Set the _max value properly so it is high enough for your N or use dynamic allocation instead. There is much more paths than cities so they could have separate _max value to preserve memory (was too lazy to add it).
You can use the RandSeed to have procedural generated maps ...
You can rescale output to better match circle layout after the generation simply by finding bounding box and rescale to radius R1.
Some constants are obtained empirically so play with them to achieve best output for your purpose.
I have n circles that must be perfectly surrounding an ellipse as shown in the picture here :
In this picture I need to find out the position of each circle around the ellipse, and also be able to calculate the ellipse that will fit perfectly inside those surrounding circles.
The information i know is the radius of each circles (all same), and the number of circles.
Hopefully this time the post is clear.
Thanks for your help.
Please let me know if you need more explanation.
OK as i understand you know common radius of circles R0 and their number N and want to know inside ellipse parameters and positions of everything.
If we convert ellipse to circle then we get this:
const int N=12; // number of satelite circles
const double R=10.0; // radius of satelite circles
struct _circle { double x,y,r; } circle[N]; // satelite circles
int i;
double x,y,r,l,a,da;
x=0.0; // start pos of first satelite circle
y=0.0;
r=R;
l=r+r; // distance ang angle between satelite circle centers
a=0.0*deg;
da=divide(360.0*deg,N);
for (i=0;i<N;i++)
{
circle[i].x=x; x+=l*cos(a);
circle[i].y=y; y+=l*sin(a);
circle[i].r=r; a+=da;
}
// inside circle params
_circle c;
r=divide(0.5*l,sin(0.5*da))-R;
c.x=circle[i].x;
c.y=circle[i].y+R+r;
c.r=r;
[Edit 1]
For ellipse its a whole new challenge (took me two hours to find all quirks out)
const int N=20; // number of satelite circles
const double R=10.0; // satelite circles radius
const double E= 0.7; // ellipse distortion ry=rx*E
struct _circle { double x,y,r; _circle() { x=0; y=0; r=0.0; } } circle[N];
struct _ellipse { double x,y,rx,ry; _ellipse() { x=0; y=0; rx=0.0; ry=0.0; } } ellipse;
int i,j,k;
double l,a,da,m,dm,x,y,q,r0;
l=double(N)*R; // circle cener lines polygon length
ellipse.x =0.0; // set ellipse parameters
ellipse.y =0.0;
r0=divide(l,M_PI*sqrt(0.5*(1.0+(E*E))))-R;// aprox radius to match ellipse length for start
l=R+R; l*=l;
m=1.0; dm=1.0; x=0.0;
for (k=0;k<5;k++) // aproximate ellipse size to the right size
{
dm=fabs(0.1*dm); // each k-iteration layer is 10x times more accurate
if (x>l) dm=-dm;
for (;;)
{
ellipse.rx=r0 *m;
ellipse.ry=r0*E*m;
for (a=0.0,i=0;i<N;i++) // set circle parameters
{
q=(2.0*a)-atanxy(cos(a),sin(a)*E);
circle[i].x=ellipse.x+(ellipse.rx*cos(a))+(R*cos(q));
circle[i].y=ellipse.y+(ellipse.ry*sin(a))+(R*sin(q));
circle[i].r=R;
da=divide(360*deg,N); a+=da;
for (j=0;j<5;j++) // aproximate next position to match 2R distance from current position
{
da=fabs(0.1*da); // each j-iteration layer is 10x times more accurate
q=(2.0*a)-atanxy(cos(a),sin(a)*E);
x=ellipse.x+(ellipse.rx*cos(a))+(R*cos(q))-circle[i].x; x*=x;
y=ellipse.y+(ellipse.ry*sin(a))+(R*sin(q))-circle[i].y; y*=y; x+=y;
if (x>l) for (;;) // if too far dec angle
{
a-=da;
q=(2.0*a)-atanxy(cos(a),sin(a)*E);
x=ellipse.x+(ellipse.rx*cos(a))+(R*cos(q))-circle[i].x; x*=x;
y=ellipse.y+(ellipse.ry*sin(a))+(R*sin(q))-circle[i].y; y*=y; x+=y;
if (x<=l) break;
}
else if (x<l) for (;;) // if too short inc angle
{
a+=da;
q=(2.0*a)-atanxy(cos(a),sin(a)*E);
x=ellipse.x+(ellipse.rx*cos(a))+(R*cos(q))-circle[i].x; x*=x;
y=ellipse.y+(ellipse.ry*sin(a))+(R*sin(q))-circle[i].y; y*=y; x+=y;
if (x>=l) break;
}
else break;
}
}
// check if last circle is joined as it should be
x=circle[N-1].x-circle[0].x; x*=x;
y=circle[N-1].y-circle[0].y; y*=y; x+=y;
if (dm>0.0) { if (x>=l) break; }
else { if (x<=l) break; }
m+=dm;
}
}
Well I know its a little messy code so here is some info:
first it try to set as close ellipse rx,ry axises as possible
ellipse length should be about N*R*2 which is polygon length of lines between circle centers
try to compose circles so they are touching each other and the ellipse
I use iteration of ellipse angle for that. Problem is that circles do not touch the ellipse in their position angle thats why there is q variable ... to compensate around ellipse normal. Look for yellowish-golden lines in image
after placing circles check if the last one is touching the first
if not interpolate the size of ellipse actually it scales the rx,ry by m variable up or down
you can adjust accuracy
by change of the j,k fors and/or change of dm,da scaling factors
input parameter E should be at least 0.5 and max 1.0
if not then there is high probability of misplacing circles because on very eccentric ellipses is not possible to fit circles (if N is too low). Ideal setting is 0.7<=E<=1.0 closser to 1 the safer the algorithm is
atanxy(dx,dy) is the same as `atan(dy/dx)
but it handles all 4 quadrants like atan2(dy,dx) by sign analysis of dx,dy
Hope it helps