/*
 * This file is part of Afterwarp Framework (http://afterwarp.io).
 * Copyright (c) 2015 - 2025 Dr. Yuriy Kotsarenko. All rights reserved.
 *
 * This example source code is licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of the License at
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 */

// This example illustrates the following features:
//  * 306 simultaneous light sources, 304 of which are casting shadows in real-time!
//  * Around 2550 objects in the scene, includes multiple vehicles that are in a constant movement.
//    The vehicles also have a proper working headlights! All these objects are rendered efficiently
//    using "auto-draw" functions, which also perform automatic instancing.
//  * First-person view camera using Raw Mouse Input API and free world roaming.
//  * Spatial fog using relative eye distance and exponential formula.
//  * Realistic real-time water simulation.

#ifdef _WIN32
  #include "Resource.h"
  #include <SDKDDKVer.h>

  #define WIN32_LEAN_AND_MEAN
  #include <windows.h>
  // Remove conflicting Windows macros.
  #undef KEY_EXECUTE
#endif

#include <float.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>

#ifdef _MSC_VER
  // _isnanf is defined on x64 platform, but not on x86, so use workaround for both.
  #define isnanf(x) (x != x)
  // NAN is declared as "__ucrt_int_to_float(0x7FC00000)" so cannot be used as initializer.
  #undef NAN
  #define NAN (0.0f / 0.0f)
#endif

#include "Afterwarp.h"
#include "Afterwarp.Vectors.h"

#define AFTERWARP_VECTORS_IMPLEMENTATION
#include "Afterwarp.Vectors.h"

// Global defines.

// Parameters for controling camera movement.
#define CAMERA_ACCELERATION 400.0f
#define CAMERA_MOVEMENT_SPEED 120.0f
#define CAMERA_MINIMUM_ALTITUDE 430.0f

// How fast cars travel.
#define VEHICLE_MOVEMENT_SPEED 100.0f

// Note: when increasing number of vehicles, bridges and/or lampposts, please remember to increase
// "shadowMapAtlasSize" to be able to accomodate additional shadow maps required for extra lights!

// Number of bridges. Increasing this number may have performance impact at negligible benefit due to
// limited viewing distance.
#define BRIDGE_COUNT 9

// Total number of vehicles over all bridges.
#define VEHICLE_COUNT 8

// Number of lampposts on each side of the bridge.
#define LAMPPOST_COUNT 16

// Calculated number of lights on each side of the bridge based on the number of lampposts.
#define LIGHTS_PER_SIDE ((BRIDGE_COUNT) * (LAMPPOST_COUNT))

// Global constants.

// The size of shadow mapping atlas. This must be sufficient to accomodate all shadow maps used by
// lampposts and vehicle's headlights.
static Point shadowMapAtlasSize = {.x = 2560, .y = 2048};

// The size of shadow map for the lampposts and vehicles.
static Point shadowMapSize = {.x = 128, .y = 128};

// Position of an abstract light source (sun/moon) that will illuminate water and the bridge.
static Vector const lightPosition = {.x = -16000.0f, .y = 2000.0f, .z = 4000.0f};

// Position relative to the origin of the bridge, where objects will be placed.
static float const bridgeAt = 421.5f;

// Global variables.

// Display characteristics.
static Point displaySize = {1280, 720};
static double displayScale = 1.0;

// Base Afterwarp interfaces.
static Application* application = NULL;
static Device* device = NULL;
static SwapChain* swapChain = NULL;
static Canvas* canvas = NULL;
static TextRenderer* textRenderer = NULL;
static Timer* timer = NULL;

static SceneMeshes* meshes = NULL;
static ObjectModels* objects = NULL;
static SceneLights* lights = NULL;
static ObjectMaterials* materials = NULL;

static TextureCabinet* textureCabinet = NULL;
static ShadowCastingAtlas* shadowCastingAtlas = NULL;

static Scene* sceneDepthNormals = NULL;
static Scene* sceneModeling = NULL;
static OceanSimulation* oceanSimulation = NULL;
static SpatialFog* spatialFog = NULL;

static ObjectModelView* viewport = NULL;
static ActorCamera* camera = NULL;
static Matrix projection = {0};

static Vector bridgeSize = {0};
static float bridgeLength = 0.0f;

static uint8_t movementDirection = 0;
static Vector velocity = {0};
static PointF lastSmoothMouse = {NAN, NAN};

static int32_t objectsVisible = 0;
static int32_t shadowMapsUpdated = 0;
static int32_t drawCalls = 0;
static bool keyAlt = false, minimized = false, active = true;
static bool shadowCastingAtlasPrepared = false;

static VertexElement const vertexElements[] = {
  {"vertexPosition", ELEMENT_FORMAT_FLOAT, 3, 0, 0},
  {"vertexNormal", ELEMENT_FORMAT_FLOAT, 3, 0, 0},
  {"vertexColor", ELEMENT_FORMAT_UNSIGNED_BYTE, 4, 0 | VERTEX_CHANNEL_NORMALIZED, 0}};

static int32_t const vertexElementsCount = sizeof(vertexElements) / sizeof(VertexElement);

// Pointers to lights on each side of the bridge.
static SceneLight* lamppostLights[2][LIGHTS_PER_SIDE] = {0};

// A circular list of shortcuts to each of the bridges.
static ObjectModel* bridges[BRIDGE_COUNT] = {0};

// An array of all vehicles.
static ObjectModel* vehicles[VEHICLE_COUNT] = {0};

// Cumulative bits that define movement direction.
enum MovementDirection
{
  // Forward movement bit.
  MOVEMENT_DIRECTION_FORWARD = 0x01,

  // Backward movement bit.
  MOVEMENT_DIRECTION_BACKWARD = 0x02,

  // Left movement bit.
  MOVEMENT_DIRECTION_LEFT = 0x04,

  // Right movement bit.
  MOVEMENT_DIRECTION_RIGHT = 0x08,

  // Up movement bit.
  MOVEMENT_DIRECTION_UP = 0x10,

  // Down movement bit.
  MOVEMENT_DIRECTION_DOWN = 0x20
};

// Cumulative bits that define characteristics of light object.
enum ObjectLightTrackingAttribute
{
  // Shadow map needs to be re-rendered.
  LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED = 0x01,

  // Shadow map needs to be re-rendered without vehicles. When the vehicle is too far away from the view, it
  // stops updating shadow maps for nearby lampposts; at this point, if there was a lamppost that had this
  // vehicle in its shadow map, it will persist and not get updated as the vehicle would have long gone.
  // Therefore, once the vehicle stops shadow map updates, all its nearby lampposts get their shadow map
  // updated for the last time, except this time without any vehicles, to avoid having persistent vehicle
  // shadow artifacts.
  LIGHT_TRACKING_ATTRIBUTE_LATE_UPDATE_NEEDED = 0x02,

  // Shadow map needs to be re-rendered without vehicles, eventually. This attribute is set when a bridge
  // has been relocated to account for observer movement, so shadows use different projected matrix.
  LIGHT_TRACKING_ATTRIBUTE_UPDATE_QUEUED = 0x04,

  // Light corresponds to a vehicle headlight.
  LIGHT_TRACKING_ATTRIBUTE_HEADLIGHT = 0x08
};

// Additional information added to each "vehicle" for movement tracking.
typedef struct MovementTracking
{
  // Parent bridge index.
  int32_t bridge;

  // Indices to closest lampposts on each side.
  int32_t lampposts[2];
} MovementTracking;

// Additional parameters added to each "important" object in form of "payload".
typedef struct ObjectLightTracking
{
  // View for a shadow caster associated with the lamppost or headlight.
  ObjectModelView* view;

  // Shadow caster associated with the lamppost or headlight.
  ShadowCaster* caster;

  // Cumulative attribute bits.
  uint8_t attributes;

  // Additional and optional information for vehicle movement.
  MovementTracking* movement;
} ObjectLightTracking;

// Forward declarations.

static void renderObjects(Scene* scene, ObjectModelView* view, bool excludeVehicles);
static void updateCameraMovement(double latency);
static void updateVehicleMovement(double latency);
static void updateBridges(bool startup);
static void synchronizeLights(void);
static bool createBridgeLampposts(ObjectModel* bridge, int32_t* sideLights);
static bool createBridgeObstacles(ObjectModel* bridge, uint64_t* prng);
static bool createVehicles(uint64_t* prng);
static bool createObjects(void);
static bool createAndLoadMeshes(void);
static void createLightsAndMaterials(void);
static void releaseObjectLightTracking(void);
static void outputError(char const* message);

// Implementation.

LibraryBool applicationCreate(Application* application, void* user)
{
  device = DeviceCreate(application, DEVICE_TECHNOLOGY_UNKNOWN, 0);
  if (!device)
  {
    outputError("Could not create graphics device.");
    return false;
  }
  bool vsync = true;

  if (DeviceGetBehavior(device) & DEVICE_BEHAVIOR_VARIABLE_RATE_REFRESH)
    vsync = false; // Enable Variable Rate Refresh

  swapChain = SwapChainCreate(device, ApplicationGetWindowHandle(application), &displaySize,
    PIXEL_FORMAT_UNKNOWN, PIXEL_FORMAT_UNKNOWN, 0, vsync);
  if (!swapChain)
  {
    outputError("Could not create rendering swap-chain.");
    return false;
  }
#ifndef _WIN32
  {
    char const* fileNames[] = {"../../../../Media/afterwarp.ico"};

    if (!ApplicationSetIconsFromFiles(application, fileNames, sizeof(fileNames) / sizeof(fileNames[0])))
    {
      outputError("Could not load application icon.");
      return false;
    }
  }
#endif
  canvas = CanvasCreate(device);
  if (!canvas)
  {
    outputError("Could not create rendering canvas.");
    return false;
  }
  textRenderer = TextRendererCreate(canvas, &(Point){512, 512}, PIXEL_FORMAT_LA8, false);
  if (!textRenderer)
  {
    outputError("Could not create text rendering module.");
    return false;
  }
  meshes = SceneMeshesCreate(device);
  if (!meshes)
  {
    outputError("Could not create meshes container.");
    return false;
  }
  objects = ObjectModelsCreate(meshes);
  if (!objects)
  {
    outputError("Could not create objects container.");
    return false;
  }
  lights = SceneLightsCreate(device);
  if (!lights)
  {
    outputError("Could not create 3D scene lights module.");
    return false;
  }
  materials = ObjectMaterialsCreate();
  if (!materials)
  {
    outputError("Could not create object materials container.");
    return false;
  }

#if (defined(_MSC_VER) && defined(_M_ARM64)) || defined(__aarch64__)
  // Optimize for ultra-portable WoA devices
  if (displayScale >= 2.0) // 4K display?
    textureCabinet = TextureCabinetCreate(device, &displaySize, TEXTURE_FIDELITY_MEDIUM, 0,
      PIXEL_FORMAT_UNKNOWN);
  else
    textureCabinet = TextureCabinetCreate(device, &displaySize, TEXTURE_FIDELITY_MEDIUM, 4,
      PIXEL_FORMAT_UNKNOWN);
#else
  textureCabinet = TextureCabinetCreate(device, &displaySize, TEXTURE_FIDELITY_HIGH, 8,
    PIXEL_FORMAT_UNKNOWN);
#endif

  if (!textureCabinet)
  {
    outputError("Could not create texture cabinet container.");
    return false;
  }
  shadowCastingAtlas = ShadowCastingAtlasCreate(device, &shadowMapAtlasSize, TECHNIQUE_SHADOWS_EVSM, 8, 0);
  if (!shadowCastingAtlas)
  {
    outputError("Could not create shadow casting atlas container.");
    return false;
  }
  sceneDepthNormals = SceneCreateDepthsNormals(device);
  if (!sceneDepthNormals || !SceneSetVertexElements(sceneDepthNormals, 0, vertexElements,
    vertexElementsCount))
  {
    outputError("Could not create depths/normals rendering scene.");
    return false;
  }
  sceneModeling = SceneCreateModeling(device);
  if (!sceneModeling || !SceneSetVertexElements(sceneModeling, 0, vertexElements,
    sizeof(vertexElements) / sizeof(VertexElement)))
  {
    outputError("Could not create 3D rendering scene.");
    return false;
  }
  oceanSimulation = OceanSimulationCreate(device);
  if (!oceanSimulation)
  {
    outputError("Could not create ocean simulation module.");
    return false;
  }
  spatialFog = SpatialFogCreate(device);
  if (!spatialFog)
  {
    outputError("Could not create spatial fog module.");
    return false;
  }
  timer = TimerCreate();
  if (!timer)
  {
    outputError("Could not create timer module.");
    return false;
  }
  viewport = ObjectModelsCreateView(objects);
  if (!viewport)
  {
    outputError("Could not create main viewport.");
    return false;
  }
  camera = ActorCameraCreate(&(Vector){.x = 0.0f, .y = 440.0f, .z = 0.0f},
    &(Vector){.x = 0.0f, .y = (float)M_PI * -0.5f, .z = 0.0f}, 0.0f);
  if (!camera)
  {
    outputError("Could not create object camera.");
    return false;
  }
  if (!createAndLoadMeshes())
  {
    outputError("Could not create and/or load 3D meshes.");
    return false;
  }
  createLightsAndMaterials();

  if (!createObjects())
  {
    outputError("Could not create 3D objects.");
    return false;
  }
  ApplicationCaptureMouseInput(application);

  return true;
}

void applicationDestroy(Application* application, void* user)
{
  ApplicationReleaseMouseInput(application);

  releaseObjectLightTracking();

  ActorCameraDestroy(camera);
  TimerDestroy(timer);
  SpatialFogDestroy(spatialFog);
  OceanSimulationDestroy(oceanSimulation);
  SceneDestroy(sceneModeling);
  SceneDestroy(sceneDepthNormals);
  ShadowCastingAtlasDestroy(shadowCastingAtlas);
  TextureCabinetDestroy(textureCabinet);
  ObjectMaterialsDestroy(materials);
  SceneLightsDestroy(lights);
  ObjectModelsDestroy(objects);
  SceneMeshesDestroy(meshes);
  TextRendererDestroy(textRenderer);
  CanvasDestroy(canvas);
  SwapChainDestroy(swapChain);
  DeviceDestroy(device);
}

void applicationResize(Application* application, void* user)
{
  Rect clientRect = {0};
  ApplicationGetClientRect(application, &clientRect);

  displaySize = rectGetSize(clientRect);

  SwapChainResize(swapChain, &displaySize);
  TextureCabinetSetSize(textureCabinet, &displaySize);
}

