Skip to content

Assets

Freya manages meshes, textures, and PBR materials through pool services.

These pools are process-wide singletons on a shared Device. Multiple windows / renderers reuse the same mesh, texture, and material IDs. Scene state (instances, lights, cull) is per-window — see Core multi-window.

MeshPool

auto meshPool = serviceProvider->GetService<fra::MeshPool>();

// Static models: geometry + imported PBR materials (file / embedded textures,
// packed MR, glTF factors). Override per submesh when needed (e.g. lamp bulb).
auto model = meshPool->CreateModelFromFile("./Resources/Models/Helmet.glb");
for (const auto& part : model)
{
    instances.push_back({ .meshId = part.meshId,
                          .materialId = part.materialId, .entityId = id++ });
}

// Skinned: submeshes + materials, joints/weights, shared skeleton/clips.
fra::SkinnedModel fox =
    meshPool->CreateSkinnedModelFromFile("./Resources/Models/Fox.glb");
for (const auto& part : fox.submeshes)
{
    instances.push_back({ .meshId = part.meshId,
                          .materialId = part.materialId, .entityId = id++,
                          .boneOffset = 0, .boneCount = fox.skeleton.JointCount() });
}

// Full stack (AnimGraph, bake, CPU/GPU skin, LOD, look/IK): see Animation.
renderer->UploadBoneMatrices(fra::EvaluateSkeletonPose(
    fox.skeleton, fox.clips[0], timeSec));

// From memory (already CPU-side Freya vertices + uint32 indices)
std::uint32_t meshId = meshPool->CreateMesh(vertices, indices);

Static meshes leave Vertex::joints/weights at defaults and SceneInstanceUpload::boneOffset = fra::kNoSkin. Skinned draws use a second/prev bone palette for TAA velocity. Full animation docs: Animation.

Draw submission goes through Renderer::UploadSceneInstances (preferred) or the legacy Draw / DrawInstanced helpers.

TexturePool

auto texturePool = serviceProvider->GetService<fra::TexturePool>();

std::uint32_t fromFile =
    texturePool->CreateTextureFromFile("./Resources/Textures/albedo.png");

// RGBA8 (or other channel count) already in memory
std::vector<std::uint8_t> rgba = /* ... */;
std::uint32_t fromMemory = texturePool->CreateTextureFromMemory(
    rgba.data(), width, height, /*channels=*/4);

Both paths create a mipmapped image and linear anisotropic sampler.

MaterialPool

auto materialPool = serviceProvider->GetService<fra::MaterialPool>();

std::uint32_t material = materialPool->Create(fra::MaterialCreateInfo {
    .albedo     = albedoId,
    .normal     = normalId,
    .roughness  = roughnessId, // or packed ORM / metallicRoughness
    .emissive   = emissiveId,
    .metalness  = metalnessId, // ignored when packedMetallicRoughness
    .occlusion  = aoId,        // .r; packed ORM uses roughness.r if unset
    .albedoFactor    = { 1.f, 1.f, 1.f, 1.f },
    .roughnessFactor = 1.f,
    .metalnessFactor = 1.f,
    .emissiveFactor  = { 1.f, 1.f, 1.f },
    .aoFactor        = 1.f,   // multiplies sampled AO into G-buffer
    .alphaCutoff     = 0.f,   // Mask: discard when alpha < cutoff
    .alphaMode       = fra::AlphaMode::Opaque, // Opaque | Mask | Blend
    .clearcoat          = 0.f,    // deferred GGX clearcoat weight
    .clearcoatRoughness = 0.03f,  // glTF-style default
    .transmission       = 0.f,    // >0: OIT + screen-space refraction
    .ior                = 1.5f,   // refraction bend (glass ≈ 1.5)
    .packedMetallicRoughness = false, // G=rough, B=metal (glTF)
    .unlit                   = false, // skip lighting (emissive only)
    .doubleSided             = false, // G-buffer flips back-face N
    .receiveShadows          = true,
});

// albedo, normal, roughness, emissive, metalness, occlusion — missing slots skip.
std::uint32_t fromFiles = materialPool->CreateFromTextureFiles({
    "./Resources/Textures/albedo.png",
    "./Resources/Textures/normal.png",
    "./Resources/Textures/roughness.png",
    "./Resources/Textures/emissive.png",
    "./Resources/Textures/metalness.png",
    "./Resources/Textures/ao.png",
});

materialPool->Update(material, updatedCreateInfo);

AlphaMode::Opaque / Mask stay in the deferred MDI camera cull. Mask uses alphaCutoff cutout in the G-buffer and in the shadow pass (bindless albedo alpha), so foliage holes do not cast solid shadows. AlphaMode::Blend is filtered into the Weighted Blended OIT pass (CullMode::Translucent); use albedoFactor.a (and albedo alpha) for coverage, and typically castShadows = false on glass instances. transmission (>0) enables physical glass: OIT samples opaque HDR with a screen-space IOR bend and adds split-sum IBL; glTF KHR_materials_transmission / KHR_materials_ior map on import.

clearcoat (>0) enables a second dielectric GGX lobe in deferred lighting (F0=0.04). The weight is stored in G-buffer PBR.a. When coated, PBR.b holds clearcoatRoughness so the lighting pass does not need a material SSBO; otherwise PBR.b is material AO.

G-buffer / OIT bindless set 1:

Binding Content
0 sampler2D uTextures[] (bindless heap)
1 MaterialGPU SSBO

The lighting fullscreen pass uses only set 0 (G-buffer, lights, IBL, shadows). G-buffer albedo.a stores an 8-bit material ID (id / 255); PostProcess::BindMaterial masks against that channel (IDs ≥ 256 alias).

Empty texture optionals use engine fallbacks (white or black). Alpha cutout samples albedo alpha × albedoFactor.a in the G-buffer and shadow passes (Mask). Packed metallic-roughness maps (glTF): roughness from .g, metal from .b; AO from occlusion .r or the packed map .r when no separate occlusion texture is set. unlit skips deferred/OIT analytic lights (emissive still writes scene color). doubleSided is lit on back-faces via no-cull G-buffer + flipped N.

Vertex

struct Vertex
{
    glm::vec3 position;
    glm::vec3 color;
    glm::vec3 normal;
    glm::vec3 tangent;
    glm::vec2 texCoord;
};

Instancing (GPU-driven MDI)

Prefer Scene::Upload for application code (dirty-aware retained list). RendererAdvanced::UploadSceneInstances remains for tooling / tests that build a span without a Scene. Frustum cull (compute) atomic-compacts visible instances into multi-draw indirect commands.

Contract: prefer ascending entityId (Freya sorts when needed). TAA prevModel is resolved by entityId (first frame / new ids: prev == model).

#include <Freya/Advanced.hpp>

std::vector<fra::SceneInstanceUpload> instances;
instances.push_back({ .model = M, .mesh = mesh, .material = mat,
                      .entityId = id, .castShadows = true });
fra::Advanced(*renderer).UploadSceneInstances(instances);

Legacy path: SetInstanceModels + Draw / DrawInstanced still works and is expanded into UploadSceneInstances internally.