如何使用XNA创建类似于Paint的应用程序?

3
使用XNA进行程序化绘制线条的问题已经在这里得到解决。但是,我想要让用户在画布上画画,就像使用MS Paint等绘画应用程序一样。
当然,这需要每次鼠标指针位置的x和/或y坐标更改时,实时以蜡笔颜色在画布上绘制另一个线条“点”。
在鼠标移动事件中,为了逐点绘制线条,有哪些XNA API需要考虑?当然,我并不是在绘制一条直线,而是一系列“点”。每个“点”都可以(并且可能应该)比单个像素大。想象一下用毛笔画画。
1个回答

7
你提供的文章建议使用基本图形(也就是矢量图形)绘制线条。例如,像Paint这样的应用程序大多数都是基于像素的(即使更高级的软件如Photoshop也具有矢量和光栅化功能)。

位图编辑器

既然您希望它类似于“Paint”,那么我肯定会选择基于像素的方法:
  1. 创建一个颜色值网格。(扩展System.Drawing.Bitmap类或实现自己的类。)
  2. 开始(游戏)循环:
    • 处理输入并相应地更新网格中的颜色值。
    • 将Bitmap转换为Texture2D
    • 使用sprite批处理或自定义渲染器将纹理绘制到屏幕上。
  3. 保存位图,如果需要。

在位图上绘制

我在答案底部添加了我正在使用的图像类的草稿。但是,代码应该相当不言自明。

如前所述,您还需要实现一种将图像转换为Texture2D并将其绘制到屏幕上的方法。


首先,我们创建一个新的 10x10 图像并将所有像素设置为白色。

var image = new Grid<Color>(10, 10);
image.Initilaize(() => Color.White);

White 10*10 pixel grid

接下来我们要设置一个画刷。画刷本质上只是一个应用于整个图像的函数。在这种情况下,该函数应该将指定圆内的所有像素设置为深红色。

// Create a circular brush
float brushRadius = 2.5f;
int brushX = 4;
int brushY = 4;
Color brushColor = new Color(0.5f, 0, 0, 1); // dark red

White 10*10 color grid with a circle and cross defining the brush area and center

现在我们开始使用画笔。可以参考我的这个Stack Overflow回答,了解如何确定圆形内的像素。 您可以使用鼠标输入来调整画笔的偏移量,并允许用户实际在位图上绘制。
double radiusSquared = brushRadius * brushRadius;

image.Modify((x, y, oldColor) =>
{
    // Use the circle equation
    int deltaX = x - brushX;
    int deltaY = y - brushY;
    double distanceSquared = Math.Pow(deltaX, 2) + Math.Pow(deltaY, 2); 

    // Current pixel lies inside the circle
    if (distanceSquared <= radiusSquared)
    {
        return brushColor;
    }

    return oldColor;
});

White 10*10 color grid with all pixels inside the circle set to a dark red

你还可以在画笔颜色和旧像素之间进行插值。例如,您可以通过让混合量取决于画笔中心和当前像素之间的距离来实现“柔和”画笔。

White 10*10 color grid with a soft dark red dot

画一条线

为了绘制自由手线条,只需反复应用刷子,每次使用不同的偏移量(取决于鼠标移动):

Drawing a line by repeatedly applying a circular brush


自定义图像类

显然,我省略了一些必要的属性、方法和数据验证,但你可以理解这个想法:

public class Image
{
    public Color[,] Pixels { get; private set; }

    public Image(int width, int height)
    {
        Pixels= new Color[width, height];
    }

    public void Initialize(Func<Color> createColor)
    {
         for (int x = 0; x < Width; x++)
         {
             for (int y = 0; y < Height; y++)
             {
                  Pixels[x, y] = createColor();
             }
         }
    }

    public void Modify(Func<int, int, Color, Color> modifyColor)
    {
         for (int x = 0; x < Width; x++)
         {
             for (int y = 0; y < Height; y++)
             {
                  Color current = Pixels[x, y];
                  Pixels[x, y] = modifyColor(x, y, current);
             }
         }
    }
}

1
好的回答 - 虽然我认为最好是使用Color[]缓冲区直接传递给Texture2D.GetData/SetData,而不是从2D数组或Bitmap中进行额外的转换步骤。 - Andrew Russell
1
谢谢!我选择了一个二维数组,因为我认为它更直观。幸运的是,如果用简单的数组替换它,接口不会有太大变化。Image方法中的循环将会被展开成一个单独的循环,并且偏移量需要计算:int x = i / Height;int y = i % Height; - Lucius
1
卢修斯,这太棒了。非常感谢。 - Buggieboy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接