tModLoader [TUTORIAL] Shockwave effect for tModLoader

Kazzymodus

Moderator
Staff member
Moderator

tIei3fF.gif

Ever wondered how you could implement a shockwave effect to your mod like the Pillar shields? How to give your explosions a bit more 'oomph'? In this guide, I'll teach you how to implement them! Don't worry, it's easier than you might think.

The basics

The shockwave effect is actually a surprisingly simple screen shader, the code of which is as follows:
Code:
float4 Shockwave(float4 position : SV_POSITION, float2 coords : TEXCOORD0) : COLOR0
{
    float2 targetCoords = (uTargetPosition - uScreenPosition) / uScreenResolution;
    float2 centreCoords = (coords - targetCoords) * (uScreenResolution / uScreenResolution.y);
    float dotField = dot(centreCoords, centreCoords);
    float ripple = dotField * uColor.y * PI - uProgress * uColor.z;

    if (ripple < 0 && ripple > uColor.x * -2 * PI)
    {
        ripple = saturate(sin(ripple));
    }
    else
    {
        ripple = 0;
    }

    float2 sampleCoords = coords + ((ripple * uOpacity / uScreenResolution) * centreCoords);

    return tex2D(uImage0, sampleCoords);
}
Essentially this code generates a radial map from a specific point (the source of the shockwave), and then feeds that map into a sine function to generate ripples. These ripples are used to distort the screen sampler coordinates to give the impression of a shockwave.

There are five parameters you can use to modify the shockwave, three of which have been mapped to uColor (you can not implement your own parameters into shader files, so I had to use existing ones): the amount of ripples (uColor.x), the size of each ripple (uColor.y) and the shockwave propagation speed (uColor.z). Size is a bit of a misnomer, as it is a scalar for the dot field, meaning that as the size increases, the ripples actually become tighter, not wider. Keep this in mind when tweaking the values. The last two parameters are uProgress, which propagates the wave, and uOpacity, which determines the strength of the distortion. You can use these two in tandem to make the shockwave fade out, but it's not necessary.

Pretty much all the complicated stuff happens in here. Actually triggering the shockwave is surprisingly easy (primarily to Skiphs' beautifully modular approach to implementing filters). Let me walk you through the process.

Step 1: Add the shader to your project

First of all, you'll need to add the shader to the project. If you're fine with the shader as described above (it gives the effect as seen in the video and the GIF), you can get a compiled version here*. If you want to make modifications (feel free to do so!), you'll have to manually compile it using a standalone application. If you're not sure how to do that, the tModLoader discord can get you on your way. I would advise you to at least have a basic grasps of how shaders work if you're going to go that route, however. I wouldn't recommend it as a first project.

* Please read the disclaimer at the bottom of this post before you download and/or use this file.

When you've downloaded or compiled the shader (it should have the .xnb extension), put it in ModSources/ModName/Effects. And that's it, onto the next step!

Step 2: Load the shader into your mod

To use the shader, you'll have to link it to a filter. Don't worry, it's incredibly easy. Simply put this in the Load method of your Mod class:

C#:
// Add these three using directives.

using Terraria.Graphics.Effects;
using Terraria.Graphics.Shaders;
using Terraria.ID;

public override void Load()
{
    // ...other Load stuff goes here

    if (Main.netMode != NetmodeID.Server)
    {
        Ref<Effect> screenRef = new Ref<Effect>(GetEffect("Effects/ShockwaveEffect")); // The path to the compiled shader file.
        Filters.Scene["Shockwave"] = new Filter(new ScreenShaderData(screenRef, "Shockwave"), EffectPriority.VeryHigh);
        Filters.Scene["Shockwave"].Load();
    }
}

Whatever you put between the square brackets will be your effect indexer. You can call this whatever you want, but you'll have to use it consistently throughout the rest of these steps. The second argument in the ScreenShaderData is the name of the pass within the shader file. If you downloaded the compiled version, just leave it as Shockwave. If you modified the shader code and renamed the shader pass, it needs to be whatever you called that pass.

Step 3: Activate the shader

This is the most difficult bit. The biggest issue is that simply activating the shockwave isn't enough: you will have to keep updating it for the duration of the shockwave. This can be problem if it's an on-kill effect (like a bomb), but thankfully there are ways around this, either by making a dummy object or simply extending the lifetime of the source. For our example, we're going to make a bomb that does the latter.

Now, suppose we want our bomb to explodes after three seconds. Normally, we would just set projectile.timeLeft to 180 and expand its size during the last three frames or so (or just use an existing aiStyle). However, to properly propagate the shockwave, the projectile needs to remain alive for the lifetime of the shockwave. So let's give the projectile a lifetime of 360 ticks: a three seconds fuse time, and then another three seconds for the shockwave to expand.

To activate the shader, you need to call this on the frame the bomb explodes:

C#:
if (Main.netMode != NetmodeID.Server && !Filters.Scene["Shockwave"].IsActive())
{
    Filters.Scene.Activate("Shockwave", projectile.Center).GetShader().UseColor(rippleCount, rippleSize, rippleSpeed).UseTargetPosition(projectile.Center);
}
This code activates the screen shader if it is not already active. If there is already a shockwave in progress, this will prevent it from suddenly moving its source to the new explosion or changing its properties. It will not prevent the new explosion from taking over the propagation (visually, it would reset the shockwave at its original origin), however, so it's up to you to make sure only one object is modifying the filter at any time.

When you activate the shader, you can (and probably should) set the origin, in our case the bomb's center. You can also specify the amount of ripples, the size of the ripples and the speed of the ripples. If you're not sure what good default values are, I'd suggest (3, 5, 15) to start with. You can then tweak them to your liking, but bear in mind they do affect each other to some extent, so modifying one might mean you'll have to tweak another.

Now we've activated the shader, but we'd also like it to actually move. To do this, you need to update the filter's progress.
C#:
if (Main.netMode != NetmodeID.Server && Filters.Scene["Shockwave"].IsActive())
{
    float progress = (180f - projectile.timeLeft) / 60f; // Will range from -3 to 3, 0 being the point where the bomb explodes.
    Filters.Scene["Shockwave"].GetShader().UseProgress(progress).UseOpacity(distortStrength * (1 - progress / 3f));
}

UseOpacity is used here to fade out the wave as it travels. This is not necessary, but it looks a bit cleaner, and it can also cover up some artifacts like the wave suddenly stopping or disappearing when the source calls it quits.

Speaking about that, once the wave is 'over', we'll need to deactivate the filter so another explosion can use it again. For our bomb, the safest place to do it is in the Kill method:
C#:
public override void Kill(int timeLeft)
{
    if(Main.netMode != NetmodeID.Server && Filters.Scene["Shockwave"].IsActive())
    {
        Filters.Scene["Shockwave"].Deactivate();
    }
}

In full, the code would look something like this:
C#:
using Terraria.Graphics.Effects; // Don't forget this one!

private int rippleCount = 3;
private int rippleSize = 5;
private int rippleSpeed = 15;
private float distortStrength = 100f;

public override void AI()
{
    // ai[0] = state
    // 0 = unexploded
    // 1 = exploded

    if (projectile.timeLeft <= 180)
    {
        if (projectile.ai[0] == 0)
        {
            projectile.ai[0] = 1; // Set state to exploded
            projectile.alpha = 255; // Make the projectile invisible.
            projectile.friendly = false; // Stop the bomb from hurting enemies.

            if (Main.netMode != NetmodeID.Server && !Filters.Scene["Shockwave"].IsActive())
            {
                Filters.Scene.Activate("Shockwave", projectile.Center).GetShader().UseColor(rippleCount, rippleSize, rippleSpeed).UseTargetPosition(projectile.Center);
            }
        }

        if (Main.netMode != NetmodeID.Server && Filters.Scene["Shockwave"].IsActive())
        {
            float progress = (180f - projectile.timeLeft) / 60f;
            Filters.Scene["Shockwave"].GetShader().UseProgress(progress).UseOpacity(distortStrength * (1 - progress / 3f));
        }
    }
}

public override void Kill(int timeLeft)
{
    if (Main.netMode != NetmodeID.Server && Filters.Scene["Shockwave"].IsActive())
    {
        Filters.Scene["Shockwave"].Deactivate();
    }
}


What if I want multiple shockwaves?

A single shockwave filter can only have one source, so it can't duplicate itself. This is fine if your shockwaves happen at regular, large intervals (for a boss' special attack, for instance), but what if you have the aforementioned bombs that you can spray all over the place?

