<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. 3D geometry and solids
All lessons in this course

Lesson 37 of 44 14 minAdvanced

Volumes and surface areas of solids

Measure any closed mesh: surface area from its triangles, volume and centre of mass from signed tetrahedra, and why the result goes wrong for open meshes or coordinates far from the origin.

Quantity take-off turns a model into numbers people pay for: cubic metres of concrete, square metres of plasterboard and paint, the air volume of a room for ventilation. For simple shapes there are formulas, but an element with openings, chamfers and sloped faces needs a method that works for any closed mesh.

Surface area

This part is easy. A triangle’s area is half the length of the cross product of two of its edges, from the cross product lesson, so a mesh’s surface area is Σ ½ |(b − a) × (c − a)| over its triangles. For a paint quantity you would add only the faces you paint, for example those whose normal is close to horizontal for walls.

Volume from signed tetrahedra

Pick any point O. Each triangle together with O forms a tetrahedron, whose volume is (a − O) · ((b − O) × (c − O)) / 6. Because the triangles are wound counter-clockwise from outside, this volume is positive when the triangle faces away from O and negative when it faces towards it. Add up all of them and the parts outside the solid cancel exactly, leaving its volume.

Interactive

Volume from signed tetrahedra

O
O(1, 1.5, 1.2)
this tetrahedron+2.600 m³
sum of the + ones30.000 m³
sum of the − ones0.000 m³
total volume30.000 m³
by formula4 × 3 × 2.5 = 30
surface area59.00 m²
Triangle 2 faces away from O, so its tetrahedron counts as positive. Move O anywhere, even outside the solid: the parts outside cancel, and the total stays 30.000 m³.

Move O outside the box in the widget: some tetrahedra turn negative and the positive ones grow, but the total does not change. This is the 3D version of the shoelace formula from the polygon area lesson, and both are cases of the divergence theorem.

