目录
1.UMaterialExpressionSpeedTree::Compile
4.Scene中的SpeedTree应用 (Scene->UpdateSpeedTreeWind)
5.GetSpeedTreeVertexOffsetInner
由于之后做风场,也需要在SpeedTree中加入风力场的交互,因此这里先整理分析一下原本UE4里面SpeedTree的逻辑,方便在之后修改。
今天的主角就是它了
。
可以看到这个节点有四个输入,在MaterialExpressionSpeedTree.h里也可以看到,分别是
GeometryInput,类型有
UENUM()
enum ESpeedTreeGeometryType
{
STG_Branch UMETA(DisplayName="Branch"),
STG_Frond UMETA(DisplayName="Frond"),
STG_Leaf UMETA(DisplayName="Leaf"),
STG_FacingLeaf UMETA(DisplayName="Facing Leaf"),
STG_Billboard UMETA(DisplayName="Billboard")
};
WindInput,类型有
UENUM()
enum ESpeedTreeWindType
{
STW_None UMETA(DisplayName="None"),
STW_Fastest UMETA(DisplayName="Fastest"),
STW_Fast UMETA(DisplayName="Fast"),
STW_Better UMETA(DisplayName="Better"),
STW_Best UMETA(DisplayName="Best"),
STW_Palm UMETA(DisplayName="Palm"),
STW_BestPlus UMETA(DisplayName="BestPlus"),
};
LODInput,类型有
UENUM()
enum ESpeedTreeLODType
{
STLOD_Pop UMETA(DisplayName="Pop"),
STLOD_Smooth UMETA(DisplayName="Smooth")
};
以及额外的弯曲偏移 ExtraBendWS。
整个Shader过程也就是在处理这些type的不同的偏移计算方式。
好的,就从节点的Compiler开始吧。
1.UMaterialExpressionSpeedTree::Compile
在MaterialExpressions.cpp里
很简单,就是Compile 4中Input的输入,没有Input就用默认值,之后,调用到实际的Compiler->SpeedTree
2.Compiler->SpeedTree
在HLSLMaterialTranslator.h里,即材质的模板翻译,会把节点信息翻译成HLSL,基本逻辑如下:
- 检查FeatureLevel是否支持
- 检查不允许应用到SkeletalMesh上
- 检查不是VS以外的Shader阶段
- 好,之后会把 bUsesSpeedTree 设为true,用于之后宏定义 USES_SPEEDTREE 设为 1,设置纹理采样坐标NumUserVertexTexCoords
- 设置 bEnablePreviousFrameInformation ,这个会用于shader设置 bUsePreviousFrame 参数,判断SpeedTreeData的来源
- 最终把 GetSpeedTreeVertexOffset(Parameters, %s, %s, %s, %g, %s, %s, %s)加入Shader,参数就是上面介绍的那些
3.GetSpeedTreeVertexOffset
这个就到shader里了,主要两个文件 MaterialTemplate.ush ,这就是一般材质最终翻译的模板文件,SpeedTreeCommon.ush,定义了很多SpeedTree有关的函数
这里主要调用 GetSpeedTreeVertexOffsetInner 方法(最重要的方法),唯一逻辑是根据上一步的传参 bUsePreviousFrame ,填充不同的FSpeedTreeData。
如果为true,调用 GetPreviousSpeedTreeData,在里,使用SpeedTreeData 的 Prev前缀的数据
否则,调用 GetCurrentSpeedTreeData,使用SpeedTreeData 的 默认的数据填充。
4.Scene中的SpeedTree应用 (Scene->UpdateSpeedTreeWind)
在介绍最后的算法之前,先介绍一下场景中是怎么应用和填充SpeedTree的,就是上面的TreeData到底是怎么来的,这样后面Shader里用到这些变量才能知道这些东西都是从哪里计算得来的。
在Scene中,跟SpeedTree有关的接口主要是
virtual void AddSpeedTreeWind(class FVertexFactory* VertexFactory, const class UStaticMesh* StaticMesh) override {}
virtual void RemoveSpeedTreeWind_RenderThread(class FVertexFactory* VertexFactory, const class UStaticMesh* StaticMesh) override {}
virtual void UpdateSpeedTreeWind(double CurrentTime) override {}
virtual FUniformBufferRHIParamRef GetSpeedTreeUniformBuffer(const FVertexFactory* VertexFactory) const override { return FUniformBufferRHIParamRef(); }
首先有类似之前WindField的 Add/RemoveSpeedTreeWind 的逻辑,每有一种SpeedTreeWind的StaticMesh,Scene会Add一个SpeedTreeWindComputation,然后保存在SpeedTreeWindComputationMap里, 然后就是场景主要的模拟更新的逻辑了,主要就一个方法UpdateSpeedTreeWind。
它的执行在 void UWorld::Tick里,每帧更新
if( Scene )
{
// Update SpeedTree wind objects.
Scene->UpdateSpeedTreeWind(TimeSeconds);
}
里面的调用主要就是遍历循环 SpeedTreeWindComputationMap ,更新每一个Wind的数据了,主要逻辑如下,这里都在渲染线程执行
- 调用Scene->GetDirectionalWindParameters(WindDirection, WindSpeed, WindMinGustAmt, WindMaxGustAmt);获取当前Scene的全局WindDirection, WindSpeed, WindMinGustAmt, WindMaxGustAmt. 这个方法内部就是之前说到的,遍历场景所有的 WindDirectionalSources ,然后通过计算权重,得到一个全局的结果,哈哈,写到这,有种之后会慢慢的全局都串起来的感觉
- 得到全局数据之后,就是依次遍历所有SpeedTreeWindComputation
- 检查Computation是否合法
- 开始更新数据,依次调用
WindComputation->Wind.SetDirection(WindDirection); WindComputation->Wind.SetStrength(WindSpeed); WindComputation->Wind.SetGustMin(WindMinGustAmt); WindComputation->Wind.SetGustMax(WindMaxGustAmt); WindComputation->Wind.Advance(true, CurrentTime);
首先设置四个全局数据,最后调用Advance更新,这个方法代码挺长不再细说,主要目标就是根据更新的各种Target值,做插值Interpolate,这里会更新到各种SpeedTree不同类型的数据。
-
计算好了之后,就是各种Set了,以及更新UniformBuffer,这里Set的就是之前说到的传入TreeData的数据,这里Set 和声明的地方的一一对应,这样可以在之后Shader应用的地方找到。
SET_SPEEDTREE_TABLE_FLOAT4V(WindVector, SH_WIND_DIR_X); SET_SPEEDTREE_TABLE_FLOAT4V(WindGlobal, SH_GLOBAL_TIME); SET_SPEEDTREE_TABLE_FLOAT4V(WindBranch, SH_BRANCH_1_TIME); SET_SPEEDTREE_TABLE_FLOAT4V(WindBranchTwitch, SH_BRANCH_1_TWITCH); SET_SPEEDTREE_TABLE_FLOAT4V(WindBranchWhip, SH_BRANCH_1_WHIP); SET_SPEEDTREE_TABLE_FLOAT4V(WindBranchAnchor, SH_WIND_ANCHOR_X); SET_SPEEDTREE_TABLE_FLOAT4V(WindBranchAdherences, SH_GLOBAL_DIRECTION_ADHERENCE); SET_SPEEDTREE_TABLE_FLOAT4V(WindTurbulences, SH_BRANCH_1_TURBULENCE); SET_SPEEDTREE_TABLE_FLOAT4V(WindLeaf1Ripple, SH_LEAF_1_RIPPLE_TIME); SET_SPEEDTREE_TABLE_FLOAT4V(WindLeaf1Tumble, SH_LEAF_1_TUMBLE_TIME); SET_SPEEDTREE_TABLE_FLOAT4V(WindLeaf1Twitch, SH_LEAF_1_TWITCH_THROW); SET_SPEEDTREE_TABLE_FLOAT4V(WindLeaf2Ripple, SH_LEAF_2_RIPPLE_TIME); SET_SPEEDTREE_TABLE_FLOAT4V(WindLeaf2Tumble, SH_LEAF_2_TUMBLE_TIME); SET_SPEEDTREE_TABLE_FLOAT4V(WindLeaf2Twitch, SH_LEAF_2_TWITCH_THROW); SET_SPEEDTREE_TABLE_FLOAT4V(WindFrondRipple, SH_FROND_RIPPLE_TIME); SET_SPEEDTREE_TABLE_FLOAT4V(WindRollingBranch, SH_ROLLING_BRANCH_FIELD_MIN); SET_SPEEDTREE_TABLE_FLOAT4V(WindRollingLeafAndDirection, SH_ROLLING_LEAF_RIPPLE_MIN); SET_SPEEDTREE_TABLE_FLOAT4V(WindRollingNoise, SH_ROLLING_NOISE_PERIOD);
声明的地方呢,在SpeedTreeWind.h 里,
enum EShaderValues { // g_vWindVector SH_WIND_DIR_X, SH_WIND_DIR_Y, SH_WIND_DIR_Z, SH_GENERAL_STRENGTH, // g_vWindGlobal SH_GLOBAL_TIME, SH_GLOBAL_DISTANCE, SH_GLOBAL_HEIGHT, SH_GLOBAL_HEIGHT_EXPONENT, // g_vWindBranch SH_BRANCH_1_TIME, SH_BRANCH_1_DISTANCE, SH_BRANCH_2_TIME, SH_BRANCH_2_DISTANCE, // g_vWindBranchTwitch SH_BRANCH_1_TWITCH, SH_BRANCH_1_TWITCH_FREQ_SCALE, SH_BRANCH_2_TWITCH, SH_BRANCH_2_TWITCH_FREQ_SCALE, // g_vWindBranchWhip SH_BRANCH_1_WHIP, SH_BRANCH_2_WHIP, SH_WIND_PACK0, SH_WIND_PACK1, // g_vWindBranchAnchor SH_WIND_ANCHOR_X, SH_WIND_ANCHOR_Y, SH_WIND_ANCHOR_Z, SH_WIND_PACK2, ... }
变量对应。
5.GetSpeedTreeVertexOffsetInner
到了最后最重要的算法部分了
完整形参是
float3 GetSpeedTreeVertexOffsetInner (FMaterialVertexParameters Parameters, int GeometryType, int WindType, int LODType, float BillboardThreshold, bool bExtraBend, float3 ExtraBend, FSpeedTreeData STData)
ps: WIND_EFFECT_FROND_RIPPLE_ADJUST_LIGHTING 由于目前(UE4.22.3)都没有用到,暂不考虑
流程如下
(1)计算LocalToWorld矩阵以及LocalPosition,如果是instance的,从Pamaters中拿取,否则通过GetPrimitiveData计算
(2)如果非Instance,并且不是勾选USE_DITHERED_LOD_TRANSITION,那如果LODType == SPEEDTREE_LOD_TYPE_SMOOTH,会计算一下 LodInterp,用于之后计算 得到的 LODPos和FinalPosition 中间插值
(3)TreeScale ,默认只计算uniform的,使用LocalToWorld的第三列,并计算长度,如果有必要不用uniform的,官方这里也提供了代码,只不过注释掉了,可以手动修改
(4)mul(LocalPosition, LocalToWorld) / TreeScale 得到 OriginalPosition,并缓存为FinalPosition
(5)如果是GeometryType 是 BILLBOARD,且 BillboardThreshold <1.0 , ViewForward 视口Forward 和 .TangentToWorld[2]即 VertexNormal (由于是billboard,只取了xy) 做点乘,夹角 < (-1.0 + BillboardThreshold * 0.25)),就把FinalPosition置为0
(6)如果是 Branch || Frond
a.非Instance且为SPEEDTREE_LOD_TYPE_SMOOTH,做FinalPosition的lerp
b.如果是Frond,且 WindType 是SPEEDTREE_WIND_TYPE_PALM,从定好的uv下标读取数据后,调用RippleFrond
RippleFrond
float3 RippleFrond(FSpeedTreeData STData,
float3 vPos,
inout float3 vDirection,
float fU,
float fV,
float fPackedRippleDir,
float fRippleScale,
float fLenghtPercent)
- 内部有两种实现 RippleFrondOneSided 和 RippleFrondTwoSided,默认调用 RippleFrondOneSided
-
TrigApproximate 传入 SH_FROND_RIPPLE_TIME和 SH_FROND_RIPPLE_TILE 得到 vOscillations,
-
计算 vOffset = fRippleScale * vOscillations.x * SH_FROND_RIPPLE_DISTANCE * vDirection
-
返回 FinalPosition += vOffset
(7)如果是FacingLeaf ||(Leaf && (LOD_SMOOTH || WindType不是 FASTEST 以及 PALM))
a.从纹理中缓存Anchor位置
b.如果是FacingLeaf,FinalPosition = LocalPosition - Anchor,然后根据视口朝向ResolvedView 校正 相对偏移
c.用LocationToworld,把Anchor转到世界空间
d.如果是Leaf,此时再计算FinalPosition -= Anchor,之后用相对偏移做旋转
e.LOD Lerp
f.计算LeafWind,
g.加回Anchor到FinalPosition
LeafWind
float3 LeafWind(FSpeedTreeData STData,
bool bLeaf2,
float3 vPos,
inout float3 vDirection,
float fScale,
float3 vAnchor,
float fPackedGrowthDir,
float fPackedRippleDir,
float fRippleTrigOffset,
int WindType)
- 如果WindType是最高级的 SPEEDTREE_WIND_TYPE_BEST_PLUS,则计算LeafRipple
-
LeafRipple:
-
fMoveAmount = fAmount* (fTime + fTrigOffset)(TrigApproximate)
-
如果 bDirectional , vPos +=vDirection.xyz * fMoveAmount * fScale;
-
否则,vPos += vRippleDir * fMoveAmount * fScale
-
-
如果WindType 高于 BestType,调用LeafTumble
-
LeafTumble
-
这里有一部分代码我也不是很明白具体的原因,如果有比较厉害的人恰巧看到这里,欢迎分享一下
-
float3 vFracs = frac((vAnchor + fTrigOffset) * 30.3); float fOffset = vFracs.x + vFracs.y + vFracs.z; float4 vOscillations = TrigApproximate(float4(fTime + fOffset, fTime * 0.75 - fOffset, fTime * 0.01 + fOffset, fTime * 1.0 + fOffset));
计算得到vOscillations ??
-
fLength 记录 vPos 到 Anhor的长度
-
float fOsc = vOscillations.x + vOscillations.y * vOscillations.y; ??
-
计算 Tumble矩阵 matTumble = 绕 vGrowthDir 旋转 fScale * fTwist * fOsc
-
如果 WindType 是BestPlus,
-
cross(vGrowthDir ,WindVector)得到旋转轴 vAxis
-
dot(vGrowthDir ,WindVector)得到角度的cos值 fDot
-
fAngle = acos(fDot);
-
fOsc = vOscillations.y - vOscillations.x * vOscillations.x;??
-
计算fTwitch = Twitch(vAnchor.xyz, vTwitch.x, vTwitch.y, vTwitch.z + fOffset); ??
-
在上面 matTumble 的基础上,再乘以旋转矩阵 绕vAxis 旋转 fScale * (fAngle * fAdherence * fAdherenceScale + fOsc * fFlip + fTwitch) ??
-
-
matTumble 旋转 vDirection 和 vOriginPos,最终输出 normalize(vOriginPos)*fLength+vAnchor
-
(8)如果WindType不是Fastest以及Fast,会应用 BrandWind到各种Geometry类型
BranchWind
float3 BranchWind(FSpeedTreeData STData, float3 vPos, float3 vInstancePos, float4 vWindData, int WindType)
- 如果WindType是Palm类型,调用DirectionalBranchWindFrondStyle
- fWeight 和 foffset 记录在 TexCoords[2]里,UnpackNormalFromFloat(fOffset) * fWeight 得到 vWindVector
- fTime 使用InstancePos 给time加一些随机扰动
- Oscillate 计算 vOscillations 和 fOsc
- vPos.xyz += vWindVector * fOsc * fDistance;
- Turbulence 计算 fAdherenceScale
- 如果是whip,fAdherenceScale += vOscillations.w * STData.WindVector.w * fWhip;
- 得到最后 vPos.xyz += STData.WindVector.xyz * fAdherence * fAdherenceScale * fWeight;
- 否则调用SimpleBranchWind,就是比上面少了最后三步
(9)应用 GlobalWind 以及 传入的 ExtraBendWS,调用GlobalWind
GlobalWind
float3 GlobalWind(FSpeedTreeData STData, float3 vPos, float3 vInstancePos, bool bPreserveShape, bool bGlobalWind, bool bExtraBend, float3 ExtraBend)
- 由于之前调用的地方 bPreserveShape 为true,计算 fLength = length(vPos.xyz) 为实际length
- 根据SH_GLOBAL_HEIGHT 和 up轴向 计算 fAdjust = vPos.z * SH_GLOBAL_HEIGHT -0.25
- fAdjust 计算指数,pow(fAdjust, SH_GLOBAL_HEIGHT_EXPONENT)
- 如果有 GlobalWind
- TrigApproximate (pos.x * SH_GLOBAL_TIME + pos.y * SH_GLOBAL_TIME *0.8f)得到vOscillations
- 同样 fOsc = vOscillations.x + (vOscillations.y * vOscillations.y)
- fMoveAmount = fOsc *SH_GLOBAL_DISTANCE
- 基于 direction adherence 计算move的最小值 fMinMove
- fMinMove = SH_GLOBAL_DIRECTION_ADHERENCE/SH_GLOBAL_HEIGHT
- fMoveAmount += fMinMove;
- fMoveAmount *= fAdjust;
- 叠加Global的direction 到非up轴向,vPos.xy += (SH_WIND_DIR_X,SH_WIND_DIR_Y) * fMoveAmount;,这里Global Wind 不会叠加 up轴向的偏移
- 如果有 ExtraBend
- 叠加ExtraBend到非up轴向,vPos.xy += ExtraBend.xy * fAdjust;
- 如果PreserveShape
- vPos.xyz = normalize(vPos.xyz) * fLength;,归一朝向*长度
- 返回 vPos
(10) 最后,返回世界空间偏移PositionOffset = (FinalPosition - OriginalPosition) * TreeScale;
好了,基本整体的流程就到这里啦,先这样~