// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build example // // This build tag means that "go install golang.org/x/exp/shiny/..." doesn't // install this example program. Use "go run main.go" to run it or "go install // -tags=example" to install it. // Fluid is a fluid dynamics simulator. It is based on Jos Stam, "Real-Time // Fluid Dynamics for Games", Proceedings of the Game Developer Conference, // March 2003. See // http://www.dgp.toronto.edu/people/stam/reality/Research/pub.html package main import ( "image" "image/color" "image/draw" "log" "sync" "time" "golang.org/x/exp/shiny/driver" "golang.org/x/exp/shiny/screen" "golang.org/x/mobile/event/lifecycle" "golang.org/x/mobile/event/mouse" "golang.org/x/mobile/event/paint" "golang.org/x/mobile/event/size" ) const ( N = 128 // The grid of cells has size NxN. tickDuration = time.Second / 60 // These remaining numbers have magic values, determined by trial and error // to look good, rather than being derived from first principles. iterations = 20 dt = 0.1 diff = 0 visc = 0 force = 5 source = 20 fade = 0.89 ) func main() { driver.Main(func(s screen.Screen) { w, err := s.NewWindow(&screen.NewWindowOptions{ Title: "Fluid Shiny Example", }) if err != nil { log.Fatal(err) } buf, tex := screen.Buffer(nil), screen.Texture(nil) defer func() { if buf != nil { tex.Release() buf.Release() } w.Release() }() go simulate(w) var ( buttonDown bool sz size.Event ) for { publish := false switch e := w.NextEvent().(type) { case lifecycle.Event: if e.To == lifecycle.StageDead { return } switch e.Crosses(lifecycle.StageVisible) { case lifecycle.CrossOn: pauseChan <- play var err error buf, err = s.NewBuffer(image.Point{N, N}) if err != nil { log.Fatal(err) } tex, err = s.NewTexture(image.Point{N, N}) if err != nil { log.Fatal(err) } tex.Fill(tex.Bounds(), color.White, draw.Src) case lifecycle.CrossOff: pauseChan <- pause tex.Release() tex = nil buf.Release() buf = nil } case mouse.Event: if e.Button == mouse.ButtonLeft { buttonDown = e.Direction == mouse.DirPress } if !buttonDown { break } z := sz.Size() x := int(e.X) * N / z.X y := int(e.Y) * N / z.Y if x < 0 || N <= x || y < 0 || N <= y { break } shared.mu.Lock() shared.mouseEvents = append(shared.mouseEvents, image.Point{x, y}) shared.mu.Unlock() case paint.Event: publish = buf != nil case size.Event: sz = e case uploadEvent: shared.mu.Lock() if buf != nil { copy(buf.RGBA().Pix, shared.pix) publish = true } shared.uploadEventSent = false shared.mu.Unlock() if publish { tex.Upload(image.Point{}, buf, buf.Bounds()) } case error: log.Print(e) } if publish { w.Scale(sz.Bounds(), tex, tex.Bounds(), draw.Src, nil) w.Publish() } } }) } const ( pause = false play = true ) // pauseChan lets the UI event goroutine pause and play the CPU-intensive // simulation goroutine depending on whether the window is visible (e.g. // minimized). 64 should be large enough, in typical use, so that the former // doesn't ever block on the latter. var pauseChan = make(chan bool, 64) // uploadEvent signals that the shared pix slice should be uploaded to the // screen.Texture via the screen.Buffer. type uploadEvent struct{} var shared = struct { mu sync.Mutex uploadEventSent bool mouseEvents []image.Point pix []byte }{ pix: make([]byte, 4*N*N), } func simulate(q screen.EventDeque) { var ( dens, densPrev array u, uPrev array v, vPrev array xPrev, yPrev int havePrevLoc bool ) ticker := time.NewTicker(tickDuration) var tickerC <-chan time.Time for { select { case p := <-pauseChan: if p == pause { tickerC = nil } else { tickerC = ticker.C } continue case <-tickerC: } shared.mu.Lock() for _, p := range shared.mouseEvents { dens[p.X+1][p.Y] = source if havePrevLoc { u[p.X+1][p.Y+1] = force * float32(p.X-xPrev) v[p.X+1][p.Y+1] = force * float32(p.Y-yPrev) } xPrev, yPrev, havePrevLoc = p.X, p.Y, true } shared.mouseEvents = shared.mouseEvents[:0] shared.mu.Unlock() velStep(&u, &v, &uPrev, &vPrev) densStep(&dens, &densPrev, &u, &v) // This fade isn't part of Stam's GDC03 paper, but it looks nice. for i := range dens { for j := range dens[i] { dens[i][j] *= fade } } shared.mu.Lock() for y := 0; y < N; y++ { for x := 0; x < N; x++ { d := int32(dens[x+1][y+1] * 0xff) if d < 0 { d = 0 } else if d > 0xff { d = 0xff } v := 255 - uint8(d) p := (N*y + x) * 4 shared.pix[p+0] = v shared.pix[p+1] = v shared.pix[p+2] = v shared.pix[p+3] = 0xff } } uploadEventSent := shared.uploadEventSent shared.uploadEventSent = true shared.mu.Unlock() if !uploadEventSent { q.Send(uploadEvent{}) } } } // All of the remaining code more or less comes from Stam's GDC03 paper. type array [N + 2][N + 2]float32 func addSource(x, s *array) { for i := range x { for j := range x[i] { x[i][j] += dt * s[i][j] } } } func setBnd(b int, x *array) { switch b { case 0: for i := 1; i <= N; i++ { x[0+0][i] = +x[1][i] x[N+1][i] = +x[N][i] x[i][0+0] = +x[i][1] x[i][N+1] = +x[i][N] } case 1: for i := 1; i <= N; i++ { x[0+0][i] = -x[1][i] x[N+1][i] = -x[N][i] x[i][0+0] = +x[i][1] x[i][N+1] = +x[i][N] } case 2: for i := 1; i <= N; i++ { x[0+0][i] = +x[1][i] x[N+1][i] = +x[N][i] x[i][0+0] = -x[i][1] x[i][N+1] = -x[i][N] } } x[0+0][0+0] = 0.5 * (x[1][0+0] + x[0+0][1]) x[0+0][N+1] = 0.5 * (x[1][N+1] + x[0+0][N]) x[N+1][0+0] = 0.5 * (x[N][0+0] + x[N+1][1]) x[N+1][N+1] = 0.5 * (x[N][N+1] + x[N+1][N]) } func linSolve(b int, x, x0 *array, a, c float32) { // This if block isn't part of Stam's GDC03 paper, but it's a nice // optimization when the diff diffusion parameter is zero. if a == 0 && c == 1 { for i := 1; i <= N; i++ { for j := 1; j <= N; j++ { x[i][j] = x0[i][j] } } setBnd(b, x) return } invC := 1 / c for k := 0; k < iterations; k++ { for i := 1; i <= N; i++ { for j := 1; j <= N; j++ { x[i][j] = (x0[i][j] + a*(x[i-1][j]+x[i+1][j]+x[i][j-1]+x[i][j+1])) * invC } } setBnd(b, x) } } func diffuse(b int, x, x0 *array, diff float32) { a := dt * diff * N * N linSolve(b, x, x0, a, 1+4*a) } func advect(b int, d, d0, u, v *array) { const dt0 = dt * N for i := 1; i <= N; i++ { for j := 1; j <= N; j++ { x := float32(i) - dt0*u[i][j] if x < 0.5 { x = 0.5 } if x > N+0.5 { x = N + 0.5 } i0 := int(x) i1 := i0 + 1 y := float32(j) - dt0*v[i][j] if y < 0.5 { y = 0.5 } if y > N+0.5 { y = N + 0.5 } j0 := int(y) j1 := j0 + 1 s1 := x - float32(i0) s0 := 1 - s1 t1 := y - float32(j0) t0 := 1 - t1 d[i][j] = s0*(t0*d0[i0][j0]+t1*d0[i0][j1]) + s1*(t0*d0[i1][j0]+t1*d0[i1][j1]) } } setBnd(b, d) } func project(u, v, p, div *array) { for i := 1; i <= N; i++ { for j := 1; j <= N; j++ { div[i][j] = (u[i+1][j] - u[i-1][j] + v[i][j+1] - v[i][j-1]) / (-2 * N) p[i][j] = 0 } } setBnd(0, div) setBnd(0, p) linSolve(0, p, div, 1, 4) for i := 1; i <= N; i++ { for j := 1; j <= N; j++ { u[i][j] -= (N / 2) * (p[i+1][j+0] - p[i-1][j+0]) v[i][j] -= (N / 2) * (p[i+0][j+1] - p[i+0][j-1]) } } setBnd(1, u) setBnd(2, v) } func velStep(u, v, u0, v0 *array) { addSource(u, u0) addSource(v, v0) u0, u = u, u0 diffuse(1, u, u0, visc) v0, v = v, v0 diffuse(2, v, v0, visc) project(u, v, u0, v0) u0, u = u, u0 v0, v = v, v0 advect(1, u, u0, u0, v0) advect(2, v, v0, u0, v0) project(u, v, u0, v0) } func densStep(x, x0, u, v *array) { addSource(x, x0) x0, x = x, x0 diffuse(0, x, x0, diff) x0, x = x, x0 advect(0, x, x0, u, v) }