- 使用js向着色器传递数据
- 获取鼠标在canvas 中的webgl 坐标系位置
一. 用js控制一个点的位置
1-1 attribute变量的概念
<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 100.0;
}
</script>
其中,点的定位:
gl_Position = vec4(0,0,0,1);
这种方式将数据写死了,缺乏可扩展性。要让这个点位可以动态改变,那就得把它变成attribute变量。
- attribute 变量是只有顶点着色器才能使用的。
- js 可以通过
attribute 变量向顶点着色器传递与顶点相关的数据。
1-2 js向attribute 变量传参的步骤
1-2-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-2-2. 在js中获取attribute 变量
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
1-2-3. 修改attribute 变量
gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);
整体代码
<!DOCTYPE html>
<html>
<body>
<div id="app">
<canvas id="canvas"></canvas>
</div>
<!-- 顶点着色器 -->
<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, 0.0, 0.0, 1.0);
}
</script>
<script>
// canvas 画布
const canvas = document.getElementById('canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// webgl画笔
const gl = canvas.getContext('webgl')
// 顶点着色器
const vsSource = document.getElementById('vertexShader').innerText
// const vsSource = `attribute vec4 a_Position;
// void main(){
// //点位
// gl_Position=a_Position;
// //尺寸
// gl_PointSize=50.0;
// }`
// 片元着色器
const fsSource = document.getElementById('fragmentShader').innerText
// 初始化着色器
initShaders(gl, vsSource, fsSource)
// 在js中获取attribute 变量
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
// 修改attribute 变量
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)
function initShaders(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
//启动程序对象
gl.useProgram(program)
//将程序对象挂到上下文对象上
gl.program = program
return true
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type)
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source)
//编译着色器对象
gl.compileShader(shader)
//返回着色器对象
return shader
}
</script>
</body>
</html>
1-3 js向attribute 变量传参的原理
1-3-1 着色器中的attribute 变量——存储限定符
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 50.0;
}
关键词
attribute被称为存储限定符,它表示接下来的变量是一个attribute变量。attribute变量必须声明成全局变量,数据从着色器外部传给该变量。变量的声明必须按照以下的格式:<存储限定符> <类型> <变量名>。
- attribute 是存储限定符,是专门用于向外部导出与点位相关的对象的,这类似于es6模板语法中export 。
- vec4 是变量类型,vec4是4维矢量对象。
- a_Position 是变量名,之后在js中会根据这个变量名导入变量。这个变量名是一个指针,指向实际数据的存储位置。也是说,如果在着色器外部改变了a_Position所指向的实际数据,那么在着色器中a_Position 所对应的数据也会修改。
1-3-2 在js中获取attribute 变量
在js 里不能直接写a_Position 来获取着色器中的变量。因为着色器和js 是两个不同的语种,着色器无法通过window.a_Position 原理向全局暴露变量。
要在js 里获取着色器暴露的变量,就需要找人来翻译,这个人就是程序对象gl.program。
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
-
gl 是webgl 的上下文对象。
-
gl.getAttribLocation() 是获取着色器中attribute 变量的方法。
-
getAttribLocation() 方法的参数中:
- gl.program 是初始化着色器时,在上下文对象上挂载的程序对象。
- 'a_Position' 是着色器暴露出的变量名。
这个过程简单来说就是:gl 上下文对象对program 程序对象说,你去顶点着色器里找一个名叫'a_Position' 的attribute变量。
1-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);
1-4 webgl 函数扩展知识
1-4-1 webgl 函数的命名规律
GLSL ES里函数的命名结构是:<基础函数名><参数个数><参数类型>
以vertexAttrib3f(location,v0,v1,v2,v3) 为例:
- vertexAttrib:基础函数名
- 3:参数个数,这里的参数个数是要传给变量的参数个数,而不是当前函数的参数个数
- f:参数类型,f 代表float 浮点类型,除此之外还有i 代表整型,v代表数字……
1-4-2 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 浮点型。
二、用鼠标控制顶点位置——对attribute 变量的操控
我们要用鼠标控制一个点的位置,首先要获取鼠标点在webgl 坐标系中的位置。
2-1 获取鼠标点在webgl 坐标系中的位置
鼠标点在webgl 坐标系中的位置,是无法直接获取的。需要先获取鼠标在canvas 这个DOM元素中的位置。
2-1-1 获取鼠标在canvas 画布中的css 位置
- canvas 2d 坐标系的原点在左上角。
- canvas 2d 坐标系的y 轴方向是朝下的。
- canvas 2d 坐标系的坐标基底有两个分量,分别是一个像素的宽和一个像素的高,即1个单位的宽便是1个像素的宽,1个单位的高便是一个像素的高
canvas.addEventListener('click', function (event) {
const { clientX, clientY } = event
const { left, top } = canvas.getBoundingClientRect()
const [cssX, cssY] = [clientX - left, clientY - top]
console.log([cssX, cssY])
})
对于cssX,cssY 的获取,这在canvas 2d 也会用到。
因为html 坐标系中的坐标原点和轴向与canvas 2d是一致的,所以在我们没有用css 改变画布大小,也没有对其坐标系做变换的情况下,鼠标点在canvas 画布中的css 位就是鼠标点在canvas 2d坐标系中的位置。
2-1-2-canvas 坐标系转webgl 坐标系
这里的变换思路就是解决差异,接着上面的代码来写。
- webgl坐标系的坐标原点在画布中心。
- webgl坐标系的y 轴方向是朝上的。
- webgl坐标基底中的两个分量分别是半个canvas的宽和canvas的高,即1个单位的宽便是半个个canvas的宽,1个单位的高便是半个canvas的高。
1. 解决坐标原点位置的差异。
const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
上面的[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]
// console.log('鼠标对应的canvas坐标', [cssX, cssY])
const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
// console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
const yBaseCenterTop = -yBaseCenter
const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
// console.log('鼠标对应的webgl坐标', [x, y])
})
获取鼠标点在webgl 坐标系中的位置,接下来基于这个位置,修改着色器暴露出来的位置变量。
2-2 按照鼠标点击位置-修改attribute 变量
步骤:
- 获取attribute 变量
- 在获取鼠标在webgl 画布中的位置的时候,修改attribute 变量
- 清理画布
- 绘图
<!DOCTYPE html>
<html>
<body>
<div id="app">
<canvas id="canvas"></canvas>
</div>
<!-- 顶点着色器 -->
<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, 0.0, 0.0, 1.0);
}
</script>
<script>
// canvas 画布
const canvas = document.getElementById('canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// webgl画笔
const gl = canvas.getContext('webgl')
// 顶点着色器
const vsSource = document.getElementById('vertexShader').innerText
// const vsSource = `attribute vec4 a_Position;
// void main(){
// //点位
// gl_Position=a_Position;
// //尺寸
// gl_PointSize=50.0;
// }`
// 片元着色器
const fsSource = document.getElementById('fragmentShader').innerText
// 初始化着色器
initShaders(gl, vsSource, fsSource)
// 在js中获取attribute 变量
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
// 修改attribute 变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
// gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)
// 指定将要用来清理绘图区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清理绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
function initShaders(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
//启动程序对象
gl.useProgram(program)
//将程序对象挂到上下文对象上
gl.program = program
return true
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type)
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source)
//编译着色器对象
gl.compileShader(shader)
//返回着色器对象
return shader
}
canvas.addEventListener('click', function (event) {
const { clientX, clientY } = event
const { left, top, width, height } = canvas.getBoundingClientRect()
const [cssX, cssY] = [clientX - left, clientY - top]
// console.log('鼠标对应的canvas坐标', [cssX, cssY])
const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
// console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
const yBaseCenterTop = -yBaseCenter
const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
console.log('鼠标对应的webgl坐标', [x, y])
gl.vertexAttrib2f(a_Position, x, y)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, 1)
})
</script>
</body>
</html>
在上面的例子中,每点击一次canvas 画布,都会画出一个点,而上一次画的点就会消失,我们无法连续画出多个点。
2-3 webgl 的同步绘图原理——Js异步进程中之前绘制的图像会清空
可能会认为无法画出多点是gl.clear(gl.COLOR_BUFFER_BIT) 清理画布导致,因为我们在用canvas 2d 做动画时,其中就有一个ctx.clearRect() 清理画布的方法。
2-3-1 WebGLRenderingContext.drawArrays()
WebGL API 中的
WebGLRenderingContext.drawArrays()方法用于从向量数组中绘制图元。
gl.drawArrays(mode, first, count);
-
mode指定绘制图元的方式,可能值如下。 -
first指定从哪个点开始绘制。 -
count指定绘制需要使用到多少个点。
2-3-2 试一试——将gl.clear() 方法注释掉
gl.vertexAttrib2f(a_Position,x,y);
//gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
当鼠标点击画布时,画布中原本的黑色已经没有了,而且我们每次也只能画一个点。
gl.drawArrays(gl.POINTS, 0, 1) 方法和canvas 2d 里的ctx.draw() 方法是不一样的,ctx.draw() 真的像画画一样,一层一层的覆盖图像。
gl.drawArrays()方法只会同步绘图,走完了js 主线程后,再次绘图时,就会从头再来。也就说,异步执行的drawArrays() 方法会把画布上的图像都刷掉。
举个例子:
- 先画两个点
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);
- 一秒后,再画一个点。
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)
以前画好的两个点没了,黑色背景也没了。这就是webgl 同步绘图原理。
- 用数组把一开始的那两个顶点存起来,在异步绘制第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);
})
}
这样就可以以叠加覆盖的方式画出第三个点了。
4.理解上面的原理后,就可以用鼠标绘制多个点了。
<!DOCTYPE html>
<html>
<body>
<div id="app">
<canvas id="canvas"></canvas>
</div>
<!-- 顶点着色器 -->
<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, 0.0, 0.0, 1.0);
}
</script>
<script>
// canvas 画布
const canvas = document.getElementById('canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// webgl画笔
const gl = canvas.getContext('webgl')
// 顶点着色器
const vsSource = document.getElementById('vertexShader').innerText
// const vsSource = `attribute vec4 a_Position;
// void main(){
// //点位
// gl_Position=a_Position;
// //尺寸
// gl_PointSize=50.0;
// }`
// 片元着色器
const fsSource = document.getElementById('fragmentShader').innerText
// 初始化着色器
initShaders(gl, vsSource, fsSource)
// 在js中获取attribute 变量
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
// 修改attribute 变量
// gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
// gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)
// 指定将要用来清理绘图区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清理绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制顶点
//gl.drawArrays(gl.POINTS, 0, 1)
function initShaders(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
//启动程序对象
gl.useProgram(program)
//将程序对象挂到上下文对象上
gl.program = program
return true
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type)
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source)
//编译着色器对象
gl.compileShader(shader)
//返回着色器对象
return shader
}
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]
// console.log('鼠标对应的canvas坐标', [cssX, cssY])
const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
// console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
const yBaseCenterTop = -yBaseCenter
const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
console.log('鼠标对应的webgl坐标', [x, y])
// webgl同步绘制多个点
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>
</body>
</html>
2-3-2 webgl 同步绘图原理介绍
webgl 的同步绘图的现象,其实是由webgl 底层内置的颜色缓冲区导致的。
- 这个颜色缓冲区,在电脑里会占用一块内存。在使用webgl 绘图的时候,是先在颜色缓冲区中画出来,这样的图像,外人看不见,只有webgl系统自己知道。
- 在我们想要将图像显示出来的时候,那就照着颜色缓冲区中的图像去画,这个步骤是webgl 内部自动完成的,我们只要执行绘图命令即可。
- 颜色缓冲区中存储的图像,只在当前线程有效。比如我们先在js 主线程中绘图,主线程结束后,会再去执行信息队列里的异步线程。在执行异步线程时,颜色缓冲区就会被webgl 系统重置,我们曾经在主线程里的图像也就没了,也就画不出那时的图像了。
2-4 用js控制顶点尺寸大小——对attribute 变量的操控
用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);
2-4-1 改变顶点大小
整体代码:
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);
// 顶点大小改成100
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);
2-4-2 用鼠标随机改变顶点大小
<!DOCTYPE html>
<html>
<body>
<div id="app">
<canvas id="canvas"></canvas>
</div>
<!-- 顶点着色器 -->
<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, 0.0, 0.0, 1.0);
}
</script>
<script>
// canvas 画布
const canvas = document.getElementById('canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// webgl画笔
const gl = canvas.getContext('webgl')
// 顶点着色器
const vsSource = document.getElementById('vertexShader').innerText
// const vsSource = `attribute vec4 a_Position;
// void main(){
// //点位
// gl_Position=a_Position;
// //尺寸
// gl_PointSize=50.0;
// }`
// 片元着色器
const fsSource = document.getElementById('fragmentShader').innerText
// 初始化着色器
initShaders(gl, vsSource, fsSource)
// 在js中获取attribute 变量
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize')
// 修改attribute 变量
// gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
// gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)
// 指定将要用来清理绘图区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清理绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制顶点
//gl.drawArrays(gl.POINTS, 0, 1)
function initShaders(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
//启动程序对象
gl.useProgram(program)
//将程序对象挂到上下文对象上
gl.program = program
return true
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type)
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source)
//编译着色器对象
gl.compileShader(shader)
//返回着色器对象
return shader
}
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]
// console.log('鼠标对应的canvas坐标', [cssX, cssY])
const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
// console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
const yBaseCenterTop = -yBaseCenter
const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
console.log('鼠标对应的webgl坐标', [x, y])
// webgl同步绘制多个不同尺寸大小的点
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)
})
})
</script>
</body>
</html>
在上面的案例中,无论是控制顶点的尺寸大小,还是控制顶点的位置,实际上都是对attribute 变量的操控。
不过如果想要再改变顶点的颜色,那就不能再用attribute 限定符了
三 用js 控制顶点的颜色
限定颜色变量的限定符叫uniform。uniform 翻译过来是一致、统一的意思。
3-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 控制顶点的颜色的基本步骤,其整体思路和控制顶点位置是一样的。
3-1-1 用js 控制单个顶点的颜色
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);
3-1-2 同样可以用鼠标随机改变顶点的颜色。
<!DOCTYPE html>
<html>
<body>
<div id="app">
<canvas id="canvas"></canvas>
</div>
<!-- 顶点着色器 -->
<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>
// canvas 画布
const canvas = document.getElementById('canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// webgl画笔
const gl = canvas.getContext('webgl')
// 顶点着色器
const vsSource = document.getElementById('vertexShader').innerText
// const vsSource = `attribute vec4 a_Position;
// void main(){
// //点位
// gl_Position=a_Position;
// //尺寸
// gl_PointSize=50.0;
// }`
// 片元着色器
const fsSource = document.getElementById('fragmentShader').innerText
// 初始化着色器
initShaders(gl, vsSource, fsSource)
// 在js中获取attribute 变量
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')
// 修改attribute 变量
// gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
// gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)
// 指定将要用来清理绘图区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清理绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制顶点
//gl.drawArrays(gl.POINTS, 0, 1)
function initShaders(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
//启动程序对象
gl.useProgram(program)
//将程序对象挂到上下文对象上
gl.program = program
return true
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type)
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source)
//编译着色器对象
gl.compileShader(shader)
//返回着色器对象
return shader
}
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]
// console.log('鼠标对应的canvas坐标', [cssX, cssY])
const [halfWidth, halfHeight] = [canvas.width / 2, canvas.height / 2]
const [xBaseCenter, yBaseCenter] = [cssX - halfWidth, cssY - halfHeight]
// console.log('解决坐标原点差异', [xBaseCenter, yBaseCenter])
const yBaseCenterTop = -yBaseCenter
const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
console.log('鼠标对应的webgl坐标', [x, y])
// webgl同步绘制多个不同尺寸大小、不同颜色的点
const color = new Float32Array([
Math.random(),
Math.random(),
Math.random(),
1.0
])
g_points.push({ x, y, z: Math.random() * 50, color })
gl.clear(gl.COLOR_BUFFER_BIT)
g_points.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>
</body>
</html>
在上面的代码中,使用uniform4fv() 修改的顶点颜色
3-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 高很多。
四 案例-用鼠标绘制星空
4-1 用鼠标绘制圆形的顶点
星星的形状是圆形的,绘制一个圆形的顶点。
gl.POINTS绘制模式 顶点默认渲染效果是方形区域,通过下面片元着色器代码设置可以把默认渲染效果更改为圆形区域。
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float; // 所有float类型数据的精度是mediump
uniform vec4 u_FragColor;
void main() {
// 计算方形区域每个片元距离方形几何中心的距离
// gl.POINTS模式点渲染的方形区域,方形中心是0.5,0.5,左上角是坐标原点,右下角是1.0,1.0,
float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
if(dist < 0.5) {
// 方形区域片元距离几何中心半径小于0.5,设置像素颜色
gl_FragColor = u_FragColor;
} else {
discard; // 方形区域距离几何中心半径不小于0.5的片元剪裁舍弃掉
}
}
</script>
- distance(p1,p2) 计算两个点位的距离
- gl_PointCoord 内置变量
- discard 丢弃片元
4-2 绘制随机透明度的星星
先给canvas 一个星空背景
#canvas {
background: url(".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)
4-3 制作闪烁的繁星
当星星会眨眼睛,会变得灵动而可爱。
4-3-1 建立补间动画的意识
画一颗星星,加几个关键帧,让它眨一下眼睛。
在这里会涉及以下概念:
- 合成:多个时间轨的集合
- 时间轨:通过关键帧,对其中目标对象的状态进行插值计算
- 补间动画:通过两个关键帧,对一个对象在这两个关键帧之间的状态进行插值计算,从而实现这个对象在两个关键帧间的平滑过渡
4-3-2 架构代码
1.建立合成对象
export default class Compose{
constructor() {
this.parent = null // parent 父对象,合成对象可以相互嵌套
this.children = [] // children 子对象集合,其集合元素可以是时间轨,也可以是合成对象
}
// add(obj) 添加子对象方法
add(obj) {
obj.parent = this
this.children.push(obj)
}
// update(t) 基于当前时间更新子对象状态的方法
update(t) {
this.children.forEach((ele) => {
ele.update(t)
})
}
}
2.建立时间轨
export default class Track{
constructor(target){
this.target = target // target 时间轨上的目标对象
this.parent = null // parent 父对象,只能是合成对象
this.start = 0 // start 起始时间,即时间轨的建立时间
this.timeLen = 5 // 时间轨总时长
this.loop = false // 是否循环
this.keyMap = new Map() // keyMap 关键帧集合
}
// update(t) 基于当前时间更新目标对象的状态。
update(t) {
const { keyMap, timeLen, target, loop, start } = this
//先计算本地时间,即世界时间相对于时间轨起始时间的的时间。
let time = t - start
// 若时间轨循环播放,则本地时间基于时间轨长度取余。
if (loop) {
time = time % timeLen
}
// 遍历关键帧集合:
// - 若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态
// - 若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态
// - 否则,计算本地时间在左右两个关键帧之间对应的补间状态
for (const [key, fms] of keyMap) {
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)
}
}
}
}
属性
- keyMap 关键帧集合,结构如下:
- 获取两个关键帧之间补间状态的方法
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
}
}
}
-
getValBetweenFms(time,fms,last)
- time 本地时间
- fms 某个属性的关键帧集合
- last 最后一个关键帧的索引位置
其实现思路如下:
- 遍历所有关键帧
- 判断当前时间在哪两个关键帧之间
- 基于这两个关键帧的时间和状态,求点斜式
- 基于点斜式求本地时间对应的状态
4-3-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}
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)
})
- 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);
})
}
4-4 案例效果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>星星眨眼</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background: url('./sky.jpg');
background-size: cover;
background-position: right bottom;
}
</style>
</head>
<body>
<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">
precision mediump float; // 所有float类型数据的精度是mediump
uniform vec4 u_FragColor;
void main() {
// 计算方形区域每个片元距离方形几何中心的距离
// gl.POINTS模式点渲染的方形区域,方形中心是0.5,0.5,左上角是坐标原点,右下角是1.0,1.0,
float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
if(dist < 0.5) {
// 方形区域片元距离几何中心半径小于0.5,设置像素颜色
gl_FragColor = u_FragColor;
} else {
discard; // 方形区域距离几何中心半径不小于0.5的片元剪裁舍弃掉
}
}
</script>
<script type="module">
import { initShaders, Compose, Track } from './Utils.js'
const canvas = document.querySelector('#canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// 获取着色器文本
const vsSource = document.querySelector('#vertexShader').innerText
const fsSource = document.querySelector('#fragmentShader').innerText
//三维画笔
const gl = canvas.getContext('webgl')
gl.enable(gl.BLEND) // 开启片元的颜色合成功能
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) // 设置片元的合成方式
//初始化着色器
initShaders(gl, vsSource, fsSource)
//设置attribute 变量
// a_Position=vec4(1,0,0,1)
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')
const stars = []
//建立合成对象
const compose = new Compose()
//给一个透明的底色,这样才能看见canvas的css背景
gl.clearColor(0, 0, 0, 0)
//刷底色
gl.clear(gl.COLOR_BUFFER_BIT)
//绘制顶点
render()
// 鼠标点击事件
canvas.addEventListener('click', ({ clientX, clientY }) => {
console.log(clientX, clientY)
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]
// 解决y 方向的差异
const yBaseCenterTop = -yBaseCenter
//解决坐标基底的差异
const [x, y] = [xBaseCenter / halfWidth, yBaseCenterTop / halfHeight]
// 星星大小2-7
const s = Math.random() * 5 + 2
const a = 1
const obj = { x, y, s, a }
stars.push(obj)
//建立轨道对象
const track = new Track(obj)
track.start = new Date()
track.timeLen = 2000
track.loop = true
track.keyMap = new Map([
[
'a',
[
[500, a],
[1000, 0],
[1500, a]
]
]
])
compose.add(track)
// render();
})
!(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)
const arr = new Float32Array([0.87, 0.91, 1, a])
gl.uniform4fv(u_FragColor, arr)
gl.drawArrays(gl.POINTS, 0, 1)
})
}
</script>
</body>
</html>
// ./Utils.js
function initShaders(gl, vsSource, fsSource) {
//创建程序对象
const program = gl.createProgram()
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader)
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader)
//连接webgl上下文对象和程序对象
gl.linkProgram(program)
//启动程序对象
gl.useProgram(program)
//将程序对象挂到上下文对象上
gl.program = program
return true
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type)
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source)
//编译着色器对象
gl.compileShader(shader)
//返回着色器对象
return shader
}
//建立合成对象
class Compose {
constructor() {
this.parent = null // parent 父对象,合成对象可以相互嵌套
this.children = [] // children 子对象集合,其集合元素可以是时间轨,也可以是合成对象
}
// add(obj) 添加子对象方法
add(obj) {
obj.parent = this
this.children.push(obj)
}
// update(t) 基于当前时间更新子对象状态的方法
update(t) {
this.children.forEach((ele) => {
ele.update(t)
})
}
}
// 建立时间轨
class Track {
constructor(target) {
this.target = target // target 时间轨上的目标对象
this.parent = null // parent 父对象,只能是合成对象
this.start = 0 // start 起始时间,即时间轨的建立时间
this.timeLen = 5 // 时间轨总时长
this.loop = false // 是否循环
this.keyMap = new Map() // keyMap 关键帧集合
}
// update(t) 基于当前时间更新目标对象的状态。
update(t) {
const { keyMap, timeLen, target, loop, start } = this
//先计算本地时间,即世界时间相对于时间轨起始时间的的时间。
let time = t - start
// 若时间轨循环播放,则本地时间基于时间轨长度取余。
if (loop) {
time = time % timeLen
}
// 遍历关键帧集合:
// - 若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态
// - 若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态
// - 否则,计算本地时间在左右两个关键帧之间对应的补间状态
for (const [key, fms] of keyMap) {
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
}
}
}
export { initShaders, Track, Compose }
使用js 与着色器间的数据传输,从而去动态控制顶点的位置、大小和颜色,这是webgl 绘图的基础。