From 7053e7b9f2d6fbcb3253f63553ec9614c3780633 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Tue, 10 Aug 2021 19:33:30 +0200 Subject: [PATCH] Reverted back to go:embed. Parallelized generation of textures. Refactored animation rendering. --- animatedtexture.go | 46 ++++ animation.go | 39 ---- cmd/tins2021/app.go | 12 +- cmd/tins2021/appcontext.go | 37 ++- cmd/tins2021/extract.go | 15 +- cmd/tins2021/levelcontroller.go | 44 ++-- cmd/tins2021/resources/textures.txt | 2 +- .../chasing-monster.png} | Bin .../{images/dwarf.png => textures/gnome.png} | Bin .../resources/{images => textures}/heart.png | Bin .../random-walking-monster.png} | Bin .../resources/{images => textures}/star.png | Bin .../straight-walking-monster.png} | Bin cmd/tins2021/texturegenerator.go | 27 +++ cmd/tins2021/textures.go | 83 +++---- cmd/tins2021/tins2021.go | 15 +- meshanimation.go | 197 ++++++++++++++++ namedtexture.go | 39 ++++ scenery.go => polygons.go | 0 rendereranimation.go | 211 ------------------ 20 files changed, 398 insertions(+), 369 deletions(-) create mode 100644 animatedtexture.go rename cmd/tins2021/resources/{images/monster-chaser.png => textures/chasing-monster.png} (100%) rename cmd/tins2021/resources/{images/dwarf.png => textures/gnome.png} (100%) rename cmd/tins2021/resources/{images => textures}/heart.png (100%) rename cmd/tins2021/resources/{images/monster-random.png => textures/random-walking-monster.png} (100%) rename cmd/tins2021/resources/{images => textures}/star.png (100%) rename cmd/tins2021/resources/{images/monster-straight.png => textures/straight-walking-monster.png} (100%) create mode 100644 cmd/tins2021/texturegenerator.go create mode 100644 meshanimation.go create mode 100644 namedtexture.go rename scenery.go => polygons.go (100%) delete mode 100644 rendereranimation.go diff --git a/animatedtexture.go b/animatedtexture.go new file mode 100644 index 0000000..71fb708 --- /dev/null +++ b/animatedtexture.go @@ -0,0 +1,46 @@ +package tins2021 + +import ( + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg/ui" +) + +type AnimatedTexture struct { + texture NamedTexture + animation ui.Texture + frames []geom.RectangleF32 +} + +func newAnimatedTexture(texture NamedTexture, animation ui.Texture, n int) AnimatedTexture { + frames := make([]geom.RectangleF32, 0, n) + height := float32(animation.Height()) + width := float32(animation.Width()) + for i := 0; i < n; i++ { + left := width * float32(i) / float32(n) + right := width * float32(i+1) / float32(n) + frames = append(frames, geom.RectF32(left, 0, right, height)) + } + return AnimatedTexture{texture: texture, animation: animation, frames: frames} +} + +func NewAnimatedTexture(texture NamedTexture, n int) AnimatedTexture { + return newAnimatedTexture(texture, texture.Texture(), n) +} + +func FitAnimatedTexture(texture NamedTexture, scale float32, n int) AnimatedTexture { + height := float32(texture.Texture().Height()) + scale = geom.Round32(height*scale) / height // clip scale to integer width/height + return newAnimatedTexture(texture, texture.Scaled(scale), n) +} + +func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) { + renderer.DrawTexturePointOptions(t.animation, pos, ui.DrawOptions{Source: &t.frames[frame]}) +} + +func (t AnimatedTexture) FrameSize(i int) geom.PointF32 { return t.frames[i].Size() } + +func (t AnimatedTexture) Frames() int { return len(t.frames) } + +func (t AnimatedTexture) Scale(scale float32) AnimatedTexture { + return FitAnimatedTexture(t.texture, scale, t.Frames()) +} diff --git a/animation.go b/animation.go index 2188356..fbce36c 100644 --- a/animation.go +++ b/animation.go @@ -5,47 +5,8 @@ import ( "time" "opslag.de/schobers/geom" - "opslag.de/schobers/zntg/ui" ) -type AnimatedTexture struct { - Texture ui.Texture - Frames []geom.RectangleF32 -} - -func NewAnimatedTexture(texture ui.Texture, n int) AnimatedTexture { - frames := make([]geom.RectangleF32, 0, n) - height := float32(texture.Height()) - width := float32(texture.Width()) - for i := 0; i < n; i++ { - left := width * float32(i) / float32(n) - right := width * float32(i+1) / float32(n) - frames = append(frames, geom.RectF32(left, 0, right, height)) - } - return AnimatedTexture{Texture: texture, Frames: frames} -} - -func FitAnimatedTexture(textures *ui.Textures, name string, scale float32, n int) AnimatedTexture { - height := float32(textures.Texture(name).Height()) - scale = geom.Round32(height*scale) / height // clip scale to integer width/height - return NewAnimatedTexture(textures.ScaledByName(name, scale), n) -} - -func (t AnimatedTexture) Scale(scale float32) AnimatedTexture { - frames := make([]geom.RectangleF32, 0, len(t.Frames)) - for _, frame := range t.Frames { - frames = append(frames, geom.RectangleF32{Min: frame.Min.Mul(scale), Max: frame.Max.Mul(scale)}) - } - return AnimatedTexture{ - Texture: t.Texture, - Frames: frames, - } -} - -func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) { - renderer.DrawTexturePointOptions(t.Texture, pos, ui.DrawOptions{Source: &t.Frames[frame]}) -} - type Animation struct { LastUpdate time.Time Frame int diff --git a/cmd/tins2021/app.go b/cmd/tins2021/app.go index 91e9484..5c83568 100644 --- a/cmd/tins2021/app.go +++ b/cmd/tins2021/app.go @@ -35,18 +35,18 @@ const fpsOverlayName = `fps` func (a *app) Init(ctx ui.Context) error { if err := a.loadFonts(ctx, - fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12}, - fontDescriptor{"default", "fonts/escheresk.ttf", 48}, - fontDescriptor{"small", "fonts/escheresk.ttf", 16}, - fontDescriptor{"score", "fonts/FiraMono-Regular.ttf", 24}, - fontDescriptor{"title", "fonts/escher.ttf", 80}, + fontDescriptor{"debug", "resources/fonts/FiraMono-Regular.ttf", 12}, + fontDescriptor{"default", "resources/fonts/escheresk.ttf", 48}, + fontDescriptor{"small", "resources/fonts/escheresk.ttf", 16}, + fontDescriptor{"score", "resources/fonts/FiraMono-Regular.ttf", 24}, + fontDescriptor{"title", "resources/fonts/escher.ttf", 80}, ); err != nil { return err } textureLoader := tins2021.NewResourceLoader() textures := ctx.Textures() - if err := textureLoader.LoadFromFile(ctx.Resources(), "textures.txt", func(name, content string) error { + if err := textureLoader.LoadFromFile(ctx.Resources(), "resources/textures.txt", func(name, content string) error { _, err := textures.CreateTexturePath(name, content, true) return err }); err != nil { diff --git a/cmd/tins2021/appcontext.go b/cmd/tins2021/appcontext.go index 5d8a09e..1790338 100644 --- a/cmd/tins2021/appcontext.go +++ b/cmd/tins2021/appcontext.go @@ -1,8 +1,6 @@ package main import ( - "image" - "opslag.de/schobers/tins2021" "opslag.de/schobers/zntg/ui" ) @@ -12,34 +10,25 @@ type appContext struct { Debug bool - MonsterTextureNames map[tins2021.MonsterType]string + StarTexture tins2021.AnimatedTexture + HeartTexture tins2021.AnimatedTexture + + MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture } func newAppContext(ctx ui.Context, setView func(ui.Control)) *appContext { - newAnimatedTexture(ctx, "star", "images/star.png", defaultAnimationFrames, func() image.Image { - return tins2021.AnimatePolygon(tins2021.CreateStar(5), tins2021.Yellow, tins2021.NewRotateAnimation(defaultAnimationFrames)) - }) - newAnimatedTexture(ctx, "heart", "images/heart.png", defaultAnimationFrames, func() image.Image { - return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, tins2021.NewRotateAnimation(defaultAnimationFrames)) - }) - + textures := textureGenerator{} app := &appContext{ - setView: setView, - MonsterTextureNames: map[tins2021.MonsterType]string{ - tins2021.MonsterTypeStraight: "straight-walking-monster", - tins2021.MonsterTypeRandom: "random-walking-monster", - tins2021.MonsterTypeChaser: "chasing-monster", + setView: setView, + StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star), + HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart), + MonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{ + tins2021.MonsterTypeStraight: newAnimatedTexture(ctx, "straight-walking-monster", defaultAnimationFrames, textures.StraightWalkingMonster), + tins2021.MonsterTypeRandom: newAnimatedTexture(ctx, "random-walking-monster", defaultAnimationFrames, textures.RandomWalkingMonster), + tins2021.MonsterTypeChaser: newAnimatedTexture(ctx, "chasing-monster", defaultAnimationFrames, textures.ChasingMonster), }, } - newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeStraight], "images/monster-straight.png", defaultAnimationFrames, func() image.Image { - return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Green, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30)) - }) - newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeRandom], "images/monster-random.png", defaultAnimationFrames, func() image.Image { - return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Blue, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30)) - }) - newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeChaser], "images/monster-chaser.png", defaultAnimationFrames, func() image.Image { - return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Purple, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30)) - }) + return app } diff --git a/cmd/tins2021/extract.go b/cmd/tins2021/extract.go index eca51be..66dc907 100644 --- a/cmd/tins2021/extract.go +++ b/cmd/tins2021/extract.go @@ -1,12 +1,11 @@ package main import ( + "embed" "io" "io/fs" "os" "path/filepath" - - rice "github.com/GeertJohan/go.rice" ) func copyFile(path string, file fs.File) error { @@ -21,19 +20,19 @@ func copyFile(path string, file fs.File) error { return err } -func copyBoxToDisk() error { - box := rice.MustFindBox(`resources`) - return box.Walk("", func(path string, info os.FileInfo, err error) error { +func copyBoxToDisk(resources embed.FS) error { + return fs.WalkDir(resources, "", func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } - if info.IsDir() { + if entry.IsDir() { return nil } - src, err := box.Open(path) + src, err := resources.Open(path) if err != nil { return err } - return copyFile(filepath.Join(box.Name(), path), src) + defer src.Close() + return copyFile(filepath.Join(`resources`, path), src) }) } diff --git a/cmd/tins2021/levelcontroller.go b/cmd/tins2021/levelcontroller.go index f152dbf..d6e1762 100644 --- a/cmd/tins2021/levelcontroller.go +++ b/cmd/tins2021/levelcontroller.go @@ -20,7 +20,8 @@ type levelController struct { Level *tins2021.Level - Cubes cubeTexture + Cube tins2021.NamedTexture + Inverted tins2021.NamedTexture Animations map[string]*tins2021.Animations IdleMonsters *tins2021.Animations @@ -32,7 +33,8 @@ type levelController struct { func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController { control := &levelController{app: app} textures := ctx.Textures() - control.Cubes = newCubeTexture(textures, tins2021.Orange) + control.Cube = tins2021.MustCreateNamedTextureImage(textures, "cube", tins2021.GenerateCube(tins2021.Orange)) + control.Inverted = tins2021.MustCreateNamedTextureImage(textures, "cube_inverted", tins2021.GenerateHole(tins2021.Orange)) small, err := tins2021.NewBitmapFont(ctx.Renderer(), ctx.Fonts().Font("small"), tins2021.NumericCharacters...) if err != nil { @@ -146,17 +148,15 @@ func (r *levelController) Play(level *tins2021.Level) { r.Level = level r.Animations = map[string]*tins2021.Animations{ - "star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true), - "heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true), + "star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true), + "heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true), + "monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true), } r.IdleMonsters = tins2021.NewAnimations(200*time.Millisecond, 100, false, false) r.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false) for monster := range level.Monsters { r.IdleMonsters.Frame(monster) } - for _, monster := range r.app.MonsterTextureNames { - r.Animations[monster] = tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true) - } } const defaultAnimationFrames = 20 @@ -189,20 +189,21 @@ func (r levelController) Render(ctx ui.Context) { } renderer := ctx.Renderer() - textures := ctx.Textures() - cubes := r.Cubes.Scaled(textures, scale) + cube := r.Cube.Scaled(scale) + inverted := r.Inverted.Scaled(scale) - cubeWidth := float32(cubes.Normal.Width()) - cubeHeight := float32(cubes.Normal.Height()) + cubeWidth := float32(cube.Width()) + cubeHeight := float32(cube.Height()) player := ctx.Textures().ScaledByName("dwarf", scale*.6) - star := tins2021.FitAnimatedTexture(textures, "star", scale*.4, defaultAnimationFrames) - heart := tins2021.FitAnimatedTexture(textures, "heart", scale*.4, defaultAnimationFrames) + star := r.app.StarTexture.Scale(scale * .4) + heart := r.app.HeartTexture.Scale(scale * .4) monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{} - for typ, name := range r.app.MonsterTextureNames { - monsterTextures[typ] = tins2021.FitAnimatedTexture(textures, name, scale*.4, defaultAnimationFrames) + for typ, animation := range r.app.MonsterTextures { + monsterTextures[typ] = animation.Scale(scale * .4) } - propOffset := geom.PtF32(-.5*float32(star.Texture.Height()), -.8*float32(star.Texture.Height())) + propHeight := star.FrameSize(0).Y + propOffset := geom.PtF32(-.5*float32(propHeight), -.8*float32(propHeight)) distances := r.Level.Tiles.Distances(r.Level.Player) @@ -222,9 +223,9 @@ func (r levelController) Render(ctx ui.Context) { continue } screenPos, platformPos := positionOfTile(pos) - tileTexture := cubes.Normal.Texture + tileTexture := cube if tile.Inversed { - tileTexture = cubes.Inversed.Texture + tileTexture = inverted } renderer.DrawTexturePoint(tileTexture, screenPos) if r.app.Debug { @@ -253,7 +254,6 @@ func (r levelController) Render(ctx ui.Context) { if tile == nil { continue } - name := r.app.MonsterTextureNames[monsterType.Type()] texture := monsterTextures[monsterType.Type()] _, platformPos := positionOfTile(pos) if target, ok := r.Level.MonsterTargets[pos]; ok { @@ -262,15 +262,15 @@ func (r levelController) Render(ctx ui.Context) { delta := targetPlatformPos.Sub(platformPos) curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth) interpolatedPos := platformPos.Add(delta.Mul(dt)).Sub(curve) - texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations[name].Frame(pos)) + texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations["monster"].Frame(pos)) } else { - texture.Draw(renderer, platformPos.Add(propOffset), r.Animations[name].Frame(pos)) + texture.Draw(renderer, platformPos.Add(propOffset), r.Animations["monster"].Frame(pos)) } } textColor := ctx.Style().Palette.Text scoreFont := ctx.Fonts().Font("score") - fontOffsetY := .5 * (float32(star.Texture.Height()) - scoreFont.Height()) + fontOffsetY := .5 * (float32(propHeight) - scoreFont.Height()) // stars & hearts scoreTopLeft := scoreView.Min diff --git a/cmd/tins2021/resources/textures.txt b/cmd/tins2021/resources/textures.txt index 0dcd41e..6411f61 100644 --- a/cmd/tins2021/resources/textures.txt +++ b/cmd/tins2021/resources/textures.txt @@ -1 +1 @@ -dwarf: images/dwarf.png \ No newline at end of file +dwarf: resources/textures/gnome.png \ No newline at end of file diff --git a/cmd/tins2021/resources/images/monster-chaser.png b/cmd/tins2021/resources/textures/chasing-monster.png similarity index 100% rename from cmd/tins2021/resources/images/monster-chaser.png rename to cmd/tins2021/resources/textures/chasing-monster.png diff --git a/cmd/tins2021/resources/images/dwarf.png b/cmd/tins2021/resources/textures/gnome.png similarity index 100% rename from cmd/tins2021/resources/images/dwarf.png rename to cmd/tins2021/resources/textures/gnome.png diff --git a/cmd/tins2021/resources/images/heart.png b/cmd/tins2021/resources/textures/heart.png similarity index 100% rename from cmd/tins2021/resources/images/heart.png rename to cmd/tins2021/resources/textures/heart.png diff --git a/cmd/tins2021/resources/images/monster-random.png b/cmd/tins2021/resources/textures/random-walking-monster.png similarity index 100% rename from cmd/tins2021/resources/images/monster-random.png rename to cmd/tins2021/resources/textures/random-walking-monster.png diff --git a/cmd/tins2021/resources/images/star.png b/cmd/tins2021/resources/textures/star.png similarity index 100% rename from cmd/tins2021/resources/images/star.png rename to cmd/tins2021/resources/textures/star.png diff --git a/cmd/tins2021/resources/images/monster-straight.png b/cmd/tins2021/resources/textures/straight-walking-monster.png similarity index 100% rename from cmd/tins2021/resources/images/monster-straight.png rename to cmd/tins2021/resources/textures/straight-walking-monster.png diff --git a/cmd/tins2021/texturegenerator.go b/cmd/tins2021/texturegenerator.go new file mode 100644 index 0000000..9d375af --- /dev/null +++ b/cmd/tins2021/texturegenerator.go @@ -0,0 +1,27 @@ +package main + +import ( + "image" + + "opslag.de/schobers/tins2021" +) + +type textureGenerator struct{} + +func (textureGenerator) Star() image.Image { + return tins2021.AnimatePolygon(tins2021.CreateStar(5), tins2021.Yellow, defaultAnimationFrames, tins2021.MeshRotateAnimation{}) +} + +func (textureGenerator) Heart() image.Image { + return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, defaultAnimationFrames, tins2021.MeshRotateAnimation{}) +} + +func (textureGenerator) monster(color string) image.Image { + return tins2021.AnimatePolygon(tins2021.CreateHexagon(), color, defaultAnimationFrames, tins2021.MeshWobbleTransformation{Wobble: 30}) +} + +func (g textureGenerator) ChasingMonster() image.Image { return g.monster(tins2021.Purple) } + +func (g textureGenerator) RandomWalkingMonster() image.Image { return g.monster(tins2021.Blue) } + +func (g textureGenerator) StraightWalkingMonster() image.Image { return g.monster(tins2021.Green) } diff --git a/cmd/tins2021/textures.go b/cmd/tins2021/textures.go index 6228cb3..d60ed73 100644 --- a/cmd/tins2021/textures.go +++ b/cmd/tins2021/textures.go @@ -1,48 +1,28 @@ package main import ( + "fmt" "image" "image/png" + "log" "os" + "path/filepath" + "time" "opslag.de/schobers/tins2021" "opslag.de/schobers/zntg/ui" ) -type namedTexture struct { - ui.Texture - - Name string +func chrono(action func()) time.Duration { + start := time.Now() + action() + return time.Now().Sub(start) } -func newNamedTexture(textures *ui.Textures, name string, im image.Image) namedTexture { - texture, err := textures.CreateTextureGo(name, im, true) - if err != nil { - panic(err) - } - return namedTexture{texture, name} -} - -func (t namedTexture) Scaled(textures *ui.Textures, scale float32) namedTexture { - return namedTexture{textures.ScaledByName(t.Name, scale), t.Name} -} - -type cubeTexture struct { - Normal, Inversed namedTexture -} - -func newCubeTexture(textures *ui.Textures, color string) cubeTexture { - return cubeTexture{ - Normal: newNamedTexture(textures, "cube_"+color, tins2021.GenerateCube(color)), - Inversed: newNamedTexture(textures, "cube_"+color+"_inversed", tins2021.GenerateHole(color)), - } -} - -func (t cubeTexture) Scaled(textures *ui.Textures, scale float32) cubeTexture { - return cubeTexture{ - Normal: t.Normal.Scaled(textures, scale), - Inversed: t.Inversed.Scaled(textures, scale), - } +func chronoErr(action func() error) (time.Duration, error) { + start := time.Now() + err := action() + return time.Now().Sub(start), err } func loadTextureImage(ctx ui.Context, resource string) image.Image { @@ -58,26 +38,29 @@ func loadTextureImage(ctx ui.Context, resource string) image.Image { return im } -func saveTextureImage(resource string, im image.Image) { - out, err := os.Create(resource) - if err != nil { - return - } - defer out.Close() - png.Encode(out, im) -} - -func newAnimatedTexture(ctx ui.Context, name, resource string, frames int, render animatedTextureRenderFn) tins2021.AnimatedTexture { - raw := loadTextureImage(ctx, resource) +func newAnimatedTexture(ctx ui.Context, name string, frames int, render func() image.Image) tins2021.AnimatedTexture { + resourceName := fmt.Sprintf("resources/textures/%s.png", name) + raw := loadTextureImage(ctx, resourceName) if raw == nil { - raw = render() - saveTextureImage(resource, raw) - } - texture, err := ctx.Textures().CreateTextureGo(name, raw, true) - if err != nil { - panic(err) + renderTime := chrono(func() { + raw = render() + }) + if err := saveTextureImage(resourceName, raw); err != nil { + log.Printf("failed to write animated texture \"%s\" to disk (%s); error: %v\n", name, resourceName, err) + } else { + log.Printf("generated animated texture \"%s\" in %v (and saved to disk \"%s\")\n", name, renderTime, resourceName) + } } + texture := tins2021.MustCreateNamedTextureImage(ctx.Textures(), name, raw) return tins2021.NewAnimatedTexture(texture, frames) } -type animatedTextureRenderFn func() image.Image +func saveTextureImage(resource string, im image.Image) error { + os.MkdirAll(filepath.Dir(resource), 0744) + out, err := os.Create(resource) + if err != nil { + return err + } + defer out.Close() + return png.Encode(out, im) +} diff --git a/cmd/tins2021/tins2021.go b/cmd/tins2021/tins2021.go index 576d0d6..797d5e6 100644 --- a/cmd/tins2021/tins2021.go +++ b/cmd/tins2021/tins2021.go @@ -1,23 +1,23 @@ package main import ( + "embed" "flag" "image/color" "log" - rice "github.com/GeertJohan/go.rice" "opslag.de/schobers/geom" "opslag.de/schobers/tins2021" "opslag.de/schobers/zntg" - "opslag.de/schobers/zntg/addons/riceres" + "opslag.de/schobers/zntg/addons/embedres" "opslag.de/schobers/zntg/ui" ) // #cgo windows LDFLAGS: -Wl,-subsystem,windows import "C" -//go:generate go get -u github.com/GeertJohan/go.rice/rice -//go:generate rice embed +//go:embed resources +var resources embed.FS func main() { err := run() @@ -27,9 +27,8 @@ func main() { } func openResources() ui.Resources { - box := rice.MustFindBox(`resources`) - embedded := riceres.New(box) - return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded) + embedded := embedres.New(resources) + return ui.NewFallbackResources(ui.NewPathResources(nil, `resources`), embedded) } func run() error { @@ -38,7 +37,7 @@ func run() error { flag.Parse() if extract { - return copyBoxToDisk() + return copyBoxToDisk(resources) } res := openResources() diff --git a/meshanimation.go b/meshanimation.go new file mode 100644 index 0000000..6a6c89a --- /dev/null +++ b/meshanimation.go @@ -0,0 +1,197 @@ +package tins2021 + +import ( + "fmt" + "image" + "os" + "runtime" + "sync" + + "github.com/fogleman/fauxgl" + "github.com/nfnt/resize" + "golang.org/x/image/draw" + "opslag.de/schobers/geom" + "opslag.de/schobers/geom/ints" + "opslag.de/schobers/zntg/ui" +) + +const ( + fovy = 40 // vertical field of view in degrees + near = 1 // near clipping plane + far = 10 // far clipping plane +) + +var ( + eye = fauxgl.V(0, 0, 4) // camera position + center = fauxgl.V(0, 0, 0) // view center position + up = fauxgl.V(0, 1, 0) // up vector + light = fauxgl.V(.5, 1, .75).Normalize() // light direction +) + +func animateMesh(mesh *fauxgl.Mesh, hexColor string, frames int, transform MeshAnimationTransformer) image.Image { + const scale = 4 + const s = 1.1 + + mesh.BiUnitCube() + + matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up)) + animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize)) + + threads := ints.Max(1, runtime.NumCPU()) + framesC := make(chan int, threads) + + wait := parallel(threads, func() { + context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale) + color := fauxgl.HexColor(hexColor) + + for i := range framesC { + context.ClearDepthBuffer() + context.ClearColorBufferWith(fauxgl.Transparent) + + shader := fauxgl.NewPhongShader(matrix, light, eye) + shader.ObjectColor = color + shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`)) + context.Shader = shader + + copy := mesh.Copy() + transform.transform(copy, FrameState{Current: i, TotalFrames: frames}) + context.DrawMesh(copy) + + frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear) + draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil) + } + }) + + for f := 0; f < frames; f++ { + framesC <- f + } + close(framesC) + wait.Wait() + return animation +} + +func AnimatePolygon(polygon geom.PolygonF, hexColor string, frames int, transform MeshAnimationTransformer) image.Image { + mesh := generateMeshFromPolygon(polygon, .2) + return animateMesh(mesh, hexColor, frames, transform) +} + +func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames int, transform MeshAnimationTransformer) image.Image { + path, err := resources.FetchResource(name) + if err != nil { + panic(err) + } + mesh, err := fauxgl.LoadSTL(path) + if err != nil { + panic(err) + } + return animateMesh(mesh, hexColor, frames, transform) +} + +type FrameState struct { + Current int + TotalFrames int +} + +func (s FrameState) Animation() float64 { return float64(s.Current) / float64(s.TotalFrames) } + +func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh { + vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) } + tri := fauxgl.NewTriangleForPoints + face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle { + return tri(vec(q, n*thickness), vec(r, n*thickness), vec(s, n*thickness)) + } + var triangles []*fauxgl.Triangle + // generate front & back + for _, t := range polygon.Triangulate() { + triangles = append(triangles, + face(t.Points[0], t.Points[1], t.Points[2], 1), // front + face(t.Points[2], t.Points[1], t.Points[0], -1), // back + ) + } + // generate side + back, front := -thickness, thickness + for i, p := range polygon.Points { + next := polygon.Points[(i+1)%len(polygon.Points)] + q, r, s, t := vec(p, back), vec(next, back), vec(next, front), vec(p, front) + triangles = append(triangles, tri(q, r, s), tri(q, s, t)) + } + + mesh := fauxgl.NewTriangleMesh(triangles) + return mesh +} + +func iterate(n int, threads int) <-chan int { + iterator := make(chan int, threads) + go func() { + for i := 0; i < n; i++ { + iterator <- i + } + }() + return iterator +} + +func parallel(n int, action func()) *sync.WaitGroup { + wait := &sync.WaitGroup{} + wait.Add(n) + for i := 0; i < n; i++ { + go func() { + action() + wait.Done() + }() + } + return wait +} + +type MeshAnimationTransformer interface { + transform(*fauxgl.Mesh, FrameState) +} + +type MeshRotateAnimation struct{} + +func (MeshRotateAnimation) transform(mesh *fauxgl.Mesh, s FrameState) { + mesh.Transform(fauxgl.Rotate(up, 2*geom.Pi*s.Animation())) +} + +type MeshWobbleTransformation struct { + Wobble float64 +} + +func (a MeshWobbleTransformation) animate(s FrameState) float64 { + animation := float64(s.Current) / float64(s.TotalFrames) + animation += .25 + if animation >= 1 { + animation -= 1 + } + return geom.Abs(animation*4-2) - 1 +} + +func (a MeshWobbleTransformation) transform(mesh *fauxgl.Mesh, s FrameState) { + mesh.Transform(fauxgl.Rotate(up, a.animate(s)*a.Wobble*geom.Pi/180)) +} + +func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error { + stl, err := os.Create(path) + if err != nil { + return err + } + defer stl.Close() + + fmt.Fprintf(stl, "solid %s\n", name) + for _, triangle := range mesh.Triangles { + normal := triangle.Normal() + fmt.Fprintf(stl, " facet normal %f, %f, %f\n", normal.X, normal.Y, normal.Z) + fmt.Fprintf(stl, " outer loop\n") + fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V1.Position.X, triangle.V1.Position.Y, triangle.V1.Position.Z) + fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V2.Position.X, triangle.V2.Position.Y, triangle.V2.Position.Z) + fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V3.Position.X, triangle.V3.Position.Y, triangle.V3.Position.Z) + fmt.Fprintf(stl, " endloop\n") + fmt.Fprintf(stl, " endfacet\n") + } + fmt.Fprintf(stl, "endsolid %s\n", name) + return nil +} + +func SaveSTLFromPolygon(path, name string, polygon geom.PolygonF, thickness float64) { + mesh := generateMeshFromPolygon(polygon, thickness) + saveMeshSTL(path, name, mesh) +} diff --git a/namedtexture.go b/namedtexture.go new file mode 100644 index 0000000..1451001 --- /dev/null +++ b/namedtexture.go @@ -0,0 +1,39 @@ +package tins2021 + +import ( + "image" + + "opslag.de/schobers/zntg/ui" +) + +type NamedTexture struct { + textures *ui.Textures + name string +} + +func NewNamedTexture(textures *ui.Textures, name string) NamedTexture { + return NamedTexture{ + textures: textures, + name: name, + } +} + +func CreateNamedTextureImage(textures *ui.Textures, name string, im image.Image) (NamedTexture, error) { + _, err := textures.CreateTextureGo(name, im, true) + if err != nil { + return NamedTexture{}, err + } + return NewNamedTexture(textures, name), nil +} + +func MustCreateNamedTextureImage(textures *ui.Textures, name string, im image.Image) NamedTexture { + texture, err := CreateNamedTextureImage(textures, name, im) + if err != nil { + panic(err) + } + return texture +} + +func (t NamedTexture) Scaled(scale float32) ui.Texture { return t.textures.ScaledByName(t.name, scale) } + +func (t NamedTexture) Texture() ui.Texture { return t.textures.Texture(t.name) } diff --git a/scenery.go b/polygons.go similarity index 100% rename from scenery.go rename to polygons.go diff --git a/rendereranimation.go b/rendereranimation.go deleted file mode 100644 index 6f1d854..0000000 --- a/rendereranimation.go +++ /dev/null @@ -1,211 +0,0 @@ -package tins2021 - -import ( - "fmt" - "image" - "os" - - "github.com/fogleman/fauxgl" - "github.com/nfnt/resize" - "golang.org/x/image/draw" - "opslag.de/schobers/geom" - "opslag.de/schobers/zntg/ui" -) - -const ( - fovy = 40 // vertical field of view in degrees - near = 1 // near clipping plane - far = 10 // far clipping plane -) - -var ( - eye = fauxgl.V(0, 0, 4) // camera position - center = fauxgl.V(0, 0, 0) // view center position - up = fauxgl.V(0, 1, 0) // up vector - light = fauxgl.V(.5, 1, .75).Normalize() // light direction -) - -func AnimatePolygon(polygon geom.PolygonF, hexColor string, renderer AnimationRenderer) image.Image { - mesh := generateMeshFromPolygon(polygon, .2) - renderer.setup(mesh) - return renderMeshAnimation(hexColor, renderer.frames(), renderer.render) -} - -func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, renderer AnimationRenderer) image.Image { - path, err := resources.FetchResource(name) - if err != nil { - panic(err) - } - mesh, err := fauxgl.LoadSTL(path) - if err != nil { - panic(err) - } - renderer.setup(mesh) - return renderMeshAnimation(hexColor, renderer.frames(), renderer.render) -} - -type animationRendererBase struct { - Frames int - Mesh *fauxgl.Mesh -} - -func (r animationRendererBase) frames() int { return r.Frames } - -func (r *animationRendererBase) setup(mesh *fauxgl.Mesh) { - r.Mesh = mesh - mesh.BiUnitCube() -} - -var _ AnimationRenderer = &RotateAnimationRenderer{} -var _ AnimationRenderer = &WobbleAnimationRenderer{} - -type AnimationRenderer interface { - frames() int - setup(*fauxgl.Mesh) - render(*fauxgl.Context, int, float64) -} - -func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh { - vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) } - tri := fauxgl.NewTriangleForPoints - face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle { - return tri(vec(q, n*thickness), vec(r, n*thickness), vec(s, n*thickness)) - } - var triangles []*fauxgl.Triangle - // generate front & back - for _, t := range polygon.Triangulate() { - triangles = append(triangles, - face(t.Points[0], t.Points[1], t.Points[2], 1), // front - face(t.Points[2], t.Points[1], t.Points[0], -1), // back - ) - } - // generate side - back, front := -thickness, thickness - for i, p := range polygon.Points { - next := polygon.Points[(i+1)%len(polygon.Points)] - q, r, s, t := vec(p, back), vec(next, back), vec(next, front), vec(p, front) - triangles = append(triangles, tri(q, r, s), tri(q, s, t)) - } - - mesh := fauxgl.NewTriangleMesh(triangles) - return mesh -} - -func renderMeshAnimation(hexColor string, frames int, render func(*fauxgl.Context, int, float64)) image.Image { - const scale = 4 - context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale) - - // matrix := fauxgl.LookAt(eye, center, up).Perspective(fovy, 1, near, far) - - const s = 1.1 - // rot3 := func(m fauxgl.Matrix) fauxgl.Matrix { - // return fauxgl.Matrix{ - // X00: m.X20, X01: m.X10, X02: m.X00, X03: m.X03, - // X10: m.X21, X11: m.X11, X12: m.X01, X13: m.X13, - // X20: m.X22, X21: m.X12, X22: m.X02, X23: m.X23, - // X30: m.X30, X31: m.X31, X32: m.X32, X33: m.X33, - // } - // } - - // sqrt_6_1 := 1 / geom.Sqrt(6) - // iso := fauxgl.Matrix{ - // X00: sqrt_6_1 * geom.Sqrt(3), X01: 0, X02: -sqrt_6_1 * geom.Sqrt(3), X03: 0, - // X10: sqrt_6_1, X11: 2 * sqrt_6_1, X12: sqrt_6_1, X13: 0, - // X20: sqrt_6_1 * geom.Sqrt(2), X21: -sqrt_6_1 * geom.Sqrt(2), X22: sqrt_6_1 * geom.Sqrt(2), X23: 0, - // X30: 0, X31: 0, X32: 0, X33: 1} - - matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up)) - - color := fauxgl.HexColor(hexColor) - - animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize)) - for i := 0; i < frames; i++ { - context.ClearDepthBuffer() - context.ClearColorBufferWith(fauxgl.Transparent) - - shader := fauxgl.NewPhongShader(matrix, light, eye) - shader.ObjectColor = color - shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`)) - context.Shader = shader - - render(context, i, float64(i)/float64(frames)) - - frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear) - draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil) - } - return animation -} - -type RotateAnimationRenderer struct { - animationRendererBase - - Rotation float64 -} - -func NewRotateAnimation(frames int) AnimationRenderer { - return &RotateAnimationRenderer{ - animationRendererBase: animationRendererBase{Frames: frames}, - Rotation: 2 * geom.Pi / float64(frames), - } -} - -func (a RotateAnimationRenderer) render(context *fauxgl.Context, _ int, _ float64) { - context.DrawMesh(a.Mesh) - a.Mesh.Transform(fauxgl.Rotate(up, a.Rotation)) -} - -func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error { - stl, err := os.Create(path) - if err != nil { - return err - } - defer stl.Close() - - fmt.Fprintf(stl, "solid %s\n", name) - for _, triangle := range mesh.Triangles { - normal := triangle.Normal() - fmt.Fprintf(stl, " facet normal %f, %f, %f\n", normal.X, normal.Y, normal.Z) - fmt.Fprintf(stl, " outer loop\n") - fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V1.Position.X, triangle.V1.Position.Y, triangle.V1.Position.Z) - fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V2.Position.X, triangle.V2.Position.Y, triangle.V2.Position.Z) - fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V3.Position.X, triangle.V3.Position.Y, triangle.V3.Position.Z) - fmt.Fprintf(stl, " endloop\n") - fmt.Fprintf(stl, " endfacet\n") - } - fmt.Fprintf(stl, "endsolid %s\n", name) - return nil -} - -func SaveSTLFromPolygon(path, name string, polygon geom.PolygonF, thickness float64) { - mesh := generateMeshFromPolygon(polygon, thickness) - saveMeshSTL(path, name, mesh) -} - -type WobbleAnimationRenderer struct { - animationRendererBase - - Wobble float64 -} - -func NewWobbleAnimation(frames int, wobble float64) AnimationRenderer { - return &WobbleAnimationRenderer{ - animationRendererBase: animationRendererBase{Frames: frames}, - Wobble: wobble, - } -} - -func (a WobbleAnimationRenderer) animate(frame float64) float64 { - frame += .25 - if frame >= 1 { - frame -= 1 - } - // return geom.Cos(float64(frame) * 2 * geom.Pi / float64(a.Frames)) - return geom.Abs(frame*4-2) - 1 -} - -func (a WobbleAnimationRenderer) render(context *fauxgl.Context, frame int, animation float64) { - context.DrawMesh(a.Mesh) - curr := a.animate(animation) - next := a.animate(float64(frame+1) / float64(a.Frames)) - a.Mesh.Transform(fauxgl.Rotate(up, (next-curr)*a.Wobble*geom.Pi/180)) -}