/*
 * 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:
//  * Signed Distance Field (SDF) text rendering in 3D: a technique to draw good looking text in 3D with
//    any zoom factor and an optional outline.
//  * Grapher module, which allows drawing lines, arrows and other simple shapes in 3D.
//  * Optional real-time 3D volumetric text.
//  * Third-person view camera with optional location-based zoom, which zooms in or out where the mouse is
//    pointing at.

namespace Chart3D
{
  using System;
  using System.Numerics;
  using System.Windows.Forms;
  using Afterwarp;
  using TextRenderer = Afterwarp.TextRenderer;
  using Application = System.Windows.Forms.Application;

  public partial class MainForm : Form
  {
    private Device device;
    private SwapChain swapChain;
    private Canvas canvas;
    private TextRenderer textRenderer;
    private TextModeller textModeller;
    private Grapher grapher;

    private SceneLights lights;
    private TextureCabinet textureCabinet;
    private Scene scene;

    private MeshModel modelPlane;
    private MeshModel modelCube;

    private ActorCamera camera;
    private Matrix4x4 view;
    private Matrix4x4 projection;
    private Matrix4x4 viewProjection;
    private Matrix4x4 viewInverse;

    private Vector2 displaySize;
    private Vector2 mousePos;
    private Ray mouseRay;

    private float axisLength;
    private readonly Vector3[] axisFinal = new Vector3[3];
    private float axisThickness;

    private Vector3 location;
    private bool locationLocked;
    private bool volumetricText;

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

    private static readonly Vector3 lightPosition = new(300.0f, 800.0f, -300.0f);

    public MainForm()
    {
      InitializeComponent();
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
      displaySize = new Vector2(ClientSize.Width, ClientSize.Height);
      try
      {
        device = new Device(DeviceTechnology.Direct3D);
        swapChain = new SwapChain(device, Handle);
        canvas = new Canvas(device);
        textRenderer = new TextRenderer(canvas, new Point(512, 512));
        textModeller = new TextModeller(device);
        grapher = new Grapher(device);

        lights = new SceneLights(device);

        textureCabinet = new TextureCabinet(device, new Point(displaySize), TextureFidelity.High, 8)
        {
          Attributes = 0
            | TextureCabinet.Attribute.Bloom
            | TextureCabinet.Attribute.BloomCubic
            | TextureCabinet.Attribute.AmbientOcclusion
        };
        { // Configure Ambient Occlusion effect.
          AmbientOcclusionParameters parameters = textureCabinet.AmbientOcclusionParameters;
          parameters.KernelBias = 0.1f;
          parameters.Strength = 1.0f;
          parameters.Radius = 6.0f;
          textureCabinet.AmbientOcclusionParameters = parameters;
        }

        scene = Scene.CreateModeling(device);
        scene.SetVertexElements(0, vertexElements);
        scene.SetVertexElementsFromTextModeller(1);

        camera = new ActorCamera(position: new Vector3(3.0f, -2.5f, -0.15f),
          rotation: new Vector3(0.584f, -0.585f, 0.0f), distance: 320.0f)
        {
          Constraints = new CameraConstraints(
            positionMin: new Vector4(-100.0f, -100.0f, -100.0f, 150.0f),
            positionMax: new Vector4( 100.0f,  100.0f,  100.0f, 600.0f),
            rotationMin: new Vector4(MathF.PI * 0.1f, 0.0f, 0.0f, 0.0f),
            rotationMax: new Vector4(MathF.PI * 0.4f, 0.0f, 0.0f, 0.0f))
        };
        {
          using MeshBuffer buffer = new();

          buffer.Plane(horizSections: 1, vertSections: 1, origin: default, horizAxis: Vector3.UnitX,
            vertAxis: Vector3.UnitZ, normal: Vector3.UnitY);

          modelPlane = buffer.Model(device, vertexElements);

          buffer.Clear();
          buffer.Transform = Matrix4x4.CreateRotationX(MathF.PI * 0.5f);

          buffer.SuperEllipse(longitudeSections: 32, latitudeSections: 32, origin: default,
            radius: new Vector3(0.5f, 0.5f, 0.5f), initLongitudeAngle: 0.0f,
            endLongitudeAngle: MathF.PI * 2.0f, longitudeShape: 0.15f, initLatitudeAngle: MathF.PI * -0.5f,
            endLatitudeAngle: MathF.PI * 0.5f, latitudeShape: 0.15f);

          modelCube = buffer.Model(device, vertexElements);
        }
        {
          SceneLight light = SceneLight.Default;
          light.AmbientColor = new FloatColorRGB(0x00FFFFFFu);
          light.AlbedoColor = new FloatColorRGB(0x00FFFFFFu);
          light.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
          light.Position = new Vector3(200.0f, 200.0f, -400.0f);
          lights.Add(light);
        }

        MouseWheel += new MouseEventHandler(MainForm_MouseWheel);
      }
      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)
    {
      scene?.Dispose();
      lights?.Dispose();
      textureCabinet?.Dispose();

      modelCube?.Dispose();
      modelPlane?.Dispose();
      camera?.Dispose();

      grapher?.Dispose();
      textModeller?.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)
    {
      float scale = DeviceDpi / 96.0f;
      SetupPlotting(scale);

      // Perform light assignment for 3D scene.
      lights.Size = new Point(displaySize);

      // Only one light is used for the entire scene, so only minimal light culling is required.
      lights.CullingMode = ClustersCullingMode.Performance;
      lights.ClusterSize = 512;
      lights.DepthSlices = 1;

      lights.Execute(view, projection);

      // Prepare scene's attributes.
      const uint sceneAttributes = Scene.Attribute.Normals | Scene.Attribute.SinglePass;

      // Render main scene.
      textureCabinet.Clear(TextureCabinetPass.Color, new FloatColor(0xFFFFFFFFu));
      textureCabinet.Begin(TextureCabinetPass.Color);
      try
      {
        scene.Lights = lights;
        scene.Prepare();

        // Render volumetric 3D text.
        if (volumetricText)
        {
          scene.ActiveVertexElements = 1;
          scene.Attributes = sceneAttributes | Scene.Attribute.Coloring;

          scene.Begin();
          try
          {
            RenderText3D();
          }
          finally
          {
            scene.End();
          }
        }

        // Render 3D cubes.
        scene.ActiveVertexElements = 0;
        scene.Attributes = sceneAttributes;
        scene.Begin();
        try
        {
          RenderCubes();
        }
        finally
        {
          scene.End();
        }

        // Render chart using Grapher module.
        RenderChart();

        // Render flat text in 3D using Canvas "projected" mode.
        canvas.Begin();
        try
        {
          RenderText();
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);

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

        canvas.Begin();
        try
        { // Draw text hints.
          canvas.Attributes = 0;
          canvas.ContextState = CanvasContextState.FlatText;
          canvas.SamplerState = CanvasSamplerState.Default;

          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.Draw(new Vector2(8.0f * scale, 4.0f * scale),
            string.Format("Direct3D ({0}.{1}), features: {2}.{3}",
            device.TechVersion >> 8, (device.TechVersion >> 4) & 0xF,
            device.TechFeatureVersion >> 8, (device.TechFeatureVersion >> 4) & 0xF),
            new ColorPair(0xA0FFFFFFu), 1.0f);

          textRenderer.DrawAligned(new Vector2(8.0f * scale, displaySize.Y - 4.0f * scale), volumetricText ?
            "3D volumetric text. Press Space to change." :  "SDF text in 3D. Press Space to change.",
            new ColorPair(0xA0FFFFFFu), TextAlignment.Start, TextAlignment.End);
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
    }

    private void MainForm_MouseDown(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      Vector3 position = new(mousePos, 0.0f);

      if (e.Button == MouseButtons.Left && ModifierKeys.HasFlag(Keys.Shift))
      {
        if (locationLocked)
        {
          locationLocked = false;
          location = Vector3.Zero;
        }
        else if (location.Length() > 1e-6f)
          locationLocked = true;
      }
      else if (e.Button == MouseButtons.Left)
        camera.Command(CameraCommand.Rotation, position, displaySize);
      else if (e.Button == MouseButtons.Right)
        camera.Command(CameraCommand.Dragging, position, displaySize);
    }

    private void MainForm_MouseMove(object sender, MouseEventArgs e)
    {
      mousePos = new Vector2(e.X, e.Y);
      mouseRay = new(mousePos, displaySize, viewInverse, projection);

      if (!locationLocked)
      {
        bool valid = false;

        if (mouseRay.IntersectPlane(Vector3.Zero, Vector3.UnitY, out Vector3 intersection, out float _) &&
          intersection.X >= 0.0f && intersection.X < axisLength - 14.0f &&
          intersection.Z <= 0.0f && intersection.Z > -(axisLength - 12.0f))
          valid = true;
        else if (mouseRay.IntersectPlane(Vector3.Zero, Vector3.UnitX, out intersection, out float _) &&
          intersection.Y >= 0.0f && intersection.Y < axisLength - 14.0f &&
          intersection.Z <= 0.0f && intersection.Z > -(axisLength - 12.0f))
          valid = true;
        else if (mouseRay.IntersectPlane(Vector3.Zero, Vector3.UnitZ, out intersection, out float _) &&
          intersection.X >= 0.0f && intersection.X < axisLength - 14.0f &&
          intersection.Y >= 0.0f && intersection.Y < axisLength - 12.0f)
          valid = true;

        if (!valid)
          intersection = Vector3.Zero;

        if (location != intersection)
        {
          location = intersection;
          Invalidate();
        }
      }
      if (camera.Command(CameraCommand.Update, new Vector3(mousePos, 0.0f), displaySize))
        Invalidate();
    }

    private void MainForm_MouseUp(object sender, MouseEventArgs e)
    {
      camera.Command(CameraCommand.Stop, new Vector3(e.X, e.Y, 0.0f), displaySize);
    }

    private void MainForm_MouseWheel(object sender, MouseEventArgs e)
    {
      camera.Zoom(ModifierKeys.HasFlag(Keys.Shift) ? mousePos : displaySize * 0.5f, displaySize,
        projection, -e.Delta * 0.125f);
      Invalidate();
    }

    private void MainForm_KeyDown(object sender, KeyEventArgs e)
    {
      if (e.KeyCode == Keys.Space)
      {
        volumetricText = !volumetricText;
        Invalidate();
      }
      else if (e.KeyCode == Keys.Escape)
        Close();
    }

    private void RenderText()
    {
      // How many numbers to show over each axis.
      int numberCount = (int)MathF.Floor(axisLength / 20.0f) - 1;

      // Offset for rendering text relative to axis.
      float offset = axisThickness * 0.5f + 1.0f;

      // Configure canvas for 3D projected SDF rendering.
      canvas.SignedDistanceField = new SignedDistanceField(SuperSampleSDF.SuperSample16x, 2.0f,
        Vector3.UnitY, 0.15f, 0.45f);

      canvas.Attributes = Canvas.Attribute.Projected | Canvas.Attribute.SDF | Canvas.Attribute.Cubic;
      canvas.ContextState = CanvasContextState.Projected;
      canvas.SamplerState = new CanvasSamplerState(TextureFilter.Linear, TextureFilter.Linear);
      canvas.ViewProjection = viewProjection;

      { // Enable depth bias to prevent depth fighting with plane.
        RenderingState renderingState = device.RenderingState;
        renderingState.States &= ~RenderingState.State.DepthWrite;
        renderingState.DepthBias = 5.0f;
        renderingState.SlopeDepthBias = 5.0f;
        device.RenderingState = renderingState;
      }
      { // Prepare SDF font.
        FontParameters parameters = new("Verdana", 32.0f, FontWeight.SemiBold);
        parameters.Effect.SignedFieldDistance = 2.0f;
        textRenderer.Parameters = parameters;
      }
      // Prepare SDF font modifiers.
      TextRenderModifiers modifiers = default;
      modifiers.Scale = 0.2f;

      // "axis1"
      canvas.World = Matrix4x4.CreateRotationX(MathF.PI * 0.5f);

      textRenderer.DrawAligned(new Vector2(axisLength, -offset), "axis1", ColorPair.Black,
        TextAlignment.Middle, TextAlignment.End, 1.0f, false, modifiers);

      // numbers for "axis2"
      for (int i = 0; i < numberCount; ++i)
        textRenderer.DrawAligned(new Vector2(axisLength + offset, (i + 1) * 20.0f), (i + 1).ToString(),
          ColorPair.Black, TextAlignment.Start, TextAlignment.Middle, 1.0f, false, modifiers);

      // Mouse pointer values.
      if (!volumetricText && location.Length() > 1e-6f)
      {
        if (MathF.Abs(location.Y) <= 0.01f)
          textRenderer.DrawAligned(new Vector2(-offset, -location.Z),
            string.Format("{0:0.##}", -location.Z / 20.0f), new ColorPair(0xFF505050u),
            TextAlignment.End, TextAlignment.Middle, 1.0f, false, modifiers);

        if (MathF.Abs(location.X) <= 0.01f)
          textRenderer.DrawAligned(new Vector2(offset, -location.Z),
            string.Format("{0:0.##}", -location.Z / 20.0f), new ColorPair(0xFF505050u),
            TextAlignment.Start, TextAlignment.Middle, 1.0f, false, modifiers);
      }

      // "axis2"
      canvas.World = Matrix4x4.CreateRotationX(MathF.PI * 0.5f) *
        Matrix4x4.CreateRotationY(MathF.PI * -0.5f);

      textRenderer.DrawAligned(new Vector2(-axisLength, -offset), "axis2", ColorPair.Black,
        TextAlignment.Middle, TextAlignment.End, 1.0f, false, modifiers);

      // numbers for "axis1"
      for (int i = 0; i < numberCount; ++i)
        textRenderer.DrawAligned(new Vector2(-axisLength - offset, (i + 1) * 20.0f),
          ((i + 1) * 5).ToString(), ColorPair.Black, TextAlignment.End, TextAlignment.Middle, 1.0f, false,
          modifiers);

      // Mouse pointer values.
      if (!volumetricText && location.Length() > 1e-6f)
      {
        if (MathF.Abs(location.Y) <= 0.01f)
          textRenderer.DrawAligned(new Vector2(offset, location.X),
            string.Format("{0:0.##}", (location.X / 20.0f) * 5.0f), new ColorPair(0xFF505050u),
            TextAlignment.Start, TextAlignment.Middle, 1.0f, false, modifiers);

        if (MathF.Abs(location.Z) <= 0.01f)
          textRenderer.DrawAligned(new Vector2(-offset, location.X),
            string.Format("{0:0.##}", (location.X / 20.0f) * 5.0f), new ColorPair(0xFF505050u),
            TextAlignment.End, TextAlignment.Middle, 1.0f, false, modifiers);
      }

      // "axis3"
      canvas.World = Matrix4x4.CreateRotationZ(MathF.PI * 0.5f);

      textRenderer.DrawAligned(new Vector2(axisLength, -offset), "axis3", ColorPair.Black,
        TextAlignment.Middle, TextAlignment.End, 1.0f, false, modifiers);

      // numbers for "axis3"
      canvas.World = Matrix4x4.Identity;

      for (int i = 0; i < numberCount; ++i)
        textRenderer.DrawAligned(new Vector2(-offset, -(i + 1) * 20.0f),
          ((int)MathF.Pow(i + 11.0f, 2.0f) * 37).ToString(), ColorPair.Black, TextAlignment.End,
          TextAlignment.Middle, 1.0f, false, modifiers);

      // Mouse pointer values.
      if (!volumetricText && location.Length() > 1e-6f)
      {
        canvas.World = Matrix4x4.CreateRotationY(MathF.PI * -0.5f);

        if (MathF.Abs(location.X) <= 0.01f || MathF.Abs(location.Z) <= 0.01f)
          textRenderer.DrawAligned(new Vector2(offset, -location.Y),
            ((int)MathF.Pow((location.Y / 20.0f) + 10.0f, 2.0f) * 37.0f).ToString(),
            new ColorPair(0xFF505050u), TextAlignment.Start, TextAlignment.Middle, 1.0f, false, modifiers);
      }

      if (!volumetricText)
      {
        // Enable Outline in SDF settings.
        canvas.SignedDistanceField = new SignedDistanceField(SuperSampleSDF.SuperSample16x, 2.0f,
          new Vector3(-0.5f, 1.0f, -1.0f), 0.15f, 0.45f);

        canvas.Attributes = Canvas.Attribute.Projected | Canvas.Attribute.SDF | Canvas.Attribute.Outline |
          Canvas.Attribute.Cubic;

        // Update SDF font modifiers for Outline.
        modifiers.Scale = 0.3f;

        // "chart 2"
        canvas.World = Matrix4x4.CreateRotationX(MathF.PI * 0.5f);

        textRenderer.DrawAligned(new Vector2(40.0f, 65.0f), "Chart 2", new ColorPair(0xFF29D48Au),
          TextAlignment.Middle, TextAlignment.Start, 1.0f, false, modifiers);

        // Change SDF outline color.
        canvas.SignedDistanceField = new SignedDistanceField(SuperSampleSDF.SuperSample16x, 2.0f,
          new Vector3(-0.5f, 1.0f, 1.0f), 0.15f, 0.45f);

        // "chart 1"
        canvas.World = Matrix4x4.Identity;

        textRenderer.DrawAligned(new Vector2(40.0f, -80.0f), "Chart 1", new ColorPair(0xFF0177FFu),
          TextAlignment.Middle, TextAlignment.End, 1.0f, false, modifiers);
      }
    }

    private void RenderText3D()
    {
      scene.World = Matrix4x4.Identity;

      ObjectMaterial material = ObjectMaterial.Default;
      material.AmbientColor = new FloatColorRGB(0x00404040u);
      material.AlbedoColor = new FloatColor(0xD0FFFFFFu);
      material.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
      material.SpecularExponent = 8.0f;
      scene.Material = material;

      textModeller.Clear();
      textModeller.SetFontParameters(new FontParameters("Verdana", 32.0f, FontWeight.SemiBold));

      TextRenderModifiers modifiers = default;
      modifiers.Scale = 0.3f;

      textModeller.Transform = Matrix4x4.Identity;
      textModeller.Draw(new Vector3(40.0f, 0.0f, -80.0f), "Chart 2", 10.0f, new ColorPair(0xFF29D48Au),
        TextAlignment.Middle, TextAlignment.End, TextAlignment.Start, false, modifiers);

      textModeller.Transform = Matrix4x4.CreateRotationX(MathF.PI * -0.5f);
      textModeller.Draw(new Vector3(40.0f, 0.0f, 80.0f), "Chart 1", 10.0f, new ColorPair(0xFF0177FFu),
        TextAlignment.Middle, TextAlignment.End, TextAlignment.Start, false, modifiers);

      textModeller.Prepare();
      textModeller.Render(scene.Program);
    }

    private void RenderChart()
    {
      int lineCount = (int)MathF.Ceiling(axisLength / 20.0f);

      Vector3[] axisDots = new Vector3[3];

      for (int i = 0; i < 3; ++i)
        axisDots[i] = Vector3.Normalize(axisFinal[i]) * (lineCount * 20.0f);

      RenderingState initialState = device.RenderingState;
      { // Enable depth bias to prevent depth fighting with plane.
        RenderingState renderingState = initialState;
        renderingState.States &= ~RenderingState.State.DepthWrite;
        renderingState.DepthBias = 5.0f;
        renderingState.SlopeDepthBias = 5.0f;
        device.RenderingState = renderingState;
      }
      grapher.Transform = viewProjection;
      grapher.Begin();
      try
      {
        // Axis arrows.
        for (int i = 0; i < 3; ++i)
          grapher.Arrow(displaySize, default, axisFinal[i], 0xFF000000u, axisThickness,
            axisThickness * 5.0f);

        // Separation dots.
        for (int line = 1; line < lineCount; ++line)
          for (int axis = 0; axis < 3; ++axis)
          {
            float lineTheta = 1.0f - (float)line / lineCount;
            int axisNext = (axis + 2) % 3;
            {
              Vector3 position1 = Vector3.Lerp(axisDots[axisNext], Vector3.Zero, lineTheta);
              Vector3 position2 = position1 + Vector3.Normalize(axisDots[axis]) * axisLength;

              grapher.DottedLine(position1, position2, 0xFF808080u, 0xFF808080u, axisThickness * 0.75f,
                axisThickness * 0.75f, 4.0f, PointShape.Round, MathF.PI * 0.15f);
            }
            {
              Vector3 position1 = Vector3.Lerp(axisDots[axis], Vector3.Zero, lineTheta);
              Vector3 position2 = position1 + Vector3.Normalize(axisDots[axisNext]) * axisLength;

              grapher.DottedLine(position1, position2, 0xFF808080u, 0xFF808080u, axisThickness * 0.75f,
                axisThickness * 0.75f, 4.0f, PointShape.Round, MathF.PI * 0.15f);
            }
          }

        // Selection markers for mouse pointer.
        if (!volumetricText && location.Length() > 1e-6f)
        {
          uint selectColor = 0xFF808080u;

          if (MathF.Abs(location.Y) <= 0.01f)
          {
            grapher.Line(new Vector3(0.0f, 0.0f, location.Z), new Vector3(location.X, 0.0f, location.Z),
              selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f);

            grapher.Line(new Vector3(location.X, 0.0f, 0.0f), new Vector3(location.X, 0.0f, location.Z),
              selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f);
          }
          else if (MathF.Abs(location.X) <= 0.01f)
          {
            grapher.Line(new Vector3(0.0f, 0.0f, location.Z), new Vector3(0.0f, location.Y, location.Z),
              selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f);

            grapher.Line(new Vector3(0.0f, location.Y, 0.0f), new Vector3(0.0f, location.Y, location.Z),
              selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f);
          }
          else if (MathF.Abs(location.Z) <= 0.01f)
          {
            grapher.Line(new Vector3(0.0f, location.Y, 0.0f), new Vector3(location.X, location.Y, 0.0f),
              selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f);

            grapher.Line(new Vector3(location.X, 0.0f, 0.0f), new Vector3(location.X, location.Y, 0.0f),
              selectColor, selectColor, axisThickness * 0.75f, axisThickness * 0.75f);
          }
        }
      }
      finally
      {
        grapher.End();
      }
      device.RenderingState = initialState;
    }

    private void RenderCubes()
    {
      float[,] heights = new float[2, 4] {
        {20.0f, 60.0f, 40.0f, 80.0f},
        {10.0f, 30.0f, 60.0f, 40.0f}};

      // Assign material to the cubes.
      ObjectMaterial material = ObjectMaterial.Default;
      material.AmbientColor = new FloatColorRGB(0x00203D53u);
      material.AlbedoColor = new FloatColor(0xD00083E4u);
      material.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
      material.SpecularExponent = 4.0f;
      scene.Material = material;

      // Render first row of cubes.
      for (int i = 0; i < 4; ++i)
      {
        scene.World = Matrix4x4.CreateScale(new Vector3(20.0f, heights[0, i], 20.0f)) *
          Matrix4x4.CreateTranslation(new Vector3(10.0f + i * 20.0f, heights[0, i] * 0.5f, -10.0f));

        modelCube.Draw(scene.Program);
      }

      // Update material for another set of cubes.
      material.AmbientColor = new FloatColorRGB(0x00235240u);
      material.AlbedoColor = new FloatColor(0xD000D784u);
      scene.Material = material;

      // Render second row of cubes.
      for (int i = 0; i < 4; ++i)
      {
        scene.World = Matrix4x4.CreateScale(new Vector3(20.0f, heights[1, i], 20.0f)) *
          Matrix4x4.CreateTranslation(new Vector3(10.0f + i * 20.0f, heights[1, i] * 0.5f, -50.0f));

        modelCube.Draw(scene.Program);
      }

      // Draw a plane on bottom to make ambient occlusion more apparent.
      material.AmbientColor = new FloatColorRGB(0x00FFFFFFu);
      material.AlbedoColor = FloatColor.TranslucentBlack;
      material.SpecularColor = FloatColorRGB.Black;
      scene.Material = material;

      scene.World = Matrix4x4.CreateScale(new Vector3(400.0f, 1.0f, 400.0f)) *
        Matrix4x4.CreateTranslation(new Vector3(0.0f, -0.1f, 0.0f));

      modelPlane.Draw(scene.Program);

      if (volumetricText)
      { // Draw another plane on the back of chart to enable ambient occlusion on 3D text.
        scene.World = Matrix4x4.CreateScale(new Vector3(90.0f, 1.0f, 100.0f)) *
          Matrix4x4.CreateRotationX(MathF.PI * -0.5f) *
          Matrix4x4.CreateTranslation(new Vector3(45.0f, 50.0f, 0.5f));

        modelPlane.Draw(scene.Program);
      }
    }

    private void SetupPlotting(float scale)
    {
      float cameraDistance = camera.Distance;

      axisThickness = MathF.Max((displaySize.Y * 3.0f / 600.0f) * 320.0f / cameraDistance, 3.0f) * scale;
      axisLength = MathF.Max(110.0f * cameraDistance / 320.0f, 110.0f);

      axisFinal[0] = new Vector3(axisLength, 0.0f, 0.0f);
      axisFinal[1] = new Vector3(0.0f, axisLength, 0.0f);
      axisFinal[2] = new Vector3(0.0f, 0.0f, -axisLength);

      // View matrix is extracted directly from object manipulation camera.
      view = camera.View;

      // Build projection matrix.
      projection = Utility.MatrixPerspective4x4(MathF.PI * 0.25f, displaySize.X / displaySize.Y,
        10.0f, 1400.0f);

      // Set the matrices to the modeling scene.
      scene.View = view;
      scene.Projection = projection;

      // Precalculate combined view / projection matrix.
      viewProjection = view * projection;

      // Calculate mouse pointing ray.
      if (!Matrix4x4.Invert(view, out viewInverse))
        viewInverse = Matrix4x4.Identity;

      mouseRay = new(mousePos, displaySize, viewInverse, projection);
    }
  }
}