Jump to content

Josh

Staff
  • Posts

    23,303
  • Joined

  • Last visited

Blog Entries posted by Josh

  1. Josh

    Articles
    A new update is available on Steam for Ultra App Kit.
    A TEXTFIELD_PASSWORD style flag has been added and is used for the password entry form:

    A WINDOW_CHILD style flag has been added. I found this was necessary while implementing a Vulkan 3D viewport in a GUI application. You can read more about that here.
    Pressing the Tab key will now switch the focus between widgets.
    The "Learn" tab in the project manager has been moved in front of the "Community" tab.
    The Visual Studio project is now using a property sheet to store the location of the headers and libs, the way Leadwerks 4 does.
    Build times have been sped up using incremental linking, /Debug.fastlink, multi-processor builds (which finally work with precompiled headers), and other tricks. Build times will typically be less than one second now if you are just modifying your own code. (This blog article from Microsoft was very helpful.)
    The precompiled header has been changed to "UltraEngine.h" which I find more intuitive and compatible than "pch.h".
    Ultra App Kit lets you easily build desktop GUI applications. If you don't have it already, you can get access to the beta by purchasing it now in our store.
  2. Josh
    An update for Ultra App Kit beta on Steam is now available. This finishes the user interface scaling to support HD, 4K, 8K, and other resolutions. My original plan was to force an application restart if the scale setting was changed, but I found a way to dynamically resize the interface in a manner that gives natural results, so it now supports dynamic rescaling. That is, if the user changes the Windows DPI setting, or if a window is dragged to a monitor with a different DPI setting, the application will receive an event and rescale the interface dynamically.
    Below you can see a sample interface scaled at 100% and 150%:


    Ultra App Kit can be pre-ordered in our store now. You will receive the finished software when it is complete, and a Steam key now to access the beta:
     
  3. Josh
    A new beta is available. In this build I cleaned up a lot of internal stuff. I removed some parts of the engine that I want to redesign in order to clean up the source.
    JSON material files loaded from MDL files are now supported.
    Added ActiveWindow() command. if the game window is not the foreground window this will return null.
    The Steamworks and all dependent classes are temporarily removed. There's a lot of stuff in there I don't intend to use in the future like all the Workshop functions, and I want to reintegrate it one piece at a time in a neater fashion. The good features like P2P networking will definitely be included in the future.
    File IO finished
    The file read and write commands are now 100% using global functions and internally using Unicode strings. You can still call functions with a regular std::string but internally it will just convert it to a wide string. The zip file read support is removed temporarily so I can rethink its design.
    Key and mouse event binding
    Since I am consciously making the decision to design the new engine for intermediate and expert users instead of beginners, it occurred to me that the MouseHit and KeyHit functions are flawed, since they rely on a global state and will cause problems if two pieces of code check for the same key or button. So I removed them and came up with this system:
    self:BindKey(KEY_E,self.Interact) self:BindKey(KEY_SPACE,self.Jump) self:BindMouseButton(MOUSE_LEFT,self.Throw) This works exactly as you would expect, by calling the entity script function when the key or mouse button is pressed. Naturally a key release event would also be useful. Perhaps a BindKeyRelease() function is the best way to do this? The KeyDown() / MouseDown() functions are still available, since they do not have the problems the Hit() commands do. The same technique will work with C++ actors though it is not yet implemented.
    This is undoubtedly more complicated than a simple MouseHit() command but it is better for people as their code gets more complex. Instead of hurting the experience for advanced users, I am going to force beginners to adjust to the correct way of doing things.
    This design is not final. I think there are other ways programmers might want to bind events. I would like to hear your ideas. I am willing to create a more complicated system if it means it is more useful. This is a big change in my idea of good design.
  4. Josh
    A new update is available for beta subscribers.
    What's new
    Added support for strip lights. To create these just call CreateLight(world, LIGHT_STRIP). The entity scale on the Z axis will determine the length of the line, and the outer range will determine the radius in which light shows. Added new properties to the JSON material scheme. "textureScroll" is a float value that can animate a texture to make it smoothly move. "textureScrollRotation" is an angle to control which direction the texture moves. An example material is included. Renamed "albedoMap", "normalMap", "emissionMap", "displacementMap", and "brdfMap" to "baseTexture", "normalTexture", "emissionTexture", "displacementTexture", and "brdfTexture" in JSON material scheme.
  5. Josh
    The Turbo Game Engine beta is updated! This will allow you to load your own maps in the new engine and see the speed difference the new renderer makes.

    LoadScene() has replaced the LoadMap() function, but it still loads your existing map files. To create a PBR material, insert a line into the material file that says "lightingmodel=1". Blinn-Phong is the default lighting model. The red and green channels on texture unit 2 represent metalness and roughness. You generally don't need to assign shaders to your materials. The engine will automatically select one based on what textures you have. Point and spot lights work. Directional lights do not. Setting the world skybox only affects PBR reflections and Blinn-Phong ambient lighting. No sky will be visible. Physics, scripting, particles, and terrain do not work. Variance shadow maps are in use. There are currently some problems with lines appearing at cubemap seams and some flickering pixels. Objects should always cast a shadow or they won't appear correctly with VSMs. I had to include glew.c in the project because the functions weren't being detected from the static lib. I'm not sure why. The static libraries are huge. The release build is nearly one gigabyte. But when compiled, your executable is small. #include "Leadwerks.h" using namespace Leadwerks; int main(int argc, const char *argv[]) { //Create a window auto window = CreateWindow("MyGame", 0, 0, 1280, 720); //Create a rendering context auto context = CreateContext(window); //Create the world auto world = CreateWorld(); //This only affects reflections at this time world->SetSkybox("Models/Damaged Helmet/papermill.tex"); shared_ptr<Camera> camera; auto scene = LoadScene(world, "Maps/turbotest.map"); for (auto entity : scene->entities) { if (dynamic_pointer_cast<Camera>(entity)) { camera = dynamic_pointer_cast<Camera>(entity); } } auto model = LoadModel(world, "Models/Damaged Helmet/DamagedHelmet.mdl"); model->Move(0, 1, 0); model->SetShadowMode(LIGHT_DYNAMIC, true); //Create a camera if one was not found if (camera == nullptr) { camera = CreateCamera(world); camera->Move(0, 1, -3); } //Set background color camera->SetClearColor(0.15); //Enable camera free look and hide mouse camera->SetFreeLookMode(true); window->HideMouse(); //Create a light auto light = CreateLight(world, LIGHT_POINT); light->SetShadowMode(LIGHT_DYNAMIC | LIGHT_STATIC | LIGHT_CACHED); light->SetPosition(0, 4, -4); light->SetRange(15); while (window->KeyHit(KEY_ESCAPE) == false and window->Closed() == false) { //Rotate model model->Turn(0, 0.5, 0); //Camera movement if (window->KeyDown(Key::A)) camera->Move(-0.1, 0, 0); if (window->KeyDown(Key::D)) camera->Move(0.1, 0, 0); if (window->KeyDown(Key::W)) camera->Move(0, 0, 0.1); if (window->KeyDown(Key::S)) camera->Move(0, 0, -0.1); //Update the world world->Update(); //Render the world world->Render(context); } Shutdown(); return 0; }
  6. Josh
    An update is available for the new Turbo Game Engine beta.
    Fixed compiling errors with latest Visual Studio version Fixed compatibility problems with AMD hardware Process is now DPI-aware Added high-resolution depth buffer (additional parameter on CreateContext()). Subscribers can download the new version here.
  7. Josh

    Articles
    I've been working hard getting all the rendering features to work together in one unified system. Ultra Engine, more than any renderer I have worked on, takes a lot of different features and integrates them into one physically-based graphics system. A lot of this is due to the excellent PBR materials system that Khronos glTF provides, and then there are my own features that are worked into this, like combined screen-space and voxel ray traced reflections.
    Anyways, it's a lot of difficult work, and I decided to take a "break" and focus something else for a few days.
    Before Leadwerks was on Steam, it had a web installer that would fetch a list of files from our server and any files that were missing, or were newer than the locally stored ones. There was no system for detecting updates, you just pressed the update button and the updater ran. The backend for this system was designed by @klepto2 and it functioned well for what it needed to do.

    With Ultra App Kit, I created a simple tool to generate projects. This introduced the account authentication / signin system, which was sort of superfluous for this application, but it was a good way to test it out:

    With Ultra Engine, I wanted some characteristics of both these applications. I wrote a new backend in about a day that handles updates. A PHP script authenticates the user and verifies product ownership, fetches a list of files, and retrieves files. Since C++ library files tend to be huge, I found it was necessary to add a compression system, so the script returns a zip compressed file. Of course, you don't want the server to be constantly creating zip files, so it caches the file and updates the zip only when I upload a new copy of the file. There's also a cache for the info retrieval, which is returned in JSON format, so it's easy to read in C++.
    For the front end, I took inspiration from the Github settings page, which I thought looked nice:

    And here's what I came up with. Projects will show when they are outdated and need to be updated (if a file in the template the project came from was changed). Each of the sections contains info and links to various topics. There's a lot there, but none of it feels extraneous to me. This is all made with the built-in GUI system. No HTML is used at all:

    The Invision Power REST API is extremely interesting. It allows authentication of accounts and purchases, but it can be made to do a lot of other things.
    Post a forum topic: https://invisioncommunity.com/developers/rest-api?endpoint=forums/topics/POSTindex Upload an image to the gallery: https://invisioncommunity.com/developers/rest-api?endpoint=gallery/images/GETindex Download a file: https://invisioncommunity.com/developers/rest-api?endpoint=downloads/files/GETitem Unlock achievements: https://invisioncommunity.com/developers/rest-api?endpoint=core/members/POSTitem_achievements_awardbadge None of that is very important right now, but it does provide some interesting ideas for future development of the game engine.
  8. Josh
    As the first release of Ultra Engine approaches, it seems clear that the best way to maximize its usefulness is to make it as compatible as possible with the Leadwerks game engine. To that end, I have implemented the following features.
    Native Loading of Leadwerks File Formats
    Ultra Engine loads and saves DDS, glTF, and OBJ files. Other formats are supported by plugins, both first and potentially third-party, for PNG, JPG, BMP, TGA, TIFF, GIF, HDR, KTX2, and other files. Additionally, all Leadwerks file formats are natively supported without requiring any plugins, so you can load TEX, MDL, PHY, MAT files, as well as full Leadwerks maps. You also have the option to save Leadwerks game engine formats in more up-to-date file formats such as glTF or DDS files with BC5/BC7 compression.

    Testing a scene from the excellent Cyclone game (available on Steam!)
    Classic Specular Shader Family
    PBR materials are wonderful but act quite differently from conventional specular/gloss game materials. I've added a "Classic" shader family which provides an additive specular effect, which may not be realistic but it makes materials expect the way you would expect them to, coming from Leadwerks game engine. When Leadwerks MAT files are loaded, this shader family is automatically applied to them, to get a closer match to their appearance in Leadwerks.
    Leadwerks Translation Layer
    I'm experimenting with a Leadwerks header for Ultra Engine. You can drop this into your Ultra Engine project folder and then start running code with the Leadwerks API. Internally, the Leadwerks game engine commands will be translated into the Ultra Engine API to execute your existing code in the new engine. I don't think this will be 100% perfect due to some differences in the way the two engines work, but I do think it will give you an easy way to get started and provide a gentler transition to Ultra.
    This code will actually work with both engines:
    #include "Leadwerks.h" using namespace Leadwerks; int main(int argc, const char* argv[]) { Window* window = Window::Create(); Context* context = Context::Create(window); World* world = World::Create(); Camera* camera = Camera::Create(); camera->Move(0, 0, -3); Light* light = DirectionalLight::Create(); light->SetRotation(35, 35, 0); Model* model = Model::Box(); model->SetColor(0.0, 0.0, 1.0); while (true) { if (window->Closed() || window->KeyDown(Key::Escape)) return false; model->Turn(0, Time::GetSpeed(), 0); Time::Update(); world->Update(); world->Render(); context->Sync(false); } return 0; } I hope these enhancements give you a more enjoyable experience using Ultra together with the Leadwerks Editor, or as a standalone programming SDK.
  9. Josh

    Articles
    As I have explained before, I plan for Ultra Engine to use glTF for our main 3D model file format, so that your final game models can be easily loaded back into a modeling program for editing whenever you need. glTF supports a lot of useful features and is widely supported, but there are a few missing pieces of information I need to add into it. Fortunately, this JSON-based file format has a mechanism for extensions that add new features and data to the format. In this article I will describe the custom extensions I am adding for Ultra Engine.
    ULTRA_triangle_quads
    All glTF models are triangle meshes, but we want to support quad meshes primarily because its better for tessellation. This extension gets added to the primitives block. If the "quads" value is set to true, this indicates that the triangle indices are stored in a manner such that the first four indices of every six indices form a quad:
    "extensions": { "ULTRA_triangle_quads": { "quads": true } } There is no other glTF extension for quads, and so there is no way to export a glTF quad mesh from any modeling program. To get quad meshes into Ultra Engine you can load an OBJ file and then resave it as glTF. Here is a glTF file using quads that was created this way. You can see the tessellation creates an even distribution of polygons:

    For comparison, here is the same mesh saved as triangles and tessellated. The long thin triangles result in a very uneven distribution of polygons. Not good!

    The mesh still stores triangle data so the file can be loaded back into a 3D modeling program without any issues.
    Here is another comparison that shows how triangle (on the left) and quads (on the right) tessellate:

    ULTRA_material_displacement
    This extension adds displacement maps to glTF materials, in a manner that is consistent with how other textures are stored:
    "ULTRA_material_displacement": { "displacementTexture": { "index": 3, "offset": -0.035, "strength": 0.05 } } The extension indicates a texture index, a maximum displacement value in meters, and a uniform offset, also in meters. This can be used to store material displacement data for tessellation or parallax mapping. Here is a model loaded straight from a glTF file with displacement info and tessellation:

     
    If the file is loaded in other programs, the displacement info will just be skipped.
    ULTRA_vertex_displacement
    Our game engine uses a per-vertex displacement factor to control how displacement maps affect geometry. This extension adds an extra attribute into the primitives structure to store these values:
    "primitives": [ { "attributes": { "NORMAL": 1, "POSITION": 0, "TEXCOORD_0": 2 }, "indices": 3, "material": 0, "mode": 4, "extensions": { "ULTRA_vertex_displacement": { "DISPLACEMENT": 7 } } } } This can be used to prevent cracks from appearing  at texcoord seams.

    Here you can see the displacement value being loaded back from a glTF file it has been saved into. I'm using the vertex color to visually verify that it's working right:

    ULTRA_extended_material
    This extension adds other custom parameters that Ultra Engine uses. glTF handles almost everything we want to do, and there are just a few settings to add. Since the Ultra Engine material format is JSON-based, it's easy to just insert the extra parameters into the glTF like so:
    "ULTRA_extended_material": { "shaderFamily": "PBR", "shadow": true, "tessellation": true } In reality I do not feel that this extension is very well-defined and do not expect it to see any adoption outside of Ultra Engine. I made the displacement parameters a separate extension because they are well-defined, and there might be an opportunity to work with other application developers using that extension.
    Here we can see the per-material shadow property is disabled when it is loaded from the glTF:

    For comparison, here is what the default setting looks like:

    These extensions are simply meant to add special information that Ultra Engine uses into the glTF format. I do not currently have plans to try to get other developers to adopt my standards. I just want to add the extra information that our game engine needs, while also ensuring compatibility with the rest of the glTF ecosystem. If you are writing a glTF importer or exporter and would like to work together to improve the compatibility of our applications, let me know!
    I used the Rock Moss Set 02 model pack from Polyhaven in this article.
  10. Josh

    Articles
    There are three new features in the upcoming Ultra Engine (Leadwerks 5) that will make game input better than ever before.
    High Precision Mouse Input
    Raw mouse input measures the actual movement of the mouse device, and has nothing to do with a cursor on the screen. Windows provides an interface to capture the raw mouse input so your game can use mouse movement with greater precision than the screen pixels. The code to implement this is rather complicated, but in the end it just boils down to one simple command expose to the end user:
    Vec2 Window::MouseMovement(const int dpi = 1000); What's really hilarious about this feature is it actually makes mouse control code a lot simpler. For first-person controls, you just take the mouse movement value, and that is your camera rotation value. There's no need to calculate the center of the window or move the mouse pointer back to the middle. (You can if you want to, but it has no effect on the raw mouse input this command measures.)
    The MousePosition() command is still available, but will return the integer coordinates the Windows mouse cursor system uses.
    High Frequency Mouse Look
    To enable ultra high-precision mouse look controls, I have added a new command:
    void Camera::SetFreeLookMode(const bool mode, const float speed = 0.1f, const int smoothing = 0) When this setting is enabled, the mouse movement will be queried in the rendering thread and applied to the camera rotation. That means real-time mouse looking at 1000 FPS is supported, even as the game thread is running at a slower frequency of 60 Hz. This was not possible in Leadwerks 4, as mouse looking would become erratic if it wasn't measured over a slower interval due to the limited precision of integer coordinates.
    XBox Controller Input
    I'm happy to say we will also have native built-in support for XBox controllers (both 360 and One versions). Here are the commands:
    bool GamePadConnected(const int controller = 0) bool GamePadButtonDown(const GamePadButton button, const int controller = 0) bool GamePadButtonHit(const GamePadButton button, const int controller = 0) Vec2 GamePadAxisPosition(const GamePadAxis axis, const int controller = 0) void GamePadRumble(const float left, const float right, const int controller = 0) To specify a button you use a button code:
    GAMEPADBUTTON_DPADUP GAMEPADBUTTON_DPADDOWN GAMEPADBUTTON_DPADLEFT GAMEPADBUTTON_DPADRIGHT GAMEPADBUTTON_START GAMEPADBUTTON_BACK GAMEPADBUTTON_LTHUMB GAMEPADBUTTON_RTHUMB GAMEPADBUTTON_LSHOULDER GAMEPADBUTTON_RSHOULDER GAMEPADBUTTON_A GAMEPADBUTTON_B GAMEPADBUTTON_X GAMEPADBUTTON_Y GAMEPADBUTTON_RTRIGGER GAMEPADBUTTON_LTRIGGER And axes are specified with these codes:
    GAMEPADAXIS_RTRIGGER GAMEPADAXIS_LTRIGGER GAMEPADAXIS_RSTICK GAMEPADAXIS_LSTICK These features will give your games new player options and a refined sense of movement and control.
  11. Josh
    With the help of @martyj I was able to test out occlusion culling in the new engine. This was a great chance to revisit an existing feature and see how it can be improved. The first thing I found is that determining visibility based on whether a single pixel is visible isn't necessarily a good idea. If small cracks are present in the scene one single pixel peeking through can cause a lot of unnecessary drawing without improving the visual quality. I changed the occlusion culling more to record the number of pixels drawn, instead just using a yes/no boolean value:
    glBeginQuery(GL_SAMPLES_PASSED, glquery); In OpenGL 4.3, a less accurate but faster GL_ANY_SAMPLES_PASSED_CONSERVATIVE (i.e. it might produce false positives) option was added, but this is a step in the wrong direction, in my opinion.
    Because our new clustered forward renderer uses a depth pre-pass I was able to implement a wireframe rendering more that works with occlusion culling. Depth data is rendered in the prepass, and the a color wireframe is drawn on top. This allowed me to easily view the occlusion culling results and fine-tune the algorithm to make it perfect. Here are the results:
    As you can see, we have pixel-perfect occlusion culling that is completely dynamic and basically zero-cost, because the entire process is performed on the GPU. Awesome!
  12. Josh
    I've begun implementing unicode in Leadwerks Game Engine 5.  It's not quite as simple as "switch all string variables to another data type".
    First, I will give you a simple explanation of what unicode is.  I am not an expert so feel free to make any corrections in the comments below.
    When computers first started drawing text we used a single byte for each character.  One byte can describe 256 different values and the English language only has 26 letters, 10 numbers, and a few other characters for punctuation so all was well.  No one cared or thought about supporting other languages like German with funny umlauts or the thousands of characters in the Chinese language.
    Then some people who were too smart for their own good invented a really complicated system called unicode.  Unicode allows characters beyond the 256 character limit of a byte because it can use more than one byte per character.  But unicode doesn't really store a letter, because that would be too easy.  Instead it stores a "code point" which is an abstract concept.  Unfortunately the people who originally invented unicode were locked away in a mental asylum where they remain to this day, so no one in the real world actually understands what a code point is.
    There are several kinds of unicode but the one favored by nerds who don't write software is UTF-8.  UTF-8 uses just one byte per character, unless it uses two, or sometimes four.  Because each character can be a different length there is no way to quickly get a single letter of a string.  It would be like trying to get a single byte of a compressed zip file; you have to decompress the entire file to read a byte at a certain position.  This means that commands like Replace(), Mid(), Upper(), and basically any other string manipulation commands simply will not work with UTF-8 strings.
    Nonetheless, some people still promote UTF-8 religiously because they think it sounds cool and they don't actually write software.  There is even a UTF-8 Everywhere Manifesto.  You know who else had a manifesto?  This guy, that's who:

    Typical UTF-8 proponent.
    Here's another person with a "manifesto":

    The Unabomber (unibomber? Coincidence???)
    The fact is that anyone who writes a manifesto is evil, therefore UTF-8 proponents are evil and should probably be imprisoned for crimes against humanity.  Microsoft sensibly solved this problem by using something called a "wide string" for all the windows internals.  A C++ wide string (std::wstring) is a string made up of wchar_t values instead of char values.  (The std::string data type is sometimes called a "narrow string").  In C++ you can set the value of a wide string by placing a letter "L" (for long?) in front of the string:
    std::wstring s = L"Hello, how are you today?"; The C++11 specification defines a wchar_t value as being composed of two bytes, so these strings work the same across different operating systems.  A wide string cannot display a character with an index greater than 65535, but no one uses those characters so it doesn't matter.  Wide strings are basically a different kind of unicode called UTF-16 and these will actually work with string manipulation commands (yes there are exceptions if you are trying to display ancient Vietnamese characters from the 6th century but no one cares about that).
    For more detail you can read this article about the technical details and history of unicode (thanks @Einlander).
    First Pass
    At first I thought "no problem, I will just turn all string variables into wstrings and be done with it".  However, after a couple of days it became clear that this would be problematic.  Leadwerks interfaces with a lot of third-party libraries like Steamworks and Lua that make heavy use of strings.  Typically these libraries will accept a chr* value for the input, which we know might be UTF-8 or it might not (another reason UTF-8 is evil).  The engine ended up with a TON of string conversions that I might be doing for no reason.  I got the compiler down to 2991 errors before I started questioning whether this was really needed.
    Exactly what do we need unicode strings for?  There are three big uses:
    Read and save files. Display text in different languages. Print text to the console and log. Reading files is mostly an automatic process because the user typically uses relative file paths.  As long as the engine internally uses a wide string to load files the user can happily use regular old narrow strings without a care in the world (and most people probably will).
    Drawing text to the screen or on a GUI widget is very important for supporting different languages, but that is only one use.  Is it really necessary to convert every variable in the engine to a wide string just to support this one feature?
    Printing strings is even simpler.  Can't we just add an overload to print a wide string when one is needed?
    I originally wanted to avoid mixing wide and narrow strings, but even with unicode support most users are probably not even going to need to worry about using wide strings at all.  Even if they have language files for different translations of their game, they are still likely to just load some strings automatically without writing much code.  I may even add a feature that does this automatically for displayed text.  So with that in mind, I decided to roll everything back and convert only the parts of the engine that would actually benefit from unicode and wide strings.
    Second Try + Global Functions
    To make the API simpler Leadwerks 5 will make use of some global functions instead of trying to turn everything into a class.  Below are the string global functions I have written:
    std::string String(const std::wstring& s); std::string Right(const std::string& s, const int length); std::string Left(const std::string& s, const int length); std::string Replace(const std::string& s, const std::string& from, const std::string& to); int Find(const std::string& s, const std::string& token); std::vector<std::string> Split(const std::string& s, const std::string& sep); std::string Lower(const std::string& s); std::string Upper(const std::string& s); There are equivalent functions that work with wide strings.
    std::wstring StringW(const std::string& s); std::wstring Right(const std::wstring& s, const int length); std::wstring Left(const std::wstring& s, const int length); std::wstring Replace(const std::wstring& s, const std::wstring& from, const std::wstring& to); int Find(const std::string& s, const std::wstring& token); std::vector<std::wstring> Split(const std::wstring& s, const std::wstring& sep); std::wstring Lower(const std::wstring& s); std::wstring Upper(const std::wstring& s); The System::Print() command has become a global Print() command with a couple of overloads for both narrow and wide strings:
    void Print(const std::string& s); void Print(const std::wstring& s); The file system commands are now global functions as well.  File system commands can accept a wide or narrow string, but any functions that return a path will always return a wide string:
    std::wstring SpecialDir(const std::string); std::wstring CurrentDir(); bool ChangeDir(const std::string& path); bool ChangeDir(const std::wstring& path); std::wstring RealPath(const std::string& path); std::wstring RealPath(const std::wstring& path); This means if you call ReadFile("info.txt") with a narrow string the file will still be loaded even if it is located somewhere like "C:/Users/约书亚/Documents" and it will work just fine.  This is ideal since Lua 5.3 doesn't support wide strings, so your game will still run on computers around the world as long as you just use local paths like this:
    LoadModel("Models/car.mdl"); Or you can specify the full path with a wide string:
    LoadModel(CurrentDir() + L"Models/car.mdl"); The window creation and text drawing functions will also get an overload that accepts wide strings.  Here's a window created with a Chinese title:

    So in conclusion, unicode will be used in Leadwerks and will work for the most part without you needing to know or do anything different, allowing games you develop (and Leadwerks itself) to work correctly on computers all across the world.
  13. Josh
    Following this tutorial, I have managed to add uniform buffers into my Vulkan graphics pipeline. Since each image in the swapchain has a different graphics pipeline object, and uniform buffers are tied to a pipeline, you end up uploading all the data three times every time it changes. OpenGL might be doing something like this under the hood, but I am not sure this is a good approach. There are three ways to get data to a shader in Vulkan. Push constants are synonymous with GLSL uniforms, although much more restrictive. Uniform buffers are the same in OpenGL, and this is where I store light data in the clustered forward renderer. Shader storage buffers are the slowest to update but they can have a very large capacity, usually as big as your entire VRAM. I have the first two working now. Below you can see a range of instanced boxes being rendered with an offset for each instance, which is being read from a uniform buffer using the instance ID as the array index.

    To prove that Vulkan can render more than just boxes, here is a model loaded from GLTF format and rendered in Vulkan:

    Figuring out the design of the new renderer using OpenGL was very smart. I would not have been able to invent it if I had jumped straight into Vulkan.
  14. Josh
    I've spent the last few days writing simple examples for every single command in Leadwerks 3. Not only does this make the documentation more friendly, it also acts as a final test to make sure all the commands work the way they say they should. I make the C++ example and then Chris converts it to Lua (and tells me what I did wrong!).
     
    I didn't realize it at first, but this really showcases the strength of API design of Leadwerks. Since you get full control over the execution and flow of a Leadwerks program, it's easy to learn from simple examples that demonstrate one idea. Below are a few examples for different commands in the API.
     
    Get the device accellerometer reading:

    #include "App.h" using namespace Leadwerks; Window* window = NULL; Context* context = NULL; bool App::Start() { window = Window::Create(); context = Context::Create(window); return true; } bool App::Continue() { if (window->Closed() || window->KeyDown(Key::Escape)) return false; Draw::SetColor(0,0,0); context->Clear(); //Display the device information on the screen Draw::SetBlendMode(Blend::Alpha); Draw::SetColor(1,1,1); Draw::Text("Orientation: "+String(Device::GetOrientation()),2,2); Draw::Text("Acceleration: "+Device::GetAcceleration().ToString(),2,22); Draw::SetBlendMode(Blend::Solid); context->Sync(); return true; }
     
    Create a physics shape from a model and use it on a scaled entity :

    #include "App.h" using namespace Leadwerks; Window* window = NULL; Context* context = NULL; World* world = NULL; Camera* camera = NULL; bool App::Start() { window = Window::Create(); context = Context::Create(window); world = World::Create(); camera = Camera::Create(); camera->SetRotation(35,0,0); camera->Move(0,0,-10); Light* light = DirectionalLight::Create(); light->SetRotation(35,35,0); //Create the ground Model* ground = Model::Box(10,1,10); ground->SetPosition(0,-0.5,0); ground->SetColor(0,1,0); //Create a shape Shape* shape = Shape::Box(0,0,0, 0,0,0, 10,1,10); ground->SetShape(shape); shape->Release(); //Load a model Model* model = Model::Load("Models/teapot.mdl"); model->SetPosition(0,0,0); model->SetColor(0,0,1); model->SetScale(4,4,4); //Create a shape shape = Shape::PolyMesh(model->GetSurface(0)); model->SetShape(shape); model->SetPosition(0,0,0); shape->Release(); //Create some objects to fall model = Model::Sphere(); shape = Shape::Sphere(); model->SetShape(shape); shape->Release(); model->SetMass(1); model->SetColor(Math::Rnd(0,1),Math::Rnd(0,1),Math::Rnd(0,1)); model->SetPosition(Math::Rnd(-1,1),Math::Rnd(3,6),Math::Rnd(-1,1)); for (int i=0; i<10; i++) { model = (Model*)model->Instance(); model->SetCollisionType(Collision::Prop); model->SetColor(Math::Rnd(0,1),Math::Rnd(0,1),Math::Rnd(0,1)); model->SetPosition(Math::Rnd(-1,1),5+i*2,Math::Rnd(-1,1)); } return true; } bool App::Continue() { if (window->Closed() || window->KeyDown(Key::Escape)) return false; Time::Update(); world->Update(); world->Render(); context->Sync(); return true; }
     
    Or in Lua, if you prefer:

    function App:Start()  
    self.window=Window:Create(self.title,0,0,1136+6,640+32,Window.Titlebar+Window.Center+8)
    self.context=Context:Create(self.window,0)
    self.world=World:Create()
    camera = Camera:Create()
    camera:SetRotation(35,0,0)
    camera:Move(0,0,-10)
    light = DirectionalLight:Create()
    light:SetRotation(35,35,0)
     
    --Create the ground
    ground = Model:Box(10,1,10)
    ground:SetPosition(0,-.05,0)
    ground:SetColor(0,1,0)
     
    --Create a shape
    shape = Shape:Box(0,0,0, 0,0,0, 10,1,10)
    ground:SetShape(shape)
    shape:Release()
     
    --Load a model
    model = Model:Load("Models/teapot.mdl")
    model:SetPosition(0,0,0)
    model:SetColor(0,0,1)
    model:SetScale(4,4,4)
     
    --Create a shape
    shape = Shape:PolyMesh((model:GetSurface(0)))
    model:SetShape(shape)
    model:SetPosition(0,0,0)
    shape:Release()
     
    --Create some objects to fall
    model = Model:Sphere()
    shape = Shape:Sphere()
    model:SetShape(shape)
    shape:Release()
    model:SetMass(1)
    model:SetColor(Math:Rnd(0,1),Math:Rnd(0,1))
    model:SetPosition(Math:Rnd(-1,1),Math:Rnd(3,6),Math:Rnd(-1,1),true)
     
    for i=0, 9 do
    model = tolua.cast(model:Instance(),"Model")
    model:SetCollisionType(Collision.Prop)
    model:SetColor(Math:Rnd(0,1),Math:Rnd(0,1),Math:Rnd(0,1))
    model:SetPosition(Math:Rnd(-1,1),5+i*2,Math:Rnd(-1,1),true)
    end
    return true
    end
     
    function App:Continue()
     
    if self.window:Closed() or self.window:KeyHit(Key.Escape) then return false end
     
    Time:Update()
    self.world:Update()
    self.world:Render()
    self.context:Sync()
     
    return true
    end
     
    Create a texture from scratch:

    #include "App.h" using namespace Leadwerks; Window* window = NULL; Context* context = NULL; World* world = NULL; Texture* texture = NULL; bool App::Start() { window = Window::Create(); context = Context::Create(window); //Create a texture texture = Texture::Create(256,256); //Set the texture pixel data char* pixels = (char*)malloc(texture->GetMipmapSize(0)); char r,g,b; for (int x=0; x<256; x++) { for (int y=0; y<256; y++) { int p = (x*texture->GetWidth() + y)*4; memcpy(&r,pixels + p + 0, 1); memcpy(&g,pixels + p + 1, 1); memcpy(&b,pixels + p + 2, 1); if (x<128) { if (y<128) { r=0; g=0; b=255; } else { r=255; g=0; b=0; } } else { if (y<128) { r=255; g=0; b=0; } else { r=0; g=0; b=255; } } memcpy(pixels + p + 0, &r, 1); memcpy(pixels + p + 1, &g, 1); memcpy(pixels + p + 2, &b, 1); } } texture->SetPixels(pixels); return true; } bool App::Continue() { if (window->Closed() || window->KeyDown(Key::Escape)) return false; Draw::SetColor(0,0,0); context->Clear(); //Display the texture on screen Draw::SetColor(1,1,1); Draw::Image(texture,0,0); context->Sync(); return true; }
  15. Josh
    A small update is out, on all branches, that fixes the vehicle wheels not being positioned correctly:
    http://www.leadwerks.com/werkspace/topic/15405-problembug-with-vehicles-cars-wheels-dont-move-since-update-4-2
  16. Josh
    An update is available on the beta branch which resolves the AMD rendering issues on Windows 10 with 1x MSAA setting enabled:
    http://www.leadwerks.com/werkspace/topic/13273-graphical-glitch/
     
    The following components have changed:
    Lighting shaders adjusted to handle regular 2D textures (MSAA level 0)
    Editor updated, for Windows.
    Lua interpreter and debug interpreter updated, for Windows.

     
    The C++ library has not been updated yet.
  17. Josh
    A new update is now available on the default (stable) branch. For a list of improvements and fixes, see my blog and the bug reports forum:
    http://www.leadwerks.com/werkspace/blog/1-joshs-blog/
    http://www.leadwerks.com/werkspace/forum/80-bug-reports/
     
    If you purchased the sci-fi DLC, the example maps will now be automatically copied into your "MyGame" project, making it easier to find them.
     
    If you would like to trade your standalone registration key in for a Steam key, so you can get access to the Workshop and get updates faster, please email support.
  18. Josh
    I did a little experiment with FPS Creator Pack #75 by upsampling the images with Gigapixel, which uses deep learning to upsample images and infer details that don't appear in the original pixels. The AI neural network does a pretty impressive job, generating results that are look better than a simple sharpen filter: I doubled the size of the textures to 1024x1024. Then I generated normal maps from the high-res images using AMD's TGA to DOT3 tool, and saved the normal maps with BC5 DDS compression. The diffuse textures were saved with BC7 DDS compression. The images below are using a 4x magnification to demonstrate the difference.


    As you can see, the image that is upsampled with deep learning looks normal and the resized image looks like there is butter on the lens! It's hard to believe the above images came from a 256x128 section of an image.
    The workflow was pretty tedious, as I had to convert images to TGA, then to uncompressed or BC5 DDS, and then to BC7 in Visual Studio. Each BC7 texture took maybe 5-10 minutes to compress! So while this set represents the optimum quality for 2019 game tech, and the format for assets we want to use in LE5, the workflow has a lot of room for improvement.
    You can download the full package here:
    FPSCPack75TexturesHD.zip
  19. Josh
    As noted previously, Leadwerks 4.2 now support built-in analytics. To enable this in your game you need to create an account at www.gameanalytics.com and create a new product. You can find your keys on the Game Settings page.
     

     
    At the beginning of the main lua script, some code has been added to initialize analytics. By default this is commented out:

    if DEBUG==false then Analytics:SetKeys("GAME_KEY_xxxxxxxxx", "SECRET_KEY_xxxxxxxxx") Analytics:Enable() end
     
    Uncomment this and insert your game's game key into the first argument. Insert your secret key into the second argument.
     
    The main Lua script has already been set up to record a progress event when the player starts your level, and another when they complete it. Below the initial map loading code, a progress event is sent indicating the player has started the level:

    --Load a map local mapfile = System:GetProperty("map","Maps/start.map") if Map:Load(mapfile)==false then return end prevmapname = FileSystem:StripAll(changemapname) --Send analytics event Analytics:SendProgressEvent("Start",prevmapname)
     
    When a new map is loaded in the game loop, another event is sent to indicate that the previous level was completed, and the new level is beginning:

    --Send analytics event Analytics:SendProgressEvent("Complete",prevmapname) --Load the next map if Map:Load("Maps/"..changemapname..".map")==false then return end prevmapname = changemapname --Send analytics event Analytics:SendProgressEvent("Start",prevmapname))
     
    The FPSPlayer script has a new line of code in the OnDead() function that sends an event indicating the player failed to complete the level:

    if type(prevmapname)=="string" then Analytics:SendProgressEvent("Fail",prevmapname) end

    Viewing Data
    Once your game has accumulated enough data, you can look at your results in the Explore section and clicking on Look up metric. Your progress events will be shown here. 

     
    You can even set up a custom funnel in the gameanalytics.com web interface, which will give you a better visualization of player progress through your game.
     


    Interpreting Data
    If you see a lot of people starting a level, but few are finishing it, it may indicate your game is too hard, or the objective is not clear. If there is a large number of failed attempts, consider reducing the difficulty. If there is just a lot of starts and no completes, maybe the player is just bored. If you see some trouble spots in your game, try making changes, collect new data, and see if your changes improve the user experience. 
    Most importantly, you need a good amount of players in order for any analytics data to be useful. A sample size of 20 is statistically significant, and gives you enough information to make decisions. Analytics will give you objective data and help reveal problems you might not otherwise realize, but you need to have a fun game people like playing in order to collect the data in the first place.
     
    MartyJ was a huge help in figuring the implementation of this out.
  20. Josh
    During development of Leadwerks Game Engine, there was some debate on whether we should allow multiple scripts per entity or just associate a single script with an entity. My first iteration of the scripting system actually used multiple scripts, but after using it to develop the Darkness Awaits example I saw a lot of problems with this. Each script used a different classname to store its variables and functions in, so you ended up with code like this:
    function Script:HurtEnemy(amount) if self.enemy ~= nil then if self.enemy.healthmanager ~= nil then if type(self.enemy.healthmanager.TakeDamage)=="function" then self.enemy.healthmanager.TakeDamage(amount) end end end end I felt this hurt script interoperability because you had to have a bunch of prefixes like healthmanager, ammomanager, etc. I settled on using a single script, which I still feel was the better choice between these two options:
    function Script:HurtEnemy(amount) if self.enemy ~= nil then if type(self.enemy.TakeDamage)=="function" then self.enemy.TakeDamage(amount) end end end Scripting in Turbo Game Engine is a bit different. First of all, all values and functions are attached to the entity itself, so there is no "script" table. When you access the "self" variable in a script function you are using the entity object itself. Here is a simple script that makes an entity spin around its Y axis:
    function Entity:Update() self:Turn(0,0.1,0) end Through some magic that is only possible due to the extreme flexibility of Lua, I have managed to devise a system for multiple script attachments that makes sense. There is no "component" or "script" objects itself, adding a script to an entity just executes some code that attached values and functions to an entity. Adding a script to an entity can be done in C++ as follows:
    model->AttachScript("Scripts/Objects/spin.lua"); Or in Lua itself:
    model:AttachScript("Scripts/Objects/spin.lua"); Note there is no concept of "removing" a script, because a script just executes a bit of code that adds values and functions to the entity.
    Let's say we have two scripts named "makeHealth100 and "makeHealth75".
    MakeHealth100.lua
    Entity.health=100 MakeHealth75.lua
    Entity.health=75 Now if you were to run the code below, which attaches the two scripts, the health value would first be set to 100, and then the second script would set the same value to 75, resulting in the number 75 being printed out:
    model->AttachScript("Scripts/Objects/MakeHealth100.lua"); model->AttachScript("Scripts/Objects/MakeHealth75.lua"); Print(entity->GetNumber("health")); Simple enough, right? The key point here is that with multiple scripts, variables are shared between scripts. If one scripts sets a variable to a value that conflicts with another script, the two scripts won't work as expected. However, it also means that two scripts can easily share values to work together and create new functionality, like this health regeneration script that could be added to work with any other scripts that treat the value "health" as a number.
    HealthRegen.lua
    Entity.healthregendelay = 1000 function Entity:Start() self.healthregenupdatetime = CurrentTime() end function Entity:Update() if self.health > 0 then if CurrentTime() - self.healthregenupdatetime > self.healthregendelay then self.health = self.health + 1 self.health = Min(self.health,100) end end end What about functions? Won't adding a script to an entity overwrite any functions it already has attached to it? If I treated functions the same way, then each entity could only have one function for each name, and there would be very little point in having multiple scripts! That's why I implemented a special system that copies any added functions into an internal table. If two functions with the same name are declared in two different scripts, they will both be copied into an internal table and executed. For example, you can add both scripts below to an entity to make it both spin and make the color pulse:
    Spin.lua
    function Entity:Update() self:Turn(0,0.1,0) end Pulse.lua
    function Entity:Update() local i = Sin(CurrentTime()) * 0.5 + 0.5 self:SetColor(i,i,i) end When the engine calls the Update() function, both copies of the function will be called, in the order they were added.
    But wait, there's more.
    The engine will add each function into an internal table, but it also creates a dummy function that iterates through the table and executes each copy of the function. This means when you call functions in Lua, the same multi-execution feature will be available. Let's consider a theoretical bullet script that causes damage when the bullet collides with something:
    function Entity:Collision(entity,position,normal,speed) if type(entity.TakeDamage) == "function" then entity:TakeDamage(20) end end If you have two (or more) different TakeDamage functions on different scripts attached to that entity, all of them would get called, in order.
    What if a function returns a value, like below?:
    function Entity:Update() if self.target ~= nil then if self.target:GetHealth() <= 0 then self.target = nil --stop chasing if dead end end end If multiple functions are attached that return values, then all the return values are returned.

    To grab multiple returned values, you can set up multiple variables like this:
    function foo() return 1,2,3 end a, b, c = foo() print(a) --1 print(b) --2 print(c) --3 But a more practical usage would be to create a table from the returned values like so:
    function foo() return 1,2,3 end t = { foo() } print(t[1]) --1 print(t[2]) --2 print(t[3]) --3 How could this be used? Let's say you had a script that was used to visually debug AI scripts. It did this by checking to see what an entity's target enemy was, by calling a GetTarget() function, and then creating a sprite and aligning it to make a line going from the AI entity to its target it was attacking:
    function Entity:UpdateDisplay() local target = self:GetTarget() self.sprite = CreateSprite() local p1 = self.entity:GetPosition() local p2 = target:GetPosition() self.sprite:SetPosition((p1 + p2) * 0.5) self.sprite:AlignToVector(p2 - p1) self.sprite:SetSize(0.1,(p2-p1):Length()) end Now let's imagine we had a tank with a main gun as well as an anti-aircraft gun that would ward off attacks from above, like this beauty I found on Turbosquid:

    Let's imagine we have two different scripts we attach to the tank. One handles AI for driving and shooting the main turret, while the other just manages the little machine gun. Both the scripts have a GetTarget() function, as the tank may be attacking two different enemies at once.
    We can easily modify our AI debugging script to handle multiple returned values as follows:
    function Entity:UpdateDisplay() local targets = { self:GetTarget() } --all returned values get put into a table for n,target in ipairs(targets) do local sprite = CreateSprite() self.sprites.insert(sprite) local p1 = self.entity:GetPosition() local p2 = target:GetPosition() sprite:SetPosition((p1 + p2) * 0.5) sprite:AlignToVector(p2 - p1) sprite:SetSize(0.1,(p2-p1):Length()) end end However, any scripts that are not set up to account for multiple returned values from a function will simply use the first returned value, and proceed as normal.
    This system supports both easy mix and match behavior with multiple scripts, but keeps the script code simple and easy to use. Scripts have easy interoperability by default, but if you want to make your function and variable names unique to the script it is easy to do so.
    Let me know if you have any other ideas for scripting in Turbo Game Engine.
  21. Josh
    Microsoft has released a cross-platform code editor called "Visual Studio Code". It's just a lightweight text editor like Notepad++. You can get it here for Windows, Linux, and Mac.
     
    Although the editor is focused on web development it actually does support Lua syntax highlighting. If you are curious to try to use it with Leadwerks, add this text to your launch.json file:

    { "version": "0.1.0", // List of configurations. Add new configurations or edit existing ones. // ONLY "node" and "mono" are supported, change "type" to switch. "configurations": [ { // Name of configuration; appears in the launch configuration drop down menu. "name": "Debug", // Type of configuration. Possible values: "node", "mono". "type": "node", // Workspace relative or absolute path to the program. "program": "C:\\Users\\Josh\\Documents\\Leadwerks\\Projects\\MyGame\\MyGame.debug.exe", // Automatically stop program after launch. "stopOnEntry": true, // Command line arguments passed to the program. "args": ["+debuggerport 5858"], // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace. "cwd": "C:\\Users\\Josh\\Documents\\Leadwerks\\Projects\\MyGame", // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. "runtimeExecutable": "C:\\Users\\Josh\\Documents\\Leadwerks\\Projects\\MyGame\\MyGame.debug.exe", // Environment variables passed to the program. "env": {} }, { "name": "Attach", "type": "node", // TCP/IP address. Default is "localhost". "address": "localhost", // Port to attach to. "port": 5858 } ] }
     
    When you press F5 to launch your game the code editor will attempt to connect to the debug process and fail, but it does work. Sort of.
  22. Josh
    After a couple days of work I got point light shadows working in the new clustered forward renderer. This time around I wanted to see if I could get a more natural look for shadow edges, as well as reduve or eliminate shadow acne. Shadow acne is an effect that occurs when the resolution of the shadow map is too low, and incorrect depth comparisons start being made with the lit pixels: By default, any shadow mapping alogirthm will look like this, because not every pixel onscreen has an exact match in the shadow map when the depth comparison is made:

    We can add an offset to the shadow depth value to eliminate this artifact:
    \
    However, this can push the shadow back too far, and it's hard to come up with values that cover all cases. This is especially problematic with point lights that are placed very close to a wall. This is why the editor allows you to adjust the light range of each light, on an individual basis.
    I came across a techniqe called variance shadow mapping. I've seen this paper years ago, but never took the time to implement it because it just wasn't a big priority. This works by writing the depth and depth squared values into a GL_RG texture (I use 32-bit floating points). The resulting image is then blurred and the variance of the values can be calculated from the average squared depth stored in the green channel.


    Then we use Chebyshev's inequality to get an average shadow value:

    So it turns out, statistics is actually good for something useful after all. Here are the results:

    The shadow edges are actually soft, without any graininess or pixelation. There is a black border on the edge of the cubemap faces, but I think this is caused by my calculated cubemap face not matching the one the hardware uses to perform the texture lookup, so I think it can be fixed.
    As an added bonus, this eliminates the need for a shadow offset. Shadow acne is completely gone, even in the scene below with a light that is extremely close to the floor.

    The banding you are seeing is added in the JPEG compression and it not visible in the original render.
    Finally, because the texture filtering is so smooth, shadowmaps look much higher resolution than with PCF filtering. By increasing the light range, I can light the entire scene, and it looks great just using a 1024x1024 cube shadow map.

    VSMs are also quite fast because they only require a single texture lookup in the final pass. So we get better image quality, and probably slightly faster speed. Taking extra time to pay attention to small details like this is going to make your games look great soon!
  23. Josh
    Building on the Asset Loader class I talked about in my previous blog post, I have added a loader to import textures from SVG files. In 2D graphics there are two types of image files. Rasterized images are made up of a grid of pixels. Vector images, on the other hand, are made up of shapes like Bezier curves. One example of vector graphics you are probably familiar with are the fonts used on your computer.
    SVG files are a vector image format that can be created in Adobe Illustrator and other programs:

    Because SVG images are resolution-independent, when you zoom in on these images they only become more detailed:

    This makes SVG images perfect for GUI elements. The Leadwerks GUI can be rendered at any resolution, to support 4K, 8K, or other displays. This is also great for in-game VR GUIs because you can scale the image to any resolution.
    SVG images can be loaded as textures to be drawn on the screen, and in the future will be able to be loaded as GUI images. You can specify a DPI to rasterize the image to, with a default setting of 96 dots per inch.

  24. Josh
    There's basically two kinds of vegetation, trees and grass.
     
    Grass is plentiful, ubiquitous, dense, but can only be seen at a near distance. Its placement is haphazard and unimportant. It typically does not have any collision. ("Grass" includes any small plants, bushes, rocks, and other debris.)
     
    Trees are fewer in number, larger, and can be seen from a great distance. Placement of groups of trees and individual instances is a little bit more important. Because these tend to be larger objects, they have physics collision.
     
    Now figuring out how to take advantage of modern GPU architecture with both of those types of objects is an interesting problem. I started with geometry shaders, thinking I could use them to create duplicate instances on the fly. This geometry shader will do just that:

    #version 400  
    #define MAX_INSTANCES 81
    #define MAX_VERTICES 243
    #define MAX_ROWS 1
     
    uniform mat4 projectioncameramatrix;
    uniform mat4 camerainversematrix;
     
    layout(triangles) in;
    layout(triangle_strip,max_vertices=MAX_VERTICES) out;
     
    in mat4 ex_entitymatrix[3];
    in vec4 ex_gcolor[3];
    in vec2 ex_texcoords0[3];
    in float ex_selectionstate[3];
    in vec3 ex_VertexCameraPosition[3];
    in vec3 ex_gnormal[3];
    in vec3 ex_tangent[3];
    in vec3 ex_binormal[3];
    in float clipdistance0[3];
     
    out vec3 ex_normal;
    //out vec4 ex_color;
    out vec3 ex_jtangent;
    out vec3 ex_jbinormal;
    out vec3 ex_jjtangent;
     
    void main()
    {
    //mat3 nmat = mat3(camerainversematrix);//[0].xyz,camerainversematrix[1].xyz,camerainversematrix[2].xyz);//39
    //nmat = nmat * mat3(ex_entitymatrix[0][0].xyz,ex_entitymatrix[0][1].xyz,ex_entitymatrix[0][2].xyz);//40
     
    for(int x=0; x<MAX_ROWS; x++)
    {
    for(int y=0; y<MAX_ROWS; y++)
    {
    for(int i=0; i<3; i++)
    {
    mat4 m = ex_entitymatrix;
    m[3][0] += x * 4.0f;
    m[3][2] += y * 4.0f;
    vec4 modelvertexposition = m * gl_in.gl_Position;
    gl_Position = projectioncameramatrix * modelvertexposition;
    ex_normal = ex_gnormal;
    //ex_color = ex_gcolor;
    ex_jjtangent = ex_gnormal;
    ex_jtangent = ex_gnormal;
    ex_jbinormal = ex_gnormal;
    EmitVertex();
    }
    EndPrimitive();
    }
    }
    }
     
    I soon discovered some severe hardware limitations that make geometry shaders unusable for this type of application. There's a 255 limit on the number of vertices that can be emitted, but there's an even harsher limit on the number of varying you can output from the shader. Once you add values for texcoords, normals, binormals, and tangents, geometry shaders actually only allow 16 instances per render...making them inappropriate for this purpose.
     
    What's really needed is an "instance shader" that could control how many times an object is rendered. This would simply discard an entire instance if it isn't in the camera frustum:

    uniform vec4 cameraplane0; uniform vec4 cameraplane1;
    uniform vec4 cameraplane2;
    uniform vec4 cameraplane3;
    uniform vec4 cameraplane4;
    uniform vec4 cameraplane5;
     
    uniform objectradius;
     
    uniform instancematrix[MAX_INSTANCES]
     
    bool PlaneDistanceToSphere(in vec4 plane in vec3 point, in float radius) {}
     
    main ()
    {
    mat4 mat = instancematrix[gl_InstanceID];
    vec3 pos = mat[3].xyz;
    float radius = objectradius * max(max(mat[0].length(),max[1].length),mat[2].length);
    if (PlaneDistanceToSphere(cameraplane0,position,radius) > 0.0) discard;
    }
     
    I realized I could render a number of instances without actually sending their 4x4 matrices to the GPU, and just generate the positions along a grid. This would start with an n X n grid and then add some noise to randomly rotate and scale each instance. The randomized positions would use the object's XZ position on the grid as the input, so I could dynamically generate the same orientation each time, without ever storing the object's actual position in memory. (This is why some Leadwerks 2 maps could be hundreds of megs of data.)
     
    Here is the code in the vertex shader that randomizes object scale and rotation:

    #define ROWSIZE 15 #define SEED 1
    #define randomness 3.0
    #define density 3.0
    #define scalevariation 1
     
    int id = gl_InstanceID;
    int x = id/ROWSIZE;
    int z = id - x * ROWSIZE;
     
    float angle = rand(vec2(SEED-z,SEED+x))*6.2831853;
     
    //Random rotation
    mat4 rotmat = mat4(1.0);
    rotmat[0][0] = sin(angle);
    rotmat[0][2] = cos(angle);
    rotmat[2][0] = -sin(angle+1.570796325);
    rotmat[2][2] = -cos(angle+1.570796325);
    entitymatrix_=rotmat*entitymatrix_;
     
    float scale = rand(vec2(SEED+z,SEED-x));
    float sgn = sign(scale-0.5);
    scale = (abs(scale-0.5));
    scale *= scale;
    scale = (scale *sgn + 0.5);
     
    scale = scale * scalevariation + 1.0-scalevariation/2.0;
    entitymatrix_[0] *= scale;
    entitymatrix_[1] *= scale;
    entitymatrix_[2] *= scale;
    entitymatrix_[3][0] = x*density + rand(vec2(SEED+z,SEED-x))*randomness;
    entitymatrix_[3][2] = z*density + rand(vec2(SEED+x,SEED-z))*randomness;
     
    Here is the result with trees randomly oriented entirely on the GPU:
     

     
    Of course you can have issues like no way to prevent two instances from being too close together, but a random seed, density, and randomness values are adjustable. It would also be possible to calculate a neighbor's position and use that to weight the position of the current instance.
     
    There are still many questions to be answered, but I think this approach is going in the right direction to design a more powerful and lower overhead vegetation rendering system for Leadwerks 3.
×
×
  • Create New...