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.
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.
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.
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
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.