Skip to content

Getting Started: Drawing a Texture

ThomasMiz edited this page Jun 17, 2022 · 8 revisions

In this tutorial, we'll be learning how to draw textures with TrippyGL using a simple tool: the TextureBatcher.

This tutorial is based on the the previous tutorial on how to open a window, so if you haven't done that or don't know how to open a window, check that out first.

Creating a Texture2D

There are multiple texture types in TrippyGL, such as Texture1D, Texture3D, and TextureCubemap. The TextureBatcher however only works with Texture2D, which is the one we will be focusing on.

The Texture2D class holds a single constructor;

public Texture2D(GraphicsDevice graphicsDevice, uint width, uint height, bool generateMipmaps = false, uint samples = 0, TextureImageFormat imageFormat = TextureImageFormat.Color4b)

So you can easily create a 2-by-2 texture and fill it up with pixel data like so:

Texture2D myTexture = new Texture2D(graphicsDevice, 2, 2);
myTexture.SetData<Color4b>(new Color4b[] { Color4b.Red, Color4b.Green, Color4b.Blue, Color4b.Coral });
// Don't forget to call myTexture.Dispose() once you're done using it!!

However, what if you want to load a Texture2D from an image file on disk? Loading and decoding JPEG or PNG images gets quite complicated, so the TrippyGL.ImageSharp package provides extension methods for setting setting/getting texture data to/from a SixLabors.ImageSharp image.

If we just want to quickly load up an image file into a texture, the following helper method does just that:

Texture2D myTexture = Texture2DExtensions.FromFile(graphicsDevice, "myImage.png");
// You still have to dispose this one when you're done using it!!

Creating a TextureBatcher

This one's easy. There's a single constructor and the only argument it needs is your GraphicsDevice:

TextureBatcher myBatcher = new TextureBatcher(graphicsDevice);

It is important to note that while the TextureBatcher isn't itself a GraphicsResource, it does hold graphics resources which need to be disposed. Therefore, you should dispose your TextureBatcher once you're done using it.

It is also important to understand that the TextureBatcher itself isn't capable of actually drawing things to the screen; it needs to be provided with a ShaderProgram, which contains the vertex shader and fragment shader for this.

If you've already written your own shader programs and want to use a custom one with your TextureBatcher, you can use

TextureBatcher.SetShaderProgram(ShaderProgram program, ShaderUniform textureUniform);

Note: Disposing a TextureBatcher does not dispose the ShaderProgram currently set to it. Since in this tutorial we're keeping things simple, we'll instead use a SimpleShaderProgram which doesn't require us to write any shader code. We can give one of these to our TextureBatcher like so:

TextureBatcher.SetShaderProgram(SimpleShaderProgram simpleProgram);

The TextureBatcher is used in Begin()-End() cycles. To draw textures, you first call TextureBatcher.Begin(). Then you call TextureBatcher.Draw() as many times as you'd like, and finally to end the operation you must call TextureBatcher.End(). This last part also ensures any remaining textures are flushed and everything is drawn. You cannot call SetShaderProgram() in between Begin() and End().

There are multiple overloads of TextureBatcher.Draw(), for anything from a simple "draw at this position without scale nor rotation", to a more complex "draw this subarea of the texture, at this position, with this color, scale, rotation, and specific origin of rotation". Feel free to take your time learning these, or ignore the more complex ones altogether until you need them.

public void Draw(Texture2D texture, Vector2 position, Color4b color, float depth = 0);
public void Draw(Texture2D texture, Vector2 position, Rectangle? source = null, float depth = 0);
public void Draw(Texture2D texture, Vector2 position, Rectangle? source, Color4b color, float depth = 0);
public void Draw(Texture2D texture, Vector2 position, Rectangle? source, Color4b color, float scale, float rotation, Vector2 origin = default, float depth = 0);
public void Draw(Texture2D texture, Vector2 position, Rectangle? source, Color4b color, Vector2 scale, float rotation, Vector2 origin = default, float depth = 0);
public void Draw(Texture2D texture, RectangleF destination, float depth = 0);
public void Draw(Texture2D texture, RectangleF destination, Color4b color, float depth = 0);
public void Draw(Texture2D texture, RectangleF destination, Rectangle? source, Color4b color, float depth = 0);

Creating a ShaderProgram

TrippyGL brings a nicely configurable ShaderProgram that doesn't require you to write any shader code; the SimpleShaderProgram. This one allows you to make basic 2D and 3D shaders (it even supports simple directional or positional 3D lights!).

To create a the SimpleShaderProgram we need, we can easily use the following helper method:

