Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Material: Added onBeforeCompile() #11475

Closed
mrdoob opened this issue Jun 9, 2017 · 88 comments
Closed

Material: Added onBeforeCompile() #11475

mrdoob opened this issue Jun 9, 2017 · 88 comments

Comments

@mrdoob
Copy link
Owner

mrdoob commented Jun 9, 2017

Over the years, a common feature request has been to be able to modify the built-in materials. Today I realised that it could be implemented in a similar way to Object3D's onBeforeRender().

e55898c

WebGLPrograms adds onBeforeCompile.toString() to the program hash so this shouldn't affect other built-in materials used in the scene.

Here's an example of the feature in action:

http://rawgit.com/mrdoob/three.js/dev/examples/webgl_materials_modified.html

material.onBeforeCompile = function ( shader ) {

	// console.log( shader )

	shader.uniforms.time = { value: 0 };

	shader.vertexShader = 'uniform float time;\n' + shader.vertexShader;
	shader.vertexShader = shader.vertexShader.replace(
		'#include <begin_vertex>',
		'vec3 transformed = vec3( position.x + sin( time + position.y ) / 2.0, position.y, position.z );'
	);

	materialShader = shader;

};

Not only we can mess with the shader code, but we can add custom uniforms.

if ( materialShader ) {

	materialShader.uniforms.time.value = performance.now() / 1000;

}

Is it too hacky?

/cc @WestLangley @bhouston @tschw

@unconed
Copy link
Contributor

unconed commented Jun 9, 2017

It does seem a bit hacky to require custom string manipulation when WebGLProgram.js already does a lot of processing. There is a related use case, which is to add new #includes rather than overriding existing ones. You can do this today by modifying THREE.ShaderLib at runtime, but it feels equally dirty.

Perhaps a nicer solution for both is to allow you to provide a custom ShaderLib for a material, that would override/augment the built-in chunks without manual string manipulation and without permanently clobbering them. That doesn't preclude an onBeforeCompile hook for other uses, but it seems more in line with what your example is trying to do.

That said, shader composition based on raw includes is always going to be very fragile from one version to the next. If you don't mind including all the skeleton code (like meshphong_vert.glsl), you can already extend the built-in materials with something like this:

export class MyPhongMaterial extends ShaderMaterial {
  constructor({ color, radius, map, normalMap, emissiveMap }) {
    super();
    this.vertexShader = "...."
    this.fragmentShader = "....";
    this.uniforms = UniformsUtils.clone(ShaderLib.phong.uniforms);

    this.isMeshPhongMaterial = true;
    this.lights = true;
   
    this.uniforms.time = { value: 1 };
    //...
  }
}

In all cases, you have to rely on the internal organization of built-in shaders not to change too much from one release to the next. Each include is already a function in disguise, with an ill-specified signature. I suspect it is going to be much cleaner and maintainable on both sides to provide function-level rather than include-level overrides.

@pailhead
Copy link
Contributor

pailhead commented Jun 9, 2017

Today I thought..

Nice! I thought so too on february 10th when i made this pull request:

#10791

It seems the only difference is that you're doing it through a callback? I tapped into the unused argument in the WebGLRenderer.

I like using this for example to get threejs to work with normal maps, but I've linked some issues that seem like they would be trivial to solve with this.

You can look at my branch and take this for a spin or check out this super simple example (just tests a custom deformation with lighting/shadows etc, and adds a few uniforms).

http://dusanbosnjak.com/test/webGL/three-material-shader-override/webgl_materials_shader_override.html

Perhaps a nicer solution for both is to allow you to provide a custom ShaderLib for a material, that would override/augment the built-in chunks without manual string manipulation and without permanently clobbering them.

Exactly sums up what happens in: #10791 I wish i could write better descriptions :)


I must admit that i don't know wth is going on here. I think i know someone who gave up contributing to three, didn't understand it then, but now i find it kinda frustrating.

Frustrating mostly because i'm having a deja vu when i read the first paragraph:

...similar way to Object3D's onBeforeRender().

This exact same thing happened with the onBeforeRender() #9738, when it took a year of convincing to get the feature in.

Jeez, twice, with the same dev? I'm obviously doing something wrong here, but what? The first time around was the build files being checked in, i didn't know what to do, and it just got forgotten. But this time around i was on top of this pull request, there's a conflict right now but it's easily solved, were no conflicts for months.

Don't get me wrong, i don't think it's vanity in the works here, i think i just want to run ideas by people and get feedback. @unconed it's obvious that you're familiar with the problem and you've probably tried different things. What may have caused an user like you to miss the other PR?

I felt pretty good about the per material shaderlib, but i did find one function in the renderer that looked/compared entire shaders as strings to figure out caching, wasn't feeling all that great about that. Wish there was some feedback given...

Was the example flawed? The head may make it much more obvious what is going on than my abstract spike ball, but the whole example works with shadows which covers more ground. It's heavy for some reason, but i think it's because sin/cos on many verts.

@pailhead
Copy link
Contributor

pailhead commented Jun 9, 2017

I guess if you're starting over... Even though scene kit is absolutely horrid, this api is really nice:

https://developer.apple.com/documentation/scenekit/scnshadable

Albeit horribly documented, i can't find the part that's of more interest, but basically they have abstracted hooks at many different stages of the shader.

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 9, 2017

Earlier this year i made a pull request that seems to do something similar if not the exact same thing:

#10791

It seems the only difference is that you're doing it through a callback? I tapped into the unused argument in the WebGLRenderer.

Sorry for not being able to take care of that PR yet @pailhead. I guess the main advantage of my approach is that it only required 3 new lines.

Having said that, I'll now spend some time studying yours and @unconed suggestions.

@pailhead
Copy link
Contributor

pailhead commented Jun 9, 2017

This one definitely feels more elegant, only three lines. I was following a different pattern in the other one. Was going to say that it may be a bit more verbose, but not so sure. I guess the only advantage of the other one is that it was good to go a few months ago :)

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 9, 2017

Jeez, twice, with the same dev? I'm obviously doing something wrong here, but what?

Sorry about that. The only thing I can think of is that probably I got overwhelmed. Maybe a long discussion or a complicated PR that would consume too much of my energies, so I decide to leave it for later and move to simpler PRs.

It's not only you @pailhead. @bhouston has had a lot of PRs like this, even @WestLangley and @Mugen87.

Again, is not that I don't like the PR, is that the PR requires some attention I can't offer at the time. A good example is the Instancing PR. I managed to find time to read through it, did my own experimentation and suggested simplifications. Hopefully I'll be able to revisit it soon.

It's difficult to manage all this and give every PR the attention they deserve.

Was the example flawed? The head may make it much more obvious what is going on than my abstract spike ball, but the whole example works with shadows which covers more ground. It's heavy for some reason, but i think it's because sin/cos on many verts.

Now you mention it... Yeah, runs at 10fps on a new MacBook Pro when zooming in 😮

@pailhead
Copy link
Contributor

pailhead commented Jun 9, 2017

I think it's the shadows and the rand, sin cos and stuff, but yeah it looks like it takes too much of a hit.

@pailhead
Copy link
Contributor

pailhead commented Jun 9, 2017

Either way, i'm surprised you pulled this off in three lines, i was focused too much on how material params get turned into uniforms and followed that pattern. The difference is that I provide an alternative ShaderLib like dictionary, so #include <> brings in different code. You remove the include before this happens, and replace it with glsl. I think if you return some kind of a wrapper around the shader, perhaps this syntax could be cleaner (just provide the chunk name + glsl, instead of replace(). But it may end up looking much more like the other one.

It would be really nice to have this, i haven't worked with that many 3d engines, but unity and scene kit seem to have something like this.

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 9, 2017

@unconed

I guess one benefit from the onBeforeCompile() is that the material properties remain unchanged. So it feels more like extending the built-in materials.

export class MyMeshPhongMaterial extends MeshPhongMaterial {
  constructor( parameters ) {
    super( parameters );
    this.onBeforeCompile = function ( shader ) {
      shader.vertexShader = shader.vertexShader.replace(
        '#include <begin_vertex>',
        'vec3 transformed = vec3( position.x + sin( position.y ) / 2.0, position.y, position.z );'
      );
    };
  }
}

var material = new MyMeshPhongMaterial();
material.color.setRGB( 1, 0, 0 ); // this still works

Messing with ShaderLib is tricky indeed. Adding forever-unchanged hooks should be doable though:

	#include <begin_vertex>
	% vertex %
	#include <morphtarget_vertex>
	#include <skinning_vertex>
	% transformed_vertex %
	#include <project_vertex>
	% projected_vertex %

The replace code will become this:

this.onBeforeCompile = function ( shader ) {
  shader.vertexShader = shader.vertexShader.replace(
    '% vertex %',
    'transformed.x += sin( position.y ) / 2.0;'
  );
);

We'll then remove any hook that hasn't been used before compiling, of course.

@pailhead
Copy link
Contributor

pailhead commented Jun 10, 2017

here's what it looks like compared to the per instance ShaderChunks

//given some material
var material = new THREE.MeshNormalMaterial();

//and some shader snippet
var myShader = [
	'float theta = sin( time + position.y ) / 2.0;',
	'float c = cos( theta );',
	'float s = sin( theta );',
	'mat3 m = mat3( c, 0, s, 0, 1, 0, -s, 0, c );',
	'vec3 transformed = vec3( position ) * m;', //and making assumptions about THREE's shader framework
	'vNormal = vNormal * m;'
].join( '\n' );

#10791 same as Material.defines approach:

material.shaderIncludes = {
	
        begin_vertex: myShader,

	//uv_pars_vertex: [
	//	THREE.ShaderChunk['uv_pars_vertex'], //this doesn't have to be
	//	"uniform float time;",
	//].join('\n')
};

material.shaderUniforms = { time: { value: 0, type: 'f' || 'float' } }; //because this could just inject it in the right place (but needs type)

It's only a dictionary of chunk names, and the uniforms object. The string manipulation here is more along the lines of "i want to reuse this chunk, and do something on top of it"

this PR:

material.onBeforeCompile = function ( shader ) {

	shader.uniforms.time = { value: 0 };

	shader.vertexShader = 'uniform float time;\n' + shader.vertexShader; //this feels hacky
	
	shader.vertexShader = shader.vertexShader.replace( //this is more verbose
		'#include <begin_vertex>',
		myShader
	);

};

I guess one benefit from the onBeforeCompile() is that the material properties remain unchanged.

It works the same in the other PR, so I think it doesn't matter how it's done.


One probably irrelevant benefit that i can think of is that you can keep the logic for manipulating the material in the same block and on the same level. Even if the code doesn't look much different, conceptually, you're manipulating .color on Material while manipulating a .uniforms object on a "shader". If you had a piece of code somewhere changing color, and you later add custom GLSL to that material, you can add the code to change the uniform with the color. (skip wall of text)


If the uv_pars_vertex is confusing, i think a good question to ask is:

"where do I inject my custom GLSL function, like float rand( vec2)?"

Then this one might make sense #11050


Another argument could be - assuming you know GLSL, assuming you know THREE's shader framework, you still have to be creative and think about where you inject what. I had to think a bit and look through most of the shaders to figure out that uv_pars_vertex is a convenient place to do my material extension.

It would be better to go from something like this towards an abstraction, have a lightingPhase, objectTransformation , perspectiveTransformation etc hooks. Or at least a guaranteedToBeAboveMainButBelowCommonsAndExtensionCalls dummy chunk that's included in every shader program. It looks like you're going for that with % vertex %? But i'm unfamiliar with that syntax.

This PR, as is, seems like it's going in the opposite direction. On top of knowing where you need to tap in, you also need to be explicit and manipulate strings, repeating some of the work that the renderer already does for you. Also, if you ask another question in addition to the first one

