Skip to main content

Advanced Rendering Techniques

Filament provides advanced rendering capabilities for demanding real-time graphics applications. This guide covers screen-space reflections, dynamic resolution scaling, instancing, and custom render targets.

Screen Space Reflections (SSR)

SSR provides real-time reflections by tracing rays in screen space, ideal for wet surfaces and polished materials.

Enabling SSR

if (params.ssr) {
    view->setScreenSpaceReflectionsOptions({
        .enabled = true,
        .thickness = 0.1f,      // Ray thickness for hit detection
        .bias = 0.01f,          // Depth bias to prevent artifacts
        .maxDistance = 3.0f,    // Maximum reflection distance
        .stride = 2.0f          // Ray marching step size
    });
}
Source: samples/material_sandbox.cpp:355,579 Screenshot Description: A glossy floor reflecting the scene above it, with reflections that accurately match the geometry and fade at grazing angles. Notice how reflections are limited to what’s visible on screen.

SSR Limitations

  • Only reflects what’s visible on screen
  • Cannot reflect off-screen objects
  • Performance intensive at high quality
  • Best combined with IBL for complete reflections

SSR Best Practices

  1. Combine with IBL: Use SSR for nearby reflections, IBL for distant ones
  2. Adjust roughness: SSR works best on smooth surfaces (low roughness)
  3. Limit distance: Keep maxDistance low to avoid artifacts
  4. Fade at edges: SSR naturally fades at screen edges

Dynamic Resolution Scaling

Dynamic resolution adjusts rendering resolution in real-time to maintain target frame rate.

Configuration

view->setDynamicResolutionOptions({
    .enabled = true,
    .minScale = 0.5f,        // Minimum resolution scale (50%)
    .maxScale = 1.0f,        // Maximum resolution scale (100%)
    .quality = View::QualityLevel::MEDIUM,
    .homogeneousScaling = false  // Allow non-uniform scaling
});

How It Works

  1. Monitor frame time each frame
  2. If frame time exceeds target, reduce resolution
  3. If frame time is below target, increase resolution
  4. Smoothly interpolate between scales
Screenshot Description: A comparison showing the same scene rendered at 100% resolution (left) and 50% resolution (right) with upscaling. The difference is subtle during motion but provides significant performance gains.

Use Cases

  • Maintaining 60fps on variable hardware
  • Mobile devices with thermal throttling
  • VR applications requiring consistent frame rates
  • Adaptive quality based on scene complexity

Hardware Instancing

Render thousands of similar objects efficiently using GPU instancing.

Basic Instancing

The hybrid_instancing.cpp sample demonstrates efficient instance rendering:
// Create instance buffer with transform data
std::vector<mat4f> transforms(INSTANCE_COUNT);
for (int i = 0; i < INSTANCE_COUNT; i++) {
    transforms[i] = mat4f::translation(float3(x, y, z)) * 
                    mat4f::scaling(scale);
}

// Configure instancing
RenderableManager::Builder(1)
    .boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }})
    .material(0, materialInstance)
    .geometry(0, PrimitiveType::TRIANGLES, vb, ib)
    .instances(INSTANCE_COUNT, transforms.data())
    .build(*engine, renderable);
Source Reference: samples/hybrid_instancing.cpp Screenshot Description: Thousands of small cubes rendered simultaneously, each with unique position and rotation, demonstrating efficient GPU instancing rendering 10,000+ objects at 60fps.

Advanced Instance Attributes

You can pass per-instance data beyond transforms:
struct InstanceData {
    mat4f transform;
    float4 color;
    float2 uvOffset;
};

// Set up instance buffer with custom attributes
VertexBuffer* instanceBuffer = VertexBuffer::Builder()
    .vertexCount(instanceCount)
    .bufferCount(1)
    .attribute(VertexAttribute::POSITION, 0, 
               VertexBuffer::AttributeType::FLOAT4, 0, sizeof(InstanceData))
    .attribute(VertexAttribute::COLOR, 0,
               VertexBuffer::AttributeType::FLOAT4, 64, sizeof(InstanceData))
    .build(*engine);

Custom Render Targets

The rendertarget.cpp sample shows how to render to custom textures for effects like mirrors or security cameras.

Creating a Render Target

// Create color texture
Texture* colorTexture = Texture::Builder()
    .width(1024)
    .height(1024)
    .levels(1)
    .format(Texture::InternalFormat::RGBA8)
    .usage(Texture::Usage::COLOR_ATTACHMENT | Texture::Usage::SAMPLEABLE)
    .build(*engine);

// Create depth texture
Texture* depthTexture = Texture::Builder()
    .width(1024)
    .height(1024)
    .levels(1)
    .format(Texture::InternalFormat::DEPTH24)
    .usage(Texture::Usage::DEPTH_ATTACHMENT)
    .build(*engine);

// Create render target
RenderTarget* renderTarget = RenderTarget::Builder()
    .texture(RenderTarget::AttachmentPoint::COLOR, colorTexture)
    .texture(RenderTarget::AttachmentPoint::DEPTH, depthTexture)
    .build(*engine);

offscreenView->setRenderTarget(renderTarget);
Source Reference: samples/rendertarget.cpp

Render Target Use Cases

  • Mirrors and Portals: Render scene from different viewpoint
  • Security Cameras: In-world camera feeds
  • Minimap: Top-down view of the scene
  • Post-Processing: Custom effects chains
  • Shadow Maps: (Filament handles this internally)
Screenshot Description: A scene showing a mirror on the wall reflecting the room, implemented using a custom render target that captures the scene from the mirror’s perspective.

Multiple Windows

The multiple_windows.cpp sample demonstrates rendering to multiple windows simultaneously:
// Create separate swap chains for each window
SwapChain* swapChain1 = engine->createSwapChain(nativeWindow1);
SwapChain* swapChain2 = engine->createSwapChain(nativeWindow2);

// Render to each window
if (renderer->beginFrame(swapChain1)) {
    renderer->render(view1);
    renderer->endFrame();
}

if (renderer->beginFrame(swapChain2)) {
    renderer->render(view2);
    renderer->endFrame();
}
Source Reference: samples/multiple_windows.cpp

Stereoscopic Rendering

The hellostereo.cpp sample shows VR/AR stereoscopic rendering:
// Configure stereoscopic rendering
view->setStereoscopicOptions({
    .enabled = true,
    .eyeCount = 2
});

// Set eye-specific cameras
camera->setEyeModelMatrix(0, leftEyeMatrix);
camera->setEyeModelMatrix(1, rightEyeMatrix);

camera->setCustomProjection(0, leftEyeProjection, 0.1f, 100.0f);
camera->setCustomProjection(1, rightEyeProjection, 0.1f, 100.0f);
Source Reference: samples/hellostereo.cpp Screenshot Description: Side-by-side stereo view showing slightly offset perspectives for left and right eyes, ready for VR headset display.

Asynchronous Resource Loading

The helloasync.cpp sample demonstrates non-blocking resource loading:
// Load texture asynchronously
std::async(std::launch::async, [engine, texture, data]() {
    Texture::PixelBufferDescriptor buffer(data, size,
        Texture::Format::RGB, Texture::Type::UBYTE,
        [](void* buf, size_t, void*) { free(buf); });
    
    texture->setImage(*engine, 0, std::move(buffer));
});

// Continue rendering while texture loads
// Filament will use a default texture until ready
Source Reference: samples/helloasync.cpp

Depth Testing & Stencil

The depthtesting.cpp sample shows advanced depth and stencil operations:
// Configure depth testing
materialInstance->setDepthCulling(true);
materialInstance->setDepthWrite(true);

// Depth comparison function
material->setDepthFunc(MaterialInstance::DepthFunc::LESS_EQUAL);

// Configure stencil operations
material->setStencilWrite(true);
material->setStencilCompareFunction(
    MaterialInstance::StencilCompareFunc::EQUAL,
    0xFF, 0xFF);
Source Reference: samples/depthtesting.cpp

Point Sprites

The point_sprites.cpp sample demonstrates efficient particle rendering:
// Create point sprite geometry
RenderableManager::Builder(1)
    .boundingBox({{ -10, -10, -10 }, { 10, 10, 10 }})
    .material(0, particleMaterial)
    .geometry(0, RenderableManager::PrimitiveType::POINTS, 
              vertexBuffer, nullptr, 0, particleCount)
    .culling(false)
    .build(*engine, particleRenderable);

// In material shader
material.pointSize = 10.0; // Point size in pixels
Source Reference: samples/point_sprites.cpp Screenshot Description: Thousands of glowing particles rendered as point sprites, creating a particle system effect with minimal geometry overhead.

View Layers

Control which renderables appear in which views:
// Assign renderable to specific layers
renderableManager.setLayerMask(instance, 0x3); // Layers 0 and 1

// Configure view to see specific layers
view->setVisibleLayers(0x1, 0x1); // See layer 0

Layer Use Cases

  • UI Layer: Render UI separately from world
  • Mirrors: Different layer for reflected objects
  • Debugging: Toggle debug visualization on/off
  • Optimization: Cull objects based on view type

Culling Optimization

// Frustum culling (automatic)
renderableManager.setCastShadows(instance, true);

// Custom bounding boxes for accurate culling
Box aabb = computeTightBoundingBox(mesh);
renderableManager.setAxisAlignedBoundingBox(instance, aabb);

// Disable culling for specific objects
RenderableManager::Builder(1)
    .culling(false)  // Always render
    .build(*engine, renderable);

Frame Graph & Custom Passes

While Filament manages the frame graph internally, you can customize rendering order:
// Render order priority
renderableManager.setPriority(instance, 7); // 0-7, higher renders later

// Useful for transparency sorting or overlay rendering

Material Variations

The materialinstancestress.cpp sample tests material instance performance:
// Create many material instances efficiently
std::vector<MaterialInstance*> instances;
for (int i = 0; i < 1000; i++) {
    MaterialInstance* mi = material->createInstance();
    mi->setParameter("baseColor", randomColor());
    mi->setParameter("roughness", randomFloat());
    instances.push_back(mi);
}
Source Reference: samples/materialinstancestress.cpp Screenshot Description: A stress test scene showing hundreds of spheres, each with unique material parameters, demonstrating efficient material instance management.

Heightfield Rendering

The heightfield.cpp sample shows efficient terrain rendering:
// Generate terrain mesh from heightmap
for (int z = 0; z < gridSize; z++) {
    for (int x = 0; x < gridSize; x++) {
        float height = heightmap[z * gridSize + x];
        vertices[index++] = float3(x, height, z);
    }
}

// Use index buffer for efficient triangle strips
Source Reference: samples/heightfield.cpp Screenshot Description: A procedurally generated terrain with smooth hills and valleys, efficiently rendered using a heightfield mesh with normal map details.

Performance Best Practices

Rendering Performance

  1. Batch Draw Calls: Use instancing for similar objects
  2. Minimize State Changes: Sort by material, then by mesh
  3. Frustum Culling: Provide tight bounding boxes
  4. LOD Systems: Use lower detail at distance
  5. Occlusion Culling: Remove hidden objects early

Memory Optimization

  1. Share Resources: Reuse materials and textures
  2. Compress Textures: Use ETC2, ASTC, or BC formats
  3. Stream Assets: Load/unload based on visibility
  4. Instance Data: Use instancing instead of duplicating geometry

Mobile Considerations

  1. Reduce Draw Calls: Critical on mobile GPUs
  2. Lower Texture Resolution: Use mipmaps effectively
  3. Simplify Shaders: Avoid complex material graphs
  4. Disable Expensive Effects: SSAO, SSR on low-end devices

  • gltf_instances.cpp - glTF instancing demonstration
  • suzanne.cpp - Advanced material and AO techniques
  • viewtest.cpp - Multiple view configuration testing
  • vbotest.cpp - Vertex buffer optimization patterns

Build docs developers (and LLMs) love