|
| 1 | +// Copyright (c) Six Labors. |
| 2 | +// Licensed under the Six Labors Split License. |
| 3 | + |
| 4 | +using SixLabors.ImageSharp.Drawing; |
| 5 | +using SixLabors.ImageSharp.Drawing.Processing; |
| 6 | +using SixLabors.ImageSharp.PixelFormats; |
| 7 | +using SixLabors.ImageSharp.Processing; |
| 8 | +using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes; |
| 9 | +using Color = SixLabors.ImageSharp.Color; |
| 10 | +using Pen = SixLabors.ImageSharp.Drawing.Processing.Pen; |
| 11 | +using Pens = SixLabors.ImageSharp.Drawing.Processing.Pens; |
| 12 | +using PointF = SixLabors.ImageSharp.PointF; |
| 13 | +using Rectangle = SixLabors.ImageSharp.Rectangle; |
| 14 | +using Size = SixLabors.ImageSharp.Size; |
| 15 | +using SizeF = SixLabors.ImageSharp.SizeF; |
| 16 | + |
| 17 | +namespace WebGPUHostedSurfaceDemo.Scenes; |
| 18 | + |
| 19 | +/// <summary> |
| 20 | +/// Hosted-surface scene that exercises canvas readback by applying CPU image processors to regions of the current frame. |
| 21 | +/// Pointer movement changes the processed regions so readback cost can be assessed interactively. |
| 22 | +/// </summary> |
| 23 | +internal sealed class ApplyReadbackScene : RenderScene |
| 24 | +{ |
| 25 | + private static readonly Color BackgroundColor = Color.MidnightBlue; |
| 26 | + private static readonly Color StripeA = Color.LimeGreen; |
| 27 | + private static readonly Color StripeB = Color.DodgerBlue; |
| 28 | + private static readonly Color StripeC = Color.Orange; |
| 29 | + private static readonly Color OutlineColor = Color.White; |
| 30 | + private static readonly Color GuideLineColor = Color.White.WithAlpha(.5F); |
| 31 | + |
| 32 | + private PointF pointer; |
| 33 | + private bool hasPointer; |
| 34 | + private float regionScale = 1F; |
| 35 | + |
| 36 | + public override string DisplayName => "Apply"; |
| 37 | + |
| 38 | + public override void Paint(DrawingCanvas<Bgra32> canvas, Size viewportSize, TimeSpan deltaTime) |
| 39 | + { |
| 40 | + if (viewportSize.Width <= 0 || viewportSize.Height <= 0) |
| 41 | + { |
| 42 | + return; |
| 43 | + } |
| 44 | + |
| 45 | + canvas.Fill(Brushes.Solid(BackgroundColor), new RectangularPolygon(0, 0, viewportSize.Width, viewportSize.Height)); |
| 46 | + DrawPattern(canvas, viewportSize); |
| 47 | + |
| 48 | + // Keep both regions tied to the pointer so every movement exercises readback from a different part |
| 49 | + // of the surface texture instead of repeatedly processing a static rectangle. |
| 50 | + PointF focus = this.hasPointer ? this.pointer : new PointF(viewportSize.Width * .5F, viewportSize.Height * .5F); |
| 51 | + float effectSize = MathF.Max(48F, MathF.Min(viewportSize.Width, viewportSize.Height) * .26F * this.regionScale); |
| 52 | + PointF blurCenter = new( |
| 53 | + ClampCenter(viewportSize.Width - focus.X, effectSize * 1.25F, viewportSize.Width), |
| 54 | + ClampCenter(focus.Y, effectSize, viewportSize.Height)); |
| 55 | + |
| 56 | + Rectangle edgeRegion = CreateRegion(viewportSize, focus, effectSize); |
| 57 | + IPath blurRegion = new EllipsePolygon( |
| 58 | + blurCenter, |
| 59 | + new SizeF(effectSize * 1.25F, effectSize)); |
| 60 | + |
| 61 | + // Apply reads the affected pixels back from the current WebGPU frame, runs the CPU processor, |
| 62 | + // then writes the processed region back before the frame is presented. |
| 63 | + canvas.Apply(edgeRegion, ctx => ctx.DetectEdges()); |
| 64 | + canvas.Apply(blurRegion, ctx => ctx.GaussianBlur(Math.Max(3F, Math.Min(viewportSize.Width, viewportSize.Height) / 120F))); |
| 65 | + |
| 66 | + canvas.Draw(Pens.Solid(OutlineColor, 3), new RectangularPolygon(edgeRegion.X, edgeRegion.Y, edgeRegion.Width, edgeRegion.Height)); |
| 67 | + canvas.Draw(Pens.Solid(OutlineColor, 3), blurRegion); |
| 68 | + } |
| 69 | + |
| 70 | + public override void OnMouseDown(MouseEventArgs e) => this.SetPointer(e); |
| 71 | + |
| 72 | + public override void OnMouseMove(MouseEventArgs e) => this.SetPointer(e); |
| 73 | + |
| 74 | + public override void OnMouseWheel(MouseEventArgs e) |
| 75 | + { |
| 76 | + this.SetPointer(e); |
| 77 | + |
| 78 | + // Wheel resizing changes the amount of data read back and written back each frame. |
| 79 | + float factor = e.Delta > 0 ? 1.12F : 1F / 1.12F; |
| 80 | + this.regionScale = Math.Clamp(this.regionScale * factor, .5F, 2.4F); |
| 81 | + } |
| 82 | + |
| 83 | + private static void DrawPattern(DrawingCanvas<Bgra32> canvas, Size viewportSize) |
| 84 | + { |
| 85 | + float stripeWidth = Math.Max(18F, viewportSize.Width / 18F); |
| 86 | + float height = viewportSize.Height; |
| 87 | + |
| 88 | + for (int i = -2; i < (viewportSize.Width / stripeWidth) + 3; i++) |
| 89 | + { |
| 90 | + Color color = (Math.Abs(i) % 3) switch |
| 91 | + { |
| 92 | + 0 => StripeA, |
| 93 | + 1 => StripeB, |
| 94 | + _ => StripeC, |
| 95 | + }; |
| 96 | + |
| 97 | + float x = i * stripeWidth; |
| 98 | + IPath stripe = new Polygon( |
| 99 | + new LinearLineSegment( |
| 100 | + new PointF(x, 0), |
| 101 | + new PointF(x + (stripeWidth * 1.6F), 0), |
| 102 | + new PointF(x + (stripeWidth * .6F), height), |
| 103 | + new PointF(x - stripeWidth, height))); |
| 104 | + |
| 105 | + canvas.Fill(Brushes.Solid(color), stripe); |
| 106 | + } |
| 107 | + |
| 108 | + Pen guideLinePen = Pens.Solid(GuideLineColor, 1.5F); |
| 109 | + for (int y = 0; y < viewportSize.Height; y += Math.Max(24, viewportSize.Height / 14)) |
| 110 | + { |
| 111 | + canvas.DrawLine( |
| 112 | + guideLinePen, |
| 113 | + new PointF(0, y), |
| 114 | + new PointF(viewportSize.Width, y + (viewportSize.Width * .08F))); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + private static Rectangle CreateRegion(Size viewportSize, PointF center, float size) |
| 119 | + { |
| 120 | + int width = Math.Max(1, Math.Min(viewportSize.Width, (int)size)); |
| 121 | + int height = Math.Max(1, Math.Min(viewportSize.Height, (int)(size * .75F))); |
| 122 | + int x = Math.Clamp((int)(center.X - (width * .5F)), 0, Math.Max(0, viewportSize.Width - width)); |
| 123 | + int y = Math.Clamp((int)(center.Y - (height * .5F)), 0, Math.Max(0, viewportSize.Height - height)); |
| 124 | + |
| 125 | + return new Rectangle(x, y, width, height); |
| 126 | + } |
| 127 | + |
| 128 | + private static float ClampCenter(float value, float size, float limit) |
| 129 | + { |
| 130 | + float radius = MathF.Min(size * .5F, limit * .5F); |
| 131 | + |
| 132 | + return Math.Clamp(value, radius, limit - radius); |
| 133 | + } |
| 134 | + |
| 135 | + private void SetPointer(MouseEventArgs e) |
| 136 | + { |
| 137 | + this.pointer = new PointF(e.X, e.Y); |
| 138 | + this.hasPointer = true; |
| 139 | + } |
| 140 | +} |
0 commit comments