Create partial reference implementation for BENTLEY_materials_planar_fill (behind)#13178
Create partial reference implementation for BENTLEY_materials_planar_fill (behind)#13178markschlosseratbentley wants to merge 33 commits into
Conversation
|
Thank you for the pull request, @markschlosseratbentley! ✅ We can confirm we have a CLA on file for you. |
|
Great work! I did a quick review of the glTF loading and the feature ID comparison logic. The approach of introducing a new pass and comparing feature IDs to determine what’s behind makes sense to me. I haven’t run it locally yet, but the overall idea and implementation look solid and follow a similar path to how edge visibility was handled. However, While testing with some cases I generated from the tileset publisher encoding work (PR: https://github.com/iTwin/imodel-native-internal/pull/950), I noticed the results weren’t behaving as expected. The results still didn’t match the rendering in the iTwinJS PR (iTwin/itwinjs-core#8906). Note: I temporarily commented out the edge visibility code locally since support for “line string” hasn’t been merged yet. For example, the “behind” flag in following test cases do not appear to be applied correctly: But in the current branch in CesiumJS, both files appear as if the “behind” flag is not being read at all. I have a question about the yellow color in this example. If both flags are enabled, my understanding is that it should either render as the black background or appear behind the other two colors. But it doesn’t seem to match either case here(it's rendering yellow and z fighting (meaning it's not behind anything)). There’s also a purple test case with the same flags enabled compare with the yellow color test case I mentioned. The only difference is wireframe mode (which maybe shouldn’t affect the result), but the output looks different. The purple I highlighted isn’t visible—possibly rendering as black instead? Am I misunderstanding how this is supposed to work? Additionally, I’ve verified that the tileset publisher encoding behaves as expected in iTwinJS for planar fill. It might be a good idea to also use this renderer to validate the tileset publisher encoding, so we can confirm that both the tileset publisher and this implementation are working correctly. We can also add more test cases if needed. |
mzschwartz5
left a comment
There was a problem hiding this comment.
Hey @markschlosseratbentley, looking good to me. Most of my comments are code style nits or questions. Functionally, seems alright. I have more reading and testing to do but shipping this round of comments in the mean time.
| defined(primitive.featureIds) && primitive.featureIds.length > 0; | ||
|
|
||
| if (hasFeatureIds) { | ||
| const featureIdMember = primitive.featureIds[0].positionalLabel; |
There was a problem hiding this comment.
Is there something in the planar fill spec that guarantees a primitive has one (and only one) feature ID?
There was a problem hiding this comment.
The spec does not guarantee a single feature ID set. The spec intentionally leaves "belonging to the same logical object" implementation-defined (for Bentley/iTwin it means "originating from the same element").
This implementation defines it as the primitive's first feature ID set, which holds for Bentley-produced tiles; primitives with no feature IDs simply don't participate in the behind test. I've now documented this assumption explicitly in the new processPlanarFill() docs and at the featureIds[0] access.
| const hasFeatureIds = | ||
| defined(primitive.featureIds) && primitive.featureIds.length > 0; | ||
|
|
||
| if (hasFeatureIds) { | ||
| const featureIdMember = primitive.featureIds[0].positionalLabel; | ||
| shaderBuilder.addDefine( | ||
| "HAS_PLANAR_FILL_BEHIND", | ||
| undefined, | ||
| ShaderDestination.FRAGMENT, | ||
| ); | ||
| shaderBuilder.addDefine( | ||
| "PLANAR_FILL_FEATURE_ID", | ||
| featureIdMember, | ||
| ShaderDestination.FRAGMENT, | ||
| ); |
There was a problem hiding this comment.
Since this logic is needed in both branches, it can be hoisted above both.
There was a problem hiding this comment.
Hoisted.
| // Mark this primitive for the pre-pass derived command. | ||
| renderResources.planarFillIdPass = true; |
There was a problem hiding this comment.
Not sure I understand why only this primitives not marked "hasBehind" get this flag set to true. Your comment above implies to me that both behind and non-behind planar-fill geometry participate in the feature-ID pre-pass.
Or is it only non-behind geometry that populates the feature ID texture? (I suppose that could make sense... "behind" geometry only cares about / needs to check "non-behind" geometry.)
There was a problem hiding this comment.
Non-behind fills write the ID texture in the pre-pass; behind fills only read it. I've reworded the comments to say READ/WRITE explicitly.
| uniformMap.u_isPlanarFillIdPass = function () { | ||
| return false; | ||
| }; |
There was a problem hiding this comment.
A comment here that this is overridden by the derived command could be helpful
There was a problem hiding this comment.
Added a comment noting the default returns false for the main command and that derivePlanarFillIdCommand overrides it to return true.
| const planarFill = new PlanarFill(); | ||
| planarFill.wireframeFill = wireframeFill; | ||
| planarFill.backgroundFill = backgroundFill; | ||
| planarFill.behind = behind; |
There was a problem hiding this comment.
Any reason for not just giving PlanarFill a constructor that takes and deconstructs a PlanarFillInfo object?
side note- could we make a jsdoc typedef for the input object here?
There was a problem hiding this comment.
Yep, that's a good suggestion and cleans it up a lot. Done.
PlanarFill now has a constructor that takes and destructures the extension JSON and has @param docs.
| * and non-behind fills — but NOT behind other objects. | ||
| * | ||
| * Layout: | ||
| * Color attachment 0 – RGBA: R = encoded feature ID (float), GBA reserved. |
There was a problem hiding this comment.
What are GBA reserved for? Could we use not just use an attachment with a single channel (R) to save memory?
There was a problem hiding this comment.
GBA was a conceptual reservation during development. It ended up we didn't need them in this implementation. Good callout. I moved this to a single-channel R attachment. All seems to work correctly still.
| // indicates we can render to float textures, not just create them. | ||
| // Fall back to UNSIGNED_BYTE if neither is supported. | ||
| let pixelDatatype; | ||
| if (context.colorBufferFloat) { |
There was a problem hiding this comment.
I wonder if we really have to check for support here. I think we decided recently that new features don't need to support webgl1, and in webgl2 this extension is established in all browsers we support.
There was a problem hiding this comment.
Also- why not use an UNSIGNED_INT data type? Does it have to do with the fact that the same shader doubles for both feature IDs and the fill?
There was a problem hiding this comment.
Thanks for the pointer about not needing webgl1 support.
I went ahead and dropped the HALF_FLOAT branch. I did keep a minimal context.colorBufferFloat ? FLOAT : UNSIGNED_BYTE guard, because tests could possibly not advertise the extension.
Open to changing this further.
There was a problem hiding this comment.
RE: UNSIGNED_INT: integer output would require an integer output declaration. Due to shader program sharing, we keep the vec4 output here (float is fine range for representing IDs).
| // ────────────────────────────────────────────────────────────────────── | ||
| // BENTLEY_materials_planar_fill constants | ||
| // ────────────────────────────────────────────────────────────────────── | ||
| // Depth pull factor (0.05% toward camera) for all planar fills. |
There was a problem hiding this comment.
Did you mean to write 5% instead of 0.05% (and same for 2%)?
Also, I think this comment is a little misleading. I think it needs to call out that the depth pull/push occur in log space, after czm_writeLogDepth. This 5% pull is 5% in log space, which is a variable amount in eye space depending on the fragment's distance from the camera. (Which may be the intended effect)
There was a problem hiding this comment.
The percentages are actually correct as written: 1 − 0.9995 = 0.0005 = 0.05%, and 1.0002 − 1 = 0.02%. But your second point about log space is fair. It's intended behavior. The comment block now spells all of this out.
| #ifdef HAS_PLANAR_FILL_BEHIND | ||
| { | ||
| vec2 screenCoord = gl_FragCoord.xy / czm_viewport.zw; | ||
| float storedEncoded = texture(czm_planarFillIdTexture, screenCoord).r; | ||
| float storedFeatureId = storedEncoded - FEATURE_ID_OFFSET; | ||
| float myFeatureId = float(featureIds.PLANAR_FILL_FEATURE_ID); | ||
|
|
||
| // storedFeatureId < 0 means "no planar fill at this pixel". | ||
| if (storedFeatureId >= 0.0 && abs(storedFeatureId - myFeatureId) < FEATURE_ID_TOLERANCE) { | ||
| // Proportional push: multiply by >1 to move away from camera. | ||
| // Net effect: PLANAR_DEPTH_PULL * BEHIND_DEPTH_PUSH ≈ 0.9997, | ||
| // still in front of non-planar but behind same-feature non-behind fills. | ||
| gl_FragDepth *= BEHIND_DEPTH_PUSH; | ||
| } | ||
| } |
There was a problem hiding this comment.
I need to think about this more... generally speaking, I've encountered numerous issues (many of which in my work with billboards) when trying to fudge depth values. It's very prone to sneaky edge cases.
With billboards, there were some issues I never solved and ultimately concluded a more robust solution would involve a separate render pass entirely...
There was a problem hiding this comment.
Fair concern.
In the end, it's the same family of technique as the u_polygonOffset built into czm_writeLogDepth. Also, a similar proportional depth tolerance is employed in the edge visibility system. So there is precedent, which I was leaning on.
Also worth noting: the spec's "planar renders in front of non-planar at the same surface" semantics require some depth override regardless; a separate pass would still need the feature-ID comparison and a bias or depth-test trick inside it, so it isolates the problem more than it removes it (unless we sort things....)
That said, if some blocker artifact turns up, we could eventually move this into a dedicated pass and try to figure something out. Most of the scaffolding here could carry over.
|
Thanks @mzschwartz5 - I'll be going through your feedback. |





Description
This PR adds support for the
BENTLEY_materials_planar_fillglTF extension, enabling CAD-style planar polygon fill rendering with proper depth sorting and configurable fill behavior including background color masking and coplanar geometry ordering. Note: ThewireframeFillproperty is currently a no-op as CesiumJS does not yet have a proper wireframe rendering mode. See this wireframe rendering mode draft PR for a possible way forward.Issue number and link
Partially fixes #12890
Testing plan
Author checklist
CONTRIBUTORS.mdCHANGES.mdwith a short summary of my change