3

我正在开发一个使用 Juce 库来显示图形的项目。到目前为止,我一直在使用该库的 API 函数来生成线性和径向渐变,但这是该库支持的仅有的两种类型的渐变。我现在需要生成一种不同类型的渐变,一种遵循正凸多边形的形状。这里的关键词是 REGULAR,意思是一个多边形,所有边的长度相同,所有顶点都位于一个圆上。

对于五边形的情况,这里有一张图片可以更好地显示我想要得到的结果: http ://www.filterforge.com/wiki/index.php/Polygonal_Gradient

对于我的应用程序,我希望能够指定具有任意数量边的多边形渐变。(五边形、六边形、八边形等……)

鉴于 API 的限制,我可以产生所需结果的唯一方法是逐像素填充表面矩阵,以数学方式计算每个像素的 R、G、B、A 分量的值。

这是我到目前为止的代码:

void render_surface(unsigned char *surface_data,
                    int width, int height, int linestride,
                    int num_vertices, t_rgba *color1, t_rgba *color2)
{
    const double center_x = 0.5 * width;
    const double center_y = 0.5 * height;
    const double radius = 0.5 * MIN(width, height);
    int x, y;

    for (y = height; --y >= 0;) {

        uint32_t *line = (uint32_t *)data;
        data += linestride;

        const double dy = y - center_y;

        for (x = width; --x >= 0;) {

            const double dx = x - center_x;

            double rho = hypot(dx, dy);
            rho /= radius;    // normalize radius 

            // constrain
            rho = CLIP(rho, 0.0, 1.0);

            // interpolate
            double a = color2->alpha + (color1->alpha - color2->alpha) * rho;
            double r = color2->red   + (color1->red   - color2->red  ) * rho;
            double g = color2->green + (color1->green - color2->green) * rho;
            double b = color2->blue  + (color1->blue  - color2->blue ) * rho;

            // premultiply alpha
            r *= a;
            g *= a;
            b *= a;

#if LITTLE_ENDIAN
            *line++ = ((unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * a) << 24) // alpha
                    | ((unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * r) << 16) // red
                    | ((unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * g) <<  8) // green
                    |  (unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * b);       // blue
#else
            *line++ = ((unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * b) << 24) // blue
                    | ((unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * g) << 16) // green
                    | ((unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * r) <<  8) // red
                    |  (unsigned int)((256.0 * (1.0 - DBL_EPSILON)) * a);       // alpha
#endif
        }
    }
}

上面的代码产生了一个径向渐变,与我使用一个 API 函数可以产生的渐变类型相同。然而,这似乎是解决问题的一个很好的起点。

surface_data - 是一个 8 位值矩阵,表示红色、绿色、蓝色和 Alpha 分量的像素强度。

num_vertices - 是我们希望多边形渐变具有的顶点数(在单个圆上等距分布)。

color1 - 渐变的起始颜色。

color2 - 渐变的结束颜色。

我想知道如何以相同的方式填充表面,创建多边形渐变而不是径向渐变。

谢谢你的帮助。

  • 路易吉

稍微重新考虑一下这个问题......如果我们将坐标系的原点视为多边形的中心,归结为找到一个方程,使得对于笛卡尔坐标中的任何输入点,输出是到最近的距离多边形的一侧。

我的直觉告诉我,一定有某种封闭形式的解决方案,因为:

对于一个圆圈,

rho = sqrt(dx*dx + dy*dy);

给我们到圆心的径向距离,可以将其视为具有无限边的多边形。

对于一个正方形,

fmax(fabs(dx), fabs(dy));

给我们距离正方形最近边的切比雪夫距离,可以将其视为具有 4 个边的多边形。

所以,我认为这两个公式的某种组合应该给出中间案例,这将解决最初的问题。

我完全不按照这些思路思考吗?

  • 路易吉
4

2 回答 2

1

这大概就是我的处理方式......

  • 将多边形的中心放置在原点“O”处。
  • 对于正多边形的给定段内的给定点“P”,让通过“O”和“P”的线为“Line1”和
  • 让通过包含多边形段的外边缘的线为“Line2”
  • 找到这两条线的交点“IP”。

现在 P 处的颜色分数由 P 到原点的距离相对于 IP 到原点的距离来定义。

在此处输入图像描述

编辑:我已经实现了上面的算法,这是输出......

在此处输入图像描述

Edit2:这是(Delphi)代码

const
  vertical: TFloat = 3.4e38;

function Slope(const pt1, pt2: TFloatPoint): single;
begin
  if (pt1.X = pt2.X) then result := vertical
  else result := (pt2.Y - pt1.Y)/(pt2.X - pt1.X);
end;
//---------------------------------------------------------------------------

procedure GetLine(const pt1, pt2: TFloatPoint; out m, b: TFloat);
begin
  m := Slope(pt1, pt2);
  if m = vertical then
    b := pt1.X else
    b := pt1.Y - m * pt1.X;
end;
//---------------------------------------------------------------------------

function GradientColor(const clr1, clr2: TColor32; fraction: TFloat): TColor32;
begin
  if fraction <= 0 then result := clr1
  else if fraction >= 1 then result := clr2
  else
  begin
    TColor32Entry(result).B :=
      trunc(TColor32Entry(clr2).B * fraction + TColor32Entry(clr1).B * (1-fraction));
    TColor32Entry(result).G :=
      trunc(TColor32Entry(clr2).G * fraction + TColor32Entry(clr1).G * (1-fraction));
    TColor32Entry(result).R :=
      trunc(TColor32Entry(clr2).R * fraction + TColor32Entry(clr1).R * (1-fraction));
    TColor32Entry(result).A :=
      trunc(TColor32Entry(clr2).A * fraction + TColor32Entry(clr1).A * (1-fraction));
  end;
end;
//---------------------------------------------------------------------------

function PointInTriangle(const pt, tr1, tr2, tr3: TFloatPoint): boolean;
begin
  result := false;
  if ((((tr1.Y <= pt.Y) and (pt.Y < tr3.Y)) or
    ((tr3.Y <= pt.Y) and (pt.Y < tr1.Y))) and
    (pt.X < (tr3.X - tr1.X) * (pt.Y - tr1.Y) /
    (tr3.Y - tr1.Y) + tr1.X)) then result := not result;
  if ((((tr2.Y <= pt.Y) and (pt.Y < tr1.Y)) or
    ((tr1.Y <= pt.Y) and (pt.Y < tr2.Y))) and
    (pt.X < (tr1.X - tr2.X) * (pt.Y - tr2.Y) /
    (tr1.Y - tr2.Y) + tr2.X)) then result := not result;
  if ((((tr3.Y <= pt.Y) and (pt.Y < tr2.Y)) or
    ((tr2.Y <= pt.Y) and (pt.Y < tr3.Y))) and
    (pt.X < (tr2.X - tr3.X) * (pt.Y - tr3.Y) /
    (tr2.Y - tr3.Y) + tr3.X)) then result := not result;
end;
//---------------------------------------------------------------------------

function GetSegmentIndex(vertex: TFloatPoint; vertices: TArrayOfFloatPoint): integer;
var
  i, highI: integer;
  prev: TFloatPoint;
const
  origin: TFloatPoint = (X: 0; Y: 0);
begin
  highI := high(vertices);
  prev := vertices[highI];
  result := -1;
  for i := 0 to highI do
  begin
    if PointInTriangle(vertex, origin, prev, vertices[i]) then
    begin
      result := i;
      break;
    end;
    prev := vertices[i];
  end;
end;
//---------------------------------------------------------------------------

procedure RegularPolygonFill(bmp: TBitmap32; const origin: TPoint;
  radius: TFloat; vertexCount: integer; InnerColor, OuterColor: TColor32);
var
  i,j,d,q: integer;
  dist1,dist2: TFloat;
  vert, intersetPt: TFloatPoint;
  verts: TArrayOfFloatPoint;
  edgeMs, edgeBs: TArrayOfFloat;
  angle, angleDiff, m, b: TFloat;
  sinAngle, cosAngle: extended;
const
  orig: TFloatPoint = (X: 0; Y: 0);
begin
  if vertexCount < 3 then exit;
  setlength(verts, vertexCount);
  setlength(edgeMs, vertexCount); //edge slopes  (ie y = M*x +b)
  setlength(edgeBs, vertexCount); //edge offsets (ie y = m*x +B)
  angleDiff := pi *2 / vertexCount;
  angle := angleDiff;
  vert.X := radius; //vert used here as prev vertex
  vert.Y := 0;
  for i := 0 to vertexCount -1 do
  begin
    SinCos(angle, sinAngle, cosAngle);
    verts[i].X := cosAngle * radius;
    verts[i].Y := sinAngle * radius;
    GetLine(vert, verts[i], edgeMs[i], edgeBs[i]);
    angle := angle + angleDiff;
    vert := verts[i];
  end;

  d := floor(radius);
  for i := -d to d do
    for j := -d to d do
    begin
      vert := FloatPoint(i,j);
      GetLine(orig, vert, m, b);
      q := GetSegmentIndex(vert, verts);
      if q < 0 then continue;
      //simultaneous equations to find intersection ...
      //y = m * x + b; y = edgeMs[q]* x + edgeBs[q];
      //edgeMs[q]* x + edgeBs[q] = m * x + b;
      //(edgeMs[q] - m) * x = b - edgeBs[q]
      //x = (b - edgeBs[q])/(edgeMs[q] - m)
      if m = vertical then
      begin
        intersetPt.X := b;
        intersetPt.Y := edgeMs[q]* intersetPt.X + edgeBs[q];
      end
      else if edgeMs[q] = vertical then
      begin
        intersetPt.X := edgeBs[q];
        intersetPt.Y := m* intersetPt.X + b;
      end else
      begin
        intersetPt.X := (b - edgeBs[q])/(edgeMs[q] - m);
        intersetPt.Y := m * intersetPt.X + b;
      end;

      //get distances from origin of vert and intersetPt ...
      dist1 := sqrt(vert.X*vert.X + vert.Y*vert.Y);
      dist2 := sqrt(intersetPt.X*intersetPt.X + intersetPt.Y*intersetPt.Y);

      bmp.Pixel[i + origin.X, j + origin.Y] :=
        GradientColor(InnerColor, OuterColor, dist1/dist2);
    end;
