Adam's Lair Forum

game development and casual madness
It is currently 2018/04/25, 06:58

All times are UTC + 1 hour [ DST ]




Post new topic Reply to topic  [ 18 posts ]  Go to page 1, 2  Next
Author Message
PostPosted: 2017/09/16, 14:47 
Junior Member
Junior Member

Joined: 2017/07/20, 18:17
Posts: 27
Location: Germany
Role: Hobbyist
Hello,

at the moment I try to render the map of my prototype game. I prerender each tile which has a size of 2048x2048 pixels. The reason for using such big textures for the tiles is, I want to zoom in to a map tile very closely. It should look quite nice then, so I choose to use big textures for it. Because the map contains many tiles, the prerendering of the tiles took some time.

Within the update call of a component the prerendering of the map gets started, after the map has been generated by the server and transfered to the Duality client. Because the time consuming prerendering is done in the update call, no actual game frame will be rendered in the meantime. But I want to archieve that the normal rendering will still happen while in the background the map tiles gets prerendered.

So I thought, I could simply do the prerendering in separate running task. But this leads to different errors which are pointing out that multi-threading is not supported here:
Image Image

Here is the renderer I used for testing together with a simple flat camera near the X=0/Y=0 coordinate:
Code:
  public class YourCustomRenderer : Renderer, ICmpInitializable, ICmpUpdatable
  {
    [DontSerialize]
    private bool doAsynchronous = false;

    [DontSerialize]
    private ContentRef<Material>[] materials;

    [DontSerialize]
    private readonly Stopwatch preRenderWatch = new Stopwatch();

    [DontSerialize]
    private readonly object synchronizationRoot = new object();

    [DontSerialize]
    private int updatesLeft = 120;

    public override float BoundRadius { get; }

    public void OnInit(InitContext context)
    {
    }

    public void OnShutdown(ShutdownContext context)
    {
    }

    public void OnUpdate()
    {
      if (this.updatesLeft == 0)
      {
        void PreRenderWatching()
        {
          this.preRenderWatch.Start();
          this.DoPreRender();
          this.preRenderWatch.Stop();
        }

        if (this.doAsynchronous)
        {
          Task.Run((Action)PreRenderWatching);
        }
        else
        {
          PreRenderWatching();
        }
      }

      lock (this.synchronizationRoot)
      {
        this.updatesLeft--;
      }
    }

    private void DoPreRender()
    {
      var preRenderMaterials = new ContentRef<Material>[50];

      for (var i = 0; i < preRenderMaterials.Length; i++)
      {
        var terrainTexture = new Texture(2048, 2048);

        // Create the render target for the texture
        var renderTarget = new RenderTarget(AAQuality.High, terrainTexture);

        // Create a simple draw device for drawing on the render target / texture
        var drawDevice = new DrawDevice();
        drawDevice.Perspective = PerspectiveMode.Flat;
        drawDevice.VisibilityMask = VisibilityFlag.AllGroups | VisibilityFlag.ScreenOverlay;
        drawDevice.RenderMode = RenderMatrix.OrthoScreen;
        drawDevice.Target = renderTarget;
        drawDevice.ViewportRect = new Rect(renderTarget.Width, renderTarget.Height);

        // Begin drawing
        drawDevice.PrepareForDrawcalls();
        var canvas = new Canvas(drawDevice);

        canvas.PushState();
        canvas.State.ColorTint = new ColorRgba(0, 256, 0);
        canvas.FillPolygon(new[] { new Vector2(200, 0), new Vector2(400, 200), new Vector2(200, 400), new Vector2(0, 200) }, 0, 0, 5);
        canvas.PopState();

        canvas.PushState();
        canvas.State.SetMaterial(new BatchInfo(DrawTechnique.Alpha, ColorRgba.White, null));
        canvas.State.ColorTint = new ColorRgba(255, 0, 0, 128);
        canvas.FillCircle(200, 200, 4, 75);
        canvas.PopState();

        // Render to target texture
        drawDevice.Render(ClearFlag.All, ColorRgba.TransparentBlack, 10f);

        // Create the material of the texture to be used on frame rendering
        preRenderMaterials[i] = new Material(DrawTechnique.Mask, ColorRgba.White, terrainTexture).GetContentRef().As<Material>();
        renderTarget.Dispose();
      }

      lock (this.synchronizationRoot)
      {
        this.materials = preRenderMaterials;
      }
    }

    public override void Draw(IDrawDevice device)
    {
      var canvas = new Canvas(device);
      ContentRef<Material>[] localMaterials;

      lock (this.synchronizationRoot)
      {
        localMaterials = this.materials;
      }

      if (localMaterials == null)
      {
        canvas.State.ColorTint = ColorRgba.Black;
        canvas.DrawText($"PreRendering starts in {this.updatesLeft} updates", 50f, 150f);
      }
      else
      {
        for (var i = 0; i < localMaterials.Length; i++)
        {
          var material = localMaterials[i];

          canvas.PushState();
          canvas.State.ColorTint = ColorRgba.White;
          canvas.State.SetMaterial(material);
          canvas.State.TextureCoordinateRect = new Rect(0f, 0f, 1f, 1f);
          canvas.FillRect(i * 2048 % (2048 * 7), i / 7 * 2048, 10, material.Res.MainTexture.Res.TexelWidth, material.Res.MainTexture.Res.TexelHeight);
          canvas.PopState();
        }

        canvas.PushState();
        canvas.State.ColorTint = ColorRgba.Black;
        canvas.DrawText($"PreRendering duration: {this.preRenderWatch.Elapsed.TotalSeconds:0.00} seconds", 50f, 150f);
      }
    }

You can enable or disable the asychronous processing by manipulating the field:
Code:
doAsynchronous
With asychonous processing enabled the code will not work.

So I have two questions now:
1. How can the prendering asynchronously be done OR in a way that the normal rendering is not stopped in the meantime?
2. Are there possibilities to accelerate the prendering so it is much faster without the needed to do complex things in code?


Top
 Profile  
 
PostPosted: 2017/09/16, 19:51 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2042
Location: Germany
Role: Professional
Hey there,

rendering code always needs to be in the main thread, and there isn't really a good way around that. To do stuff in the background, you'd need to split your workload into small chunks, where each chunk is fast enough to complete within a frame in addition to existing rendering, and then pre-render one chunk per frame.

Other than that, what exactly is it that you're pre-rendering? And are you sure that pre-rendering is really the ideal way to speed this up?

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2017/09/17, 13:36 
Junior Member
Junior Member

Joined: 2017/07/20, 18:17
Posts: 27
Location: Germany
Role: Hobbyist
Hi Adam,

thanks for you reply. In my case I have a hexagonal map. Each hexagon tile consists of 37 smaller textures currently pre renderered to a 2048x2048 pixel texture per tile. Because the hexagon tile texture is static, I thought it is best to pre render it once and then render the tile texture every frame instead of 37 smaller textures to improve performance. The point is the hexagonal map should contain hundreds, maybe even thousands of hexes later and the rendering performance should still be smooth.

So, what do you think? Is prerendering the wrong way here? Should I simply render the smaller textures every frame?


Top
 Profile  
 
PostPosted: 2017/09/17, 14:15 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2042
Location: Germany
Role: Professional
If you have a tilemap anyway, pre-rendering really shouldn't be necessary. Tilemaps can be rendered very efficient if you stick to some rules. Take a look at the TilemapRenderer components Draw method for reference. You should be able to come up with an optimized renderer for your hexagonal tilemap:

  • The graphics for all hexes are on a single shared texture, so rendering different hexes only means "different texture coordinates", but not "different materials".
  • A single renderer determines which region of hexes from the entire hex map are currently in the viewport / visible.
  • All visible hexes are rendered in one single batch by that renderer.

With this approach, you can render thousands and thousands of tiles with very little performance impact and the number of visible tiles is almost constant regardless of map size. For example, if you have a square tilemap with 32x32 tiles on a 1920x1080 screen, you're seeing ~2000 tiles on screen at all times regardless of map size, which is no problem at all perf-wise - and this is already an unusual case, as the ratio of resolution to tile size is usually smaller.



Edit: I just re-read your posting and noticed that I might have misunderstood you. Correct me if I'm wrong: You are not pre-rendering chunks of a hexmap as I initially assumed, but you are pre-rendering individual (giant?) hexes where each is filled with about 40 smaller things that are not grid-aligned?

If that's true, you probably don't want a generalized hexmap renderer, but a specialized "your games map renderer", though the above list of points can still apply: Just make sure to keep all your small sprites graphics on a single texture and have a single map renderer to first determine which parts of the map are visible and then render them all in a single batch.

If your sprite counts are low enough or your map size isn't too big, you might even just skip the optimizations and have each of the smaller sprites as actual game objects. Around 40 objects for an area of 2048x2048 should be completely fine even without any custom rendering when the loaded area of your map isn't too big.

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2017/09/17, 17:45 
Junior Member
Junior Member

Joined: 2017/07/20, 18:17
Posts: 27
Location: Germany
Role: Hobbyist
Yes, you right with you edit. Here is a screenshot to make it clear:
Image
You can say: There are small pointy-topped hexes in big flat-topped hexes. With this method every big hex is unique and distinguishable from every other big hex. You can also fully zoom in to one big hex and it still looks sharp. The game will be played only on the big hexes, so the small hexes have no meaning for the game mechanic.

I want to have the possibility to draw round about 2000 to 3000 big hexes. It should be possible to zoom in to one big hex or zoom out to see all hexes and therefore the complete map. I don't know if the game makes sense on such big maps, but I don't want to run into a dead-end here, so I prefer solutions which provides a good performance.

The small hexes have a resolution of 256x384 pixel. So what you suggest is to put the small hexes all on a huge tilemap. I have round about 230 of these tiles. With 15 columns and 16 rows the tilemap has a theoretical size of 3840x6144 pixel. Is there a size limit or a recommendation to the tilemap size?

Adam wrote:
If your sprite counts are low enough or your map size isn't too big, you might even just skip the optimizations and have each of the smaller sprites as actual game objects. Around 40 objects for an area of 2048x2048 should be completely fine even without any custom rendering when the loaded area of your map isn't too big.
Because of the big map size, this may not be a suitable solution. But the custom rendering you described may be the best way to draw the map.

If you don't have any other advices or proposals, after I explained the game map in more detail, I will try implementing a custom hexagon render using a huge tile map. One small question here: Are there any issues with creating the tilemap at runtime from all individual small hex textures?


Top
 Profile  
 
PostPosted: 2017/09/17, 19:05 
Site Admin
Site Admin
User avatar

Joined: 2013/05/11, 22:30
Posts: 2042
Location: Germany
Role: Professional
So if I understand this correctly, what you're dealing with is some sort of a two-stage hexmap, where the big / coarse one is for gameplay, and the small / more fine-grained one is for visuals? Interesting approach!

Faithless wrote:
I want to have the possibility to draw round about 2000 to 3000 big hexes. It should be possible to zoom in to one big hex or zoom out to see all hexes and therefore the complete map. I don't know if the game makes sense on such big maps, but I don't want to run into a dead-end here, so I prefer solutions which provides a good performance.


Hm, that sound reasonable, and I do see your point in running into perf problems here - when zooming out completely and seeing 3000 big hexes at once, that's fine in itself, but it would be a big problem if each of those hexes contains 37 hexes on its own and you'd end up rendering all 111000 hexes at once.

What you'd need to pull that off is a level-of-detail algorithm that would stop to draw the small hexes when zooming out a certain level, and only draw the big ones instead. But if the big ones consist only of small ones, I can see why you planned to pre-render the big ones. Thinking about that, it actually makes a lot of sense in your case - it's simply a way to retrieve a graphic representation of a big hex, which is actually unique.

You could still need an efficient hexmap renderer as described before, but that wouldn't solve your problem when you actually have all hexes on screen at once, as 111000 is really a number where even that starts to get inefficient. So maybe you're in for a mix of pre-rendering individual big hexes and a hexmap renderer to display lots of big (or small) hexes on screen efficiently.

As far as the pre-rendering goes, you might be able to speed it up further by doing it at a lower resolution: You only need the pre-rendered representation of a hex when zoomed out, right? A low-res rendering could be a lot more efficient and also more memory-friendly as a bonus and after all, the reason for that pre-rendering was to be able to display in lower res in the first place when zoomed out.

You could also only render one hex at a time when it's needed and allow your renderer to switch between small hexes and one big hex on a per-big-hex level, which would allow you to spread the processing time across multiple frames. If you know all big hexes up front, you could also put their pre-rendering into a loading screen.

If you were willing to try an entirely different approach, you could look into 3D terrain rendering for inspiration, which always needed to render at both very low and very high distances without performance loss. There might be techniques out there that you could adapt.

Anyway, this is really just a bunch of ideas I'm throwing out there - you're tackling an interesting problem there xD

Faithless wrote:
One small question here: Are there any issues with creating the tilemap at runtime from all individual small hex textures?

No issues there, but keep an eye on loading times as the number of different tiles grows.

_________________
Blog | GitHub | Twitter (@Adams_Lair)


Top
 Profile  
 
PostPosted: 2017/09/18, 07:39 
Forum Addict
Forum Addict
User avatar

Joined: 2013/09/19, 14:31
Posts: 859
Location: Italy
Role: Hobbyist
Another choice could be, assuming that your map is static, the big hexes don't change during game, and you know their composition beforehand, is to perform the tile composition in a loading screen before the actual game start. That way, you could simply have a static "Loading" picture on screen and not care if one update step takes 1 ms or 2 seconds. Maybe add a counter or progress bar that fills whenever a new tile is done, but there is absolutely no need to do 60fps there :D

Also, another less-known tip, is that you can "draw" on other threads, just not by using Duality facilities; you could very well prepare your Pixmaps in whatever way you desire (which is the most resource-intensive part of the job) and delegate only the Texture and Material handling parts to the main thread :)

