mirror of
https://github.com/gonum/gonum.git
synced 2025-10-08 00:20:11 +08:00
spatial/r3: add Triangle
This commit is contained in:
104
spatial/r3/icosahedron_example_test.go
Normal file
104
spatial/r3/icosahedron_example_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright ©2022 The Gonum Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package r3_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gonum.org/v1/gonum/spatial/r3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleTriangle_icosphere() {
|
||||||
|
// This example generates a 3D icosphere from
|
||||||
|
// a starting icosahedron by subdividing surfaces.
|
||||||
|
// See https://schneide.blog/2016/07/15/generating-an-icosphere-in-c/.
|
||||||
|
const subdivisions = 5
|
||||||
|
// vertices is a slice of r3.Vec
|
||||||
|
// triangles is a slice of [3]int indices
|
||||||
|
// referencing the vertices.
|
||||||
|
vertices, triangles := icosahedron()
|
||||||
|
for i := 0; i < subdivisions; i++ {
|
||||||
|
vertices, triangles = subdivide(vertices, triangles)
|
||||||
|
}
|
||||||
|
var faces []r3.Triangle
|
||||||
|
for _, t := range triangles {
|
||||||
|
var face r3.Triangle
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
face[i] = vertices[t[i]]
|
||||||
|
}
|
||||||
|
faces = append(faces, face)
|
||||||
|
}
|
||||||
|
fmt.Println(faces)
|
||||||
|
// The 3D rendering of the icosphere is left as an exercise to the reader.
|
||||||
|
}
|
||||||
|
|
||||||
|
// edgeIdx represents an edge of the icosahedron
|
||||||
|
type edgeIdx [2]int
|
||||||
|
|
||||||
|
func subdivide(vertices []r3.Vec, triangles [][3]int) ([]r3.Vec, [][3]int) {
|
||||||
|
// We generate a lookup table of all newly generated vertices so as to not
|
||||||
|
// duplicate new vertices. edgeIdx has lower index first.
|
||||||
|
lookup := make(map[edgeIdx]int)
|
||||||
|
var result [][3]int
|
||||||
|
for _, triangle := range triangles {
|
||||||
|
var mid [3]int
|
||||||
|
for edge := 0; edge < 3; edge++ {
|
||||||
|
lookup, mid[edge], vertices = subdivideEdge(lookup, vertices, triangle[edge], triangle[(edge+1)%3])
|
||||||
|
}
|
||||||
|
newTriangles := [][3]int{
|
||||||
|
{triangle[0], mid[0], mid[2]},
|
||||||
|
{triangle[1], mid[1], mid[0]},
|
||||||
|
{triangle[2], mid[2], mid[1]},
|
||||||
|
{mid[0], mid[1], mid[2]},
|
||||||
|
}
|
||||||
|
result = append(result, newTriangles...)
|
||||||
|
}
|
||||||
|
return vertices, result
|
||||||
|
}
|
||||||
|
|
||||||
|
// subdivideEdge takes the vertices list and indices first and second which
|
||||||
|
// refer to the edge that will be subdivided.
|
||||||
|
// lookup is a table of all newly generated vertices from
|
||||||
|
// previous calls to subdivideEdge so as to not duplicate vertices.
|
||||||
|
func subdivideEdge(lookup map[edgeIdx]int, vertices []r3.Vec, first, second int) (map[edgeIdx]int, int, []r3.Vec) {
|
||||||
|
key := edgeIdx{first, second}
|
||||||
|
if first > second {
|
||||||
|
// Swap to ensure edgeIdx always has lower index first.
|
||||||
|
key[0], key[1] = key[1], key[0]
|
||||||
|
}
|
||||||
|
vertIdx, vertExists := lookup[key]
|
||||||
|
if !vertExists {
|
||||||
|
// If edge not already subdivided add
|
||||||
|
// new dividing vertex to lookup table.
|
||||||
|
edge0 := vertices[first]
|
||||||
|
edge1 := vertices[second]
|
||||||
|
point := r3.Unit(r3.Add(edge0, edge1)) // vertex at a normalized position.
|
||||||
|
vertices = append(vertices, point)
|
||||||
|
vertIdx = len(vertices) - 1
|
||||||
|
lookup[key] = vertIdx
|
||||||
|
}
|
||||||
|
return lookup, vertIdx, vertices
|
||||||
|
}
|
||||||
|
|
||||||
|
// icosahedron returns an icosahedron mesh.
|
||||||
|
func icosahedron() (vertices []r3.Vec, triangles [][3]int) {
|
||||||
|
const (
|
||||||
|
radiusSqrt = 1.0 // Example designed for unit sphere generation.
|
||||||
|
X = radiusSqrt * .525731112119133606
|
||||||
|
Z = radiusSqrt * .850650808352039932
|
||||||
|
N = 0.0
|
||||||
|
)
|
||||||
|
return []r3.Vec{
|
||||||
|
{-X, N, Z}, {X, N, Z}, {-X, N, -Z}, {X, N, -Z},
|
||||||
|
{N, Z, X}, {N, Z, -X}, {N, -Z, X}, {N, -Z, -X},
|
||||||
|
{Z, X, N}, {-Z, X, N}, {Z, -X, N}, {-Z, -X, N},
|
||||||
|
}, [][3]int{
|
||||||
|
{0, 1, 4}, {0, 4, 9}, {9, 4, 5}, {4, 8, 5},
|
||||||
|
{4, 1, 8}, {8, 1, 10}, {8, 10, 3}, {5, 8, 3},
|
||||||
|
{5, 3, 2}, {2, 3, 7}, {7, 3, 10}, {7, 10, 6},
|
||||||
|
{7, 6, 11}, {11, 6, 0}, {0, 6, 1}, {6, 10, 1},
|
||||||
|
{9, 11, 0}, {9, 2, 11}, {9, 5, 2}, {7, 11, 2},
|
||||||
|
}
|
||||||
|
}
|
117
spatial/r3/triangle.go
Normal file
117
spatial/r3/triangle.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// Copyright ©2022 The Gonum Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package r3
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// Triangle represents a triangle in 3D space and
|
||||||
|
// is composed by 3 vectors corresponding to the position
|
||||||
|
// of each of the vertices. Ordering of these vertices
|
||||||
|
// decides the "normal" direction.
|
||||||
|
// Inverting ordering of two vertices inverts the resulting direction.
|
||||||
|
type Triangle [3]Vec
|
||||||
|
|
||||||
|
// Centroid returns the intersection of the three medians of the triangle
|
||||||
|
// as a point in space.
|
||||||
|
func (t Triangle) Centroid() Vec {
|
||||||
|
return Scale(1.0/3.0, Add(Add(t[0], t[1]), t[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal returns the vector with direction
|
||||||
|
// perpendicular to the Triangle's face and magnitude
|
||||||
|
// twice that of the Triangle's area. The ordering
|
||||||
|
// of the triangle vertices decides the normal's resulting
|
||||||
|
// direction. The returned vector is not normalized.
|
||||||
|
func (t Triangle) Normal() Vec {
|
||||||
|
s1, s2, _ := t.sides()
|
||||||
|
return Cross(s1, s2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDegenerate returns true if all of triangle's vertices are
|
||||||
|
// within tol distance of its longest side.
|
||||||
|
func (t Triangle) IsDegenerate(tol float64) bool {
|
||||||
|
longIdx := t.longIdx()
|
||||||
|
// calculate vertex distance from longest side
|
||||||
|
ln := line{t[longIdx], t[(longIdx+1)%3]}
|
||||||
|
dist := ln.distance(t[(longIdx+2)%3])
|
||||||
|
return dist <= tol
|
||||||
|
}
|
||||||
|
|
||||||
|
// longIdx returns index of the longest side. The sides
|
||||||
|
// of the triangles are are as follows:
|
||||||
|
// - Side 0 formed by vertices 0 and 1
|
||||||
|
// - Side 1 formed by vertices 1 and 2
|
||||||
|
// - Side 2 formed by vertices 0 and 2
|
||||||
|
func (t Triangle) longIdx() int {
|
||||||
|
sides := [3]Vec{Sub(t[1], t[0]), Sub(t[2], t[1]), Sub(t[0], t[2])}
|
||||||
|
len2 := [3]float64{Norm2(sides[0]), Norm2(sides[1]), Norm2(sides[2])}
|
||||||
|
longLen := len2[0]
|
||||||
|
longIdx := 0
|
||||||
|
if len2[1] > longLen {
|
||||||
|
longLen = len2[1]
|
||||||
|
longIdx = 1
|
||||||
|
}
|
||||||
|
if len2[2] > longLen {
|
||||||
|
longIdx = 2
|
||||||
|
}
|
||||||
|
return longIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Area returns the surface area of the triangle.
|
||||||
|
func (t Triangle) Area() float64 {
|
||||||
|
// Heron's Formula, see https://en.wikipedia.org/wiki/Heron%27s_formula.
|
||||||
|
// Also see William M. Kahan (24 March 2000). "Miscalculating Area and Angles of a Needle-like Triangle"
|
||||||
|
// for more discussion. https://people.eecs.berkeley.edu/~wkahan/Triangle.pdf.
|
||||||
|
a, b, c := t.orderedLengths()
|
||||||
|
A := (c + (b + a)) * (a - (c - b))
|
||||||
|
A *= (a + (c - b)) * (c + (b - a))
|
||||||
|
return math.Sqrt(A) / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// orderedLengths returns the lengths of the sides of the triangle such that
|
||||||
|
// a ≤ b ≤ c.
|
||||||
|
func (t Triangle) orderedLengths() (a, b, c float64) {
|
||||||
|
s1, s2, s3 := t.sides()
|
||||||
|
l1 := Norm(s1)
|
||||||
|
l2 := Norm(s2)
|
||||||
|
l3 := Norm(s3)
|
||||||
|
// sort-3
|
||||||
|
if l2 < l1 {
|
||||||
|
l1, l2 = l2, l1
|
||||||
|
}
|
||||||
|
if l3 < l2 {
|
||||||
|
l2, l3 = l3, l2
|
||||||
|
if l2 < l1 {
|
||||||
|
l1, l2 = l2, l1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l1, l2, l3
|
||||||
|
}
|
||||||
|
|
||||||
|
// sides returns vectors for each of the sides of t.
|
||||||
|
func (t Triangle) sides() (Vec, Vec, Vec) {
|
||||||
|
return Sub(t[1], t[0]), Sub(t[2], t[1]), Sub(t[0], t[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// line is an infinite 3D line
|
||||||
|
// defined by two points on the line.
|
||||||
|
type line [2]Vec
|
||||||
|
|
||||||
|
// vecOnLine takes a value between 0 and 1 to linearly
|
||||||
|
// interpolate a point on the line.
|
||||||
|
// vecOnLine(0) returns l[0]
|
||||||
|
// vecOnLine(1) returns l[1]
|
||||||
|
func (l line) vecOnLine(t float64) Vec {
|
||||||
|
lineDir := Sub(l[1], l[0])
|
||||||
|
return Add(l[0], Scale(t, lineDir))
|
||||||
|
}
|
||||||
|
|
||||||
|
// distance returns the minimum euclidean distance of point p
|
||||||
|
// to the line.
|
||||||
|
func (l line) distance(p Vec) float64 {
|
||||||
|
// https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html
|
||||||
|
num := Norm(Cross(Sub(p, l[0]), Sub(p, l[1])))
|
||||||
|
return num / Norm(Sub(l[1], l[0]))
|
||||||
|
}
|
203
spatial/r3/triangle_test.go
Normal file
203
spatial/r3/triangle_test.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// Copyright ©2022 The Gonum Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package r3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/exp/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTriangleDegenerate(t *testing.T) {
|
||||||
|
const (
|
||||||
|
// tol is how much closer the problematic
|
||||||
|
// vertex is placed to avoid floating point error
|
||||||
|
// for degeneracy calculation.
|
||||||
|
tol = 1e-12
|
||||||
|
// This is the argument to Degenerate and represents
|
||||||
|
// the minimum permissible distance between the triangle
|
||||||
|
// longest edge and the opposite vertex.
|
||||||
|
spatialTol = 1e-2
|
||||||
|
)
|
||||||
|
rnd := rand.New(rand.NewSource(1))
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
// Generate a random line for the longest triangle side.
|
||||||
|
ln := line{randomVec(rnd), randomVec(rnd)}
|
||||||
|
lineDir := Sub(ln[1], ln[0])
|
||||||
|
|
||||||
|
perpendicular := Unit(Cross(lineDir, randomVec(rnd)))
|
||||||
|
// generate 3 permutations of needle triangles for
|
||||||
|
// each vertex. A needle triangle has two vertices
|
||||||
|
// very close to eachother an its third vertex far away.
|
||||||
|
var needle Triangle
|
||||||
|
for j := 0; j < 3; j++ {
|
||||||
|
needle[j] = ln[0]
|
||||||
|
needle[(j+1)%3] = ln[1]
|
||||||
|
needle[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, perpendicular))
|
||||||
|
if !needle.IsDegenerate(spatialTol) {
|
||||||
|
t.Error("needle triangle not degenerate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
midpoint := ln.vecOnLine(0.5)
|
||||||
|
// cap triangles are characterized by having two sides
|
||||||
|
// of similar lengths and whose sum is approximately equal
|
||||||
|
// to the remaining longest side.
|
||||||
|
var cap Triangle
|
||||||
|
for j := 0; j < 3; j++ {
|
||||||
|
cap[j] = ln[0]
|
||||||
|
cap[(j+1)%3] = ln[1]
|
||||||
|
cap[(j+2)%3] = Add(midpoint, Scale((1-tol)*spatialTol, perpendicular))
|
||||||
|
if !cap.IsDegenerate(spatialTol) {
|
||||||
|
t.Error("cap triangle not degenerate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var degenerate Triangle
|
||||||
|
for j := 0; j < 3; j++ {
|
||||||
|
degenerate[j] = ln[0]
|
||||||
|
degenerate[(j+1)%3] = ln[1]
|
||||||
|
// vertex perpendicular to some random point on longest side.
|
||||||
|
degenerate[(j+2)%3] = Add(ln.vecOnLine(rnd.Float64()), Scale((1-tol)*spatialTol, perpendicular))
|
||||||
|
if !degenerate.IsDegenerate(spatialTol) {
|
||||||
|
t.Error("random degenerate triangle not degenerate")
|
||||||
|
}
|
||||||
|
// vertex about longest side 0 vertex
|
||||||
|
degenerate[(j+2)%3] = Add(ln[0], Scale((1-tol)*spatialTol, Unit(randomVec(rnd))))
|
||||||
|
if !degenerate.IsDegenerate(spatialTol) {
|
||||||
|
t.Error("needle-like degenerate triangle not degenerate")
|
||||||
|
}
|
||||||
|
// vertex about longest side 1 vertex
|
||||||
|
degenerate[(j+2)%3] = Add(ln[1], Scale((1-tol)*spatialTol, Unit(randomVec(rnd))))
|
||||||
|
if !degenerate.IsDegenerate(spatialTol) {
|
||||||
|
t.Error("needle-like degenerate triangle not degenerate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTriangleCentroid(t *testing.T) {
|
||||||
|
const tol = 1e-12
|
||||||
|
rnd := rand.New(rand.NewSource(1))
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
tri := randomTriangle(rnd)
|
||||||
|
got := tri.Centroid()
|
||||||
|
want := Vec{
|
||||||
|
X: (tri[0].X + tri[1].X + tri[2].X) / 3,
|
||||||
|
Y: (tri[0].Y + tri[1].Y + tri[2].Y) / 3,
|
||||||
|
Z: (tri[0].Z + tri[1].Z + tri[2].Z) / 3,
|
||||||
|
}
|
||||||
|
if !vecApproxEqual(got, want, tol) {
|
||||||
|
t.Fatalf("got %.6g, want %.6g", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTriangleNormal(t *testing.T) {
|
||||||
|
const tol = 1e-12
|
||||||
|
rnd := rand.New(rand.NewSource(1))
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
tri := randomTriangle(rnd)
|
||||||
|
got := tri.Normal()
|
||||||
|
expect := goldenNormal(tri)
|
||||||
|
if !vecApproxEqual(got, expect, tol) {
|
||||||
|
t.Fatalf("got %.6g, want %.6g", got, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTriangleArea(t *testing.T) {
|
||||||
|
const tol = 1e-16
|
||||||
|
for _, test := range []struct {
|
||||||
|
T Triangle
|
||||||
|
Expect float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
T: Triangle{
|
||||||
|
{0, 0, 0},
|
||||||
|
{1, 0, 0},
|
||||||
|
{0, 1, 0},
|
||||||
|
},
|
||||||
|
Expect: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
T: Triangle{
|
||||||
|
{1, 0, 0},
|
||||||
|
{0, 1, 0},
|
||||||
|
{0, 0, 0},
|
||||||
|
},
|
||||||
|
Expect: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
T: Triangle{
|
||||||
|
{20, 0, 0},
|
||||||
|
{0, 0, 20},
|
||||||
|
{0, 0, 0},
|
||||||
|
},
|
||||||
|
Expect: 20 * 20 / 2,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
got := test.T.Area()
|
||||||
|
if math.Abs(got-test.Expect) > tol {
|
||||||
|
t.Errorf("got area %g, expected %g", got, test.Expect)
|
||||||
|
}
|
||||||
|
if test.T.IsDegenerate(tol) {
|
||||||
|
t.Error("well-formed triangle is degenerate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const tol2 = 1e-12
|
||||||
|
rnd := rand.New(rand.NewSource(1))
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
tri := randomTriangle(rnd)
|
||||||
|
got := tri.Area()
|
||||||
|
want := Norm(Cross(Sub(tri[1], tri[0]), Sub(tri[2], tri[0]))) / 2
|
||||||
|
if math.Abs(got-want) > tol2 {
|
||||||
|
t.Errorf("got area %g not match half norm of cross product %g", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTriangleOrderedLengths(t *testing.T) {
|
||||||
|
rnd := rand.New(rand.NewSource(1))
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
tri := randomTriangle(rnd)
|
||||||
|
s1, s2, s3 := tri.sides()
|
||||||
|
l1 := Norm(s1)
|
||||||
|
l2 := Norm(s2)
|
||||||
|
l3 := Norm(s3)
|
||||||
|
a, b, c := tri.orderedLengths()
|
||||||
|
if a != l1 && a != l2 && a != l3 {
|
||||||
|
t.Error("shortest ordered length not a side of the triangle")
|
||||||
|
}
|
||||||
|
if b != l1 && b != l2 && b != l3 {
|
||||||
|
t.Error("middle ordered length not a side of the triangle")
|
||||||
|
}
|
||||||
|
if c != l1 && c != l2 && c != l3 {
|
||||||
|
t.Error("longest ordered length not a side of the triangle")
|
||||||
|
}
|
||||||
|
if a > b || a > c {
|
||||||
|
t.Error("ordered short side not shortest side")
|
||||||
|
}
|
||||||
|
if c < b {
|
||||||
|
t.Error("ordered long side not longest side")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// taken from soypat/sdf library where it has been thoroughly tested empirically.
|
||||||
|
func goldenNormal(t Triangle) Vec {
|
||||||
|
e1 := Sub(t[1], t[0])
|
||||||
|
e2 := Sub(t[2], t[0])
|
||||||
|
return Cross(e1, e2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomTriangle(rnd *rand.Rand) Triangle {
|
||||||
|
return Triangle{
|
||||||
|
randomVec(rnd),
|
||||||
|
randomVec(rnd),
|
||||||
|
randomVec(rnd),
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user