Shader Node system improvements

Hi there, it’s me again!

Lately shaders in my project became so big and complex they can not be maintained by humans any more (just my texture atlas code were 300 lines and based on defines and global variables). And just when I started to think about refactoring all shaders I accidentally stumbled upon Shader Nodes in JME. I don’t know why I never noticed it (probably because no basic jme materials use it) - it is literally the holy grail!

So Shader Node system is amazing, but as soon as I started working with it, I found tons of stuff to improve. And I wrote the code and everything, and everything is working great in my project but - I have pretty old version on JME (more than 2 y.o.), so it is very hard to port my changes back to JME.

That’s why I have decided to share some cool features and fixes that I implemented in Shader Nodes, and if any of this are really needed in JME, I can add them when I have free time, or I can share the source code.

Here we go, the list is long and code here is.

Fixes

  • Fixed cardinality and swizzling with non-float values and vectors (i.e. int, ivec3…)
  • Fixed varyings could not be declared with the same name from different shader nodes
  • Integer varyings are flat
  • Samplers can be used in vertex shaders
  • Multiple input variables can be declared with the same name (f.e. with swizzles)

Features

  • InputMapping and OutputMapping now can have constants as their input, for example (see: Const):
ShaderNode TextureTriplanarCoords {
    Definition : TextureTriplanarPreFragment : materials/shaders/generic/texture.triplanar.j3sn
    InputMappings {
        divider = Const.vec3(4.0, 4.0, 4.0)
        textureWorldSpace = WorldSpaceTextureCoord.vec3Var
    }
}

It is harder to implement w/o Const keyword, but possible. Same is vialbe for OutputMappings, you can event output constants to Global variables!

  • Added Library property to Shader Node and Shader Node Definition - declaring shader node as library will prevent it’s source from being removed by compiler as unused. It is required for shader libraries. Example:
    ShaderNodeDefinition TextureAtlasLibFragment {
        Type: Fragment
        Shader GLSL150: materials/shaders/generic/texture.atlas.lib.frag
        Library
        Documentation{
            Contains functions to work with atlas in fragment shader
        }
        Input {
        }
    }
  • Added LOCAL. prefix to shader source code that will be replaced to shader node’s name to prevent conflicts of local variable names. Example:
void main() {
    vec3 LOCAL.blending = abs(normal);
    LOCAL.blending = normalize(max(LOCAL.blending, 0.00001)); // Force weights to sum to 1.0
    float LOCAL.b = (LOCAL.blending.x + LOCAL.blending.y + LOCAL.blending.z);
    LOCAL.blending /= vec3(LOCAL.b, LOCAL.b, LOCAL.b);
    color = color1 * LOCAL.blending.x + color2 * LOCAL.blending.y + color3 * LOCAL.blending.z;
}

will be compiled to

vec3 TextureTriplanarPost1_blending = abs(TextureTriplanarPost1_normal);
TextureTriplanarPost1_blending = normalize(max(TextureTriplanarPost1_blending, 0.00001)); // Force weights to sum to 1.0
float TextureTriplanarPost1_b = (TextureTriplanarPost1_blending.x + TextureTriplanarPost1_blending.y + TextureTriplanarPost1_blending.z);
TextureTriplanarPost1_blending /= vec3(TextureTriplanarPost1_b, TextureTriplanarPost1_b, TextureTriplanarPost1_b);
TextureTriplanarPost1_color = TextureTriplanarPost1_color1 * TextureTriplanarPost1_blending.x + TextureTriplanarPost1_color2 * TextureTriplanarPost1_blending.y + TextureTriplanarPost1_color3 * TextureTriplanarPost1_blending.z;
  • Defines in Shader Nodes: you can now add defines to shader nodes and they will be replaced with define from mateiral param and there will be no conflicts as they are namespaced by shader node name. For example, we have this shader code:
...
    #if ATLAS_MAG_MIN_FILTER == 0
        color = getAtlasNearestNoMipmap(atlas, texCoord, atlasTileData);
    #endif
    #if ATLAS_MAG_MIN_FILTER == 1
        color = getAtlasNearestMipMapNearest(atlas, texCoord, atlasTileData);
    #endif
