Jump to content

Josh

Staff
  • Posts

    23,140
  • Joined

  • Last visited

Blog Entries posted by Josh

  1. Josh
    With tessellation now fully implemented, I was very curious to see how it would perform when applied to arbitrary models. With tessellation, vertices act like control points for a Bezier mesh that is subdivided dynamically in screen space. Could tessellation be used to add new details to any low-poly model?
    Here is a low-res character model with a pointy head and obvious sharp edges all around his silhouette:


    When tessellation is enabled, the sharp edges go away and the mesh magically appears more detailed:


    Let's take a look at the "Doom Seeker" enemy model from Doom 3. The original model is 640 triangles:


    Tessellation gets rid of the blocky outline.

    And displacement adds new details to the model.


    What about other shapes? Can this technique be used to turn any low-poly model into a high-res version? Here is a tire model from "The Zone" DLC. Even with displacement set to zero on edge vertices, cracks still appear.

    So my conclusion on this is that tessellation is fantastic for closed organic shapes. In fact, I think it totally replaces separate LOD meshes for rocks, characters, and other organic shapes. However, it is not something you should use everywhere without any discretion.
  2. Josh
    Now that I have all the Vulkan knowledge I need, and most work is being done with GLSL shader code, development is moving faster. Before starting voxel ray tracing, another hard problem, I decided to work one some *relatively* easier things for a few days. I want tessellation to be an every day feature in the new engine, so I decided to work out a useful implementation of it.
    While there are a ton of examples out there showing how to split a triangle up into smaller triangles, useful discussion and techniques of in-game tessellation is much more rare. I think this is because there are several problems to solve before this technical feature can really be made practical.
    Tessellation Level
    The first problem is deciding how much to tessellate an object. Tessellation should use a single detail level per set of primitives being drawn. The reason for this is that cracks will appear when you apply displacement if you try to use a different tessellation level for each polygon. I solved this with some per-mesh setting for tessellation parameters.
    Note: In Leadwerks Game Engine, a model is an entity with one or more surfaces. Each surface has a vertex array, indice array, and a material. In Turbo Game Engine, a model contains one of more LODs, and each LOD can have one or more meshes. A mesh object in Turbo is like a surface object in Leadwerks.
    We are not used to per-mesh settings. In fact, the material is the only parameter meshes contained other than vertex or indice data. But for tessellation parameters, that is exactly what we need, because the density of the mesh polygons gives us an idea of how detailed the tessellation should be. This command has been added:
    void Mesh::SetTessellation(const float detail, const float nearrange, const float farrange) Here are what the parameters do:
    detail: the higher the detail, the more the polygons are split up. Default is 16. nearrange: the distance below which tessellation stops increasing. Default is 1.0 meters. farrange: the distance below which tessellation starts increasing. Default is 20.0 meters. This command gives you the ability to set properties that will give a roughly equal distribution of polygons in screen space. For convenience, a similar command has been added to the model class, which will apply the settings to all meshes in the model.
    Surface Displacement
    Another problem is culling offscreen polygons so the GPU doesn't have to process millions of extra vertices. I solved this by testing if all control vertices lie outside one edge of the camera frustum. (This is not the same as testing if any control point is inside the camera frustum, as I have seen suggested elsewhere. The latter will cause problems because it is still possible for a triangle to be visible even if all its corners are outside the view.) To account for displacement, I also tested the same vertices with maximum displacement applied.
    To control the amount of displacement, a scale property has been added to the displacementTexture object scheme:
    "displacementTexture": { "file": "./harshbricks-height5-16.png", "scale": 0.05 } A Boolean value called "tessellation" has also been added to the JSON material scheme. This will tell the engine to choose a shader that uses tessellation, so you don't need to explicitly specify a shader file. Shadows will also be rendered with tessellation, unless you explicitly choose a different shadow shader.
    Here is the result:

    Surface displacement takes the object scale into account, so if you scale up an object the displacement will increase accordingly.
    Surface Curvature
    I also added an implementation of the PN Triangles technique. This treats a triangle as control points for a Bezier curve and projects tessellated curved surfaces outwards.
     
    You can see below the shot using the PN Triangles technique eliminates the pointy edges of the sphere.


    The effects is good, although it is more computationally expensive, and if a strong displacement map is in use, you can't really see a difference. Since vertex positions are being changed but texture coordinates are still using the same interpolation function, it can make texture coordinates appear distorted. To counter this, texture coordinates would need to be recalculated from the new vertex positions.
    EDIT:
    I found a better algorithm that doesn't produce the texcoord errors seen above.


    Finally, a global tessellation factor has been added that lets you scale the effect for different hardware levels:
    void SetTessellationDetail(const float detail) The default setting is 1.0, so you can use this to scale up or down the detail any way you like.
  3. Josh
    It is now possible to compile shaders into a single self-contained file that can loaded by any Vulkan program, but it's not obvious how this is done. After poking around for a while I found all the pieces I needed to put this together.
    Compiling
    First, you need to compile each shader stage from a source code file into a precompiled SPIR-V file. There are several tools available to do this, but I prefer GLSlangValidator because it supports the Google #include extension. Put your vertex shader code in a text file named "shader.vert" and your pixel shader code in a file called "shader.frag". Create a .bat file in the same directory with the following contents:
    glslangValidator.exe "shader.vert" -V -o "vert.spv" glslangValidator.exe "shader.frag" -V -o "frag.spv" Run the bat file and two .spv files will be saved.
    Linking
    Now we want to combine our two files representing different shader stages into a single file. This is done with the link tool from Khronos. Add the following lines to your .bat file to compile the two .spv files into one. It will also delete the existing files to clean things up a little:
    spirv-link "vert.spv" "frag.spv" -o "shader.spv" del "vert.spv" del "frag.spv" This will save a single file named "shader.spv" that you can load as one shader module and use for different stages in Vulkan.
    Here are the required executables and a .bat file:
    BuildShader.zip
    Parsing
    If you always use vertex and fragment stages then there is no problem, but what if the combined .spv file contains other stages, or is missing a fragment stage? We can easily account for this with a minimal SPIR-V file parser. We're not going to include any big bloated libraries to do this because we only need some basic information about what stages are contained in the shader. Fortunately, the SPIR-V specification is pretty simple and it doesn't take much code to extract the information we want:
    std::string entrypointname[6]; auto stream = ReadFile(L"Shaders/shader.spv"); // Parse SPIR-V data Assert(stream->ReadInt() == 0x07230203); int version = stream->ReadInt(); int genmagnum = stream->ReadInt(); int bound = stream->ReadInt(); int reserved = stream->ReadInt(); bool stages[6] = {false,false,false,false,false,false}; // Instruction stream while (stream->Ended() == false) { int pos = stream->GetPos(); unsigned int bytes = stream->ReadUInt(); int opcode = LOWORD(bytes); int wordcount = HIWORD(bytes); if (opcode == 15) { int executionmodel = stream->ReadInt(); Assert(executionmodel >= 0); if (executionmodel < 6) { stream->ReadInt(); // entry point stages[executionmodel] = true; entrypointname[executionmodel] = stream->ReadString(); } } stream->Seek(pos + wordcount * 4); } This code even retrieves the entry point name for each stage, so you can be sure you are loadng the shader correctly.
    Here are the different shader stages from the SPIR-V specification:
    0: Vertex 1: TessellationControl 2: TessellationEvaluation 3: Geometry 4: Fragment 5: GLCompute That's it! We now have a standard single-file shader format for Vulkan programs. Your code for creating these will look something like this:
    VkShaderModule shadermodule; // Create shader module VkShaderModuleCreateInfo shaderCreateInfo = {}; shaderCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; shaderCreateInfo.codeSize = bank->GetSize(); shaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(bank->buf); VkAssert(vkCreateShaderModule(device->device, &shaderCreateInfo, nullptr, &shadermodule)); // Create vertex stage info VkPipelineShaderStageCreateInfo vertShaderStageInfo = {}; vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; vertShaderStageInfo.module = shadermodule; vertShaderStageInfo.pName = entrypointname[0].c_str(); VkPipelineShaderStageCreateInfo fragShaderStageInfo = {}; if (stages[4]) { // Create fragment stage info fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; fragShaderStageInfo.module = shadermodule; fragShaderStageInfo.pName = entrypointname[4].c_str(); } // Create your graphics pipeline...  
  4. Josh
    First, I was experiencing some crashes due to race conditions. These are very very bad, and very hard to track down. The problems were being caused by reuse of thread returned objects. Basically, a thread performs some tasks, returns an object with all the processed data, and then once the parent thread is done with that data it is returned to a pool of objects available for the thread to use. This is pretty complicated, and I found that when I switched to just creating a new return object each time the thread runs, the speed was the same as before. So the system is nice and stable now. I tend to be very careful about sharing data between threads and only doing it in a prescribed manner (through a command buffer and using separate objects) and I will continue to use this approach.
    Second, I added a built-in mouselook mode for cameras. You can call Camera::SetFreeLook(true) and get a automatic mouse controls that make the camera look around. I am not doing this to make things easier, I am doing it because it allows fast snappy mouse looking even if your game is running at a lower frequency. So you can run your game at 30 hz, giving you 33 milliseconds for all your game code to complete, but it will feel like 60+ hz because the mouse will update in the rendering thread, which is running at a faster speed. The same idea will be used to eliminate head movement latency in VR.
    Finally, I switched the instance indexes that are uploaded to the GPU from integers to 16-bit unsigned shorts. You can still have up to 131072 instances of a single object, because the engine will store instances above and below 65536 in two separate batches, and then send an integer to the shader to add to the instance index. Again, this is an example of a hard limit I am putting in place in order to make a more structured and faster performing engine, but it seems like the constraints I am setting so far are unlikely to even be noticed.
    Animation is working great, and performance is just as fast as before I started adding it, so things are looking good. Here's a funny picture of me trying to add things to the renderer to slow it down and failing :

    I'm not sure what I will tackle next. I could work on threading the physics and AI, spend some time exploring new graphics options, or implement lighting so that we have a basic usable version of Leadwerks 5 for indoors games. What would you like to see next in the Leadwerks Game Engine 5 Alpha?
  5. Josh
    The latest design of my OpenGL renderer using bindless textures has some problems, and although these can be resolved, I think I have hit the limit on how useful an initial OpenGL implementation will be for the new engine. I decided it was time to dive into the Vulkan API. This is sort of scary, because I feel like it sets me back quite a lot, but at the same time the work I do with this will carry forward much better. A Vulkan-based renderer can run on Windows, Linux, Mac, iOS, Android, PS4, and Nintendo Switch.
    So far my impressions of the API are pretty good. Although it is very verbose, it gives you a lot of control over things that were previously undefined or vendor-specific hacks. Below is code that initializes Vulkan and chooses a rendering device, with a preference for discrete GPUs over integrated graphics.
    VkInstance inst; VkResult res; VkDevice device; VkApplicationInfo appInfo = {}; appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = "MyGame"; appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.pEngineName = "TurboEngine"; appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.apiVersion = VK_API_VERSION_1_0; // Get extensions uint32_t extensionCount = 0; vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); std::vector<VkExtensionProperties> availableExtensions(extensionCount); vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, availableExtensions.data()); std::vector<const char*> extensions; VkInstanceCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo; createInfo.enabledExtensionCount = (uint32_t)extensions.size(); createInfo.ppEnabledExtensionNames = extensions.data(); #ifdef DEBUG createInfo.enabledLayerCount = 1; const char* DEBUG_LAYER = "VK_LAYER_LUNARG_standard_validation"; createInfo.ppEnabledLayerNames = &DEBUG_LAYER; #endif res = vkCreateInstance(&createInfo, NULL, &inst); if (res == VK_ERROR_INCOMPATIBLE_DRIVER) { std::cout << "cannot find a compatible Vulkan ICD\n"; exit(-1); } else if (res) { std::cout << "unknown error\n"; exit(-1); } //Enumerate devices uint32_t gpu_count = 1; std::vector<VkPhysicalDevice> devices; res = vkEnumeratePhysicalDevices(inst, &gpu_count, NULL); if (gpu_count > 0) { devices.resize(gpu_count); res = vkEnumeratePhysicalDevices(inst, &gpu_count, &devices[0]); assert(!res && gpu_count >= 1); } //Sort list with discrete GPUs at the beginning std::vector<VkPhysicalDevice> sorteddevices; for (int n = 0; n < devices.size(); n++) { VkPhysicalDeviceProperties deviceprops = VkPhysicalDeviceProperties{}; vkGetPhysicalDeviceProperties(devices[n], &deviceprops); if (deviceprops.deviceType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { sorteddevices.insert(sorteddevices.begin(),devices[n]); } else { sorteddevices.push_back(devices[n]); } } devices = sorteddevices; VkDeviceQueueCreateInfo queue_info = {}; unsigned int queue_family_count; for (int n = 0; n < devices.size(); ++n) { vkGetPhysicalDeviceQueueFamilyProperties(devices[n], &queue_family_count, NULL); if (queue_family_count >= 1) { std::vector<VkQueueFamilyProperties> queue_props; queue_props.resize(queue_family_count); vkGetPhysicalDeviceQueueFamilyProperties(devices[n], &queue_family_count, queue_props.data()); if (queue_family_count >= 1) { bool found = false; for (int i = 0; i < queue_family_count; i++) { if (queue_props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { queue_info.queueFamilyIndex = i; found = true; break; } } if (!found) continue; float queue_priorities[1] = { 0.0 }; queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queue_info.pNext = NULL; queue_info.queueCount = 1; queue_info.pQueuePriorities = queue_priorities; VkDeviceCreateInfo device_info = {}; device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; device_info.pNext = NULL; device_info.queueCreateInfoCount = 1; device_info.pQueueCreateInfos = &queue_info; device_info.enabledExtensionCount = 0; device_info.ppEnabledExtensionNames = NULL; device_info.enabledLayerCount = 0; device_info.ppEnabledLayerNames = NULL; device_info.pEnabledFeatures = NULL; res = vkCreateDevice(devices[n], &device_info, NULL, &device); if (res == VK_SUCCESS) { VkPhysicalDeviceProperties deviceprops = VkPhysicalDeviceProperties{}; vkGetPhysicalDeviceProperties(devices[n], &deviceprops); std::cout << deviceprops.deviceName; vkDestroyDevice(device, NULL); break; } } } } vkDestroyInstance(inst, NULL);  
  6. Josh
    Shadow maps are now supported in the new Vulkan renderer, for point, spot, and box lights!

    Our new forward renderer eliminates all the problems that deferred renderers have with transparency, so shadows and lighting works great with transparent surfaces. Transparent objects even receive lighting from their back face automatically!

    There is some shadow acne, which I am not going to leave alone right now because I want to try some ideas to eliminate it completely, so you never have to adjust offsets or other settings. I also want to take another look at variance shadow maps, as these can produce much better results than depthmap shadows. I also noticed some seams in the edges of point light shadows.
    Another interesting thing to note is that the new renderer handles light and shadows with orthographic projection really well.

    A new parameter has also been added to the JSON material loader. You can add a scale factor inside the normalTexture block to make normal maps appear bumpier. The default value is 1.0:
    "normalTexture": { "file": "./glass_dot3.tex", "scale": 2.0 } Also, the Context class has been renamed to "Framebuffer". Use CreateFramebuffer() instead of CreateContext().
  7. Josh
    After about four days of trying to get render-to-texture working in Vulkan, I have everything working except...it doesn't work. No errors, no clue what is wrong, but the renderer is not even clearing the depth attachment, which is why the texture read shown here is flat red.

    There's not much else to say right now. I will keep trying to find the magic combination of cryptic obscure settings it takes to make Vulkan do what I want.
    This is very hard stuff, but once I have it working I think that gives me all the functionality I need to finish the Vulkan renderer. Post-processing effects are just another render-to-texture problem. Now, the Vulkan renderer is much more strict than OpenGL, and I think the way post-processing will work is with a an automatically created blur texture and a single post-process shader. It's very wasteful to render a bunch of different passes for each effect, so instead I think we will just have some different effects files and a shader that can include whatever effects you want. In fact, the post-process pass can probably be done as a third Vulkan subpass which would be very efficient.
  8. Josh
    Vulkan gives us explicit control over the way data is handled in system and video memory. You can map a buffer into system memory, modify it, and then unmap it (giving it back to the GPU) but it is very slow to have a buffer that both the GPU and CPU can access. Instead, you can create a staging buffer that only the CPU can access, then use that to copy data into another buffer that can only be read by the GPU. Because the GPU buffer may be in-use at the time you want to copy data to it, it is best to insert the copy operation into a command buffer, so it happens after the previous frame is rendered. To handle this, we have a pool of transfer buffers which are retrieved by a command buffer when needed, then released back into the pool once that command buffer is finished drawing. A fence is used to tell when the command buffer completes its operations.
    One issue we came across with OpenGL in Leadwerks was when data was uploaded to the GPU while it was still being accessed to render a frame. You could actually see this on some cards when playing my Asteroids3D game. There was no mechanism in OpenGL to synchronize memory, so the best you could do was put data transfers at the start of your rendering code, and hope that there was enough of a delay before your drawing actually started that the memory copying had completed. With the super low-overhead approach of Vulkan rendering, this problem would become much worse. To deal with this, Vulkan uses explicit memory management with something called pipeline barriers. When you add a command into a Vulkan command buffer, there is no guarantee what order those commands will be executed in, and pipeline barriers allow you to create a point where certain commands must be executed before other ones can begin.
    Here are the order of operations:
    Start recording new command buffer. Retrieve staging buffer from pool and remove from pool. Copy data into staging buffer. Insert command to copy from staging buffer to the GPU buffer. Insert pipeline barrier to make sure data is transferred before drawing begins. Execute the command buffer. When the fence is completed, move all staging buffers back into the staging buffer pool. In the new game engine, we have several large buffers to store the following data:
    Mesh vertices Mesh indices Entity 4x4 matrices (and other info) A list of visible entity IDs Visible light information. Skeleton animation data I found this data tends to fall into two categories.
    Some data is large and only some of it gets updated each frame. This includes entity 4x4 matrices, skeleton animation data, and mesh vertex and index data. Other data tends to be smaller and only concerns visible objects. This includes visible entity IDs and light information. This data is updated completely each time a new visibility set arrives. The first type of data requires data buffers that can be resized, because they can be very large, and more objects or data might be added at any time. For example, the vertex buffer contains all vertices that exist, in all meshes the user creates or loads. If a new mesh is loaded that requires space greater than the buffer capacity, a new buffer must be created, then the full contents of the old buffer are copied over, directly in GPU memory. A new pipeline barrier is inserted to ensure the data transfer to the new buffer is finished, and then additional data is copied.
    The second type of data is a bit simpler. If the existing buffer is not big enough, a new bigger buffer is created. Since the entire contents of the buffer are uploaded with each new visibility set, there is no need to copy any existing data from the old buffer.
    I currently have about 2500 lines of Vulkan-specific code. Calling this "boilerplate" is disingenuous, because it is really specific to the way you set your renderer up, but the core mesh rendering system I first implemented in OpenGL is working and I will soon begin adding support for textures.
     
  9. Josh
    I am experimenting with a system for creating a sequence of actions using Lua coroutines. This allows you to define a bunch of behavior at startup and let the game just run without having to keep track of a lot of states.
    You can add coroutines to entities and they will be executed in order. The first one will complete, and then the next one will start.
    A channel parameter allows you to have separate stacks of commands so you can have multiple sequences running on the same object. For example, you might have one channel that controls entity colors while another channel is controlling position.
    function Script:Start() local MotionChannel = 0 local ColorChannel = 1 local turnspeed = 1 local colorspeed = 3 --Rotate back and forth at 1 degree / second self:AddCoroutine(MotionChannel, ChangeRotation, 0, 45, 0, turnspeed) self:AddCoroutine(MotionChannel, ChangeRotation, 0, -45, 0, turnspeed) self:LoopCourtines(MotionChannel)--keeps the loop going instead of just running once --Flash red and black every 3 seconds self:AddCoroutine(ColorChannel, ChangeColor, 1, 0 , 0, 1, colorspeed) self:AddCoroutine(ColorChannel, ChangeColor, 0, 0, 0, 1, colorspeed) self:LoopCourtines(ColorChannel)--keeps the loop going instead of just running once end There's no Update() function! Where do the coroutine functions come from? These can be in the script itself, or they can be general-use functions loaded from another script. For example, you can see an example of a MoveToPoint() coroutine function in this thread.
    The same script could be created using an Update function but it would involve a lot of stored states. I started to write it out actually for this blog, but then I said "ah screw it, I don't want to write all that" so you will have to use your imagination.
    Now if you can imagine a game like the original Warcraft, you might have a script function like this that is called when the player assigns a peasant to collect wood:
    function Script:CollectWood() self:ClearCoroutines(0) self:AddCoroutine(0, self.GoToForestAndFindATree) self:AddCoroutine(0, self.ChopDownTree) self:AddCoroutine(0, self.GoToCastle) self:AddCoroutine(0, self.Wait, 6000) self.AddCoroutine(0, self.DepositWood, 100) self:LoopCoroutines(0) end I wonder if there is some way to create a sub-loop so if the NPC gets distracted they carry out some actions then return to the sequence they were in before, at the same point in the sequence.
    Of course this would work really well for cutscenes or any other type of one-time sequence of events.
  10. 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.
  11. Josh
    This is a good time to write about some very broad changes I expect to come about over the next year in our community as our new engine "Turbo" arrives. Turbo Game Engine, as the name suggests, offers really fast performance using a groundbreaking Vulkan-based renderer, which is relevant to everyone but particularly beneficial for VR developers who struggle to keep their framerates up using conventional game engines. I want to help get you onboard with some of the ideas that I myself am processing.
    Less emphasis on how-to tutorials, more emphasis on API documentation
    The new engine assumes you are either an artist or a programmer, and if you are a programmer you already know basic C++ or Lua. More attention will be paid to precisely documenting how commands behave. There will be a more strict division between supported and unsupported features. There will be less "guessing" what the user is trying to do, and more formal documentation saying "if you do X then Y will occur". For example, every entity creation function requires the world object be explicitly supplied in the creation command, instead of hiding this away in a global state. There will not be tutorials explaining what a variable is or teaching basic programming concepts.
    More responsiveness to user requests, especially for programming features
    Leadwerks 4 features have been in a semi-frozen state for a while now. Although many new features have been added, I have not wanted to create breaking changes, and have been reluctant to introduce things that might create new bugs, because I knew an entire new infrastructure for future development was on the way. With the new engine I will be more receptive to suggestions that make the engine better. One example would be an animation events system that lets users set a point in an animation where an event is called. These changes need to be implemented within the design philosophy of the new engine. For example, I would use an Actor class method to call the event function rather than a raw pointer. Emphasis should be placed on what is practical and useful for competent programmers and artists, and how everything fits into the overall design.
    Less attempts at hand-holding for new developers
    The new engine will not attempt to teach children to make their own MMORPG. Our marketing materials will not even suggest this is possible. The new engine will deliver performance faster than any other game engine in the world, period. Consequently, I think the community will gain a lot more advanced users, and though some of them will not even interact on the forum I do think you will see more organic creativity and quality. In its own way, the new engine actually is quite a lot easier to work with, but the sales pitch is not going to emphasize that, it will just be something people discover as they use it. I love seeing all the weird and cool creations that comes from people who are completely new to game development, but those people were new to game development and did well with Leadwerks had a lot of natural talent. Instead of trying to come up with a magic combination of features and tutorials to turn novices into John Carmack, we are going to rely on the product benefits to draw them and expect them to get up to speed quickly. Discussions should be about what is best for intermediate / experts, not trying to figure out what beginners want. Ease of use is subjective and I feel we have hit the point of diminishing returns chasing after this. If beginners want to jump in and learn that is great, but it is not our reason for existing.
    Stronger focus on the core essentials
    At the time of this writing, there are only eight entity types in the beta of the new engine. We can't win based on number of features, but we can do the core essentials much better than anyone else. Our new Vulkan renderer offers performance that developers (especially VR) can't live without. Models, lights, and rendering are the core features I want to focus on, and these can be expanded by the end user to create their own. For example, a custom particle system with support for all kinds of behaviors could easily be created with the model class and a few custom shaders, without breaking the performance that makes this engine valuable. Our new technology is very well thought out and will give us a stable base for a long time. I am planning on a plugin / extensions system because its best for this to be integrated in the core design, but you should not expect this to be very useful for a couple of years. Plugin systems require huge network effects to offer anything valuable. We can only reach that type of scale by offering something else unique that no one can match us on. Fortunately, we have something. It's right in the name.
    More formal support for good standards
    Vulkan has turned out to be a very good move. I don’t think anyone realizes how big a deal GLTF support is yet: you can download thousands of models from Sketchfab and other sources and load them right now with no adjustments. I may join the Khronos consortium and I have some ideas for additional useful GLTF extensions. I'm using JSON for a lot of files and it's great. DDS will be our main texture file format. There are more good standards today than there were ten years ago, and I will adopt the ones that fit our goals.
    Different type of new user appearing
    With Leadwerks, the average new user appears on the forum and says “hey, I want to make a game but I don’t really know how, please tell me what I need to know.” With the new engine I think it will be more like “hey, I’m more or less an expert already, I know exactly what I want to make, please tell me what I need to know.” I expect them to have less tolerance for bugs, undefined behavior, or undocumented features, and at the same time I think it will be easier to have frank discussions about exactly what developers need.
    In very general terms that is how I want to focus things. I think everyone here will adjust to this more strict and well-defined approach and end up liking it a lot better.
  12. Josh
    It's always fun when I can do something completely new that people have never seen in a game engine. I've had the idea for a while to create a new light type for light strips, and I got to implement this today. The new engine has taken a tremendous amount of effort to get working over two years, but as development continues I think I will become much more responsive to your suggestions since we have a very strong foundation to build on now.
    Using this test scene provided by @reepblue you can see how this new light type looks and behaves. They are great for placing along walls, but what really made me interested was the idea to calculate specular lighting not from a single point, but with a different way. I thought if I could figure out the math I would get a realistic reflection on the ground, and it worked!

    The reflection on the floor is actually the specular component of the light. We are used to thinking of specular reflections as a little white circle that moves around, but the light doesn't have to be coming from a single point. Some calculations in the shader can be used to determine the closest point to the light strip and use that for reflections. The net effect is that a long bar appears on the floor, matching the length of the light. This is not a screen-space effect or a cubemap. When you look down at the floor the specular component is still there shining back at you. Every surface is using the same exact equation, but it appears very different on the walls, the ceiling, and the floor due to the different angles.

    Even a surface facing opposite the light will correctly reflect it back to the camera.

    In this image, I created a small green strip light that looks like a laser. There is no visible laser beam, but if there was it would appear above the soft green lighting. The hard line on the ground is actually the specular reflection of the light. You can see it reflecting off the sphere as well.

    The new Vulkan renderer also supports box lights, which are a directional light with a defined boundary, and I have an idea for one more type of light.
  13. 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.
  14. Josh
    A new update is available for beta subscribers. Transparent materials are now supported. Unlike the old deferred renderer, our new clustered forward renderer supports transparency really really well! You can add these in a JSON material file with a Boolean property called "transparent" set to true:
    "transparent": true There are no separate blend modes now, since pre-multiplied alpha allows alpha and additive blending in a single pass. This is actually a really simple technique but for some reason all the information about it online are these horrible academic examples that don't show any clear benefit. It wasn't until I thought "how do I make shiny glass?" that I actually started looking for a way to do this.
    The command Material::SetTransparent(true) replaces the old SetBlendMode(blendmode) function. GLTF materials with blending will be automatically loaded. Per-object Z-sorting is not yet supported, but transparent groups of objects will always be rendered on top of opaque objects automatically, so you don't actually need to enable sorting if you don't expect to have two layers of transparency visible anywhere..

    We haven't seen a lot of transparency in games since the mid-2000s, because at the time deferred rendering was the best lighting technique. I think the capabilities of our new renderer will open up a lot of possibilities to create games that look different from anything in the past.
    You can get access to the beta and private forum right now for just $5.
  15. Josh
    An update for the beta of the new engine is now available with the following changes:
    GLTF loader is now working for most models. A large collection of GLTF files are available online for free from many sources, and they can be loaded right into the engine without any adjustment for materials or textures. Single-file GLB files also work. Added support for GLTF extension KHR_materials_pbrSpecularGlossiness. Disabled PNG loader gamma correction. world->SetSkybox(texture) can now be used to make PBR reflections appear. (The sky will not yet be visible though.) I'm going to try to use a voxel GI system for further reflections, and not use environment probes at all. Window::GetWidth(), Window::GetHeight(), Context::GetWidth(), and Context::GetHeight() are removed. Use GetSize() instead. It will return an iVec2 object with x and y components. JSON material files are changed slightly. in order to accommodate additional per-texture settings. None of these work yet, but you can see where it is going: "albedoMap": { "file": "./Rough-rockface1_Base_Color.jpg", "filter": "linear", "tilingU": "repeat", "tilingV": "repeat" }, So before if you had this:
    "albedoMap": "./Rough-rockface1_Base_Color.jpg" Just change it to this:
    "albedoMap": {"file": "./Rough-rockface1_Base_Color.jpg"} And then it will work.
    The screenshot below is a GLTF loaded and rendered with Vulkan. Note that this model uses baked lighting that is already included in the model. But the fact I can download these things and have them appear correctly with no adjustments is great.

    You can get access to the beta and private forum right now for just $5.
  16. Josh
    A new beta update is available for subscribers. What's new?
    Lighting
    Point and spot lights are now supported in the new Vulkan renderer, with either PBR or Blinn-Phong lighting. Lighting is controlled by the shader in the material file. There are two main shaders you can use, "Shaders/PBR.spv" and "Shaders/Blinn-Phong.spv". See below for more details.

    JSON Materials
    Materials can now be loaded from JSON files. I am currently using the .json file extension instead of "mat", "mtl", or something else. If you load a scene and a JSON file is available with the same name as a material in that scene, the material will be loaded from a JSON file instead of the Leadwerks 4 .mat files. For example, you can create a JSON file named "brick01.json", place it in the same folder as "brick01.mat" and the new engine will load the JSON material if the brick material is used in a scene. However, it is not necessary to do this as the engine can also load Leadwerks 4 material files.
    A Turbo JSON material file looks like this. The string tokens are more or less locked in now and it is safe to start using them.
    { "turboMaterialDef": { "color": [ 1, 1, 1, 1 ], "emission": [ 0, 0, 0 ], "metallic": 0, "roughness": 0.6, "doubleSided": false, "blend": false, "albedoMap": "./concrete_clean_diff.tex", "normalMap": "./concrete_clean_dot3.tex", "metallicRoughnessMap": "", "emissionMap": "", "baseShader": "Shaders/PBR.spv", "shadowShader": "Shaders/Shadow.spv", "depthShader": "Shaders/DepthPass.spv" } } You can also indicate a shader for the new engine to use in an old Leadwerks 4 material file by adding a text line like this to the .mat file:
    baseshader="Shaders/myshader.spv" You do not need to specify a shader unless you are using a custom shader. JSON material files, by default, will use the PBR shader. Leadwerks 4 material files, by default, will use the Blinn-Phong shader.
    BC5 / BC7 Texture Compression
    A ton of new compression formats have been added, including the BC7 and BC5 formats, which provide better quality than DXT compression. Visual Studio 2019 actually has some good built-in DDS tools, although the BC7 compressor Is very slow. A sample material is provided using DDS textures (see "Materials/Rough-rockface1.json").
    Lua Commands
    A set of simple global Lua commands has been added.
    template<typename T> void LuaSetGlobal(const std::string& name, T var) template<typename T> void LuaPushObject(const std::string& name, T var) template<typename T> T LuaToObject(const int index = -1) int LuaCollectGarbage(const int what = LUA_GCCOLLECT, const int data = 0); void LuaPushString(const std::string& s); void LuaPushNumber(const double n); void LuaPushBoolean(const bool b); void LuaPushNil(); void LuaPushValue(const int index = -1); bool LuaIsTable(const int index = -1); bool LuaIsNumber(const int index = -1); bool LuaIsString(const int index = -1); bool LuaIsBoolean(const int index = -1); bool LuaIsObject(const int index = -1); bool LuaIsNil(const int index = -1); bool LuaIsFunction(const int index = -1); bool LuaToBoolean(const int index = -1); std::string LuaToString(const int index = -1); double LuaToNumber(const int index = -1); int LuaType(const int index = -1); int LuaGetField(const std::string& name, const int index = -1); int LuaGetTable(const std::string& name, const int index = -1); int LuaGetGlobal(const std::string& name); void LuaSetField(const std::string& name, const int index = -1); void LuaSetTable(const int index = -1); void LuaPop(const int levels = 1); void LuaRemove(const int index = -1); void LuaSetStackSize(const int sz); int LuaGetStackSize(); void LuaNewTable(); This makes our code simpler and more readable:
    #include "Turbo.h" using namespace Turbo; 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); //Set some variables in the script environment LuaSetGlobal("mainwindow", window); LuaSetGlobal("maincontext", context); //Create the world auto world = CreateWorld(); //Load a scene auto scene = LoadScene(world, "Maps/start.map"); //Show off a PBR material auto sphere = CreateSphere(world); auto mtl = LoadMaterial("Materials/Rough-rockface1.json"); sphere->SetMaterial(mtl); sphere->Move(0, 1, 0); sphere->SetScale(2); while (window->KeyHit(KEY_ESCAPE) == false and window->Closed() == false) { world->Update(); world->Render(context); } return 0; } You can gain access to the beta and support development by subscribing for just $5.
  17. Josh
    The Vulkan renderer now supports new texture compression formats that can be loaded from DDS files. I've updated the DDS loader to support newer versions of the format with new features.
    BC5 is a format ATI invented (originally called ATI2 or 3Dc) which is a two-channel compressed format specifically designed for storing normal maps. This gives you better quality normals than what DXT compression (even with the DXT5n swizzle hack) can provide.
    BC7 is interesting because it uses the same size as DXT5 images but provides much higher quality results. The compression algorithm is also very long, sometimes taking ten minutes to compress a single texture!  Intel claims to have a fast-ish compressor for it but I have not tried it yet. Protip: You can open DDS files in newer versions of Visual Studio and select the compression format there.
    Here is a grayscale gradient showing uncompressed, DXT5, BC7 UNORM, and BC7 SNORM formats. You can see BC7 UNORM and SNORM have much less artifacts than DXT, but is not quite the same as the original image.





    The original image is 256 x 256, giving the following file sizes:
    Uncompressed: 341 KB DXT1: 42.8 KB (12.6% compression) DXT5, BC7: 85.5 KB (25% compression) I was curious what would happen if I zipped up some of the files, although this is only a minor concern. I guess that BC7 would not work with ZIP compression as well, since it is a more complicated algorithm.
    DXT5: 16.6 KB BC7 UNORM: 34 KB BC7 SNORM: 42.4 KB Based on the results above, I would probably still use uncompressed images for skyboxes and gradients, but anything else can benefit from this format. DXT compression looks like a blocky green mess by comparison.
    I was curious to see how much of a difference the BC5 format made for normal maps so I made some similar renders for normals. You can see below that the benefits for normal maps are even more extreme. The BC5 compressed image is indistinguishable from the original while the DXT5n image has clear artifacts.



    In conclusion, these new formats in the Vulkan renderer, when used properly, will provide compression without visible artifacts.
  18. Josh
    I now have point and spot lights working (without shadows) in the Vulkan renderer. Here are the results, with both "Physically-based rendering" (PBR) and Blinn-Phong shaders: Without the IBL contribution it's not terribly impressive, but this is progress.


  19. Josh
    Vulkan is pretty wonderful because I can take all the optimal techniques I worked out in OpenGL and it just makes everything much faster. I've successfully completed the implementation of early Z-pass, which is important for our lighting system. We are using a forward clustered renderer, similar to the technique id Software's new DOOM games use. Because the fragment shader is fairly intensive, a depth pre-pass is rendered to ensure we only process each screen pixel once.

    This technique also easily supports transparency with multiple layers of shadows.

    Vulkan has a feature called subpasses specifically designed for this type of functionality. It's really fantastic to have this kind of fine control over the hardware, even if it does involved some pretty convoluted code.
    You can read more about this rendering technique below.
     
  20. Josh
    I have basic point lights working in the Vulkan renderer now. There are no shadows or any type of reflections yet. I need to work out how to set up a depth pre-pass. In OpenGL this is very simple, but in Vulkan it requires another complicated mess of code. Once I do that, I can add in other light types (spot, box, and directional) and pull in the PBR lighting shader code. Then I will add support for a cubemap skybox and reflections, and then I will upload another update to the beta.

    Shadows will use variance shadow maps by default. With these, all objects must cast a shadow, but our renderer is so fast that this is not a problem. I've had very good results with these in earlier experiments.
    I then want to complete my work on voxel-based global illumination and reflections. I looked into Nvidia RTX ray tracing but the performance is awful even with a GEForce 2080. My voxel approach should provide good results with fast performance.
    Once these features are in place, I may release the new engine on Steam as a programming SDK, until the new editor is ready.
  21. Josh
    The beta of our new game engine has been updated with a new renderer built with the Vulkan graphics API, and all OpenGL code has been removed. Vulkan provides us with low-overhead rendering that delivers a massive increase in rendering performance. Early benchmarks indicate as much as a 10x improvement in speed over the Leadwerks 4 renderer.
    The new engine features an streamlined API with modern C++ features and an improved binding library for Lua. Here's a simple C++ program in Turbo:
    #include "Turbo.h" using namespace Turbo; 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); //Set some Lua variables VirtualMachine::lua->set("mainwindow", window); VirtualMachine::lua->set("maincontext", context); //Create the world auto world = CreateWorld(); //Load a scene auto scene = LoadScene(world, "Maps/start.map"); while (window->KeyHit(KEY_ESCAPE) == false and window->Closed() == false) { world->Update(); world->Render(context); } return 0; } Early adopters can get access to beta builds with a subscription of just $4.99 a month, which can be canceled at any time. New updates will come more frequently now that the basic renderer is working.
  22. Josh
    Shadows with a constant softness along their edges have always bugged me. Real shadows look like this. Notice the shadow becomes softer the further away it gets from the door frame.

    Here is a mockup of roughly what that shadow looks like with a constant softness around it. It looks so fake!

    How does this effect happen? There's not really any such thing as a light that all emits from a single point. The closest thing would be a very small bulb, but that still has volume. Because of this, shadows have a soft edge around them that gets less sharp the further away from the occluding object they are. I think some of this also has to do with photons hitting the edge of the object and scattering a bit as they go past it. The edge of the photon catches on the object and knocks it off course.

    We have some customers who need very realistic renderings, ideally as close to a photo as possible, and I wanted to see if I could create this behavior with our variance shadow maps. Here are the results: The shadows are sharp when they start being cast and become more blurry as light is scattered.

    Here's another shot. The shadows actually look real instead of just being blobby silhouettes.

    This is really turning out great!
  23. Josh
    I have it worked out now where the new engine will handle multiple shaders. The renderer groups meshes (renamed from "surfaces" in Leadwerks) by shader. A single draw call renders many batches of instances, with different materials applied. It's a very advanced and complex system, so something that was simple before, changing the shader, now requires a lot of code to make work! You can see here the barbed wire is using an alpha-discard shader that removes pixels while the rest of the scene uses the normal default shader.

    A new material file format will be implemented using the JSON data format. To indicate a shader in an existing Leadwerks material file for the new engine you can add a line of code like this to the file:
    newshader = "Shaders/myshader.spv" This will make it so the new engine can load the new shader, while the old engine will still see the old shader, in case you are using this in Leadwerks Editor.
    Although this new system took a mountain of code to get working, I am starting to feel the tremendous power it offers. The Zone is now rendering 10x faster than it does in Leadwerks.
  24. Josh
    I now have different materials with textures working in Vulkan. The API allows us to access every loaded texture in any shader, although some Intel chips have limitations and will require a fallback. This is interesting because some of our design decisions in Leadwerks 4 were made because we had a limit of 16 textures a shader could access. Terrain clipmaps were a good solution to this problem, but since the same limitations no longer exist it may be time to revisit this design. We could, for example, implement a shader that can access any loaded texture and use a single RGBA texture to indicate which texture should be used for each terrain point. This would allow up to 256 different layers. Best of all, the number of texture layers would have no effect on speed. It would run the same speed no matter how many textures you use. In fact the whole idea of "layers" is obsolete and not descriptive at all of what is happening. This would also eliminate the blurriness and weird filtering that can occur with clipmaps, and give us pixel-perfect terrain at any distance.


    An application has been uploaded for beta subscribers which will load and display any Leadwerks map with the new Vulkan renderer:
    Our new lighting system will work seamlessly with Vulkan. However, before I continue with lighting I want to resolve some problems in the current scope of the renderer. I've worked out a huge piece of the core renderer design now, and I think things will get easier soon.
     
  25. Josh
    A new update to Leadwerks 3.0 is now available. Registered developers can run the Leadwerks updater to download and install the patch. This update adds terrain, bug fixes, and a few small feature enhancements.
    Our new terrain system, described in our Kickstarter campaign to bring Leadwerks to the Linux operating system, is based on a unique "dynamic megatextures" approach. This technique renders sections of the terrain into virtual textures and places them around the camera. The terrain presently allows a maximum size of 1024 meters and 16 texture layers, but these constraints can be lifted in the future once it's been thoroughly tested. You can see an example terrain the the "terrain.map" scene included in the example project folder.

    With the increased scene geometry terrain brings, I found it necessary to precalculate navmeshes in the editor. To calculate a navmesh for a map, select the Tools > Build NavMesh menu item to being up the Build NavMesh Dialog. The navigation data will be saved directly into your map file for pathfinding. Two values have been exposed to control the navmesh calculation and the appearance of the navmesh has been improved to allow easier visual debugging. Additionally, the new World::BuildNavMesh command lets you calculate navigation meshes in code.
    The bug report forum contains info about recently fixed problems. The most notable fix was for character controller physics. Some frame syncing issues were fixed which were causing entities to sometimes pass through walls and floors. This problem was very apparent in the recent game demo GreenFlask,
    A new command World::SetPhysicsDetail allows you to balance the speed and accuracy of the physics simulator.
    The Transform::Plane command has been enhanced to work with Lua, which had trouble understanding the syntax of the command.
×
×
  • Create New...