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.
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:
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
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:
Screen shaders
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:
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
Let's start with some armour shaders. To start off, let's write the absolute smallest working shader possible.
That's quite a lot to unpack. The parameters you already know, so let's look at the middle bit.
What we have here is a single pass. Now, you might recognise that the
As for
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:
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
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.
So two things happened here: one, we added
So, imagine the shader running on this texture. It will start in the top left corner, in which case
What does
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:
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:
Here, we're using our
The line
This member access is what we call a swizzle. We use
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:
Anyway, let's get back to our original pass. Let's pass in pure red and see what happens:
Not bad. Let's look at a slightly more complex example:
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:
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:
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:
Simple, right? You can even combine two gradients into opposite directions:
Alright, so that's horizontally. Let's try it vertically!
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:
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:
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:
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:
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:
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:
Apart from sine waves, there's another, less-useful-but-still-nice way to establish pattern, and that's by using
Okay, one more. What happens if we combine coordinates with time? Cool stuff, that's what:
Becase we're adding
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:
To convert texture coordinates to noise coordinates, you can use this formula:
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:
You can then do all sort of things with it (most of them ugly):
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:
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:
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.
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:
- 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.
- Screen shaders: consists of screen filters, such as the Celestial Invasion and Blood Moon tints.
- Tile shaders: consists of tile paints.
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
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
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();
}
}
Code:
float4 ArmorBasic(float4 sampleColor : COLOR0) : COLOR0
{
return sampleColor;
}
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();
}
}
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;
}
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: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;
}
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;
}
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;
}
colour.rgb *= uColor;
is equal to:
Code:
colour.r *= uColor.r;
colour.g *= uColor.g;
colour.b *= uColor.b;
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;
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:
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:
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;
}
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;
}
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;
}
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:
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;
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;
}
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;
}
Code:
float4 ArmorAnimatedSine(float4 sampleColor : COLOR0, float2 coords : TEXCOORD0) : COLOR0
{
float4 colour = tex2D(uImage0, coords);
colour.rgb *= sin(uTime);
return colour * sampleColor;
}
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.
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;
}
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;
}
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.
}
To convert texture coordinates to noise coordinates, you can use this formula:
Code:
float2 noiseCoords = (coords * uImageSize0 - uSourceRect.xy) / uImageSize1;
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.
}
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;
}
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);
}
}
}
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:
tModLoader - [TUTORIAL] Shockwave effect for tModLoader
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...
forums.terraria.org
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.
- 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. - 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. - 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. - 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.
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: