webgl学习笔记(二)

856 阅读8分钟

一、用JS控制一个点的位置

attribute变量的概念

attribute变量是只有顶点着色器才能使用,JS可以通过attribute变量向顶点着色器传递与顶点相关的数据。

JS向attribute变量传参的步骤

1、在顶点着色器中声明attribute变量。

<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    void main(){
        gl_Position = a_Position;
        gl_PointSize = 50.0;
    }
</script>
  • attribute是存储限定符,是专门用于向外部导出与点位相关的对象的,类似es6中的export。

  • vec4是变量类型,vec4是四维矢量对象

  • a_Position是变量名,是一个指针,指向实际存储数据的位置。就是说,在着色器外部修改了a_Position指向的实际数据,那么着色器中a_Position所对应的数据也会修改。

2、在JS中获取attribute变量

const a_Position=gl.getAttribLocation(gl.program,'a_Position');

在JS中不能直接写a_Position来获取着色器中的变量。因为着色器和JS是两个不同的语种,着色器无法通过window.a_Position原理向全局暴露变量。

要在JS中获取着色器暴露的变量,要使用程序对象。

  • gl是webgl上下文对象。
  • gl.getAttribLocation()是获取着色器中attribute变量的方法。其参数为:
    • gl.program是初始化着色器时,在上下文对象中挂载的程序对象。    
    • 'a_Position'是着色器暴露出的变量名

这个过程就是:gl 上下文对象对program 程序对象说,你去顶点着色器里找一个名叫'a_Position' 的attribute变量。

3、修改attribute变量

gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);

在JS中通过getAttribLocation方法获取了attribute变量,但是此变量是GLSL ES语言的,不能使用JS的语法来修改它。

必须使用特定的方法vertexAttrib3f来改变。

  • gl.vertexAttrib3f()是改变变量值的方法。其参数:
    • a_Position是前面获取的着色器变量。
    • 后面三个参数是顶点的x、y、z位置

整体代码:

<canvas id="canvas"></canvas>
<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    void main(){
        gl_Position = a_Position;
        gl_PointSize = 50.0;
    }
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
    void main() {
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
    }
</script>
<script type="module">
    import {initShaders} from '../jsm/Utils.js';

    const canvas = document.getElementById('canvas');
    canvas.width=window.innerWidth;
    canvas.height=window.innerHeight;
    const gl = canvas.getContext('webgl');
    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;
    initShaders(gl, vsSource, fsSource);
    const a_Position=gl.getAttribLocation(gl.program,'a_Position');
    gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.POINTS, 0, 1);
</script>

扩展

vertexAttrib3f的同族函数

gl.vertexAttrib3f(location,v0,v1,v2) 方法是一系列修改着色器中的attribute 变量的方法之一,它还有许多同族方法,如:

gl.vertexAttrib1f(location,v0) 
gl.vertexAttrib2f(location,v0,v1)
gl.vertexAttrib3f(location,v0,v1,v2)
gl.vertexAttrib4f(location,v0,v1,v2,v3)

比如 vertexAttrib1f() 方法自定一个矢量对象的v0值,v1、v2 则默认为0.0,v3默认为1.0,其数值类型为float 浮点型

webgl函数的命名规律

GLSL ES里函数的命名结构是: <基础函数名><参数个数><参数类型>

以vertexAttrib3f为例:

  • vertexAttrib 基础函数名
  • 3 参数个数 3个
  • f 浮点型 i 整型 v 数字

二、用鼠标控制点位

获取鼠标在webgl坐标系中的位置

canvas.addEventListener('click',function(event){
    const {clientX,clientY}=event;
    const {left,top}=canvas.getBoundingClientRect();
    const [cssX,cssY]=[
        clientX-left,
        clientY-top
    ];
})

canvas坐标系转为webgl坐标系

  • 解决坐标原点位置的差异
const [halfWidth,halfHeight]=[width/2,height/2];// canvas画布中心位置
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];// 用鼠标位减去canvas画布的中心位,得到的就是鼠标基于画布中心的位置
  • 解决y方向的差异
const yBaseCenterTop=-yBaseCenter;// 因为webgl里的y轴和canvas 2D里的y轴相反,所以这里取负值
  • 解决坐标基底的差异
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight]

由于canvas 2d的坐标基底中的两个分量分别是一个像素的宽高,而webgl的坐标基底的两个分量是画布的宽高,所以要求个比值。

修改attribute变量

