Modern Planet Rendering : Networking, Roads and Particles

It’s always good practice to implement the networking part of a project early on, this way you can design the other components around it, thus avoiding some major refactoring in the later stages of development. This article covers how I implemented networking in a planet renderer, and the difficulties I faced during the process. I also talk about road rendering using a vectorial graph system, skeleton based animation using the ozz animation library and finally a particle system using OpenGL’s transform feedback.

Networking:

Network programming is a very challenging field, the idea that the information relayed between a server and a client takes a certain amount of time to reach it’s destination, if it ever reaches it, makes any trivial implementation suddenly much more complex. Most modern games find clever ways to give the player the illusion that everything occurs in real-time. On the server-side we usually try to predict the client actions using techniques such as dead reckoning, while on the client-side, we simultaneously perform the action and send the message to the server, correcting the result based on the given response. These approaches solve most of the issues that can be predicted and corrected, but some systems simply can’t be truly synchronized across a network, the major one being dynamic physics simulation. The reason is actually quite simple, if a desynchronization occurs between the server and a client, it’s impossible to correct the dynamic simulation while it’s still happening, at least as far as I’m aware. It would actually be possible to work around this by performing the simulation on each connected clients, but we are limited by the fact that the results would vary due to some precision issues that usually occurs across different machines (Unless we are using a deterministic physics engine). Knowing these limitations, we can start making some choices that could potentially make a network feel more responsive.

  • Altough it isn’t possible to correct a dynamic simulation, it’s possible to correct a kinematic one. In consequence, the character controller should use a kinematic approach to allow the implementation of most client-side prediction techniques.
  • Not all simulations needs to yield the same results across the connected machines. When the gameplay is not affected, it’s worth considering running some simulations on the client-side such as debris or other small objects.
  • Some things are naturally not very responsive. Per instance, when steering the driving wheel of a car, it’s acceptable to have a small delay before seeing the car change direction, the key is to give as much feedback to the player as possible (This of course does not apply to driving focused games).

I won’t go into all the gritty details that comes with network programming, but it should be clear by now that both the server and the client needs to run the simulation based on the same shared data, which leads me to my next subject.

Replica System:

In some cases, it really helps to build an architecture around a simple concept, the replica system is built around the idea that a class should be replicated on both the server and the client, and should be synchronized in a continuous way based on a fixed interval. Since I don’t know a whole lot about low-level networking, I used RakNet‘s ReplicaManager3 implementation, it allowed me to start with an already optimized base, meaning I could concentrate more on how to do the actual implementation. The main challenge was to make the core framework side completely versatile, allowing as much freedom on the scripting side as possible. (You can view the client script here, the server script here, the object_base script here and the player script here)

Here’s a simple usage breakdown:

  • The server creates a network object, and waits for incoming connections.
class 'server' (sf.Window)

function server:__init(w, h, title, style)
    sf.Window.__init(self, sf.VideoMode(w, h), title, style, sf.ContextSettings(0, 0, 0))

    -- create server network object
    self.network_ = root.Network(root.Network.SERVER_TO_CLIENT)
    self.network_:set_auto_serialize_interval(100)

    -- signal called when network is ready
    root.connect(self.network_.on_connect, function()

        -- we create the shared planet data here
        self.planet_ = planet("sandbox/untitled_planet.json")
    end)

    -- signal called on object creation request
    root.connect(self.network_.on_request, function(bs, network_id)

        local type_id = bs:read_integer()
        local object = nil

        if type_id == ID_PLAYER_OBJECT then

            object = player(self.planet_)
            object:on_network_start(self.network_) -- replica created here
        end

        -- reference object creation here
        return object.replica_
    end)

    -- on_connect is called when ready
    self.network_:connect("localhost", 5000)
end
  • The client creates a network object, connects to it, and sends a player creation request to the server.
class 'client' (sf.Window)

function client:__init(w, h, title, style)
    sf.Window.__init(self, sf.VideoMode(w, h), title, style, sf.ContextSettings(0, 0, 0))
    self.active_player_ = nil
    self.player_id_ = 0

    -- create client network object
    self.network_ = root.Network(root.Network.CLIENT_TO_SERVER)

    -- signal called when connected to server
    root.connect(self.network_.on_connect, function()

        -- we create the shared planet data here
        self.planet_ = planet("sandbox/untitled_planet.json", true)

        local bs = root.BitStream()
        bs:write_integer(ID_PLAYER_OBJECT)

        -- send player creation request, return requested network id
        self.player_id_ = self.network_:request(bs)
    end)

    -- signal called on object creation request (after reference from server)
    root.connect(self.network_.on_request, function(bs, network_id)

        local type_id = bs:read_integer()
        local object = nil

        if type_id == ID_PLAYER_OBJECT then

            object = player(self.planet_)
            object:on_network_start(self.network_) -- replica created here

            if self.player_id_ == network_id then
                self.active_player_ = object
            end
        end

        return object.replica_
    end)

    -- on_connect is called when connected to server
    self.network_:connect("localhost", 5000)
end
  • The server receives the creation request, creates the object, assigns the requested network id and replicate it across the connected clients. Let’s take a look at what a replica implementation looks like.
class 'object_base_net' (root.NetObject)

function object_base_net:__init(network, object)
    root.NetObject.__init(self, network)
    self.type = root.ScriptObject.Dynamic
    self.object_ = object

    if not network.autoritative then
        self.history_ = root.TransformHistory(10, 1000)
    end
end

function object_base_net:__write_allocation_id(connection, bs)

    root.NetObject.__write_allocation_id(self, connection, bs) -- call first
    bs:write_integer(self.object_.type_id_)
end

function object_base_net:__serialize_construction(bs, connection)

    bs:write_vec3(self.object_.transform_.position)
    bs:write_quat(self.object_.transform_.rotation)
    root.NetObject.__serialize_construction(self, bs, connection)
end

function object_base_net:__deserialize_construction(bs, connection)

    self.object_.transform_.position = bs:read_vec3()
    self.object_.transform_.rotation = bs:read_quat()
    return root.NetObject.__deserialize_construction(self, bs, connection)
end

function object_base_net:__serialize(parameters)

    parameters:get_bitstream(0):write_vec3(self.object_.transform_.position)
    parameters:get_bitstream(0):write_quat(self.object_.transform_.rotation)
    return root.Network.BROADCAST_IDENTICALLY
end

function object_base_net:__deserialize(parameters)

    self.object_.transform_.position = parameters:get_bitstream(0):read_vec3()
    self.object_.transform_.rotation = parameters:get_bitstream(0):read_quat()
    self.history_:write(self.object_.transform_)
end

function object_base_net:__update(dt)

    if not self.network.autoritative then
        self.history_:read(self.object_.transform_, self.network.auto_serialize_interval)
    end
end
  • When the object creation request is received on the client-side, we create the object, check if the network id matches the requested one, if it does we assign it as the active player. We now have a replicated object shared across the network.

Vectorial and Residual Data (WIP):

Although it’s possible to generate very realistic and detailed terrain using all sorts of different algorithms, a real life terrain differs a lot from just its own elevation data. It is made out of several kinds of area that each follows their own specific set of rules, a field or a city is generally more flat, a road will flatten the terrain surrounding it, a river will only flow downhill, and so on. In order to accurately represent those elements, we can use the concept of a Graph, holding all this information in the form of vectorial data. Per instance, a simple road can be represented by a curve, linked by two control nodes. In order to apply this road on a terrain, we use the concept of a GraphLayer, since a road affects both the elevation and the color of the terrain, we make use of two different GraphLayer, both are using the same Graph, but are drawn differently. I’m still in the process of understanding how it all works, I will post a more detailed explanation in a future post, for now I only managed to get a simple road with a fixed height working.

Residual data is what allows a the terrain to be deformed, it is the elevation difference between a generated tile and the modified values. It is useful to manually adjust the terrain where needed, but can also be extended to support various types of tools such as a heightmap brush that can print a generated heightmap from other software such as World Machine or L3DT, a noise tool that can apply certain type of noise on top of the generated one, a flatten tool that can equalize the surrounding elevation, and so on. (You can view the planet_producer script here and the road_graph_layers script here)

screenshot

Skeleton Based Animation (WIP):

I’ve been meaning to implement skeleton based animation for a while now. I used the Assimp library in the past but the loading time where less than ideal for a streaming solution. After looking around for other implementations, I found the ozz-animation library. It’s worth saying that this is not a drop-in solution and will not work out of the box, however once you take the time to write an implementation that fits your needs, it becomes by far the most complete solution out there. It performs the skinning job on the CPU, which is exactly what I need since I already use the GPU quite extensively, and it also allows the process to be done on a seperate thread. Additionally, it comes with a set of command line based tools that will convert a FBX animation to it’s custom binary based archive format, making the loading process very fast. I spent a couple of days learning a basic animation making workflow, and let’s just say I’ve gained a whole lot of respect for all the animators out there. (You can view the player script here)

Particle System:

Finally, I implemented a particle system using OpenGL’s Transform Feedback (the equivalent of the stream-output stage in DX11). I won’t explain the implementation details here since there’s already tons of very well written articles (1, 2, 3) out there, but I just want to point out one small detail that I overlooked at first, and would like to see higher up in the documentation. the call

glTransformFeedbackVaryings(id, 1, varyings, GL_INTERLEAVED_ATTRIBS)