void applicationRender(Application* application, void* user)
{
  displayScale = ApplicationGetWindowScale(application);
  drawCalls = 0;

  static float const farPlane = 8000.0f;
  double const latency = TimerGetLatency(timer) / (1000.0 * 1000.0);

  // Perform animation of the ocean water simulation.
  OceanSimulationUpdate(oceanSimulation, latency);

  // Prepare view and projection matrices.
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;

  Matrix view = {0};
  ActorCameraGetView(camera, &view);

  projection = matrixPerspectiveFOVY((float)(M_PI * 0.25), (float)displaySize.x / displaySize.y, 4.0f,
    farPlane, depthClipNegative);

  // Set view/projection matrices and update visible objects.
  ObjectModelViewSetView(viewport, &view);
  ObjectModelViewSetProjection(viewport, &projection, depthClipNegative);
  ObjectModelViewUpdate(viewport);

  { // Update texture cabinet's attributes.
    TextureCabinetAttributes attributes =
      TEXTURE_CABINET_ATTRIBUTE_BLOOM |
    #if (!defined(_MSC_VER) || !defined(_M_ARM64)) && !defined(__aarch64__)
      TEXTURE_CABINET_ATTRIBUTE_BLOOM_CUBIC |
    #endif
      TEXTURE_CABINET_ATTRIBUTE_AMBIENT_OCCLUSION |
      TEXTURE_CABINET_ATTRIBUTE_LINEAR_DEPTHS;

    if (displayScale >= 2.0)
      attributes |=
        TEXTURE_CABINET_ATTRIBUTE_BLOOM_DOWNSCALE |
        TEXTURE_CABINET_ATTRIBUTE_AMBIENT_OCCLUSION_DOWNSCALE;

    TextureCabinetSetAttributes(textureCabinet, attributes);

    // Adjust tone-mapping bloom parameters for HDR rendering.
    ToneMappingBloom toneMappingBloom = {0};
    TextureCabinetGetToneMappingBloom(textureCabinet, &toneMappingBloom);
    toneMappingBloom.toneWhite = 2.0f;
    toneMappingBloom.bloomThreshold = 1.5f;
    TextureCabinetSetToneMappingBloom(textureCabinet, &toneMappingBloom);

    // Adjust ambient occlusion parameters.
    AmbientOcclusionParameters parameters = {0};
    TextureCabinetGetAmbientOcclusionParameters(textureCabinet, &parameters);
    parameters.radius = 4.0f;
    parameters.kernelBias = 0.05f;
    TextureCabinetSetAmbientOcclusionParameters(textureCabinet, &parameters);
  }

  // Prepare scene's attributes.
  SceneAttributes const attributes =
    SCENE_ATTRIBUTE_NORMALS |
    SCENE_ATTRIBUTE_COLORING |
    SCENE_ATTRIBUTE_SHADOWS_CUBIC |
    SCENE_ATTRIBUTE_DEPTH_PREPASS |
    SCENE_ATTRIBUTE_LINEAR_DEPTHS;

  // Render depth pre-pass.
  TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_DEPTH, &(FloatColor){farPlane});

  if (TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_DEPTH))
  {
    SceneSetAttributes(sceneDepthNormals, attributes);
    SceneSetView(sceneDepthNormals, &view);
    SceneSetProjection(sceneDepthNormals, &projection);

    if (SceneBegin(sceneDepthNormals))
    {
      renderObjects(sceneDepthNormals, viewport, false);
      SceneEnd(sceneDepthNormals);
    }
    TextureCabinetEnd(textureCabinet);
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_OCCLUSION, &projection);

  // Perform light assignment for 3D scene.
  SceneLightsSetViewSize(lights, &displaySize);
  SceneLightsSetCullingMode(lights, CLUSTERS_CULLING_MODE_QUALITY);
  SceneLightsSetClusterSize(lights, (int32_t)round(32.0 * displayScale));
  SceneLightsSetDepthSlices(lights, 24);
  SceneLightsExecute(lights, &view, &projection);

  { // Render shadow maps.
    shadowMapsUpdated = 0;

    ShadowParameters shadowParams = {0};
    ShadowCastingAtlasGetParameters(shadowCastingAtlas, &shadowParams);
    shadowParams.blurSigma = 0.75f; // Shadow maps are small, so use less blur.
    shadowParams.variance = 0.3e-5f; // Compensate for wider viewing angles in shadow projections.
    ShadowCastingAtlasSetParameters(shadowCastingAtlas, &shadowParams);

    if (!shadowCastingAtlasPrepared)
    { // Fill shadow casting atlas with border color only after startup.
      ShadowCastingAtlasBorderFill(shadowCastingAtlas);
      shadowCastingAtlasPrepared = true;
    }
    ShadowCaster* casterToFilter = NULL;
    int32_t lightQueueProcessed = -1;

    // Update shadow maps for all lights that have "update needed" attribute set in the associated object.
    // Note: in case of headlights, two lights refer to the same "tracking" object.
    for (int32_t i = 0; i < SceneLightsGetCount(lights); ++i)
    {
      SceneLight* const light = SceneLightsGetElement(lights, i);
      ObjectLightTracking* const tracking = ObjectModelGetPayload(light ? light->payload : NULL);
      if (!tracking || !tracking->caster)
        continue;

      if (lightQueueProcessed == -1 && (tracking->attributes & LIGHT_TRACKING_ATTRIBUTE_UPDATE_QUEUED))
        lightQueueProcessed = i; // Process a single shadow map from queue.

      if (lightQueueProcessed == i || (tracking->attributes &
        (LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED | LIGHT_TRACKING_ATTRIBUTE_LATE_UPDATE_NEEDED)))
      {
        ObjectModelViewUpdate(tracking->view);

        Matrix viewProjection;
        ObjectModelViewGetViewProjection(tracking->view, &viewProjection);
        ShadowCasterSetViewProjection(tracking->caster, &viewProjection);

        bool const excludeVehicles = !(tracking->attributes & LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED);

        if (casterToFilter)
        { // Filter previous caster before continuing with the current one.
          ShadowCasterFilter(casterToFilter);
          casterToFilter = NULL;
        }
        if (ShadowCasterClear(tracking->caster) && ShadowCasterBegin(tracking->caster))
        {
          Matrix view, projection;
          ObjectModelViewGetView(tracking->view, &view);
          ObjectModelViewGetProjection(tracking->view, &projection, NULL);

          SceneSetAttributes(sceneDepthNormals, 0);
          SceneSetView(sceneDepthNormals, &view);
          SceneSetProjection(sceneDepthNormals, &projection);

          if (SceneBegin(sceneDepthNormals))
          {
            renderObjects(sceneDepthNormals, tracking->view, excludeVehicles);
            SceneEnd(sceneDepthNormals);
          }
          ShadowCasterEnd(tracking->caster);
          ++shadowMapsUpdated;

          // Delay filtering of shadow caster to enable better paralellism between CPU and GPU.
          casterToFilter = tracking->caster;

          // Remove "update needed" bit and if such doesn't exist, remove "late update needed" instead.
          if (tracking->attributes & LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED)
            tracking->attributes &= ~LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED;
          else
            tracking->attributes &= ~LIGHT_TRACKING_ATTRIBUTE_LATE_UPDATE_NEEDED;

          // If the shadow map was waiting in queue to be processed, remove its "queue" bit.
          tracking->attributes &= ~LIGHT_TRACKING_ATTRIBUTE_UPDATE_QUEUED;
        }
      }
    }
    if (casterToFilter)
      ShadowCasterFilter(casterToFilter);
  }

  // Render main scene.
  FloatColor const clearColor = floatColor(0xFF404040u);
  TextureCabinetClear(textureCabinet, TEXTURE_CABINET_PASS_COLOR, &clearColor);

  if (TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_COLOR))
  {
    SceneSetAttributes(sceneModeling, attributes);
    SceneSetView(sceneModeling, &view);
    SceneSetProjection(sceneModeling, &projection);

    SceneSetLights(sceneModeling, lights);
    SceneSetShadowCastingAtlas(sceneModeling, shadowCastingAtlas);
    SceneSetTextureCabinet(sceneModeling, textureCabinet);

    if (ScenePrepare(sceneModeling) && SceneBegin(sceneModeling))
    {
      renderObjects(sceneModeling, viewport, false);
      SceneEnd(sceneModeling);
    }

    // Update ocean's material.
    OceanMaterial material;
    OceanSimulationGetMaterial(oceanSimulation, &material);

    material.extinction = 0.008f;
    OceanSimulationSetMaterial(oceanSimulation, &material);

    // Configure ocean's appearance.
    OceanSimulationSetPlane(oceanSimulation, &(Vector4){0.0f, 1.0f, 0.0f, -200.0f});
  #if (!defined(_MSC_VER) || !defined(_M_ARM64)) && !defined(__aarch64__)
    OceanSimulationSetSections(oceanSimulation, &(Point){1280 * 3 / 2, 720 * 3 / 2});
  #else
    OceanSimulationSetSections(oceanSimulation, &(Point){1280, 720});
  #endif

    OceanSimulationSetView(oceanSimulation, &view);
    OceanSimulationSetProjection(oceanSimulation, &projection);

    Texture* const linearDepths = TextureCabinetGetTexture(textureCabinet,
      TEXTURE_CABINET_TYPE_LINEAR_DEPTHS);

    OceanSimulationRender(oceanSimulation, linearDepths, lights, NULL);

    TextureCabinetEnd(textureCabinet);
  }

  // Perform object and camera movement.
  updateCameraMovement(latency);
  updateVehicleMovement(latency);
  updateBridges(false);
  synchronizeLights();

  // Render a plane to generate linear depths right above ocean for proper fog simulation.
  // This has to be performed AFTER ocean simulation has been rendered, since it'll override ocean depths.
  if (TextureCabinetBegin(textureCabinet, TEXTURE_CABINET_PASS_DEPTH))
  {
    Vector position;
    ActorCameraGetPosition(camera, &position);

    SceneSetAttributes(sceneDepthNormals, attributes);

    Matrix transform = matrixScale((Vector){32000.0f, 1.0f, 32000.0f});
    transform = matrixMultiply(transform, matrixTranslate((Vector){position.x, 215.0f, position.z}));

    SceneSetWorld(sceneDepthNormals, &transform);
    SceneSetView(sceneDepthNormals, &view);
    SceneSetProjection(sceneDepthNormals, &projection);

    if (SceneBegin(sceneDepthNormals))
    {
      MeshModelDraw(SceneMeshGetModel(SceneMeshesGetMeshByName(meshes, "Plane")),
        SceneGetProgram(sceneDepthNormals), PRIMITIVE_TOPOLOGY_TRIANGLES, 0, 0, 0, true);

      SceneEnd(sceneDepthNormals);
    }
    TextureCabinetEnd(textureCabinet);
  }

  { // Apply fog.
    float const fade = 0.15f;

    FogParameters parameters = {0};
    parameters.color = (FloatColor){0.011765f * fade, 0.333333f * fade, 0.470588f * fade, 0.0f};
    parameters.opacity = 1.0f;
    parameters.groundPlane = (Vector4){0.0f, 1.0f, 0.0f, -600.0f};
    parameters.scattering = 1.0f;
    parameters.extinction = 0.0003f;
    parameters.densityGround = 0.03f;
    SpatialFogSetParameters(spatialFog, &parameters);

    SpatialFogSetFormula(spatialFog, FOG_FORMULA_EXPONENTIAL);
    SpatialFogSetDepthDistance(spatialFog, DEPTH_FOG_DISTANCE_EYE_RELATIVE);

    SpatialFogSetView(spatialFog, &view);
    SpatialFogSetProjection(spatialFog, &projection);

    SpatialFogExecute(spatialFog, TextureCabinetGetTexture(textureCabinet, TEXTURE_CABINET_TYPE_COLOR_HDR),
      TextureCabinetGetTexture(textureCabinet, TEXTURE_CABINET_TYPE_LINEAR_DEPTHS));
  }
  TextureCabinetFilter(textureCabinet, TEXTURE_CABINET_FILTER_TYPE_BLOOM, NULL);

  // Render on the application's window.
  if (SwapChainBegin(swapChain))
  {
    FloatColor const clearColor = floatColor(0xFF404040u);
    DeviceClear(device, DEVICE_CLEAR_LAYER_COLOR, &clearColor, 1.0f, 0);

    TextureCabinetPresent(textureCabinet);

    if (CanvasBegin(canvas))
    {
      CanvasSetContextState(canvas, CANVAS_CONTEXT_STATE_FLAT_SCENE);
      { // Draw a crosshair to know where the camera is pointing at.
        float const crossHairInside = 8.0f * displayScale;
        float const crossHairOutside = 12.0f * displayScale;

        float const crossHairInsideMask = 8.0f * displayScale - 1.0f;
        float const crossHairOutsideMask = 12.0f * displayScale + 1.0f;

        PointF const center = (PointF){.x = displaySize.x * 0.5f, .y = displaySize.y * 0.5f};

        Color const color = 0x80FFFFFFu;
        ColorPair const colorInside = (ColorPair){.first = MakeColor(color, 0), .second = color};
        ColorPair const colorOutside = (ColorPair){.first = color, .second = MakeColor(color, 0)};

        CanvasRibbon(canvas, &center, &(PointF){crossHairInside, crossHairInside},
          &(PointF){crossHairOutside, crossHairOutside}, 32, color, BLENDING_EFFECT_NORMAL);

        // Simulate anti-aliasing by rendering 1-pixel secondary ribbons inside and outside of the main
        // ribbon.
        CanvasRibbonTri(canvas, &center, &(PointF){crossHairInsideMask, crossHairInsideMask},
          &(PointF){crossHairInside, crossHairInside}, 32,
          &colorInside, &colorInside, &colorInside, BLENDING_EFFECT_NORMAL);

        CanvasRibbonTri(canvas, &center, &(PointF){crossHairOutside, crossHairOutside},
          &(PointF){crossHairOutsideMask, crossHairOutsideMask}, 32,
          &colorOutside, &colorOutside, &colorOutside, BLENDING_EFFECT_NORMAL);
      }
      CanvasSetContextState(canvas, CANVAS_CONTEXT_STATE_FLAT_TEXT);
      CanvasSetSamplerState(canvas, &(CanvasSamplerState){TEXTURE_FILTER_NEAREST, TEXTURE_FILTER_NEAREST});

      FontParameters parameters = {
        .family = "Segoe UI", .size = 14.0f * displayScale, .weight = FONT_WEIGHT_SEMI_BOLD,
        .stretch = FONT_STRETCH_NORMAL, .slant = FONT_SLANT_NONE, .effect = fontEffectDefault};
      parameters.effect.borderType = FONT_BORDER_SEMI_HEAVY;
      parameters.effect.borderBrightness = 1.0f;
      parameters.effect.fillBrightness = 0.0f;
      parameters.effect.shadowBrightness = 0.75f;
      parameters.effect.shadowOpacity = 1.0f;
      TextRendererSetFontParameters(textRenderer, &parameters);

      TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x * 0.5f, 4.0f * displayScale},
        "W/S keys - move forward and backward, A/D keys - move left and right.",
        &(ColorPair){0xFFA5E7B8u, 0xFF77FFCEu}, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_START,
        0.75f, true, NULL);

      TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x * 0.5f, displaySize.y -
        4.0f * displayScale}, "Mouse movement - rotate scene, Space/C keys - move up and down.",
        &(ColorPair){0xFFA5E7B8u, 0xFF77FFCEu}, TEXT_ALIGNMENT_MIDDLE, TEXT_ALIGNMENT_END,
        0.75f, true, NULL);

      char const* const textDirect3D = "Direct3D (%d.%d), features: %d.%d";
      char const* const textOpenGL = "OpenGL (%d.%d+), features: %d.%d";

      char const* const formatText = DeviceGetTechnology(device) == DEVICE_TECHNOLOGY_DIRECT3D ?
        textDirect3D : textOpenGL;

      char text[128];

      snprintf(text, sizeof(text), "Frame Rate: %1.0f", TimerGetFrameRate(timer));

      TextRendererDrawAligned(textRenderer, &(PointF){8.0f * displayScale, 4.0f * displayScale}, text,
        &COLOR_PAIR(0xA0FFFFFFu), TEXT_ALIGNMENT_START, TEXT_ALIGNMENT_START, 1.0f, true, NULL);

      snprintf(text, sizeof(text), formatText,
        DeviceGetTechVersion(device) >> 8, (DeviceGetTechVersion(device) >> 4) & 0xF,
        DeviceGetTechFeatureVersion(device) >> 8, (DeviceGetTechFeatureVersion(device) >> 4) & 0xF);

      TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x - 12.0f * displayScale,
        4.0f * displayScale}, text, &COLOR_PAIR(0xA0FFFFFFu), TEXT_ALIGNMENT_END,
        TEXT_ALIGNMENT_START, 1.0f, true, NULL);

      snprintf(text, sizeof(text), "Lights: %d, shadow maps updated: %d", SceneLightsGetCount(lights),
        shadowMapsUpdated);

      TextRendererDrawAligned(textRenderer, &(PointF){8.0f * displayScale, 32.0f * displayScale}, text,
        &COLOR_PAIR(0xA0FFFFFFu), TEXT_ALIGNMENT_START, TEXT_ALIGNMENT_START, 1.0f, true, NULL);

      snprintf(text, sizeof(text), "Objects visible: %d (out of %d), draw calls: %d", objectsVisible,
        ObjectModelsGetObjectCount(objects), drawCalls);

      TextRendererDrawAligned(textRenderer, &(PointF){displaySize.x - 12.0f * displayScale,
        32.0f * displayScale}, text, &COLOR_PAIR(0xA0FFFFFFu), TEXT_ALIGNMENT_END,
        TEXT_ALIGNMENT_START, 1.0f, true, NULL);

      CanvasEnd(canvas);
    }
    SwapChainEnd(swapChain);
  }
  TimerUpdate(timer);
}

