Jump to content

Virtual Texture Terrain

Josh

11,992 views

The Leadwerks 2 terrain system was expansive and very fast, which allowed rendering of huge landscapes. However, it had some limitations. Texture splatting was done in real-time in the pixel shader. Because of the limitations of hardware texture units, only four texture units per terrain were supported. This limited the ability of the artist to make terrains with a lot of variation. The landscapes were beautiful, but somewhat monotonous.

With the Leadwerks 3 terrain system, I wanted to retain the advantages of terrain in Leadwerks 2, but overcome some of the limitations. There were three different approaches we could use to increase the number of terrain textures.

  • Increase the number of textures used in the shader.
  • Allow up to four textures per terrain chunk. These would be determined either programmatically based on which texture layers were in use on that section, or defined by the artist.
  • Implement a virtual texture system like id Software used in the game "Rage".

Since Leadwerks 3 runs on mobile devices as well as PC and Mac, we couldn't use any more texture units than we had before, so the first option was out. The second option is how Crysis handles terrain layers. If you start painting layers in the Crysis editor, you will see when "old" layers disappear as you paint new ones on. This struck me as a bad approach because it would either involve the engine "guessing" which layers should have priority, or involve a tedious process of user-defined layers for each terrain chunk.

A virtual texturing approach seemed liked the ideal choice. Basically, this would render near sections of the terrain at a high resolution, and far sections of the terrain at low resolutions, with a shader that chose between them. If done correctly, the result should be the same as using one impossibly huge texture (like 1,048,576 x 1,048,576 pixels) at a much lower memory cost. However, there were some serious challenges to be overcome, so much so that I added a disclaimer in our Kickstarter campaign basically saying "this might not work"..

Previous Work

id Software pioneered this technique with the game Rage (a previous implementation was in Quake Wars). However, id's "megatexture" technique had some serious downsides. First, the data size requirements of storing completely unique textures for the entire world were prohibitive. "Rage" takes about 20 gigs of hard drive space, with terrains much smaller than the size I wanted to be able to use. The second problem with id's approach is that both games using this technique have some pretty blurry textures in the foreground, although the unique texturing looks beautiful from a distance.

blogentry-1-0-99422000-1373307489_thumb.jpg

I decided to overcome the data size problem by dynamically generating the megatexture data, rather than storing in on the hard drive. This involves a pre-rendering step where layers are rendered to the terrain virtual textures, and then the virtual textures are applied to the terrain. Since id's art pipeline was basically just conventional texture splatting combined with "stamps" (decals), I didn't see any reason to permanently store that data. I did not have a simple solution to the blurry texture problem, so I just went ahead and started implementing my idea, with the understanding that the texture resolution issue could kill it.

I had two prior examples to work from. One was a blog from a developer at Frictional Games (Amnesia: The Dark Descent and Penumbra). The other was a presentation describing the technique's use in the game Halo Wars. In both of these games, a fixed camera distance could be relied on, which made the job of adjusting texture resolution much easier. Leadwerks, on the other hand, is a general-purpose game engine for making any kind of game. Would it be possible to write an implementation that would provide acceptable texture resolution for everything from flight sims to first-person shooters? I had no idea if it would work, but I went forward anyway.

Implementation

Because both Frictional Games and id had split the terrain into "cells" and used a texture for each section, I tried that approach first. Our terrain already gets split up and rendered in identical chunks, but I needed smaller pieces for the near sections. I adjusted the algorithm to render the nearest chunks in smaller pieces. I then allocated a 2048x2048 texture for each inner section, and used a 1024x1024 texture for each outer section:

blogentry-1-0-97225800-1373308168_thumb.jpg

The memory requirements of this approach can be calculated as follows:

1024 * 1024 * 4 * 12 = 50331648 bytes

2048 * 2048 * 4 * 8 = 134217728

Total = 184549376 bytes = 176 megabytes

176 megs is a lot of texture data. In addition, the texture resolution wasn't even that good at near distances. You can see my attempt with this approach in the image below. The red area is beyond the virtual texture range, and only uses a single low-res baked texture. The draw distance was low, the memory consumption high, and the resolution was too low.

blogentry-1-0-99321200-1373308232_thumb.jpg

