Skip to content

Conversation

@doodlum
Copy link
Owner

@doodlum doodlum commented Oct 20, 2025

image image

Summary by CodeRabbit

  • New Features
    • Enhanced particle lighting with per-fragment lighting accumulation for richer visuals.
    • Added clustered light sampling and optional inverse-square lighting for more accurate illumination.
    • Improved light attenuation and shadow-aware contributions for more realistic particle appearance.
    • Particles now incorporate directional and ambient lighting data for consistent scene integration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 20, 2025

Walkthrough

Added per-fragment lighting to the particle shader: vertex shader now outputs view-space position, and pixel shader accumulates directional, ambient, and clustered light contributions with conditional attenuation and compile-time includes.

Changes

Cohort / File(s) Summary
Particle shader
package/Shaders/Particle.hlsl
Added float3 ViewPositionVS : TEXCOORD2 to VS_OUTPUT. Vertex shader propagates view-space position. Pixel shader replaced single ColorScale path with per-fragment lighting: reads directional & ambient from SharedData.hlsli, computes world position, conditionally samples clustered lights via LightLimitFix.hlsli and applies attenuation via InverseSquareLighting.hlsli when enabled. Added conditional include directives and light accumulation loop.

Sequence Diagram

sequenceDiagram
    participant VS as Vertex Shader
    participant PS as Pixel Shader
    participant Cluster as Light Cluster (LightLimitFix)
    participant Math as Lighting Math

    VS->>VS: compute finalViewPosition
    VS->>VS: vsout.ViewPosition = finalViewPosition.xyz
    VS-->>PS: emit VS_OUTPUT (with ViewPositionVS)

    PS->>PS: sample baseColor
    PS->>PS: read directional & ambient (SharedData)
    PS->>PS: propertyColor = dir+ambient
    PS->>PS: worldPos = ViewToWorld * ViewPositionVS

    rect rgb(235,245,255)
    Note over PS,Cluster: if LIGHT_LIMIT_FIX defined
    PS->>Cluster: fetch lights for fragment/cluster
    Cluster-->>PS: list of lights
    end

    loop for each light
        PS->>Math: compute lightDir, distance
        alt ISL enabled
            Math-->>PS: inverse-square attenuation
        else
            Math-->>PS: quadratic falloff attenuation
        end
        PS->>PS: accumulate propertyColor += lightContribution
    end

    PS->>PS: finalColor = propertyColor * baseColor
    PS-->>PS: output final fragment color
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nudged view pos into the vertex stream,
Collected suns within each tiny gleam,
Direction, ambient, clusters in a swirl,
Each particle now catches light and twirls,
Shader hops brighter — a luminous dream.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly conveys the primary change of this pull request, which is adding lighting support specifically for rain and snow particles in the shader. It aligns with the code modifications that introduce per-fragment lighting, directional and ambient contributions, and cluster light integration for particle effects. Therefore, it accurately captures the main purpose of the changes without unnecessary detail.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch particle-lighting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Using provided base ref: 79aebb6
Using base ref: 79aebb6
Base commit date: 2025-10-18T22:52:09+01:00 (Saturday, October 18, 2025 10:52 PM)
No actionable suggestions for changed features.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
package/Shaders/Particle.hlsl (1)

299-331: Clustered light loop is OK; confirm shadow flag semantics and add safety for zero radius.

  • Skipping lights with LightFlags::Shadow may drop all shadow‑casting lights. Confirm intent.
  • Consider guarding against zero/denormal radius to avoid NaNs.

Apply:

-                float intensityFactor = saturate(lightDist / light.radius);
+                float safeRadius      = max(light.radius, 1e-3);
+                float intensityFactor = saturate(lightDist / safeRadius);
                 float intensityMultiplier = 1 - intensityFactor * intensityFactor;

If shadow flag means “shadow‑maps only,” keep skip; otherwise remove the check and rely on IsLightIgnored.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79aebb6 and 311fa6e.

