当前位置: 首页 > news >正文

webgl入门-js与着色器间的数据传输

js与着色器间的数据传输

前言

课堂目标

  1. 使用js向着色器传递数据
  2. 获取鼠标在canvas 中的webgl 坐标系位置

知识点

  1. attribute 变量
  2. gl.vertextAttribute3f() 的同族函数
  3. 鼠标在canvas 中的css 位置转webgl 坐标位
  4. uniform 变量
  5. gl.uniform4f() 的同族函数

第一章 用js控制一个点的位置

1-attribute 变量的概念。

回顾一下我们上一篇中点的定位:

gl_Position = vec4(0,0,0,1);

这是一种将数据写死了的硬编码,缺乏可扩展性。

我们要让这个点位可以动态改变,那就得把它变成attribute变量。

attribute 变量是只有顶点着色器才能使用它的。

js 可以通过attribute 变量向顶点着色器传递与顶点相关的数据。

2-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>
  1. 在js中获取attribute 变量
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
  1. 修改attribute 变量
gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);

整体代码

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

接下来详细解释一下。

3-js向attribute 变量传参的原理

3-1-着色器中的attribute 变量
attribute vec4 a_Position;
void main(){gl_Position = a_Position;gl_PointSize = 50.0;
}
  • attribute 是存储限定符,是专门用于向外部导出与点位相关的对象的,这类似于es6模板语法中export 。
  • vec4 是变量类型,vec4是4维矢量对象。
  • a_Position 是变量名,之后在js中会根据这个变量名导入变量。这个变量名是一个指针,指向实际数据的存储位置。也是说,我们如果在着色器外部改变了a_Position所指向的实际数据,那么在着色器中a_Position 所对应的数据也会修改。

接下来,咱们说一下在js 里如何获取attribute 变量。

3-2-在js中获取attribute 变量

我们在js 里不能直接写a_Position 来获取着色器中的变量。

因为着色器和js 是两个不同的语种,着色器无法通过window.a_Position 原理向全局暴露变量。

那我们要在js 里获取着色器暴露的变量,就需要找人来翻译,这个人就是程序对象。

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

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

现在a_Position变量有了,接下来就可以对它赋值了。

3-3-在js中修改attribute 变量

attribute 变量即使在js中获取了,他也是一个只会说GLSL ES语言的人,他不认识js 语言,所以我们不能用js 的语法来修改attribute 变量的值:

a_Position.a=1.0

我们得用特定的方法改变a_Position的值:

gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);
  • gl.vertexAttrib3f() 是改变变量值的方法。
  • gl.vertexAttrib3f() 方法的参数中:
    • a_Position 就是咱们之前获取的着色器变量。
    • 后面的3个参数是顶点的x、y、z位置

a_Position被修改后,我们就可以使用上下文对象绘制最新的点位了。

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

4-扩展

4-1-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)

它们都可以改变attribute 变量的前n 个值。

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

4-2-webgl 函数的命名规律

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

以vertexAttrib3f(location,v0,v1,v2,v3) 为例:

  • vertexAttrib:基础函数名
  • 3:参数个数,这里的参数个数是要传给变量的参数个数,而不是当前函数的参数个数
  • f:参数类型,f 代表float 浮点类型,除此之外还有i 代表整型,v代表数字……

关于用js 控制点位的方法咱们就说到这,接下咱们说一个用鼠标控制点位的例子。

第二章 用鼠标控制点位

我们要用鼠标控制一个点的位置,首先要知道鼠标点在webgl 坐标系中的位置,这样才能让一个点出现在我们鼠标点击的位置。

接下来咱们就说一下如何获取鼠标点在webgl 坐标系中的位置。

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

对于鼠标点在webgl 坐标系中的位置,我们是无法直接获取的。所以我们得先获取鼠标在canvas 这个DOM元素中的位置。

1-1-获取鼠标在canvas 画布中的css 位置
canvas.addEventListener('click',function(event){const {clientX,clientY}=event;const {left,top}=canvas.getBoundingClientRect();const [cssX,cssY]=[clientX-left,clientY-top];
})

对于cssX,cssY 的获取,大家应该都不陌生,这在canvas 2d 也会用到。

我们可以用向量减法来求解。

image-20210228111229154

已知:向量a(clientX,clientY),向量b(left,top)

求:向量c

解:

由向量的减法得:向量a减向量c,等于以向量c 的终点为起点,以向量a的终点为终点的向量c

所以:向量c=a-b=(clientX-left,clientY-top)

将向量c 视之为坐标点c,那点c 就是鼠标在canvas 画布中的css 位。

因为html 坐标系中的坐标原点和轴向与canvas 2d是一致的,所以在我们没有用css 改变画布大小,也没有对其坐标系做变换的情况下,鼠标点在canvas 画布中的css 位就是鼠标点在canvas 2d坐标系中的位置。

2-2-canvas 坐标系转webgl 坐标系

咱们这里的变换思路就是解决差异,接着上面的代码来写。

1.解决坐标原点位置的差异。

const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];

上面的[halfWidth,halfHeight]是canvas 画布中心的位置。

[xBaseCenter,yBaseCenter] 是用鼠标位减去canvas 画布的中心位,得到的就是鼠标基于画布中心的位置。

2.解决y 方向的差异。

const yBaseCenterTop=-yBaseCenter;

因为webgl 里的y 轴和canvas 2d 里的y轴相反,所以咱们对yBaseCenter 值取一下反即可。

3.解决坐标基底的差异。

const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight]

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

整体代码:

canvas.addEventListener('click',function(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];
})

关于获取鼠标点在webgl 坐标系中的位置的方法,我们就说到这,接下来咱们基于这个位置,修改着色器暴露出来的位置变量即可。

2-修改attribute 变量

这个步骤和第一章的内容是差不多的:

  1. 获取attribute 变量
  2. 在获取鼠标在webgl 画布中的位置的时候,修改attribute 变量
  3. 清理画布
  4. 绘图
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.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);canvas.addEventListener('click',function(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];gl.vertexAttrib2f(a_Position,x,y);gl.clear(gl.COLOR_BUFFER_BIT);gl.drawArrays(gl.POINTS, 0, 1);
})

在上面的例子中,大家每点击一次canvas 画布,都会画出一个点,而上一次画的点就会消失,我们无法连续画出多个点。

1

为何会如此呢?我们分析一下。

3-webgl 的同步绘图原理

具备canvas 2d可能会认为无法画出多点是gl.clear(gl.COLOR_BUFFER_BIT) 清理画布导致,因为我们在用canvas 2d 做动画时,其中就有一个ctx.clearRect() 清理画布的方法。

那咱们将gl.clear() 方法注释掉试试。

3-1-用实践得真知
gl.vertexAttrib2f(a_Position,x,y);
//gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

2

当我们鼠标点击画布时,画布中原本的黑色已经没有了,而且我们每次也只能画一个点。

我们分析一下。

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

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

举个栗子:

1.我先画两个点

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib2f(a_Position,0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib2f(a_Position,-0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);

image-20200920110015824

好的,没问题。

2.我想一秒后,再画一个点。

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib2f(a_Position,0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib2f(a_Position,-0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
setTimeout(()=>{gl.vertexAttrib2f(a_Position,0,0);gl.drawArrays(gl.POINTS, 0, 1);
},1000)

3

以前画好的两个点没了,黑色背景也没了。这就是咱们之前说过的webgl 同步绘图原理。

那这个问题如何解决呢?这就是一个简单的逻辑问题了。

3.我们可以用数组把一开始的那两个顶点存起来,在异步绘制第3个顶点的时候,把那两个顶点也一起画上。

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const g_points=[{x:0.1,y:0},{x:-0.1,y:0},
];
render();
setTimeout(()=>{g_points.push({x:0,y:0});render();
},1000)
function render(){gl.clear(gl.COLOR_BUFFER_BIT);g_points.forEach(({x,y})=>{gl.vertexAttrib2f(a_Position,x,y);gl.drawArrays(gl.POINTS, 0, 1);})
}    

image-20200920110718268

这样就可以以叠加覆盖的方式画出第三个点了。

4.理解上面的原理后,那我们接下来就可以用鼠标绘制多个点了。

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);const g_points=[];
canvas.addEventListener('click',function(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);})})

4

关于用鼠标控制点位,并绘制多点的方法我们就说到这。咱们最后简单总结一下这个原理。

3-2-webgl 同步绘图原理总结

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

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

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

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

webgl 绘图原理我就说到这。接下来咱们用js控制顶点尺寸。

4-用js控制顶点尺寸

用js 控制顶点尺寸的方法和控制顶点位置的方法是一样的,所以咱们这里就不再另起一章了。

