为Rhino动画实现重复并行循环,规避Parallel.For提升性能
Rhino光线追踪动画多核优化方案
核心循环的线程复用实现
方案1:使用Parallel.For(推荐,低开销且高效)
你担心Parallel.For会频繁创建新线程是误解——它默认复用.NET线程池中的线程,不会每次启动新线程,25000+次迭代的调度开销几乎可以忽略。直接替换核心循环即可:
// 替换原单线程for循环 Parallel.For(0, sphereNum, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount // 用满CPU核心,也可根据需求调整 }, i => { if (lengthtraveled > spheres[i].pathlength) { do { double interimlength = lengthtraveled - spheres[i].pathlength; var direction = spheres[i].nextdirection; var start = spheres[i].nextstart; var hits = Embree.Intersect(start, direction); spheres[i].pathlength += hits.Ray.tfar; spheres[i].step = direction.ToVector3d(); spheres[i].step.Unitize(); points[i].Location = spheres[i].nextstart.ToPoint3d() + (spheres[i].step * interimlength); spheres[i].nextstart = start + direction * hits.Ray.tfar; hits.Hit.Normalize(); spheres[i].nextdirection = Vector3.Reflect(direction, hits.Hit.normal); } while (lengthtraveled > spheres[i].pathlength); spheres[i].step *= spherestep; } else { points[i].Location += spheres[i].step; } });
关键注意点:必须确认Embree.Intersect方法是线程安全的。如果Embree实例不支持多线程调用,需要为每个线程创建独立的EMBContainer实例,避免竞态问题。
方案2:自定义固定线程池(完全控制线程复用)
如果需要严格控制线程数量(比如适配低性能多核设备),可以创建固定数量的后台线程,用同步信号量协调每一帧的计算:
1. 初始化线程池(在Animate函数开头添加)
int threadCount = Math.Max(2, Environment.ProcessorCount - 1); // 留一个核心给UI var frameStartSignal = new ManualResetEventSlim(false); var frameCompleteCountdown = new CountdownEvent(threadCount); var threads = new Thread[threadCount]; int chunkSize = sphereNum / threadCount; for (int t = 0; t < threadCount; t++) { int startIdx = t * chunkSize; // 最后一个线程处理剩余所有索引 int endIdx = t == threadCount - 1 ? sphereNum : (t + 1) * chunkSize; threads[t] = new Thread(() => { while (!cts.IsCancellationRequested) { // 等待主线程的计算启动信号 if (!frameStartSignal.Wait(cts.Token)) break; // 处理分配的索引范围 for (int i = startIdx; i < endIdx; i++) { // 原循环逻辑,和方案1一致 if (lengthtraveled > spheres[i].pathlength) { do { double interimlength = lengthtraveled - spheres[i].pathlength; var direction = spheres[i].nextdirection; var start = spheres[i].nextstart; var hits = Embree.Intersect(start, direction); spheres[i].pathlength += hits.Ray.tfar; spheres[i].step = direction.ToVector3d(); spheres[i].step.Unitize(); points[i].Location = spheres[i].nextstart.ToPoint3d() + (spheres[i].step * interimlength); spheres[i].nextstart = start + direction * hits.Ray.tfar; hits.Hit.Normalize(); spheres[i].nextdirection = Vector3.Reflect(direction, hits.Hit.normal); } while (lengthtraveled > spheres[i].pathlength); spheres[i].step *= spherestep; } else { points[i].Location += spheres[i].step; } } // 标记当前线程完成计算 frameCompleteCountdown.Signal(); // 重置信号等待下一帧 frameStartSignal.Reset(); } }); threads[t].IsBackground = true; threads[t].Start(); }
2. 触发帧计算(替换原单线程for循环)
// 启动所有线程计算当前帧 frameStartSignal.Set(); // 等待所有线程完成计算 frameCompleteCountdown.Wait(cts.Token); // 重置计数器准备下一帧 frameCompleteCountdown.Reset();
3. 资源清理(动画结束时添加)
// 在return前添加 frameStartSignal.Dispose(); frameCompleteCountdown.Dispose(); foreach (var thread in threads) { thread.Join(100); }
刷新率适配优化建议
- 动态调整步长:当前每100帧调整一次
spherestep,可以改为每20-30帧调整一次,让帧率变化更平滑。 - 减少重绘开销:将
doc.Views.Redraw()改为只重绘当前活跃视图:doc.Views.ActiveView?.Redraw(); - 预计算方向向量:
StartingVectors()生成的向量可以提前归一化,避免后续重复调用Unitize()。 - Embree性能优化:如果Embree支持,启用硬件加速(比如SIMD),或者减少每次
Intersect调用的计算量(比如限制射线长度)。 - 避免不必要的计算:对于已经完成路径的点(
spheres[i].done),可以跳过后续循环中的处理。
内容的提问来源于stack exchange,提问作者NoseHornScribe
相关产品推荐
相关产品推荐

