Commit Graph
62 Commits
Author SHA1 Message Date
emil28092005 e813e5e90b fix: force B8G8R8A8_UNORM swapchain — fixes acid video colors
Root cause: driver returned format=58 (A2B10G10R10_PACK32) as fallback
when B8G8R8A8_SRGB wasn't found. 10-bit packed format has different
byte layout → vkCmdCopyImageToBuffer gives garbage when interpreted as
8-bit BGRA → acid colors in video.

Fix:
- QuerySurfaceFormat: try B8G8R8A8_UNORM first (NVIDIA supports it)
- Fallback: B8G8R8A8_SRGB, then first available
- Removed SRGB LUT conversion — UNORM data is already linear
- Raw BGRA bytes go directly to FFmpeg (pixel_format=bgra)
2026-06-19 09:21:26 +03:00
emil28092005 9531b1e9d5 fix: imageCubeArray via core features + SRGB swapchain + LUT color conversion
Three fixes:

1. imageCubeArray: enabled via VkPhysicalDeviceFeatures (core 1.0 feature)
   instead of VkPhysicalDeviceImageCubeArrayFeatures (wrong sType in pNext
   chain was interpreted as VkExternalMemoryImageCreateInfoNV)

2. Swapchain format: reverted to B8G8R8A8_SRGB (was UNORM which driver
   didn't support → fell back to A2B10G10R10 10-bit format=58 → wrong
   pixel layout for capture)

3. Video colors: SRGB→linear conversion via 256-entry LUT in
   ReadCapturedBuffer. SRGB swapchain gives gamma-compressed bytes,
   FFmpeg expects linear. LUT converts each B/G/R channel using
   standard SRGB formula: s<=0.04045 ? s/12.92 : ((s+0.055)/1.055)^2.4

Also: video captures before ImGui (no UI in recording)
2026-06-19 09:17:35 +03:00
emil28092005 d6939e9efa fix: video without UI + correct colors
1. No UI in video: end render pass before ImGui, capture, start new
   render pass with loadOp=LOAD for ImGui, end again. Video captures
   only the 3D scene.

2. Acid colors fixed: swapchain format changed from B8G8R8A8_SRGB to
   B8G8R8A8_UNORM. SRGB format stores gamma-compressed values that
   vkCmdCopyImageToBuffer reads raw — FFmpeg treated them as linear
   causing acid colors. UNORM stores linear values, copy gives linear
   data, FFmpeg bgra→yuv420p conversion is correct.
2026-06-19 09:13:04 +03:00
emil28092005 2daf10b3b1 debug: check if captured frame has non-zero data 2026-06-19 09:09:29 +03:00
emil28092005 277c8aea93 fix: video colors — use BGRA pixel format for FFmpeg, remove swap
Swapchain format is B8G8R8A8_SRGB — vkCmdCopyImageToBuffer gives BGRA
data. Was converting BGRA→RGBA in C# and telling FFmpeg 'rgba' —
but SRGB data needs to stay as-is. Now:
- No BGRA→RGBA swap in ReadCapturedBuffer
- FFmpeg pixel_format=bgra (matches swapchain)
- FFmpeg handles SRGB→yuv420p conversion correctly
2026-06-19 09:07:19 +03:00
emil28092005 b41e64db4a fix: CaptureFrame no longer overwrites CapturedFrame with null
Root cause: CaptureFrame returned null and was assigned to CapturedFrame,
overwriting the data that ReadCapturedBuffer had just set at the start
of the same frame.

Fix: CaptureFrame is now void — only records GPU copy commands.
ReadCapturedBuffer runs at start of next frame (after WaitFrame/fence)
and sets CapturedFrame with actual pixel data.
Program.cs reads CapturedFrame after RenderWorld — now contains data
from the previous frame's GPU copy.
2026-06-19 09:04:03 +03:00
emil28092005 ab1a2d05c3 fix: enable imageCubeArray feature + transition all 24 shadow layers
Two validation errors fixed:
1. imageCubeArray feature not enabled → added VkPhysicalDeviceImageCubeArrayFeatures
   to device creation chain (pNext: cubeArray → sync2 → dynamicRendering)
2. Shadow layers 18-23 in UNDEFINED layout → transition ALL 24 layers
   (MaxShadowLights * 6) instead of only numShadowLights * 6
   Descriptor references all 24 layers via CubeArray view
2026-06-19 08:59:22 +03:00
emil28092005 d7b6851217 fix: replace all 0x2000 with 0x1000 in capture barriers
0x2000 = VK_ACCESS_2_HOST_READ_BIT (wrong for GPU transfer)
0x1000 = VK_ACCESS_2_TRANSFER_WRITE_BIT (correct for copy operations)
2026-06-19 08:56:25 +03:00
emil28092005 9b80ab7881 fix: video recording — proper layout transitions + deferred buffer read
Three validation errors fixed:
1. vkCmdCopyImageToBuffer inside render pass → moved AFTER vkCmdEndRendering
2. Swapchain images missing TRANSFER_SRC usage → added to swapchain creation
3. Wrong layout transitions → ColorAttachment→TransferSrc→copy→ColorAttachment

Architecture change:
- CaptureFrame now only records commands (returns null)
- ReadCapturedBuffer called at start of NEXT frame (after WaitFrame/fence)
  GPU has finished by then, safe to map memory
- CapturedFrame available 1 frame late (acceptable for video)
- Image transitions: ColorAttachmentOptimal→TransferSrcOptimal→ColorAttachmentOptimal
  (present transition still works because image is back in ColorAttachmentOptimal)
2026-06-19 08:52:41 +03:00
emil28092005 b6299e8048 fix: capture video frame before ImGui — no UI in recordings
Moved CaptureFrame call before _imGui.Render() so the screenshot
buffer contains only the 3D scene without ImGui overlay.
2026-06-19 08:46:22 +03:00
emil28092005 ace815c77d feat: video recording via FFmpeg pipe
- vkCmdCopyImageToBuffer added to Vk.cs
- VulkanRenderer.CaptureFrame: copies swapchain image to staging buffer,
  converts BGRA→RGBA, returns byte[]
- ImGui 'Video Recording' panel with Start/Stop buttons
- Start: launches FFmpeg process (rawvideo rgba → libx264 mp4)
- Each 2nd frame: writes RGBA pixels to FFmpeg stdin pipe
- Output: Videos/cortex_<timestamp>.mp4 at 30fps
- Stop: closes stdin, waits for FFmpeg to finish
- Requires ffmpeg installed (already available on system)
- Captures at 1280x720 resolution
2026-06-19 08:37:37 +03:00
emil28092005 c3cca30bba feat: multi-light system with per-light cubemap array shadows
Architecture:
- SceneUBO (448B): mat4 vp + numLights + numShadowLights + LightData[8] + shadowParams[4]
- LightData: vec4 posAndIntensity + vec4 colorAndRange (32B per light)
- Push constants (main): mat4 model only (64B) — all light data in UBO
- Push constants (shadow): model(64) + lightViewProj(64) + lightPos(16) + shadowParams(16) = 160B
- Shadow: cubemap array (24 layers = 4 lights × 6 faces), samplerCubeArray in shader
- Shadow passes: numShadowLights × 6 faces (2 lights = 12 passes)
- Fragment shader: loop over lights, calcPBR per light, shadow from corresponding cubemap layer

New OBJ files with verified CCW winding:
- pyramid.obj, diamond.obj, torus.obj, cone.obj, sphere.obj

Scene 'Chaos Architecture':
- 28 sphere pyramid (physics)
- 30 falling diamonds (physics)
- 8 floating torus rings (decorative)
- 4 pyramid decorations (static)
- 4 cone pillars (static)
- Central torus knot (floating)
- 2 dynamic point lights (warm + cool), both cast shadows
- Both lights orbit in chaotic circles

ImGui panel:
- Per-light intensity, range, RGB sliders
- Shadow bias, sample radius, far plane sliders

227 tests, all pass
2026-06-19 07:15:29 +03:00
emil28092005 19bed7fe1a fix: regenerate all OBJ files with verified CCW winding + remove grid
- pyramid.obj: verified CCW via cross-product for all 6 faces
- diamond.obj: verified CCW for all 8 octahedron faces
- torus.obj: verified CCW — normal points outward from torus center
- cone.obj: verified CCW for side faces (radial outward) and bottom (-Y)
- Removed Grid entity — balls were falling through it (no physics)
- Removed 'Grid' from castShadow exclusion list
- All objects now render correctly with CULL_MODE_BACK
2026-06-18 22:41:54 +03:00
emil28092005 eb2cfec41f fix: reduce shadow bias to 0.0001 — near zero with back-face culling 2026-06-18 22:03:02 +03:00
emil28092005 576d4586a3 fix: reduce default shadow bias to 0.01 — minimal acne with back-face culling
Back-face culling eliminated most self-shadowing, so bias can be much
lower. Default 0.01 (was 0.08). Slider still allows 0.001-0.5 tuning.
2026-06-18 22:01:43 +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 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 a572f9d360 fix: transition all 6 cubemap layers — shadows now work on floor
Root cause: TransitionImageLayoutDepth used LayerCount=1, so only
layer 0 of the 6-layer cubemap was transitioned to ShaderReadOnlyOptimal.
Layers 1-5 stayed in Undefined/DepthStencilAttachmentOptimal → sampling
returned garbage → no shadows on surfaces facing those directions.

Fix: added layerCount parameter to TransitionImageLayoutDepth (default=1).
Shadow cubemap transitions use layerCount=6.
2026-06-18 21:15:50 +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 c32c75cd69 fix: single push constant range (0-160, Vertex|Fragment) — fixes validation errors
- Vulkan spec: each stage can only appear in ONE push constant range
- Merged 3 ranges into 1: offset=0, size=160, stageFlags=Vertex|Fragment
- Both main and shadow pipelines use same single range
- Push constants packed into 160-byte buffer and sent in one call
- Shadow pass: restored vertex/index buffer binding + push constants
- Fixes: device lost, validation errors, missing shadow geometry
2026-06-18 20:21:54 +03:00
emil28092005 dd86f6e9ad fix: exclude Floor and Grid from shadow pass — no more blocked light
- drawCalls now include castShadow flag (bool)
- Floor and Grid entities: castShadow=false (skip in shadow pass)
- Shadow pass: foreach with 'if (!dc.castShadow) continue;'
- Main pass: renders all objects (floor receives shadows but doesn't cast)
2026-06-18 20:19:16 +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 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 ff8bec00f6 fix: use MemoryCopy instead of Marshal.StructureToPtr for UBO packing
- Marshal.StructureToPtr may not handle Matrix4x4 correctly (record struct)
- Replaced with MemoryCopy from local copies of vp, lightPos, lightColor
- Removed per-frame light debug logging
- Simple diffuse shader still active for testing
2026-06-18 19:35:44 +03:00
emil28092005 321b1f62cc debug: simple diffuse fragment shader to test point light UBO 2026-06-18 19:33:37 +03:00
emil28092005 e6cc9e6c4e feat: dynamic point light on a physics ball — falls and illuminates
- LightBall entity: sphere mesh + DynamicSphere physics + Light.Point
- Renderer reads light position from Transform (not static Light.Position)
  so the light moves with the physics body
- Light intensity 20, range 30, warm color (1.0, 0.9, 0.7)
- Ball falls from y=15, bounces on floor, light follows it
- Renderer: prefers Light+Transform pair for dynamic position, falls back to Light only
2026-06-18 19:25:02 +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 bfde04b381 feat: ImGui debug overlay — FPS, camera position, entity count
- ImGui.NET 1.91.6.1 integrated into Engine.Graphics.Vulkan
- VulkanImGui.cs: font atlas upload (staging → VkImage), sampler,
  descriptor set (COMBINED_IMAGE_SAMPLER), separate pipeline with
  alpha blending, no depth write, dynamic scissor
- ImGui shaders: imgui.vert (ortho MVP push constant) + imgui.frag
  (font texture sampler)
- Dynamic vertex/index buffers (HOST_VISIBLE, grow on demand)
- IRenderer interface: BeginImGuiFrame/EndImGuiFrame default methods
- Program.cs: debug window with FPS, camera pos/target, entity count
- 0 critical validation errors, ~750 FPS with physics + ImGui
2026-06-18 17:34:42 +03:00
emil28092005 a4914f28c5 feat: ECS scene with 7 objects — torus knot + 6 rotating cubes
- VulkanRenderer: mesh cache by entity ID, lazy buffer creation
- RenderWorld iterates entities with Transform+Mesh, draws each with
  per-entity model matrix via push constants
- Program.cs: creates scene with 1 torus knot (center, scale 1.5) +
  6 cubes at different positions, all rotating around Y at different speeds
- Camera at (0, 2, -8) looking at origin, FreeFly controls
- 0 validation errors, ~2100 FPS with 7 objects
2026-06-18 17:00:14 +03:00
emil28092005 44bae31495 fix: flip projection Y for Vulkan (M22 *= -1) — fixes fish-eye distortion
System.Numerics.CreatePerspectiveFieldOfView uses DirectX convention (Y up).
Vulkan clip space has Y down. Negating M22 corrects the projection.
Depth is already [0,1] in System.Numerics (DirectX-style), no remap needed.
2026-06-18 16:53:48 +03:00
emil28092005 fcbc42959e feat: FreeFlyCameraController — interactive WASD + mouse look
- Program.cs: create Camera entity in ECS World, FreeFlyCameraController
  with WASD movement, Q/E up/down, Shift boost, right-click mouse look
- VulkanRenderer.RenderWorld: queries Camera from World, computes VP
  from Camera.GetViewMatrix() * GetProjectionMatrix()
- Camera aspect ratio auto-updated on window resize
- Fallback to hardcoded VP if no Camera entity found
- Torus knot still rotating via push constant model matrix
- 0 validation errors
2026-06-18 16:49:54 +03:00
emil28092005 2886dd77f2 feat: render torus knot (4800 verts, 9600 faces, smooth normals)
- Load Content/torusknot.obj instead of cube.obj
- 28800 vertices (duplicated per-face), 28800 indices
- OBJ has vn lines so smooth normals are used (not face normals)
- Removed slide animation, kept Y-axis rotation
- Camera z=-6, FOV 45°
- 0 validation errors, ~3000 FPS
2026-06-18 16:46:27 +03:00
emil28092005 0978b82322 fix: remove projection matrix hacks — back to clean FOV 45° + z=-5
- Removed M22 Y-flip and M33/M43 depth remap that caused edge distortion
- System.Numerics.CreatePerspectiveFieldOfView works as-is with row_major
- FOV 45°, camera z=-5, 1 cube sliding left-right + rotating
2026-06-18 13:29:26 +03:00
emil28092005 00f23187d5 feat: 1 cube slowly sliding left-right (sin wave) + rotating 2026-06-18 13:27:36 +03:00
emil28092005 a801e7fb16 fix: Vulkan projection matrix — Y-flip + depth [0,1] remap
- Negate proj.M22 for Vulkan Y-down coordinate system
- Remap depth from OpenGL [-1,1] to Vulkan [0,1]: M33*0.5+0.5, M43*0.5+0.5
- FOV 45°, camera z=-5, cubes at x=±2
- 0 validation errors
2026-06-18 13:25:59 +03:00
emil28092005 ce3dc8c41b fix: FOV 72°, camera z=-10, cubes 6 units apart 2026-06-18 13:24:26 +03:00
emil28092005 1a277abc7a fix: wider FOV (81°) for more distance 2026-06-18 13:23:19 +03:00
emil28092005 c9358bf080 fix: wider FOV (60°) + camera z=-6 for better view 2026-06-18 13:22:41 +03:00
emil28092005 51f94b8923 fix: move camera to z=-12 2026-06-18 13:21:39 +03:00
emil28092005 45fcccbd34 fix: move camera to z=-7 for more distance 2026-06-18 13:20:50 +03:00
emil28092005 edfcf33578 feat: render 2 rotating cubes side by side
- Push constant changed from float angle (4B) to mat4 model (64B)
- Two draw calls with different model matrices (x=-1.5 and x=+1.5)
- Both cubes rotate around Y axis at 0.2 rad/s
- 0 validation errors
2026-06-18 13:20:22 +03:00
emil28092005 567d88825d fix: slow down rotation to 0.2 rad/s 2026-06-18 13:18:45 +03:00
emil28092005 f8cbacb422 feat: slow cube rotation (0.5 rad/s) 2026-06-18 13:16:46 +03:00
emil28092005 441ce1f511 fix: move camera to z=-4 so cube is further away 2026-06-18 13:15:17 +03:00
emil28092005 bdd4d3eaf8 feat: cube from OBJ with depth buffer, no rotation
- Load Content/cube.obj via ObjLoader (36 vertices, 36 indices)
- Depth buffer re-enabled: depth test + write, CompareOp=Less
- Depth image transition + depth attachment in rendering info
- Push constant angle=0 (no rotation)
- Camera at z=-2, perspective FOV 45°
- UINT32 index type (fixed in previous commit)
- Face normals computed by ObjLoader (no vn in cube.obj)
- 0 validation errors
2026-06-18 13:14:13 +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