Skip to content

feature: rendering features (area lights, etc) - #35

Merged
sheazywi merged 15 commits into
devfrom
feature/render-features
Oct 25, 2026
Merged

sheazywi merged 15 commits into
devfrom
feature/render-features

Conversation

@sheazywi

Copy link
Copy Markdown
Member

What

The render features plan (docs/RENDER_FEATURES_PLAN.md) and its Phase 0: a RenderFeatures test scene plus recorded baselines.

Why

The plan ports the rest of the Lucky Engine shader catalog as features: LTC area lights, glass refraction, FFT convolution bloom (replacing the mip bloom), and frosted-glass editor overlays. After those come three performance items: a slimmer G-buffer, a meshlet path that pays off, and QTangent vertices. Every later phase needs a scene that exercises its feature and a recorded "before"; Benchmark has neither area lights nor a refraction target, and changing Benchmark would break its golden and perf history.

Changes

  • scripts/GenerateBenchmarkScenes.py --scene features writes Scenes/RenderFeatures.luxscene:
    • floor strips of roughness 0.05 / 0.3 / 0.7 under rect, disk, sphere and tube area-light slots (empty entities until area lights exist);
    • clear, frosted, rough and tinted glass in front of a dark-grid wall;
    • bright emitters and an emissive bar for bloom;
    • chrome and satin spheres, under a dusk sun.
  • 15 RF_* materials, registered under their own handles (16000000000000001000+), with their own entity UUID range. AssetRegistry.lzr gains only those entries and the scene's.
  • The default (Benchmark) mode is unchanged: the original and new scripts' outputs (scene, materials, registry) were compared identical.
  • Rendering.md (golden scenes) and the profile skill mention the new scene.

Verification

  • Release build of this branch. No C++ or shader changes, so no Debug build.
  • Golden label rf0 (Benchmark + 16 feature variants + RenderFeatures): 18 captures, 0 validation ids.
  • RenderFeatures inspected by eye.
  • Baseline medians of 3 recorded in the plan's Part 4.
  • Note: rf0 was captured with a working-copy LuxSample.luxproj whose ShadowFilterMode was 0; a different value changes every shadowed pixel.

Regeneration

None needed (no source files added or removed).

Docs

.claude/docs/Rendering.md, .claude/skills/profile/SKILL.md, new docs/RENDER_FEATURES_PLAN.md.

Notes

This branch is cut from dev. Foliage PR #34 is not in it; the G-buffer and material phases rebase onto #34 once it merges.

docs/RENDER_FEATURES_PLAN.md plans the rest of the Lucky shader catalog as features: LTC area lights,
glass refraction, FFT convolution bloom (replacing the mip bloom), frosted-glass editor overlays, then
a slimmer G-buffer, a meshlet path that pays off, and QTangent vertices.

Phase 0 gives those phases a scene and a baseline. GenerateBenchmarkScenes.py --scene features writes
RenderFeatures.luxscene: floor strips of roughness 0.05/0.3/0.7 under rect, disk, sphere and tube
area-light slots, clear/frosted/rough/tinted glass in front of a dark-grid wall, bright emitters for
bloom, chrome and satin spheres, under a dusk sun. Its 15 RF_* materials sit under their own handles
(16000000000000001000+) and entity range; the default Benchmark output is unchanged (original and new
script outputs compared identical). Golden label rf0 (Benchmark + 16 variants + RenderFeatures):
18 captures, 0 validation ids; medians of 3 recorded in the plan.
Copilot AI balanced review requested due to automatic review settings October 22, 2026 07:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 82d2615a-313c-43e5-bbf6-cb4ebce7487e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

… (render features phase 1)

New AreaLightComponent: a rect (Width x Height) or disk (Radius) emitting along the entity's -Z, sized
by its transform, intensity as luminance or lumens (PhysicalLight::AreaIntensity), two-sided option,
range. It goes through every component touch point (copy, prefab, serializer both ways, hierarchy
panel create menu / add row / inspector) and reaches the renderer in LightEnvironment::AreaLights.
The viewport draws each light's shape outline.

SceneRenderer uploads the nearest 32 to AreaLightData at (1, 18) and adds an "Area Lights" pass
(DeferredLighting_Area.glsl + LTC.glslh) over deferred lighting, only on frames with area lights, so
deferred lighting never carries the LTC code. Diffuse is Lambert, specular GGX through the fitted LTC
tables; a disk is integrated as an area-matched 12-gon; horizon clipping uses the tables' sphere form
factor. The tables are Renderer::GetLTCMatrixTexture / GetLTCAmplitudeTexture at (3, 6) / (3, 7),
built from Core/vendor/ltc/LTCTables.h, converted from ltc_1/ltc_2.dds of selfshadow/ltc_code
(BSD-2-Clause, license alongside).

