Skip to content

Screen-Space Ambient Occlusion

This example compares a synthetic molecular aggregate with and without ambient occlusion.

Preview

Run And Adapt

Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.

Route Availability Command or action
C Canonical native source just example-c features/technique_ssao (build and run), or rerun ./build/examples/c/features/technique_ssao
Python Available; direct-engine adaptation python3 -m examples.python.gallery.features.technique_ssao
Browser Deferred Use the native route for this example.

This example is approved as a starting point for user code and coding agents. Keep the object lifetimes and data shapes intact while adapting the data and styling.

What To Look For

Both panels render the same irregular, multi-lobed aggregate of variable-sized spheres, while the right panel applies SSAO. Narrow clefts, recessed pockets, and near-contact atom clusters become easier to separate without a floor or external dataset. In live mode, use the GUI and linked arcball to inspect how radius, strength, bias, power, visibility, sample count, and blur affect local occlusion.

Source

#!/usr/bin/env python3
"""Screen-space ambient occlusion on a protein-like synthetic aggregate."""

from __future__ import annotations

import ctypes

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


LABELS = (b"Plain lighting", b"Ambient occlusion")
INITIAL_ANGLES = (ctypes.c_float * 3)(-0.239547, -0.407204, +0.824558)
INITIAL_PAN = (ctypes.c_float * 2)(0.0, -0.020)
INITIAL_ZOOM = 0.505017
AGGREGATE_TARGET_SPHERES = 2600
AGGREGATE_MAX_CANDIDATES = 200_000
AGGREGATE_HASH_STEP = 0.080


def _hash_u32(value: int) -> int:
    value &= 0xFFFFFFFF
    value ^= value >> 16
    value = (value * 0x7FEB352D) & 0xFFFFFFFF
    value ^= value >> 15
    value = (value * 0x846CA68B) & 0xFFFFFFFF
    value ^= value >> 16
    return value & 0xFFFFFFFF


def _hash_unit(value: int) -> float:
    return (_hash_u32(value) & 0x00FFFFFF) / 16777215.0


def _sphere_data():
    positions = []
    radii = []
    spatial_hash: dict[tuple[int, int, int], list[int]] = {}
    lobe_centers = (
        (-0.40, +0.12, -0.02),
        (+0.18, +0.17, +0.06),
        (+0.55, -0.24, -0.08),
        (-0.26, -0.38, +0.10),
        (+0.08, -0.18, -0.30),
    )
    lobe_radii = (
        (0.58, 0.45, 0.43),
        (0.54, 0.49, 0.46),
        (0.40, 0.34, 0.36),
        (0.43, 0.32, 0.35),
        (0.38, 0.32, 0.34),
    )

    for candidate in range(AGGREGATE_MAX_CANDIDATES):
        if len(positions) >= AGGREGATE_TARGET_SPHERES:
            break
        px = -1.05 + 2.10 * _hash_unit(4 * candidate + 0)
        py = -0.90 + 1.80 * _hash_unit(4 * candidate + 1)
        pz = -0.80 + 1.60 * _hash_unit(4 * candidate + 2)
        radius = 0.024 + 0.012 * _hash_unit(4 * candidate + 3)
        inside = any(
            ((px - cx) / rx) ** 2 + ((py - cy) / ry) ** 2 + ((pz - cz) / rz) ** 2 <= 1
            for (cx, cy, cz), (rx, ry, rz) in zip(lobe_centers, lobe_radii)
        )
        pocket0 = (px + 0.03) ** 2 + (py - 0.31) ** 2 + (pz - 0.48) ** 2 < 0.070
        pocket1 = (px - 0.48) ** 2 + (py - 0.08) ** 2 + (pz - 0.24) ** 2 < 0.030
        if not inside or pocket0 or pocket1:
            continue

        cell = (
            int(np.floor((px + 1.05) / AGGREGATE_HASH_STEP)),
            int(np.floor((py + 0.90) / AGGREGATE_HASH_STEP)),
            int(np.floor((pz + 0.80) / AGGREGATE_HASH_STEP)),
        )
        overlaps = False
        for dz in range(-1, 2):
            for dy in range(-1, 2):
                for dx in range(-1, 2):
                    for index in spatial_hash.get(
                        (cell[0] + dx, cell[1] + dy, cell[2] + dz), ()
                    ):
                        qx, qy, qz = positions[index]
                        minimum = radius + radii[index] + 0.002
                        if (px - qx) ** 2 + (py - qy) ** 2 + (pz - qz) ** 2 < minimum**2:
                            overlaps = True
                            break
                    if overlaps:
                        break
                if overlaps:
                    break
            if overlaps:
                break
        if overlaps:
            continue
        spatial_hash.setdefault(cell, []).append(len(positions))
        positions.append((px, py, pz))
        radii.append(radius)

    count = len(positions)
    colors = np.tile(np.array([[ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, ex.CYAN.a]]), (count, 1))
    return (
        np.asarray(positions, dtype=np.float32),
        np.asarray(radii, dtype=np.float32),
        colors.astype(np.uint8),
    )


def _add_label(panel, label: bytes) -> None:
    desc = dvz.dvz_label_desc()
    desc.text = label

    style = dvz.dvz_text_style()
    style.size_px = 18.0
    style.renderer = dvz.DVZ_TEXT_RENDERER_MSDF_ATLAS
    style.color[:] = (ex.TEXT.r, ex.TEXT.g, ex.TEXT.b, ex.TEXT.a)

    placement = dvz.dvz_text_placement()
    placement.mode = dvz.DVZ_TEXT_PLACEMENT_SCREEN
    placement.anchor = dvz.DVZ_SCENE_ANCHOR_PANEL_TOP_LEFT
    placement.position[:] = (20.0, 20.0, 0.0)
    placement.text_anchor[:] = (0.0, 0.0)
    placement.has_text_anchor = True

    annotation = dvz.dvz_annotation_label(panel, ctypes.byref(desc))
    if not annotation:
        raise RuntimeError("dvz_annotation_label() failed")
    if dvz.dvz_annotation_set_style(annotation, ctypes.byref(style)) != 0:
        raise RuntimeError("dvz_annotation_set_style() failed")
    if dvz.dvz_annotation_set_placement(annotation, ctypes.byref(placement)) != 0:
        raise RuntimeError("dvz_annotation_set_placement() failed")


