Possible bug in wade.drawLayerToImage?
schen7913

I'm using wade_3.8.1.js, and wade.drawLayerToImage does not appear to be working correctly.

I'm making an isometric game, so I am experimenting with making a dynamic minimap. To test this, I first called

wade.screenCapture('test/image', () => {

      // code to create a sprite and add it to the scene as a SceneObject

});

This worked fine.

But I decided this was not suitable for my purposes and I replaced this code with

wade.drawLayerToImage(30, 'test/new_image', true, null, null, null, () => {

      // code to create the sprite based on 'test/new_image' and add it to the scene as a SceneObject

});

My terrain is on layer 30, so I should be getting only my terrain. Instead, the screenshot from wade.screenCapture is what shows up.

This problem persists even as I change the image name, refresh the page, etc. I'll see if I can reproduce the bug with something simpler.

All 9 Comments
schen7913

Ah yes, and I'm using an official build of Google Chrome Version 64.0.3282.186

Gio

Thanks for reporting the problem.

I think this is what's going on: internally wade merges your terrain layer with the isometric object layer because they are both using webgl and, to make things faster, wade draws them into the same canvas. This results in wade.drawLayerToImage being incorrect. I'll try to fix it for the next release.

In the meantime, as a quick workaround, you can hide the layer(s) that you don't want by calling wade.setLayerOpacity(layerId, 0), and then use wade.screenCapture.

schen7913

I see. But then, is wade.drawLayerToImage only supposed to draw  the part of the layer currently accessible on the screen? The documentation suggests that it draws the entire layer (even the parts that are technically far off the screen), but my usage of it suggests that it only draws the screen portion of the layer.

schen7913

I'm experiencing similar problems with wade.getSpritesInArea(). To try to get around the merging problem, I called

wade.mergeGlLayers(false)

in app.init(). I suppose this is costly in some way, but I'm just trying to get something started.

 

To try to generate an area for wade.getSpritesIn Area, I noticed that I set the camera bounds for the entire world using

wade.setCameraBounds(-1500 1500, -1500, 10, 3, 10).

So I call

terrainSprites = wade.getSpritesInArea( {minX: -1500, maxX: 1500, minY: -1500, maxY: 10}, 30)

to try to get the terrain sprites. But when I loop over the terrainSprites array and draw each to the virtual image, using Sprite.getPosition() as an offset, I only see the lower corner of the terrain when I add the image to the scene.

schen7913

I've tried drawing a transparent sprite to the virtual image first before drawing the terrain sprites. What's bizarre about this is that after doing this, the number of terrain tiles shown in the resulting SceneObject seems to depend on how large I make the virtual image sprite. The larger I make it, the more numerous the terrain tiles. The smaller I make it, the fewer the terrain tiles.

schen7913

So, I figured a few things out about wade.getSpritesInArea. It actually works great! To make sure I grab all the terrain Sprites, I have to cast a wider net, since the Camera Bounds ultimately refer to the limits of where I can move the *center* of the screen.

The real bizarre behavior is with Sprite.drawToImage. Here's my understanding of what to do.

The first thing to do is to make a transparent sprite with a no-op draw function, set its size to the desired image size (about), and then draw the transparent sprite.

Then, for each sprite you got back from wade.getSpritesInArea, call sprite.drawToImage(path, false, position, transform).

The keys here are the position and transform parameters.

The sprites are big, and if you are drawing them to a small image, you need to make the sprites smaller. For example, you can quarter the size of the sprite using the transform

var transform = {   horizontalScale: 0.5, verticalScale: 0.5  }

To position the multiple sprites as they are in your game, you want to use

position = sprite.getPosition(),

but this fails because of the transform, which throws off the sprite position in the resulting image. To fix this (in this example), you have to then call

position.x *= 2; position.y *= 2;

to "undo" the effects of the transform on the position.

Once you do that, it will seem like it worked! For me, I was I able to draw a tiny minimap with my fog of war right on top of it.

But this all broke down when I tried to batch the Sprite.drawToImage operations using setTimeout (and wade.setTimeout). All the sprites will still draw to the correct positions, but somewhere around 1/4 of the way through my sprites array, the Sprite.drawToImage breaks, and each sprite drawn to the virtual image is much smaller than it should be, although it is positioned correctly.

All that suggests that the virtual image must be drawn to all at once, uninterrupted, or else the process breaks down somewhere.

It's weird behavior. But for my use case, I've found that Sprite.drawToImage takes too long to draw, so even batching doesn't improve performance enough to stop my game from stuttering.

Gio

Hi

That is all correct, but allow me to clarify a couple of points:

1. The second parameter of Sprite.drawToImage is a boolean to say whether you want a new image, or you want to draw on top of an existing image. When you call Sprite.drawToImage with that parameter set to true, or when you try to draw to an image that doesn't exist yet, a new image is created, whose size is the same as the sprite that you are drawing into it. This is why you want to draw a transparent sprite first, to set the size of the image. However there's an easier way, which is to use wade.createTranspareintImage(imageName, width, height)

2. Sprite.drawToImage is not and cannot be fast, as it creates a new image in CPU memory. When you are using webgl, this involves creating a texture in GPU memory, loading another texture in GPU memory to draw into it, and after the draw, we have to wait for the image to go from GPU to CPU memory. It's good in some cases - for example vector-based text drawing is a slow operations, so you can use drawToImage on a text sprite to speed things up. But for the minimap, where you have to call it several times in a row, Sprite.drawToImage is probably not what you want to use.

Now assuming that you have all your terrain tiles in the same texture (aka a terrain texture atlas), you are much better off creating a set of (small) sprites that make up your minimap, and you simply update their UV coordinates when your camera moves.

It may be counterintuitive, but this is going to be orders of magnitude faster because wade will batch the sprites, i.e. it won't draw them one by one, but will instead detect that they can be drawn more efficiently by creating a mesh and drawing it all in one go. This probably means making one single draw call, without textures having to move around from GPU to CPU - as opposed to making dozens or hundreds of draw calls, waiting for texture transfers between draws.

I hope this helps. If you decide to go with that approach, it'd be interesting to see how it works in practice.

schen7913

This helped quite a lot. Since my map is a square full of green tiles, I got a green square for the minimap background, a bunch of circle images to represent my characters, and semi-transparent gray squares to represent my fog of war.

Every time a unit moves, I update the circle marker to the corresponding position on the minimap, and every time the fog of war updates, I set the gray squares to visible/invisible based on the updates.

I don't know anything about terrain texture atlases, so I did this with a SceneObject for every image, rather than one minimap SceneObject containing many Sprites. I implemented the minimap's fog of war with a 20x20 array, which made it easy for me to place the SceneObject mini-map markers: I would get the gridCoords of the SceneObjects, use those coords to find the corresponding fog of war sprite on the minimap, and use the the fog of war sprite's position as the position of the marker.

It's very, very fast, even though I have 400 fog of war images on the minimap (one for each tile of the 20x20 map). Since it's so fast, I assume that the sprite batching must still be happening. The drawing time isn't noticeable.

Gio

Yes, the batching happens regardless of whether it's multiple Sprites of the same SceneObject, or just multiple SceneObjects. It should be noted that it can only happen when drawing a sequence of sprites that are using the same textures (and the same shader).

If at some point you draw a sprite that uses a different texture, that "breaks" the batching, i.e. only sprites up to that point will be batched together into a mesh, as soon as you change textures that's the end of the batch (and potentially the start of a new batch).

This is why I was suggesting a texture atlas, which basically means that you put all your textures into the same PNG, and then change UV coordinates (or if you are doing this in code, you can use Sprite.setImageArea and Animation.setImageArea) to say that each sprite only uses a portion of that texture. This pretty much guarantees that all your terrain sprites will end up in the same batch.

However I should also note that you can change the draw order of all the sprites in your layer, using wade.setLayerSorting, to optimize the batching, i.e. to draw Sprites using the same texture in sequence, where applicable.

Post a reply
Add Attachment
Submit Reply
Login to Reply