ShaderProgram myProgram = SimpleShaderProgram.Create<VertexColorTexture>(graphicsDevice);
// Don't make me tell you again to dispose your resources when you're done with them, mom gets angry at a dirty room!

This helper method however does not reveal the full abilities of the SimpleShaderProgram. If you're interested in seeing them fully, you can try using a SimpleShaderProgramBuilder. That will allow you to, for example, disable vertex colors or texture sampling.

The SimpleShaderProgram contains a simple vertex shader with the three typical transformation matrices; World, View and Projection. These are initialized to identity matrices for your convenience. If we want to draw our textures in screen-space (so the position at which we draw the texture is measured in screen pixels), then we must change these matrices. Adding the following code to our Window_FramebufferResize method will do the trick:

simpleProgram.Projection = Matrix4x4.CreateOrthographicOffCenter(0, size.X, size.Y, 0, 0, 1);

If you want to implement a 2D camera, you can set the simpleProgram.View matrix to a Matrix4x4.CreateTranslation() matrix!

Setting the graphics states

Here are some graphics states we might want to change:

  • If your texture uses alpha for transparency, you might wanna set graphicsDevice.BlendState = BlendState.NonPremultiplied.
  • You can change your Texture2D's filtering by using texture.SetTextureFilters(). Setting this to "nearest neighbor" will keep the image pixelated even when rotated/scaled. Setting this to "linear" will interpolate the pixels instead.

Result

I'll be using this image from the TextureBatcherTest test project:

ball.png

Your code should look something like this:

using System.Numerics;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
using TrippyGL;
using TrippyGL.ImageSharp;

namespace TrippyExample
{
    class Program
    {
        static IWindow window;
        static GL gl;
        static GraphicsDevice graphicsDevice;

        static Texture2D ballTexture;
        static SimpleShaderProgram simpleProgram;
        static TextureBatcher textureBatcher;

        static void Main(string[] args)
        {
            WindowOptions windowOpts = WindowOptions.Default;
            windowOpts.Title = "My TrippyGL Window!";
            windowOpts.API = new GraphicsAPI(ContextAPI.OpenGL, ContextProfile.Core, ContextFlags.Debug, new APIVersion(3, 3));

            using IWindow myWindow = Window.Create(windowOpts);
            window = myWindow;

            window.Load += Window_Load;
            window.Render += Window_Render;
            window.FramebufferResize += Window_FramebufferResize;
            window.Closing += Window_Closing;

            window.Run();
        }

        private static void Window_Load()
        {
            gl = window.CreateOpenGL();
            graphicsDevice = new GraphicsDevice(gl);

            ballTexture = Texture2DExtensions.FromFile(graphicsDevice, "ball.png");
            textureBatcher = new TextureBatcher(graphicsDevice);
            simpleProgram = SimpleShaderProgram.Create<VertexColorTexture>(graphicsDevice);
            textureBatcher.SetShaderProgram(simpleProgram);

            graphicsDevice.BlendState = BlendState.NonPremultiplied;

            Window_FramebufferResize(window.FramebufferSize);
        }

        private static void Window_Render(double dt)
        {
            float time = (float)window.Time;

            graphicsDevice.ClearColor = Color4b.Gray;
            graphicsDevice.Clear(ClearBuffers.Color);

            textureBatcher.Begin();
            textureBatcher.Draw(ballTexture, new Vector2(50, 50));
            textureBatcher.Draw(ballTexture, new Vector2(150, 50), Color4b.FromHSV(time * 0.3f % 1, 1, 1));
            textureBatcher.Draw(ballTexture, new Vector2(350, 100), null, Color4b.White, 1.6f, time, new Vector2(ballTexture.Width, ballTexture.Height) / 2);
            textureBatcher.End();
        }

        private static void Window_FramebufferResize(Vector2D<int> size)
        {
            graphicsDevice.SetViewport(0, 0, (uint)size.X, (uint)size.Y);
            simpleProgram.Projection = Matrix4x4.CreateOrthographicOffCenter(0, size.X, size.Y, 0, 0, 1);
        }

        private static void Window_Closing()
        {
            simpleProgram.Dispose();
            textureBatcher.Dispose();
            ballTexture.Dispose();

            graphicsDevice.Dispose();
            gl.Dispose();
        }
    }
}

Troubleshooting

  • Problems loading an image? TrippyGL.ImageSharp uses SixLabors.ImageSharp for loading images. However, it requires the images to be stored sequentially in memory, which ImageSharp might not do depending on the library's version and configuration (an exception saying this should happen if this issue were to arise). If you're using a very big image, a smaller one might not show this issue.