步骤:

  • 获取attribute变量
  • 在获取鼠标在webgl画布中的位置的时候,修改attribute变量
  • 清理画布
  • 绘图
 <canvas id="canvas"></canvas>
  <script id="vertexShader" type="x-shader/s-vertex">
    attribute vec4 a_Position;
    void main(){
      gl_Position = a_Position;
      gl_PointSize = 50.0;
    }
  </script>
  <script id="fragmentShader" type='x-shader/x-fragment'>
   void main(){
     gl_FragColor = vec4(1.0,1.0,0.0,1.0);
   }
 </script>
  <script type="module">
    import { initShaders } from './../001/jsm/Utils.js'
    const canvas = document.getElementById('canvas');

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const gl = canvas.getContext('webgl');

    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;

    initShaders(gl,vsSource,fsSource);
    const a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    canvas.addEventListener('click',(event)=>{
      const { clientX ,clientY} = event;
      const {left,top,width,height} = canvas.getBoundingClientRect();
      const [cssX,cssY] = [clientX-left,clientY-top];
      const [halfWidth,halfHeight] = [width/2,height/2];
      const [xBaseCenter,yBaseCenter] = [cssX-halfWidth,cssY-halfHeight];
      const yBaseCenterTop = -yBaseCenter;
      const [x,y] = [xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
      console.log(x,y);
      gl.vertexAttrib2f(a_Position,x,y);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.POINTS,0,1)
    })
    gl.drawArrays(gl.POINTS,0,1);
  </script>

webgl的同步绘图原理

上面的例子中,每点击一次,都会画出一个点,而上一次画的会消失。

gl.drawArrays(gl.POINT,0,1)方法和canvas 2d中的ctx.draw()方法不同,ctx.draw()真的像画画一样,一层一层的覆盖图像。

gl.drawArrays()方法只会同步绘图,走完了JS主线程后,再次绘图时,就会从头再来。就是说,异步执行的gl.drawArrays方法会把画布上的图像都刷掉。

解决方案:

