perf(lighting): shader-based light bubbles + organize into lighting/ - #121
perf(lighting): shader-based light bubbles + organize into lighting/#121santi1584 wants to merge 4 commits into
Conversation
Replace the 256x256 canvas-rendered light mask with a custom fragment
shader that computes the radial gradient per fragment. Eliminates the
mask's VRAM footprint, makes the gradient look smooth at any scale
(no banding when a torch is scaled up), and plumbs a uTime uniform so
a future flicker pass can land without CPU work.
Approach: each light is a Mesh with a shared unit-quad geometry and a
per-instance Shader carrying its own `lightUniforms` (uTint, uTime,
uCoreSize). The vertex shader uses Pixi's auto-assigned `globalUniforms`
(group 0) + `localUniforms` (group 1) for the mvp transform; group 2 is
ours. Both WebGL (GLSL 300 es) and WebGPU (WGSL) variants ship.
Falloff: clamp(1 - (r - core)/(1 - core), 0, 1)^2 — a small flat core
(0.1 of radius) like the canvas mask had, then a smooth quadratic ramp.
Also folds in two memory wins discussed last PR:
- Pool the overlay Sprite (was newly allocated per rebuild). Caller now
owns it; we detach it before tileContainer.destroy so it survives.
- Drop createLightMaskTexture + lightMask plumbing entirely.
Reorganized lib/lighting.ts into src/lib/lighting/{index.ts,shader.ts}
to match the lib/render/ pattern as the lighting code grows.
All 359 tests pass. Build succeeds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 17 minutes and 57 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
otclient-web | a944827 | Commit Preview URL Branch Preview URL |
Jun 10 2026, 04:24 PM |
There was a problem hiding this comment.
Code Review
This pull request replaces the sprite-based lighting system with a more efficient shader-based mesh system using custom GLSL and WGSL shaders. It introduces a LightMeshPool to recycle meshes and a new shader-based approach for rendering light bubbles with quadratic falloff. A critical issue was identified in the WGSL fragment shader where a missing closing brace would cause compilation errors on WebGPU-enabled browsers.
| fn mainFrag(input: VSOut) -> @location(0) vec4<f32> { | ||
| let d = input.vUV - vec2<f32>(0.5); | ||
| let r = length(d) * 2.0; | ||
| let t = clamp(1.0 - (r - lightUniforms.uCoreSize) / (1.0 - lightUniforms.uCoreSize), 0.0, 1.0); | ||
| let intensity = t * t; | ||
| return vec4<f32>(lightUniforms.uTint * intensity, intensity); |
There was a problem hiding this comment.
The WGSL fragment shader mainFrag is missing a closing brace }. This will cause a shader compilation error on WebGPU-enabled browsers.
Additionally, for consistency with the GLSL fragment shader, it would be beneficial to include the comment explaining that uTime is plumbed for future use. This helps other developers understand why the uniform is present but unused.
fn mainFrag(input: VSOut) -> @location(0) vec4<f32> {
let d = input.vUV - vec2<f32>(0.5);
let r = length(d) * 2.0;
let t = clamp(1.0 - (r - lightUniforms.uCoreSize) / (1.0 - lightUniforms.uCoreSize), 0.0, 1.0);
let intensity = t * t;
// uTime is plumbed but unused — reserved for a future torch flicker pass
// (e.g. intensity *= 1.0 + 0.05 * sin(uTime * 6.0)) without CPU work.
return vec4<f32>(lightUniforms.uTint * intensity, intensity);
}There was a problem hiding this comment.
@gemini-code-assist confirmed real — and worse than 'compilation error on WebGPU-enabled browsers' suggests: PixiJS prefers WebGPU when available, so the broken WGSL bites modern Chrome by default rather than staying on the GLSL path. Will be fixed as part of the rebase plan in my assessment comment.
There was a problem hiding this comment.
Thanks for the confirmation, @santi1584. Since you are planning to address this in your upcoming rebase, I will leave this thread open for now. Please ensure the final WGSL implementation includes the closing brace and the requested comment for consistency with the GLSL version once you push the fix.
| fn mainFrag(input: VSOut) -> @location(0) vec4<f32> { | |
| let d = input.vUV - vec2<f32>(0.5); | |
| let r = length(d) * 2.0; | |
| let t = clamp(1.0 - (r - lightUniforms.uCoreSize) / (1.0 - lightUniforms.uCoreSize), 0.0, 1.0); | |
| let intensity = t * t; | |
| return vec4<f32>(lightUniforms.uTint * intensity, intensity); | |
| fn mainFrag(input: VSOut) -> @location(0) vec4<f32> { | |
| let d = input.vUV - vec2<f32>(0.5); | |
| let r = length(d) * 2.0; | |
| let t = clamp(1.0 - (r - lightUniforms.uCoreSize) / (1.0 - lightUniforms.uCoreSize), 0.0, 1.0); | |
| let intensity = t * t * 0.55; | |
| // uTime is plumbed but unused — reserved for a future torch flicker pass | |
| // (e.g. intensity *= 1.0 + 0.05 * sin(uTime * 6.0)) without CPU work. | |
| return vec4<f32>(lightUniforms.uTint * intensity, intensity); | |
| } |
There was a problem hiding this comment.
Correcting my earlier reply after actually verifying against the current branch: the WGSL is brace-balanced (6 open / 6 close; mainFrag closes properly before the template literal ends). Either this was fixed in a later push or the original finding was off — either way the critical doesn't hold against today's diff, and I repeated it without re-checking first. The remaining blockers are the ones in my assessment: the rebase over the pooling commits and renderer verification. Doing the rebase now.
At the center of a bright torch, tint + night ambient summed past 1.0 per channel, so red and green clamped to 1.0 and the bubble's center looked white. Color was only visible at the edges where intensity was partial and the sum stayed under the cap. Scale intensity by 0.55 in both GLSL and WGSL fragments. Picked so a worst-case pure-white tint plus the brightest night-ambient channel (0x68 = 0.408) leaves headroom under 1.0; orange/yellow torches keep their hue all the way to the center at the cost of a slightly dimmer night overall. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Assessment — the direction is right and worth landing, but not mergeable as it stands. Three findings:
Verification constraint: I can validate the GLSL path headlessly (software WebGL), but the WGSL path needs a WebGPU-capable run — worth a manual check on real hardware before merge. Recommendation: rebase over the pooling commits (removing them as part of the rebase), fix the WGSL brace, verify GLSL headlessly + WGSL on hardware, then merge — and close #125 if the regression doesn't survive the new renderer. Happy to execute the rebase + fix next session; flagging rather than doing it now because shader work without hardware verification is exactly the kind of change that merges green and breaks on someone's phone. |
# Conflicts: # src/main.ts
|
Rebase done and pushed (b5974d7): merged current main into this branch — the rename detection carried the lighting.ts → lighting/index.ts move cleanly, the pooled-sprite/canvas-mask code from 650da85+7824f43 is consciously gone (only Verification status:
@santi1584 it's ready for that look whenever you next play-test — and per #125, judge the new falloff against your memory of the good lighting, since this deletes the implicated code path entirely. |
|
@santi1584 — this PR is ready except for two things only you can do. Clear steps: 1. Visual check of the new shader lighting (~3 min) npx vite # in otclient-web, then open http://localhost:5173/map.html
2. WebGPU run (~1 min) — the WGSL path PixiJS prefers on modern Chrome
Bonus (closes #125 in the same look): while you're at step 1, this branch deletes the code #125 blames — if the lighting looks right here, comment on #125 and we close it. CI note: the current red is GitHub's auth incident hitting CodeQL's own runner token (third wave today) — I'm retriggering until it sticks; ignore it. |
Summary
Phase 3 of the lighting optimization: the 256×256 canvas-rendered light mask is gone. Each light is now a
Meshwith a custom fragment shader that computes the radial gradient per pixel.Three wins:
Great Lightscaled up no longer shows gradient banding; the falloff is mathematical.uTimeuniform is wired into the shader (currently unused). A torch-flicker pass can land later without any CPU work, just by samplingsin(uTime * k).Shader cross-platform: ships both GLSL (300 es) for WebGL and WGSL for WebGPU. The vertex shader piggy-backs on Pixi's auto-assigned
globalUniforms(group 0) andlocalUniforms(group 1) for the projection/world/transform matrices; ourlightUniformsis group 2.Falloff math:
clamp(1 - (r - coreSize)/(1 - coreSize), 0, 1)^2withcoreSize = 0.1. Keeps the small flat core the canvas mask had, then a clean quadratic ramp to zero — no piecewise stops.Bundled small fixes:
rebuildTiles). Caller detaches it beforetileContainer.destroy({ children: true })so it survives the destroy.createLightMaskTextureand thelightMaskparameter are gone.lib/lighting.tsintolib/lighting/{index.ts,shader.ts}matching the existinglib/render/pattern; the public import path (./lib/lighting) is unchanged so callers don't move.PR is over the 100-line target (~150 net new) — the cross-platform shader code is the main bulk. Acceptable trade-off vs. dropping WebGPU support.
Test plan
npx tsc --noEmitcleannpx eslintcleannpx vitest run— 37 files / 359 tests passnpm run buildsucceeds (shaders bundle as strings)[render] PixiJS renderer: webgpu[render] PixiJS renderer: webgl🤖 Generated with Claude Code