General – Linear color space - ac-custom-shaders-patch/acc-extension-config GitHub Wiki

Linear color space in CSP is an new feature introduced in 0.2.3 which allows WeatherFX styles to change the way Assetto Corsa processes shading. It’s disabled by default, so old WeatherFX styles still look the same.

What is it?

In computer graphics, there are two more popular ways to store and process image data (aka color spaces). One is a linear color space, where the stored brightness value linearly proportional to, let’s say, resulting luminance of a display pixel (so, RGB=127 would be half as bright as RGB=255 if you were to measure a screen with a light meter). But the thing is that our perception is non-linear: we don’t see things twice as bright when turning on a second lamp, and can barely notice a flashlight spot in the direct sunlight while it can sometimes even be blinding at nights.

Because of that, an alternative color space exists called sRGB (or gamma color space), which is a lot closer to how humans perceive light. Storing data in that format leads to a lot more efficient space usage and helps to eliminate banding in dark areas (RGB=2 is not much different from RGB=1 in gamma color space, while in linear it’s pretty huge). Because of that, it’s pretty the main standard for any computer graphics data. JPEGs and PNGs store data in sRGB, Windows desktop composer expects images to return data in sRGB and then passes that sRGB data to the monitor as sRGB.

Unfortunately, because of that it’s easy for game developers to make this mistake where the actual 3D shading is also done in sRGB. But a lot of models used by 3D rendering fail miserably if not done in linear color space. Even something as simple as “iterate over nearby light sources and sum their contributions for a given pixel”: while it’s a perfectly reasonable thing to do when working in linear color space, and it will produce the effect of a flashlight spot fading away under direct sunlight, if done in gamma space it’ll end up looking like garbage. Same goes for pretty much every other aspect of a renderer, including reflections (ever noticed how black cars IRL seem more reflective than white cars when illuminated?), fog (if done in linear color space with proper scattering, illuminated objects should be visible much further than dark objects), the way lights interact with surfaces and much more.

So, this is what the new option is doing. A simple configuration line, but inside it switches to a completely new set of shaders and upgrades quite a few parts of Assetto Corsa rendering. And that’s why it’s up to a WeatherFX style to decide wherever to activate it or not: before, styles were painstakingly tuned to try and workaround the fact that any shading-related computation in AC was completely incorrect by providing all sorts of fake values, even for something as basic as sun/ambient light relation. Now with a lot more accurate renderer most of those hacks are no longer needed.

And, of course, it’s not just WeatherFX styles that were finetuned to combat the problem, a lot of, for example, materials or PP filters were configured this way as well. But I expect impact there to be less severe: new set of shaders does some transformations in background to try and keep the look similar (for example, fresnelMaxLevel gets squared); although, of course, it’s impossible to match things perfectly: if nothing else, now black cars would appear more reflective in AC as well.

Current state

CSP v0.2.3 should have pretty much all of its features adapted for the new color space. One notable exception however is Lua shaders: sadly there isn’t really a good way to add a compatibility layer, so the shading might be off. There might also be some issues with transparency: correct linear color space changes the visual result of alpha blending mixing, and sometimes it might lead to some discrepancies with the way some, for example, car dashboards look. There are a few tweaks in place already to try and mitigate the issue, but it might not be enough.

Another problem is that linear color space underlines any general issues and shortcuts with rendering a lot more, but that will be fixed in the future as we keep working on visual side of AC and upgrading various visual effects.

Oh, and linear color space doesn’t apply in showroom or previews generation modes yet. And some original effects, such as old smoke, might misbehave in certain lighting conditions.

Performance impact

New set of shaders does have some extra instructions to convert things between color spaces and adjust parameters, as well as new double-layed fog. But the old set had two variants of fog, original Kunos fog and CSP one. So, generally speaking, the performance impact should be close to negligible. Oh, and if you’re not using YEBIS replacement, it might add an extra post-processing step, but if you do care about performance YEBIS replacement might be a cool option for you to try anyway (autoexposure in the one used by default WeatherFX style has improved greatly in 0.2.3).

Material configuration tips

Main tip for now is, unless you’re using fuPBR shaders please consider targeting old color space for now instead of reconfiguring materials for the linear color space, especially if you’re using material templates from all those [INCLUDE: common/material…] files. Material parameters remapping might change in the future for something more fitting, so your old materials could improve, while materials adapted specifically for linear color space might break.

All of that, of course, doesn’t apply to fuPBR shaders. Just use the regular PBR maps for those without any adjustments. And if things do look off, please don’t edit things like reflectivity or roughness maps, fuPBR shaders instead should update to be as accurate as possible. That’s the whole point of proper PBR after all.

Lua scripting tips

There are two key points to consider now when writing scripts. First of all, if you draw anything with a shader in a main render pass, you might need to update your shaders. Use USE_LINEAR_COLOR_SPACE (can be used in both #if USE_LINEAR_COLOR_SPACE and if (USE_LINEAR_COLOR_SPACE)) to change some logic if the fix is applied. You can also use toLinearColorSpace() and toSrgbColorSpace() to convert to linear color space and back. For example, if you’re reading a diffuse texture in your computation and multiply it by some analog of ksDiffuse, with linear color space you should pass the result through toLinearColorSpace() and it should work out nicely (no need to branch there, if linear color space is disabled toLinearColorSpace() won’t do anything and just return its input).

Another point is how you should work with input textures containing shot of a scene if you want to output them, for example, on a car dashboard in a LDR texture. In general, make sure to pass HDR scene input through convertHDR(). With linear color space WeatherFX styles can set overall scene brightness to something like 0.001 for more efficient use of float16 range, and convertHDR() will counteract that. Set second argument to true if you need to do the opposite operation for some reason.

Also, you can access the same transformation using ac.convertHDRToLDR() function from Lua itself.

WeatherFX development tips

To enable the fix, simply call ac.useLinearColorSpace() with true argument. As for its second parameter, that’s the first caveat: AC and CSP use float16 (aka half) format for pretty much all of their render targets, and while in general it’s great, its range is only something like 1/65000…65000 (and the same negatives). Not that bad for the way things were originally drawn, but now with conversion stage like resultEmissive = pow(ksEmissive * txDiffuse, 2.2) it’s a lot easier to hit that 65k boundary (white texture and ksEmissive of 200 would already hit the limit). Best way I found to solve it is to use scene brightness multiplier and set it to somewhere around 0.001, since we have a lot of unused precision in thousandths. And the same goes for lights: to optimize data exchange, their colors are stored in float16 format when being passed to GPU, so a light with color of 200 would end up clamped. That’s what the second parameter of ac.useLinearColorSpace() is for. Set to to something like 100 and light colors will be divided by 100 when sent to GPU, and the LightingFX contribution multiplied by 100 on GPU side later.

When working on updating default WeatherFX style, I noticed quite a few bugs, too many to fix with options, so apart from switching color space that function also applies a bunch of fixes altering behavior of some functions:

  • Clouds take sky fog offset and exponent into account, while previously they were ignoring those values;
  • Distant glow brightness doesn’t get affected by scene brightness multiplier twice;
  • White reference point is affected by scene brightness multiplier;
  • Moon mie is working with v2 sky shader (previously due to a typo it wasn’t added to the final result);
  • Function computing absorption no longer confuses v2 sky facing sun and facing away values (that was a particularly bad one, sorry 🤦‍♂️);
  • Volumetric light now uses brightness multiplier;
  • ac.setBrightnessMult() scales other values (previously, for example, you’d have to set brightness multiplier and then set fog color for it to apply; now you can set fog color, change brightness multiplier and fog color will be rescaled);
  • Sky color computation wasn’t taking sun color into account at all.

Another big change is in fog functions. Old set of shaders had two fog implementations, one in Kunos style and another is a height-based fog from this post which I reguarly saw being used on ShaderToy before. Unfortunately, with correct linear color space it became clear it’s not quite what we need: because of that height-based feature it always reaches 1 at some point (or at least it does in my version, maybe I missed something, but I couldn’t figure out how to use it properly). Ccorrect fog should never reach one, that’s what makes it so bright and shiny objects could be seen much further in fog than bright ones. So, it was replaced with a new fog controlled by ac.setNearbyFog(), and it’s a second layer of fog you can add on top of the scene for, well, actual fog and mist. And old-new fog was simplified so it could be used for distant haze or simple atmosphere light absorption, so now there are two fog layers you can use together.

Of course, linear correct color space also requires a post-processing step turning linear image back to sRGB one. By default CSP would do that step for you before sending image to YEBIS for post-processing using values you can provide with ac.setHDRToLDRConversionHints(), but if you’re working on a custom post-processing implementation, maybe you’d want to use something special. For example, for default WeatherFX style I used a different linear→sRGB conversion which is supposed to be more accurate. Might also make sense to apply linear→sRGB conversion right before tonemapping step, and not at the start. But if you don’t want to replace YEBIS, you can always just return a canvas from ac.onPostProcessing() with sRGB data (see “render_linear.lua” in the default WeatherFX style for an example).

And one more thing. Same as others, WeatherFX Lua first run when AC is already loaded, which includes shaders. That can be a problem if the first thing script is doing is to switch to a linear color space shaders set causing a massive lag. To prevent such issues, please add [CORE] LINEAR_COLOR_SPACE_HINT = … value to your “manifest.ini”. It could be 0, 1 or a section and key name if your linear color space is optional and controlled by a checkbox in “settings.ini”.