FRONTIERS.JS

Where modders get to tear my game apart when I *finally* release the tools

FRONTIERS.JS

PostPosted by SignpostMarv » Fri Jul 17, 2015 11:38 pm

Towards the end of last year, I'd started trying to rework my webgl experiments with the Frontiers data, but hit a bit of a stumbling block & ended up getting distracted with work.

Cut to 6 months later & I've been tinkering a little with features in the newer specs for JavaScript (ES6 aka ES2015) at my day job & wanted to try my hand at writing something on a project outside the office. So I start to revist the Frontiers data and with Lars open-sourcing chunks of the code I decide an interesting approach would be to port Frontiers' c# code to JS. Transposing the project from one language to another made good sense from a development stand point, as it's a lot easier than trying to reverse-engineer data formats.

My first step was to transpose Frontiers.ChunkTerrainData alongside some smartened up unity terrain parsing code (hat tip to TooTallNate for the endianness detection snippet, resulting in the first demo that accepts a ChunkTerrainData object & generates a greyscale canvas of the heightmap.




One of the challenges in transposing c# to JS is that c# has nice graceful syntax for a getters/setters that perform no constraints beyond type, whereas JS does not- at least not yet, anyway, I'm hoping that something decorator-esque would help me cut down on the type validation aspects. A similar challenge is the (current) lack of access modifiers. Since I'd like to avoid runtime modification of properties & methods in unsupported ways, in the JS transposition I'm achieving both property restrictions & private methods by hiding the values/methods behind Symbols. For properties, this generally involves creating a single Symbol('props') instance for the class which is used to populate an externally inaccessible object that's unique to the instance whereas with methods it involves creating a Symbol('nameOfMethod'), then adding to the class prototype;
Code: Select all
var
  props = Symbol('props'),
  privateMethod = Symbol('privateMethod')
;
Class Foo{
  constructor(){
    this[props] = {
      bar: 'bar',
      baz: +1,
    };
  }

  get bar(){
    return this[props].bar;
  }

  set bar(val){
    this[props].bar = (val + '');
  }

  get baz(){
    return this[props].baz;
  }

  doSomething(){
    return 100 * this[privateMethod]();
  }
}

Foo.prototype[privateMethod] = function(){
  return Math.random();
};


Another challenge is the lack of native Enum support. This is a pain, but perhaps more on that at a later date.




After porting in the old heightmap code, my next challenge was to revisit the problem I had last year- subdividing a 2d array into tiles; It's not really recommended to throw the all of the heightmap chunks into a scene at once (at the time I was working with higher-resolution heightmaps that consisted of over a million vertices over nearly 30 chunks.). The chunks on Steam are smaller, coming in at 263,169 vertices a piece. Splitting the heightmap into patches lets us load only what we need to as well as downsampling far-off parts of the terrain gradually.

This time around it was achieved relatively painlessly, since I was [url=u1nBOX0e9hc]visualising the task in 2D rather than 3d[/url], although it initially confused me a little because of the effects seen when the coatline gets it's own tiles- after a bit of wall banging I realised it was down to the heightmap normalisation that the greyscale canvas generation performs. The heightmap comes in as an array of unsigned 16 bit integers, which Frontiers.Data.GameData.IO.LoadTerrainHeights() converts to an array of floats (consisting of the uint divided by 65535), which is then converted to an 8bit integer for the greyscale image. To preserve some fidelity in the canvas, the lowest point in the heightmap is rendered as full black whereas the highest point is full white.


Up until this point, my demos had hard-coded the chunks to load & the terrain file to use, so the next challenge was to decode Data/Worlds/FRONTIERS.frontiers & transpose the class that defines the world- Frontiers.ChunkTerrainData. As I was becoming more familiar with the XML serialisation that Frontiers employs (with the JS using a lovely wrapper around Mozilla's JXON library), this task became easier (especially when it came to write my own version of Frontiers.Data.GameData.XmlHelper :D)


I'll leave the dev rambling off at this point for now, more to come tomorrow!
User avatar
SignpostMarv
Alchemist
Alchemist
 
Posts: 307
Joined: Thu Jul 18, 2013 2:28 pm

Re: So I did a thing because of reasons.

PostPosted by SignpostMarv » Sun Jul 19, 2015 11:31 am

Now that I was starting to load in the data files dynamically, I could start pushing towards getting the 3D rendering working.

The first step in the 3D rendering was to just load in full-size meshes without any textures- just to make sure I was positioning the chunks & scaling them in roughly the correct proportions; This was fairly straightforward as it's something I've done previously. The next step- loading in textures- was also relatively straight forward as I just needed to drop in a Promise manager that accepts a ChunkState object & a texture resolution to handle the earlier code I'd done with porting Frontiers.Data.GameData.IO.LoadTerrainMap.

Then things got a little tricky. Loading in full-sized meshes is all well and good but pretty inefficient if you're standing in one corner of a chunk & everything behind you still gets rendered. So the next step was to use the code from the earlier tile loading demo to generate patches, which was the easy part. The hard part came when it came to apply the textures; We're dealing with about 4096 patches per chunk, and we really don't want to be uploading 4096 textures to the GPU when 1 will do, so this meant editing the UV coordinates of the meshes :cry:. This 8 line loop took me a little over an hour of head scratching;
Code: Select all
for(i=0;i<geometry.attributes.uv.array.length;i+=2){
    geometry.attributes.uv.array[i + 0] /= ((resolution - 1) / (tile.width - 1));
    geometry.attributes.uv.array[i + 1] /= ((resolution - 1) / (tile.height - 1));
    geometry.attributes.uv.array[i + 0] += ((tile.x - 1) / (resolution - 1));
    geometry.attributes.uv.array[i + 1] = 1 - geometry.attributes.uv.array[i + 1];
    geometry.attributes.uv.array[i + 1] += ((tile.height - 1) / (resolution - 1)) * ((Math.max(0, tile.y - 1) / (tile.height - 1)) + 1);
    geometry.attributes.uv.array[i + 1] %= 1;
}

First we're scaling the UVs to match the patch size.
Then we're offsetting the horizontal to match the patch offset relative to the main chunk.
Finally we're flipping the vertical coordinates, applying an offset & wrapping them around from 0-1.

All of this results in a neat patched WebGL render of the MobileReference instances serialized under WorldSettings.DefaultRevealedLocations :biggrin:

However, this leaves me with the next big headache; getting the patches to have dynamic level-of-detail; When we're zoomed out we don't want to be rendering all 300k+ vertices per world chunk.

Something you may notice is that I'm adapting trying to write the interface for the rendering class in such a way that I can toss any suitable deserialised object into the AddObject method and get the resulting 3D objects on screen :)
User avatar
SignpostMarv
Alchemist
Alchemist
 
Posts: 307
Joined: Thu Jul 18, 2013 2:28 pm

Re: FRONTIERS.JS

PostPosted by Railboy » Sat Aug 01, 2015 9:43 pm

Looking awesome, man. Let me know what resources you need to keep going with this. Responses may be slow but I'll try to keep up.
Language is to the mind more than light is to the eye.
User avatar
Railboy
Developer
Developer
 
Posts: 1845
Joined: Mon Jul 15, 2013 10:46 pm
Location: Seattle, WA


Return to Modding Central

Who is online

Users browsing this forum: No registered users and 1 guest

cron