MeshVolume.cs
var shapes = new (string Name, Mesh M)[]{    ("box 4 x 3 x 2.5", Mesh.Box(4, 3, 2.5)),    ("L-shaped room", Mesh.Prism(new[] { (0.0, 0.0), (4.0, 0.0), (4.0, 2.0), (2.0, 2.0), (2.0, 4.0), (0.0, 4.0) }, 3)),    ("pyramid", Mesh.Pyramid(4, 3))}; foreach (var (name, m) in shapes)    Console.WriteLine($"{name}: area {Area(m):0.###} m2, volume {Volume(m, new V3(0, 0, 0)):0.###} m3, from O = (50, -20, 7): {Volume(m, new V3(50, -20, 7)):0.###} m3"); static double Area(Mesh m) => m.F.Sum(t => m.Normal(t).Length / 2); static double Volume(Mesh m, V3 o) =>    m.F.Sum(t => V3.Dot(m.V[t.A] - o, V3.Cross(m.V[t.B] - o, m.V[t.C] - o)) / 6); sealed class Mesh{    public readonly List<V3> V = new();    public readonly List<(int A, int B, int C)> F = new();   // counter-clockwise seen from outside     public static Mesh Box(double lx, double ly, double lz) =>        Prism(new[] { (0.0, 0.0), (lx, 0.0), (lx, ly), (0.0, ly) }, lz);     // A counter-clockwise footprint extruded up by h (floor and roof fanned from corner 0)    public static Mesh Prism((double X, double Y)[] poly, double h)    {        var m = new Mesh();        int n = poly.Length;        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, 0));        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, h));        for (int k = 1; k < n - 1; k++)        {            m.F.Add((0, k + 1, k));                 // floor, facing down            m.F.Add((n, n + k, n + k + 1));         // roof, facing up        }        for (int i = 0; i < n; i++)        {            int j = (i + 1) % n;            m.F.Add((i, j, j + n));                 // each wall as two triangles            m.F.Add((i, j + n, i + n));        }        return m;    }     public static Mesh Pyramid(double b, double h)    {        var m = new Mesh();        m.V.AddRange(new V3[] { new(0, 0, 0), new(b, 0, 0), new(b, b, 0), new(0, b, 0), new(b / 2, b / 2, h) });        m.F.AddRange(new[] { (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (0, 2, 1), (0, 3, 2) });        return m;    }     // (b - a) x (c - a): points out of the solid, and its length is twice the triangle's area    public V3 Normal((int A, int B, int C) t) => V3.Cross(V[t.B] - V[t.A], V[t.C] - V[t.A]);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
box 4 x 3 x 2.5: area 59 m2, volume 30 m3, from O = (50, -20, 7): 30 m3
L-shaped room: area 72 m2, volume 36 m3, from O = (50, -20, 7): 36 m3
pyramid: area 44.844 m2, volume 16 m3, from O = (50, -20, 7): 16 m3

With O at the origin the formula becomes Σ a · (b × c) / 6, which is how it is usually written. The pyramid comes out at ⅓ × base × height = 16 m³ without any special case for sloping faces.

Centre of mass

The same tetrahedra give the centre of mass (centroid) of a solid of uniform density. A tetrahedron’s centroid is the average of its four corners, so the solid’s centroid is the volume-weighted average of the tetrahedra’s centroids. Signed volumes take care of the parts outside again. That is what a lifting plan or a structural model needs for a precast element.

Centroid.cs
var footprint = new[] { (0.0, 0.0), (4.0, 0.0), (4.0, 2.0), (2.0, 2.0), (2.0, 4.0), (0.0, 4.0) };var slab = Mesh.Prism(footprint, 0.25);                        // an L-shaped precast slab double vol = 0;var moment = new V3(0, 0, 0);foreach (var t in slab.F){    V3 a = slab.V[t.A], b = slab.V[t.B], c = slab.V[t.C];      // tetrahedron (origin, a, b, c)    double v = V3.Dot(a, V3.Cross(b, c)) / 6;    vol += v;    moment += (a + b + c) * (v / 4);                           // its centroid is (0 + a + b + c) / 4} Console.WriteLine($"volume {vol:0.###} m3, mass {vol * 2400:0} kg at 2400 kg/m3");Console.WriteLine($"centre of mass {moment * (1 / vol)}"); sealed class Mesh{    public readonly List<V3> V = new();    public readonly List<(int A, int B, int C)> F = new();   // counter-clockwise seen from outside     public static Mesh Box(double lx, double ly, double lz) =>        Prism(new[] { (0.0, 0.0), (lx, 0.0), (lx, ly), (0.0, ly) }, lz);     // A counter-clockwise footprint extruded up by h (floor and roof fanned from corner 0)    public static Mesh Prism((double X, double Y)[] poly, double h)    {        var m = new Mesh();        int n = poly.Length;        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, 0));        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, h));        for (int k = 1; k < n - 1; k++)        {            m.F.Add((0, k + 1, k));                 // floor, facing down            m.F.Add((n, n + k, n + k + 1));         // roof, facing up        }        for (int i = 0; i < n; i++)        {            int j = (i + 1) % n;            m.F.Add((i, j, j + n));                 // each wall as two triangles            m.F.Add((i, j + n, i + n));        }        return m;    }     public static Mesh Pyramid(double b, double h)    {        var m = new Mesh();        m.V.AddRange(new V3[] { new(0, 0, 0), new(b, 0, 0), new(b, b, 0), new(0, b, 0), new(b / 2, b / 2, h) });        m.F.AddRange(new[] { (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (0, 2, 1), (0, 3, 2) });        return m;    }     // (b - a) x (c - a): points out of the solid, and its length is twice the triangle's area    public V3 Normal((int A, int B, int C) t) => V3.Cross(V[t.B] - V[t.A], V[t.C] - V[t.A]);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
volume 3 m3, mass 7200 kg at 2400 kg/m3
centre of mass (1.667, 1.667, 0.125)

Check it by hand: the L is a 4 × 2 rectangle with its centre at (2, 1) plus a 2 × 2 square with its centre at (1, 3). Weighted by area (8 and 4), x̄ = (8·2 + 4·1)/12 = 1.667 and ȳ = (8·1 + 4·3)/12 = 1.667, and the slab’s mid-depth is z = 0.125.

Keep O close to the solid

Mathematically O can be anywhere. Numerically it should be near the solid. Projects placed in real survey coordinates have points like (512 340, 6 401 220): then every tetrahedron from the origin is enormous, the true volume is the small difference between huge positive and negative parts, and rounding errors that are tiny relative to the parts are large relative to the answer.

FarFromOrigin.cs
var box = Mesh.Box(4, 3, 2.5);var site = new V3(512_340.37, 6_401_220.81, 35.2);         // survey coordinatesvar placed = new Mesh();placed.V.AddRange(box.V.Select(p => p + site));placed.F.AddRange(box.F); Console.WriteLine($"near the origin:         {Volume(box, new V3(0, 0, 0))} m3");Console.WriteLine($"at site, O = origin:     {Volume(placed, new V3(0, 0, 0))} m3");Console.WriteLine($"at site, O = a corner:   {Volume(placed, placed.V[0])} m3"); static double Volume(Mesh m, V3 o) =>    m.F.Sum(t => V3.Dot(m.V[t.A] - o, V3.Cross(m.V[t.B] - o, m.V[t.C] - o)) / 6); sealed class Mesh{    public readonly List<V3> V = new();    public readonly List<(int A, int B, int C)> F = new();   // counter-clockwise seen from outside     public static Mesh Box(double lx, double ly, double lz) =>        Prism(new[] { (0.0, 0.0), (lx, 0.0), (lx, ly), (0.0, ly) }, lz);     // A counter-clockwise footprint extruded up by h (floor and roof fanned from corner 0)    public static Mesh Prism((double X, double Y)[] poly, double h)    {        var m = new Mesh();        int n = poly.Length;        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, 0));        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, h));        for (int k = 1; k < n - 1; k++)        {            m.F.Add((0, k + 1, k));                 // floor, facing down            m.F.Add((n, n + k, n + k + 1));         // roof, facing up        }        for (int i = 0; i < n; i++)        {            int j = (i + 1) % n;            m.F.Add((i, j, j + n));                 // each wall as two triangles            m.F.Add((i, j + n, i + n));        }        return m;    }     public static Mesh Pyramid(double b, double h)    {        var m = new Mesh();        m.V.AddRange(new V3[] { new(0, 0, 0), new(b, 0, 0), new(b, b, 0), new(0, b, 0), new(b / 2, b / 2, h) });        m.F.AddRange(new[] { (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (0, 2, 1), (0, 3, 2) });        return m;    }     // (b - a) x (c - a): points out of the solid, and its length is twice the triangle's area    public V3 Normal((int A, int B, int C) t) => V3.Cross(V[t.B] - V[t.A], V[t.C] - V[t.A]);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
near the origin:         30 m3
at site, O = origin:     29.99652267806232 m3
at site, O = a corner:   30 m3

Even in double precision the volume measured from the far-away origin is a few litres out; in single precision (float, which meshes for display often use) it would be meaningless. Measuring from one of the solid’s own corners gives the exact answer. This is one reason BIM programs keep geometry in a project coordinate system near the origin and store the survey position separately, as the georeferencing lesson explains.

QuantityFrom a mesh
Surface areaΣ ½ |(b − a) × (c − a)|
VolumeΣ (a − O) · ((b − O) × (c − O)) / 6, O near the solid
Centre of massΣ volume × (O + a + b + c)/4, divided by the volume
Extruded elementFootprint area (shoelace) × height: no mesh needed
Quick check

You compute a mesh’s volume and get −24 m³. What is the most likely cause?

Quick check

Why does the choice of O not change the volume of a closed, well-wound mesh?

Practice

A room is the L-shaped footprint (0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4), 3 m high. Compute its volume twice: from the mesh with signed tetrahedra, and as the footprint’s shoelace area times the height. Then compute the area of its walls only (faces whose normal is horizontal).

Show a hint

Build the mesh with Mesh.Prism. A face is a wall when the Z part of its normal is (close to) 0.

Show the solution
Solution.cs
var footprint = new[] { (0.0, 0.0), (4.0, 0.0), (4.0, 2.0), (2.0, 2.0), (2.0, 4.0), (0.0, 4.0) };var room = Mesh.Prism(footprint, 3); double fromMesh = room.F.Sum(t => V3.Dot(room.V[t.A], V3.Cross(room.V[t.B], room.V[t.C])) / 6); double shoelace = 0;for (int i = 0; i < footprint.Length; i++){    var (x1, y1) = footprint[i];    var (x2, y2) = footprint[(i + 1) % footprint.Length];    shoelace += (x1 * y2 - x2 * y1) / 2;} double walls = room.F.Select(room.Normal).Where(n => Math.Abs(n.Z) < 1e-9).Sum(n => n.Length / 2); Console.WriteLine($"volume from the mesh: {fromMesh:0.###} m3");Console.WriteLine($"floor area x height: {shoelace} x 3 = {shoelace * 3} m3");Console.WriteLine($"wall area: {walls:0.###} m2"); sealed class Mesh{    public readonly List<V3> V = new();    public readonly List<(int A, int B, int C)> F = new();   // counter-clockwise seen from outside     public static Mesh Box(double lx, double ly, double lz) =>        Prism(new[] { (0.0, 0.0), (lx, 0.0), (lx, ly), (0.0, ly) }, lz);     // A counter-clockwise footprint extruded up by h (floor and roof fanned from corner 0)    public static Mesh Prism((double X, double Y)[] poly, double h)    {        var m = new Mesh();        int n = poly.Length;        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, 0));        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, h));        for (int k = 1; k < n - 1; k++)        {            m.F.Add((0, k + 1, k));                 // floor, facing down            m.F.Add((n, n + k, n + k + 1));         // roof, facing up        }        for (int i = 0; i < n; i++)        {            int j = (i + 1) % n;            m.F.Add((i, j, j + n));                 // each wall as two triangles            m.F.Add((i, j + n, i + n));        }        return m;    }     public static Mesh Pyramid(double b, double h)    {        var m = new Mesh();        m.V.AddRange(new V3[] { new(0, 0, 0), new(b, 0, 0), new(b, b, 0), new(0, b, 0), new(b / 2, b / 2, h) });        m.F.AddRange(new[] { (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (0, 2, 1), (0, 3, 2) });        return m;    }     // (b - a) x (c - a): points out of the solid, and its length is twice the triangle's area    public V3 Normal((int A, int B, int C) t) => V3.Cross(V[t.B] - V[t.A], V[t.C] - V[t.A]);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
volume from the mesh: 36 m3
floor area x height: 12 x 3 = 36 m3
wall area: 48 m2

Remember

  • Surface area is the sum of ½ |(b − a) × (c − a)| over the triangles.
  • Volume is the sum of signed tetrahedra (a − O) · ((b − O) × (c − O)) / 6; the parts outside the solid cancel.
  • It needs a closed mesh with consistent winding; a negative total means the mesh is inside out.
  • The centre of mass is the volume-weighted average of the tetrahedra’s centroids.
  • Put O near the solid: far-away coordinates lose precision even in double.
0 of 44 lessons completed