RenderFeatures gains a rect and a disk light; the generator no longer rewrites the asset registry
when its entries are already registered. Verified: Release, Debug and Lux-Runtime build; golden rf1:
all 17 Benchmark captures identical to rf0 (known 2-px noise), RenderFeatures shows the lights;
Debug + sync validation on RenderFeatures and Benchmark: 0 validation ids; inspector, create menu,
add-component row and outline checked in the editor.
@sheazywi sheazywi changed the title Render features plan and its test scene (phase 0) feature: rendering features (area lights, etc) Oct 23, 2026
…nts (render features phase 2)

AreaLightShape gains Sphere and Tube (new Length key; Radius shared). A sphere is integrated as a disk
facing the shaded point and a tube as a rectangle facing it around its axis, lengthened by pi r / 4 per
end to keep the capsule silhouette's area (LTC_FacingShape), so the existing LTC polygon integral serves
all four shapes; lumens convert over each shape's full surface. Two-sided applies to flat shapes only
(hidden in the inspector otherwise), and the viewport outlines spheres and tubes.

HazelPBR_Transparent.glsl, the transparent pass's shader, adds LuxEvaluateAreaLights; the transparent
pass binds AreaLightData and the LTC tables, and the area buffer is now uploaded every frame (count 0
included) so glass never keeps lights that were removed. RenderFeatures gains a sphere and a tube.

Exported games carry everything (the shader pack is built from the whole library; the tables are
compiled into Core). Verified: Release, Debug and Lux-Runtime build; golden rf2: Benchmark identical to
rf1 except the colliders variant's other known line-blending state; RenderFeatures shows all four
shapes and lit glass; Debug + sync validation: 0 validation ids; shapes checked in the editor.
… phase 3)

Transparent materials gain glTF-style refraction inputs on MaterialSurfaceParameters: Transmission,
IOR, Thickness, AttenuationColor and AttenuationDistance (YAML keys written when not default, so old
.lmat files load unchanged; asset packs reuse the YAML), imported from KHR_materials_transmission /
_ior / _volume, and edited in a "Refraction" section of the material editor shown for transparent
materials. The GPU material row grows to 11 vec4 (Transmission, AttenuationColor), both layouts together.

In HazelPBR_Transparent.glsl transmission takes its share from the diffuse lobe and adds the opaque
scene behind: the view ray refracted by the IOR and carried Thickness into the volume (0 = thin-walled,
blurred but not displaced), projected back with the flipped-Y convention and read from the
pre-convolved scene at a roughness mip capped at 5, the sky when it leaves the screen, absorbed
Beer-Lambert, tinted by the base colour and weighted by (1 - Fresnel). Alpha stays coverage, so
transmission 0 is exactly the old blend.

MaterialScene counts refractive rows (HasRefractiveMaterials); with any in the submitted render scene,
Pre-Convolution runs even without SSR and the transparent pass reads it at (1, 11), bound in
TransparentForwardPass (a black texture otherwise). RenderFeatures' glass becomes refractive: thin
panes, thick spheres, an absorbing block.

Verified: Release, Debug and Lux-Runtime build; golden rf3: Benchmark identical to rf2; RenderFeatures
panes sharp / frosted / rough, spheres bend, the block absorbs; with SSR off the glass is the same and
Pre-Convolution runs alone (0.088 ms); Debug + sync validation: 0 validation ids; the Refraction
section checked in the editor.
…er features phase 4)

PostProcessing/BloomFFT.glsl, one compute shader with five modes: the scene area-averaged and
soft-thresholded into half of a 512x512 grid along its longer side (the other half is padding, so the
circular convolution never wraps), a lens-glare PSF (core, power-law halo, six faint streaks, slightly
chromatic), a 512-point shared-memory FFT per grid row or column, a spectral multiply normalised by the
kernel's DC term (energy is redistributed, not added), and a resolve into the bloom texture the composite
already reads. The kernel spectrum is built once; forward and inverse row passes cover only the source
rows. Grids: 2x RGBA32F + 2x RG32F 512^2, about 12 MB.

SceneRendererOptions::BloomFFT (golden option BloomFFT=1, not serialized) swaps it in for the mip bloom
to measure it before Phase 5 replaces the mip bloom. A-B-A on Benchmark, medians of 3: 1280x720 mip
0.059 / FFT 0.109 / mip 0.060 ms; 1920x1080 0.083 / 0.133 / 0.083 ms, far under the 0.6 ms budget.
Option off: Benchmark and RenderFeatures pixel-identical to the previous golden set; FFT on, Debug +
sync validation: 0 validation ids.
…rnel settings (render features phase 5)

Bloom.glsl, its mip textures, views and materials, and the temporary BloomFFT option are removed;
SceneRenderer::BloomCompute runs the FFT chain and writes one RGBA16F bloom texture at the bloom
resolution scale, which the composite and the Bloom debug view read.

The glare kernel is now configurable: streak count (0-8), length, intensity and rotation, halo
intensity and chromatic spread. They live in BloomSettings and the project's scene-renderer
settings with the other bloom values (YAML under PostFX/Bloom, runtime format 28), reset with the
post-process quality preset, and show in the Renderer panel's Post FX card. The kernel spectrum is
rebuilt only when they change. Bloom Upsample Scale no longer applies: its row is gone, the field
is kept so project files round-trip.

Golden rf5: every bloom capture changes, noBloom identical to rf3, and the defaults reproduce the
Phase 4 FFT look (max 2). Benchmark bloom 0.059 -> 0.109 ms. Debug sync validation: 0 ids.
…render features phase 6)

Popups, menus and tooltips, and the translucent viewport overlays (Performance HUD, viewport
settings toolbar, acoustic legend, room acoustics readout), now draw their background over a blur
of what is behind them. ImGuiLayer puts an ImGuiGlassBackdropCallback command first in each such
window's draw list after EndFrame; it carries the window's rectangle and rounding by value, so the
render-thread snapshot holds everything. ImGuiRenderer draws a frame with backdrops into an RGBA8
UI target, runs a dual-Kawase blur of each window's region over three smaller levels and a
rounded-rectangle composite (ImGuiGlass.hlsl), and presents the target at the end. The window's own
translucent background tints the blur; popup backgrounds keep 72% of their alpha while glass is on.
Frames without backdrops, the runtime's overlay and the preference off draw straight into the back
buffer as before.

Editor preference "Glass Overlays" (Application Settings > Editor, Editor.GlassOverlays, default on).

Verified in the editor: the HUD and the settings, File and View popups blur, preference off restores
the old look, a resize is clean. ~0.10 ms GPU with two backdrops at 3862x2110 (RenderDoc). Debug sync
validation: no errors. Goldens rf6 identical to rf5.
Both sides added material inputs. The GPU material row keeps dev's diffuse transmission as
Transmission + TransmissionTextureIndices and renames this branch's refraction vec4 to Refraction
(with AttenuationColor), so the row is 13 vec4 and GetGPUMaterialRefraction reads the refraction
share. The serializer writes both sets of keys and keeps dev's surface-before-maps read order; the
material editor shows Refraction for transparent materials and Translucency for opaque ones.

Golden rf7m identical to rf5 (colliders within its known noise); Debug sync validation clean.
…hase 7)

No pass reads motion vectors since the temporal techniques were removed, so the G-buffer loses its
RG16F velocity attachment, and GBuffer_Static its previous-transform fetch, clip-space varyings and
velocity output; emission moves to attachment 4. The dead forwardSpec the transparent framebuffer
was copied from goes too, and with it GetGeometryVelocityOutput. PreviousViewProjection stays in the
shared camera block.

Golden rf7a identical to the merge's rf7m across all 18 captures; G-buffer pass 0.371 -> 0.337 ms at
1080p (Benchmark, medians of 3). Debug sync validation clean.

The plan's other two steps were measured and rejected by its identical-image gate: octahedral
normals (RG16F, and a trial RG16 snorm format) change grazing-angle rim highlights by up to 35-52
levels on hundreds of pixels for no time saved, and an RGBA8 sRGB base colour clips colours above 1.
Rendering.md records the G-buffer layout and why.
GTAO scaled its final power by AOShadowTolerance, lerped with the luminance the old forward path
stored in the normal's w. The deferred G-buffer never stored one: that w is 0, or a foliage pixel's
diffuse transmission since the foliage materials, so the setting did nothing except on foliage, by
accident. A real luminance is not available either, because GTAO runs before lighting and the
previous frame's would be a temporal dependency.

The setting, its Renderer panel row, the GTAO constant and the shader term are gone. A pad keeps
CBGTAOData at 88 bytes with every later field where it was. Runtime format 29; packs before it skip
the stored value, and old project YAML keeps loading with the key ignored.

Golden rf7f identical to rf7a across all 18 captures (the project used the default of 1.0).
…m (render features phase 8)

The mesh-shader prepass measured 24 ms on Benchmark against 0.12 ms for the classic one. RenderDoc
and a stage-by-stage bisection put the cost in launching task and mesh work, and the cause was the
PIPELINE_STATISTICS query every command-buffer segment kept open: on NVIDIA it serializes mesh
shading. Without it the mesh-shader prepass takes 0.15 ms, level with the classic path.

Statistics are now counted only in render frames that took a
RenderCommandBuffer::RequestPipelineStatistics(), which the Profiler makes every frame its Pipeline
Statistics section is open; the request lapses on its own, however the panel is closed. The section
says that counting slows mesh shaders.

Golden rf8a identical to rf7f (all 18); Debug sync validation clean on the classic and mesh-shader
paths; the Profiler shows the counts when the section is opened.
Records the measurements behind it. With the statistics fix the mesh-shader prepass is 0.151 ms on
Benchmark against 0.118 ms for the classic one. Four ways to close that were built, checked
pixel-identical and timed, then reverted: whole-instance and backface-cone culling (0.178 ms),
task threads over (instance, meshlet) pairs (0.154 ms), a position-only opaque output (0.145 ms),
and meshlets built with meshoptimizer as Lucky Engine does (0.163 ms, 0.184 ms with cones). On
Benchmark's many small instanced meshes the classic instanced draw is cheaper than task and mesh
dispatch, so no meshlet G-buffer pass is built.
Mesh vertex buffers now hold a 28-byte PackedVertex instead of the 56-byte Vertex: position, the
tangent frame as a unit quaternion in four snorm16 values with the binormal's handedness in the sign
of w, and the UV, as Lucky Engine does it. MeshSource packs it when it creates the vertex buffer; the
CPU keeps the full Vertex, so mesh files, caches and runtime packs are unchanged.

The mesh layout is position, QTangent (new ShaderDataType::Short4N, RGBA16_SNORM) and UV; every
shader on it declares those three, the ones that need the frame decode it with QTangent_ToTBN
(Include/GLSL/QTangent.glslh), and the mesh-shader prepass reads the buffer at a 7-float stride.

The encoder orthonormalizes the frame, so meshes whose imported tangents were skewed now shade with
the corrected frame (about 1.5% of Benchmark's pixels, mostly SSR reflections on glossy normal-mapped
floors); that is accepted as the new reference, golden rf9. Vertex buffers halve; no measurable
GPU-time change at 1080p. Debug sync validation clean, including the mesh-shader prepass.
They still declared the old 56-byte vertex layout. No pipeline binds them:
the engine has no skeletal animation, and runtime packs map LuxPBR_* names
to HazelPBR_* before the lookup. shader.glsl was never loaded.
…t toggles, volume overlays, camera fix

Area light shadows (docs/AREA_LIGHT_SHADOWS_PLAN.md, phases 0-4):
- AreaLightComponent CastsShadows (default off) and SoftShadows; area lights
  take the spot atlas tiles spot and point lights leave: a rect or disk one
  per lit side (a 120-degree cone, apex behind the emitter, near plane at the
  emitter), a sphere or tube six cube faces from its centre.
- PCSS for area tiles (RawPCSS_AreaLight): the emitter in front of the apex,
  an elliptical kernel sized by the shape along each tile axis. Spot and
  point PCSS unchanged.
- The area UBO uploads after the atlas selection; the Area Lights pass binds
  the shadow maps; transparents get the same shadows.
- One-time warning when shadowed area lights get no tile.
- RenderFeatures gains shadow casters; run_shadow_shader.py checks the area
  shader too.

Editor:
- Renderer Debugger Shadows tab: the atlas with each tile labelled by light,
  sizes, and how many shadowed lights got a tile.
- Viewport overlays: Camera Frustums and Light Volumes (Off / Selected / All).
- Entity Active and per-component Enabled toggles, saved only when off,
  honoured by lights, meshes, 2D, audio and scripts (not physics yet).
- The viewport camera flies and orbits again with an entity selected.

Builds: Release and Debug (Editor, Lux-Runtime), Dist (Lux-Runtime). Not yet
run in the editor: golden captures, runtime export, threading A/B.
@sheazywi
sheazywi merged commit 76062c9 into dev Oct 25, 2026
7 checks passed
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.

2 participants