/*
 * 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.

namespace NightBridge
{
  using System.Numerics;
  using System.Windows.Forms;
  using Afterwarp;
  using TextRenderer = Afterwarp.TextRenderer;
  using Timer = Afterwarp.Timer;
  using Application = System.Windows.Forms.Application;
  using static System.Runtime.InteropServices.JavaScript.JSType;

  public partial class MainForm : Form
  {
    private Device device;
    private SwapChain swapChain;
    private Canvas canvas;
    private TextRenderer textRenderer;
    private Timer timer;

    private SceneMeshes meshes;
    private ObjectModels objects;
    private SceneLights lights;
    private ObjectMaterials materials;

    private TextureCabinet textureCabinet;
    private ShadowCastingAtlas shadowCastingAtlas;

    private Scene sceneDepthNormals;
    private Scene sceneModeling;
    private OceanSimulation oceanSimulation;
    private SpatialFog spatialFog;

    private ObjectModelView viewport;
    private ActorCamera camera;
    private Matrix4x4 projection;

    private Vector2 displaySize;
    private Vector3 bridgeSize;
    private float bridgeLength;

    private uint movementDirection;
    private Vector3 velocity;
    private Vector2 lastSmoothMouse = new(float.NaN, float.NaN);

    private bool shadowCastingAtlasPrepared;
    private int shadowMapsUpdated;
    private int objectsVisible;
    private int drawCalls;

    private static readonly VertexElement[] vertexElements = [
      new VertexElement("vertexPosition", ElementFormat.Float, 3),
      new VertexElement("vertexNormal", ElementFormat.Float, 3),
      new VertexElement("vertexColor", ElementFormat.UnsignedByte, 4, VertexElement.ChannelNormalized)
    ];

    // Position of an abstract light source (sun/moon) that will illuminate water and the bridge.
    private static readonly Vector3 lightPosition = new(-16000.0f, 2000.0f, 4000.0f);

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

    // Parameters for controling camera movement.
    private const float cameraAcceleration = 400.0f;
    private const float cameraMovementSpeed = 120.0f;
    private const float cameraMinimumAltitude = 430.0f;

    // How fast cars travel.
    private const float vehicleMovementSpeed = 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.
    private const int bridgeCount = 9;

    // Total number of vehicles over all bridges.
    private const int vehicleCount = 8;

    // The size of shadow mapping atlas. This must be sufficient to accomodate all shadow maps used by
    // lampposts and vehicle's headlights.
    private Point shadowMapAtlasSize = new (2560, 2048);

    // The size of shadow map for the lampposts and vehicles.
    private Point shadowMapSize = new (128, 128);

    // Number of lampposts on each side of the bridge.
    private const int lamppostCount = 16;

    // Calculated number of lights on each side of the bridge based on the number of lampposts.
    private const int lightsPerSide = bridgeCount * lamppostCount;

    // Indices to lampposts on each side of the bridge for quick lookups.
    private int[,] lamppostLights;

    // A circular list of shortcuts to each of the bridges.
    private ObjectModel[] bridges;

    // An array of all vehicles.
    private ObjectModel[] vehicles;

    private struct MovementDirection
    {
      /// Forward movement bit.
      public const uint Forward = 0x01u;

      /// Backward movement bit.
      public const uint Backward = 0x02u;

      /// Left movement bit.
      public const uint Left = 0x04u;

      /// Right movement bit.
      public const uint Right = 0x08u;

      /// Up movement bit.
      public const uint Up = 0x10u;

      /// Down movement bit.
      public const uint Down = 0x20u;
    }

    // Cumulative bits that define characteristics of light object.
    private struct LightTrackingAttribute
    {
      // Shadow map needs to be re-rendered.
      public const byte UpdateNeeded = 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.
      public const byte LateUpdateNeeded = 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.
      public const byte UpdateQueued = 0x04;

      // Light corresponds to a vehicle headlight.
      public const byte Headlight = 0x08;
    }

    // Additional information added to each "vehicle" for movement tracking.
    private class MovementTracking
    {
      public MovementTracking(int bridge, int lamppostLeft = -1, int lamppostRight = -1)
      {
        Bridge = bridge;
        LamppostLeft = lamppostLeft;
        LamppostRight = lamppostRight;
      }

      // Parent bridge index.
      public int Bridge { get; set; }

      // Index to closest lamppost on left side.
      public int LamppostLeft { get; set; }

      // Index to closest lamppost on right side.
      public int LamppostRight { get; set; }
    }

    // Additional parameters added to each "important" object in form of "payload".
    private class ObjectLightTracking
    {
      public ObjectLightTracking(ObjectModelView view, ShadowCaster caster, byte attributes,
        MovementTracking movement = null)
      {
        View = view;
        Caster = caster;
        Attributes = attributes;
        Movement = movement;
      }

      // View for a shadow caster associated with the lamppost or headlight.
      public ObjectModelView View { get; }

      // Shadow caster associated with the lamppost or headlight.
      public ShadowCaster Caster { get; }

      // Cumulative attribute bits.
      public byte Attributes { get; set; }

      // Additional and optional information for vehicle movement.
      public MovementTracking Movement { get; }
    }

    public MainForm()
    {
      InitializeComponent();
      Application.Idle += Application_Idle;
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
      displaySize = new Vector2(ClientSize.Width, ClientSize.Height);
      try
      {
        device = new Device(DeviceTechnology.Unknown);
        swapChain = new SwapChain(device, Handle, null, PixelFormat.Unknown, PixelFormat.Unknown, 0, true);
        canvas = new Canvas(device);
        textRenderer = new TextRenderer(canvas, new Point(512, 512));

        meshes = new SceneMeshes(device);
        objects = new ObjectModels(meshes);
        lights = new SceneLights(device);
        materials = new ObjectMaterials();

        textureCabinet = new TextureCabinet(device, new Point(displaySize), TextureFidelity.High, 8);
        shadowCastingAtlas = new ShadowCastingAtlas(device, shadowMapAtlasSize,
          TechniqueShadows.EVSM, 8, 0);

        sceneDepthNormals = Scene.CreateDepthNormals(device);
        sceneDepthNormals.SetVertexElements(0, vertexElements);

        sceneModeling = Scene.CreateModeling(device);
        sceneModeling.SetVertexElements(0, vertexElements);

        oceanSimulation = new OceanSimulation(device);
        spatialFog = new SpatialFog(device);
        timer = new Timer();
        viewport = objects.CreateView();

        camera = new ActorCamera(position: new Vector3(x: 0.0f, y: 440.0f, z: 0.0f),
          rotation: new Vector3(x: 0.0f, y: MathF.PI * -0.5f, z: 0.0f), distance: 0.0f); // first-person view

        CreateLightsAndMaterials();
        CreateAndLoadMeshes(meshes);
        CreateObjects();

        WindowState = FormWindowState.Normal;
        FormBorderStyle = FormBorderStyle.None;
        WindowState = FormWindowState.Maximized;

        Cursor.Position = PointToScreen(new System.Drawing.Point(Width / 2, Height / 2));
        Cursor.Hide();
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.Message, "Initialization failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
        Application.Exit();
      }
      SetStyle(ControlStyles.Opaque, true);
      SetStyle(ControlStyles.UserPaint, true);
      SetStyle(ControlStyles.AllPaintingInWmPaint, true);
      SetStyle(ControlStyles.DoubleBuffer, false);
    }

    private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
    {
      for (int i = 0; i < lights.Count; ++i)
        Utility.FreePayload(lights.GetLight(i).Payload);

      foreach (ObjectModel obj in objects)
        Utility.FreePayload(obj.Payload);

      camera?.Dispose();
      timer?.Dispose();
      spatialFog?.Dispose();
      oceanSimulation?.Dispose();
      sceneModeling?.Dispose();
      sceneDepthNormals?.Dispose();
      shadowCastingAtlas?.Dispose();
      textureCabinet?.Dispose();
      materials?.Dispose();
      lights?.Dispose();
      objects?.Dispose();
      meshes?.Dispose();
      textRenderer?.Dispose();
      canvas?.Dispose();
      swapChain?.Dispose();
      device?.Dispose();
    }

    private void MainForm_Resize(object sender, EventArgs e)
    {
      if (WindowState != FormWindowState.Minimized && device is not null)
      {
        displaySize = new Vector2(ClientSize.Width, ClientSize.Height);
        swapChain.Resize(new Point(displaySize));
        textureCabinet.Size = new Point(displaySize);
      }
    }

    private void MainForm_Paint(object sender, PaintEventArgs e)
    {
      const float farPlane = 8000.0f;

      float scale = DeviceDpi / 96.0f;
      double latency = timer.Latency / (1000.0 * 1000.0);
      drawCalls = 0;

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

      // Prepare view and projection matrices.
      Matrix4x4 view = camera.View;
      projection = Utility.MatrixPerspective4x4(MathF.PI * 0.25f, displaySize.X / displaySize.Y,
        4.0f, farPlane);

      // Set view/projection matrices and update visible objects.
      viewport.View = view;
      viewport.Projection = projection;
      viewport.Update();

      { // Update texture cabinet's attributes.
        uint attributes = 0
          | TextureCabinet.Attribute.Bloom
          | TextureCabinet.Attribute.BloomCubic
          | TextureCabinet.Attribute.AmbientOcclusion
          | TextureCabinet.Attribute.LinearDepths;

        if (scale >= 2.0f)
          attributes |= TextureCabinet.Attribute.BloomDownscale |
            TextureCabinet.Attribute.AmbientOcclusionDownscale;

        textureCabinet.Attributes = attributes;

        // Adjust tone-mapping bloom parameters for HDR rendering.
        ToneMappingBloom toneMappingBloom = textureCabinet.ToneMappingBloom;
        toneMappingBloom.ToneWhite = 2.0f;
        toneMappingBloom.BloomThreshold = 1.5f;
        textureCabinet.ToneMappingBloom = toneMappingBloom;

        // Adjust ambient occlusion parameters.
        AmbientOcclusionParameters parameters = textureCabinet.AmbientOcclusionParameters;
        parameters.Radius = 4.0f;
        parameters.KernelBias = 0.05f;
        textureCabinet.AmbientOcclusionParameters = parameters;
      }

      // Prepare scene's attributes.
      uint sceneAttributes = 0
        | Scene.Attribute.Normals
        | Scene.Attribute.Coloring
        | Scene.Attribute.ShadowsCubic
        | Scene.Attribute.DepthPrePass
        | Scene.Attribute.LinearDepths;

      // Render depth pre-pass.
      textureCabinet.Clear(TextureCabinetPass.Depth, new FloatColor(
        farPlane, farPlane, farPlane, farPlane));
      textureCabinet.Begin(TextureCabinetPass.Depth);
      try
      {
        sceneDepthNormals.Attributes = sceneAttributes;
        sceneDepthNormals.View = view;
        sceneDepthNormals.Projection = projection;

        sceneDepthNormals.Begin();
        try
        {
          RenderObjects(sceneDepthNormals, viewport);
        }
        finally
        {
          sceneDepthNormals.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);

      // Perform light assignment for 3D scene.
      lights.Size = new Point(displaySize);
      lights.CullingMode = ClustersCullingMode.Quality;
      lights.ClusterSize = (int)MathF.Round(32 * scale);
      lights.DepthSlices = 24;
      lights.Execute(view, projection);

      { // Render shadow maps.
        shadowMapsUpdated = 0;

        ShadowParameters shadowParams = shadowCastingAtlas.Parameters;
        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.
        shadowCastingAtlas.Parameters = shadowParams;

        if (!shadowCastingAtlasPrepared)
        { // Fill shadow casting atlas with border color only after startup.
          shadowCastingAtlas.BorderFill();
          shadowCastingAtlasPrepared = true;
        }
        ShadowCaster casterToFilter = null;
        int 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 (int i = 0; i < lights.Count; ++i)
        {
          SceneLight light = lights.GetLight(i);
          ObjectLightTracking tracking = Utility.RetrievePayload<ObjectLightTracking>(
            Utility.RetrievePayload<ObjectModel>(light.Payload)?.Payload ?? IntPtr.Zero);

          if (tracking is null)
            continue;

          if (lightQueueProcessed == -1 && (tracking.Attributes & LightTrackingAttribute.UpdateQueued) != 0)
            lightQueueProcessed = i; // Process a single shadow map from queue.

          if (lightQueueProcessed == i || (tracking.Attributes &
            (LightTrackingAttribute.UpdateNeeded | LightTrackingAttribute.LateUpdateNeeded)) != 0)
          {
            tracking.View.Update();
            tracking.Caster.ViewProjection = tracking.View.ViewProjection;

            bool excludeVehicles = (tracking.Attributes & LightTrackingAttribute.UpdateNeeded) == 0;
            casterToFilter?.Filter(); // Filter previous caster before continuing with the current one.

            tracking.Caster.Clear();
            tracking.Caster.Begin();
            try
            {
              sceneDepthNormals.Attributes = 0;
              sceneDepthNormals.View = tracking.View.View;
              sceneDepthNormals.Projection = tracking.View.Projection;

              sceneDepthNormals.Begin();
              try
              {
                RenderObjects(sceneDepthNormals, tracking.View, excludeVehicles);
              }
              finally
              {
                sceneDepthNormals.End();
              }
            }
            finally
            {
              tracking.Caster.End();
            }
            ++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 & LightTrackingAttribute.UpdateNeeded) != 0)
              tracking.Attributes &= unchecked((byte)~LightTrackingAttribute.UpdateNeeded);
            else
              tracking.Attributes &= unchecked((byte)~LightTrackingAttribute.LateUpdateNeeded);

            // If the shadow map was waiting in queue to be processed, remove its "queue" bit.
            tracking.Attributes &= unchecked((byte)~LightTrackingAttribute.UpdateQueued);
          }
        }
        casterToFilter?.Filter(); // Filter previous caster before continuing with the current one.
      }

      // Render main scene.
      textureCabinet.Clear(TextureCabinetPass.Color, new FloatColor(0xFF404040u));
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        sceneModeling.Attributes = sceneAttributes;
        sceneModeling.View = viewport.View;
        sceneModeling.Projection = viewport.Projection;

        sceneModeling.Lights = lights;
        sceneModeling.TextureCabinet = textureCabinet;
        sceneModeling.Atlas = shadowCastingAtlas;

        sceneModeling.Prepare();
        sceneModeling.Begin();
        try
        {
          RenderObjects(sceneModeling, viewport);
        }
        finally
        {
          sceneModeling.End();
        }

        // Update ocean's material.
        OceanMaterial material = oceanSimulation.Material;
        material.Extinction = 0.008f;
        oceanSimulation.Material = material;

        // Configure ocean's appearance.
        oceanSimulation.Plane = new Vector4(0.0f, 1.0f, 0.0f, -200.0f);
        oceanSimulation.Sections = new Point(1280 * 3 / 2, 720 * 3 / 2);
        oceanSimulation.View = view;
        oceanSimulation.Projection = projection;

        // Render the ocean.
        oceanSimulation.Render(textureCabinet.GetTexture(TextureCabinetType.LinearDepths), lights);
      }
      finally
      {
        textureCabinet.End();
      }

      UpdateCameraMovement(latency);
      UpdateVehicleMovement(latency);
      UpdateBridges();
      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.
      textureCabinet.Begin(TextureCabinetPass.Depth);
      try
      {
        Vector3 position = camera.GetPosition();

        sceneDepthNormals.Attributes = sceneAttributes;
        sceneDepthNormals.World = Matrix4x4.CreateScale(32000.0f, 1.0f, 32000.0f) *
          Matrix4x4.CreateTranslation(position.X, 215.0f, position.Z);

        sceneDepthNormals.View = view;
        sceneDepthNormals.Projection = projection;

        sceneDepthNormals.Begin();
        try
        {
          meshes.Mesh("Plane").Model?.Draw(sceneDepthNormals.Program);
        }
        finally
        {
          sceneDepthNormals.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      { // Apply fog.
        float fade = 0.15f;

        FogParameters parameters = FogParameters.Default;
        parameters.Color = new FloatColor(0.011765f * fade, 0.333333f * fade, 0.470588f * fade, 0.0f);
        parameters.Opacity = 1.0f;
        parameters.GroundPlane = new Vector4(0.0f, 1.0f, 0.0f, -600.0f);
        parameters.Scattering = 1.0f;
        parameters.Extinction = 0.0003f;
        parameters.DensityGround = 0.03f;
        spatialFog.Parameters = parameters;

        spatialFog.Formula = FogFormula.Exponential;
        spatialFog.Distance = DepthFogDistance.EyeRelative;
        spatialFog.View = view;
        spatialFog.Projection = projection;

        spatialFog.Execute(textureCabinet.GetTexture(TextureCabinetType.ColorHDR),
          textureCabinet.GetTexture(TextureCabinetType.LinearDepths));
      }
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);

      // Render on the form.
      swapChain.Begin();
      try
      {
        device.Clear(DeviceClear.Color, new FloatColor(0xFF404040u));
        textureCabinet.Present();

        canvas.Begin();
        try
        {
          canvas.ContextState = CanvasContextState.FlatScene;
          { // Draw a crosshair to know where the camera is pointing at.
            float crossHairInside = 8.0f * scale;
            float crossHairOutside = 12.0f * scale;

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

            Vector2 center = new(displaySize.X * 0.5f, displaySize.Y * 0.5f);

            const uint color = 0x80FFFFFFu;
            ColorPair colorInside = new(first: Utility.MakeColor(color, 0), second: color);
            ColorPair colorOutside = new(first: color, second: Utility.MakeColor(color, 0));

            canvas.Ribbon(center, new Vector2(crossHairInside), new Vector2(crossHairOutside), 32, color);

            // Simulate anti-aliasing by rendering 1-pixel secondary ribbons inside and outside of the main
            // ribbon.
            canvas.Ribbon(center, new Vector2(crossHairInsideMask), new Vector2(crossHairInside), 32,
              colorInside, colorInside, colorInside);

            canvas.Ribbon(center, new Vector2(crossHairOutside), new Vector2(crossHairOutsideMask), 32,
              colorOutside, colorOutside, colorOutside);
          }
          // Render information text.
          canvas.ContextState = CanvasContextState.FlatText;
          canvas.SamplerState = CanvasSamplerState.Default;
          { // Draw text hints.
            FontParameters parameters = new("Segoe UI", 14.0f * scale, FontWeight.SemiBold);
            parameters.Effect.BorderType = FontBorder.SemiHeavy;
            parameters.Effect.BorderBrightness = 1.0f;
            parameters.Effect.FillBrightness = 0.0f;
            parameters.Effect.ShadowBrightness = 0.75f;
            parameters.Effect.ShadowOpacity = 1.0f;
            textRenderer.Parameters = parameters;

            textRenderer.DrawAligned(new Vector2(displaySize.X * 0.5f, 4.0f * scale),
              "W/S keys - move forward and backward, A/D keys - move left and right.",
              new ColorPair(0xFFA5E7B8u, 0xFF77FFCEu), TextAlignment.Middle, TextAlignment.Start);

            textRenderer.DrawAligned(new Vector2(displaySize.X * 0.5f, displaySize.Y - 4.0f * scale),
              "Mouse movement - rotate scene, Space/C keys - move up and down.",
              new ColorPair(0xFFA5E7B8u, 0xFF77FFCEu), TextAlignment.Middle, TextAlignment.End);

            textRenderer.DrawAligned(new Vector2(8.0f * scale, 4.0f * scale),
              $"Frame Rate: {(int)MathF.Round(timer.FrameRate)}", new ColorPair(0xA0FFFFFFu),
              TextAlignment.Start, TextAlignment.Start);

            textRenderer.DrawAligned(new Vector2(displaySize.X - 12.0f * scale, 4.0f * scale),
              (device.Technology == DeviceTechnology.Direct3D ?
              $"Direct3D ({device.TechVersion >> 8}.{(device.TechVersion >> 4) & 0xF}), " :
              $"OpenGL ({device.TechVersion >> 8}.{(device.TechVersion >> 4) & 0xF}+), ") +
              $"features: {device.TechFeatureVersion >> 8}.{(device.TechFeatureVersion >> 4) & 0xF}",
              new ColorPair(0xA0FFFFFFu), TextAlignment.End, TextAlignment.Start);

            textRenderer.Draw(new Vector2(8.0f * scale, 32.0f * scale),
              $"Lights: {lights.Count}, shadow maps updated: {shadowMapsUpdated}",
              new ColorPair(0xA0FFFFFFu));

            textRenderer.DrawAligned(new Vector2(displaySize.X - 12.0f * scale, 28.0f * scale),
              $"Objects visible: {objectsVisible} (out of {objects.Count}), draw calls: {drawCalls}",
              new ColorPair(0xA0FFFFFFu), TextAlignment.End, TextAlignment.Start);
          }
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
      timer.Update();
    }

    private void Application_Idle(object sender, EventArgs e)
    {
      if (WindowState != FormWindowState.Minimized)
        Invalidate(); // Continuously repaint the form.
    }

    private void MainForm_MouseMove(object sender, MouseEventArgs e)
    {
      const float sensitivity = 0.001f;
      const float alpha = 0.6f;

      Point position = new(e.X, e.Y);
      Point middle = new(Width / 2, Height / 2);
      Point offset = position - middle;

      if (offset.X != 0 || offset.Y != 0)
      {
        Vector2 smoothMouse;

        if (!float.IsNaN(lastSmoothMouse.X) && !float.IsNaN(lastSmoothMouse.Y))
          smoothMouse = Vector2.Lerp(new Vector2(offset.X, offset.Y), lastSmoothMouse, alpha);
        else
          smoothMouse = new Vector2(offset.X, offset.Y);

        lastSmoothMouse = smoothMouse;

        Vector4 rotation = camera.GetRotation();
        rotation.X += smoothMouse.Y * sensitivity;
        rotation.Y += smoothMouse.X * sensitivity;
        camera.SetRotation(rotation);

        Cursor.Position = PointToScreen(new System.Drawing.Point(middle.X, middle.Y));
      }
    }

    private void MainForm_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Return && e.Modifiers.HasFlag(Keys.Alt))
      {
        if (WindowState == FormWindowState.Maximized)
        {
          FormBorderStyle = FormBorderStyle.Sizable;
          WindowState = FormWindowState.Normal;
        }
        else
        {
          WindowState = FormWindowState.Normal;
          FormBorderStyle = FormBorderStyle.None;
          WindowState = FormWindowState.Maximized;
        }
      }
      else if (e.KeyCode == Keys.Escape)
        Close();
      else if (e.KeyCode == Keys.W || e.KeyCode == Keys.Up)
        movementDirection |= MovementDirection.Forward;
      else if (e.KeyCode == Keys.S || e.KeyCode == Keys.Down)
        movementDirection |= MovementDirection.Backward;
      else if (e.KeyCode == Keys.A || e.KeyCode == Keys.Left)
        movementDirection |= MovementDirection.Left;
      else if (e.KeyCode == Keys.D || e.KeyCode == Keys.Right)
        movementDirection |= MovementDirection.Right;
      else if (e.KeyCode == Keys.C)
        movementDirection |= MovementDirection.Down;
      else if (e.KeyCode == Keys.Space)
        movementDirection |= MovementDirection.Up;
    }

    private void MainForm_KeyUp(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.W || e.KeyCode == Keys.Up)
        movementDirection &= ~MovementDirection.Forward;
      else if (e.KeyCode == Keys.S || e.KeyCode == Keys.Down)
        movementDirection &= ~MovementDirection.Backward;
      else if (e.KeyCode == Keys.A || e.KeyCode == Keys.Left)
        movementDirection &= ~MovementDirection.Left;
      else if (e.KeyCode == Keys.D || e.KeyCode == Keys.Right)
        movementDirection &= ~MovementDirection.Right;
      else if (e.KeyCode == Keys.C)
        movementDirection &= ~MovementDirection.Down;
      else if (e.KeyCode == Keys.Space)
        movementDirection &= ~MovementDirection.Up;
    }

    private void UpdateCameraMovement(double latency)
    {
      Vector3 direction = default;

      if ((movementDirection & MovementDirection.Forward) != 0)
        direction += camera!.Forward;
      else if ((movementDirection & MovementDirection.Backward) != 0)
        direction -= camera!.Forward;

      if ((movementDirection & MovementDirection.Left) != 0)
        direction -= camera!.Right;
      else if ((movementDirection & MovementDirection.Right) != 0)
        direction += camera!.Right;

      if ((movementDirection & MovementDirection.Up) != 0)
        direction += camera!.Ceiling;
      else if ((movementDirection & MovementDirection.Down) != 0)
        direction -= camera!.Ceiling;

      float acceleration = (float)(latency * cameraAcceleration);

      if (direction.Length() > 1e-6f)
      {
        direction = Vector3.Normalize(direction) * acceleration;
        velocity += direction;

        float magnitude = velocity.Length();
        if (magnitude > cameraMovementSpeed)
          velocity *= cameraMovementSpeed / magnitude;
      }
      else
      {
        float magnitude = velocity.Length();
        if (magnitude > 1e-6f)
        {
          float newMagnitude = MathF.Max(magnitude - acceleration, 0.0f);
          velocity *= newMagnitude / magnitude;
        }
      }

      if (velocity.Length() > 1e-6f)
      {
        Vector3 position = camera.GetPosition();

        position += velocity * (float)latency;
        if (position.Y < cameraMinimumAltitude)
          position.Y = cameraMinimumAltitude;

        camera.SetPosition(position);

        SceneLight light = lights.GetLight(0);
        light.Position.X = lightPosition.X + position.X;
        lights.SetLight(0, light);
      }
    }

    private void UpdateVehicleMovement(double latency)
    {
      float vehicleLength = 0.0f;
      Matrix4x4 view = camera.View;
      Vector3 cameraPosition = camera.GetPosition();

      for (int vehicleIndex = 0; vehicleIndex < vehicleCount; ++vehicleIndex)
      {
        ObjectModel obj = vehicles[vehicleIndex];
        ObjectLightTracking trackingVehicle = Utility.RetrievePayload<ObjectLightTracking>(obj.Payload);

        if (trackingVehicle is null || trackingVehicle.Movement is null)
          continue;

        if (vehicleLength < 1e-6f)
        { // Determine vehicle maximum dimensions.
          Vector3 dimensions = obj.Size;
          vehicleLength = (float)Math.Max(Math.Max(dimensions.X, dimensions.Y), dimensions.Z);
        }
        Matrix4x4 transform = obj.GetTransform(ModelTransform.Global);

        // Retrieve vehicle's global 3D position.
        Vector3 position = transform.Translation;

        // Retrieve vehicle's local 3D position to move it around the bridges.
        transform = obj.GetTransform(ModelTransform.Local);

        float velocityDirection = transform.M13;
        transform.M41 += (float)(latency * vehicleMovementSpeed * velocityDirection);

        if (transform.M41 < -bridgeSize.X * 0.5f)
        {
          trackingVehicle.Movement.Bridge = (trackingVehicle.Movement.Bridge +
            bridgeCount - 1) % bridgeCount;
          obj.Parent = bridges[trackingVehicle.Movement.Bridge];
          transform.M41 += bridgeSize.X;
        }
        else if (transform.M41 > bridgeSize.X * 0.5f)
        {
          trackingVehicle.Movement.Bridge = (trackingVehicle.Movement.Bridge + 1) % bridgeCount;
          obj.Parent = bridges[trackingVehicle.Movement.Bridge];
          transform.M41 -= bridgeSize.X;
        }
        obj.SetTransform(ModelTransform.Local, transform);

        // Calculate vehicle position in "view" space to determine if it is in front or behind the camera.
        Vector3 positionView = Vector3.Transform(position, view);

        // Calculate 2D distance between the vehicle and camera position.
        float distanceToCamera = Vector2.Distance(new Vector2(cameraPosition.X, cameraPosition.Y),
          new Vector2(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 && distanceToCamera > 500.0f) || distanceToCamera > 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 |= LightTrackingAttribute.UpdateNeeded;

        // Determine nearby lampposts to update their shadow maps.
        for (int side = 0; side < 2; ++side)
        {
          float distanceBest = float.MaxValue;
          int lamppostBest = -1;
          int currentLamppostBest = side == 1 ? trackingVehicle.Movement.LamppostRight :
            trackingVehicle.Movement.LamppostLeft;

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

            for (int i = direction; i < lightsPerSide; ++i)
            {
              int lightIndex = (currentLamppostBest + (i * directionSign) + lightsPerSide) % lightsPerSide;

              SceneLight light = lights.GetLight(lamppostLights[side, lightIndex]);
              ObjectLightTracking tracking = Utility.RetrievePayload<ObjectLightTracking>(
                Utility.RetrievePayload<ObjectModel>(light.Payload)?.Payload ?? IntPtr.Zero);

              if (tracking is null || (tracking.Attributes & LightTrackingAttribute.Headlight) != 0)
                continue;

              float distance = Vector3.Distance(light.Position, position);

              if (distance < light.AttenuationEnd + vehicleLength)
              {
                tracking.Attributes |= LightTrackingAttribute.UpdateNeeded |
                  LightTrackingAttribute.LateUpdateNeeded;

                if (distance < distanceBest)
                {
                  distanceBest = distance;
                  lamppostBest = lightIndex;
                }
              }
              else if (lamppostBest != -1)
                break;
            }
          }
          if (side == 1)
            trackingVehicle.Movement.LamppostRight = lamppostBest;
          else
            trackingVehicle.Movement.LamppostLeft = lamppostBest;
        }
      }
    }

    private void RenderObjects(Scene scene, ObjectModelView view, bool excludeVehicles = false)
    {
      bool multisampling = textureCabinet.Samples > 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.
      uint options = AutoDrawOption.MaterialSkipTransparent;

      if (excludeVehicles)
        options |= AutoDrawOption.ObjectSkipTransparent;

      drawCalls += view.AutoDraw(scene, materials, options);

      if (!excludeVehicles && (scene.Attributes & Scene.Attribute.Modeling) != 0)
      {
        // Update number of visible objects only when main scene is being rendered.
        objectsVisible = view.ObjectCount;

        // Retrieve current rendering state.
        RenderingState initialState = device.RenderingState;

        // 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 |= RenderingState.State.BlendEnable;
        state.BlendColor.Source = BlendFactor.One;
        state.BlendColor.Dest = BlendFactor.Zero;
        // Destination alpha-channel actually contains bloom factors, so reduce it based on source alpha values.
        state.BlendAlpha.Source = BlendFactor.Zero;
        state.BlendAlpha.Dest = BlendFactor.InvSourceAlpha;

        if (!multisampling)
        {
          state.BlendColor.Source = BlendFactor.SourceAlpha;
          state.BlendColor.Dest = BlendFactor.InvSourceAlpha;
        }
        else
          state.States |= RenderingState.State.AlphaToCoverage;

        state.DepthFunc = ComparisonFunc.GreaterEqual;
        device.RenderingState = 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.
        drawCalls += view.AutoDraw(scene, materials, AutoDrawOption.ObjectSkipNonTransparent |
          AutoDrawOption.MaterialTransparency | AutoDrawOption.MaterialSkipOpaque);

        // Restore previous rendering state.
        device.RenderingState = initialState;
      }
    }

    private void UpdateBridges(bool startup = false)
    {
      Vector3 cameraPos = camera.GetPosition();

      for (int bridgeIndex = 0; bridgeIndex < bridgeCount; ++bridgeIndex)
      {
        ObjectModel obj = bridges[bridgeIndex];

        Matrix4x4 transform = obj.GetTransform(ModelTransform.Local);
        float position = transform.M41 - cameraPos.X;

        if ((velocity.X < -1e-6f || startup) && position > bridgeLength * bridgeCount * 0.5f)
        {
          transform.M41 -= bridgeLength * bridgeCount;
          obj.SetTransform(ModelTransform.Local, transform);
        }
        else if ((velocity.X > 1e-6f || startup) && position < -bridgeLength * bridgeCount * 0.5f)
        {
          transform.M41 += bridgeLength * bridgeCount;
          obj.SetTransform(ModelTransform.Local, transform);
        }
      }
    }

    private void SynchronizeLights()
    {
      for (int i = 0; i < lights.Count; ++i)
      {
        SceneLight light = lights.GetLight(i);
        ObjectModel obj = Utility.RetrievePayload<ObjectModel>(light.Payload);
        ObjectLightTracking tracking = Utility.RetrievePayload<ObjectLightTracking>(
          obj?.Payload ?? IntPtr.Zero);

        if (tracking is null)
          continue;

        if ((tracking.Attributes & LightTrackingAttribute.Headlight) != 0)
        { // Headlight
          int headlightIndex = i % 2; // Headlights are created in pairs.

          Matrix4x4 transform = obj.GetLatchTransform(headlightIndex);

          light.Position = transform.Translation;
          light.Direction = Vector3.Normalize(Vector3.TransformNormal(new Vector3(0.0f, -0.25f, -1.0f),
            transform));

          if (headlightIndex == 0)
          { // There is only one shadow caster per two headlights.
            transform = obj.GetLatchTransform(headlightIndex ^ 1);

            // Shadow caster should be located in between the headlights.
            Vector3 position = (light.Position + transform.Translation) * 0.5f;

            tracking.View.View = Utility.MatrixLookAt4x4(position, position + light.Direction,
              Vector3.UnitY);

            tracking.View.Projection = Utility.MatrixPerspective4x4((float)Math.PI * 0.55f, 1.0f,
              1.0f, 300.0f);
          }
        }
        else
        { // Lamppost
          Matrix4x4 transform = obj.GetTransform(ModelTransform.Global);

          Vector3 position = (Matrix4x4.CreateTranslation(new Vector3(-2.85f, 24.4f, 0.0f)) *
            transform).Translation;

          if (light.Position != position)
            tracking.Attributes |= LightTrackingAttribute.UpdateQueued;

          light.Position = position;

          tracking.View.View = Utility.MatrixLookAt4x4(light.Position, light.Position + light.Direction,
            -Vector3.UnitZ);

          tracking.View.Projection = Utility.MatrixPerspective4x4((float)Math.PI * 0.68f, 1.0f,
            10.0f, 60.0f);
        }
        // Update light parameters.
        lights.SetLight(i, light);
      }
    }

    private void CreateBridgeLampposts(ObjectModel bridge, ref int[] sideLights)
    {
      for (int i = 0; i < lamppostCount; ++i)
      {
        float theta = ((float)i / lamppostCount) - 0.5f;

        for (int side = 0; side < 2; ++side)
        {
          // Create light that will be associated with the lamppost.
          int lightIndex = lights.Count;

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

          SceneLight light = SceneLight.Default;
          light.AmbientColor = lightColor;
          light.AlbedoColor = lightColor;
          light.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
          light.SpecularExponent = 1.0f;
          light.Direction = Vector3.Normalize(Vector3.TransformNormal(new Vector3(0.0f, -1.0f, 0.0f),
            Matrix4x4.CreateRotationX(-(float)Math.PI * 0.15f * (side == 0 ? 1 : -1))));
          light.AttenuationEnd = 80.0f;
          light.AttenuationStart = 40.0f;
          light.AngleCutoff = (float)Math.PI * 0.4f;
          light.Angle = light.AngleCutoff * 0.45f;
          light.ShadowCaster = shadowCastingAtlas.Count;

          // Create lamppost payload while creating the lamppost itself.
          ObjectModel lamppost = objects.Add(null, Utility.AssignPayload(new ObjectLightTracking(
            objects.CreateView(), shadowCastingAtlas.Add(shadowMapSize),
            LightTrackingAttribute.UpdateNeeded | LightTrackingAttribute.LateUpdateNeeded, null)));

          lamppost.Parent = bridge;
          lamppost.MeshName = "Lamppost";
          lamppost.Material = 0;
          lamppost.SetTransform(ModelTransform.Local, Matrix4x4.CreateRotationY(MathF.PI * (0.5f + side)) *
            Matrix4x4.CreateTranslation(bridgeLength * theta + (side *
            (bridgeLength / lamppostCount) * 0.5f), bridgeAt, ((side * 2) - 1) * 37.0f));

          light.Position = (Matrix4x4.CreateTranslation(new Vector3(-2.85f, 24.4f, 0.0f)) *
            lamppost.GetTransform(ModelTransform.Global)).Translation;

          // Assign lamppost as a payload to light source.
          light.Payload = Utility.AssignPayload(lamppost);
          lights.Add(light);

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

          // Increment number of lights on the current side.
          ++sideLights[side];
        }
      }
    }

    private void CreateBridgeObstacles(ObjectModel bridge, ref RandomSequence prng)
    {
      string[] meshNames = {"Roadblock2", "TrafficDrum", "Roadblock"};

      for (int side = 0; side < 2; ++side)
      {
        int sign = side * 2 - 1;
        int meshIndex = -1;

        for (float position = -bridgeSize.X * 0.5f; position < bridgeSize.X * 0.5f;)
        {
          meshIndex = prng.Ranged(meshIndex != 2 ? 3 : 2);

          ObjectModel obj = objects.Add();
          obj.Parent = bridge;
          obj.MeshName = meshNames[meshIndex];
          obj.Material = 2;

          Vector3 objectSize = obj.Size;
          position += objectSize.X * 0.5f;

          if (meshIndex == 2)
            position += 10.0f + prng.Value * 20.0f;

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

          if (meshIndex == 2)
          {
            obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateRotationY((1 - side) * (float)Math.PI) *
              Matrix4x4.CreateTranslation(new Vector3(position, bridgeAt, sign * 28.0f)));

            obj = objects.Add();
            obj.Parent = bridge;
            obj.MeshName = meshNames[meshIndex];
            obj.Material = 2;

            obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateRotationY((1 - side) * (float)Math.PI) *
              Matrix4x4.CreateTranslation(new Vector3(position, bridgeAt, sign * 35.0f)));
          }
          else
            obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(new Vector3(position,
              bridgeAt, sign * (26.0f + prng.Value - 0.5f))));

          position += objectSize.X * 0.5f + 1.0f;

          if (meshIndex == 2)
            position += 10.0f + prng.Value * 20.0f;
          else if (prng.Ranged(2) == 1)
            position += prng.Value * 40.0f;
        }
      }
    }

    private void CreateVehicles(ref RandomSequence prng)
    {
      const int vehicleCountHalf = (vehicleCount + 1) / 2;

      for (int vehicleIndex = 0; vehicleIndex < vehicleCount; ++vehicleIndex)
      {
        int facing = vehicleIndex / vehicleCountHalf;
        int facingSign = 1 - (facing * 2);

        // Create lights that will serve as vehicle's headlights.
        SceneLight[] headlights = new SceneLight[2];
        int shadowCasterIndex = shadowCastingAtlas.Count;

        for (int i = 0; i < headlights.Length; ++i)
        {
          FloatColorRGB lightColor = new (1.0f, 0.9f, 0.65f);

          headlights[i] = SceneLight.Default;
          headlights[i].AmbientColor = lightColor;
          headlights[i].AlbedoColor = lightColor;
          headlights[i].SpecularColor = new 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)Math.PI * 0.2f;
          headlights[i].Angle = headlights[i].AngleCutoff * 0.45f;
          headlights[i].ShadowCaster = shadowCasterIndex;
        }

        // Create vehicle's payload.
        ObjectLightTracking tracking = new (objects.CreateView(), shadowCastingAtlas.Add(shadowMapSize),
          LightTrackingAttribute.Headlight | LightTrackingAttribute.UpdateNeeded,
          new (vehicleCount > 8 ? vehicleIndex % bridgeCount : (((vehicleIndex % vehicleCountHalf) * 2) +
          bridgeCount - facing) % bridgeCount));

        // Create the vehicle itself.
        vehicles[vehicleIndex] = objects.Add(null, Utility.AssignPayload(tracking),
          ObjectModel.Attribute.Visible | ObjectModel.Attribute.Selectable |
          ObjectModel.Attribute.Transparent | ObjectModel.Attribute.Hierarchyless);

        vehicles[vehicleIndex].Parent = bridges[tracking.Movement.Bridge];
        vehicles[vehicleIndex].MeshName = "Convertible";
        vehicles[vehicleIndex].Material = 1;
        vehicles[vehicleIndex].SetTransform(ModelTransform.Local, Matrix4x4.CreateRotationY(
          (float)Math.PI * 0.5f * facingSign) * Matrix4x4.CreateTranslation(new Vector3(
            (prng.Value - 0.5f) * bridgeSize.X, bridgeAt, 11.0f * facingSign)));

        // Associate headlights with the vehicle for synchronization purposes.
        headlights[0].Payload = headlights[1].Payload = Utility.AssignPayload(vehicles[vehicleIndex]);

        // Add headlights to the list of lights.
        lights.Add(headlights[0]);
        lights.Add(headlights[1]);
      }
    }

    private void CreateObjects()
    {
      int[] sideLights = {0, 0};
      RandomSequence prng = new ();

      bridges = new ObjectModel[bridgeCount];
      vehicles = new ObjectModel[vehicleCount];
      lamppostLights = new int[2, lightsPerSide];

      for (int bridgeIndex = 0; bridgeIndex < bridgeCount; ++bridgeIndex)
      {
        ObjectModel bridge = objects.Add(null, IntPtr.Zero, ObjectModel.Attribute.Visible |
          ObjectModel.Attribute.Selectable | ObjectModel.Attribute.Hierarchyless);
        bridge.MeshName = "Bridge";
        bridge.Material = 0;

        if (bridgeSize.Length() < 1e-6f)
        { // Retrieve bridge size, if it hasn't been retrieved yet.
          bridgeSize = bridge.Size;
          bridgeLength = bridgeSize.X - 0.3f;
        }
        bridge.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(
          bridgeLength * bridgeIndex, 0.0f, 0.0f));

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

        // Create lampposts, roadblocks and road drums on the bridge.
        CreateBridgeLampposts(bridge, ref sideLights);
        CreateBridgeObstacles(bridge, ref prng);
      }
      CreateVehicles(ref prng);

      // Make sure to update bridge positions to be around the camera and synchronize light positions.
      UpdateBridges(true);
      UpdateCameraMovement(0.0);
      SynchronizeLights();
    }

    private static void CreateAndLoadMeshes(SceneMeshes meshes)
    {
      using (MeshBuffer buffer = new())
      {
        buffer.Plane(4, 4, default, Vector3.UnitX, Vector3.UnitZ, Vector3.UnitY);

        meshes.AddFromBuffer("Plane", buffer, vertexElements);

        buffer.Clear();
        buffer.Cube(1, 1, 1, default, Vector3.UnitX, Vector3.UnitY, Vector3.UnitZ);

        meshes.AddFromBuffer("Cube", buffer, vertexElements);
      }
      string basePath = "../../../../Media/Bridge/";
      string[] meshNames = {"Bridge", "Lamppost", "Roadblock", "Roadblock2", "TrafficDrum", "Convertible"};

      foreach (string meshName in meshNames)
      {
        uint options = 0;
        meshes.AddFromFile(meshName, basePath + meshName + ".mesh", vertexElements, ref options,
          null, 0, 0, 10.0f);
      }
    }

    private void CreateLightsAndMaterials()
    {
      { // (0) Light for water and bridge
        SceneLight light = SceneLight.Default;
        light.AmbientColor = new FloatColorRGB(0x00FFFFFFu);
        light.AlbedoColor = new FloatColorRGB(0x00FFFFFFu);
        light.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
        light.SpecularExponent = 1.0f;
        light.Intensity = 1.0f;
        light.Position = lightPosition;
        light.Bitmask = 0x01;
        lights.Add(light);
      }
      { // (1) Ambient light for objects on the bridge
        SceneLight light = SceneLight.Default;
        light.AmbientColor = new FloatColorRGB(0x00FFFFFFu);
        light.AlbedoColor = new FloatColorRGB(0x00000000u);
        light.SpecularColor = new FloatColorRGB(0x00000000u);
        light.SpecularExponent = 1.0f;
        light.Intensity = 1.0f;
        light.Bitmask = 0x02;
        lights.Add(light);
      }

      { // (0) Bridge and Lampposts
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.15f, 0.15f, 0.15f);
        material.AlbedoColor = new FloatColor(1.0f, 1.0f, 1.0f, 0.5f);
        material.SpecularColor = new FloatColorRGB(0.25f, 0.25f, 0.25f);
        material.SpecularExponent = 16.0f;
        material.Occlusion = 1.0f;
        material.Roughness = new Vector3(0.9f, 0.7f, 0.2f);
        material.Technique = TechniqueLighting.CookTorrance;
        material.Bitmask = 0x01;
        materials.Add(material);
      }
      { // (1) Vehicles
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.2f, 0.2f, 0.2f);
        material.AlbedoColor = new FloatColor(1.0f, 1.0f, 1.0f, 0.5f);
        material.SpecularColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.SpecularExponent = 32.0f;
        material.Occlusion = 1.0f;
        material.Roughness = new Vector3(0.9f, 0.7f, 0.2f);
        material.Technique = TechniqueLighting.CookTorrance;
        material.Bitmask = 0x02;
        materials.Add(material);
      }
      { // (2) Roadblocks and traffic drums
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.2f, 0.2f, 0.2f);
        material.AlbedoColor = new FloatColor(1.0f, 1.0f, 1.0f, 0.5f);
        material.SpecularColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.SpecularExponent = 4.0f;
        material.Occlusion = 1.0f;
        material.Technique = TechniqueLighting.Phong;
        material.Bitmask = 0x02;
        materials.Add(material);
      }
    }
  }
}