Flexibility¶
Freya exposes configuration and composition points so apps can tune the deferred stack without forking the renderer.
What you can customize¶
| Area | Mechanism |
|---|---|
| Window / lighting / shadows | FreyaOptions / FreyaOptionsBuilder |
| Post features | SetEnableShadows / SetEnableSsao / SetEnableTaa / SetEnableBloom |
| SPIR-V root | SetShaderRoot("./Resources/Shaders") |
| Frame graph | Renderer::InsertFrameStage / ReplaceFrameStage with Freya factories |
| Custom post shaders | PostProcessBuilder + BindMaterial (G-buffer albedo.a IDs) |
| Custom G-buffer shaders | MaterialTechniqueRegistry + MaterialCreateInfo::techniqueId |
| Textures from memory | TexturePool::CreateTextureFromMemory |
| Meshes from memory | MeshPool::CreateMesh(vertices, indices) |
| Materials | MaterialCreateInfo (AO map, packed MR, unlit, double-sided) |
| Model import | MeshPool::CreateModelFromFile (Assimp PBR + textures) |
Feature flags¶
freya.WithOptions([](fra::FreyaOptionsBuilder& o) {
o.SetEnableSsao(true)
.SetEnableTaa(true)
.SetEnableBloom(false)
.SetShaderRoot("./Resources/Shaders")
.SetShadowQuality(fra::ShadowQuality::High)
.SetSsaoQuality(fra::SsaoQuality::Medium)
.SetTaaQuality(fra::TaaQuality::High)
.SetBloomQuality(fra::BloomQuality::Medium);
});
Quality presets¶
Each effect has a Low / Medium / High / Ultra / Off preset (same idea as shadows). Presets write the per-effect knobs in FreyaOptions (startup via FreyaOptionsBuilder); individual setters can override afterward. At runtime prefer Renderer::Set*Quality over mutating FreyaOptions fields directly — resolution / pass rebuilds are handled there. Off clears the matching enable* flag without wiping resolution / strength knobs.
| API | Controls |
|---|---|
SetShadowQuality | map res, cascades, spot/point slots, soft taps; Off skips maps |
SetSsaoQuality | resolution divisor (1/2/4), radius, bias, power, intensity |
SetTaaQuality | current-frame blend weight, Halton period |
SetBloomQuality | resolution divisor, threshold, extract scale, composite strength |
Runtime (after build):
mRenderer->SetSsaoQuality(fra::SsaoQuality::Ultra);
mRenderer->SetTaaQuality(fra::TaaQuality::Off);
mRenderer->SetBloomQuality(fra::BloomQuality::High);
mRenderer->SetShadowQuality(fra::ShadowQuality::Off);
Resolution changes rebuild the related pass; TAA weight / bloom strength update without a full rebuild when the divisor is unchanged. Toggling Off ↔ any quality rebuilds SSAO/TAA/Bloom as needed; shadow Off only gates the pass and clears castShadows in the light UBO.
Defaults match the previous always-on post stack. Disabling SSAO still runs lighting with a white AO fallback. Disabling TAA skips Halton jitter. Disabling bloom clears the bloom tap so composite stays dark for that input.
TAA motion vectors use G-buffer velocity (current unjittered VP vs prevViewProjection, plus per-instance previous model matrices). Prefer Renderer::UploadSceneInstances so Freya maintains that history; drawing without the scene-upload API will not supply correct object motion alone.
DI configure hooks¶
.WithExtension<fra::FreyaExtension>([](fra::FreyaExtension& freya) {
freya.WithOptions([](fra::FreyaOptionsBuilder& o) {
o.SetTitle("My App")
.SetVSync(true)
.SetShadowQuality(fra::ShadowQuality::High);
});
})
Present mode, GPU type, and device extensions are chosen inside the engine from FreyaOptions (vsync, sample count, quality presets). Vulkan builders are not part of the public extension API.
Multiple windows¶
AbstractApplication::CreateWindow opens another Freya window at runtime. Each window gets its own scoped Renderer / LightService / IndirectDrawSystem. Asset pools stay shared:
| Shared (singleton) | Per window (scoped) |
|---|---|
MeshPool, TexturePool, MaterialPool | Window, Renderer, SwapChain |
Device, Instance, IPlatform | LightService, ShadowPass, CommandPool |
auto window = CreateWindow([](fra::FreyaOptionsBuilder& o) {
o.SetTitle("Game View").SetWidth(1280).SetHeight(720);
});
// Shared mesh ids from the main window's MeshPool still work here.
// Close with window->Close().
Use GetMainServiceProvider() for scoped services belonging to the main window. Secondary windows use GetWindowServices(*window) / GetRenderer(*window).
Frame stages¶
Renderer::EndScene runs an ordered list of IFrameStage adapters:
GpuAnim → Pick → Shadow → DeferredGeometry → Ssao → Lighting → Taa → Translucent → Bloom → Composite → DebugDraw
Apps may insert Freya factories (PostProcess::MakeStage) or implement IFrameStage themselves. Execute / Rebuild receive a public StageContext (Vulkan-free): options, extent, frame index, G-buffer / HDR GpuImageRef taps, DispatchCull / ExecuteDraws, and opaque NativeCommandBuffer / NativeDevice (void* → cast to Vulkan in the app). ReplaceFrameStage rebuilds the new stage immediately (same as insert).
RenderFrameContext (pass pointers, typed vk:: fields) remains internal to Freya.
Custom lighting fragment¶
Deferred lighting uses stock DeferredCompressed/lighting.frag.spv. Override the fullscreen lighting fragment globally:
auto* lighting =
serviceProvider->GetService<fra::LightingTechniqueRegistry>().get();
lighting->SetFragment("Cell/lighting_cell.frag.spv");
mRenderer->RebuildSwapChain();
The custom SPIR-V must keep the stock descriptor layout (bindings 0–15), push constant debugMode, lighting.vert, and additive HDR output. Clear() restores the default. Shadow / pick / OIT / G-buffer stay on stock shaders unless also customized via MaterialTechniqueRegistry.
Custom post-process shaders¶
The deferred stack uses one G-buffer pipeline for all opaque draws. User shaders are post-process (fullscreen) effects that sample those targets via PostProcess + InsertFrameStage. SPIR-V is still compiled at CMake time (glslc); there is no runtime GLSL.
Put the fragment under Shaders/<Name>/ (or your own add_shader_target) and insert before Bloom so ACES / bloom see the result:
#include <Freya/Freya.hpp>
struct CellPushConstants
{
float bands = 4.0f;
float edgeDepthScale = 80.0f;
float edgeNormalScale = 2.0f;
float strength = 1.0f;
glm::vec4 edgeColor { 0.02f, 0.02f, 0.04f, 1.0f };
float reverseZ = 0.0f;
float shadowLift = 0.22f;
float edgeWidth = 1.0f;
};
auto cell = serviceProvider->GetService<fra::PostProcessBuilder>()
->SetName("Cell")
.SetFragment("Cell/cell.frag.spv")
.SetInputs({ fra::PostProcessInput::SceneColor,
fra::PostProcessInput::Depth,
fra::PostProcessInput::Normal })
.SetPushConstantSize(sizeof(CellPushConstants))
.Build();
CellPushConstants params {};
params.reverseZ = options->ReverseZ ? 1.0f : 0.0f;
cell->SetPushConstants(params);
cell->BindMaterial(bodyMaterial); // albedo.a material ID; omit = all pixels
mRenderer->InsertFrameStage("Bloom", cell->MakeStage());
SetInputs order is descriptor binding order (set = 0). Vertex defaults to DeferredCompressed/composing.vert.spv (fullscreen triangle). Set 1 is always G-buffer albedo (binding 0, material ID in .a) and a PostProcessMaterialMask UBO (binding 1). BindMaterial / UnbindMaterial / ClearMaterials fill that mask (count == 0 means every pixel). IDs are 0–255 (G-buffer albedo.a is R8G8B8A8Unorm; IDs ≥ 256 alias). The pass writes an HDR ping-pong image and blits back onto the current scene HDR (OIT composite, else TAA, else deferred scene color).
Push-constant layouts are app-defined POD matching the fragment shader; Freya does not ship effect-specific constant structs.
IndustrialPipeLamp is the PBR deferred reference. Cell + edges lives in Examples/CellBulbasaur/ (F4 toggles the effect on the Bulbasaur materials, not the ground).
Custom material G-buffer shaders¶
Opaque draws use stock DeferredCompressed/gbuffer.frag (technique 0). Register alternate fragments that keep the same vertex inputs and G-buffer attachments, then assign MaterialCreateInfo::techniqueId:
auto* techniques =
serviceProvider->GetService<fra::MaterialTechniqueRegistry>().get();
const auto cellTech =
techniques->Register("CellGBuffer", "Cell/gbuffer_cell.frag.spv");
// Rebuild deferred pipelines after Register (e.g. RebuildSwapChain).
auto mat = materialPool->Create({
.albedo = albedoTex,
.techniqueId = cellTech,
});
Up to kMaxMaterialTechniques (8) slots. Shadow / pick / OIT stay on stock shaders; lighting uses LightingTechniqueRegistry (default stock). Custom G-buffer fragments must still write albedo+matID, normal+flags, PBR, emissive HDR, and velocity.
Stock technique frags under Shaders/Material/:
| SPIR-V | Role |
|---|---|
Material/unlit_emissive.frag.spv | Skip lighting; albedo+emissive → HDR |
Material/triplanar.frag.spv | World-space triplanar albedo/normal |
Cell/gbuffer_cell.frag.spv | Matte PBR (pairs with cell lighting / post) |
Cell/lighting_cell.frag.spv | Cel-banded deferred lighting override |
Stock post frags under Shaders/Post/ (and Cell/):
| SPIR-V | Inputs | Notes |
|---|---|---|
Post/outline.frag.spv | Scene, Depth, Normal | Depth/normal edges; BindMaterial optional |
Post/color_grade.frag.spv | Scene | Contrast / sat / exposure / vignette / lift-gain |
Post/underwater.frag.spv | Scene, Depth | Wave warp + tint + depth fog |
Post/heat_haze.frag.spv | Scene, Depth | Shimmer; mask with BindMaterial |
Post/glow.frag.spv | Scene, Depth | Item highlight aura; requires BindMaterial |
Post/mu_item_glow.frag.spv | Scene, Depth | Mu Online +0…+13 tiers + wave flash; BindMaterial |
| Cell/cell.frag.spv | Scene, Depth, Normal | Cel bands + edges |
Animated effects (underwater, heat_haze) expect a time field in push constants — update each frame via SetPushConstants.
Residual roadmap¶
Not in this release (documented for planning):
- IBL cubemap (equirect GGX prefilter is in; cube faces still open)
- Clustered lighting (naive fullscreen loop;
kMaxLights= 64) - HDR / wide color-space swapchain policy
- Event unsubscribe API
WBOIT translucency supports physical glass: transmission / ior on MaterialCreateInfo drive screen-space refraction of opaque HDR plus IBL in oit_accum. Non-transmission AlphaMode::Blend keeps the weighted coverage path. Instances use a dedicated MDI cull (CullMode::Translucent), resolve over TAA/opaque before Bloom, then Composite tonemaps the combined HDR scene.
See also API boundary.