找回密码
 立即注册
查看: 355|回复: 0

游戏引擎应用-Unreal Engine的Niagara Fluids Template分析 ...

[复制链接]
发表于 2022-10-4 06:22 | 显示全部楼层 |阅读模式
0. 前言

本人之前在研究基于网格的方法,链接如下,这里面写了一些与流体模拟有关的基础知识,后面想往Unity上迁移,但是感觉整个流体设计框架是个问题,故分析Niagara Fluids的设计思路以作参考,当然Niagara Fluids基于Niagara实现,自己设计的框架可以更灵活一些。前面自己学习了些网格方法的基础,现在利用Niagara学习一下流体模拟的整体框架设计。
不过个人是Niagara的新手,所以是小白视角,可能根据认知写的详略不当,见谅。前面有一篇理论推导,在后面涉及到流体模拟物理方法的地方需要用到。
1. Grid 3D Gas Smoke

首先在Unreal Engine中下载好Niagara以及Niagara Fluids插件,在Content Drawer中右键选择Niagara System,选择New system from a template or behavior example,再点击左边的Templates,选择Grid 3D Gas Smoke,右键Edit就可以打开这个蓝图了(或者也可以放在一个Level里面去,这样还可以看见User控制的参数),我自己是先从这个例子分析起的。
1.1 Emitter部分

对于这个例子来说,Emitter部分更像是一个变量定义和环境参数设定的地方。GPU和CPU的使用设定在PropertiesSim Target选项;系统的最大步长在PropertiesMax Delta Time Per Tick
1.1.1 Emitter Spawn

这个部分设定了Grid的属性。
①Grid 3D GAS CONTROLS SPAWN设置了Grid、Simulation等参数,不过这些单单看这个List肯定是晕头转向,我们在需要拿取的时候再回来看设置,其中在Grid中有部分参数是用Link的方法赋值的,并且Link的变量上面有USER标识,这部分就是我们用户可以实例化之后修改的量,这些量对于每个实例都可以自由设置不相互影响。这个中间的红框就是这个Module的输入界面,双击蓝图中的这一条就可以看见其非常详细的参数配置,不过我们后面在使用一个量的时候,会解释其来源,所以可以先只需要稍微浏览。还可以通过查看Attribute前面的Tag判断他是不是在Grid 3D GAS CONTROLS SPAWN被初始化的。



USER标识变量

②Grid 3D GAS SCALABILITY SPAWN是设置了网格的可扩展性,通俗来讲就是,网格在世界空间中的X、Y、Z轴的长度是固定的,然而我们怎么通过降分辨率提升性能呢?这就需要XYZ轴能够支持的网格数减小,同时,对于不同的场量,需要的场的分辨率也不尽相同,这样降低了模拟的网格分辨率,这样就能减少场量计算次数。同样我们还可以限制线性方程求解的迭代次数提高性能。这些都是在性能效果中间做抉择。双击可以点开,可以看见根据输入QualityEngineQualityLevel匹配,设置了很多与上面说的可扩展性相关的数值,在我的机器上,如果调整为了Epic,那么Editor里面的Niagara就会执行这一步。
③Set,纯看数据太复杂了,下面的Set就是一个比较具体的东西,Set设定了6个Grid,每个Grid都是Grid3D Collection类型,这个主要在NiagaraDataInterfaceGrid3DCollection.h中。其中每个网格体可以设定自己的Override Buffer Format,这个部分逻辑在下面,可以发现默认的GridFormat是HalfFloat半精度。
//------NiagaraDataInterfaceGrid3DCollection.cpp
        ENiagaraGpuBufferFormat BufferFormat = bOverrideFormat ? OverrideBufferFormat : GetDefault<UNiagaraSettings>()->DefaultGridFormat;
        if (GNiagaraGrid3DOverrideFormat >= int32(ENiagaraGpuBufferFormat::Float) && (GNiagaraGrid3DOverrideFormat < int32(ENiagaraGpuBufferFormat::Max)))
        {
                BufferFormat = ENiagaraGpuBufferFormat(GNiagaraGrid3DOverrideFormat);
        }
        InstanceData->PixelFormat = FNiagaraUtilities::BufferFormatToPixelFormat(BufferFormat);

//------NiagaraCommon.h
/** Niagara supported buffer formats on the GPU. */
UENUM()
enum class ENiagaraGpuBufferFormat : uint8
{
        /** 32-bit per channel floating point, range [-3.402823 x 10^38, 3.402823 x 10^38] */
        Float,
        /** 16-bit per channel floating point, range [-65504, 65504] */
        HalfFloat,
        /** 8-bit per channel fixed point, range [0, 1]. */
        UnsignedNormalizedByte,

        Max UMETA(Hidden),
};

//------NiagaraSettings.h
        /** The default buffer format used by all Niagara Grid Data Interfaces unless overridden. */
        UPROPERTY(config, EditAnywhere, Category = Renderer)
        ENiagaraGpuBufferFormat DefaultGridFormat = ENiagaraGpuBufferFormat::HalfFloat;
④Set Resolution,设定好Grid之后,我们还需要对Grid其他的参数进行配置,下面的函数就是对这几个Grid的Resolution分别进行设置,双击可以进入一个小复杂的蓝图,这个蓝图是Grid3D_SetResolution,逻辑可以用下面指出的Switcher分离开,我们详细看下他的Grid有多少种初始化Resolution的方式



Grid3D_SetResolution

Switcher就非常有意思,这里面就是表示了这个Module可以支持的初始化方式,在这个例子中用了其中两种初始化方式,第一种是MaxAxis,第二种就是OtherGrid。在示例中,几个不同的Grid设定如下:



Resolution Method初始化方法,特别的是,这个流控制是后面的节点控制前面的节点,有点动画蓝图逻辑的味道

Max Axis
输入NumCellsMaxAxisResolution MultWorldGridExtents,只有Scalar Grid用此初始化方式。
其中,NumCellsMaxAxisGrid 3D GAS CONTROLS SPAWN中被User Input ResolutionMaxAxis初始化,Resolution Mult为1,WorldGridExtentsGrid 3D GAS CONTROLS SPAWN中被User Input WorldSpaceSize初始化。
Max Axis的初始化方法如下,其本质是根据空间网格最长边能包含的网格设定范围,所以后续就需要考虑不足的边进行调整,调整完毕之后再将目前的WorldGridExtents与NumCellsXYZ作为输出。
float CellSize = max(WorldGridExtents.z, max(WorldGridExtents.x, WorldGridExtents.y)) / NumCellsMaxAxis;
NumCellsX = floor(WorldGridExtents.x / CellSize);
NumCellsY = floor(WorldGridExtents.y / CellSize);
NumCellsZ = floor(WorldGridExtents.z / CellSize);

//...对不足的边进行调整
Out_WorldGridExtents = float3(NumCellsX, NumCellsY, NumCellsZ) * CellSize;根据计算出来的NumCells和Resolution Mult相乘,得到最终的NumCells,也就是每条边应该有多少个方格,赋值给当前的Grid。再通过上面输出的实际空间WorldGridExtents与NumCells求出均匀网格的小格WorldCellSize,同时输出WorldGridExtents,这些数据都是后续计算的时候需要的。
Other Grid
输入OtherGridResolution MultWorldGridExtents,除了Scalar Grid都使用这种方式。
其中,计算方法就是从OtherGrid获取NumCellsXYZWorldGridExtents直接使用输入,后续的方法就是和上面计算完毕之后相同了,等于说同等条件下的网格只需要计算一个Max Axis,其他就可以“渔翁得利”了。
所以剩下的网格的区别就是OtherGridResolution Mult。LightingGrid与SimGrid都是使用Scalar Grid作为OtherGrid,其Resolution Mult分别在Grid 3D GAS CONTROLS SPAWN被赋值。
而剩下三个网格就是SimGrid的直接复制,因为网格总共就只有两种,一种是给模拟的,一种是给光照的,如果模拟默认场量的网格都一致,那么就没必要调整了。



这里是Input显示,双击中间蓝图的这一项可以进去看赋值关系,因为有些东西很少用,在内部Input就被标记了Advanced Display属性,所以要记得开Advanced

需要注意的是,整个系统存在很多同名变量,区别在于其Tag,Tag标记的顺序与Set里面有关。所以在后面就可以看见不同Tag的WorldCellSizeWorldGridExtents,而其序号就对应了Set中的顺序(因为SimGrid是第一个,按理来说是000,在程序自动生成的时候不给000序号,所以其没有序号),用的比较多的就是004号Scalar Grid。
⑤Modify Grid Scale提供了一种Scale的初始化,其中标量场输入提供ScalarGridWorldCellSize,模拟场输入提供SimGridWorldCellSizeWorldGridExtents也是SimGrid的,还输入了一个引擎提供的RenderScale以及恒为1的SimDxScale。
根据这些Scale信息,模块得到了Scalar的dx、标准的dx、Render的dx,以及WorldRenderGrid的Scale。计算如下,等于说就是给几个原本的量乘上了其对应的Scale。如果在这里对这几个不同的Grid用处不太清晰,下面一步就提供了对应关系。