def _add_sphere_cluster(scene, panel) -> None:
    spheres = dvz.dvz_sphere(scene, dvz.DVZ_SPHERE_FLAGS_LIGHTING)
    if not spheres:
        raise RuntimeError("dvz_sphere() failed")
    if dvz.dvz_sphere_set_mode(spheres, dvz.DVZ_SPHERE_MODE_RAYCAST_IMPOSTOR) != 0:
        raise RuntimeError("dvz_sphere_set_mode() failed")

    positions, radii, colors = _sphere_data()
    if dvz.dvz_visual_set_data_many(
        spheres,
        {
            "position": positions,
            "radius": radii,
            "color": colors,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(spheres) failed")
    material = dvz.dvz_standard_material_desc()
    material.light_direction[:] = (-0.38, 0.52, 0.76)
    material.standard.roughness = 0.72
    material.standard.specular = 0.22
    material.standard.rim_strength = 0.10
    if dvz.dvz_visual_set_material(spheres, ctypes.byref(material)) != 0:
        raise RuntimeError("dvz_visual_set_material() failed")
    ex.add_visual(panel, spheres)


def _set_ssao(panel) -> None:
    desc = dvz.dvz_ssao_desc()
    desc.radius = 0.560
    desc.strength = 3.318
    desc.bias = 0.032
    desc.power = 1.541
    desc.min_visibility = 0.000
    desc.sample_count = 32
    desc.blur_radius = 4.520
    desc.blur_depth_sigma = 0.834
    desc.blur_normal_sigma = 0.309
    desc.blur_enabled = True
    desc.debug_view = False
    if dvz.dvz_panel_set_ssao(panel, ctypes.byref(desc)) != 0:
        raise RuntimeError("dvz_panel_set_ssao() failed")


def _build_scene():
    scene = dvz.dvz_scene()
    if not scene:
        raise RuntimeError("dvz_scene() failed")
    figure = dvz.dvz_figure(scene, ex.WIDTH, ex.HEIGHT, 0)
    if not figure:
        raise RuntimeError("dvz_figure() failed")

    grid = dvz.dvz_figure_grid(figure, 1, 2)
    if not grid:
        raise RuntimeError("dvz_figure_grid() failed")
    margins = dvz.DvzPanelReserve(42.0, 42.0, 38.0, 38.0)
    if dvz.dvz_grid_set_margins(grid, ctypes.byref(margins)) != 0:
        raise RuntimeError("dvz_grid_set_margins() failed")
    if dvz.dvz_grid_set_gutter(grid, 30.0, 0.0) != 0:
        raise RuntimeError("dvz_grid_set_gutter() failed")

    panels = []
    for col, label in enumerate(LABELS):
        panel = dvz.dvz_grid_panel(grid, 0, col)
        if not panel:
            raise RuntimeError("dvz_grid_panel() failed")
        dvz.dvz_panel_set_background_color(panel, ex.BG)
        ex.manual_camera(panel)
        _add_label(panel, label)
        _add_sphere_cluster(scene, panel)
        panels.append(panel)

    _set_ssao(panels[1])
    return scene, figure, panels


def _configure_view(view, scene, panels) -> None:
    controllers = []
    for panel in panels:
        controller = dvz.dvz_arcball(scene, None)
        if not controller:
            raise RuntimeError("dvz_arcball() failed")
        if dvz.dvz_view_bind_controller(view, panel, controller, dvz.DVZ_DIM_MASK_XYZ) != 0:
            raise RuntimeError("dvz_view_bind_controller() failed")
        arcball = dvz.dvz_controller_arcball(controller)
        if not arcball:
            raise RuntimeError("dvz_controller_arcball() failed")
        if dvz.dvz_arcball_initial(arcball, INITIAL_ANGLES) != 0:
            raise RuntimeError("dvz_arcball_initial() failed")
        if dvz.dvz_arcball_zoom(arcball, INITIAL_ZOOM) != 0:
            raise RuntimeError("dvz_arcball_zoom() failed")
        if dvz.dvz_arcball_pan(arcball, INITIAL_PAN) != 0:
            raise RuntimeError("dvz_arcball_pan() failed")
        controllers.append(controller)

    components = (
        int(dvz.DVZ_CONTROLLER_LINK_ROTATION)
        | int(dvz.DVZ_CONTROLLER_LINK_PAN)
        | int(dvz.DVZ_CONTROLLER_LINK_ZOOM)
    )
    link = dvz.dvz_controller_link(
        scene, controllers[0], controllers[1], components, dvz.DVZ_CONTROLLER_LINK_TWO_WAY
    )
    if not link:
        raise RuntimeError("dvz_controller_link() failed")


def main() -> None:
    scene, figure, panels = _build_scene()

    def configure(view) -> None:
        _configure_view(view, scene, panels)

    ex.run_with_view(scene, figure, "Screen-Space Ambient Occlusion", configure)


if __name__ == "__main__":
    main()
/*
 * Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for details.
 * SPDX-License-Identifier: MIT
 */

/* This example compares a synthetic molecular aggregate with and without ambient occlusion.
 *
 * What to look for: both panels render the same irregular, multi-lobed aggregate of variable-sized
 * spheres, while the right panel applies SSAO. Narrow clefts, recessed pockets, and near-contact
 * atom clusters become easier to separate without a floor or external dataset. In live mode, use
 * the GUI and linked arcball to inspect how radius, strength, bias, power, visibility, sample count,
 * and blur affect local occlusion.
 *
 * Scenario: features_technique_ssao
 * Style: features, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c features/technique_ssao
 * Run:    ./build/examples/c/features/technique_ssao --live
 * Smoke:  ./build/examples/c/features/technique_ssao --png
 */



/*************************************************************************************************/
/*  Includes                                                                                     */
/*************************************************************************************************/

#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include "_alloc.h"
#include "datoviz/scene.h"
#include "example_common.h"
#include "example_controller_preview.h"
#include "example_style.h"
#include "example_tuner.h"
#include "runner/scenario_runner.h"



/*************************************************************************************************/
/*  Constants                                                                                    */
/*************************************************************************************************/

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define AGGREGATE_TARGET_SPHERES 2600u
#define AGGREGATE_MAX_CANDIDATES 200000u
#define AGGREGATE_HASH_X         27
#define AGGREGATE_HASH_Y         23
#define AGGREGATE_HASH_Z         20
#define AGGREGATE_HASH_CELLS                                                            \
    (AGGREGATE_HASH_X * AGGREGATE_HASH_Y * AGGREGATE_HASH_Z)
#define AGGREGATE_HASH_STEP 0.080f



/*************************************************************************************************/
/*  Helpers                                                                                      */
/*************************************************************************************************/

typedef struct SsaoDemoState
{
    DvzPanel* ssao_panel;
    DvzArcball* plain_arcball;
    DvzArcball* ssao_arcball;
    DvzExampleGuiSsaoControls ssao;
    vec3 arcball_angles;
    vec2 arcball_pan;
    float arcball_zoom;
    ExampleTuner tuner;
} SsaoDemoState;



static void _apply_arcball(SsaoDemoState* state)
{
    if (state == NULL)
        return;

    if (state->plain_arcball != NULL)
    {
        dvz_arcball_set(state->plain_arcball, state->arcball_angles);
        dvz_arcball_zoom(state->plain_arcball, state->arcball_zoom);
        dvz_arcball_pan(state->plain_arcball, state->arcball_pan);
    }
    if (state->ssao_arcball != NULL)
    {
        dvz_arcball_set(state->ssao_arcball, state->arcball_angles);
        dvz_arcball_zoom(state->ssao_arcball, state->arcball_zoom);
        dvz_arcball_pan(state->ssao_arcball, state->arcball_pan);
    }
}



/**
 * Return a deterministic pseudo-random 32-bit value.
 *
 * @param value input value
 * @return mixed value
 */
static uint32_t _hash_u32(uint32_t value)
{
    value ^= value >> 16u;
    value *= 0x7feb352du;
    value ^= value >> 15u;
    value *= 0x846ca68bu;
    value ^= value >> 16u;
    return value;
}



/**
 * Return a deterministic value in the [0, 1] interval.
 *
 * @param value input value
 * @return normalized mixed value
 */
static float _hash_unit(uint32_t value)
{
    return (float)(_hash_u32(value) & 0x00ffffffu) / 16777215.0f;
}



/**
 * Add a protein-like aggregate made from irregular lobes and recessed pockets.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @return true on success
 */
static bool _add_sphere_cluster(DvzScene* scene, DvzPanel* panel)
{
    vec3 positions[AGGREGATE_TARGET_SPHERES] = {{0}};
    float radii[AGGREGATE_TARGET_SPHERES] = {0};
    DvzColor colors[AGGREGATE_TARGET_SPHERES] = {{0}};
    int32_t hash_heads[AGGREGATE_HASH_CELLS] = {0};
    int32_t hash_next[AGGREGATE_TARGET_SPHERES] = {0};
    uint32_t count = 0;

    const vec3 lobe_centers[5] = {
        {-0.40f, +0.12f, -0.02f},
        {+0.18f, +0.17f, +0.06f},
        {+0.55f, -0.24f, -0.08f},
        {-0.26f, -0.38f, +0.10f},
        {+0.08f, -0.18f, -0.30f},
    };
    const vec3 lobe_radii[5] = {
        {0.58f, 0.45f, 0.43f},
        {0.54f, 0.49f, 0.46f},
        {0.40f, 0.34f, 0.36f},
        {0.43f, 0.32f, 0.35f},
        {0.38f, 0.32f, 0.34f},
    };

    for (int32_t i = 0; i < AGGREGATE_HASH_CELLS; i++)
        hash_heads[i] = -1;

    for (uint32_t candidate = 0;
         candidate < AGGREGATE_MAX_CANDIDATES && count < AGGREGATE_TARGET_SPHERES;
         candidate++)
    {
        const float px = -1.05f + 2.10f * _hash_unit(4u * candidate + 0u);
        const float py = -0.90f + 1.80f * _hash_unit(4u * candidate + 1u);
        const float pz = -0.80f + 1.60f * _hash_unit(4u * candidate + 2u);
        const float radius = 0.024f + 0.012f * _hash_unit(4u * candidate + 3u);

        bool inside = false;
        for (uint32_t i = 0; i < 5; i++)
        {
            const float dx = (px - lobe_centers[i][0]) / lobe_radii[i][0];
            const float dy = (py - lobe_centers[i][1]) / lobe_radii[i][1];
            const float dz = (pz - lobe_centers[i][2]) / lobe_radii[i][2];
            if (dx * dx + dy * dy + dz * dz <= 1.0f)
            {
                inside = true;
                break;
            }
        }

        const float pocket0_x = px + 0.03f;
        const float pocket0_y = py - 0.31f;
        const float pocket0_z = pz - 0.48f;
        const bool pocket0 = pocket0_x * pocket0_x + pocket0_y * pocket0_y +
                             pocket0_z * pocket0_z < 0.070f;
        const float pocket1_x = px - 0.48f;
        const float pocket1_y = py - 0.08f;
        const float pocket1_z = pz - 0.24f;
        const bool pocket1 = pocket1_x * pocket1_x + pocket1_y * pocket1_y +
                             pocket1_z * pocket1_z < 0.030f;
        if (!inside || pocket0 || pocket1)
            continue;

        const int32_t cell_x = (int32_t)floorf((px + 1.05f) / AGGREGATE_HASH_STEP);
        const int32_t cell_y = (int32_t)floorf((py + 0.90f) / AGGREGATE_HASH_STEP);
        const int32_t cell_z = (int32_t)floorf((pz + 0.80f) / AGGREGATE_HASH_STEP);
        if (
            cell_x < 0 || cell_x >= AGGREGATE_HASH_X || cell_y < 0 ||
            cell_y >= AGGREGATE_HASH_Y || cell_z < 0 || cell_z >= AGGREGATE_HASH_Z)
        {
            continue;
        }

        bool overlaps = false;
        for (int32_t dz = -1; dz <= 1 && !overlaps; dz++)
        {
            const int32_t nz = cell_z + dz;
            if (nz < 0 || nz >= AGGREGATE_HASH_Z)
                continue;
            for (int32_t dy = -1; dy <= 1 && !overlaps; dy++)
            {
                const int32_t ny = cell_y + dy;
                if (ny < 0 || ny >= AGGREGATE_HASH_Y)
                    continue;
                for (int32_t dx = -1; dx <= 1 && !overlaps; dx++)
                {
                    const int32_t nx = cell_x + dx;
                    if (nx < 0 || nx >= AGGREGATE_HASH_X)
                        continue;
                    const int32_t hash_cell =
                        nx + AGGREGATE_HASH_X * (ny + AGGREGATE_HASH_Y * nz);
                    for (int32_t j = hash_heads[hash_cell]; j >= 0; j = hash_next[j])
                    {
                        const float sx = px - positions[j][0];
                        const float sy = py - positions[j][1];
                        const float sz = pz - positions[j][2];
                        const float minimum = radius + radii[j] + 0.002f;
                        if (sx * sx + sy * sy + sz * sz < minimum * minimum)
                        {
                            overlaps = true;
                            break;
                        }
                    }
                }
            }
        }
        if (overlaps)
            continue;

        positions[count][0] = px;
        positions[count][1] = py;
        positions[count][2] = pz;
        radii[count] = radius;
        colors[count] = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
        const int32_t hash_cell =
            cell_x + AGGREGATE_HASH_X * (cell_y + AGGREGATE_HASH_Y * cell_z);
        hash_next[count] = hash_heads[hash_cell];
        hash_heads[hash_cell] = (int32_t)count;
        count++;
    }
    if (count == 0)
        return false;

    DvzVisual* spheres = dvz_sphere(scene, DVZ_SPHERE_FLAGS_LIGHTING);
    if (spheres == NULL)
        return false;
    if (dvz_sphere_set_mode(spheres, DVZ_SPHERE_MODE_RAYCAST_IMPOSTOR) != 0)
        return false;

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = positions, .item_count = count},
        {.attr_name = "radius", .data = radii, .item_count = count},
        {.attr_name = "color", .data = colors, .item_count = count},
    };
    if (dvz_visual_set_data_many(spheres, updates, 3) != 0)
        return false;

    DvzMaterialDesc material = dvz_standard_material_desc();
    material.light_direction[0] = -0.38f;
    material.light_direction[1] = +0.52f;
    material.light_direction[2] = +0.76f;
    material.standard.roughness = 0.72f;
    material.standard.specular = 0.22f;
    material.standard.rim_strength = 0.10f;
    if (dvz_visual_set_material(spheres, &material) != 0)
        return false;
    return dvz_panel_add_visual(panel, spheres, NULL) == 0;
}



static DvzController* _bind_arcball(DvzScenarioContext* ctx, DvzPanel* panel, vec3 angles)
{
    DvzController* controller = dvz_arcball(ctx->scene, NULL);
    if (controller == NULL)
        return NULL;
    DvzArcball* arcball = dvz_controller_arcball(controller);
    if (arcball == NULL)
        return NULL;
    if (dvz_scenario_bind_controller(ctx, panel, controller, DVZ_DIM_MASK_XYZ) != 0)
        return NULL;
    if (dvz_arcball_initial(arcball, angles) != DVZ_OK)
        return NULL;
    return controller;
}



/*************************************************************************************************/
/*  Scenario callbacks                                                                           */
/*************************************************************************************************/

/**
 * Initialize the SSAO feature scenario.
 *
 * @param ctx scenario context
 * @param out_user scenario state output
 * @return true on success
 */
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
    if (ctx == NULL)
        return false;
    if (out_user != NULL)
        *out_user = NULL;

    SsaoDemoState* state = (SsaoDemoState*)dvz_calloc(1, sizeof(*state));
    if (state == NULL)
        return false;
    if (out_user != NULL)
        *out_user = state;
    state->tuner = example_tuner("SSAO calibration");

    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    if (ctx->figure == NULL)
        return false;
    example_tuner_figure(&state->tuner, ctx->figure);

    DvzGrid* grid = dvz_figure_grid(ctx->figure, 1, 2);
    if (grid == NULL)
        return false;
    if (dvz_grid_set_margins(
            grid, &(DvzPanelReserve){
                      .left_px = 42.0f, .right_px = 42.0f, .top_px = 38.0f, .bottom_px = 38.0f}) != DVZ_OK)
        return false;
    if (dvz_grid_set_gutter(grid, 30.0f, 0.0f) != DVZ_OK)
        return false;

    DvzPanel* plain = dvz_grid_panel(grid, 0, 0);
    DvzPanel* ssao_panel = dvz_grid_panel(grid, 0, 1);
    if (plain == NULL || ssao_panel == NULL)
        return false;
    state->ssao_panel = ssao_panel;
    example_graphite_cyan_set_panel_background(plain);
    example_graphite_cyan_set_panel_background(ssao_panel);

    DvzTextStyle label_style = example_graphite_cyan_text_style(EXAMPLE_STYLE_TEXT_PANEL_LABEL);
    label_style.renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS;
    label_style.size_px = EXAMPLE_PANEL_LABEL_LARGE_SIZE;
    DvzTextPlacement label_placement = dvz_text_placement();
    label_placement.mode = DVZ_TEXT_PLACEMENT_SCREEN;
    label_placement.anchor = DVZ_SCENE_ANCHOR_PANEL_TOP_LEFT;
    label_placement.position[0] = EXAMPLE_PANEL_LABEL_LARGE_X_PX;
    label_placement.position[1] = EXAMPLE_PANEL_LABEL_LARGE_Y_PX;
    label_placement.text_anchor[0] = 0.0f;
    label_placement.text_anchor[1] = 0.0f;
    label_placement.has_text_anchor = true;
    DvzLabelDesc label = dvz_label_desc();
    label.text = "plain lighting";
    DvzAnnotation* annotation = dvz_annotation_label(plain, &label);
    if (annotation == NULL || dvz_annotation_set_style(annotation, &label_style) != 0 ||
        dvz_annotation_set_placement(annotation, &label_placement) != 0)
        return false;
    label.text = "ambient occlusion";
    annotation = dvz_annotation_label(ssao_panel, &label);
    if (annotation == NULL || dvz_annotation_set_style(annotation, &label_style) != 0 ||
        dvz_annotation_set_placement(annotation, &label_placement) != 0)
        return false;
    if (example_set_default_3d_camera(plain, 1.0f) == NULL ||
        example_set_default_3d_camera(ssao_panel, 1.0f) == NULL)
        return false;
    if (!_add_sphere_cluster(ctx->scene, plain) || !_add_sphere_cluster(ctx->scene, ssao_panel))
        return false;

    state->arcball_angles[0] = -0.239547f;
    state->arcball_angles[1] = -0.407204f;
    state->arcball_angles[2] = +0.824558f;
    state->arcball_zoom = 0.505017f;
    state->arcball_pan[0] = +0.000f;
    state->arcball_pan[1] = -0.020f;
    DvzController* plain_controller = _bind_arcball(ctx, plain, state->arcball_angles);
    DvzController* ssao_controller = _bind_arcball(ctx, ssao_panel, state->arcball_angles);
    if (plain_controller == NULL || ssao_controller == NULL)
        return false;
    state->plain_arcball = dvz_controller_arcball(plain_controller);
    state->ssao_arcball = dvz_controller_arcball(ssao_controller);
    if (state->plain_arcball == NULL || state->ssao_arcball == NULL)
        return false;
    if (dvz_controller_link(
            ctx->scene, plain_controller, ssao_controller,
            DVZ_CONTROLLER_LINK_ROTATION | DVZ_CONTROLLER_LINK_PAN | DVZ_CONTROLLER_LINK_ZOOM,
            DVZ_CONTROLLER_LINK_TWO_WAY) == NULL)
        return false;
    _apply_arcball(state);

    state->ssao = (DvzExampleGuiSsaoControls){
        .enabled = true,
        .blur = true,
        .debug_view = false,
        .show_blur_sigmas = true,
        .show_debug_view = true,
        .radius = 0.560f,
        .strength = 3.318f,
        .bias = 0.032f,
        .power = 1.541f,
        .min_visibility = 0.000f,
        .samples = 32.0f,
        .min_samples = 4.0f,
        .max_samples = 32.0f,
        .blur_radius = 4.520f,
        .blur_radius_max = 16.0f,
        .blur_depth_sigma = 0.834f,
        .blur_normal_sigma = 0.309f,
    };
    example_tuner_ssao(&state->tuner, "Occlusion", state->ssao_panel, &state->ssao);
    example_tuner_arcball(
        &state->tuner, "Arcball", state->plain_arcball, state->arcball_angles,
        state->arcball_zoom, state->arcball_pan);
    return true;
}


