Skip to content

[Rendering] feat: a per-view flag tells a material it is drawn for a shadow map - #3387

Open
Nicogo1705 wants to merge 1 commit into
stride3d:masterfrom
Nicogo1705:rendering-fixes
Open

Nicogo1705 wants to merge 1 commit into
stride3d:masterfrom
Nicogo1705:rendering-fixes

Conversation

@Nicogo1705

@Nicogo1705 Nicogo1705 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

PR Details

A material whose surface is not its mesh (a ray-marched volume, a parallax search) needs to know when it is drawn for a shadow map, to settle for a depth good enough for a shadow and skip the rest. Nothing told it.

  • ShadowMapCasterPassInfo carries ShadowMapViewFlag in a PerView constant buffer: 1 in a shadow map view, 0 in every other one. A material mixes it in and reads the flag in the pixel stage.
  • The value is the view's, not a constant: the caster effect also runs the camera's depth-only prepass, where such a material must not write its shadow depth. ShadowCasterRenderFeature writes the flag for every view that has the group, and skips a layout the view draws nothing through, which has no resources prepared for it.

Reworked after review: no stream, no marker shader, no effect mixins, one loop over the view layouts. The bright pass clamp that was here moved to #3429.

Verified on Direct3D 11 with the voxel GI demo: the grid's shadow material casts in the shadow views, draws nothing in the camera prepass and the voxelization views.

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

⚠️ Written with AI assistance (Claude Code), driven and verified by me; needs a real review. 🤖 Generated with Claude Code

@Ethereal77 Ethereal77 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe better as two separate PRs, but anyway:

  1. The optimization I wrote about the clamping I think should be better in theory. Better to test its correctness and performance, of course. min + max should be branchless while also not falling into the previous infinite/NaN problem.

  2. Nice idea the shadow caster pass identification!
    I would have preferred however if this were called something like DepthOnlyPass or something alike. It's weird (even if logically correct) that the depth-prepass invokes a shadow-caster pass. But maybe renaming those are a bigger change not worth the effort.

Comment thread sources/engine/Stride.Rendering/Rendering/Shadows/ShadowMapCasterPassInfo.sdsl Outdated
@xen2

xen2 commented Sep 8, 2026

Copy link
Copy Markdown
Member

@Ethereal77
Sorry for the grouped PR, @Nicogo1705 asked me before submitting and I approved his proposed grouping.
But in retrospective, I agree it might have been better to split them further, for all the independent parts.
I think there is a large pass to do on the comments for all his PRs (too long/detailed, cf #3388 (comment)), then maybe @Nicogo1705 can reopen them with a bit more fine-grained PRs?

@Nicogo1705

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

  1. Branchless clamp applied in both shaders, see above.
  2. On the name: I agree it reads oddly that the depth prepass runs a "shadow caster" effect. But the stream answers "am I in a shadow map view" and is 0 in the camera's depth prepass, so IsShadowMapCasterPass is the accurate name for what it carries.
  3. Fair point on splitting, I will go one fix per PR from now on.

@Nicogo1705

Copy link
Copy Markdown
Contributor Author

Comments trimmed on this PR and on #3388 / #3389: 1 to 2 lines, no more references to the previous behaviour. If you would rather have these two fixes as separate PRs, I can reopen them that way, just say so.

@Ethereal77

Copy link
Copy Markdown
Contributor

Hey, no need to apologize. I'm not opposed to grouping if the changes are small. I understand it's more convenient sometimes to dump several changes into a single PR than open several. I just like that PRs sometimes serve as a kind of auto-documentation on how the software evolved and why some changes were needed, and combining several changes into one makes each change less discoverable and sometimes less understandable (context switching overhead 😄). Just my opinion, of course

@Ethereal77

Copy link
Copy Markdown
Contributor

On the name: I agree it reads oddly that the depth prepass runs a "shadow caster" effect. But the stream answers "am I in a shadow map view" and is 0 in the camera's depth prepass, so IsShadowMapCasterPass is the accurate name for what it carries.

Oh, now I see what you mean. I have misread the PR and thought you have also rewritten the depth-prepass to use this and this just served to compute the depth-only output (hence my comment). But I see now that the depth-prepass was already using the shadow caster effect. Sorry for the confusion

Comment thread sources/engine/Stride.Rendering/Rendering/Images/HalfPrecisionUtils.sdsl Outdated
Comment thread sources/engine/Stride.Rendering/Rendering/Shadows/ShadowMapCasterPassInfo.sdsl Outdated
ShadowMapCasterPassInfo carries ShadowMapViewFlag in a PerView constant buffer: 1 in a shadow
map view, 0 in every other one. A material whose surface is computed in the pixel shader (a
ray-marched volume, a parallax search) reads it to settle for a depth good enough for a shadow
and skip the rest.

The value is the view's, not a constant: the caster effect also runs the camera's depth-only
prepass, where such a material must not write its shadow depth. ShadowCasterRenderFeature
writes the flag for every view that has the group, skipping a layout the view draws nothing
through, which has no resources prepared for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Nicogo1705 Nicogo1705 changed the title Rendering: a shadow-caster stream for materials, and a bright pass that survives NaN [Rendering] feat: a per-view flag tells a material it is drawn for a shadow map Sep 13, 2026
@Nicogo1705

Copy link
Copy Markdown
Contributor Author

Both fixes were reworked after review, so this is the moment to split as suggested: this PR keeps the shadow map flag, the bright pass clamp moved to #3429. Rebased on master, one commit each. Verified on the voxel GI demo on Direct3D 11: shadows, camera prepass and voxelization views.

}
// A layout the view draws nothing through has no resources prepared for it.
var resourceGroup = viewLayout.Entries[view.Index].Resources;
if (resourceGroup == null || resourceGroup.ConstantBuffer.Data == System.IntPtr.Zero)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"A layout the view draws nothing through has no resources prepared for it."
Not sure I get that.

resourceGroup should not be null (always created in RootEffectRenderFeatures).
I think ConstantBuffer.Data should always exists.
Data == Zero only happens for a layout without a PerView cbuffer. Such a layout has neither logical group, so without the guard both Hash == ObjectId.Empty checks would skip it before anything reads ConstantBuffer.Data.

Did you hit a crash without it? If yes, can you share the repro? That would be a different bug. If not, let's drop the guard.

cbuffer PerView.ShadowMapView
{
/// 1 in a shadow map view, 0 in every other view, the camera's depth-only prepass included.
stage float ShadowMapViewFlag;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was wondering, if no ShadowCasterRenderFeature, this viewflag is basically undefined memory?
Should we accept that and add to doc, or do something for this case (i.e. always default to 0 in MaterialRenderFeature)?

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.

3 participants