⑥Create Grid Attribute Indices设定了多个Grid的Index,其中Velocity为SimGrid,Density、Temperature为ScalarGrid,SolidVelocity、Boundary为TransientGrid,Pressure为PressureGrid,SimFloat为TemporaryGrid。这几个Grid都是之前初始化过的Grid,所以其Size等属性已经存在了。里面有写Get Attribule Index的函数,这个的综合分析可以在目前的网页搜索:Link To 1.2.1 ⑥(还方便回来)
⑦g&SimRT两个地方分别是一个Set,先Set重力常数,从输入的标签可以看出来Gravity是从哪里初始化的,SimRT是模拟时使用的Render Target,应该是方便HLSL读取的一个Texture,这一条里面初始化很简单是因为下一步就是用Grid去初始化这个Render Target
⑧Grid 3D Init RT是给刚刚设置好的数据结构初始化,里面的结构也非常简单,就是将RenderTarget的Size赋值成了参考的Grid的NumCellsXYZ,并且用一个Resolution Mult给Scale一下,不过这个例子中Resolution Mult就是1,所以就等于将Grid三个轴上的Cell数量给RenderTarget这样一个三维Texture。
至此,如果自己琢磨一下,整个过程其实逻辑部分并不复杂,复杂的是无尽的参数,而这些参数因为Tag非常好的展示了部分集中初始化的参数位置,比如Grid 3D GAS CONTROLS SPAWN、以及后面的Grid 3D GAS CONTROLS UPDATE,其实整体阅读难度是大大降低了的。
1.1.2 Emitter Update

①Emitter State似乎是Niagara用来控制粒子系统运行的东西,这里就是使用系统自动的Update参数,所以暂时可以先不管。
②Grid 3D GAS CONTROLS UPDATE和上面的那个异曲同工,就是基础参数配置,后续用到的Attribute前面的Tag可以看到是否从这里初始化,主要配置的参数类型是TurbulenceRender。
Grid 3D GAS SCALABILITY UPDATE主要调整了一些渲染的伸缩性。
④dt对DeltaTime的设置,这样的初始化应该是这个系统的一个特性,其中OverrideDeltaTime在这个例子中为False,所以采用的dt就是引擎提供的DeltaTime与上面设定的DeltaTimeScale相乘,DeltaTimeScale在这个例子中恒为1。
⑤Grid 3D Create Unit to World Transform是初始化局部空间到世界空间的坐标变换,其中一个比较核心的变换就是UnitToWorld,其表明的意思是从Local(0~1,0~1,0~1)转换到世界坐标系,因为蓝图画的很乱,下面展示HLSL部分,并且对其输入进行解释。



UnitToWorld的变换构成

TLocal将(0~1)^3转化到(-0.5~0.5)^3,
S的三个轴上的值就是WorldGridExtents的值,此时结果就是长度与世界坐标对应的了,
再进行RLocal,这个变换很有名堂,根据Camera Facing的状态,会有不同的LocalRotation以及GlobalRotation,这个例子中CameraFacing默认关闭,可能在部分2D的例子中才是开启的,关闭的时候RLocal为单位矩阵,RotationMatrix为Component的Rotation变换。



Rotation的初始化

TPivot是Scale之后的局部空间中心,LocalPivot(0,0,0.5)与WorldGridExtents相乘得到的,这个变换是因为之前做了中心变换,而整个Actor的中心在下表面中心,这一步变换就是将坐标系转化为Actor的局部坐标系。


R&T就是在Actor的局部坐标系转化到世界坐标系。整个UnitToWorld的计算方案就是如此,基本上把这个模块的所有计算都涉及到了,其他的变换在后续需要使用的时候会稍微解释下其功能。所以现在就有这样几个坐标系,名字是我现编的。
一个是UnitLocal,就是从左下角(0,0,0)到右上角(1,1,1);
一个是ActorLocal,左下角(-WorldGridExtents.x/2,-WorldGridExtents.y/2,0),右上角(WorldGridExtents.x /2,WorldGridExtents.y/2,WorldGridExtents.z);
以及世界坐标系,这里UnitToWorld就是UnitLocal到世界坐标系。
⑥Grid 3D Draw Bounds Overlay是绘制整个调整过后的Grid,其绘制由用户(以及Grid 3D GAS SCALABILITY SPAWN)控制,开启之后会在世界坐标绘制边界矩形,在边界矩阵六个方向中央绘制表示CellSize的小矩形。如果你理解上面UnitToWorld的计算方式,这个就很简单了。
⑦Set这个Set看起来像是一个Cache。
⑧Grid 3D Set Bounds双击打开,很简单的逻辑,就是将ActorLocal的左下角和右上角设置到Emitter Properties中。
1.2 Particle部分

这个就是求解的地方,注意每个Generic Simulation Stage Settings里面都可以设置,下文中,带上初始化位置TAG的控制变量,将不会被赘述初始化位置,请读者注意,如果要找,首先查看输入,再看Link关系或者直接数值初始化,如果是Link,那么查看TAG,寻找其初始化方法。
①Data Interface:设置这个Stage中使用的DataInterface,暂时不知道是怎样的运行逻辑,但是可以观察到每个Stage里面Set的所有Grid其数据类型都是与DataInterface一致的。
②Execute Behavior:可以设置初始化执行还是运行时执行。
1.2.1 Initial Scalar Grid

这里面逻辑很简单,就是初始化DensityTemperature,每个GridCell设置为0。
1.2.2 Initial Sim Grid

①Grid 3D Turbulence这个有点小复杂,输入Calculate Turbulence控制是否计算,不然就是(0,0,0),如果需要计算,最主要是这两个计算过程,左边的是利用DensityTemperature与预设的值求出Gain,然后利用Gain在3D Perlin Noise里面采样,怎么说呢?感觉用处不大。


②Set Velocity将之前计算出来的Turbulence结果作为Velocity(SimGrid)的初始化。
1.2.3 Initial Lighting Grid

设置两个LightGird的初值。
1.2.4 Compute Curl

①Grid 3D Compute Curl内置的计算旋度的方法,这里面可以稍微看下Data Interface是怎么运作的,我浅看了一下,确实有点反直觉,其中我们主要分析GetGridValue是怎么定义的
// NiagaraHlslTranslator.cpp
// 这个文件非常庞大,其功能是将蓝图中需要的信息转化为HLSL文件,我讲HLSL函数调用的部分提取出来了
const FNiagaraTranslateResults &FHlslNiagaraTranslator::Translate(
        const FNiagaraCompileRequestData* InCompileData, const FNiagaraCompileRequestDuplicateData* InCompileDuplicateData,
        const FNiagaraCompileOptions& InCompileOptions, FHlslNiagaraTranslatorOptions InTranslateOptions)
{
//...       
        if (TranslateResults.bHLSLGenSucceeded)
        {
//...
                if (TranslationOptions.SimTarget == ENiagaraSimTarget::GPUComputeSim)
                {
                        FString DataInterfaceHLSL;
                        DefineDataInterfaceHLSL(DataInterfaceHLSL); //---------------------↓
                        HlslOutput += DataInterfaceHLSL;

                        DefineExternalFunctionsHLSL(HlslOutput);

                        HlslOutput += StageSetupAndTeardownHLSL;
                }
//...
        }
//...
}

// NiagaraHlslTranslator.cpp
void FHlslNiagaraTranslator::DefineDataInterfaceHLSL(FString& InHlslOutput)
{
        FString InterfaceCommonHLSL;
        FString InterfaceUniformHLSL;
        FString InterfaceFunctionHLSL;
        TSet<FName> InterfaceClasses;

        for (int32 i = 0; i < CompilationOutput.ScriptData.DataInterfaceInfo.Num(); i++)
        {
                FNiagaraScriptDataInterfaceCompileInfo& Info = CompilationOutput.ScriptData.DataInterfaceInfo;

                UNiagaraDataInterface* CDO = CompileDuplicateData->GetDuplicatedDataInterfaceCDOForClass(Info.Type.GetClass());
                check(CDO != nullptr);
                if (CDO && CDO->CanExecuteOnTarget(ENiagaraSimTarget::GPUComputeSim))
                {
                        if ( !InterfaceClasses.Contains(Info.Type.GetFName()) )
                        {
第一类代码                        CDO->GetCommonHLSL(InterfaceCommonHLSL);
                                InterfaceClasses.Add(Info.Type.GetFName());
                        }

                        FNiagaraDataInterfaceGPUParamInfo& DIInstanceInfo = DIParamInfo.AddDefaulted_GetRef();
                        ConvertCompileInfoToParamInfo(Info, DIInstanceInfo);

第二类代码                CDO->GetParameterDefinitionHLSL(DIInstanceInfo, InterfaceUniformHLSL);

                        // Ask the DI to generate HLSL.
                        TArray<FNiagaraDataInterfaceGeneratedFunction> PreviousHits;
                        for (int FunctionInstanceIndex = 0; FunctionInstanceIndex < DIInstanceInfo.GeneratedFunctions.Num(); ++FunctionInstanceIndex)
                        {
                                const FNiagaraDataInterfaceGeneratedFunction& DIFunc = DIInstanceInfo.GeneratedFunctions[FunctionInstanceIndex];
                                ensure(!PreviousHits.Contains(DIFunc));
第三类代码                        const bool HlslOK = CDO->GetFunctionHLSL(DIInstanceInfo, DIFunc, FunctionInstanceIndex, InterfaceFunctionHLSL);
                                if (!HlslOK)
                                {
                                        Error(FText::Format(LOCTEXT("GPUDataInterfaceFunctionNotImplemented", "DataInterface {0} function {1} is not implemented for GPU."), FText::FromName(Info.Type.GetFName()), FText::FromName(DIFunc.DefinitionName)), nullptr, nullptr);
                                }
                                else
                                {
                                        PreviousHits.Add(DIFunc);
                                }
                        }
                }
                else
                {
                        Error(FText::Format(LOCTEXT("NonGPUDataInterfaceError", "DataInterface {0} ({1}) cannot run on the GPU."), FText::FromName(Info.Name), FText::FromString(CDO ? CDO->GetClass()->GetName() : TEXT(""))), nullptr, nullptr);
                }
        }
        //三个部分按照顺序合成
        InHlslOutput += InterfaceCommonHLSL + InterfaceUniformHLSL + InterfaceFunctionHLSL;
}

// NiagaraDataInterfaceGrid3DCollection.cpp
// 第一类代码生成的是Common部分的include(就等于说很多地方都能用的量总在了一起)
const FName UNiagaraDataInterfaceGrid3DCollection::GetValueFunctionName("GetGridValue");
//...
// UNiagaraDataInterfaceGrid3DCollection数据结构的第二类代码生成
void UNiagaraDataInterfaceGrid3DCollection::GetParameterDefinitionHLSL(const FNiagaraDataInterfaceGPUParamInfo& ParamInfo, FString& OutHLSL)
{
        Super::GetParameterDefinitionHLSL(ParamInfo, OutHLSL);

        static const TCHAR *FormatDeclarations = TEXT(R"(
                Texture3D<float> {GridName};
                RWTexture3D<float> RW{OutputGridName};
                int3 {NumTiles};
                float3 {OneOverNumTiles};
                float3 {UnitClampMin};
                float3 {UnitClampMax};
                SamplerState {SamplerName};
                int4 {AttributeIndicesName}[{AttributeInt4Count}];
                Buffer<float4> {PerAttributeDataName};
                int {NumAttributesName};
                int {NumNamedAttributesName};
        )");

        // If we use an int array for the attribute indices, the shader compiler will actually use int4 due to the packing rules,
        // and leave 3 elements unused. Besides being wasteful, this means that the array we send to the CS would need to be padded,
        // which is a hassle. Instead, use int4 explicitly, and access individual components in the generated code.
        // Note that we have to have at least one here because hlsl doesn't support arrays of size 0.
        const int AttributeCount = ParamInfo.GeneratedFunctions.Num();
        const int AttributeInt4Count = FMath::Max(1, FMath::DivideAndRoundUp(AttributeCount, 4));

        TMap<FString, FStringFormatArg> ArgsDeclarations = {                               
                { TEXT("GridName"),    GridName + ParamInfo.DataInterfaceHLSLSymbol },
                { TEXT("SamplerName"),    SamplerName + ParamInfo.DataInterfaceHLSLSymbol },
                { TEXT("OutputGridName"),    OutputGridName + ParamInfo.DataInterfaceHLSLSymbol },
                { TEXT("NumTiles"),    NumTilesName + ParamInfo.DataInterfaceHLSLSymbol },
                { TEXT("OneOverNumTiles"), OneOverNumTilesName + ParamInfo.DataInterfaceHLSLSymbol },
                { TEXT("UnitClampMin"), UnitClampMinName + ParamInfo.DataInterfaceHLSLSymbol },
                { TEXT("UnitClampMax"), UnitClampMaxName + ParamInfo.DataInterfaceHLSLSymbol },

                { TEXT("UnitToUVName"), UNiagaraDataInterfaceRWBase::UnitToUVName + ParamInfo.DataInterfaceHLSLSymbol},
                { TEXT("AttributeIndicesName"), AttributeIndicesBaseName + ParamInfo.DataInterfaceHLSLSymbol},
                { TEXT("PerAttributeDataName"), PerAttributeDataName + ParamInfo.DataInterfaceHLSLSymbol},
                { TEXT("AttributeInt4Count"), AttributeInt4Count},
                { TEXT("NumAttributesName"), UNiagaraDataInterfaceRWBase::NumAttributesName + ParamInfo.DataInterfaceHLSLSymbol},
                { TEXT("NumNamedAttributesName"), UNiagaraDataInterfaceRWBase::NumNamedAttributesName + ParamInfo.DataInterfaceHLSLSymbol},
        };
        OutHLSL += FString::Format(FormatDeclarations, ArgsDeclarations);
}
//...
// UNiagaraDataInterfaceGrid3DCollection数据结构的第三类代码生成
bool UNiagaraDataInterfaceGrid3DCollection::GetFunctionHLSL(const FNiagaraDataInterfaceGPUParamInfo& ParamInfo, const FNiagaraDataInterfaceGeneratedFunction& FunctionInfo, int FunctionInstanceIndex, FString& OutHLSL)
{
        bool ParentRet = Super::GetFunctionHLSL(ParamInfo, FunctionInfo, FunctionInstanceIndex, OutHLSL);
        if (ParentRet)
        {
                return true;
        }

        // 这里将所有一般量与其HLSL里面的Symbol联系起来
        TMap<FString, FStringFormatArg> ArgsBounds =
        {
                {TEXT("FunctionName"), FunctionInfo.InstanceName},
                {TEXT("Grid"), GridName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("OutputGrid"), OutputGridName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("NumAttributes"), UNiagaraDataInterfaceRWBase::NumAttributesName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("NumNamedAttributes"), UNiagaraDataInterfaceRWBase::NumNamedAttributesName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("NumCells"), UNiagaraDataInterfaceRWBase::NumCellsName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("UnitToUVName"), UNiagaraDataInterfaceRWBase::UnitToUVName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("SamplerName"), SamplerName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("NumTiles"), NumTilesName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("OneOverNumTiles"), OneOverNumTilesName + ParamInfo.DataInterfaceHLSLSymbol },
                {TEXT("UnitClampMin"), UnitClampMinName + ParamInfo.DataInterfaceHLSLSymbol },
                {TEXT("UnitClampMax"), UnitClampMaxName + ParamInfo.DataInterfaceHLSLSymbol },
                {TEXT("NumCellsName"), UNiagaraDataInterfaceRWBase::NumCellsName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("PerAttributeDataName"), PerAttributeDataName + ParamInfo.DataInterfaceHLSLSymbol},
                {TEXT("NumAttributesName"), UNiagaraDataInterfaceRWBase::NumAttributesName + ParamInfo.DataInterfaceHLSLSymbol},
        };
        // 这里用很多if判断函数,然后用打上标记的HLSL代码,在过一趟Format将上面设置的值转换到HLSL代码中
        if (FunctionInfo.DefinitionName == GetValueFunctionName || FunctionInfo.DefinitionName == GetPreviousValueAtIndexFunctionName)
        {
                static const TCHAR *FormatBounds = TEXT(R"(
                        void {FunctionName}(int In_IndexX, int In_IndexY, int In_IndexZ, int In_AttributeIndex, out float Out_Val)
                        {
                                Out_Val = 0;
                                if ( In_AttributeIndex < {NumAttributesName} )
                                {
                                        int3 TileOffset = {PerAttributeDataName}[In_AttributeIndex].xyz;
                                        Out_Val = {Grid}.Load(int4(In_IndexX + TileOffset.x, In_IndexY + TileOffset.y, In_IndexZ + TileOffset.z, 0));
                                }
                        }
                )");
                OutHLSL += FString::Format(FormatBounds, ArgsBounds);
                return true;
        }
        else if
//...
        else if (FunctionInfo.DefinitionName == GetFloatAttributeIndexFunctionName)
        {
                WriteAttributeGetIndexHLSL(ParamInfo, FunctionInfo, FunctionInstanceIndex, 1, OutHLSL);
                return true;
        }
//...
}
非常震撼啊对于我来说,读者可以自行尝试一下Debug开一下,然后断点打在这里,如果错过了断点,要在蓝图中Compile那里点Full Rebuild,可以用查看看得到完整的输出代码(虽然里面部分“\t”没调好没对齐,不过不影响观看),有当年用代码生成代码那味了,这种工作工程量真的都是巨型的。梳理一下查找逻辑,一般将蓝色函数的空格去掉就可以在VS里面搜索,然后会搜索到一个FunctionName,利用这个FName变量再搜索,就可以找到对应的位置了。
在int32 FNiagaraEditorModule::CompileScript打上断点可以得到一个比较完整的HLSL,可惜VS自带的没法直接在里面查看搜索,得复制出来到一个文本浏览器好一些。


对于这里的例子来说,HLSL代码就翻译成为了,可以看出来以下事实
一个Data Interface Grid里面可能装载了不止一个Grid,就等于说SimGrid是Data Interface,而Velocity Grid只是我们虚构出来的一个Grid,他只是作为我们提取出来的一个SimGrid中开始的Index逻辑而存在着,使用的时候是通过这个Index的Offset查询到的数据!这个就有点像OpenGL的EBO那味道。只不过这些步骤不看源码是无法得知的。
当然Module Usage上面有写,不过我觉得这并不是用文字能够解释清楚的一件事情,还是代码来的正常。
This module caches grid attribute indices to emitter parameters. These attributes can then be used to efficiently reference grid data while maintaining a readable format.
Check the module writes to see the attributes.
// AttributeIndices_Emitter_SimGrid是预处理过的数值,由CPU传递到GPU

// 值得注意的是,这里的所有函数都需要用CUDA或者Compute Shader的思维来看待

