Center of gravity coordinates: definition, formulas and applications

Barycentric Coordinates are particularly important in CG. They have some functionality and are key to the next ray triangle intersection algorithm proposed by Möller-Trumbore, which will be studied in the next chapter. The chapter concludes with a discussion of how to use barycentric coordinates in CG.

NSDT tool recommendationThree.js AI texture development kit - YOLO synthetic data generator - GLTF/GLB online editing - 3D model format online conversion - Programmable 3D scene editor -  REVIT export 3D model plug-in - 3D model semantic search engine

1. Definition of center of gravity coordinates

Figure 1: The barycentric coordinates can be viewed as the areas of the subtriangles CAP(u), ABP(v) and BCP(w) on the triangle ABC, which is why they are also called area coordinates.

The barycentric coordinates can use three scalars to represent the position of any point located on the triangle. The location of the point includes any location within the triangle, any location on any three sides of the triangle, or any of the three vertices themselves. To calculate the position of this point using barycentric coordinates, we use the following equation (1):

where A, B and C are the vertices of the triangle, u, v and w are the coordinates of the center of gravity, and the three real numbers satisfy u+v+w=1 . Note that from two of the coordinates we can find the third coordinate: w=1-u-v . From this we can determine
u+v<=1 (we will use this simple but important property later).

Equation 1 defines the position of point P on the plane of the triangle formed by vertices A, B, and C. If the condition 0<=u, v , w <=1 is met, then the point lies within the triangle (A, B, C). If any of the coordinates is less than 0 or greater than 1, the point is outside the triangle. If either of these is zero, then P lies on one of the lines connecting the vertices of the triangle.

You can also simply use two coordinates (let's say u and v) to represent the coordinates of point P in a two-dimensional coordinate system defined by the origin (A) and the sides AB and AC (much like in A 2D point represented in an intersection coordinate system) A two-dimensional coordinate system defined by the x-axis and y-axis. The only difference in our case is that AB and AC are not necessarily orthogonal, and the origin of this coordinate system is A). Positions within a triangle can be defined using the equation P=A+u*AB+v*AC where u>=0, v>=0, u+v<=1 . This equation can be understood as:

Start at A, move a little toward AB, then a little toward AC, and you'll find P.

Now, if you build this equation, you can write:

This equation is similar to Equation 1, except that when w=1-u-v , we get the formula P=w*A+u*B+v*C instead of P=u*A+v*B+w*C.

2. Calculation method of center of gravity coordinates

The center of gravity coordinates are also called area coordinates (Areal Coordinates). Although less commonly used, this term means that the coordinates u, v, and w are proportional to the areas of the three subtriangles defined by P, the points lying on the triangle, and the vertices of the triangle (A, B, C). These three sub-triangles are represented as ABP, BCP and CAP respectively (see Figure 1).

This leads to the formula for calculating the coordinates of the center of gravity:

For the remainder of this lesson, we will assume that u moves us along edge AB (if u=1 then P=B) and v moves us along edge AC (if v=1 then P=C). That's why we use the area of ​​subtriangle CAP to calculate u and the area of ​​subtriangle ABP to calculate v. This is the convention followed by most people in the CG programming community on how to use barycentric coordinates to interpolate vertex data, and you will have a visual example (Figure 3) to better understand this.

Figure 2: To calculate the area of ​​a triangle, we start with the formula for calculating a parallelogram (base times height H). To calculate H, we multiply the length of vector AC by sin(theta)

Calculating the area of ​​a triangle is easy. If you copy the triangle and mirror it along its longest side, you will get a parallelogram. To find the area of ​​a parallelogram, simply calculate its base and sides, then multiply those two numbers by sin(theta), where theta is the angle subtended by vectors AB and AC (Figure 2). To create a parallelogram, we use two triangles, so the area of ​​one triangle is half the area of ​​the parallelogram.

With this, it becomes easy to calculate u and v, w is calculated from u and v, as mentioned before:

To make things easier, we can exploit the fact that in order to find the intersection point P: the areas of the subtriangles ABP, BCP and CAP are proportional to the length of the cross product we calculated in the previous chapter. This is one of the properties of the cross product: the size of the cross product can be interpreted as the area of ​​the parallelogram. Therefore, we do not need to explicitly calculate the previous formula, which includes the calculation of sin(theta). We can simply use:

Note that in mathematical terms, the double bar symbol (|| ||) means "length" (Lesson 4). In other words, we need to calculate the length of the vector produced by the cross product (C-B)x(P-B) . We know how to calculate the cross product of two vectors and the length of a vector, so we now have everything we need to calculate the barycentric coordinates of the intersection:

bool rayTriangleIntersect(
    const Vec3f &orig, const Vec3f &dir,
    const Vec3f &v0, const Vec3f &v1, const Vec3f &v2,
    float &t, float &u, float &v)
{
    // compute the plane's normal
    Vec3f v0v1 = v1 - v0;
    Vec3f v0v2 = v2 - v0;
    // no need to normalize
    Vec3f N = v0v1.crossProduct(v0v2); // N
    float area = N.length() / 2; // area of the triangle

    ...
 
    // Step 2: inside-outside test
    Vec3f C; // vector perpendicular to triangle's plane
 
    // edge 0
    ...
 
    // edge 1
    Vec3f edge1 = v2 - v1; 
    Vec3f vp1 = P - v1; 
    C = edge1.crossProduct(vp1);
    u = (C.length() / 2) / area;
    if (N.dotProduct(C) < 0)  return false; // P is on the right side
 
    // edge 2
    Vec3f edge2 = v0 - v2; 
    Vec3f vp2 = P - v2; 
    C = edge2.crossProduct(vp2);
    v = (C.length() / 2) / area;
    if (N.dotProduct(C) < 0) return false; // P is on the right side;

    return true; // this ray hits the triangle
}

Plane normals should not be normalized since we use the length of the vector to calculate the triangle area.

3. Application of barycenter coordinates in vertex data interpolation

Barycentric coordinates are most useful in shading. A triangle is a flat surface to which we can associate any additional information or data (points, colors, vectors, etc.) to each of its vertices. This information is often called vertex data. For example, let's say you want vertex A to be red, vertex B to be green, and vertex C to be blue:

Figure 3: Centroid coordinates can be used to insert vertex data at the hit point location. For example, in this case we use the vertex color to calculate the color at P.

If the intersection coincides with one of the triangle's vertices, the color of the object at the intersection is the color associated with that vertex. Simple enough. The question is what happens when the ray intersects the triangle anywhere else, whether on an edge or inside the triangle? If the position of a point located on a triangle is calculated from the triangle vertices using barycentric coordinates, we can insert in the same way any other data (such as color) defined at the triangle vertices. In other words, barycentric coordinates are used to interpolate vertex data on the triangle surface (this technique can be applied to any data type, floats, colors, etc.). This technique is useful for shading, such as interpolating normals at intersection points. An object's normals can be defined on a per-face or vertex basis (what we call face normals or vertex normals). If they are defined per vertex, we can use this interpolation technique to simulate smooth shading of the triangle's surface, even if the triangle is "mathematically" flat (the normal at the hit point is a combination of the vertex normals, so if the vertex normal lines differ from each other, the result of this interpolation is not constant over the triangular surface):

// vertex position
Vec3f triVertex[3] = {
   
   {-3,-3,5}, {0,3,5}, {3,-3,5}};
// vertex data
Vec3f triColor[3] = {
   
   {1,0,0}, {0,1,0}, {0,0,1}};
if (rayTriangleIntersect(...)) {
    // compute pixel color
    // col = w*col0 + u*col1 + v*col2 where w = 1-u-v
    Vec3f PhitColor = u * triColor[0] + v * triColor[1] + (1 - u - v) * triColor[2];
}

The center of gravity coordinates are also used to calculate texture coordinates (we will look at this in the textures course).

4. Optimize the calculation of center of gravity coordinates

You'll notice that in the version of the algorithm we've described so far, we compute u and v using the cross product of AB and AP and the cross product of CA and CP. But if you look at the code you'll notice again that we've calculated these cross products for the inner-outer tests. They are used to calculate whether P is to the right or left of edge0 (ABxAP) and edge2 (CAxCP). Of course, the first optimization involves reusing the results of these values. Also note (Equation 2):

There is no need to calculate the area of ​​the triangle because the ratio between triangle ABP and triangle ABC is the same as the ratio between parallelogram ABP (twice the area of ​​triangle ABP) and parallelogram ABC (twice the area of ​​triangle ABP), so we Dividing by 2 can also be avoided. The code becomes:

bool rayTriangleIntersect(
    const Vec3f &orig, const Vec3f &dir,
    const Vec3f &v0, const Vec3f &v1, const Vec3f &v2,
    float &t, float &u, float &v)
{
    // compute the plane's normal
    Vec3f v0v1 = v1 - v0;
    Vec3f v0v2 = v2 - v0;
    // no need to normalize
    Vec3f N = v0v1.crossProduct(v0v2); // N
    float area2 = N.length();
 
    // Step 1: finding P
    
    // check if the ray and plane are parallel.
    float NdotRayDirection = N.dotProduct(dir);
    if (fabs(NdotRayDirection) < kEpsilon) // almost 0
        return false; // they are parallel so they don't intersect! 

    // compute d parameter using equation 2
    float d = -N.dotProduct(v0);
    
    // compute t (equation 3)
    t = -(N.dotProduct(orig) + d) / NdotRayDirection;
    // check if the triangle is behind the ray
    if (t < 0) return false; // the triangle is behind
 
    // compute the intersection point using equation 1
    Vec3f P = orig + t * dir;
 
    // Step 2: inside-outside test
    Vec3f C; // vector perpendicular to triangle's plane
 
    // edge 0
    Vec3f edge0 = v1 - v0; 
    Vec3f vp0 = P - v0;
    C = edge0.crossProduct(vp0);
    if (N.dotProduct(C) < 0) return false; // P is on the right side
 
    // edge 1
    Vec3f edge1 = v2 - v1; 
    Vec3f vp1 = P - v1;
    C = edge1.crossProduct(vp1);
    u = C.length() / area2;
    if (N.dotProduct(C) < 0)  return false; // P is on the right side
 
    // edge 2
    Vec3f edge2 = v0 - v2; 
    Vec3f vp2 = P - v2;
    C = edge2.crossProduct(vp2);
    v = C.length() / area2;
    if (N.dotProduct(C) < 0) return false; // P is on the right side;

    return true; // this ray hits the triangle
}

Finally, we can prove (Equation 3):

First note that N=ABxAC, so the above equation can be rewritten as:

We now need to prove that this equation is correct. Remember from geometry classes that the dot product of two vectors can be interpreted as:

The angle theta is the angle between the two vectors A and B, ||A|| and ||B|| are the lengths of the vectors A and B respectively. We also know that when two vectors A and B are collinear (they point in the same direction), the opposite angle is 0, so cos(0)=1. We can rewrite the numerator as:

Since ABxAP=A and ABxAC=N=B, then:

In this case, A and B are also collinear because they are constructed from coplanar vectors. Finally, we can replace the result with the numerator and denominator in Equation 3:

If we replace A back to ABxAP and B back to ABxAC, we get:

Here is the final version of our routine code, including an optimized method for calculating barycentric coordinates:

bool rayTriangleIntersect(
    const Vec3f &orig, const Vec3f &dir,
    const Vec3f &v0, const Vec3f &v1, const Vec3f &v2,
    float &t, float &u, float &v)
{
    // compute the plane's normal
    Vec3f v0v1 = v1 - v0;
    Vec3f v0v2 = v2 - v0;
    // no need to normalize
    Vec3f N = v0v1.crossProduct(v0v2); // N
    float denom = N.dotProduct(N);
    
    // Step 1: finding P
    
    // check if the ray and plane are parallel.
    float NdotRayDirection = N.dotProduct(dir);
    if (fabs(NdotRayDirection) < kEpsilon) // almost 0
        return false; // they are parallel so they don't intersect! 

    // compute d parameter using equation 2
    float d = -N.dotProduct(v0);
    
    // compute t (equation 3)
    t = -(N.dotProduct(orig) + d) / NdotRayDirection;
    // check if the triangle is behind the ray
    if (t < 0) return false; // the triangle is behind
 
    // compute the intersection point using equation 1
    Vec3f P = orig + t * dir;
 
    // Step 2: inside-outside test
    Vec3f C; // vector perpendicular to triangle's plane
 
    // edge 0
    Vec3f edge0 = v1 - v0; 
    Vec3f vp0 = P - v0;
    C = edge0.crossProduct(vp0);
    if (N.dotProduct(C) < 0) return false; // P is on the right side
 
    // edge 1
    Vec3f edge1 = v2 - v1; 
    Vec3f vp1 = P - v1;
    C = edge1.crossProduct(vp1);
    if ((u = N.dotProduct(C)) < 0)  return false; // P is on the right side
 
    // edge 2
    Vec3f edge2 = v0 - v2; 
    Vec3f vp2 = P - v2;
    C = edge2.crossProduct(vp2);
    if ((v = N.dotProduct(C)) < 0) return false; // P is on the right side;

    u /= denom;
    v /= denom;

    return true; // this ray hits the triangle
}

5. Source code

In this version of the ray-triangle intersection method, we implement the technique described in Chapter 3 to calculate the hit point coordinates, and the method described in this chapter to calculate the barycentric coordinates of the intersection point. When a ray hits a triangle, you can choose to interpolate the three colors using barycentric coordinates or visualize the original coordinates directly (lines 94/95).

...
constexpr float kEpsilon = 1e-8;

inline
float deg2rad(const float °)
{ return deg * M_PI / 180; }

inline
float clamp(const float &lo, const float &hi, const float &v)
{ return std::max(lo, std::min(hi, v)); }

bool rayTriangleIntersect(
    const Vec3f &orig, const Vec3f &dir,
    const Vec3f &v0, const Vec3f &v1, const Vec3f &v2,
    float &t, float &u, float &v)
{
    // compute the plane's normal
    Vec3f v0v1 = v1 - v0;
    Vec3f v0v2 = v2 - v0;
    // no need to normalize
    Vec3f N = v0v1.crossProduct(v0v2); // N
    float denom = N.dotProduct(N);
    
    // Step 1: finding P
    
    // check if the ray and plane are parallel.
    float NdotRayDirection = N.dotProduct(dir);
    if (fabs(NdotRayDirection) < kEpsilon) // almost 0
        return false; // they are parallel so they don't intersect! 

    // compute d parameter using equation 2
    float d = -N.dotProduct(v0);
    
    // compute t (equation 3)
    t = -(N.dotProduct(orig) + d) / NdotRayDirection;
    // check if the triangle is behind the ray
    if (t < 0) return false; // the triangle is behind
 
    // compute the intersection point using equation 1
    Vec3f P = orig + t * dir;
 
    // Step 2: inside-outside test
    Vec3f C; // vector perpendicular to triangle's plane
 
    // edge 0
    Vec3f edge0 = v1 - v0; 
    Vec3f vp0 = P - v0;
    C = edge0.crossProduct(vp0);
    if (N.dotProduct(C) < 0) return false; // P is on the right side
 
    // edge 1
    Vec3f edge1 = v2 - v1; 
    Vec3f vp1 = P - v1;
    C = edge1.crossProduct(vp1);
    if ((u = N.dotProduct(C)) < 0)  return false; // P is on the right side
 
    // edge 2
    Vec3f edge2 = v0 - v2; 
    Vec3f vp2 = P - v2;
    C = edge2.crossProduct(vp2);
    if ((v = N.dotProduct(C)) < 0) return false; // P is on the right side;

    u /= denom;
    v /= denom;

    return true; // this ray hits the triangle
}

int main(int argc, char **argv)
{
    Vec3f v0(-1, -1, -5);
    Vec3f v1( 1, -1, -5);
    Vec3f v2( 0,  1, -5);
    
    const uint32_t width = 640;
    const uint32_t height = 480;
    Vec3f cols[3] = {
   
   {0.6, 0.4, 0.1}, {0.1, 0.5, 0.3}, {0.1, 0.3, 0.7}};
    Vec3f *framebuffer = new Vec3f[width * height];
    Vec3f *pix = framebuffer;
    float fov = 51.52;
    float scale = tan(deg2rad(fov * 0.5));
    float imageAspectRatio = width / (float)height;
    Vec3f orig(0);
    for (uint32_t j = 0; j < height; ++j) {
        for (uint32_t i = 0; i < width; ++i) {
            // compute primary ray
            float x = (2 * (i + 0.5) / (float)width - 1) * imageAspectRatio * scale;
            float y = (1 - 2 * (j + 0.5) / (float)height) * scale;
            Vec3f dir(x, y, -1);
            //cameraToWorld.multDirMatrix(Vec3f(x, y, -1), dir);
            dir.normalize();
            float t, u, v;
            if (rayTriangleIntersect(orig, dir, v0, v1, v2, t, u, v)) {
                *pix = u * cols[0] + v * cols[1] + (1 - u - v) * cols[2];
                //*pix = Vec3f(u, v, 1 - u - v);
            }
            pix++;
        }
    }
    
    // Save the result to a PPM image (keep these flags if you are on Windows)
    ...

    return 0;
}

The following image shows the output of the program:


Original link:A brief tutorial on center of gravity coordinates - BimAnt

Guess you like

Origin blog.csdn.net/shebao3333/article/details/134907417