1.首先咱们还是要在着色器里暴露出一个可以控制顶点尺寸的attribute 变量。

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

上面的a_PointSize 是一个浮点类型的变量。

2.在js 里获取attribute 变量

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

3.修改attribute 变量

gl.vertexAttrib1f(a_PointSize,100.0);

整体代码:

<canvas id="canvas"></canvas>
<script id="vertexShader" type="x-shader/x-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 '../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.vertexAttrib1f(a_PointSize,100.0);gl.clearColor(0.0, 0.0, 0.0, 1.0);gl.clear(gl.COLOR_BUFFER_BIT);gl.drawArrays(gl.POINTS, 0, 1);
</script>

后面我们也可以用鼠标随机改变顶点大小:

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const g_points=[];
canvas.addEventListener('click',function(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,z:Math.random()*50});gl.clear(gl.COLOR_BUFFER_BIT);g_points.forEach(({x,y,z})=>{gl.vertexAttrib2f(a_Position,x,y);gl.vertexAttrib1f(a_PointSize,z);gl.drawArrays(gl.POINTS, 0, 1);})
})

在我们上面的案例中,无论是控制点位的尺寸,还是控制点位的位置,实际上都是对attribute 变量的操控。

那我们如果想要再改变顶点的颜色呢?那就不能再用attribute 限定符了,因为attribute 限定符限定的就是顶点相关的数据。

接下来咱们就说一下如何用js 控制顶点的颜色。

第三章 用js 控制顶点的颜色

首先我们要知道,限定颜色变量的限定符叫uniform。

uniform 翻译过来是一致、统一的意思。

接下来咱们说一下用js 控制顶点颜色的步骤。

1-用js 控制顶点颜色的步骤

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

<script id="fragmentShader" type="x-shader/x-fragment">precision mediump float;uniform vec4 u_FragColor;void main() {gl_FragColor = u_FragColor;}
</script>

上面的uniform 就是咱们刚才说过的限定符,vec4 是4维的变量类型,u_FragColor 就是变量名。

这里还要注意一下,第一行的precision mediump float 是对浮点数精度的定义,mediump 是中等精度的意思,这个必须要有,不然画不出东西来。

2.在js 中获取片元着色器暴露出的uniform 变量

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

上面的getUniformLocation() 方法就是用于获取片元着色器暴露出的uniform 变量的,其第一个参数是程序对象,第二个参数是变量名。这里的参数结构和获取attribute 变量的getAttributeLocation() 方法是一样的。

3.修改uniform 变量

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

用js 控制顶点的颜色的基本步骤就是这样,其整体思路和控制顶点点位是一样的。

下面咱们一起对比着看一下整体代码。

<script id="vertexShader" type="x-shader/x-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 '../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.vertexAttrib1f(a_PointSize,100.0);gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);gl.clearColor(0.0, 0.0, 0.0, 1.0);gl.clear(gl.COLOR_BUFFER_BIT);gl.drawArrays(gl.POINTS, 0, 1);
</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');const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');gl.clearColor(0.0, 0.0, 0.0, 1.0);gl.clear(gl.COLOR_BUFFER_BIT);const arr=[];canvas.addEventListener('click',function(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])arr.push({x,y,z:Math.random()*50,color});gl.clear(gl.COLOR_BUFFER_BIT);arr.forEach(({x,y,z,color})=>{gl.vertexAttrib2f(a_Position,x,y);gl.vertexAttrib1f(a_PointSize,z);gl.uniform4fv(u_FragColor,color);gl.drawArrays(gl.POINTS, 0, 1);})})
</script>

在上面的代码中,我们使用uniform4fv() 修改的顶点颜色,类似的代码结构咱们之前提到过,在这里再给大家详细介绍一下。

2-uniform4fv() 方法

我们在改变uniform 变量的时候,既可以用uniform4f() 方法一个个的写参数,也可以用uniform4fv() 方法传递类型数组。

  • uniform4f 中,4 是有4个数据,f 是float 浮点类型,在我们上面的例子里就是r、g、b、a 这四个颜色数据。
  • uniform4fv 中,4f 的意思和上面一样,v 是vector 矢量的意思,这在数学里就是向量的意思。由之前的4f 可知,这个向量由4个浮点类型的分量构成。

在上面呢的案例中,我们可以知道,在修改uniform变量的时候,这两种写法是一样的:

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

uniform4f() 和uniform4fv() 也有着自己的同族方法,其中的4 可以变成1|2|3。

uniform4fv() 方法的第二个参数必须是Float32Array 数组,不要使用普通的Array 对象。

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

案例-用鼠标绘制星空

1-用鼠标绘制圆形的顶点

星星的形状是圆形的,所以,我们需要绘制一个圆形的顶点。

<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 丢弃,即不会一个片元进行渲染

着色器语法参考地址:https://www.khronos.org/registry/OpenGL-Refpages/gl4/

2-绘制随机透明度的星星

首先我们可以先给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)

3-制作闪烁的繁星

当星星会眨眼睛,会变得灵动而可爱,接下来我要让星星对你眨眼睛。

3-1-建立补间动画的意识

在这里推荐大家玩一下AE,因为它可以让你对动画的运行原理和架构方式有一个具象的认知。

比如,我在AE里画一颗星星,加几个关键帧,让它眨一下眼睛。

image-20210301150839658

在这里会涉及以下概念:

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

1.建立合成对象

export default class Compose{constructor(){this.parent=nullthis.children=[]}add(obj){obj.parent=thisthis.children.push(obj)}update(t){this.children.forEach(ele=>{ele.update(t)})}
}

属性

  • parent 父对象,合成对象可以相互嵌套
  • children 子对象集合,其集合元素可以是时间轨,也可以是合成对象

方法:

  • add(obj) 添加子对象方法
  • update(t) 基于当前时间更新子对象状态的方法

2.建立时间轨

export default class Track{constructor(target){this.target=targetthis.parent=nullthis.start=0this.timeLen=5this.loop=falsethis.keyMap=new Map()}update(t){const {keyMap,timeLen,target,loop}=thislet time=t-this.startif(loop){time=time%timeLen}for(const [key,fms] of keyMap.entries()){const last=fms.length-1if(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)}}}
}

属性

  • target 时间轨上的目标对象
  • parent 父对象,只能是合成对象
  • start 起始时间,即时间轨的建立时间
  • timeLen 时间轨总时长
  • loop 是否循环
  • keyMap 关键帧集合,结构如下:
[['对象属性1',[[时间1,属性值], //关键帧[时间2,属性值], //关键帧]],['对象属性2',[[时间1,属性值], //关键帧[时间2,属性值], //关键帧]],
]

方法

  • update(t) 基于当前时间更新目标对象的状态。

    先计算本地时间,即世界时间相对于时间轨起始时间的的时间。

    若时间轨循环播放,则本地时间基于时间轨长度取余。

    遍历关键帧集合:

    • 若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态
    • 若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态
    • 否则,计算本地时间在左右两个关键帧之间对应的补间状态

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

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.xconst b=fm1[1]-fm1[0]*kreturn k*time+b}}
}
  • getValBetweenFms(time,fms,last)

    • time 本地时间
    • fms 某个属性的关键帧集合
    • last 最后一个关键帧的索引位置

    其实现思路如下:

    • 遍历所有关键帧
    • 判断当前时间在哪两个关键帧之间
    • 基于这两个关键帧的时间和状态,求点斜式
    • 基于点斜式求本地时间对应的状态
3-3-使用合成对象和轨道对象制作补间动画
  1. 建立动画相关的对象
const compose=new Compose()
const stars=[]
canvas.addEventListener('click',function(event){const {x,y}=getPosByMouse(event,canvas)const a=1const s=Math.random()*5+2const 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=2000track.loop=truecompose.add(track)
})
  • compose 合成对象的实例化
  • stars 存储顶店数据的集合
  • 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>

总结

这一章我们学习了js 与着色器间的数据传输,从而去动态控制顶点的位置、大小和颜色,这是webgl 绘图的基础。在接下来的篇章里咱们会说更复杂的图形绘制。

相关文章:

webgl入门-js与着色器间的数据传输

js与着色器间的数据传输 前言 课堂目标 使用js向着色器传递数据获取鼠标在canvas 中的webgl 坐标系位置 知识点 attribute 变量gl.vertextAttribute3f() 的同族函数鼠标在canvas 中的css 位置转webgl 坐标位uniform 变量gl.uniform4f() 的同族函数 第一章 用js控制一个点…...

springmvc异常处理

springmvc异常处理 spring中有三种方式可以优雅的处理异常 使用ExceptionHandler 使用HandlerExceptionResolver 使用ControllerAdviceExceptionHandler 使用ExceptionHandler 该方式只在指定的Controller有效&#xff0c;不会对其他的Controller产生影响 ControllerRequestMap…...

可拖动、连线的React画布组件有哪些? 官网分别是什么?

下面是一些常用的可拖动、连线的React画布组件以及它们的官方网站&#xff1a; react-dagre-d3&#xff1a;这是一个基于React和D3.js的可拖动、连线的图形编辑器组件。它使用DAG&#xff08;有向无环图&#xff09;布局算法&#xff0c;支持节点拖拽、连线、缩放等功能。官网&…...

专访 Staynex 创始人 Yuen Wong:酒店行业的变革者

整理&#xff1a;Tia&#xff0c;Techub News 传统酒店业其实已经很中心化了&#xff0c;几大巨头 OTA 平台几乎已经完成对行业的垄断&#xff0c;而酒店商家也不得不受制于平台的规则制度&#xff0c;向平台支付高比例的费用。Staynex 看到了其中的机会&#xff0c;并想利用区…...

最新版Ceph( Reef版本)块存储简单对接k8s(上集)

当前ceph 你的ceph集群上执行 1.创建名为k8s-rbd 的存储池 ceph osd pool create k8s-rbd 64 642.初始化 rbd pool init k8s-rbd3 创建k8s访问块设备的认证用户 ceph auth get-or-create client.kubernetes mon profile rbd osd profile rbd poolk8s-rbd部署 ceph-rbd-csi c…...

稳态大面积光伏组件IV测试太阳光模拟器

稳态大面积光伏组件IV测试太阳光模拟器是太阳能光伏组件质量检测和评价的重要步骤之一。本文将介绍光伏组件IV测试的原理及标准板选择。 I. 光伏组件IV测试原理 光伏组件IV测试即电流电压特性测试&#xff0c;是评估光伏组件性能的重要手段。其测量的主要参数为组件的电流和电…...

编写HTTP协议代理的一些知识(源码)

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 早期上网经常需要使用代理服务…...

LabVIEW天然气压缩因子软件设计

LabVIEW天然气压缩因子软件设计 项目背景 天然气作为一种重要的能源&#xff0c;其压缩因子的准确计算对于流量的计量和输送过程的优化具有关键意义。传统的计算方法不仅步骤繁琐&#xff0c;而且难以满足现场快速响应的需求。因此&#xff0c;开发一款既能保证计算精度又便于…...

GCP谷歌云有什么数据库类型,该怎么选择

GCP谷歌云提供的数据库类型主要包括&#xff1a; 关系型数据库&#xff1a;这类数据库适用于结构化数据&#xff0c;通常用于数据结构不经常发生变化的场合。在GCP中&#xff0c;关系型数据库选项包括Cloud SQL和Cloud Spanner。Cloud SQL提供托管的MySQL、PostgreSQL和SQL Se…...

项目经理之路:裁员与内卷下的生存策略

作为一名项目经理&#xff0c;身处这个充满挑战与机遇的行业中&#xff0c;今年所面临的裁员潮和内卷化趋势无疑给我的工作带来了前所未有的压力。然而&#xff0c;正是这些压力和挑战&#xff0c;让我们更加深刻地思考了在这个快速变化的时代中&#xff0c;我们项目经理应该如…...

MWM触摸屏工控机维修TEM-EV0 EN00-Z312yy-xx

触摸屏维修是一个比较复杂的过程&#xff0c;并且其中会涉及到各个部件的问题&#xff0c;这对于操作人员来说&#xff0c;关键在于是否可以找到问题所在。维修过程中建议先检查各接线接口是否出现松动&#xff0c;然后检查串口及中断号是否有冲突&#xff0c;若有冲突&#xf…...

idm下载到99.99%不动了 idm突然不下载了 idm下载到最后没速度咋办 IDM下载后没网了是怎么回事

idm能够帮助我们下载不同类型的网页视频&#xff0c;并且基于多线程下载技术的助力下使其下载速度比原来提升数倍以上&#xff0c;因此成为了许多朋友下载的小助手。但也有朋友反映idm下载网页视频超时连接不上&#xff0c;idm下载网页视频突然停止&#xff0c;究竟这些情况我们…...

设计模式-07 设计模式-观察者模式(Observer Pattern)

设计模式-07 设计模式-观察者模式&#xff08;Observer Pattern&#xff09; 1.定义 观察者模式是一种软件设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;其中一个对象&#xff08;称为“主题”&#xff09;维护了一个依赖对象的列表&#xff08;称为“观察者”…...

