一、用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>