White Shining Rock Logo
White Shining Rock Logo

Porting: OpenGL and Shaders

March 14, 2015 | Dukus | 23 Comments

Getting a new graphics API running in an existing game engine is somewhat painful - there's always some kind of issue or architectural oversight that doesn't meld well with new APIs. But the game engine for Banished is written in such a way to abstract all the graphics functionality behind a common API, so porting it isn't as awful as it could be. The game can already switch between DirectX 9 and 11 at runtime, so adding OpenGL to the mix shouldn't be too bad.

However, there's a critical mass of code required to get even the simplest graphics to display.

Vertex Buffers need to be implemented to store mesh data, as do Index Buffers. Vertex Layouts need to be defined to tell the GPU how to read that mesh data. Constant Buffers are needed to send uniform constant data to the GPU for use in pixel and vertex shaders. Render State needs an implementation - it tells the GPU how to blend new geometry into the scene (transparent, solid, etc), how to update depth and stencil buffers, and how each triangle gets rasterized. Vertex Shaders are needed to transform mesh data into something that's a triangle on screen, and Pixel Shaders are needed to read textures, performing lighting, and output the final color for each pixel.

All that just to get a simple triangle on the screen.

Once that triangle is displayed, textures need to be supported - there's 2D Textures, Cubemaps, compressed formats, mipmaps, copying textures, and scaling of images - lots of code there. Then Render Targets need to be implemented to be able to render off screen buffers and render shadow maps.

Only then will the renderer be complete. Once I get all that code done, the game should just render properly with OpenGL. It's all common code at the level of drawing a building, the terrain, UI, or anything else.

It's only about 80K of code, but getting it all right takes some debugging time.

OpenGL has a few items that DirectX doesn't - like Vertex Array Objects and Shader Programs that pre link vertex and pixel shaders. Not a big deal, but these are things my engine wasn't designed around, so there are a few hoops to jump through.

All this implementation is fairly mechanical - with a good OpenGL reference none of this is terrible - but I've been struggling with how to implement one particular item, and that's vertex shader and pixel shaders.

At its simplest, a shader is a short program that tells the GPU how to deal with mesh data, or how to color a pixel. Each API (DirectX 9, 11, OpenGL) tend to have they're own way of specifying these programs. For each API, a slightly different version of the same program is required.

For example, here's a simple vertex program written for DirectX 9, HLSL for Shader Model 2.0/3.0. (I didn't actually compile any of these... so forgive minor errors)

uniform float4x4 g_localToView;

void main(in float4 iposition : POSITION, in float4 icolor : COLOR0, 
          in float2 itexture : TEXCOORD0, out float4 oposition : POSITION, 
          out float4 ocolor : COLOR0, out float2 otexcoord : TEXCOORD0)
{
    oposition = mul(g_localToView, iposition);
    otexcoord = itexture;
    ocolor = icolor;
}

Here's the same vertex program, written for DirectX 11 - HLSL 4/5 - fairly similar, but there are some differences. There are more differences in more complex cases, such as texture sampling and dealing with input and output registers.

cbuffer globals
{
    float4x4 g_localToView;
}

void main(in float4 iposition : POSITION, in float4 icolor : COLOR0, 
          in float2 itexture : TEXCOORD0, out float4 oposition : SV_Position, 
          out float4 ocolor : COLOR0, out float2 otexcoord : TEXCOORD0)
{
    oposition = mul(g_localToView, iposition);
    otexcoord = itexture;
    ocolor = icolor;
}

Now, the OpenGL GLSL 3.3 version:

uniform Globals
{
    mat4 localToView;
} globals;

in vec4 iposition;
in vec4 icolor;
in vec2 itexture;

out vec4 ocolor;
out vec2 otexcoord;

void main(void)
{
    gl_Position = globals.localToView * iposition;
    otexcoord = itexture;
    ocolor = icolor;
}

Currently, the game engine supports just DirectX 9 and 11 - and those shaders are close enough to be able to switch between them using some generated code for the constants and some macros to account for language differences. It's probably possible to do the same with GLSL, but I'm not keen to do so.

I'm generally not a coder that worries about future proofing code - I write what I need now, and refactor when needed. But I already refactored my shaders when I added DirectX 11 code, and I'm doing it again now for OpenGL's GLSL language.

If one day I want to include support for other hardware that has programmable GPUs, more versions of these programs are required. Banished already has 50-60 different shaders, so for each new platform supported, there's the same programs to implement over and over. There's already other shading languages out there that I'm not currently targeting, but may do so in the future - There are differences for OpenGL ES, OpenGL 2, OpenGL 4, Vulkan, Xbox 360, Xbox One, PS3, PS4, AMD Mantle, and DirectX 12. There's probably more I don't know about.

There are a few solutions to this:

Option 1. Just deal with multiple versions.

  • This is really not ideal. What's frustrating here is during development, these shaders are often in flux - and updating many programs that do the same thing is error prone and a bit wasteful of time.

    This would be like writing Banished in C on OSX and C++ on Windows. It's not as much code, but you get the idea. Not much point to implementing the same thing different ways when the result is identical.

    Worse is that since I use the PC for development, I'd probably get lazy, only update the PC shaders, and then when I want to test on another platform, would have to spend all day updating shaders in other languages.

Option 2. Use macros, find/replace, and partially generated code to account for language differences.

  • This is the option that is currently in use. It works, but it's not future proof. The more languages that are supported, the more macros are used, and the messier the code gets to look at. If you've spent time modding Banished and looked at the shaders, you'll notice all the crap around the actual body text of the shaders to make them compatible just with DX9/11.

    There's also a lot of #ifdef/#else/#endif code sections to account for differences between the two. It's messy.

    I can get this to work for OpenGL, but it still requires at least one more refactoring of all the shaders.

Option 3. Use an existing tool, like HLSL2GLSL

  • This is a good option. There are some tools to convert from HLSL to GLSL, but they don't support as many languages as I'd like. It could work for the OpenGL version of Banished, but that's about it.

    I could probably modify the tool to output other languages, but I really don't like messing with other people's code. (Yup, I'm one of those coders...)

    Additionally I'd prefer the language I use not be pure HLSL of some specific version. I'd like a language that would be updated to support new hardware features. Nvidia provided the Cg language for a while, which targeted many platforms, but development has been discontinued, so it's a no go for future proofing.

Option 4. Design a new shading language that can be read in and then output any language.

  • To me, this is the best option, but one I've been trying to decide if it's worth the effort. I basically would have to write a compiler. It's not a small undertaking, but it has advantages.

    There are things I never really liked about HLSL, and I'm finding I don't like GLSL that much either. Both languages are very C like, but they don't need to be - I'd rather see a language that is designed around the hardware rather than looking like the code that everything else is written in with special syntax just to bind certain variables to hardware features.

    My own language would give the freedom to implement just the features I need, and add features as they are needed. I've been thinking about my ideal shading language for a while (being a graphics programmer and all) but never really saw a need for it.

    With my own language, my engine would become API agnostic. I've got my own graphics layer anyway, that hides the platform specific details, so having a shading language to match would make sense.

    Plus, never having had written a compiler, it's one of those programming tasks that my fingers are begging to implement.

    To do this, a fairly feature complete compiler with proper parsing, semantic checking, type checks, symbol tables, full error messages, and more would be required. It's a big project.

Option 5 - OpenGL Only

  • I could just limit the engine to using OpenGL. However this is more restrictive than I'd like.

    It would support Windows, OSX, and Linux, but in the future it would limit me to only working on OpenGL systems. Granted, it's a pretty large set of systems, but I don't see any reason to do this since I already have DirectX implementations working and a framework for supporting multiple graphics APIs and hardware.

    I'd like to make console games again one day, and OpenGL tends to either not be available on them, or it isn't the base graphics library which gives full GPU control.

Option 6 - Node Graph or Alternate Representation

  • Shaders could be built using an alternate representation, like the node graph material editor in Unreal and some other game engines. The idea is to use small blocks each with their own snippet of code, and link them together visually. For each target language, a different code snippet would be used per node.

    This type of system can be a great tool for prototyping features and allows non-coders to generate shaders. It's fairly hard to generate invalid code. It also allows for multiple types of outputs to any shading language.

    However, this type of tool takes a bit of code to implement, quite a bit of UI. There's still a significant code generation step, and hand coding of each node in the graph for each language. In an environment without a large number of artists, this may also be overkill in terms of toolset.

    The game engine that Banished runs on only has text inputs without any visual tools, so adding this type of tool probably doesn't make a whole lot of sense.

Decision?

I'm writing a compiler. It's the most flexible solution, so that's what I've been working on the past two months (well, a few other things too, including some other tools, and going to GDC)

I've been thinking about this language issue for a long time, designing and changing the syntax, and making sure I can extend it to geometry shaders (and other hardware features) at some point. I've also been looking at the feature set that all current shading languages are providing and making sure I can provide the full feature set I need for the games I make and the hardware I'm targeting.

While it's been a slower road to getting done, I'm pretty sure at this point my own language that does just what it needs and has a clean syntax is the best thing.

I still have to do one more refactor of all the shaders in the game - but as that's required anyway regardless of what I do to make OpenGL work. If I end up porting Banished to some other platform with a different graphics API, I'll be able to just write a new output for the compiler. The next game I make will just be portable to any system.

At this point I've got most of the compiler written. It reads in my syntax and can output GLSL 3.2, HLSL 2/3, and 4/5 and I've converted enough shaders that the UI terrain, and buildings show up in game.

As a side motive, I'm excited to be writing a compiler - it makes me start thinking that the next game I make could possibly have a scripting language for game code - but that's a ways off.

Leave a Reply

Your email address will not be published.

23 comments on “Porting: OpenGL and Shaders”

  1. Call me a fanboy, but as far as i'm concerned the best answer to the question is "whatever you think is right". That's done pretty fine by us players so far. 🙂

    A question though: if you create this custom script compiler, is there any chance you'll release it to the world? That is, make the language and compiler available?

  2. Luke,

    I'm proud of you for taking the plunge and "biting the bullet", so to speak. You really CAN afford to spend the time working on the compiler now, since we will all reap the benefits down the road.
    Plus, as Jascha mentioned, you could license the code to other devs and then even more games would be more easily portable as well.

  3. Does anyone know how long it will be until it is out on Mac. I have been waiting since the game came out, so a few more months won't hurt.

  4. I'm not a graphics programmer but I am a Systems Engineer and I find these posts very exciting. Thank you for keeping us in the loop with the technical details. This amazing game and your relationship with the fans is the reason why I support indie games. Keep up the great work!

    Keaton,

    I'm a Mac guy and I've been playing this on my MacBook Pro since launch. You can use wine or a commercial version like CrossOver. It works great.

  5. You need to set up some Patreon or something that we can push money in to to help you continue development.
    More developers should take note of what you're doing. You're not taking the easy way out, but are actually looking at what makes the most sense and keep an eye on future development.

  6. Agree with Maki. Would love to be able to contribute to the development effort. Banished fans (addicts) appreciate all your hard work!

  7. Hi Luke,

    Thank you for this post. It's, as usual, very well written and insanely instructive.

    I'm still having a ton of fun with banished, it is, in my opinion, the best city builder of the decades, hands down and tied behind the back.

    Having your own shader writer will more than probably save you ton of time in the future, I could not picture it being a waste of time in any way, especially if one takes into account the sheer fun you're having writing it (well, mostly fun, I guess... right ? =) ).

    I concur with Maki, there are a lot of people out there that would consider supporting you financially a very sensible decision; it would be a way for us to further express our thanks for your time and amazing work. If it can also help the development of future software of yours, I would consider it money wisely invested.

    Cheers from Belgium,

    Bob

  8. Mate, honestly, with all the respect man! I just could imagine all the work you could do, or you had already. You are similar to those great creators, back in '80s days, who had inspired us, and we could love, and got this "computer-fan" thing. Mate, I wish the best for you! Take care!

  9. Will this change help prevent or lessen the crashes I experience on WinXP and lag-outs on Windows 7? I loved the game, but had to eventually stop playing it as much because of these two issues (I have one Desktop with WinXP and a Laptop that runs Win7)

  10. Luke this was again very interesting to read. I'm not a graphics designer but as information engineer I find this very interesting, although I don't literally understand the code.

    I agree with the support thingy.

    As always: good luck with further progress.

  11. Impressive! I'd love to see a snippet of your chosen syntax. GLSL feels so archaic (although it's better than an OpenCL Kernel. Hell, FORTRAN is better than an OpenCL Kernel). Please continue to share! 🙂

  12. Linux is also PC... Nothing changes for Windows users. Nothing changes for anyone right now. But in a future update Banished will support more than just DirectX. You probably won't notice a difference when this rolls out.

  13. Rather than build a compiler completely from scratch, I hope you'll look at LLVM and see if it meets your needs? There is already at least one shader compiler for it, http://www.lunarglass.org and it seems to be still actively developed, if you want to try it and send improvements upstream.

    Even if you roll your own from scratch, I'd imagine LLVM might make a great framework for the compiler, and reduce the redundant work.

    FYI, if you ever choose to embed a scripting language for game logic/mods/etc, I'd encourage you to look at Lua (http://lua.org). It's very fast and easy to tailor to this sort of thing, and lets the authors leverage existing documentation and experience.

    Just my thoughts! Keep up the great work! I look forward to a Mac version!

  14. I assume (hope) you use generators (flex, bison, yacc whatever) instead of writing all scanner, parser, etc from scratch? Would speed things up

  15. Survival games are very popular. I could compare this game with vanilla minecraft. This dev is going to be very weathly in a few years....

    I just hope development of this game continues all that time making it better and fixing issues etc... rather than making api's and then abandoning it to modders.

  16. Wish you all the best. I can understand the satisfaction you'll have when you finish writing your own compiler... along with the comfort you'll have ... long term :p.
    Good luck to you.

  17. I totally agree, designing an intermediate language (since there isn't one) is a good approach to making your engine highly portable and future-proof in just the same way as your other abstractions have.

    If you want this to be the foundation of a multi-platform engine that'll serve you well for years, you're doing it right.

  18. It's not clear to me *when* this compilation should take place:
    - On your development machine: you'd then ship the installer with just API specific shaders.
    - Say, at game startup: the game ships with API agnostic shaders, which are compiled when it's first run (maybe caching the result?).

  19. I was just wondering:
    Do you have any plans to implement DirectX12 compatibility for Windows 10 users?

More Posts

Code Rot

April 17, 2022
1 2 3 47
Back to devlog
Back to devlog
© Copyright 2021 Shining Rock Software
Website Design & Branding by Carrboro Creative
menu-circlecross-circle linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram