• WebGL笔记:WebGL中绘制圆点,设定透明度,渲染动画


    WebGL 绘制圆点

    • 基于片元着色器来画圆形
    • 片元着色器在屏幕中画图是基于一个个的像素的
    • 每次画一个像素时,都会执行片元着色器中的main方法
    • 那么,我们就可以从这一堆片元中(n个像素点)找出属于圆形的部分
    • 片元的位置叫做 gl_PointCoord (一个点中片元的坐标位)
      • 比如,一个点的宽高都是1 , 片元的 x,y 位置在 0 - 1 之间
      • 点的中心点的坐标位置是(0.5, 0.5), 如果片元到中心的位置 小于 0.5
      • 那么可以认为这个片元是在圆内的,这样,只渲染圆内的片元,圆外的片元就不再渲染
    • 文档:
      • https://registry.khronos.org/OpenGL-Refpages/gl4/html/gl_PointCoord.xhtml
      • https://registry.khronos.org/OpenGL-Refpages/gl4/
    <script id="fragmentShader" type="x-shader/x-fragment">
        precision mediump float;
        uniform vec4 u_FragColor;
        void main() {
            float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
            if(dist < 0.5) {
                gl_FragColor = u_FragColor;
            } else {
                discard;
            }
        }
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • distance 是计算两个点之间距离的函数
    • discard 丢弃,即不会一个片元进行渲染
    • 其他参考上篇文章,来画出圆形

    WebGL 中与CSS配合展示背景图

    • 在 css 中设置背景图
      #canvas {
          background: url("./bg.jpg");
          background-size: cover;
          background-position: right bottom;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 在 js 刷底色的时候, 给一个透明色, 这样才能看见canvas的css背景
      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      • 1
      • 2

    WebGL中片元透明度的设定

    • 一般来说,绘制图形,让图形有一定的颜色,并且有一定的透明度,光是使用 uniform4fv 设置是不行的
    • 还需要做两件事:
      gl.enable(gl.BLEND); // 开启片元的颜色合成功能
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // 设置片元的合成方式
      
      • 1
      • 2
    • 之后正常使用uniform4fv进行渲染即可
      
      const arr = new Float32Array([0.87, 0.91, 1, a]);
      gl.uniform4fv(u_FragColor, arr);
      
      • 1
      • 2
      • 3

    WebGL中设置图形的动画

    • 所谓的动画,就是一帧一帧的图像变化累计的结果,如果我们想要使用代码实现动画,还需要了解一下概念
      • 关键帧:可以说这是动画中的里程碑,连续两个关键帧之间,我们可以用数学来实现渐变
      • 时间轨:一次动画执行完成所需要的时间轨道,包含不同的关键帧,通过关键帧,对其中目标对象的状态进行插值计算
      • 补间动画:一个物体不同关键帧,即连续相邻的两个关键帧之间的状态进行差值运算,得到当前过渡时间的不同画面,来实现平滑过渡
      • 合成:不同物体所表示的多个时间轨的集合
    • 对动画进行封装
      • 框架层面,我们要针对合成对象做设计:画布上可能会有多个对象做动画运动
      • 运动层面,我们要对时间轨做计算:针对每个对象,在一个完整动画的时间轨上应如何补全关键帧之间的间隔渲染

    1 )框架层面的设计

    export default class Compose {
        constructor() {
            this.parent = null // 当前对象parent置空,这是一个编程习惯
            this.children = [] // 用于存储多个对象
        }
        add(obj) {
            obj.parent = this // 当前对象和当前工具建立关系
            this.children.push(obj) // 将当前对象加入子队列数组
        }
        // 这里是工具的update
        update(t) {
            // 内部调用每个对象的update方法实现动画
            this.children.forEach(ele => {
                ele.update(t)
            })
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2 )时间轨的设计

    export default class Track {
        constructor(target) {
            this.target = target // 当前对象
            this.parent = null // 父对象,合成对象,默认为空
            this.start = 0 // 开始时间默认是0
            this.timeLen = 5 // 一个时间轨的长度,也就是完成一次完整动画所需要的时间,单位毫秒
            this.loop = false // 是否循环播放动画
            this.keyMap = new Map() // 关键帧的集合
        }
        // 当前对象的每一帧运动函数
        update(t) {
            const { keyMap, timeLen, target, loop } = this
            let time = t - this.start // 当前时间距离开始时间的时间长度
            // 如果开启循环,则加入取余操作,将当前时间循环递增,不让超过自身设定
            if(loop) {
                time = time % timeLen
            }
            for(const [key,fms] of keyMap.entries()) {
                const last = fms.length - 1 // 最后一项
                if(time < fms[0][0]) {
                    // 在第一个关键帧之前的设置为第一个关键帧的状态
                    target[key] = fms[0][1]
                } else if(time > fms[last][0]) {
                    // 时间在最后一个关键帧之后设定为最后一个关键帧的状态
                    target[key] = fms[last][1]
                } else {
                    // 在各个中间态实行数学计算状态渐变,即:补间状态
                    target[key] = getValBetweenFms(time, fms, last)
                }
            }
        }
    }
    
    // 补间状态计算函数
    function getValBetweenFms(time,fms,last) {
        for(let i = 0; i < last; i++) {
            const fm1 = fms[i]
            const fm2 = fms[i+1]
            if(time >= fm1[0] && time <= fm2[0]) {
                const delta = {
                    x: fm2[0] - fm1[0],
                    y: fm2[1] - fm1[1],
                }
                const k = delta.y / delta.x
                const b = fm1[1] - fm1[0] * k
                return k * time + b
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 上述 keyMap 关键帧集合,这里是一个对象的关键帧集合,结构如下,
      [
        [
          '对象属性1',
          [
            [时间1,属性值], //关键帧
            [时间2,属性值], //关键帧
          ]
        ],
        [
          '对象属性2',
          [
            [时间1,属性值], //关键帧
            [时间2,属性值], //关键帧
          ]
        ],
      ]
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    • time 当前时间
    • fms 某个属性的关键帧集合
    • last 最后一个关键帧的索引位置
    • 其实现思路如下
      • 遍历所有关键帧
      • 判断当前时间在哪两个关键帧之间
      • 基于这两个关键帧的时间和状态,求点斜式
      • 基于点斜式求本地时间对应的状态
    • 这里 y = kx + b 这个公式是一般公式(推荐),还可以用其他曲线公式来处理动画

    3 )应用

    const compose = new Compose()
    const stars = [] // 点数据的集合
    canvas.addEventListener('click', function(event) {
        const { x, y } = getPosByMouse(event,canvas) // 获取当前坐标,这里具体实现可看之前博客代码,只是做了个函数封装
        const a = 1
        const s = Math.random() * 5 + 2
        const obj = { x, y, s, a } // x坐标,y坐标,s尺寸,a透明度
        stars.push(obj)
    
        const track = new Track(obj)
        track.start = new Date()
        track.keyMap = new Map([
            ['a', [
                [500, a],
                [1000, 0],
                [1500, a],
            ]]
        ])
        track.timeLen = 2000
        track.loop = true
        compose.add(track)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    渲染方法如下,参考之前博客代码

    function render(){
        gl.clear(gl.COLOR_BUFFER_BIT);
        stars.forEach(({x,y,s,a}) => {
            gl.vertexAttrib2f(a_Position,x,y);
            gl.vertexAttrib1f(a_PointSize,s);
            gl.uniform4fv(u_FragColor, new Float32Array([0.87,0.92,1, a]));
            gl.drawArrays(gl.POINTS, 0, 1);
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    用请求动画帧驱动动画,连续更新数据,渲染视图

    !(function ani() {
        compose.update(new Date())
        render()
        requestAnimationFrame(ani) // 重复执行
    })()
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    【编程英语】Python常用英语单词
    计算机专业毕设课设选题攻略
    面经积累---持续更新
    宝塔安装BounceStudio扩展
    LabVIEW在不同平台之间移植VI
    Linux环境的Windows子系统
    【算法挨揍日记】day08——30. 串联所有单词的子串、76. 最小覆盖子串
    ES 架构及基础 - 1
    阿里云服务器部署Web环境
    如何书写一篇好的博客?
  • 原文地址:https://blog.csdn.net/Tyro_java/article/details/133391742