Related
I have a mildly interesting problem which I can't quite figure out (although in fairness, I am pretty drunk)
The "Seed of Life" is a pattern created from drawing circles of equal radius, centred on the intersection of the previous circle.
Language doesn't really matter, the theory is more important here. Anything which can draw a circle will do it. For example, HTML5 + JS canvas can do it. It's a lovely example of how recursion can help solve problems.
The problem is that a naive approach will end up redrawing many, many circles. With 7 layers, you'll end up with over 300,000 circle draws.
A simple approach is to maintain a list of previous circle centre points, and only draw circles which are not in that list.
My question is whether there's a "better" way to approach this? Something which doesn't require checking that list.
A fun problem to ponder.
I think I have this solved thanks to a friend. I'll post here what I'm doing now in case someone ever is curious.
In short, starting from the center and working out, calculate the vertices of a hexagon, and subdivide each edge of the hexagon into i number of places, where i is the layer number.
I drew it in C# using SkiaSharp, but the code is nothing special to the language, there's no reason this couldn't be written in any language. Here's the significant bits:
const float seedAngle = (float)(Math.PI / 3.0);
static void SeedOfLifeDemo(int x, int y) {
//setting up Skia stuff, this will be different depending what language you're using.
var info = new SKImageInfo(x, y);
using var bitmap = FlatImage(info, SKColors.White);
SKCanvas canvas = new SKCanvas(bitmap);
float radius = Math.Min(x, y) / 15;
SKPoint center = new SKPoint(x / 2f, y / 2f);
SKPaint strokePaint = new SKPaint {
Color = SKColors.Black,
Style = SKPaintStyle.Stroke,
StrokeWidth = 1,
IsAntialias = true,
};
int layers = 4;
//Draw the very central circle. This is just a little easier than adding that edge case to SubdividedHexagonAboutPoint
canvas.DrawCircle(center, radius, strokePaint);
for (int i = 1; i <= layers; i++) {
foreach (SKPoint p in SubdividedHexagonAboutPoint(center, radius * i, i)) {
canvas.DrawCircle(p, radius, strokePaint);
}
}
SaveImage(bitmap, "SeedOfLifeFastDemo.Jpg");//More Skia specific stuff
}
//The magic!
static List<SKPoint> SubdividedHexagonAboutPoint(SKPoint centre, float radius, int subdivisions) {
List<SKPoint> points = new List<SKPoint>(6 * subdivisions);
SKPoint? prevPoint = null;
for (int i = 0; i < 7; i++) {//Step around the circle. The 7th step is to close the last edge
float x = (float)(Math.Sin(seedAngle * i) * radius + centre.X);
float y = (float)(Math.Cos(seedAngle * i) * radius + centre.Y);
SKPoint point = new SKPoint(x, y);
if (prevPoint != null) {
points.Add(point);//include the "primary" 6 points
if (subdivisions > 0) {
float xDist = (point.X - prevPoint.Value.X) / subdivisions;
float yDist = (point.Y - prevPoint.Value.Y) / subdivisions;
for (int sub = 1; sub < subdivisions; sub++) {
SKPoint subPoint = new SKPoint(point.X - xDist * sub, point.Y - yDist * sub);
points.Add(subPoint);//include the edge subdivisions
}
}
}
prevPoint = point;
}
return points;
}
This is quite an interesting exercise really, and another example of where recursion can really bite you when used badly.
I have a list of vector points, which define a path of straight line segments to be followed by an object. Currently, I do linear interpolation to animate motion along the path like this:
public class Demo
{
public float speed = 1;
private List<Vector3> points;
private float t; // [0..1]
private Vector3 Evaluate(float t)
{
// Find out in between which points we currently are
int lastPointIndex = GetLast(t);
int nextPointIndex = GetNext(t);
// Obviously, I need to somehow transform parameter t
// to adjust for the individual length of each segment.
float segmentLength = GetLength(lastPointIndex, nextPointIndex);
// But how would I do this?
return Vector3.Lerp(points[lastPointIndex], points[nextPointIndex], t);
}
public void Update()
{
// Curve parameter t moves between 0 and 1 at constant speed.
t = Mathf.PingPong(Time.time * speed, 1);
// Then just get the evaluated position for the curve time, but
// this gives variant speed if points are not evenly spaced.
Vector3 position = Evaluate(t);
SetObjectPosition(position);
}
}
I realize, that to achieve constant speed, I need to rescale the parameter t to account for the length of each segment, but I seem to be unable to find out exactly how.
I also know, that I could approximate the path by moving towards the next point at my desired speed and only change direction, once I'm in close proximity or keep track of t as well and change direction once it moves over the next segment, but this seems hacky, when I actually know the exact length of each segment and should be able to interpolate this exactly.
That's actually quiet easy. First, define a speed you want for your object. For example, 6 units per second. That means if a line segment has a length of 6 units then your object will take 1 second to go from its start to its end point. This also means that if you have a line segment that is half that length (i.e. 3 units) it will take the object 0.5 second to cross it. So, what you have to do is calculate the length of all your line segments and divide that by the speed you want to go (3/6 = 0.5 = scaleFactor). Then instead of interpolating between 0 and 1, interpolate between 0 and 1*scaleFactor. Your code then becomes:
public class Demo
{
public float speed = 1;
private List<Vector3> points;
private float t; // [0..1]
private Vector3 Evaluate(float t)
{
// Find out in between which points we currently are
int lastPointIndex = GetLast(t);
int nextPointIndex = GetNext(t);
float segmentLength = GetLength(lastPointIndex, nextPointIndex);
float scaleFactor = segmentLength/speed;
// note that I divided t by scaleFactor instead of multiplication.
// That's because Lerp always takes an interval of [0..1]. So, we
// adjust the curve parameter instead.
return Vector3.Lerp(points[lastPointIndex], points[nextPointIndex], t/scaleFactor);
}
public void Update()
{
// Curve parameter t moves between 0 and 1 at constant speed.
t = Mathf.PingPong(Time.time * speed, 1);
// Then just get the evaluated position for the curve time, but
// this gives variant speed if points are not evenly spaced.
Vector3 position = Evaluate(t);
SetObjectPosition(position);
}
}
I have a simple Processing Sketch, drawing a continuous line of ellipses with a 20px diameter. Is there a way to modify the sketch so that it draws vector shapes instead of pixels?
void setup() {
size(900, 900);
background(110, 255, 94);
}
void draw() {
ellipse(mouseX, mouseY, 20, 20);
fill(255);
}
Thanks to everyone who can provide some helpful advice.
Expanding my comment above, there a couple of things to tackle:
drawing a continuous line of ellipses with a 20px diameter
draws vector shapes
Currently you're drawing ellipses based on mouse movement.
A side effect is that if you move the mouse fast enough you will have gaps in between ellipses.
To fill the gaps you can work out the distance between every two ellipses.
If the distance is greater than the sizes of these two ellipses you can draw some in between.
The PVector class provides a lerp() function that allows you easily interpolate between two points.
You can read more on this and run some examples here
Using the ratio between these distance of two points and the ellipse size the number of points needed in between.
Here is an example that stores mouse locations to a list of PVectors as you drag the mouse:
//create an array list to store points to draw
ArrayList<PVector> path = new ArrayList<PVector>();
//size of each ellipse
float size = 20;
//how tight will the extra ellipses be drawn together
float tightness = 1.25;
void setup() {
size(900, 900);
}
void draw() {
background(110, 255, 94);
fill(255);
//for each point in the path, starting at 1 (not 0)
for(int i = 1; i < path.size(); i++){
//get a reference to the current and previous point
PVector current = path.get(i);
PVector previous = path.get(i-1);
//calculate the distance between them
float distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
int extraPoints = (int)(round(distance/size * tightness));
//draw the previous point
ellipse(previous.x,previous.y,size,size);
//if there are any exta points to be added, compute and draw them:
for(int j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
float interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
PVector inbetween = PVector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,size,size);
}
}
//draw instructions
fill(0);
text("SPACE = clear\nLEFT = decrease tightness\nRIGHT = increase tightness\ntightness:"+tightness,10,15);
}
void mouseDragged(){
path.add(new PVector(mouseX,mouseY));
}
void keyPressed(){
if(keyCode == LEFT) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == RIGHT) tightness = constrain(tightness+0.1,0.0,3.0);
if(key == ' ') path.clear();
}
Note that the interpolation between points is linear.
It's the simplest, but as the name implies, it's all about lines:
it always connects two points in a straight line, not curves.
I've added the option to control how tight interpolated ellipses will be packed together. Here are a couple of screenshots with different tightness levels. You'll notice as tightness increases, the lines will become more evident:
You run the code bellow:
//create an array list to store points to draw
var path = [];
//size of each ellipse
var ellipseSize = 20;
//how tight will the extra ellipses be drawn together
var tightness = 1.25;
function setup() {
createCanvas(900, 900);
}
function draw() {
background(110, 255, 94);
fill(255);
//for each point in the path, starting at 1 (not 0)
for(var i = 1; i < path.length; i++){
//get a reference to the current and previous point
var current = path[i];
var previous = path[i-1];
//calculate the distance between them
var distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
var extraPoints = round(distance/ellipseSize * tightness);
//draw the previous point
ellipse(previous.x,previous.y,ellipseSize,ellipseSize);
//if there are any exta points to be added, compute and draw them:
for(var j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
var interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
var inbetween = p5.Vector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,ellipseSize,ellipseSize);
}
}
//draw instructions
fill(0);
text("BACKSPACE = clear\n- = decrease tightness\n+ = increase tightness\ntightness:"+tightness,10,15);
}
function mouseDragged(){
path.push(createVector(mouseX,mouseY));
}
function keyPressed(){
if(keyCode == 189) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == 187) tightness = constrain(tightness+0.1,0.0,3.0);
if(keyCode == BACKSPACE) path = [];
}
//https://stackoverflow.com/questions/40673192/processing-draw-vector-instead-of-pixels
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js"></script>
If you want smoother lines you will need to use a different interpolation such as quadratic or cubic interpolation. You can start with existing Processing functions for drawing curves such as curve() or bezier(),and you'll find some helpful resources unrelated to Processing here,here and here.
On vector shapes
You're not directly working with pixels[], you're drawing shapes.
These shapes can easily be saved to PDF using Processing's PDF library
Check out the Single Frame from an Animation (With Screen Display) example.
Here is a version that saves to PDF when pressing the 's' key:
import processing.pdf.*;
//create an array list to store points to draw
ArrayList<PVector> path = new ArrayList<PVector>();
//size of each ellipse
float size = 20;
//how tight will the extra ellipses be drawn together
float tightness = 1.25;
//PDF saving
boolean record;
void setup() {
size(900, 900);
}
void draw() {
background(110, 255, 94);
fill(255);
//if we need to save the current frame to pdf, begin recording drawing instructions
if (record) {
// Note that #### will be replaced with the frame number. Fancy!
beginRecord(PDF, "frame-####.pdf");
}
//for each point in the path, starting at 1 (not 0)
for(int i = 1; i < path.size(); i++){
//get a reference to the current and previous point
PVector current = path.get(i);
PVector previous = path.get(i-1);
//calculate the distance between them
float distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
int extraPoints = (int)(round(distance/size * tightness));
//draw the previous point
ellipse(previous.x,previous.y,size,size);
//if there are any exta points to be added, compute and draw them:
for(int j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
float interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
PVector inbetween = PVector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,size,size);
}
}
//once what we want to save has been recorded to PDF, stop recording (this will skip saving the instructions text);
if (record) {
endRecord();
record = false;
println("pdf saved");
}
//draw instructions
fill(0);
text("SPACE = clear\nLEFT = decrease tightness\nRIGHT = increase tightness\ntightness:"+tightness+"\n's' = save PDF",10,15);
}
void mouseDragged(){
path.add(new PVector(mouseX,mouseY));
}
void keyPressed(){
if(keyCode == LEFT) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == RIGHT) tightness = constrain(tightness+0.1,0.0,3.0);
if(key == ' ') path.clear();
if(key == 's') record = true;
}
In addition to George's great answer (which I've +1'd), I wanted to offer a more basic option:
The problem, like George said, is that when you move the mouse, you actually skip over a bunch of pixels. So if you only draw ellipses or points at mouseX, mouseY then you'll end up with gaps.
The dumb fix: the pmouseX and pmouseY variables hold the previous position of the cursor.
That might not sound very useful, but they allow you to solve exactly your problem. Instead of drawing ellipses or points at the current mouse position, draw a line from the previous position to the current position. This will eliminate any gaps in your lines.
void draw(){
line(pmouseX, pmouseY, mouseX, mouseY);
}
Shameless self-promotion: I've written a tutorial on getting user input in Processing available here.
Note: This dumb solution will only work if you aren't redrawing the background every frame. If you need to redraw everything every frame, then George's answer is the way to go.
So, like the title said, I'm making a 2D platform game with multi direction physics, but my lack of trigonometry skills are killing me.
When my character hits something, it returns a Vector2 hitPoint.
If this was a one-way gravity platform game, I would do the following:
// considering the player's origin point in the middle
public void HitGround(Vector2 hitPoint)
{
this.position.y = hitPoint.y + height /2;
}
But since this have multi gravity physics, this simple method can't be done. So I've worked with relative positions, suppose the gravity is (-3, -3), the player would fall southwest.
// considering the player's origin point in the middle
public void HitGround(Vector2 hitPoint)
{
this.position = hitPoint + GetRelativeYPos(height / 2);
}
// returns the relative Y position based on its rotation
public Vector2 GetRelativeYPos(float offset)
{
// rotation is a Quaternion
return this.rotation * new Vector2(0, 1) * offset;
}
So far, so good. It works perfectly!!! But wait... this is only working when the CENTER of the player hits the ground. In fact, the player has 3 raycasts when he is falling. If the center raycast hits the ground, the code WORKS in EVERY direction he falls.
But when the EDGE RAYCASTS hits the ground. The Player tilts to the center, because the code do not allow him to move right. Now, my last code idea was born:
public void HitGround(Vector2 hitPoint, Vector2 raycastOrigin)
{
// Considering that the raycastOrigin is in the same "relative X" as the player's origin
float signedDistance = SignedDistance(hitPoint, raycastOrigin);
this.position = hitPoint + GetRelativeYPos(height / 2) + GetRelativeXPos(signedDistance);
}
// returns the relative Y position based on its rotation
public Vector2 GetRelativeYPos(float offset)
{
return this.rotation * new Vector2(0, 1) * offset;
}
// returns the relative X position based on its rotation
public Vector2 GetRelativeXPos(float offset)
{
return this.rotation * new Vector2(1, 0) * offset;
}
public float SignedDistance(Vector2 p1, Vector2 p2)
{
float distance = Distance(p1, p2);
float angle = Atan2(p2.y, p2.x) + Atan2(p1.y, p1.x);
// Returns the magnitude AND direction of the vector
return distance * Sign(angle);
}
And there you go!
When the code doesn't work, its just because of the sign of the angle, that, for some reason, it's being calculated wrong, When the angle has to be positive, it returns negative, and vice versa, but it only returns the wrong value when the "player's relative north position" is below 0.
For example:
if the player is falling down, when the Y position is higher than 0, it works, when it is lower, it doesn't.
If the player is falling up, when the Y position is lower than 0, it works, when it is higher, it doesn't.
If the player is falling right, when the X position is lower than 0, it works, when it is
higher, it doesn't.
If the player is falling left, when the X position is greater than 0, it works, when it is lower, it doesn't.
Just one more:
If the player is falling southeast, when the Y position is higher than 0 AND the X position is lower than 0, it works, otherwise, it doesn't
The following method is the responsible for calculating the "signed distance" of two points. I mean, the magnitude of the resulting vector and its direction
public float SignedDistance(Vector2 p1, Vector2 p2)
{
float distance = Distance(p1, p2);
// THIS LINE HERE!!!
float angle = Atan2(p2.y, p2.x) + Atan2(p1.y, p1.x);
return distance * Sign(angle);
}
There is something wrong with that line that I CAN'T FIGURE IT OUT. Is there someone kind enough to answer this question?
I hope I was clear. Bye
I render isosurfaces with marching cubes, (or perhaps marching squares as this is 2D) and I want to do set operations like set difference, intersection and union. I thought this was easy to implement, by simply choosing between two vertex scalars from two different implicit surfaces, but it is not.
For my initial testing, I tried with two spheres circles, and the set operation difference. i.e A - B. One circle is moving and the other one is stationary. Here's the approach I tried when picking vertex scalars and when classifying corner vertices as inside or outside. The code is written in C++. OpenGL is used for rendering, but that's not important. Normal rendering without any CSG operations does give the expected result.
void march(const vec2& cmin, //min x and y for the grid cell
const vec2& cmax, //max x and y for the grid cell
std::vector<vec2>& tri,
float iso,
float (*cmp1)(const vec2&), //distance from stationary circle
float (*cmp2)(const vec2&) //distance from moving circle
)
{
unsigned int squareindex = 0;
float scalar[4];
vec2 verts[8];
/* initial setup of the grid cell */
verts[0] = vec2(cmax.x, cmax.y);
verts[2] = vec2(cmin.x, cmax.y);
verts[4] = vec2(cmin.x, cmin.y);
verts[6] = vec2(cmax.x, cmin.y);
float s1,s2;
/**********************************
********For-loop of interest******
*******Set difference between ****
*******two implicit surfaces******
**********************************/
for(int i=0,j=0; i<4; ++i, j+=2){
s1 = cmp1(verts[j]);
s2 = cmp2(verts[j]);
if((s1 < iso)){ //if inside circle1
if((s2 < iso)){ //if inside circle2
scalar[i] = s2; //then set the scalar to the moving circle
} else {
scalar[i] = s1; //only inside circle1
squareindex |= (1<<i); //mark as inside
}
}
else {
scalar[i] = s1; //inside neither circle
}
}
if(squareindex == 0)
return;
/* Usual interpolation between edge points to compute
the new intersection points */
verts[1] = mix(iso, verts[0], verts[2], scalar[0], scalar[1]);
verts[3] = mix(iso, verts[2], verts[4], scalar[1], scalar[2]);
verts[5] = mix(iso, verts[4], verts[6], scalar[2], scalar[3]);
verts[7] = mix(iso, verts[6], verts[0], scalar[3], scalar[0]);
for(int i=0; i<10; ++i){ //10 = maxmimum 3 triangles, + one end token
int index = triTable[squareindex][i]; //look up our indices for triangulation
if(index == -1)
break;
tri.push_back(verts[index]);
}
}
This gives me weird jaggies:
(source: mechcore.net)
It looks like the CSG operation is done without interpolation. It just "discards" the whole triangle. Do I need to interpolate in some other way, or combine the vertex scalar values? I'd love some help with this.
A full testcase can be downloaded HERE
EDIT: Basically, my implementation of marching squares works fine. It is my scalar field which is broken, and I wonder what the correct way would look like. Preferably I'm looking for a general approach to implement the three set operations I discussed above, for the usual primitives (circle, rectangle/square, plane)
EDIT 2: Here are some new images after implementing the answerer's whitepaper:
1.Difference
2.Intersection
3.Union
EDIT 3: I implemented this in 3D too, with proper shading/lighting:
1.Difference between a greater sphere and a smaller sphere
2.Difference between a greater sphere and a smaller sphere in the center, clipped by two planes on both sides, and then union with a sphere in the center.
3.Union between two cylinders.
This is not how you mix the scalar fields. Your scalars say one thing, but your flags whether you are inside or not say another. First merge the fields, then render as if you were doing a single compound object:
for(int i=0,j=0; i<4; ++i, j+=2){
s1 = cmp1(verts[j]);
s2 = cmp2(verts[j]);
s = max(s1, iso-s2); // This is the secret sauce
if(s < iso) { // inside circle1, but not inside circle2
squareindex |= (1<<i);
}
scalar[i] = s;
}
This article might be helpful: Combining CSG modeling with soft blending using
Lipschitz-based implicit surfaces.