/*
 * 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:
//  * Loading different external 3D mesh formats and rendering them as objects.
//    This includes native Wavefront OBJ loader, native binary mesh formats and many other formats supported
//    by an external library Assimp, which is loaded dynamically by the framework as needed.
//  * Rendering voxel representations using instancing technique for optimal performance.
//  * Order-Independent Transparency (OIT) using fast approximated technique.

namespace ModelViewer
{
  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 SceneMeshes meshes;
    private ObjectModels objects;
    private SceneLights lights;
    private ObjectMaterials materials;
    private TextureCabinet textureCabinet;
    private ShadowCastingAtlas shadowCastingAtlas;
    private ObjectModelView shadowCasterView;
    private Scene sceneDepthNormals;
    private Scene sceneModeling;
    private ActorCamera camera;

    private ObjectModelView viewport;
    private MeshVoxel voxel;
    private Matrix4x4 voxelWorld;
    private readonly Matrix4x4[] voxelMatrices = new Matrix4x4[256];
    private readonly FloatColor[] voxelColors = new FloatColor[256];
    private int voxelCount = 0;

    private Vector2 displaySize;
    private float viewScale = 1.0f;
    private uint meshOptions;

    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)
    ];

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

    private readonly RenderingPanel panelRender;

    public MainForm()
    {
      InitializeComponent();
      panelRender = new RenderingPanel();
    }

    private void MainForm_Load(object sender, EventArgs e)
    {
      displaySize = new Vector2(panelRender.Width, panelRender.Height);
      try
      {
        device = new Device(DeviceTechnology.Direct3D);
        swapChain = new SwapChain(device, panelRender.Handle);
        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(panelRender.Width, panelRender.Height),
          TextureFidelity.High, 8)
        {
          Attributes = TextureCabinet.Attribute.Bloom
            | TextureCabinet.Attribute.BloomCubic
            | TextureCabinet.Attribute.AmbientOcclusion
            | TextureCabinet.Attribute.GlassyFast
        };

        shadowCastingAtlas = new ShadowCastingAtlas(device, new Point(256, 256));
        shadowCastingAtlas.Add(new Point(256, 256));

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

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

        viewport = objects.CreateView();

        shadowCasterView = objects.CreateView();
        shadowCasterView.View = Utility.MatrixLookAt4x4(lightPosition, Vector3.Zero, Vector3.UnitY);
        shadowCasterView.Projection = Utility.MatrixPerspective4x4(MathF.PI * 0.25f, 1.0f, 100.0f, 1400.0f);

        camera = new ActorCamera(400.0f);

        CreateStaticModels(meshes);
        CreateLightsAndMaterials(lights, materials);
        CreateStaticObjects(objects);

        ConfigureRenderingPanel();
      }
      catch (API.Exception ex)
      {
        MessageBox.Show(ex.Message, "Initialization failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
        Application.Exit();
      }
    }

    private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
    {
      objects?.Dispose();
      meshes?.Dispose();
      materials?.Dispose();
      camera?.Dispose();

      sceneModeling?.Dispose();
      sceneDepthNormals?.Dispose();
      textureCabinet?.Dispose();
      lights?.Dispose();
      shadowCastingAtlas?.Dispose();

      textRenderer?.Dispose();
      canvas?.Dispose();
      swapChain?.Dispose();
      device?.Dispose();
    }

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

    private void PanelRender_Paint(object sender, PaintEventArgs e)
    {
      float scale = DeviceDpi / 96.0f;

      Matrix4x4 view = camera.View;
      Matrix4x4 projection = Utility.MatrixPerspective4x4(MathF.PI * 0.25f, displaySize.X / displaySize.Y,
        10.0f, 1800.0f);

      viewport.View = view;
      viewport.Projection = projection;

      viewport.Update();

      // 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.
      uint sceneAttributes = Scene.Attribute.Coloring | Scene.Attribute.ShadowsCubic;

      if (checkBoxAmbientOcclusion.Checked)
        sceneAttributes |= Scene.Attribute.DepthPrePass;

      if (!checkBoxAutoNormals.Checked)
        sceneAttributes |= Scene.Attribute.Normals;

      { // Render shadow maps
        shadowCastingAtlas.BorderFill();

        ShadowCaster shadowCaster = shadowCastingAtlas.Caster(0);
        shadowCaster.ViewProjection = shadowCasterView.ViewProjection;

        shadowCasterView.Update();

        shadowCaster.Clear();
        shadowCaster.Begin();
        try
        {
          sceneDepthNormals.Attributes = 0;
          sceneDepthNormals.View = shadowCasterView.View;
          sceneDepthNormals.Projection = shadowCasterView.Projection;

          sceneDepthNormals.Begin();
          try
          {
            RenderObjects(sceneDepthNormals, shadowCasterView);

            if (voxel is not null && !checkBoxTransparency.Checked)
            {
              sceneDepthNormals.Attributes = Scene.Attribute.Instancing;
              RenderMeshVoxel(sceneDepthNormals, voxel);
            }
          }
          finally
          {
            sceneDepthNormals.End();
          }
        }
        finally
        {
          shadowCaster.End();
        }
        shadowCaster.Filter();
      }

      // Depth pre-pass.
      textureCabinet.Clear(TextureCabinetPass.Depth);
      textureCabinet.Begin(TextureCabinetPass.Depth);
      try
      {
        sceneDepthNormals.Attributes = sceneAttributes;
        sceneDepthNormals.View = viewport.View;
        sceneDepthNormals.Projection = viewport.Projection;

        sceneDepthNormals.Begin();
        try
        {
          RenderObjects(sceneDepthNormals, viewport);

          if (voxel is not null && !checkBoxTransparency.Checked)
          {
            sceneDepthNormals.Attributes = sceneAttributes | Scene.Attribute.Instancing;
            RenderMeshVoxel(sceneDepthNormals, voxel);
          }
        }
        finally
        {
          sceneDepthNormals.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Occlusion, projection);

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

        if (checkBoxShadows.Checked)
          sceneModeling.Atlas = shadowCastingAtlas;
        else
          sceneModeling.Atlas = null;

        sceneModeling.TextureCabinet = textureCabinet;

        sceneModeling.Attributes = sceneAttributes;
        sceneModeling.View = viewport.View;
        sceneModeling.Projection = viewport.Projection;

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

          if (voxel is not null)
          {
            sceneModeling.Attributes = sceneAttributes | Scene.Attribute.Instancing;
            RenderMeshVoxel(sceneModeling, voxel);
          }
        }
        finally
        {
          sceneModeling.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Bloom);

      // Render semi-transparent objects.
      textureCabinet.Clear(TextureCabinetPass.Glassy);
      textureCabinet.Begin(TextureCabinetPass.Glassy);
      try
      {
        sceneModeling.Attributes = sceneAttributes | Scene.Attribute.Glassy;

        sceneModeling.Begin();
        try
        {
          RenderObjects(sceneModeling, viewport, true);

          if (voxel is not null && checkBoxTransparency.Checked)
          {
            sceneModeling.Attributes = sceneAttributes | Scene.Attribute.Glassy | Scene.Attribute.Instancing;
            RenderMeshVoxel(sceneModeling, voxel);
          }
        }
        finally
        {
          sceneModeling.End();
        }
      }
      finally
      {
        textureCabinet.End();
      }
      textureCabinet.Filter(TextureCabinetFilterType.Glassy);

      swapChain.Begin();
      try
      {
        // Clear the device to discard previous swap-chain contents, which may improve performance.
        device.Clear(DeviceClear.Color, new FloatColor(0.5f, 0.0f, 0.0f));

        // Present 3D scene on the screen.
        textureCabinet.Present();

        // Draw 2D text on top of 3D scene.
        canvas.Begin();
        try
        {
          canvas.ContextState = CanvasContextState.FlatText;
          canvas.SamplerState = new CanvasSamplerState();

          { // Draw technology string.
            FontParameters parameters = new("Segoe UI", 16.0f * scale, FontWeight.SemiBold);
            parameters.Effect.FillBrightness = 0.0f;
            parameters.Effect.BorderBrightness = 1.0f;
            parameters.Effect.BorderType = FontBorder.SemiHeavy;
            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(0x80FFFFFFu), 1.0f);
          }
        }
        finally
        {
          canvas.End();
        }
      }
      finally
      {
        swapChain.End();
      }
    }

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

      if (e.Button == MouseButtons.Left)
        camera.Command(CameraCommand.Rotation, position, displaySize);
      else
        camera.Command(CameraCommand.Dragging, position, displaySize);
    }

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

      if (camera.Command(CameraCommand.Update, position, displaySize))
        panelRender.Invalidate();
    }

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

    private void PanelRender_MouseWheel(object sender, MouseEventArgs e)
    {
      if (e.Delta > 0)
        viewScale *= 1.25f;
      else
        viewScale /= 1.25f;

      ObjectModel obj = objects.TryObject("Model");
      obj?.SetTransform(ModelTransform.Local, Matrix4x4.CreateScale(viewScale));

      toolStripZoomLabel.Text = string.Format("{0:0.##}%", viewScale * 100.0f);

      panelRender.Invalidate();
    }

    private void CheckBoxAmbientOcclusion_CheckedChanged(object sender, EventArgs e)
    {
      uint attributes = textureCabinet.Attributes;

      if (checkBoxAmbientOcclusion.Checked)
        attributes |= TextureCabinet.Attribute.AmbientOcclusion;
      else
        attributes &= ~TextureCabinet.Attribute.AmbientOcclusion;

      textureCabinet.Attributes = attributes;
      panelRender.Invalidate();
    }

    private void CheckBoxTransparency_CheckedChanged(object sender, EventArgs e)
    {
      ObjectModel obj = objects.TryObject("Model");
      if (obj is not null)
      {
        if (checkBoxTransparency.Checked)
          obj.Attributes |= ObjectModel.Attribute.Transparent;
        else
          obj.Attributes &= ~ObjectModel.Attribute.Transparent;
      }
      panelRender.Invalidate();
    }

    private void CheckBoxOption_CheckedChanged(object sender, EventArgs e)
    {
      panelRender.Invalidate();
    }

    private void ButtonOpenMesh_Click(object sender, EventArgs e)
    {
      if (openFileDialog.ShowDialog() != DialogResult.OK)
        return;

      meshOptions = 0;

      // Remove previous voxel representation.
      voxel?.Dispose();
      voxel = null;

      // Reset mesh for an existing object.
      ObjectModel obj = objects.TryObject("Model");
      if (obj is not null)
      {
        obj.Mesh = null;
        obj.Size = Vector3.Zero;
      }
      viewScale = 1.0f;

      // Remove previous mesh.
      SceneMesh mesh = meshes.TryMesh("Model");
      if (mesh is not null)
      {
        meshes.Erase(mesh);
        mesh = null;
      }

      if (string.Equals(Path.GetExtension(openFileDialog.FileName), ".voxel",
        StringComparison.OrdinalIgnoreCase))
      { // Load voxel mesh representation.
        try
        {
          voxel = new MeshVoxel();
          voxel.LoadFromFile(openFileDialog.FileName, out int[] _, out byte _, out bool _);
        }
        catch (API.Exception)
        {
          MessageBox.Show("Failed loading mesh voxel representation " +
            $"({Path.GetFileName(openFileDialog.FileName)}).", "Voxel mesh loading error",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
          return;
        }
        voxel.Extents(out Vector3 position, out Vector3 size);
        voxelWorld = Matrix4x4.CreateTranslation((new Vector3(0.0f, size.Y * 0.5f, 0.0f)) - position);
        toolStripStatusLabel.Text = $"Loaded voxel mesh ({Path.GetFileName(openFileDialog.FileName)}).";
      }
      else if (string.Equals(Path.GetExtension(openFileDialog.FileName), ".obj",
        StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetExtension(openFileDialog.FileName),
        ".mesh", StringComparison.OrdinalIgnoreCase))
      { // Load from an OBJ file format using native loading mechanism.
        toolStripProgressBar.Visible = true;

        mesh = meshes.TryAddFromFile("Model", openFileDialog.FileName, vertexElements,
          Feedback, out string debug, ref meshOptions);
        if (mesh is null)
        {
          MessageBox.Show(
            $"Could not load Wavefront OBJ ({Path.GetFileName(openFileDialog.FileName)}): {debug}",
            "Mesh loading error", MessageBoxButtons.OK, MessageBoxIcon.Error);
          return;
        }
        toolStripProgressBar.Visible = false;
        toolStripStatusLabel.Text = $"Loaded file ({Path.GetFileName(openFileDialog.FileName)}).";
      }
      else
      { // Load from some other file format using Assimp library.
        using MeshBuffer buffer = new();

        if (!buffer.TryLoadFromFileEx(openFileDialog.FileName, null, out Vector3 minBounds,
          out Vector3 maxBounds, true, out string debug))
        {
          MessageBox.Show(
            $"Could not load mesh ({Path.GetFileName(openFileDialog.FileName)}): {debug}",
            "Mesh loading error", MessageBoxButtons.OK, MessageBoxIcon.Error);
          return;
        }
        mesh = meshes.AddFromBuffer("Model", buffer, vertexElements);
      }
      if (mesh is not null)
        obj ??= objects.Add("Model");

      if (obj is not null)
      {
        obj.Mesh = mesh;
        obj.Material = 1;
        obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateScale(viewScale));
      }
      checkBoxTransparency.Checked = false;
      panelRender.Invalidate();
    }

    private void RenderObjects(Scene scene, ObjectModelView view, bool glassy = false)
    {
      FloatColorRGB defaultDiffuse = new (0.65f, 0.65f, 0.65f);

      for (int i = 0; i < view.ObjectCount; ++i)
      {
        ObjectModel obj = view.Object(i);
        if ((!glassy && (obj.Attributes & ObjectModel.Attribute.Transparent) != 0u) ||
          (glassy && (obj.Attributes & ObjectModel.Attribute.Transparent) == 0u))
          continue;

        scene.World = obj.GetTransform(ModelTransform.GlobalModel);

        SceneMesh mesh = obj.Mesh;
        if (mesh is null)
          continue;

        if ((scene.Attributes & Scene.Attribute.Modeling) != 0)
        {
          SceneMeshMaterials meshMaterials = mesh.Materials;

          if (meshMaterials.Count > 0 && (meshOptions & MeshLoadingOption.MaterialVertexColor) == 0)
          {
            for (int indexMat = 0; indexMat < meshMaterials.Count; ++indexMat)
            {
              SceneMeshMaterial meshMaterial = meshMaterials.GetMaterial(indexMat);
              ObjectMaterial material = materials.GetMaterial(obj.Material);
              SceneMeshMaterialShading shading = meshMaterial.Shading;

              if (shading.Lighting == 0)
              { // Compatibility mode
                if (!string.IsNullOrEmpty(meshMaterial.Name))
                {
                  material.AmbientColor *= shading.Diffuse.RGB;
                  material.AlbedoColor = new FloatColor(material.AlbedoColor.RGB * shading.Diffuse.RGB,
                    material.AlbedoColor.Alpha);
                  material.EmissiveColor = shading.Emissive;
                }
                else
                { // Default material
                  material.AmbientColor *= defaultDiffuse;
                  material.AlbedoColor = new FloatColor(material.AlbedoColor.RGB * defaultDiffuse,
                    material.AlbedoColor.Alpha);
                }
              }
              else
              {
                if (shading.Lighting == 1)
                { // Default mode
                  if (shading.Ambient.Defined)
                    material.AmbientColor *= shading.Diffuse.RGB;

                  material.AlbedoColor = new FloatColor(material.AlbedoColor.RGB * shading.Diffuse.RGB,
                    material.AlbedoColor.Alpha);

                  material.SpecularColor *= shading.Specular;
                  material.SpecularExponent *= shading.SpecularExponent;
                }
                else
                { // Override mode
                  material.Technique = (TechniqueLighting)(shading.Lighting - 2);

                  if (shading.Ambient.Defined)
                    material.AmbientColor = shading.Diffuse.RGB;

                  material.AlbedoColor = new FloatColor(shading.Diffuse.RGB, material.AlbedoColor.Alpha);
                  material.SpecularColor = shading.Specular;
                  material.SpecularExponent = shading.SpecularExponent;
                }
                if (!float.IsNaN(shading.Bloom) && !float.IsInfinity(shading.Bloom))
                  material.AlbedoColor.Alpha *= shading.Bloom;

                material.EmissiveColor = shading.Emissive;

                if (!float.IsNaN(shading.Roughness.X) && !float.IsInfinity(shading.Roughness.X))
                  material.Roughness.X = shading.Roughness.X;

                if (!float.IsNaN(shading.Roughness.Y) && !float.IsInfinity(shading.Roughness.Y))
                  material.Roughness.X = shading.Roughness.Y;

                if (!float.IsNaN(shading.Roughness.Z) && !float.IsInfinity(shading.Roughness.Z))
                  material.Roughness.X = shading.Roughness.Z;
              }
              material.AmbientColor *= obj.Color.RGB;
              material.AlbedoColor *= obj.Color;

              if (glassy)
                material.AlbedoColor.Alpha = 0.5f; // Transparency

              scene.Material = material;

              scene.SetTexture(SceneTextureType.Albedo,
                meshMaterial.GetTexture(SceneMeshTexture.Diffuse));

              scene.SetTexture(SceneTextureType.NormalMap,
                meshMaterial.GetTexture(SceneMeshTexture.Normals));

              scene.SetTexture(SceneTextureType.ParallaxMap,
                meshMaterial.GetTexture(SceneMeshTexture.Parallax));

              for (int indexRange = 0; indexRange < meshMaterial.RangeCount; ++indexRange)
              {
                SceneMeshMaterialRange range = meshMaterial.GetRange(indexRange);
                mesh.Model.Draw(scene.Program, PrimitiveTopology.Triangles, range.IndexCount,
                  range.IndexStart);
              }
            }
          }
          else
          {
            ObjectMaterial material = materials.GetMaterial(obj.Material);
            material.AmbientColor *= obj.Color.RGB;
            material.AlbedoColor *= obj.Color;

            if (glassy)
              material.AlbedoColor.Alpha = 0.5f; // Transparency

            scene.Material = material;

            scene.SetTexture(SceneTextureType.Albedo, null);
            scene.SetTexture(SceneTextureType.NormalMap, null);
            scene.SetTexture(SceneTextureType.ParallaxMap, null);

            mesh.Model.Draw(scene.Program);
          }
        }
        else
          mesh.Model.Draw(scene.Program);
      }
    }

    private void RenderMeshVoxel(Scene scene, MeshVoxel voxel)
    {
      if (scene.Equals(sceneModeling))
      {
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = FloatColorRGB.White * 0.25f;
        material.AlbedoColor = FloatColor.White;
        material.AlbedoColor.Alpha = checkBoxTransparency.Checked ? 0.1f : 0.5f; // Alpha or Bloom
        material.SpecularColor = FloatColorRGB.White * 0.25f;
        material.SpecularExponent = 4.0f;
        material.Shadows = 0.5f;
        material.Occlusion = 0.5f;
        material.Technique = TechniqueLighting.Phong;
        scene.Material = material;
      }
      voxel.Visualize(255, delegate(Vector3 position, Vector3 size, FloatColor color)
      {
        voxelColors[voxelCount] = new FloatColor(color.Red, color.Green, color.Blue, 0.2f);
        voxelMatrices[voxelCount] = Matrix4x4.CreateScale(size * 0.999f) *
          Matrix4x4.CreateTranslation(position) * voxelWorld * Matrix4x4.CreateScale(viewScale);

        if (++voxelCount >= scene.InstancesCount)
          VoxelVisualizeFlush(scene);
      });

      VoxelVisualizeFlush(scene);
    }

    public void VoxelVisualizeFlush(Scene scene)
    {
      if (voxelCount > 0)
      {
        scene.Instances(voxelMatrices, voxelColors, voxelCount);
        meshes.Mesh("Cube").Model.DrawInstances(scene.Program, voxelCount);
        voxelCount = 0;
      }
    }

    private static void CreateStaticObjects(ObjectModels objects)
    {
      ObjectModel obj = objects.Add("Floor");
      obj.MeshName = "Plane";
      obj.SetTransform(ModelTransform.Local, Matrix4x4.CreateTranslation(new Vector3(0.0f, -0.1f, 0.0f)));
      obj.Color = new FloatColor(0xFFE0E0E0u);
      obj.Material = 0;
    }

    private static void CreateStaticModels(SceneMeshes meshes)
    {
      using MeshBuffer buffer = new();

      buffer.Plane(1, 1, Vector3.Zero, new Vector3(1000.0f, 0.0f, 0.0f), new Vector3(0.0f, 0.0f, 1000.0f),
        Vector3.UnitY);

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

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

      meshes.AddFromBuffer("Cube", buffer, vertexElements);
    }

    private static void CreateLightsAndMaterials(SceneLights lights, ObjectMaterials materials)
    {
      { // Create default light.
        SceneLight light = SceneLight.Default;
        light.AmbientColor = new FloatColorRGB(0x00FFFFFFu);
        light.AlbedoColor = new FloatColorRGB(0x00FFFFFFu);
        light.SpecularColor = new FloatColorRGB(0x00FFFFFFu);
        light.SpecularExponent = 1.0f;
        light.Position = lightPosition;
        light.ShadowCaster = 0;

        lights.Add(light);
      }
      { // Floor material
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(1.0f, 1.0f, 1.0f);
        material.AlbedoColor = new FloatColor(0.0f, 0.0f, 0.0f, 0.0f);
        material.SpecularColor = new FloatColorRGB(0.0f, 0.0f, 0.0f);
        material.Shadows = 0.5f;
        material.Occlusion = 0.5f;

        materials.Add(material);
      }
      { // Object material
        ObjectMaterial material = ObjectMaterial.Default;
        material.AmbientColor = new FloatColorRGB(0.25f, 0.25f, 0.25f);
        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.Shadows = 0.5f;
        material.Occlusion = 0.5f;

        materials.Add(material);
      }
    }

    public void ConfigureRenderingPanel()
    {
      panelRender.Dock = DockStyle.Fill;

      panelRender.Resize += PanelRender_Resize;
      panelRender.Paint += PanelRender_Paint;
      panelRender.MouseDown += PanelRender_MouseDown;
      panelRender.MouseMove += PanelRender_MouseMove;
      panelRender.MouseUp += PanelRender_MouseUp;
      panelRender.MouseWheel += PanelRender_MouseWheel;

      Controls.Add(panelRender);

      panelRender.BringToFront();
    }

    private int Feedback(string message, int progress)
    {
      toolStripProgressBar.Value = progress;
      toolStripStatusLabel.Text = $"{message} ({progress}%)...";
      Application.DoEvents();
      return 0;
    }
  }

  public class RenderingPanel : Panel
  {
    public RenderingPanel()
    {
      SetStyle(ControlStyles.Opaque, true);
      SetStyle(ControlStyles.UserPaint, true);
      SetStyle(ControlStyles.AllPaintingInWmPaint, true);
      SetStyle(ControlStyles.DoubleBuffer, false);
    }
    protected override CreateParams CreateParams
    {
      get
      {
        CreateParams createParams = base.CreateParams;
        createParams.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
        return createParams;
      }
    }
  }
}