从零开始的CAD|CAE开发: LBM源码实现分享
起因:
上期我们写了流体仿真的经典案例: 通过LBM,模拟计算涡流的形成,
当时承诺: 只要验证通过,就把代码开源出来;
ok.验证通过了,那么我也就将代码全都贴出来
代码开源并贴出:
public class LidDrivenCavityFlow : IDisposable { public LidDrivenCavityFlow(int width = 200, int height = 100) { Width = width; Height = height; Initialize(); } public int Width { get; set; } = 256; public int Height { get; set; } = 256; public double Error { get; set; } = 0; public int TimeStep { get; set; } = 0; public bool IsRunning { get; set; } = false; private double dx = 1.0;//X方向的步长 private double dy = 1.0;//Y方向的步长 private double Lx = 1.0;//X方向的长度; private double Ly = 1.0;//Y方向的长度; private double dt = 1.0;//时间步长; private double c = 1.0;//格子的速度 // 模拟参数 public double rho0 = 1.0; //密度 public double U = 0.1;//顶盖的速度; public double Re = 1000; //雷诺数 private double niu = 0; //粘力-运动粘度系数v private double tau_f = 1.0;//松弛时间-无量纲 public double Omega { get; set; } = 1.0;//松弛时间 private int[,] e; // 离散速度; // LBM 计算变量 private double[,,] f; // 演化前的分布函数 private double[,,] F; // 演化后的分布函数; private double[,] rho; // 密度场 private double[,] ux0; // n层时的速度X private double[,] uy0; // n层时的速度Y private double[,] ux; //n+1层 x方向速度 private double[,] uy; //n+1层 y方向速度 // D2Q9 模型常量 private const int Q = 9; //权重系数; private readonly double[] w = { 4.0/9, 1.0/9, 1.0/9, 1.0/9, 1.0/9, 1.0/36, 1.0/36, 1.0/36, 1.0/36 }; private readonly int[] cx = { 0, 1, 0, -1, 0, 1, -1, -1, 1 }; private readonly int[] cy = { 0, 0, 1, 0, -1, 1, 1, -1, -1 }; // 可视化相关 public VisualizationMode VisualizationMode { get; set; } = VisualizationMode.Streamline; private List[] streamlines; private int streamlineCounter = 0; public event Action VisualizationUpdated; public event Action StepCompleted; public void Initialize() { dx = 1.0; dy = 1.0; Lx = dx*Width; Ly = dy*Height;//长度; dt = dx; c = dx / dt; //格子的速度; // 计算粘度 niu = U * Width / Re; tau_f = 3.0 * niu + 0.5;// 松弛步长 Omega = 1.0 / tau_f;// 松弛时间-无量纲 e = new int[Q, 2]; //离散速度; for (int i = 0; i < Q; i++) { e[i, 0] = cx[i]; e[i, 1] = cy[i]; } // 初始化数组 f = new double[Width + 1, Height + 1, Q]; F = new double[Width + 1, Height + 1, Q]; rho = new double[Width + 1, Height + 1]; ux = new double[Width + 1, Height + 1]; uy = new double[Width + 1, Height + 1]; ux0 = new double[Width + 1, Height + 1]; uy0 = new double[Width + 1, Height + 1]; TimeStep = 0; Error = 0; // 初始化为静止流体 for (int i = 0; i <= Width; i++) { for (int j = 0; j <= Height; j++) { rho[i, j] = rho0; ux[i, j] = 0; uy[i, j] = 0; ux0[i, j] = 0; uy0[i, j] = 0; ux[i, Height] = U; //顶盖速度为U; for (int k = 0; k < Q; k++) { f[i, j, k] = feq(k, rho[i, j], ux[i, j], uy[i, j]); } } } InitializeStreamlines(); UpdateVisualization(); } public void Step() { if (!IsRunning) return; // 碰撞迁移步骤,去除边界. for (int i = 1; i < Width; i++) { for (int j = 1; j < Height; j++) { for (int k = 0; k < Q; k++) { int ip = i - e[k, 0]; int jp = j - e[k, 1]; F[i, j, k] = f[ip, jp, k] + (feq(k, rho[ip, jp], ux[ip, jp], uy[ip, jp]) - f[ip, jp, k]) / tau_f; } } } // 计算宏观量 for (int i = 1; i < Width; i++) { for (int j = 1; j < Height; j++) { ux0[i, j] = ux[i, j]; //记录上一次结果 uy0[i, j] = ux[i, j]; rho[i, j] = 0; ux[i, j] = 0.0; uy[i, j] = 0.0; for (int k = 0; k < Q; k++) { f[i, j, k] = F[i, j, k]; rho[i, j] += f[i, j, k]; ux[i, j] += e[k, 0]* f[i, j, k]; uy[i, j] += e[k, 1]* f[i, j, k]; } ux[i, j] /= rho[i, j]; uy[i, j] /= rho[i, j]; } } // ➤ 左右边界处理 (非平衡外推法) for (int j = 1; j < Height; j++) { for (int k = 0; k < Q; k++) { var NX = Width; // 右边界(i = Width - 1) rho[NX, j] = rho[NX-1, j]; f[NX, j, k] = feq(k, rho[NX, j], ux[NX, j], uy[NX, j]) + (f[NX-1, j, k] - feq(k, rho[NX-1, j], ux[NX-1, j], uy[NX-1, j])); // 左边界(i = 0) rho[0, j] = rho[1, j]; f[0, j, k] = feq(k, rho[0, j], ux[0, j], uy[0, j]) + (f[1, j, k] - feq(k, rho[1, j], ux[1, j], uy[1, j])); } } // ➤ 上下边界处理 for (int i = 0; i <= Width; i++) //注意这里 { for (int k = 0; k < Q; k++) { var NY = Height; // 下边界(j = 0) rho[i, 0] = rho[i, 1]; f[i, 0, k] = feq(k, rho[i, 0], ux[i, 0], uy[i, 0]) + (f[i, 1, k] - feq(k, rho[i, 1], ux[i, 1], uy[i, 1])); // 上边界(j = Height - 1) rho[i, NY] = rho[i, NY-1]; ux[i, NY] = U; // 设置移动壁速度 uy[i, NY] = 0.0; // 假设只在 x 方向移动 f[i, NY, k] = feq(k, rho[i, NY], ux[i, NY], uy[i, NY]) + (f[i, NY-1, k] - feq(k, rho[i, NY-1], ux[i, NY-1], uy[i, NY-1])); } } // 计算误差 if (TimeStep % 100 == 0) { CalculateError(); } TimeStep++; UpdateVisualization(); StepCompleted?.Invoke(TimeStep); } public void OutputData(int step, string filePath = null) { // 如果没有指定路径,使用默认文件名 string fileName = filePath ?? $\"cavity_{step}.dat\"; using (StreamWriter outFile = new StreamWriter(fileName)) { // 写入文件头 outFile.WriteLine(\"Title=\\\"LBM顶盖驱动流模拟\\\"\"); outFile.WriteLine(\"VARIABLES=\\\"X\\\",\\\"Y\\\",\\\"U\\\",\\\"V\\\"\"); outFile.WriteLine($\"ZONE T=\\\"Box\\\", I={Width + 1}, J={Height + 1}, F=POINT\"); // 写入数据(注意行列顺序与C++保持一致) for (int j = 0; j <= Height; j++) { for (int i = 0; i <= Width; i++) { // 使用InvariantCulture确保小数点格式一致 string line = string.Format(System.Globalization.CultureInfo.InvariantCulture, \"{0:F8} {1:F8} {2:F8} {3:F8}\", (double)i / Width, // X坐标归一化 (double)j / Height, // Y坐标归一化 ux[i, j], // X方向速度 uy[i, j]); // Y方向速度 outFile.WriteLine(line); } } } Console.WriteLine($\"结果已写入文件: {fileName}\"); } private void CalculateError() { double temp1 = 0.0, temp2 = 0.0; for (int i = 1; i < Width; i++) { for (int j = 1; j RenderDensity(), VisualizationMode.Velocity => RenderVelocity(), VisualizationMode.Vorticity => RenderVorticity(), VisualizationMode.Pressure => RenderPressure(), // 新增压力场渲染 VisualizationMode.Combined => RenderCombined(), VisualizationMode.Streamline => RenderStreamline(), _ => RenderCombined(), // 默认渲染密度 }; VisualizationUpdated?.Invoke(fluidImage); } // 辅助方法:获取双线性插值的速度值(提高流线平滑度) private (double ux, double uy) GetInterpolatedVelocity(double x, double y) { // 确保坐标在有效范围内 x = Math.Clamp(x, 0, Width); y = Math.Clamp(y, 0, Height); // 找到当前点所在的网格单元(i,j) int i = (int)Math.Floor(x); int j = (int)Math.Floor(y); // 确保索引在有效范围内 i = Math.Clamp(i, 0, Width - 2); // 确保i+1不会越界 j = Math.Clamp(j, 0, Height - 2); // 确保j+1不会越界 // 四个角点的坐标 double x0 = i; double x1 = i + 1; double y0 = j; double y1 = j + 1; // 双线性插值权重 double dx = x - x0; double dy = y - y0; // 确保权重在[0,1]范围内 dx = Math.Clamp(dx, 0, 1); dy = Math.Clamp(dy, 0, 1); // 获取四个角点的速度值 double ux00 = ux[i, j]; double ux10 = ux[i + 1, j]; double ux01 = ux[i, j + 1]; double ux11 = ux[i + 1, j + 1]; double uy00 = uy[i, j]; double uy10 = uy[i + 1, j]; double uy01 = uy[i, j + 1]; double uy11 = uy[i + 1, j + 1]; // 双线性插值计算ux double ux_val = (1 - dx) * (1 - dy) * ux00 + dx * (1 - dy) * ux10 + (1 - dx) * dy * ux01 + dx * dy * ux11; // 双线性插值计算uy double uy_val = (1 - dx) * (1 - dy) * uy00 + dx * (1 - dy) * uy10 + (1 - dx) * dy * uy01 + dx * dy * uy11; return (ux_val, uy_val); } private Bitmap RenderPressure() { Bitmap bmp = new Bitmap(Width + 1, Height + 1); BitmapData bmpData = bmp.LockBits( new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); // 计算压力范围 double minPressure = double.MaxValue; double maxPressure = double.MinValue; for (int i = 0; i < Width; i++) { for (int j = 0; j < Height; j++) { double pressure = rho[i, j] / 3.0; // 压力计算 if (pressure maxPressure) maxPressure = pressure; } } double range = Math.Max(maxPressure - minPressure, 0.001); unsafe { byte* ptr = (byte*)bmpData.Scan0; int bytesPerPixel = 4; int stride = bmpData.Stride; Parallel.For(0, Height, j => { byte* row = ptr + (j * stride); for (int i = 0; i < Width; i++) { double pressure = rho[i, j] / 3.0; double normalized = (pressure - minPressure) / range; // 使用蓝-白-红色谱表示压力 Color color; if (normalized < 0.5) { // 蓝色到白色渐变 int blue = 255; int green = (int)(normalized * 2 * 255); int red = (int)(normalized * 2 * 255); color = Color.FromArgb(red, green, blue); } else { // 白色到红色渐变 int red = 255; int green = (int)((1.0 - (normalized - 0.5) * 2) * 255); int blue = (int)((1.0 - (normalized - 0.5) * 2) * 255); color = Color.FromArgb(red, green, blue); } row[i * bytesPerPixel] = color.B; // Blue row[i * bytesPerPixel + 1] = color.G; // Green row[i * bytesPerPixel + 2] = color.R; // Red row[i * bytesPerPixel + 3] = 255; // Alpha } }); } bmp.UnlockBits(bmpData); return bmp; } #region 绘制密度. /// /// 绘制密度 /// /// private Bitmap RenderDensity() { Bitmap fluidImage = new Bitmap(Width, Height); // 使用LockBits提高性能 BitmapData bmpData = fluidImage.LockBits( new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); // 计算密度范围(动态适应) double minDensity = double.MaxValue; double maxDensity = double.MinValue; for (int x = 0; x < Width; x++) { for (int y = 0; y < Height; y++) { if (rho[x, y] maxDensity) maxDensity = rho[x, y]; } } double densityRange = maxDensity - minDensity; if (densityRange < 0.0001) densityRange = 0.0001; // 避免除零 unsafe { byte* ptr = (byte*)bmpData.Scan0; int bytesPerPixel = 4; int stride = bmpData.Stride; for (int y = 0; y < Height; y++) { byte* row = ptr + (y * stride); for (int x = 0; x < Width; x++) { // 动态映射密度到灰度值 double normalized = (rho[x, y] - minDensity) / densityRange; byte value = (byte)(normalized * 255); // BGRA格式 row[x * bytesPerPixel] = value; // Blue row[x * bytesPerPixel + 1] = value; // Green row[x * bytesPerPixel + 2] = value; // Red row[x * bytesPerPixel + 3] = 255; // Alpha } } } fluidImage.UnlockBits(bmpData); return fluidImage; } /// /// 绘制密度(增强对比度黑白灰版) /// /// private Bitmap RenderDensityGama() { if (rho == null || Width <= 0 || Height <= 0) return new Bitmap(1, 1); // 返回空图像以防无效状态 Bitmap fluidImage = new Bitmap(Width, Height); // 使用LockBits提高性能 BitmapData bmpData = fluidImage.LockBits( new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); // 初始化修正 - 使用安全的初始值 double minDensity = double.MaxValue; double maxDensity = double.MinValue; // 首次遍历找出大致范围 for (int x = 0; x < Width; x++) { for (int y = 0; y < Height; y++) { double density = rho[x, y]; if (!double.IsFinite(density)) continue; // 跳过非数值 if (density maxDensity) maxDensity = density; } } // 范围有效性检查 const double defaultRange = 1.0; if (minDensity > maxDensity) { minDensity = 0; maxDensity = defaultRange; } // 计算裁剪边界 double range = Math.Max(maxDensity - minDensity, defaultRange * 0.01); // 最小范围保护 double lowerBound = minDensity + range * 0.05; double upperBound = maxDensity - range * 0.05; // 确保边界有效 if (lowerBound >= upperBound) { lowerBound = minDensity; upperBound = maxDensity; } // 二次遍历计算实际有效范围 double effectiveMin = double.MaxValue; double effectiveMax = double.MinValue; int validPixelCount = 0; // 有效像素计数器 for (int x = 0; x < Width; x++) { for (int y = 0; y < Height; y++) { double density = rho[x, y]; // 跳过无效值和极端值 if (!double.IsFinite(density)) continue; if (density upperBound) continue; validPixelCount++; if (density effectiveMax) effectiveMax = density; } } // 当有效像素不足时的处理 if (validPixelCount < 10) // 至少需要10个有效点 { effectiveMin = minDensity; effectiveMax = maxDensity; } // 确保有效范围不为零 double densityRange = effectiveMax - effectiveMin; if (densityRange < double.Epsilon) densityRange = defaultRange; unsafe { byte* ptr = (byte*)bmpData.Scan0; int bytesPerPixel = 4; int stride = bmpData.Stride; // 对比度增强参数(带保护) const double gamma = 1.8; double contrastBoost = Math.Clamp(1.2, 0.1, 5.0); for (int y = 0; y < Height; y++) { byte* row = ptr + (y * stride); for (int x = 0; x < Width; x++) { double rawValue = rho[x, y]; // 处理非数值和极端值 if (!double.IsFinite(rawValue)) { // 异常值显示为品红色 row[x * bytesPerPixel] = 255; // Blue row[x * bytesPerPixel + 1] = 0; // Green row[x * bytesPerPixel + 2] = 255; // Red row[x * bytesPerPixel + 3] = 255; // Alpha continue; } // 安全映射密度到[0,1]范围 double clamped = Math.Clamp(rawValue, effectiveMin, effectiveMax); double normalized = (clamped - effectiveMin) / densityRange; normalized = Math.Clamp(normalized, 0.0, 1.0); // 二次保护 // 伽马校正增强对比度(带安全保护) double gammaCorrected = Math.Pow(normalized, gamma); gammaCorrected = Math.Clamp(gammaCorrected, 0.0, 1.0); // S曲线增强(带安全计算) double contrastEnhanced = gammaCorrected; if (gammaCorrected < 0.5) { double factor = Math.Clamp(gammaCorrected * 2, 0.0, 1.0); contrastEnhanced = Math.Pow(factor, contrastBoost) * 0.5; } else { double factor = Math.Clamp((1 - gammaCorrected) * 2, 0.0, 1.0); contrastEnhanced = 1 - Math.Pow(factor, contrastBoost) * 0.5; } byte value = (byte)(Math.Clamp(contrastEnhanced, 0.0, 1.0) * 255); // BGRA格式 row[x * bytesPerPixel] = value; // Blue row[x * bytesPerPixel + 1] = value; // Green row[x * bytesPerPixel + 2] = value; // Red row[x * bytesPerPixel + 3] = 255; // Alpha } } } fluidImage.UnlockBits(bmpData); return fluidImage; } #endregion private Bitmap RenderVelocity() { Bitmap fluidImage = new Bitmap(Width, Height); BitmapData bmpData = fluidImage.LockBits( new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); // 计算当前帧最大速度 double maxSpeed = 0.001; for (int x = 0; x < Width; x++) { for (int y = 0; y maxSpeed) maxSpeed = speed; } } unsafe { byte* ptr = (byte*)bmpData.Scan0; int bytesPerPixel = 4; int stride = bmpData.Stride; for (int y = 0; y < Height; y++) { byte* row = ptr + (y * stride); for (int x = 0; x < Width; x++) { double speed = Math.Sqrt(ux[x, y] * ux[x, y] + uy[x, y] * uy[x, y]); double normalizedSpeed = speed / maxSpeed; // 使用HSV色相表示速度方向 double angle = Math.Atan2(uy[x, y], ux[x, y]); double hue = (angle + Math.PI) / (2 * Math.PI); // 饱和度表示速度大小(非线性增强) double saturation = Math.Pow(normalizedSpeed, 0.5); // 平方根增强低速度可见性 Color color = HsvToRgb(hue, saturation, 1.0); row[x * bytesPerPixel] = color.B; // Blue row[x * bytesPerPixel + 1] = color.G; // Green row[x * bytesPerPixel + 2] = color.R; // Red row[x * bytesPerPixel + 3] = 255; // Alpha } } } fluidImage.UnlockBits(bmpData); return fluidImage; } private Bitmap RenderCombined() { int scale = 4; Bitmap fluidImage = new Bitmap(Width * scale, Height * scale); using (Graphics g = Graphics.FromImage(fluidImage)) { g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Color.Black); // 绘制密度背景(使用纹理刷提高性能) using (TextureBrush brush = new TextureBrush(densityBmp)) { brush.ScaleTransform(scale, scale); g.FillRectangle(brush, 0, 0, fluidImage.Width, fluidImage.Height); } // 自适应箭头参数 int arrowCount = Math.Max(10, Width / 15); // 箭头数量自适应 int step = Math.Max(3, Width / arrowCount); double minArrowSpeed = 0.01 * U; // 基于顶盖速度 for (int x = step / 2; x < Width; x += step) { for (int y = step / 2; y minArrowSpeed) { Point start = new Point(x * scale + scale / 2, y * scale + scale / 2); // 箭头长度自适应(速度越大箭头越长) double arrowLength = scale * 2 + speed * scale * 5; Point end = new Point( (int)(start.X + ux[x, y] * arrowLength), (int)(start.Y + uy[x, y] * arrowLength)); // 箭头颜色根据速度大小变化 // 计算速度比例值,限制在0-255范围内 int sv = Math.Clamp((int)(255 * speed / U), 0, 255); Color arrowColor = Color.FromArgb( 255, // Alpha (完全不透明) sv, // Red 2, // Green 2 // Blue ); DrawArrow(g, start, end, arrowColor, Math.Max(1, scale / 4)); } } } } return fluidImage; } /// /// 漩涡 /// /// private Bitmap RenderVorticity()// { Bitmap fluidImage = new Bitmap(Width, Height); BitmapData bmpData = fluidImage.LockBits( new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); double[,] vorticity = new double[Width, Height]; double maxVorticity = 0.001; // 计算涡量场(内部点) for (int x = 1; x < Width - 1; x++) { for (int y = 1; y maxVorticity) maxVorticity = Math.Abs(vorticity[x, y]); } } // 边界处理(置零) for (int x = 0; x < Width; x++) { vorticity[x, 0] = 0; vorticity[x, Height - 1] = 0; } for (int y = 0; y < Height; y++) { vorticity[0, y] = 0; vorticity[Width - 1, y] = 0; } unsafe { byte* ptr = (byte*)bmpData.Scan0; int bytesPerPixel = 4; int stride = bmpData.Stride; for (int y = 0; y < Height; y++) { byte* row = ptr + (y * stride); for (int x = 0; x 0) ? (int)(v * 255) : 0; int b = (v < 0) ? (int)(-v * 255) : 0; int g = (int)((1 - Math.Abs(v)) * 200); // 动态绿色分量 r = Math.Clamp(r, 0, 255); g = Math.Clamp(g, 0, 255); b = Math.Clamp(b, 0, 255); row[x * bytesPerPixel] = (byte)b; // Blue row[x * bytesPerPixel + 1] = (byte)g; // Green row[x * bytesPerPixel + 2] = (byte)r; // Red row[x * bytesPerPixel + 3] = 255; // Alpha } } } fluidImage.UnlockBits(bmpData); return fluidImage; } #region 流线 private Bitmap RenderStreamline2() { // 每4步重置一次流线 if (streamlineCounter % 4 == 0 || streamlines == null) { InitializeStreamlines(); } streamlineCounter++; Bitmap fluidImage = new Bitmap(Width, Height); using (Graphics g = Graphics.FromImage(fluidImage)) { // 使用纯黑色背景 g.Clear(Color.Black); using (Bitmap densityBmp = RenderDensityColor()) { ColorMatrix matrix = new ColorMatrix(); matrix.Matrix33 = .5f; // 极低透明度 ImageAttributes attributes = new ImageAttributes(); attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap); g.DrawImage( densityBmp, new Rectangle(0, 0, Width, Height), 0, 0, Width, Height, GraphicsUnit.Pixel, attributes); } // 使用抗锯齿保证线条平滑 g.SmoothingMode = SmoothingMode.AntiAlias; // 绘制所有流线 for (int i = 0; i 1) { // 使用朴素的细线(宽度0.5-1.0) float lineWidth = 0.3f + (i % 3) * 0.25f; // 使用固定颜色(白色)或根据速度动态着色 //Color lineColor = Color.White; // 或者根据速度动态着色(可选) Color lineColor = GetDynamicLineColor(line); using (Pen pen = new Pen(lineColor, lineWidth)) { // 绘制平滑曲线 g.DrawLines(pen, line.ToArray()); } } } } return fluidImage; } private StreamlineRenderer mSteamHelper { get; set; } private Bitmap RenderStreamline() { // 根据速度场数据完成流线渲染 if (mSteamHelper == null) mSteamHelper = new StreamlineRenderer(); // ⚠️ 这里建议确认 Width 和 Height 是否已经 +1,否则可能越界或空白边缘 mSteamHelper.SetDataParams(Width + 1, Height + 1, ux, uy); // 创建最终的渲染图像 Bitmap fluidImage = new Bitmap(Width, Height); using (Graphics g = Graphics.FromImage(fluidImage)) { // 使用黑色背景清除画布 g.Clear(Color.Black); // 渲染密度图(带低透明度叠加) using (Bitmap densityBmp = RenderDensityColor()) { ColorMatrix matrix = new ColorMatrix { Matrix33 = 0.5f // 设置图像整体 alpha 为 50% }; using (ImageAttributes attributes = new ImageAttributes()) { attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap); g.DrawImage( densityBmp, new Rectangle(0, 0, Width, Height), 0, 0, Width, Height, GraphicsUnit.Pixel, attributes); } } // 生成流线图像并绘制在画布上(叠加方式) mSteamHelper.RenderToBitmap(g); } return fluidImage; } // 绘制发光点(带光晕效果) private void DrawGlowingPoint(Graphics g, PointF point, int size, Color color) { RectangleF rect = new RectangleF(point.X - size/2, point.Y - size/2, size, size); // 绘制光晕 using (SolidBrush glowBrush = new SolidBrush(Color.FromArgb(100, Color.White))) { g.FillEllipse(glowBrush, point.X - size, point.Y - size, size*2, size*2); } // 绘制主点 using (SolidBrush brush = new SolidBrush(color)) { g.FillEllipse(brush, rect); } } // 动态计算流线颜色(基于平均速度) private Color GetDynamicLineColor(List line) { if (line.Count = 0 && x = 0 && y 0.8) return Color.FromArgb(255, 255, 50, 50); // 高速: 亮红色 if (normalizedSpeed > 0.5) return Color.FromArgb(255, 255, 150, 50); // 中高速: 橙色 if (normalizedSpeed > 0.3) return Color.FromArgb(255, 255, 255, 100); // 中速: 亮黄色 return Color.FromArgb(255, 100, 200, 255); // 低速: 亮蓝色 } private void InitializeStreamlines() { // 减少流线数量(4-8条) int lineCount = Math.Max(10, Math.Min(8, Width / 40)); streamlines = new List[lineCount]; streamlineCounter = 0; // 重置流线计数器 Random rand = new Random(); for (int i = 0; i < lineCount; i++) { streamlines[i] = new List(); float startX, startY; // 在主要涡旋区域放置起点(避开固体边界) do { if (i % 2 == 0) { // 主涡旋区域(靠近顶盖中心) startX = (float)(Width * (0.3 + 0.4 * rand.NextDouble())); // X: 30%-70% startY = (float)(Height * (0.6 + 0.3 * rand.NextDouble())); // Y: 60%-90%(靠近顶盖) } else { // 次涡旋区域(靠近底部中心) startX = (float)(Width * (0.3 + 0.4 * rand.NextDouble())); // X: 30%-70% startY = (float)(Height * (0.1 + 0.2 * rand.NextDouble())); // Y: 10%-30%(靠近底部) } } while (IsSolidBoundary((int)startX, (int)startY)); // 确保起点不在固体边界 // 添加起点到流线 streamlines[i].Add(new PointF(startX, startY)); // 沿速度场追踪后续点(最多500步,防止无限循环) int maxSteps = 600; int currentStep = 0; double currentX = startX; double currentY = startY; while (currentStep < maxSteps) { // 获取当前网格点的速度(双线性插值提高精度) (double uxVal, double uyVal) = GetInterpolatedVelocity(currentX, currentY); // 计算移动步长(速度越大,步长越小,避免跳过细节) float stepSize = (float)Math.Max(0.1f, 1.0f / (Math.Sqrt(uxVal * uxVal + uyVal * uyVal) + 1e-3f)); // 计算下一步坐标 double nextX = currentX + uxVal * stepSize; double nextY = currentY + uyVal * stepSize; // 检查是否超出计算域边界 if (nextX Width || nextY Height) break; // 添加新点到流线 streamlines[i].Add(new PointF((float)nextX, (float)nextY)); // 更新当前坐标 currentX = nextX; currentY = nextY; currentStep++; } } } #endregion // 辅助方法:判断是否为固体边界(顶盖、左右、底边界) private bool IsSolidBoundary(int x, int y) { // 顶盖驱动流中,边界是固体壁面 return x == 0 || x == Width || y == 0 || y == Height; } private bool IsBoundary(int x, int y) { return x == 0 || x == Width || y == 0 || y == Height; } private Color HsvToRgb(double h, double s, double v) { h = Math.Max(0, Math.Min(1, h - Math.Floor(h))); s = Math.Max(0, Math.Min(1, s)); v = Math.Max(0, Math.Min(1, v)); int hi = (int)(Math.Floor(h * 6)) % 6; double f = h * 6 - Math.Floor(h * 6); double p = v * (1 - s); double q = v * (1 - f * s); double t = v * (1 - (1 - f) * s); double r, g, b; switch (hi) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; default: r = v; g = p; b = q; break; } return Color.FromArgb( (int)(r * 255), (int)(g * 255), (int)(b * 255)); } // 更健壮的HSV转RGB函数 private static Color ColorFromHSV(float hue, float saturation, float value) { // 输入验证 if (float.IsNaN(hue) || float.IsInfinity(hue)) hue = 0; if (float.IsNaN(saturation) || float.IsInfinity(saturation)) saturation = 0; if (float.IsNaN(value) || float.IsInfinity(value)) value = 0; // 参数边界限制 hue = Math.Clamp(hue % 360, 0, 360); if (hue < 0) hue += 360; // 处理负值 saturation = Math.Clamp(saturation, 0, 1); value = Math.Clamp(value, 0, 1); // 核心计算(保证中间值范围安全) float c = Math.Clamp(value * saturation, 0, 1); float x = Math.Clamp(c * (1 - Math.Abs((hue / 60) % 2 - 1)), 0, 1); float m = Math.Clamp(value - c, 0, 1); float r = 0, g = 0, b = 0; // 角度范围处理 float sector = hue / 60; if (sector < 1) { r = c; g = x; b = 0; } else if (sector < 2) { r = x; g = c; b = 0; } else if (sector < 3) { r = 0; g = c; b = x; } else if (sector < 4) { r = 0; g = x; b = c; } else if (sector < 5) { r = x; g = 0; b = c; } else // 5 <= sector < 6 { r = c; g = 0; b = x; } // 安全转换到RGB字节值 int red = Math.Clamp((int)((r + m) * 255), 0, 255); int green = Math.Clamp((int)((g + m) * 255), 0, 255); int blue = Math.Clamp((int)((b + m) * 255), 0, 255); // 最后防线:确保颜色有效 if (red < 0 || green < 0 || blue 255 || green > 255 || blue > 255) { // 记录错误但不中断流程 //Debug.WriteLine($\"Invalid HSV conversion: h={hue}, s={saturation}, v={value} => r={r}, g={g}, b={b}\"); return Color.Magenta; // 视觉标记错误 } return Color.FromArgb(red, green, blue); } #endregion public void Dispose() { f = null; F = null; rho = null; ux = null; uy = null; ux0 = null; uy0 = null; GC.Collect(); } public void Start() => IsRunning = true; public void Pause() => IsRunning = false; public void Reset() { Pause(); Initialize(); } public void SetGridSize(int width, int height) { Width = Math.Clamp(width, 16, 512); Height = Math.Clamp(height, 16, 512); Initialize(); } public void SetVisualizationMode(VisualizationMode mode) { VisualizationMode = mode; UpdateVisualization(); } }