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

Lighting support in the batch renderer #1970

Merged
merged 8 commits into from
Jan 10, 2023
Merged

Lighting support in the batch renderer #1970

merged 8 commits into from
Jan 10, 2023

Conversation

mosra
Copy link
Collaborator

@mosra mosra commented Dec 20, 2022

Motivation and Context

Things left to do:

  • Base implementation in the batch renderer
  • Distinguish between flat-shaded and shaded materials and render them appropriately
  • Make it possible to draw shaded and flat meshes in a single draw call (currently the batch count is 2 instead of 1 in the test and it binds some additional texture, huh?) it's fine, the second draw call is for the third untextured sphere
  • Tests for the base implementation
  • Tests for clearLights()
  • Properly implement the point vs directional light distinction
  • Check code coverage -- all good
  • Enable and test per-scene light culling (i.e., the shader using not always the maximum light count, but the exact count each scene uses)
    • Note that this PR won't include per-draw light culling. Apart from this being out of scope of this initial PR, Habitat first needs to include a "range" value for all lights, which is essential as it contributes to the falloff. Without it the falloff is infinite.
  • Hook up with the gfx-replay pipeline
  • Hook up with replayer, enabled with an explicit --max-light-count option.
  • Adapt Compositor "scripts" for the batch renderer #1969 to include normals and properly mark flat-shaded vs PBR materials

How to enable this within the Python viewer, for @0mdc:

  1. Pass an appropriate maxLightCount to the renderer configuration, as the default is 0, which flat-shades everything.
  2. There's no step 2. Just ensure you're using correct composites and that an appropriate light setup is passed through.

How Has This Been Tested

Look at those pretty pictures! 🔴 🟡

yaaay

@facebook-github-bot facebook-github-bot added the CLA Signed Do not delete this pull request or issue due to inactivity. label Dec 20, 2022
@0mdc
Copy link
Contributor

0mdc commented Dec 20, 2022

Looks good so far! Nice progress.

@mosra mosra force-pushed the let-there-be-light branch 6 times, most recently from f1adc6a to afe4d87 Compare December 20, 2022 19:23
@mosra mosra marked this pull request as ready for review December 20, 2022 19:23
@mosra
Copy link
Collaborator Author

mosra commented Dec 20, 2022

This PR is now ready for a review. What's left is implementing the distinction between point vs directional lights, and updating the compositor scripts in #1969 to include normals and mark only the acually baked materials as flat.

A horrible screenshot for your viewing pleasure -- the overlapping objects are an issue with the input data, and the extremely overblown lighting as well. I'll dive into this and try to create some more visually pleasing setup.

image

@mosra mosra force-pushed the let-there-be-light branch 2 times, most recently from 5f6ebb1 to 09f3909 Compare December 20, 2022 20:41
@mosra
Copy link
Collaborator Author

mosra commented Dec 20, 2022

#1969 is now updated with lighting-aware composite creation and this PR is thus considered finished from my side. Output with the new composites for reference (above was using the loose glTF files) -- the main difference is that the Replica room model is now correctly using a flat-shaded material and thus isn't affected by lighting, which was making it look weird.

image

Non-ugly screenshots posted on Slack.

}

renderer_.lightColors(sceneId_)[lightId] = light.color;
// TODO range, once Habitat has that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@0mdc 0mdc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

Copy link
Contributor

@eundersander eundersander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good progress. My comments are mostly just aimed at gaining understanding.

* By default no lights are used, i.e. flat-shaded rendering.
* @see @ref Renderer::maxLightCount()
*/
RendererConfiguration& setMaxLightCount(Magnum::UnsignedInt count);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a comment with some guidance for how to pick this number? E.g. Assuming a single draw can possibly be the entire scene, then the max count should probably be similar to the expected max light count across all scenes (is that even true?).

Copy link
Collaborator Author

@mosra mosra Jan 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the max number of lights applied per draw.

The semantic will change slightly once light culling is in (for which it's essential that the lights contain range info, which they do in glTF files but not yet in Habitat-supplied lights). With light culling, this will be an upper bound of lights used per draw (picking the brightest from the set, ignoring the rest) and the number of lights in total will be handled internally, without any need for user-specified settings.

Currently without the culling it's both the max number of lights applied per draw and the max number in total, because that's the same thing. I'll update the docs when light culling is a thing.

const auto& phongMaterial =
material->as<Mn::Trade::PhongMaterialData>();
materials[i] = Mn::Shaders::PhongMaterialUniform{}
.setAmbientColor(phongMaterial.diffuseColor() * 0.1f)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ug, what is this 0.1? Are we hard-coding the amount of ambient light?

Copy link
Collaborator Author

@mosra mosra Jan 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes -- see this Slack post for details.

I wanted to achieve an acceptable look until there's a proper PBR pipeline where one doesn't need to hand-tune this kind of thing anymore. Same for specular highlights. The materials are all encoded as PBR, which means there's no info about "classic" specular highlights or ambient anymore, and so those are completely under "artist control", let's say.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about re-using LightSetup.h getAmbientLightColor? If we matched that value, would it be easier to get visual parity with the classic renderer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could look into that, yup -- I remember you spent a considerable amount of time on the PBR -> Phong conversion, so it would be a shame to ignore it.

How should I deal with the original requirement that gfx_batch stays completely isolated from the rest of Habitat? (I was considering putting some PBR -> Phong conversion directly into Magnum, but that time would probably be better spent on actually making PBR work.)

But, re visual parity -- the recent Floorplanner discussion made me think I should maybe try something from scratch instead of trying to match how it looks in the classic renderer 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the original requirement that gfx_batch stays completely isolated from the rest of Habitat?

Yep. So this would be some caller like replayer.cpp passing LightSetup.h getAmbientLightColor to the batch renderer, maybe via a new public method or the Configuration struct.

the recent Floorplanner discussion made me think I should maybe try something from scratch instead of trying to match how it looks in the classic renderer
I don't have the full context here, but I think it'll be valuable going forward to be able to swap between the classic and batch renderer, with Mikael working to reach parity.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, wait, this is about the light ambient color, not the per-material ambient color, I missed that.

So, just to clarify we're on the same page -- the aspect of diffuse / base color used partially for ambient is fine in this case, and you're only concerned about the factor being hardcoded, right? Not about the fact that the same texture is used for both.

Having it configurable from the light makes sense, but it'll be a global factor (i.e., there's no way to have different lights with different ambient factors affecting different meshes, the ambient factors will get all summed up and used as a single value). But that should be fine, I think?

Copy link
Contributor

@eundersander eundersander Jan 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're only concerned about the factor being hardcoded, right?

Yes.

Not about the fact that the same texture is used for both.

No problem here. I expect this.

Somewhat related, I know we've disagreed in the past a bit on this nuance of the Phong lighting model. My preference is for a material's ambient color to generally match its diffuse color, and for that material ambient color to get multiplied with an ambient light color at runtime (every frame), such that you could hypothetically vary the color/amount of ambient light in the scene every frame. Whereas I know you've spoke in the past of preferring to pre-multiply these two and basically bake them into the material's ambient color. I'm too lazy to track down that Slack thread from a year ago, but more specifically, you said that many 3D assets in the wild already do this pre-baking by convention. Anyway, we don't have to debate this now or change the current code in this respect.

Copy link
Contributor

@eundersander eundersander Jan 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having it configurable from the light makes sense, but it'll be a global factor

Exactly, a global factor, not per light. I think of ambient light color as sorta like the cosmic background radiation. It's a constant amount of light shining in all directions, everywhere in the scene. A lazy proxy for e.g. env-map-based light.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nuance of the Phong lighting model

Yes, I remember -- and together with the global ambient factor I think I'm now taking your approach here :) My original reasoning (and it was possibly even more than a year ago) was that there are existing "classically lighted" models that set the ambient color on their own (or even have dedicated ambient textures), and so ignoring that and overriding it with some fraction of diffuse would lead to information loss. But now, and especially with the workflow here being that the composite files are changed to use PBR materials only, most models is made with the PBR workflow in the first place, so there isn't any "ambient color" info present anymore and so overriding ambient this way won't lead to any information loss.


In b447462 I added a function to change the ambient factor to not have it hardcoded to 0.1. But due to the way materials are set up, it's currently only possible to change it upfront, not later at runtime from the gfx-replay file. OTOH, the getAmbientLightColor() had it hardcoded too, so not really a runtime value either -- I added a TODO.

