From a15c89d7698c1e7d96e684ac89ab84fa67ef0acf Mon Sep 17 00:00:00 2001 From: iegod Date: Fri, 5 Dec 2025 09:22:20 -0500 Subject: [PATCH] Added grid render. Added particle toggle. Added boundary shape adjustment. --- elements/fluidsim10.go | 107 +++++++++++++++++++++----- fluid/fluid.go | 171 ++++++++++++++++++++++++----------------- game/game.go | 13 +++- utils/clamp.go | 27 +++++++ 4 files changed, 225 insertions(+), 93 deletions(-) create mode 100644 utils/clamp.go diff --git a/elements/fluidsim10.go b/elements/fluidsim10.go index 4d62732..b6fadde 100644 --- a/elements/fluidsim10.go +++ b/elements/fluidsim10.go @@ -2,6 +2,7 @@ package elements import ( "fluids/fluid" + "fluids/gamedata" "image/color" "github.com/hajimehoshi/ebiten/v2" @@ -9,24 +10,29 @@ import ( ) const ( - FS10PixelWidth = 200 + FS10PixelWidth = 100 FS10PixelHeight = 100 - FS10FluidWidth = 3.0 //meters + FS10FluidWidth = 1.0 //meters FS10FluidHeight = 1.0 //meters FS10Resolution = 20 //need to workshop this ) type FluidSim10 struct { MappedEntityBase - fluid *fluid.Fluid - angle float64 - particlebuff *ebiten.Image + fluid *fluid.Fluid + angle float64 + particlebuff *ebiten.Image + renderparticles bool + renderfield bool + fieldscale gamedata.Vector } func NewFluidSim10() *FluidSim10 { fsim := &FluidSim10{ - fluid: fluid.NewFluid(fluid.FieldVector{X: FS10FluidWidth, Y: FS10FluidHeight}, FS10FluidHeight/FS10Resolution), - particlebuff: ebiten.NewImage(1, 1), + fluid: fluid.NewFluid(fluid.FieldVector{X: FS10FluidWidth, Y: FS10FluidHeight}, FS10FluidHeight/FS10Resolution), + particlebuff: ebiten.NewImage(1, 1), + renderfield: true, //false, + renderparticles: false, //true, } fsim.Initialize() return fsim @@ -36,6 +42,12 @@ func (f *FluidSim10) Initialize() { f.Sprite = ebiten.NewImage(FS10PixelWidth, FS10PixelHeight) f.particlebuff.Fill(color.White) f.fluid.Initialize() + + //fieldscale for rendering purposes + f.fieldscale = gamedata.Vector{ + X: FS10PixelWidth / float64(f.fluid.Field.Nx-1), + Y: FS10PixelHeight / float64(f.fluid.Field.Ny-1), + } } func (f *FluidSim10) SetAngle(angle float64) { @@ -50,20 +62,61 @@ func (f *FluidSim10) GetAngle() float64 { func (f *FluidSim10) Draw() { f.Sprite.Clear() - vector.StrokeRect(f.Sprite, 0, 0, FS10PixelWidth, FS10PixelHeight, 2, color.White, true) + if f.renderfield { + /* + alternatively, single loop (not nested) with x/y cell coordinates given by + xi := i % f.fluid.Field.Ny + yi := i / f.fluid.Field.Ny + */ + for i := range f.fluid.Field.Nx { + for j := range f.fluid.Field.Ny { - for i := range f.fluid.Particles { - //for each particle, compute its relative position based on its - //position within the fluid field - p := &f.fluid.Particles[i] - percentx := p.Position.X / f.fluid.Field.Dimensions.X - percenty := p.Position.Y / f.fluid.Field.Dimensions.Y - ox := float64(percentx * FS10PixelWidth) - oy := float64(percenty * FS10PixelHeight) + idx := i*f.fluid.Field.Ny + j - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(ox, oy) - f.Sprite.DrawImage(f.particlebuff, op) + if f.fluid.Field.CellType[idx] != fluid.CellTypeFluid { + continue + } + + celldensity := f.fluid.ParticleDensity[idx] / f.fluid.ParticleRestDensity + if celldensity > 0.8 { + celldensity = 1 + } + + ox := float64(i) * f.fieldscale.X + oy := float64(j) * f.fieldscale.Y + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(-.5, -.5) + op.GeoM.Scale(f.fieldscale.X, f.fieldscale.Y) + op.GeoM.Translate(ox, oy) + op.ColorScale.ScaleAlpha(celldensity) + op.ColorM.Scale(0, 0, 1, 1) + f.Sprite.DrawImage(f.particlebuff, op) + } + } + } + + if f.renderparticles { + for i := range f.fluid.Particles { + //for each particle, compute its relative position based on its + //position within the fluid field + p := &f.fluid.Particles[i] + percentx := (p.Position.X - f.fluid.Field.H/2) / f.fluid.Field.Dimensions.X + percenty := (p.Position.Y - f.fluid.Field.H/2) / f.fluid.Field.Dimensions.Y + ox := float64(percentx * FS10PixelWidth) + oy := float64(percenty * FS10PixelHeight) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(ox, oy) + f.Sprite.DrawImage(f.particlebuff, op) + } + } + + if f.fluid.Block { + vector.StrokeRect(f.Sprite, 0, 0, FS10PixelWidth, FS10PixelHeight, 2, color.White, true) + } else { + radius := float32(f.fluid.Field.Nx) * f.fluid.Field.H / 2 + pixelradius := radius/f.fluid.Field.Dimensions.X*FS10PixelWidth - 4 + vector.StrokeCircle(f.Sprite, FS10PixelWidth/2, FS10PixelHeight/2, pixelradius, 2, color.White, true) } } @@ -75,3 +128,19 @@ func (f *FluidSim10) Update() { f.fluid.Step() } + +func (f *FluidSim10) EnableParticleRender(v bool) { + f.renderparticles = v +} + +func (f *FluidSim10) EnableFieldRender(v bool) { + f.renderfield = v +} + +func (f *FluidSim10) ToggleShape() { + f.fluid.ToggleShape() +} + +func (f *FluidSim10) ToggleParticles() { + f.renderparticles = !f.renderparticles +} diff --git a/fluid/fluid.go b/fluid/fluid.go index 4696f5e..9fd1179 100644 --- a/fluid/fluid.go +++ b/fluid/fluid.go @@ -4,6 +4,7 @@ package fluid import ( + "fluids/utils" "math" ) @@ -30,15 +31,16 @@ type Fluid struct { pNumX, pNumY int //SPATIAL HASHING: number of particle cells x,y pNumCells int //SPATIAL HASHING: number of particle cells in total pInvSpacing float32 //SPATIAL HASHING: one over the particle spacing - particleDensity []float32 //density of particles within each field cell - particleRestDensity float32 + ParticleDensity []float32 //density of particles within each field cell + ParticleRestDensity float32 fluidDensity float32 angle float32 dt float32 //delta time step gravitynormal FieldVector //unit vector for gravity stepacceleration FieldVector //step acceleration due to gravity flipPicRatio float32 - numSubSteps int //number of simulation substeps + numSubSteps int //number of simulation substeps + Block bool //rectangular or circular field container } func NewFluid(dimensions FieldVector, spacing float32) *Fluid { @@ -46,9 +48,10 @@ func NewFluid(dimensions FieldVector, spacing float32) *Fluid { Field: NewVelocityField(dimensions, spacing), dt: FluidDefaultTimeStep, fluidDensity: FluidDefaultDensity, - particleRestDensity: FluidDefaultRestDensity, + ParticleRestDensity: FluidDefaultRestDensity, flipPicRatio: FluidDefaultFlipPicRatio, numSubSteps: FluidDefaultSubSteps, + Block: true, } f.Initialize() @@ -59,7 +62,7 @@ func (f *Fluid) Initialize() { f.InitializeParticles() f.SetAngle(FluidDefaultAngle) - f.particleDensity = make([]float32, f.Field.Nx*f.Field.Ny) + f.ParticleDensity = make([]float32, f.Field.Nx*f.Field.Ny) } func (f *Fluid) InitializeParticles() { @@ -158,32 +161,6 @@ func (f *Fluid) IntegrateParticles() { } } -// returns adjusted value between start and end, whichever is closer -// unless value is between, in which case it returns value -func clamp(value, start, end int) int { - if value < start { - return start - } - - if value < end { - return value - } - - return end -} - -func clamp32(value, start, end float32) float32 { - if value < start { - return start - } - - if value < end { - return value - } - - return end -} - func (f *Fluid) PerformParticleSpatialHash() { clear(f.numCellParticles) @@ -191,8 +168,8 @@ func (f *Fluid) PerformParticleSpatialHash() { //find spatcial hash cell for this particle's position, increment count there for i := range f.Particles { p := &f.Particles[i] - xi := clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1) - yi := clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1) + xi := utils.Clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1) + yi := utils.Clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1) cellnumber := xi*f.pNumY + yi f.numCellParticles[cellnumber]++ } @@ -209,8 +186,8 @@ func (f *Fluid) PerformParticleSpatialHash() { //firstCellParticle bucket as we go for i := range f.Particles { p := &f.Particles[i] - xi := clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1) - yi := clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1) + xi := utils.Clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1) + yi := utils.Clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1) cellnumber := xi*f.pNumY + yi f.firstCellParticle[cellnumber]-- f.cellParticleIds[f.firstCellParticle[cellnumber]] = i @@ -291,32 +268,78 @@ func (f *Fluid) HandleParticleCollisions() { minDist2 := minDist * minDist */ - minX := f.particleRadius - maxX := float32(f.Field.Nx-1)*f.Field.H - f.particleRadius - minY := f.particleRadius - maxY := float32(f.Field.Ny-1)*f.Field.H - f.particleRadius + if f.Block { + minX := f.Field.H + f.particleRadius + maxX := float32(f.Field.Nx-1)*f.Field.H - f.particleRadius + minY := f.Field.H + f.particleRadius + maxY := float32(f.Field.Ny-1)*f.Field.H - f.particleRadius - for i := range f.Particles { - p := &f.Particles[i] + for i := range f.Particles { + p := &f.Particles[i] - if p.Position.X < minX { - p.Position.X = minX + if p.Position.X < minX { + p.Position.X = minX + p.Velocity.X = 0 + } + + if p.Position.X > maxX { + p.Position.X = maxX + p.Velocity.X = 0 + } + + if p.Position.Y < minY { + p.Position.Y = minY + p.Velocity.Y = 0 + } + + if p.Position.Y > maxY { + p.Position.Y = maxY + p.Velocity.Y = 0 + } + + } + } else { + //find fluid cell in which the particle resides + //compute distance of this cell from 'center' cell of our circle + //if distance exceeds radius, clamp particle velocity and position + //to edge cell + + //find origin of circle, conveniently these dimensions are also our radius + originX := float32(f.Field.Nx) * f.Field.H / 2 + originY := float32(f.Field.Ny) * f.Field.H / 2 + radius := originX - f.Field.H - f.particleRadius + + //cxi := utils.Clamp(int(math.Floor(float64(originX*f.Field.InvH))), 0, f.Field.Nx-1) + //cyi := utils.Clamp(int(math.Floor(float64(originY*f.Field.InvH))), 0, f.Field.Ny-1) + //originCellIdx := cxi*f.Field.Ny + cyi + + //find fluid cell for particle + for i := range f.Particles { + p := &f.Particles[i] + + //xi := utils.Clamp(int(math.Floor(float64(p.Position.X*f.Field.InvH))), 0, f.Field.Nx-1) + //yi := utils.Clamp(int(math.Floor(float64(p.Position.Y*f.Field.InvH))), 0, f.Field.Ny-1) + // + //dx := originX - px + //dy := originY - py + + dx := p.Position.X - originX + dy := p.Position.Y - originY + dist2 := dx*dx + dy*dy + if dist2 < radius*radius || dist2 == 0 { + continue + } + + dist := float32(math.Sqrt(float64(dist2))) + + newx := originX + dx*radius/dist + newy := originY + dy*radius/dist + + p.Position.X = newx + p.Position.Y = newy p.Velocity.X = 0 - } - - if p.Position.X > maxX { - p.Position.X = maxX - p.Velocity.X = 0 - } - - if p.Position.Y < minY { - p.Position.Y = minY p.Velocity.Y = 0 - } - if p.Position.Y > maxY { - p.Position.Y = maxY - p.Velocity.Y = 0 } } @@ -348,8 +371,8 @@ func (f *Fluid) TransferVelocities(togrid bool) { for i := range f.Particles { x := f.Particles[i].Position.X y := f.Particles[i].Position.Y - xi := clamp(int(math.Floor(float64(x*h1))), 0, f.Field.Nx-1) - yi := clamp(int(math.Floor(float64(y*h1))), 0, f.Field.Ny-1) + xi := utils.Clamp(int(math.Floor(float64(x*h1))), 0, f.Field.Nx-1) + yi := utils.Clamp(int(math.Floor(float64(y*h1))), 0, f.Field.Ny-1) cellnumber := xi*n + yi if f.Field.CellType[cellnumber] == CellTypeAir { f.Field.CellType[cellnumber] = CellTypeFluid @@ -380,8 +403,8 @@ func (f *Fluid) TransferVelocities(togrid bool) { x := p.Position.X y := p.Position.Y - x = clamp32(x, f.Field.H, float32((f.Field.Nx-1))*f.Field.H) - y = clamp32(y, f.Field.H, float32((f.Field.Ny-1))*f.Field.H) + x = utils.Clamp32(x, f.Field.H, float32((f.Field.Nx-1))*f.Field.H) + y = utils.Clamp32(y, f.Field.H, float32((f.Field.Ny-1))*f.Field.H) //find cell components neighbouring x,y x0 := min(int(math.Floor(float64((x-dx)*h1))), f.Field.Nx-2) @@ -513,15 +536,15 @@ func (f *Fluid) UpdateParticleDensity() { h1 := f.Field.InvH h2 := 0.5 * h - clear(f.particleDensity) + clear(f.ParticleDensity) for i := range f.Particles { p := &f.Particles[i] x := p.Position.X y := p.Position.Y - x = clamp32(x, h, float32(f.Field.Nx-1)*h) - y = clamp32(y, h, float32(f.Field.Ny-1)*h) + x = utils.Clamp32(x, h, float32(f.Field.Nx-1)*h) + y = utils.Clamp32(y, h, float32(f.Field.Ny-1)*h) x0 := int(math.Floor(float64((x - h2) * h1))) x1 := min(x0+1, f.Field.Nx-2) @@ -536,32 +559,32 @@ func (f *Fluid) UpdateParticleDensity() { sy := 1.0 - ty if x0 < f.Field.Nx && y0 < f.Field.Ny { - f.particleDensity[x0*n+y0] += sx * sy + f.ParticleDensity[x0*n+y0] += sx * sy } if x1 < f.Field.Nx && y0 < f.Field.Ny { - f.particleDensity[x1*n+y0] += tx * sy + f.ParticleDensity[x1*n+y0] += tx * sy } if x1 < f.Field.Nx && y1 < f.Field.Ny { - f.particleDensity[x1*n+y1] += tx * ty + f.ParticleDensity[x1*n+y1] += tx * ty } if x0 < f.Field.Nx && y1 < f.Field.Ny { - f.particleDensity[x0*n+y1] += sx * ty + f.ParticleDensity[x0*n+y1] += sx * ty } } - if f.particleRestDensity == 0.0 { + if f.ParticleRestDensity == 0.0 { var sum float32 = 0.0 numFluidCells := 0 for i := 0; i < f.Field.Nx*f.Field.Ny; i++ { if f.Field.CellType[i] == CellTypeFluid { - sum += f.particleDensity[i] + sum += f.ParticleDensity[i] numFluidCells++ } } if numFluidCells > 0 { - f.particleRestDensity = sum / float32(numFluidCells) + f.ParticleRestDensity = sum / float32(numFluidCells) } } } @@ -602,9 +625,9 @@ func (f *Fluid) SolveIncompressibility() { //divergence -> we want this to be zero div := f.Field.U[right] - f.Field.U[center] + f.Field.V[top] - f.Field.V[center] - if f.particleRestDensity > 0.0 { + if f.ParticleRestDensity > 0.0 { var k float32 = 1.0 - var compression float32 = f.particleDensity[i*n+j] - f.particleRestDensity + var compression float32 = f.ParticleDensity[i*n+j] - f.ParticleRestDensity if compression > 0.0 { div = div - k*compression } @@ -624,3 +647,7 @@ func (f *Fluid) SolveIncompressibility() { } } + +func (f *Fluid) ToggleShape() { + f.Block = !f.Block +} diff --git a/game/game.go b/game/game.go index dc15294..5c5ef83 100644 --- a/game/game.go +++ b/game/game.go @@ -234,9 +234,17 @@ func (g *Game) ManageFluidSim10Inputs() { g.mdown = false } -} + if inpututil.IsKeyJustPressed(ebiten.KeyB) { + for _, sim := range g.fluidsim10 { + sim.ToggleShape() + } + } -func (g *Game) ManageFluidSimGPTInputs() { + if inpututil.IsKeyJustPressed(ebiten.KeyV) { + for _, sim := range g.fluidsim10 { + sim.ToggleParticles() + } + } } @@ -244,6 +252,7 @@ func (g *Game) Initialize() { g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10()) g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10()) + //g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10()) g.fluidsim10width = float64(g.fluidsim10[0].GetSprite().Bounds().Dx()) g.fluidsim10height = float64(g.fluidsim10[0].GetSprite().Bounds().Dy()) diff --git a/utils/clamp.go b/utils/clamp.go new file mode 100644 index 0000000..f202acc --- /dev/null +++ b/utils/clamp.go @@ -0,0 +1,27 @@ +package utils + +// returns adjusted value between start and end, whichever is closer +// unless value is between, in which case it returns value +func Clamp(value, start, end int) int { + if value < start { + return start + } + + if value < end { + return value + } + + return end +} + +func Clamp32(value, start, end float32) float32 { + if value < start { + return start + } + + if value < end { + return value + } + + return end +}