Files
Juliet/AgentData/lighting_and_skybox_plan.md
Patedam f98be3c7f3 Made a ship version
Remove imgui from ship release, script to export fast. Can read assets from dev folder and ship folder.
2026-02-22 14:19:59 -05:00

7.8 KiB
Raw Blame History

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.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 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)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:

// 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 .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:

// 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

  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 Add InverseViewProjection
Matrix.h Add MatrixInverse()
MeshRenderer.h Add InverseViewProjection to PushData
main.cpp Create skybox pipeline, load cubemap, render skybox

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

  1. Cubemap source — procedural gradient skybox (no asset needed) vs actual HDR cubemap .dds file?
  2. Sampler heap — does the engine already have a linear sampler registered, or does one need to be created?
  3. Specular — want Blinn-Phong specular in Phase 2, or just diffuse + ambient for now?