Jump to content

Terrain Building API in Leadwerks 5 Beta



An often-requested feature for terrain building commands in Leadwerks 5 is being implemented. Here is my script to create a terrain. This creates a 256 x 256 terrain with one terrain point every meter, and a maximum height of +/- 50 meters:

--Create terrain
local terrain = CreateTerrain(world,256,256)

Here is what it looks like:


A single material layer is then added to the terrain.

--Add a material layer
local mtl = LoadMaterial("Materials/Dirt/dirt01.mat")
local layerID = terrain:AddLayer(mtl)

We don't have to do anything else to make the material appear because by default the entire terrain is set to use the first layer, if a material is available there:


Next we will raise a few terrain points.

--Modify terrain height
for x=-5,5 do
	for y=-5,5 do
		h = (1 - (math.sqrt(x*x + y*y)) / 5) * 20
		terrain:SetElevation(127 + x, 127 + y, h)

And then we will update the normals for that whole section, all at once. Notice that we specify a larger grid for the normals update, because the terrain points next to the ones we modified will have their normals affected by the change in height of the neighboring pixel.

--Update normals of modified and neighboring points
terrain:UpdateNormals(127 - 6, 127 - 6, 13, 13)

Now we have a small hill.


Next let's add another layer and apply it to terrain points that are on the side of the hill we just created:

--Add another layer
mtl = LoadMaterial("Materials/Rough-rockface1.json")
rockLayerID = terrain:AddLayer(mtl)

--Apply layer to sides of hill
for x=-5,5 do
	for y=-5,5 do
		slope = terrain:GetSlope(127 + x, 127 + y)
		alpha = math.min(slope / 15, 1.0)
		terrain:SetMaterial(rockLayerID, 127 + x, 127 + y, alpha)

We could improve the appearance by giving it a more gradual change in the rock layer alpha, but it's okay for now.


This gives you an idea of the basic terrain building API in Leadwerks 5, and it will serve as the foundation for more advanced terrain features. This will be included in the next beta.

  • Like 4


Recommended Comments

Yes, you can manipulate the terrain in real-time with no problems. Collision and everything will work just fine.

  • Like 1

Share this comment

Link to comment

And what about Navmesh ? This should be updated too, the problem was regarding performances on bigger maps....

  • Like 1

Share this comment

Link to comment
1 hour ago, Marcousik said:

And what about Navmesh ? This should be updated too, the problem was regarding performances on bigger maps....

A static cache will be used for the terrain and other static objects, instead of reprocessing everything every time something changes.

Share this comment

Link to comment
2 hours ago, Josh said:

A static cache will be used for the terrain and other static objects, instead of reprocessing everything every time something changes.

Does this mean you're not planning on implementing a dynamic navmesh, like for doors that open to let characters through?  Or is that something different?

Share this comment

Link to comment

Objects are separated into static and dynamic. When a dynamic object moves only the dynamic objects gets recalculated. The static data stays the same.

Shadow maps work with the same principle, so this is actually convenient because static objects can be marked as such for both purposes.

  • Like 1

Share this comment

Link to comment

Here is the code for terrain.lua

--Get the primary display
local displaylist = ListDisplays()
local display = displaylist[1];
if display == nil then RuntimeError("Primary display not found.") end
local displayscale = display:GetScale()

--Create a window
local window = CreateWindow(display, "Terrain", 0, 0, math.min(1280 * displayscale.x, display.size.x), math.min(720 * displayscale.y, display.size.y), WINDOW_TITLEBAR + WINDOW_CENTER)

--Create a rendering framebuffer
local framebuffer = CreateFramebuffer(window);

--Create a world
local world = CreateWorld()

--Create a camera
local camera = CreateCamera(world)

--Create a light
local light = CreateLight(world,LIGHT_DIRECTIONAL)

--Create terrain
local terrain = CreateTerrain(world,256,256)