void applicationMouse(Application* application, MouseEvent event, MouseButton button, Point const* position,
  void* user)
{
  switch (event)
  {
  case MOUSE_EVENT_MOUSE_DOWN:
    break;

  case MOUSE_EVENT_MOUSE_MOVE:
    if (!minimized && active)
    {
      float const sensitivity = 0.002f;
      float const alpha = 0.6f;
      PointF smoothMouse;

      if (!isnanf(lastSmoothMouse.x) && !isnanf(lastSmoothMouse.y))
        smoothMouse = pointLerpF((PointF){.x = position->x, .y = position->y}, lastSmoothMouse, alpha);
      else
        smoothMouse = (PointF){.x = position->x, .y = position->y};

      lastSmoothMouse = smoothMouse;

      Vector4 rotation;
      ActorCameraGetRotation(camera, &rotation);

      rotation.x += smoothMouse.y * sensitivity;
      rotation.y += smoothMouse.x * sensitivity;

      ActorCameraSetRotation(camera, &rotation);
    }
    break;

  case MOUSE_EVENT_MOUSE_UP:
    break;
  }
}

void applicationKey(Application* application, KeyEvent event, int32_t virtualKey, uint16_t keyCode,
  void* user)
{
  Key const key = ApplicationTranslateVirtualKey(application, virtualKey);

  if (key == KEY_ALT_LEFT || key == KEY_ALT_RIGHT)
  {
    keyAlt = event == KEY_EVENT_PRESSED;
    ApplicationInvalidate(application);
  }
  else if (key == KEY_ESCAPE && event == KEY_EVENT_PRESSED)
    ApplicationTerminate(application);
  else if (key == KEY_RETURN && event == KEY_EVENT_PRESSED && keyAlt)
  {
    ApplicationWindowState state = ApplicationGetWindowState(application);

    if (state != APPLICATION_WINDOW_STATE_FULLSCREEN)
      state = APPLICATION_WINDOW_STATE_FULLSCREEN;
    else
      state = APPLICATION_WINDOW_STATE_WINDOWED;

    ApplicationSetWindowState(application, state);
  }
  else if (key == 'w' || key == KEY_UP)
  {
    if (event == KEY_EVENT_PRESSED)
      movementDirection |= MOVEMENT_DIRECTION_FORWARD;
    else if (event == KEY_EVENT_RELEASED)
      movementDirection &= ~MOVEMENT_DIRECTION_FORWARD;
  }
  else if (key == 's' || key == KEY_DOWN)
  {
    if (event == KEY_EVENT_PRESSED)
      movementDirection |= MOVEMENT_DIRECTION_BACKWARD;
    else if (event == KEY_EVENT_RELEASED)
      movementDirection &= ~MOVEMENT_DIRECTION_BACKWARD;
  }
  else if (key == 'a' || key == KEY_LEFT)
  {
    if (event == KEY_EVENT_PRESSED)
      movementDirection |= MOVEMENT_DIRECTION_LEFT;
    else if (event == KEY_EVENT_RELEASED)
      movementDirection &= ~MOVEMENT_DIRECTION_LEFT;
  }
  else if (key == 'd' || key == KEY_RIGHT)
  {
    if (event == KEY_EVENT_PRESSED)
      movementDirection |= MOVEMENT_DIRECTION_RIGHT;
    else if (event == KEY_EVENT_RELEASED)
      movementDirection &= ~MOVEMENT_DIRECTION_RIGHT;
  }
  else if (key == 'c')
  {
    if (event == KEY_EVENT_PRESSED)
      movementDirection |= MOVEMENT_DIRECTION_DOWN;
    else if (event == KEY_EVENT_RELEASED)
      movementDirection &= ~MOVEMENT_DIRECTION_DOWN;
  }
  else if (key == KEY_SPACE)
  {
    if (event == KEY_EVENT_PRESSED)
      movementDirection |= MOVEMENT_DIRECTION_UP;
    else if (event == KEY_EVENT_RELEASED)
      movementDirection &= ~MOVEMENT_DIRECTION_UP;
  }
}

