可视化学习:使用极坐标参数方程和SDF绘制有趣的图案

sdf · 浏览次数 : 0

小编点评

```cpp #include void main() { // 设置背景颜色 vec2 st = vUv * 10.0; gl_FragColor.rgb = vec3(0.0, 0.0, 0.0); gl_FragColor.a = 1.0; // 四边形 if (random() < 0.14) { vec2 fpos = fpos - vec2(0.5); float d = polygon_distance2(fpos, 4, vec2(0.0, 0.4)); gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0); gl_FragColor.a = smoothstep(0.01, 0.0, d); } // 四片花瓣 else if (random() < 0.28) { float u_k = 4.0; vec2 fpos = fpos - vec2(0.5); fpos = polar(fpos); d = 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x; gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0); gl_FragColor.a = smoothstep(-0.01, 0.01, d); } // 苹果 else if (random() < 0.42) { float u_k = 1.3; vec2 fpos = fpos - vec2(0.5, 0.7); fpos = polar(fpos); fpos.y += 3.14 / 2.0; if (fpos.y > 3.14) fpos.y -= 3.14 * 2.0; float d = 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x; gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0); gl_FragColor.a = smoothstep(-0.01, 0.01, d); } // 六边形 else if (random() < 0.56) { vec2 fpos = fpos - vec2(0.5); float d = polygon_distance2(fpos, 6, vec2(0.0, 0.4)); gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0); gl_FragColor.a = smoothstep(0.01, 0.0, d); } // 花苞 else { float u_k = 5.0; float u_scale = 0.13; float u_offset = 0.2; vec2 fpos = fpos - vec2(0.5); fpos = polar(fpos); float d = smoothstep(-0.3, 1.0, u_scale * 0.5 * cos(fpos.y * u_k) + u_offset) - fpos.x; gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0); gl_FragColor.a = smoothstep(-0.01, 0.01, d); } } ```

正文

前言

本文将介绍如何使用极坐标参数方程和上一篇文章提到的距离场SDF来绘制有趣的图案。

说到曲线和几何图形的绘制,我们知道图形系统默认支持的是通过直角坐标绘制,但是有些曲线呢,不太容易使用直角坐标系来表示,却可以很方便地使用极坐标来表示,这个时候我们可以选择通过极坐标和直角坐标的相互转换,来实现图形的绘制。

下面我就用玫瑰线、花瓣线等曲线作为例子来进行演示。

在开始演示之前,我先简单介绍下极坐标和参数方程。

  • 极坐标是使用相对极点的距离,以及与X轴正向的夹角,使用这一对值来表示平面上点的坐标。

  • 参数方程表示的是点的坐标分别和相关参数的关系,所以通常会对应一个方程组,比如说二维平面上的点,会使用两个参数方程来表示。

我这里只是简单介绍一下,可能说的并不是很准确。我们也可以用圆作为例子,圆的标准方程大家都知道,假设圆心在原点,在直角坐标系下,圆的公式就是x的平方加上y的平方等于半径的平方,圆的参数方程是x等于半径乘以与X轴夹角的余弦值,y等于半径乘以与X轴夹角的正弦值。

\[\begin{cases} x = r * cosθ \\ y = r * sinθ \end{cases} \]

但在极坐标系下,圆的参数方程就变成了r等于一个常量,θ等于与X轴的夹角。这基本上可以算是最简单的极坐标参数方程组了。

\[\begin{cases} r = r \\ θ = t \end{cases} \]

那么基本的知识了解后,我们就可以开始使用极坐标参数方程来绘制图形和曲线了。

具体实现

现在就来演示通过曲线的极坐标参数方程来绘制曲线。

Canvas

首先先来看Canvas2D的例子,在Canvas中我们可以通过lineTo方法绘制足够多的线段将它们连在一起,来模拟曲线,所以我们可以通过参数方程获取足够多的点,将它们连接起来,这样就能最终完成曲线的绘制。

因此我们先来定义一个高阶函数parametric用于创建方程组,接收三个参数xFunc、yFunc和rFunc。xFunc和yFunc表示点的一对坐标值各自分别对应的参数方程,rFunc表示坐标映射函数。

export default function parametric(xFunc, yFunc, rFunc) {
    return function(start, end, seg = 100, ...args) {
        const points = [];
        for (let i = 0; i <= seg; i ++) {
            const p = i / seg;
            // const t = start * (1 - p) + end * p;
            const t = start + (end - start) * i / seg;
            // console.log(t);
            const x = xFunc(t, ...args);
            const y = yFunc(t, ...args);
            if (rFunc) points.push(rFunc(x, y));
            else points.push([x, y]);
        }
        return {
            draw: draw.bind(null, points),
            points
        }
    }
}

这个高阶函数的返回值也是一个函数,在这个匿名函数应该不难理解,我简单说一下,它接收多个参数,必选的三个是坐标相关参数 t 的上下限start和end,以及要收集的点的数量seg,最终返回一个对象,这个对象带有一个draw方法,通过调用这个draw方法就可以完成曲线的绘制。

下面我们就通过parametric函数构造不同的曲线方程组。来看一个玫瑰线:

// 玫瑰线
const rose = parametric(
    (t, a, k) => a * Math.cos(k * t), // r
    t => t, // θ
    fromPolar,
);

这里fromPolar是一种坐标映射函数,作用是将极坐标转换为直角坐标,这样我们才能在Canvas2d中使用坐标值。现在我们就可以通过调用rose函数,来获取曲线上的点并绘制曲线了。

rose(0, Math.PI, 100, 200, 5).draw(ctx, {strokeStyle: 'blue'}); // 玫瑰线

通过传入不同的a,我们可以改变曲线的大小,而不同的k,可以改变叶子的数目;这样就能构造出不同的图案。

所以我们也可以应用不同的曲线方程,绘制出各种曲线。

WebGL

那么除了Canvas2d,我们当然也可以在WebGL中应用曲线的参数方程实现图形的绘制。接下来我就演示几个在shader中应用参数方程的例子,并结合上篇文章中提到的距离场,完成图形的绘制。

首先还是玫瑰线:

void main() {
  vec2 st = vUv - vec2(0.5);
  st = polar(st);
  float d = 0.5 * cos(st.y * 3.0) - st.x;

  gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

在这段Shader代码中,这里的0.5就是刚刚在Canvas例子中的a,而3.0就是Canvas例子中的k,所以这里叶子的数目就是3。那为什么这里的d要这样定义呢?这是因为玫瑰线上的所有点都满足 0 = a * cos(k * θ) - r 这个等式,也就是说玫瑰线上的点对应的d都是0,并且玫瑰线形成的图形内部的点对应的d都大于0,而外部的点对应的d都小于0,因此这样我们就可以得到一个内部填充为白色的玫瑰线图形。

我们对玫瑰线的参数和公式做些微调,就能得到不同的图形,比如添加一个取绝对值的操作,并且给角度乘以一个常量0.5。

void main() {
  float u_k = 3.0; 

  vec2 st = vUv - vec2(0.5);
  st = polar(st);
  float d = 0.5 * abs(cos(st.y * u_k * 0.5)) - st.x;
  gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

可以看出这个图形有些类似花瓣的形状。

当我们设置不同的u_k值时,也能得到不同的图案,比如当u_k设置为1.3时,图形看上去就像一个横放的苹果。

void main() {
  float u_k = 1.3; // 横放的苹果

  vec2 st = vUv - vec2(0.5);
  st = polar(st);
  float d = 0.5 * abs(cos(st.y * u_k * 0.5)) - st.x;
  gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

继续微调参数方程,我们还能画出类似横放的葫芦图案:

void main() {
  float u_k = 1.7; // 横放的葫芦
  float u_scale = 0.5; 
  float u_offset = 0.2; 
  
  vec2 st = vUv - vec2(0.5);
  st = polar(st);
  float d = u_scale * 0.5 * abs(cos(st.y * u_k * 0.5)) - st.x + u_offset;
  gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

相信小伙伴们通过结合三角函数、abs、smoothstep等等这些函数,还能绘制出更多有趣的图案,大家可以动手自己尝试一下。

结合随机数

接下来,我们结合一个随机数函数,来实现一个类似剪纸的图案。

在这个例子中,我们会使用一个生成随机数的函数叫做random,从代码中我们可以看出random实际上生成的是一个伪随机数,因为纹理坐标是确定的,所以根据纹理坐标生成的随机数也是确定的。

float random(vec2 st) {
  return fract(
    sin(
      dot(st.xy, vec2(12.9898, 78.233))
    ) *
    43758.5453123
  );
}

这个函数是我学的课程上给的一个函数,应该是一个经验公式。那么接下来我们就开始来实现这个剪纸图案。

首先,我们利用实现网格的方式,将画布变为10 x 10的网格,此时得到的st变量就是片元对应的纹理坐标乘以10,接着我们分别获取到st的整数部分和小数部分。

整数部分ipos将用于生成随机数,而小数部分就用于绘制各类图案。

vec2 st = vUv * 10.0;
vec2 ipos = floor(st); // integer
vec2 fpos = fract(st); // fraction

float r = random(ipos);

接着我们就可以按照随机数在不同区间绘制不同的图案,我这里就用上期学的着色器几何造型里的不同距离公式和上面的几个参数方程来实现不同图案的绘制。

void main() {
  vec2 st = vUv * 10.0;
  vec2 ipos = floor(st); // integer
  vec2 fpos = fract(st); // fraction

  float r = random(ipos);

  float d = 0.0;
  if(r < 0.14) { // 四边形
    fpos = fpos - vec2(0.5);
     float d = polygon_distance2(
        fpos,
        4,
        vec2(0.0, 0.4)
     );
     gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0);
     gl_FragColor.a = smoothstep(0.01, 0.0, d);
  } else if (r < 0.28) { // 四片花瓣
    float u_k = 4.0;

    fpos = fpos - vec2(0.5);
    fpos = polar(fpos);
    d = 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x;
    gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
    gl_FragColor.a = smoothstep(-0.01, 0.01, d);
  } else if (r < 0.42) { // 苹果
    float u_k = 1.3;

    fpos = fpos - vec2(0.5, 0.7);
    fpos = polar(fpos);
    fpos.y += 3.14 / 2.0;
    // atan 的返回值是:从第一到第二象限为 0~PI,从第三到第四象限为 -PI~0
    // 旋转极坐标后要保证函数定义域的一致性
    if (fpos.y > 3.14) fpos.y -= 3.14 * 2.0;
    float d = 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x;
    gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
    gl_FragColor.a = smoothstep(-0.01, 0.01, d);
  } else if (r < 0.56) { // 六边形
     fpos = fpos - vec2(0.5);
     float d = polygon_distance2(
        fpos,
        6,
        vec2(0.0, 0.4)
     );
     gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0);
     gl_FragColor.a = smoothstep(0.01, 0.0, d);
  } else if (r < 0.70) { // 五角星
    fpos = fpos - vec2(0.5);
    float d = star_distance(
      fpos,
      5,
      vec2(0.15, 0.2)
    );
    gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0);
    gl_FragColor.a = smoothstep(0.01, 0.0, d);
  } else if (r < 0.84) { // 葫芦
    float u_k = 1.7;
    float u_scale = 0.5;
    float u_offset = 0.2;

    fpos = fpos - vec2(0.5);
    fpos = polar(fpos);
    fpos.y += 3.14 / 2.0;
    if (fpos.y > 3.14) fpos.y -= 3.14 * 2.0;
    float d = u_scale * 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x + u_offset;
    gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
    gl_FragColor.a = smoothstep(-0.01, 0.01, d);
  } else { // 花苞
    float u_k = 5.0;
    float u_scale = 0.13;
    float u_offset = 0.2;

    fpos = fpos - vec2(0.5);
    fpos = polar(fpos);
    float d = smoothstep(-0.3, 1.0, u_scale * 0.5 * cos(fpos.y * u_k) + u_offset) - fpos.x;
    gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
    gl_FragColor.a = smoothstep(-0.01, 0.01, d);
  }
}

这样我们就实现了一个类似于剪纸的图案,实现的方式比较简单粗暴,就是用了一堆if-else,看上去不太优雅。

相信大家一定都知道怎么去实现更多图形图案了,接下去就是多动手多尝试。

代码参考:Canvas

代码参考:WebGL

与可视化学习:使用极坐标参数方程和SDF绘制有趣的图案相似的内容:

可视化学习:使用极坐标参数方程和SDF绘制有趣的图案

本文将介绍如何使用极坐标参数方程和上一篇文章提到的距离场SDF来绘制有趣的图案。有些曲线比起直角坐标系来说,更方便使用极坐标来表示,这个时候我们可以选择通过极坐标和直角坐标的相互转换,来实现图形的绘制

DeepSpeed框架:1-大纲和资料梳理

DeepSpeed是一个深度学习优化软件套件,使分布式训练和推理变得简单、高效和有效。它可以做些什么呢?训练/推理具有数十亿或数万亿参数的密集或稀疏模型;实现出色的系统吞吐量并有效扩展到数千个GPU;在资源受限的GPU系统上进行训练/推理;实现前所未有的低延迟和高吞吐量的推理;以低成本实现极限压缩,

小白都会的数据可视化大屏搭建,速来学习

华为云aPaaS DTSE技术布道师左倩与开发者和伙伴们交流了SVE的独特价值优势和应用实践,手把手教大家基于开天aPaaS集成工作台流编排搭建轻应用和0码构建业务可视化大屏,体验“一次开发、多端使用”的极致便利。

Python学习之二:不同数据库相同表是否相同的比较方法

摘要 昨天学习了使用python进行数据库主键异常的查看. 当时想我们有跨数据库的数据同步场景. 对应的我可以对不同数据库的相同表的核心字段进行对比. 这样的话能够极大的提高工作效率. 我之前写过很长时间的shell.昨天跟着同事开始学python. 感觉的确用python能够节约大量的时间. 生活

5.1 汇编语言:汇编语言概述

汇编语言是一种面向机器的低级语言,用于编写计算机程序。汇编语言与计算机机器语言非常接近,汇编语言程序可以使用符号、助记符等来代替机器语言的二进制码,但最终会被汇编器编译成计算机可执行的机器码。较于高级语言(如C、Python等),汇编语言学习和使用难度相对较大,需要对计算机内部结构、指令集等有深入的了解,以及具有良好的编程习惯和调试能力。但对于需要对计算机底层进行操作的任务,汇编语言是极其高效的,

ebpf的简单学习

ebpf的简单学习-万事开头难 前言 bpf 值得是巴克利包过滤器 他的核心思想是在内核态增加一个可编程的虚拟机. 可以在用户态定义很多规则, 然后直接在内核态进行过滤和使用. 他的效率极高. 因为避免了上下文切换,中断等导致的cycle损失. 很多先进的工具,比如XDP以及K8S的cilium等网

使用.NET7和C#11打造最快的序列化程序-以MemoryPack为例

## 译者注 本文是一篇不可多得的好文,MemoryPack 的作者 neuecc 大佬通过本文解释了他是如何将序列化程序性能提升到极致的;其中从很多方面(可变长度、字符串、集合等)解释了一些性能优化的技巧,值得每一个开发人员学习,特别是框架的开发人员的学习,一定能让大家获益匪浅。 ## 简介 我发

可视化学习:如何使用后期处理通道增强图像效果

GPU是并行渲染的,这样的渲染很高效。但是在实际需求中,有时我们计算片元色值时,需要依赖周围像素点或者某个其他位置像素点的颜色信息,这样的话想要一次性完成绘制就无法做到,需要对纹理进行二次加工处理。

可视化学习 | 如何使用噪声生成纹理

什么是噪声呢?在自然界中,离散的随机是比较常见的,比如蝉鸣突然响起又突然停下,比如雨滴随机落在一个位置,但是随机和连续并存是更常见的情况,比如山脉的走向是随机的,但山峰之间的高度又是连续的,比如天上的云朵、水面的波纹等等。这种把随机和连续结合起来,就形成了噪声。通过利用噪声,我们就可以去模拟真实自然...

深度学习(二)——TensorBoard的使用

内含使用Tensorboard中的SummaryWriter子类add_scalar()和add_image(),将函数数据、图像进行可视化的详解。