Just wanted to share this as I’m kinda chuffed about it. Obviously, yes, I know, please don’t tell me, a shader is better for this. But nontheless here’s my monogame plasma effect based on this old tutorial:
https://lodev.org/cgtutor/plasma.html
You can import colors from a texture or use HSV to create a palette. I made my palette in photoshop using the gradient tool on a 256 x 1px image. If you create an instance and pass in a texture, the colors of that texture will be used to color the plasma, otherwise it will use straight max saturation rainbow colors. The for loop to create the palette in this way can be manipulated to restrain the colors used.
I’ve also included a HSV to RGB color space function. I’m not totally sure it’s correct however.
The plasma pattern is generated once in the constructor and then animated with palette cycling. You can also regenerate the Sin functions with time as a factor every frame for more impressive animation, but I’m not doing that here. Performance tips to this end would be appreciated (lookup tables??)
You could get very creative here, maybe adding the alpha channel to the mix. In the version I’m using in my game I’m masking the final texture as well by passing in an extra texture, but I’m not totally happy with my implementation yet as it has to re-mask every frame.
Also, there’s only a Draw function. You can break out the work from the Draw function into an update function, but then you have to worry about timing a bit more, so it may or may not make sense to do so.
This is the first time I’ve ever tried something like this, so any tips are appreciated.
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using static System.MathF;
namespace Graphix {
public class Plasma
{
public readonly int width, height;
Texture2D texture;
int[] plasma;
Color[] palette, buffer;
public Plasma(int width, int height, GraphicsDevice graphicsDevice, Texture2D paletteImport = null) {
this.width = width;
this.height = height;
//create the texture to write to each frame. Using the Color struct makes our lives easy.
texture = new(graphicsDevice, width, height, false, SurfaceFormat.Color);
if (paletteImport != null) {
palette = new Color[paletteImport.Width * paletteImport.Height];
paletteImport.GetData(palette);
} else {
//Rainbow palette. Try limiting the values and doing different stuff here.
palette = new Color[360];
for (int i = 0; i < palette.Length; i++) {
palette[i] = HSVtoRGB((i) % 360, 1, 1);
}
}
plasma = new int[width * height];
buffer = new Color[width * height];
float gray = 128.0f; // h
//initial plasma pattern generated here, once, by adding Sin functions.
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int value = (int)(
gray + (gray * Sin(x / 30.0f))
+ gray + (gray * Sin(y / 50.0f))
+ gray + (gray * Sin((Sqrt(((x - width / 2.0f) * (x - width / 2.0f) + (y - height / 2.0f) * (y - height / 2.0f))) / 20.37f)))
+ gray + (gray * Sin((Sqrt(x * x + y * y) / 30.4f)))
) / 8;
plasma[(y * width) + x] = value;
}
}
texture.SetData(plasma);
}
public void Draw(SpriteBatch spriteBatch, GameTime gameTime, Rectangle destination, Rectangle source) {
//30fps. This can be optimized so the for loop only computes when there's a new t value.
int t = (int)(gameTime.TotalGameTime.TotalSeconds * 30.0f);
//animation created through palette cycling
for (int i = 0; i < width * height; i++) {
buffer[i] = palette[(plasma[i] + t) % palette.Length];
}
texture.SetData(buffer);
spriteBatch.Draw(texture, destination, source, Color.White);
}
/// <summary>
/// Takes a colour in HSV colour space and returns it in RGB space as an XNA Color object.
/// </summary>
/// <param name="H">'Hue' between 0 and 360.</param>
/// <param name="S">'Saturation' between 0 and 1</param>
/// <param name="V">'Value' between 0 and 1</param>
/// <returns></returns>
public static Color HSVtoRGB(int H, double S, double V) {
double C = V * S;
double X = C * (1 - Math.Abs((H / 60.0) % 2 - 1));
double m = V - C;
int r, g, b;
if (H < 60) {
r = (int)((C + m) * 255);
g = (int)((X + m) * 255);
b = (int)(m * 255);
} else if (H < 120) {
r = (int)((X + m) * 255);
g = (int)((C + m) * 255);
b = (int)(m * 255);
} else if (H < 180) {
r = (int)(m * 255);
g = (int)((C + m) * 255);
b = (int)((X + m) * 255);
} else if (H < 240) {
r = (int)(m * 255);
g = (int)((X + m) * 255);
b = (int)((C + m) * 255);
} else if (H < 300) {
r = (int)((X + m) * 255);
g = (int)(m * 255);
b = (int)((C + m) * 255);
} else {
r = (int)((C + m) * 255);
g = (int)(m * 255);
b = (int)((X + m) * 255);
}
return new Color(r, g, b);
}
} }