Tutorial: Particle materials

    This tutorial demonstrates how to create custom shaders and materials for a particle system, providing functionality not available in the core engine. It focuses on shaders and rendering. For simulation, see the .

    If you're not familiar with editing particles, see Create particles.

    Start by creating a new Sample: Particles project.

    This project contains four scenes, each demonstrating a different way to use particles: AnimatedParticles, ChildParticles, CustomMaterials, and CustomParticles.

    Open the CustomMaterials scene.

    There are three particle entities in the scene: Rad Particle System, Radial Particle System, and Two Textures Particle System.

    media/particles-samples-material-1.png

    Select one of the particle entities and navigate to its source particle system, expanding the emitter in it and its material.

    The red particle system has a very simple customization. Since the already provide an option to use shaders as a leaf node input, we can create a custom shader and assign it to that node.

    First, create a shader () with a derived class for ComputeColor:

    The only thing this shader does is return the red color for pixel shading every time Compute is called. We'll try something more difficult later, but for now let's keep it simple.

    Save the file and reload the scripts in Game Studio. You should see the new shader in Asset View.

    If the shader isn't there, reload the project.

    media/particles-samples-material-3.png

    The particles are red. With Game Studio running, edit and save ComputeColorRed.xksl to make them yellow.

    1. class ComputeColorRed : ComputeColor
    2. {
    3. override float4 Compute()
    4. {
    5. return float4(1, 1, 0, 1);
    6. }
    7. };

    Because Xenko supports dynamic shader compilation, the particles immediately turn yellow.

    For the next shader we'll use texture coordinates expose arbitrary values to the editor.

    Check ComputeColorRadial.xksl.

    1. class ComputeColorRadial<float4 ColorCenter, float4 ColorEdge> : ComputeColor, Texturing
    2. {
    3. override float4 Compute()
    4. {
    5. float radialDistance = length(streams.TexCoord - float2(0.5, 0.5)) * 2;
    6. float4 unclamped = lerp(ColorCenter, ColorEdge, radialDistance);
    7. // We want to allow the intensity to grow a lot, but cap the alpha to 1
    8. float4 clamped = clamp(unclamped, float4(0, 0, 0, 0), float4(1000, 1000, 1000, 1));
    9. // Remember that we use a premultiplied alpha pipeline so all color values should be premultiplied
    10. clamped.rgb *= clamped.a;
    11. return clamped;
    12. }
    13. };

    This is similar to ComputeColorRed and can be compiled and loaded the same way.

    There are several key differences. The shader now inherits from the Texturing shader base class as well. This allows it to use texture coordinates in from the streams. On the material side in Game Studio, we can force the texture coordinates to be streamed in case we don't use texture animation.

    The input values float4 ColorCenter and float4 ColorEdge in our shader are permutations. When we load the shader the Property Grid displays them under the Generics dictionary.

    The values we set here will be used by the ComputeColorRadial shader for the particles. The rest of the shader simply calculates a gradient color based on the distance of the shaded pixel from the center of the billboard.

    This demonstrates how to create custom materials and effects for the particles. The DynamicColor material supports one RGBA channel. For our sample, we'll separate the RGB and A channels, allowing them to use different texture coordinate animations and different textures and binary trees to compute the color.

    Parameter keys

    Parameter keys are used to map data and pass it to the shader. Some of them are generated, and we can define our own too.

    If we define more streams in our shader (ParticleCustomShader), they're exported to an automatically generated class. Try adding the following to ParticleCustomShader.xksl:

    The generated .cs file should now contain:

    1. namespace Xenko.Rendering
    2. {
    3. {
    4. public static readonly ParameterKey<Vector4> SomeRandomKey = ParameterKeys.New<Vector4>();
    5. }

    We don't need this stream for now, so we can delete it.

    1. namespace Xenko.Rendering
    2. {
    3. public partial class ParticleCustomShaderKeys
    4. {
    5. static ParticleCustomShaderKeys()
    6. {
    7. }
    8. public static readonly ParameterKey<ShaderSource> BaseColor = ParameterKeys.New<ShaderSource>();
    9. public static readonly ParameterKey<Texture> EmissiveMap = ParameterKeys.New<Texture>();
    10. public static readonly ParameterKey<Color4> EmissiveValue = ParameterKeys.New<Color4>();
    11. public static readonly ParameterKey<ShaderSource> BaseIntensity = ParameterKeys.New<ShaderSource>();
    12. public static readonly ParameterKey<Texture> IntensityMap = ParameterKeys.New<Texture>();
    13. public static readonly ParameterKey<float> IntensityValue = ParameterKeys.New<float>();
    14. }
    15. }

    As we saw above, the generated class has the same name and the namespace is Xenko.Rendering, so we have to make our class partial and match the namespace. This has no effect on this specific sample, but will result in compilation error if your shader code auto-generates some keys.

    The rest of the code is self-explanatory. We'll need the map and value keys for shader generation later, and we'll set our generated code to the BaseColor and BaseIntensity keys respectively so the shader can use it.

    Custom Shader

    Let's look at ParticleCustomShader.xksl:

    It defines two composed shaders, baseColor and abseIntensity, where we'll plug our generated shaders for RGB and A respectively. It inherits ParticleBase which already defines VSMain, PSMain and texturing, and uses very simple Shading() method.

    By overriding the Shading() method we can define our custom behavior. Because the composed shaders we use are derived from ComputeColor, we can easily evaluate them using Compute(), which gives us the root of the compute tree for color and intensity.

    Custom effect

    Our effect describes how to mix and compose the shaders. It's in ParticleCustomEffect.xkfx:

    1. namespace Xenko.Rendering
    2. {
    3. partial shader ParticleCustomEffect
    4. {
    5. // Use the ParticleBaseKeys for constant attributes, defined in the game engine
    6. // Use the ParticleCustomShaderKeys for constant attributes, defined in this project
    7. // Inherit from the ParticleBaseEffect.xkfx, defined in the game engine
    8. mixin ParticleBaseEffect;
    9. // Use the ParticleCustomShader.xksl, defined in this project
    10. mixin ParticleCustomShader;
    11. // If the user-defined shader for the baseColor is not null use it
    12. if (ParticleCustomShaderKeys.BaseColor != null)
    13. {
    14. mixin compose baseColor = ParticleCustomShaderKeys.BaseColor;
    15. }
    16. // If the user-defined shader for the baseIntensity (alpha) is not null use it
    17. if (ParticleCustomShaderKeys.BaseIntensity != null)
    18. {
    19. mixin compose baseIntensity = ParticleCustomShaderKeys.BaseIntensity;
    20. }
    21. };
    22. }

    ParticleBaseKeys and ParticleBaseEffect are required by the base shader which we inherit.

    ParticleCustomShaderKeys provides the keys we defined earlier, where we'll plug our shaders.

    Finally, for both shaders we only need to check if there is user-defined code for it and plug it. The baseColor and baseIntensity parameters are from the shader we created earlier.

    Last, we need a material which sets all the keys and uses the newly created effect.

    Custom particle material

    We'll copy ParticleMaterialComputeColor into ParticleCustomMaterial.cs in our project and customize it to use two shaders for color binary trees.

    1. [DataMemberIgnore]
    2. protected override string EffectName { get; set; } = "ParticleCustomEffect";

    The base class automatically tries to load the effect specified with EffectName. We give it the name of the effect we crated earlier.

    In addition to the already existing , we'll use IComputeScalar for intensity, which returns a float, rather than a float4. We will also add another for a second texture coordinates animation.

    1. var shaderBaseColor = ComputeColor.GenerateShaderSource(shaderGeneratorContext, new MaterialComputeColorKeys(ParticleCustomShaderKeys.EmissiveMap, ParticleCustomShaderKeys.EmissiveValue, Color.White));
    2. shaderGeneratorContext.Parameters.Set(ParticleCustomShaderKeys.BaseColor, shaderBaseColor);
    3. var shaderBaseScalar = ComputeScalar.GenerateShaderSource(shaderGeneratorContext, new MaterialComputeColorKeys(ParticleCustomShaderKeys.IntensityMap, ParticleCustomShaderKeys.IntensityValue, Color.White));
    4. shaderGeneratorContext.Parameters.Set(ParticleCustomShaderKeys.BaseIntensity, shaderBaseScalar);

    We load the two shaders: one for the main color and one for the intensity. These are similar to the shaders we wrote manually in the last two examples, except we generate them on the fly directly from the ComputeColor and ComputeScalar properties, which you can edit in the Property Grid. The generated code is similar to the shader code we wrote in the way that it calls Compute() and it returns the final result of our color or scalar compute tree.

    After we generate the shader code, we set it to the respective key we need. Check how is defined in ParticleCustomShaderKeys.cs. In the effect file we check if this key is set, and if yes, we pass it to the stream defined in our shader code.