From 51103910ebe81cdcaa3bfad4d0c4ac1bae138f8d Mon Sep 17 00:00:00 2001 From: Yeicor Date: Wed, 29 Dec 2021 23:59:33 +0100 Subject: [PATCH 1/3] Multithreaded Render3 implementation, parallelizing any other Render3 implementation. --- examples/cylinder_head/main.go | 12 +++- go.mod | 1 + go.sum | 2 + render/dc/dc3v1.go | 9 +-- render/dc/dc3v2.go | 21 +++---- render/march3.go | 14 +---- render/march3x.go | 9 +-- render/multithread3.go | 112 +++++++++++++++++++++++++++++++++ render/render.go | 12 +++- 9 files changed, 153 insertions(+), 39 deletions(-) create mode 100644 render/multithread3.go diff --git a/examples/cylinder_head/main.go b/examples/cylinder_head/main.go index a6f6c4dc8..7e243147c 100644 --- a/examples/cylinder_head/main.go +++ b/examples/cylinder_head/main.go @@ -16,6 +16,7 @@ import ( . "github.com/deadsy/sdfx/sdf" "log" "math" + "runtime" "time" ) @@ -400,7 +401,10 @@ func subtractive() SDF3 { func main() { s := Difference3D(additive(), subtractive()) - render.RenderSTL(s, 400, "head.stl") + + mtRenderer := render.NewMtRenderer3(&render.MarchingCubesUniform{}, 0) + mtRenderer.AutoSplitsMinimum(runtime.NumCPU()) + render.ToSTL(s, 400, "head.stl", mtRenderer) t1 := time.Now() render.ToSTL(s, 128, "head2.stl", dc.NewDualContouringV1(-1, 0, false)) t2 := time.Now() @@ -408,6 +412,12 @@ func main() { td2 := time.Since(t2) td1 := t2.Sub(t1) log.Println("DualContouringV1 delta time:", td1, "- DualContouringDefault delta time:", td2) + mtRenderer2 := render.NewMtRenderer3(dc.NewDualContouringDefault(), 1) + mtRenderer2.AutoSplitsMinimum(runtime.NumCPU()) + t3 := time.Now() + render.ToSTL(s, 128, "head2.stl", mtRenderer2) + td3 := time.Since(t3) + log.Println("DualContouringDefault delta time:", td2, "- DualContouringDefault MT delta time:", td3) } //----------------------------------------------------------------------------- diff --git a/go.mod b/go.mod index 522aa671c..c65cdc9dc 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb + github.com/barkimedes/go-deepcopy v0.0.0-20200817023428-a044a1957ca4 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/llgcode/draw2d v0.0.0-20200930101115-bfaf5d914d1e github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index bbf40fc22..972c14214 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb h1:EVl3FJLQCzSbgBezKo/1A4ADnJ4mtJZ0RvnNzDJ44nY= github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/barkimedes/go-deepcopy v0.0.0-20200817023428-a044a1957ca4 h1:iBbhlt5YmtL9QXsMVf/XPXgYBQLifn38Cdjc0J/+uYg= +github.com/barkimedes/go-deepcopy v0.0.0-20200817023428-a044a1957ca4/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/render/dc/dc3v1.go b/render/dc/dc3v1.go index 6d6765d81..f0102ba24 100644 --- a/render/dc/dc3v1.go +++ b/render/dc/dc3v1.go @@ -14,7 +14,6 @@ Based on: https://github.com/nickgildea/DualContouringSample package dc import ( - "fmt" "github.com/deadsy/sdfx/render" "github.com/deadsy/sdfx/sdf" "gonum.org/v1/gonum/mat" @@ -43,12 +42,8 @@ func NewDualContouringV1(simplify float64, RCond float64, lockVertices bool) *Du return &DualContouringV1{Simplify: simplify, RCond: RCond, LockVertices: lockVertices} } -// Info returns a string describing the rendered volume. -func (m *DualContouringV1) Info(s sdf.SDF3, meshCells int) string { - bbSize := s.BoundingBox().Size() - resolution := bbSize.MaxComponent() / float64(meshCells) - cells := bbSize.DivScalar(resolution).ToV3i() - return fmt.Sprintf("%dx%dx%d, resolution %.2f", cells[0], cells[1], cells[2], resolution) +func (m *DualContouringV1) Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) { + return render.DefaultRender3Cells(s, meshCells) } // Render produces a 3d triangle mesh over the bounding volume of an sdf3. diff --git a/render/dc/dc3v2.go b/render/dc/dc3v2.go index 68f74e42d..8b7eaa27d 100644 --- a/render/dc/dc3v2.go +++ b/render/dc/dc3v2.go @@ -66,24 +66,22 @@ func NewDualContouringV2(farAway float64, centerPush float64, raycastScaleAndSig // Info returns a string describing the rendered volume. func (dc *DualContouringV2) Info(s sdf.SDF3, meshCells int) string { - resolution, cells := dc.getCells(s, meshCells) + resolution, cells := dc.Cells(s, meshCells) return fmt.Sprintf("%dx%dx%d, resolution %.2f", cells[0], cells[1], cells[2], resolution) } // Render produces a 3d triangle mesh over the bounding volume of an sdf3. func (dc *DualContouringV2) Render(s sdf.SDF3, meshCells int, output chan<- *render.Triangle3) { // Place one vertex for each cellIndex - _, cells := dc.getCells(s, meshCells) + _, cells := dc.Cells(s, meshCells) s2 := &dcSdf{s, map[sdf.V3]float64{}} vertexBuffer, vertexVoxelInfo, vertexVoxelInfoIndexed := dc.placeVertices(s2, cells) // Stitch vertices together generating triangles dc.generateTriangles(s2, vertexBuffer, vertexVoxelInfo, vertexVoxelInfoIndexed, output) } -func (dc *DualContouringV2) getCells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) { - bbSize := s.BoundingBox().Size() - resolution := bbSize.MaxComponent() / float64(meshCells) - return resolution, bbSize.DivScalar(resolution).ToV3i() +func (dc *DualContouringV2) Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) { + return render.DefaultRender3Cells(s, meshCells) } //----------------------------------------------------------------------------- @@ -215,7 +213,7 @@ func (dc *DualContouringV2) placeVertex(s *dcSdf, cellStart, cellCenter, cellSiz dc.RaycastEpsilon, dirLength*2, dc.RaycastMaxSteps) if t < 0 || t > dirLength { if !dc.raycastFailedWarned { - log.Println("[DualContouringV1] WARNING: raycast failed (steps:", steps, "- try modifying options), using fallback low accuracy implementation") + log.Println("[DualContouring] WARNING: raycast failed (steps:", steps, "- try modifying options), using fallback low accuracy implementation") dc.raycastFailedWarned = true } edgeSurfPos = dcApproximateZeroCrossingPosition(s, cornerPos1, cornerPos2) @@ -247,7 +245,7 @@ func (dc *DualContouringV2) placeVertex(s *dcSdf, cellStart, cellCenter, cellSiz // Check if vertex positioning failed if math.IsInf(vertexPos.X, 0) { if !dc.qefFailedWarned { - log.Println("[DualContouringV1] WARNING: vertex positioning failed, centering vertex position!") + log.Println("[DualContouring] WARNING: vertex positioning failed, centering vertex position!") dc.qefFailedWarned = true } vertexPos = cellCenter @@ -258,7 +256,7 @@ func (dc *DualContouringV2) placeVertex(s *dcSdf, cellStart, cellCenter, cellSiz math.Abs(vertexPos.Y-cellCenter.Y) > dc.FarAway*cellSize.Y || math.Abs(vertexPos.Z-cellCenter.Z) > dc.FarAway*cellSize.Z { if !dc.farAwayWarned { - log.Print("[DualContouringV1] WARNING: generated a vertex two far away from voxel (by ", + log.Print("[DualContouring] WARNING: generated a vertex two far away from voxel (by ", vertexPos.Sub(cellCenter), ", from ", cellCenter, " to ", vertexPos, "), clamping vertex position!\n") dc.farAwayWarned = true } @@ -312,7 +310,8 @@ func (dc *DualContouringV2) generateTriangles(s *dcSdf, vertices []sdf.V3, info if v1 == nil || v2 == nil || v3 == nil { // Shouldn't ever happen if !dc.faceVertexNotFoundWarned { - log.Println("[DualContouringV1] WARNING: no vertex found for completing face, there will be holes") + log.Println("[DualContouring] WARNING: no vertex found for completing face, there will be holes " + + "(this happens if the surface crosses the bounding box)") dc.faceVertexNotFoundWarned = true } continue @@ -361,7 +360,7 @@ func (dc *DualContouringV2) computeVertexPos(normals []sdf.V3, planeDs []float64 //err := res.Solve(A, b) //if err != nil { // if !dc.qefFailedImplWarned { - // log.Println("[DualContouringV1] WARNING: QEF solver failed: ", err.Error()) + // log.Println("[DualContouring] WARNING: QEF solver failed: ", err.Error()) // dc.qefFailedImplWarned = true // } // return sdf.V3{X: math.Inf(1)} diff --git a/render/march3.go b/render/march3.go index de1c63844..c06f6eebf 100644 --- a/render/march3.go +++ b/render/march3.go @@ -11,7 +11,6 @@ Convert an SDF3 to a triangle mesh. package render import ( - "fmt" "math" "runtime" "sync" @@ -264,15 +263,8 @@ func mcInterpolate(p1, p2 sdf.V3, v1, v2, x float64) sdf.V3 { type MarchingCubesUniform struct { } -// Info returns a string describing the rendered volume. -func (m *MarchingCubesUniform) Info(s sdf.SDF3, meshCells int) string { - bb0 := s.BoundingBox() - bb0Size := bb0.Size() - meshInc := bb0Size.MaxComponent() / float64(meshCells) - bb1Size := bb0Size.DivScalar(meshInc) - bb1Size = bb1Size.Ceil().AddScalar(1) - cells := bb1Size.ToV3i() - return fmt.Sprintf("%dx%dx%d", cells[0], cells[1], cells[2]) +func (m *MarchingCubesUniform) Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) { + return DefaultRender3Cells(s, meshCells) } // Render produces a 3d triangle mesh over the bounding volume of an sdf3. @@ -282,7 +274,7 @@ func (m *MarchingCubesUniform) Render(s sdf.SDF3, meshCells int, output chan<- * bb0Size := bb0.Size() meshInc := bb0Size.MaxComponent() / float64(meshCells) bb1Size := bb0Size.DivScalar(meshInc) - bb1Size = bb1Size.Ceil().AddScalar(1) + bb1Size = bb1Size /*.Ceil().AddScalar(1) - Changed to work with multithread renderer: same behaviour as other renderers*/ bb1Size = bb1Size.MulScalar(meshInc) bb := sdf.NewBox3(bb0.Center(), bb1Size) for _, tri := range marchingCubes(s, bb, meshInc) { diff --git a/render/march3x.go b/render/march3x.go index 9c0968509..03d7995d3 100644 --- a/render/march3x.go +++ b/render/march3x.go @@ -12,7 +12,6 @@ Uses octree space subdivision. package render import ( - "fmt" "math" "sync" @@ -159,12 +158,8 @@ func marchingCubesOctree(s sdf.SDF3, resolution float64, output chan<- *Triangle type MarchingCubesOctree struct { } -// Info returns a string describing the rendered volume. -func (m *MarchingCubesOctree) Info(s sdf.SDF3, meshCells int) string { - bbSize := s.BoundingBox().Size() - resolution := bbSize.MaxComponent() / float64(meshCells) - cells := bbSize.DivScalar(resolution).ToV3i() - return fmt.Sprintf("%dx%dx%d, resolution %.2f", cells[0], cells[1], cells[2], resolution) +func (m *MarchingCubesOctree) Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) { + return DefaultRender3Cells(s, meshCells) } // Render produces a 3d triangle mesh over the bounding volume of an sdf3. diff --git a/render/multithread3.go b/render/multithread3.go new file mode 100644 index 000000000..6b9d978b2 --- /dev/null +++ b/render/multithread3.go @@ -0,0 +1,112 @@ +//----------------------------------------------------------------------------- +/* + +Multithreaded 3D renderer + +*/ +//----------------------------------------------------------------------------- + +package render + +import ( + "github.com/barkimedes/go-deepcopy" + "github.com/deadsy/sdfx/sdf" + "math" + "sync" +) + +// MTRenderer3 converts a SDF3 to a triangle mesh, parallelizing the rendering process by splitting the space. +// It supports MarchingCubesUniform and dc.DualContouringV2 (not MarchingCubesOctree for some reason), +// and should support other Render3 voxel-based implementation. +type MTRenderer3 struct { + // impl the base implementation to copy and use for rendering partial surfaces. + impl Render3 + // NumSplits is the number of times to split each dimension (0 splits means 1 partition). + NumSplits sdf.V3i + // OverlappingCells provides overlapping boundaries so that N cells are shared between contiguous renderers. + // Set to 0 for Marching Cubes renderer and to 1 for Dual Contouring (places vertices instead of faces per voxel) + OverlappingCells float64 +} + +var _ Render3 = &MTRenderer3{} + +// NewMtRenderer3 see MTRenderer3 +func NewMtRenderer3(impl Render3, overlappingCells float64) *MTRenderer3 { + return &MTRenderer3{impl: impl, OverlappingCells: overlappingCells, NumSplits: sdf.V3i{0, 0, 0}} +} + +// AutoSplitsMinimum auto-partitions the space to have at least `routines` partitions. +// +// Example: `MTRenderer3.AutoSplitsMinimum(runtime.NumCPU())` +func (m *MTRenderer3) AutoSplitsMinimum(minPartitions int) { + // Try to share splits in all dimensions, forcing last dimension to get more splits if needed (may overshoot `minPartitions`) + splits := float64(minPartitions) * math.Pow(2, -3 /* dimensions */) + m.NumSplits = sdf.V3i{int(splits), int(splits), int(splits)} + // If more splits are needed, give the first dimension more splits (generate at least minPartitions splits) + // NOTE: partitions = splits + 1 + m.NumSplits[0] += (minPartitions - (m.NumSplits[0]+1)*(m.NumSplits[1]+1)*(m.NumSplits[2]+1)) / + ((m.NumSplits[1] + 1) * (m.NumSplits[2] + 1)) +} + +func (m *MTRenderer3) Cells(sdf3 sdf.SDF3, meshCells int) (float64, sdf.V3i) { + return m.impl.Cells(sdf3, meshCells) +} + +func (m *MTRenderer3) Render(sdf3 sdf.SDF3, meshCells int, output chan<- *Triangle3) { + // Get cells to render on each dimension (change Render3 Info) + originalResolution, originalCells := m.Cells(sdf3, meshCells) + fullBb := sdf3.BoundingBox() + fullBbSize := fullBb.Size() + cellSize := fullBbSize.Div(originalCells.ToV3()) + numPartitions := m.NumSplits.AddScalar(1) + + // The priority is to keep the cellSize the same on all partitions (in case overlapping cells are needed) + // Then, try to have all partitions be of the same size to avoid waiting for one goroutine to finish + cellsPerPartitionBase := originalCells.ToV3().Div(numPartitions.ToV3()) + partitionSizeBase := cellSize.Mul(cellsPerPartitionBase) + partitionSize := cellSize.Mul(cellsPerPartitionBase.AddScalar(m.OverlappingCells)) + subMeshCells := int( /*math.Ceil*/ partitionSize.MaxComponent() / originalResolution) + + cellIndex := sdf.V3i{0, 0, 0} + wg := &sync.WaitGroup{} + for cellIndex[0] = 0; cellIndex[0] <= m.NumSplits[0]; cellIndex[0]++ { + for cellIndex[1] = 0; cellIndex[1] <= m.NumSplits[1]; cellIndex[1]++ { + for cellIndex[2] = 0; cellIndex[2] <= m.NumSplits[2]; cellIndex[2]++ { + // Get bounded sub-surface + // WARNING: Will be slightly outside the original bounding-box (for simplicity, if OverlappingCells > 0), + // but keeps partition size and cells per partition the same so that OverlappingCells works + from := fullBb.Min.Add(partitionSizeBase.Mul(cellIndex.ToV3())).AddScalar(-m.OverlappingCells / 2) + to := from.Add(partitionSize) + subSurface := &customBbSdf{impl: sdf3, bb: sdf.Box3{Min: from, Max: to}} + // Spawn the rendering goroutine + go func(impl Render3, subSurface sdf.SDF3) { + // Debug + //resolution2, cells2 := impl.Cells(subSurface, subMeshCells) + //fmt.Printf("rendering part (%dx%dx%d, resolution %.2f) from %s to %s\n", + // cells2[0], cells2[1], cells2[2], resolution2, fmt.Sprint(subSurface.BoundingBox().Min), + // fmt.Sprint(subSurface.BoundingBox().Max)) + // Just call render on the sub-surface, generating triangles to the same output + impl.Render(subSurface, subMeshCells, output) + wg.Done() + }(deepcopy.MustAnything(m.impl).(Render3), subSurface) + wg.Add(1) + } + } + } + wg.Wait() // Wait for all spawned goroutines (space partition renderers) to finish +} + +var _ sdf.SDF3 = &customBbSdf{} + +type customBbSdf struct { + impl sdf.SDF3 + bb sdf.Box3 +} + +func (m *customBbSdf) Evaluate(p sdf.V3) float64 { + return m.impl.Evaluate(p) +} + +func (m *customBbSdf) BoundingBox() sdf.Box3 { + return m.bb +} diff --git a/render/render.go b/render/render.go index 45df509b0..7daa2d934 100644 --- a/render/render.go +++ b/render/render.go @@ -20,7 +20,14 @@ import ( // Render3 implementations produce a 3d triangle mesh over the bounding volume of an sdf3. type Render3 interface { Render(sdf3 sdf.SDF3, meshCells int, output chan<- *Triangle3) - Info(sdf3 sdf.SDF3, meshCells int) string + Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) +} + +// DefaultRender3Cells is an internal function that avoids duplicate code. Used for Render3 implementations +func DefaultRender3Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) { + bbSize := s.BoundingBox().Size() + resolution := bbSize.MaxComponent() / float64(meshCells) + return resolution, bbSize.DivScalar(resolution).ToV3i() } // ToSTL renders an SDF3 to an STL file. @@ -30,7 +37,8 @@ func ToSTL( path string, // path to filename r Render3, // rendering method ) { - fmt.Printf("rendering %s (%s)\n", path, r.Info(s, meshCells)) + resolution, cells := r.Cells(s, meshCells) + fmt.Printf("rendering %s (%dx%dx%d, resolution %.2f)\n", path, cells[0], cells[1], cells[2], resolution) // write the triangles to an STL file var wg sync.WaitGroup output, err := WriteSTL(&wg, path) From f1aa227b5f8fd9aaf4507532b777957b3f181ce5 Mon Sep 17 00:00:00 2001 From: Yeicor Date: Tue, 4 Jan 2022 12:39:31 +0100 Subject: [PATCH 2/3] Avoid generating goroutines on package init(), configurable MarchingCubesUniform goroutines, better MTRenderer. --- examples/cylinder_head/main.go | 29 +++++--- go.mod | 1 + go.sum | 3 +- render/march3.go | 117 +++++++++++++++++-------------- render/multithread3.go | 4 ++ render/{render.go => render3.go} | 0 6 files changed, 93 insertions(+), 61 deletions(-) rename render/{render.go => render3.go} (100%) diff --git a/examples/cylinder_head/main.go b/examples/cylinder_head/main.go index 7e243147c..0a90a4e78 100644 --- a/examples/cylinder_head/main.go +++ b/examples/cylinder_head/main.go @@ -402,22 +402,33 @@ func subtractive() SDF3 { func main() { s := Difference3D(additive(), subtractive()) - mtRenderer := render.NewMtRenderer3(&render.MarchingCubesUniform{}, 0) - mtRenderer.AutoSplitsMinimum(runtime.NumCPU()) - render.ToSTL(s, 400, "head.stl", mtRenderer) t1 := time.Now() - render.ToSTL(s, 128, "head2.stl", dc.NewDualContouringV1(-1, 0, false)) + mcuDefRenderer := &render.MarchingCubesUniform{} + render.ToSTL(s, 400, "head.stl", mcuDefRenderer) t2 := time.Now() - render.ToSTL(s, 128, "head2.stl", dc.NewDualContouringDefault()) + mcuMtRenderer := render.NewMtRenderer3(&render.MarchingCubesUniform{EvaluateGoroutines: 1}, 0) + mcuMtRenderer.AutoSplitsMinimum(runtime.NumCPU()) + render.ToSTL(s, 400, "head_tmp.stl", mcuMtRenderer) td2 := time.Since(t2) td1 := t2.Sub(t1) - log.Println("DualContouringV1 delta time:", td1, "- DualContouringDefault delta time:", td2) + // MarchingCubesUniform{1} + MTRenderer is slightly faster than the parallel evaluation version (left as default) + // MarchingCubesUniform{0} + MTRenderer is slightly faster than MarchingCubesUniform{1} + MTRenderer + // (due to too many goroutines, using NumCPU() * 2 when splitting the MarchingCubesUniform{1} + MTRenderer + // gives the same improvement --- chunks are shared better between goroutines and there is less wait for the last one) + log.Println("MarchingCubesUniform + NumCPU goroutines:", td1, "- MarchingCubesUniform + MTRenderer:", td2) + t3 := time.Now() + render.ToSTL(s, 128, "head2.stl", dc.NewDualContouringV1(-1, 0, false)) + t4 := time.Now() + render.ToSTL(s, 128, "head2.stl", dc.NewDualContouringDefault()) + td4 := time.Since(t4) + td3 := t4.Sub(t3) + log.Println("DualContouringV1 delta time:", td3, "- DualContouringDefault delta time:", td4) mtRenderer2 := render.NewMtRenderer3(dc.NewDualContouringDefault(), 1) mtRenderer2.AutoSplitsMinimum(runtime.NumCPU()) - t3 := time.Now() + t5 := time.Now() render.ToSTL(s, 128, "head2.stl", mtRenderer2) - td3 := time.Since(t3) - log.Println("DualContouringDefault delta time:", td2, "- DualContouringDefault MT delta time:", td3) + td5 := time.Since(t5) + log.Println("DualContouringDefault delta time:", td4, "- DualContouringDefault MT delta time:", td5) } //----------------------------------------------------------------------------- diff --git a/go.mod b/go.mod index c65cdc9dc..8f51a540d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb github.com/barkimedes/go-deepcopy v0.0.0-20200817023428-a044a1957ca4 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/llgcode/draw2d v0.0.0-20200930101115-bfaf5d914d1e github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index 972c14214..0afa4b9a4 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,9 @@ github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb/go.mod h1:K08gAheRH3 github.com/barkimedes/go-deepcopy v0.0.0-20200817023428-a044a1957ca4 h1:iBbhlt5YmtL9QXsMVf/XPXgYBQLifn38Cdjc0J/+uYg= github.com/barkimedes/go-deepcopy v0.0.0-20200817023428-a044a1957ca4/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= diff --git a/render/march3.go b/render/march3.go index c06f6eebf..f58d43b26 100644 --- a/render/march3.go +++ b/render/march3.go @@ -21,15 +21,16 @@ import ( //----------------------------------------------------------------------------- type layerYZ struct { - base sdf.V3 // base coordinate of layer - inc sdf.V3 // dx, dy, dz for each step - steps sdf.V3i // number of x,y,z steps - val0 []float64 // SDF values for x layer - val1 []float64 // SDF values for x + dx layer + base sdf.V3 // base coordinate of layer + inc sdf.V3 // dx, dy, dz for each step + steps sdf.V3i // number of x,y,z steps + evalProcessCh chan evalReq // the evaluation channel for parallelization + val0 []float64 // SDF values for x layer + val1 []float64 // SDF values for x + dx layer } -func newLayerYZ(base, inc sdf.V3, steps sdf.V3i) *layerYZ { - return &layerYZ{base, inc, steps, nil, nil} +func newLayerYZ(base, inc sdf.V3, steps sdf.V3i, evalProcessCh chan evalReq) *layerYZ { + return &layerYZ{base, inc, steps, evalProcessCh, nil, nil} } // evalReq is used for processing evaluations in parallel. @@ -43,23 +44,6 @@ type evalReq struct { wg *sync.WaitGroup } -var evalProcessCh = make(chan evalReq, 100) - -func init() { - for i := 0; i < runtime.NumCPU(); i++ { - go func() { - var i int - var p sdf.V3 - for r := range evalProcessCh { - for i, p = range r.p { - r.out[i] = r.fn(p) - } - r.wg.Done() - } - }() - } -} - // Evaluate the SDF for a given XY layer func (l *layerYZ) Evaluate(s sdf.SDF3, x int) { @@ -80,10 +64,13 @@ func (l *layerYZ) Evaluate(s sdf.SDF3, x int) { p.X = l.base.X + float64(x)*dx // define the base struct for requesting evaluation - eReq := evalReq{ - wg: new(sync.WaitGroup), - fn: s.Evaluate, - out: l.val1, + var eReq evalReq + if l.evalProcessCh != nil { + eReq = evalReq{ + wg: new(sync.WaitGroup), + fn: s.Evaluate, + out: l.val1, + } } // evaluate the layer @@ -92,16 +79,22 @@ func (l *layerYZ) Evaluate(s sdf.SDF3, x int) { // Performance doesn't seem to improve past 100. const batchSize = 100 - eReq.p = make([]sdf.V3, 0, batchSize) + if l.evalProcessCh != nil { + eReq.p = make([]sdf.V3, 0, batchSize) + } for y := 0; y < ny+1; y++ { p.Z = l.base.Z for z := 0; z < nz+1; z++ { - eReq.p = append(eReq.p, p) - if len(eReq.p) == batchSize { - eReq.wg.Add(1) - evalProcessCh <- eReq - eReq.out = eReq.out[batchSize:] // shift the output slice for processing - eReq.p = make([]sdf.V3, 0, batchSize) // create a new slice for the next batch + if l.evalProcessCh == nil { // Singlethread mode (just cache the evaluation) + l.val1[idx] = s.Evaluate(p) + } else { // Multithread mode: prepare and send slice for parallel processing using the evaluation goroutines + eReq.p = append(eReq.p, p) + if len(eReq.p) == batchSize { + eReq.wg.Add(1) + l.evalProcessCh <- eReq + eReq.out = eReq.out[batchSize:] // shift the output slice for processing + eReq.p = make([]sdf.V3, 0, batchSize) // create a new slice for the next batch + } } idx++ p.Z += dz @@ -109,14 +102,16 @@ func (l *layerYZ) Evaluate(s sdf.SDF3, x int) { p.Y += dy } - // send any remaining points for processing - if len(eReq.p) > 0 { - eReq.wg.Add(1) - evalProcessCh <- eReq - } + if l.evalProcessCh != nil { + // send any remaining points for processing + if len(eReq.p) > 0 { + eReq.wg.Add(1) + l.evalProcessCh <- eReq + } - // Wait for all processing to complete before returning - eReq.wg.Wait() + // Wait for all processing to complete before returning + eReq.wg.Wait() + } } func (l *layerYZ) Get(x, y, z int) float64 { @@ -129,16 +124,31 @@ func (l *layerYZ) Get(x, y, z int) float64 { //----------------------------------------------------------------------------- -func marchingCubes(s sdf.SDF3, box sdf.Box3, step float64) []*Triangle3 { +func marchingCubes(s sdf.SDF3, box sdf.Box3, step float64, out chan<- *Triangle3, goroutines int) { + var evalProcessCh chan evalReq + if goroutines > 1 { + evalProcessCh = make(chan evalReq, 100) + for i := 0; i < goroutines; i++ { + go func() { + var i int + var p sdf.V3 + for r := range evalProcessCh { + for i, p = range r.p { + r.out[i] = r.fn(p) + } + r.wg.Done() + } + }() + } + } - var triangles []*Triangle3 size := box.Size() base := box.Min steps := size.DivScalar(step).Ceil().ToV3i() inc := size.Div(steps.ToV3()) // create the SDF layer cache - l := newLayerYZ(base, inc, steps) + l := newLayerYZ(base, inc, steps, evalProcessCh) // evaluate the SDF for x = 0 l.Evaluate(s, 0) @@ -175,15 +185,16 @@ func marchingCubes(s sdf.SDF3, box sdf.Box3, step float64) []*Triangle3 { l.Get(1, y, z+1), l.Get(1, y+1, z+1), l.Get(0, y+1, z+1)} - triangles = append(triangles, mcToTriangles(corners, values, 0)...) + //triangles = append(triangles, mcToTriangles(corners, values, 0)...) + for _, tri := range mcToTriangles(corners, values, 0) { + out <- tri + } p.Z += dz } p.Y += dy } p.X += dx } - - return triangles } //----------------------------------------------------------------------------- @@ -261,6 +272,9 @@ func mcInterpolate(p1, p2 sdf.V3, v1, v2, x float64) sdf.V3 { // MarchingCubesUniform renders using marching cubes with uniform space sampling. type MarchingCubesUniform struct { + // How many goroutines to spawn for parallel evaluation of the SDF3 (0 is runtime.NumCPU()) + // Set to 1 to avoid generating goroutines, useful for using the parallel renderer as a wrapper + EvaluateGoroutines int } func (m *MarchingCubesUniform) Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3i) { @@ -269,6 +283,9 @@ func (m *MarchingCubesUniform) Cells(s sdf.SDF3, meshCells int) (float64, sdf.V3 // Render produces a 3d triangle mesh over the bounding volume of an sdf3. func (m *MarchingCubesUniform) Render(s sdf.SDF3, meshCells int, output chan<- *Triangle3) { + if m.EvaluateGoroutines == 0 { + m.EvaluateGoroutines = runtime.NumCPU() // Keep legacy behavior + } // work out the region we will sample bb0 := s.BoundingBox() bb0Size := bb0.Size() @@ -277,9 +294,7 @@ func (m *MarchingCubesUniform) Render(s sdf.SDF3, meshCells int, output chan<- * bb1Size = bb1Size /*.Ceil().AddScalar(1) - Changed to work with multithread renderer: same behaviour as other renderers*/ bb1Size = bb1Size.MulScalar(meshInc) bb := sdf.NewBox3(bb0.Center(), bb1Size) - for _, tri := range marchingCubes(s, bb, meshInc) { - output <- tri - } + marchingCubes(s, bb, meshInc, output, m.EvaluateGoroutines) } //----------------------------------------------------------------------------- diff --git a/render/multithread3.go b/render/multithread3.go index 6b9d978b2..f9e166659 100644 --- a/render/multithread3.go +++ b/render/multithread3.go @@ -37,6 +37,10 @@ func NewMtRenderer3(impl Render3, overlappingCells float64) *MTRenderer3 { // AutoSplitsMinimum auto-partitions the space to have at least `routines` partitions. // +// It is recommended to use the number of logical CPUs of the system or more. More partitions may result in better +// performance (as long as synchronization overhead is not too much and meshCells is big enough), +// as chunks are shared among goroutine workers and there is less wait for the last chunk to finish. +// // Example: `MTRenderer3.AutoSplitsMinimum(runtime.NumCPU())` func (m *MTRenderer3) AutoSplitsMinimum(minPartitions int) { // Try to share splits in all dimensions, forcing last dimension to get more splits if needed (may overshoot `minPartitions`) diff --git a/render/render.go b/render/render3.go similarity index 100% rename from render/render.go rename to render/render3.go From 5514dfb92896ca70903b3e3b916bc44e70e87b62 Mon Sep 17 00:00:00 2001 From: Yeicor Date: Thu, 6 Jan 2022 13:37:54 +0100 Subject: [PATCH 3/3] MTRenderer3: fix partition seams by merging vertices. --- examples/cylinder_head/main.go | 5 +- render/multithread3.go | 184 ++++++++++++++++++++++++++++++++- render/multithread3_test.go | 40 +++++++ 3 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 render/multithread3_test.go diff --git a/examples/cylinder_head/main.go b/examples/cylinder_head/main.go index 0a90a4e78..cf7f2ed1c 100644 --- a/examples/cylinder_head/main.go +++ b/examples/cylinder_head/main.go @@ -411,10 +411,6 @@ func main() { render.ToSTL(s, 400, "head_tmp.stl", mcuMtRenderer) td2 := time.Since(t2) td1 := t2.Sub(t1) - // MarchingCubesUniform{1} + MTRenderer is slightly faster than the parallel evaluation version (left as default) - // MarchingCubesUniform{0} + MTRenderer is slightly faster than MarchingCubesUniform{1} + MTRenderer - // (due to too many goroutines, using NumCPU() * 2 when splitting the MarchingCubesUniform{1} + MTRenderer - // gives the same improvement --- chunks are shared better between goroutines and there is less wait for the last one) log.Println("MarchingCubesUniform + NumCPU goroutines:", td1, "- MarchingCubesUniform + MTRenderer:", td2) t3 := time.Now() render.ToSTL(s, 128, "head2.stl", dc.NewDualContouringV1(-1, 0, false)) @@ -425,6 +421,7 @@ func main() { log.Println("DualContouringV1 delta time:", td3, "- DualContouringDefault delta time:", td4) mtRenderer2 := render.NewMtRenderer3(dc.NewDualContouringDefault(), 1) mtRenderer2.AutoSplitsMinimum(runtime.NumCPU()) + mtRenderer2.MergeVerticesEpsilon = 1e-2 t5 := time.Now() render.ToSTL(s, 128, "head2.stl", mtRenderer2) td5 := time.Since(t5) diff --git a/render/multithread3.go b/render/multithread3.go index f9e166659..67bfaaecd 100644 --- a/render/multithread3.go +++ b/render/multithread3.go @@ -11,6 +11,7 @@ package render import ( "github.com/barkimedes/go-deepcopy" "github.com/deadsy/sdfx/sdf" + "gonum.org/v1/gonum/spatial/kdtree" "math" "sync" ) @@ -26,13 +27,17 @@ type MTRenderer3 struct { // OverlappingCells provides overlapping boundaries so that N cells are shared between contiguous renderers. // Set to 0 for Marching Cubes renderer and to 1 for Dual Contouring (places vertices instead of faces per voxel) OverlappingCells float64 + // MergeVerticesEpsilon is the minimum distance between vertices to merge them. This is needed because vertices in + // the chunk seams can be slightly different (due to running independent algorithms and operation order might change), + // causing software to think they are different vertices when they shouldn't be. + MergeVerticesEpsilon float64 } var _ Render3 = &MTRenderer3{} // NewMtRenderer3 see MTRenderer3 func NewMtRenderer3(impl Render3, overlappingCells float64) *MTRenderer3 { - return &MTRenderer3{impl: impl, OverlappingCells: overlappingCells, NumSplits: sdf.V3i{0, 0, 0}} + return &MTRenderer3{impl: impl, OverlappingCells: overlappingCells, NumSplits: sdf.V3i{0, 0, 0}, MergeVerticesEpsilon: 1e-8} } // AutoSplitsMinimum auto-partitions the space to have at least `routines` partitions. @@ -71,8 +76,16 @@ func (m *MTRenderer3) Render(sdf3 sdf.SDF3, meshCells int, output chan<- *Triang partitionSize := cellSize.Mul(cellsPerPartitionBase.AddScalar(m.OverlappingCells)) subMeshCells := int( /*math.Ceil*/ partitionSize.MaxComponent() / originalResolution) - cellIndex := sdf.V3i{0, 0, 0} + // Intercept output to merge vertices generated on seams wg := &sync.WaitGroup{} + var wgRet *sync.WaitGroup + if m.MergeVerticesEpsilon > 0 { + output, wgRet = m.mergeGeneratedVertices(output, m.MergeVerticesEpsilon*cellSize.MaxComponent(), + fullBb.Min.AddScalar(-m.OverlappingCells/2), partitionSizeBase, cellSize) + } + + // Generate each partition in a goroutine + cellIndex := sdf.V3i{0, 0, 0} for cellIndex[0] = 0; cellIndex[0] <= m.NumSplits[0]; cellIndex[0]++ { for cellIndex[1] = 0; cellIndex[1] <= m.NumSplits[1]; cellIndex[1]++ { for cellIndex[2] = 0; cellIndex[2] <= m.NumSplits[2]; cellIndex[2]++ { @@ -98,8 +111,14 @@ func (m *MTRenderer3) Render(sdf3 sdf.SDF3, meshCells int, output chan<- *Triang } } wg.Wait() // Wait for all spawned goroutines (space partition renderers) to finish + if m.MergeVerticesEpsilon > 0 { + close(output) // Actually the input for internal goroutine + wgRet.Wait() // Wait for post-processing to complete + } } +//----------------------------------------------------------------------------- + var _ sdf.SDF3 = &customBbSdf{} type customBbSdf struct { @@ -114,3 +133,164 @@ func (m *customBbSdf) Evaluate(p sdf.V3) float64 { func (m *customBbSdf) BoundingBox() sdf.Box3 { return m.bb } + +//----------------------------------------------------------------------------- + +func mtToKdPoint(v3 sdf.V3) kdtree.Point { + return kdtree.Point{v3.X, v3.Y, v3.Z} +} + +func mtFromKdPoint(v3 kdtree.Point) sdf.V3 { + return sdf.V3{X: v3[0], Y: v3[1], Z: v3[2]} +} + +func (m *MTRenderer3) mergeGeneratedVertices(output chan<- *Triangle3, mergeVerticesEpsilon float64, sdfStart sdf.V3, partitionSize sdf.V3, cellSize sdf.V3) (chan *Triangle3, *sync.WaitGroup) { + input := make(chan *Triangle3) + mergeVerticesEpsilonSq := mergeVerticesEpsilon * mergeVerticesEpsilon // Squared euclidean distance used in k-d tree + mmod := func(a, b float64) float64 { + r := math.Mod(a, b) + if r > b/2 { + r = b - r + } + return r + } + isVertCloseToSeam := func(v sdf.V3) bool { // Filter to speed-up postprocessing + offset := v.Sub(sdfStart) + return mmod(offset.X, partitionSize.X) < mergeVerticesEpsilon+cellSize.X || + mmod(offset.Y, partitionSize.Y) < mergeVerticesEpsilon+cellSize.Y || + mmod(offset.Z, partitionSize.Z) < mergeVerticesEpsilon+cellSize.Z + } + + vertToPassOriginalTris := map[sdf.V3][]*Triangle3{} + + wg := &sync.WaitGroup{} + wgRet := &sync.WaitGroup{} + wg.Add(1) + go func() { // Collect all initial vertices in k-d tree + for tri := range input { + if bypassVertTris(isVertCloseToSeam, nil, []*Triangle3{tri}) { + output <- tri + } else { + for _, vert := range tri.V { + // Do not modify the vertices with math operations, as floating point operations might make the map fail + prevTris, _ := vertToPassOriginalTris[vert] + prevTris = append(prevTris, tri) + vertToPassOriginalTris[vert] = prevTris + } + } + } + wg.Done() + }() + + wgRet.Add(1) + go func() { // Merge all close together vertices (modifying generated triangles) + wg.Wait() + // Only the subset of triangles that are close to the seams (others already published) + trianglesToPublish := map[Triangle3]*Triangle3{} + // Build K-D tree for this pass + allVertices := make(kdtree.Points, 0, len(vertToPassOriginalTris)) + for vert, _ := range vertToPassOriginalTris { + allVertices = append(allVertices, mtToKdPoint(vert)) + } + tree := kdtree.New(allVertices, false) + //log.Println("Post-processing vertex count", len(vertToPassOriginalTris)) + movedVertices := make([]sdf.V3Set, 0) // {from, to} + for vert, originalTriangles := range vertToPassOriginalTris { + //log.Println("vertToPassOriginalTris", vert) + closest := kdtree.NewNKeeper(3) // The first is always a perfect match with the current vertex + tree.NearestSet(closest, mtToKdPoint(vert)) + for _, comparableDist := range closest.Heap[1:] { + nthPos := mtFromKdPoint(comparableDist.Comparable.(kdtree.Point)) + nthDist := comparableDist.Dist + modifiedTriangles := make([]*Triangle3, 0) + for _, originalTriangle := range originalTriangles { + modifiedTriangle, ok := trianglesToPublish[*originalTriangle] + if !ok { + modifiedTriangle = &*originalTriangle // Clone + } + modifiedTriangles = append(modifiedTriangles, modifiedTriangle) + } + if nthDist > mergeVerticesEpsilonSq { + for i, originalTriangle := range originalTriangles { + trianglesToPublish[*originalTriangle] = modifiedTriangles[i] + } + break + } else if nthDist > 0 { + //log.Println("Nth closest #", i, "tris", len(originalTriangles), "dist", nthDist, "from", vert, "to", nthPos) + if nthPos.X > vert.X || nthPos.X == vert.X && (nthPos.Y > vert.Y || nthPos.Y == vert.Y && (nthPos.Z > vert.Z)) { + continue // Only merge in a specific direction to avoid too many merges + } + // Get the modified triangle (we might have already changed another vertex) + // Move vert to the found closest triangle + for i, originalTriangle := range originalTriangles { + modifiedTriangle := modifiedTriangles[i] + if modifiedTriangle == nil { + continue // Deleted triangle + } + for i, v := range modifiedTriangle.V { + if v == vert { + modifiedTriangle.V[i] = nthPos + movedVertices = append(movedVertices, sdf.V3Set{vert, nthPos}) + break + } + } + if modifiedTriangle.Degenerate(0) { // We created a degenerate triangle by merging vertices + trianglesToPublish[*originalTriangle] = nil + } else { + trianglesToPublish[*originalTriangle] = modifiedTriangle + } + } + break + } + } + } + for _, vertMoved := range movedVertices { + prevTris := vertToPassOriginalTris[vertMoved[1]] + addedTris := vertToPassOriginalTris[vertMoved[0]] + prevTris = append(prevTris, addedTris...) + vertToPassOriginalTris[vertMoved[1]] = mtRemoveDuplicatesAndNils(prevTris) + delete(vertToPassOriginalTris, vertMoved[0]) + } + for _, modifiedTriangle := range trianglesToPublish { + if modifiedTriangle == nil { + continue // Deleted + } + output <- modifiedTriangle + } + // DO NOT CLOSE: will be closed by parent: close(output) + wgRet.Done() + }() + + return input, wgRet +} + +func bypassVertTris(isVertCloseToSeam func(v sdf.V3) bool, v *sdf.V3, tris []*Triangle3) bool { + bypass := v == nil || !isVertCloseToSeam(*v) + if bypass { + for _, tri := range tris { + bypass = bypass && !isVertCloseToSeam(tri.V[0]) && !isVertCloseToSeam(tri.V[1]) && !isVertCloseToSeam(tri.V[2]) + if !bypass { + break + } + } + } + return bypass +} + +func mtRemoveDuplicatesAndNils(strList []*Triangle3) []*Triangle3 { + var list []*Triangle3 + for _, item := range strList { + if item != nil && mtContains(list, item) == false { + list = append(list, item) + } + } + return list +} +func mtContains(s []*Triangle3, e *Triangle3) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/render/multithread3_test.go b/render/multithread3_test.go new file mode 100644 index 000000000..04e72fb21 --- /dev/null +++ b/render/multithread3_test.go @@ -0,0 +1,40 @@ +package render + +import ( + "github.com/deadsy/sdfx/sdf" + "log" + "sync" + "testing" +) + +func TestMergeVertices(t *testing.T) { + // Config + mergedOut := make(chan *Triangle3) + vertDist := 0.1 + trianglesIn, wg := NewMtRenderer3(nil, 1).mergeGeneratedVertices(mergedOut, vertDist, + sdf.V3{X: 0, Y: -1, Z: -0.001}, sdf.V3{X: 1, Y: 1, Z: 0.002}, sdf.V3{X: 1, Y: 1, Z: 0.002}) + + // Input data + for _, triIn := range []*Triangle3{ // 4 triangles forming a rectangle with 2 split by the middle + {V: [3]sdf.V3{{0, vertDist, 0}, {1, vertDist, 0}, {1, 1, 0}}}, + {V: [3]sdf.V3{{0, vertDist, 0}, {1, 1, 0}, {0, 1, 0}}}, + {V: [3]sdf.V3{{0, 0, 0}, {1, 0, 0}, {1, -1, 0}}}, + {V: [3]sdf.V3{{0, 0, 0}, {1, -1, 0}, {0, -1, 0}}}, + } { + trianglesIn <- triIn + } + close(trianglesIn) + + // Output processing + wg2 := &sync.WaitGroup{} + wg2.Add(1) + go func() { + defer wg2.Done() + for tri := range mergedOut { + log.Println(tri) // Should print 4 triangles with no hole in the middle + } + }() + wg.Wait() + close(mergedOut) + wg2.Wait() +}