What do I want to achieve?
I want to have a performant system that checks if a part is visible to the camera or not.
What is the issue?
There’s no built-in function for it, only one that checks if a specific position (Vector3) is visible or not, while I want it to check for a whole part.
What solutions have I tried so far?
So I’ve tried going through the 4 corner verticies on a block (I am just using a block, no complicated sphere or such thankfully) and it worked and looked like this:
local function onScreen(object)
if object:IsA("BasePart") then
local bool = false
for _, vector in pairs(verticies) do
if not bool then
local cornerPosition = (object.CFrame * CFrame.new(object.Size.X / 2 * vector[1], object.Size.Y / 2 * vector[2], object.Size.Z / 2 * vector[3])).Position
local _, onScreen = MovementCamera:WorldToScreenPoint(cornerPosition)
if onScreen then
bool = true
end
end
end
return bool
end
end
But even tho it worked, just checking the verticies is not enough. You see, if the part is big enough, the verticies might not be visible to the camera, but the part might still be.
In the example image below, I put on wireframe rendering, took a picture and put it in paint. Then I drew the outline of a cameras view and the 4 verticies of a part that is very big (The size I use so It’s a genuine problem).
Even for some positions of small objects, just point-in-box checks are not enough; you need to check for vertices being on different sides of any of sides of the viewport. In typical frustum culling code, the frustum has 6 planes to consider, but in your case the WorldToScreenPoint is aready doing the projection to 2D, so you only need to think about the 4 lines that are infinitely long extensions of the viewport rectangle sides.
Even with this separating plane check implemented, the code can still fail when the projection of your object is much larger than the viewport (the Part is huge, or simply very close to the camera). For this case, I refer you to the clever blog post below. TLDR is that you can check part vertices against viewport rect, but also viewport corners against the projected bounds of the Part. This is a bit trickier in 2D, since you need to know which vertices of the part bounding box are part of the convex hull of the projection, but there are lots of algorithms for this and they are not complicated. 2D Graham Scan is pretty simple.
No problem! I should have noted also, if you don’t need this to be 100% accurate–and in fact for things like convex MeshParts you can’t even make it perfect–you can not bother with trying to figure out the actual polygonal projection and convex hull business and instead just use the 2D axis-aligned bounding box of the part’s projection. Once you project all the part vertices onto the screen with WorldToScreenPoint, you can just use math.min and math.max to get the bounding box extents, and then proceed with just the 4 corners of that box. It will err on the side of thinking the object is visible, which is generally the direction you want it to fail if you’re just trying to hide off-screen things, as it will make sure nothing that should actually be visible remains culled, so the user won’t see things popping on and off.