# Frustum Culling Not Utilizing Entire Bounding Sphere in OpenGL

My frustum culling is detecting an object as culled even though not the entire object's bounding sphere is out of the frustum yet. It's like it believes the bounding sphere's radius is smaller than it is, although I'm confident that's not the case.

Thus far, I'm only checking if the top of the sphere is within the frustum, although I could get the same results (but in reverse, of course) by changing the frustum-checking code's if statement.

Frustum-Checking Code:

# vector to chunk from camera
chunk_vec = chunk.center - self.camera.pos

# chunk's y coordinate distance from camera on camera axes
y_dist = glm.dot(chunk_vec, self.camera.up) + CHUNK_BOUNDING_SPHERE_RADIUS
# chunk's z coordinate distance from camera on camera axes
z_dist = glm.dot(chunk_vec, self.camera.forward)
HIGHEST_ALLOWED_THETA = VFOV / 2
theta = glm.atan(y_dist / z_dist)
print(glm.degrees(theta), glm.degrees(HIGHEST_ALLOWED_THETA))
if theta <= HIGHEST_ALLOWED_THETA:
print('render')
else:
print('cull')


Bounding Sphere Radius Calculation: (I'm pretty confident although this seems to be the prime suspect, this isn't the error here because my bounding sphere render fits perfectly and I've rechecked this calculation multiple times)

length = glm.sqrt(glm.pow(CHUNK_LENGTH_VOXELS * VOXEL_SIZE / 2, 2) * 2)
height = CHUNK_HEIGHT_VOXELS * VOXEL_SIZE / 2
glm.pow(length, 2) + glm.pow(height, 2)
)


Example of how much of the bounding sphere is left when my frustum-checking code determines that the bounding sphere is no longer within the frustum: (note that this proportion of the sphere that is left unaccounted for by my code does not vary no matter the object's distance nor the camera's position) https://imgur.com/a/Y4gKiSI

My Object with its bounding sphere (to understand the scene better): https://imgur.com/a/Jk0Kjcf

I can manually calibrate the radius value to get perfect results, although that's obviously an unideal solution and I wish to understand what my error is.

Projection Matrix Calculation:

ASPECT_RATIO = 16 / 9
NEAR = 0.1
FAR = 20000
FOV = 70
HFOV = 2 * glm.atan(glm.tan(VFOV * 0.5) * ASPECT_RATIO)

self.proj_mat = glm.perspective(
VFOV, ASPECT_RATIO, NEAR, FAR
)


View Matrix Calculation: (in camera class)

yaw_change, pitch_change = pg.mouse.get_rel()
self.yaw -= yaw_change * MOUSE_SENSITIVITY
self.pitch -= pitch_change * MOUSE_SENSITIVITY
self.pitch = glm.clamp(self.pitch, -80, 80)

yaw_sin = glm.sin(yaw)
yaw_cos = glm.cos(yaw)
pitch_sin = glm.sin(pitch)
pitch_cos = glm.cos(pitch)

self.right = glm.normalize(glm.vec3(yaw_cos, 0, -yaw_sin))
self.up = glm.normalize(glm.vec3(yaw_sin * pitch_sin, pitch_cos, yaw_cos * pitch_sin))
self.forward = glm.normalize(glm.vec3(yaw_sin * pitch_cos, -pitch_sin, pitch_cos * yaw_cos))

self.view_mat = glm.mat4(
glm.vec4(self.right.x, self.up.x, self.forward.x, 0),
glm.vec4(self.right.y, self.up.y, self.forward.y, 0),
glm.vec4(self.right.z, self.up.z, self.forward.z, 0),
glm.vec4(-glm.dot(self.right, self.pos), -glm.dot(self.up, self.pos), -glm.dot(self.forward, self.pos), 1)
)