--Add a material layer
local mtl = LoadMaterial("Materials/Dirt/dirt01.mat")
local layerID = terrain:AddLayer(mtl)
terrain:SetMaterial(layerID, 0, 0, 256, 256, 1)


	local p = {}

	local permutation = {151,160,137,91,90,15,
	  190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
	  88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
	  102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
	  135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
	  223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
	  129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
	  251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
	  49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,

	for i = 0, 255 do
	  p[i] = permutation[i + 1]
	  p[256 + i] = permutation[i + 1]

	local function fade(t)
	  return t * t * t * (t * (t * 6 - 15) + 10)

	local function lerp(t, a, b)
	  return a + t * (b - a)

	local function grad(hash, x, y, z)
	  local h, u, v = hash % 16
	  if h < 8 then u = x else u = y end
	  if h < 4 then v = y elseif h == 12 or h == 14 then v = x else v = z end
	  local r
	  if h % 2 == 0 then r = u else r = -u end
	  if h % 4 == 0 then r = r + v else r = r - v end
	  return r

	local function perlin(x, y, z)
	  y = y or 0
	  z = z or 0
	  local X = math.floor(x % 255)
	  local Y = math.floor(y % 255)
	  local Z = math.floor(z % 255)
	  x = x - math.floor(x)
	  y = y - math.floor(y)
	  z = z - math.floor(z)
	  local u = fade(x)
	  local v = fade(y)
	  local w = fade(z)
	  A   = p[X     ] + Y
	  AA  = p[A     ] + Z
	  AB  = p[A + 1] + Z
	  B   = p[X + 1] + Y
	  BA  = p[B     ] + Z
	  BB  = p[B + 1] + Z
	  return lerp(w, lerp(v, lerp(u, grad(p[AA      ], x    , y     , z     ),
																	 grad(p[BA      ], x - 1, y     , z     )),
													 lerp(u, grad(p[AB      ], x    , y - 1, z      ),
																	 grad(p[BB      ], x - 1, y - 1, z      ))),
									 lerp(v, lerp(u, grad(p[AA + 1], x      , y     , z - 1), 
																	 grad(p[BA + 1], x - 1, y       , z - 1)),
													 lerp(u, grad(p[AB + 1], x      , y - 1, z - 1),
																	 grad(p[BB + 1], x - 1, y - 1, z - 1))))
	 function fbm(x, y, z, octaves, lacunarity, gain)
	  octaves = octaves or 8
	  lacunarity = lacunarity or 2
	  gain = gain or 0.5
	  local amplitude = 1.0
	  local frequency = 1.0
	  local sum = 0.0
	  for i = 0, octaves do
			sum = sum + amplitude * perlin(x * frequency, y * frequency, z * frequency)
			amplitude = amplitude * gain
			frequency = frequency * lacunarity
	  return sum

--- ### This should match terrain created
local terrainsize=256
local octaves=12  		-- more is finer noise but slower
local lacunarity=1.9		-- size of noise
local gain=0.6		-- height gain
for i=-127,127 do
    for j=-127,127 do
		local noise=fbm(i/terrainsize, 11, j/terrainsize, octaves, lacunarity, gain)
		terrain:SetElevation(127+i,127+j, 15+noise*20)

--Update normals of modified and neighboring points
terrain:UpdateNormals(0, 0, 256, 256)

--Add another layer
mtl = LoadMaterial("Materials/Rough-rockface1.json")
rockLayerID = terrain:AddLayer(mtl)

--Apply layer to sides of hill
for x=-127,127 do
	for y=-127,127 do
		slope = terrain:GetSlope(127 + x, 127 + y)
		alpha = math.min(slope / 15, 1.0)
		terrain:SetMaterial(rockLayerID, 127 + x, 127 + y, alpha)

--Main loop
while window:Closed() == false do




  • Like 1

Share this comment

Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By jen in jen's Blog 0
      My small project will be called Foregate, it will be a dark medieval Diablo style single player action RPG.
      The graphics will be simple, no PBR, 256x256 map, reasonably low-res models.
      Camera style? Top-down-ish I think? Like in Diablo exactly - and because the camera is not directly in-front of the 3 models, I can get away with low-resolution assets - bonus. Also, with top-down view, I won't have to worry about high resolution sky-boxes. 
      What's my plan for this project?
      I plan to make this project as small and as simple as possible, possibly release it as open-source, and have fun with it of course.
      My previous experience with game development (1-2 years ago?) was amateurish I think, still is now. I want to give it a go again, this time with experience although my skill in C++ is not really that good? Maybe I can improve it in this project.
      More about the game
      The content is not set in stone yet but I have a general idea of how the mechanics is going to look and feel - Diablo-ish obviously. It'll have monsters (ancient & mythical probably), loot when killing a monster, gold as in-game currency, visual grid inventory, player stats (level, strength, agility, vitality, energy, &c.). 
      The game will be single-player. Possibly a coop multiplayer also? I don't have any interest in making massive multi-player. 
      I started my development yesterday with the basic preparations (setting up project environment, &c.), today I made my first step in developing the core components; worker class, game state, task class.
      I have a game state that keeps a single source of truth for the entire application; all game data will be stored in this class as "states". 
      I also have a "Worker" which will do the processing of tasks in the game.
      I also have "object" class, this can be a monster, the player, a weapon, a prop, or an NPC.
      So the idea is to have a CQRS type of interaction between the classes and the data. Any action in the game will be interpreted as "Task" for the Worker class. The worker class iterates through the Task. Tasks can be created by any class interfaced with the Worker class trough "addNewTask" and the new tasks can be of a certain type i.e.: ATTACK, IDLE, SAVE_GAME, EXIT_GAME, the new task will also have a payload data and it's processed according to its task type e.g. an ATTACK with payload "{ Damage: 10, Target: MonsterA }" will reduce the health of MonsterA by 10 - the worker class will change the game state; find MonsterA in MonsterState and reduce its health by 10. 
      I think it's advantageous to have this type of centralized module where all actions are processed; I can do all sorts of procedures during the processes, maybe debug data, filter actions, mutate payloads, and such.
      How much time am I going to put into this?
      A couple of hours a day for 3 days a week maybe.
      So it's all a rough sketch for now and it's heading the right direction. I'll have more to report later on. 

      This is Forgate Castle, minus the castle, in the map Forgate; the starting location for the player. The fortification will have merchants, and quest givers.
    • By jen in jen's Blog 4
      I've got a bit of free time on my hands for a while. I plan to take up Leadwerks again and come up with a simple project to have fun with.
      I use VSCode at work and I don't have a Windows machine at the moment. Codeblocks is a bit dated now and I'm not sure if it has all the features to fit my workflow so I can't use it. 
      It's a bit fiddly to build C++ apps on VSCode. There's a prescribed method of setting up your C++ project in the official hubs but I don't think those are necessary in my case, I just need a script that builds the project which I have below.
      By the way, VSCode website is here: https://code.visualstudio.com/
      Have fun.
      Save as build.sh in your project's root folder and run using build.sh or build.sh -r for release. Works for Stable version LW and any previous versions(?) - at least v4.5 (archived). BETA branch (4.6?) doesn't seem to build for me, missing Leadwerks.a file.
      Disclaimer: not really fully tested for bigger projects? I've only tested this with sample projects that have no additional project folder structure in its Source so I'm not sure if the script will propagate its search for CPP files thoroughly and process each file successfully in bigger projects. Back-up project files before running the script for the first time please.
      Updated: 26/02/2020
      #!/bin/sh if [ $1 == "-h" ]; then echo "Build script is set to DEBUG by default. Set -r in first argument of the script to build for release."; exit 1; fi if [ $1 != "" ] && [ $1 != "-r" ] && [ $1 != "-h" ]; then echo "Type build.sh -h to see brief help description."; exit 1; fi debugFlags='-g -DDEBUG -D_DEBUG'; debugLibPath='Debug'; PROJECT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) LEADWERKS_PATH=~/.local/share/Steam/steamapps/common/Leadwerks declare a compiledFiles; declare a includedFilePaths; if [ $1 == "-r" ]; then debugFlags=''; debugLibPath='Release'; fi truncate -s 0 $PROJECT_PATH/build.log echo "Leadwerks Path: $LEADWERKS_PATH" echo "Project Path: $PROJECT_PATH" echo "Compiling source..." if [ ! -d "$PROJECT_PATH/Compilations" ]; then mkdir $PROJECT_PATH/Compilations; fi if [ ! -d "$PROJECT_PATH/Compilations/$debugLibPath" ]; then mkdir $PROJECT_PATH/Compilations/$debugLibPath; fi for f in $(find $PROJECT_PATH/Source -name '*.h' ); do includedFilePaths+=("-I $(dirname "${f}")") done includedFilesJoined=$(printf "%s " "${includedFilePaths[@]}") includedFilesJoined="-${includedFilesJoined:1}" for f in $(find $PROJECT_PATH/Source -name '*.cpp' ); do originalSourcePath=$PROJECT_PATH/Source; compilationsPath=$PROJECT_PATH/Compilations/$debugLibPath; compiledFilePath=${f/.cpp/'.o'}; compiledFilePath=${compiledFilePath/$originalSourcePath/$compilationsPath}; mkdir -p $(dirname "${compiledFilePath}") g++ -std=c++0x $debugFlags -w -fexceptions -msse3 -DDG_DISABLE_ASSERT -DZLIB \ -DPLATFORM_LINUX -D_NEWTON_STATIC_LIB -DFT2_BUILD_LIBRARY -DOPENGL \ -Dunix -D__STEAM__ -D_POSIX_VER -D_POSIX_VER_64 -DDG_THREAD_EMULATION \ -D_STATICLIB -DDG_USE_THREAD_EMULATION -DGL_GLEXT_PROTOTYPES -DLEADWERKS_3_1 \ -DLUA_USE_LINUX -D_GLIBCXX_USE_CXX11_ABI=1 -D_CUSTOM_JOINTS_STATIC_LIB -fPIC -O2 -std=c++0x \ $includedFilesJoined \ -I $LEADWERKS_PATH/Include/Libraries/VHACD/src/VHACD_Lib/inc \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/sdk/dMath \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/sdk/dgNewton \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/sdk/dContainers \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/sdk/dgCore \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/sdk/dgTimeTracker \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/sdk/dgPhysics \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/sdk/dCustomJoints \ -I $LEADWERKS_PATH/Include/Libraries/tolua++-1.0.93/include \ -I $LEADWERKS_PATH/Include/Libraries/lua-5.1.4 \ -I $LEADWERKS_PATH/Include/Libraries/freetype-2.4.7/include \ -I $LEADWERKS_PATH/Include/Libraries/enet-1.3.1/include \ -I $LEADWERKS_PATH/Include/Libraries/RecastNavigation/DebugUtils/Include \ -I $LEADWERKS_PATH/Include/Libraries/RecastNavigation/Detour/Include \ -I $LEADWERKS_PATH/Include/Libraries/RecastNavigation/DetourCrowd/Include \ -I $LEADWERKS_PATH/Include/Libraries/RecastNavigation/DetourTileCache/Include \ -I $LEADWERKS_PATH/Include/Libraries/RecastNavigation/Recast/Include \ -I $LEADWERKS_PATH/Include/Libraries/libvorbis/include \ -I $LEADWERKS_PATH/Include/Libraries/NewtonDynamics/packages/thirdParty/timeTracker \ -I $LEADWERKS_PATH/Include/Libraries/libvorbis/lib \ -I $LEADWERKS_PATH/Include/Libraries/libogg/include \ -I $LEADWERKS_PATH/Include \ -I $LEADWERKS_PATH/Include/Libraries/zlib-1.2.5 \ -I $LEADWERKS_PATH/Include/Libraries/zlib-1.2.5/contrib/minizip \ -I $LEADWERKS_PATH/Include/Libraries/freetype-2.4.7/include/freetype \ -I $LEADWERKS_PATH/Include/Libraries/freetype-2.4.7/include/freetype/config \ -I $LEADWERKS_PATH/Include/Libraries/LuaJIT/dynasm \ -I $LEADWERKS_PATH/Include/Libraries/glew-1.6.0/include \ -c $f -o $compiledFilePath; compiledFiles+=($compiledFilePath) done compiledFilesJoined=$(printf "%s " "${compiledFiles[@]}") compiledFilesJoined="/${compiledFilesJoined:1}" echo "Building project..."; g++ -o $PROJECT_PATH/${PWD##*/} $compiledFilesJoined -s "$LEADWERKS_PATH/Library/Linux/$debugLibPath/Leadwerks.a" -ldl \ -lopenal -lGL -lGLU "$LEADWERKS_PATH/Library/Linux/libluajit.a" $PROJECT_PATH/libsteam_api.so \ -lX11 -lXext -lXrender -lXft -lpthread -lcurl "$LEADWERKS_PATH/Library/Linux/libopenvr_api.so" echo "Build complete. See build.log for result summary." About the game: I'm kind of disappointed with the Action RPGs released recently. I want to give the genre a shot, present my own interpretation of how it should look like? Dark, medieval, less electric, modest if not rare use of glow shaders... maybe?

    • By Josh in Josh's Dev Blog 0
      A new update is available for beta testers. This adds a new LOD system to the terrain system, fixes the terrain normals, and adds some new features. The terrain example has been updated ans shows how to apply multiple material layers and save the data.

      Terrain in LE4 uses a system of tiles. The tiles are rendered at a different resolution based on distance. This works great for medium sized terrains, but problems arise when we have very large view distances. This is why it is okay to use a 4096x4096 terrain in LE4, but your camera range should only show a portion of the terrain at once. A terrain that size will use 1024 separate tiles, and having them all onscreen at once can cause slowdown just because of the number of objects. That have to be culled and drawn.

      Another approach is to progressively divide the terrain up into quadrants starting from the top and working down to the lowest level. When a box is created that is a certain distance from the camera, the algorithm stops subdividing it and draws a tile. The same resolution tile is drawn over and over, but it is stretched to cover different sized areas.

      This approach is much better suited to cover very large areas. At the furthest distance, the entire terrain will be drawn with just one single 32x32 patch. Here it is in action with a 2048x2048 terrain, the same size as The Zone:
      This example shows how to load a heightmap, add some layers to it, and save the data, or load the data we previously saved:
      --Create terrain local terrain = CreateTerrain(world,2048,32) terrain:SetScale(1,150,1) --Load heightmap terrain:LoadHeightmap("Terrain/2048/2048.r16", VK_FORMAT_R16_UNORM) --Add base layer local mtl = LoadMaterial("Materials/Dirt/dirt01.mat") local layerID = terrain:AddLayer(mtl) --Add rock layer mtl = LoadMaterial("Materials/Rough-rockface1.json") rockLayerID = terrain:AddLayer(mtl) terrain:SetLayerSlopeConstraints(rockLayerID, 35, 90, 25) --Add snow layer mtl = LoadMaterial("Materials/Snow/snow01.mat") snowLayerID = terrain:AddLayer(mtl) terrain:SetLayerHeightConstraints(snowLayerID, 50, 1000, 8) terrain:SetLayerSlopeConstraints(snowLayerID, 0, 35, 10) --Normals if FileType("Terrain/2048/2048_N.raw") == 0 then terrain:UpdateNormals() terrain:SaveNormals("Terrain/2048/2048_N.raw") else terrain:LoadNormals("Terrain/2048/2048_N.raw") end --Layers if FileType("Terrain/2048/2048_L.raw") == 0 or FileType("Terrain/2048/2048_A.raw") == 0 then terrain:SetLayer(rockLayerID, 1) terrain:SetLayer(snowLayerID, 1) terrain:SaveLayers(WriteFile("Terrain/2048/2048_L.raw")) terrain:SaveAlpha(WriteFile("Terrain/2048/2048_A.raw")) else terrain:LoadLayers("Terrain/2048/2048_L.raw") terrain:LoadAlpha("Terrain/2048/2048_A.raw") end The x86 build configurations have also been removed from the game template project. This is available now in the beta tester forum.
  • Create New...