Delphi FMX中TImage波浪扭曲动画的性能优化咨询
在Delphi 10.2 FireMonkey框架开发Windows 10桌面应用时,为TImage实现基于波浪函数(或柏林噪声函数)的扭曲动画,已预计算波浪数据存储于三维数组WaveData,通过TTimer(间隔32ms)调用DrawWaveDistortion过程实现动画,但存在帧率低、动画卡顿的问题,添加其他效果时情况更严重。以下是针对性的性能优化方案:
原始代码
procedure DrawWaveDistortion; var SourceBitmap, TargetBitmap: TBitmap; SourceBitmapData, TargetBitmapData: TBitmapData; MapAccessOk: Boolean; BMWidth, BMHeight: Integer; ix, iy: Integer; Shift: Integer; Shiftedix, Shiftediy: Integer; Color: TAlphaColor; begin SourceBitmap:=Image1.Bitmap; BMWidth:=SourceBitmap.Width; BMHeight:=SourceBitmap.Height; TargetBitmap:=TBitmap.Create(BMWidth, BMHeight); try MapAccessOk:=SourceBitmap.Map(TMapAccess.Read, SourceBitmapData); try if MapAccessOk then begin MapAccessOk:=TargetBitmap.Map(TMapAccess.Write, TargetBitmapData); try if MapAccessOk then begin for ix := 0 to BMWidth-1 do begin for iy := 0 to BMHeight-1 do begin Shift:=WaveData[StepCounter, ix, iy]; Shiftedix:=EnsureRange(ix+Shift, 0, BMWidth-1); Shiftediy:=EnsureRange(iy+Shift, 0, BMHeight-1); Color:=SourceBitmapData.GetPixel(Shiftedix, Shiftediy); TargetBitmapData.SetPixel(ix, iy, Color); end; end; end; finally TargetBitmap.Unmap(TargetBitmapData); end; end; finally SourceBitmap.Unmap(SourceBitmapData); end; Image1.Bitmap.Assign(TargetBitmap); Inc(StepCounter); if StepCounter>=MaxNumOfFunctionValues then begin StepCounter:=0; end; finally FTargetBitmap.Free; end; end;
优化方案
复用TargetBitmap,避免频繁创建销毁
将TargetBitmap声明为窗体类的成员变量,在窗体创建时初始化其尺寸与Image1.Bitmap一致,后续仅在Image1.Bitmap尺寸变化时重新创建。避免每次调用都执行TBitmap.Create和Free,减少内存分配开销。替换
GetPixel/SetPixel为直接内存访问GetPixel和SetPixel是封装后的逐像素调用,性能极低。直接通过BitmapData.Scanline获取每行像素的内存指针,结合像素格式计算内存偏移,直接读写颜色值。例如:var SourceRow, TargetRow: PAlphaColorArray; begin // ... SourceRow := SourceBitmapData.Scanline[Shiftediy]; TargetRow := TargetBitmapData.Scanline[iy]; TargetRow[ix] := SourceRow[Shiftedix]; // ... end;调整循环顺序,优化CPU缓存命中率
FireMonkey的位图数据按行存储,原始代码先遍历x轴再y轴会导致内存非连续访问,触发频繁缓存失效。改为先遍历y轴(行)再遍历x轴(列),让内存访问连续,充分利用CPU缓存:for iy := 0 to BMHeight-1 do begin for ix := 0 to BMWidth-1 do begin // 像素处理逻辑 end; end;预计算边界限制,减少运行时计算量
在预计算WaveData时,直接将偏移值Shift处理为ix+Shift不会超出0到BMWidth-1、iy+Shift不会超出0到BMHeight-1的范围,运行时无需再调用EnsureRange函数,节省计算开销。改用GPU加速(自定义Shader)
利用FireMonkey的Shader渲染能力,将波浪扭曲的计算转移到GPU执行,充分发挥GPU并行计算的优势。编写自定义Fragment Shader,在Shader中根据WaveData(可通过Uniform变量传递)计算像素偏移,直接渲染到TImage,性能提升显著。优化动画触发机制
替换TTimer为后台线程计算+UI线程同步更新的方式,避免UI线程被计算任务阻塞。或者使用FireMonkey内置的TAnimation组件体系,让动画调度由框架优化处理。
优化后示例代码(核心部分)
// 先在类中声明成员变量:FTargetBitmap: TBitmap; procedure TForm1.DrawWaveDistortion; var SourceBitmap: TBitmap; SourceBitmapData, TargetBitmapData: TBitmapData; MapAccessOk: Boolean; BMWidth, BMHeight: Integer; ix, iy: Integer; Shift: Integer; Shiftedix, Shiftediy: Integer; SourceRow, TargetRow: PAlphaColorArray; begin SourceBitmap := Image1.Bitmap; BMWidth := SourceBitmap.Width; BMHeight := SourceBitmap.Height; // 初始化或更新TargetBitmap尺寸 if (FTargetBitmap = nil) or (FTargetBitmap.Width <> BMWidth) or (FTargetBitmap.Height <> BMHeight) then begin FTargetBitmap.Free; FTargetBitmap := TBitmap.Create(BMWidth, BMHeight); FTargetBitmap.PixelFormat := SourceBitmap.PixelFormat; end; MapAccessOk := SourceBitmap.Map(TMapAccess.Read, SourceBitmapData); try if MapAccessOk then begin MapAccessOk := FTargetBitmap.Map(TMapAccess.Write, TargetBitmapData); try if MapAccessOk then begin // 调整循环顺序:先y后x for iy := 0 to BMHeight - 1 do begin TargetRow := TargetBitmapData.Scanline[iy]; for ix := 0 to BMWidth - 1 do begin Shift := WaveData[StepCounter, ix, iy]; // 假设WaveData已预计算好边界限制,无需EnsureRange Shiftedix := ix + Shift; Shiftediy := iy + Shift; SourceRow := SourceBitmapData.Scanline[Shiftediy]; TargetRow[ix] := SourceRow[Shiftedix]; end; end; end; finally FTargetBitmap.Unmap(TargetBitmapData); end; end; finally SourceBitmap.Unmap(SourceBitmapData); end; Image1.Bitmap.Assign(FTargetBitmap); Inc(StepCounter); if StepCounter >= MaxNumOfFunctionValues then StepCounter := 0; end;
内容的提问来源于stack exchange,提问作者user3384674

