MeshRenderer.isVisible issues

Hi all. As I was making more levels for my Project Boost game I wanted to add a directional arrow pointing towards the landing pad so it was easy to tell where you were supposed to be going. I made a simple cone in Blender and added that to my rocket prefab, and it seemed that the code would be easy enough using MeshRenderer.isVisible but it isn’t working the way I expected. I was just wondering if anyone might be able to offer some advice (either with a different way of knowing if the landing pad is currently visible on the screen, or if there’s a better way to do this). The relevant code is below.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Movement : MonoBehaviour {
	[SerializeField] GameObject DirectionalCone = null; // Gets assigned in the inspector
	[SerializeField] GameObject LandingPad = null; // Gets assigned in the inspector

	MeshRenderer DirectionalConeMeshRenderer;
	MeshRenderer LandingPadMeshRenderer;

	// Start is called before the first frame update
	void Start() {
		Rigidbody = GetComponent<Rigidbody>();
		RigidbodyConstraints = Rigidbody.constraints;

		DirectionalConeMeshRenderer = DirectionalCone.GetComponent<MeshRenderer>();
		LandingPadMeshRenderer = LandingPad.GetComponent<MeshRenderer>();
	}

	// Update is called once per frame
	void Update() {
		if (LandingPadMeshRenderer.isVisible == false) {
			DirectionalConeMeshRenderer.enabled = true;
			DirectionalCone.transform.LookAt(LandingPad.transform);
		} else {
			DirectionalConeMeshRenderer.enabled = false;
		}
	}
}

The .LookAt() command works great, but my if (LandingPadMeshRenderer.isVisible == false) command only works some of the time. Even on scenes where the landing pad isn’t visible at the start the directional cone will be hidden until I fly up a little bit, but then as I lower back down it’ll disappear again (see video below). I know the .isVisible returns true if you’re looking at the scene view and the object is visible there, but I made sure to only be in the Game window, but it’s still happening.

Any ideas of how to fix this?

https://imgur.com/a/0hvotbj

The MeshRenderer.isVisible is a little unreliable during development. This is because it will say it is visible if any camera is looking at it. This includes the Scene View’s camera. If you make sure that the game screen is the only camera looking at it you should be good. And in a build, there will never be a scene view and it should all be ok.


You could, of course, do it yourself. Then you will have the control to only use the camera you want. What you’d probably want to do is create a function that will take the relevant camera and the mesh renderer and, using its bounds, see if it is within the bounds of the camera. It would be something like

    private bool MeshIsVisibleToCamera(Camera camera, MeshRenderer renderer)
    {
        // easy part first. if the renderer's pivot is in view we should be ok
        var viewPos = camera.WorldToViewportPoint(renderer.transform.position);
        if (viewPos.x >= 0 && viewPos.y >= 0 && viewPos.x <= 1 && viewPos.y <= 1)
            return true;

        // now the more tricky part. if the pivot is not in view,
        // the mesh may still have some bits that are visible
        var bounds = renderer.bounds;

        // check the min extent of the bounds
        var minPos = camera.WorldToViewportPoint(bounds.min);
        if (minPos.x >= 0 && minPos.y >= 0 && minPos.x <= 1 && minPos.y <= 1)
            return true;

        // check the max extent of the bounds
        var maxPos = camera.WorldToViewportPoint(bounds.max);
        if (maxPos.x >= 0 && maxPos.y >= 0 && maxPos.x <= 1 && maxPos.y <= 1)
            return true;

        // if none of those are visible, we're likely not visible
        return false;
    }

Note: I have not tested the above code. It may be completely wrong and broken

Use it in your Update()

// Update is called once per frame
void Update() {
    DirectionalConeMeshRenderer.enabled = !MeshIsVisibleToCamera(Camera.main, LandingPadMeshRenderer);
    DirectionalCone.transform.LookAt(LandingPad.transform);
}
1 Like

I appreciate the quick response and explanation, but unfortunately the first portion of your post doesn’t seem to fix the issue for me. I’m playing without the Scene View being visible and it’s still not showing up correctly. I even tried rotating the Scene View camera away to be looking at nothing just in case and got the same result.

I then tried building the game and running it and while it seems a little bit better, it’s definitely still not right. In the video below you can see that you can just barely see the green landing pad on the right side of the screen and the directional cone isn’t showing (good). But as I start to move to the left the pad can’t be seen anymore but the cone doesn’t show up until I move a decent distance away (bad). It’s almost like the game think it’s running at a wider resolution than it actually is.

I’ll try changing to use code similar to yours later today when I get a chance and see if that makes a difference.

https://imgur.com/a/QTs6wk6

This is from the documentation:

Perhaps try the code I posted above. If it doesn’t work, we’ll troubleshoot that

1 Like

I tested the code now and while it works, there are some issues;

image
The minPos is the most min position of the renderer. In my test case, this is the green sphere. The blue sphere is the most max position. In both cases the perspective causes the indicator to show up before the cube is out of view because that specific point goes out of view (the grey sphere is the indicator)

image
image

So, what we probably want to do is look at all 8 corners (bounding box is a cube) and check if any of them is in view

The updated code is starting to border on ‘strange’. I am sure someone else can do it better. But it does work now.

private bool MeshIsVisibleToCamera(Camera camera, Renderer renderer)
{
    // easy part first. if the renderer's pivot is in view we should be ok
    var viewPos = camera.WorldToViewportPoint(renderer.transform.position);
    if (viewPos.x >= 0 && viewPos.y >= 0 && viewPos.x <= 1 && viewPos.y <= 1)
        return true;

    // now the more tricky part. if the pivot is not in view,
    // the mesh may still have some bits that are visible
    var bounds = renderer.bounds;

    // let's check each of the points and see if any is visible
    // Helper constants
    const int LEFT = -1;
    const int RIGHT = 1;
    const int TOP = 1;
    const int BOTTOM = -1;
    const int FRONT = -1;
    const int BACK = 1;

    // left-bottom-front
    if (PointIsVisible(LEFT, BOTTOM, FRONT)) return true;
    // left-top-front
    if (PointIsVisible(LEFT, TOP, FRONT)) return true;
    // left-bottom-back
    if (PointIsVisible(LEFT, BOTTOM, BACK)) return true;
    // left-top-back
    if (PointIsVisible(LEFT, TOP, BACK)) return true;
    // right-bottom-front
    if (PointIsVisible(RIGHT, BOTTOM, FRONT)) return true;
    // right-top-front
    if (PointIsVisible(RIGHT, TOP, FRONT)) return true;
    // right-bottom-back
    if (PointIsVisible(RIGHT, BOTTOM, BACK)) return true;
    // right-top-back
    if (PointIsVisible(RIGHT, TOP, BACK) ) return true;

    // if none of those are visible, we're likely not visible
    return false;

    // All the duplicated code in one place
    bool PointIsVisible(int xDir, int yDir, int zDir)
    {
        var point = GetPoint(xDir, yDir, zDir);
        var viewPos = camera.WorldToViewportPoint(point);
        return (viewPos.x >= 0 && viewPos.y >= 0 && viewPos.x <= 1 && viewPos.y <= 1);
    }
    // Just a simple helper to get the points I want
    Vector3 GetPoint(int xDir, int yDir, int zDir)
        => new Vector3(
            bounds.center.x + (bounds.extents.x * xDir),
            bounds.center.y + (bounds.extents.y * yDir),
            bounds.center.z + (bounds.extents.z * zDir));
}
1 Like

Here is a far less verbose version, but it’s a little more advanced

private bool MeshIsVisibleToCamera(Camera camera, Renderer renderer)
{
    // easy part first. if the renderer's pivot is in view we should be ok
    if (IsVisible(renderer.transform.position))
        return true;

    // now the more tricky part. if the pivot is not in view,
    // the mesh may still have some bits that are visible
    // let's check each of the 8 points and see if one is visible
    for (var i = 0; i < 8; i++)
        if (IsVisible(GetPointFromBits(i)))
            return true;

    // no points are visible
    return false;

    // bit shifting
    Vector3 GetPointFromBits(int bits)
    {
        var xDir = (int)(((bits >> 2) & 0x1) * 2f - 1f); // get the bit and remap
        var yDir = (int)(((bits >> 1) & 0x1) * 2f - 1f); // get the bit and remap
        var zDir = (int)(((bits >> 0) & 0x1) * 2f - 1f); // get the bit and remap
        return GetPoint(xDir, yDir, zDir);
    }
    bool IsVisible(Vector3 point)
    {
        var viewPos = camera.WorldToViewportPoint(point);
        return (viewPos.x >= 0f && viewPos.y >= 0f && viewPos.x <= 1f && viewPos.y <= 1f);
    }
    // Just a simple helper to get the point that I want
    Vector3 GetPoint(int xDir, int yDir, int zDir)
    {
        var bounds = renderer.bounds;
        return new Vector3(
            bounds.center.x + (bounds.extents.x * xDir),
            bounds.center.y + (bounds.extents.y * yDir),
            bounds.center.z + (bounds.extents.z * zDir));
    }
}
1 Like

I think there are much easier ways to achieve this.

If what you want is to show the arrow when the player is close to the goal then you can calculate the distance between the player and the goal, then activate the arrow, this can be achieved with a single line of code.

DirectionalConeMeshRenderer.enabled = Vector3.Distance(player.position, LandingPad.position) <= showThreshold;

This approach has some issues, but it works just fine, and you can use squared magnitude instead if you don’t want to use square root operation.

If you definitely need the show-when-in-camera behavior, take a look at @bixarrio ‘s code, I would simplify it a lot since you don’t need to check the 8 points since this is pretty much a 2D game, you can only check two points; the bounds’ min and max vectors and the result would be pretty much the same regardless of how the camera moves.

Another super simple way that gives you more control is with trigger colliders, activate the arrow when the player enters the “near goal zone”, this saves you way too much code and also gives a lot of flexibility, since perhaps, some levels might benefit with a much larger goal zone, of course, you’ll have to do it manually for each level. In the end, it will depend on if you want this to be automatic, or have a little bit more flexibility.

2 Likes

There definitely are

I had this initially (here) but my tests showed it didn’t work (see here). Granted, I was testing in 3d with a perspective camera at 60 fov. The videos also show a perspective camera, so the 2 points alone won’t work.

The ‘distance to target’ and triggers are both good ideas, though. I got so caught up in the ‘renderer.isVisible’ issue that I failed to consider different methods.

2 Likes

There’s another way to check if the camera is looking at a target.

Create bounds using the camera’s min and max vectors, set a semi-arbitrary depth, and then check if those bounds intersect with the goal’s bounds.

This saves a lot of calculations.

2 Likes

I super appreciate all the suggestions! I won’t be able to try them out until after work at the soonest, and I’ll probably try each different method just to play around with how each one works as a learning experience, but I’ll definitely post here again once I’ve done so and I’ll mark the best answer as the solution.

1 Like

This code mostly worked (I’m sure the more advanced version you posted would have worked too, but I’m not familiar with bit shifting so I went with the version that was longer but easier for me to understand). The only problem I had is the compiler didn’t like that you declared viewPos in MeshIsVisibleToCamera and then declared it again in PointIsVisible within MeshIsVisibleToCamera (which I would have thought would be allowed, but the compiler disagreed).

I initially just renamed the one within PointIsVisible to localViewPos to avoid the conflict but for some reason that didn’t work, but once I moved PointIsVisible and GetPoint out into the class itself as their own methods (and passed camera and bounds as new arguments when appropriate) then it worked great. Thank you so much for the help!

@Yee I’ll definitely be looking through those docs you provided to try to learn a bit more and may even switch out my code if it all makes sense, so I appreciate the help from you as well.

Hmmm, I ran the code and it was working fine. At least you understood it enough to fix it. Good job

1 Like

Yeah, not sure why it didn’t like that since it’s supposed to be allowed (I’ve done similar things in other projects), but like you said at least I was able to edit it to get it working.

The bit shifting version is a little strange. I basically just used the integers to tell me which corner I’m looking at.

Each axis of a point is either +bounds.extents or -bounds.extents from the bounds.center, so to get the point I would either add or subtract bounds.extents from the bounds.center. The integers 0 through 7 take up 3 bits (0 = 000, 4 = 100, 7 = 111) so I used these bits to represent the axes

0__ = x
_0_ = y
__0 = z

with the shifting I shifted the bit I wanted to the right-most position

0__ >> 2 -> shift the 0 two positions to the right -> __0

and then ‘AND’ (&) it with 1 (I used the hex version 0x1) to sorta set all the other bits to 0. The result is that I get either a 0 or 1 depending on what that bit holds, for example

(011 >> 2) & 1 = 0 : shift the bits 2 positions to the right -> 000 (0) -> take the right-most bit -> 0 (0)
(011 >> 1) & 1 = 1 : shift the bits 1 position  to the right -> 001 (1) -> take the right-most bit -> 1 (1)
(011 >> 0) & 1 = 1 : shift the bits 0 positions to the right -> 011 (3) -> take the right-most bit -> 1 (1)

Then I just remapped them by multiplying by 2 and then subtracting 1. This then gives me either a -1 (if the bit is 0) or a 1 (if the bit is 1). If I then multiply bounds.extents with this value and add it to bounds.center I’m essentially adding or subtracting it.

With this I could get each of the 8 corners with a simple for-loop.

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms