feat/pbr-shader-pipeline #90

Merged
akadmin merged 7 commits from feat/pbr-shader-pipeline into main 2026-06-29 05:39:57 +00:00
Owner

closes #17

closes #17
Replaces the single-texture flat-color renderer with a full
Cook-Torrance GGX BRDF pipeline supporting albedo, metallic-roughness,
normal map, and ambient-occlusion per entity.

Key changes:
- TextureComponent → MaterialComponent (4 AssetId fields + metallic/roughness factors)
- Fragment shader: GGX NDF, Schlick-GGX geometry, Schlick Fresnel, TBN normal mapping,
  Reinhard tonemapping + gamma correction
- Normal matrix moved on-GPU (transpose(inverse(mat3(model)))) to free push-constant bytes
- GpuTexture split into GpuTextureImage + GpuMaterial (4-binding DS at set 0)
- Camera UBO extended with camera_pos for specular view direction
- glTF loader extracts all 4 PBR channels and resolves absolute texture URIs
- Inspector UI updated to 4 path inputs + metallic/roughness drag sliders
- SetMaterialCommand / AddMaterialCommand replace old texture commands
- Default 1×1 fallback textures for absent maps (no shader branching)
- 152 tests pass; zero clippy warnings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The depth pipeline was created with a separate PipelineLayout, making
the camera descriptor set (built from shaded pipeline's set_layouts()[1])
incompatible when switching to depth view. Vulkan layout compatibility
requires the same VkDescriptorSetLayout object, not just structurally
identical ones.

Fix: use one shared Arc<PipelineLayout> for both pipelines. Also
pre-build a default_material_ds and bind it at set 0 in depth mode,
since depth_fragment.glsl still declares 4 sampler bindings so the
pipeline layout requires set 0 to always be bound.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The "…" browse button sets lost=true without triggering gained_focus on
the text_edit, so material_before was never captured and no
SetMaterialCommand was emitted. The path disappeared on the next frame
because material_edit was reset from the unmodified world.

Fix: fall back to reading the world state as the before-snapshot when
material_before is None at commit time. Covers both the file-picker
path and any other case where a field commit fires without a prior focus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three related fixes:

1. Disable metallic/roughness sliders when a MR texture is assigned.
   The texture already encodes per-texel metallic/roughness values; the
   factors would only globally tint them (advanced use), which is more
   likely to confuse than help in the common case.

2. Fix the default MR fallback texture from [0,128,0,255] to [0,255,255,255].
   glTF: roughness = G channel, metallic = B channel. The old default had
   B=0, so metallic_factor * 0 = 0 — metallic was always zero regardless
   of the slider. With all-255 the factors pass through directly (factor *
   1.0 = factor) when no texture is set, matching glTF semantics.

3. Auto-reset metallic_factor/roughness_factor to 1.0 when a MR texture
   is first assigned (before=None → after=Some). The glTF default for
   both factors is 1.0 ("use texture as-is"); without this a prior
   metallic_factor=0.0 would zero out the metallic channel of the texture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Salt-and-pepper grain with roughness=1.0 confirmed not specular aliasing —
root cause is the 100:1 lit/shadow ratio from a single directional light
making high-frequency normal-map detail look like noise. Adds a soft sky
fill light from the opposing hemisphere (FILL_DIR, 0.6 intensity) to cut
that ratio and fill shadow-side column faces. Reduces key light from 3.0
to 2.5 to keep overall exposure balanced.

Viewport "Depth" SelectableLabel now shows the current mode ("Shaded" or
"Depth") instead of always rendering the string "Depth" regardless of state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Salt-and-pepper grain persisted at roughness=1.0, confirming it is NOT
specular aliasing but texture minification aliasing. Without mipmaps the
GPU pulls a random texel from a 2048×2048 normal map for each screen
pixel, producing wildly different N values on adjacent fragments → per-
fragment NdotL jumps → noise that looks like grain regardless of BRDF.

Changes:
- upload_rgba now computes mip_levels = floor(log2(max(w,h))) + 1, creates
  the image with that many levels, and uploads each level in a single CB
  after CPU box-filter downsampling (box_downsample helper). Keeps all
  four 1×1 default textures working correctly (mip_levels = 1, no-op).
- create_sampler switches to SamplerMipmapMode::Linear + lod 0..=LOD_CLAMP_NONE,
  enabling full trilinear filtering across the mip chain.
- Sampler is shared per-texture (created at upload, stored in GpuTextureImage)
  so 1-mip default textures are safe: driver clamps to available levels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(depth): switch to reversed-Z for better depth precision
All checks were successful
CI / build (pull_request) Successful in 3m1s
ac2f09fdfa
Standard perspective (near→0, far→1) concentrates float precision near the
camera, leaving almost nothing for the rest of the scene. With near=0.1 and
far=1000, 90% of depth buffer bits cover the first meter, causing z-fighting
on coplanar Sponza geometry (column bases vs floor tiles, arch decals, etc.).

Reversed-Z maps near→1 and far→0. Float precision is concentrated near 0.0,
which is now the far plane, giving exponentially better precision at distance.

Changes:
- mat4_perspective: m[2][2] = near/(far-near), m[3][2] = near*far/(far-near)
- frustum_planes: swap near/far entries (plane[4] = sub(r3,r2), plane[5] = r2)
- Both pipelines: CompareOp::Less → CompareOp::Greater
- Depth clear value: 1.0 → 0.0 (clear to far so Greater passes on first write)
- Tests updated for new formula and frustum plane semantics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
akadmin deleted branch feat/pbr-shader-pipeline 2026-06-29 05:39:57 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
akadmin/NemesisEngine!90
No description provided.