# Lighting & Skybox Plan ## Current State | Item | Current | |------|---------| | Vertex format | `float3 Position` + `float4 Color` (28 bytes, no normals) | | Vertex shader | [Triangle.vert.hlsl](file:///w:/Classified/Juliet/Assets/source/Triangle.vert.hlsl) — bindless buffer read, `mul(VP, mul(Model, pos))` | | Fragment shader | [SolidColor.frag.hlsl](file:///w:/Classified/Juliet/Assets/source/SolidColor.frag.hlsl) — passthrough `return Color` | | Push constants | `ViewProjection` + `Model` + `BufferIndex` + extras | | Texturing | None — no sampler usage, no texture reads | --- ## Phase 1: Add Normals to Vertex Data Lighting requires normals. This is the foundation for everything else. ### Vertex Format Change ```diff struct Vertex { float Position[3]; + float Normal[3]; float Color[4]; }; ``` - **Stride**: 28 → 40 bytes - Update `Triangle.vert.hlsl` stride constant: `uint stride = 40;` - Load normal after position: `float3 normal = asfloat(buffer.Load3(offset + 12));` - Load color shifts to: `float4 col = asfloat(buffer.Load4(offset + 24));` ### Files to modify | File | Change | |------|--------| | [VertexData.h](file:///w:/Classified/Juliet/Juliet/include/Graphics/VertexData.h) | Add `float Normal[3]` to `Vertex` | | [MeshRenderer.cpp](file:///w:/Classified/Juliet/Juliet/src/Graphics/MeshRenderer.cpp) | Update `AddCube()` to include per-face normals | | [Triangle.vert.hlsl](file:///w:/Classified/Juliet/Assets/source/Triangle.vert.hlsl) | Update stride, load normal, pass to fragment | --- ## Phase 2: Basic Directional Light (Diffuse) Simple single directional light with diffuse (Lambert) shading. ### Approach: Push light data through RootConstants Add light direction and color to `RootConstants.hlsl` and the C++ `PushData`: ```hlsl // RootConstants.hlsl additions float3 LightDirection; // Normalized, world-space float _LightPad; float3 LightColor; float AmbientIntensity; ``` ```cpp // PushData additions Vector3 LightDirection; float _LightPad; Vector3 LightColor; float AmbientIntensity; ``` ### Shader Changes **Vertex shader** — transform normal to world space and pass it to fragment: ```hlsl // Triangle.vert.hlsl output struct struct Output { float4 Color : TEXCOORD0; float3 WorldNormal : TEXCOORD1; float4 Position : SV_Position; }; // In main(): float3 worldNormal = mul((float3x3)Model, normal); output.WorldNormal = worldNormal; ``` > [!NOTE] > Using `(float3x3)Model` for normal transform is only correct for uniform-scale transforms. For non-uniform scale, you'd need the inverse-transpose. Fine for now with translation-only transforms. **Fragment shader** — apply Lambert diffuse: ```hlsl // SolidColor.frag.hlsl → rename to Lit.frag.hlsl #include "RootConstants.hlsl" float4 main(float4 Color : TEXCOORD0, float3 WorldNormal : TEXCOORD1) : SV_Target0 { float3 N = normalize(WorldNormal); float NdotL = saturate(dot(N, -LightDirection)); float3 diffuse = Color.rgb * LightColor * NdotL; float3 ambient = Color.rgb * AmbientIntensity; return float4(diffuse + ambient, Color.a); } ``` ### Files to modify | File | Change | |------|--------| | [RootConstants.hlsl](file:///w:/Classified/Juliet/Assets/source/RootConstants.hlsl) | Add light params | | [MeshRenderer.h](file:///w:/Classified/Juliet/Juliet/include/Graphics/MeshRenderer.h) | Add light params to `PushData` | | [Triangle.vert.hlsl](file:///w:/Classified/Juliet/Assets/source/Triangle.vert.hlsl) | Pass world normal to fragment | | [SolidColor.frag.hlsl](file:///w:/Classified/Juliet/Assets/source/SolidColor.frag.hlsl) | Lambert diffuse + ambient | | [DebugDisplayRenderer.cpp](file:///w:/Classified/Juliet/Juliet/src/Graphics/DebugDisplayRenderer.cpp) | Update push data struct to match new layout | | [main.cpp](file:///w:/Classified/Juliet/JulietApp/main.cpp) | Set `LightDirection`, `LightColor`, `AmbientIntensity` | --- ## Phase 3: Skybox A skybox renders a cubemap texture behind all geometry, giving the scene a background. ### Approach: Fullscreen-triangle with inverse VP Render a fullscreen triangle as the very last thing (or first with depth write off), sample a cubemap using the camera view direction reconstructed from screen coordinates. ### New Assets - **Skybox cubemap texture** — a `.dds` or 6 `.png` face images loaded as a `TextureCube` - **New shaders**: `Skybox.vert.hlsl` + `Skybox.frag.hlsl` ### Shader Design **Vertex shader** — fullscreen triangle using `SV_VertexID`: ```hlsl // Skybox.vert.hlsl #include "RootConstants.hlsl" struct Output { float3 ViewDir : TEXCOORD0; float4 Position : SV_Position; }; Output main(uint vertexID : SV_VertexID) { Output output; // Fullscreen triangle float2 uv = float2((vertexID << 1) & 2, vertexID & 2); float4 clipPos = float4(uv * 2.0 - 1.0, 1.0, 1.0); clipPos.y = -clipPos.y; output.Position = clipPos; // Reconstruct view direction from clip space // InverseViewProjection needs to be added to RootConstants float4 worldPos = mul(InverseViewProjection, clipPos); output.ViewDir = worldPos.xyz / worldPos.w; return output; } ``` **Fragment shader** — sample cubemap: ```hlsl // Skybox.frag.hlsl #include "RootConstants.hlsl" float4 main(float3 ViewDir : TEXCOORD0) : SV_Target0 { TextureCube skybox = ResourceDescriptorHeap[TextureIndex]; SamplerState samp = SamplerDescriptorHeap[0]; // Linear clamp return skybox.Sample(samp, normalize(ViewDir)); } ``` ### Pipeline Requirements | Setting | Value | |---------|-------| | Depth write | **Off** (skybox is infinitely far) | | Depth test | **LessEqual** or **Off** (render behind everything) | | Cull mode | **None** (fullscreen triangle) | | Draw call | `Draw(3, 0)` — no vertex buffer needed | ### New C++ Components 1. **Cubemap loading** — need to create `TextureCube` from 6 face images or a `.dds` cubemap file 2. **Skybox pipeline** — new `GraphicsPipeline` with the skybox shaders and the pipeline settings above 3. **Sampler** — need at least one linear sampler in the sampler heap (may already exist for ImGui) 4. **`InverseViewProjection`** — add to `RootConstants` and compute in C++ via a `MatrixInverse` function > [!IMPORTANT] > `MatrixInverse` needs to be implemented in `Matrix.h`. This is a non-trivial 4×4 matrix inversion (adjugate/determinant method or Gauss-Jordan). ### Files to modify/create | File | Change | |------|--------| | [NEW] `Skybox.vert.hlsl` | Fullscreen triangle + view direction | | [NEW] `Skybox.frag.hlsl` | Cubemap sample | | [RootConstants.hlsl](file:///w:/Classified/Juliet/Assets/source/RootConstants.hlsl) | Add `InverseViewProjection` | | [Matrix.h](file:///w:/Classified/Juliet/Juliet/include/Core/Math/Matrix.h) | Add `MatrixInverse()` | | [MeshRenderer.h](file:///w:/Classified/Juliet/Juliet/include/Graphics/MeshRenderer.h) | Add `InverseViewProjection` to `PushData` | | [main.cpp](file:///w:/Classified/Juliet/JulietApp/main.cpp) | Create skybox pipeline, load cubemap, render skybox | --- ## Recommended Implementation Order ```mermaid graph LR A["Phase 1
Add Normals"] --> B["Phase 2
Directional Light"] B --> C["Phase 3
Skybox"] ``` 1. **Phase 1** (normals) is the smallest and unblocks Phase 2 2. **Phase 2** (lighting) gives the cubes visible 3D depth immediately 3. **Phase 3** (skybox) is independent of lighting but benefits from having `MatrixInverse` and sampler infrastructure > [!TIP] > Phases 1+2 together are a single session of work (~8 files). Phase 3 is larger due to cubemap loading and new pipeline creation. --- ## Open Questions (Answered) 1. **Cubemap source** — **Procedural gradient skybox** chosen because asset loading infrastructure is not yet established. 2. **Sampler heap** — Evaluate what exists. However, with a procedural skybox, we won't need a sampler for Phase 3! (The sky color can be procedurally generated from the reconstructed view direction). 3. **Specular** — **Blinn-Phong** specular, structured in an agnostic way so PBR (physically based rendering) parameters can be plugged in later.