end;
于 2012-08-05T04:25:21.257 回答
0

我的直觉告诉我,一定有某种封闭形式的解决方案

有...

分析

为此,我采用了从中心到六边形边缘的距离公式(此处),并注意到它可以推广到任何多边形。在该特定公式中,sqrt(3)使用了常数(我现在将其称为z)。该数字相当于多边形中点与其一条边的中点之间的距离与中点与其顶点之一之间的距离之比的两倍(该距离在单位长度的多边形中为 1)。

因此,这个常数(sqrt(3)用于六边形)由下式给出:

在此处输入图像描述

我之前描述的这个比率由下式给出:

在此处输入图像描述

注意 2s 抵消了,所以为任何多边形推导这个常数的一般函数是:

在此处输入图像描述

SIDES是多边形边数(例如六边形为 6)

现在我们简单地将这个常数代入公式中,计算一个点在给定单位长度多边形上的位置与它在单位长度圆上的位置之比:

在此处输入图像描述

例子

下面显示了六边形的结果(因此SIDES=6z=sqrt(3))。对于 θ=30°,我们得到d为 0.866,对于 θ=45°(也相当于 θ=15°) , d为 0.897。

请注意,d仅正确定义为 0 <= θ <= segmentAngle(由2PI/SIDES弧度给出)。

在此处输入图像描述

执行

现在我们拥有了编写解决方案所需的一切。

以下函数将 2D(像素)坐标映射到 0 到 1 之间的数字;此数字指定每个像素出现在颜色渐变中的哪个点(步骤)。

最终,这与径向梯度非常相似,其中像素和圆中点之间的欧几里德距离用于定义像素的步长。然而,对于多边形梯度,我们希望将像素 (x,y) 和多边形中点之间的欧几里得距离缩放d,这样颜色就会被“推”向多边形的边缘(也就是圆的弦)。

在渲染方面,唯一的事情就是将d乘以adenominator以便多边形以适当的比例渲染。

代码

public void polygonGradient(colour[][] pixels, Gradient gradient, Vector2 midPoint, float angle, float zoom, int sides) {

    final double z = Math.tan(HALF_PI - (PI / sides)); // z, as derived in answer
    final double SEGMENT_ANGLE = (2 * PI) / sides; // max angle of polygon segment in radians

    angle %= SEGMENT_ANGLE; // mod angle to minimise difference between theta and SEGMENT_ANGLE in loop (less while calls)

    final double denominator = 1 / ((Math.max(pixels.height, pixels.width)) * zoom); // calc here once, not in loop

    double yDist; // y distance between midpoint and a given pixel
    double xDist; // x distance between midpoint and a given pixel

    for (int y = 0, x; y < pixels.height; ++y) {
        yDist = (midPoint.y - y) * (midPoint.y - y);
        
        for (x = 0; x < pixels.width; ++x) {
            xDist = (midPoint.x - x) * (midPoint.x - x);
            
            double theta = Math.atan2((midPoint.y - y), (midPoint.x - x));
            theta += (TWO_PI - angle); // TWO_PI - angle, so we rotate clockwise
            theta = Math.abs(theta); // abs() here to simplify while loop 

            // polygon is split into N segments; restrict theta to angle of one segment
            while (theta > SEGMENT_ANGLE) { // effectively modulo (faster than using % operator)
                theta -= SEGMENT_ANGLE;
            }

            final double pointDistance = Math.sqrt(yDist + xDist); // euclidean dist between (x,y) and midpoint

            double d = z * (z * Math.cos(theta) + Math.sin(theta)); // d, as derived in answer

            // now calculate the position of the pixel on the gradient
            double step = d  * denominator * pointDistance; // multiply euclidean distance by d

            if (step > 1) { // clamp to 1
                step = 1;
            }
            
            pixels[x][y] = gradient.colourAt(step); // get the colour of the gradient at the step given by dist
        }
    }
}

输出

边数 = 6

在此处输入图像描述

边数 = 9

在此处输入图像描述

边数 = 3;放大一点;中点偏离中心

在此处输入图像描述

边数 = 5;非线性颜色插值 在此处输入图像描述

于 2020-08-05T11:55:04.620 回答