// Link To 1.2.1 ⑥
// 这里就是获取Index,可以看上面的事实有一个大概的印象
void GetVectorAttributeIndex_Emitter_SimGrid_AttributeVelocity(out int Out_Val)
{
        int In_AttributeIndex = AttributeIndices_Emitter_SimGrid[0].z;
        Out_Val = In_AttributeIndex;
}
// ExecutionIndexToGridIndex,这个就是类似于CUDA的Index索引提取
// 如果是提供的ThreadIndex非线性模式(通常我比较喜欢这样),就各自赋值,不然就需要反解XYZ
void ExecutionIndexToGridIndex_Emitter_SimGrid(out int Out_IndexX, out int Out_IndexY, out int Out_IndexZ)
{
        #if NIAGARA_DISPATCH_TYPE == NIAGARA_DISPATCH_TYPE_THREE_D || NIAGARA_DISPATCH_TYPE == NIAGARA_DISPATCH_TYPE_CUSTOM
                Out_IndexX = GDispatchThreadId.x;
                Out_IndexY = GDispatchThreadId.y;
                Out_IndexZ = GDispatchThreadId.z;
        #else
                const uint Linear = GLinearThreadId;
                Out_IndexX = Linear % NumCells_Emitter_SimGrid.x;
                Out_IndexY = (Linear / NumCells_Emitter_SimGrid.x) % NumCells_Emitter_SimGrid.y;
                Out_IndexZ = Linear / (NumCells_Emitter_SimGrid.x * NumCells_Emitter_SimGrid.y);
        #endif
}
// In_AttributeIndex在使用的时候x轴为VectorIndex+0,y轴为VectorIndex+1,z轴为VectorIndex+2
// 上面赋值完的IndexXYZ、以及GetFloatAttributeIndex的输出就作为这里的输入,在蓝图中也可以看得很清楚这部分逻辑
void GetGridValue_Emitter_SimGrid(int In_IndexX, int In_IndexY, int In_IndexZ, int In_AttributeIndex, out float Out_Val)
{
        Out_Val = 0;
        if ( In_AttributeIndex < NumAttributes_Emitter_SimGrid )
        {
                int3 TileOffset = PerAttributeDataName_Emitter_SimGrid[In_AttributeIndex].xyz;
                Out_Val = Grid_Emitter_SimGrid.Load(int4(
                        In_IndexX + TileOffset.x, In_IndexY + TileOffset.y,
                        In_IndexZ + TileOffset.z, 0));
        }
}
// 下面两个函数不是这个Module里面使用的,但是可以体现出一些上面说的部分事实,从for可以看出来
void GetPreviousFloatValue_Emitter_TemporaryGrid_AttributeSimFloat(int In_IndexX, int In_IndexY, int In_IndexZ, out float Out_Val)
{
        int In_AttributeIndex = AttributeIndices_Emitter_TemporaryGrid[1].y;
        for (int i = 0; i < 1; i++)
        {
                int CurAttributeIndex = In_AttributeIndex + i;
                int3 TileOffset = PerAttributeDataName_Emitter_TemporaryGrid[CurAttributeIndex].xyz;
                float Val = Grid_Emitter_TemporaryGrid.Load(int4(
                        In_IndexX + TileOffset.x, In_IndexY + TileOffset.y,
                        In_IndexZ + TileOffset.z, 0));
                        Out_Val = Val;
        }
}
void GetPreviousVectorValue_Emitter_SimGrid_AttributeVelocity(int In_IndexX, int In_IndexY, int In_IndexZ, out float3 Out_Val)
{
        int In_AttributeIndex = AttributeIndices_Emitter_SimGrid[1].z;
        for (int i = 0; i < 3; i++)
        {
                int CurAttributeIndex = In_AttributeIndex + i;
                int3 TileOffset = PerAttributeDataName_Emitter_SimGrid[CurAttributeIndex].xyz;
                float Val = Grid_Emitter_SimGrid.Load(int4(
                        In_IndexX + TileOffset.x, In_IndexY + TileOffset.y,
                        In_IndexZ + TileOffset.z, 0));
                Out_Val = Val;
        }
}

// 完整的HLSL代码其中的一段,也就是这个Module转化为HLSL的流程代码
// 可以清晰的看出来流程,DI的信息应该就是在Context里面了
// 状态设置->执行ComputeCurl->配置VectorLength->执行VectorLength->配置SetVariables->执行SetVariables
#if ((SimulationStageIndex == 4)) // MapSimStage4_ComputeCurl
void SimulateMapSimStage4_ComputeCurl(inout FSimulationContext Context)
{
        //Begin Stage Script: MapSimStage4_ComputeCurl!
        Context.MapSimStage4_ComputeCurl.Grid3D_ComputeCurl.dx = Context.MapSimStage4_ComputeCurl.Emitter.dx;
        Context.MapSimStage4_ComputeCurl.Grid3D_ComputeCurl.VectorIndex = Context.MapSimStage4_ComputeCurl.Emitter.SimGrid_VelocityIndex;
        EnterStatScope(6 /**Grid3D_ComputeCurl_Emitter_Func_*/);
        Grid3D_ComputeCurl_Emitter_Func_(Context);
        ExitStatScope(/**Grid3D_ComputeCurl_Emitter_Func_*/);
        Context.MapSimStage4_ComputeCurl.VectorLength.VECTOR_VAR = Context.MapSimStage4_ComputeCurl.OUTPUT_VAR.Grid3D_ComputeCurl.Curl;
        float VectorLength_Emitter_Func_Output_NewOutput;
        VectorLength_Emitter_Func_(VectorLength_Emitter_Func_Output_NewOutput, Context);
        Context.MapSimStage4_ComputeCurl.SetVariables_6C0313394B4550FBC5D741A95A8D1161.Emitter.TemporaryGrid.SimFloat = VectorLength_Emitter_Func_Output_NewOutput;
        EnterStatScope(7 /**SetVariables_6C0313394B4550FBC5D741A95A8D1161_Emitter_Func_*/);
        SetVariables_6C0313394B4550FBC5D741A95A8D1161_Emitter_Func_(Context);
        ExitStatScope(/**SetVariables_6C0313394B4550FBC5D741A95A8D1161_Emitter_Func_*/);
        //End Simulation Stage Script: MapSimStage4_ComputeCurl
}
#endif // MapSimStage4_ComputeCurl这下会不会有豁然开朗的感觉。
②Grid 3D Compute Curl根据计算出来的Curl设置了Velocity的初值,其实这个步骤在上面的代码里面都有了,按照上面的思路,可以调整我们对蓝图节点的思维模式,其实他就是描述了一个除了IndexFetch的Kernel部分。
1.2.5 Pre Sim Velocity

根据上面的分析,下面的过程我们对于其底层运行是有一定认识的,确定性的运行逻辑总是好的!这样我们就可以专注于上层的实现了。这一个部分主要的功能是计算外力。
①Grid 3D Get Transients from Grid Values 002是提取场量到对应的TransientGrid中,ScalarGrid量都是直接用Get Previous <Type> Value实现的,而SimGrid对其封装了一层,是因为SimGrid和ScalarGrid的大小并不是对应的,SimGrid有可能比ScalarGrid小(Default的话是1:1,但是这个可以由一个参数控制,而TransientGrid就是纯纯的1:1了),所以就需要对Get Previous <Type> Value套一层封装,对于ScalarGrid比SimGrid大的地方,就需要补0;
虽然例子中只勾选了SimGrid,但是其中有个Transient Values比较吸引我的注意,Boundary在示例中是Transient的Tag,所以应该是不能获取上一帧的值的,而这里又有一个GetPerviousFloatValue调用了Boundary,所以这里还需要修正一个观念,也就是,Niagara允许在某些情况下变量重名,这样子其实还是蛮容易混淆的,一定要注意TransientGrid与Transient不是同一个概念,Transient量就是一个临时输出,而TransientGrid是我们存放可持久数据的地方。在③中您可能更好理解我所说的含义。


②Grid 3D Compute Boundary这是一个很重要的大家伙,这里输出的Boundary就是Transient,先看一下需要检测的碰撞类型



Grid 3D GAS CONTROLS SPAWN



Grid 3D Compute Boundary Input,从上到下依次为FTFFFT

分析源码可以得到碰撞查询函数为DIRigidMeshCollision_GetClosestPointMeshDistanceFieldNoNormal,也就是下图标注的节点,返回空间SDF值物体速度,如果空间SDF值小于等于0那么该空间位置就是在需要查询可碰撞的物体内部,也就是这个点是属于边界的,也就有了下图后面的Check以及分支行为。


还有模拟边界的设置,部分模拟边界我们可以认为是Boundary,这个计算也在这个模块中,这样在上面计算Check为False的时候,就可以利用这个部分计算出来的值作为代替(也就是Select的逻辑)。下面这个就是边界设置BoundaryWidth就是我们设置空间中有多少个Cell是用来给边界使用的,Boundary的值有如下的表示
0:表示不是边界,是流体的一个部分。
0-1:表示是固体表面,这个空间不允许流体通过,需要执行第二类边界条件
0-2:表示是自由表面,对于水来说,他的自由表面边界需要特殊处理,因为自由表面边界属于流体,这就有一个特性,那就是他旁边一半的空间是没有流体的,一半的空间是有流体的,如果按照平时有限差分的方法来计算肯定有一部分就拿不到数据,如果计算类似密度,就会产生错误。这部分边界执行第一类边界条件


可以看到示例中提供了对模拟环境边界的设置,如果我们认为模拟边界是Boundary,那么就会计算目前处理的Cell在不在BoundaryWidth里面,如果在里面,还需要Check此时要通过的边界是不是Open的,如果是Open的那么场量可以从这个边界流失,需要以自由表面的方式处理(就是上面说明的自由表面的特殊性),否则就直接标记为不可通过。




// 部分源码,因为涉及的东西太多,看一下就好,大概清楚里面的逻辑
// NiagaraDataInterfaceRigidMeshCollisionQuery.ush

// Given a world space position (WorldPosition) compute the sphere closest point (position,normal,velocity)
int DIRigidMeshCollision_GetClosestElement(in FDIRigidMeshCollisionContext DIContext, in float3 WorldPosition, out float3 OutClosestPosition,
                                                                                  out float3 OutClosestNormal, out float OutMinDistance, in float TimeFraction)
{       
        float MinDistance = MAX_DISTANCE;
        int ElementIndex = -1;
        float3 CollisionPosition = float3(0,0,0);
        float3 CollisionNormal = float3(0,0,0);

        const int SpheresBegin = DIContext.ElementOffsets[SPHERE_INDEX];
        const int SpheresEnd = DIContext.ElementOffsets[SPHERE_INDEX+1];
        for (int SphereIndex = SpheresBegin; SphereIndex < SpheresEnd; ++SphereIndex)
        {
                const float3 LocalPosition = DIRigidMeshCollision_GetLocalPosition(DIContext,WorldPosition,SphereIndex,TimeFraction);

                DIRigidMeshCollision_GetSphereProjection(LocalPosition, float3(0,0,0), DIContext.ElementExtentBuffer[SphereIndex].x, SphereIndex,
                                CollisionPosition, CollisionNormal, ElementIndex, MinDistance);
        }

        const int CapsulesBegin = DIContext.ElementOffsets[CAPSULE_INDEX];
        const int CapsulesEnd = DIContext.ElementOffsets[CAPSULE_INDEX+1];
        for (int CapsuleIndex = CapsulesBegin; CapsuleIndex < CapsulesEnd; ++CapsuleIndex)
        {
                const float3 LocalPosition = DIRigidMeshCollision_GetLocalPosition(DIContext,WorldPosition,CapsuleIndex,TimeFraction);

                DIRigidMeshCollision_GetCapsuleProjection(LocalPosition, DIContext.ElementExtentBuffer[CapsuleIndex].xy, CapsuleIndex,
                                CollisionPosition, CollisionNormal, ElementIndex, MinDistance);
        }

        const int BoxesBegin = DIContext.ElementOffsets[BOX_INDEX];
        const int BoxesEnd = DIContext.ElementOffsets[BOX_INDEX+1];
        for (int BoxIndex = BoxesBegin; BoxIndex < BoxesEnd; ++BoxIndex)
        {
                const float3 LocalPosition = DIRigidMeshCollision_GetLocalPosition(DIContext,WorldPosition,BoxIndex,TimeFraction);

                DIRigidMeshCollision_GetBoxProjection(LocalPosition, DIContext.ElementExtentBuffer[BoxIndex].xyz, BoxIndex,
                                CollisionPosition, CollisionNormal, ElementIndex, MinDistance);
        }
        OutClosestPosition = CollisionPosition;
        OutClosestNormal = CollisionNormal;
        OutMinDistance = MinDistance;
        return ElementIndex;
}

// Given a world space position (WorldPosition) compute the static mesh closest point (position,normal,velocity)
void DIRigidMeshCollision_GetClosestPointMeshDistanceFieldNoNormal(in FDIRigidMeshCollisionContext DIContext, in float3 WorldPosition, in float DeltaTime, in float TimeFraction, out float OutClosestDistance, out float3 OutClosestPosition, out float3 OutClosestVelocity)
{       
        float3 CollisionPosition = float3(0,0,0);
        float3 CollisionNormal = float3(0,0,0);
        OutClosestDistance = MAX_DISTANCE;       
       
        OutClosestPosition = float3(0,0,0);
        OutClosestVelocity = float3(0,0,0);

        float OutClosestDistanceTmp;
        const int ElementIndex = DIRigidMeshCollision_GetClosestElement(DIContext,WorldPosition,CollisionPosition,CollisionNormal,OutClosestDistanceTmp,TimeFraction);

        if (ElementIndex != -1)
        {
                int DFIndex = DIContext.DFIndexBuffer[ElementIndex];

                if (DFIndex >= 0 && DFIndex < NumSceneObjects)
                {
                        // #todo(dmp): this does a dynamic branch based on intersecting the bbox.  Maybe we can factor that out due to the broadphase here?
                        OutClosestDistance = DistanceToNearestSurfaceForObject(DFIndex, WorldPosition, 1e-3);               

                        const float3 PreviousPosition = mul(DIRigidMeshCollision_GetPreviousTransform(DIContext,ElementIndex), float4(CollisionPosition,1.0)).xyz;
                        const float3 CurrentPosition = mul(DIRigidMeshCollision_GetCurrentTransform(DIContext,ElementIndex), float4(CollisionPosition,1.0)).xyz;
                        OutClosestVelocity = ( CurrentPosition - PreviousPosition ) / DeltaTime;
                        OutClosestPosition = PreviousPosition + TimeFraction * (CurrentPosition-PreviousPosition);
                }
        }
}
③Grid 3D Set Boundary Grid Values将上面计算出来的Transient Boundary临时输出赋值到TransientGrid的管理Boundary的那部分内存中(希望你们能看懂这句话,因为在前面也提醒过很多次Transient与TransientGrid的区别)。并且将求出来的固体速度也放到TransientGrid的SolidVelocity中。给后面计算流体提供了充足的边界数据。
④Grid 3D Gas Buoyancy提供了计算浮力的方法,浮力并不难,初中都学过,主要是其中参数的配置,因为烟雾上飘的主要原因是,烟其实是固体,是在下降的,而其热扩散加热了周围的空气,这个效应形成了低压区,固体颗粒的收尾速度小于上升气流速度,整体就呈现向上飘的形态了。但是基本上,模拟的时候不会这样考虑,而是将温度、密度乘以他们对浮力影响的系数。整体就是一个简化模型,最后将Force加到Physics Force上面。


当然,这个模型应该还可以更加高级才对,这个系数不应该是HardCode的,应该与烟属性有关。
⑤Grid 3D Compute Gradient 001&Grid 3D Compute Curl 001&Grid 3D Vorticity Confinement Force
这个三个过程结合起来就是现在标准的Artificial Force方法[1],在1.2.4里面计算出来的速度旋度就用在了这里,就是一个计算公式,其中 |\mathbf{u}|_{i,j} = length(\mathbf{u}_{i,j}) ,其他的数学表示可以参考之前写的基础知识与基础方法实现
\begin{align} \omega &= \nabla \times \mathbf{u} \tag{Grid 3D Compute Curl}\\ \eta &= \nabla\left|\omega\right|  \tag{Grid 3D Compute Gradient 001} \\  \omega &= \nabla \times \mathbf{u} \tag{Grid 3D Compute Curl 001}\\ \mathbf{f}_{conf} &= \epsilon \Delta x\left(\frac{\eta}{\left|\eta\right|}\times\omega\right) \tag{Grid 3D Vorticity Confinement Force} \end{align}\\是的,你没看错,旋度就是求了两次,可能后面有需要用到的地方吧,如果没有,那么这显然是一个错误的设计,但这里也看出来了,Grid保存的数据必须要Fetch出来才能用,如果没Fetch成为Transient(Output),那么函数就至少需要传递Index用来Fetch,部分函数的输入是Transient(Output),部分函数的输入是Grid,所以特别要注意。
Grid 3D Resample Float 003/004&Grid 3D Turbulence 001/002
这先是一个Resample,不知道是不是这个系统具有的一个特性,后面Grid 3D Turbulence 001/002我们的一些计算需要用SimGrid,而亟待使用的TemperatureDensity现在都是在ScalarGrid中而且他们的输入需要是Transient(Output),并且没有Index接口,所以在使用Grid 3D Turbulence之前就需要进行一次Resample,把需要使用的数据Fetch出来。
Grid 3D Turbulence 001/002和之前的计算Turbulence一样,不过这次多了Density与Temperature的影响,具体可以看下蓝图节点的参数设置。
⑦Grid 3D Wind计算风力,其中有一个Local开关,就是说目前输入的风力信息是局部坐标系还是世界坐标系,注意一下这个就行,如果打开了Local,这个力的累积就不会到PhysicsForce,而是到PhysicsForceLocal,这个在后面的积分可以看见。
⑧Grid 3D Integrate Forces将上面的力进行时间积分,其实就是将PhysicsForce转换到局部坐标与PhysicsForceLocal相加,再乘以 \Delta t 就变成了速度变化量。
⑨Set Velocity把上面计算出来的新速度放回到Grid中。
注:这里需要放回去,就是因为Transient(Output)类型不能跨Stage,那么如果要下面的Stage能够使用新的Velocity,就需要这里赋值到Grid中,然后在后面使用的时候拿Grid和Index再拿出来。
到这里我们其实已经窥探到很多Niagara的设计思想了,但是个人觉得,这个玩意怎么说呢?论简易性,我觉得物理模拟做的很简易是不太可行的,至少任何的扩展都不会变得非常简易;论效率,这样的设计效率肯定没有精心设计的Pipeline效率高,重复的Load与Store肯定会存在一定的消耗。


1.2.6 Sourcing

①Grid 3D Get Vector就是将前面的Velocity取出来,里面是用的Index,所以就没法用Get Previous Vector Value了,按理来说即使是General Function应该做个用Index索引的Get Vector也不是什么难事吧,怪了。现在Transient就有了一个Velocity的副本VectorValue
②Set三个Transient变量,这里Velocity的设置就用了刚刚获取VectorValue,但是上面两个ScalarGrid由不需要先提取一次,好奇心让我直接把Velocity的赋值改成了SimGrid Tag的Velocity,果然报错了
Variable Emitter.SimGrid.Velocity was read before being set. It's default mode is "Fail If Previously Not Set", so this isn't allowed. - Node: Map Get -  Grid3D_Gas_Controls_Base_Emitter, Particle Simulation Stage Script,
盲猜一波Stage Settings的锅,果然将这个Stage改成了SimGrid立马另外两个就报错了,唉,好奇怪的设计(过段时间看一下,这个部分应该与其Compile与HLSL Gen有关),所以这里为了让Scalar Grid用起来方便,就行库了基于SimGrid的Velocity,③中给出了更加详细的分析。
③Grid 3D Gas Init是用来给源头提供“虚空物品”的东西,其主体控制逻辑在后面(有几个与效果有关的控制参数,到这个部分读者应该可以自行看懂),前面先采样了一下PerlinNoise,利用采样到的Noise在Source部分制造噪声,让Source不均匀得奇怪。中间有一步很迷,他把VelocityTemperatureDensity的赋值分开了,但是我自己觉得应该能合并,实际上我把两个合并之后,也是正确运行的。由此,我大概能理解,他可能是觉得Velocity和前面不一样,但是在一个Stage中,给的Index都是一样的,所以,如果真的要按Grid分开设计,那么的确每个Stage只能处理一种Grid,显然他们自己也没有贯彻自己的设计哲学,我都想帮帮他,可惜我不太认可这种设计哲学,哈哈。下面代码不复杂,理解一下逻辑很容易懂。
这个部分阐述了这样一个事实,与Data Interface匹配的Grid,HLSL生成器会自动Load与Store,这个操作是访问GPU GlobalMemory很慢,所以中间直接使用的量是Cache在Context中的数据,更快,而对于Velocity和他们不一样,Velocity在Stage中间做Get、Set就需要调用Load与Store,但又由于Transient Tag量的客观存在,所以其实。。。我沉默了。
// HLSL中部分源码,可以看出来这里ScalarGrid和SimGrid的处理完全不一样,这里展示一部分与初始化有关的
// Load与Store中,自动执行Grid的部分之后与DataInterface相同的部分。
[numthreads(THREADGROUP_SIZE_X, THREADGROUP_SIZE_Y, THREADGROUP_SIZE_Z)]
void SimulateMainComputeCS(
        uint3 DispatchThreadId : SV_DispatchThreadID,
        uint3 GroupThreadId : SV_GroupThreadID) {
//...
        InitConstants(Context);
        // 内部执行SetupFromIterationSource_MapSimStage6_Sourcing(Context);
        LoadUpdateVariables(Context, GLinearThreadId);
//...
        // 执行Sourcing
        SimulateMapSimStage6_Sourcing(Context);
//...
        // 更新数据
        // 内部执行TeardownFromIterationSource_MapSimStage6_Sourcing(Context);
        StoreUpdateVariables(Context, bRunUpdateLogic || bRunSpawnLogic);
}

// 初始化
// 可以看见这里对SimGrid和ScalarGrid的态度完全不一样,Context现在有ScalarGrid字段
// Niagara自动将Tag为ScalarGrid并且需要使用的Index放在Context中,就有了②中差异化的原因。
void InitConstants(inout FSimulationContext Context) {
//...
#if ((SimulationStageIndex == 6)) // MapSimStage6_Sourcing
        Context.MapSimStage6_Sourcing.Emitter.SimGrid_VelocityIndex = Emitter_SimGrid_VelocityIndex;
        Context.MapSimStage6_Sourcing.Emitter.ScalarGrid.Temperature = Emitter_ScalarGrid_Temperature;
        Context.MapSimStage6_Sourcing.Emitter.Grid3D_GAS_CONTROLS_SPAWN.DebugSources = Emitter_Grid3D_GAS_CONTROLS_SPAWN_DebugSources;
        Context.MapSimStage6_Sourcing.Emitter.ScalarGrid.Density = Emitter_ScalarGrid_Density;
        Context.MapSimStage6_Sourcing.Emitter.Grid3D_CreateUnitToWorldTransform.UnitToWorld = Emitter_Grid3D_CreateUnitToWorldTransform_UnitToWorld;
        Context.MapSimStage6_Sourcing.Emitter.Grid3D_CreateUnitToWorldTransform.WorldToLocal = Emitter_Grid3D_CreateUnitToWorldTransform_WorldToLocal;
        Context.MapSimStage6_Sourcing.Emitter.dt = Emitter_dt;
        Context.MapSimStage6_Sourcing.Emitter.Age = Emitter_Age;
#endif // MapSimStage6_Sourcing
//...
}
// 在全局Load,PerAttributeDataName_Emitter_ScalarGrid -> Grid_Emitter_ScalarGrid
// 在全局Store,Grid_Emitter_ScalarGrid -> RWOutputGrid_Emitter_ScalarGrid
// 中间参与计算都是 Grid_Emitter_ScalarGrid。
Texture3D<float> Grid_Emitter_ScalarGrid;
RWTexture3D<float> RWOutputGrid_Emitter_ScalarGrid;
Buffer<float4> PerAttributeDataName_Emitter_ScalarGrid;

// Velocity Set的计算
void SetGridValue_Emitter_SimGrid(int In_IndexX, int In_IndexY, int In_IndexZ, int In_AttributeIndex, float In_Value, out int val){                       
        val = 0;
        if ( In_AttributeIndex < NumAttributes_Emitter_SimGrid ){
                int3 TileOffset = PerAttributeDataName_Emitter_SimGrid[In_AttributeIndex].xyz;
                RWOutputGrid_Emitter_SimGrid[int3(In_IndexX + TileOffset.x, In_IndexY + TileOffset.y, In_IndexZ + TileOffset.z)] = In_Value;
        }
}
// -------------------------------LoadUpdateVariables(Context, GLinearThreadId)---Begin--------------------
void GetPreviousFloatValue_Emitter_ScalarGrid_AttributeDensity(int In_IndexX, int In_IndexY, int In_IndexZ, out float Out_Val){
        int In_AttributeIndex = AttributeIndices_Emitter_ScalarGrid[0].z;
        for (int i = 0; i < 1; i++){
                int CurAttributeIndex = In_AttributeIndex + i;
                nt3 TileOffset = PerAttributeDataName_Emitter_ScalarGrid[CurAttributeIndex].xyz;
                float Val = Grid_Emitter_ScalarGrid.Load(int4(In_IndexX + TileOffset.x, In_IndexY + TileOffset.y, In_IndexZ + TileOffset.z, 0));
                Out_Val = Val;
        }
}
#if ((SimulationStageIndex == 6)) // MapSimStage6_Sourcing
void SetupFromIterationSource_MapSimStage6_Sourcing_GeneratedSetup_Func_(inout FSimulationContext Context){
}
void SetupFromIterationSource_MapSimStage6_Sourcing_GeneratedReadAttributesEmitter_ScalarGrid_Func_(inout FSimulationContext Context){
        //Generated by UNiagaraDataInterfaceGrid3DCollection::GenerateIterationSourceNamespaceReadAttributesHLSL
        // Argument Name "Map" Type "NiagaraParameterMap"
        // Argument Name "TargetDataInterface" Type "NiagaraDataInterfaceGrid3DCollection"
        int X, Y, Z;
        ExecutionIndexToGridIndex_Emitter_ScalarGrid(X, Y, Z);
        // Variable Name "Emitter.ScalarGrid.Temperature" Type "NiagaraFloat" Var "Map.Emitter.ScalarGrid.Temperature"
        GetPreviousFloatValue_Emitter_ScalarGrid_AttributeTemperature(X, Y, Z, Context.MapSimStage6_Sourcing.Emitter.ScalarGrid.Temperature);
        // Variable Name "Emitter.ScalarGrid.Density" Type "NiagaraFloat" Var "Map.Emitter.ScalarGrid.Density"
        GetPreviousFloatValue_Emitter_ScalarGrid_AttributeDensity(X, Y, Z, Context.MapSimStage6_Sourcing.Emitter.ScalarGrid.Density);
}
void SetupFromIterationSource_MapSimStage6_Sourcing(inout FSimulationContext Context) {
        SetupFromIterationSource_MapSimStage6_Sourcing_GeneratedSetup_Func_(Context);
        SetupFromIterationSource_MapSimStage6_Sourcing_GeneratedReadAttributesEmitter_ScalarGrid_Func_(Context);
}
// -------------------------------LoadUpdateVariables(Context, GLinearThreadId)---End--------------------

// ------------StoreUpdateVariables(Context, bRunUpdateLogic || bRunSpawnLogic)---Begin--------------------
void CopyMaskedPreviousToCurrentForCell_Emitter_ScalarGrid_UEImpureCall(int In_IndexX, int In_IndexY, int In_IndexZ, int NumAttributesSet, int AttributeMask){       
        // early out if we set all the attributes that exist on the DI
        [branch]
        if (NumAttributesSet == NumAttributes_Emitter_ScalarGrid.x){
                return;                                       
        }
        for (int AttributeIndex = 0; AttributeIndex < NumAttributes_Emitter_ScalarGrid.x; AttributeIndex++){                                       
                // check the attribute index in the attribute mask to see if it has been set
                // we automatically pass through attributes higher than 31 since we can only
                // store 32 attributes in the mask
                [branch]
                if ((AttributeMask & (1 << AttributeIndex)) == 0 || AttributeIndex >= 32){                                               
                        int3 TileOffset = PerAttributeDataName_Emitter_ScalarGrid[AttributeIndex].xyz;
                        float Val = Grid_Emitter_ScalarGrid.Load(int4(In_IndexX + TileOffset.x, In_IndexY + TileOffset.y, In_IndexZ + TileOffset.z, 0));       
                        RWOutputGrid_Emitter_ScalarGrid[int3(In_IndexX + TileOffset.x, In_IndexY + TileOffset.y, In_IndexZ + TileOffset.z)] = Val;
                }
        }
}
void TeardownFromIterationSource_MapSimStage6_Sourcing_GeneratedTeardown_Func_(inout FSimulationContext Context){
        //Generated by UNiagaraDataInterfaceGrid3DCollection::GenerateTeardownHLSL
}
void TeardownFromIterationSource_MapSimStage6_Sourcing_GeneratedWriteAttributesEmitter_ScalarGrid_Func_(inout FSimulationContext Context){
        //Generated by UNiagaraDataInterfaceGrid3DCollection::GenerateIterationSourceNamespaceWriteAttributesHLSL
        // Argument Name "Map" Type "NiagaraParameterMap"
        // Argument Name "TargetDataInterface" Type "NiagaraDataInterfaceGrid3DCollection"
        int AttributeIsSetMask = 0;
        int CurrAttributeIndex;
        int X, Y, Z;
        ExecutionIndexToGridIndex_Emitter_ScalarGrid(X, Y, Z);
        // Name "Emitter.ScalarGrid.Temperature" Type "NiagaraFloat" Var "Map.Emitter.ScalarGrid.Temperature"
        GetFloatAttributeIndex_Emitter_ScalarGrid_AttributeTemperature(CurrAttributeIndex);
        AttributeIsSetMask |= 1 << (CurrAttributeIndex+0);
        SetFloatValue_Emitter_ScalarGrid_UEImpureCall_AttributeTemperature(X, Y, Z,  Context.MapSimStage6_Sourcing.Emitter.ScalarGrid.Temperature);
        // Name "Emitter.ScalarGrid.Density" Type "NiagaraFloat" Var "Map.Emitter.ScalarGrid.Density"
        GetFloatAttributeIndex_Emitter_ScalarGrid_AttributeDensity(CurrAttributeIndex);
        AttributeIsSetMask |= 1 << (CurrAttributeIndex+0);
        SetFloatValue_Emitter_ScalarGrid_UEImpureCall_AttributeDensity(X, Y, Z,  Context.MapSimStage6_Sourcing.Emitter.ScalarGrid.Density);       
        CopyMaskedPreviousToCurrentForCell_Emitter_ScalarGrid_UEImpureCall(X,Y,Z,2,AttributeIsSetMask);
}
void TeardownFromIterationSource_MapSimStage6_Sourcing(inout FSimulationContext Context){
        TeardownFromIterationSource_MapSimStage6_Sourcing_GeneratedTeardown_Func_(Context);
        TeardownFromIterationSource_MapSimStage6_Sourcing_GeneratedWriteAttributesEmitter_ScalarGrid_Func_(Context);
}
// ------------StoreUpdateVariables(Context, bRunUpdateLogic || bRunSpawnLogic)---End--------------------
④Set&Grid 3D Set Vector Value 001将临时输出写回,上面呈现的SetGridValue_Emitter_SimGrid就是最主要的操作。
1.2.7 Advect Velocity

差不多从这里开始,就进入经典的网格流体模拟计算方法了,部分Get、Set以及与之前理论基础有关就不会多说明,基本上可以和我们之前的教程对应上。不过这个模块在我的电脑上部分是没法编译的(这玩意不走那边的话都是可以生成HLSL的好像)
Function call "SampleGrid" is not allowed for stack context ENiagaraScriptUsage::Module. Allowed: ENiagaraScriptUsage::ParticleSpawnScript, ENiagaraScriptUsage::ParticleUpdateScript, ENiagaraScriptUsage::ParticleEventScript, ENiagaraScriptUsage::ParticleSimulationStageScript -
还有RK2、RK3尼玛是一样的代码,我也是佩服,应该是没开发了。
1.2.8 Compute Divergence

很简单的计算散度,装到SimFloat里面,这是一个Temporary Grid。再用一个Set清零Pressure。
1.2.9 Solve Pressure

大的来了,这个部分是利用泊松方程解压强,首先Stage Settings就不一样,这个Stage存在迭代次数设置。
在计算之前,获取了BoundaryPressureVelocityDivergence,Solve的时候Use Solid Velocity是True,但是我楞没看出来Boundary处理的时候是怎么更新这个的,甚至Output连Soild Velocity都不见了。有点小破防,暂时先不管Soild的速度(但是更破防的是,我测试了一下,Soild有可能在通过流体的时候,固体身上某些部分变成Source一样的效果),先看下怎么解的吧
Pressure = 0; // Output

#if GPU_SIMULATION
const  int FLUID_CELL = 0;
const  int SOLID_CELL = 1;
const  int EMPTY_CELL = 2;
int CellType = round(B_center); // Input:B_center 这个点的Boundary

[branch]
if (CellType == FLUID_CELL) // 我们当前处理的Index对应的点,是Fluid
{
float Scale = dx / dt;

int FluidCellCount = 6;
float BoundaryAdd = 0.0;

float Weight;

if (Relaxation < 1e-8) // 如果不选择松弛
{
    // same as regular jacobi
    Weight = 1;
}
else
{   
    int SliceParity = (IndexZ + IterationIndex) % 2;
    int RowParity = (IndexY + SliceParity+1) % 2;
    int CellParity = (IndexX + RowParity ) % 2;

    // will do red-black SOR
    // add 1 since we want to expose a 0-1 parameter
    Weight = CellParity * min(1.93, Relaxation + 1);
}

// 六个方向都计算一次
int CellType_right/up/front = round(B_right/up/front);
if (CellType_right/up/front == SOLID_CELL)
{
    FluidCellCount--;
    BoundaryAdd += Scale * (Velocity.x/y/z - SV_x_right/up/front);
    P_right/up/front = 0;
}
else if (CellType_right/up/front == EMPTY_CELL)
{
   P_right/up/front = 0;
}

int CellType_left/down/back = round(B_left/down/back);
if (CellType_left/down/back == SOLID_CELL)
{
    FluidCellCount--;
    BoundaryAdd -= Scale * (Velocity.x/y/z - SV_x_left/down/back);
    P_left/down/back = 0;
}
else if (CellType_left/down/back == EMPTY_CELL)
{
   P_left/down/back = 0;
}

// 这个例子里面并没有考虑Surface
if (FluidCellCount > 0)
{
    float JacobiPressure = (P_right + P_left + P_up + P_down + P_front + P_back -  dx * dx * Divergence / dt + BoundaryAdd) / FluidCellCount;
    Pressure = (1.f - Weight) * P_center + Weight * JacobiPressure;
}
}
#endif
为什么会有DeltaT呢?我百思不得其解,根据自己浅薄的流体模拟知识,不足以能够让我想明白这个问题。但如果一定要引进DeltaT,按照[2]里面的说明,以及我现有的知识来说,至少在推导的时候用的是Forward的方式,也就是用时间积分的视角去看,那就应该是基于下面的方程,而不是基于投影
\frac{\partial \omega}{\partial t} = -\frac{1}{\rho}\nabla p \\
可是这么计算 \rho 去哪里了呢?我真的百思不得其解,后面才知道为什么,那么先往下看。
1.2.10 Project Pressure

按照之前的理论,在Project之前要将Pressure的梯度计算出来,然后用目前的速度减去Pressure的梯度,剩下的结果就是一个无散度场。
在做Projection的时候只有上面那几个计算,这就是最坑爹的地方,里面有Density,然后我就以为在Projection的时候用了Density但是前面迭代没用Density,以为是什么神奇的东西,弄了好久才发现,里面这个Density打的是Local,而不是Input,所以其实就是1。而和我们之前的区别就是,一个DeltaT的Scale,所以按理来说,我们之前的计算方法应该是更合理的。
通过修改BoundaryAdd的Scale以及JacobiPressure里面的dt,还有Projection里面的计算都去除,发现结果还是正确的。
还有一个我不太了解的点,可能是部分Coupling的通用方法,就是最后Projection的时候需要把固体速度给流体的思路,以及之前BoundaryAdd的正确性。更新:这个地方其实是完全仿照的[3]里面的计算方法,这本教材在上一篇文章中有链接,BoundaryAdd是书中提出的一个Ghost Pressure,思想是可以将Soild通过近似让其属性可以放在流体框架中直接用。
最后有个Dissipate,也就是人工给一个衰减,这个我其实是不太推荐的,但是对应源头不断产生,这个做法是可以接受的。
1.2.11 Advect Scalars

这里面对DensityTemperature做了Advection,这里面的RK2和RK3就是正确的了,为啥上面Advect Vector是错的。同样这里也做了人工Dissipate。
1.2.12 Debug Slice Prep

这个模块Generic Simulation Stage Settings里面有个Enable Binding比较新,这是控制这个模块是否执行的一个地方,除了手动点击开关,还可以用Attribute控制,这个RenderDebugSlice也可以控制下面Render的开关,所以在Grid 3D GAS CONTROLS SPAWN中,开关Render Debug Slice可以控制这里的计算,也可以控制Render的计算,开启置RenderDebugSlice为True,计算Debug Slice Prep,并且执行第二个Mesh Render,不然RenderBeauty为True,计算Post Sim Scalars,并且执行第一个Mesh Render



输入的Bool为RenderDebugSlice

Resample前面出现过,但是这里Index用了Embedded的数字,按理来说应该要写Attribute形式的SimGrid_DensityIndex以及SimGrid_TemperatureIndex
Grid 3D Set RTValues 002有点意思,新的程序设计方式出现了!我第一次看人都有点麻,其实整体是不复杂的,只是表现方式有点问题。这个主要是将现在有的参数放到Render Target(SimRT)中,其中Red、Green、Blue分别存储Density、Temperature、前面两个相加
1.2.13 Computing Lighting

Grid 3D GAS CONTROLS UPDATE设置了Render的参数以及Lighting的参数,下面是为渲染计算透光率的Code,不过不知道这是经验方法还是有物理依据的方法。
// 此时的光照强度作为初始的透光率
Transmittance = LightIntensity;
#if GPU_SIMULATION

// quality of 1 means we have 1 step per cell
// 设置RayMarching的StepSize,这个和dx有关,所以在Scalability修改的时候渲染也会变差
float StepSize = dx / RenderQuality;
float WorldStepSize = StepSize;
float t = 0;

// Make sure we never take more than some max number of steps
// 计算需要的Step,这里tmax是WorldGridExtents计算出来的
int NumStepsForRay = min(MaxNumSteps, tmax / StepSize);
// 计算系数
DensityMult *= WorldStepSize;

[loop]
for (int i = 0; i < NumStepsForRay && Transmittance > 1e-5; ++i)
{
  
  float3 CurrPos = RayStart + RayDir * t;

  // Convert local space to unit (0-1)
  float3 CurrUnit = CurrPos / WorldGridExtents + .5;  
  // 采样CurrUnit位置的Density,Grid是ScalarGrid
  float Density = 0;
  Grid.SampleGrid(CurrUnit.x, CurrUnit.y, CurrUnit.z, DensityIndex, Density);

  if( UseLinearCurve || UseDensityCurve )
  {
    float remap = saturate((Density - DensityCurveOffset) / DensityCurveRange);

    if( UseLinearCurve )
    {
        Density = remap;
    }
    else
    {
        DensityCurve.SampleCurve(remap, Density);
    }
  }
  // 结果叠加到Transmittance
  Transmittance *= exp(-1. * Density * DensityMult);

  t += StepSize;  
}

#endif
1.2.14 Post Sim Scalars

这个模块就是数据获取,也把数据放到SimRT上面去,不过RGBA放的东西和之前Debug Slice Prep不一样,分别是Density、Light1、Temperature、Light2Light1Light2Computing Lighting计算出来的两个光源各自的透光度Texture。
1.3.1 Render

双击Mesh Render可以看见其Override Material里面MI_RayMarch_Fire_Ramps,下面是Material中的计算方法。
// Include /Plugin/FX/NiagaraFluids/NiagaraFluids.ush
#define MARCH_SINGLE_RAY_EMISSION(RayStart, RayDir, UnitStepSize, LocalStepSize, NumSteps, Volume, RetV) \
RetV = float4(0,0,0,0); \ // 初始化返回值
float Transmittance = 1; \ // 初始化透光率
int Steps = ceil(NumSteps); \ // 计算需要的Step
if(Steps >= 1.0 ) Steps += 1.0; \ // 因为上面是Ceil,这里稳妥就加了一格
[loop] \
for (int i = 0; i < Steps && Transmittance > 1e-5; i++) \ // 循环
{ \
float Extinction; \
float Scattering; \
float3 Luminance; \
float EmissionExtinction = 0; \ // 初始化
float3 EmissionColor = float3(0,0,0); \ // 初始化计算颜色
// Volume结构体的函数,计算出
Volume.ComputeStep(RayStart, RayDir, Extinction, Scattering, Luminance, EmissionExtinction, EmissionColor); \
float ClampedEmissionExtinction = max(EmissionExtinction, 1e-6); \ //
float StepEmissionTransmittance = exp(-EmissionExtinction * LocalStepSize); \ //
// 自发光颜色累加
RetV.rgb += Transmittance * (EmissionColor - EmissionColor * StepEmissionTransmittance) / ClampedEmissionExtinction; \
// 透光率累乘
Transmittance *= StepEmissionTransmittance; \
// 散射累乘
Luminance *= Scattering; \
float ClampedExtinction = max(Extinction, 1e-6); \
float StepTransmittance = exp(-Extinction * LocalStepSize); \
// 散射颜色累加
RetV.rgb += Transmittance * (Luminance - Luminance * StepTransmittance) / ClampedExtinction; \
Transmittance *= StepTransmittance; \
if(i == Steps-2) UnitStepSize, LocalStepSize *= frac(NumSteps); \ //还有这种语法?这应该就是后面的减少Ray的速度
RayStart += RayDir * UnitStepSize; \
} \
RetV.a = 1. - Transmittance; //将透光率作用到结果上

// ------------------------Start Material----------------------
struct FireRampsRenderVolume
{
        Texture3D VolumeTexture;
        SamplerState VolumeTextureSampler;
        float Albedo;

        float3 SunColor;
        float3 FillColor;
        float ConstantExtinction;
        float DensityOffset;
        float DensityRange;
        Texture2D DensityRamp;
        SamplerState DensityRampSampler;
        float DensityGain;
        float TemperatureOffset;
        float TemperatureRange;
        Texture2D TemperatureRamp;
        SamplerState TemperatureRampSampler;
        float TemperatureGain;
        float FireOpacityGain;
        float UseBlackbodyCurve;
        float UseDensityCurve;
        float UseColorTexture;
        Texture3D ColorTexture;
        SamplerState ColorTextureSampler;

// Computes volume extinction coefficient and luminance for a given point in the volume
void ComputeStep(
        float3 Position, float3 RayDirection, out float OutExtinction,
        out float OutScattering, out float3 OutLuminance,
        out float OutEmissionExtinction , out float3 OutEmissionColor)
{  
        // Position是Ray目前March到的位置,
        // VolumeTexture就是Post Sim Scalars存储的RimRT,r就是Density
        float4 VolumeSample = VolumeTexture.SampleLevel(VolumeTextureSampler, Position, 0);
        // 重新映射
        float RemapDensity = DensityRange > 1e-8 ? saturate((VolumeSample.r - DensityOffset) / DensityRange) : 0.0;
        float DensityCurve = RemapDensity;
        // 使用Curve再映射一次
        [branch]
        if (UseDensityCurve)
        {
            DensityCurve = DensityRamp.SampleLevel(DensityRampSampler, RemapDensity, 0 ).r;
        }
        // 计算烟雾的,Density重映射之后的最终值乘以强度系数
        float SmokeExtinction = DensityCurve * DensityGain;

        OutExtinction = DensityCurve * DensityGain;
        OutExtinction += ConstantExtinction * (OutExtinction > 1e-5);

        // 计算颜色,Light1的透光率控制SunColor,Light2的透光率控制FillColor,Color都是Light的参数
        OutLuminance = SunColor * VolumeSample.g;
        OutLuminance += FillColor * VolumeSample.a;       
        // 这个应该就是可以控制烟雾颜色的玩意,Niagara示例中带颜色的烟雾可能就是这个实现的
        [branch]
        if (UseColorTexture)
        {
                float3 ColorSample = ColorTexture.SampleLevel(ColorTextureSampler, Position, 0);
                OutLuminance *= ColorSample;
        }
       
        OutScattering = OutExtinction * Albedo;
        // 重映射Temperature
        float RemapTemperature = TemperatureRange > 1e-8 ? saturate((VolumeSample.b - TemperatureOffset) / TemperatureRange) : 0;
        float4 TemperatureCurve = TemperatureRamp.SampleLevel(TemperatureRampSampler, RemapTemperature, 0 );
        // 初始化自发光
        OutEmissionExtinction = TemperatureCurve.a * FireOpacityGain;
        // 使用黑体辐射
        [branch]
        if (UseBlackbodyCurve)
        {
                const float MaxKelvin = 4500;
                OutEmissionColor = MaterialExpressionBlackBody(RemapTemperature * MaxKelvin) * TemperatureGain;
        }
        else
        {
                OutEmissionColor = TemperatureCurve.rgb * TemperatureGain;   
        }
}
};

FireRampsRenderVolume Volume;
Volume.VolumeTexture = VolumeTexture;
Volume.VolumeTextureSampler = VolumeTextureSampler;
Volume.Albedo = Albedo;
Volume.SunColor = SunColor;
Volume.FillColor = FillColor;
Volume.ConstantExtinction = ConstantExtinction.r;
Volume.DensityOffset = DensityOffset;
Volume.DensityRange = DensityRange;
Volume.DensityRamp = DensityRamp;
Volume.DensityRampSampler = DensityRampSampler;
Volume.DensityGain = DensityGain;
Volume.TemperatureOffset = TemperatureOffset;
Volume.TemperatureRange = TemperatureRange;
Volume.TemperatureRamp = TemperatureRamp;
Volume.TemperatureRampSampler = TemperatureRampSampler;
Volume.TemperatureGain = TemperatureGain;
Volume.FireOpacityGain = FireOpacityGain;
Volume.UseBlackbodyCurve = UseBlackbodyCurve;
Volume.UseDensityCurve = UseDensityCurve;
Volume.UseColorTexture = UseColorTexture;
Volume.ColorTexture = ColorTexture;
Volume.ColorTextureSampler = ColorTextureSampler;

float4 ret;
MARCH_SINGLE_RAY_EMISSION(RayStart, RayDir, UnitStepSize, LocalStepSize, NumSteps, Volume, ret);

return ret;
这样就计算完了。
注:

下篇应该还会分析一个粒子方法,再做总结,下篇提供的Niagara基础可能就会少很多了。
参考


  • ^Visual simulation of smokehttps://dl.acm.org/doi/abs/10.1145/383259.383260
  • ^Smoke Simulation for Fire Engineering using a Multigrid Method on Graphics Hardwarehttp://diglib.eg.org/handle/10.2312/PE.vriphys.vriphys09.011-020
  • ^Fluid Simulation for Computer Graphics, Second Edition

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-13 08:07 , Processed in 0.104345 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表