Files
Juliet/AgentData/lighting_and_skybox_plan.md

293 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
## Phase 4: Forward Rendered Entity Lights
A simple, fast forward renderer using a Global Structured Buffer for entity-based point lights.
### Approach: Bindless Global Lights Buffer
Instead of passing each light via PushConstants, we pass a `StructuredBuffer<PointLight>` index and iterate over the active lights in the fragment shader.
### New C++ Components
```cpp
struct PointLight
{
Vector3 Position;
float Radius;
Vector3 Color;
float Intensity;
};
```
1. **Lights Buffer Allocation**: Allocate a `StructuredBuffer` large enough for all potential lights (e.g., max 1024).
2. **Buffer Upload**: Loop through the active game entity lights every frame and upload them using the TransferBuffer system.
3. **PushData Update**:
- `uint32 LightsBufferIndex`
- `uint32 ActiveLightCount`
### Shader Design
**Fragment Shader Expansion** loop over all `ActiveLightCount` to compute the attenuation and diffuse, then accumulate.
```hlsl
StructuredBuffer<PointLight> lights = ResourceDescriptorHeap[LightsBufferIndex];
float3 totalDiffuse = Color.rgb * LightColor * NdotL; // Include Directional Sun
for (uint i = 0; i < ActiveLightCount; ++i)
{
PointLight light = lights[i];
float3 lightVector = light.Position - WorldPosition;
float distance = length(lightVector);
if (distance > light.Radius) continue;
float3 L = lightVector / distance;
float NdotL = saturate(dot(N, L));
float attenuation = 1.0 - saturate(distance / light.Radius);
attenuation *= attenuation; // inverse square-ish
totalDiffuse += Color.rgb * light.Color * light.Intensity * NdotL * attenuation;
}
```
> [!NOTE]
> Passing `WorldPosition` to the Fragment shader from the Vertex shader is required for positional lighting.