Remove imgui from ship release, script to export fast. Can read assets from dev folder and ship folder.
7.8 KiB
Lighting & Skybox Plan
Current State
| Item | Current |
|---|---|
| Vertex format | float3 Position + float4 Color (28 bytes, no normals) |
| Vertex shader | Triangle.vert.hlsl — bindless buffer read, mul(VP, mul(Model, pos)) |
| Fragment shader | 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
struct Vertex
{
float Position[3];
+ float Normal[3];
float Color[4];
};
- Stride: 28 → 40 bytes
- Update
Triangle.vert.hlslstride 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 | Add float Normal[3] to Vertex |
| MeshRenderer.cpp | Update AddCube() to include per-face normals |
| 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:
// RootConstants.hlsl additions
float3 LightDirection; // Normalized, world-space
float _LightPad;
float3 LightColor;
float AmbientIntensity;
// PushData additions
Vector3 LightDirection;
float _LightPad;
Vector3 LightColor;
float AmbientIntensity;
Shader Changes
Vertex shader — transform normal to world space and pass it to fragment:
// 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)Modelfor 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:
// 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 | Add light params |
| MeshRenderer.h | Add light params to PushData |
| Triangle.vert.hlsl | Pass world normal to fragment |
| SolidColor.frag.hlsl | Lambert diffuse + ambient |
| DebugDisplayRenderer.cpp | Update push data struct to match new layout |
| 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
.ddsor 6.pngface images loaded as aTextureCube - New shaders:
Skybox.vert.hlsl+Skybox.frag.hlsl
Shader Design
Vertex shader — fullscreen triangle using SV_VertexID:
// 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:
// 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
- Cubemap loading — need to create
TextureCubefrom 6 face images or a.ddscubemap file - Skybox pipeline — new
GraphicsPipelinewith the skybox shaders and the pipeline settings above - Sampler — need at least one linear sampler in the sampler heap (may already exist for ImGui)
InverseViewProjection— add toRootConstantsand compute in C++ via aMatrixInversefunction
Important
MatrixInverseneeds to be implemented inMatrix.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 | Add InverseViewProjection |
| Matrix.h | Add MatrixInverse() |
| MeshRenderer.h | Add InverseViewProjection to PushData |
| main.cpp | Create skybox pipeline, load cubemap, render skybox |
Recommended Implementation Order
graph LR
A["Phase 1<br/>Add Normals"] --> B["Phase 2<br/>Directional Light"]
B --> C["Phase 3<br/>Skybox"]
- Phase 1 (normals) is the smallest and unblocks Phase 2
- Phase 2 (lighting) gives the cubes visible 3D depth immediately
- Phase 3 (skybox) is independent of lighting but benefits from having
MatrixInverseand 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
- Cubemap source — procedural gradient skybox (no asset needed) vs actual HDR cubemap
.ddsfile? - Sampler heap — does the engine already have a linear sampler registered, or does one need to be created?
- Specular — want Blinn-Phong specular in Phase 2, or just diffuse + ambient for now?