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

Color management: linear workflow #808

Merged
merged 6 commits into from
May 24, 2021

Conversation

garyo
Copy link
Collaborator

@garyo garyo commented Nov 10, 2020

Here's my color-management code -- linear workflow internally, turned on by default.

I've been using it for a while and it works well for me. There are two issues to discuss, I think:

  1. How to allow users to specify the color encoding, if they want to go back to the default? I've added a colorEncoding param to the viewer, which seems like a good place for it, but I don't have a good idea on how to thread it through to colormaker.ts where it's needed. So right now that param does nothing -- in order to get back to the old workflow you have to change colorSpace in colormaker.ts. Ideas appreciated.

  2. @arose commented in Color management in NGL #775 that it might be better to implement color management in the shader; I'd like to propose that it's at least OK if not better to do it this way because the values (vertex colors, param values, and textures) are explicitly linear, which works properly e.g. when exporting models as glTF. The only real issue is that in colormaker.ts, it's still creating 8-bit linear color values because it uses .toHex. Ideally it should produce float rgb triples and use those everywhere, but that's a bigger change than I wanted to get into for this PR. It does mean you lose some precision when specifying very dark colors, but I don't think that's so bad for now; moving to full-float colors would be a good next step.

@garyo
Copy link
Collaborator Author

garyo commented Feb 16, 2021

@fredludlow any thoughts on this? I'd like to get it merged at some point.

@fredludlow
Copy link
Collaborator

Sorry - I made a few false starts at looking at it earlier.

The transparent surface example you put in #775 looks great,

I'm slightly concerned that we lose a bit of the 'depth' effect, e.g. here (managed):
image

vs original:
image

I find it harder to perceive 3D structure from a static image in the linear color managed version - I guess because the darkening of the curved surfaces is less extreme they don't seem to pop out of the image as much - is this by design, or something to do with the loss of precision? (Or might it be fixed/compensated for by updating the lighting model - or maybe it's just what I'm used to and I'd adapt if I tried it for a few weeks...?)

@fredludlow
Copy link
Collaborator

ps - Built versions here (this PR: https://fredludlow.com/ngl/pr-808/examples/webapp.html?script=color/atomindex ) and current master https://fredludlow.com/ngl/master/examples/webapp.html?script=color/atomindex ) in case anyone wants to see side-by-side. If you stick an 'av' surface at ~70% opacity the linear one looks significantly nicer!

@garyo
Copy link
Collaborator Author

garyo commented Mar 2, 2021

Hi Fred; thanks so much for putting up these builds for comparison! You're right that in the lighter colors there's somewhat less contrast and there's less over-darkening around the edges. It's more physically accurate but you're also right that sometimes inaccurate can look better...

Currently ambient light defaults to 0.2; I think that's partly to compensate for the fact that the mids and darks were too dark otherwise. Perhaps along with this PR I should default ambient lower, like 0.05 (or even off altogether). That gets back some of the contrast. Also with a linear workflow that makes more phyical sense. What do you think?

Adding fog can help too (fog should be more accurate now). Try lowering the Fog Near param. Also adding a bit of metalness always looks good; that should bring back some of the darkening around the edges due to the Fresnel effect. But I don't know that we want to change those defaults; they'll be kind of specific to the model.

@fredludlow
Copy link
Collaborator

I've asked a few of my users, and one came up with a view sort of like this (similar to the images above but more typical of what they'd look at day-to-day):

Linear Color (with ambientIntensity: 0)
https://fredludlow.com/ngl/pr-808/examples/webapp.html?script=test/binding-site
image

Original Version
https://fredludlow.com/ngl/master/examples/webapp.html?script=test/binding-site
image

Their view was the linear color version looks 'flatter' and a bit more cartoony, which I think is what I was trying to get across above. This is more of an issue if you're preparing images for publication (when you're working 'live' you adjust the rotation constantly to help maintain your 3D mental model).

In your original post you proposed having the option to switch between modes, which would be my preference if it's doable. You said you weren't sure how to thread the value through - I'm not sure I have any great ideas, but is it something you'd want to allow different settings on different representations/buffers or is it a global setting? (I'm guessing global as you change renderer.outputEncoding?)

@garyo
Copy link
Collaborator Author

garyo commented Mar 8, 2021

Thanks Fred -- user feedback is invaluable for this. I'll work on creating an option. Yes, it has to be global -- it affects the colormakers as well.

Set the renderer output to the recommended sRGB encoding,
and linearize all object colors so the internal three.js
workflow is fully linear. This also allows correct glTF
export of models.

This is WIP because it doesn't propagate the color management
settings from the top-level viewer down to the colormaker yet.
This allows use with the three.js EffectComposer, which needs NGL to render
into a non-null renderTarget. Should have no impact on normal use.
This makes the color space for colormaker global, since it doesn't
really work to have different color spaces for each colormaker. That
makes it possible to easily set the color space (encoding) from the
viewer, just as we set the output color encoding.

This commit resets the default back to sRGB internal workflow (no
conversion of colormaker colors to linear, no color conversion back to
sRGB on output) to preserve existing behavior, per comments in nglviewer#808.
To set up a linear workflow, just do:

```ts
    stage.viewer.setColorEncoding(Three.LinearEncoding)
    stage.viewer.setOutputEncoding(Three.sRGBEncoding)
```
@garyo
Copy link
Collaborator Author

garyo commented Apr 9, 2021

Added a viewer function viewer.setColorEncoding() to match viewer.setOutputEncoding() and rejigger the colormaker code to make that work. This resets the default behavior back to sRGB all the way through. Should address your notes, @fredludlow .

@fredludlow
Copy link
Collaborator

Apologies once again for the big time-lag... I'm looking at this now and wondering if you can help me understand it a bit...

In the master branch, renderer.outputEncoding etc are set to 3000 (which is LinearEncoding). The manageColor decorator doesn't exist here so it's sort of equivalent to having called setColorEncoding(sRGBEncoding)

With a fresh build from your branch the outputEncoding is set to 3001 - sRGBEncoding and colorSpace (setColorEncoding) is linear - the overall brightness is correct but I'm getting the slightly flat look described above,

I can recreate the original behaviour with stage.setOutputEncoding(3000 /* Linear */) and stage.setColorEncoding(3001 /* sRGBEncoding */) - this makes sense I think...

So, to summarize:

The old way is working in sRGB internally with no transformation upon output, the properly managed way is to work with linear colors internally and transform to sRGB on output?

If I'm right (big if :) ), is there any reason why you'd ever want to setOutputEncoding and setColorEncoding to the same value? If not, should there be a single parameter which switches between the two schemes?

If I've got this all wrong please let me know! (Happy to have a call if it's easier than my long-delayed responses..)

@garyo
Copy link
Collaborator Author

garyo commented May 20, 2021

Thanks for the review, @fredludlow . Yes, you're exactly right in your summary. My intent with the last commit 3652a34 was to set the default behavior back to the traditional way (sRGB colors internally, compositing in sRGB, no output conversion), but looks like perhaps I didn't get it all the way done.
And yes, you have a good point about just using a single 'setColorWorkflow' method in viewer.ts. I didn't go that way originally because the output setting was in the viewer and the colormaker setting was... complicated. But now that they're both in the viewer, it makes sense to collapse them.
I'll fix both of those issues.

Just one new API function: `viewer.setColorWorkflow(encoding)`
@garyo
Copy link
Collaborator Author

garyo commented May 20, 2021

Fixed defaults, and tested default behavior vs. master and it looks the same.
Now there's only one new API function, viewer.setColorWorkflow("linear"|"sRGB").

@fredludlow fredludlow merged commit f2a9a50 into nglviewer:master May 24, 2021
@fredludlow
Copy link
Collaborator

Thank you for the fix @garyo, now merged!

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

Successfully merging this pull request may close these issues.

2 participants