...

And in shader node we can specify ATLAS_MAG_MIN_FILTER as this:

ShaderNode TextureAtlasFragment1_1 {
    Definition : TextureAtlasFragment : ....j3sn
    Define : AtlasMagMinFilter as ATLAS_MAG_MIN_FILTER
    InputMappings {
        ...
    }
}

Generator will create new define with value from material parameter named AtlasMagMinFilter and resulting shader code will be like this:

...
#define TextureAtlasFragment1_1_ATLAS_MAG_MIN_FILTER 1
...
void main() {
...
    #if TextureAtlasFragment1_1_ATLAS_MAG_MIN_FILTER == 0
        color = getAtlasNearestNoMipmap(atlas, texCoord, atlasTileData);
    #endif
    #if TextureAtlasFragment1_1_ATLAS_MAG_MIN_FILTER == 1
        color = getAtlasNearestMipMapNearest(atlas, texCoord, atlasTileData);
    #endif
...
}

You don’t need to add anything to Material’s Defines section, generator will handle all that stuff.

  • Even cooler, Defines can set by constant values too:
ShaderNode TextureAtlasFragment1_1 {
    Definition : TextureAtlasFragment : ....j3sn
    Define : ATLAS_MAG_MIN_FILTER = 5
    InputMappings {
        ...
    }
}

You can imagine resulting source code yourself. And yes, syntax is not very obvious, I know, there is a work to do…

Node generation on-the-fly

Coolest feature - shader node definitions can now be generated on-the-fly from ShaderCode section of material. Now you don’t need separate shader node definition for every sneeze you want to do. It is cool, until some one starts to write everything here… But it is still better than simple shader system because of namespacing…

Let me give you an example:

ShaderCode TexCoord {
    in vec3 vec3Var = Attr.inTexCoord
    out ivec3 ivec3Var
    ivec3Var = ivec3(int(vec3Var.x), int(vec3Var.y), int(vec3Var.z))
}

It is a section in material definition, just like ShaderNode. Cool, eh? :smiley: You can add as much lines of code as you want, but you can not wrap lines of code, because generator will add ; at the end of each line (sadly, statement parser limitations).

Ofc every variable will be replaced accordingly as in ShaderNode with Definition. You can use outputs in any shader node below, this node is working perfectly fine:

ShaderNode TextureAtlasVertex1 {
    Definition : TextureAtlasVertex : ....j3sn
    InputMappings {
        texLayer = TexCoord.ivec3Var.x
        atlasTilesTBO = MatParam.AtlasTilesTBO
    }
}

That is not all, you can declare variables as inout, just like specifying input and output with the same name. Here is shader node to transfer attributes to fragment shader:

ShaderCode Normal {
    inout vec3 vec3Var = Attr.inNormal
}
ShaderCode WorldSpaceTextureCoord {
    inout vec3 vec3Var = Global.position.xyz
}

You can output from shader code into Global, but it’s a bit tricky:

ShaderCode ColorTest {
    out vec3 color to Global.color.rgb
    color = vec3(1.0, 0.0, 0.0)
}

(Syntax, as usual, not great) Of course, you can also assign constants to in or inout using Const.. (Variable declaration is starting with ‘in’, ‘out’ or ‘inout’ key words and must be at the start of a node.)

Aftermath

So, what do you think? :smiley:

6 Likes

I personally have no idea of shaders. But I think @nehon should have a look at this :stuck_out_tongue_closed_eyes:

me too :blush:, but i want to say my thanks to @Eirenliel . :slight_smile:

1 Like

I haven’t done anything yet :wink:

1 Like

Man that’s awesome.
I wanted to add many of these features (especially the constant assignation to inputs).
Let me digest all of that because I have not that much time today and I will answer point by point.
There has not been much change in the system since 2 years so I guess it could be integrated to master with not that much effort.

3 Likes

I wanted to make a pull, not that topic, but as I started today in the morning I found that there have been some changes in material and shader system and I better move them from current jme version to my project. But I was to lazy and instead implemented ShaderCode sections and defines :smiley:

Last feature I want to implement in shader nodes is shader node packs: creating shader node definitions from shader nodes. Sometimes you have logic out of few same shader nodes in different material definitions and they are very long together, but basically have input and chained together…

yeah… I was going to call this “groups” :stuck_out_tongue: but “pack” is fine. And yeah also something I’d want :wink:

1 Like

I came up with the name while I was typing the message :smiley:

So I’ll wait for you reply then: we can come up with better syntax for all this stuff (and for shader node groups too) and I’ll just make it the way it is good for everyone and will not have to redo it later.

Is there like any chat room or something where it can be discussed live?)

Hi there! So I was busy with more important stuff, but lately I’ve upgraded shader nodes system with shader node packs, they are defined in separate files like that (read comments for more insight):

ShaderNodesPack {
    Documentation {
        // This section is for humans...
    }
    Output {
        color = TextureSplatting.color
    }
    Input {
        // This section is only for documentation purpose
        atlasTileData1
        atlasTileData2
        atlasTileData3
        normal
        textureBlend
    }
    
    ShaderNode TextureTriplanarCoords {
        Definition : TextureTriplanarPreFragment : materials/shaders/generic/texture.triplanar.j3sn
        InputMappings {
            divider = vec3(4.0, 4.0, 4.0)
            textureWorldSpace = WorldSpaceTextureCoord.vec3Var
        }
    }
    
    // Set up atlas
    ShaderNode TextureAtlasLib {
        Definition : TextureAtlasLibFragment : materials/shaders/generic/texture.atlas.j3sn
        Define : AtlasWrapMode as ATLAS_WRAP_MODE
    }
    
    // Read for first texture
    ShaderNode TextureAtlasFragment1_1 {
        Definition : TextureAtlasFragment : materials/shaders/generic/texture.atlas.j3sn
        Define : ATLAS_MAG_MIN_FILTER = 5
        InputMappings {
            atlasTileData = Input.atlasTileData1 // 'Input' is a special namespace
            atlas = MatParam.ColorAtlas
             // Curent snp's namespaces can be used as is
            texCoord = TextureTriplanarCoords.texCoord1
        }
    }
    // ... And so on, ShaderNode and ShaderCode are allowed in any combination
    Include : materials/shaders/OtherShaderNodePack.snp
    // Includes are just included 'as is`, no second layer of namespacing is present currently
}

And they can be included in material like that:

        FragmentShaderNodes {
            ShaderNodesPack TextureTriplanarFrag {
                Source : materials/shaders/generic/texture.atlasblend.frag.snp
                InputMappings {
                    atlasTileData1 = TextureTriplanarVert.atlasTileData1
                    atlasTileData2 = TextureTriplanarVert.atlasTileData2
                    atlasTileData3 = TextureTriplanarVert.atlasTileData3
                    textureBlend = TextureTriplanarVert.textureBlend
                    normal = TextureTriplanarVert.normal
                }
            }
            // ... And so on...
            ShaderCode TextureTriplanarFragOutput {
                // We can use outputs from SNP as outputs from any other shader nodes
                // Notice namespace: it is SNP's name, not name of shader node within SNP
                in vec3 color = TextureTriplanarFrag.color.rgb
                out vec3 color2 to Global.color.rgb
                
                color2 = color
            }
        }

I also fixed constants in shader nodes inputs, they do not require Const namespace now (checked by regexp when parsing).

Still kinda waiting for @nehon

5 Likes

Awesome job! :slight_smile:

This is a really useful feature I’ve been wishing for!
I have a quite complex shader node I would like to use in different materials. This beats copy-pasting.

Edit: Besides, it’s not possible to copy the generated shader code O_o

so… the shader pack is like a reusable shader node composed of several shader nodes right?
I didn’t have that in mind, but that sounds pretty cool.

What do you mean by this?

Yes. Currently I am rewriting it to support better constants and conditions in shader packs and shader packs in shader packs with proper input-output chaining :smiley:

I tried to rewrite my shader with this and found that I need a few more things) It will be cool, I guess I’ll share the code after this.

5 Likes