234 lines
8.0 KiB
Markdown
234 lines
8.0 KiB
Markdown
# 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<br/>Add Normals"] --> B["Phase 2<br/>Directional Light"]
|
||
B --> C["Phase 3<br/>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.
|