A Beginner's Guide to Shaders

Kazzymodus

Moderator
Staff member
Moderator
Shaders are extremely powerful and, provided you know what you are doing, lightweight tools to create fancy visuals. However, they are also quite complex, and the implementation barrier is rather high. The purpose of this guide is to help remedy this, and provide an introductory course for writing your very own shaders for Terraria modding.

GZ4aAqa.gif


Introduction

What is a shader?

Very generally speaking, a shader is a piece of code that creates or modifies rendering output. In layman's terms, this basically means "code that makes pretty things". In game development, we primarily use two types of shaders: vertex shaders and pixel shaders (also known as fragment shaders). Vertex shaders are used to modify 3D models, for instance to move or deform them. Pixel shaders are used to modify the actual pixel output of your screen. They often work in tandem with vertex shaders, but can also function perfectly well on their own, as we see in Terraria.

An important thing to understand about shaders is that they're not executed just once, but "for every X". A vertex shader applied to a 3D model will run its code for every vertex (point) of that model. A pixel shader applied to a texture will run for every pixel that that texture is covering (which, yes, can cover the entire screen). This might be confusing at first, but you'll get the hang of it after writing a few passes.

Because Terraria doesn't contain any 3D models (sort of), it doesn't use any vertex shaders. It does use pixel shaders, which can be broadly divided into three groups:
  1. Armour shaders*: primarily consists of player dyes, but also contains some miscellaneous effects such as (part of) the Pillar force field and the water shader.
  2. Screen shaders: consists of screen filters, such as the Celestial Invasion and Blood Moon tints.
  3. Tile shaders: consists of tile paints.
* Internally, these are known as Pixel Shaders, but to avoid confusion with the containing set I refer to these as Armour Shaders instead.

Implementing your own tile shaders is rather complex, and in my personal opinion shouldn't be done to begin with unless you know Terraria's engine (and more importantly, its limitations) inside out, so you'll be limited to making armour shaders and screen shaders. There is a fundamental difference between how these two shaders work, as well as their parameters, but their basic structure remains the same.

Shader structure

Terraria shaders consists of three parts: parameters, passes and techniques. Passes and techniques aren't really used to their full capability, so all you really need to know that for the purposes of writing shaders for Terraria, a pass is a single function that modifies the pixel output, and all passes are contained within a technique. If we take the vanilla dyes as an example, every dye uses a pass to change the appearance of an armour piece, accessory or mount, and all of these passes are contained within a single technique.

A parameter is information passed from outside (i.e. Terraria) into the shader, such as the colour the shader should use. It allows you to make multiple effects from a single pass (all basic colour dyes use the same pass, just with a different colour parameter). When you create your own shader and want to use Terraria's pre-existing shader system (which you should), your shader needs to contain all the parameters the default shaders does: if not, Terraria will attempt to set parameters that don't exist, causing a crash. Armour shaders and screen shaders do not use the same set of parameters, so they will be discussed individually in their respective chapters.

You can find blank examples of both armour and screen shaders attached to the bottom of this post.

Armour shaders

bCz7fFb.gif


Terraria's armour shaders modify how a specific texture is drawn. The most obvious example of this are the various dyes that can be applied to armour pieces, accessories and mounts, however they can also be used to apply entirely shaded visuals to blank textures (think of the Pillar's force field: that is entirely a shader).

The parameters of the armour shader are as follows:

Code:
sampler uImage0 : register(s0); // The texture that you are currently drawing.
sampler uImage1 : register(s1); // A secondary texture that you can use for various purposes. This is usually a noise map.
float3 uColor;
float3 uSecondaryColor;
float uOpacity;
float uSaturation;
float uRotation;
float uTime;
float4 uSourceRect; // The position and size of the currently drawn frame.
float2 uWorldPosition;
float uDirection;
float3 uLightSource; // Used for reflective dyes.
float2 uImageSize0;
float2 uImageSize1;

Screen shaders

pjpwfxO.png