📒 Files selected for processing (1)
  • package/Shaders/Particle.hlsl (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{cpp,cxx,cc,c,h,hpp,hxx,hlsl,hlsli,fx,fxh,py}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not include TODO/FIXME placeholders; provide complete, working solutions

Files:

  • package/Shaders/Particle.hlsl
🧠 Learnings (1)
📓 Common learnings
Learnt from: alandtse
PR: doodlum/skyrim-community-shaders#0
File: :0-0
Timestamp: 2025-06-24T07:17:36.604Z
Learning: When reviewing PRs, always clarify the scope if there are multiple related features or dependencies. WeatherPicker was a separate PR that was already merged, while this PR focuses specifically on WetnessEffects climate preset system enhancements.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Validate shader compilation (VR, .github/configs/shader-validation-vr.yaml)
  • GitHub Check: Validate shader compilation (Flatrim, .github/configs/shader-validation.yaml)
  • GitHub Check: Build plugin and addons
🔇 Additional comments (3)
package/Shaders/Particle.hlsl (3)

3-11: Includes look fine; verify build flags across all permutations.

Conditional includes are correct. Please confirm the shader compiles in these permutations: plain, LIGHT_LIMIT_FIX only, ISL+LIGHT_LIMIT_FIX, and VR+ENVCUBE combos.

Would you run your shader CI/build for all macro sets used by the project and share failures, if any?


288-295: Minor: tighten ambient computation types and clamping.

Ensure consistent vector dimensions and clamp to [0,1] to avoid negative ambient.
[ suggest_recommended_refactor ]
Apply:

-    float3 propertyColor = 0.0;
-    float3 dirLightColor = SharedData::DirLightColor.xyz * 0.5;
-    float3 ambientColor = max(0, mul(SharedData::DirectionalAmbient, float4(0, 0, 1, 1)));
+    float3 propertyColor = 0.0.xxx;
+    float3 dirLightColor = SharedData::DirLightColor.xyz * 0.5;
+    float3 ambientColor  = saturate(mul(SharedData::DirectionalAmbient, float4(0, 0, 1, 1)).xyz);

Optional: consider exposing the 0.5 scalars as tunables.


296-299: Incomplete renaming: Input struct definition still uses ViewPosition, not ViewPositionVS.

The current code at line 296–297 is functionally correct—both FrameBuffer::ViewToWorld() and FrameBuffer::ViewToUV() correctly expect view-space input. However, the proposed diff to rename input.ViewPosition to input.ViewPositionVS will not compile without also updating the input struct definition at line 44, which still declares float3 ViewPosition : POSITION0;.

Either:

  1. The input struct renaming was already applied elsewhere in this PR (requires verification), or
  2. The struct definition change must be included: change line 44 from float3 ViewPosition : POSITION0; to float3 ViewPositionVS : POSITION0;

Additionally, update all three usages:

  • Line 296: input.ViewPosition.xyzinput.ViewPositionVS
  • Line 302: FrameBuffer::ViewToUV(viewPosition, ...)FrameBuffer::ViewToUV(viewPositionVS, ...)
  • Line 305: viewPosition.zviewPositionVS.z

Comment on lines +124 to +125
vsout.ViewPosition = finalViewPosition.xyz;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Wrong space passed to PS; provide view‑space, not clip‑space. Also not set in all paths.

You assign clip‑space (post‑projection) to ViewPosition, then PS treats it as view‑space. Provide view‑space and write it in both ENVCUBE and non‑ENVCUBE paths.

Apply (paired with the TEXCOORD rename):

-    vsout.ViewPosition = finalViewPosition.xyz;
+    // Compute view-space for precipitation motion blend
+#if defined(ENVCUBE)
+#  if defined(RAIN)
+    float3 viewVS             = mul(WorldView[eyeIndex], msPosition).xyz;
+    float3 adjustedViewVS     = mul(WorldView[eyeIndex], adjustedMsPosition).xyz;
+    float3 finalViewPositionVS = lerp(adjustedViewVS, viewVS, positionBlendParam);
+    vsout.ViewPositionVS = finalViewPositionVS;
+#  else
+    vsout.ViewPositionVS = mul(WorldView[eyeIndex], msPosition).xyz;
+#  endif
+#else
+    vsout.ViewPositionVS = mul(WorldView[eyeIndex], msPosition).xyz;
+#endif

Note: This also fixes the missing assignment when ENVCUBE is not defined.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vsout.ViewPosition = finalViewPosition.xyz;
// Compute view-space for precipitation motion blend
#if defined(ENVCUBE)
# if defined(RAIN)
float3 viewVS = mul(WorldView[eyeIndex], msPosition).xyz;
float3 adjustedViewVS = mul(WorldView[eyeIndex], adjustedMsPosition).xyz;
float3 finalViewPositionVS = lerp(adjustedViewVS, viewVS, positionBlendParam);
vsout.ViewPositionVS = finalViewPositionVS;
# else
vsout.ViewPositionVS = mul(WorldView[eyeIndex], msPosition).xyz;
# endif
#else
vsout.ViewPositionVS = mul(WorldView[eyeIndex], msPosition).xyz;
#endif

doodlum and others added 2 commits October 22, 2025 22:14
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
package/Shaders/Particle.hlsl (2)

124-125: Wrong space + wrong field name; also not written in non-ENVCUBE.

You’re assigning clip-space xyz to a field named ViewPosition (which doesn’t exist anymore) and never writing the view-space varying in the non-ENVCUBE path. Provide view-space and write ViewPositionVS in all VS paths.

Apply in ENVCUBE block:

-    vsout.ViewPosition = finalViewPosition.xyz;
+    // Provide view-space position for PS lighting
+#if defined(RAIN)
+    float3 viewVS         = mul(WorldView[eyeIndex], msPosition).xyz;
+    float3 adjustedViewVS = mul(WorldView[eyeIndex], adjustedMsPosition).xyz;
+    float3 finalViewVS    = lerp(adjustedViewVS, viewVS, positionBlendParam);
+    vsout.ViewPositionVS  = finalViewVS;
+#else
+    vsout.ViewPositionVS  = mul(WorldView[eyeIndex], msPosition).xyz;
+#endif

Also add in the non-ENVCUBE path near where you set viewPosition/Position (after Line 176):

   vsout.Position.zw = viewPosition.zw;
+  // Provide view-space position for PS lighting
+  vsout.ViewPositionVS = mul(WorldView[eyeIndex], msPosition).xyz;

296-303: PS reads the wrong field and wrong space. Use ViewPositionVS consistently.

Consume the TEXCOORD varying you added and keep naming consistent across all uses.

-    float3 viewPosition = input.ViewPosition.xyz;
-    float3 worldPosition = FrameBuffer::ViewToWorld(viewPosition);
+    float3 viewPositionVS = input.ViewPositionVS;
+    float3 worldPosition  = FrameBuffer::ViewToWorld(viewPositionVS);
@@
-        float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex);
+        float2 screenUV = FrameBuffer::ViewToUV(viewPositionVS, true, eyeIndex);