How do i use a function, declared in the commons chunk, inside the function that i'm injecting?

you might find yourself doing more string manipulation than just prepending the uniforms to the entire shader.

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 11, 2017

I had to think a bit and look through most of the shaders to figure out that uv_pars_vertex is a convenient place to do my material extension.

I don't think there is a way around that unless we over-engineer. The idea with % vertex % is to try helping with that a bit by naming the hooks and more or less tell you at what state you're injecting code, but it would be hard to document what variables are available at what point.

We're opening a door for allowing built-in materials hacking, but I don't think we can provide proper "support". The user should be aware that chances are things may break.

@pailhead
Copy link
Contributor

pailhead commented Jun 12, 2017

I tried to tackle it in the other PR. I added a dummy hook at least for the functions, varyings, attributes and uniforms. Lots of the GLSL logic already happens on structs, scenekit documented each variable (cant find the same documentation anymore though).

It would probably need to be refactored as we go along but something along these lines:

#ifdef PHASE_FOO

#include <shader_foo> 
//someGlobalStruct.mvPosition = modelViewMatrix * myLogic( transformed );
//if there is glsl provided for phase "foo" document that it should operate on "transformed" 
//and that it should return "mvPosition" 
#end including <shader_foo>

#else 

  //default
  #ifdef USE_SKINNING

	vec4 mvPosition = modelViewMatrix * skinned;

  #else

	vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );

  #endif

#ifdef PHASE_BAR

#include <something_like_this>
//mvPosition = myPostDefaultTransformationLogic( mvPosition );

#endif

gl_Position = projectionMatrix * mvPosition;

#endif

Of course ifdefs and includes would proably not work like this. But i can see it being relatively straight forward with the other approach. We could add #include <some_phase> always, with empty strings. Something could help manage what gets triggered by using #ifdefs. So for example, if you provide logic for "vertex_transformation" phase, the shader will define whatever you provide. Documenting what each chunk does with GLSL would be overall helpful. Without going further into abstraction, i think it would be great to have this map to how the chunks are structured now.

@pailhead
Copy link
Contributor

pailhead commented Jun 12, 2017

We're opening a door for allowing built-in materials hacking, but I don't think we can provide proper "support". The user should be aware that chances are things may break.

We already have the defines stuff available on every material. #10764 suggested to modify THREE.ShaderLib and then define a .defines property on an instance of a Material. Granted, the property was undocumented until i wrote the entry (but you approved it :)), and is still undefined.

But the way this currently works, and is inherited by every material, it is supported - if you put stuff in it, it will affect the shader for that material. Furthermore, if you happen to define something that is already part of the shader library, it may break things.

Not trying to be a smart-ass but trying to come up with an example. I mostly used defines with ShaderMaterial but it does affect all materials, just because how WebGLRenderer works. There is no logic that says:

if you're a much smarter, surface like abstraction of a material like Phong then don't take defines into account. Because three is not designed to have it's shader chunks overriden, and the Phong template is the final authority on what GLSL needs to do, and serves to hide this from the use, it makes sense to not have GLSL defines interfere with this.

I'm not entirely sure if it can be broken though, i'd have to try.

With that being said, i'd like the option to break things, if it provides a lot of gain. With a pretty encapsulated and tiny GLSL snippet, i could easily change my version of three to render normal maps differently. Taking a normal, and transforming it to get another normal is super straight forward, unless we start doing computer graphics in a completely different way, i can't see this ever breaking :)

Instancing was another example:

oh i'd like to use ANGLE_INSTANCED_ARRAYS, three doesn't seem to support it with it's shadows or PBR materials, let me just add these two lines, and make it work for me"

@fernandojsg
Copy link
Collaborator

I'm happy to see that this features is finally coming to three.js :). Yes I also created a yet another material modifier PR (#7581) It was so old that the code is not relevant anymore, but is basically injecting hooks like @mrdoob proposes and then just replace them by your own code.
I like the idea of predefined hooks as it's easy to understand what you're modifying as I believe most of the people that want to use this feature want to modify "slightly" the default material instead of completely rewrite it.

I like the simplicity of this PR but if we're going for a more structured/clean way to do things I'd prefer to have already a dictionary of hooks to replace with your code and a simpler way to add uniforms or custom code than just replacing strings.

@pailhead
Copy link
Contributor

pailhead commented Jun 12, 2017

@fernandojsg any chance you could check out #10791, give feedback, it seems super similar to what you did except i've put the additional hook just outside of main (something like your "pre_vertex") and there's slightly more management.

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 12, 2017

@fernandojsg I had forgotten about your PR 😚. I think your PR looks quite a lot of how I was starting to see this working. Subconscious?

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 12, 2017

Not trying to be a smart-ass but trying to come up with an example. I mostly used defines with ShaderMaterial but it does affect all materials, just because how WebGLRenderer works.

I'm glad the library is hackeable in that way, but we shouldn't use abuse features inernally for things that were not intended for. It's easy to end up with fragile code that way.

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 12, 2017

Reading #7581 again... On top of e55898c we can add the % HOOKS % and then create a class like this:

THREE.ExtendedMaterial = function ( material, hooks ) {

    material.onBeforeCompile = function ( shader ) {
        var vertexShader = shader.vertexShader;
        var fragmentShader = parameters.fragmentShader;
        for ( var name in hooks ) {
           vertexShader = vertexShader.replace( '%' + name + '%', hooks[ name ] );
           fragmentShader = fragmentShader.replace( '%' + name + '%', hooks[ name ] );
        }
        shader.vertexShader = vertexShader;
        shader.fragmentShader = fragmentShader;
    };

    return material;

};

Then we can do this:

var material = new THREE.ExtendedMaterial(
    new THREE.MeshBasicMaterial(),
    { vertex: 'transformed.x += sin( position.y ) / 2.0;' }
);

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 12, 2017

So the question is, what hooks should we have?

VERTEX_UNIFORMS
VERTEX
TRANSFORMED_VERTEX
PROJECTED_VERTEX

NORMAL
TRANSFORMED_NORMAL

FRAGMENT_UNIFORMS
INPUT_FRAGMENT
OUTPUT_FRAGMENT

@WestLangley
Copy link
Collaborator

WestLangley commented Jun 12, 2017

@mrdoob As an example, you can see in this gist where code needs to be injected into the MeshBasicMaterial vertex shader to support instancing.

@pailhead
Copy link
Contributor

pailhead commented Jun 12, 2017

And here for the rest of the materials :)

#10750

@pailhead
Copy link
Contributor

pailhead commented Jun 12, 2017

VERTEX_UNIFORMS sounds too opinionated, where should you add a function, an attribute or a varying? PRE_VERTEX or something?

NORMAL_MAP is definitely needed

vert:

PRE_VERTEX
VERTEX
TRANSFORMED_VERTEX
PROJECTED_VERTEX

NORMAL
TRANSFORMED_NORMAL

UV
fragment:

PRE_FRAGMENT
INPUT_FRAGMENT
LIGHT
NORMAL

OUTPUT_FRAGMENT

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 12, 2017

VERTEX_UNIFORMS sounds too opinionated, where should you add a function, an attribute or a varying? PRE_VERTEX or something?

Hmm, opinionated? How so?

@pailhead
Copy link
Contributor

pailhead commented Jun 12, 2017

attribute vec4 aMyAttribute; is not a uniform :)

float myRand( vec4 foo ) { /*logic*/ return vec4(bar,baz)} is not a uniform

varying vec3 vMyVarying; is also not a uniform

either not plural, or not uniforms at all

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 12, 2017

What's the use case for "injecting" attributes?
Shouldn't you be using geometry.addAttribute( 'aMyAttribute', ... ) instead?

@pailhead
Copy link
Contributor

I didn't know you don't have to declare it. Still leaves varyings and functions?

@mrdoob
Copy link
Owner Author

mrdoob commented Jun 12, 2017

Still leaves varyings and functions?

Good point.

@takahirox
Copy link
Collaborator

takahirox commented Dec 22, 2018

I think I wrote poorly. What I wanted to mention is

  • Built-in Material hacked with .onBeforeCompile() isn't copied, cloned, serialized correctly in the same API as built-in materials'. It's handled as non-hacked material.
  • If users want the correct result, they need some extra work in user side.
  • If users want the hacked built-in material with Three.js core API without any user-side extra work, they should think of other options, like ShaderMaterial.

My question is just for the policy of .onBeforeCompile() rather than for the specific case. I wanted to clarify the limitation and add a note in the doc.

@pailhead
Copy link
Contributor

pailhead commented Dec 22, 2018

If i forget about three.js and the whole rendering system, i think i see the point. As a generic object, if it has .clone() you want to be able to clone and have consistent expectations. The problem is both solutions (clone this not clone it) seem to have a valid argument. You don't clone listeners, but you also somewhat expect something that influences drawing to be consistent.

I still think that my proposal from above is a valid way to do this, as i've encountered the same problem in other areas (like sharing depth buffers between targets can also be solved with ownThing || outsideThing.

Instead of caching the function, which doesn't work, you would basically cache userData but a subset of it. Makes no sense to cache userData.hamburger: 'rare' but it does userData.effectOn: true". The ownInput | ownChunks | ownWhatever` would solve this problem.

It also solves the problem of:

As onBeforeCompile method could contain very arbitrary code, it's certainly impossible to robustly serialize that callback.

It has been a long time, people have used onBeforeCompile we should know by now what kind of arbitrary code can be found in there. I think it only operates on shader object that is passed in. You mutate that object, but only mutations that make sense with ShaderMaterial would have an effect anyway. Setting some gl stuff yourself would probably be overridden, and that's about the only thing that pops to my mind.

It's essentially a pre three parse step, where you get a chance to parse the shader yourself along with the mandatory uniforms:{} interface, since you don't have it available on Material (defines:{} on the other hand you do, and both are siblings in ShaderMaterial).

How you do it, i.e whatever the arbitrary code is, doesn't matter. It does not return anything, but it mutates shader:{} and renderer then uses it.

Approaches that i suggest:

  1. Add ownThing to various classes. THREE.WebGLRenderTarget could have .ownStencilDepthBuffer. Here, as i've suggested above, it would be some some version of ownGLSL:{} ownInput | ownUniforms . You remove the need for arbitrary code to mutate the shader:{} object:

Given input like this that originates from inside the WebGLRenderer (super private):

const shader = {
  uniforms: buildFromMaterial(mat),
  vs: getVSTemplate(key),
  fs: getFSTemplate(key)
}

nuke this:

shader = onBeforeCompile( shader )

shader.vs //invalid GLSL
shader.uniforms //you got some extra ones that onBeforeCompile may have mutated, maybe removed some others

shader = ___parse(shader) //private step

shader.vs //valid glsl, (not a template)

compile(shader)

Use this:

function parseShader(shader){
   return {
     uniforms: shader.uniforms,
     vs: parseChunks(shader.vs)
     fs: parseChunks(shader.fs)
   }
}
//FIX FOR HASHING BUG
function parseChunk( material, chunkName ){
  return material.ownChunks[chunkName] || THREE.ShaderChunks[chunkName]
}

//do whatever you want with the templates, maybe remove an `#include <>`
shader = onBeforeParse(shader)

shader.vs //template || valid glsl

shader = parseShader(shader) //sample OWN || PRIVATE 

shader.vs //valid GLSL 

//if you want to apply a regex on the entire valid glsl, or compile node material or something else
shader = onBeforeCompile( shader )

compile(shader)
  1. Make WebGLRenderer not know about anything other than ShaderMaterial.

Wherever you would have

const mat = new THREE.MeshBasicMaterial()
const sm = new THREE.ShaderMaterial()

Have

const mat = nodeMaterial.compile() //returns ShaderMaterial, with a friendly interface (same as StandardMaterial for example)

const mat = chunkMaterial.compile()

@pailhead
Copy link
Contributor

pailhead commented Dec 22, 2018

The tl:dr of the stuff above is user does not care about the particular point in time when "compile" (parse) occurs. I think they care more about what happens, and what happens can be isolated to a handful if not a single use case in: shaderString, out: shaderString. The reason why it has to be done at that particular point in the pipeline i think is worth investigating and understanding. It seems to be getting in the way of things more than its helping.

@mrdoob
Copy link
Owner Author

mrdoob commented Dec 25, 2018

@mrdoob I think it's difficult (or impossible) to serialize Materials hacked with .onBeforeCompile(). Do you think users should use ShaderMaterial if they want full functional(render, copy, clone, serialize, and so on) modified built-in materials?

I didn't expect (considered) the the use of onBeforeCompile() to be serialised...

@donaldr
Copy link

donaldr commented Jan 1, 2019

I just got bitten hard by using onBeforeCompile. I have a shader helper class that allows me to make modifications to built-in shaders in a defined way. I use the onBeforeCompile method to do this. Apparently, like stated in the first comment, WebGLProgram uses onBeforeCompile.toString() as a hash. But since my function is generic (it substitutes parts of the vertex and fragment shaders with variables) onBeforeCompile.toString() doesn't look different for different shaders. This meant that all my different shaders got cached as the same program. An eval call and a uuid and now my functions all look different. Took forever to figure this out.

@pailhead
Copy link
Contributor

pailhead commented Jan 1, 2019

@donaldr See #13192. I think it would be good to document these limitations.

could you share how you're using the eval? I've been thinking about that but it just seemed too dirty. You could basically add const fdscxnmdrek435rkjl to the top of the body and then eval?

@donmccurdy
Copy link
Collaborator

This meant that all my different shaders got cached as the same program. An eval call and a uuid and now my functions all look different. Took forever to figure this out.

That sounds very frustrating, sorry. 😞 If the modifications you needed to make seem like they'd make sense in the library itself please feel welcome to open issues, too.

@pailhead
Copy link
Contributor

pailhead commented Jan 3, 2019

please feel welcome to open issues, too.

Would you consider re-opening existing ones? #13192 seems to be the same but it was closed 😿 It would be really nice if a work around was posted in there if it was deemed a feature.

@pailhead
Copy link
Contributor

pailhead commented Jan 3, 2019

Hmmm...

const _obc = shader => {...}
const obc = `
function (shader){
  const _${ hash() }
  ${ _obc.toString() }
  _obc(shader)
}
`
material.onBeforeCompile = eval(obc)

^Would this work? I never used eval.

@Mugen87
Copy link
Collaborator

Mugen87 commented Jul 7, 2020

The introduction of Material.customProgramCacheKey() via #17567 ensures that developers now have the possibility that shader programs are not shared for materials modified via onBeforeCompile() with conditional statements.

Please discuss other existing issues or enhancements in context of onBeforeCompile() in new threads (like #13446).

@borisghidaglia
Copy link
Contributor

Hey everyone 👋

Read everything and I'm clearly not competent enough to be relevant in the technical implementation details.

However, what I can do is give feedback as a (noob) Three.js user (coming from threejs-journey.com).

String manipulation felt hacky to me when I discovered the technique. Reading this page, I see that it was a shared feeling from the start (cf two first posts here).

The ExtendedMaterial @mrdoob suggested here would be very helpful! String manipulation under the hood but:

  1. no need to manually look for "hooks" / text to be replaced based on what we want to tweak
  2. no risk of replacing something that shouldn't be replaced

mrdoob implementation example, for reference:

var material = new THREE.ExtendedMaterial(
    new THREE.MeshBasicMaterial(),
    { vertex: 'transformed.x += sin( position.y ) / 2.0;' }
);

This along with some JSDoc to explicitly state which variable has to be modified (transformed, in the example) would be 👌


In the meantime, another thing I can do is updating the doc so that people understand onBeforeCompile more easily.
I created a PR #30043 to add these two examples to the Material doc.

@pailhead
Copy link
Contributor

pailhead commented Dec 5, 2024

I believe that TSL should replace this.

This may also be interesting, for the approach in general.

https://news.ycombinator.com/item?id=40510468

if you really want to compose materials as before I experimented with this many many years ago:

https://github.com/pailhead/three-refactor-chunk-material

You don’t have to manipulate strings in onBeforeCompile, you don’t even need to use it. You just… well, compile, and you’re done with it, at the end of the day it’s a compiled ShaderMaterial.

@borisghidaglia
Copy link
Contributor

I believe that TSL should replace this.

Oh yeah I've seen that as well: https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language#new

Should I add it to the doc?

@pailhead
Copy link
Contributor

pailhead commented Dec 5, 2024

Hm not sure about that, I took a glance and I couldn’t figure out what is added. You made a whole example?

This was, for a while at least, the only documentation available:
https://medium.com/@pailhead011/extending-three-js-materials-with-glsl-78ea7bbb9270

@borisghidaglia
Copy link
Contributor

You don’t have to manipulate strings in onBeforeCompile, you don’t even need to use it. You just… well, compile, and you’re done with it, at the end of the day it’s a compiled ShaderMaterial.

Not sure I'm following you here. Maybe I could reach out to you elsewhere so we don't clutter up this thread?

@pailhead
Copy link
Contributor

pailhead commented Dec 5, 2024

  1. You can write any shader, and use it as a RawShaderMatrial. It takes a string and the string should be your final shader.
  2. You can write most of the shader and let three inject some things, this is the ShaderMaterial.

In both of these cases you can just save a string to a file and load it like myshader.shader.

You can thus do what three does, offline. You don’t need do upload one string, then use three to combine it, you can do this any way you want. For example with nodejs. I’m just trying to explain the drawbacks and gotchas of onBeforeCompile.

If you look at the source of how the old Webgl renderer “compiles” shaders (it’s not really compiling) you can just copy that method and run it yourself. With node, with python, or just before you even load threejs in the browser.

@pailhead
Copy link
Contributor

pailhead commented Dec 5, 2024

Sorry, I realized it still may be confusing. Three assembles “built-in” material when it first encounters them, when rendering. So at best, three will take a bit longer during the first rendering to “compile” all the materials. This can be done before you even try to render anything, before you even create a renderer.

@borisghidaglia
Copy link
Contributor

So at best, three will take a bit longer during the first rendering to “compile” all the materials

I might not have been clear enough in my first message, but I was not worried about performance at all.
Also, I'm aware of how to use onBeforeCompile, but I wrote my first message here because the ExtendedMaterial solution feels better to me than using .replace manually as it is done now (or at least how it's shown in the three.js journey course).

I believe that TSL should replace this.

I'm perfectly happy with using TSL though! In fact I love that it exists!
It will be amazing to see more and more examples of its usage overtime.

@pailhead
Copy link
Contributor

pailhead commented Dec 6, 2024

Ah, I don’t think you’re the first to propose this lol

@borisghidaglia
Copy link
Contributor

I'm not! Never thought I was. But @mrdoob that I referenced and quoted in my first message probably was.

I posted the message because I thought it was a really good idea which might have gone lost along the way.
But if the subject is settled and that you decided not to do it, perfectly fine!
It just wasn't my feeling when I read the thread.

@borisghidaglia
Copy link
Contributor

But if the subject is settled and that you decided not to do it, perfectly fine!

Does your thumbs up mean that it is settled and that it won't be implemented, then?
Thanks for the responsiveness btw! Answering this fast on a (closed) issue this old is nuts!

@mrdoob
Copy link
Owner Author

mrdoob commented Dec 11, 2024

We are trying to move away from the hacky string replacement approach.
TSL is the solution we are betting on.

@borisghidaglia
Copy link
Contributor

Got it! Added link to the new TSL approach in the doc with the PR #30118.
Hopefully, it will help get the word out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests