I’m making some custom shaders and replacing the source code of Babylon shaders. I’m using the customShaderNameResolve
method. For some reason, sometimes this seems to get called every frame. Is this supposed to get called every frame?
No, if it is called every frame it means the material effect is recreated each frame which should normally not happen. It means you are doing some changes on the material that triggers a recompilation each frame.
A repro would be great if it happens without changing materials properties on standard/pbr/nme as it could be a pretty annoying bug ?
I found at least one case I don’t understand, my material defines._renderId
is for some reason always behind the scene’s renderID. I discovered this by debugging PushMaterial.prototype._isReadyForSubMesh
. Every frame, the defines._renderId
is two lower than the scene render ID.
Both render IDs increment every frame, but the mesh material is always lower.
I’m trying to track down how my material gets into this state.
I seem to get into this state when, on my material, I set material.bumpSampler = someTexture
, where I have someTexture
previously set as new BABYLON.Texture('/somefile.jpg', scene)
. I’m not re-creating someTexture
every frame. But the act of setting this property:
sceneData.mesh.material[input.property] = newValue
Where input.property
is bumpSampler
, then I get into the state where the renderID lags behind the scene.
I’m only setting this value once, I don’t set bumpSampler
every loop.
I’m trying to re-create what my code is doing in a playground. I haven’t yet been able to reproduce my bug in a playground, but you can see generally what I’m doing at this link, if anything looks out of place:
This is not a bug, it’s normal operation.
scene.renderId
is incremented once if render targets are enabled (which is the default, even if there are no render targets to render) and once more before each camera is processed. Thus, at each frame, scene.renderId
is incremented by at least 2 (more if there are render targets, more than one camera, or for other reasons).
The PushMaterial._isReadyForSubMesh
function is an optimization to avoid checking a material more than once in a frame. So the first time this function is run (for a material/subMesh) for a frame, you will have a difference between defines._renderId
and scene.renderId
of at least 2.
I think I’m confused, because you said:
if [customShaderNameResolve] called every frame it means the material effect is recreated each frame which should normally not happen
But now you are saying
This is not a bug, it’s normal operation.
In my debugging image, you can see that _isReadyForSubMesh
is false
. The call stack looks like:
Mesh.prototype.render
calls material.isReadyForSubMesh(...)
.
That itself calls this._isReadyForSubMesh(subMesh)
and inside of there it checks defines._renderId === this.getScene().getRenderId()
which you’re saying will always be false.
So then back up in isReadyForSubMesh
it continues on to call var effect = this._prepareEffect(...)
which itself calls customShaderNameResolve
.
So because the render IDs are different every frame, customShaderNameResolve
is called every frame.
Is this expected behavior? Is something else in isReadyForSubMesh
supposed to bail out before it gets to _prepareEffect
to avoid customShaderNameResolve
getting called every frame?
Not exactly, it will be false
the first time it is called in a frame, but true
in case it is called multiple times in the same frame.
No, this._prepareEffect(...)
won’t call customShaderNameResolve
if defines.isDirty === false
.
The fact that in your case customShaderNameResolve
is called every time means that defines.isDirty === true
, which means that you are modifying the material with every frame and dirtyfies it. You should find out why the material is dirty in each frame.
Hmm. You’re right, defines._isdirty
is true. When I compare the defines
object between two frames, only one key is different between the frames, which is _renderId
. I’m not explicitly setting _renderId
. All the other keys/values on the two defines objects are ===
identical.
Does Babylon’s internals setting _renderId
mark the defines as dirty?
No, renderId
is set to scene.renderId
at the end of isReadyForSubMesh
and does not dirtyfy the material.
You should look at these properties of the defines
object:
public _areLightsDirty;
public _areLightsDisposed;
public _areAttributesDirty;
public _areTexturesDirty;
public _areFresnelDirty;
public _areMiscDirty;
public _arePrePassDirty;
public _areImageProcessingDirty;
You should have at least one being true
when defines.isDirty == true
. That should help you find which changes to the material dirtyfies it.
Interesting. All of my _are* keys appear to be false, but _isDirty is still true. Here’s a copy-paste from all of those keys:
_areAttributesDirty: false
_areFresnelDirty: false
_areImageProcessingDirty: false
_areLightsDirty: false
_areLightsDisposed: false
_areMiscDirty: false
_arePrePassDirty: false
_areTexturesDirty: false
_isDirty: true
_keys: (218) [...]
_needNormals: true
_needUVs: true
_normals: true
_renderId: 9976
_uvs: true
The only key suffixed with “Dirty” that’s true is the _isDirty
. Do either any of the other keys like _needsNormals affect dirty?
Still tracing, looks like markAsUnprocessed()
is getting called because _isReadyInternal
returns false, because there is no _pipelineContext
on the effect? Each frame it gets to the last line of this method
Effect.prototype._isReadyInternal = function () {
if (this._isReady) {
return true;
}
if (this._pipelineContext) {
return this._pipelineContext.isReady;
}
return false;
};
I’m starting to suspect this is happening because my shader is failing to compile, causing no pipeline context to be created, but I’m still investigating.
I might need to open a separate thread for this.
I found an issue, which has been confusing me for a while. I have been getting a compile error after setting valid GLSL inside of processFinalCode
. After some debugging it seems like processFinalCode
is a poor API name, because it’s not “final.”
In the GLSL I put in proessFinalCode
, I create my own out variable, as in out vec4 myCustomOut;
. Babylon also apparently hijacks the code after processFinalCode
and adds its own out variable: Babylon.js/webGL2ShaderProcessors.ts at 6fa632432ec5e7940933922c5018ccfd25aa2c47 · BabylonJS/Babylon.js · GitHub
code = code.replace(/void\s+?main\s*\(/g, (hasDrawBuffersExtension ? "" : "out vec4 glFragColor;\n") + "void main(");
The end result of this is my shader ends up containing two output variables:
out vec4 myCustomOut;
...
out vec4 glFragColor;
void main() {
This causes the GLSL error:
Error: FRAGMENT SHADER ERROR: 0:114: ‘myCustomOut’ : must explicitly specify all locations when using multiple fragment outputs
ERROR: 0:658: ‘glFragColor’ : must explicitly specify all locations when using multiple fragment outputs
My suspicion is that the invalid GLSL in the shader causes a compilation failure which prevents creation of a pipeineContext, which I first noticed as customShaderNameResolve getting called on each frame. At least, I think, I’m not sure. I can’t reproduce this bug in the playground I linked above. If I enter invalid GLSL in the textarea then I don’t see customShaderNameResolve
called in a loop. Is there some edge case bug in Babylon where invalid GLSL causes customShaderNameResolve
to get called every frame?
My goal is to take the raw GLSL created by a Babylon PBRMaterial, manipulate it with a compiler, and then put the compiled GLSL back into the source code of a new PBRMaterial. Is there a way to prevent Babylon from manipulating the code after the processFinalCode method? I’m trying to set my own out
variable, which is conflicting with the one that Babylon hijacks the GLSL with.
Yes, you need to fix the compilation errors you have first, because once you fix them, your other errors might disappear.
In fact, processFinalCode is really called as a last step: the injection of out vec4 glFragColor
is done before, so you can remove it by string manipulation if you want (replacing it with an empty string - there’s no way to avoid Babylon doing this injection).
See this PG:
It dumps the fragment code from the processFinalCode
callback: you will see that out vec4 glFragColor
is there.
Making progress! I think what I’m seeing happening is that my material customShaderNameResolve
/processFinalCode
is getting called multiple times as part of my compilation process. I intentionally bail out of customShaderNameResolve
if my internal compiled source code hasn’t changed. I’m trying to determine if that’s the issue - that on subsequent calls of customShaderNameResolve
for (what should be) the same custom material, since I bail out, it doesn’t set processFinalCode
, so it doesn’t remove the glFragColor
line on subsequent calls.
In my compiler, what I’m trying to do is:
-
I kick off my compile process, something like
myEngine.compile()
-
I create a
new PBRMaterial
and create aprocessFinalCode
so that I can scrape its source code. -
Right after creation, I call
newPbrShaderMaterial.forceCompilation(mesh)
to force the scrape to occur. My expectation is thatforceCompilation
is a synchronous operation, all within the same frame. -
I (synchronously) pass the returned source code back to the scene component, which itself creates a
new PBRMaterial
withprocessFinalCode
that injects the compiled source code (the first paragraph of this post).
I have a few questions now, because I think my assumptions are wrong:
-
Can I count on
forceCompilation
to be synchronous like I am to scrape the source code? Or must I wait until the callback happens? The code path I described works, I get the initial PBR source code synchronously (I think). -
When I call
newPbrShaderMaterial.forceCompilation(mesh)
- I think it also causes the mesh’s current material to recompile. Does cause bothnewPbrShaderMaterial
andmesh.material
to recompile? Does it cause any other materials in the scene to recompile? Can I force the compilation of a single material only? -
I notice you call the original _saveCustomShaderNameResolve on the shader - should I be doing that in my case? What is the risk of not doing that?
Thanks for your help by the way, you’ve moved me very far along.
No, it is not synchronous, except if you set engine.getCaps().parallelShaderCompile = null
. It can be synchronous even when parallelShaderCompile
is enabled, depending on the fact that the effect already exists or not. If it does not exist and a new one must be created, then it is not synchronous because it “waits” (using setTimeout
regularly) for the effect to be ready before calling the onCompiled
callback.
No, only the material for which you call forceCompilation
will be recompiled, for the mesh you pass in parameter.
If you know that nobody else will have registered a customShaderNameResolve
then you are safe not calling the existing one (if it exists). In any case, it’s not used by the system, so you can’t generate internal errors if you don’t call an existing customShaderNameResolve
.
Ah, ok, I think I have been able to reproduce the specific bug that’s confusing me.
Click “change texture” and observe the console. https://playground.babylonjs.com/#YPXI4C#19
How can this infinite dirty/re-check be prevented when modifying a material such as this? And why does it only happen when a material is added?
From debugging the code, it’s because the renderId
is different in _isReadyForSubMesh
.
You are doing defines.AMBIENTDIRECTUV = 0.00001 * id();
in the callback, so in effect you are dirtyfying the material each time the callback is called. [EDIT] Hum, not so sure, even if removing the line makes it work… Disabling parallel shading compilation makes it also work, so I will have to debug it when I come back to work next week. [/EDIT]
You probably don’t need this line, as customShaderNameResolve
being called means the effect is being recreated.