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? 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?