流体力学仿真听起来离移动端开发很远,但实际上在工业监控、教学演示类App中,流量计可视化需求并不少见。本文以仪表盘流量计为载体,用Core Graphics在iOS端实现一个能够反映流体粘度影响的模拟界面,重点展示粘度如何改变管道内的速度分布,以及如何通过雷诺数判断流态是层流还是湍流。整个过程不依赖第三方物理引擎,纯2D绘制即可获得流畅的可视化效果。

流体粘度与流动速度的物理关系
要做出有说服力的模拟,首先要理解粘度在物理层面扮演的角色。流体在管道中流动时,由于管壁与流体分子之间存在附着作用,贴近管壁的流体速度几乎为零,而管道中心处的流体速度最高。粘度越大,流体层与层之间传递这种“拖拽”作用越强,速度剖面的形状也随之变化。对于牛顿流体,速度剖面呈抛物线形,中心最大速度是平均速度的两倍;粘度升高时,抛物线会更“胖”,也就是整体速度分布更平缓。
这一关系由泊肃叶定律描述:在层流条件下,体积流量与管道半径的四次方成正比,与压力梯度成正比,与动力粘度成反比。这意味着粘度翻倍,流量直接减半。我们在模拟中用这个定律来计算中心线速度,进而驱动粒子运动。下面是核心计算代码:
import Foundation
struct FluidModel {
var dynamicViscosity: Double // 动力粘度,单位 Pa·s
var pipeRadius: Double = 0.02 // 管道半径,单位 m
var pipeLength: Double = 0.5 // 管道长度,单位 m
var pressureDiff: Double = 200 // 压差,单位 Pa
var density: Double = 998 // 密度,单位 kg/m³
/// 泊肃叶定律计算平均流速
var averageVelocity: Double {
let r2 = pipeRadius * pipeRadius
return pressureDiff * r2 / (8.0 * dynamicViscosity * pipeLength)
}
/// 层流条件下的中心最大速度,为平均速度的两倍
var maxVelocity: Double {
return averageVelocity * 2.0
}
/// 抛物线速度剖面:给定归一化半径位置 0..1,返回归一化速度 0..1
func velocityProfile(normalizedRadius: Double) -> Double {
let ratio = normalizedRadius
return max(0, 1.0 - ratio * ratio)
}
}实现时需要注意单位换算。真实物理量单位是米和帕斯卡,而屏幕坐标是像素,直接换算会出现速度过快或过慢的问题。常见做法是为模拟设置一个缩放系数,把真实速度映射到一个视觉上舒适的像素速度范围,比如0.5到120像素每秒。同时把粘度设计成可调节的参数,用户拖动滑块改变粘度,粒子速度剖面实时变化,粘度的物理意义就直观地呈现出来了。水在20摄氏度的粘度约为0.001 Pa·s,甘油约为1.4 Pa·s,这个跨越三个数量级的范围正好能让层流到接近湍流的过渡都展现出来。
雷诺数的计算与流态判定逻辑
仅靠粘度本身并不能直接判断流体是层流还是湍流,真正决定流态的是雷诺数。雷诺数是一个无量纲数,定义为流体密度、特征速度、特征长度(管道取直径)的乘积除以动力粘度。它本质上反映了惯性力与粘性力的比值:雷诺数低说明粘性力占主导,流体质点规规矩矩沿管道轴向运动,形成层流;雷诺数高说明惯性力占主导,微小扰动被不断放大,流体产生混乱的横向脉动,形成湍流。
工程上通用的判定标准是:圆管流动中雷诺数小于2300时为层流,大于4000时为充分发展的湍流,两者之间为过渡区。这个临界值并非绝对精确,过渡区内的流态会受入口条件、管壁粗糙度、振动等因素影响,但对于模拟展示来说已经足够。计算代码如下:
enum FlowRegime: String {
case laminar = "层流"
case transition = "过渡流"
case turbulent = "湍流"
}
extension FluidModel {
/// 计算雷诺数:密度 x 速度 x 特征长度 / 粘度
var reynoldsNumber: Double {
let characteristicLength = pipeRadius * 2.0
return density * averageVelocity * characteristicLength / dynamicViscosity
}
/// 根据雷诺数判定流态
var flowRegime: FlowRegime {
let re = reynoldsNumber
if re < 2300 {
return .laminar
} else if re > 4000 {
return .turbulent
} else {
return .transition
}
}
}有一个实现细节值得注意:湍流状态下泊肃叶定律的速度剖面公式不再准确。湍流时速度剖面变得更平坦,接近“钝头”形状,中心最大速度大约是平均速度的1.2倍左右,而不是层流时的2倍。更精确的描述可以使用七分之一幂律,即速度与半径偏离量的七分之一次方成正比。在模拟中可以根据流态切换速度剖面函数:层流用抛物线,湍流用幂律剖面,过渡区则在两种剖面之间做插值平滑过渡,这样用户调节参数时视觉上不会出现剖面形状的跳变。
用Core Graphics绘制仪表盘与速度剖面
界面绘制部分分三层:底层是管道和仪表盘的静态框架,中层是速度剖面的半透明填充,顶层是跟随流场运动的粒子。自定义一个继承自UIView的视图,重写draw方法用Core Graphics绘制静态部分,粒子层用CADisplayLink驱动的独立CALayer来渲染,避免每帧重绘全部内容造成性能浪费。
速度剖面的绘制思路是:把管道横截面沿半径方向采样若干个点,每个点计算归一化速度,再映射到管道宽度方向,得到一组速度矢量箭头或一条速度曲线。层流时这条曲线是清晰的抛物线,湍流时变成中间鼓起的钝形。绘制代码示例:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let pipeRect = CGRect(x: 20, y: rect.midY - 60,
width: rect.width - 40, height: 120)
let centerX = pipeRect.midY
// 绘制管道外框
context.setStrokeColor(UIColor.systemGray.cgColor)
context.setLineWidth(4)
context.stroke(pipeRect)
// 绘制速度剖面:沿垂直方向采样,水平线长度代表速度大小
let samples = 30
let model = fluidModel
let isLaminar = model.flowRegime != .turbulent
for i in 0...samples {
let t = Double(i) / Double(samples) // 归一化半径 0..1
let y = pipeRect.minY + CGFloat(t) * pipeRect.height
let v = model.velocityProfile(normalizedRadius: t)
let halfLength = CGFloat(v) * pipeRect.width / 2.0
// 速度矢量线,中心最长,管壁处为零
let path = UIBezierPath()
path.move(to: CGPoint(x: pipeRect.midX - halfLength, y: y))
path.addLine(to: CGPoint(x: pipeRect.midX + halfLength, y: y))
UIColor.systemBlue.withAlphaComponent(0.6).setStroke()
path.lineWidth = 2
path.stroke()
}
_ = centerX
}粒子动画层负责表现流体运动本身。每个粒子携带一个归一化半径属性,决定它所在流层的速度。层流时粒子严格沿水平线匀速前进;湍流时在前进基础上叠加随机的横向抖动,抖动幅度与速度脉动相关。过渡区粒子则表现为偶发的轻微横向漂移。通过CADisplayLink每帧更新粒子位置并用Core Graphics在离屏上下文绘制,60帧率的动画在中端机型上毫无压力,因为粒子总数通常控制在两百个以内,绘制成本极低。
final class ParticleLayer: CALayer {
private var particles: [Particle] = []
private var displayLink: CADisplayLink?
struct Particle {
var x: CGFloat
var normalizedRadius: CGFloat // 0 为中心,1 为管壁
var phase: Double // 用于湍流抖动的随机相位
}
func start(with model: FluidModel, bounds: CGRect) {
particles = (0..<200).map { _ in
Particle(x: CGFloat.random(in: 0...bounds.width),
normalizedRadius: sqrt(CGFloat.random(in: 0...1)),
phase: Double.random(in: 0...(2 * Double.pi)))
}
let link = CADisplayLink(target: self, selector: #selector(step))
link.add(to: .main, forMode: .common)
displayLink = link
}
@objc private func step() {
let now = CACurrentMediaTime()
var frame = UIImage()
UIGraphicsBeginImageContext(bounds.size)
if let ctx = UIGraphicsGetCurrentContext() {
for p in particles {
let v = 1.0 - p.normalizedRadius * p.normalizedRadius
let speed = CGFloat(v) * 6.0
// 湍流时按雷诺数增加横向抖动
let jitter = CGFloat(sin(now * 8 + p.phase)) * 2.0
let y = bounds.midY + p.normalizedRadius * 55 + jitter
ctx.setFillColor(UIColor.systemTeal.cgColor)
ctx.fill(CGRect(x: p.x, y: y, width: 3, height: 3))
}
frame = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
}
UIGraphicsEndImageContext()
contents = frame.cgImage
}
}仪表盘部分用一个半圆刻度盘展示当前雷诺数。刻度从0到10000,用绿色标出层流安全区,黄色标出过渡区,红色标出湍流区,指针角度随雷诺数实时变化。这种红黄绿分区的设计符合工业仪表的通用习惯,用户一眼就能看出当前工况处于什么状态。配合粘度、压差两个滑块,调节参数时指针摆动、剖面变形、粒子流态切换三者在视觉上同步联动,整个模拟的物理一致性和直观性就都具备了。如果后续想进一步提升表现力,可以引入Metal实现数千粒子的GPU渲染,但对于教学和演示场景,Core Graphics方案在开发成本和可维护性上无疑是更优的选择。
Core Graphics流体粘度模拟雷诺数修改时间:2026-09-04 09:49:05