`onBeforeCompile` fires only once, although I set `needsUpdate = true`

I thought setting material.needsUpdate = true would cause a material’s onBeforeCompile to fire again, but it doesn’t.

From the docs:

.needsUpdate : Boolean

Specifies that the material needs to be recompiled.

The term “recompile” there implies that onBeforeCompile should run again.


When WebGLRenderer checks

if ( program !== undefined ) {

program is undefined only the first time (which makes sense), so it skips the branch that calls material.onBeforeCompile any time after that.

When new programs are made, it seems program is not undefined again, and onBeforeCompile never fires again.

(cc @marcofugaro, Changing the .camera in three-projected-material expects state changes to happen with onBeforeCompile but they don’t, github issue).

A problem that I’m seeing is that if a material subclass does the following, it won’t work as expected:

class MyMaterial extends MeshPhongMaterial {
  #camera = new PerspectiveCamera
  get camera() {return this.#camera}
  set camera(c) {
    this.#camera = c
    this.needsUpdate = true // "re-compile"
  }

  onBeforeCompile(shader) {
    // only runs once, initially
    if (this.#camera instanceof OrthographicCamera)
      shader.defines.ORTHOGRAPHIC = ""
    else delete shader.defines.ORTHOGRAPHIC

    // ... patch shader for example ...
  }
}

and the problem is that, if we set myMat.camera = new OrthographicCamera, the ORTHOGRAPHIC define does not get updated.

Workaround:

The above example can be written like the following instead:

class MyMaterial extends MeshPhongMaterial {
  #camera = new PerspectiveCamera
  get camera() {return this.#camera}
  set camera(c) {
    this.#camera = c
    this.needsUpdate = true // "re-compile"
    this.onBeforeCompile = c instanceof PerspectiveCamera
      ? this.#onBeforeCompilePerspective
      ? this.#onBeforeCompileOrtho
  }

  #onBeforeCompilePerspective = (shader) => {
    delete shader.defines.ORTHOGRAPHIC
    patch(shader)
  }

  #onBeforeCompileOrtho = (shader) => {
    shader.defines.ORTHOGRAPHIC = ""
    patch(shader)
  }

  onBeforeCompile = this.#onBeforeCompilePerspective
}

function patch(shader) {...}

And now, because WelGLProgram.getProgramCacheKey will return a different key of the a different onBeforeCompile function is detected, it will make a new program upon toggle (and successive toggles use one program or the other).

If you set needsUpdate to true and expect a recompilation of the shader, you have to ensure that the program cache key actually changes. If not, three.js won’t recompile the shader since no structural changes happened.

When using onBeforeCompile() like in your above example, you have to make use of customProgramCacheKey() which was introduced some while ago for a similar use case. This method returns onBeforeCompile() as a string by default, but you can implement it with any code you like. The documentation contains a code example to identify two different permutations that onBeforeCompile() can produce.

1 Like

Indeed I realized that after looking at the source code, namely that onBeforeCompile functions are used in the cache key hence I could swap them.

Maybe needsUpdate needs some addition regarding the “recompile” that it mentions. The docs made it seem like needsUpdate would surely trigger a recompile.

The doc for customProgramCacheKey says

Unlike properties, the callback is not supported by .clone(), .copy() and .toJSON().

What does it mean? material.clone() will not include the customProgramCacheKey for the cloned material?

Yes, the same principles for onBeforeCompile() also apply to customProgramCacheKey().