【视觉基础篇】11 # 图案生成:如何生成重复图案、分形图案以及随机效果?

简介: 【视觉基础篇】11 # 图案生成:如何生成重复图案、分形图案以及随机效果?

说明

【跟月影学可视化】学习笔记。



如何绘制大批量重复图案

比如绘制带有网格背景的画布

0c6a5eec79fd4d16a5ef35500703b20c.png


如果将网格绘制在 Canvas2D 画布上,网格的线条很多,重绘消耗系统的性能。



方案一:使用 background-image 来绘制重复图案

因为浏览器将渐变属性视为图片,所以可以将渐变设置在任何可以接受图片的 CSS 属性上

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用 background-image 来绘制重复图案</title>
    <style>
        canvas {
            border: 1px dashed salmon;
            background-image: linear-gradient(to right, transparent 90%, #ccc 0),
                linear-gradient(to bottom, transparent 90%, #ccc 0);
            background-size: 8px 8px, 8px 8px;
        }
    </style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
</body>
</html>


06d3541650d44804b5f4454cddca58e0.png


不足之处:

  • 和直接绘制在画布上的其他图形就处于不同的层,没法将它覆盖在这些图形上
  • 用坐标变换来缩放或移动元素时,作为元素背景的网格是不会随着缩放或移动而改变的。



方案二:使用 Shader 来绘制重复图案


利用 GPU 并行计算的特点,使用着色器来绘制背景网格这样的重复图案。使用 Shader 绘制重复图案,不管绘制多么细腻,图案重复多少次,绘制消耗的时间几乎是常量,不会遇到性能瓶颈。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>使用 Shader 来绘制重复图案</title>
        <style>
            canvas {
                border: 1px dashed salmon;
            }
        </style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script src="./common/lib/gl-renderer.js"></script>
        <script type="module">
            const canvas = document.querySelector('canvas');
            // 这里使用基础库 gl-renderer 库,它在 WebGL 底层的基础上进行了一些简单的封装
            const renderer = new GlRenderer(canvas);
            console.log(renderer);
            // 顶点着色器
            const vertex = `
                attribute vec2 a_vertexPosition;
                attribute vec2 uv;
                varying vec2 vUv;
                void main() {
                    gl_PointSize = 1.0;
                    vUv = uv;
                    gl_Position = vec4(a_vertexPosition, 1, 1);
                }
            `;
            /**
             * 片元着色器:渲染过程
             *      1、获得重复的 rows 行 rows 列的值 st
             *          fract:用来获取一个数的小数部分
             *          vUv:由顶点着色器传来的 uv 属性(纹理坐标)乘上 rows 值
             *      2、step 函数是 Shader 中另一个很常用的函数,它就是一个阶梯函数。
             *          它的原理是:当 step(a, b) 中的 b < a 时,返回 0;当 b >= a 时,返回 1。
             *      3、mix 是线性插值函数,mix(a, b, c) 表示根据 c 是 0 或 1,返回 a 或者 b。
             *          vec3(1.0)是白色,vec3(0.8)是灰色
             * */ 
            const fragment = `
                #ifdef GL_ES
                precision mediump float;
                #endif
                varying vec2 vUv;
                uniform float rows;
                void main() {
                    vec2 st = fract(vUv * rows);
                    float d1 = step(st.x, 0.9);
                    float d2 = step(0.1, st.y);
                    gl_FragColor.rgb = mix(vec3(0.8), vec3(1.0), d1 * d2);
                    gl_FragColor.a = 1.0;
                }
            `;
            // 加载片元着色器并创建程序
            const program = renderer.compileSync(fragment, vertex);
            renderer.useProgram(program);
            // 设置 uniform 变量,rows:表示每一行显示多少个网格。
            renderer.uniforms.rows = 64;
            // 将顶点数据送入缓冲区
            renderer.setMeshData([{
                // positions:顶点,这四个顶点坐标正好覆盖了整个 Canvas 画布
                positions: [
                    [-1, -1],
                    [-1, 1],
                    [1, 1],
                    [1, -1],
                ],
                attributes: {
                    // uv:纹理坐标,这个坐标系的左下角为 0,0,右上角为 1,1
                    uv: [
                        [0, 0],
                        [0, 1],
                        [1, 1],
                        [1, 0],
                    ],
                },
                // cells:顶点索引,这个矩形画布剖分成两个三角形,顶点下标分别是 (0, 1, 2) 和 (2, 0, 3)。
                cells: [[0, 1, 2], [2, 0, 3]],
            }]);
            // 渲染
            renderer.render();
        </script>
    </body>
</html>

顶点坐标和 uv(纹理)坐标:

b99be21be61f489788be5a147782ec6d.png


y = fract(x) 在整数区间内周期重复示意图:bf671fe39d004f5d86321272703b95b6.png

Step 函数:a4c76b604c0c4f51b969014f3e2a6a37.png


效果如下:


b93426867b7e4a7cbe8d6a3460dda180.png

如何绘制分形图案

一个分形图案可以划分成无数个部分,而每个部分的形状又都和这个图案整体具有相似性。

比如:自然界中的分形——罗马花椰菜


594fff406aee4a48bee38616ea3f6d66.png


分形公式(曼德勃罗特集):( Z n {Z}_{n} Zn 和 Z n + 1 {Z}_{n+1} Zn+1是复数,C 是一个实数常量。)

ad8e2bd83f034d6eabda885fe6b0cf69.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>如何绘制分形图案</title>
    <style>
        canvas {
            border: 1px dashed salmon;
        }
    </style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
        <script src="./common/lib/gl-renderer.js"></script>
        <script type="module">
            const canvas = document.querySelector('canvas');
            // 这里使用基础库 gl-renderer 库,它在 WebGL 底层的基础上进行了一些简单的封装
            const renderer = new GlRenderer(canvas);
            console.log(renderer);
            // 顶点着色器
            const vertex = `
                attribute vec2 a_vertexPosition;
                attribute vec2 uv;
                varying vec2 vUv;
                void main() {
                    gl_PointSize = 1.0;
                    vUv = uv;
                    gl_Position = vec4(a_vertexPosition, 1, 1);
                }
            `;
            // 曼德勃罗特集是无限迭代的,给一个足够精度的最大迭代次数,比如:65536
            const fragment = `
                #ifdef GL_ES
                precision mediump float;
                #endif
                varying vec2 vUv;
                uniform vec2 center;
                uniform float scale;
                uniform int iterations;
                vec2 f(vec2 z, vec2 c) {
                    return mat2(z, -z.y, z.x) * z + c;
                }
                vec3 palette(float t, vec3 c1, vec3 c2, vec3 c3, vec3 c4) {
                    float x = 1.0 / 3.0;
                    if (t < x) return mix(c1, c2, t/x);
                    else if (t < 2.0 * x) return mix(c2, c3, (t - x)/x);
                    else if (t < 3.0 * x) return mix(c3, c4, (t - 2.0*x)/x);
                    return c4;
                }
                void main() {
                    vec2 uv = vUv;
                    vec2 c = center + 4.0 * (uv - vec2(0.5)) / scale;
                    vec2 z = vec2(0.0);
                    bool escaped = false;
                    int j;
                    for (int i = 0; i < 65536; i++) {
                        if(i > iterations) break;
                        j = i;
                        z = f(z, c);
                        if (length(z) > 2.0) {
                            escaped = true;
                            break;
                        }
                    }
                    gl_FragColor.rgb = escaped ? 
                        max(
                            1.0, log(scale)) * palette(float(j)/ float(iterations),
                            vec3(0.02, 0.02, 0.03),
                            vec3(0.1, 0.2, 0.3),
                            vec3(0.0, 0.3, 0.2),
                            vec3(0.0, 0.5, 0.8)
                        ) : vec3(0.0);
                    gl_FragColor.a = 1.0;
                }
            `;
            // 加载片元着色器并创建程序
            const program = renderer.compileSync(fragment, vertex);
            renderer.useProgram(program);
            renderer.uniforms.center = [0.367, 0.303];
            renderer.uniforms.scale = 1;
            renderer.uniforms.iterations = 256;
            // 将顶点数据送入缓冲区
            renderer.setMeshData([{
                // positions:顶点,这四个顶点坐标正好覆盖了整个 Canvas 画布
                positions: [
                    [-1, -1],
                    [-1, 1],
                    [1, 1],
                    [1, -1],
                ],
                attributes: {
                    // uv:纹理坐标,这个坐标系的左下角为 0,0,右上角为 1,1
                    uv: [
                        [0, 0],
                        [0, 1],
                        [1, 1],
                        [1, 0],
                    ],
                },
                // cells:顶点索引,这个矩形画布剖分成两个三角形,顶点下标分别是 (0, 1, 2) 和 (2, 0, 3)。
                cells: [[0, 1, 2], [2, 0, 3]],
            }]);
            // 渲染
            renderer.render();
            function update() {
                const factor = Math.max(0.1, Math.log(renderer.uniforms.scale));
                renderer.uniforms.scale = (renderer.uniforms.scale += factor) % 10000;
                renderer.uniforms.iterations = factor * 500;
                requestAnimationFrame(update);
            }
            setTimeout(update, 2000);
        </script>
</body>
</html>

image.png



如何给图案增加随机效果

伪随机函数的原理是,取正弦函数偏后部的小数部分的值来模拟随机。如果我们传入一个确定的 st 值,它就会返回一个符合随机分布的确定的 float 值。


重复网格

可以用 floor 取整函数,来生成随机的色块。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>重复网格</title>
        <style>
            canvas {
                border: 1px dashed salmon;
            }
        </style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script src="./common/lib/gl-renderer.js"></script>
        <script>
            const vertex = `
                attribute vec2 a_vertexPosition;
                attribute vec2 uv;
                varying vec2 vUv;
                void main() {
                    gl_PointSize = 1.0;
                    vUv = uv;
                    gl_Position = vec4(a_vertexPosition, 1, 1);
                }
            `;
            const fragment = `
                #ifdef GL_ES
                precision highp float;
                #endif
                varying vec2 vUv;
                float random (vec2 st) {
                    return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
                }
                void main() {
                    vec2 st = vUv * 10.0;
                    gl_FragColor.rgb = vec3(random(floor(st)));
                    gl_FragColor.a = 1.0;
                }
            `;
            const canvas = document.querySelector("canvas");
            const renderer = new GlRenderer(canvas);
            // 加载片元着色器并创建程序
            const program = renderer.compileSync(fragment, vertex);
            renderer.useProgram(program);
            // 将顶点数据送入缓冲区
            renderer.setMeshData([
                {
                    positions: [
                        [-1, -1],
                        [-1, 1],
                        [1, 1],
                        [1, -1],
                    ],
                    attributes: {
                        uv: [
                            [0, 0],
                            [0, 1],
                            [1, 1],
                            [1, 0],
                        ],
                    },
                    cells: [
                        [0, 1, 2],
                        [2, 0, 3],
                    ],
                },
            ]);
            // 渲染
            renderer.render();
        </script>
    </body>
</html>

0c32049fb2a142d2add2f8a84782d502.png


网格移动

结合随机和动态效果的网格

image.png

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>结合随机和动态效果的网格</title>
        <style>
            canvas {
                border: 1px dashed salmon;
            }
        </style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script src="./common/lib/gl-renderer.js"></script>
        <script>
            const vertex = `
                attribute vec2 a_vertexPosition;
                attribute vec2 uv;
                varying vec2 vUv;
                void main() {
                    gl_PointSize = 1.0;
                    vUv = uv;
                    gl_Position = vec4(a_vertexPosition, 1, 1);
                }
            `;
            const fragment = `
                #ifdef GL_ES
                precision highp float;
                #endif
                varying vec2 vUv;
                uniform float uTime;
                float random (vec2 st) {
                    return fract(sin(dot(st.xy, vec2(12.9898,78.233)))* 43758.5453123);
                }
                void main() {
                    vec2 st = vUv * vec2(100.0, 50.0);
                    st.x -= (1.0 + 10.0 * random(vec2(floor(st.y)))) * uTime;
                    vec2 ipos = floor(st); // integer
                    vec2 fpos = fract(st); // fraction
                    vec3 color = vec3(step(random(ipos), 0.7));
                    color *= step(0.2,fpos.y);
                    gl_FragColor.rgb = color;
                    gl_FragColor.a = 1.0;
                }
            `;
            const canvas = document.querySelector("canvas");
            const renderer = new GlRenderer(canvas);
            // 加载片元着色器并创建程序
            const program = renderer.compileSync(fragment, vertex);
            renderer.useProgram(program);
            renderer.uniforms.uTime = 0.0;
            // 将顶点数据送入缓冲区
            renderer.setMeshData([
                {
                    positions: [
                        [-1, -1],
                        [-1, 1],
                        [1, 1],
                        [1, -1],
                    ],
                    attributes: {
                        uv: [
                            [0, 0],
                            [0, 1],
                            [1, 1],
                            [1, 0],
                        ],
                    },
                    cells: [
                        [0, 1, 2],
                        [2, 0, 3],
                    ],
                },
            ]);
            // 渲染
            renderer.render();
            requestAnimationFrame(function update(t) {
                renderer.uniforms.uTime = 4 * t / 1000;
                requestAnimationFrame(update);
            });
        </script>
    </body>
</html>



迷宫

在 Shader 中用 smoothstep 函数生成可以随机旋转方向的线段生成迷宫。


image.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>迷宫</title>
    <style>
        canvas {
            border: 1px dashed salmon;
        }
    </style>
</head>
<body>
    <canvas width="512" height="512"></canvas>
        <script src="./common/lib/gl-renderer.js"></script>
        <script>
            const vertex = `
                attribute vec2 a_vertexPosition;
                attribute vec2 uv;
                varying vec2 vUv;
                void main() {
                    gl_PointSize = 1.0;
                    vUv = uv;
                    gl_Position = vec4(a_vertexPosition, 1, 1);
                }
            `;
            const fragment = `
                #ifdef GL_ES
                precision mediump float;
                #endif
                #define PI 3.14159265358979323846
                varying vec2 vUv;
                uniform vec2 u_resolution;
                uniform int rows;
                float random (in vec2 _st) {
                    return fract(sin(dot(_st.xy, vec2(12.9898,78.233))) * 43758.5453123);
                }
                vec2 truchetPattern(in vec2 _st, in float _index){
                    _index = fract(((_index-0.5)*2.0));
                    if (_index > 0.75) {
                        _st = vec2(1.0) - _st;
                    } else if (_index > 0.5) {
                        _st = vec2(1.0-_st.x,_st.y);
                    } else if (_index > 0.25) {
                        _st = 1.0-vec2(1.0-_st.x,_st.y);
                    }
                    return _st;
                }
                void main() {
                    vec2 st = vUv * float(rows);
                    vec2 ipos = floor(st);  // integer
                    vec2 fpos = fract(st);  // fraction
                    vec2 tile = truchetPattern(fpos, random( ipos ));
                    float color = 0.0;
                    color = smoothstep(tile.x-0.3,tile.x,tile.y) - smoothstep(tile.x,tile.x+0.3,tile.y);
                    gl_FragColor = vec4(vec3(color),1.0);
                }
            `;
            const canvas = document.querySelector("canvas");
            const renderer = new GlRenderer(canvas);
            // 加载片元着色器并创建程序
            const program = renderer.compileSync(fragment, vertex);
            renderer.useProgram(program);
            renderer.uniforms.rows = 20;
            // 将顶点数据送入缓冲区
            renderer.setMeshData([
                {
                    positions: [
                        [-1, -1],
                        [-1, 1],
                        [1, 1],
                        [1, -1],
                    ],
                    attributes: {
                        uv: [
                            [0, 0],
                            [0, 1],
                            [1, 1],
                            [1, 0],
                        ],
                    },
                    cells: [
                        [0, 1, 2],
                        [2, 0, 3],
                    ],
                },
            ]);
            // 渲染
            renderer.render();
        </script>
</body>
</html>



【The Book of Shaders】

【The Book of Shaders】

这是一本关于 Fragment Shaders(片段着色器)的入门指南,它将一步一步地带你领略其中的纷繁与抽象。


比如上面提到的 smoothstep 函数


d6da91ea8e0c49bcba85585ea7a45653.png











目录
相关文章
|
3月前
|
移动开发 安全 搜索推荐
深度链接(Deep Link)实战指南:何时做?如何做?一篇讲透
本文探讨了 App 是否需要实现深度链接的问题,从产品复杂度、营销需求和用户旅程三个维度分析,帮助判断深度链接的最佳接入时机,并提供实操建议。
144 0
|
自然语言处理 搜索推荐 数据挖掘
自然语言处理(NLP)技术对教育领域产生了深远的影响
【7月更文挑战第29天】自然语言处理(NLP)技术对教育领域产生了深远的影响
597 14
|
数据采集 机器学习/深度学习 存储
基于多项式朴素贝叶斯的中文垃圾邮件识别
基于多项式朴素贝叶斯的中文垃圾邮件识别
|
存储 算法 NoSQL
数据结构和算法——哈希查找冲突处理方法(开放地址法-线性探测、平方探测、双散列探测、再散列,分离链接法)
数据结构和算法——哈希查找冲突处理方法(开放地址法-线性探测、平方探测、双散列探测、再散列,分离链接法)
669 1
|
11月前
|
供应链 数据可视化 安全
Leangoo 化解医药行业痛点,助力各领域发展
Leangoo 针对医药行业痛点,如制药研发周期长、生物技术创新协同难等问题,提供解决方案。通过看板功能实现信息共享、流程优化和协同增效,提升各领域效率与质量,促进医药行业整体进步。覆盖制药、生物技术、医疗器械及医疗保健服务四大领域,助力项目管理、供应链协调、市场推广及服务质量提升。
|
12月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
182 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
Java Android开发 C++
Kotlin vs Java:选择最佳语言进行安卓开发
【4月更文挑战第13天】Java曾是安卓开发的主流语言,但Kotlin的崛起改变了这一局面。Google在2017年支持Kotlin,引发两者优劣讨论。Java以其成熟稳定、强大生态和跨平台能力占优,但代码冗长、开发效率低和语言特性过时是短板。Kotlin则以简洁语法、空安全设计和高度兼容Java脱颖而出,但社区和生态系统仍在发展中,可能存在学习曲线和性能问题。选择语言应考虑项目需求、团队熟悉度、维护性、性能和生态系统。无论选择哪种,理解其差异并适应新技术至关重要。
737 4
|
SQL 大数据 数据挖掘
深入解析力扣178题:分数排名(DENSE_RANK详解及模拟面试问答)
深入解析力扣178题:分数排名(DENSE_RANK详解及模拟面试问答)
|
设计模式 Java 开发者
Java一分钟之-Swing组件:JTable, JTree, JTextArea
本文介绍了Java Swing的三个关键组件:`JTable`、`JTree`和`JTextArea`,用于数据展示和用户输入。`JTable`展示二维数据,如表格;`JTree`展示层次结构数据,如文件系统;`JTextArea`则用于多行文本输入和显示。每个组件都提供了示例代码,并列出常见问题及避免方法,如数据源未设置、滚动面板缺失等。理解并掌握这些组件,能帮助开发者创建高效用户界面。
325 0
|
SQL 数据库连接 数据库
ODBC配置数据源及相关问题(“找不到工程和库”“实时错误91对象变量或with块变量未设置”等)
ODBC配置数据源及相关问题(“找不到工程和库”“实时错误91对象变量或with块变量未设置”等)