Getting the area of the orthographic projection of a mesh

I’m trying to determine the area that an aircraft’s wing is pressured against air (usually the underside, but could theoretically be any side) in order to use the area in lift and drag calculations, and want the calculation to be as realistic as possible. You could think of the area that I’m trying to measure as the orthographic projection of a mesh from a specific viewing angle. If I could get the points from the mesh that are visible, as well as the projection of those points onto a plane, then I could calculate the area based on the triangles between those (now 2D) points… Problem is: I have no idea how I could possibly do this with Roblox’s MeshPart class.

You might wanna implement raycasting and maybe the model has mesh parts so create a script that can calculate the area but that can be something of a problem there. More context pls

I suppose a good way to illustrate the goal is by considering how the selection tool in Blender works when faces are set to solid (not wireframe) in orthographic mode. The selector only selects faces that are visible to the camera, and the image that you see as a result is those faces projected to a clipping plane. I want to implement a similar thing, just without actually using a camera. It’d be a flat projection of an arbitrary side of a mesh for later use to get the area of that side from it’s triangles.

You’ve been somewhat lucky that you’ve come across this problem now as getting mesh data like this wasn’t possible until recently. Well, not unless you were willing to import the mesh data as a constant somewhere, e.g. stored as a string in a module script.

Unfortunately EditableMesh is still in beta so you’ll have to turn this on in Studio to use it; and do note that you won’t be able to use this in a live game until Roblox announces that it’s live.

1. Getting mesh data

If you need it in a live experience you’ll have to import the data as a constant in something like a ModuleScript - the best option here will depend on your use case etc.

If you’re happy to wait for EditableMesh release, you can get the data by:

Note: The vertices of an EditableMesh are not in world-space, you would need to use CFrame:PointToWorldSpace(vertexPosition * scale + offset) if you needed to get the surface area scaled & relative to the world

  1. Create an EditableMesh instance from the MeshPart instances you want to project - announcement found here - which you can do via AssetService::CreateEditableMeshFromPartAsync, documentation here.

  2. Use the EditableMesh:GetVertices(), EditableMesh:GetTriangleVertices and EditableMesh:GetVertexNormal(vertexId: number) methods; found here, here and here respectively. This will give you the mesh’s vertices & indices, sadly you will have to iterate through each of the vertices to get the normal(s) though

  3. Cache these somewhere for each of the assets so you’re not doing this repeatedly and they can be used for other calculation(s)

2. Computing surface area

I’m assuming you’re wanting to calculate the surface normal for the lift?

This is less relevant to calculating drag, you’ll probs need to look into getting the cross-sectional area of the convex hull if you want to do that - if you get stuck here then feel free to send me a link of a #help-and-feedback:scripting-support post in the future and I can take a look.

If we’re only considering computing the surface area so we can calculate lift though:

2.1. Explanation

Right now we only have the vertices, the triangles and the normals but we need the normal of the face so that we can exclude/cull the faces that are facing away from the face we’re interested in.

To get the normals of each of the faces, we would so something like the following:

Face normals
--[=[
  compute the normal of the triangle

  @param           v0 Vector3 - the 1st vertex of the triangle
  @param           v1 Vector3 - the 2nd vertex of the triangle
  @param           v2 Vector3 - the 3rd vertex of the triangle
  @param vertexNormal Vector3 - the normal of the 1st vertex of the triangle

  @returns Vector3 - the triangle surface normal
]=]
local function computeTriangleNormal(v0, v1, v2, vertexNormal)
  local p0 = v1 - v0
  local p1 = v2 - v0
  local normal = p0:Cross(p1)

  if vertexNormal then
    -- we could also compute the average of
    -- the three vertex normals and use the
    -- following instead:
    --
    --    d = normal:Dot((na + nb + nc) / 3)
    --
    local d = normal:Dot(vertexNormal)
    normal = d < 0 and -normal or normal
  end

  return normal -- .Unit to normalise
end

Now that we have the normals of each of the triangle, we need to exclude any that aren’t facing towards us.

Since we want the bottom of the object, we would need to cull from the perspective of the object’s up vector, i.e. meshPart.CFrame.UpVector, to get the faces that are looking towards us.

We can do this by checking if the dot product of the triangle normal & the point-to-triangle is equal to or greater than zero, e.g.

Back-face culling
--[=[
  det. whether a triangle is facing away
  from our viewpoint

  @param  point Vector3 - the viewpoint position
  @param vertex Vector3 - the first vertex of the triangle
  @param normal Vector3 - the normal of the triangle

  @returns boolean - reflects the back-facing status of the triangle
]=]
local function isBackFacing(point, vertex, normal)
  return normal:Dot(vertex - point) >= 0
end

Finally, we can calculate the area of each of the remaining faces such that:

Triangle area
--[=[
  compute the area of the triangle

  @param v0 Vector3 - the 1st vertex of the triangle
  @param v1 Vector3 - the 2nd vertex of the triangle
  @param v2 Vector3 - the 3rd vertex of the triangle

  @returns number - the triangle area
]=]
local function computeArea(v0, v1, v2)
  local p0 = v1 - v0
  local p1 = v2 - v0
  return p1:Cross(p0).Magnitude * 0.5
end

2.2. Example

Condensed Example Code
--[=[
  compute the surface area of a given mesh for triangles
  that are facing towards the given NormalId

  @param     face       NormalId - NormalId enum _e.g._ `Enum.NormalId.Bottom`
  @param vertices table<Vector3> - the vertices array of the mesh, _e.g._ [Vector3, Vector3, ...]
  @param  indices  table<number> - the indices array that make up the mesh, _e.g._
                                   [tri1, tri1, tri1, tri2, tri2, tri2, ..., tri_n, tri_n, tri_n]

  @returns number - the triangle area
]=]
local function computeSurfaceArea(face, vertices, indices)
  local direction = -Vector3.fromNormalId(face)

  local area = 0
  for i = 0, #indices - 1, 3 do
    local v0 = vertices[indices[i + 1]]
    local v1 = vertices[indices[i + 2]]
    local v2 = vertices[indices[i + 3]]

    local p0 = v1 - v0
    local p1 = v2 - v0
    local normal = p1:Cross(p0)

    local d = normal:Dot(direction)
    if d >= 0 then
      area += d
    end
  end

  return 0.5*area
end

3. Projecting onto a plane

Unsure if this is actually necessary for your use case but after culling the triangles of the mesh, you could project the vertices of each of the triangles onto the plane like so:

Project a vector onto a plane
local function projectOnPlane(vec, normal)
  local m = normal:Dot(normal)
  if m < 1e-6 then
    return vec
  end

  local d = vec:Dot(normal)
  return vec - normal*d / m
end

The issue then becomes how you would calculate the area. You could compute the area of the triangles as above but I would imagine you would yield better results if you first computed the convex hull of the points and then calculated the area of the resulting hull.

3 Likes

Whoa thank you! This is amazing and will really help when EditableMesh releases into public beta! Wonderfully formatted!

1 Like

Apologies for what might seem like a random bump, but I forgot to mention something important which may have gotten you a bit stuck if you hadn’t already noticed yourself!

In 3. Projecting onto a Plane I mentioned that you could project the vector onto the plane using the vector and the plane normal.

This is the case but I should have noted that the plane that you would be projecting the vector onto here would pass through the origin of the world, i.e. Vector3.new(0, 0, 0), but I doubt this is behaviour you would be expecting and/or wanting for your application here.

Instead, you probably want to get the closest point on the plane, e.g.

1. Deriving the plane from 3 points
--[=[
  create a plane from 3 points that lie within it

    NOTE:
      These points should be clockwise from the perspective
      of viewing the surface of the plane.

      If you need to do this programmatically, you can
      compute the centroid of the plane and get the polar angle
      of each of the points relative to the perpendicular axis
      of the plane and then sort them such that:

      ```lua
      table.sort(function (a, b)
        local a0 = getPolarAngle(a - centroid, perpendicularPlaneDirection)
        local a1 = getPolarAngle(b - centroid, perpendicularPlaneDirection)
        return a0 > a1
      end)
      ```

    Learn more:
      - Plane implementation: https://gdbooks.gitbooks.io/3dcollisions/content/Chapter1/plane.html
      - Plane interaction demo: https://www.wolframalpha.com/examples/mathematics/geometry/plane-geometry
      - Plane from 3 points demo: https://demonstrations.wolfram.com/ThreePointsDetermineAPlane/

  @param a Vector3 - some world-space position
  @param b Vector3 - some world-space position
  @param c Vector3 - some world-space position

  @returns
    Vector3 - the plane normal vector
    number  - the distance from the world origin to the plane
    point   - the plane's origin

]=]

local function planeFromVectors(a, b, c)
  local normal = (b - a):Cross(c - a).Unit
  return normal, -normal:Dot(a), a:Lerp(b, 0.5):Lerp(c, 0.5)
end
2. Closest point on some arbitrary plane from some arbitrary vector
--[=[
  compute the closest point on some arbitrary plane

    NOTE:

      Since we already have the distance above we can skip some steps,
      but if you only had the plane origin + normal, you would do something
      like the following:

      ```lua
      local function closestPointOnPlane(vec, planeOrigin, planeNormal)
        local displacement = planeOrigin - vec
        local distanceFromPlane = displacement:Dot(planeNormal)
        return vec + normal*distanceFromPlane
      end
      ```

      Learn more:

        - Why pont-plane distance is the closest point:
            Ref @ https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_plane#Why_this_is_the_closest_point

        - Point-Plane Distance:
            Ref @ https://mathworld.wolfram.com/Point-PlaneDistance.html

  @param vec           Vector3 - some world-space position
  @param planeNormal   Vector3 - the plane normal vector
  @param planeDistance  number - the distance from the world origin to the plane

  @returns Vector3 - the closest point on the given plane

]=]
local function closestPointOnPlane(vec, planeNormal, planeDistance)
  local d = planeNormal:Dot(vec) + planeDistance
  return vec - planeNormal*d
end

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