Here is the solution. It is based on a wonderful answer at https://math.stackexchange.com/a/205589/81266 . I found this answer by searching for "random points on a spherical cap" after I learned at Mathworld that a spherical cap is a section of a 3-sphere with a plane.
Here's the function:
function r = randSphericalCap(coneAngleDegree, coneDir, N, RNG) if ~exist('coneDir', 'var') || isempty(coneDir) coneDir = [0;0;1]; end if ~exist('N', 'var') || isempty(N) N = 1; end if ~exist('RNG', 'var') || isempty(RNG) RNG = RandStream.getGlobalStream(); end coneAngle = coneAngleDegree * pi/180; % Generate points on the spherical cap around the north pole [1]. % [1] See https://math.stackexchange.com/a/205589/81266 z = RNG.rand(1, N) * (1 - cos(coneAngle)) + cos(coneAngle); phi = RNG.rand(1, N) * 2 * pi; x = sqrt(1-z.^2).*cos(phi); y = sqrt(1-z.^2).*sin(phi); % If the spherical cap is centered around the north pole, we're done. if all(coneDir(:) == [0;0;1]) r = [x; y; z]; return; end % Find the rotation axis `u` and rotation angle `rot` [1] u = normc(cross([0;0;1], normc(coneDir))); rot = acos(dot(normc(coneDir), [0;0;1])); % Convert rotation axis and angle to 3x3 rotation matrix [2] % [2] See https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle crossMatrix = @(x,y,z) [0 -zy; z 0 -x; -yx 0]; R = cos(rot) * eye(3) + sin(rot) * crossMatrix(u(1), u(2), u(3)) + (1-cos(rot))*(u * u'); % Rotate [x; y; z] from north pole to `coneDir`. r = R * [x; y; z]; end function y = normc(x) y = bsxfun(@rdivide, x, sqrt(sum(x.^2))); end
This code simply implements jorikis response to math.stackexchange , filling in all the details that are missing joriki.
Heres a script that shows how to use it.
clearvars coneDir = [1;1;1]; coneAngleDegree = 30; N = 1e4; sol = randSphericalCap(coneAngleDegree, coneDir, N); figure;plot3(sol(1,:), sol(2,:), sol(3,:), 'b.', 0,0,0,'rx'); grid xlabel('x'); ylabel('y'); zlabel('z') legend('random points','origin','location','best') title('Final random points on spherical cap')
Here is a 3D graph of 10,000 points from a 30 ° spherical cap centered around a vector [1; 1; 1] [1; 1; 1] [1; 1; 1] :

Heres a 120 ° spherical cap:

Now, if you look at the histogram of the angles between these random points at coneDir = [1;1;1] , you will see that the distribution is distorted. Heres the distribution:

Code to generate:
normc = @(x) bsxfun(@rdivide, x, sqrt(sum(x.^2))); mysubspace = @(a,b) real(acos(sum(bsxfun(@times, normc(a), normc(b))))); angs = arrayfun(@(i) mysubspace(coneDir, sol(:,i)), 1:N) * 180/pi; nBins = 16; [n, edges] = histcounts(angs, nBins); centers = diff(edges(1:2))*[0:(length(n)-1)] + mean(edges(1:2)); figure('color','white'); bar(centers, n); xlabel('Angle (degrees)') ylabel('Frequency') title(sprintf('Histogram of angles between coneDir and random points: %d deg', coneAngleDegree))
Well, that makes sense! If you create points from a 120 ° spherical cap around coneDir , of course, there will be very few such samples per 1 ° cap, while the strip between the 10 ° and 11 ° caps will have much more points. Therefore, we want to normalize the number of points at a given angle over the surface area of a spherical cap at that angle.
Here is a function that gives us the surface area of a spherical cap with radius R and angle in theta radians (equation 16 on a Mathworlds spherical cap ):
rThetaToH = @(R, theta) R * (1 - cos(theta)); rThetaToS = @(R, theta) 2 * pi * R * rThetaToH(R, theta);
Then we can normalize the histogram score for each bin ( n above) by the difference in the surface area of the spherical caps at the edges of the bin:
figure('color','white'); bar(centers, n ./ diff(rThetaToS(1, edges * pi/180)))
Picture:

This tells us "the number of random vectors divided by the surface area of the spherical segment between the edges of the histogram bin." This is uniform!
(NB If you make this normalized histogram for vectors generated by your source code using a reject selection, the same thing happens: the normalized histogram is homogeneous. Just so that the reject selection is expensive compared to this.)
(NB 2: note that the naive way of selecting random points on a sphere - first creating azimuth / elevation angles and then converting these spherical coordinates to Cartesian coordinates - is not good because it will thicken the points near the poles: Mathworld , example , example 2. One way to select points across the entire sphere is to select from the normal 3D distribution: you won’t get close to the poles, so I think your original technique is perfect, giving you nice, evenly distributed points on the spheres e without any groupings. This algorithm described above also does the “right thing” and should avoid grouping. Carefully evaluate any proposed algorithms to ensure that the problem of grouping near the poles is avoided.)