Hello Guest

Author Topic: Is it possible to modify SpriteCollection material/texture at runtime?  (Read 20311 times)

theremin

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 20
    • View Profile
I would like to be able to make a copy of my animated sprite's atlas texture and dynamically change the pixels in-memory at runtime, but I'm having trouble figuring out which of the sprite instance's texture(s) I have to change in code.

For example, take the following code:

Code: [Select]
var texture = (Texture2D)Instantiate(renderer.material.mainTexture);
renderer.material.mainTexture = texture;

var cols = texture.GetPixels();
for (var x = 0; x < cols.Length; x++)
{
if (cols[x].a > 0f)
{
cols[x] = Color.red;
}
}

texture.SetPixels(cols);
texture.Apply();

This code takes the texture and changes every visible pixel (alpha>0) to red. I put this code into a new MonoBehaviour script and attached it to my animated sprite GameObject.

If I put the code in the Start() method, it seems to do absolutely nothing (the sprite animates normally with its original texture, as defined in the editor). But if I put that code in the Update() method, I can see that the code partially works -- the sprite frames turn red, and flash quickly between red and their original colors. It seems as the code is "battling" against the 2D Toolkit code, which is constantly swapping back to the original texture.

How can I completely swap out the sprite atlas texture for a specific animated sprite without fighting against the 2D Toolkit code?

I've tried changing the texture as seen in the code above, and I've also tried changing the tk2dAnimatedSprite anim.collection.textures. Is there any way to tell a tk2dAnimatedSprite to use a completely different material/texture at runtime?

theremin

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 20
    • View Profile
Let me provide a little more detail, and ask a slightly different (but related) question:

How can I change the material of just one sprite dynamically at runtime (without changing the material for every other sprite that's using the same atlas texture)?

Unikron mentioned that you can use a different shader (and thus force a new draw call) on a specific sprite in this question:
http://unikronsoftware.com/2dtoolkit/forum/index.php/topic,351.msg1502.html

What I'm trying to figure out is: How?

I'm OK with having a new draw call, but I have no idea how to do it. How do I apply a new material/texture/shader without having to create two copies of every Sprite/SpriteCollection/SpriteAnimation in the editor?

To give the full backstory, I'm trying to create a border around my sprites under certain conditions (whichever sprite was clicked most recently). I would like to avoid manually creating every sprite twice in the editor if possible, and I especially don't want to create all of the animations twice.

I have a few different methods that work, I'm just not sure how to integrate them into my 2D Toolkit workflow. I have a custom shader that draws a 1px border around a sprite, and I also have code that copies a Texture2D and modifies its colors with GetPixels/SetPixels. I suppose a third alternative would be to somehow duplicate the 2D Toolkit SpriteCollectionData in the editor and replace the texture with a bordered version.

I just can't figure out how to switch between the two textures/materials dynamically in code. The only way I know that would definitely work is to create two completely different sets of SpriteCollections, SpriteAnimations, and sprite GameObjects, and then manually keep them in sync during gameplay and selectively choose which one is visible --- which is a lot of extra setup work, and probably a real performance hit too!

How can I avoid doing that?

unikronsoftware

  • Administrator
  • Hero Member
  • *****
  • Posts: 9709
    • View Profile
First things first. If you've worked out how to change the contents of a texture, then thats half the problem solved. Don't forget the import mode needs to be set to advanced and read/write needs to be set to true for the atlas.

There will undoubtedly be quite a high CPU cost to updating textures like this - and in a lot of cases, it might be better to draw into the texture using pixel sized geometry. The reason for this is the GPU usually is drawing a previous frame to the one currently being rendered into, and for the setpixel function to occur at the correct time, it involves making a copy, and then updating the gpu copy at the correct time. As you can imagine this will involve some quite a bit of traffic, and performance will vary somewhat depending on the platform you're working on and what it really does internally.

If you really want to modify the texture, you don't have to care about animations, or indeed anything else apart from the texture. To get the texture, the easiest thing to do is to grab it from a sprite using the sprite collection. Simply get sprite.GetCurrentSpriteDef().material.mainTexture.



You can also change the material of this sprite at runtime - the only catch being you'll have to do it again after each frame change. This is due to how the whole thing works internally. I'm going to be adding an "ignore material" flag on the sprite, so it will not change the material explicitly, but that won't be in the next release. The material change is necessary mainly for multi-atlas support. You can also have sprites in an animation from any sprite collection and mix and match that way, so it does need to change material.


Another option which sounds like a LOT of work, but really isn't at all, is to create a copy of all sprites in the sprite collection, and automatically create a duplicate set of animations automatically. Doing it by hand would be ridiculous, but you can script all of this really easily. Just create a menu entry which edits the sprite collection, taking each sprite and creating a duplicate named _COPY, and for the animations, do the same, and switch the spriteIDs around. That way, you won't have to do any nasty stuff, but everytime the sprite collection changes, run this process again - it should just work, if you code it to delete all _COPY entries before it starts. Once you have template editor script for something like this, it really does improve your workflow very significantly.


theremin

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 20
    • View Profile
Thanks for your response! This is a good start, but I still have some questions.

Question 1:

In the most basic terms, I don't understand how to change the material, even after each frame change. I just want to make a copy of the material at runtime, change its shader to something other than the default, and selectively use that on one specific sprite (but not all sprites that share that material).

Here's some code I've been playing with:

Code: [Select]
protected tk2dAnimatedSprite anim;
protected Material material;

void Start()
{
anim = (tk2dAnimatedSprite)GetComponent<tk2dAnimatedSprite>();

var material = (Material)Instantiate(anim.GetCurrentSpriteDef().material);
material.shader = Shader.Find("Custom/Sprite Border");
}

void Update()
{
anim.GetCurrentSpriteDef().material = material;
}

Unfortunately, this doesn't seem to work at all. First of all, it changes the material for all sprites that are using that material, instead of just one of the sprites. And secondly, it permanently changes the material, even after I stop running the program. The connection between the SpriteCollection and material/data gets broken, and I'm left with just a pink box for all of my sprites in the Unity Editor. The only way to fix it is to re-open the SpriteCollection and Commit again.

What am I doing wrong?

Question 2:

If I tried something like you're talking about in your last paragraph (the one that "sounds like a LOT of work"), that would require me to keep two sets of GameObjects in my scene, right? (Two copies of each sprite; one without a border, and one with.) How would I keep the sprite pairs in sync at runtime? Would I have to constantly update both sprites whenever anything changes in one? Or is there a way to copy a sprite's state (especially the current animation frame and how far the sprite is into it) at a specific moment?

unikronsoftware

  • Administrator
  • Hero Member
  • *****
  • Posts: 9709
    • View Profile
1. What you're doing is changing the material globally on all of those sprites. If you'd like to change it locally to just the local one, you should do something like this. This will change it on the instance.

if (renderer.sharedMaterial != material)
   renderer.material = material;

Also, in your code you're storing the material into a local variable and not the one in class scope, don't think that's what you intended.

2. No, I mean writing some code to generate programattically, and offline 2 of everything, sprites in the sprite collection editor, and 2 animations for each. So, when you need the one with the outline, simply do Play("anim_COPY"); instead of Play("anim"); So you just call the right one depending on the situation.

theremin

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 20
    • View Profile
RE: #1:

Brilliant! That's the solution I've been searching for... almost. Unfortunately, the material is still flickering -- it's bouncing back and forth between border and no border very quickly whenever an animation is playing -- but at least I have one sprite isolated now with its own material/shader.

I suspect this would be solved by the "ignore material" option that you're thinking about adding. Any chance that option just affects a small piece of td2k code that I could just edit myself in the meantime? My texture atlases are pretty small, so I don't intend to use multi-atlases anyway. If it's just a few lines of code I could comment out, that would be awesome.

Thanks unikron. This is a great library, and your support is top notch. I'll definitely be recommending 2D Toolkit to my game programming friends.

unikronsoftware

  • Administrator
  • Hero Member
  • *****
  • Posts: 9709
    • View Profile
In tk2dSprite.cs - do something like this

static bool ignoreMaterialChange = false;

protected override void UpdateMaterial()
{
   if (ignoreMaterialChange)
   {
      if (renderer.sharedMaterial == null)
         renderer.material = collection.spriteDefinitions[spriteId].material;
   }
   else
   {
      if (renderer.sharedMaterial != collection.spriteDefinitions[spriteId].material)
         renderer.material = collection.spriteDefinitions[spriteId].material;
   }
}


So when ignoring material, it will only ever set it once, presumably when the sprite is created. This means that your animation will be limited to one sprite collection, but you should be good to just go in and do what you want to the material after its been created, or even before its been created.

theremin

  • 2D Toolkit
  • Newbie
  • *
  • Posts: 20
    • View Profile
Just tried this out and it works brilliantly! Thanks unikron.