| // 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. |
| |
| //go:build example |
| // +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) |
| } |