materials[i] = Mn::Shaders::PhongMaterialUniform{}
.setAmbientColor(phongMaterial.diffuseColor() * 0.1f)
.setDiffuseColor(phongMaterial.diffuseColor())
/* Specular not used (and shader compiled with NoSpecular). Much
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this better? Is the problem that our existing assets have too much spec on their materials? Or is the problem that our point lighting doesn't produce nice-looking highlights?

Copy link
Collaborator Author

@mosra mosra Jan 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in case of the ambient, the PBR materials in glTF don't encode any info about "classic" specular so the renderer has to guesstimate and it usually guesses wrong.

From my experience the specular "white spots" were what usually people complained about when seeing the renderer output (such as the recent Floorplanner discussion on Slack), and removing them makes the lighting look significantly less "everything is made out of shiny plastic".

shaderFlags |= Mn::Shaders::PhongGL::Flag::AmbientTexture |
Mn::Shaders::PhongGL::Flag::TextureArrays |
Mn::Shaders::PhongGL::Flag::TextureTransformation;
/* Enable also a diffuse texture if we're rendering with lights */
if (state_->maxLightCount)
shaderFlags |= Mn::Shaders::PhongGL::Flag::DiffuseTexture;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To sanity-check: a PBR material's baseColor texture would get loaded once and be set as both the ambient texture and diffuse texture for the corresponding Phong material, yes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

.setNormalMatrix(state_->absoluteTransformationsSorted[i]
.transformationMatrix.normalMatrix())
// TODO light culling should happen here
.setLightOffsetCount(0, scene.lights.size());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is maxLightCount enforced?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On line 831, in setLightCount() passed to Shaders::PhongGL::Configuration -- it's a shader-compile-time setting, not a uniform input.

As I said above, this will change slightly when light culling is done -- the compile-time light count will be handled internally, and the max per-draw light count will be limiting the count of lights used per draw, picking just the ones with most influence.

* @brief Add a light
* @param sceneId Scene ID, expected to be less than
* @ref sceneCount()
* @param nodeId Node ID to inherit transformation from, returned
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we support the use case of a gltf mesh hierarchy with a light attached to some subnode? E.g. a lamp asset with a light attached, where the subnode offsets from the base of the lamp to the location of the bulb.

Er I'm guessing we do and it doesn't require an explicit addLight. I'm guessing adding that mesh hierarchy also adds the child light. Which is a whole can of worms we haven't really discussed (adding lights embedded in a gltf: the legacy renderer doesn't support this, right?).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we support the use case of a gltf mesh hierarchy with a light attached to some subnode?

Yes, although not everything is in place yet, that's what I designed for, to make it easier to author scenes with uniform lighting, as it's significantly easier to tune light intensities/ranges in Blender and export a self-contained glTF than supplying them externally via a JSON config, or having to explicitly add also a light every time you add a lamp to the scene.

I want to treat those as being a part of the asset (and thus not something to be explicitly supplied by the simulator apart from a high-level use like "hold a torch" or "add a lamp there"), same as e.g. emissive textures are. And eventually the datasets will contain also lightprobe locations and other light-related data needed by PBR, which would otherwise have to be proxied through Habitat's data pipeline.

adding lights embedded in a gltf: the legacy renderer doesn't support this, right?

I was pushing for loading lights from the glTF several times (because, again, it's significantly easier to author the assets that way), all the scaffolding is in place, but probably nobody did the work to hook it up within habitat. ¯\_(ツ)_/¯

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was pushing for loading lights from the glTF several times

Ok. But please don't go rogue on us! Let's get consensus on this stuff. For the classic renderer, lights come from the JSON metadata. The new batch renderer should work the same way, at least for now. We don't want to introduce confusion where someone is switching between renderers and wondering why extra lights start showing up or disappearing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lights come from the JSON metadata. The new batch renderer should work the same way

It does, it consumes lights from the JSON metadata, via the gfx-replay :) But I see what you mean -- when I get to hooking this up, I'll add a flag to skip lights coming from the input file, similarly to how there's a flag to skip texture.

*
* Returns a view on colors of all lights in given scene. Desired usage is to
* update only colors at indices returned by @ref addLight(), updating colors
* at other indices is possible but could have unintended consequences.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, how do we end up with lights in here other than via addLight?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From lights being implicitly added via addHierarchy(), as discussed above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear: is this fully hooked up? If we add a hierarchy that includes a light, will the light get added?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet -- I didn't find time to actually equip datasets with lights to have something to test with and didn't want to add completely uncovered code to this PR, so it's not here here. The "compositor" in #1969 and the glTF exporter also don't handle those for a similar reason, but both are relatively trivial pieces of code.

Once I have data to test with, I will be definitely bugging everyone on Slack with this feature ;)


const std::size_t nodeId = renderer_.addEmptyNode(sceneId_);

