Alright I think I've mostly figured out how the MagicTile works, the source code at least (not really the Math as much yet). It all begins with the build and render calls in the MainForm.cs. It generates a tessellation like this:
First, it "generates" the tessellation. Since MagicTile is a Rubic's cube-like game, I guess it just statically computes all of the tiles up front. It does this by starting with a central tile, and reflecting its polygon (and the polygon's segments and points) using some sort of math which I've read about several times but I couldn't explain. Then it appears they allow rotations of the tessellation, where they call code like this in the "renderer":
Polygon p = sticker.Poly.Clone();
p.Transform( m_mouseMotion.Isometry );
Color color = GetStickerColor( sticker );
GLUtils.DrawConcavePolygon( p, color, GrabModelTransform() );
They track the mouse position, like if you are dragging, and somehow that is used to create an "isometry" to augment / transform the overall tessellation. So then we transform the polygon using that isometry. _It appears they only do the central tile and 1 or 2 levels after that, but I can't quite tell, I haven't gotten the app to run and debug yet (it's also in C# and that's a new language for me, coming from TypeScript). The Transform function digs down like this (here it is in TypeScript, as I've been converting it):
TransformIsometry(isometry: Isometry) {
for (let s of this.Segments) {
s.TransformIsometry(isometry)
}
this.Center = isometry.Apply(this.Center)
}
That goes into the transform for the segments here:
/// <summary>
/// Apply a transform to us.
/// </summary>
TransformInternal<T extends ITransform>(transform: T) {
// NOTES:
// Arcs can go to lines, and lines to arcs.
// Rotations may reverse arc directions as well.
// Arc centers can't be transformed directly.
// NOTE: We must calc this before altering the endpoints.
let mid: Vector3D = this.Midpoint
if (UtilsInfinity.IsInfiniteVector3D(mid)) {
mid = this.P2.MultiplyWithNumber(UtilsInfinity.FiniteScale)
}
mid = UtilsInfinity.IsInfiniteVector3D(this.P1)
? this.P2.MultiplyWithNumber(UtilsInfinity.FiniteScale)
: this.P1.MultiplyWithNumber(UtilsInfinity.FiniteScale)
this.P1 = transform.ApplyVector3D(this.P1)
this.P2 = transform.ApplyVector3D(this.P2)
mid = transform.ApplyVector3D(mid)
// Can we make a circle out of the transformed points?
let temp: Circle = new Circle()
if (
!UtilsInfinity.IsInfiniteVector3D(this.P1) &&
!UtilsInfinity.IsInfiniteVector3D(this.P2) &&
!UtilsInfinity.IsInfiniteVector3D(mid) &&
temp.From3Points(this.P1, mid, this.P2)
) {
this.Type = SegmentType.Arc
this.Center = temp.Center
// Work out the orientation of the arc.
let t1: Vector3D = this.P1.Subtract(this.Center)
let t2: Vector3D = mid.Subtract(this.Center)
let t3: Vector3D = this.P2.Subtract(this.Center)
let a1: number = Euclidean2D.AngleToCounterClock(t2, t1)
let a2: number = Euclidean2D.AngleToCounterClock(t3, t1)
this.Clockwise = a2 > a1
} else {
// The circle construction fails if the points
// are colinear (if the arc has been transformed into a line).
this.Type = SegmentType.Line
// XXX - need to do something about this.
// Turn into 2 segments?
// if( UtilsInfinity.IsInfiniteVector3D( mid ) )
// Actually the check should just be whether mid is between p1 and p2.
}
}
So as far as I can tell, this will adjust the segments based on the mouse position, somehow. Mouse position isometry updating code is here.
So it appears they don't have the functionality to "move" the tiling, like if you were walking on it, like in HyperRogue.
So after having studied this code for a few days, I am not sure how to move or walk along the tiles, moving the outer tiles toward the center, like you're a giant walking on Earth.
First small question, can you do this with MagicTile? Can you somehow update the tessellation to move a different tile to the center? (And have a function which I could plug a tween/animation into so it animates there). Or do I need to write some custom new code? If so, what do I need to do roughly speaking, maybe some pseudocode?
What I imagine is, user clicks on the outer part of the tessellation. We convert that click data to the tile index in the tessellation, then basically want to do tiling.moveToCenter(tile), but frame-by-frame-animation, so not quite sure how that would work. But that moveToCenter, what would that do in terms of the MagicTile rendering/tile-generating code?
As I described in the beginning, it first generates the full tessellation, then only updates 1-3 layers of the tiles for their puzzles. So it's like I need to first shift the frame of reference, and recompute all the potential visible tiles, somehow not recreating the ones that were already created. I don't quite see how that would work, do you? Once the tiles are recomputed, then I just re-render and it should show the updated center.
Is it a simple matter of calling some code like this again, for each tile, where the isometry is somehow updated with a border-ish position on the tessellation?
Polygon p = sticker.Poly.Clone();
p.Transform( m_mouseMotion.Isometry );
Or must I do something else? I can't quite see the full picture yet.
Or is that what these 3 functions are doing! TypeScript port of the C# MagicTile:
// Move from a point p1 -> p2 along a geodesic.
// Also somewhat from Don.
Geodesic(g: Geometry, p1: Complex, p2: Complex) {
let t: Mobius = Mobius.construct()
t.Isometry(g, 0, p1.Negate())
let p2t: Complex = t.ApplyComplex(p2)
let m2: Mobius = Mobius.construct()
let m1: Mobius = Mobius.construct()
m1.Isometry(g, 0, p1.Negate())
m2.Isometry(g, 0, p2t)
let m3: Mobius = m1.Inverse()
this.Merge(m3.Multiply(m2.Multiply(m1)))
}
Hyperbolic(g: Geometry, fixedPlus: Complex, scale: number) {
// To the origin.
let m1: Mobius = Mobius.construct()
m1.Isometry(g, 0, fixedPlus.Negate())
// Scale.
let m2: Mobius = Mobius.construct()
m2.A = new Complex(scale, 0)
m2.C = new Complex(0, 0)
m2.B = new Complex(0, 0)
m2.D = new Complex(1, 0)
// Back.
// Mobius m3 = m1.Inverse(); // Doesn't work well if fixedPlus is on disk boundary.
let m3: Mobius = Mobius.construct()
m3.Isometry(g, 0, fixedPlus)
// Compose them (multiply in reverse order).
this.Merge(m3.Multiply(m2.Multiply(m1)))
}
// Allow a hyperbolic transformation using an absolute offset.
// offset is specified in the respective geometry.
Hyperbolic2(
g: Geometry,
fixedPlus: Complex,
point: Complex,
offset: number,
) {
// To the origin.
let m: Mobius = Mobius.construct()
m.Isometry(g, 0, fixedPlus.Negate())
let eRadius: number = m.ApplyComplex(point).Magnitude
let scale: number = 1
switch (g) {
case Geometry.Spherical:
let sRadius: number = Spherical2D.e2sNorm(eRadius)
sRadius = sRadius + offset
scale = Spherical2D.s2eNorm(sRadius) / eRadius
break
case Geometry.Euclidean:
scale = (eRadius + offset) / eRadius
break
case Geometry.Hyperbolic:
let hRadius: number = DonHatch.e2hNorm(eRadius)
hRadius = hRadius + offset
scale = DonHatch.h2eNorm(hRadius) / eRadius
break
default:
break
}
this.Hyperbolic(g, fixedPlus, scale)
}
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed last month.
Improve this question
Suppose I have a finite set of points distributed in a unit square. I can't access the point coordinates; instead, I can only specify a (point, radius) pair and see how many points fall inside that circle. I want to find a set of circles such that each point is in at least one circle, and no circle contains more than 1000 points. What's an efficient way to do this? E.g. a way that minimizes the expected number of (point, radius) searches?
I tried a recursive approach. E.g. f(point, radius) takes a circle and returns a set of smaller circles that cover it. Then recurse until each circle contains fewer than 1000 points. But there's not a straightforward (to me) way to choose the smaller circles in the recursive step.
Edit: Circles are allowed to overlap with each other / with the outside of the square.
Not having a strict partition ("strict" - where the circles in the solution may not overlap and points must appear in exactly 1 circle) simplifies the problem.
The straight-forward way to subdivide a circle under those circumstances is to form a set of child circles that circumscribe the four quadrants of the parent...
Here's a (cursorily tested) demo using that approach
class Circle {
constructor(x,y,radius) {
Object.assign(this, { x, y, radius })
this.rSquared = radius*radius
}
contains(point) {
let dx = point.x - this.x
let dy = point.y - this.y
return dx*dx + dy*dy < this.rSquared
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.stroke();
}
subdivide() {
const halfR = this.radius / 2.0
const smallR = this.radius * Math.SQRT2 / 2.0
return [
new Circle(this.x-halfR, this.y-halfR, smallR),
new Circle(this.x-halfR, this.y+halfR, smallR),
new Circle(this.x+halfR, this.y-halfR, smallR),
new Circle(this.x+halfR, this.y+halfR, smallR),
]
}
}
// this class keeps a set of random points and answers countInCircle()
// solutions may only call countInCircle()
class Puzzler {
constructor(count) {
this.points = []
for (let i=0; i<count; i++) {
let point = { x: Math.random()*width, y: Math.random()*height}
this.points.push(point)
}
}
// answer how many points fall inside circle
countInCircle(circle) {
return this.points.reduce((total, p) => total += circle.contains(p) ? 1 : 0, 0);
}
drawSolution(circles) {
// draw the random points
this.points.map(p => ctx.fillRect(p.x,p.y,2,2))
// draw the circles in the solution
ctx.strokeStyle = 'lightgray'
circles.forEach(circle => circle.draw())
// log some stats - commented a few of these out for snippet brevity
const counts = circles.map(circle => this.countInCircle(circle));
console.log('circles:', circles.length)
// console.log('counts:', counts.join(', '))
// console.log('counts above 100:', counts.filter(c => c > 100).length)
const averageCount = counts.reduce((a, b) => a + b) / counts.length
console.log('average count:', averageCount.toFixed(2))
const uncovered = this.points.reduce((total, point) => {
return total + (circles.some(circle => circle.contains(point)) ? 0 : 1)
}, 0)
console.log('uncovered points:', uncovered)
}
}
// setup canvas
const canvas = document.getElementById('canvas')
const { width, height } = canvas
const ctx = canvas.getContext('2d')
// setup puzzle
const count = 1000
const maxCountPer = 100
const puzzler = new Puzzler(count, maxCountPer)
// begin with an encompasing circle, subdivide and solve recursively
// until all subdivided circles meet the count criterion
let r = width*Math.SQRT2/2
let c = new Circle(width/2.0, width/2.0, r)
let solution = solve(c);
function solve(circle) {
let result = []
let count = puzzler.countInCircle(circle)
if (count > 0 && count <= maxCountPer) {
result.push(circle);
} else if (count > maxCountPer) {
circle.subdivide().forEach(c => {
result.push(...solve(c))
})
}
return result
}
requestAnimationFrame(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
puzzler.drawSolution(solution)
});
<h1>Circles Puzzle</h1>
<canvas style="border: 1px solid gray;" id="canvas" height="800" width="800"></canvas>
Assumption: When you pick a point and a radius, you get back a list of points that are in the containing circle. I.e., you know which points are covered by which circles.
If that's correct,then you can map out the approximate relative location of all points, after which answers to this similar question should carry you over the finish line.
To map out the relative location of all points:
Note that you can find the distance between any pair of points by centering your circle on one and using binary search on your radius to find the distance to the other within whatever precision you want to use.
Next choose three arbitrary points that aren't too close together. Pick an arbitrary point. Grow the radius, say to 1/4. Pick an arbitrary point close to that radius (by incrementing radius a bit to get another point, or using binary search on radius). Say the distance between these first two points is d. Pick a third point at distance >= d from the first two points but ideally close to d, again by incrementing the two radii or binary search on the same.
Now you have a roughly equilateral triangle. It isn't important that it's equilateral, but it is important that the points aren't very close, and aren't co-linear.
Next, give these three points coordinates. Say the first point is at (0,0), the second point is at (0, dist to first point). The third point will have two possible locations based on its distance from the first two. Choose the one in the first quadrant (arbitrarily).
All other points can now be positioned relative to this triangle by finding their distance two the points of the triangle.
For purposes of your problem, it doesn't matter that the cloud of points is rotated relative to the input, or that we don't know where the unit square is relative to the points. You have a cloud of points with (approximately) known coordinates, and can proceed accordingly.
I'm trying to create a smooth "wave" when the mouse moves over isometric logo shape.
I've created in in processing now I'm trying to recreate it in THREE.js
The shape acts strangely - the shape doesn't look as smooth when elevated compared to the processing sketch. If you look at the edges you can see segments that are not supposed to be there. I'm not sure what causes this.
Basically the shape is created through a loops that goes over 2 arrays:
for (var i = 0; i < xpos0.length; i++) {
shape.lineTo(xpos0[i], ypos0[i]);
}
Then it animates through another for loop that checks the distance between verteces[i].x and mouse location intersection with the ground
for (let p = 0; p < mesh.geometry.vertices.length; p=p+1) {
let delta = Math.abs(mesh.geometry.vertices[p].x - intersects[0].point.x);
mesh.geometry.vertices[p].z = bump2(-2, 2000, -1, 2, delta);
}
z value is calculated through this function:
function bump2(a,b,c,d,xval) {
xval = parseFloat(xval);
// console.log(typeof xval);
return Math.exp(a / (-xval * xval / b + c) + d) * -1;
}
https://codepen.io/NotYetDesignLab/pen/JjYaqRJ
How it looks on THREE.JS
notice how some segments appear "broken", like it's made of stiff parts instead of the many points that make up the segment in the array and give the illusion of "paper".
THIS IS HOW IT'S SUPPOSED TO LOOK: (Processing/java)
This has been done using Processing. Notice how the elevation of the edges is smooth and not broken.
I want to make the face of half a cylinder, using the boundary edges (two vertical lines and two 180° arcs). I know there are easier ways to do this, but my real problem is much more complex, and the edges I have are mostly splines. So, I tried to make a very simple example in hope someone might help me.
With my real input data, I do not know the order and orientation of the boundary edges. All I have is "cylindrical face" and "bucket of edges, that form a closed loop". So, if my orientation is bad, how can I automatically fix that?
Here's my example code:
// make an ARC from 'start' to 'end', counter-clockwise around 'center'.
TopoDS_Edge mkArc(gp_Pnt start, gp_Pnt center, gp_Pnt end, double normalZ) {
gp_Circ geometricCircle = GC_MakeCircle(center
, gp_Dir(0, 0, normalZ)
, center.Distance(start)
).Value()->Circ();
return BRepBuilderAPI_MakeEdge(geometricCircle, start, end);
}
// Make half-cylinder face by using boundary edges
TopoDS_Face MakeClinderFaceTest() {
// ^Z
// _
// ,´ `.
// a c b
// | _ |
// |,´ `.|
// A C B -->X
// top nodes
gp_Pnt a = gp_Pnt(-1, 0, 0);
gp_Pnt b = gp_Pnt( 1, 0, 0);
gp_Pnt c = gp_Pnt( 0, 0, 0);
// bottom nodes
gp_Pnt A = gp_Pnt(-1, 0, -1);
gp_Pnt B = gp_Pnt( 1, 0, -1);
gp_Pnt C = gp_Pnt( 0, 0, -1);
// boundary wire
std::list<TopoDS_Edge> edges;
if (0) { // 1/0 to reverse the order and direction of edges
edges.push_back(mkArc(a, c, b, -1)); // top arc
edges.push_back(BRepBuilderAPI_MakeEdge(b, B)); // right line
edges.push_back(mkArc(B, C, A, 1)); // bottom arc
edges.push_back(BRepBuilderAPI_MakeEdge(A, a)); // left line
} else {
edges.push_back(mkArc(b, c, a, 1));
edges.push_back(BRepBuilderAPI_MakeEdge(a, A));
edges.push_back(mkArc(A, C, B, -1));
edges.push_back(BRepBuilderAPI_MakeEdge(B, b));
}
BRepBuilderAPI_MakeWire wire;
for (auto& e : edges) {
wire.Add(e);
}
auto cylinder = gp_Cylinder( gp_Ax2( C, gp_Dir(0, 0, 1) ), C.Distance(A) /* =R */ );
#if 0
// surface geometry: infinite length cylinder
BRepBuilderAPI_MakeFace cylface(cylinder, wire);
#else
// cylindrical face with limits in V direction.
TopoDS_Face cylinder_face = BRepBuilderAPI_MakeFace(cylinder, 0, 2 * M_PI, 0, 1.0).Face();
// Limit cylinder by wired edges
BRepBuilderAPI_MakeFace cylface(cylinder_face, wire);
#endif
return cylface;
}
It is always better performing direct modeling - e.g. constructing vertices, edges, wires, faces in reliable way defining a valid topology.
But considering the question:
So, if my orientation is bad, how can I automatically fix that?
The shape healing services are provided by ShapeFix package in Open CASCADE Technology. It's main purpose is solving topology issues on shapes imported from external CAD systems (applying different criteria of validity or just writing an invalid shape into STEP / IGES file).
These tools might be also used for making a 'lazy' building algorithm or for handling user input. Beware, that tools provides a lot of 'fixes' - e.g. algorithms solving particular kind of issue. Enabling all of them is not a good idea as it will dramatically affect performance and result might be unexpected.
In your particular case, you are interested in fixing wire orientation. This can be achieved with help of ShapeFix_Face like this (if I understand your question correctly):
// cylindrical face with limits in V direction.
TopoDS_Face cylinder_face = BRepBuilderAPI_MakeFace(cylinder, 0, 2 * M_PI, 0, 1.0).Face();
Handle(Geom_Surface) aSurf = BRep_Tool::Surface (cylinder_face);
// Limit cylinder by wired edges
//BRepBuilderAPI_MakeFace cylface(cylinder_face, wire);
BRepBuilderAPI_MakeFace cylface(aSurf, wire);
ShapeFix_Face aFaceFixer;
aFaceFixer.FixWireMode() = 1;
aFaceFixer.FixOrientationMode() = 1;
//aFaceFixer.FixSplitFaceMode() = 1;
Handle(ShapeFix_Wire) aWireFixer = aFaceFixer.FixWireTool();
aWireFixer->FixConnectedMode() = 1;
aWireFixer->ClosedWireMode() = Standard_True;
Handle(ShapeBuild_ReShape) aContext = new ShapeBuild_ReShape();
aFaceFixer.SetContext (aContext);
aFaceFixer.Init (cylface);
aFaceFixer.Perform();
TopoDS_Shape aFixResult = aFaceFixer.Result();
if (!aFixResult.IsNull()) { return TopoDS::Face (aFixResult); }
Im working on a project where i want to generate a 3D mesh to represent a certain amount of data.
To create this mesh i want to use transformation Matrixes, so i created a class based upon the mathematical algorithms found on a couple of websites.
Everything seems to work, scale/translation but as soon as im rotating a mesh on its x-axis its starts to deform after 2 to 3 complete rotations. It feels like my scale values are increasing which transforms my mesh. I'm struggling with this problem for a couple of days but i can't figure out what's going wrong.
To make things more clear you can download my complete setup here.
I defined the coordinates of a box and put them through the transformation matrix before writing them to the screen
This is the formula for rotating my object
void appendRotation(float inXAngle, float inYAngle, float inZAngle, PVector inPivot ) {
boolean setPivot = false;
if (inPivot.x != 0 || inPivot.y != 0 || inPivot.z != 0) {
setPivot = true;
}
// If a setPivot = true, translate the position
if (setPivot) {
// Translations for the different axises need to be set different
if (inPivot.x != 0) { this.appendTranslation(inPivot.x,0,0); }
if (inPivot.y != 0) { this.appendTranslation(0,inPivot.y,0); }
if (inPivot.z != 0) { this.appendTranslation(0,0,inPivot.z); }
}
// Create a rotationmatrix
Matrix3D rotationMatrix = new Matrix3D();
// xsin en xcos
float xSinCal = sin(radians(inXAngle));
float xCosCal = cos(radians(inXAngle));
// ysin en ycos
float ySinCal = sin(radians(inYAngle));
float yCosCal = cos(radians(inYAngle));
// zsin en zcos
float zSinCal = sin(radians(inZAngle));
float zCosCal = cos(radians(inZAngle));
// Rotate around x
rotationMatrix.setIdentity();
// --
rotationMatrix.matrix[1][1] = xCosCal;
rotationMatrix.matrix[1][2] = xSinCal;
rotationMatrix.matrix[2][1] = -xSinCal;
rotationMatrix.matrix[2][2] = xCosCal;
// Add rotation to the basis matrix
this.multiplyWith(rotationMatrix);
// Rotate around y
rotationMatrix.setIdentity();
// --
rotationMatrix.matrix[0][0] = yCosCal;
rotationMatrix.matrix[0][2] = -ySinCal;
rotationMatrix.matrix[2][0] = ySinCal;
rotationMatrix.matrix[2][2] = yCosCal;
// Add rotation to the basis matrix
this.multiplyWith(rotationMatrix);
// Rotate around z
rotationMatrix.setIdentity();
// --
rotationMatrix.matrix[0][0] = zCosCal;
rotationMatrix.matrix[0][1] = zSinCal;
rotationMatrix.matrix[1][0] = -zSinCal;
rotationMatrix.matrix[1][1] = zCosCal;
// Add rotation to the basis matrix
this.multiplyWith(rotationMatrix);
// Untranslate the position
if (setPivot) {
// Translations for the different axises need to be set different
if (inPivot.x != 0) { this.appendTranslation(-inPivot.x,0,0); }
if (inPivot.y != 0) { this.appendTranslation(0,-inPivot.y,0); }
if (inPivot.z != 0) { this.appendTranslation(0,0,-inPivot.z); }
}
}
Does anyone have a clue?
You never want to cumulatively transform matrices. This will introduce error into your matrices and cause problems such as scaling or skewing the orthographic components.
The correct method would be to keep track of the cumulative pitch, yaw, roll angles. Then reconstruct the transformation matrix from those angles every update.
If there is any chance: avoid multiplying rotation matrices. Keep track of the cumulative rotation and compute a new rotation matrix at each step.
If it is impossible to avoid multiplying the rotation matrices then renormalize them (starts on page 16). It works for me just fine for more than 10 thousand multiplications.
However, I suspect that it will not help you, numerical errors usually requires more than 2 steps to manifest themselves. It seems to me the reason for your problem is somewhere else.
Yaw, pitch and roll are not good for arbitrary rotations. Euler angles suffer from singularities and instability. Look at 38:25 (presentation of David Sachs)
http://www.youtube.com/watch?v=C7JQ7Rpwn2k
Good luck!
As #don mentions, try to avoid cumulative transformations, as you can run into all sort of problems. Rotating by one axis at a time might lead you to Gimbal Lock issues. Try to do all rotations in one go.
Also, bare in mind that Processing comes with it's own Matrix3D class called PMatrix3D which has a rotate() method which takes an angle(in radians) and x,y,z values for the rotation axis.
Here is an example function that would rotate a bunch of PVectors:
PVector[] rotateVerts(PVector[] verts,float angle,PVector axis){
int vl = verts.length;
PVector[] clone = new PVector[vl];
for(int i = 0; i<vl;i++) clone[i] = verts[i].get();
//rotate using a matrix
PMatrix3D rMat = new PMatrix3D();
rMat.rotate(angle,axis.x,axis.y,axis.z);
PVector[] dst = new PVector[vl];
for(int i = 0; i<vl;i++) {
dst[i] = new PVector();
rMat.mult(clone[i],dst[i]);
}
return dst;
}
and here is an example using it.
HTH
A shot in the dark: I don't know the rules or the name of the programming language you are using, but this procedure looks suspicious:
void setIdentity() {
this.matrix = identityMatrix;
}
Are you sure your are taking a copy of identityMatrix? If it is just a reference you are copying, then identityMatrix will be modified by later operations, and soon nothing makes sense.
Though the matrix renormalization suggested probably works fine in practice, it is a bit ad-hoc from a mathematical point of view. A better way of doing it is to represent the cumulative rotations using quaternions, which are only converted to a rotation matrix upon application. The quaternions will also drift slowly from orthogonality (though slower), but the important thing is that they have a well-defined renormalization.
Good starting information for implementing this can be:
http://www.cprogramming.com/tutorial/3d/quaternions.html
http://www.scheib.net/school/library/quaternions.pdf
A useful academic reference can be:
K. Shoemake, “Animating rotation with quaternion curves,” ACM
SIGGRAPH Comput. Graph., vol. 19, no. 3, pp. 245–254, 1985. DOI:10.1145/325165.325242
I have made the game, "Breakout". A small fun side-project.
Now, I usually do not make games, so collision-handling is not something I normally think about.
I have a paddle, a ball and some bricks.
For now, when there is a collision (I draw rectangles around each of the objects mentioned), I simply change the Y value of the ball to -Y.
This works fine, EXCEPT if the ball hits a brick from the side (either East or West). The side-effect is not pretty and ruins the gameplay.
I think I can safely assume that instead of the above technique, I need to change the X value to -X when this happens.
So far I have: if (ballRect.IntersectsWith(brickRect))
ballRect and brickRect being rectangles around each object.
Now, what if I created a rectangle around the eastern border of the brick, the western border, etc? I guess the width would be about a pixel.
If collision happens with western or eastern rectangle, then the balls X value should be -X.
And vice versa.
What about the corners though? Should I just randomly choose which rectangle to control of x corner?
Or perhaps should I make a rectangle around each corner? the rectangle being 1*1 in side.
If there is a collision => -x AND -y values of the ball?
Please share your thoughts.
Here is the process so far:
foreach (var brick in Bricks)
{
if (brick.IsAlive)
{
var brickRect = new Rectangle(brick.X, brick.Y, BrickWidth, BrickHeight);
if (ballRect.IntersectsWith(brickRect)) //Ball has hit brick. lets find out which side of the brick
{
var brickRectNorth = new Rectangle(brick.X, brick.Y + BrickHeight, BrickWidth, 1);
var brickRectSouth = new Rectangle(brick.X, brick.Y, BrickWidth, 1);
var brickRectEast = new Rectangle(brick.X, brick.Y, 1, BrickHeight);
var brickRectWest = new Rectangle(brick.X + BrickWidth, brick.Y, 1, BrickHeight);
if (ballRect.IntersectsWith(brickRectNorth) || ballRect.IntersectsWith(brickRectSouth))
{
//STUFF that makes ball.y = -ball.y
}
if (ballRect.IntersectsWith(brickRectWest) || ballRect.IntersectsWith(brickRectEast))
{
//STUFF that makes ball.x = -ball.x
}
}
}
}
Rather than looking for rectangle intersections, I'd intersect the actual edges. At the corner, your ball is touching two edges simultaneously, so its motion vector should be affected by both.
I would keep the single rectangle for collision detection, since that reduces the number of rectangles you need to test in your outer loop, but then once a collision with a brick has been detected, go into an inner loop to detect which edge it was that was hit. If you just test each edge and adjust the vector accordingly for each one, the corner will come for free (as long as you don't break out of the loop when you find the first intersecting edge).
Edit: In response to your updated question:
Actually, this is how I would do it (given your code, this appears to be C# 3.0, so that's what I've assumed below):
foreach(var brick in Bricks) {
if(brick.IsAlive) {
var brickRect = new Rectangle(brick.X, brick.Y, BrickWidth, BrickHeight);
if(ballRect.IntersectsWith(brickRect)) {
// Ball has hit brick. Now let's adjust the ball's vector accordingly
// Convenience variables. Compiler will probably inline.
var brickLeft = brick.X;
var brickRight = brick.X + BrickWidth;
var brickTop = brick.Y;
var brickBottom = brick.Y + BrickHeight;
var ballLeft = ball.X - ball.Radius;
var ballRight = ball.X + ball.Radius;
var ballTop = ball.Y - ball.Radius;
var ballBottom = ball.Y + ball.Radius;
// Test which vector(s) we need to flip
bool flipX = (ballRight >= brickLeft || ballLeft <= brickRight);
bool flipY = (ballTop >= brickBottom || ballBottom <= brickTop);
// Flip the vectors (there are probably ways to optimize this,
// too, but without seeing your code I can't tell).
if(flipY) {
// Stuff that makes ball.y = -ball.y
}
if(flipX) {
// Stuff that makes ball.x = -ball.x
}
}
}
}
Basically, the point is that since you already know the ball actually intersects the brick, you can simplify to a simple box test, which is much faster. Also, there's no need to create extra rectangles for the edges -- just use the edges of the rectangle you already have.