Terraria's screen shaders modify the contents of the entire screen rather than a specific texture. The most common application of screen shaders is to give the screen a certain tint, but they can be used for other effects as well, such as distortion effects, overlays or simply turning the screen upside down, if for whatever reason you'd want to do that. Screen shaders should not be confused with custom skies, such as the meteors that fall in the background at a Solar Pillar: those have nothing to do with shaders, and are not covered in this thread.

Screen shaders are significantly more complex than pixel shaders, so unless you're trying to do something very simple like tinting the screen, I advise you to start with armour shaders first. This guide doesn't contain any examples of screen shaders because most of the information about armour shaders carries over. If you do end up with some screen shader specific questions, feel free to ask them in this thread.

The parameters of the screen shader are as follows:
Code:
sampler uImage0 : register(s0); // The contents of the screen.
sampler uImage1 : register(s1); // Up to three extra textures you can use for various purposes (for instance as an overlay).
sampler uImage2 : register(s2);
sampler uImage3 : register(s3);
float3 uColor;
float3 uSecondaryColor;
float2 uScreenResolution;
float2 uScreenPosition; // The position of the camera.
float2 uTargetPosition; // The "target" of the shader, what this actually means tends to vary per shader.
float2 uDirection;
float uOpacity;
float uTime;
float uIntensity;
float uProgress;
float2 uImageSize1;
float2 uImageSize2;
float2 uImageSize3;
float2 uImageOffset;
float uSaturation;
float4 uSourceRect; // Doesn't seem to be used, but included for parity.
float2 uZoom;

How to write a shader

Now this is all fine and dandy, but how do you actually go about writing a shader? In this chapter, I'll show you some examples of shaders and what they do.

Basic shader

To start of with, a shader file in XNA (the framework Terraria uses) has the extension .fx. Apart from that, it works like any other text file, so you don't need any special software to start writing shaders.

Let's start with some armour shaders. To start off, let's write the absolute smallest working shader possible.
Code:
sampler uImage0 : register(s0);
sampler uImage1 : register(s1);
float3 uColor;
float3 uSecondaryColor;
float uOpacity;
float uSaturation;
float uRotation;
float uTime;
float4 uSourceRect;
float2 uWorldPosition;
float uDirection;
float3 uLightSource;
float2 uImageSize0;
float2 uImageSize1;

float4 ArmorBasic(float4 sampleColor : COLOR0) : COLOR0
{
    return sampleColor;
}

technique Technique1
{
    pass ArmorBasic
    {
        PixelShader = compile ps_2_0 ArmorBasic();
    }
}
That's quite a lot to unpack. The parameters you already know, so let's look at the middle bit.
Code:
float4 ArmorBasic(float4 sampleColor : COLOR0) : COLOR0
{
    return sampleColor;
}
What we have here is a single pass. Now, you might recognise that the float4 in front of ArmorMyShader means that that is the return type. The : COLOR0 indicated where this return type should be stored, namely in the COLOR0 register of the GPU. Put two and two together, and what this means is that this pass writes a colour value consisting of four floats (red, green, blue and alpha) to the GPU.

As for float4 sampleColor : COLOR0), this means you're declaring a parameter equal to what's in that COLOR0 register. So what this pass does is take the value of COLOR0 and put it in COLOR0. Not very useful, as you might imagine.

We'll move on to something more useful in a bit, but let's first take a look at that last part of the shader:
Code:
technique Technique1
{
    pass ArmorBasic
    {
        PixelShader = compile ps_2_0 ArmorBasic();
    }
}
All this really does is compile the technique, by in turn compiling all the the passes we want it to contain. Every time you create a pass, you'll have to add it to the technique if you want to use it in Terraria. Also, make sure you use ps_2_0, or you'll get compatibility problems.

I'll leave out the parameters and techniques from now on to keep the code snippets short, but obviously you still need those if you're going to compile.

Okay, so let's make something more useful. Right now, we're writing the contents of a register to that same register, which is entirely useless. What we want to do for starters is to just draw the texture. To do so, we have to make some adjustments.
Code:
float4 ArmorBasic(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    return colour;
}
So two things happened here: one, we added float2 coords : TEXCOORD0 as a parameter, and two, we added something called tex2D. To understand either of these, you first have to remember what I mentioned in the introduction: a pixel shader is executed for every pixel that the texture covers (not every pixel of the texture, but if you're drawing sprites at a scale of one that basically amounts to the same thing). Now, you can probably tell that coords takes the value of the TEXCOORD0 register just like sampleColor did, but what does the value actually mean? Basically, it represents the coordinates of the current pixel relative to the texture. This might sound complex, but hopefully this image clears it up:

A6Chqtu.png


So, imagine the shader running on this texture. It will start in the top left corner, in which case coords will be equal to (0, 0). Halfway through the first row, it'll be (0.5, 0). In the dead center of the texture, it will be (0.5, 0.5), and at the bottom right corner it'll be (1, 1).

What does tex2D do then? Simply put, it samples the colour data of uImage0 at position coords. And because the shader runs for every pixel, and coords updates accordingly, essentially it's tracing the texture pixel by pixel. So what this pass does is nothing short of simply drawing your texture.

There's a problem, however, in that you are overwriting the contents of the colour register. This can sometimes be useful when it is intentional (screen shaders generally do this), but it can also cause issues, especially if you're overwriting transparency. So usually, you want to multiply whatever your result is with the content of the register before writing to it:
Code:
float4 ArmorBasic(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    return colour * sampleColor;
}
Sampling will form the basis of almost all of your passes, but doesn't do much by itself. Let's introduce some parameters.

Parameters & Swizzles

Now we can draw our sprites, but that's not really any different from what the game already does: we want a custom effect. Say, for instance, we want to tint our sprite. If we didn't know any better, we might try to do this:
Code:
float4 ArmorTint(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    colour *= uColor;
    return colour * sampleColor;
}
Here, we're using our uColor parameter from our parameter list, but there's a problem: colour is a float4, but uColor is a float3, and you can't multiply those with each other. What we want to is multiply the red, green and blue channels of our texture with the value of the parameter, so instead we do this:
Code:
float4 ArmorTint(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    colour.rgb *= uColor;
    return colour * sampleColor;
}
The line colour.rgb *= uColor; is equal to:
Code:
colour.r *= uColor.r;
colour.g *= uColor.g;
colour.b *= uColor.b;
This member access is what we call a swizzle. We use rgba to refer to colour data, but sometimes you're working with positions, in which case xyzw makes more sense. Therefore, you could also do this:
Code:
colour.xyz *= uColor;
This is exactly the same as the previous line, it's just syntactic sugar. You can even mix them up in the same line. What you can not do, however, is mix them up in the same swizzle:
Code:
colour1.rgb = colour2.xyz; // This is fine.
colour1.xgb = colour2.xyz; // This is not.

Anyway, let's get back to our original pass. Let's pass in pure red and see what happens:

2IhgYzX.png


Not bad. Let's look at a slightly more complex example:
Code:
float4 ArmorLuminosity(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    float luminosity = (colour.r + colour.g + colour.b) / 3;
    colour.rgb = luminosity * uColor;
    return colour * sampleColor;
}

What we do here is calculate the luminosity (brightness) of each pixel, and then multiply our colour with it. This prevents large dark patches where your tint complements the colour of the texture.

The result:

Pj1ozO8.png


Getting creative with coordinates

While we can already can do rather nice stuff, it's all rather uniform. To add some variety, we can work with our coordinates to get something nice. Let's make a simple gradient:
Code:
float4 ArmorGradient(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    colour.rgb *= coords.x;
    return colour * sampleColor;
}
If you remember the image above that explained how coordinates work, you might be able to guess what happens here: the leftmost pixels will be black, and will brighten to their original colour. I had to slightly enhance the result for clarity, but it would look something like this:

hSNMYTl.png


Simple, right? You can even combine two gradients into opposite directions:
Code:
float4 ArmorColourGradient(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    float luminosity = (colour.r + colour.g + colour.b) / 3;
    colour.rgb *= ((coords.x * uColor) + ((1 - coords.x) * uSecondaryColor)) * luminosity;
    return colour * sampleColor;
}
w3PTOr3.png


Alright, so that's horizontally. Let's try it vertically!
Code:
float4 ArmorVerticalGradient(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    colour.rgb *= coords.y;
    return colour * sampleColor;
}
yIMcvQB.png


Hm, that doesn't look right. If you look very closely, you might be able to see that the bottom of the robe is getting slightly brighter, but surely it should be fully bright at that point? What's going on?

The problem here is that coordinates are relative to the texture you are drawing, not the frame you are drawing. As you might know, every armour set in Terraria is twenty frames long, so the y-coordinate at the end of our first frame is only 0.05, rather than 1. This is especially evident once you look at a walk cycle:

pZuvvF8.gif


To solve this, we want the y-coordinate to be 0 at the beginning of our current frame and to be 1 at the end. Fortunately, there's a (relatively) simple formula for that:
Code:
float frameY = (coords.y * uImageSize0.y - uSourceRect.y) / uSourceRect.w;
You can work out exactly what this does in your own time, but it "shrinks" the coordinates to encompass just a single frame, and then shifts the origin to the active frame. Now, let's try this again:
Code:
float4 ArmorVerticalGradient(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    float frameY = (coords.y * uImageSize0.y - uSourceRect.y) / uSourceRect.w;
    colour.rgb *= frameY;
    return colour * sampleColor;
}

TD8CvHI.png


Yep, that's much better!

Passing time

(That might be the absolute worst pun I've ever made)

Now, there's a good chance you're here not for the boring gradient stuff, but because you want to make animated shaders, and you'd be right too! Actually, making an animated shader is deceptively easy: all you have to do is use the uTime parameter. However, the main problem is using it correctly.

Say I were to do this:
Code:
float4 ArmorAnimated(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    colour.rgb *= uTime;
    return colour * sampleColor;
}
Now, this would just increase the brightness until infinity (well, until one hour has passed, at which point it would reset, but that's still a long wait), and that's not really helpful. Instead, most dyes use a sine function to turn raw time into a sine wave. This will give you a nice and predictable scalar to use:
Code:
float4 ArmorAnimatedSine(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    colour.rgb *= sin(uTime);
    return colour * sampleColor;
}
xQAqvm1.gif

As you might know, a sine wave... well, waves... between 1 and -1. In this particular case, that negative range isn't really useful, as negative colours just display as black. If we wanted to remove that negative range and just bounce between 0 and 1, we could do this:
Code:
float time = (sin(uTime) + 1) / 2;
float altTime = sin(uTime) * 0.5f + 0.5f; // Same thing.
Apart from sine waves, there's another, less-useful-but-still-nice way to establish pattern, and that's by using frac, which gets the fractional component of a number. When used with uTime, you get a nice sawtooth pattern, ranging from 0 to 1 exclusive.
Code:
float4 ArmorAnimatedFrac(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    colour.rgb *= frac(uTime);
    return colour * sampleColor;
}

djj1vSI.gif


Okay, one more. What happens if we combine coordinates with time? Cool stuff, that's what:
Code:
float4 ArmorRadar(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(uImage0, coords);
    float wave = 1 - frac(coords.x + uTime);
    color.rgb = color.rgb * wave;
    return color * sampleColor;
}
XvnaDFY.gif


Becase we're adding uTime to coords.x, our 'split point' (where the results of frac jumps from ~0.99 back to 0) keeps shifting to the left, giving us a nice radar-like effect. Want to speed up or slow down the shift? Simply multiply uTime by a scalar:
Code:
float wave = 1 - frac(coords.x + uTime * 0.5f); // This would be half as fast.
float wave = 1 - frac(coords.x + uTime * 2); // This would be twice as fast. Or half as slow. Whatever takes your fancy.


Making some noise

Finally, we'll take a look at how we can use noise to pep up our dyes a bit. Most dyes have a rather geometric feel to them, and sometimes that can be very nice. However, if you want to add some randomness (perceived or otherwise), you can use a noise map. Several dyes already do this: the stars in Twilight Dye, the sand in Shifting Sands Dye and the... eh, vortex... in Vortex Dye are all created through use of a noise map.

The applications of noise are very diverse, and I won't touch upon them all. However, one of the more common applications is to use it as an overlay. For that to work, we do have to do quite a bit of work, though.

The first problem you have to solve is the different pixel ratios: because coordinates range from 0 to 1 regardless of how large an image is, using one set of coordinates for both images will not cover the same amount of pixels if the images aren't the same size, which results in ugly stretching:
Code:
float4 ArmorNoise(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(uImage0, coords);
    float4 noise = tex2D(uImage1, coords); // Same coords
    color.rgb = noise.rgb;
    return color * sampleColor * color.a; // Multiplying by color.a to mask the invisible pixels.
}
Kuw015o.png

To convert texture coordinates to noise coordinates, you can use this formula:
Code:
float2 noiseCoords = (coords * uImageSize0 - uSourceRect.xy) / uImageSize1;
This gets the Cartesian coordinates of the pixel you want and then determines the coordinates that will get you that pixel. In practice, that would look like this:
Code:
float4 ArmorNoise(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(uImage0, coords);
    float2 noiseCoords = (coords * uImageSize0 - uSourceRect.xy) / uImageSize1;
    float4 noise = tex2D(uImage1, noiseCoords);
    color.rgb = noise.rgb;
    return color * sampleColor * color.a; // Multiplying by color.a to mask the invisible pixels.
}
CjpZhBE.png

You can then do all sort of things with it (most of them ugly):
Code:
float4 ArmorNoise(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(uImage0, coords);
    float2 noiseCoords = (coords * uImageSize0 - uSourceRect.xy) / uImageSize1;
    float4 noise = tex2D(uImage1, noiseCoords);
    float luminosity = (color.r + color.g + color.b) / 3;
    color.rgb = luminosity * noise.b * 2;
    return color * sampleColor;
}
WP9ld0c.png

If you simple want to sample the noise map for a pseudo-random scalar, you can dispense with all this: just sample with the normal coordinates, and it should work just as well.

These are pretty much all the building blocks you need to make your own shaders. While there are some more neat tricks you can pull off, this should be sufficient to get you going.

Compiling your shader

Once you have finished writing a shader, you'll need to compile it before you can use it. There are two ways to do this: the most straightforward is to use fxcompiler (Reach version), an application that can compile .fx files into .xnb without having to build them with a XNA application, which is the second way.

Using fxcompiler is very simple: just put the files you want to compile into the same folder as fxcompiler.exe, run it, and you're done. Move the .xnb files to your mod folder (YourModName/Effects is a good place to put them) and you're done. If you build them with a XNA application, they will end up in your bin folder.

Using your shader

How to use your shader largely depends on what kind of shader it is, but first and foremost you'll have to load them first. You do this in the Load function of your Mod:
C#:
using Microsoft.Xna.Framework.Graphics;
using Terraria.Graphics.Effects;
using Terraria.Graphics.Shaders;
using Terraria.ID;
using static Terraria.ModLoader.ModContent;

public class MyMod : Mod
{
    // stuff...

    public override void Load()
    {

        // All of this loading needs to be client-side.
  
        if (Main.netMode != NetmodeID.Server)
        {
            // First, you load in your shader file.
            // You'll have to do this regardless of what kind of shader it is,
            // and you'll have to do it for every shader file.
            // This example assumes you have both armour and screen shaders.

            Ref<Effect> dyeRef = new Ref<Effect>(GetEffect("Effects/MyDyes"));
            Ref<Effect> specialRef = new Ref<Effect>(GetEffect("Effects/MySpecials"));
            Ref<Effect> filterRef = new Ref<Effect>(GetEffect("Effects/MyFilters"));
  
            // To add a dye, simply add this for every dye you want to add.
            // "PassName" should correspond to the name of your pass within the *technique*,
            // so if you get an error here, make sure you've spelled it right across your effect file.

            GameShaders.Armor.BindShader(ItemType<MyDyeItem>(), new ArmorShaderData(dyeRef, "PassName"));

            // If your dye takes specific parameters such as colour, you can append them after binding the shader.
            // IntelliSense should be able to help you out here.  

            GameShaders.Armor.BindShader(ItemType<MyColourDyeItem>(), new ArmorShaderData(dyeRef, "ColourPass")).UseColor(1.5f, 0.15f, 0f);
            GameShaders.Armor.BindShader(ItemType<MyNoiseDyeItem>(), new ArmorShaderData(dyeRef, "NoisePass")).UseImage("Images/Misc/noise"); // Uses the default Terraria noise map.

            // To bind a miscellaneous, non-filter effect, use this.
            // If you're actually using this, you probably already know what you're doing anyway.

            GameShaders.Misc["EffectName"] = new MiscShaderData(specialref, "PassName");

            // To bind a screen shader, use this.
            // EffectPriority should be set to whatever you think is reasonable.  

            Filters.Scene["FilterName"] = new Filter(new ScreenShaderData(filterRef, "PassName"), EffectPriority.Medium);
        }
    }
}
For dyes, this is entirely sufficient: equipping your dye item will show the dye. Simple, right? For filters, you'll have to activate them when appropriate. Activating (and deactivating) a filter is very simple:
C#:
if (Main.netMode != NetmodeID.Server) // This all needs to happen client-side!
{
    Filters.Scene.Activate("FilterName");

    // Updating a filter
    Filters.Scene["FilterName"].GetShader().UseProgress(progress);

    Filters.Scene["FilterName"].Deactivate();
}

For an example of how you can put filters to use, check out my other guide for a shockwave filter:

Tips & Tricks

I've already given most of my trade secrets away by now, but there are a few tidbits left that might be able to help you if you get stuck.

  1. Mask your sprite when necessary
    Sometimes, your shader will fill up the transparent parts of your sprite as well. You can generally remedy this by multiplying your colour by the alpha of your sample (unless you've overwritten it, which you shouldn't really be doing do). See the examples with noise maps above for an... well... example.
  2. Be efficient
    Shaders are executed many times per frame. Make sure they're as fast as possible, or at the very least not any slower than necessary. Personally, I think that extends to all code, but I seem to be in the minority on that front.
  3. Be mindful with flashing effects
    Certain people who suffer from epilepsy can get an epileptic insult from flashing effects. Be mindful of this, and if your mod does end up containing content that may trigger such insults, give due warning.
  4. Experiment!
    Many of my dyes are the result of sheer mucking about. Don't be afraid to push the envelope (without blowing up your GPU, that is), you might be pleasantly suprised. ;)
That's all I have to say. If you have any questions about anything in this thread, feel free to ask them here!

Code:
sampler uImage0 : register(s0);
sampler uImage1 : register(s1);
float3 uColor;
float3 uSecondaryColor;
float uOpacity;
float uSaturation;
float uRotation;
float uTime;
float4 uSourceRect;
float2 uWorldPosition;
float uDirection;
float3 uLightSource;
float2 uImageSize0;
float2 uImageSize1;

float4 ArmorMyShader(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
    float4 color = tex2D(uImage0, coords);
    return color * sampleColor;
}

technique Technique1
{
    pass ArmorMyShader
    {
        PixelShader = compile ps_2_0 ArmorMyShader();
    }
}
Code:
sampler uImage0 : register(s0);
sampler uImage1 : register(s1);
sampler uImage2 : register(s2);
sampler uImage3 : register(s3);
float3 uColor;
float3 uSecondaryColor;
float2 uScreenResolution;
float2 uScreenPosition;
float2 uTargetPosition;
float2 uDirection;
float uOpacity;
float uTime;
float uIntensity;
float uProgress;
float2 uImageSize1;
float2 uImageSize2;
float2 uImageSize3;
float2 uImageOffset;
float uSaturation;
float4 uSourceRect;
float2 uZoom;

float4 FilterMyShader(float2 coords : TEXCOORD0) : COLOR0
{
    float4 colour = tex2D(uImage0, coords);
    return colour;
}

technique Technique1
{
    pass FilterMyShader
    {
        PixelShader = compile ps_2_0 FilterMyShader();
    }
}
 
Last edited:
So does this mean we're getting weapon dyes???

Joke aside, this is pretty great information - I've not seen any guides about shaders in Terraria yet. Do you think it would be a good idea to commit some exapmple shaders to the tModloader ExampleMod too so people can see about them there?
 
Instead of an armor dye, say I wanted to apply a shader to an NPC, corresponding to a specific debuff. If I've already got my .xnb file made, how would the loading and activating process change?
 
Instead of an armor dye, say I wanted to apply a shader to an NPC, corresponding to a specific debuff. If I've already got my .xnb file made, how would the loading and activating process change?
First of all, you need to bind the shader in your Load function. Since you are intending to use it on NPCs, you should bind it to MiscShaders:
C#:
if (Main.netMode != NetmodeID.Server)
{
    Ref<Effect> shaderRef = new Ref<Effect>(GetEffect("Effects/YourShader"));
    GameShaders.Misc["EffectName"] = new MiscShaderData(shaderRef, "PassName");
}
Then, to activate it, you'd need to override PreDraw and PostDraw on a GlobalNPC:
C#:
public MyGlobal : GlobalNPC
{
    private bool resetBatchInPost;
   
    public override bool PreDraw(NPC npc, SpriteBatch spriteBatch)
    {
        if (npc.HasYourBuff && Main.netMode != NetmodeID.Server) // The netmode check might be redundant but I can't verify whether or not it is.
        {
            resetBatchInPost = true; // We're using a dedicated bool for this in the *very* unlikely case your buff somehow gets purged during drawing.
           
            spriteBatch.End();
            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone, null, Main.GameViewMatrix.ZoomMatrix); // SpriteSortMode needs to be set to Immediate for shaders to work.
           
            GameShader.Misc["EffectName"].Apply(); // If you need to set any parameters, you can so before Apply (e.g. Misc["EffectName'].UseColor(something).Apply() )
        }
       
        return true;
    }
   
    public override void PostDraw(NPC npc, SpriteBatch spriteBatch, Color drawColor)
    {
        if (resetBatchInPost)
        {
            spriteBatch.End();
            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone, null, Main.GameViewMatrix.ZoomMatrix);
            resetBatchInPost = false;
        }
    }
}
This is very basic, so might not work for your specific situation. If you run into any issues, please let me know. ;)
 
