VisibleOnScreenNotifier3D offset in non-cardinal directions?

Godot Version

v4.4.stable.mono.official [4c311cbee]

Question

I’m trying to confine a camera to a rectangular area on the XZ plane by making the camera stop moving when it sees a certain VisibleOnScreenNotifier3D.

I’ve gotten it working in the cardinal directions, but when I rotate the camera 45° to the left, the VisibleOnScreenNotifier3D suddenly behaves as if the point at which it becomes visible is much further into the rectangular area than it actually is.

It’s probably easier to explain with a short video:

The basics of the code are as follows, with a few bits that should be irrelevant to the current problem commented out:

private void BoundEntryExit(int angle, bool entered)
{
    //keep track of which bounds are on screen
    camBoundInfo.SetVisAndOverride(angle, entered, isRotatedIncrement);

    /*if (isRotatedIncrement % (Math.Pow(2, camRotPower) / 4) == 0)
    //enable and disable movement in cardinal directions
    {
        camBoundInfo.CamMovements[(angle + (isRotatedIncrement / (int) (Math.Pow(2, camRotPower) / 4))) % 4] = !entered;
    }*/
}
private partial class CamBoundInfo
{
    /*public bool[] CamMovements = [true, true, true, true];*/
    public bool[] BoundsVisible = new bool[4];

    public void SetVisAndOverride(int angle, bool entered)
    {
        BoundsVisible[angle] = entered;

        if (angle == 2) //2 is north
        {
            if (BoundsVisible[2] == true)
            {
                GD.Print("north wall entered vision");
            }
            else
            {
                GD.Print("north wall left vision");
            }
        }
    }
}

private readonly CamBoundInfo camBoundInfo = new();

I’m happy to provide more of the code if this isn’t enough to diagnose the problem; I just want to make sure this isn’t a common problem with the way VisibleOnScreenNotifier3D nodes work first.

Haha, wow it’s been a little bit! :laughing:

I’ve been dealing with some real-life things, but now I’m settled enough to try dealing with this again. The code snippets above are outdated, it looks like; here is the current code, which is in a different form but has the same function…including the unintended behavior:

public partial class OwCamBound : VisibleOnScreenNotifier3D
{
    public static event Action<int, bool> ScreenEnterExit;
    private static readonly float[] angleKey = [0, -1.5707964f, -3.1415925f, 1.5707965f]; //[South, West, North, East]

    public override void _Ready()
    {
        ScreenEntered += WhichEnteredScreen;
        ScreenExited += WhichExitedScreen;
    }

    private void WhichEnteredScreen()
    {
        GD.Print(Array.FindIndex(angleKey, angle => angle == GlobalRotation.Y) + " entered");
        ScreenEnterExit?.Invoke(Array.FindIndex(angleKey, angle => angle == GlobalRotation.Y), true);
    }

    private void WhichExitedScreen()
    {
        GD.Print(Array.FindIndex(angleKey, angle => angle == GlobalRotation.Y) + " exited");
        ScreenEnterExit?.Invoke(Array.FindIndex(angleKey, angle => angle == GlobalRotation.Y), false);
    }
}

The above is everything from the object to which it’s tied. The below I trimmed a good chunk for legibility, but everything related to the camera’s movement in 3D space should be there. Some of the functions I’m pretty sure are unrelated to the problem, so I’ve put them at the end after a horizontal line.

public partial class OwCamera : Camera3D
{
    private Vector3 camVelocity;
    private Vector3 relativeForward;
    private Vector3 relativeLeft;
    private float[] camVelOverride = [0, 0];

    private bool[] directionsEnabled = [true, true, true, true];
    private bool[] boundsVisible = new bool[4];
    private int[] directionsMoving = new int[2];

    public override void _Ready()
    {
        OwCamDebuginfo.RequestDebugInfo += RequestDebugInfoEventHandler;
        OwCamBound.ScreenEnterExit += BoundEntryExit;
    }

    private void BoundEntryExit(int angle, bool entered)
    {
        //keep track of which bounds are on screen
        SetBoundVisAndMoveOverride(angle, entered, isRotatedIncrement);

        if (isRotatedIncrement % (Math.Pow(2, camRotPower) / 4) == 0)
        //enable and disable movement in cardinal directions
        {
            directionsEnabled[(angle + (isRotatedIncrement / (int) (Math.Pow(2, camRotPower) / 4))) % 4] = !entered;
        }
    }

    private void SetBoundVisAndMoveOverride(int angle, bool entered, int increment)
    {
        boundsVisible[angle] = entered;

        if (boundsVisible[2] && increment == 1 && angle == 2)
        //THIS NEEDS TO BE MADE SCALABLE/GENERIC AFTER FIRST CASE IS FINISHED
        {

            GD.Print("northwest override");

            //camVelOverride[0] = 3 * (float) Math.PI / 4;
            //camVelOverride[1] = 7 * (float) Math.PI / 4;
        }

        if (!entered) //for some reason there's no Array.FalseForAll() :/
        {
            bool allFalse = true;
            for (int i = 0; i < boundsVisible.Length; i ++)
            {
                if (boundsVisible[i])
                {
                    allFalse = false;
                    break;
                }
            }

            if (allFalse)
            {
                camVelOverride[0] = camVelOverride[1] = 0;
            }
        }
    }

    public override void _Process(double delta)
	{
        Moving((float) delta);
    }

