Distributing spheres around a bigger sphere.

So, I’m using small planets from the Solar System sample to serve as targets, and they have a scale of 0.5. How do I use that in the function that creates points around a bigger sphere with the render as the center? As it is, the points spread around the center in a spiral pattern, but it doesn’t take into account the smaller radius of each planet, and so they overlap when rendered. Could you help me figure this out?

def renderTargets(self, points):
	
		for each in points:
			self.target = loader.loadModel("/c/Panda3D-1.8.1/samples/Solar-System/models/planet_sphere")
			self.target_tex = loader.loadTexture("/c/Panda3D-1.8.1/samples/Solar-System/models/mercury_1k_tex.jpg")
			self.target.setTexture(self.target_tex, 1)
			self.target.setScale(0.5)
			self.target.reparentTo(render)
			self.target.setPos(each)
		
	def sphereTargets(self, N):
		
		# Define points of around a sphere as positions for targets
		points = []
		increment = math.pi * (3.0 - math.sqrt(5.0))
		offset = 2.0/N
		
		for n in range(N):
			y = n * offset - 1.0 + (offset / 2.0)
			r = math.sqrt(1 - y * y)
			phi = n * increment
			points.append(Point3(math.cos(phi) * r, y, math.sin(phi) * r))
		
		self.renderTargets(points)

Do you not just add the size of the sphere to the radius in your “for”-loop? Something like this:

(In your “for”-loop, replacing the line that begins "r = ".)

r = math.sqrt(1 - y * y) + sphereSize

Where “sphereSize” is a variable defined outside the “for”-loop that is assigned the size of the sphere.

A note of caution, however: the scale applied to the planet model may not be the same as its size–that would only be the case if the model originally had a radius of 1, I believe. You might consider using “calcTightBounds” to find the diameter, and thus the radius, of the sphere.

Well, the size of the scaled sphere is 1, because when I used getTightBounds for both sphereSizeMax and sphereSizeMin, I got (0.5, 0.5, '0.5) and (-0.5, -0.5, -0.5). Now, when I added 1 to the radius of the sphere (just like what you described), I got a circle distribution instead of a spiral one.

I get better results when I scale the radius (i.e. multiply it by some number) as well as Y like this:

y = n * offset - 1.0 + (offset / 2.0)
			r = math.sqrt(1 - y * y) * 2
			phi = n * increment
			points.append(Point3(math.cos(phi) * r, y * 2, math.sin(phi) * r))

For 20 points generated, that is. It feels without reason why I should multiply it by that number, I have to find a relation between the number of points generated and the space necessary between them.

Hmm… Looking again at your code, why are you calculating r as sqrt([b]1[b] - yy)? As I recall, the radius of a circle should be rr = xx + yy, giving us r = sqrt(xx + yy).

As it is, the result is that r starts out small, increases, then decreases again–are you sure that that’s what you want?

Here, for example, is the output of printing out the resultant “r” values in a Python shell with N = 20:

N = 20
offset = 2.0/N
for n in range(N):
         y = n * offset - 1.0 + (offset / 2.0)
         r = math.sqrt(1 - y * y)
         print r
         
0.31224989992
0.526782687643
0.661437827766
0.759934207679
0.835164654425
0.893028554975
0.93674969976
0.968245836552
0.988685996664
0.998749217772
0.998749217772
0.988685996664
0.968245836552
0.93674969976
0.893028554975
0.835164654425
0.759934207679
0.661437827766
0.526782687643
0.31224989992

Either way, adding 1 to the value of r shouldn’t produce a circle–it should produce exactly the same output, but larger by one unit. Are you sure that you replaced the line as I showed?

By way of comparison, here’s the result of adding one to the value of r, again printing from a Python shell:

N = 20
offset = 2.0/N
for n in range(N):
         y = n * offset - 1.0 + (offset / 2.0)
         r = math.sqrt(1 - y * y) + 1
         print r
         
1.31224989992
1.52678268764
1.66143782777
1.75993420768
1.83516465442
1.89302855497
1.93674969976
1.96824583655
1.98868599666
1.99874921777
1.99874921777
1.98868599666
1.96824583655
1.93674969976
1.89302855497
1.83516465442
1.75993420768
1.66143782777
1.52678268764
1.31224989992

http://cubo2.net/blog/distributing-n-points-on-a-sphere/

This is where I based my code from. Look at the picture with the spheres because it’s the desired effect.

It’s the function of a spiral, I believe, which means that the radius should start small, get bigger and then small again.

Ah, I see–I took “spiral” to refer to something more resembling a whirlwind–ever increasing (or decreasing).

Hmm… I’m not familiar with the mathematics on which this is based, and the link to the paper on which that site’s code is based seems to be dead…

However, now that I see what you’re trying to do, the answer seems straightforward: multiply the point that you create at the end of your method by the desired size. Something like this:

points.append(Point3(math.cos(phi) * r, y, math.sin(phi) * r)*sphereSize)

Again, where “sphereSize” is the desired size of the sphere.

Since your points seem to describe a uniform distribution of points around a sphere, and more importantly, a sphere located at the origin, multiplying them by a given value should turn them into a similar distribution of points around a sphere of that size.

(If you want to generate points that are not located around the origin, either parent them to a NodePath and move that, or add the new position after the multiplication that I introduced above, I believe.)

Yes, that’s what I used in a post above, but the problem is another (sorry if I’m not making myself clear). The problem is that the number of small spheres (given by N) has to be proportional to the size of the bigger sphere (given by sphereSize, like you suggested) so that the small ones don’t overlap. I have to build a relationship between them by using the size of a scaled small sphere (which is 1).

Ah, I’m sorry–I’m perhaps not at my best today. I do see now that you did indeed mention that in your first post. ^^;

So–to check that I’m following you correctly–your given values are the sizes of the large and small spheres, and you want to produce a value of N such that you can cover the large sphere in small spheres, but without the small spheres overlapping, yes?

Offhand, my guess is that the answer might be related to the angle between spheres, which you call “increment”, I believe–come to that, shouldn’t that value depend on the number of lesser spheres? After all, fewer spheres should surely have a larger angle between them… Perhaps, if that does vary according to the number of lesser spheres, finding the chord-length associated with that would give you the radius of the lesser spheres.

Otherwise, a little digging past that post to which you linked suggests that this technique is referred to as a “Fibonacci sphere”–perhaps if you did some searches based on that term you might find more resources, specifically detailing the mathematics behind this?

Thank you, I’ll definitely do that.

If you are wanting to get the small spheres as large as possible without overlapping, I would recommend looking at the sphere packing datasets on this page (neilsloane.com/ under Tables). Otherwise, it is a difficult optimization problem to calculate while running a program.

So, I use those values in lists or dictionaries and call them whenever I want to assign models of the same size, and in the same quantity. Thank you!

http://neilsloane.com/packings/index.html#I

I’m using Part 1 of this page’s list of number of points and their dimension. However, I don’t know how to scale the small spheres to 3, so I tried doing it by eye and used this:

targets["target"+str(n)].setScale(0.7)

The coordinates that appear in each packing file were placed in a dictionary with the number of points as the key, and the list of points as the value so that I can access them like this:

def sphereTargets(self, N):
	
		points = self.points[str(N)]
		targets = {}
		
		for n in range(N):
			# Load the models and their textures
			targets["target"+str(n)] = loader.loadModel("/c/Panda3D-1.8.1/samples/Solar-System/models/planet_sphere")
			target_tex = loader.loadTexture("/c/Panda3D-1.8.1/samples/Solar-System/models/mercury_1k_tex.jpg")
			targets["target"+str(n)].setTexture(target_tex, 1)
			# And position them around the sphere
			targets["target"+str(n)].reparentTo(render)
			targets["target"+str(n)].setScale(0.7)
			targets["target"+str(n)].setPos(points[n])

And this is the list of points so far:

self.points = {"4": [Point3(-0.577350269072, 0.577350269072, -0.577350269072), Point3(0.577350269072, 0.577350269072, 0.577350269072),
			Point3(-0.577350269072, -0.577350269072, 0.577350269072), Point3(0.577350269072, -0.577350269072, -0.577350269072)],
			"5": [Point3(-1.478255937088018300e-01, 8.557801392177640800e-01, 4.957700547280610200e-01), Point3(9.298520676823500700e-01,
			-3.330452755499895800e-01, -1.563840677968503200e-01), Point3(-7.820264758448114400e-01, -5.227348665222011400e-01, -3.393859902820995400e-01),
			Point3(-3.612306945786420600e-02, -5.056147808319168000e-01, 8.620027942282061400e-01), Point3(3.612306958303366400e-02, 5.056147801034870400e-01, 
			-8.620027946502272200e-01)],
			"6": [Point3(0.212548255920, -0.977150570601, 0.000000000000), Point3(-0.977150570601, -0.212548255920, 0.000000000000), Point3(-0.212548255920, 
			0.977150570601, 0.000000000000), Point3(0.977150570601, 0.212548255920, 0.000000000000), Point3(0.000000000000, 0.000000000000, 1.000000000000), 
			Point3(0.000000000000, 0.000000000000, -1.000000000000)]}

I’m glad if you’re making progress. :slight_smile:

If I may, two things occur to me:
First, why are you using strings for your dictionary keys? Why not simply use the numeric values that go into your current string keys?

Second, you mention uncertainty in how to scale the spheres as decided–you might be able to calculate appropriate values using “calcTightBounds”, as I suggested above.

I’m sorry if this is a dumb question, but how do you use calcTightBounds exactly?

First of all, a link to the API documentation.

Now, if I’m not much mistaken:

The “calcTightBounds” method takes two Point3 objects, and sets their values such that they represent the “minimum” and “maximum” points of a box that surrounds the NodePath in question (as well as returning a boolean value to indicate whether the calculation was successful); these points are simply two diagonally-opposite corners of this box. Given this, the size of the box can be calculated by taking the difference of the two points (maxPt - minPt)–the values of the resultant Vec3 should be size of the box (width, height and depth, not necessarily in that order).

Since your models are spheres, the width, height or depth of the box should correspond reasonably well with the diameter of the sphere, and can thus be used to get a base size with which to produce a useful scalar in order to produce spheres of other sizes.

A basic example of the usage of “calcTightBounds”; presume that we already have a NodePath named “np”, which holds a model of some sort:

minPt = Point3()
maxPt = Point3()

success = np.calcTightBounds(minPt, maxPt)
if success:
    size = maxPt - minPt

Thank you! I decided to normalize the vector that I get from maxPnt - minPnt and it gives me a fairly good result, but now I want to test it with a big number of positions. However, I’m not going to type them in one by one because they’re 390 lines! I tried to use this, but it’s not working:

with open("packing/pack3-"+str(N)+".txt") as data:
			for first, second, third in data:
				points = Point3(first, second, third)

But it returns me “ValueError: too many values to unpack”. I don’t get the nature of this error, or maybe I’m not that experienced with Python, but I also can’t find the answer. I decided to iterate through the file 3 values at a time, in order to fill a Point and put it in the list. Since they’re 390, it should repeat itself 130 times, but I don’t know what I am doing wrong. This is the list that I’m talking about - http://neilsloane.com/packings/dim3/pack.3.130.txt

EDIT: I’m getting closer:

with open("packing/pack3-"+str(N)+".txt") as data:
			for line in data:
				lines.append(line)
			for n in range(3, 390, 3):
				points[n/3-1] = lines[n-3:n]
			print points

Why are you doing that, if I may ask? That should result in a vector that always has a length of 1, and since your objects are spheres, and thus are likely to produce a cubic bounding box, I would expect this to result in the same vector regardless of the actual size of the sphere…

(If you want to check this for yourself, replace your spheres with a custom sphere-model of much larger size, and have your program print the calculated size.)

This is nearing the edges of my own knowledge of Python, but as I understand it, the syntax that you used–“for a, b, c, etc. in data”–considers each element of “data” to be itself a sequence of some sort (list, string, tuple, etc.), and “unpacks” the contents of each element of data into a, b, c, etc. However, this requires that there be the right number of variables on the left-hand side for the contents of each element of data.

For example, this should work, I believe:

test = ["cat", "mew", "paw"]
for a, b, c in test:
    print a
    print b
    print c
    print "====="

Which should print:

c
a
t
=====
m
e
w
=====
p
a
w
=====

Your second approach makes sense, however–there are other, cleverer ways with Python, I believe, but I’m not sufficiently familiar with them that I feel comfortable in recommending them. You’ll presumably want to convert the textual data in the file into actual floats–it’s possible that Point3 can do this automatically, but if not, you can convert a string to a float simply by calling “float(myString)”, where “myString” is the variable containing your string.

def sphereTargets(self, N):
	
		coordinates = []
		angles = []
		points = {}
		targets = {}
		
		with open("packings/pack3-"+str(N)+".txt") as data:
			unpack = [coordinates.append(float(lines)) for lines in data]
		
		with open("packings/angles.txt") as file:
			unpack = [angles.append(float(digits[8:20])) for digits in file]
				
		for n in range(3, N * 3 + 3, 3):
			points[n/3-1] = Point3(coordinates[n-3], coordinates[n-2], coordinates[n-1])
		
		for n in range(N):
			
			# Load the models and their textures
			targets[n] = loader.loadModel("/c/Panda3D-1.8.1/samples/Solar-System/models/planet_sphere")
			target_tex = loader.loadTexture("/c/Panda3D-1.8.1/samples/Solar-System/models/mercury_1k_tex.jpg")
			targets[n].setTexture(target_tex, 1)
			
			# Find the radius of the small spheres
			angleRad = angles[N-13] * math.pi/180.0
			cscAngle = 1.0/math.sin(angleRad/2.0)
			radius = 1.0/(cscAngle - 1.0)
			
			# And position them around the sphere
			targets[n].reparentTo(render)
			targets[n].setScale(radius/2)
			targets[n].setPos(points[n])

Ok, this works for me. I went to the Wolfram Math page for Spherical Code and learned that the above works for 13 spheres or more, and that there are specific distributions for a number of spheres below 13, such as the vertices of the platonic solids. Sorry for all the trouble, but it was nice having your input.