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);
}
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);
}
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();
}
}
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: