Landslag3D - A Unity like terrain system


{ Icelandic - Noun }
landslag n.
- countryside​

Landslag3D aims to be the definitive terrain system for GameMaker Studio 2.0 and beyond. I originally began work on this terrain system four years ago and have recently picked up working on it again. I'm aiming to create a terrain system that's so highly optimized that it can be used to display Elder Scrolls sized terrains with minimal impact on your performance.

Here are some early screenshots of the system during it's original development:

And now here are some shots of the system in 2019:

There is still much to be done on the system, I'm only about 20% complete with the quadtree implementations, and I still need to construct the terrain editor app that will accompany the system and make it's use super seamless.

Here are some of the features currently supported by L3D:
  • Patching
  • Patch Lods
  • Normals Baked to texture ( so lower lods don't lose detail )
  • Automatic Frustum Culling ( only draw what the current view matrix can see )
  • Height based terrain collision
  • Multi-texture support with height biased blending
And here are some of the planned/in devlopment features:
  • Quadtree based optimizations.
  • Patch Instancing
  • Polygonal Terrain Collision
  • Improved generation speed
  • Terrain stitching
  • Foliage instancing
And here are some of the planned features of the editor:
  • Import heightmap from standard images or floating point TIFF formats
  • Sculpt terrain with textured stamps
  • Erode the terrain either with a brush, or across the whole surface
  • Set up multiple materials and change materials per patch
  • Paint sections of foliage into your terrain.

I'd love to know what you guys think of the project so far, and if you have any questions or suggestions please feel free to share them. The goal for this project is to have something robust and capable of handling pretty much all of your needs pretty much automatically. Easy to use terrain for everyone :)


@Amon Thanks a bunch! If anyone has any ideas for features you'd like to see, let me know!

My goal is to render massive terrains in GM that look like this while still retaining high frame rates:
Last edited:


I'm making a 3d game called Moor, which is a dungeon crawler. However I've been thinking if I ever were to make a sequel to the game I'd want it to be a open world rpg. If so, I'd love to use your engine if possible :)


Thanks for the support guys, been really busy working on my game lately. Before I can get too much farther with this, I'll have to implement my own TGA parser to reduce the stepping artifacts present with standard png heightmaps.
Last edited by a moderator:


Hey fellas, just wanted to post a little update here. I haven't touched this in a bit, been bouncing back and forth between projects that actually pay me lol.

Anywho, I've been experimenting with larger scale terrain rendering, and various optimization techniques. Check this out:

So here we have a huge super scaled up terrain (scale is hard to convey with a single image ) You can see that the CPU is totally hogging all of the render speed since this is just a brute force render of nearly all of the terrain chunks. However if you see that little red blip in that bar at the top, you can tell the actually vertex submissions themselves have almost no impact on the performance! This is great news considering how huge the terrain here is.

I'm going to be working on improving the frustum culling methods that I'm using, as well as implementing proper quad tree walking to determine which chunks are visible without having to cycle through every single chunk! Things are looking bright for Landslag3D!


So for the past few days I've been experimenting with rendering a realistic atmosphere, and in the process stumbled upon a 2d atmospheric scatter shader that I think some of you might enjoy. Converted this gem to work with GMS2 thanks to a little help from Misu on the xordev discord server:

Here is the GLSL ES shader:

Vertex Shader:
attribute vec4 in_Position;                  // (x,y,z)
attribute vec2 in_TextureCoord;              // (u,v)
attribute vec4 in_Colour;                    // (r,g,b,a)
//attribute vec3 in_Normal;                  // (x,y,z)  

varying vec2 v_vTexcoord;
varying vec3 v_vPosition;
varying vec4 v_vColour;

void main() {

    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
    gl_Position.z = gl_Position.w; // set z to camera.far
    v_vPosition =;
Fragment Shader:
uniform vec3 sunPosition;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec3 v_vPosition;
#define PI 3.141592
#define iSteps 16
#define jSteps 8
vec2 rsi(vec3 r0, vec3 rd, float sr) {
    // ray-sphere intersection that assumes
    // the sphere is centered at the origin.
    // No intersection when result.x > result.y
    float a = dot(rd, rd);
    float b = 2.0 * dot(rd, r0);
    float c = dot(r0, r0) - (sr * sr);
    float d = (b*b) - 4.0*a*c;
    if (d < 0.0) return vec2(1e5,-1e5);
    return vec2(
        (-b - sqrt(d))/(2.0*a),
        (-b + sqrt(d))/(2.0*a)
vec3 atmosphere(vec3 r, vec3 r0, vec3 pSun, float iSun, float rPlanet, float rAtmos, vec3 kRlh, float kMie, float shRlh, float shMie, float g) {
    // Normalize the sun and view directions.
    pSun = normalize(pSun);
    r = normalize(r);
    // Calculate the step size of the primary ray.
    vec2 p = rsi(r0, r, rAtmos);
    if (p.x > p.y) return vec3(0,0,0);
    p.y = min(p.y, rsi(r0, r, rPlanet).x);
    float iStepSize = (p.y - p.x) / float(iSteps);
    // Initialize the primary ray time.
    float iTime = 0.0;
    // Initialize accumulators for Rayleigh and Mie scattering.
    vec3 totalRlh = vec3(0,0,0);
    vec3 totalMie = vec3(0,0,0);
    // Initialize optical depth accumulators for the primary ray.
    float iOdRlh = 0.0;
    float iOdMie = 0.0;
    // Calculate the Rayleigh and Mie phases.
    float mu = dot(r, pSun);
    float mumu = mu * mu;
    float gg = g * g;
    float pRlh = 3.0 / (16.0 * PI) * (1.0 + mumu);
    float pp = 1.0 + gg - 2.0 * mu * g;
    float pMie = 3.0 / (8.0 * PI) * ((1.0 - gg) * (mumu + 1.0)) / (sign(pp)*pow(abs(pp), 1.5) * (2.0 + gg));
    // Sample the primary ray.
    for (int i = 0; i < iSteps; i++) {
        // Calculate the primary ray sample position.
        vec3 iPos = r0 + r * (iTime + iStepSize * 0.5);
        // Calculate the height of the sample.
        float iHeight = length(iPos) - rPlanet;
        // Calculate the optical depth of the Rayleigh and Mie scattering for this step.
        float odStepRlh = exp(-iHeight / shRlh) * iStepSize;
        float odStepMie = exp(-iHeight / shMie) * iStepSize;
        // Accumulate optical depth.
        iOdRlh += odStepRlh;
        iOdMie += odStepMie;
        // Calculate the step size of the secondary ray.
        float jStepSize = rsi(iPos, pSun, rAtmos).y / float(jSteps);
        // Initialize the secondary ray time.
        float jTime = 0.0;
        // Initialize optical depth accumulators for the secondary ray.
        float jOdRlh = 0.0;
        float jOdMie = 0.0;
        // Sample the secondary ray.
        for (int j = 0; j < jSteps; j++) {
            // Calculate the secondary ray sample position.
            vec3 jPos = iPos + pSun * (jTime + jStepSize * 0.5);
            // Calculate the height of the sample.
            float jHeight = length(jPos) - rPlanet;
            // Accumulate the optical depth.
            jOdRlh += exp(-jHeight / shRlh) * jStepSize;
            jOdMie += exp(-jHeight / shMie) * jStepSize;
            // Increment the secondary ray time.
            jTime += jStepSize;
        // Calculate attenuation.
        vec3 attn = exp(-(kMie * (iOdMie + jOdMie) + kRlh * (iOdRlh + jOdRlh)));
        // Accumulate scattering.
        totalRlh += odStepRlh * attn;
        totalMie += odStepMie * attn;
        // Increment the primary ray time.
        iTime += iStepSize;
    // Calculate and return the final color.
    return iSun * (pRlh * kRlh * totalRlh + pMie * kMie * totalMie);
void main() {
   //vec3 sunPosition = sunPosition;
    vec3 color = atmosphere(
        normalize( v_vPosition ),           // normalized ray direction
        vec3(0,6372e3,0),               // ray origin
        sunPosition,                        // position of the sun
        22.0,                           // intensity of the sun
        6371e3,                         // radius of the planet in meters
        6471e3,                         // radius of the atmosphere in meters
        vec3(5.5e-6, 13.0e-6, 22.4e-6), // Rayleigh scattering coefficient
        21e-6,                          // Mie scattering coefficient
        8e3,                            // Rayleigh scale height
        1.2e3,                          // Mie scale height
        0.758                           // Mie preferred scattering direction
    // Apply exposure.
    color = 1.0 - exp(-1.0 * color);
    gl_FragColor = vec4(color, 1.0);

Managed to get this working in 3D now with some help from Xor and Thesnidr:

Experimenting with the fog shader:
Last edited:


Got multiple render passes set up. I've begun project formatting, and have a system in place that will handle the rendering of all 3D objects automatically to each different render buffer with a way for users to tell what pass is being handled currently to allow for proper shader switching.

Here's a look at the Regular buffer split with the encoded depth buffer:

This is all leading to the next big feature, atmospheric scatter through volumetric fog possibly with support for light scatter, something like this:
Last edited:


Yeah I do plan to release it at some point. I have just been busy working on my video game project. Once that's complete I'll have far more free time to continue developing this.


Damn, this just keeps looking better! What with all the crazy 3D stuff I've been seeing people make, building 3D games in GM is starting to look more and more like a legitimate option.