戒烟网站|基于SSM+vue的戒烟网站系统的设计与实现(源码+数据库+文档)

戒烟网站 目录 基于SSM&#xff0b;vue的戒烟网站系统的设计与实现 一、前言 二、系统设计 三、系统功能设计 1网站功能模块 2管理员功能模块 3用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主…...

研发管理之认识DevOps

文章目录 一、什么是DevOps二、DevOps的背景和起源三、DevOps的特点和价值1、特点&#xff1a;2、价值&#xff1a; 四、DevOps如何帮助提高软件交付速度和质量 一、什么是DevOps DevOps&#xff08;Development和Operations的组合词&#xff09;是一组过程、方法与系统的统称…...

Spring MVC(五) 文件上传

1 单文件上传 在程序开发中&#xff0c;有时候需要上传一些文件。我们在学习Servlet的时候&#xff0c;也做过文件上传的操作&#xff0c;只不过基于Servlet的文件上传操作起来过于复杂&#xff0c;因此所有的MVC框架都提供了自己的文件上传操作&#xff0c;基本上都是基于File…...

Redis——Redis数据分片的三种算法

Redis的数据分片通常是为了实现水平扩展&#xff0c;将数据分散到多个Redis节点上&#xff0c;以提高系统的容量和性能。在Redis的不同实现和集群方案中&#xff0c;数据分片的算法有所不同。以下是Redis数据分片的三种常见算法&#xff1a; 哈希取模分片&#xff08;Hash Modu…...

【专利】一种日志快速分析方法、设备、存储介质

公开号CN116560938A申请号CN202310311478.5申请日2023.03.28 是我在超音速人工智能科技股份有限公司(833753) 职务作品&#xff0c;第一发明人是董事长夫妇&#xff0c;第二发明人是我。 ** 注意** &#xff1a; 内容比较多&#xff0c;还有流程图、界面等。请到 专利指定页面…...

HFSS学习-day5-边界条件

边界条件 概述边界条件类型1、理想导体边界条件&#xff08;Perfect E&#xff09;2、理想磁边界条件&#xff08;Perfect H&#xff09;3、有限导体边界条件&#xff08;Finite Conductivity&#xff09;4、辐射边界条件&#xff08;Radiation&#xff09;5、对称边界条件&…...

spring Aop使用示例

简介&#xff08;aop作用&#xff09;&#xff1a;1.在不改变源代码的基础上进行功能添加&#xff0c;如日志打印、执行时间统计。2.与代理效果类似但更加便捷。 示例&#xff1a; maven依赖&#xff1a; <dependency><groupId>org.springframework</groupId&g…...

Linux链表操作全解析

Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表&#xff1f;1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

【JVM】- 内存结构

引言 JVM&#xff1a;Java Virtual Machine 定义&#xff1a;Java虚拟机&#xff0c;Java二进制字节码的运行环境好处&#xff1a; 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收的功能数组下标越界检查&#xff08;会抛异常&#xff0c;不会覆盖到其他代码…...

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢

随着互联网技术的飞速发展&#xff0c;消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁&#xff0c;不仅优化了客户体验&#xff0c;还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用&#xff0c;并…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)

一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解&#xff0c;适合用作学习或写简历项目背景说明。 &#x1f9e0; 一、概念简介&#xff1a;Solidity 合约开发 Solidity 是一种专门为 以太坊&#xff08;Ethereum&#xff09;平台编写智能合约的高级编…...

汇编常见指令

汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX&#xff08;不访问内存&#xff09;XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

GC1808高性能24位立体声音频ADC芯片解析

1. 芯片概述 GC1808是一款24位立体声音频模数转换器&#xff08;ADC&#xff09;&#xff0c;支持8kHz~96kHz采样率&#xff0c;集成Δ-Σ调制器、数字抗混叠滤波器和高通滤波器&#xff0c;适用于高保真音频采集场景。 2. 核心特性 高精度&#xff1a;24位分辨率&#xff0c…...

python执行测试用例,allure报乱码且未成功生成报告

allure执行测试用例时显示乱码&#xff1a;‘allure’ &#xfffd;&#xfffd;&#xfffd;&#xfffd;&#xfffd;ڲ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ⲿ&#xfffd;&#xfffd;&#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;ǿ&#xfffd;&am…...