/* Clang Tidy, you're stupid, why do you say that "lightId" is not
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an accidentally zero-initialized variable is also a bug, you know?

wut

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mosra Just add NOLINTNEXTLINE comment then. :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because if the variable was meant to be initialized to something, but by accident wasn't, the zero-initialization would hide that bug, causing the program to behave seemingly fine but incorrect. While not having it initialized at all (kept as a random memory) could possibly lead to a random OOB assertion down the line, or cause the program to behave randomly, making it easier to discover the bug.

At least in my experience it was always like this, bugs caused by random memory were easier to discover than bugs where the variables were zero-initialized by accident.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cause the program to behave randomly, making it easier to discover the bug

Not my experience. Nondeterministic bugs are scary. E.g. by leaving it uninitialized, it gets initialized to zero in debug builds but something random and nonzero in release builds, so the entire EA Sports Nascar 2005 eng team spends the entire afternoon tracking down why only the release build is broken. :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counterpoint to a counterpoint: GCC 12 has -ftrivial-auto-var-init=pattern which initializes local variables in debug builds to some bit pattern. MemorySanitizer can do something similar for the heap as well, and maybe for local variables too.

So if the variable is accidentally left uninitialized, these options can be used to make the bug behave deterministically. But if it gets zero-initialized because Clang Tidy demanded to, and then accidentally not set to the value it should have, it's harder to discover, as it would behave as a "regular" value and won't trigger any assertions or sanitizer reports. In my opinion. But my general opinion is fail fast & fail hard, which is probably something completely undesirable in game development :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this were my code, I'd still prefer to initialize to an aggressively-invalid value like -1 or something, but I don't feel too strongly and you've clearly given this some thought. Fine to leave this as-is!

lightId = renderer_.addLight(sceneId_, nodeId,
gfx_batch::RendererLightType::Point);
} else {
/* The matrix will be partially NaNs if the light vector is in the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well that sounds pretty shady. You're really okay with NaNs floating around here? What if one day we want to assert that all transformations are orthonormal?

In past codebases, I used to have a "getArbitraryOrthogonalVector" and/or "getArbitraryLookatTransform" for this kinda thing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with that, yes. I view it as the NaNs signifying that the X/Y basis is unimportant and only Z matters, and it works as expected even in presence of matrix transformations -- NaNs stay where they should stay:

>>> Matrix4.rotation_x(Deg(30.0))@\
    Matrix4.look_at(Vector3(), Vector3.y_axis(), Vector3.y_axis())
Matrix(-nan, -nan, 0, 0,
       -nan, -nan, -0.866025, 0,
       -nan, -nan, -0.5, 0,
       -nan, -nan, 0, 1)

If it eventually causes asserts for whatever reason, I can fix those, but even orthonormal transforms are sometimes impossible such as in this case of meshes flattened along a certain axis. And inventing an always-orthogonal transformation just to avoid NaNs would only mean having extra code to test and it'd be all discarded in the end anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some bold ideas from Mosra today, haha. Another counterpoint: what if someone tries to debug-render all the node coordinate frames as red/green/blue axes or whatever? Now you have NaNs in vertices you're submitting to the driver.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's right -- a mesh transformed by this matrix would be all NaNs. A visualizer using the base axes directly wouldn't tho, it would show just the blue axis, which is maybe what is meant to be visualized?

I'm taking it too far here, am I :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would show just the blue axis, which is maybe what is meant to be visualized?

Ohhhh, that's going to be a mystery rabbit hole for whatever poor junior SWE attempts this visualization.

Speaking of rabbit holes, I don't like dragging out these little code review nits, and I don't want to get into a mode of demanding changes, so I'll ask you to make the final decision here!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a TODO -- once I have a convenience utility for this in Magnum, I change to it instead of lookAt().

Copy link
Contributor

@eundersander eundersander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you have more changes in progress related to my recent comments, but I feel like our threads are resolved at least philosophically, so I'm giving the green check!

range asserts in all functions so your "suggestions" are completely
unhelpful. */
std::size_t lightId; // NOLINT
if (light.vector.z()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to look at the w component of the light vector to determine its type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0mdc I am not following, is this flagging a bug as it is currently written?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, whoops -- yep, that's a bug. Fixing...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3906aaa.

In a preparation for the hierarchy to contain more than just meshes --
such as lights.

What is *very amazing* about this change is that it's the exact same
number of characters as before, so Clang Format has nothing to wildly
rearrange here.
Due to how materials are implemented right now, this can't be easily
changed at runtime, thus it's not propagated from the replay file.
@mosra mosra merged commit 4ed8d04 into main Jan 10, 2023
@mosra mosra deleted the let-there-be-light branch January 10, 2023 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Do not delete this pull request or issue due to inactivity.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants