Changes to how Diffuse & Specular is combined (since v1.55)

Since engine version 1.55.0, the standard shader renders differently than before. We’ve tracked down this change in the chunk combineDiffuseSpecular, which looks like the culprit.

Previously, the more specular you had, the less diffuse you’d get. Now diffuse and specular is added, which produces a wildly different result than before. Compare materials that are using a combination of high specular and high diffuse:

Here’s the project we used for testing the above materials. Compare the look when launching version 1.54.1 vs version 1.55.0.

This change has affected all our projects, since the Playcanvas editor always runs the latest engine version. This means we have to tweak all our previous materials (hundreds, if not thousands!) in all our projects, and that’s not something we or our customers expected to pay for.

Also, our apps are often based on a fixed Playcanvas engine version. And since the editor is always using the latest version, we now get a mis-match between what artists create in the editor, and what our apps look like (since they’re using different shaders). This makes it impossible for artists to see what they’re working on - kind of a big deal! To get visual parity between the editor and the apps, we’re forced to update the apps versions as well, which brings even more changes to the API, which means we have to re-write parts of our apps.

As you can imagine, this is not a sustainable way to work. We need to be able to work from a stable foundation, and that’s impossible if the tools we’re using are changing when working on a project. If we could choose what version of the engine a playcanvas project was using (the entire editor, not just when launching), this wouldn’t be a problem.

What’s the correct way forward here? We currently don’t see any other alternative than to use the latest version, since that’s what the editor uses. However, then we’d still need to tweak all our previous materials so that they look like they did before. How can we “migrate” our materials to recreate the old look with this new shader?

Our first guess when we tried to recreate the old look using the new shader was to calculate a linear interpolation between Old Diffuse color and Black, with Specular as the alpha value. However, this produced colors that were too dark. If we manually tweak each color so that it matches as closely as possible, then plot the Old Diffuse vs the New Diffuse at different Specular levels, we get the following graph:

We can see that for any given Specular value, the change is linear (with some margin for error). This means we can use the Specular value with some factor to modify the Old Diffuse to get the correct New Diffuse.

If we plot Specular vs the New Diffuse, we can see that they all follow the same function (again with some margin for error):

We haven’t figured out what the function is, but thought this might help you help us migrate to the new shader.

Data points
Old Diffuse* Specular New Diffuse**
1 1 0
0,75 1 0
0,5 1 0
0,25 1 0
0 1 0
1 0,95 0,369
0,75 0,95 0,275
0,5 0,95 0,188
0,25 0,95 0,098
0 0,95 0
1 0,75 0,714
0,75 0,75 0,537
0,5 0,75 0,361
0,25 0,75 0,192
0 0,75 0
1 0,5 0,898
0,75 0,5 0,674
0,5 0,5 0,459
0,25 0,5 0,227
0 0,5 0
1 0,25 0,992
0,75 0,25 0,737
0,5 0,25 0,494
0,25 0,25 0,25
0 0,25 0
1 0 1
0,75 0 0,75
0,5 0 0,5
0,25 0 0,25
0 0 0

*The diffuse value we input in the old shader
**The diffuse value we need to set in the new shader to recreate the old look

I would need one of the GFX engineers to come and answer on what has specifically changed on Diffuse and Specular between the engine versions and how to migrate.

Have you checked against the latest engine version with your test project?( is private so I can’t access it to check myself?) It’s possible we’ve made fixes/changes based on reports over the versions.

If this does still happen with the latest version of the engine, this looks more like a bug and we need to investigate.

However, I can give some information across the other points mentioned above.

At PlayCanvas, we will ensure that our engine is as backwards compatible as it can be and have tests in place for areas such as rendering where image diff between engine versions (as seen in this video Improvements and changes to Shader Chunks - PlayCanvas Bytes Jul 6 '22 - YouTube)

The exception has been with version 1.55 where we had to make the difficult choice to make a large number of changes and refactoring to build a much stronger foundation going forward with our shaders. This unfortunately caused issues for some developers (including yourself) that we’ve addressed or fixed in a later engine release.

This refactor has allowed us to support all the glTF 2.0 materials to the standard and has paved the way to make shader development easier, both via code and in the future a shader node based graph editor

With GLB import Editor support in progress, this would mean that as long as the GLB conforms to the glTF 2.0 spec, materials would be setup correctly on import as well, improving the asset workflow pipeline.

Updates to shaders (if you have custom shader chunks) have been documented here as well.

When it comes to material migration, (assuming it isn’t a bug), it is possible that we could use the Editor API to write a small code snippet that will set the properties for the material assets rather than needing to do it by hand.

As a short term workaround, it is possible to run the Editor with an older version of the Engine but is not an end user supported feature and is meant to be used by us for debugging.

This is because the Editor may depend on Engine API that is in the current version of the Engine to render in the viewport.

Use the same use_local_engine URL param for the Editor (eg

Yes, the latest versions show the same behavior 1.55.0. I changed the project to public now. It’s unclear if this is a bug or not, since different engines seem to handle diffuse and specular blending differently. We assumed this was part of making playcanvas behave more like glTF.

I understand that changes need to be made - I fully support them, and we’re looking forward to more interoperability between Playcanvas and glTF as well as the shader node editor. But the problem is when these changes are forced upon developers in the middle of a project.

Thanks for the use_local_engine param, I’ll see if we can use it for now. But of course, a better long-term solution would be to allow users to specify editor version as a project setting.

Some projects require updating from time to time though, so we’d still need a way to migrate materials made before 1.55.0 to the lastest version (assuming it isn’t a bug). We’d appreciate if someone from the graphics team could take a look at this.

Added an issue to the Engine repo to investigate to see if it’s a bug: Diffuse and Specular combination difference between 1.54.1 and 1.55+ · Issue #4945 · playcanvas/engine · GitHub

Hej @Sidelity!

So I’ve been having a look at this and I remember this change. It’s a bit of a contention within the different PBR models whether or not diffuse should be reduced by specular energy as a linear interpolation or if it should be a simple concatenation of diffuse light with specular light with a weight, so I decided to go for the solution mostly agreed on when I refactored this code.

With that said, we 100% understand your situation and have a solution which adds back the scaling of diffuse light with regards to specular for your project. Before I finish that up, I want to give some preliminary results:

I hope this matches what you are looking for?

When everyone is back from vacation, we will sit down and discuss whether or not we want to support this type of behaviour on the material directly, instead of providing a custom made solution. However, given the nature of your projects, it’s probably safe to assume we want to create a special solution for your projects which applies this shader change to all materials.


Hej igen @Sidelity!

I created a fork of your project here: PlayCanvas | HTML5 Game Engine, which contains a script that sets an alternative combine shader for all materials in the scene. I attached it to the main camera, which should suffice for any other projects you may have.

As mentioned before, we will have a look at perhaps adding this to the material definition so it’s accessible on a per-material basis, but I hope that this solution will suffice for the time being. Obviously, this change won’t show in the editor but only shows when you launch.


1 Like

Is the thread “Does the clearcoat material not work for glTF files?” related to this issue?

Because the result of PlayCanvas seems (for some values) less reflective compared to the Khronos glTF previewer.


Yes, when we spoke about this internally, I mentioned that I recognized this new shader behavior from Unity. That’s what lead me to believe that this was a feature and not a bug. As I tried to convey in my previous messages - it’s not the change itself that we have a problem with, just that it’s a breaking change that’s forced upon all projects.

I appreciate the work-around script. The problem with a script though is, as you mentioned, it’s not going to show in the editor. This is a cumbersome workflow for artists for a couple of reasons. Launching apps takes time, there is a lag between editor changes and running apps, some changes require launched apps to be refreshed, scripts may crash preventing the app from launching, etc. Also in our experience, relying on scripts that modify/replace shader chunks is just asking for trouble. Shader chunks are subject to change at any time, which may bring changes that make the script incompatible (for example, renaming or removal of variables or entire chunks). The changes in 1.55.0 is a perfect example of this.

Personally, I think the best way forward (if this shader change is kept) is to aid users in migrating from the old shader to the new one. In our case, this is only necessary for apps that are updated to engine 1.55.0 or higher. For older apps, we can use the use_local_engine parameter in the editor to see how things used to look. It would be a balancing act though, with the artist’s inconvinience of using the url param on one hand, and the time to upgrade an app’s engine version on the other. We’d also have to keep track of what projects use which engine version and communicate this to team members, so it might get messy.

1 Like


The graphics team will be back in January next year to discuss as more permanent solution for this problem, one which might end up being a new material property. The reason we don’t jump on that immediately is that we don’t want to add material properties that aren’t absolutely necessary, as the UI is already quite littered with different parameters. Furthermore, adding a code-only material parameter would mean a script is still needed to set the value, so there wouldn’t be any clear benefit from doing that straight away either :slightly_smiling_face:

In your case though, you’d still need a script to set the material parameter across your projects, so there isn’t any really good way to avoid using a script to apply the fix on a broad scale. That script should solve the immediate issue for your customers though, and with the chunk API validation, there shouldn’t be any issues in detecting if the replacement chunk in the script is out of date and needs replacing.

We do have it on our backlog to implement in-editor scripts, although that most likely won’t come any time soon.

As for internal purposes, I suggest using the use_local_engine to select an older engine, or grabbing PlayCanvas latest and making the necessary change to combine.js, and then host the modified engine internally.

Since this change came into effect in 1.55 and we have many customers already adapted to the many changes in that release, it’s hard for us to simply revert this change as that would break a lot of other projects.

We do also provide the shader chunk migrations document mentioned by @yaustar previously which should help with migrating between versions. With that said, we haven’t provided a document of visual changes between versions - something we will definitely keep in mind if we make future changes that modifies the visuals. On top of that, I think it’s also very reasonable to provide some guidance for how to migrate changes over to the newer version of the engine, so we will definitely do that moving forward.



Extra note on the chunk override. It can be done in a way that doesn’t require a scriptType and therefore doesn’t need to be added to a scene:

Have the following in a script asset

(function () {
    const combine = `
        vec3 combineColor() {
            vec3 ret = vec3(0);
        #ifdef LIT_OLD_AMBIENT
            ret += (dDiffuseLight - light_globalAmbient) * dAlbedo + material_ambient * light_globalAmbient;
            ret += dAlbedo * dDiffuseLight;
        #ifdef LIT_SPECULAR
            // Apply diffuse scaling before adding specular component
            ret *= 1.0f - dSpecularity;
            ret += dSpecularLight;
        #ifdef LIT_REFLECTIONS
            ret += dReflection.rgb * dReflection.a;

        #ifdef LIT_SHEEN
            float sheenScaling = 1.0 - max(max(sSpecularity.r, sSpecularity.g), sSpecularity.b) * 0.157;
            ret = ret * sheenScaling + (sSpecularLight + sReflection.rgb) * sSpecularity;
        #ifdef LIT_CLEARCOAT
            float clearCoatScaling = 1.0 - ccFresnel * ccSpecularity;
            ret = ret * clearCoatScaling + (ccSpecularLight + ccReflection.rgb) * ccSpecularity;
        return ret;

    pc.shaderChunks.combinePS = combine;

And change the loading type to be ‘After Engine’

This will execute the script after the engine script has been added so it doesn’t matter which scene you are in etc.

I’m trying to find a way to get this working in a project plugin style but is a little difficult because there is no ‘hook’ to add code after the Engine is loaded in the Editor :thinking:

1 Like

Update on our side. In the next minor release (1.60.0), we’ve changed the combined render to be the same approach as <1.55. There will be very minor differences as there was a bug in the rendering <1.55 but should be very close.

1.60 is now in release candidate on the Editor