Maybe I'm missing something obvious, but I'm a bit confused on how to pass in a solid color (like the examples in this guide where he made the armor red). I'm new to shaders so apologies if this seems simple.
 
Maybe I'm missing something obvious, but I'm a bit confused on how to pass in a solid color (like the examples in this guide where he made the armor red). I'm new to shaders so apologies if this seems simple.
Not at all! We were all beginners once, so it's only normal you run into issues. ;)

For armour shaders, you pass in a colour at the loading stage, so in Mod.Load. You can do that immediately after binding a shader pass to a dye item:
C#:
// See the MyMod.cs spoiler example in "Using your shader" for a more complete example.
GameShaders.Armor.BindShader(ItemType<MyColourDyeItem>(), new ArmorShaderData(dyeRef, "ColourPass")).UseColor(1f, 0f, 0f); // Pure red
For screen shaders, you can update the colour whenever you like (both when you activate it or at any point afterwards):
C#:
Filters.Scene.Activate("MyFilter", projectile.Center).GetShader().UseColor(0f, 1f, 0f); // Pure green on activation.

// ...

Filters.Scene["MyFilter"].GetShader().UseColor(0f, 0f, 1f); // Pure blue on update

This will set the uColor parameter in your shader, which you can then use to your liking.
 
Not at all! We were all beginners once, so it's only normal you run into issues. ;)

For armour shaders, you pass in a colour at the loading stage, so in Mod.Load. You can do that immediately after binding a shader pass to a dye item:
C#:
// See the MyMod.cs spoiler example in "Using your shader" for a more complete example.
GameShaders.Armor.BindShader(ItemType<MyColourDyeItem>(), new ArmorShaderData(dyeRef, "ColourPass")).UseColor(1f, 0f, 0f); // Pure red
For screen shaders, you can update the colour whenever you like (both when you activate it or at any point afterwards):
C#:
Filters.Scene.Activate("MyFilter", projectile.Center).GetShader().UseColor(0f, 1f, 0f); // Pure green on activation.

// ...

Filters.Scene["MyFilter"].GetShader().UseColor(0f, 0f, 1f); // Pure blue on update

This will set the uColor parameter in your shader, which you can then use to your liking.
Thank you very much, this worked out. Appreciate it! :happy:
 
I very much appreciate your help. I've been playing around with armor shaders all night now! :D

I just have one quick question, how would i go about checking a specific pixel's RGB or RGBA value?
 
I am new to Terraria modding and experimented with shaders all night, the dyes work great but i encountered a problem i cant seem to solve. I wanted to make a pair of wings wich per default have my custom shader, but i cant work out how to do. I have tried it with DrawEffect() in my modplayer, using drawInfo.wingShader but coded myself into countles dead ends.
 
Here's another Shader guide for tModLoader (in Chinese), with examples of greyscale, scale, gaussian blur, rotation
You could try machine translation or just copying the code blocks and see what'll happen.
Understanding some of them requires math-related knowledge like matrix operation
 
Do you know how to add a shader onto a projectile? I'm pretty new to shaders and coding and modding (been modding for a month) so id greatly appreciate if you could help me.
 
Do you know how to add a shader onto a projectile? I'm pretty new to shaders and coding and modding (been modding for a month) so id greatly appreciate if you could help me.
You will need decent knowledge in Drawing functions (IE PreDraw/PostDraw for example) and handling of spritebatches before you can consider that.
 
How exactly would I go about making a screen shader? I know you said its just conversions of what you wrote in the thread, but I don't exactly know what kind of conversions to make...
 
how would i go about making a circle around something with shaders? if its possible? im trying to recreate the clicker class mod but i dont know how to draw the circle around the player (without making a projectile that is the circle) is it possible?
 
How would I make a screenshader tint the screen a certain color when a boss is spawned, and then set it back to normal when the boss dies?
 
Been stressing out trying to get a sobel filter to work, I'm believe the code works but applying it in terraria seems to be a nightmare.

If I do it as a filter it actually does something, practically nothing because the screen goes black but its at least something...
So I thought i'd change it to a misc shader and see if just the view and projection would outline a specific sprite but i'm not even getting results, no errors.

Need it as a shader because it will eventually have some time based noise function to move/change color/fade any part of the outline. Send help
 
Been stressing out trying to get a sobel filter to work, I'm believe the code works but applying it in terraria seems to be a nightmare.

If I do it as a filter it actually does something, practically nothing because the screen goes black but its at least something...
So I thought i'd change it to a misc shader and see if just the view and projection would outline a specific sprite but i'm not even getting results, no errors.

Need it as a shader because it will eventually have some time based noise function to move/change color/fade any part of the outline. Send help
I'm not really good enough yet this, but it's possible to directly pass shaders without going through Terraria first
Take a look at Spirit's Repo on Github, it has a decent example of how to use a shader with custom params on primitive drawing

I would also suggest Starlight River too, but I don't think Scalie wants me sharing that one around, you can find links thou in TML's discord if you wanna dig, but I'm not posting a direct one on the forums
 
Back
Top Bottom