static bool _scenario_native_view(DvzScenarioContext* ctx, DvzApp* app, DvzView* view, void* user)
{
    (void)app;
    SsaoDemoState* state = (SsaoDemoState*)user;
    if (
        ctx == NULL || ctx->presentation != DVZ_RUNNER_PRESENT_GLFW || state == NULL ||
        view == NULL)
        return true;

    return example_tuner_attach(&state->tuner, view);
}



static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
    (void)ctx;
    SsaoDemoState* state = (SsaoDemoState*)user;
    if (state != NULL)
        example_tuner_detach(&state->tuner);
    dvz_free(state);
}


static void _scenario_frame(DvzScenarioContext* ctx, void* user)
{
    SsaoDemoState* state = (SsaoDemoState*)user;
    if (ctx == NULL || !ctx->preview_mode || state == NULL)
        return;
    ExamplePreviewArcballDesc desc = example_preview_arcball_cube_desc();
    example_preview_arcball(
        state->plain_arcball, ctx->preview_frame_index, ctx->preview_frame_count, &desc);
    example_preview_arcball(
        state->ssao_arcball, ctx->preview_frame_index, ctx->preview_frame_count, &desc);
}



/**
 * Return the SSAO scenario specification.
 *
 * @return scenario specification
 */
static DvzScenarioSpec _ssao_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "features_technique_ssao",
        .title = "Screen-Space Ambient Occlusion",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .init = _scenario_init,
        .frame = _scenario_frame,
        .destroy = _scenario_destroy,
    };
}



/*************************************************************************************************/
/*  Functions                                                                                    */
/*************************************************************************************************/

/**
 * Run the SSAO feature example through the native scenario runner.
 *
 * @param argc command-line argument count
 * @param argv command-line argument vector
 * @return process exit code
 */
int main(int argc, char** argv)
{
    DvzScenarioSpec spec = _ssao_scenario();
    if (example_cli_wants_live_gui(argc, argv))
        spec.native_view = _scenario_native_view;
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
Example details

Data

Field Value
kind synthetic