void applicationStatus(Application* application, ApplicationEvent event, void* user)
{
  switch (event)
  {
  case APPLICATION_EVENT_ACTIVATE:
    if (!ApplicationMouseInputCaptured(application))
      ApplicationCaptureMouseInput(application);
    active = true;
    break;

  case APPLICATION_EVENT_DEACTIVATE:
    if (ApplicationMouseInputCaptured(application))
      ApplicationReleaseMouseInput(application);
    active = false;
    break;

  case APPLICATION_EVENT_RESTORE:
    minimized = false;
    break;

  case APPLICATION_EVENT_MINIMIZE:
    minimized = true;
    break;

  default:
    break;
  }
}

LibraryBool applicationIdle(Application* application, void* user)
{
  if (active && !minimized)
    ApplicationInvalidate(application);
  return false;
}

void renderObjects(Scene* scene, ObjectModelView* view, bool excludeVehicles)
{
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;
  bool const multisampling = TextureCabinetGetSamples(textureCabinet) > 1;

  // Draw all objects, skipping portions of objects that are semi-transparent. Only vehicles have their
  // windshields semi-transparent, so this will draw vehicles without their windshields, the last of which
  // will be rendered separately below.
  uint32_t options = AUTO_DRAW_OPTION_MATERIAL_SKIP_TRANSPARENT;

  if (excludeVehicles)
    options |= AUTO_DRAW_OPTION_OBJECT_SKIP_TRANSPARENT;

  ObjectModelViewAutoDraw(view, scene, materials, options, &drawCalls);

  if (!excludeVehicles && (SceneGetAttributes(scene) & SCENE_ATTRIBUTE_MODELING))
  {
    // Update number of visible objects only when main scene is being rendered.
    objectsVisible = ObjectModelViewGetObjectCount(view);

    // Retrieve current rendering state.
    RenderingState initialState = {0};
    DeviceGetRenderingState(device, &initialState);

    // Simulate some sort of transparency without resorting to more complex techniques by using alpha-
    // blending and alpha-to-coverage mode. As this is done during HDR rendering, it would be inaccurate.
    // However, vehicle windshield is a minor detail, so simplicity is a reasonable compromise.

    RenderingState state = initialState;
    state.states |= DEVICE_STATE_BLEND_ENABLE;
    state.blendColor.source = BLEND_FACTOR_ONE;
    state.blendColor.dest = BLEND_FACTOR_ZERO;
    // Destination alpha-channel actually contains bloom factors, so reduce it based on source alpha values.
    state.blendAlpha.source = BLEND_FACTOR_ZERO;
    state.blendAlpha.dest = BLEND_FACTOR_INV_SOURCE_ALPHA;

    if (!multisampling)
    {
      state.blendColor.source = BLEND_FACTOR_SOURCE_ALPHA;
      state.blendColor.dest = BLEND_FACTOR_INV_SOURCE_ALPHA;
    }
    else
      state.states |= DEVICE_STATE_ALPHA_TO_COVERAGE;

    // Depth clip negative behavior only occurs under older OpenGL implementations that do not support clip
    // control either under normal API or through extension. It has significantly less depth precision.
    state.depthFunc = depthClipNegative ? COMPARISON_FUNC_LESS_EQUAL : COMPARISON_FUNC_GREATER_EQUAL;

    DeviceSetRenderingState(device, &state);

    // Draw all vehicles windshields separately. Note that during creation, vehicles were marked with "transparent"
    // attribute. The following function is instructed to omit all objects that do not have this attribute
    // set, so it would quickly skip all objects that aren't vehicles.
    ObjectModelViewAutoDraw(view, scene, materials, AUTO_DRAW_OPTION_OBJECT_SKIP_NON_TRANSPARENT |
      AUTO_DRAW_OPTION_MATERIAL_TRANSPARENCY | AUTO_DRAW_OPTION_MATERIAL_SKIP_OPAQUE, &drawCalls);

    // Restore previous rendering state.
    DeviceSetRenderingState(device, &initialState);
  }
}

void updateCameraMovement(double latency)
{
  Vector direction = {0};

  if (movementDirection & MOVEMENT_DIRECTION_FORWARD)
  {
    Vector forward;
    ActorCameraGetForward(camera, &forward);
    direction = vectorAdd(direction, forward);
  }
  else if (movementDirection & MOVEMENT_DIRECTION_BACKWARD)
  {
    Vector forward;
    ActorCameraGetForward(camera, &forward);
    direction = vectorSubtract(direction, forward);
  }
  if (movementDirection & MOVEMENT_DIRECTION_LEFT)
  {
    Vector right;
    ActorCameraGetRight(camera, &right);
    direction = vectorSubtract(direction, right);
  }
  else if (movementDirection & MOVEMENT_DIRECTION_RIGHT)
  {
    Vector right;
    ActorCameraGetRight(camera, &right);
    direction = vectorAdd(direction, right);
  }
  if (movementDirection & MOVEMENT_DIRECTION_UP)
  {
    Vector ceiling;
    ActorCameraGetCeiling(camera, &ceiling);
    direction = vectorAdd(direction, ceiling);
  }
  else if (movementDirection & MOVEMENT_DIRECTION_DOWN)
  {
    Vector ceiling;
    ActorCameraGetCeiling(camera, &ceiling);
    direction = vectorSubtract(direction, ceiling);
  }

  float const acceleration = latency * CAMERA_ACCELERATION;

  if (!vectorEmpty(direction))
  {
    direction = vectorRescale(vectorNormalize(direction), acceleration);

    velocity = vectorAdd(velocity, direction);

    float const magnitude = vectorLength(velocity);
    if (magnitude > CAMERA_MOVEMENT_SPEED)
      velocity = vectorRescale(velocity, CAMERA_MOVEMENT_SPEED / magnitude);
  }
  else
  {
    float const magnitude = vectorLength(velocity);
    if (magnitude > 1e-6f)
    {
      float const newMagnitude = MAX(magnitude - acceleration, 0.0f);
      velocity = vectorRescale(velocity, newMagnitude / magnitude);
    }
  }
  if (!vectorEmpty(velocity))
  {
    Vector position;
    ActorCameraGetPosition(camera, &position);

    position = vectorAdd(position, vectorRescale(velocity, latency));
    if (position.y < CAMERA_MINIMUM_ALTITUDE)
      position.y = CAMERA_MINIMUM_ALTITUDE;

    ActorCameraSetPosition(camera, &position);

    SceneLight* light = SceneLightsGetElement(lights, 0);
    if (light)
      light->position.x = lightPosition.x + position.x;
  }
}

void updateVehicleMovement(double latency)
{
  float vehicleLength = 0.0f;

  Matrix view = {0};
  ActorCameraGetView(camera, &view);

  Vector cameraPosition = {0};
  ActorCameraGetPosition(camera, &cameraPosition);

  for (int32_t vehicleIndex = 0; vehicleIndex < VEHICLE_COUNT; ++vehicleIndex)
  {
    ObjectModel* const object = vehicles[vehicleIndex];
    ObjectLightTracking* const trackingVehicle = ObjectModelGetPayload(object);
    if (!object || !trackingVehicle || !trackingVehicle->movement)
      continue;

    if (vehicleLength < vectorEpsilon)
    { // Determine vehicle maximum dimensions.
      Vector dimensions = {0};
      ObjectModelGetSize(object, &dimensions);
      vehicleLength = MAX(MAX(dimensions.x, dimensions.y), dimensions.z);
    }
    Matrix transform;

    // Retrieve vehicle's global 3D position.
    ObjectModelGetTransform(object, MODEL_TRANSFORM_GLOBAL, &transform);
    Vector const position = matrixWorldPosition(transform);

    // Retrieve vehicle's local 3D position to move it around the bridges.
    ObjectModelGetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

    float const velocityDirection = transform.data[0][2];
    transform.data[3][0] += latency * VEHICLE_MOVEMENT_SPEED * velocityDirection;

    if (transform.data[3][0] < -bridgeSize.x * 0.5f)
    {
      trackingVehicle->movement->bridge = (trackingVehicle->movement->bridge +
        BRIDGE_COUNT - 1) % BRIDGE_COUNT;
      ObjectModelSetParent(object, bridges[trackingVehicle->movement->bridge]);
      transform.data[3][0] += bridgeSize.x;
    }
    else if (transform.data[3][0] > bridgeSize.x * 0.5f)
    {
      trackingVehicle->movement->bridge = (trackingVehicle->movement->bridge + 1) % BRIDGE_COUNT;
      ObjectModelSetParent(object, bridges[trackingVehicle->movement->bridge]);
      transform.data[3][0] -= bridgeSize.x;
    }
    ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

    // Calculate vehicle position in "view" space to determine if it is in front or behind the camera.
    Vector const positionView = vectorTransform(position, 1.0f, view);

    // Calculate 2D distance between the vehicle and camera position.
    float const distance = pointDistance(
      (Point){cameraPosition.x, cameraPosition.y},
      (Point){position.x, position.y});

    // If the vehicle is far behind the camera, or too far in front of the camera, skip shadow map updates.
    if ((positionView.z < 0.0f && distance > 500.0f) || distance > 4000.0f)
      continue; // Do not refresh shadow maps if vehicle is far behind the camera.

    // Mark the headlights to have their shadow maps updated.
    trackingVehicle->attributes |= LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED;

    // Determine nearby lampposts to update their shadow maps.
    for (int_fast8_t side = 0; side < 2; ++side)
    {
      float distanceBest = FLT_MAX;
      int32_t lamppostBest = -1;

      // Search in both directions to determine what lights may have the vehicle in their shadow caster view.
      for (int_fast8_t direction = 0; direction < 2; ++direction)
      {
        int_fast8_t const directionSign = (direction * 2) - 1;

        for (int_fast32_t i = direction; i < LIGHTS_PER_SIDE; ++i)
        {
          int32_t const lightIndex = (trackingVehicle->movement->lampposts[side] +
            (i * directionSign) + LIGHTS_PER_SIDE) % LIGHTS_PER_SIDE;
          SceneLight* const light = lamppostLights[side][lightIndex];

          ObjectLightTracking* const tracking = ObjectModelGetPayload(light ? light->payload : NULL);
          if (!tracking || (tracking->attributes & LIGHT_TRACKING_ATTRIBUTE_HEADLIGHT))
            continue;

          float const distance = vectorDistance(light->position, position);

          if (distance < light->attenuationEnd + vehicleLength)
          {
            tracking->attributes |=
              LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED |
              LIGHT_TRACKING_ATTRIBUTE_LATE_UPDATE_NEEDED;

            if (distance < distanceBest)
            {
              distanceBest = distance;
              lamppostBest = lightIndex;
            }
          }
          else if (lamppostBest != -1)
            break;
        }
        trackingVehicle->movement->lampposts[side] = lamppostBest;
      }
    }
  }
}

void updateBridges(bool startup)
{
  Vector cameraPos;
  ActorCameraGetPosition(camera, &cameraPos);

  for (int32_t bridgeIndex = 0; bridgeIndex < BRIDGE_COUNT; ++bridgeIndex)
  {
    ObjectModel* const object = bridges[bridgeIndex];
    if (!object)
      continue;

    Matrix transform;
    ObjectModelGetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

    float const position = transform.data[3][0] - cameraPos.x;

    if ((velocity.x < -vectorEpsilon || startup) && position > bridgeLength * BRIDGE_COUNT * 0.5f)
    {
      transform.data[3][0] -= bridgeLength * BRIDGE_COUNT;
      ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
    }
    else if ((velocity.x > vectorEpsilon || startup) && position < -bridgeLength * BRIDGE_COUNT * 0.5f)
    {
      transform.data[3][0] += bridgeLength * BRIDGE_COUNT;
      ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
    }
  }
}

void synchronizeLights(void)
{
  bool const depthClipNegative = DeviceGetBehavior(device) & DEVICE_BEHAVIOR_DEPTH_CLIP_NEGATIVE;

  for (int32_t i = 0; i < SceneLightsGetCount(lights); ++i)
  {
    SceneLight* const light = SceneLightsGetElement(lights, i);
    ObjectModel* const object = light ? light->payload : NULL;
    ObjectLightTracking* const tracking = ObjectModelGetPayload(object);
    if (!object || !tracking)
      continue;

    if (tracking->attributes & LIGHT_TRACKING_ATTRIBUTE_HEADLIGHT)
    { // Headlight
      int_fast8_t const headlightIndex = i % 2; // Headlights are created in pairs.

      Matrix transform = {0};
      ObjectModelGetLatchTransform(object, &transform, headlightIndex, false);

      light->position = matrixWorldPosition(transform);
      light->direction = vectorNormalize(vectorTransform((Vector){0.0f, -0.25f, -1.0f}, 0.0f, transform));

      if (headlightIndex == 0)
      { // There is only one shadow caster per two headlights.
        ObjectModelGetLatchTransform(object, &transform, headlightIndex ^ 1, false);

        // Shadow caster should be located in between the headlights.
        Vector const position = vectorRescale(vectorAdd(light->position,
          matrixWorldPosition(transform)), 0.5f);

        Matrix const lightView = matrixLookAt(position, vectorAdd(position, light->direction), vectorAxisY);
        Matrix const lightProjection = matrixPerspectiveFOVY((float)(M_PI * 0.55), 1.0f, 1.0f, 300.0f,
          depthClipNegative);

        ObjectModelViewSetView(tracking->view, &lightView);
        ObjectModelViewSetProjection(tracking->view, &lightProjection, depthClipNegative);
      }
    }
    else
    { // Lamppost
      Matrix transform = {0};
      ObjectModelGetTransform(object, MODEL_TRANSFORM_GLOBAL, &transform);

      Vector const position = matrixWorldPosition(matrixMultiply(matrixTranslate(
        (Vector){-2.85f, 24.4f, 0.0f}), transform));

      if (!vectorEquals(light->position, position))
        tracking->attributes |= LIGHT_TRACKING_ATTRIBUTE_UPDATE_QUEUED;

      light->position = position;

      Matrix const lightView = matrixLookAt(light->position, vectorAdd(light->position, light->direction),
        vectorNegate(vectorAxisZ));

      Matrix const lightProjection = matrixPerspectiveFOVY((float)(M_PI * 0.68), 1.0f, 10.0f, 60.0f,
        depthClipNegative);

      ObjectModelViewSetView(tracking->view, &lightView);
      ObjectModelViewSetProjection(tracking->view, &lightProjection, depthClipNegative);
    }
  }
}

bool createBridgeLampposts(ObjectModel* bridge, int32_t* sideLights)
{
  for (int32_t i = 0; i < LAMPPOST_COUNT; ++i)
  {
    float const theta = ((float)i / LAMPPOST_COUNT) - 0.5f;

    for (int32_t side = 0; side < 2; ++side)
    {
      // Create light that will be associated with the lamppost.
      SceneLight* const light = SceneLightsAdd(lights);
      if (!light)
        return false;

      FloatColorRGB const lightColor = (((i - side) % 3) != 2) ^ side ?
        (FloatColorRGB){1.0f, 0.95f, 0.85f} :
        (FloatColorRGB){0.85f, 0.95f, 1.0f};

      light->ambientColor = lightColor;
      light->albedoColor = lightColor;
      light->specularColor = floatColorRGB(0xFFFFFFu);
      light->specularExponent = 1.0f;
      light->intensity = 1.0f;
      light->direction = vectorNormalize(vectorTransform((Vector){0.0f, -1.0f, 0.0f}, 0.0f,
        matrixRotateX(-(float)(M_PI * 0.15) * (side == 0 ? 1 : -1))));
      light->attenuationEnd = 80.0f;
      light->attenuationStart = 40.0f;
      light->angleCutoff = (float)(M_PI * 0.4);
      light->angle = light->angleCutoff * 0.45f;
      light->shadowCaster = ShadowCastingAtlasGetCount(shadowCastingAtlas);

      // Assign light to cyclic array for fast searching.
      lamppostLights[side][sideLights[side]] = light;

      // Create lamppost's payload.
      ObjectLightTracking* tracking = calloc(1, sizeof(ObjectLightTracking));
      if (!tracking)
        return false;

      tracking->caster = ShadowCastingAtlasAdd(shadowCastingAtlas, &shadowMapSize, true);
      tracking->view = ObjectModelsCreateView(objects);
      tracking->attributes =
        LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED |
        LIGHT_TRACKING_ATTRIBUTE_LATE_UPDATE_NEEDED;

      if (!tracking->caster || !tracking->view)
        return false;

      // Create the lampost itself.
      ObjectModel* const lamppost = ObjectModelsAdd(objects, NULL, tracking,
        OBJECT_MODEL_ATTRIBUTE_VISIBLE | OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
      if (lamppost)
      {
        light->payload = lamppost;

        ObjectModelSetMeshName(lamppost, "Lamppost");
        ObjectModelSetParent(lamppost, bridge);
        ObjectModelSetMaterial(lamppost, 0);

        Matrix const transform = matrixMultiply(matrixRotateY((float)M_PI * (0.5f + side)),
          matrixTranslate((Vector){bridgeLength * theta + (side * (bridgeLength / LAMPPOST_COUNT) * 0.5f),
          bridgeAt, ((side * 2) - 1) * 37.0f}));

        ObjectModelSetTransform(lamppost, MODEL_TRANSFORM_LOCAL, &transform);
      }
      else
        return false;

      ++sideLights[side];
    }
  }
  return true;
}

bool createBridgeObstacles(ObjectModel* bridge, uint64_t* prng)
{
  for (int_fast8_t side = 0; side < 2; ++side)
  {
    int32_t const sign = side * 2 - 1;
    int32_t meshIndex = -1;

    for (float position = -bridgeSize.x * 0.5f; position < bridgeSize.x * 0.5f; )
    {
      char const* const meshNames[] = {"Roadblock2", "TrafficDrum", "Roadblock"};
      meshIndex = RandomSequenceGenerateRanged(prng, meshIndex != 2 ? 3 : 2);

      ObjectModel* object = ObjectModelsAdd(objects, NULL, NULL,
        OBJECT_MODEL_ATTRIBUTE_VISIBLE | OBJECT_MODEL_ATTRIBUTE_SELECTABLE);
      if (!object)
        return false;

      ObjectModelSetMeshName(object, meshNames[meshIndex]);
      ObjectModelSetParent(object, bridge);
      ObjectModelSetMaterial(object, 2);

      Vector objectSize = {0};
      ObjectModelGetSize(object, &objectSize);

      position += objectSize.x * 0.5f;
      if (meshIndex == 2)
        position += 10.0f + RandomSequenceGenerateFloat(prng) * 20.0f;

      if (position + objectSize.x * 0.5f >= bridgeSize.x * 0.5f)
        break;

      if (meshIndex == 2)
      {
        Matrix transform = matrixMultiply(matrixRotateY((1 - side) * (float)M_PI),
          matrixTranslate((Vector){position, bridgeAt, sign * 28.0f}));
        ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);

        object = ObjectModelsAdd(objects, NULL, NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
          OBJECT_MODEL_ATTRIBUTE_SELECTABLE);

        ObjectModelSetMeshName(object, meshNames[meshIndex]);
        ObjectModelSetParent(object, bridge);
        ObjectModelSetMaterial(object, 2);

        transform = matrixMultiply(matrixRotateY((1 - side) * (float)M_PI),
          matrixTranslate((Vector){position, bridgeAt, sign * 35.0f}));
        ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
      }
      else
      {
        Matrix const transform = matrixTranslate((Vector){position, bridgeAt, sign *
          (26.0f + RandomSequenceGenerateFloat(prng) - 0.5f)});
        ObjectModelSetTransform(object, MODEL_TRANSFORM_LOCAL, &transform);
      }
      position += objectSize.x * 0.5f + 1.0f;

      if (meshIndex == 2)
        position += 10.0f + RandomSequenceGenerateFloat(prng) * 20.0f;
      else if (RandomSequenceGenerateRanged(prng, 2) == 1)
        position += RandomSequenceGenerateFloat(prng) * 40.0f;
    }
  }
  return true;
}

bool createVehicles(uint64_t* prng)
{
  int32_t const vehicleCountHalf = (VEHICLE_COUNT + 1) / 2;

  for (int32_t vehicleIndex = 0; vehicleIndex < VEHICLE_COUNT; ++vehicleIndex)
  {
    int32_t const facing = vehicleIndex / vehicleCountHalf;
    int32_t const facingSign = 1 - (facing * 2);

    // Create lights that will serve as vehicle's headlights.
    SceneLight* headlights[2] = {0};
    int32_t const shadowCasterIndex = ShadowCastingAtlasGetCount(shadowCastingAtlas);

    for (int_fast8_t i = 0; i < 2; ++i)
    {
      headlights[i] = SceneLightsAdd(lights);
      if (!headlights[i])
        return false; // Failed creating one or more headlights.

      FloatColorRGB const lightColor = (FloatColorRGB){1.0f, 0.9f, 0.65f};

      headlights[i]->ambientColor = lightColor;
      headlights[i]->albedoColor = lightColor;
      headlights[i]->specularColor = floatColorRGB(0xFFFFFFu);
      headlights[i]->specularExponent = 4.0f;
      headlights[i]->intensity = 1.0f;
      headlights[i]->attenuationEnd = 300.0f;
      headlights[i]->attenuationStart = 100.0f;
      headlights[i]->angleCutoff = (float)(M_PI * 0.2);
      headlights[i]->angle = headlights[i]->angleCutoff * 0.45f;
      headlights[i]->shadowCaster = shadowCasterIndex;
    }
    // Create vehicle's payload.
    ObjectLightTracking* const tracking = calloc(1, sizeof(ObjectLightTracking));
    if (!tracking)
      return false;

    tracking->movement = calloc(1, sizeof(MovementTracking));
    if (!tracking->movement)
      return false;

    // Create vehicle's shadow caster and its view.
    tracking->caster = ShadowCastingAtlasAdd(shadowCastingAtlas, &shadowMapSize, true);
    tracking->view = ObjectModelsCreateView(objects);
    tracking->attributes = LIGHT_TRACKING_ATTRIBUTE_HEADLIGHT | LIGHT_TRACKING_ATTRIBUTE_UPDATE_NEEDED;
    tracking->movement->bridge = VEHICLE_COUNT > 8 ? vehicleIndex % BRIDGE_COUNT :
      (((vehicleIndex % vehicleCountHalf) * 2) + BRIDGE_COUNT - facing) % BRIDGE_COUNT;
    tracking->movement->lampposts[0] = tracking->movement->lampposts[1] = -1;
    if (!tracking->caster || !tracking->view)
      return false;

    // Create the vehicle itself.
    vehicles[vehicleIndex] = ObjectModelsAdd(objects, NULL, tracking,
      OBJECT_MODEL_ATTRIBUTE_VISIBLE | OBJECT_MODEL_ATTRIBUTE_SELECTABLE |
      OBJECT_MODEL_ATTRIBUTE_TRANSPARENT | OBJECT_MODEL_ATTRIBUTE_HIERARCHYLESS);

    ObjectModelSetMeshName(vehicles[vehicleIndex], "Convertible");
    ObjectModelSetParent(vehicles[vehicleIndex], bridges[tracking->movement->bridge]);
    ObjectModelSetMaterial(vehicles[vehicleIndex], 1);

    Matrix const transform = matrixMultiply(matrixRotateY((float)M_PI * 0.5f * facingSign), matrixTranslate(
      (Vector){(RandomSequenceGenerateFloat(prng) - 0.5f) * bridgeSize.x, bridgeAt, 11.0f * facingSign}));
    ObjectModelSetTransform(vehicles[vehicleIndex], MODEL_TRANSFORM_LOCAL, &transform);

    // Associate headlights with the vehicle for synchronization purposes.
    headlights[0]->payload = headlights[1]->payload = vehicles[vehicleIndex];
  }
  return true;
}

