C#: SharpDX - 绘制基本星星

C#: SharpDX - Draw Basic Stars

问题

几天来我一直在寻找这个问题的答案。我需要帮助找到一种生成基本星星的方法。我有随机生成完成位置的代码;然而,我是 DirectX 的新手,来自 XNAUnity 的世界。 DirectX 开发在最好的时候似乎过于复杂。我找到了一些教程,但是,我发现它们很难理解。清除后,我无法在屏幕上呈现任何内容。就渲染而言,我使用的是基本设置,我没有创建任何特殊的 classes 或结构。我一直在尝试遵循 Frank D. Luna 书中 3D Game Programming With DirectX 11 中从 C++ 转换为 C# 的 Richard's Software tutorials。我能够成功完成的最远距离是 Color.CornflowerBlue.

问题

  1. 是否有任何 简单化 方法将 draw/render 对象显示到屏幕上,我能够很好地渲染文本,但图像(精灵)和 3D 网格似乎给我问题。有没有简单的方法来绘制基本的几何形状?例如:Primitives.DrawSphere(float radius, Vector3 location, Color c);
  2. 如果没有任何可用于绘制图元的简单方法,那么渲染星星的最简单方法是什么?我可以制作球体、带 alpha 混合的精灵以模拟距离、广告牌等。最简单的实现方法是什么?
  3. 如何实现上面问题 2 揭示的最简单的方法?代码示例、教程(无视频)、文章等非常感谢,因为我很难找到好的 C# 参考资料,看起来大多数都在利用 UnityUnreal 这些天,但我没有这些选项。

备注

我在政府环境中工作,无法使用未经批准的第三方工具。批准过程是一场噩梦,因此第三方工具通常是行不通的。所有提供的答案、文档、示例等都应严格使用 SharpDX.

我的代码

我的项目是 WindowsFormsApplicaiton,其中主要形式派生自 RenderForm。我创建了一个名为 Engine 的 class 来处理 DirectX 代码。

Engine.cs:

internal class Engine : IDisposable {

    #region Fields

    private Device device;
    private SwapChain swapChain;
    private DeviceContext context;
    private Texture2D backBuffer;
    private RenderTargetView renderView;
    private SynchronizationContext syncContext;

    #endregion

    #region Events

    public event EventHandler Draw;
    public event EventHandler Update;
    private void SendDraw(object data) { Draw(this, new EventArgs()); }
    private void SendUpdate(object data) { Update(this, new EventArgs()); }

    #endregion

    #region Constructor(s)

    public Engine(RenderForm form) {
        SwapChainDescription description = new SwapChainDescription() {
            ModeDescription = new ModeDescription(form.Width, form.Height, new Rational(60, 1), Format.R8G8B8A8_UNorm),
            SampleDescription = new SampleDescription(1, 0),
            Usage = Usage.RenderTargetOutput,
            BufferCount = 1,
            OutputHandle = form.Handle,
            IsWindowed = !form.IsFullscreen
        };
        Device.CreateWithSwapChain(DriverType.Hardware, DeviceCreationFlags.Debug, description, out device, out swapChain);
        backBuffer = Resource.FromSwapChain<Texture2D>(swapChain, 0);
        renderView = new RenderTargetView(device, backBuffer);
        context = device.ImmediateContext;
        context.OutputMerger.SetRenderTargets(renderView);
        context.Rasterizer.SetViewport(new Viewport(0, 0, form.Width, form.Height));
        renderForm = form;
    }

    #endregion

    #region Public Methods

    public void Initialize() {
        if (SynchronizationContext.Current != null)
            syncContext = SynchronizationContext.Current;
        else
            syncContext = new SynchronizationContext();

        RenderLoop.Run(renderForm, delegate() {
            context.ClearRenderTargetView(renderView, Color.CornflowerBlue);

            syncContext.Send(SendUpdate, null);
            syncContext.Send(SendDraw, null);

            swapChain.Present(0, 0);
        });
    }
    public void Dispose() { }

    #endregion

}

Form1.cs:

public partial class Form1: RenderForm {
    private Engine gameEngine;
    int count = 0;

    public Form1() {
        InitializeComponent();
        gameEngine = new Engine(this);
        gameEngine.Update += GameEngine_Update;
        gameEngine.Draw += GameEngine_Draw;
        gameEgnine.Initialize();
    }

    private void GameEngine_Update(object sender, EventArgs e) => Debug.WriteLine("Updated.");
    private void GameEngine_Draw(object sender, EventArgs e) => Debug.WriteLine($"I've drawn {++count} times.");
}

最后备注

此时感谢任何帮助,因为它正在进行第 4 天,我仍在努力理解大部分 DirectX 11 代码。我绝不是 C# 或开发的新手;我只是习惯了Windows FormsASP.NETUnityXNAWPF等。这是我第一次体验DirectX及其绝对是最重要的。甚至比 OpenGL 十年前我几乎没有任何开发经验的时候还要糟糕。

开始的事情很少。

首先,DirectX是一个很低级的API。在 Windows 上获得较低级别 API 的唯一方法是直接与图形驱动程序对话,这将是一场噩梦。结果,事情往往非常通用,这允许以相当复杂为代价的高度灵活性。如果您想知道 Unity 或 Unreal 在幕后做了什么,就是它了。

其次,DirectX,尤其是 Direct3D,是用 C++ 编写并为 C++ 编写的。 C# 资源很难获得,因为 API 并非真正用于 C#(这不是一件好事)。因此,丢弃为 C++ 编写的文档和答案是一个非常糟糕的主意。 C++ 的所有注意事项和限制 API 也适用于 C# 世界中的您,您需要了解它们。

第三,我无法为您提供完全 C#/SharpDX 的答案,因为我不使用 C# 中的 DirectX,而是使用 C++ 中的 DirectX。我会尽我所能提供准确的映射,但请注意,您使用的是 API 包装器,它可以而且会向您隐藏一些细节。发现这些细节的最佳选择是在浏览 C++ 文档时获取 SharpDX 的源代码。

现在回答你的问题。系好安全带,这会很长。

首先:在 Direct3D 11 中渲染原始对象没有简单的方法。渲染六面立方体与渲染纽约市的 2 亿个顶点网格的步骤相同。

在渲染循环中,我们需要做几个动作来渲染任何东西。在此列表中,您已经完成了第 1 步和第 7 步,并部分完成了第 2 步:

  1. 清除后台缓冲区和 depth/stencil 缓冲区。
  2. 设置当前渲染过程中使用的输入布局、着色器、管线状态对象、渲染目标和视口。
  3. 设置当前正在绘制的网格所使用的顶点缓冲区、索引缓冲区、常量缓冲区、着色器资源和采样器。
  4. 为给定的网格发出绘制调用。
  5. 对必须在当前渲染过程中绘制的所有网格重复步骤 3 和 4。
  6. 对应用程序定义的所有传递重复步骤 2 到 5。
  7. 展示交换链。

相当复杂,只是为了渲染像立方体这样简单的东西。这个过程需要几个对象,其中我们已经有几个:

  • 一个Device对象实例,用于创建新的D3D对象
  • 一个DeviceContext对象实例,用于发出绘图操作和设置管道状态
  • 一个 DXGI.SwapChain 对象实例,用于管理后台缓冲区并将链中的下一个缓冲区呈现给桌面
  • 一个Texture2D对象实例,表示交换链拥有的后台缓冲区
  • 一个RenderTargetView对象实例,允许显卡使用纹理作为渲染操作的目标
  • 一个DepthStencilView对象实例,如果我们正在使用深度缓冲区
  • VertexShaderPixelShader对象实例,表示GPU在图形管线的顶点和像素着色器阶段使用的着色器
  • 一个InputLayout对象实例,表示我们顶点缓冲区中一个顶点的确切布局
  • 一组 Buffer 个对象实例,表示包含我们的几何图形的顶点缓冲区和索引缓冲区以及包含我们着色器参数的常量缓冲区
  • 一组 Texture2D 个对象实例以及关联的 ShaderResourceView 个对象实例,表示要应用于我们的几何体的任何纹理或表面贴图
  • 一组 SamplerState 个对象实例,用于从我们的着色器中采样上述纹理
  • 一个 RasterizerState 对象实例,用于描述光栅化器应使用的剔除、深度偏置、多重采样和抗锯齿参数
  • 一个DepthStencilState对象实例,描述GPU应该如何进行深度测试,深度测试失败的原因,以及失败应该做什么
  • 一个 BlendState 对象实例,描述 GPU 应如何将多个渲染目标混合在一起

现在,这看起来像实际的 C# 代码吗?

可能是这样的(用于渲染):

//Step 1 - Clear the targets
// Clear the back buffer to blue
context.ClearRenderTargetView(BackBufferView, Color.CornflowerBlue);
// Clear the depth buffer to the maximum value.
context.ClearDepthStencilView(DepthStencilBuffer, DepthStencilClearFlags.Depth, 1.0f, 0);

//Step 2 - Set up the pipeline.
// Input Assembler (IA) stage
context.InputAssembler.InputLayout = VertexBufferLayout;
// Vertex Shader (VS) stage
context.VertexShader.Set(SimpleVertexShader);
// Rasterizer (RS) stage
context.Rasterizer.State = SimpleRasterState;
context.Rasterizer.SetViewport(new Viewport(0, 0, form.Width, form.Height));
// Pixel Shader (PS) stage
context.PixelShader.Set(SimplePixelShader);
// Output Merger (OM) stage
context.OutputMerger.SetRenderTargets(DepthStencilBuffer, BackBufferView);
context.OutputMerger.SetDepthStencilState(SimpleDepthStencilState);
context.OutputMerger.SetBlendState(SimpleBlendState);

//Step 3 - Set up the geometry
// Vertex buffers
context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(VertexBuffer, sizeof(Vertex), 0));
// Index buffer
context.InputAssembler.SetIndexBuffer(IndexBuffer, Format.R16_UInt, 0);
// Constant buffers
context.VertexShader.SetConstantBuffer(0, TransformationMatrixBuffer);
context.PixelShader.SetConstantBuffer(0, AmbientLightBuffer);
// Shader resources
context.PixelShader.SetShaderResource(0, MeshTexture);
// Samplers
context.PixelShader.SetSampler(0, MeshTextureSampler);

//Step 4 - Draw the object
context.DrawIndexed(IndexBuffer.Count, 0, 0);

//Step 5 - Advance to the next object and repeat.
// No next object currently.

//Step 6 - Advance to the next pipeline configuration
// No next pipeline configuration currently.

//Step 7 - Present to the screen.
swapChain.Present(0, 0);

此示例代码中的顶点和像素着色器期望:

  • 每个顶点都有位置、法线和纹理坐标的模型
  • 摄像机在世界中的位置space、世界视图投影矩阵、世界逆转置矩阵和作为顶点着色器常量缓冲区的世界矩阵
  • 光的环境光、漫反射和镜面反射颜色,以及它在世界中的位置,作为像素着色器常量缓冲区
  • 在像素着色器中应用于模型表面的 2D 纹理,以及
  • 访问上述纹理的像素时使用的采样器。

现在渲染代码本身相当简单 - 设置是其中较难的部分:

  //Create the vertex buffer
  VertexBuffer = new Buffer(device, RawVertexInfo, new BufferDescription {
    SizeInBytes = RawVertexInfo.Length * sizeof(Vertex),
    Usage = ResourceUsage.Default,
    BindFlags = BindFlags.VertexBuffer,
    CpuAccessFlags = CpuAccessFlags.None,
    OptionFlags = ResourceOptionFlags.None,
    StructureByteStride = sizeof(Vertex)
  });
  //Create the index buffer
  IndexCount = (int)RawIndexInfo.Length;
  IndexBuffer = new Buffer(device, RawIndexInfo, new BufferDescription {
    SizeInBytes = IndexCount * sizeof(ushort),
    Usage = ResourceUsage.Default,
    BindFlags = BindFlags.IndexBuffer,
    CpuAccessFlags = CpuAccessFlags.None,
    OptionFlags = ResourceOptionFlags.None,
    StructureByteStride = sizeof(ushort)
  });
  //Create the Depth/Stencil view.
  Texture2D DepthStencilTexture = new Texture2D(device, new Texture2DDescription {
    Format = Format.D32_Float,
    BindFlags = BindFlags.DepthStencil,
    Usage = ResourceUsage.Default,
    Height = renderForm.Height,
    Width = renderForm.Width,
    ArraySize = 1,
    MipLevels = 1,
    SampleDescription = new SampleDescription {
      Count = 1,
      Quality = 0,
    },
    CpuAccessFlags = 0,
    OptionFlags = 0
  });
  DepthStencilBuffer = new DepthStencilView(device, DepthStencilTexture);
  SimpleDepthStencilState = new DepthStencilState(device, new DepthStencilStateDescription {
    IsDepthEnabled = true,
    DepthComparison = Comparison.Less,
  });
  //default blend state - can be omitted from the application if defaulted.
  SimpleBlendState = new BlendState(device, new BlendStateDescription {

  });
  //Default rasterizer state - can be omitted from the application if defaulted.
  SimpleRasterState = new RasterizerState(device, new RasterizerStateDescription {
    CullMode = CullMode.Back,
    IsFrontCounterClockwise = false,
  });
  // Input layout.
  VertexBufferLayout = new InputLayout(device, VertexShaderByteCode, new InputElement[] {
    new InputElement {
      SemanticName = "POSITION",
      Slot = 0,
      SemanticIndex = 0,
      Format = Format.R32G32B32_Float,
      Classification = InputClassification.PerVertexData,
      AlignedByteOffset = 0,
      InstanceDataStepRate = 0,
    },
    new InputElement {
      SemanticName = "NORMAL",
      Slot = 0,
      SemanticIndex = 0,
      Format = Format.R32G32B32_Float,
      Classification = InputClassification.PerVertexData,
      AlignedByteOffset = InputElement.AppendAligned,
      InstanceDataStepRate = 0,
    },
    new InputElement {
      SemanticName = "TEXCOORD0",
      Slot = 0,
      SemanticIndex = 0,
      Format = Format.R32G32_Float,
      Classification = InputClassification.PerVertexData,
      AlignedByteOffset = InputElement.AppendAligned,
      InstanceDataStepRate = 0,
    },
  });
  //Vertex/Pixel shaders
  SimpleVertexShader = new VertexShader(device, VertexShaderByteCode);
  SimplePixelShader = new PixelShader(device, PixelShaderByteCode);
  //Constant buffers
  TransformationMatrixBuffer = new Buffer(device, new BufferDescription {
    SizeInBytes = sizeof(TransformationMatrixParameters),
    BindFlags = BindFlags.ConstantBuffer,
    Usage = ResourceUsage.Default,
    CpuAccessFlags = CpuAccessFlags.None,
  });
  AmbientLightBuffer = new Buffer(device, new BufferDescription {
    SizeInBytes = sizeof(AmbientLightParameters),
    BindFlags = BindFlags.ConstantBuffer,
    Usage = ResourceUsage.Default,
    CpuAccessFlags = CpuAccessFlags.None,
  });
  // Mesh texture
  MeshTexture = new Texture2D(device, new Texture2DDescription {
    Format = Format.B8G8R8A8_UNorm,
    BindFlags = BindFlags.ShaderResource,
    Usage = ResourceUsage.Default,
    Height = MeshImage.Height,
    Width = MeshImage.Width,
    ArraySize = 1,
    MipLevels = 0,
    CpuAccessFlags = CpuAccessFlags.None,
    OptionFlags = ResourceOptionFlags.None,
    SampleDescription = new SampleDescription {
      Count = 1, 
      Quality = 0,
    }
  });
  //Shader view for the texture
  MeshTextureView = new ShaderResourceView(device, MeshTexture);
  //Sampler for the texture
  MeshTextureSampler = new SamplerState(device, new SamplerStateDescription {
    AddressU = TextureAddressMode.Clamp,
    AddressV = TextureAddressMode.Clamp,
    AddressW = TextureAddressMode.Border,
    BorderColor = new SharpDX.Mathematics.Interop.RawColor4(255, 0, 255, 255),
    Filter = Filter.MaximumMinMagMipLinear,
    ComparisonFunction = Comparison.Never,
    MaximumLod = float.MaxValue,
    MinimumLod = float.MinValue,
    MaximumAnisotropy = 1,
    MipLodBias = 0,
  });

如您所见,有很多事情要解决。

由于这已经比大多数人耐心等待的时间长得多,我建议阅读 Frank D. Luna 的书,因为他在解释管道阶段和Direct3D 对您的应用程序的期望。

我还建议通读 Direct3D API 的 C++ 文档,因为这里的所有内容都适用于 SharpDX。

此外,您需要研究 HLSL,因为您需要定义和编译着色器才能使上述任何代码正常工作, 如果你想要任何纹理,你需要弄清楚如何将图像数据导入 Direct3D。

从好的方面来说,如果您设法以干净、可扩展的方式实现所有这些,您将能够几乎不费吹灰之力地渲染任何内容。