网站后缀名,网站建设对电子商务的意义,智能建站价格,网站解析后显示在建设中前言说来这是个我和我老婆的爱情故事。从小以来“坦克大战”、“魂斗罗”等游戏总令我魂牵梦绕。这些游戏的基础就是 2D实时渲染#xff0c;以前没意识#xff0c;直到后来找到了 Direct2D。我的 2D实时渲染入门#xff0c;是从这个 动态时钟开始的。本文将使用我写的“准游… 前言说来这是个我和我老婆的爱情故事。从小以来“坦克大战”、“魂斗罗”等游戏总令我魂牵梦绕。这些游戏的基础就是 2D实时渲染以前没意识直到后来找到了 Direct2D。我的 2D实时渲染入门是从这个 动态时钟开始的。本文将使用我写的“准游戏引擎” FlysEngine完成。它是对 Direct2D和 .NET库 SharpDX浅层次的封装隐藏了一些细节简化了一些调用。同时还保留了 Direct2D的原汁原味。本文的最终效果如下绘制动态时钟要绘制动态时钟需要有以下步骤创建一个实时渲染窗口画一个圆圈表示时钟边缘在圆圈内等距离画上 60个分钟刻度其中 12个比较长为小时刻度用不同粗细、不同长短、不同颜色的画笔画上时钟、分钟和秒钟。实时渲染窗口using var form new RenderWindow { ClientSize new System.Drawing.Size(400, 400) };
form.Draw (RenderWindow sender, DeviceContext ctx)
{ ctx.Clear(Color.CornflowerBlue);
};
RenderLoop.Run(form, () form.Render(1, PresentFlags.None));其中 form.Render(1,...)中的 1表示垂直同步玩过游戏的可能见过这个设置可以在尽可能节省 CPU/GPU资源的同时得到最佳的呈现效果。熟悉 glut的肯定知道这种写法和 glut非常像执行效果如下注意RenderWindow其实继承于 System.Windows.Forms.Form确实是基于“ WinForm”但实质却和“拖控件”完全不一样。“控件”是模态的本身有状态但 Direct2D是实时渲染界面完全没有状态需要动态每隔一个垂直同步时间如 1/60秒全部清除然后再重绘一次。画圆圈RenderWindow简单封装了 Direct2D可以直接使用里面的 XResource属性来访问 DirectX相关资源包括Direct2DFactoryDirect2DDeviceContextDirectWriteFactoryTransitionLibrary 动画库AnimationManager 动画管理器SwapChainWICImagingFactory2除此之外还进一步封装了以下组件以简化图片、文字、颜色等调用和渲染TextFormatManager 简化创建 TextFormatBitmapManager 简化加载图片TextLayoutManager 简化创建 TextLayout.GetColor(color) 方法简化使用颜色这里我们将使用 Direct2DDeviceContext这在 COM中的名字叫 ID2D1DeviceContext。回到 Draw事件它包含两个参数(RenderWindowsender,DeviceContextctx)其中 sender就是原窗口可以用外层的 form代替;ctx参数就是 D2D绘图的核心我们将围绕它进行绘制。要画圆圈得先算出一个能放下一个完整圆的半径并留下少许空间 5float r Math.Min(ctx.Size.Width, ctx.Size.Height) / 2 - 5然后调用 ctx参数使用黑色画笔将圆画出来线宽为 1/40半径ctx.DrawEllipse(new Ellipse(Vector2.Zero, r, r), sender.XResource.GetColor(Color.Black), r/40);执行效果如下可见圆只显示了四分之一要显示完整的圆必须将其“移动”到屏幕正中心我们可以调整圆的参数将中心点从 Vector2.Zero改成 newVector2(ctx.Size.Width/2,ctx.Size.Height/2)或者用更简单的办法通过矩阵变换ctx.Transform Matrix3x2.Translation(ctx.Size.Width/2, ctx.Size.Height/2);注意“矩阵变换”这几个字听起来总令人联想到“高数”挺吓人的。但实际是并不是非要知道线性代码基础才能使用。首先只要知道它能完成任务即可之后再慢慢理解也行。有多种方法可以完成像平移这样的任务但通常来说使用“矩阵变换”更简单更不伤脑筋尤其是多个对象进行旋转、扭曲等复杂、或者组合操作等这些操作如果不使用“矩阵变换”会非常非常麻烦。这样即可将该圆“平移”至屏幕正中心执行效果如下Draw方法完整代码ctx.Clear(Color.CornflowerBlue);
float r Math.Min(ctx.Size.Width, ctx.Size.Height) / 2 - 5;
ctx.Transform Matrix3x2.Translation(ctx.Size.Width/2, ctx.Size.Height/2);
ctx.DrawEllipse(new Ellipse(Vector2.Zero, r, r), sender.XResource.GetColor(Color.Black), r/40);画刻度刻度就是线条共 60-1248个分钟刻度和 12个时钟刻度其中分钟刻度较短时钟刻度较长。刻度的一端是沿着圆的边缘另一端朝着圆的中心边缘位置可以通过 sin/cos等三角函数计算出来……呃可能早忘记了不怕我们有“矩阵变换”。利用矩阵变换可以非常容易地完成这项工作for (var i 0; i 60; i)
{ ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 60 * i) * Matrix3x2.Translation(ctx.Size.Width/2, ctx.Size.Height/2); ctx.DrawLine(new Vector2(r-r/30,0), new Vector2(r,0), form.XResource.GetColor(Color.Black),r/200);
}执行效果如下注意此处用到了矩阵乘法Matrix3x2.Rotation(MathF.PI * 2 / 60 * i) *
Matrix3x2.Translation(ctx.Size.Width/2, ctx.Size.Height/2);注意乘法是有顺序的这符合空间逻辑可以这样想想先旋转再平移和先平移再旋转显然是有区别的。然后再加上长时钟只需在原代码基础上加个判断即可如果 i%50则为长时钟粗细设置为 r/100for (var i 0; i 60; i)
{ ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 60 * i) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2); if (i % 5 0) { // 时钟 ctx.DrawLine(new Vector2(r - r / 15, 0), new Vector2(r, 0), form.XResource.GetColor(Color.Black), r/100); } else { // 分钟 ctx.DrawLine(new Vector2(r - r / 30, 0), new Vector2(r, 0), form.XResource.GetColor(Color.Black), r/200); }
}执行效果如下画时、分、秒钟时、分、秒钟是动态的必须随着时间变化而变化其中时钟最短、最粗分钟次之秒钟最细长然后时钟必须叠在分钟和秒钟之上。用代码实现可以先画秒钟、再画分钟和时钟即可实现重叠效果。还可以通过设置一定的透明度和不同的颜色可以让它们区分更明显。获取当前时间可以通过 DateTime.Now来完成 DateTime提供了时、分、秒和毫秒可以轻松地计算各个指针应该指向的位置。画秒钟的代码如下显示为蓝色长度为 0.9倍半径宽度为 1/50半径// 秒钟
ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 60 * time.Second) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2);
ctx.DrawLine(Vector2.Zero, new Vector2(0,-r*0.9f), form.XResource.GetColor(Color.Blue), r/50);效果如下依法炮制可以画出分钟和时钟// 分钟
ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 60 * time.Minute) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2);
ctx.DrawLine(Vector2.Zero, new Vector2(0, -r * 0.8f), form.XResource.GetColor(Color.Green), r / 35);
// 时钟
ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 12 * time.Hour) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2);
ctx.DrawLine(Vector2.Zero, new Vector2(0, -r * 0.7f), form.XResource.GetColor(Color.Red), r / 20);效果如下优化其实到了这一步已经是一个完整的可运行的时钟了但还能再优化优化。半透明时钟首先可以设置一定的半透明度使三根钟重叠时不显得很突兀代码如下var blue new Color(red: 0.0f, green: 0.0f, blue: 1.0f, alpha: 0.7f);
ctx.DrawLine(Vector2.Zero, new Vector2(0,-r*0.9f), form.XResource.GetColor(blue), r/50);只需将原本的 Color.Blue等颜色改成自定义并且指定 alpha参数为 0.7表示 70%半透明即可效果如下时钟两端的尖角或者圆角Direct2D可以很方便地控制绘制的线段两端有许多风格可供选择具体可以参见 CapStyle枚举public enum CapStyle
{ /// unmanagedD2D1_CAP_STYLE_FLAT/unmanaged Flat, /// unmanagedD2D1_CAP_STYLE_SQUARE/unmanaged Square, /// unmanagedD2D1_CAP_STYLE_ROUND/unmanaged Round, /// unmanagedD2D1_CAP_STYLE_TRIANGLE/unmanaged Triangle
}此处我们将使用 Round用于做中心点用 Triangle用于做针尖首先创建一个 StrokeStyle对象using var clockLineStyle new StrokeStyle(form.XResource.Direct2DFactory, new StrokeStyleProperties
{ StartCap CapStyle.Round, EndCap CapStyle.Triangle,
});然后在调用 ctx.DrawLine()时将 clockLineStyle参数传入最后一个参数即可ctx.DrawLine(Vector2.Zero, new Vector2(0,-r*0.9f), form.XResource.GetColor(blue), r/50, clockLineStyle);执行效果如下可见有那么点意思了平滑移动Direct2D是实时渲染我们不能浪费这实时二字带来的好处。更何况显示出来的时钟也不太合理因为当时时间是 9:57此时时钟应该指向偏 10点的位置。但现在由于忽略了这一分量指向的是 9点这不符合实时的时钟。因此计算小时角度时可以加入分钟分量计算分钟角度时可以加入秒钟分量计算秒钟角度时也可以加入毫秒的分量。代码只需将矩阵变换代码稍微变动一点点即可// 秒钟
ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 60 * (time.Second time.Millisecond / 1000.0f)) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2);
// ...
// 分钟
ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 60 * (time.Minute time.Second / 60.0f)) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2);
// ...
// 时钟
ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 12 * (time.Hour time.Minute / 60.0f)) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2);执行效果如下阴影效果和边缘刻度不一样时钟多少是和窗口底层有距离的因此怎么说也会显示一些阴影效果。这在 Direct2D中也能轻易实现。代码会复杂一点过程如下先将创建一个临时的 Bitmap1;将时、分、秒钟绘制到这个 Bitmap中;创建一个 ShadowEffect传入这个 Bitmap的内容生成一个阴影贴图;调用 ctx.DrawImage()将 ShadowEffect先绘制;调用 ctx.DrawBitmap()绘制最后真正的时、分、秒钟。注意这个过程的顺序不能错否则可能出现阴影显示的真实物体上的虚幻效果。临时的 Bitmap1和 ShadowEffect可以在 CreateDeviceSizeResources和 ReleaseDeviceSizeResources事件中创建和销毁Bitmap1 bitmap null;
Shadow shadowEffect null;
form.CreateDeviceSizeResources (RenderWindow sender)
{ bitmap new Bitmap1(form.XResource.RenderTarget, form.XResource.RenderTarget.PixelSize, new BitmapProperties1(new PixelFormat(Format.B8G8R8A8_UNorm, SharpDX.Direct2D1.AlphaMode.Premultiplied), dpi, dpi, BitmapOptions.Target)); shadowEffect new SharpDX.Direct2D1.Effects.Shadow(form.XResource.RenderTarget);
};
form.ReleaseDeviceSizeResources o
{ bitmap.Dispose(); shadowEffect.Dispose();
};其中 dpi从 Direct2DFactory.DesktopDpi.Width进行获取。先将 ctx的 Target属性指定这个 bitmap但又同时保存老的 Target属性用于稍后绘制var oldTarget ctx.Target;
ctx.Target bitmap;
ctx.BeginDraw();
{ ctx.Clear(Color.Transparent); // 上文中的绘制时钟部分...
}
ctx.EndDraw();注意 ctx.Clear(Color.Transparent);是有必要的否则将出现重影这样即可将时钟单独绘制到 bitmap中对这个 bitmap生成一个阴影shadowEffect.SetInput(0, ctx.Target, invalidate: new RawBool(false));最后进行绘制绘制时记得顺序ctx.Target oldTarget;
ctx.BeginDraw();
{ ctx.Transform Matrix3x2.Identity; ctx.UnitMode UnitMode.Pixels; ctx.DrawImage(shadowEffect); ctx.DrawBitmap(bitmap, 1.0f, InterpolationMode.NearestNeighbor); ctx.UnitMode UnitMode.Dips;
}注意两点首先设置 ctx.Transformidentity是有必要的否则会上文的矩阵变换会一直保持作用然后两次设置 ctx.UnitModepixels/dips也是有必要的因为此时的绘制相当于是图片按照默认的高 DPI显示会导致显示模糊因此显示图片时需要改成点对点显示;效果如下这个阴影默认是完全重叠的现实中这种光线较小加一点点平移效果可能会更好ctx.DrawImage(shadowEffect, new Vector2(r/20,r/20));效果如下显然更逼真了更好的动画有些时钟的秒确实是这样动的但我印象中儿时的记忆秒是一格一格地动它是每动一下停顿一下再动的那种感觉。为了实现这种感觉我加入了 WindowsAnimationManager的功能这也是 COM组件的一部分我的 FlysEngine中稍微封装了一下。使用时需要引入一个 timer进行配合float secondPosition DateTime.Now.Second;
Variable secondVariable null;
var timer new System.Windows.Forms.Timer { Enabled true, Interval 1000 };
timer.Tick (o, e)
{ secondVariable?.Dispose(); secondVariable form.XResource.CreateAnimation(secondPosition, DateTime.Now.Second, 0.2f);
};
form.FormClosing delegate { timer.Dispose(); };
form.UpdateLogic (window, dt)
{ secondPosition (float)(secondVariable?.Value ?? 0.0f);
};注意此处我使用了 UpdateLogic事件这也是 FlysEngine中封装的可以在绘制呈现前执行一段更新逻辑的代码。然后后面的绘制时将获取秒的矩阵变换参数改为 secondPosition变量即可ctx.Transform Matrix3x2.Rotation(MathF.PI * 2 / 60 * secondPosition) * Matrix3x2.Translation(ctx.Size.Width / 2, ctx.Size.Height / 2);最后的执行效果如下看起来一切正常但……如果经过分钟满时会出现这种情况这是因为秒数从 59秒到 00秒的动画是一个递减的过程 59-00因此秒钟反向转了一圈这明显不对。解决这个问题可以这样考虑如果当前是 59秒我们假装它是 -1秒即可这时计算角度不会出错矩阵变换也没任何问题通过 C# 8.0强大的 switchexpression功能可以不需要额外语句在表达式内即可解决secondVariable form.XResource.CreateAnimation(secondPosition switch
{ 59 -1, var x x,
}, DateTime.Now.Second, 0.2f);最后的最后最终效果如下结语记得6年前我老婆第一次来我出租房玩然后……我给她感受了作为一个程序员的“浪漫”花了一整个下午时间把这个 demo从 0开始做了出来给她看不过那时我还在用 C。多年后和她说起这个入门 demo她仍记忆尤新。本文中最终效果的代码可以从我的 github仓库下载https://github.com/sdcb/blog-data/blob/master/2019/20191021-render-clock-using-dotnet/clock.linq有了 .NET那些代码已经远比当年简单我的确是从这个例子出发做出了许多好玩的东西以后有机会我会慢慢介绍敬请期待。喜欢的朋友 请关注我的微信公众号【DotNet骚操作】