用数组将一开始的顶点存起来,在异步绘制的时候,再一起画。

  <canvas id="canvas"></canvas>
  <script id="vertexShader" type="x-shader/s-vertex">
    attribute vec4 a_Position;
    void main(){
      gl_Position = a_Position;
      gl_PointSize = 50.0;
    }
  </script>
  <script id="fragmentShader" type='x-shader/x-fragment'>
   void main(){
     gl_FragColor = vec4(1.0,1.0,0.0,1.0);
   }
 </script>
  <script type="module">
    import { initShaders } from './../001/jsm/Utils.js'
    const canvas = document.getElementById('canvas');

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const gl = canvas.getContext('webgl');

    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;

    initShaders(gl,vsSource,fsSource);
    const a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const g_points = [] // 点的集合
    canvas.addEventListener('click',(event)=>{
      const { clientX ,clientY} = event;
      const {left,top,width,height} = canvas.getBoundingClientRect();
      const [cssX,cssY] = [clientX-left,clientY-top];
      const [halfWidth,halfHeight] = [width/2,height/2];
      const [xBaseCenter,yBaseCenter] = [cssX-halfWidth,cssY-halfHeight];
      const yBaseCenterTop = -yBaseCenter;
      const [x,y] = [xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
      g_points.push({x,y});
      gl.clear(gl.COLOR_BUFFER_BIT);
      g_points.forEach(({x,y})=>{
        gl.vertexAttrib2f(a_Position,x,y);
        gl.drawArrays(gl.POINTS,0,1)
      })
    })
  </script>

总结:

webgl 的同步绘图的现象,其实是由webgl 底层内置的颜色缓冲区导致的。

“胸有成竹”大家知道吧?这个颜色缓冲区就是“胸有成竹”的胸,它在电脑里会占用一块内存。在我们使用webgl 绘图的时候,是先在颜色缓冲区中画出来,这样的图像还在胸中,所以外人看不见,只有webgl系统自己知道。

在我们想要将图像显示出来的时候,那就照着颜色缓冲区中的图像去画,这个步骤是webgl 内部自动完成的,我们只要执行绘图命令即可。

颜色缓冲区中存储的图像,只在当前线程有效。比如我们先在js 主线程中绘图,主线程结束后,会再去执行信息队列里的异步线程。在执行异步线程时,颜色缓冲区就会被webgl 系统重置,我们曾经在主线程里的“胸有成竹”也就没了,既然没了,也就画不出那时的图像了。

三、控制顶点尺寸

1、在着色器里暴露出一个可以控制顶点尺寸的attribute变量。

  <script id="vertexShader" type="x-shader/s-vertex">
    attribute vec4 a_Position;
    attribute float a_PointSize; // 定义变量
    void main(){
      gl_Position = a_Position;
      gl_PointSize = a_PointSize;
    }
  </script>

2、在JS中获取attribute变量

 const a_PointSize = gl.getAttribLocation(gl.program,'a_PointSize'); 

3、修改attribute变量

gl.vertexAttrib1f(a_PointSize,100.0);

整体代码:

<body>
  <canvas id="canvas"></canvas>
  <script id="vertexShader" type="x-shader/s-vertex">
    attribute vec4 a_Position;
    attribute float a_PointSize; // 定义变量
    void main(){
      gl_Position = a_Position;
      gl_PointSize = a_PointSize;
    }
  </script>
  <script id="fragmentShader" type='x-shader/x-fragment'>
   void main(){
     gl_FragColor = vec4(1.0,1.0,0.0,1.0);
   }
 </script>
  <script type="module">
    import { initShaders } from './../001/jsm/Utils.js'
    const canvas = document.getElementById('canvas');

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const gl = canvas.getContext('webgl');

    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;

    initShaders(gl,vsSource,fsSource);
    const a_Position = gl.getAttribLocation(gl.program,'a_Position');
    const a_PointSize = gl.getAttribLocation(gl.program,'a_PointSize');
    // gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const g_points = [] // 点的集合
    canvas.addEventListener('click',(event)=>{
      const { clientX ,clientY} = event;
      const {left,top,width,height} = canvas.getBoundingClientRect();
      const [cssX,cssY] = [clientX-left,clientY-top];
      const [halfWidth,halfHeight] = [width/2,height/2];
      const [xBaseCenter,yBaseCenter] = [cssX-halfWidth,cssY-halfHeight];
      const yBaseCenterTop = -yBaseCenter;
      const [x,y] = [xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
      g_points.push({x,y,size:Math.random()*100});
      gl.clear(gl.COLOR_BUFFER_BIT);
      g_points.forEach(({x,y,size})=>{
        gl.vertexAttrib2f(a_Position,x,y);
        gl.vertexAttrib1f(a_PointSize,size);
        gl.drawArrays(gl.POINTS,0,1)
      })
    })
  </script>
</body>

四、控制顶点的颜色

限定颜色变量的限定符是uniform。

步骤:

1、在片元着色器里把控制顶点颜色的变量暴露出来。

 <script id="fragmentShader" type='x-shader/x-fragment'>
    precision mediump float;// 对浮点数精度的定义
    uniform vec4 u_FragColor;//uniform 是限定符 vec4是4维的变量类型 
    void main(){
      gl_FragColor = u_FragColor;
    }
 </script>

2、在JS中获取片元着色器暴露的uniform变量

 const u_FragColor = gl.getUniformLocation(gl.program,'u_FragColor');

getUniformLocation方法用于获取片元着色器暴露出来的uniform变量,第一个参数是程序对象,第二个参数是变量名。

3、修改uniform变量

gl.uniform4fv(u_FragColor,1.0,1.0,0.0,1.0);

uniform4fv方法可以一个一个写参数,如上面示例。也可以传递类型数组。

  • 4 是有4个数据,f是float类型 v 是矢量
gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
//等同于
const color=new Float32Array([1.0,1.0,0.0,1.0]);
gl.uniform4fv(u_FragColor,color);
  • uniform4fv的同族方法 4可以为1,2,3
  • uniform4fv的第二个参数必须是Float32Array数组,不能是普通的Array对象。

Float32Array是一种32位的浮点型数组,他在浏览器中的运行效率比普通的Array高很多。

完整代码

<body>
  <canvas id="canvas"></canvas>
  <script id="vertexShader" type="x-shader/s-vertex">
    attribute vec4 a_Position;
    attribute float a_PointSize; // 定义变量
    void main(){
      gl_Position = a_Position;
      gl_PointSize = a_PointSize;
    }
  </script>
  <script id="fragmentShader" type='x-shader/x-fragment'>
    precision mediump float;
    uniform vec4 u_FragColor;
    void main(){
      gl_FragColor = u_FragColor;
    }
 </script>
  <script type="module">
    import { initShaders } from './../001/jsm/Utils.js'
    const canvas = document.getElementById('canvas');

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const gl = canvas.getContext('webgl');

    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;

    initShaders(gl,vsSource,fsSource);
    const a_Position = gl.getAttribLocation(gl.program,'a_Position');
    const a_PointSize = gl.getAttribLocation(gl.program,'a_PointSize');
    const u_FragColor = gl.getUniformLocation(gl.program,'u_FragColor');

    // gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const g_points = [] // 点的集合
    canvas.addEventListener('click',(event)=>{
      const { clientX ,clientY} = event;
      const {left,top,width,height} = canvas.getBoundingClientRect();
      const [cssX,cssY] = [clientX-left,clientY-top];
      const [halfWidth,halfHeight] = [width/2,height/2];
      const [xBaseCenter,yBaseCenter] = [cssX-halfWidth,cssY-halfHeight];
      const yBaseCenterTop = -yBaseCenter;
      const [x,y] = [xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
      const color = new Float32Array([
        Math.random(),
        Math.random(),
        Math.random(),
        1.0
      ]);
      console.log(color);
      g_points.push({x,y,size:Math.random()*50,color});
      gl.clear(gl.COLOR_BUFFER_BIT);
      g_points.forEach(({x,y,size,color})=>{
        gl.vertexAttrib2f(a_Position,x,y);
        gl.vertexAttrib1f(a_PointSize,size);
        gl.uniform4fv(u_FragColor,color);
        gl.drawArrays(gl.POINTS,0,1)
      })
    })
  </script>
</body>

五、绘制圆形的顶点

 <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>
  • distance(p1,p2)计算两个点位的距离

  • gl_PointCoord片元在一个点中的位置,此位置是被归一化的。

  • discard丢弃

着色器语法参考

六、案例: 用鼠标绘制星空

1、绘制随机透明度的星星

先给canvas一个星空背景图。

#canvas {
    background: url("./images/sky.jpg");
    background-size: cover;
    background-position: right bottom;
}

刷底色的时候给一个透明的底色。这样就能看见canvas的css背景了。

gl.clearColor(0, 0, 0, 0);

随机透明度颜色:

const arr = new Float32Array([0.87, 0.91, 1, a]);
gl.uniform4fv(u_FragColor, arr);

开启片元的颜色合成功能。

gl.enable(gl.BLEND)

设置片元的合成方式。

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

2、制作闪烁的繁星

补间动画涉及的几个概念:

  • 合成:多个时间轨的集合
  • 时间轨:通过关键帧,对其中的目标对象的状态进行插值计算
  • 补间动画:通过两个关键帧,对一个对象在这两个关键帧之间的状态进行插值计算,从而实现这个对象在两个关键帧间的平滑过渡

1、建立合成对象

export default class Compose{
  constructor(){
    this.parent = null // 父对象, 合成对象可以相互嵌套
    this.children = [] // 子对象集合,其集合元素可以是时间轨  也可以是合成对象
  }
  // 添加子对象
  add(obj){
    obj.parent = this;
    this.children.push(obj)
  }
  // 基于当前时间更新子对象状态的方法
  update(t){
    this.children.forEach(ele => {
      ele.update(t)
    })
  }
}

2、建立时间轨

export default class Track{
  constructor(target){
    this.target = target; // 时间轨上的目标对象
    this.parent = null; // 父对象,只能算是合成对象
    this.start = 0; //  起始时间 即 时间轨的建立时间
    this.timeLen = 5; //  时间轨总长
    this.loop = false; //  是否循环
    this.keyMap = new Map() //  关键帧集合
  }
  // 基于当前时间更新目标对象的状态
  // 先计算本地时间,即世界时间相对于时间轨起始时间的时间
  // 若时间轨循环播放,则本地时间基于时间轨长度取余
  // 遍历关键帧集合
  // 若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态
  // 若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态
  // 否则,计算本地时间在左右两个关键帧之间对应的补间状态
  update(t){
    const {start,target,timeLen,loop,keyMap} = this;
    // 本地时间
    let time = t - this.start;
    if(loop){
      time = time % timeLen
    }
    for(const [k,fms] of keyMap){
      const last = fms.length - 1;
      if(time< fms[0][0]){
        target[k] = fms[0][1]
      }else if(time>fms[last][0]){
        target[k] = fms[last][1]
      }else{
        target[k] = getValBetweenFms(time,fms,last)
      }
    }
  }
}

keyMap 关键帧集合,结构如下:

[
	[
		'对象属性1',
		[
			[时间1,属性值], //关键帧
			[时间2,属性值], //关键帧
		]
	],
	[
		'对象属性2',
		[
			[时间1,属性值], //关键帧
			[时间2,属性值], //关键帧
		]
	],
]

3、获取两个关键帧之间补间状态的方法

/** 获取两个关键帧之间补间状态的方法
* @time 本地时间
* @ms 某个属性的关键帧集合
* @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]-fm[1]
      }
      const k = delta.y/delta.x
      const b = fm1[1] - fm1[0] * k
      return k*time + b
    }
  }
}

使用合成对象和轨道对象制作补间动画

1、建立动画相关的对象

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}
    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)
})

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

!(function ani(){
    compose.update(new Date())
    render()
    requestAnimationFrame(ani)
})()

渲染方法:

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);
    })
}

3、配点背景音乐

#audio{
    position: absolute;
    right: 20px;
    bottom: 20px;
    opacity: 10%;
    transition: opacity 200ms;
    z-index: 20;
}
#audio:hover{
	opacity: 90%;
}
<audio id="audio" controls loop autoplay>
    <source src="./audio/cef.mp3" type="audio/mpeg">
</audio>