Render Texture > SetPixels

There are a lot of reasons you might want to draw on a texture. Maybe you want your players to literally be able to draw on something. Maybe you want to add decals to an image. Maybe you just want to procedurally create simple images like circles and not have them take up a bunch of space in your app download.

If so, your first mistake will likely be to use SetPixel to draw on pixels. This will work, but it's agonizingly slow if you're doing more than one pixel. So you do a little reading, learn that GetPixels/SetPixels is faster than setting them one at a time, and dust off your hands and call it a job well done. Right?

Nope. There's a problem with SetPixels, and it's a pretty fundamental one: It's CPU-bound. Modern GPUs are orders of magnitude more powerful than the general-purpose processors. A modern Core i7 will probably have around 70 GigaFLOPS (floating point operations per second); a modern GPU like the GTX 1080 will have around 10-12 teraFLOPS. Now these values aren't directly comparable for a number of reasons, including the greater versatility of CPUs and the massive parallelization of GPUs, but without a doubt the graphics cards have a lot of potential that is being untapped by using software drawing methods SetPixels. Even if your users aren't running the latest top-of-the-line GPU (or are on a mobile device), their graphics hardware will still be better at drawing stuff simply because that's what it's designed for. The bottom line is that any processing (especially graphics-related) than can be moved to the GPU, probably should be.

So how do you offload, for example, drawing of circles into textures to the GPU? I can see fear in the eyes of some of you, worrying that I'm about to give you a lesson in shader programming. And while that's certainly a good way to draw stuff onto textures, it's a whole new field of expertise, and one I'm most certainly not qualified to write an article about. Instead, I'm going to teach you a disgustingly simple strategy which takes full advantage of Unity's simplicity and your experience with its API.

Enter the Render Texture

If you're not familiar with Render Textures, you're missing out on one of Unity's most powerful visual tools. In fact, until Unity 5 (and the updated subscription pricing model), the Render Texture was one of maybe three major features that separated the free version from Unity Pro - and many, many people forked over thousands of dollars just to be able to use Render Textures. Fortunately, these days everyone gets render textures for free, and their ubiquity now only makes them more useful, because it allows articles like this one to exist.

Put simply, a Render Texture is a texture onto which a camera can render an image. It's pretty straightforward, right? This most obvious example of its usage is something like a security camera displaying its image on a screen in-game, or rendering an image of an item into a window to be displayed in the GUI. These are fine uses of Render Textures, but they are capable of so much more if used cleverly. I'm going to describe one such usage here.

The Camera Setup

The in-scene setup of a system like this is crucial to its success. Here are the basics:

  1. Create a new layer that will be used explicitly for this camera. Ensure that all other cameras in the scene have this layer removed from their culling masks.
  2. Create a new Camera. Let's call this "Drawing Camera" for reference.
  3. Place it at the location (0.5, 0.5, -1) in world space. Set its render mode to Orthographic, and its Orthographic Size to 0.5. This will ensure that its render area covers the space from (0,0) to (1,1) in the world, and will make placement of objects to be rendered much simpler.
  4. Set the drawing camera's culling mask to include only the layer created in the first step.
  5. Create a Render Texture in your Project pane. It can go anywhere you like. It's possible to create them from script if desired, but for this, I'll use one from the project folder.
  6. Assign this render texture to the "Target Texture" slot of the drawing camera, and then assign it as the texture onto which you're hoping to draw things.

That's pretty much it. We now have a camera setup that can render stuff onto your object, and we haven't even touched the code yet. So how do we use this to draw?

The Background Setup

Our next step will be to set up the image that will be used as our backdrop. This is actually very simple:

  1. Place the desired image in the scene. You can use either a Sprite or a Quad with its own material for this; in the case of the latter, I recommend using a material with an unlit shader, as lighting affecting the render texture would be odd.
  2. Place this object at (0.5, 0.5, 10) and scale it such that its bounds go from 0 to 1. It should match your camera's rendering box precisely.
  3. Set its layer to the layer we created in the first section.

And you're done. If you have used the old texture you had here before as your background, you should now be able to play your game, and it'll look the same as it did before. (It'll be a smidgeon slower because it's now rendering that texture each frame, but we'll clean that up at the end.)

If you don't have a texture, you can just create a Quad with an unlit material that's the color of the background you want.

Example 1: Drawing a circle with a LineRenderer

Now that we have our background, what can we do with it? Well, let's create a new GameObject and add a LineRenderer and a new script to it - let's call it CircleDrawer. In CircleDrawer, we're going to set the LineRenderer's points to be in a circle, using Mathf.Sin and Cos in a loop. The circle will be drawn based on parameters that are chosen in the inspector. For ease of editing, we're also going to add a OnValidate function - which is called every time a value is changed in the inspector. With this, we'll be able to change our circle in realtime in the editor.

public Vector2 center = new Vector2(0.5f, 0.5f);
public float radius = 0.45f;
public float thickness = 0.1f;
public int pointCount = 32;

private LineRenderer lr;

public void UpdateCircle() {
    if (lr == null) lr = GetCompoennt<LineRenderer>();
    Vector3[] points = new Vector3[pointCount];

    for (int p=0; p<points.Length; p++) {
        float theta = (float)p * Mathf.PI * 2f / points.Length; // theta will be from 0 to 6.28, perfect for trig operations
        points[p] = new Vector3( center.x + Mathf.Cos(theta) * radius, center.y + Mathf.Sin(theta) * radius, 0f);
    }
    lr.SetPositions(points);
    lr.loop = true;
    lr.useWorldSpace = true;
    lr.widthMultiplier = thickness;
}

void OnValidate() {
    UpdateCircle();
}

Make sure that your LineRenderer's layer is set to the same layer we've been using.

Now, you shouldn't need to hit play to see the results - just start editing the values in the inspector, and you should be able to watch the circle move around on your texture.

Now the cool thing is this: the coordinate system being used by the script above should place it at precisely the same coordinates that are used by the UV texture system. So, for example, if you're using a raycast and want to place a circle on the texture using raycastHit.textureCoord, you can assign that value directly to the "center" field, and the circle will be centered where your ray hit the surface!

Example 2: Sketching with raycast

Example 3: Merging/layering textures

For Efficiency's Sake: Rendering at will

results for ""

    No results matching ""