//############################## Ellipse #####################
//Creates an ellipse and allows points to be projected onto it.
//
class Ellipse
{
float x;
float y;
float z;
float radius;
float scaleX;
float scaleY;
float scaleZ;
//############################## CONSTRUCTORS #####################
//-----------------------------------------
//Constructs the ellipse - based off a distored sphere.
//XYZ is the center.
//Radius is the overall scale.
//scaleXYZ is the amount to scale the sphere by.
//-----------------------------------------
public Ellipse(float _x, float _y, float _z, float _radius, float _scaleX, float _scaleY, float _scaleZ)
{
x = _x;
y = _y;
z = _z;
radius = _radius;
scaleX = _scaleX;
scaleY = _scaleY;
scaleZ = _scaleZ;
}
//############################## SETS #####################
//-----------------------------------------
// Use to resize ellipse.
// Adds the passed number to the scale of the ellise.
//-----------------------------------------
public void addX(float _x){scaleX += _x;}
public void addY(float _y){scaleY += _y;}
public void addZ(float _z){scaleZ += _z;}
//############################## GETS #####################
//-----------------------------------------
// Returns the radius of the ellipse, and the scales.
//-----------------------------------------
public float getRadius(){ return radius;}
public float getX(){ return scaleX;}
public float getY(){ return scaleY;}
public float getZ(){ return scaleZ;}
//-----------------------------------------
//Projects a point in xyz space onto the ellipse.
//
//Does this by transforming the ellipse into a sphere - also do this to the coordinates of the points.
//
//The point is then pojected onto the sphere by projecting it towards the center of the sphere.
//The projected point is therefore the distance of the radius along the vector that joins
//the center of the sphere with the original point.
//
//This is a fairly nieve way to do this, but since the points wont be far from the ellipse it does not matter.
//
//If the point goes below the base plane (y=0) it is reflected.
//Returns the new xyz coordinates of the projected point.
//-----------------------------------------
public float[] projectPoint(float pX, float pY, float pZ)
{
//first transform point from eliptical space into spherical space
pX /= scaleX;
pY /= scaleY;
pZ /= scaleZ;
//find the vector between the point and the center of the sphere.
float vX = pX - x;
float vY = pY - y;
float vZ = pZ - z;
//normalise the vector.
float d = sqrt(vX * vX + vY * vY + vZ * vZ);
float factor = 1/d;
vX *= factor;
vY *= factor;
vZ *= factor;
//scale the vector to the same length of the radius.
vX *= radius;
vY *= radius;
vZ *= radius;
//Point is at end of this vector when it starts from the center of the sphere.
float newX = x + vX;
float newY = y + vY;
float newZ = z + vZ;
//scale point back into elliptical space
newX *= scaleX;
newY *= scaleY;
newZ *= scaleZ;
//Use this to make sure no points drop below the base plane
if(newY > 0) newY = -newY;
//return new point
return new float[]{newX,newY, newZ};
}
//############################## DRAWING #####################
//-----------------------------------------
// Draws ellipse as a sphere.
// Only works if xyz scale is 1.
//-----------------------------------------
public void draw()
{
pushMatrix();
fill(204, 102, 0, 150);
translate(x, y, z);
sphere(radius);
popMatrix();
}
}
//############################## Point #####################
//A point on the surface.
//Stores the xyz location of the point, and what other points are close.
//Has functions to update location based on a flocking algorithm
//And project the point back onto the surface.
//Also draws the point
class Point
{
private int id; //Id of point in global point array
private float x; //location of point
private float y;
private float z;
private float updatedX; //new location of point before it is projected.
private float updatedY;
private float updatedZ;
private ArrayList connected; //Id's of points that are the ideal distance apart.
private ArrayList pushing; //points that are too close
private ArrayList pulling; ///points taht are almost ideal.
//############################## CONSTRUCTORS #####################
//-----------------------------------------
// Constructs the point from xyz data.
// Sets the arrays and tells point to move onto the surface.
//-----------------------------------------
public Point(int _id, float _x, float _y, float _z)
{
id = _id;
x = _x;
y = _y;
z = _z;
updatedX = _x;
updatedY = _y;
updatedZ = _z;
connected = new ArrayList();
pushing = new ArrayList();
pulling= new ArrayList();
setPosition();
}
//-----------------------------------------
// Constructs the point randomly in xyz space.
// As no functions can be called before 'this' all maths is done in the function.
// Use the ellipse scale to set bounds of the random placement to ensure it is close to the sphere.
// Then passed to the normal constructor.
//-----------------------------------------
public Point(int _id)
{
this(_id,
(random(ellipseScale) - ellipseScale/2) * ellipseX,
(random(ellipseScale/2) -ellipseScale/2) * ellipseY,
(random(ellipseScale) -ellipseScale/2) * ellipseZ);
}
//############################## SETS #####################
//-----------------------------------------
// Projects the updated position back onto the ellipse and
// sets the point position to this projected position.
//-----------------------------------------
public void setPosition()
{
float[] newPoint = globalEllipse.projectPoint(updatedX,updatedY,updatedZ);
x = newPoint[0];
y = newPoint[1];
z = newPoint[2];
}
//-----------------------------------------
// Summary:
// Updates the position of the point by looping through every point
// and finding the average vector away from points that are too close.
// This algorithm is some sort of flocking / dynamic relaxation.
//
//
// Logic:
// Loops through every point - This loop could be optimised but it works ok.
// Finds the vector between this point and the other point.
// If vector is too short (as defined by minLength) finds the vector away from this point.
// Scales this vector so points that are very close create larger vectors.
// Adds this vector to the away vector.
// After the loop is complete average away vector, based on how many vectors were added to it.
// Takes a fraction of that vector (as defined by speed) - Do not want to move all of it or the relaxation will not work.
// And move the point along that vector.
//
// Sets the updated values to this new point.
// Note that the updated values are not on the ellipse. setPostion() places them on the ellipse.
// In this process, points that are too close, just the right distance, and just over the right distance
// are stored in the arrayLists.
//
//-----------------------------------------
public void updatePosition()
{
connected = new ArrayList();
pushing = new ArrayList();
pulling= new ArrayList();
int count = 0;
float vectorAwayX = 0;
float vectorAwayY = 0;
float vectorAwayZ = 0;
for( int i = 0; i < numPoints; i++)
{
if(i != id)
{
//vector from current point towards next point
float diffX = points[i].getX() - x;
float diffY = points[i].getY() - y;
float diffZ = points[i].getZ() - z;
//As we are only interested in points under 1.2 * maxLength, we can disgard
//all the points who are orthogonally outside this.
//This saves calculating the distance between every point. By disgarding points early.
if(diffX < maxLength * 1.2 && diffX > maxLength * -1.2)
{
if(diffY < maxLength * 1.2 && diffY > maxLength * -1.2)
{
if(diffZ < maxLength * 1.2 && diffZ > maxLength * -1.2)
{
float distance = sqrt(sq(diffX) + sq(diffY) + sq(diffZ));
if(distance < midPoint)
{
float factor = (distance - midPoint) / midPoint;
vectorAwayX += factor * diffX;
vectorAwayY += factor * diffY;
vectorAwayZ += factor * diffZ;
count++;
}
if(distance < minLength)
{
pushing.add(i);
}
else if(distance < maxLength)
{
connected.add(i);
}
else if(distance < maxLength * 1.2)
{
pulling.add(i);
}
}
}
}
}
}
//if the vector was changed.
if(count > 0) {
//average vector.
vectorAwayX /= count;
vectorAwayY /= count;
vectorAwayZ /= count;
//take a fraction of the vector.
vectorAwayX *= speed;
vectorAwayY *= speed;
vectorAwayZ *= speed;
//move point along this vector.
updatedX = x + vectorAwayX;
updatedY = y + vectorAwayY;
updatedZ = z + vectorAwayZ;
}
}
//############################## GETS #####################
public float getX() {return x;}
public float getY() {return y;}
public float getZ() {return z;}
//returns number of nodes that are within the allowed distance from this node.
public int getNumberOfNeighbours()
{
return connected.size();
}
//-----------------------------------------
// Returns point as CSV line.
// Note that z and y are swapped to match Rhino space.
//-----------------------------------------
public String getCSV()
{
return x + "," + -z + "," + -y;
}
//############################## DRAWING #####################
//-----------------------------------------
// Writes the point into the passed output stream in CSV format.
//-----------------------------------------
public void printPointCSV(PrintWriter output)
{
output.println(getCSV());
}
//-----------------------------------------
// Writes the lines into the passed output stream in CSV format.
//-----------------------------------------
public void printLinesCSV(PrintWriter output)
{
for(int i = 0; i < connected.size(); i ++)
{
int n = (Integer) connected.get(i);
//prevents printing the line twice
if(n > id)
{
output.println(getCSV());
output.println(points[n].getCSV());
}
}
}
//-----------------------------------------
// Draws the point as a sphere.
// If the text needs to be drawn, draws the ID centered above the point.
// Then draws all the lines showing relationships between surrounding points.
//-----------------------------------------
public void draw()
{
//draws the point
pushMatrix();
fill(100, 100, 100, 255);
translate(x, y, z);
sphere(pointDrawSize);
popMatrix();
if(drawText)
{
float tw = textWidth(str(id));
text(id, x-tw/2, y-10, z);
}
//draw the lines from point
stroke(255,130,130);
drawLines(pushing);
stroke(80);
drawLines(connected);
stroke(210);
drawLines(pulling);
noStroke();
}
//-----------------------------------------
// Takes an array list of point ids.
// Draws a line from this point, to each of the points in the array list.
// Only draws to lines of a higher ID so that lines are not drawn twice
// EG. P1 draws to P2, but P2 does not draw to P1
//-----------------------------------------
private void drawLines(ArrayList pointToConnectTo)
{
for(int i = 0; i < pointToConnectTo.size(); i ++)
{
int n = (Integer) pointToConnectTo.get(i);
//prevents drawing the line twice
if(n > id)
{
beginShape(LINES);
vertex(x, y, z);
vertex(points[n].getX(), points[n].getY(),points[n].getZ());
endShape();
}
}
}
}
//##################################################
// Daniel Davis - nzrchitecture.com
//
// Program to generate members of a specified length on an eliptical surface.
//
// Uses a swarming type algorithm to ensure all the points are the
// specified distance away from eachother (minLength & maxLength).
//
// Overall flow:
// Generates an array of points.
// Every draw function:
// Each point is moved away from its nearest neighbours.
// All the points are then projected back onto the eliptical surface.
// Object is then rendered.
//
// In the render:
// White members are close to the maximum range.
// Gray members are in the ideal range.
// Red members are below the ideal range and are pushing the point.
//
// Keys:
// r - randomly relocates a point that is not connected
// t - draws the point number over each point.
// 1 - Increase ellipse on x-axis.
// 2 - Decrease ellipse on x-axis.
// 3 - Increase ellipse on y-axis.
// 4 - Decrease ellipse on y-axis.
// 5 - Increase ellipse on z-axis.
// 6 - Decrease ellipse on z-axis.
// i - saves the frame as a tif image.
// s - saves three files:
// - points.csv - the location of all the points.
// - lines.csv - the location of the start and end of the lines.
// - setup.csv - the size of the ellipse.
//##################################################
//############################## GLOBAL #####################
Point[] points;
Ellipse globalEllipse;
PFont myFont;
//############################## SETTINGS #####################
int numPoints = 205; //number of points on surface
float ellipseScale = 1000; //scale of sphere in cm
float ellipseX = 1.3; //scale of spehre on x axis - normalised.
float ellipseY = 0.6; //scale of sphere on y axis (height) - normalised
float ellipseZ = 0.75; //scale of sphere on z axis - normalised.
float speed = 0.8; //how fast the points move
float minLength = 160; //the min length of a member
float maxLength = 175; //the max length of a member
float midPoint = (minLength + maxLength) / 2; //the middle of the min and max length.
float pointDrawSize = 3; //how large a point is when it is drawn.
boolean drawText = true; //whether the points IDs should be written above them.
boolean printPointsEveryFrame = true; //whether the points should be written to file every frame.
//-----------------------------------------
// Creates the scene. Sets the global variables.
//-----------------------------------------
void setup() {
size(1000, 1000, P3D);
noStroke();
myFont = createFont("FFScala", 16);
textFont(myFont);
globalEllipse = new Ellipse(0,0,0, ellipseScale, ellipseX, ellipseY, ellipseZ);
points = new Point[numPoints];
for(int i =0; i < numPoints; i++)
{
reSetPoint(i);
}
saveSetup();
}
//-----------------------------------------
// Reconstructs the specified point by telling it to generate randomly.
//-----------------------------------------
void reSetPoint(int pointId)
{
points[pointId] = new Point(pointId);
}
//-----------------------------------------
// Called every frame. Calls the basic drawing setup.
// Tells every point to update its location.
// Then sets the location of every point to its updated postion
// - We do this so points are not updating their location with the new location of points.
// Then draws all the points.
// Then checks for user interface commands.
//-----------------------------------------
void draw() {
setupDrawing();
for(int i =0; i<points.length; i++)
{
points[i].updatePosition();
}
for(int i =0; i<points.length; i++)
{
points[i].setPosition();
}
for(int i =0; i<points.length; i++)
{
points[i].draw();
}
if(printPointsEveryFrame)
{
//savePoints();
saveLines();
}
if(keyPressed) {
checkKeys();
}
}
//-----------------------------------------
// If R is pressed, it randomly moves a point not connected.
// If T is pressed, displays the node id above each point.
// If S is pressed, saves the point location to a text file.
//-----------------------------------------
void checkKeys()
{
if (key == 'r' || key == 'R')
{
for(int i =0; i<points.length; i++)
{
if(points[i].getNumberOfNeighbours() == 0)
{
println("Removed " + i);
reSetPoint(i);
break;
}
}
}
else if(key == 't' || key == 'T')
{
if(drawText) drawText = false;
else drawText = true;
}
//saves the point location to a text file.
else if(key == 's' || key == 'S')
{
saveSetup();
savePoints();
saveLines();
}
//resize ellipse and save new setup.
else if(key == '1')
{
globalEllipse.addX(0.025);
if(printPointsEveryFrame){saveSetup();}
}
else if(key == '2')
{
globalEllipse.addX(-0.025);
if(printPointsEveryFrame){saveSetup();}
}
else if(key == '3')
{
globalEllipse.addY(0.025);
if(printPointsEveryFrame){saveSetup();}
}
else if(key == '4')
{
globalEllipse.addY(-0.025);
if(printPointsEveryFrame){saveSetup();}
}
else if(key == '5')
{
globalEllipse.addZ(0.025);
if(printPointsEveryFrame){saveSetup();}
}
else if(key == '6')
{
globalEllipse.addZ(-0.025);
if(printPointsEveryFrame){saveSetup();}
}
else if(key == 'i')
{
saveFrame("frame-####.tif");
}
}
//-----------------------------------------
// Sets up basic part of drawing like lights and camera.
// Based on the Processing example 3D/Camera/Perspective
// Also draws the ground plane.
//-----------------------------------------
void setupDrawing()
{
lights();
background(204);
float cameraY = height/2.0;
float fov = 0.9;//mouseX/float(width) * PI/2;
float cameraZ = cameraY / tan(fov / 2.0);
float aspect = float(width)/float(height);
if (mousePressed) {
aspect = aspect / 2.0;
}
perspective(fov, aspect, cameraZ/10.0, cameraZ*10.0);
translate(width/2, height/2, -2000);
rotateX(-PI/6);
rotateY(PI + mouseX/float(width) * 2 * PI);
//Ground plane
beginShape();
fill(255, 255, 255, 255);
float squareSize = ellipseScale + 400;
vertex(-squareSize, 0, squareSize);
vertex(-squareSize, 0, -squareSize);
vertex(squareSize, 0 ,-squareSize);
vertex(squareSize, 0, squareSize);
endShape(CLOSE);
}
//-----------------------------------------
// Saves all the points as a CSV file called points.csv
// Use try and catch because sometimes the file might be read as we try to write it.
// Format:
// x, y, z
//-----------------------------------------
void savePoints()
{
PrintWriter output= null;
try {
output = createWriter("points.csv");
} catch(Throwable t) {
//could catch more gracefully.
}
if(output != null )
{
for (int i = 0; i < points.length; i++) {
points[i].printPointCSV(output);
}
output.flush();
output.close();
}
}
//-----------------------------------------
// Saves all the lines as a CSV file called lines.csv
// Use try and catch because sometimes the file might be read as we try to write it.
// Format (over two lines):
// xStart, yStart, zStart,
// xEnd, yEnd, ZEnd
//-----------------------------------------
void saveLines()
{
PrintWriter output= null;
try {
output = createWriter("lines.csv");
} catch(Throwable t) {
//could catch more gracefully.
}
if(output != null )
{
for (int i = 0; i < points.length; i++) {
points[i].printLinesCSV(output);
}
output.flush();
output.close();
}
}
//-----------------------------------------
// Saves the setup of the ellipse in setup.csv.
// Format:
// radius
// scaleX
// scaleY
// scaleZ
//-----------------------------------------
void saveSetup()
{
PrintWriter output= null;
try {
output = createWriter("setup.csv");
} catch(Throwable t) {
//could catch more gracefully.
}
if(output != null )
{
output.println(globalEllipse.getRadius());
output.println(globalEllipse.getX());
output.println(globalEllipse.getY());
output.println(globalEllipse.getZ());
output.flush();
output.close();
}
}