教程:粒子材质
中级 美术 程序员
本教程演示如何为粒子系统创建自定义着色器和材质,提供核心引擎中没有的功能。它侧重于着色器和渲染。有关模拟,请参见自定义粒子教程。
如果你不熟悉编辑粒子,请参见创建粒子。
首先创建一个新的 Sample: Particles 项目。

此项目包含四个场景,每个场景演示一种不同的粒子使用方式:AnimatedParticles、ChildParticles、CustomMaterials 和 CustomParticles。
打开 CustomMaterials 场景。
场景中有三个粒子实体:Rad Particle System、Radial Particle System 和 Two Textures Particle System。

选择其中一个粒子实体,导航到其源粒子系统,展开其中的发射器及其材质。
红色粒子系统
红色粒子系统有一个非常简单的自定义。由于材质贴图已经提供了将着色器用作叶节点输入的选项,我们可以创建一个自定义着色器并将其分配给该节点。
首先,创建一个着色器(ComputeColorRed.sdsl),为 ComputeColor 派生一个类:
class ComputeColorRed : ComputeColor
{
override float4 Compute()
{
return float4(1, 0, 0, 1);
}
};
这个着色器唯一做的就是在每次调用 Compute 时返回用于像素着色的红色。我们稍后会尝试更复杂的内容,但现在先保持简单。
保存文件并在 Game Studio 中重新加载脚本。你应该能在 Asset View 中看到新的着色器。

如果着色器不在那里,请重新加载项目。
着色器加载后,你可以在 Property Grid 中粒子的 dynamic emissive material 下访问它。选择一种着色器类型,然后从下拉菜单中选择你刚刚添加到场景中的着色器。

粒子变红了。在 Game Studio 运行的情况下,编辑并保存 ComputeColorRed.sdsl 使它们变黄。
class ComputeColorRed : ComputeColor
{
override float4 Compute()
{
return float4(1, 1, 0, 1);
}
};
由于 Stride 支持动态着色器编译,粒子立即变成了黄色。
径向粒子系统
在下一个着色器中,我们将使用纹理坐标向编辑器公开任意值。
查看 ComputeColorRadial.sdsl。
class ComputeColorRadial<float4 ColorCenter, float4 ColorEdge> : ComputeColor, Texturing
{
override float4 Compute()
{
float radialDistance = length(streams.TexCoord - float2(0.5, 0.5)) * 2;
float4 unclamped = lerp(ColorCenter, ColorEdge, radialDistance);
// 我们希望允许强度大幅增长,但将 alpha 限制为 1
float4 clamped = clamp(unclamped, float4(0, 0, 0, 0), float4(1000, 1000, 1000, 1));
// 请记住,我们使用预乘 alpha 管线,因此所有颜色值都应该是预乘的
clamped.rgb *= clamped.a;
return clamped;
}
};
这与 ComputeColorRed 类似,可以用相同的方式编译和加载。
有几个关键区别。该着色器现在还继承自 Texturing 着色器基类。这使它能够使用来自流(streams)的纹理坐标。在 Game Studio 的材质方面,我们可以强制流式传输纹理坐标,以防我们不使用纹理动画。
我们着色器中的输入值 float4 ColorCenter 和 float4 ColorEdge 是排列参数(permutations)。当我们加载着色器时,Property Grid 会在 Generics 字典下显示它们。