The answer is simple: make more filters.
C#:
public override void Load()
{
    // ...

    if (Main.netMode != NetmodeID.Server)
    {
       Ref<Effect> screenRef = new Ref<Effect>(GetEffect("Effects/ShockwaveEffect")); // The path to the compiled shader file
       Filters.Scene["Shockwave1"] = new Filter(new ScreenShaderData(screenRef, "Shockwave"), EffectPriority.VeryHigh);
       Filters.Scene["Shockwave1"].Load();
       Filters.Scene["Shockwave2"] = new Filter(new ScreenShaderData(screenRef, "Shockwave"), EffectPriority.VeryHigh);
       Filters.Scene["Shockwave2"].Load();
       Filters.Scene["Shockwave3"] = new Filter(new ScreenShaderData(screenRef, "Shockwave"), EffectPriority.VeryHigh);
       Filters.Scene["Shockwave3"].Load();
       Filters.Scene["Shockwave4"] = new Filter(new ScreenShaderData(screenRef, "Shockwave"), EffectPriority.VeryHigh);
       Filters.Scene["Shockwave4"].Load();
   }
}
Through this method you can have 4 individual shockwaves (yes, they stack!) that can have independent sources, speeds, sizes and ripple counts. It is up to you however to manage these properly, and to keep track which waves are in use and which are ready to be reused. Also, be careful to not go overboard: while the screen shader is not very expensive, it is still being applied to every single pixel on the screen every frame for every single filter. So keep it reasonable.

Disclaimer

Hope you enjoyed this guide! Unfortunately, I have to end with some legal stuff to make sure everyone behaves and nobody starts complaining:

  • By downloading or using any of the content provided in or by this thread, you agree with everything in this disclaimer. If you don't, don't use the content. Simple as that.
  • All the code in this thread was written by me. You are free to use and adapt this code to your liking, with the following conditions:
    • You don't have to credit me for the use of any of this code or the guide (although it's always appreciated!), but don't go passing it off as your own. That's just rude.
    • Similarly, if you use this code, and someone asks you how you did it, either show them or point them towards this thread. This information is meant to be available to everyone, don't try to make it some sort of exclusive thing.
    • I reserve the right to refuse usage of my code to people, retroactively or otherwise, for whatever reason, for example if they use it for content I deem inappropriate, or if they generally behave in a way I consider deconstructive to the modding community at large. I don't expect it to come to that, and let's not try, okay?
    • Any of the conditions that apply to using tModLoader or modding in general also apply to the usage of this code. If anything I said here contradicts those conditions, then they take precedence over mine.
  • I'm not responsible for anything that happens to you or your computer when you download or use any of the content in this post. If you're experiencing reliable/frequent crashes and are confident they are caused by any of the content in this thread, please let me know at your earliest convenience.
  • In the same vein, I'm not responsible for any adaptations of this work. So if someone runs my shader a thousand times per frame and fries your GPU (which is extremely unlikely but technically possible), that's not my fault.
  • If you go absolutely crazy with some of the shader parameters (like, beyond-the-scope-of-any-conceivably-reasonable-use-of-this-shader-crazy), you just might be able to come up with something sufficiently messed up to trigger an epileptic insult in people who are vulnerable to those. Be careful with that, and don't go and try to trigger insults in people on purpose because that is assault and you are a terrible person for doing so.

That'll do, I think. If you have any more questions, feel free to ask them below!
 
Last edited:
Could you clarify why you use `Ref<Effect>` and Filter instead of something like Overlay?
I've honestly never looked into overlays. I pretty much tried to emulate the ForceField filter, and replicated its implementation because that had already proven to work.

Having looked into it just now, it seems that an overlay is essentially a texture being drawn with a specific shader, like the blizzard and sandstorm effects. My shockwave doesn't actually draw a texture, so it seems like an improper format for it.
 
Kazzymodus said:
This code activates the screen shader if it is not already active. If there is already a shockwave in progress, this will prevent it from suddenly moving its source to the new explosion or changing its properties. It will not prevent the new explosion from taking over the propagation (visually, it would reset the shockwave at its original origin), however, so it's up to you to make sure only one object is modifying the filter at any time.

Perhaps I misread this portion of the post, so forgive me - Does this mean that when using this shader, there cannot be any other shaders like it active, even if they originate from a completely separate projectile? In other words, projectile1.cs has this shader code, and it activates the shockwave, and is now in progress - projectile2.cs's shockwave attempts to start, but can't because projectile1.cs is already in progress.

Or, is this on a per projectile basis, meaning that as long as it's not the "same" projectile.cs, there can be multiple instances of this shockwave?

Again, my apologies for the confusion on this point :)
 
Perhaps I misread this portion of the post, so forgive me - Does this mean that when using this shader, there cannot be any other shaders like it active, even if they originate from a completely separate projectile? In other words, projectile1.cs has this shader code, and it activates the shockwave, and is now in progress - projectile2.cs's shockwave attempts to start, but can't because projectile1.cs is already in progress.

Or, is this on a per projectile basis, meaning that as long as it's not the "same" projectile.cs, there can be multiple instances of this shockwave?

Again, my apologies for the confusion on this point :)
They can not, no. A single instance of a filter can only be active for any one source at a time; reactivating it (say, for a new projectile) would simply move the filter to the new location, cutting the old shockwave short. You can, however, make multiple filters using the same shader pass (e.g. Filters.Scene["Shockwave1"], Filters.Scene["Shockwave2"], etc.), but in which case you'd need to keep a very good track of which filters are active and which aren't.

Does that answer your question?
 
They can not, no. A single instance of a filter can only be active for any one source at a time; reactivating it (say, for a new projectile) would simply move the filter to the new location, cutting the old shockwave short. You can, however, make multiple filters using the same shader pass (e.g. Filters.Scene["Shockwave1"], Filters.Scene["Shockwave2"], etc.), but in which case you'd need to keep a very good track of which filters are active and which aren't.

Does that answer your question?

It does, thank you. It sounds as if creating multiple filters, and keeping track of them, is more complicated than I had anticipated, so I think I'll leave that alone for now :)
 
Is there anything specific you'd be interested in? :)

I know that Gore might be a simple thing to accomplish, but there aren't very many tutorials on Gore specifically, and the examplemod doesn't have much, either. That'd be my request. I'm sure others have their own specific ideas in mind :)

Perhaps Gore isn't really tutorial worthy, since it's not exactly complicated? Just a thought ;-)
 
I know that Gore might be a simple thing to accomplish, but there aren't very many tutorials on Gore specifically, and the examplemod doesn't have much, either. That'd be my request. I'm sure others have their own specific ideas in mind :)

Perhaps Gore isn't really tutorial worthy, since it's not exactly complicated? Just a thought ;-)
Gores aren't very complex, but they're very flexible, so it's hard to write a catch-all tutorial for them. It depends primarily on what you're using them for.

Did you have a specific implementation in mind?
 
Did you have a specific implementation in mind?

Actually, yes. My projectiles will stick into the ground, and after a short period of time, explode. When they explode, there should be not only pieces of the projectile itself, but also some gore (blood - similar to the Gore Galore mod). I know how to implement the projectile behavior, so that's not an issue. I just needed a bit of help with the gore itself, and how to set it up so that once the explosion happens, pieces of the projectile and some blood are splattered on the ground. Again, this might be very simple to implement. I've just never dealt with gore before ;-)
 
Could this work, technically for a weapon projectile?
My weapon currently does a star pattern where ever the mouse is on the screen when clicked... Could I technically add the activation code to the onclick code?
Idk how to go about this in all honesty, shaders aren't my thing.
 
Could this work, technically for a weapon projectile?
My weapon currently does a star pattern where ever the mouse is on the screen when clicked... Could I technically add the activation code to the onclick code?
Idk how to go about this in all honesty, shaders aren't my thing.
You can activate it anywhere you want, but you'll have to update it every frame to propagate the wave (i.e. make it spread out). If it's part of a projectile, that'd work, but if it's just an item, you might have to put it in ModPlayer instead.
 
Okay.. so I followed the tutorial and it doesn't work?
I have no errors in the code or while building, but it won't do the visual?
 
Okay.. so I followed the tutorial and it doesn't work?
I have no errors in the code or while building, but it won't do the visual?
I presume the problem there is the update code.

Could you outline (or even better, show) how and where you're updating the filter?
 
UPDATE: I'm very bad with multiplayer stuff, but someone who isn't has pointed out to me that filters are entirely client-side, so attempting to use (or even load) filters server-side will cause issues. Therefore, you need to use Main.netMode != NetmodeID.Server whenever you are working with filters. I've updated my code to reflect this, and anyone currently using it should probably as well.

The shader itself is unaffected by this, so you don't need to update it.
 
Could you show me the code where you update the shockwave? If nothing is happening but you're not experiencing any errors, my first guess is that the issue is there.
I copied your code after many attempts without success, but it still didn't work.
I build it like this
Filters.Scene["Effect"] = new Filter(new ScreenShaderData(new Ref<Effect>(GetEffect("Effects/Effect")), "passName"), EffectPriority.High);
Use it like this
player.ManageSpecialBiomeVisuals("Effect", true, player.Center);
Because I want player to make shockwave.
 
Back
Top Bottom