1

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:

  1. I know Material uses chunks.foo
  2. I want Material to use a different foo

In the callback i notice:

  1. I have to be aware of the parsing/compilation event
  2. I have to know the entire #include <> syntax
  3. I have to explicitly manipulate a string
  4. I have to provide 'foo'

With the properties:

  1. (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)
  2. 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?

4
  • I think you've managed to give both too much and too little information. It's really hard to understand what you've described and are asking. Perhaps if you dispensed with the vagaries and said which tools you're actually using, it might be understandable to others? I work with many of these things daily but can't understand what it is you're talking about. Can you actually name the libraries/frameworks you're dealing with? (Aside from WebGL/glsl, obviously.) Commented Mar 3, 2018 at 3:27
  • Thank you for the feedback. The library is three.js. The tool is Material.onBeforeCompile() Commented Mar 3, 2018 at 6:23
  • If you have any other advice on how to make this more clear and possibly more concise i would really appreciate it! I tried to speak in abstract terms and give enough background on the issue but it looks like i failed. I'd gladly give a bounty. Commented Mar 3, 2018 at 21:03
  • After rereading it several times, I think it's starting to make a little more sense. I'll try to write up something, but I'm not entirely sure it's what you're looking for. Commented Mar 4, 2018 at 1:02

1 Answer 1

0

Is your question essentially, "what is it called when an API works this way?" And also "what patterns would be involved?" If so, I would say it falls into a couple of categories.

Data Driven Development

In data driven development, rather than writing code to do a specific thing, you write code to process data which tells the program what to do. In this specific case, the data is the set of glsl-like templates and the pieces of those templates that the user can replace. Rather than just telling the GPU to process each vertex or fragment a particular way, the Material framework (apparently) has methods to pre-process some shader templates, replacing parts here and there and generating the final shader that's actually sent to the GPU.

Domain-Specific Language

glsl is itself a domain-specific language in that it's mainly dealing with vertex processing and fragment processing (and a few other related things). But the templates that this framework uses are an even more domain-specific language. They offer some pre-existing shaders where you can combine specific pieces or replace specific pieces with your own.

I think your assessment of convention vs. configuration is correct.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.