Added grid render.

Added particle toggle.
Added boundary shape adjustment.
This commit is contained in:
2025-12-05 09:22:20 -05:00
parent d37413a400
commit a15c89d769
4 changed files with 225 additions and 93 deletions

View File

@@ -2,6 +2,7 @@ package elements
import ( import (
"fluids/fluid" "fluids/fluid"
"fluids/gamedata"
"image/color" "image/color"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
@@ -9,24 +10,29 @@ import (
) )
const ( const (
FS10PixelWidth = 200 FS10PixelWidth = 100
FS10PixelHeight = 100 FS10PixelHeight = 100
FS10FluidWidth = 3.0 //meters FS10FluidWidth = 1.0 //meters
FS10FluidHeight = 1.0 //meters FS10FluidHeight = 1.0 //meters
FS10Resolution = 20 //need to workshop this FS10Resolution = 20 //need to workshop this
) )
type FluidSim10 struct { type FluidSim10 struct {
MappedEntityBase MappedEntityBase
fluid *fluid.Fluid fluid *fluid.Fluid
angle float64 angle float64
particlebuff *ebiten.Image particlebuff *ebiten.Image
renderparticles bool
renderfield bool
fieldscale gamedata.Vector
} }
func NewFluidSim10() *FluidSim10 { func NewFluidSim10() *FluidSim10 {
fsim := &FluidSim10{ fsim := &FluidSim10{
fluid: fluid.NewFluid(fluid.FieldVector{X: FS10FluidWidth, Y: FS10FluidHeight}, FS10FluidHeight/FS10Resolution), fluid: fluid.NewFluid(fluid.FieldVector{X: FS10FluidWidth, Y: FS10FluidHeight}, FS10FluidHeight/FS10Resolution),
particlebuff: ebiten.NewImage(1, 1), particlebuff: ebiten.NewImage(1, 1),
renderfield: true, //false,
renderparticles: false, //true,
} }
fsim.Initialize() fsim.Initialize()
return fsim return fsim
@@ -36,6 +42,12 @@ func (f *FluidSim10) Initialize() {
f.Sprite = ebiten.NewImage(FS10PixelWidth, FS10PixelHeight) f.Sprite = ebiten.NewImage(FS10PixelWidth, FS10PixelHeight)
f.particlebuff.Fill(color.White) f.particlebuff.Fill(color.White)
f.fluid.Initialize() 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) { func (f *FluidSim10) SetAngle(angle float64) {
@@ -50,20 +62,61 @@ func (f *FluidSim10) GetAngle() float64 {
func (f *FluidSim10) Draw() { func (f *FluidSim10) Draw() {
f.Sprite.Clear() 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 { idx := i*f.fluid.Field.Ny + j
//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)
op := &ebiten.DrawImageOptions{} if f.fluid.Field.CellType[idx] != fluid.CellTypeFluid {
op.GeoM.Translate(ox, oy) continue
f.Sprite.DrawImage(f.particlebuff, op) }
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() 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
}

View File

@@ -4,6 +4,7 @@
package fluid package fluid
import ( import (
"fluids/utils"
"math" "math"
) )
@@ -30,15 +31,16 @@ type Fluid struct {
pNumX, pNumY int //SPATIAL HASHING: number of particle cells x,y pNumX, pNumY int //SPATIAL HASHING: number of particle cells x,y
pNumCells int //SPATIAL HASHING: number of particle cells in total pNumCells int //SPATIAL HASHING: number of particle cells in total
pInvSpacing float32 //SPATIAL HASHING: one over the particle spacing pInvSpacing float32 //SPATIAL HASHING: one over the particle spacing
particleDensity []float32 //density of particles within each field cell ParticleDensity []float32 //density of particles within each field cell
particleRestDensity float32 ParticleRestDensity float32
fluidDensity float32 fluidDensity float32
angle float32 angle float32
dt float32 //delta time step dt float32 //delta time step
gravitynormal FieldVector //unit vector for gravity gravitynormal FieldVector //unit vector for gravity
stepacceleration FieldVector //step acceleration due to gravity stepacceleration FieldVector //step acceleration due to gravity
flipPicRatio float32 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 { func NewFluid(dimensions FieldVector, spacing float32) *Fluid {
@@ -46,9 +48,10 @@ func NewFluid(dimensions FieldVector, spacing float32) *Fluid {
Field: NewVelocityField(dimensions, spacing), Field: NewVelocityField(dimensions, spacing),
dt: FluidDefaultTimeStep, dt: FluidDefaultTimeStep,
fluidDensity: FluidDefaultDensity, fluidDensity: FluidDefaultDensity,
particleRestDensity: FluidDefaultRestDensity, ParticleRestDensity: FluidDefaultRestDensity,
flipPicRatio: FluidDefaultFlipPicRatio, flipPicRatio: FluidDefaultFlipPicRatio,
numSubSteps: FluidDefaultSubSteps, numSubSteps: FluidDefaultSubSteps,
Block: true,
} }
f.Initialize() f.Initialize()
@@ -59,7 +62,7 @@ func (f *Fluid) Initialize() {
f.InitializeParticles() f.InitializeParticles()
f.SetAngle(FluidDefaultAngle) 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() { 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() { func (f *Fluid) PerformParticleSpatialHash() {
clear(f.numCellParticles) clear(f.numCellParticles)
@@ -191,8 +168,8 @@ func (f *Fluid) PerformParticleSpatialHash() {
//find spatcial hash cell for this particle's position, increment count there //find spatcial hash cell for this particle's position, increment count there
for i := range f.Particles { for i := range f.Particles {
p := &f.Particles[i] p := &f.Particles[i]
xi := clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1) xi := utils.Clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1)
yi := clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1) yi := utils.Clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1)
cellnumber := xi*f.pNumY + yi cellnumber := xi*f.pNumY + yi
f.numCellParticles[cellnumber]++ f.numCellParticles[cellnumber]++
} }
@@ -209,8 +186,8 @@ func (f *Fluid) PerformParticleSpatialHash() {
//firstCellParticle bucket as we go //firstCellParticle bucket as we go
for i := range f.Particles { for i := range f.Particles {
p := &f.Particles[i] p := &f.Particles[i]
xi := clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1) xi := utils.Clamp(int(p.Position.X*f.pInvSpacing), 0, f.pNumX-1)
yi := clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1) yi := utils.Clamp(int(p.Position.Y*f.pInvSpacing), 0, f.pNumY-1)
cellnumber := xi*f.pNumY + yi cellnumber := xi*f.pNumY + yi
f.firstCellParticle[cellnumber]-- f.firstCellParticle[cellnumber]--
f.cellParticleIds[f.firstCellParticle[cellnumber]] = i f.cellParticleIds[f.firstCellParticle[cellnumber]] = i
@@ -291,32 +268,78 @@ func (f *Fluid) HandleParticleCollisions() {
minDist2 := minDist * minDist minDist2 := minDist * minDist
*/ */
minX := f.particleRadius if f.Block {
maxX := float32(f.Field.Nx-1)*f.Field.H - f.particleRadius minX := f.Field.H + f.particleRadius
minY := f.particleRadius maxX := float32(f.Field.Nx-1)*f.Field.H - f.particleRadius
maxY := float32(f.Field.Ny-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 { for i := range f.Particles {
p := &f.Particles[i] p := &f.Particles[i]
if p.Position.X < minX { if p.Position.X < minX {
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 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 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 { for i := range f.Particles {
x := f.Particles[i].Position.X x := f.Particles[i].Position.X
y := f.Particles[i].Position.Y y := f.Particles[i].Position.Y
xi := clamp(int(math.Floor(float64(x*h1))), 0, f.Field.Nx-1) xi := utils.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) yi := utils.Clamp(int(math.Floor(float64(y*h1))), 0, f.Field.Ny-1)
cellnumber := xi*n + yi cellnumber := xi*n + yi
if f.Field.CellType[cellnumber] == CellTypeAir { if f.Field.CellType[cellnumber] == CellTypeAir {
f.Field.CellType[cellnumber] = CellTypeFluid f.Field.CellType[cellnumber] = CellTypeFluid
@@ -380,8 +403,8 @@ func (f *Fluid) TransferVelocities(togrid bool) {
x := p.Position.X x := p.Position.X
y := p.Position.Y y := p.Position.Y
x = clamp32(x, f.Field.H, float32((f.Field.Nx-1))*f.Field.H) x = utils.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) y = utils.Clamp32(y, f.Field.H, float32((f.Field.Ny-1))*f.Field.H)
//find cell components neighbouring x,y //find cell components neighbouring x,y
x0 := min(int(math.Floor(float64((x-dx)*h1))), f.Field.Nx-2) 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 h1 := f.Field.InvH
h2 := 0.5 * h h2 := 0.5 * h
clear(f.particleDensity) clear(f.ParticleDensity)
for i := range f.Particles { for i := range f.Particles {
p := &f.Particles[i] p := &f.Particles[i]
x := p.Position.X x := p.Position.X
y := p.Position.Y y := p.Position.Y
x = clamp32(x, h, float32(f.Field.Nx-1)*h) x = utils.Clamp32(x, h, float32(f.Field.Nx-1)*h)
y = clamp32(y, h, float32(f.Field.Ny-1)*h) y = utils.Clamp32(y, h, float32(f.Field.Ny-1)*h)
x0 := int(math.Floor(float64((x - h2) * h1))) x0 := int(math.Floor(float64((x - h2) * h1)))
x1 := min(x0+1, f.Field.Nx-2) x1 := min(x0+1, f.Field.Nx-2)
@@ -536,32 +559,32 @@ func (f *Fluid) UpdateParticleDensity() {
sy := 1.0 - ty sy := 1.0 - ty
if x0 < f.Field.Nx && y0 < f.Field.Ny { 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 { 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 { 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 { 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 var sum float32 = 0.0
numFluidCells := 0 numFluidCells := 0
for i := 0; i < f.Field.Nx*f.Field.Ny; i++ { for i := 0; i < f.Field.Nx*f.Field.Ny; i++ {
if f.Field.CellType[i] == CellTypeFluid { if f.Field.CellType[i] == CellTypeFluid {
sum += f.particleDensity[i] sum += f.ParticleDensity[i]
numFluidCells++ numFluidCells++
} }
} }
if numFluidCells > 0 { 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 //divergence -> we want this to be zero
div := f.Field.U[right] - f.Field.U[center] + f.Field.V[top] - f.Field.V[center] 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 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 { if compression > 0.0 {
div = div - k*compression div = div - k*compression
} }
@@ -624,3 +647,7 @@ func (f *Fluid) SolveIncompressibility() {
} }
} }
func (f *Fluid) ToggleShape() {
f.Block = !f.Block
}

View File

@@ -234,9 +234,17 @@ func (g *Game) ManageFluidSim10Inputs() {
g.mdown = false 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.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10())
//g.fluidsim10 = append(g.fluidsim10, elements.NewFluidSim10())
g.fluidsim10width = float64(g.fluidsim10[0].GetSprite().Bounds().Dx()) g.fluidsim10width = float64(g.fluidsim10[0].GetSprite().Bounds().Dx())
g.fluidsim10height = float64(g.fluidsim10[0].GetSprite().Bounds().Dy()) g.fluidsim10height = float64(g.fluidsim10[0].GetSprite().Bounds().Dy())

27
utils/clamp.go Normal file
View File

@@ -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
}