Add Studio2 diffuse irradiance IBL
This commit is contained in:
parent
1bd080da1d
commit
374a4a0cea
7 changed files with 74 additions and 5 deletions
|
|
@ -177,6 +177,24 @@ def prefilter_face(env: np.ndarray, face: int, size: int, roughness: float, samp
|
|||
return result / np.maximum(weight_sum, 1e-6)
|
||||
|
||||
|
||||
def irradiance_face(env: np.ndarray, face: int, size: int, sample_count: int) -> np.ndarray:
|
||||
n = face_directions(face, size)
|
||||
tangent, bitangent = tangent_basis(n)
|
||||
result = np.zeros((size, size, 3), dtype=np.float32)
|
||||
|
||||
for xi1, xi2 in hammersley(sample_count):
|
||||
radius = math.sqrt(float(xi1))
|
||||
phi = 2.0 * math.pi * float(xi2)
|
||||
direction = normalize(
|
||||
tangent * (math.cos(phi) * radius)
|
||||
+ bitangent * (math.sin(phi) * radius)
|
||||
+ n * math.sqrt(max(0.0, 1.0 - float(xi1)))
|
||||
)
|
||||
result += sample_latlong(env, direction)
|
||||
|
||||
return result * (math.pi / sample_count)
|
||||
|
||||
|
||||
def dds_header(width: int, height: int, mip_count: int) -> bytes:
|
||||
flags = 0x1 | 0x2 | 0x4 | 0x8 | 0x1000 | 0x20000
|
||||
caps = 0x1000 | 0x8 | 0x400000
|
||||
|
|
@ -223,6 +241,7 @@ def main() -> None:
|
|||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("output", type=Path)
|
||||
parser.add_argument("--mode", choices=["specular", "irradiance"], default="specular")
|
||||
parser.add_argument("--size", type=int, default=256)
|
||||
parser.add_argument("--samples", type=int, default=128)
|
||||
args = parser.parse_args()
|
||||
|
|
@ -231,6 +250,14 @@ def main() -> None:
|
|||
raise ValueError("--size must be a power of two")
|
||||
|
||||
env = read_radiance_hdr(args.input)
|
||||
if args.mode == "irradiance":
|
||||
print(f"irradiance: {args.size}x{args.size}, samples={args.samples}")
|
||||
write_dds(
|
||||
args.output,
|
||||
[[irradiance_face(env, face, args.size, args.samples) for face in range(6)]])
|
||||
print(f"wrote {args.output}")
|
||||
return
|
||||
|
||||
mip_count = int(math.log2(args.size)) + 1
|
||||
mip_faces: list[list[np.ndarray]] = []
|
||||
for mip_index in range(mip_count):
|
||||
|
|
|
|||
BIN
src/Aquarium.Engine/Assets/Textures/studio2_irradiance.dds
Normal file
BIN
src/Aquarium.Engine/Assets/Textures/studio2_irradiance.dds
Normal file
Binary file not shown.
|
|
@ -41,6 +41,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
private const string SceneShaderRelativePath = "Render/Shaders/D3D12Scene.hlsl";
|
||||
private const string PostShaderRelativePath = "Render/Shaders/D3D12Post.hlsl";
|
||||
private const string StudioPmremRelativePath = "Assets/Textures/studio2_pmrem.dds";
|
||||
private const string StudioIrradianceRelativePath = "Assets/Textures/studio2_irradiance.dds";
|
||||
private static readonly DebugUi.DebugUiOption[] RenderDebugOptions =
|
||||
[
|
||||
new(0, "Final"),
|
||||
|
|
@ -95,6 +96,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
private readonly D3D12RenderTarget[] bloomScratchTargets = new D3D12RenderTarget[BloomLevelCount];
|
||||
private readonly D3D12StructuredBuffer bodyLightBuffer;
|
||||
private readonly D3D12CubeTexture studioPmremTexture;
|
||||
private readonly D3D12CubeTexture studioIrradianceTexture;
|
||||
private readonly BodyLightGpu[] bodyLights = new BodyLightGpu[BodyLightCount];
|
||||
private readonly GridHeightBrushCpu[] gridHeightBrushes = new GridHeightBrushCpu[GridHeightBrushCount];
|
||||
private Viewport viewport;
|
||||
|
|
@ -192,9 +194,11 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
fence.Name = "Aquarium D3D12 Frame Fence";
|
||||
commandQueue.ExecuteCommandList(commandList);
|
||||
WaitForGpu();
|
||||
ReportStartupProgress(startupProgress, "Loading studio PMREM cubemap");
|
||||
ReportStartupProgress(startupProgress, "Loading studio IBL cubemaps");
|
||||
studioPmremTexture = LoadStudioPmremTexture();
|
||||
resourceRegistry.Add("studio-pmrem-cubemap", studioPmremTexture);
|
||||
studioIrradianceTexture = LoadStudioIrradianceTexture();
|
||||
resourceRegistry.Add("studio-irradiance-cubemap", studioIrradianceTexture);
|
||||
ReportStartupProgress(startupProgress, "Creating D3D12 render pipelines");
|
||||
fullscreenRootSignature = CreateFullscreenRootSignature();
|
||||
fullscreenRootSignature.Name = "Aquarium D3D12 Fullscreen Root Signature";
|
||||
|
|
@ -316,6 +320,8 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
bodyLightBuffer.CreateShaderResourceView(device, frameResources.BodyLightDescriptor);
|
||||
frameResources.StudioPmremDescriptor = frameResources.TransientShaderDescriptors.Allocate();
|
||||
studioPmremTexture.CreateShaderResourceView(device, frameResources.StudioPmremDescriptor);
|
||||
frameResources.StudioIrradianceDescriptor = frameResources.TransientShaderDescriptors.Allocate();
|
||||
studioIrradianceTexture.CreateShaderResourceView(device, frameResources.StudioIrradianceDescriptor);
|
||||
frameResources.GridHeightDescriptor = frameResources.TransientShaderDescriptors.Allocate();
|
||||
gridHeightRenderTarget.CreateShaderResourceView(device, frameResources.GridHeightDescriptor);
|
||||
frameResources.SceneDescriptor = frameResources.TransientShaderDescriptors.Allocate();
|
||||
|
|
@ -432,6 +438,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
overlayDevice.Dispose();
|
||||
DisposePipelineStates();
|
||||
fullscreenRootSignature.Dispose();
|
||||
studioIrradianceTexture.Dispose();
|
||||
studioPmremTexture.Dispose();
|
||||
bodyLightBuffer.Dispose();
|
||||
DisposeBloomRenderTargets();
|
||||
|
|
@ -469,16 +476,26 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
|
||||
private D3D12CubeTexture LoadStudioPmremTexture()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, StudioPmremRelativePath);
|
||||
return LoadStudioCubeTexture(StudioPmremRelativePath, "Studio PMREM", "Aquarium D3D12 Studio PMREM Cubemap");
|
||||
}
|
||||
|
||||
private D3D12CubeTexture LoadStudioIrradianceTexture()
|
||||
{
|
||||
return LoadStudioCubeTexture(StudioIrradianceRelativePath, "Studio irradiance", "Aquarium D3D12 Studio Irradiance Cubemap");
|
||||
}
|
||||
|
||||
private D3D12CubeTexture LoadStudioCubeTexture(string relativePath, string label, string resourceName)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, relativePath);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("Studio PMREM cubemap asset was not copied to the runtime asset directory.", path);
|
||||
throw new FileNotFoundException($"{label} cubemap asset was not copied to the runtime asset directory.", path);
|
||||
}
|
||||
|
||||
var frameResources = frames[frameIndex];
|
||||
frameResources.CommandAllocator.Reset();
|
||||
commandList.Reset(frameResources.CommandAllocator, null);
|
||||
var texture = D3D12CubeTexture.LoadRgba16FloatDds(device, commandList, path, "Aquarium D3D12 Studio PMREM Cubemap", out var uploadResource);
|
||||
var texture = D3D12CubeTexture.LoadRgba16FloatDds(device, commandList, path, resourceName, out var uploadResource);
|
||||
commandList.Close();
|
||||
commandQueue.ExecuteCommandList(commandList);
|
||||
WaitForGpu();
|
||||
|
|
@ -793,6 +810,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
context.CommandList.SetGraphicsRootDescriptorTable(1, frameResources.GridHeightDescriptor.Gpu);
|
||||
context.CommandList.SetGraphicsRootDescriptorTable(3, frameResources.BodyLightDescriptor.Gpu);
|
||||
context.CommandList.SetGraphicsRootDescriptorTable(14, frameResources.StudioPmremDescriptor.Gpu);
|
||||
context.CommandList.SetGraphicsRootDescriptorTable(15, frameResources.StudioIrradianceDescriptor.Gpu);
|
||||
context.CommandList.RSSetViewports(viewport);
|
||||
context.CommandList.RSSetScissorRects(scissorRect);
|
||||
context.CommandList.OMSetRenderTargets(
|
||||
|
|
@ -1459,6 +1477,12 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
22,
|
||||
0,
|
||||
D3D12.DescriptorRangeOffsetAppend);
|
||||
var studioIrradianceRange = new DescriptorRange(
|
||||
DescriptorRangeType.ShaderResourceView,
|
||||
1,
|
||||
23,
|
||||
0,
|
||||
D3D12.DescriptorRangeOffsetAppend);
|
||||
var rootParameters = new[]
|
||||
{
|
||||
new RootParameter(new RootDescriptorTable([constantBufferRange]), ShaderVisibility.All),
|
||||
|
|
@ -1476,6 +1500,7 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
new RootParameter(new RootDescriptorTable([historyEventColorRange]), ShaderVisibility.Pixel),
|
||||
new RootParameter(new RootDescriptorTable([historyEventMetadataRange]), ShaderVisibility.Pixel),
|
||||
new RootParameter(new RootDescriptorTable([studioPmremRange]), ShaderVisibility.Pixel),
|
||||
new RootParameter(new RootDescriptorTable([studioIrradianceRange]), ShaderVisibility.Pixel),
|
||||
};
|
||||
var staticSamplers = new[]
|
||||
{
|
||||
|
|
@ -1811,6 +1836,8 @@ public sealed class D3D12Renderer : IAquariumRenderer
|
|||
|
||||
public D3D12DescriptorSlot StudioPmremDescriptor { get; set; }
|
||||
|
||||
public D3D12DescriptorSlot StudioIrradianceDescriptor { get; set; }
|
||||
|
||||
public D3D12DescriptorSlot GridHeightDescriptor { get; set; }
|
||||
|
||||
public D3D12DescriptorSlot SceneDescriptor { get; set; }
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ cbuffer AquariumFrame : register(b0)
|
|||
|
||||
Texture2D<float4> gridHeightTexture : register(t0);
|
||||
TextureCube<float4> studioPmremTexture : register(t22);
|
||||
TextureCube<float4> studioIrradianceTexture : register(t23);
|
||||
SamplerState gridSampler : register(s0);
|
||||
|
||||
struct BodyLight
|
||||
|
|
@ -47,6 +48,7 @@ static const float GRID_HEIGHT_TEXEL_COUNT = 128.0;
|
|||
static const int BODY_LIGHT_COUNT = 8;
|
||||
static const float STUDIO_PMREM_MAX_LOD = 9.0;
|
||||
static const float STUDIO_PMREM_SPECULAR_INTENSITY = 0.34;
|
||||
static const float STUDIO_IRRADIANCE_INTENSITY = 0.74;
|
||||
static const float GRID_FLAT_REFLECTION_MAX_LOD = 3.0;
|
||||
static const float BACKGROUND_PMREM_LOD = 3.0;
|
||||
static const float BACKGROUND_PMREM_CONE = 0.16;
|
||||
|
|
@ -634,6 +636,16 @@ float3 studioPmremSpecularRadiance(float3 p, float3 normal, float roughness, flo
|
|||
return radiance * fresnel * STUDIO_PMREM_SPECULAR_INTENSITY;
|
||||
}
|
||||
|
||||
float3 studioIrradianceDiffuseRadiance(float3 p, float3 normal, float3 albedo, float roughness, float3 f0)
|
||||
{
|
||||
float3 viewDirection = normalize(cameraPosition - p);
|
||||
float ndv = saturate(dot(normal, viewDirection));
|
||||
float3 fresnel = fresnelSchlickRoughness(ndv, f0, roughness);
|
||||
float3 diffuseShare = 1.0 - fresnel;
|
||||
float3 irradiance = studioIrradianceTexture.SampleLevel(gridSampler, studioPmremDirection(normal), 0.0).rgb;
|
||||
return diffuseShare * albedo * irradiance * (STUDIO_IRRADIANCE_INTENSITY / PI);
|
||||
}
|
||||
|
||||
float3 shadeBody(float2 uv, float travel, float3 p, float3 normal, int primitiveId)
|
||||
{
|
||||
float fieldId = primitiveFieldId(primitiveId);
|
||||
|
|
@ -658,6 +670,7 @@ float3 shadeBody(float2 uv, float travel, float3 p, float3 normal, int primitive
|
|||
float3 dielectricF0 = 0.04;
|
||||
return emission
|
||||
+ albedo * bodyLightIrradianceAt(p, normal) / PI
|
||||
+ studioIrradianceDiffuseRadiance(p, normal, albedo, roughness, dielectricF0)
|
||||
+ studioPmremSpecularRadiance(p, normal, roughness, dielectricF0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,3 +13,4 @@
|
|||
{"timestamp":"2026-05-11T07:22:00+01:00","topic":"grid-flatness-roughness","lesson":"Grid reflection roughness is now slope-driven: flat regions sample higher Studio2 PMREM mips to avoid an over-literal HDRI mirror, while sloped or waved regions sharpen toward mip 0.","files":["src/Aquarium.Engine/Render/Shaders/D3D12Scene.hlsl","state/memory.json"]}
|
||||
{"timestamp":"2026-05-11T07:28:00+01:00","topic":"pmrem-background-mip","lesson":"Flat Grid reflection now tops out at Studio2 PMREM mip 3, and ray misses use Studio2 PMREM mip 3 as the soft background so the reflective Grid is not contrasted against black void. Low-resolution mip texels are hidden with a small multi-direction cone average rather than one magnified TextureCube sample.","files":["src/Aquarium.Engine/Render/Shaders/D3D12Scene.hlsl","state/memory.json"]}
|
||||
{"timestamp":"2026-05-11T07:36:00+01:00","topic":"studio2-pmrem-resolution","lesson":"Studio2 PMREM was rebaked at 512px per cubemap face with 10 mips, so mip 3 is 64px per face instead of 32px. Shader roughness LOD max moved to 9 to match the chain.","files":["src/Aquarium.Engine/Assets/Textures/studio2_pmrem.dds","src/Aquarium.Engine/Render/Shaders/D3D12Scene.hlsl","state/memory.json"],"source":"E:\\Projects\\Aetheria-Economy\\Assets\\Textures\\studio2.hdr"}
|
||||
{"timestamp":"2026-05-11T07:45:00+01:00","topic":"diffuse-ibl-irradiance","lesson":"Diffuse IBL is not GGX PMREM with a softer mip. Studio2 diffuse lighting now uses a separate irradiance cubemap baked by cosine-weighted hemisphere convolution, stores the Li*cos(theta) irradiance integral, and shades non-metal bodies with Fresnel-gated albedo/PI diffuse response.","files":["scripts/bake-pmrem-cubemap.py","src/Aquarium.Engine/Assets/Textures/studio2_irradiance.dds","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/Shaders/D3D12Scene.hlsl","state/map.yaml","state/memory.json"],"source":"E:\\Projects\\Aetheria-Economy\\Assets\\Textures\\studio2.hdr"}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ renderer_state:
|
|||
- Direct-traced Grid surface is a terminating perfect mirror over the scalar height field, sampling Studio2 PMREM mip 0.
|
||||
- SDF/analytic solids for Self, planets, and the cursor locator.
|
||||
- Body light buffer feeds Self emission and direct body lighting; future other Selves should join this light lane, not a generic field-instance table.
|
||||
- Studio2 PMREM DDS cubemap is loaded as a D3D12 TextureCube and sampled for roughness-mip PBR specular reflections.
|
||||
- Studio2 PMREM and irradiance DDS cubemaps are loaded as D3D12 TextureCubes; PMREM serves roughness-mip specular, irradiance serves Lambertian diffuse IBL for non-metal bodies.
|
||||
- DirectWrite/Direct2D debug UI panel with code-first controls bound to renderer state.
|
||||
- Global GraphicsSettings document feeding render debug, exposure, bloom intensity, and bloom veil.
|
||||
- Bundled blue-noise texture assets are retained for future dithering/noise work even though the removed medium path no longer samples them.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"Lighting is diegetic. Self is the initial emitter; later other Selves should emit through an explicit body light lane, not generic field instances or ambient convenience fill.",
|
||||
"Keep bundled blue-noise texture assets available for future dithering/noise work even when no current pass samples them.",
|
||||
"Specular IBL uses the Studio2 PMREM cubemap baked at 512px per face: bake HDR latlong radiance into roughness-indexed cubemap mips, load it as a D3D12 TextureCube, and sample it by reflection direction plus roughness LOD. Convert Aquarium's Z-up world directions to cubemap Y-up directions before sampling: (x, z, y). Grid mirror reflection samples mip 0 directly; body material reflections use roughness LOD. Keep this as material reflection response, not a replacement for diegetic body light authority.",
|
||||
"Diffuse IBL uses a separate Studio2 irradiance cubemap, not blurred GGX PMREM. Bake it by cosine-weighted hemisphere convolution storing irradiance integral Li*cos(theta) dω, then shade non-metal bodies with Fresnel-gated diffuse share times albedo/PI times irradiance.",
|
||||
"HDR/PBR presentation follows research/rendering/hdr-pbr-presentation.md: scene-linear lighting, explicit exposure, pre-tonemap bloom/veil, then display transform.",
|
||||
"Current temporal diagnostics are for inspection. With projection jitter disabled, final presentation should not blindly trust temporal color history until a real temporal contract earns authority."
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue