GLSL to Babylon.js ShadersStore code

This is about writing a .NET Core application to automatically convert GLSL into Babylon.js ShaderStore code. The project is available on github here.

Introduction and features

I had to write a custom shader for a post-processing component for a BabylonJS project and I found the process of writing BabylonJS ShadersStore code time consuming. ShadersStore is GLSL code as a string value with each line added as a separate value for readability.


BABYLON.Effect.ShadersStore["LinesPixelShader"] =
"#ifdef GL_ES\n" +
"   precision highp float;\n" +
"#endif\n\n" +
"varying vec2 vUV; \n" +
"void main(void) {\n" +
"   gl_FragColor = vec4(vUV.x,vUV.y,-vUV.x, 1.0);\n" +
"}\n" +
"";

The snippet is from doc.babylonjs.com/tutorials/How_to_use_Procedural_Textures.

So what I wanted to do was work directly in .fx files and then convert that into ShadersStore code. If you don't want to use ShadersStore format you can import the shader code as a <script> element or load an .fx file programmatically in Babylon.

I had been wanting to test .NET Core console development for a while and decided to automate converting the GLSL inside a .fx file into ShadersStore code. This kind of program is probably better suited as a gulp/grunt task to reach a wider audience.

What is Babylon.js?

Babylon.js is an open source framework for building 3D games with html5. It really simplifies the usage of WebGL.

It is written in Typescript which I think gives it an edge over for example three.js.

I have been using Babylon.js for about 7 months (Jan 2017) and the community is great. If you have a question you can ask it on their forum. They are very good at answering and active and if the feature is missing in Babylon.js they might even develop it for you!

The only bad thing I can think of Babylon.js is that I've had to look at the source code sometimes to understand some things but it's a massive project so it's hard to have documentation for everything. Looking at the source code has also in some cases allowed for a deeper understanding of how the framework works.

The glsl-babylonshader project

This project converts *.fx into *.fx.output with the correct ShadersStore code.

When I started this project my goal was to just simply convert the GLSL shader code into the ShadersStore format. But I realized that having to use the app every time I did file save was a bit tedious so I looked into using System.IO.FileSystemWatcher. What that did was every time I updated the .fx file I now just had to copy paste it into the correct js file. Instead of also first doing the conversion.

Then I eventually decided to try and go one step further and have the correct js file also update which meant I only had to edit the .fx file. So now once you update the .fx file it will also update the .output file and the .js files that uses the ShadersStore with that name. test.vertex.fx will update all the js files with:


    ShadersStore["TestVertexShader"]

Example of output

For example the file test.vertex.fx with file content:


attribute vec3 aPosition;
uniform mat4 projModelViewMatrix;
varying vec3 vPosition;
void main(void)
{
    vPosition = projModelViewMatrix * vec4(aPosition, 1.0);
    gl_Position = vPosition;
}

Will generate this output in test.vertex.fx.output


BABYLON.Effect.ShadersStore["TestVertexShader"]=
"attribute vec3 aPosition;"+
"uniform mat4 projModelViewMatrix;"+
"varying vec3 vPosition;"+
"void main(void)"+
"{"+
"vPosition = projModelViewMatrix * vec4(aPosition, 1.0);"+
"gl_Position = vPosition;"+
"}";

Working with .NET Core

Working with .NET Core is very similar to how the current .NET console app development works. There have been minor changes which I assume comes from it being cross-platform now. I've mainly only worked with System.IO for this application and how you read a file with System.IO.StreamReader is now different. One example of a change is the removal of Environment.CurrentDirectory which you instead get by System.IO.Directory.GetCurrentDirectory();

System.IO

The nature of this project made it use System.IO a lot for I/O operations to manipulate files.

FileReading

The old way of reading a file for example was:


using (StreamReader sr = new StreamReader("file.txt"))
{
    // Use the sr object
}

But now StreamReader no longer takes a string as input instead it only takes a stream. Same with BinaryReader. So for reading a file it looks like this now:


using (FileStream fs = new FileStream("file.txt", FileMode.Open))
{
    using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
    {
        // Use the sr object
    }
}

You can still use File.ReadAllText("file.text") if you need all the text from a file for example.

FileSystemWatcher

I had never used FileSystemWatcher so it was new to me. To get it to work I had to add it to my project.json file.


    "System.IO.FileSystem.Watcher": "4.3.0"

Getting the settings correctly I remember it took some testing and this is how I setup the FileSystemWatcher to work for these scenarios:

  • Creating File
  • Changing FileContents
  • Renaming File
  • Removing File

    FileSystemWatcher watcher;
    public Watcher() 
    {
        // In the constructor
        watcher = new FileSystemWatcher();
        watcher.Path = "someFolderToWatch;
        // Only watch .fx files
        watcher.Filter = "*.fx";
        // Did not use NotifyFilters.CreationTime, Security, Size
        watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.LastAccess
            | NotifyFilters.FileName | NotifyFilters.DirectoryName
            | NotifyFilters.Attributes;
        watcher.EnableRaisingEvents = true;
                    
        watcher.Changed += new FileSystemEventHandler(OnChanged);
                    
        // If the file is deleted or renamed then delete the old output
        watcher.Deleted += new FileSystemEventHandler(OnRemoved);
        watcher.Renamed += new RenamedEventHandler(OnRenamed);
    }
    // ------------------------
    // The event functions 
    
    // When a file's content changed    
    private void OnChanged(object source, FileSystemEventArgs e)
    {            
        m_converter.ConvertFile(e.FullPath);
    }
    // This happens 2 times every time you save the a file, why I'm not sure
    private void OnRemoved(object source, FileSystemEventArgs e)
    {          
        if (!File.Exists(e.FullPath))
        {
            string outputPath = e.FullPath + ".output";
            // Get output file
            if (File.Exists(outputPath))
            {
                File.Delete(outputPath);
            }
        }                        
    }
    private void OnRenamed(object source, RenamedEventArgs e)
    {   
        // I'm sorry but I don't remember why it could end with ~
        if (!e.OldName.EndsWith("~"))
        {
            string oldOutput = e.OldFullPath + ".output";
            if (File.Exists(oldOutput))
            {
                File.Delete(oldOutput);
            }              
        }            
    }

Unit tests

When I first started building a .NET Core application before this one when .NET Core was recently announced the tooling for Visual Studio hadn't been released yet properly. So I tried to set up something called xUnit without proper success.

Luckily when I decided to make this application the tooling had caught up and I could use visual studio to set everything up for me. And Microsoft had ported MS Unit Tests to .NET Core by then so I could use that instead of a 3rd party.

I like using Moq for unit tests but at the time Moq wasn't ported to .NET Core so I had to go the old fashioned way and create a MockInterface myself to properly unit test. Moq might be ported to .NET Core now based off this post: stackoverflow.com/questions/40127329/how-to-add-moq-as-a-dependency-in-dotnet-core

Building the app

Building the application used to be very different than it is today. Before all you got when building was a dll file and you had to use command prompt for example: dotnet glsl-babylon.dll

Now when you build the application in project.json the run times property will create an .exe file on windows and on linux and osx it will generate similar binaries.

Improvements to the application

Currently it only handles Fragment and not also Pixel as the ShadersStore key. Which it can also be called.


    // Fragment
    BABYLON.Effect.ShadersStore["TestFragmentShader"];
    // Pixel
    BABYLON.Effect.ShadersStore["TestPixelShader"];

This application can also quite easily be tweaked to instead of outputting the format for Babylon.js it can instead do it for Three.js as it uses similar syntax.

There are some miscellaneous things like improved user friendliness and a help command to show all the inputs. The js file finder could be improved in how it finds files as it only searches folders where the convert command was. Meaning if you have 2 folders shaders, js and you tell converter to convert files in shaders it won't find js.

Conclusion

Building a .NET Core application has been an experience in in how .NET Core evolved over time. It's been interesting to see how Microsoft changed .NET tooling over the year (2016) and they made big improvements. Since it's C# and it's .NET it's very similar to how you write a console application for .NET Framework. As someone who loves C# as a language if I ever have to write a cross platform application .NET Core is great!

About the app itself I'm satisfied with the result and now hopefully if I need to change some ShadersStore code in the future all I have to do is edit 1 file!

No comments:

Post a Comment