How to get cube face visibility relative to a point?

I have a cube, with rotation quaternion.identity,

lets say the cube is sitting at position (2,0,2). I then have point A at position (2,0,-2), and point B at position (17,0,-2).

My goal is to determine to which degree a given face of the cube is visible from a point, 2 of these possible points are points A and B. From point A the front face of the cube is directly alligned with point A, so there should be a variable that indicates that the front face is dead center visible from point A, and no other faces are visible. On the other hand, from point B the situation is different, both the front face and the left face of the cube are visible, but to different degrees of visibility.

What is the best method to create this evaluation?

After this, how can I then account for the cube to have a different rotation than quaternion.indentity, and have the world rotation of the cube taken into account for the calculations?

My endgoal is to get the (relative angle of the cube)/(amount of each face visible) from a given vector 3 position.

Here is an image to try to express what im trying to detect:

here from point A, I expect that only the front face of the cube (red) to be fully visible, and be the only visible face, while from point b, both the front and left(blue) face of the cube should be visible, to different degrees of visibility.

Thanks in advance

Im sure mathematically you can determine this. so, again assuming cube as described, you can tell point in relative from the center of the cube, so using human angles. If at 0,0,0 you are looking down on the cube, if any are 180 you are somewhere directly under the cube…

Probably just a case for using Dot product: Unity - Scripting API: Vector3.Dot

With respect to a the camera’s forward direction, and a direction leaving perpendicular to each face, you should get a result of less than zero if it’s facing towards the camera, and greater than zero if it’s facing away from the camera.

If you only care about comparing to a cube, I would just put an empty on each face with it’s forward direction facing outwards.

1 Like

hmmm well, truly i think what really matters to me is the rotation of the cube relative to the point that is observing the cube.

I only made it a cube because its easy to observe the state of the rotation by looking at the model by placing the camera at the desired point, to debug if the code was behaving as it should, but I was now trying to translate it to numbers.

What I mean I guess is that if the cube was so small that the faces were almost in the same position in theory I would expect the same results as if the cube was gigantic and the distance between each face was large

To determine cube face visibility relative to a point, consider the point’s position and cube orientation. By applying similar logical methods, GPA to CGPA calculator adjustments can be easier.

Here you say you have a cube, and yet in that other thread you claimed you don’t have a cube, that it’s just an simplified illustration for a more general problem.

Look, the general idea behind this is what I already answered there, however if you are working with a cube, the solution is incredibly simple and boils down to finding dots with cube surface normals.

This sentence contains a horrible problem as well. “Relative angle of the cube” is not the same as “amount of each face visible”. The latter can be a very hard problem because it involves knowing the absolute render area which depends on the distance from the object, camera’s field of view, and most importantly the skewness of camera’s direction against the center of the object. That said, if the object is known to be a cube, you can somewhat approximate the areas if you know the surface normals’ angles.

Again, as I explained in the other thread, you can compute the relative angle of the cube if you obtain the difference between two orientations, where one is the orientation (world-space rotation) of the cube, and the other corresponds to the orientation of the camera as if the target object was in the origin.

With this difference (represented with a quaternion) you can use the Unity’s API to obtain per-axis Euler angles from it.

If you need this angle to be expressed on a single plane (say XZ), all you need to do is to project the camera’s direction onto this plane, which is as easy as

Vector3 projectDirectionToXZ(Vector3 dir) => new(dir.x, 0f, dir.z).normalized;

Unfortunately, I really really don’t have time right now to make you a useful prototype, but rest assured that this solution is very easy to implement and modify.

Once more, if you do have a cube, you can obtain the cube’s front surface normal simply by taking cube.transform.forward, and then you dot this against camera.transform.forward, and if the result is close to -1 the two are looking at each other (aka lie anti-parallel; this is because a\cdot b=\|a\|\|b\| \cos(\theta) and because directions are unit vectors {\cos(180^\circ)=-1}). You can very easily detect the most dominant face (of a cube) by doing this

static Vector3[] _n;

Vector3 getDominantNormal(Vector3 lookDir) {
  _n??= new Vector3[6] {
    Vector3.right, Vector3.up, Vector3.forward,
    -Vector3.right, -Vector3.up, -Vector3.forward
  };

  var smallest = float.PositiveInfinity;
  int index = -1;

  for(int i = 0; i < _n.Length; i++) {
    var d = Vector3.Dot(_n[i], lookDir);

    if(smallest > d) {
      smallest = d;
      index = i;
    }
  }

  return _n[index];
}

It’s a sloppy example, could be much more streamlined, but should get the work done.
Similarly you can get all faces that are visible by camera

static Vector3[] _n;

int getListOfVisibleFaces(List<Vector3> results, Vector3 lookDir) {
  _n??= new Vector3[6] {
    Vector3.right, Vector3.up, Vector3.forward,
    -Vector3.right, -Vector3.up, -Vector3.forward
  };

  results.Clear();

  for(int i = 0; i < _n.Length; i++) {
    var d = Vector3.Dot(_n[i], lookDir);
    if(d < 0f) results.Add(_n[i]);
  }

  return results.Count;
}

make sure to instantiate the results list before calling, i.e.

var list = new List<Vector3>();
if(getListOfVisibleFaces(list, camera.transform.forward) > 0) {
  // do something with list elements
}

If you also have a cube that is rotating freely, then do this

static Vector3[] _n;

int getListOfVisibleFaces(List<Vector3> results, Vector3 lookDir, Quaternion? cubeRotation = null) {
  _n??= new Vector3[6] {
    Vector3.right, Vector3.up, Vector3.forward,
    -Vector3.right, -Vector3.up, -Vector3.forward
  };

  results.Clear();
  var rot = cubeRotation.HasValue? cubeRotation.Value : Quaternion.identity;

  for(int i = 0; i < _n.Length; i++) {
    var d = Vector3.Dot(rot * _n[i], lookDir);
    if(d < 0f) results.Add(_n[i]);
  }

  return results.Count;
}
var list = new List<Vector3>();
if(getListOfVisibleFaces(list, camera.transform.forward, cube.transform.rotation) > 0) {
  // do something with list elements
}

Edit:
fixed typo in the first example

1 Like

To add some more thought on this, here is in fact a neat area computation technique.
You can project triangle edges to an arbitrary frustum plane. To achieve this you can use ProjectOnPlane where you take a plane defined via normal -camera.transform.forward.

A triangle edge is just a vector between any two successive vertices, and you must follow the exact order (i.e. if triangle was defined by indices 8, 3, 11, its edges are 8->3, 3->11, and 11->8; to come up with vectors you substitute the indice with the associated vector data, but their order is very important to determine whether the triangle is visible or not).

In practice, you only ever need two edges for each mesh triangle. After you’ve projected them, you can take a cross, and if the result is pointing away from the camera you simply ignore the triangle (it is facing away). Mathematically this is the case when Vector3.Dot(Vector3.Cross(edge1, edge2), -camera.transform.forward) > 0f.

If not, you take the magnitude of the cross result and divide that by 2. This is the visible triangle’s area, relative to all other triangles.

If you want the area to be expressed in exact units like pixels squared or whatever, more work is required to find a good factor by which to scale the whole thing, I can’t do that from my head alone. In that case, you’d probably want to project the triangles to screen space, and have the areas computed relative to the screen, which has some known area.

var camBackDir = -camera.transform.forward;
var totalArea = 0f;

foreach(var tri in triangles) { // as a pseudo-code, you need to do this differently
  var edge1 = verts[tri[1]] - verts[tri[0]];
  var edge2 = verts[tri[2]] - verts[tri[0]];

  edge1 = Vector3.ProjectOnPlane(edge1, camBackDir);
  edge2 = Vector3.ProjectOnPlane(edge2, camBackDir);
  
  var cross = Vector3.Cross(edge1, edge2);
  var area = Vector3.Dot(cross, camBackDir) > 0f? cross.magnitude / 2f : 0f;

  totalArea += area;
}

Note: More work is needed to determine whether a triangle is occluded by another or clipped by a frustum plane. This is just a rough estimate that is perhaps useful if you want to determine the most or least prominent triangle after making sure that the object is truly in sight.

2 Likes

thanks a lot for all the all the extremely thought out solution

in the end I found that this code worked out well so far:

   private float AngleFromCubeToCam(Vector3 cubeFwdVec, Vector3 cubeUpVec, Vector3 camFwdVec)
    {
     
Vector3 projectedCamDir = Vector3.ProjectOnPlane(camFwdVec, cubeUpVec);

float angle = Vector3.SignedAngle(cubeFwdVec, projectedCamDir, cubeUpVec);

        return angle;
    }

It appears to work correctly but seems very simple compared to your more avanced check, could I get your opinion on it and if it seems like it will lead to problems in the future?

it ignores the topface and bottom face of the cube as intended and only responds to the XZ plane faces

Thanks again for sharing your vast knowledge

This here is a problematic version of this

Vector3 projectDirectionToXZ(Vector3 dir) => new(dir.x, 0f, dir.z).normalized;

Namely because a projection won’t return a proper direction (which has to be a unit vector). You have to at least normalize it.

Other than that, if you just want the signed angle between the two directions (as projected on the XZ plane), that’s fine.

I mean there are half a dozen ways to do this, because the angle in 2D can be easily found with arc tangent. I think Kurt mentioned it before.

For example, taking arc tangent from the so-called perp-dot product between two directions in 2D should give you a signed angle.

float signedAngle(Vector2 a, Vector2 b) // returns angle in radians
  => MathF.Atan2(a.x * b.y - a.y * b.x, a.x * b.x + a.y * b.y); // MathF is a System library, not Unity's

This is also the cheapest way to achieve this.
In your particular case, there are a couple of things to add here. First, you want to flatten your camera’s backward direction to XZ plane.

Vector2 flattenDirXZ(Vector3 vec) => new(vec.x, vec.z).normalized;

Next you want to incorporate cube’s rotation, so you wouldn’t use world cardinal directions for the cube, but use the transformed local directions. And finally you want to convert the result to degrees, which should be between -180 and +180.

var camBackward = flattenDirXZ(-camera.transform.forward);
var cubeForward = flattenDirXZ(cube.transform.forward);
var sa = signedAngle(cubeForward, camBackward);
Debug.Log($"Angle = {sa * Mathf.Rad2Deg} degrees");

Edit:
Also I forgot to mention that, in your code, the cube’s up vector is used like a plane normal, so by rotating the cube on the X or Z axes, you also rotate the base plane, and I’m not sure if that’s something you want to happen. Yes, you’ll get the angle on the 4 lateral sides (around the cube’s Y axis), but I can also imagine a scenario where you want to use a plane that is common to both the cube and the camera.

In that case the plane’s normal would be defined by (cube.transform.position - cam.transform.position).normalized (assuming that the cube’s pivot is in its center), and then you get a vector that is perpendicular to both this vector and a middle vector between two directions (cube.transform.forward - cam.transform.forward).normalized.

It’s complicated for me to explain this with words alone. But anyway
here’s the previous example as a function

float AngleFromCubeToCam(Vector3 cubeFwdDir, Vector3 cameraFwdDir) {
  var camBackward = flatten(-cameraFwdDir);
  var cubeForward = flatten(cubeFwdDir);
  return signedAngle(ref cubeFwdDir, ref cameraFwdDir) * Mathf.Rad2Deg;

  static Vector2 flatten(Vector3 vec) => new(vec.x, vec.z).normalized;
  static float signedAngle(ref Vector2 a, ref Vector2 b) // in radians
    => System.MathF.Atan2(a.x * b.y - a.y * b.x, a.x * b.x + a.y * b.y);
}

If you really want to have the plane rotate with the cube, then your code is fine (when you normalize the projection).

1 Like