    private void Moving(float delta)
	{
        SetRelativeDirections();
        directionsMoving = ConvertInputToDirection();

        //calculate movement vector from inputs
        //this isn't scaleable but it doesn't need to be
		if      (directionsMoving[0] == 2 && directionsEnabled[2]) {camVelocity -= relativeForward;}
        else if (directionsMoving[0] == 0 && directionsEnabled[0]) {camVelocity += relativeForward;}
        if      (directionsMoving[1] == 1 && directionsEnabled[1]) {camVelocity -= relativeLeft * camMoveHozRatio;}
        else if (directionsMoving[1] == 3 && directionsEnabled[3]) {camVelocity += relativeLeft * camMoveHozRatio;}
        
        camVelocity *= camMoveSpeed * delta;

        if (camVelocity != Vector3.Zero)
        //check for boundaries and then move camera after
        {
            if (camVelOverride[0] != 0)
            //override is zero unless a bound is on screen
            {
                //FOLLOWING SECTION IS SPECIFIC TO NW-FACING CAMERA SEEING N BOUND
                if (camVelOverride[0] == 3 * (float) Math.PI / 4)
                {
                    if (directionsMoving[0] == 2 || directionsMoving[1] == 3)
                    {
                        //convert above velocity into a vector of camVelOverride's direction and camVelocity's preexisting magnitude
                        //if there's a tie, don't move (it'd be a corner) (or maybe a cardinal direction..?)
                    }
                }
            }

            Position = new Vector3(Position.X + camVelocity.X, Position.Y, Position.Z + camVelocity.Z);
        }
	}

//______________________________________________________________________________

    private void SetRelativeDirections()
    {
        relativeForward = new Vector3(Transform.Basis.Z.X, 0, Transform.Basis.Z.Z);
        relativeLeft = new Vector3(Transform.Basis.X.X, 0, Transform.Basis.X.Z);
    }

    private int[] ConvertInputToDirection()
    //index 0 holds forward-backward, index 1 holds left-right
    //value of -1 means no movement
    {
        int[] returnMe = [-1, -1];

        if (Input.IsActionPressed("cam_move_forward") ^ Input.IsActionPressed("cam_move_backward"))
        {
            if (Input.IsActionPressed("cam_move_forward")) {returnMe[0] = 2;}
            else                                           {returnMe[0] = 0;}
        }
        if (Input.IsActionPressed("cam_move_left") ^ Input.IsActionPressed("cam_move_right"))
        {
            if (Input.IsActionPressed("cam_move_left")) {returnMe[1] = 1;}
            else                                        {returnMe[1] = 3;}
        }

        return returnMe;
    }
}

I should note, in case it isn’t apparent from this code, that none of the velocity override functionality has actually been implemented.

And another video demonstration:

As should hopefully be apparent from the debug logs, the entry and exit functions are being triggered either too early or too late. Some of them seem to be tied to the bottom of the wall, others to something I can’t figure out.

What’s the position and size of notifier’s bounding box?

The notifier’s bounding box is 62.0 m long by 1.0 m wide, with a height of 0.0. The positions of each wall’s notifiers are as follows:

  • North: (0, 6.498, -30.5)
  • West: (-31.5, 6.508, 0.5)
  • East: (31.52, 6.5030003, 0.5)
  • South: (-0.51, 6.502, 31.5)

Those numbers mean nothing by themselves. I meant visually, in respect to map geometry.

I’m sorry, but I’m not sure what information you’re asking for. Do you mean the notifiers’ XY position in the game window?

A screenshot/graphics that shows notifier bounding box in respect to level geometry.

Oh, I seem to have neglected to mention that the notifier is exactly concurrent with the yellow and black chevron texture on top of each wall. Sorry about that; do the two videos attached give enough information with that context?

Delete all but one notifier and determine if it works as expected.

No changes in behavior after adding this if statement to the notifier object’s _Ready function:

public override void _Ready()
{
    if (GlobalRotation.Y != -3.1415925f)
    {
        GetParent().QueueFree();
    }

    ScreenEntered += WhichEnteredScreen;
    ScreenExited += WhichExitedScreen;
    }

You need to make a minimal testing project and test if your notifier is working with the camera as expected. Maybe something else in the project is messing things up.

Btw don’t use operator == on floating point numbers.

I can try that, but I’m not sure how much more minimal I can make the current project, since all that exists right now is the camera, the notifiers, and a bunch of StaticBody3Ds that are the walls and floor.

There are only three scripts in the entire thing:

  • The one attached to the Camera3D
  • The one attached to the VisibleOnScreenNotifier3D
  • One attached to a sibling Node2D of the camera that shows the debug info in the top left.

But I think I just noticed something. It looks like the trigger area for the VisibleOnScreenNotifier3D uses a right triangle with the node itself as the hypotenuse. Like this:

The threshold for the trigger to be sent isn’t the yellow texture that overlays the notifier exactly, but the area outlined in red, which I’ve verifies goes in the other direction (up-right) as well. Is this intended behavior for it?

Parent some a box to every notifier that’s the same size as notifier’s bounding box, and check if they are where you expect them to be at runtime.

Sorry, but I’m not sure what node you’re recommending. I did try parenting a StaticBody3D with a MeshInstance3D to the object OwCamBound and gave it the exact same dimensions as the VisibleOnScreenNotifier3D, and it did appear where I expected it to at runtime.