Run to confirm no stale uses remain:

#!/bin/bash
rg -nP -C2 '(\bvsout|input)\.ViewPosition\b' package/Shaders/Particle.hlsl
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 311fa6e and 5a2f44e.

📒 Files selected for processing (1)
  • package/Shaders/Particle.hlsl (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{cpp,cxx,cc,c,h,hpp,hxx,hlsl,hlsli,fx,fxh,py}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not include TODO/FIXME placeholders; provide complete, working solutions

Files:

  • package/Shaders/Particle.hlsl
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Validate shader compilation (VR, .github/configs/shader-validation-vr.yaml)
  • GitHub Check: Build plugin and addons
  • GitHub Check: Validate shader compilation (Flatrim, .github/configs/shader-validation.yaml)
🔇 Additional comments (2)
package/Shaders/Particle.hlsl (2)

44-45: Good: TEXCOORD varying for PS.

Using TEXCOORD2 for view-space transfer is correct and PS-friendly.


332-334: Final color combine looks correct.

RGB multiply and alpha passthrough align with prior feedback.

Comment on lines +291 to +292
float3 ambientColor = max(0, mul(SharedData::DirectionalAmbient, float4(0, 0, 1, 1)));

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid implicit float4→float3 truncation in ambient.

Make the type explicit and clamp only the RGB you use.

-    float3 ambientColor = max(0, mul(SharedData::DirectionalAmbient, float4(0, 0, 1, 1)));
+    float3 ambientColor = max(0.0.xxx, mul(SharedData::DirectionalAmbient, float4(0, 0, 1, 1)).xyz);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In package/Shaders/Particle.hlsl around lines 291-292, the current expression
uses an implicit float4→float3 truncation when computing ambientColor; replace
that implicit truncation by extracting the .xyz from the mul result and perform
the clamp/max on a float3 explicitly (e.g. use mul(...).xyz and clamp or max
with a float3(minR,minG,minB) so only RGB are clamped and types match).

Comment on lines +321 to +326
float intensityFactor = saturate(lightDist / light.radius);
float intensityMultiplier = 1 - intensityFactor * intensityFactor;
# endif

float3 lightColor = light.color.xyz * intensityMultiplier * 0.5;
propertyColor += lightColor;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Division by zero when light.radius == 0.

Guard the denominator to avoid INF/NaN in clustered lights.

-                float intensityFactor = saturate(lightDist / light.radius);
-                float intensityMultiplier = 1 - intensityFactor * intensityFactor;
+                float radiusSafe = max(light.radius, 1e-3);
+                float intensityFactor = saturate(lightDist / radiusSafe);
+                float intensityMultiplier = 1.0 - intensityFactor * intensityFactor;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
float intensityFactor = saturate(lightDist / light.radius);
float intensityMultiplier = 1 - intensityFactor * intensityFactor;
# endif
float3 lightColor = light.color.xyz * intensityMultiplier * 0.5;
propertyColor += lightColor;
float radiusSafe = max(light.radius, 1e-3);
float intensityFactor = saturate(lightDist / radiusSafe);
float intensityMultiplier = 1.0 - intensityFactor * intensityFactor;
# endif
float3 lightColor = light.color.xyz * intensityMultiplier * 0.5;
propertyColor += lightColor;
🤖 Prompt for AI Agents
In package/Shaders/Particle.hlsl around lines 321 to 326, the computation float
intensityFactor = saturate(lightDist / light.radius) can divide by zero when
light.radius == 0; change the division to use a guarded denominator (e.g. use
max(light.radius, a small epsilon like 1e-6) or branch on radius > 0) so the
divisor is never zero, then compute intensityMultiplier and subsequent color
using that safe intensityFactor.

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