_________________
Come on Duality's Discord channel. We have cookies! :mrgreen:


Top
 Profile  
 
PostPosted: 2017/09/18, 13:00 
Junior Member
Junior Member

Joined: 2017/07/20, 18:17
Posts: 27
Location: Germany
Role: Hobbyist
Thanks for all the ideas you two are coming up with ^^

Without any experiences yet I favor some tilemap rendering of small hexes on close scale and prerendering with low resolution big hexes on large scale. An advantage I see on using a tilemap rendering is, you have the possibility to also animate the small hexes. In contrast while zoomed out animations of small hexes cannot really be noticed so static big hexes are fine here. I still concentrate on the gameplay and not on animations in the prototype of the game, but I like the potential options.

So, first I focus on the prerendering of the big hexes, because the bottleneck can be expected to be there while drawing 3.000 big hexes with much more small hexes in them. But I also will need the tilemap rendering for that task to render the big hexes fast and efficently. A loading screen sounds fine to me. But I don't want to do too complicated things like using 3D terrain or do rendering stuff in a worker thread without Duality. Both seems to be time consuming for me which I like to avoid. My goal is to write some good performing hex map rendering in a reasonable time.

So, now I will play around a bit, I am sure new questions are coming soon :)


Top
 Profile  
 
PostPosted: 2017/09/20, 15:32 
Junior Member
Junior Member

Joined: 2017/07/20, 18:17
Posts: 27
Location: Germany
Role: Hobbyist
I made some progress here, so let me tell a bit and ask for help :)

I created a loading screen. After the client gets the map from the server the prerendering starts and the progress gets reported back to the loading screen:
Image

I found an easy way sharing the rendering times between the displayed loading screen and the prerendering stuff in the background. I use the async and await C# compiler feature together with a custom SynchronizationContext. With this technique I only need to trigger the load once and have no need for making explicit chunks of loading which then had to be called in the update cycles of the loading screen. The load method itself contains all code for loading. I don't need any state tracking and simply do all the preloading stuff in a sequential order. I only need some await calls from time to time to enable the loading screen to render the progress. I found out, that some of the Duality calls can also be done in a worker thread. Only the render calls and texture creation have to be called in the main thread. So this rendering sharing works quite well.

However there is one crucial issue I found: The frame limit of 60 frames without debugger attached will slow down the preloading, because the background rendering seems also to be done at that limit. When the debugger is attached and the frame limit is disabled the loading is much faster. So my first question here is: How can the frame limit be disabled on general?

While preloading the tilemap is created and after that the big hexes are rendered in the background using the just created tilemap. For the tests I did, I used the following setup:
I used 3025 big hexes for the map which result in approximately 110.000 small hexes. Every big hex has a unique disinguishable texture, so no two big hexes are the same. Besides the big hex textures I also rendered the border grid with black color half transparent. The viewport was always 1600x900 pixel. I used a full release build of Duality and the core plugin without debugger attached. I have a Nvidia GeForce GTX 760, a graphic card which is a few years old.

First, I made some tests with using 512x512 pixel textures for the big hexes. Performance on full view is round about 26 FPS on my machine. At the beginning when trying to get the map into full view it's totally laggy and the game feels unresponsive. After the map was complete visible once the lags disappear but zooming in and out leads still to some lags in rendering. Loading time of the map was 100 seconds without debugger and 25 seconds with debugger attached. Overall the rendering performance is not satisfying here.
Image

Second, I made some tests with using 256x256 pixel textures for the big hexes. No laggs here for zooming and scrolling right from the beginning and frame rate is 57 FPS. Loading time of the map was 105 seconds without debugger and only 15 seconds with debugger attached. So the performance is quite convincing, but I am still a bit disappointed not getting the 60 frames here. I know there are still other sprites and textures which have to been drawn on the global map view, so I expect the performance to drop later. But overall its acceptable.
Image

I also tried performance tweaking via trial and error to get the frames up to 60 FPS, but I don't have the experience on Duality to really know what have to be done. So heres the code I used for rendering the prerendered big hexes, but I am thankfull for any performance hints.
Code:
public override void Draw(IDrawDevice device)
{
  var canvas = new Canvas(device);

  if (this.occurredException != null)
  {
    canvas.State.ColorTint = ColorRgba.Red;
    canvas.DrawText($"Exception: {this.occurredException.Message}", this.GameObj.Transform.Pos.X, this.GameObj.Transform.Pos.Y);
    return;
  }

  try
  {
    if (this.isMapLoaded)
    {
      var hexVectors = this.hexGeometry.TopLeftOriginHexagonVectors;
      var isGridBorderEnabled = this.IsGridBorderEnabled;
      // TODO: Define pixel boundaries in VisualHexMap directly
      var mapFieldBoundRadius = this.visualHexMap[new AxialCoord(10, 10)].Texture.Res.TexelWidth * GameMapRenderer.PreRenderedStructureTileDownScaleFactor;

      // Texture state
      var structureState = new CanvasState();
      structureState.ColorTint = ColorRgba.White;
      structureState.TransformScale = Vector2.One * GameMapRenderer.PreRenderedStructureTileDownScaleFactor;

      // Hex border state
      var borderState = new CanvasState();
      borderState.ColorTint = this.BorderColor;

      for (var column = this.visualHexMap.MinQuadCoord.Column; column <= this.visualHexMap.MaxQuadCoord.Column; column++)
      {
        for (var row = this.visualHexMap.MinQuadCoord.Row; row <= this.visualHexMap.MaxQuadCoord.Row; row++)
        {
          var field = this.visualHexMap[new QuadCoord(column, row)];
          if (field != null)
          {
            var hexOffset = this.hexGeometry.CalculateFieldPosition(column, row, 0, 0);

            if (device.IsCoordInView(this.GameObj.Transform.Pos + new Vector3(hexOffset, 1f), mapFieldBoundRadius))
            {
              canvas.State = structureState;
              canvas.State.SetMaterial(field.Material);
              canvas.State.TextureCoordinateRect = new Rect(0f, 0f, 1f, 1f);
              canvas.FillRect(hexOffset.X - this.leftMargin, hexOffset.Y - this.topMargin, 1000 - (field.AxialCoord.Q + field.AxialCoord.R), field.Texture.Res.TexelWidth, field.Texture.Res.TexelHeight);
            }
          }
        }
      }

      if (isGridBorderEnabled)
      {
        canvas.State = borderState;

        for (var column = this.visualHexMap.MinQuadCoord.Column; column <= this.visualHexMap.MaxQuadCoord.Column; column++)
        {
          for (var row = this.visualHexMap.MinQuadCoord.Row; row <= this.visualHexMap.MaxQuadCoord.Row; row++)
          {
            var field = this.visualHexMap[new QuadCoord(column, row)];
            if (field != null)
            {
              var hexOffset = this.hexGeometry.CalculateFieldPosition(column, row, 0, 0);
              if (device.IsCoordInView(new Vector3(hexOffset, 1f), mapFieldBoundRadius))
              {
                canvas.FillPolygonOutline(hexVectors, this.borderWidth, 0, hexOffset.X, hexOffset.Y, 500f);
              }
            }
          }
        }
      }
    }
  }
  catch (Exception exception)
  {
    this.occurredException = exception;
    this.logger.LogError(PresentationLogEvents.UnspecifiedException, exception, "Error while drawing");
  }
}

The big hex textures are stored with the following settings. This variant uses the 256x256 pixel big hex textures:
Code:
var terrainTexture = new Texture(240, 224,  /* calculated values depending on the down scale factor */
  TextureSizeMode.Default,
  TextureMagFilter.Nearest,
  TextureMinFilter.Nearest, TextureWrapMode.Clamp, TextureWrapMode.Clamp);
 
var terrainMaterial = new Material(DrawTechnique.Mask, ColorRgba.White, terrainTexture).GetContentRef().As<Material>();


Top
 Profile  
 
PostPosted: 2017/09/20, 15:53 
Forum Addict
Forum Addict
User avatar

Joined: 2013/09/19, 14:31
Posts: 859
Location: Italy
Role: Hobbyist
Whoa, that's some serious stuff you put together :+1: great idea on using async/await as well.. it might be time for me to review my thread-based loading screens as well :D

Back on topic, from what I could see in your code, the next step now for you would be to ditch the Canvas-based drawing and start pushing vertices directly to the IDrawDevice. Try and have a peek to Duality's SpriteRenderer class..

_________________
Come on Duality's Discord channel. We have cookies! :mrgreen:


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 18 posts ]  Go to page 1, 2  Next

All times are UTC + 1 hour [ DST ]


Who is online

Users browsing this forum: No registered users and 7 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Jump to:  
cron
Powered by phpBB® Forum Software © phpBB Group