I'm trying to make sense out of a pattern used with a javascript webgl framework.
background
WebGL uses shader programs to draw or compute stuff. These programs have to be compiled at runtime, so at one point they exist in javascript as strings.
The library offers an abstraction layer called "Material". It exposes high level properties describing what color something is, or how shiny it is, which is passed to a specific shader under the hood.
API
These shaders belonging to materials are pre-written templates, that look like this:
#include <foo> //non standard shader syntax
#include <bar>
#include <baz>
void main(){ //standard shader syntax
#include <foo_vert>
#include <bar_vert>
#include <baz_vert>
gl_Position = baz * bar + foo; //standard shader syntax
}
Some material FOO may consist of foo and bar, while another material BAR may only consist of foo, while BAZ may consist of all three, etc.
The library parses this template, it's not valid shader code. The namespace has a dictionary of "shader chunks"
chunks = {
foo: string,
bar: string,
baz: string,
...
}
So if one makes a Material, all the shader work is hidden from the user. The library itself modifies this string, replacing the #include
statement with the corresponding value from the chunks
dictionary.
advanced
However, users can be familiar with the structure of these templates and the content of the chunks, and often want to modify some parts of the shader while keeping everything else as is.
I'm having a problem digesting the available solution, and notice that a different pattern is often requested.
Without the need to modify, the high level abstraction looks like this:
//not even aware that shaders happen beyond this
var redPlasticMaterial = new Material({
shininess: veryShiny,
color: red
})
With the need to modify, one needs to know the logic inside the chunks, and the template of the Material being modified (where the chunks are used). One identifies that 'foo', 'bar', and 'baz' need to change, aka "the defaultChunk[name] doesnt work" and one can define these modifications chunkName -> string
:
var myShaderChunk_foo = 'lots of GLSL'
var myShaderChunk_bar = defaultChunks[bar] + 'lots of GLSL'
var myShaderChunk_baz = 'baz glsl'
the solution
var myCustomRedPlasticMaterial = new Material({
color: red,
shininess: veryShiny
})
//a callback that is called before processing the template
myCustomRedPlasticMaterial.onBeforeCompile = function( shader ){
shader.vertexShader // template for one shader program
shader.fragmentShader // template for another shader program
//if i want to replace foo bar baz
shader.vertexShader = shader.vertexShader.replace( '#include <foo>', myShaderChunk_foo)
shader.vertexShader = shader.vertexShader.replace( '#include <bar>', myShaderChunk_bar)
shader.vertexShader = shader.vertexShader.replace( '#include <baz>', myShaderChunk_baz)
...
}
In order to optimize the graphics calls, the library caches these shader programs. If this callback branching logic, two materials with different branches would hash the same because it uses the source code of the function for the hash.
//cant be used since body of the function is used for hashing
onBeforeCompile( shader ) {
if( qux ){
//replace shader foo
} else {
//replace shader bar
}
}
// i have to do
onBeforeCompileQux( shader ) {
//replace foo
}
onBeforeCompileNonQux( shader ){
//replace bar
}
What is going on here and does it have a name? The API gives you a raw string and the language gives you the ability to modify it. I notice that searching for the whole statement #include <name>
seems redundant but is there more to be said about it?
Without my intervention - searching for this and replacing it, the library knows how to handle #include <>
and knows where to find chunk[name]
.
Having to manually chain these string manipulation calls seems like "convention over configuration"? But then this raw code thing seems more configuration over convention. The function itself becomes data?
same thing through configuration?
The different pattern i've seen is to provide these chunks as data to the material, through a different api. If i'm aware of chunk
that it is a dictionary - whatever uses it can be told to look at something else.
myMaterial.chunks.foo = foo
myMaterial.chunks.bar = bar
myMaterial.chunks.baz = baz
^ is this configuration over convention? In both cases i see the intent:
- I know Material uses
chunks.foo
- I want Material to use a different
foo
In the callback i notice:
- I have to be aware of the parsing/compilation event
- I have to know the entire
#include <>
syntax - I have to explicitly manipulate a string
- I have to provide 'foo'
With the properties:
- (i have to know where foo goes, but it should be the same as defaultChunk that i'm aware of so not sure if it counts, but i can also see it being "i have to know the replacement API" which can maybe be more complicated)
- I have to provide 'foo'
On the other hand, "heres a string do whatever" seems really flexible and works with the primitive methods of the language. The latter needs familiarity with another api? What kind of terminology can i apply here to better explain the situation, and what patterns if any can be named?
Material.onBeforeCompile()