This was a failure, and I thought maybe this technique was just impractical for anything but very controlled cases in certain games. I wasn't ready to give up yet without trying one last approach. Instead of allocating textures for a grid section, I tried creating a radiating series of textures extending away from the camera:

blogentry-1-0-74950800-1373308648_thumb.jpg

The resulting resolution wasn't great, but the memory consumption was a lot lower, and terrain texturing was now completely decoupled from the terrain geometry. I found by adjusting the distances at which the texture switches, I could get a pretty good resolution in the foreground. I was using only three texture stages, so I increased the number to six and found I could get a good resolution at all distances, using just six 1024x1024 textures. The memory consumption for this was just 24 megabytes, a very reasonable number. Since the texturing is independent from terrain geometry, the user can fine-tune the texture distances to accommodate flight sims, RPGs, or whatever kind of game they are making.

blogentry-1-0-17294200-1373309733_thumb.jpg

The last step was to add some padding around each virtual texture, so the virtual textures did not have to be complete redrawn each time the camera moves. I used a value of 0.25 the size of the texture range so the various virtual textures only get redrawn once in a while.

Advantages of Virtual Textures

First, because the terrain shader only has to perform a few lookups each pixel with almost no math, the new terrain shader runs much faster than the old one. When the bottleneck for most games is the pixel fillrate, this will make Leadwerks 3 games faster. Second, this allows us to use any number of texture layers on a terrain, with virtually no difference in rendering speed. This gives artists the flexibility to paint anything they want on the terrain, without worrying about budgets and constraints. A third advantage is that this allows the addition of "stamps", which are decals rendered directly into the virtual texture. This allows you to add craters, clumps of rocks, and other details directly onto the terrain. The cost of rendering them in is negligible, and the resulting virtual texture runs at the exact same speed, no matter how much detail you pack into it. Below you can see a simple example. The smiley face is baked into the virtual texture, not rendered on top of the terrain:

blogentry-1364-0-87706300-1373609026_thumb.jpg

Conclusion

The texture resolution problem I feared might make this approach impossible was solved by using a graduated series of six textures radiating out around the camera. I plan to implement some reasonable default settings, and it's only a minor hassle for the user to adjust the virtual texture draw distances beyond that.

Rendering the virtual textures dynamically eliminates the high space requirements of id's megatexture technique, and also gets rid of the problems of paging texture data dynamically from the hard drive. At the same time, most of the flexibility of the megatexture technique is retained.

Having the ability to paint terrain with any number of texture layers, plus the added stamping feature gives the artist a lot more flexibility than our old technique offered, and it even runs faster than the old terrain. This removes a major source of uncertainty from the development of Leadwerks 3.1 and turned out to be one of my favorite features in the new engine.



18 Comments


Recommended Comments

One of the tools I work with pretty much lets you paint up to 32 layers across very large datasets but renders them down on export so you never have results as good as the source.

 

One of the biggest limitations we had when building the Afghan map was the 10 meter square resolution and shortcuts made to fit everything into memory.

 

Interesting stuff.

Share this comment


Link to comment

TL;DR :P I'll assume you're doing cool things with terrain textures.

 

Will there be any way to query what's under our feet on the terrain? ie could we tell if we are standing on the smiley face?

Share this comment


Link to comment

Tessellation and displacement are hard to marry with collisions. I know one unreleased engine does this, it doesn't seem trivial.

 

Back to textures, what if I have surface imagery of Oahu Hawaii (say it was a car racing game like Test Drive Unlimited) and want texture stages 4-5 to be a raw sat image. For low altitude rendering, stages 0-3 as m-textures. What different approaches would facilitate handling this kind of game scenario? We might want to graduate between sat images with pre-computed splats based on altitude.

 

"Gods eye" to unit view, and back again.

 

Would the mega-textures be computed at run-time, or during map load time or a longer tool export operation? Your description implies you're having much success with small drawing operations, but how far will it scale I wonder?

Share this comment


Link to comment

Indeed we can't ask LE3 to be AAA engine big tech team, but Vector terrain is not possible one day in LE3 ? (from the HAlo tech link laugh.png , don't give ideass to your users lol)

Will it be terrain LOD (adjustable ?) in LE3 ?

Specially adjustable for mobile optimisation (less polygons to display).

Great terrains are coming smile.png

Share this comment


Link to comment

Interesting article and awesome stuff! The concentric textures reminded me of the geometry clipmapping technique for terrains as describes in GPU Gems 2. Maybe this can be used together for vast Terrains?

Share this comment


Link to comment

@yougroove: do you mean voxel?

 

What is mostly amazing is how Josh is able to do this all by himself. I mean how many people and how long have they been experimenting with mega textures in Rage and other games? Okay the idea is out there and there are some good reads here and there, but to be able to build such a thing like it is nothing: that is really impressive.

Share this comment


Link to comment

Interesting...interesting indeed! :)

Would be great to have some kinda road/river tool in the editor.

Maybe even an option to attach sounds to terrain texture materials?

Any plans for that, Josh? :)?

Share this comment


Link to comment
Back to textures, what if I have surface imagery of Oahu Hawaii (say it was a car racing game like Test Drive Unlimited) and want texture stages 4-5 to be a raw sat image. For low altitude rendering, stages 0-3 as m-textures. What different approaches would facilitate handling this kind of game scenario? We might want to graduate between sat images with pre-computed splats based on altitude.

The base map / blend method I used in LE2 worked well for large-scale satellite images. The splatted images are blended together to create one large baked texture for long-range rendering.

 

Tessellation and displacement are hard to marry with collisions. I know one unreleased engine does this, it doesn't seem trivial.

I don't think this is a problem because tessellated geometry is small compared to the physics geometry.

 

Would the mega-textures be computed at run-time, or during map load time or a longer tool export operation? Your description implies you're having much success with small drawing operations, but how far will it scale I wonder?

The whole megatexture never exists at once, but parts of it are drawn on-the-fly. Since it's working now, it will work independent from terrain size. Distant terrain is slightly blurred, but in Leadwerks 2 we actually went to great lengths to get this effect, with the special "blur mipmaps" setting. The reason is that blurred terrain textures in the distance actually look better because it eliminates obvious tiling.

Share this comment


Link to comment

 

 

 

I don't think this is a problem because tessellated geometry is small compared to the physics geometry.

 

 

 

I think Flexman was thinking of somthing different than simply tesselating terrain. What if you were to use a displacement map as a decal to tesselate a bomb crater? Once the crater was made the physics mesh would not be anywhere close.You would some how have to tesselate the physics mesh also.

Share this comment


Link to comment

I will have to experiment some more and see what it can do. This is by far the most flexible terrain system I've ever worked with.

Share this comment


Link to comment

I'm guessing the texture stages are generated on the GPU? So this doesn't give you the constant VRAM usage that mega-textures traditionally do, but does give you unlimited texture layers and decals w/ only a small VRAM overhead?

Share this comment


Link to comment

I'm guessing the texture stages are generated on the GPU? So this doesn't give you the constant VRAM usage that mega-textures traditionally do, but does give you unlimited texture layers and decals w/ only a small VRAM overhead?

Yes, it's all created on the GPU. The VRAM usage is constant, and probably will weigh in at just 24 mb.

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.

Guest
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 reepblue in reepblue's Blog 0
      Loading sounds in Leadwerks has always been straight forward. A sound file is loaded from the disk, and with the Source class emits the sound in 3D space. The sound entity also has a play function, but it's only really good for UI sounds. There is also Entity::EmitSound() which will play the sound at the entity's location. (You can also throw in a Source, but it'll auto release the object when it's done.)
      While this is OK for small games, larger games in which sounds may change might mean you have to open your class, and adjust the sounds accordingly. What if you use the sound in multiple places and you're happy with the volume and pitch settings from an earlier implementation? You could just redefine the source in a different actor, but why should you?
      A solution I came up with comes from SoundScripts from the Source Engine. With that engine, you had to define each sound as a SoundScript entry. This allowed you to define a sound once, and it allowed for other sound settings such as multiple sounds per entry. I thought this over, and with JSON, we can easily create a similar system for Leadwerks 4 and the new engine.
      I first started with a dummy script so I can figure out how I wanted the end result to be.
      { "soundData": { "Error": { "file": "Sound/error.wav", "volume": 1.0, "pitch": 1.0, "range": 0.25 }, "RandomSound": { "files": { "file1": "Sound/Test/tone1.wav", "file2": "Sound/Test/tone2.wav", "file3": "Sound/Test/tone3.wav" }, "volume": 1.0, "pitch": 1.0, "range": 0.25 } } } In this script, we have two sound entries. We have an error sound (Which is suppose to be the fall back sound for an invalid sound entry) and we have a sound entry that holds multiple files. We want a simple, straight forward. entry like "Error" to work, while also supporting something "RandomSound" which can be used for something like footstep sounds.
      The script is streamed and stored into multiple structs in a std::map at the application start. We use the key for the name, and the value is the struct.
      typedef struct { std::string files[128]; char filecount; float volume; float pitch; float range; bool loopmode; } sounddata_t; std::map<std::string, sounddata_t> scriptedsounds; Also notice that we don't store any pointers, just information. To do the next bit, I decided to derive off of the engine's Source class and call it "Speaker". The Speaker class allows us to load sounds via the script entry, and support multiple sounds.
      You create one like this, and you have all the functionalities with the Source as before, but a few differences.
      // Speaker: auto speaker = CreateSpeaker("RandomSound"); When you use Play() with the speaker class and if the sound entry has a "files" table array, it'll pick a sound at random. You can also use PlayIndex() to play the sound entry in the array. I also added a SetSourceEntity() function which will create a pivot, parent to the target entity. From there, the Play function will always play from the pivot's position. This is a good alternative to Entity::EmitSound(), as you don't need to Copy/Instance the Source before calling the function as that function releases the Source as mentioned earlier. Just play the speaker, and you'll be fine! You can also change the sound entry at anytime by calling SetSoundEntry(const std::string pSoundEntryName); The creation of the Speaker class will start the JSON phrasing. If it has already been done, it will not do it again.
      Having sounds being loaded and stored like this opens up a lot of possibles. One thing I plan on implementing is a volume modifier which will adjust the volume based on the games volume setting.Right now, it uses the defined volume setting. It's also a part of another system I have in the works.
    • By Josh in Josh's Dev Blog 7
      An update for Leadwerks 5 is now available.
      The Vulkan data transfer system has been revised and is now simpler but uses more memory. Data is likely to be quadruple-buffered, but it's a fairly small amount of data and this isn't a big concern. 
      I fixed a bad bug where multiple threads were accessing a global variable in the Mat4::GetQuaternion function. This fixes the object flashing glitch that was visible in previous builds.
      The engine is updated to the latest version of Newton Dynamics and kinematic joints work correctly now. The upvector joint is removed and a plane joint has been added for 2D physics, but I don't think it will work yet. Object picking up is implemented in the player controller script.
      I switched out the default scene with a new one using some of @TWahl 's materials.
      Added an FPSWeapon script that loads a gun and makes it sway.
      Entity::AddScript() can now be called in the Start() function of another script with no problems.
      Fullscreen windows are now working.
      The window / display system is changed a bit. New display commands:
      std::vector<shared_ptr<Display> > ListDisplays() std::vector<iVec2> Display::GraphicsModes() You must pass a display object in the window creation command now. Here's how I do it in Lua:
      --Get the primary display local displaylist = ListDisplays() local display = displaylist[1]; --Get the display's highest resolution graphics mode gfxmodes = display:GraphicsModes() gfx = gfxmodes[#gfxmodes] --Create a window local fullscreenmode = false local window if fullscreenmode then window = CreateWindow(display, "My Game", 0, 0, gfx.x, gfx.y, WINDOW_FULLSCREEN) else window = CreateWindow(display, "My Game", 0, 0, 1280 * display.scale.x, 720 * display.scale.y, WINDOW_CENTER + WINDOW_TITLEBAR) end And in C++:
      const bool fullscreenmode = false; //Get the primary display auto displays = ListDisplays(); auto display = displays[0]; //Create a window shared_ptr<Window> window; if (fullscreenmode) { auto gfxmodes = display->GraphicsModes(); auto gfx = gfxmodes[gfxmodes.size() - 1]; window = CreateWindow(display, L"My Game", 0, 0, gfx.x, gfx.y, WINDOW_FULLSCREEN); } else { Vec2 displayscale = display->GetScale(); window = CreateWindow(display, L"My Game", 0, 0, 1280 * displayscale.x, 720 * displayscale.y, WINDOW_TITLEBAR | WINDOW_RESIZABLE | WINDOW_CENTER); } The speed of the point light shadow updating is unbelievably fast. Point light shadows in Leadwerks 4 are very expensive to update because they require six different render passes for each of the six cubemap faces, but in Leadwerks 5 beta with Vulkan they are basically free. I'm sure it will slow down if I add enough points lights and have them all constantly updating, but I don't see any difference at all in the framerate right now when shadows are active. If you are having any trouble with their appearance you can set the global variable MULTIPASS_CUBEMAP to false in C++ at the very beginning of your program.

      This script can be used to display performance statistics. At this time it only shows the framerate but I can expand on this in the future.
      function Script:Start() self.statsEnabled = true self.textcache = {} self.font = LoadFont("Fonts/arial.ttf") self.fontsize = 16 self.textalignment = TEXT_LEFT self.world:EnableStats(self.statsEnabled) self:BindKey(KEY_F11, self.ToggleStats) end function Script:ToggleStats() self.statsEnabled = not self.statsEnabled self.world:EnableStats(self.statsEnabled) end function Script:Update() --Return if disabled or font missing if self.statsEnabled == false or self.font == nil then return end --Hide previously used sprite if self.displayfps ~= nil then self.displayfps:Hide() end --Retrieve the framerate and convert to string --Convert to integer to limit the amount of different string values local fps = tostring(math.ceil(self.world.renderstats.framerate - 0.5)).." FPS" --Check for cached version and create it if it doesn't exist if self.textcache[fps] == nil then self.textcache[fps] = CreateText(self.world, self.font, fps, self.fontsize, self.textalignment, 1) self.textcache[fps]:SetPosition(4,4) self.textcache[fps]:SetColor(0,1,0,0.75) end --Set current sprite and show self.displayfps = self.textcache[fps] self.displayfps:Show() end It may seem like a lot of code just to draw a bit of text onscreen, but the benefit is extreme performance. Instead of drawing one character at a time like the Leadwerks renderer does, this creates persistent text objects and reuses them when needed. That cuts the performance cost of displaying text down to basically zero, making it great for complex GUIs and game interfaces.
    • By Josh in Josh's Dev Blog 10
      Leadwerks 3 / 4 was aimed at beginners who were completely new to game development. Since we were the first game engine on Steam, this made a lot of sense at the time, and the decision resulted in a successful outcome. However, in the next engine we are taking a different approach. (This is a direct result of Steam Direct.)
      Enterprise is a new market I am pursuing, and we have a lot of interest from aerospace and defense VR developers. The fact that I am American also helps here. There are special features these customers need that aren't necessarily needed by game developers, but I think you will like some of the possibilities this unlocks.
      For game developers, I have been moving back to an approach more like Leadwerks 2 where we focus on extreme high-end PC game technology, so that comes down to graphical quality and performance. I think most people here will be pretty happy with that direction. We're going to sell on Steam, Humble Store, Amazon, Microsoft store, Mac App Store, and direct from our website. Less importance will be attached to Steam, as they are just one more storefront we sell through. We're not going to use Steam Workshop.
      For pricing of the non-enterprise version, I am thinking $59.99 / $99.99 standard / pro with a monthly subscription option at $4.99 / $9.99. This is actually cheaper than the pricing of Leadwerks, but I think keeping things under $100 is better for the consumer market.
      The paid beta subscription is going to end before the end of the year, and it will be replaced with an open beta. The reason I did this was because I wanted a very small group of people testing the early betas (really more of an alpha), and I wanted to test out our subscriptions system. During that time we found and fixed a couple of small issues, so this was a good idea. A big thanks to all who participated and bought me many espressos.  ☕
      Finally, we are not going to call Leadwerks 5 "Turbo Game Engine". The name just isn't sticking for me, and I don't think we can get rid of the :"retro" connotations it has. My new technology has developed quite a lot since then, and speed is not the only advantage it brings to the table. I do have a new name in mind, but I am not ready to announce it yet. Until then, I will refer to the new engine as "Leadwerks 5 beta".
×
×
  • Create New...