bool createObjects(void)
{
  int32_t sideLights[2] = {0};

  uint64_t prng = {0};
  RandomSequenceInit(&prng);

  for (int32_t bridgeIndex = 0; bridgeIndex < BRIDGE_COUNT; ++bridgeIndex)
  {
    ObjectModel* const bridge = ObjectModelsAdd(objects, NULL, NULL, OBJECT_MODEL_ATTRIBUTE_VISIBLE |
      OBJECT_MODEL_ATTRIBUTE_SELECTABLE | OBJECT_MODEL_ATTRIBUTE_HIERARCHYLESS);
    if (!bridge)
      return false;

    ObjectModelSetMeshName(bridge, "Bridge");
    ObjectModelSetMaterial(bridge, 0);

    if (vectorEmpty(bridgeSize))
    { // Retrieve bridge size, if it hasn't been retrieved yet.
      ObjectModelGetSize(bridge, &bridgeSize);
      bridgeLength = bridgeSize.x - 0.3f;
    }
    Matrix const transform = matrixTranslate((Vector){bridgeLength * bridgeIndex, 0.0f, 0.0f});
    ObjectModelSetTransform(bridge, MODEL_TRANSFORM_LOCAL, &transform);

    // Assign the bridge to cyclic array for fast vehicle positioning.
    bridges[bridgeIndex] = bridge;

    // Create lampposts, roadblocks and road drums on the bridge.
    if (!createBridgeLampposts(bridge, sideLights) || !createBridgeObstacles(bridge, &prng))
      return false;
  }
  if (!createVehicles(&prng))
    return false;

  // Make sure to update bridge positions to be around the camera and synchronize light positions.
  updateBridges(true);
  updateCameraMovement(0.0);
  synchronizeLights();

  return true;
}

bool createAndLoadMeshes(void)
{
  bool created = false;
  { // Create geometric meshes.
    MeshBuffer* meshBuffer = MeshBufferCreate();
    if (!meshBuffer)
      return false;

    MeshBufferPlane(meshBuffer, 4, 4, NULL, &(Vector){1.0f, 0.0f, 0.0f}, &(Vector){0.0f, 0.0f, 1.0f},
      &vectorAxisY, NULL, NULL, NULL, true);

    created = SceneMeshesAddFromBuffer(meshes, "Plane", meshBuffer, NULL, vertexElements,
      vertexElementsCount, NULL, NULL, 0, 0, 1.0f);

    MeshBufferDestroy(meshBuffer);
  }
  if (created)
  { // Load meshes from disk.
    static char const* const basePath = "../../../../Media/Bridge/";
    static char const* const meshNamesBinary[] = {"Bridge", "Lamppost", "Roadblock", "Roadblock2",
      "TrafficDrum", "Convertible"};

    for (size_t i = 0; i < sizeof(meshNamesBinary) / sizeof(char const*); ++i)
    {
      char fileName[128] = {0};

      strcpy(fileName, basePath);
      strcat(fileName, meshNamesBinary[i]);
      strcat(fileName, ".mesh");

      if (!SceneMeshesAddFromFile(meshes, meshNamesBinary[i], fileName, NULL, vertexElements,
        vertexElementsCount, 0, 0, NULL, 10.0f, NULL, NULL, NULL, NULL))
      {
        created = false;
        break;
      }
    }
  }
  return created;
}

void createLightsAndMaterials(void)
{
  SceneLight* light = SceneLightsAdd(lights);
  if (light)
  { // (0) Light for water and bridge
    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0xFFFFFFu);
    light->specularColor = floatColorRGB(0xFFFFFFu);
    light->specularExponent = 1.0f;
    light->intensity = 1.0f;
    light->position = lightPosition;
    light->bitmask = 0x01;
  }
  light = SceneLightsAdd(lights);
  if (light)
  { // (1) Ambient light for objects on the bridge
    light->ambientColor = floatColorRGB(0xFFFFFFu);
    light->albedoColor = floatColorRGB(0x000000u);
    light->specularColor = floatColorRGB(0x000000u);
    light->specularExponent = 1.0f;
    light->intensity = 1.0f;
    light->bitmask = 0x02;
  }

  ObjectMaterial* material = ObjectMaterialsAdd(materials);
  if (material)
  { // (0) Bridge and Lampposts
    material->ambientColor = (FloatColorRGB){0.15f, 0.15f, 0.15f};
    material->albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.5f};
    material->specularColor = (FloatColorRGB){0.25f, 0.25f, 0.25f};
    material->specularExponent = 16.0f;
    material->occlusion = 1.0f;
    material->roughness = (Vector){0.9f, 0.7f, 0.2f};
    material->technique = TECHNIQUE_LIGHTING_COOK_TORRANCE;
    material->bitmask = 0x01;
  }
  material = ObjectMaterialsAdd(materials);
  if (material)
  { // (1) Vehicles
    material->ambientColor = (FloatColorRGB){0.2f, 0.2f, 0.2f};
    material->albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.5f};
    material->specularColor = (FloatColorRGB){1.0f, 1.0f, 1.0f};
    material->specularExponent = 32.0f;
    material->occlusion = 1.0f;
    material->roughness = (Vector){0.9f, 0.7f, 0.2f};
    material->technique = TECHNIQUE_LIGHTING_COOK_TORRANCE;
    material->bitmask = 0x02;
  }
  material = ObjectMaterialsAdd(materials);
  if (material)
  { // (2) Roadblocks and traffic drums
    material->ambientColor = (FloatColorRGB){0.2f, 0.2f, 0.2f};
    material->albedoColor = (FloatColor){1.0f, 1.0f, 1.0f, 0.5f};
    material->specularColor = (FloatColorRGB){1.0f, 1.0f, 1.0f};
    material->specularExponent = 4.0f;
    material->occlusion = 1.0f;
    material->technique = TECHNIQUE_LIGHTING_PHONG;
    material->bitmask = 0x02;
  }
}

void releaseObjectLightTracking(void)
{
  for (ObjectModel* object = ObjectModelsGetFirst(objects); object; object = ObjectModelGetNext(object))
  {
    ObjectLightTracking* tracking = ObjectModelGetPayload(object);
    if (tracking)
    {
      if (tracking->movement)
        free(tracking->movement);

      free(tracking);
    }
  }
}

void outputError(char const* message)
{
#ifdef _WIN32
  static char const* initErrorTitle = "Initialization Error";
  MessageBoxA(NULL, message, initErrorTitle, MB_ICONERROR | MB_OK | MB_DEFAULT_DESKTOP_ONLY);
#else
  printf("%s\n", message);
#endif
}

// Main application entry.

#ifdef _WIN32

int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prevInstance, _In_ LPWSTR cmdLine,
  _In_ int showCmd)
{
  UNREFERENCED_PARAMETER(prevInstance);
  UNREFERENCED_PARAMETER(cmdLine);
  UNREFERENCED_PARAMETER(showCmd);

  char const* windowClassName = "AfterwarpNightBridgeDemo";

#else

int main(int argc, char **argv)
{

#endif

  ApplicationConfiguration configuration = {0};
  configuration.size = displaySize;
  configuration.position = (Point){.x = INT32_MAX, .y = INT32_MAX}; // default position
  configuration.state = APPLICATION_WINDOW_STATE_FULLSCREEN;
#ifdef _WIN32
  configuration.startup.win.handleInstance = instance;
  configuration.startup.win.handleIconSmall = LoadIcon(instance, MAKEINTRESOURCE(IDI_AFTERWARP));
  configuration.startup.win.handleIconBig = LoadIcon(instance, MAKEINTRESOURCE(IDI_AFTERWARP));

  strncpy(configuration.startup.win.windowClassName, windowClassName,
    sizeof(configuration.startup.win.windowClassName));
#else
  configuration.startup.nx.parameterCount = argc;
  configuration.startup.nx.parameterStrings = argv;
#endif

  application = ApplicationCreate(&configuration);
  if (!application)
  {
    outputError("Could not create Application object.");
    return -1;
  }

  ApplicationEvents events = {0};
  events.eventCreate = applicationCreate;
  events.eventDestroy = applicationDestroy;
  events.eventResize = applicationResize;
  events.eventMouse = applicationMouse;
  events.eventKey = applicationKey;
  events.eventStatus = applicationStatus;
  events.eventRender = applicationRender;
  events.eventIdle = applicationIdle;
  ApplicationSetEvents(application, &events, NULL);

  displayScale = ApplicationGetWindowScale(application);
  ApplicationSetTitle(application, "Afterwarp: NightBridge example");

  { // Retrieve client size back in case it has changed since startup.
    Rect clientRect = {0};
    ApplicationGetClientRect(application, &clientRect);

    displaySize = rectGetSize(clientRect);
  }

  bool const executed = ApplicationExecute(application);
  ApplicationDestroy(application);

  return executed ? 0 : -1;
}