Commit Graph
24 Commits
Author SHA1 Message Date
emil28092005 ed919edb0a fix: enable back-face culling — removes self-shadowing artifacts
Root cause: CULL_MODE_NONE in both main and shadow pipelines caused
back faces to render, creating self-shadowing acne on all objects:
- Shadow map: back faces written at closer depth → false shadows on front
- Main render: back faces visible through objects → wrong lighting

Fix:
- Main pipeline: CULL_MODE_BACK (was None) — only front faces render
- Shadow pipeline: CULL_MODE_BACK (was None) — only front faces cast shadows
- Removed normal flip hack (if dot(N,V) < 0 N = -N) — no longer needed
  with proper culling + correct CCW winding in cube.obj
- This is the standard approach: correct geometry + back-face culling
2026-06-18 22:00:03 +03:00
emil28092005 6a473bdc53 fix: flip normal only by view direction — fixes both cubes and spheres
Previous fix flipped normal by both light AND view direction:
  if (dot(N,L) < 0) N = -N;
  if (dot(N,V) < 0) N = -N;
This double-flip broke spheres: when light is behind sphere, front faces
get N flipped by light (now facing away from camera), then flipped back
by view — but back faces get flipped once, causing inconsistency.

Correct approach: flip ONLY by view direction:
  if (dot(N, V) < 0.0) N = -N;
This ensures the normal always faces the camera. Faces pointing away
from light naturally get NdotL=0 (no lighting) — correct behavior.
Spheres keep smooth normals, cubes get inward normals fixed.
2026-06-18 21:56:10 +03:00
emil28092005 2f7430e3b1 fix: flip normals when facing away from light/view — fixes cube lighting
Root cause: cube.obj has inconsistent winding order. Many faces have
normals pointing inward (e.g. back face normal = +Z instead of -Z).
With cullMode=None, both sides render, but inward normals cause:
- No diffuse lighting (NdotL < 0 → max() = 0)
- Wrong shadow bias (slope-dependent bias uses wrong NdotL)
- Inconsistent shadow edges on cube faces

Fix: in fragment shader, flip normal if dot(N,L) < 0 or dot(N,V) < 0
This is the standard approach for double-sided rendering with
non-watertight geometry or inconsistent winding order.
2026-06-18 21:53:15 +03:00
emil28092005 29a58d204b feat: ImGui shadow parameters panel with clipboard copy
- Push constant block expanded: 160B → 176B (added vec4 shadowParams)
- shadowParams: x=bias, y=sampleRadius, z=farPlane, w=unused
- Fragment shader: bias and sampleRadius now read from push constants
  (dynamic, adjustable at runtime)
- Shadow.frag: uses pc.shadowParams.z for farPlane (dynamic)
- Slope-dependent bias: bias + bias*2*(1-NdotL) — adaptive
- VulkanRenderer: public ShadowBias, ShadowSampleRadius, ShadowFarPlane properties
- Program.cs: ImGui 'Shadow Parameters' panel with sliders:
  - Light Intensity (0-100), Range (5-100), RGB color
  - Shadow Bias (0.001-0.5), Sample Radius (0.001-0.1), Far Plane (10-120)
  - 'Copy Parameters to Clipboard' button — outputs all values as text
- 227 tests, all pass
2026-06-18 21:48:38 +03:00
emil28092005 2454af1c89 fix: balanced shadow bias — no acne, no peter-panning
- Fragment bias: 0.08 + 0.15 * (1 - NdotL) — smaller base, moderate slope
  (was 0.3 + 0.5 — too much → peter-panning, shadows detached from objects)
- vkCmdSetDepthBias: 1.75/0/2.5 (was 2.5/0/3.5 — too aggressive)
- Removed early exit logic — was causing hard shadow edges
- Always run 16-tap PCF for consistent soft shadow quality
- Smaller sample radius: clamp(0.015 * dist/20, 0.003, 0.05)
2026-06-18 21:40:43 +03:00
emil28092005 23b38f8f17 fix: increase shadow bias — slope-dependent + larger depth bias
- Fragment shader: bias = 0.3 + 0.5 * (1 - NdotL) — slope-dependent
  Grazing angles get larger bias to prevent acne on cube faces
- vkCmdSetDepthBias: 2.5/0/3.5 (was 1.25/0/1.75) — more aggressive
  polygon offset for shadow map rendering
- Early exit tolerance reduced from 4x to 3x bias
2026-06-18 21:38:48 +03:00
emil28092005 7e33b64ac9 feat: soft shadows — 16-tap Poisson disk PCF on cubemap
- 16 precomputed Poisson disk offsets on tangent plane
- Tangent basis built from sampling direction (cross product trick)
- Sample radius scales with distance: closer = sharper, farther = softer
  radius = clamp(0.02 * (dist / 20), 0.005, 0.08)
- Early exit: skip PCF if clearly lit or clearly shadowed (performance)
- Bias increased to 0.15 for cubemap distance comparison
- Only triangle.frag changed — no C# or Vulkan resource changes
- 227 tests, all pass
2026-06-18 21:36:30 +03:00
emil28092005 a2000b55b5 feat: cubemap shadows with R32_SFLOAT color attachment — linear distance
Root cause of broken shadows: depth buffer stores non-linear NDC depth,
not linear distance. closestDepth * 60.0 was wrong conversion.

Fix: switch from depth-only to R32_SFLOAT color attachment approach:
- Shadow vertex shader outputs world position to fragment
- Shadow fragment shader writes length(fragPos - lightPos) / farPlane
- Main fragment shader samples cubemap, multiplies by FAR_PLANE=60
- Separate color cube (R32_SFLOAT, sampled) + depth cube (D32_SFLOAT, depth test)
- Shadow pipeline: 1 color attachment (R) + depth attachment
- Color clear = 1.0 (max distance), depth clear = 1.0

Tests: 227 total, all pass
- ShadowMapFaceDirectionTests: 6 face directions, 90° FOV, up vectors,
  valid matrices, far plane consistency
- ShadowShaderTests: all 6 SPIR-V shaders exist
2026-06-18 21:27:48 +03:00
emil28092005 fa937ecd94 feat: cubemap shadow mapping — true omnidirectional point light shadows
- VulkanShadowMap: 1024x1024x6 layer D32_SFLOAT cube image
  - CubeCompatible flag, cube view for sampling, 6 face views for rendering
  - GetFaceViewProj: 6 directions (+X, -X, +Y, -Y, +Z, -Z) with correct up vectors
- 6 shadow render passes per frame (one per cube face)
- Fragment shader: samplerCube instead of sampler2D
  - Shadow: direction from light to fragment, distance comparison
  - No more perspective frustum limitation — omnidirectional shadows
- shadow.vert: same push constant block (160B), uses lightViewProj per face
- triangle.vert: removed fragLightSpacePos (not needed for cubemap)
- Reduced shadow map size to 1024 (6x memory vs single 2048)
- Floor/Grid excluded from shadow casting
2026-06-18 20:35:01 +03:00
emil28092005 e6e305dfa9 feat: shadow mapping — depth-only render pass from light POV + PCF
- VulkanShadowMap.cs: 2048x2048 D32_SFLOAT image, sampler (clamp-to-border),
  separate shadow pipeline (depth-only, no color, depth bias enabled, CULL_NONE)
- Shadow shaders: shadow.vert (lightViewProj * model * pos), shadow.frag (empty)
- Main shaders updated: fragLightSpacePos output from vertex, PCF 3x3 in fragment
- Push constants expanded to 160B: model(64) + lightPos(16) + lightColor(16) + lightViewProj(64)
- 3 push constant ranges: Vertex(model), Fragment(light), Vertex|Fragment(lightViewProj)
- Descriptor set: 2 bindings — UBO(vp) + CombinedImageSampler(shadowMap)
- Shadow pass: before main pass, renders scene depth from light position
- Image transitions: shadow map UNDEFINED→DEPTH→SHADER_READ_ONLY each frame
- vkCmdSetDepthBias(1.25, 0, 1.75) for acne prevention
- Light VP: CreateLookAt(lightPos, origin, up) * Perspective(60°, 1.0, 0.1, 60)
- 0 validation errors on build
2026-06-18 20:09:32 +03:00
emil28092005 1dde764342 fix: move light data from UBO to push constants — fixes std140 layout issue
Root cause: GLSL std140 layout adds padding after mat4 for vec4 fields,
causing light data to be read at wrong offsets → NaN/artifacts.

Fix:
- UBO back to 64 bytes (mat4 vp only)
- Light data (pos + color, 32 bytes) sent via push constants at offset 64
- Two push constant ranges: Vertex (0-64, model) + Fragment (64-96, light)
- Both vertex and fragment shaders read from same push_constant block
- PBR shader fully restored with Cook-Torrance BRDF
- Dynamic point light follows physics ball
2026-06-18 19:53:45 +03:00
emil28092005 ad8a14a4f7 debug: hardcoded light in fragment shader — no UBO light data
Light at (0,10,0) hardcoded in GLSL. Tests if lighting math works.
If light works: problem is UBO layout for light data (std140 padding).
If artifacts: problem is in fragWorldPos or fragNormal.
2026-06-18 19:50:20 +03:00
emil28092005 7ed81040ed debug: simple diffuse point light — test if light data reaches fragment 2026-06-18 19:47:29 +03:00
emil28092005 b162d681aa debug: albedo-only fragment shader — tests if vp matrix works with 128B UBO
If objects are colored: vp matrix is fine, problem is in lighting
If objects are black: vp matrix is broken by 128B UBO change
2026-06-18 19:46:05 +03:00
emil28092005 4771371021 fix: restore MemoryCopy for UBO + PBR shader with point light
- UBO packing: MemoryCopy for vp (64B) + lightPos (16B) + lightColor (16B)
- PBR fragment shader with Cook-Torrance BRDF + point light
- No directional light, near-zero ambient — only point light illuminates
- Guards against div-by-zero in attenuation and specular
- Depth buffer properly configured in pipeline + rendering info
2026-06-18 19:43:31 +03:00
emil28092005 e37e7a4eed debug: fragment shader outputs light position as color — test UBO
- If objects are colored (not black/artifacts), UBO data reaches fragment shader
- Also: manual float-by-float UBO packing instead of MemoryCopy
- vp matrix packed as M11..M44 (row-major, no row_major in GLSL = transpose)
2026-06-18 19:36:57 +03:00
emil28092005 321b1f62cc debug: simple diffuse fragment shader to test point light UBO 2026-06-18 19:33:37 +03:00
emil28092005 6a7d6ee3eb feat: single point light only — dark scene, dynamic light ball visible
- Removed directional light from fragment shader
- Ambient reduced to near-zero (0.01) — scene is dark
- Only light source is the physics LightBall falling from y=15
- Objects near the ball are illuminated, far objects are in darkness
- Dramatic contrast shows dynamic lighting clearly
2026-06-18 19:27:05 +03:00
emil28092005 f718e6de49 feat: PBR point light — Unity-style attenuation, warm glow
- UBO expanded from 64 to 128 bytes: vp(64) + lightPos(16) + lightColor(16)
- Vertex shader: passes point light UBO data through
- Fragment shader: refactored calcLight() function, shared by directional + point
  - Point light: Unity-style attenuation pow(1 - dist/range, 2)
  - Directional light reduced intensity (0.4) to balance with point light
  - Point light: position (0,8,0), warm color (1,0.9,0.7), intensity 15, range 25
- Renderer: reads Light component from ECS, packs into UBO with Marshal.StructureToPtr
- Scene: MainLight entity with Light.Point at (0,8,0)
- 0 C# struct changes — pure UBO layout + shader upgrade
2026-06-18 19:23:00 +03:00
emil28092005 22748ed9ba feat: PBR shading — Cook-Torrance BRDF with ACES tonemapping
- Vertex shader: passes world position, world normal, albedo to fragment
- Fragment shader: full PBR implementation
  - D term: Trowbridge-Reitz GGX distribution
  - G term: Smith geometry with Schlick-GGX
  - F term: Schlick Fresnel approximation
  - kD/kS split based on metallic
  - Directional light (0.5, 0.8, 0.3) with warm color
  - Ambient term (0.15, 0.18, 0.22) for fill light
  - ACES filmic tonemapping
  - Gamma 2.2 correction
- Fixed roughness=0.5, metallic=0.1 (per-object materials = future)
- No C# code changes — pure shader upgrade
2026-06-18 19:18:01 +03:00
emil28092005 e36cbc7774 feat: diffuse lighting — normals passed to fragment shader
- Vertex shader: pass normal through mat3(model) to fragment
- Fragment shader: directional light (0.5, 0.8, 0.3) with 0.25 ambient
  + 0.75 diffuse = faces now have depth and 3D form
- Torus knot has smooth normals (vn in OBJ), cubes have face normals
- 0 validation errors, ~2300 FPS
2026-06-18 17:03:15 +03:00
emil28092005 233041ab00 fix: VK_INDEX_TYPE_UINT32 (was UINT16) + remove unused depth from pipeline
- vkCmdBindIndexBuffer indexType changed from 0 (UINT16) to 1 (UINT32)
  to match uint[] index data — this was causing black screen
- Removed depth stencil state, depth attachment format, and depth
  rendering attachment from pipeline/renderer (depth buffer still
  created in swapchain but unused)
2026-06-18 12:59:01 +03:00
emil28092005 d6ca80c678 fix: add normal-based diffuse lighting so cube is visible as 3D
- Fragment shader: directional light dot(normal, lightDir) with 0.2 ambient
- Vertex shader: pass normal through mat3(model) to fragment
- Same-color faces now have different shading based on orientation
2026-06-18 12:39:51 +03:00
emil28092005 2e0970e769 feat: rebuild Vulkan renderer from scratch — pure P/Invoke triangle (Vulkan 1.3)
- Complete rewrite of Engine.Graphics.Vulkan with pure P/Invoke (no wrapper libs)
- Vulkan 1.3: dynamic rendering (vkCmdBeginRendering/vkCmdEndRendering),
  synchronization2 (vkQueueSubmit2, vkCmdPipelineBarrier2)
- Split types into VulkanHandles.cs, VulkanEnums.cs, VulkanStructs.cs
- Staging buffer → device-local vertex buffer pattern
- Correct swapchain semaphore indexing (per-image, not per-frame-in-flight)
- VK_EXT_debug_utils debug messenger with validation layer fallback
- Dynamic viewport/scissor (no pipeline recreation on resize)
- Simplified Program.cs to triangle-only rendering
- Removed old Silk.NET renderer, ImGui, PBR shaders, screenshot code
- Updated VULKAN_IMPLEMENTATION_PLAN.md with full architecture decisions
2026-06-18 01:49:25 +03:00