Must be called before linking the actual program. Maybe it’s just me, but it’s the first time I’ve seen a shader specific call that needed to be called before the linking stage, so I spent quite some time debugging it, thanks to open.gl for writting it clearly on their article. (You can view the object_misc script here)

 

References:

[1] Source Multiplayer Networking
[2] Real-time rendering and editing of vector-based terrains
[3] Particle System using Transform Feedback

Modern Planet Rendering : Physically Based Rendering

For the last few weeks I’ve been working on ways to get realistic shading in an environement as large as a planet while maintaining as much details in the near view as possible. In order to achieve this, I use Physically Based Rendering (PBR) for the light shading model, and combine it with the values supplied by the precomputed atmosphere. Additionally, a global/local volumetric environement probe system is used to seamlessly provide information for Image Based Lighting (IBL) in real time.

Volumetric Deferred Lights:

When using forward rendering, the shading cost is usually related the number of lights present in the scene. In constrast, when using deferred rendering, the shading cost is shifted to the rendering resolution, since we now store the vertex data in textures. For most operations in deferred rendering, a screen quad mesh is used to process a texture, this make sure that every pixels in the screen is processed. In order to reduce the shading cost, it’s possible to draw basic shapes instead of a screen quad, and use projective mapping to perform the textures lookup instead. (You can view the PBR common shader here, and the point light shader here)

This would be a regular deferred pass using a screen quad.

-- vertex shader
layout(location = 0) in vec2 vs_Position;
layout(location = 1) in vec2 vs_TexCoord;
out vec2 fs_TexCoord;

void main()
{
    gl_Position = vec4(vs_Position, 0, 1);
    fs_TexCoord = vs_TexCoord;
}

-- fragment shader
layout(location = 0) out vec4 frag;
in vec2 fs_TexCoord;

void main()
{
    float depth = texture(s_Tex0, fs_TexCoord).r;
    vec4 albedo = texture(s_Tex1, fs_TexCoord);

    vec3 wpos = GetWorldPos(fs_TexCoord, depth);
    frag = vec4(albedo.rgb, 1);
}

And this would be a deferred volumetric pass using a cube mesh.

-- vertex shader
layout(location = 0) in vec3 vs_Position;
out vec3 fs_ProjCoord;

void main()
{ 
    gl_Position = u_ViewProjMatrix * u_ModelMatrix * vec4(vs_Position.xyz, 1);
    fs_ProjCoord.x = (gl_Position.x + gl_Position.w) * 0.5;
    fs_ProjCoord.y = (gl_Position.y + gl_Position.w) * 0.5;
    fs_ProjCoord.z = gl_Position.w;
}

-- fragment shader
layout(location = 0) out vec4 frag;
in vec3 fs_ProjCoord;

void main()
{
    float depth = textureProj(s_Tex0, fs_ProjCoord).r;
    vec4 albedo = textureProj(s_Tex1, fs_ProjCoord);

    vec2 uv = fs_ProjCoord.xy / fs_ProjCoord.z; 
    vec3 wpos = GetWorldPos(uv, depth);
    frag = vec4(albedo.rgb, 1);
}

Volumetric Environement Probes:

For this approach, the environement probes are treated as another type of light, just like a point, a spot or an area light. It consist of two parts, a global cubemap, and a list of smaller parallax corrected cubemaps. The global cubemap is generated first and contains the sky, sun and clouds lighting information. Next I generate the local cubemaps, but change the clear color to transparent so that they can be blended later on, at this point all the information is generated and ready to be drawn. For the actual drawing, I use a screen quad volume for the global cubemap, and a box volume for the local cubemaps. First I clear all the buffers and draw the local volumes, then I draw the global volume while making sure to skip the pixels already shaded using a stencil buffer. This works but the local cubemaps still shades pixel outside of it’s range, to fix this I discard the pixel if the reconstructed world position is outside of the volume range. Finally, in the local volume passes, I blend the local cubemap with the global one using it’s alpha channel. (You can view the render pipeline object here,  the envprobe script object here, and the envprobe shader here)

Procedural Terrain Shading:

Now that the IBL information is ready, it’s time to actually shade the terrain. First I generate a splatmap using information such as the terrain slope and range. The detail color and normal textures are loaded from memory and stored in texture arrays. To improve the quality, they are mipmapped and use anisotropic and linear filtering. Several different techniques are used to shade the terrain such as normal mapping, height and distance based blending and Parallax Occlusion Mapping (POM) for the rocks. (You can view the tile producer script object here, the splatmap shader here, the planet script object here, and the planet shader here)

hq2

Tessellation:

While the planet is still using a quadtree for the tile generation and such, tessellation is now used for the actual mesh rendering. This is needed to boost the amount of polygons close to the player camera, and fixes some collision mismatch I had when generating the tile colliders. It’s also very useful to control the terrain quality based on the GPU capabilities. (You can view the planet shader here)

Conclusion:

I also did a lot of work around model loading, I’m using the gltf pipeline to generate binary models, and added the abilities to create collider directly from the vertices/indices buffer, meaning it’s now possible to stream large models as they load almost instantly.

hq3

References:

[1] Encelo’s Blog (Volumetric Lights)
[2] Cinder-Experiments (Parallax Corrected Cubemap)
[3] Asylum_Tutorials (Physically Based Rendering)
[4] Proland (Planet Rendering)

Modern Planet Rendering : Reflections, Object Picking and Serialization

One of the most widely used technique to render photorealistic scenes in a game nowadays is PBR. While it’s implementation varies from one engine to another, it’s common practice to use a reflection buffer to represent the surrounding environment. In my last post I talked about some of the issues I had with SSLR and how it introduced many artifacts related to what is not visible on the screen, following this I experimented with regular planar reflections but hit a roadblock when it comes to actually blending the reflection with the gbuffer normals in a deferred setup. I have worked with Marmoset in the past, and I really liked their hierarchical approach to cubemap based reflections, how they use a global cubemap to represent the environment and smaller parallax corrected cubemaps for local reflections, so I tried this approach. Turns out “crafting” your reflection buffer this way really gives you the best results, and is actually very cheap with proper optimization.

Reflections:

For the scope of this post, I’ll cover only the global cubemap. For starter, I had to figure out where to place this cubemap, usually you’d want to place it in the middle of a scene slightly higher on the y axis. In my case I get the center of the closest sector bounding box and add a fixed value on the y axis. The best way to handle this would be to cast a ray down the y axis and get the actual terrain height at this point, but for now it works just fine. Now to render the actual cubemap, I first perform a very cheap cloud pass with a low max iterations value, then I perform the sky pass using the already generated athmosphere textures and.. that’s it really, there’s already enough information to provide a decent global reflection to the scene. The important part is the optimization, first we need to render at a very low resolution, something like 24×24 with linear filtering should be enough. The really crucial part is to render only one cubemap face per frame, this’ll of course make the reflection a little more laggy but it wont be noticable when the environment changes slowly. Finally, there’s no need for the negative y face, it’s almost always black, unless you’re very particular about the color of the reflected ground.

reflection_hq

Object Picking:

I also worked on the editor a bit, I fleshed out the sector view so that it now shares one perspective view and three orthographic views of the scene in a cross splitter setup. To make editing sectors easier, the terrains in those views are not spherical. I also added a grid for the ortho views and a shared manipulator used to transform the selected objects. The actual selection was the biggest challenge, in the past I used to project the mouse position using the gbuffer depth and check if it was inside a slightly enlarged bounding box of an object, it worked pretty well since I was using a more scene graph oriented design, meaning I was traversing a tree instead of a list, usually finishing the traversal with the smaller objects. In this project I choosed to handle the objects in a non hierarchical manner, mainly because I think that the whole parent/child approach should be used either on the asset creation stage or should be object specific, since it usually gets in the way when it’s not needed. I went with the more regular color based picking approach, the drawback is that it requires an additional color buffer and draw pass, but it can be overcome by performing the picking pass only when the mouse button is clicked. I also added multiple objects picking support by extracting all the colors found inside a given rect when the mouse is released.

editor_cross

Serialization:

Since I need to start placing smaller cubemaps in a scene, I needed to imlement the core of a serialization system. The process is pretty vanilla, I’m exposing picojson to the scripting side basically. Serializable objects have both a parse/write virtual that can be overload to write information specific to them. The serialization is done on the scripting side and it’s completly up to the user to define how it’s going to be done, this way you could implement a more ECS based approach to a project if you needed to. The parse/write approach is meant to be expended with RakNet’s ReplicaManager3 plugin, allowing serialized objects to be replicated across a network, but this’ll be covered in another post.

function object:parse(config)

    self.transform_.position.x = config:get_number("pos_x")
    self.transform_.position.y = config:get_number("pos_y")
    self.transform_.position.z = config:get_number("pos_z")
 
    self.transform_.scale.x = config:get_number("scale_x")
    self.transform_.scale.y = config:get_number("scale_y")
    self.transform_.scale.z = config:get_number("scale_z")
end

function object:write(config)

    config:set_number("pos_x", self.transform_.position.x)
    config:set_number("pos_y", self.transform_.position.y)
    config:set_number("pos_z", self.transform_.position.z)
 
    config:set_number("scale_x", self.transform_.scale.x)
    config:set_number("scale_y", self.transform_.scale.y)
    config:set_number("scale_z", self.transform_.scale.z)
end

Conclusion:

I also implemented rigid body picking since it was using the same kinda math. You should expect to see more visually pleasing results pretty soon, having a decent reflection was the only thing preventing me from implementing PBR, and eventually procedural terrain shading, right now the color values are all over the place.