diff --git a/README.md b/README.md index d09a406..697ab56 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,19 @@ for visualization or 3D printing file outputs. Quick jump to usage: [bolt exampl All images and shapes in readme were generated using this library. -![circle](https://github.com/user-attachments/assets/91c99f47-0c52-4cb1-83e7-452b03b69dff) ![bolt-example](https://github.com/user-attachments/assets/8da50871-2415-423f-beb3-0d78ad67c79e) +![circle](https://github.com/user-attachments/assets/91c99f47-0c52-4cb1-83e7-452b03b69dff) +![text](https://github.com/user-attachments/assets/73a90941-9279-449d-9f4d-3f2746af5dd5) ## Requirements + - [Go](https://go.dev/) - **Optional**: See latest requirements on [go-glfw](https://github.com/go-gl/glfw) if using GPU ## Features +- High test coverage (when GPU available, not the case in CI) + - Extremely coherent API design. - UI for visualizing parts, rendered directly from shaders. See [UI example](./examples/ui-mandala) by running `go run ./examples/ui-mandala` @@ -128,4 +132,4 @@ go run ./examples/fibonacci-showerhead -resdiv 350 36,16s user 0,76s system 100 ![iso-screw](https://github.com/user-attachments/assets/6bc987b9-d522-42a4-89df-71a20c3ae7ff) ![array-triangles](https://github.com/user-attachments/assets/6a479889-2836-464c-b8ea-82109a5aad13) -![geb-book-cover](https://github.com/user-attachments/assets/1ed945fb-5729-4028-bed8-26e0de3073ab) \ No newline at end of file +![geb-book-cover](https://github.com/user-attachments/assets/a6727481-07f3-4636-8e1c-9b1a02bb108f) \ No newline at end of file diff --git a/cpu_evaluators.go b/cpu_evaluators.go index d8bdd34..f89e26a 100644 --- a/cpu_evaluators.go +++ b/cpu_evaluators.go @@ -1,6 +1,8 @@ package gsdf import ( + "math" + "github.com/chewxy/math32" "github.com/soypat/glgl/math/ms1" "github.com/soypat/glgl/math/ms2" @@ -8,6 +10,13 @@ import ( "github.com/soypat/gsdf/gleval" ) +// minReduce takes element-wise minimum of arguments and stores to first argument. +func minReduce(d1AndDst, d2 []float32) { + for i := range d1AndDst { + d1AndDst[i] = math32.Min(d1AndDst[i], d2[i]) + } +} + func (u *sphere) Evaluate(pos []ms3.Vec, dist []float32, userData any) error { r := u.r for i, p := range pos { @@ -129,9 +138,7 @@ func (u *OpUnion) Evaluate(pos []ms3.Vec, dist []float32, userData any) error { if err != nil { return err } - for i, d := range dist { - dist[i] = math32.Min(d, auxDist[i]) - } + minReduce(dist, auxDist) } return nil } @@ -619,8 +626,8 @@ func (c *hex2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error { func (c *ellipse2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error { // https://iquilezles.org/articles/ellipsedist - a, b := c.a, c.b for i, p := range pos { + a, b := c.a, c.b p = ms2.AbsElem(p) if p.X > p.Y { p.X, p.Y = p.Y, p.X @@ -646,8 +653,8 @@ func (c *ellipse2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error co = (ry + signf(l)*rx + math32.Abs(g)/(rx*ry) - m) / 2 } else { h := 2 * m * n * math32.Sqrt(d) - s := signf(q+h) * math32.Pow(math32.Abs(q+h), 1./3.) - u := signf(q-h) * math32.Pow(math32.Abs(q-h), 1./3.) + s := signf(q+h) * math32.Cbrt(math32.Abs(q+h)) + u := signf(q-h) * math32.Cbrt(math32.Abs(q-h)) rx := -s - u - 4*c + 2*m2 ry := sqrt3 * (s - u) @@ -927,7 +934,6 @@ func (c *circarray) Evaluate(pos []ms3.Vec, dist []float32, userData any) error ncirc := float32(c.circleDiv) ninsm1 := float32(c.nInst - 1) for i, p := range pos { - pangle := math32.Atan2(p.Y, p.X) id := math32.Floor(pangle / angle) if id < 0 { @@ -958,9 +964,7 @@ func (c *circarray) Evaluate(pos []ms3.Vec, dist []float32, userData any) error if err != nil { return err } - for i, d := range dist { - dist[i] = math32.Min(d, dist1[i]) - } + minReduce(dist, dist1) return nil } @@ -1031,3 +1035,46 @@ func (l *lines2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error { } return nil } + +func (c *translateMulti2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error { + vp, err := gleval.GetVecPool(userData) + if err != nil { + return err + } + for i := range dist { + dist[i] = math.MaxFloat32 + } + d1 := vp.Float.Acquire(len(pos)) + defer vp.Float.Release(d1) + for _, p := range c.displacements { + t2d := translate2D{ + s: c.s, + p: p, + } + err = t2d.Evaluate(pos, d1, userData) + if err != nil { + return err + } + minReduce(dist, d1) + } + return nil +} + +func (c *rotation2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error { + sdf, err := gleval.AssertSDF2(c.s) + if err != nil { + return err + } + vp, err := gleval.GetVecPool(userData) + if err != nil { + return err + } + posTransf := vp.V2.Acquire(len(pos)) + defer vp.V2.Release(posTransf) + invT := c.tInv + for i, p := range pos { + posTransf[i] = ms2.MulMatVec(invT, p) + } + err = sdf.Evaluate(posTransf, dist, userData) + return err +} diff --git a/examples/image-text/text.go b/examples/image-text/text.go index 61c1d87..ef37a16 100644 --- a/examples/image-text/text.go +++ b/examples/image-text/text.go @@ -8,8 +8,6 @@ import ( "runtime" "time" - "github.com/chewxy/math32" - "github.com/soypat/glgl/math/ms1" "github.com/soypat/gsdf" "github.com/soypat/gsdf/forge/textsdf" "github.com/soypat/gsdf/glbuild" @@ -66,32 +64,12 @@ func main() { charHeight := sdf2.Bounds().Size().Y edgeAliasing := charHeight / 1000 + conversion := gsdfaux.ColorConversionLinearGradient(edgeAliasing, color.Black, color.White) start := time.Now() - err = gsdfaux.RenderPNGFile(filename, sdf2, 300, blackAndWhite(edgeAliasing)) + err = gsdfaux.RenderPNGFile(filename, sdf2, 300, conversion) if err != nil { log.Fatal(err) } + _ = conversion fmt.Println("PNG file rendered to", filename, "in", time.Since(start)) } - -func blackAndWhite(edgeSmooth float32) func(d float32) color.Color { - if edgeSmooth <= 0 { - return blackAndWhiteNoSmoothing - } - return func(d float32) color.Color { - // Smoothstep anti-aliasing near the edge - blend := 0.5 + 0.5*math32.Tanh(d/edgeSmooth) - // Clamp blend to [0, 1] for valid grayscale values - blend = ms1.Clamp(blend, 0, 1) - // Convert blend to grayscale - grayValue := uint8(blend * 255) - return color.Gray{Y: grayValue} - } -} - -func blackAndWhiteNoSmoothing(d float32) color.Color { - if d < 0 { - return color.Black - } - return color.White -} diff --git a/examples/ui-geb/uigeb.go b/examples/ui-geb/uigeb.go index 41fda69..fc5ed50 100644 --- a/examples/ui-geb/uigeb.go +++ b/examples/ui-geb/uigeb.go @@ -24,6 +24,7 @@ func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) { var f textsdf.Font f.Configure(textsdf.FontConfig{ RelativeGlyphTolerance: 0.01, + Builder: bld, }) err := f.LoadTTFBytes(textsdf.ISO3098TTF()) if err != nil { @@ -57,7 +58,7 @@ func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) { sclG := ms2.DivElem(sz, szG) sclE := ms2.DivElem(sz, szE) sclB := ms2.DivElem(sz, szB) - fmt.Println(sclG, sclE, sclB) + // Create 3D letters. L := sz.Max() G3 := bld.Extrude(G, L) @@ -89,6 +90,7 @@ func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) { func main() { var bld gsdf.Builder + // bld.SetFlags(gsdf.FlagUseShaderBuffers) shape, err := scene(&bld) shape = bld.Scale(shape, 0.3) if err != nil { diff --git a/forge/textsdf/font.go b/forge/textsdf/font.go index 063793a..5970ef0 100644 --- a/forge/textsdf/font.go +++ b/forge/textsdf/font.go @@ -16,9 +16,12 @@ import ( const firstBasic = '!' const lastBasic = '~' +var defaultBuilder = &gsdf.Builder{} + type FontConfig struct { // RelativeGlyphTolerance sets the permissible curve tolerance for glyphs. Must be between 0..1. If zero a reasonable value is chosen. RelativeGlyphTolerance float32 + Builder *gsdf.Builder } // Font implements font parsing and glyph (character) generation. @@ -28,8 +31,8 @@ type Font struct { // basicGlyphs optimized array access for common ASCII glyphs. basicGlyphs [lastBasic - firstBasic + 1]glyph // Other kinds of glyphs. - otherGlyphs map[rune]glyph - bld gsdf.Builder + otherGlyphs map[rune]*glyph + bld *gsdf.Builder reltol float32 // Set by config or reset call if zeroed. } @@ -39,6 +42,9 @@ func (f *Font) Configure(cfg FontConfig) error { } f.reset() f.reltol = cfg.RelativeGlyphTolerance + if cfg.Builder != nil { + f.bld = cfg.Builder + } return nil } @@ -59,7 +65,7 @@ func (f *Font) reset() { f.basicGlyphs[i] = glyph{} } if f.otherGlyphs == nil { - f.otherGlyphs = make(map[rune]glyph) + f.otherGlyphs = make(map[rune]*glyph) } else { for k := range f.otherGlyphs { delete(f.otherGlyphs, k) @@ -68,6 +74,9 @@ func (f *Font) reset() { if f.reltol == 0 { f.reltol = 0.15 } + if f.bld == nil { + f.bld = defaultBuilder + } } type glyph struct { @@ -133,30 +142,38 @@ func (f *Font) AdvanceWidth(c rune) float32 { // Glyph returns a SDF for a character defined by the argument rune. func (f *Font) Glyph(c rune) (_ glbuild.Shader2D, err error) { - var g glyph + g, err := f.glyph(c) + if err != nil { + return nil, err + } + return g.sdf, nil +} + +func (f *Font) glyph(c rune) (g *glyph, err error) { if c >= firstBasic && c <= lastBasic { // Basic ASCII glyph case. - g = f.basicGlyphs[c-firstBasic] + g = &f.basicGlyphs[c-firstBasic] if g.sdf == nil { // Glyph not yet created. create it. - g, err = f.makeGlyph(c) + gc, err := f.makeGlyph(c) if err != nil { return nil, err } - f.basicGlyphs[c-firstBasic] = g + *g = gc } - return g.sdf, nil + return g, nil } // Unicode or other glyph. g, ok := f.otherGlyphs[c] if !ok { - g, err = f.makeGlyph(c) + gc, err := f.makeGlyph(c) if err != nil { return nil, err } + g = &gc f.otherGlyphs[c] = g } - return g.sdf, nil + return g, nil } func (f *Font) scale() fixed.Int26_6 { @@ -180,7 +197,7 @@ func (f *Font) scaleout() float32 { func (f *Font) makeGlyph(char rune) (glyph, error) { g := &f.gb - bld := &f.bld + bld := f.bld idx := f.ttf.Index(char) scale := f.scale() @@ -219,8 +236,8 @@ func (f *Font) makeGlyph(char rune) (glyph, error) { func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int, tol, scale float32) (glbuild.Shader2D, bool, error) { var ( - sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: tol} - sum float32 + sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: tol} + windingSum float32 ) points = points[start:end] n := len(points) @@ -241,7 +258,7 @@ func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int, tol, // on-on Straight line. poly = append(poly, v0) i += 1 - sum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y) + windingSum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y) vPrev = v0 continue @@ -269,10 +286,10 @@ func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int, tol, } poly = append(poly, v0) // Append start point. poly = sampler.SampleBisect(poly, 4) - sum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y) + windingSum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y) vPrev = v0 } - return bld.NewPolygon(poly), sum > 0, bld.Err() + return bld.NewPolygon(poly), windingSum > 0, bld.Err() } func p2v(p truetype.Point, scale float32) ms2.Vec { @@ -282,12 +299,7 @@ func p2v(p truetype.Point, scale float32) ms2.Vec { } } -var quadBezier = ms2.NewSpline3([]float32{ - 1, 0, 0, 0, - -2, 2, 0, 0, - 1, -2, 1, 0, - 0, 0, 0, 0, -}) +var quadBezier = ms2.SplineBezierQuadratic() func onbits3(points []truetype.Point, start, end, i int) uint32 { n := end - start diff --git a/glbuild/glbuild.go b/glbuild/glbuild.go index d255461..2470243 100644 --- a/glbuild/glbuild.go +++ b/glbuild/glbuild.go @@ -268,33 +268,49 @@ func (p *Programmer) writeShaders(w io.Writer, nodes []Shader) (n int, objs []Sh clear(p.names) p.scratch = p.scratch[:0] p.objsScratch = p.objsScratch[:0] - currentBase := 2 + const startBase = 2 + currentBase := startBase + objIdx := 0 for i := len(nodes) - 1; i >= 0; i-- { - // Start by generating Shader Objects. + // Start by generating all Shader Objects. node := nodes[i] - prevIdx := len(p.objsScratch) p.objsScratch = node.AppendShaderObjects(p.objsScratch) - newObjects := p.objsScratch[prevIdx:] - for i := range newObjects { - if newObjects[i].Binding != -1 { - return n, nil, fmt.Errorf("shader buffer object binding should be set to -1 until shader generated for %T, %q", unwraproot(node), newObjects[i].NamePtr) + newObjs := p.objsScratch[objIdx:] + OBJWRITE: + for i := range newObjs { + obj := &newObjs[i] + if obj.Binding != -1 { + return n, nil, fmt.Errorf("shader buffer object binding should be set to -1 until shader generated for %T, %q", unwraproot(node), obj.NamePtr) } - newObjects[i].Binding = currentBase - currentBase++ - obj := newObjects[i] nameHash := hash(obj.NamePtr, 0) _, nameConflict := p.names[nameHash] if nameConflict { + oldObjs := p.objsScratch[:objIdx] + for _, old := range oldObjs { + conflictFound := nameHash == hash(old.NamePtr, 0) + if !conflictFound { + continue + } + // Conflict found! + if obj.Data == old.Data && obj.Size == old.Size && obj.Element == old.Element { + continue OBJWRITE // Skip this object, is duplicate and already has been added. + } + break // Conflict is not identical. + } return n, nil, fmt.Errorf("shader buffer object name conflict resolution not implemented: %T has buffer conflicting name %q of type %s", unwraproot(node), obj.NamePtr, obj.Element.String()) } + obj.Binding = currentBase + currentBase++ p.names[nameHash] = nameHash - blockName := unsafe.String(&obj.NamePtr[0], len(obj.NamePtr)) + "Buffer" - p.scratch, err = AppendShaderBufferDecl(p.scratch, blockName, "", obj) + blockName := string(obj.NamePtr) + "Buffer" + p.scratch, err = AppendShaderBufferDecl(p.scratch, blockName, "", *obj) if err != nil { return n, nil, err } } + objIdx += len(newObjs) } + if len(p.scratch) > 0 { // Write shader buffer declarations if any. ngot, err := w.Write(p.scratch) @@ -481,8 +497,7 @@ func AppendShaderBufferDecl(dst []byte, BlockName, instanceName string, ssbo Sha return nil, errors.New("AppendShaderBufferDecl requires BlockName for a valid SSBO declaration") } - const std = "std140" // Subject to change, would be provided by ShaderBuffer. - typename, err := glTypename(ssbo.Element) + typename, std, err := glTypename(ssbo.Element) if err != nil { return dst, fmt.Errorf("typename failed for %q: %w", ssbo.NamePtr, err) } @@ -522,14 +537,15 @@ func (obj ShaderObject) Validate() error { } else if obj.Binding < 0 { return errors.New("shader object negative binding point") } - _, err := glTypename(obj.Element) + _, _, err := glTypename(obj.Element) if err != nil { return err } return nil } -func glTypename(tp reflect.Type) (typename string, err error) { +func glTypename(tp reflect.Type) (typename, std string, err error) { + std = "std430" switch tp { case reflect.TypeOf(md2.Vec{}): typename = "dvec2" @@ -568,7 +584,7 @@ func glTypename(tp reflect.Type) (typename string, err error) { default: err = fmt.Errorf("equivalent type not implemented for %s", tp.String()) } - return typename, err + return typename, std, err } // AppendShaderSource appends the GL code of a single shader to the dst byte buffer. If dst's @@ -602,6 +618,7 @@ func AppendAllNodes(dst []Shader, root Shader) ([]Shader, error) { children := []Shader{root} nextChild := 0 nilChild := errors.New("got nil child in AppendAllNodes") + // found := make(map[Shader]struct{}) for len(children[nextChild:]) > 0 { newChildren := children[nextChild:] for _, obj := range newChildren { @@ -618,6 +635,10 @@ func AppendAllNodes(dst []Shader, root Shader) ([]Shader, error) { if s == nil || *s == nil { return nilChild } + // if _, skip := found[*s]; skip { + // return nil + // } + // found[*s] = struct{}{} children = append(children, *s) return nil }) @@ -627,6 +648,10 @@ func AppendAllNodes(dst []Shader, root Shader) ([]Shader, error) { if s == nil || *s == nil { return nilChild } + // if _, skip := found[*s]; skip { + // return nil + // } + // found[*s] = struct{}{} children = append(children, *s) return nil }) @@ -638,6 +663,10 @@ func AppendAllNodes(dst []Shader, root Shader) ([]Shader, error) { if s == nil || *s == nil { return nilChild } + // if _, skip := found[*s]; skip { + // return nil + // } + // found[*s] = struct{}{} children = append(children, *s) return nil }) diff --git a/go.mod b/go.mod index 5cb6beb..d37c144 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,13 @@ go 1.22.1 require ( github.com/chewxy/math32 v1.11.1 github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 - github.com/go-gl/glfw v0.0.0-20221017161538-93cebf72946b github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/soypat/glgl v0.0.0-20241124175250-a2463fe190a5 golang.org/x/image v0.22.0 ) -require golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 // indirect +require ( + github.com/go-gl/glfw v0.0.0-20221017161538-93cebf72946b // indirect + golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 // indirect +) diff --git a/go.sum b/go.sum index 684c6dd..678cdc0 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOY github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/soypat/glgl v0.0.0-20241121001014-cc8498d2a83d h1:kDdWM661L/RAxg0j4gV+18hky7/3Tvbhd8O6p8CLB7w= -github.com/soypat/glgl v0.0.0-20241121001014-cc8498d2a83d/go.mod h1:1LcEp6XHSMCI91WlJHzl/aW4Bp5v6yQOiYFyjrlk350= github.com/soypat/glgl v0.0.0-20241124175250-a2463fe190a5 h1:PyD0ceAopD2FDv3ddx99Q+h7QxIzDPPuOQiaZrRA7yU= github.com/soypat/glgl v0.0.0-20241124175250-a2463fe190a5/go.mod h1:1LcEp6XHSMCI91WlJHzl/aW4Bp5v6yQOiYFyjrlk350= golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws= diff --git a/gsdf.go b/gsdf.go index 5b88a88..cce4827 100644 --- a/gsdf.go +++ b/gsdf.go @@ -24,12 +24,16 @@ const ( epstol = 6e-7 ) +// Flags is a bitmask of values to control the functioning of the [Builder] type. type Flags uint64 const ( // FlagNoDimensionPanic controls panicking behavior on invalid shape dimension errors. // If set then these errors do not panic, instead storing the error for later inspection with [Builder.Err]. FlagNoDimensionPanic Flags = 1 << iota + // FlagUseShaderBuffers enforces the use of shader object for all newly built + // SDFs which require a dynamic array(s) to be rendered correctly. + FlagUseShaderBuffers ) // Builder wraps all SDF primitive and operation logic generation. @@ -42,8 +46,8 @@ type Builder struct { limVecGPU int } -func (bld *Builder) useGPU(n int) bool { - return bld.limVecGPU != 0 && n > bld.limVecGPU || n > 1 +func (bld *Builder) useGPU(_ int) bool { + return bld.flags&FlagUseShaderBuffers != 0 // bld.limVecGPU != 0 && n > bld.limVecGPU || n > 1 } func makeHashName[T any](dst []byte, name string, vec []T) []byte { diff --git a/gsdf2d.go b/gsdf2d.go index 3cf833e..7bd8db3 100644 --- a/gsdf2d.go +++ b/gsdf2d.go @@ -1,6 +1,7 @@ package gsdf import ( + "errors" "fmt" "math" "strconv" @@ -538,30 +539,36 @@ type poly2D struct { // NewPolygon creates a polygon from a set of vertices. The polygon can be self-intersecting. func (bld *Builder) NewPolygon(vertices []ms2.Vec) glbuild.Shader2D { + vertices, err := bld.validatePolygon(vertices) + if err != nil { + bld.shapeErrorf(err.Error()) + } + poly := poly2D{vert: vertices} + if bld.useGPU(len(vertices)) { + return &polyGPU{poly2D: poly, bufname: makeHashName(nil, "ssboPoly", vertices)} + } + return &poly +} + +func (bld *Builder) validatePolygon(vertices []ms2.Vec) ([]ms2.Vec, error) { prevIdx := len(vertices) - 1 if vertices[0] == vertices[prevIdx] { vertices = vertices[:prevIdx] // Discard last vertex if equal to first (this algorithm closes automatically). prevIdx-- } if len(vertices) < 3 { - bld.shapeErrorf("polygon needs at least 3 distinct vertices") + return vertices, errors.New("polygon needs at least 3 distinct vertices") } for i := range vertices { if math32.IsNaN(vertices[i].X) || math32.IsNaN(vertices[i].Y) { - bld.shapeErrorf("NaN value in vertices") + return vertices, errors.New("NaN value in vertices") } if vertices[i] == vertices[prevIdx] { - bld.shapeErrorf("found two consecutive equal vertices in polygon") + return vertices, errors.New("found two consecutive equal vertices in polygon") } prevIdx = i } - - poly := poly2D{vert: vertices} - if bld.useGPU(len(vertices)) { - // println("poly") - // return &polyGPU{poly2D: poly, bufname: makeHashName(nil, "ssboPoly", vertices)} - } - return &poly + return vertices, nil } func (c *poly2D) Bounds() ms2.Box { @@ -1301,3 +1308,64 @@ func (s *scale2D) AppendShaderBody(b []byte) []byte { func (u *scale2D) AppendShaderObjects(objects []glbuild.ShaderObject) []glbuild.ShaderObject { return objects } + +// TranslateMulti2D displaces N instances of s SDF to positions given by displacements of length N. +func (bld *Builder) TranslateMulti2D(s glbuild.Shader2D, displacements []ms2.Vec) glbuild.Shader2D { + if s == nil { + bld.nilsdf("TranslateMulti2D") + } + return &translateMulti2D{ + displacements: displacements, + s: s, + bufname: makeHashName(nil, "translateMulti2D", displacements), + } +} + +type translateMulti2D struct { + displacements []ms2.Vec + s glbuild.Shader2D + bufname []byte +} + +func (tm *translateMulti2D) Bounds() ms2.Box { + var bb ms2.Box + elemBox := tm.s.Bounds() + for i := range tm.displacements { + bb = bb.Union(elemBox.Add(tm.displacements[i])) + } + return bb +} + +func (tm *translateMulti2D) ForEach2DChild(userData any, fn func(userData any, s *glbuild.Shader2D) error) error { + return fn(userData, &tm.s) +} + +func (tm *translateMulti2D) AppendShaderName(b []byte) []byte { + b = append(b, "translateMulti2D_"...) + b = tm.s.AppendShaderName(b) + return b +} + +func (tm *translateMulti2D) AppendShaderBody(b []byte) []byte { + b = glbuild.AppendDefineDecl(b, "v", string(tm.bufname)) + b = fmt.Appendf(b, + `const int num = v.length(); + float d = 1.0e23; + for( int i=0; i 0 { + c = ms3.Vec{X: 0.9, Y: 0.6, Z: 0.3} + } else { + c = ms3.Vec{X: 0.65, Y: 0.85, Z: 1.0} + } + c = ms3.Scale(1-math.Exp(-6*math.Abs(d)), c) + c = ms3.Scale(0.8+0.2*math.Cos(150*d), c) + max := 1 - ms1.SmoothStep(0, 0.01, math.Abs(d)) + c = ms3.InterpElem(c, one, ms3.Vec{X: max, Y: max, Z: max}) + return color.RGBA{ + R: uint8(c.X * 255), + G: uint8(c.Y * 255), + B: uint8(c.Z * 255), + A: 255, + } + } +} + +// ColorConversionLinearGradient creates a color conversion function that creates a gradient centered +// along d=0 that extends gradientLength. +func ColorConversionLinearGradient(gradientLength float32, c0, c1 color.Color) func(d float32) color.Color { + if c0 == color.Black && c1 == color.White { + return blackAndWhiteLinearSmooth(gradientLength) + } + h0, s0, v0 := colorToHSV(c0) + h1, s1, v1 := colorToHSV(c1) + return func(d float32) color.Color { + // Smoothstep anti-aliasing near the edge + blend := d/gradientLength + 0.5 + if blend <= 0 { + return c0 + } else if blend >= 1 { + return c1 + } + // Clamp blend to [0, 1] for colors in gradient range. + h, s, v := interpHSV(h0, s0, v0, h1, s1, v1, blend) + r, g, b := hsvToRGB(h, s, v) + c := rgbToC(r, g, b) + // Convert blend to gradient. + return color.RGBA{R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c), A: 255} + } +} + +func blackAndWhiteLinearSmooth(edgeSmooth float32) func(d float32) color.Color { + if edgeSmooth == 0 { + return blackAndWhiteNoSmoothing + } + return func(d float32) color.Color { + // Smoothstep anti-aliasing near the edge + blend := d/edgeSmooth + 0.5 + // blend := 0.5 + 0.5*math32.Tanh(x) + if blend <= 0 { + return color.Black + } else if blend >= 1 { + return color.White + } + // Clamp blend to [0, 1] for valid grayscale values + blend = ms1.Clamp(blend, 0, 1) + // Convert blend to grayscale + grayValue := uint8(blend * 255) + return color.Gray{Y: grayValue} + } +} + +func blackAndWhiteNoSmoothing(d float32) color.Color { + if d < 0 { + return color.Black + } + return color.White +} + +func percentUint64(num, denom uint64) float32 { + return math.Trunc(10000*float32(num)/float32(denom)) / 100 +} + +func cInterp(c0, c1 uint32, t float32) uint32 { + h0, s0, v0 := rgbToHSV(cToRGB(c0)) + h1, s1, v1 := rgbToHSV(cToRGB(c1)) + return rgbToC(hsvToRGB(interpHSV(h0, s0, v0, h1, s1, v1, t))) +} + +func interpHSV(h0, s0, v0, h1, s1, v1, t float32) (h, s, v float32) { + switch { + case h1-h0 > 0.5: + h0 += 1.0 + case h1-h0 < -0.5: + h1 += 1.0 + } + h = ms1.Interp(h0, h1, t) + s = ms1.Interp(s0, s1, t) + v = ms1.Interp(v0, v1, t) + return h, s, v +} + +func colorToHSV(c color.Color) (h, s, v float32) { + r0, g0, b0, _ := c.RGBA() + return rgbToHSV(float32(r0>>8)/math.MaxUint8, float32(g0>>8)/math.MaxUint8, float32(b0>>8)/math.MaxUint8) +} + +// cToRGB converts a 24 bit RGB value stored in the least significant bits +func cToRGB(c uint32) (r, g, b float32) { + r = float32(uint8(c>>16)) / math.MaxUint8 + g = float32(uint8(c>>8)) / math.MaxUint8 + b = float32(uint8(c)) / math.MaxUint8 + return r, g, b +} + +// rgbToC converts r, g, and b float64 values on the range of 0.0 to 1.0 to a +// 24 bit RGB value stored in the least significant bits of a uint32. The inputs +// are clamped to the range of 0.0 to 1.0 +func rgbToC(r, g, b float32) (c uint32) { + return uint32(ms1.Clamp(r, 0, 1)*math.MaxUint8)<<16 | + uint32(ms1.Clamp(g, 0, 1)*math.MaxUint8)<<8 | + uint32(ms1.Clamp(b, 0, 1)*math.MaxUint8) +} + +// hsvToRGB converts hue, saturation and brightness values on the range of 0.0 +// to 1.0 to RGB floating point values on the range of 0.0 to 1.0 +func hsvToRGB(h, s, v float32) (r, g, b float32) { + var ( + c = s * v + x = c * (1 - math.Abs(math.Mod(h*6, 2)-1)) + m = v - c + ) + + switch { + case h >= 0 && h <= 1.0/6: + r, g, b = c, x, 0 + case h > 1.0/6 && h <= 2.0/6: + r, g, b = x, c, 0 + case h > 2.0/6 && h <= 3.0/6: + r, g, b = 0, c, x + case h > 3.0/6 && h <= 4.0/6: + r, g, b = 0, x, c + case h > 4.0/6 && h <= 5.0/6: + r, g, b = x, 0, c + case h > 5.0/6 && h <= 1.0: + r, g, b = c, 0, x + } + + r, g, b = r+m, g+m, b+m + return r, g, b +} + +// rgbToHSV converts red, green, and blue floating point values on the range +// 0.0 to 1.0 to hue, saturation and brightness values on the range 0.0 to 1.0 +func rgbToHSV(r, g, b float32) (h, s, v float32) { + var ( + xmax = max(r, g, b) + xmin = min(r, g, b) + c = xmax - xmin + ) + v = xmax + switch { + case c == 0: + h = 0 + case v == r: + h = (g - b) / (c * 6) + case v == g: + h = 1.0/3 + (b-r)/(c*6) + case v == b: + h = 2.0/3 + (r-g)/(c*6) + } + if h < 0 { + h += 1 + } + if xmax > 0 { + s = c / xmax + } + return +} diff --git a/gsdfaux/gsdfaux.go b/gsdfaux/gsdfaux.go index 3844c01..2cf623f 100644 --- a/gsdfaux/gsdfaux.go +++ b/gsdfaux/gsdfaux.go @@ -13,10 +13,7 @@ import ( "time" - "github.com/chewxy/math32" math "github.com/chewxy/math32" - "github.com/soypat/glgl/math/ms1" - "github.com/soypat/glgl/math/ms3" "github.com/soypat/glgl/v4.6-core/glgl" "github.com/soypat/gsdf" "github.com/soypat/gsdf/glbuild" @@ -273,40 +270,3 @@ func MakeGPUSDF2(s glbuild.Shader2D) (sdf gleval.SDF2, err error) { ShaderObjects: objects, }) } - -var red = color.RGBA{R: 255, A: 255} - -// ColorConversionInigoQuilez creates a new color conversion using [Inigo Quilez]'s style. -// A good value for characteristic distance is the bounding box diagonal divided by 3. Returns red for NaN values/ -// -// [Inigo Quilez]: https://iquilezles.org/articles/distfunctions2d/ -func ColorConversionInigoQuilez(characteristicDistance float32) func(float32) color.Color { - inv := 1. / characteristicDistance - return func(d float32) color.Color { - if math.IsNaN(d) { - return red - } - d *= inv - var one = ms3.Vec{X: 1, Y: 1, Z: 1} - var c ms3.Vec - if d > 0 { - c = ms3.Vec{X: 0.9, Y: 0.6, Z: 0.3} - } else { - c = ms3.Vec{X: 0.65, Y: 0.85, Z: 1.0} - } - c = ms3.Scale(1-math32.Exp(-6*math32.Abs(d)), c) - c = ms3.Scale(0.8+0.2*math32.Cos(150*d), c) - max := 1 - ms1.SmoothStep(0, 0.01, math32.Abs(d)) - c = ms3.InterpElem(c, one, ms3.Vec{X: max, Y: max, Z: max}) - return color.RGBA{ - R: uint8(c.X * 255), - G: uint8(c.Y * 255), - B: uint8(c.Z * 255), - A: 255, - } - } -} - -func percentUint64(num, denom uint64) float32 { - return math.Trunc(10000*float32(num)/float32(denom)) / 100 -} diff --git a/gsdfaux/gsdfaux_test.go b/gsdfaux/gsdfaux_test.go new file mode 100644 index 0000000..79b8949 --- /dev/null +++ b/gsdfaux/gsdfaux_test.go @@ -0,0 +1,24 @@ +package gsdfaux + +import ( + "image" + "image/color" + "image/png" + "os" + "testing" +) + +func TestColorGradient(t *testing.T) { + const Xdim = 256 + img := image.NewRGBA(image.Rect(0, 0, Xdim, Xdim)) + conv := ColorConversionLinearGradient(Xdim, color.White, red) + var x float32 = -Xdim / 2 + for i := range Xdim { + for j := range Xdim { + img.Set(i, j, conv(x)) + } + x += 1 + } + fp, _ := os.Create("test.png") + png.Encode(fp, img) +}