Jump to content

Rastar

Members
  • Posts

    421
  • Joined

  • Last visited

Blog Entries posted by Rastar

  1. Rastar
    Leadwerks has a very nice terrain system that allows terrains of up to 4096x4096 units (in version 3.1, 3.0 goes up to 1024x1024) that performs very well and has a pretty unique texturing method. For most game applications, this is more than enough - filling such an area with objects and interesting gameplay is already quite a challenge. But some game ideas still require larger terrains. In my case, I am pondering something similar to a racing game, taking place in real-world areas, where a single level features tracks of between 50 and 200km.
    My testbed is the Mediterranean Island of Mallorca, which is about 80x80 km in size. The image above shows its general topology, with its digital elevation data loaded into World Machine, a very nice terrain generator (more on this in another post). To render something like this with sufficient close-view detail, but still provide panoramic views (e.g. down a mountain pass) at acceptable frame rates is quite a challenge. Fortunately, there are a couple of algorithms that have been developed for just that. I'll list a few that I know of and of course would be interested to hear of others.
    Chunked LOD
    Pretty old but still in heavy use is the Chunked LOD algorithm of T. Ulrich (see http://tulrich.com/geekstuff/chunklod.html, where you can also find C++ sources of an implementation). In its typical incarnation it works off a heightmap and generates a quadtree of meshes for it, based on a simple level of detail calculation. So, at the root of this tree is a very coarse representation of the complete terrain, which is divided into 4 child nodes of the same basic patch size (e.g. 256x256) and correspondingly higher level of detail. Every of those child patches has four children with... and so on. These patches are created during design time and are tessellated according to the underlying terrain structure - fewer vertices for flat planes, more for rugged mountains.
    During run-time, the Chunked LOD algorithm selects levels from the quad-tree based on a simple screen space error calculation. This will lead to patches of different detail levels lying next to each other and correspondingly to T-junctions - vertices of one patch lying on an edge of the next level. The resulting visible cracks in the surface have to be covered, usually by adding a small, angled skirt around every patch. In addition, to avoid a sudden popping of vertices when changing for lower or higher detailed patches, vertices are slowly "morphed" to their final position.
    The Chunked LOD method, though pretty old, still has a few advantages:
    the meshes are optimally tessellated, so you don't create vertices where you don't need them (flat areas) it works on legacy hardware, even mobile devices (though it's of course questionable if rendering such a large terrain on a phone really makes sense). Most other algorithms make use of displacing a heightmap in the vertex shader, and the corresponding GLSL functions (texture or textureLod) aren't available in the vertex shader for lower-end devices. But, as always, there are also many significant disadvantages:
    the preprocessing step (creating the mesh quad-tree) is quite time-consuming a lot of data has to be stored on disk (all three coordinates of the vertices plus additional morph data for the morphing algorithm) all those meshes have to be transferred from the CPU to the GPU Geo Clipmapping
    In the set of algorithms using heightmap displacement in the vertex shader, the geometry clipmaps approach of Losasso and Hoppe is a particularly interesting one (see http://research.microsoft.com/en-us/um/people/hoppe/geomclipmap.pdf or their GPU Gems 2 article http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter02.html). They construct a set of base mesh patches arranged in concentric rings around the viewer, with higher vertex density close to the viewer and fewer vertices farther away. Those base patches are reused, being only scaled and moved, so very few vertex buffers actually have to be sent to the GPU. Together with an algorithm for updating the heightmap being used for vertex displacement (thus taking care of viewer movement), this is a very efficient rendering method since it uses only a few static vertex and index buffers and most work is done on the GPU.
    Among the set of six different patch types being used there is also a strip of degenerate triangles (ie triangles whose three vertices lie on a line). Their job is to cover T-junctions between adjacent rings. Remaining discontinuities in geometry (and texturing) are covered in the shaders by smoothly blending between values in a transition region.
    In their paper, the authors also describe an interesting method for efficiently compressing and reconstructing heightmaps, which can be used for any heightmap-based approach and which I'll cover in a separate post. As an example, they managed to compress the USGS height data for the complete USA (a dataset of 40 GB) to just 355MB.
    As a sidenote, if I remember correctly Josh once mentioned that the new "virtual megatextures" for the Leadwerks terrain use a similar approach.
    Advantages:
    pretty fast rendering of large terrains minimal preprocessing small data size - just the heightmap, also only very few and small static buffers on the GPU good visual continuity Disadvantages:
    doesn't run on legacy hardware (Shader model 2.0 and below) fixed amount of triangles, so a flat plane might be rendered using thousands of triangles while a rugged mountain might be represented by large, stretched triangles CDLOD
    Then there is the "Continuous Distance-Dependent Level of Detail" approach (see http://www.vertexasylum.com/downloads/cdlod/cdlod_latest.pdf). I must confess I haven't looked too deeply into this. As I understand, it is somewhat of a mixture between the quadtree structure of Chunked LOD and the heightmap displacement of geo clipmapping.
    Real-time tessellation on the GPU
    Finally we're coming to the title of this blog... Tessellation on the GPU can also be used for terrain rendering. While not a "large scale" approach itself, it of course can help to improve other algorithms by doing an optimal tessellation during run-time, based on both camera position and terrain structure. In addition, the generated mesh can be made water-tight, so you don't have to deal with cracks and T-junctions. An example of such an algorithm was described by N. Tatarchuk in the Advanced ShaderX 7 book ("Dynamic Terrain Rendering on GPUs using Real-Time Tessellation"). They start with a coarse, regular mesh and then render the terrain in two passes: First they render the heightmap as a point cloud to a buffer and use that information in a second pass to optimally tessellate the terrain.
    OK - what's next?
    What I'm going to try over the next couple of blog posts is a mixture of two approaches: A geometry clipmapping approach, but with a simpler patch structure and coarser grid. Tessellation will then be used to make the mesh water-tight and create an optimally structured mesh. I will move slowly, because many required techniques are still new to me, but if this is of interest to you - stay tuned!
  2. Rastar
    Physically-based Rendering (PBR), often also called Physically-based Shading (PBS), has taken the world of game engines and content creation tools by storm over the last couple of years. And for good reason: Art assets can be produced in a much more predictable and consistent way, since its properties directly relate to (measurable) real-world data, like the diffuse color of a material or the luminous flux (in lumen) of a light source. Even better, those assets also behave predictably and cosistently under varying lighting conditions. And since I really like the results of PBR pipelines (and love fooling around with shaders) I had a go (actually a second one) at implementing PBR in Leadwerks.
     
    So what is (different in) PBR? There are many good resources on this around (e.g. the tutorials by Marmoset http://www.marmoset.co/toolbag/learn), but in a nutshell:
     
    - Special care is taken to make the reflection by a material energy-conserving. Meaning: There can't be more light coming out of a material than went in (unless it is an emitter, of course).
    - The properties of a material are modeled after its real-world cousin. This is especially obvious for metals: Metals have no diffuse reflection (which is actually the main contributor to any material's reflection in a classical pipeline). Physically, the diffuse reflection of a material consists of light that is absorbed and then re-emitted (with the diffuse color of the respective material). Metals don't do that (much) - any light hitting a material is being reflected right away, never entering its surface. As a consequence, the diffuse color (usuall called albedo) of a metal is pitch black.
    - Everything is shiny: Even non-glossy non-metals do have a (low) amount of reflectivity, being especially apparent at glazing angles (Fresnel effect). Most materials don't have a colored reflection, athough some metals do (like gold or copper).
     
    You will find two main workflows for PBR pipelines: specular-gloss and metalness-roughness. These are basically just two different ways of specifying a material's properties, one giving more artistic freedom and less artifact (specular-gloss), with the other being slightly more intuitive and memory/bandwidth friendly (metalness-roughness). Since I don't have access to the setup of the G buffers in Leadwerks' deferred renderer, I went with the metalness-roughness variant since I could squeeze that into the current setup.
     
    Apart from modifying the lighting shaders to use the different rendering algorithm (I used the GGX specular lighting), it was important to include indirect specular lighting because otherwise metals would be mostly black. The standard way to do this is to use special cubemaps (I created mine using https://www.knaldtech.com/lys/). I also added a simple form of diffuse IBL (image-based lighting) using spherical harmonics.
     
    Some other things are important when adopting a PBR lighting algorithm:
     
    - Use linear space. PC monitors actually use gamma space, which is why most texture files are also encoded in gamma space. The problem here is that adding several colors in gamma space gives an incorrect result (for a more detailed description see http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html). Therefore, textures have to be converted to linear space, and the rendering result must be converted back to gamma space for displaying on a monitor.
    - The accuracy of an 8bit per channel frame buffer (which is what Leadwerks currently uses) does not yield best results, 16bits per channel would be preferable. And some textures (cubemaps, e.g.) should actually be in 16bit formats as well.
     
    But enough chit-chat, what does all this actually look like?
     
    First of all, we have to "usual" diffuse lighting, here by a directional light
     
    http://images.akamai.steamusercontent.com/ugc/318997058028854065/DDFD846082BF282E8CC7279CBF5FE022A03EE367/
     
    and its corresponding specular companion
     
    http://images.akamai.steamusercontent.com/ugc/318997058028856863/0BD31B453D6ABD8B16E7435CBD9EE86649D92C4D/
     
    Together with the diffuse ambient term
     
    http://images.akamai.steamusercontent.com/ugc/318997058028860663/961C026CA6DB56192FFB356C9F6BCA53BB893CAC/
     
    and the indirect specular
     
    http://images.akamai.steamusercontent.com/ugc/318997058028863634/8925ED33DDDD375EC0161B843F465BCCD3AC737E/
     
    this adds up to the total lighting seen at the beginning of the page
     
    http://images.akamai.steamusercontent.com/ugc/318997058028866463/8771AB07CB9F5D472F034A4EC83EA4A96A82EA06/
     
    So, what's next? Well, actually I'm not convinced I have done all this right - there are some artifacts I have to look at, and I'm sure if the calibration is right. Also I need to write a cubemap generator to create those textures in-engine.
     
    Stay tuned!
  3. Rastar
    I feel a bit like I'm spamming the community blog... but of course that won't stop me from doing it again! With the basic mesh generated it's now time for diving into shader programming. I will displace the vertices in the vertical using a heightmap, and then apply a global color and normal map to make the terrain look more... terrainy. I'll explain the shader line by line. All of this will actually be very basic (so, shadmar and klepto: Move along, nothing to see here... whew, we're amongst ourselves now!). But it will still be so lengthy that I've split the post in two parts. I've put the files for this in my Dropbox folder (due to the large textures it's about 100MB) at https://www.dropbox.com/s/clvbremwdjhdp9c/Terrain.zip. Just extract the folder into your project root and attach the MTTerrain_v2.lua script to a pivot entity. All textures were generated using World Machine (it's from their Tutorial 4 file). Side note: You are not allowed to use them commercially, but I guess you didn't plan to anyway... If you enter a "Total terrain size" of 16384 and a "Vertical scale" of 2625 you have the dimensions that the terrain was generated for.
     
    Leadwerks 3.1 provides access to four different stages of the shader pipline: vertex, tessellation, geometry and fragment shader (the tessellation stage actually consists of two programmable parts, the control and evaluation shaders). At least the vertex and the fragment stage have to be present for your shader to compile and produce any output on the screen, and that's what I'll be using here.
     
    1) The vertex shader is the first programmable stage. It is executed for every vertex of the rendered model. It cannot produce any geometry or drop it, but can be used to change vertex positions, transform between different spaces, calculate colors and lighting and much more.
     
    2) The fragment shader is executed for every fragment (potential pixel on your screen). Its job is to produce the final color value for each fragment.
     
    For a short summary of the shader stages see the OpenGL wiki. Leadwerks shaders are programmed in GLSL, a special C-like shading language. I won't go into detail about syntax, data types and so on, but rather focus on two things that have always confused me (and still do): How data flows in, between and out of shaders; and the various transformations between coordinate spaces. So, here is the vertex shader for the terrain (I have stripped it down to almost the bare minimum that I need):
     

    #version 400 #define MAX_INSTANCES 256
     
    It starts easy enough with the minimum GLSL version number required by this shader. For the more recent releases, the GLSL and OpenGL version numbers are identical. Then it defines the maximum number of (hardware) instances for a model. Frankly, I don't know what happens when you exceed that limit and how large that number can be - maybe somebody can chime in?
     

    //Uniforms uniform vec4 materialcolordiffuse; uniform sampler2D texture0;//diffuse+height map uniform mat4 projectioncameramatrix; uniform mat4 camerainversematrix; uniform instancematrices { mat4 matrix[MAX_INSTANCES];} entity;
     
    Then a couple of uniforms follows. Uniform values are identical across vertices. The values above are defined and filled by Leadwerks. The first two are defined in the material editor as the diffuse color and diffuse (first) texture. The next three are Leadwerks internals and lead us into the wonderful world of coordinate transformations:

    Starting with the last, this is an array of entity transforms, ie the values for position, rotation and scale of an entity. This is hardware instancing at play: Instead of rendering the same model with numerous draw calls, the different entity matrices of those models are stored in a uniform buffer and then passed to the shader who renders them in a single draw call. By multiplying a vertex with such a matrix it is transformed into world space (from local to global coordinates).
    The camerainversematrix (aka "view matrix") transforms vertices into camera space, ie into a space with the camera at the origin and typically looking down the z axis.
    Finally, the projectioncameramatrix both transforms into camera space and then into "clip space", the final perspective view.

     

    uniform float terrainSize; uniform float verticalScale;
     
    The next two lines are uniforms that I have defined myself and that are set by the MTTerrain.lua script like this
     

    shader:SetFloat("terrainSize", self.terrainSize) shader:SetFloat("verticalScale", self.verticalScale)
     
    They contain the overall size of the terrain (in meters) and its maximum height (also in meters).
     

    //Attributes in vec3 vertex_position; in vec4 vertex_color; //Outputs out vec4 ex_color; out vec2 ex_texcoords0; out mat3 nmat;
     
    And finally the per-vertex (non-uniform) inputs and outputs of the vertex shader follow. The "ins" are predefined and filled by Leadwerks (they contain the object-space vertex position and vertex color). The "outs" are arbitrary variable names that are then provided to later shader stages and can be used there by a simple naming convention. So an "out vec4 ex_color" can be accessed in another shader by defining "in vec4 ex_color".
     
    Then the magic happens: Every shader stage needs a void main(void) function that does the actual processing (though you can define additional functions to structure your code).
     

    mat4 entitymatrix = entity.matrix[gl_InstanceID]; mat4 entitymatrix_=entitymatrix; entitymatrix_[0][3]=0.0; entitymatrix_[1][3]=0.0; entitymatrix_[2][3]=0.0; entitymatrix_[3][3]=1.0; vec4 modelvertexposition = entitymatrix_ * vec4(vertex_position,1.0);
     
    First, the entity matrix of the current instance (identified by gl_InstanceID) is retrieved from the array and stored in the variable entitymatrix. Now, as a little trick Leadwerks stores a per-entity color in the (otherwise unused) fourth column of that matrix - the entitymatrix_ variable is relieved from that burden (if you select an entity in the scene explorer and switch to the Appearance tab - the "Diffuse" down there - that's it!). Then, (object-space) vertex position is transformed into world space.
     

    ex_texcoords0 = modelvertexposition.xz / terrainSize + 0.5;
     
    The (first) UV coordinates of a mesh are passed into the shader in a uniform called vertex_texcoord0, but that is of no use here: I want to apply a color and normal map that covers the entire terrain (not just a single mesh patch), so I have to calculate them myself. Since the terrain is centered on the origin, after dividing the vertex position by the terrain size an addition of 0.5 gives the correct range from 0.0 to 1.0.
     

    modelvertexposition.y = (texture(texture0, ex_texcoords0)).a * verticalScale;
     
    Tadaa - this innocent looking line does all the magic of the vertex displacement. Using our calculated UV coordinates, it samples the alpha channel of the diffuse texture (where I have encoded the height map), multiplies that value (which again will be between 0.0 and 1.0) with the maximum height and displaces each vertex by the result in the y direction. Simple.
     

    gl_Position = projectioncameramatrix * modelvertexposition;
     
    Every vertex shader must return the vertex position in the gl_Position variable, in this case in clip space coordinates.
     

    nmat = mat3(camerainversematrix[0].xyz,camerainversematrix[1].xyz,camerainversematrix[2].xyz);//39 nmat = nmat * mat3(entitymatrix[0].xyz,entitymatrix[1].xyz,entitymatrix[2].xyz);//40 ex_color = vec4(entitymatrix[0][3],entitymatrix[1][3],entitymatrix[2][3],entitymatrix[3][3]); ex_color *= vec4(1.0-vertex_color.r,1.0-vertex_color.g,1.0-vertex_color.b,vertex_color.a) * materialcolordiffuse;
     
    Almost done... Finally a matrix for transforming normals into camera space and a vertex color value is calculated and exported.
     
    What's next
    I know, I know, that's not really a joy to wrap your head around. Part 2 goes quickly ove the fragment shader (will be much shorter) and then present the final result. Stamina!
  4. Rastar
    After some crunch time at work and a bit of vacation it's about time to continue with my "me too" attempts at using tessellation. Before proceeding with my terrain implementation I'd first like to give a bit of information about the new tessellation functionality in OpenGL. Again, the usual disclaimer: I'm just learning that stuff as well, so this will be basic explanations.
     
    Introduction
    Together with the subsequent geometry shader, tessellation is one of the stages where vertices can be created (or discarded). It operates on patches, i.e. sets of vertices (usually triangles or quads). When the application issues the draw call, it can specify the patch size, e.g. by calling
     

    glPatchParameteri(GL_PATCH_VERTICES​, 3​);
     
    This informs OpenGL that three subsequent vertices are to be considered a patch. The tessellation shader then subdivides the patch into additional triangles. Leadwerks currently only uses triangle tessellation, so that's what I'll consider here, but especially quad tessellation (patches of size four) is of interest as well (suggestion filed).
     
    An OpenGL4 tessellation shader actually consists of three separate stages (as in DirectX11, by the way), one fixed and two programmable:
    The first (programmable) stage is the tessellation control shader. Its main job is to specify the tessellation factors, ie how often the patch should be subdivided.
    The second (fixed) stage actually creates the additional vertices.
    The third (programmable) stage, the evaluation shader, provides the opportunity to process the vertices and their attributes before they are passed to the fragment stage.

    Variable names
    Before actually using the tessellation shader for anything meaningful, I'd first like to describe a pass-through shader: a tessellation stage that actually does nothing (apart from using up valuable resources...). In principle, the vertex and fragment shaders can be left as is when adding a tessellation shader. However, care must be taken with the variable names for data that is passed between stages. For example, a typical Leadwerks vertex shader might have the following input:
     

    in vec4 vertex_color; in vec2 vertex_texcoords0;
     
    and the corresponding output
     

    out vec4 vColor; out vec2 vTexCoords0;
     
    which is passed to the fragment shader by name
     

    in vec2 vTexCoords0; in vec4 vColor;
     
    Now, if you put in a tessellation shader, vColor and vTexCoords0 will be passed into the control shader, and you can't directly write back to those variables. So, you might have an intermediate variable pair
     

    out vec2 cTexCoords0[]; out vec4 cColor[];
     
    (those are arrays because the control shader operates on the whole patch) and then in the evaluation shader define
     

    //Inputs in vec2 cTexCoords0[]; in vec4 cColor[]; //Outputs out vec2 vTexCoords0; out vec4 vColor;
     
    so you're back to the original variable names that the fragment shader expects. By the way, there is one thing you might want to change in the vertex shader: You usually pass vertices in world space to the tessellation, not camera space. So, instead of the usual
     

    gl_Position = projectioncameramatrix * ex_vertexposition;
     
    in the vertex shader, you would rather do a simple
     

    gl_Position = ex_vertexposition;
     
    and do the vertex transformation later on in the evaluation shader.
     
    Control shader
    The tessellation control shader starts with a layout definition
     

    layout(vertices = 3) out;
     
    This defines the size of the patch that is submitted to the tessellation stage. While you will normally find a value of three here, it doesn't have to be that way - you could define a patch size of four and fill in the data for the missing vertex right in the control shader.
     
    Now, since we're heading for a pass-through shader, the vertex attributes should be handed down to the next stage unmodified:
     

    cTexCoords0[gl_InvocationID] = vTexCoords0[gl_InvocationID]; cColor[gl_InvocationID] = vColor[gl_InvocationID];
     
    and so on for the other interpolators. This needs a bit of explanation: First, as mentioned, the control shader has access to the whole patch data, so the attributes are defined as arrays rather than single values. Since the patch size is three, cColor[] for example will have the values cColor[0], cColor[1] and cColor[2], storing the colors for all three patch vertices. The control shader is executed once per vertex in the patch (before everything is passed to the next stage), and the predefined variable gl_InvocationID gives the current execution count (so it runs from 0 to 2).
     
    Now to the most important thing in the control stage: defining values for gl_TessLevelInner[] and gl_TessLevelOuter[] - the tessellation factors. They define how often the patch's edge (gl_TessLevelOuter) and interior (gl_TessLevelInner) should be subdivided. It depends on the patch type (triangle, quad) how many tessellation factors there are; for a triangle, there are one inner factor and three outer ones. I guess a picture is worth a thousand words:
     

     
    In this image, the triangle has an inner tessellation factor gl_TessLevelInner[0] of 3 (you need three small triangle to cross the large one), while glTessLevelOuter[0], glTessLevelOuter[1] and glTessLevelOuter[2] are all 2 (every edge is divided into two segments). Now, for a pass-through shader all tessellation factors should be equal to one, leaving the original trangle unharmed:
     

    if (gl_InvocationID==0) { gl_TessLevelInner[0] = 1.0; gl_TessLevelOuter[0] = 1.0; gl_TessLevelOuter[1] = 1.0; gl_TessLevelOuter[2] = 1.0; }
     
    The if statement in the beginning makes sure that we calculate those factors only once for the patch (remember: the whole control shader is executed for every vertex in the patch).
     
    And that's it for the (pass-through) control shader! This definition will then be send to the fixed tessellation stage which takes your recipe and creates many shiny new triangles (or in our case: not). Next time I'll go from there and describe the evaluation shader. So long!
     
    By the way: The title image is one of my hasty smartphone snapshots taken while going cycling on the Isle of Mallorca. This is the road to Sa Calobra, steeply winding down to the sea where you can relax in one of those nice cafés - after which you unfortunately have to ride back up again...
  5. Rastar
    At this point just a little WIP note to make sure Josh doesn't take over the blog space...
     
    I am still tuning the tessellation procedure. Temporarily (?) I have switched from my beatifully crafted concentric mesh rings to a uniform grid structure - it is more difficult to get a smooth LOD transition for those rings than for a uniform mesh, and I have enough problems with that already. So, I'll first try to get things right with the simpler mesh structure and then see if things work as well with those rings.
     
    I made wo little videos of my experiments. The first runs in wireframe mode and shows the tessellation change with the moving camera. Notice the intense "activity" when mving clode to the ground.
     
    http://steamcommunity.com/sharedfiles/filedetails/?id=229124907
     
    This is also visible in the second video which shows a similar scene in normal shading mode: The terrain "bubbles" when flying close over it. In part this certainly is due to the heightmap resolution: I had to switch from a one-channel, 8bit heightmap to a two-channel, 16bit format to get this effect under control (otherwise the terrain was boiling...). But still, the horizontal resolution is low, with a 4096 texture covering 16km. So when a new vertex is created or moved around, it might suddenly lie on a different texel of the heightmap and is displaced differently.
     
    http://steamcommunity.com/sharedfiles/filedetails/?id=229127339
     
    Well, a lot of stuff to do...
  6. Rastar
    After torturing you with a walk-through of my terrain vertex shader I will do the same harm again, this time using the fragment shader... Without further ado, here we go:
     

    #version 400 //Uniforms uniform sampler2D texture0;//diffuse map uniform sampler2D texture1;//normal map
     
    After the obligatory version information, this time two texture uniforms are declared, the (already used) diffuse map and an additional normal map.
     

    //Inputs in vec2 ex_texcoords0; in vec4 ex_color; in mat3 nmat;
     
    Using that "naming-convention trick", three variables of the vertex shader are wired into the fragment shader, namingly the UV coordinates, the vertex color and the matrix for transforming normals into view space.
     

    out vec4 fragData0; out vec4 fragData1; out vec4 fragData2; out vec4 fragData3;
     
    And now I have to take a deeper breath: Leadwerks 3.1 is using a deferred renderer. This means that all lighting calculations are done behind the scenes in a separate processing step taking place after the fragment shader has run. So you won't see lighting calculations for diffuse reflection, specular reflection etc. here, but rather a filling-up of several buffers:

    fragData0 contains the diffuse color (rgb) and transparency (alpha) of the fragment
    fragData1 stores the normal (rgb) and specularity (alpha)
    fragData2 holds the emissive color (rgb) and a flag for lighting on/off (alpha)
    fragData3 - no idea if this is being used and what it might hold

     

    vec4 outcolor = ex_color;
     
    That's it for the ouverture, now into the main play. The vertex color is passed to the outcolor variable. Actually, the result up to here would look something like this:
     

     
    An amorphous white mass with holes in it. Those holes are the cracks described earlier, caused by T-junctions between mesh levels. Hopefully, this can later be healed by a healthy dose of tessellation potion.
     

    outcolor *= vec4((texture(texture0,ex_texcoords0)).xyz, 1.0); fragData0 = outcolor;
     
    To make things a bit more interesting, the outcolor is multiplied by a sample of the diffuse map (retrieved using our self-calculated, global UV coordinates). The result is then stored in the diffuse buffer.
     

    vec3 normal = texture(texture1,ex_texcoords0).xzy * 2.0 - 1.0; normal = normalize(nmat*normal); fragData1 = vec4(normal*0.5+0.5,0.0);
     
    A bit more code - the normal map is sampled and "extended". Since a texture can store values between 0.0 and 1.0, but every (xyz) component of a normal can range from -1 to +1, those sampled values have to be brought into that range. The normal is transformed into view space, normalized again and stored in the normal buffer (back to a range of 0.0 to 1.0). The specularity is set to 0.0.
     

    int materialflags=1; fragData2 = vec4(0.0,0.0,0.0,materialflags/255.0);
     
    No emission, and setting the lighting flag to on. And here we go, the final result of our labor:
     

     
    What's next?
    I'll try to heal those cracks using tessellation, and I'll probably need some time to figure this out. To win that time, the next post will be about the general structure of a (pass-through) tessellation shader.
     
    PS: Of course the entry images to these two posts were done using klepto2's and shadmar's wonderful Posteffect Pack. Thanks you two!
  7. Rastar
    As described in my last post, I'd like to create a homegrown system for rendering large terrains. The basic idea is to use a set of planar mesh patches that are displaced in the vertical using a heightmap in the shaders, and using OpenGL 4's tessellation features to create a more detailed and optimally structured mesh during run-time. Also mentioned before was the fact that this might be a bit of trial and error, so bear with me, and if you see me running in the wrong direction - please let me know!
     
    Mesh patches
    To displace a mesh in the shaders I first need a mesh... I use an approach similar to the one described by http://developer.nvidia.com/GPUGems2/gpugems2_chapter02.html, but with a simpler patch structure. The basic idea of using such patches is to 1) use a small set of static vertex buffers to keep memory consumption and required CPU-GPU bandwidth low, and 2) to allow patches to be culled. Since the final mesh struture will be determined later on in the tessellation shaders, I think I don't have to be overly concerned with the patch structure. It only has to provide a good enough base for later tessellation, and to potentially cover a large terrain. I will use an arrangement like this:
     

     
    So there will be just one base patch, scaled and moved around to create a concentric ring of meshes centered on the viewer. Since I am using the Model:Instance() call, and Leadwerks 3.1 by default uses hardware instancing, this should mean that the terrain will actually be rendered in a single draw call (am I right here?). The innermost level (Level 0) is comprised of 4x4 patches with the highest resolution, while every higher-numbered level consists of twelve patches that are double the size of the next lower level (as seen by the grid structure in the lower left). There are three parameters to control the terrain generation:
    Patch size - the number of quads per side in a patch, so a value of 8 results in an 8x8 patch
    Total terrain size - the size of terrain in Leadwerks units (usually meters)
    Quad size level 0 - the size of a quad in Leadwerks units

    Using those values, the number of required levels is calculated, afterwards the quad size for level 0 is recalculated to make sure the numbers match. It is probably best to use powers of two for those numbers to reduce the possibility of rounding errors, which might result in vertices on the boundaries not lying on top of each other (e.g. 8 - 4096 - 1).
     
    Code, issues to solve
    I have attached the code for generating such a mesh structure below. To use it, just create a pivot entity in your scene and attached the script to it. When running the app, you should see something like this:
     

     
    Yes, not very impressive, I know - but we have to start somewhere. Unfortunately, Leadwerks scripts currently can't run in the editor, and there is no way to render a scene in wireframe mode outside the editor when using Lua (there is an unoffical C++ call for this, though), so we have to hope the triangles are all nicely aligned...
     
    EDIT: With shadmars wireframe material it is actually possible to visualize the triangle structure (thanks!). Looking good so far:
     

     
    There are a couple of issues with this code, among them
    the automatically calculated axis-aligned bounding boxes (AABB) don't know about the shader displacement later on, so I will have to manually correct them
    at the boundary between two levels there are lots of T junctions, where a vertex of the lower level lies on an edge (and not a vertex) of the next higher level. This will generate cracks in the surface once we displace the heights. I plan to solve this in the tessellation control stage (hope it works, just an idea up to now...)

    What's next?
    In the next post I'll use a shader to do the vertex displacement. This is actually something like a one-liner, so I'll use the remaining space to describe some of the things about shaders that I think I have understood.
     
    Oh, by the way: I took the entry picture to this post last week while cross-country skiing in the Alps on the German-Austrian border (I'm rubbish at it, but what the heck!). Just to give an idea what I'm aiming for - isn't nature just beautiful! I'll probably also create a dataset for that region as well, now that I have some reference images... Oh, and at the end of the month I'm going on a biking trip to Mallorca, my "test terrain", hopefully I'll be able to take some nice images there, too.
  8. Rastar
    After cluttering the regular Leadwerks forums with irregular posts and screenshots about terrains, tessellation and the like, I thought it might be better to collect those in a blog where I can happily mumble to myself without distracting anyone...
     
    So what's this about? Well, from day one of my (not so long ago) foray into game development I have been interested in creating large outdoor scenes. Now, most game engines don't support the required features for that out of the box, and I guess for good reason: Rendering large outdoor areas is difficult and resource-consuming, while playing in them tends to be a bit - boring. But I still find this to be a fascinating subject, and I also have a game in mind that requires large terrains (and hopefully won't be that boring...). And Leadwerks with its open low-level APIs makes it possible to implement some of the needed features myself.
     
    And why the blog's title "Tessellate This!"? Well, the new 3.1 version of Leadwerks does not only come with a new renderer, it also provides complete access to the additional shader stages of OpenGL 4, namely the tessellation control and evaluation shaders. That is something you won't find in many other engines, and it enables the implementation of some interesting algorithms for rendering large terrains etc.
     
    On my laundry list of things that I'd like to do (and mumble about) are
    simple shader programming for dummies (like myself)
    algorithms for large terrains (not virtual globes, but scenes of about 100x100km)
    rendering forests
    atmospheres, clouds
    roads
    water, especially oceans.

    As I said, I'm still pretty new to game dev, so don't expect any expert advice. But if you like you might follow me stumbling around, making some mistakes so you don't have to, and maybe producing the odd nice screenshot.
     
    So, here I go - mumblemumble...
  9. Rastar
    Yeah, I know, the suspense was too much... :-) Who am I to torture you any longer? Here is the eagerly awaited next step in the tessellation pipeline - the evaluation shader.
     
    After having defined the tessellation factors in the control shader, a fixed (non-programmable) tessellation stage takes over and creates all those little vertices and triangles for you. The result is then passed into the evaluation stage as patches whose size was defined in the control shader by the
     

    layout (vertices = 3) out;
     
    statement. Or rather, you're getting access to every single vertex of the patch (already existing and newly created) and the original vertex data at the same time. But we're getting ahead of ourselves, let's do this line by line.
     
    Ouverture
    Like the control stage, the evaluation shader starts with a layout definition statement:
     

    layout (triangles, equal_spacing, ccw) in;
     
    So we're expecting triangles to be passed in (the default). The next parameter can be a bit tricky: It defines how the triangles are distributed along the edges. There are three options:
    equal_spacing: every segment of the subdivided edge has the same length. This only makes sense for integral tessellation factors, so any fractional factors are rounded up or down. This has the disadvantage that new vertices pop into existence when e.g. you're increasing the factor from 3.4 to 3.6, which might be noticeable.
    fractional_odd_spacing: The tessellation factor is rounded down to the next lower odd integer (for example from 4.7 to 3). The edge is divided into (in this case) 3 equal-length segments and two additional smaller ones (taking up the space for the remainder of 1.7, each standing in for half of it).
    fractional_even_spacing: Same as above, but the tessellation factor is rounded down to the next lower even integer (here from 4.7 to 4).

    Sounds complicated, actually is a little complicated, but the simple rule is: In most cases where the tessellation factor is calculated (e.g. by a LOD formula) both fractional options give better results than the even one, because new vertices are created close to existing ones and smoothly glide along the edge with changing factors, giving less popping (you can see the effect in a video that I posted earlier on).
     
    The final parameter in the layout statement defines in what order you would like your vertices to passed in, counterclockwise (ccw) or clockwise (cw). This has nothing to do with backface culling or anything, it just gives the order in the array data:
     

    in ControlData { vec2 texcoord0; vec4 color; vec3 normal; vec3 binormal; vec3 tangent; } cData[]; out VertexData { vec2 texcoord0; vec4 color; vec3 normal; vec3 binormal; vec3 tangent; } vData;
     
    So you're getting passed in the data for the original patch vertices as an array and have to produce corresponding data for every vertex in the patch. The evaluation shader is called once for every vertex (newly created or not). But where is its data? Well, you have to create it, which brings us into the
     
    Main play
    All information that the tessellation stage gives you for any vertex is its postiion - in barycentric coordinates. These are basically numbers between 0 and 1 defining the distance of the new vertex from the original corner vertices of the patch. And you use that information to interpolate all vertex data from the corresponding values of the corner vertices. Don't get your head spinning - it basically comes down to applying the same formula for every vertex attribute that you would like to pass along (the formula for quads is different, of course):
     

    vData.texcoord0 = cData[0].texcoord0 * gl_TessCoord.x + cData[1].texcoord0 * gl_TessCoord.y + cData[2].texcoord0 * gl_TessCoord.z;
     
    The built-in vector variable gl_TessCoord contains the mentioned barycentric coordinates. As you can see, you basically multiply the vertex attribute for every corner vertex with the corresponding barycentric coordinate to get the value for the newly minted one - in this case for the first set of UV coordinates, but the same formula applies to color, normal etc. Only the position uses an additional built-in variable
     

    vec4 vPos = (gl_TessCoord.x * gl_in[0].gl_Position) + (gl_TessCoord.y * gl_in[1].gl_Position) + (gl_TessCoord.z * gl_in[2].gl_Position);
     
    As you probably have guessed, gl_in contains the original vertices and the field gl_Position their position. And last but least, here again you have to export the vertex position in the special variable gl_Position:
     

    gl_Position = projectioncameramatrix * vPos;
     
    If you remember, I removed the projection of vertices from world into camera space from the vertex shader, so that we can later calculate tessellation factors using world space data. But at some point before the fragment shader we have to apply this projection, and this is actually the last chance to do that...
     
    Finale
    well, nothing left to say, the pass-through tessellation shader is done! I have attached a small zip file, if you extract that into a project folder you will have
    the PassThrough.shader (in Shaders->Model)
    a PassThrough.mat (in Materials->Developer)
    a simple triangle.map (well, in Maps...) containing a box with the shader applied.

    The material displays the box in wireframe mode so you can see the effect (by the way, you have to run the game to see that, it won't be shown in-editor). Since a pass-through isn't too exciting, you might want to experiment a little with the shader: modify the tessellation factors (gl_TessLevelInner and gl_TessLevelOuter in the Control shader) and the edge subdivision mode (equal, odd, even) in the Evaluation shader.
     
    What's next?
    Those manual factors aren't of great help, so we'll dynamically calculate the tessellation based on camera distance and other factors. Hope to see you again!
×
×
  • Create New...