我们在这里设置的值将被粒子的 ComputeColorRadial 着色器使用。着色器的其余部分只是根据着色像素到公告板中心的距离计算渐变颜色。
双纹理粒子系统
这演示了如何为粒子创建自定义材质和效果。DynamicColor 材质支持一个 RGBA 通道。在我们的示例中,我们将分离 RGB 和 A 通道,允许它们使用不同的纹理坐标动画、不同的纹理和二叉树来计算颜色。
参数键(Parameter keys)
参数键用于映射数据并将其传递给着色器。其中一些是自动生成的,我们也可以定义自己的。
如果我们在着色器(ParticleCustomShader)中定义更多的流,它们会被导出到一个自动生成的类中。试着将以下内容添加到 ParticleCustomShader.sdsl:
// -------------------------------------
// streams
// -------------------------------------
stage float4 SomeRandomKey;
生成的 .cs 文件现在应该包含:
namespace Stride.Rendering
{
public static partial class ParticleCustomShaderKeys
{
public static readonly ParameterKey<Vector4> SomeRandomKey = ParameterKeys.New<Vector4>();
}
}
我们现在不需要这个流,所以可以删除它。
我们将在 ParticleCustomMaterialKeys.cs 中定义一些额外的键,以便在材质和效果中使用。
namespace Stride.Rendering
{
public partial class ParticleCustomShaderKeys
{
static ParticleCustomShaderKeys()
{
}
public static readonly ParameterKey<ShaderSource> BaseColor = ParameterKeys.New<ShaderSource>();
public static readonly ParameterKey<Texture> EmissiveMap = ParameterKeys.New<Texture>();
public static readonly ParameterKey<Color4> EmissiveValue = ParameterKeys.New<Color4>();
public static readonly ParameterKey<ShaderSource> BaseIntensity = ParameterKeys.New<ShaderSource>();
public static readonly ParameterKey<Texture> IntensityMap = ParameterKeys.New<Texture>();
public static readonly ParameterKey<float> IntensityValue = ParameterKeys.New<float>();
}
}
正如我们在上面看到的,生成的类具有相同的名称,命名空间是 Stride.Rendering,所以我们必须使我们的类为 partial 并匹配命名空间。这对这个特定的示例没有影响,但如果你的着色器代码自动生成了某些键,将导致编译错误。
其余的代码不言自明。我们稍后需要 map 和 value 键用于着色器生成,并且我们将把生成的代码分别设置到 BaseColor 和 BaseIntensity 键,以便着色器可以使用它。
自定义着色器
让我们看看 ParticleCustomShader.sdsl:
class ParticleCustomShader : ParticleBase
{
// 这个着色器可以由用户设置,它是由更小的着色器组成的二叉树
compose ComputeColor baseColor;
// 这个着色器可以由用户设置,它是由更小的着色器组成的二叉树
compose ComputeColor baseIntensity;
// 精灵的着色 — 我们重写基类的 Shading(),它只返回 ColorScale
stage override float4 Shading()
{
// -----------------------------------------------
// 基础粒子颜色 RGB
// -----------------------------------------------
float4 finalColor = base.Shading() * baseColor.Compute();
// -----------------------------------------------
// 基础粒子 alpha
// -----------------------------------------------
finalColor.a = baseIntensity.Compute();
// 不要忘记预乘 alpha
finalColor.rgb *= finalColor.aaa;
return finalColor;
}
};
它定义了两个组合着色器 baseColor 和 baseIntensity,我们将分别为 RGB 和 A 插入生成的着色器。它继承了 ParticleBase,后者已经定义了 VSMain、PSMain 和纹理映射,并使用了非常简单的 Shading() 方法。
通过重写 Shading() 方法,我们可以定义自定义行为。因为我们使用的组合着色器派生自 ComputeColor,我们可以轻松地使用 Compute() 来求值,它为我们提供了颜色和强度的计算树的根。
自定义效果
我们的效果描述了如何混合和组合着色器。它在 ParticleCustomEffect.sdfx 中:
namespace Stride.Rendering
{
partial shader ParticleCustomEffect
{
// 使用游戏引擎中定义的 ParticleBaseKeys 作为常量属性
using params ParticleBaseKeys;
// 使用本项目中定义的 ParticleCustomShaderKeys 作为常量属性
using params ParticleCustomShaderKeys;
// 继承自游戏引擎中定义的 ParticleBaseEffect.sdfx
mixin ParticleBaseEffect;
// 使用本项目中定义的 ParticleCustomShader.sdsl
mixin ParticleCustomShader;
// 如果 baseColor 的用户自定义着色器不为 null,则使用它
if (ParticleCustomShaderKeys.BaseColor != null)
{
mixin compose baseColor = ParticleCustomShaderKeys.BaseColor;
}
// 如果 baseIntensity (alpha) 的用户自定义着色器不为 null,则使用它
if (ParticleCustomShaderKeys.BaseIntensity != null)
{
mixin compose baseIntensity = ParticleCustomShaderKeys.BaseIntensity;
}
};
}
ParticleBaseKeys 和 ParticleBaseEffect 是我们继承的基础着色器所必需的。
ParticleCustomShaderKeys 提供了我们之前定义的键,我们将把着色器插入其中。
最后,对于两个着色器,我们只需要检查是否有用户定义的代码并将其插入即可。baseColor 和 baseIntensity 参数来自我们之前创建的着色器。
最后,我们需要一个设置所有键并使用新创建的效果的材质。
自定义粒子材质
我们将把 @'Stride.Particles.Materials.ParticleMaterialComputeColor' 复制到我们项目中的 ParticleCustomMaterial.cs,并对其进行自定义,以使用两个着色器进行颜色二叉树。
[DataMemberIgnore]
protected override string EffectName { get; set; } = "ParticleCustomEffect";
基类会自动尝试加载用 EffectName 指定的效果。我们给它我们之前创建的效果的名称。
[DataMember(300)]
[Display("Alpha")]
public IComputeScalar ComputeScalar { get; set; } = new ComputeTextureScalar();
[DataMember(400)]
[Display("TexCoord1")]
public UVBuilder UVBuilder1;
private AttributeDescription texCoord1 = new AttributeDescription("TEXCOORD1");
除了已有的 @'Stride.Rendering.Materials.IComputeColor' 之外,我们还将使用 @'Stride.Rendering.Materials.IComputeScalar' 来处理强度,它返回一个 float 而不是 float4。我们还将添加另一个 @'Stride.Particles.Materials.UVBuilder' 用于第二组纹理坐标动画。
var shaderBaseColor = ComputeColor.GenerateShaderSource(shaderGeneratorContext, new MaterialComputeColorKeys(ParticleCustomShaderKeys.EmissiveMap, ParticleCustomShaderKeys.EmissiveValue, Color.White));
shaderGeneratorContext.Parameters.Set(ParticleCustomShaderKeys.BaseColor, shaderBaseColor);
var shaderBaseScalar = ComputeScalar.GenerateShaderSource(shaderGeneratorContext, new MaterialComputeColorKeys(ParticleCustomShaderKeys.IntensityMap, ParticleCustomShaderKeys.IntensityValue, Color.White));
shaderGeneratorContext.Parameters.Set(ParticleCustomShaderKeys.BaseIntensity, shaderBaseScalar);
我们加载两个着色器:一个用于主颜色,一个用于强度。这些与我们在前两个示例中手动编写的着色器类似,只是我们直接从 ComputeColor 和 ComputeScalar 属性(你可以在 Property Grid 中编辑)动态生成它们。生成的代码与我们编写的着色器代码类似,它调用 Compute() 并返回我们的颜色或标量计算树的最终结果。
生成着色器代码后,我们将它设置到所需的相应键。查看 ParticleCustomShaderKeys.cs 中 ParticleCustomShaderKeys.BaseColor 的定义方式。在效果文件中,我们检查这个键是否已设置,如果已设置,就将它传递给我们着色器代码中定义的流。