JS 与着色器之间的数据传输
用 js 控制一个点的位置
attribute 变量的概念
gl_Position = vec4(0,0,0,1);这是一种将数据写死了的硬编码,缺乏可扩展性
要让这个点位可以动态改变,那就得把它变成 attribute 变量,attribute 变量是只有顶点着色器才能使用它的
js 可以通过 attribute 变量向顶点着色器传递与顶点相关的数据
JS 向 attribute 变量传参的步骤
- 在顶点着色器中声明 attribute 变量
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 50.0;
}
</script>
// 在 js 中获取 attribute 变量
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
// 修改 attribute 变量
gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>js改变点位</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</style>
</head>
<body>
<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,1,0,1);
}
</script>
<script type="module">
import { initShaders } from "../jsm/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")
// 初始化着色器
initShaders(gl, vsSource, fsSource)
// 声明颜色 rgba
gl.clearColor(0, 0, 0, 1)
// 刷底色
gl.clear(gl.COLOR_BUFFER_BIT)
// 获取 attribute 变量
const a_Position = gl.getAttribLocation(gl.program, "a_Position")
// 修改 attribute 变量
// gl.vertexAttrib3f(a_Position, 0, 1, 0);
// gl.vertexAttrib2f(a_Position, 0.5, 0.5);
gl.vertexAttrib1f(a_Position, 0.1)
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
</script>
</body>
</html>扩展
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 浮点型
webgl 函数的命名规律
GLSL ES 里函数的命名结构是:<基础函数名><参数个数><参数类型>
以vertexAttrib3f(location,v0,v1,v2,v3) 为例:
- vertexAttrib:基础函数名
- 3:参数个数,这里的参数个数是要传给变量的参数个数,而不是当前函数的参数个数
- f:参数类型,f 代表float 浮点类型,除此之外还有 i 代表整型,v 代表数字……
用鼠标控制点位
用鼠标控制一个点的位置,首先要知道鼠标点在 webgl 坐标系中的位置,这样才能让一个点出现在我们鼠标点击的位置
对于鼠标点在 webgl 坐标系中的位置,我们是无法直接获取的。所以我们得先获取鼠标在 canvas 这个 DOM 元素中的位置
canvas.addEventListener('click',function(event){
const {clientX,clientY}=event;
const {left,top}=canvas.getBoundingClientRect();
const [cssX,cssY]=[
clientX-left,
clientY-top
];
})我们可以用向量减法来求解
已知:向量 a(clientX,clientY),向量 c(left,top) 求:向量 c
由向量的减法得:向量 a 减向量 c,等于以向量 c 的终点为起点,以向量 a 的终点为终点的向量 c
所以:向量 c=a-c=(clientX-left,clientY-top)
将向量 c 视之为坐标点 c,那点 c 就是鼠标在 canvas 画布中的 css 位
因为 html 坐标系中的坐标原点和轴向与 canvas 2d 是一致的,所以在我们没有用 css 改变画布大小,也没有对其坐标系做变换的情况下,鼠标点在 canvas 画布中的 css 位就是鼠标点在 canvas 2d 坐标系中的位置。
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 坐标系中的位置的方法,我们就说到这,接下来咱们基于这个位置,修改着色器暴露出来的位置变量即可。
修改 attribute 变量
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>鼠标控制点位</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</style>
</head>
<body>
<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,1,0,1);
}
</script>
<script type="module">
import { initShaders } from "../jsm/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")
//初始化着色器
initShaders(gl, vsSource, fsSource)
// 获取 attribute 变量
const a_Position = gl.getAttribLocation(gl.program, "a_Position")
// 修改attribute 变量
gl.vertexAttrib1f(a_Position, 0.1)
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
// 鼠标点击事件
canvas.addEventListener("click", ({ clientX, clientY }) => {
console.log(clientX, clientY)
// 获取 canvas 的位置和尺寸 left 是距离左边的距离,top 是距离上边的距离
const { left, top, width, height } = canvas.getBoundingClientRect()
console.log(left, top, width, height)
// 这里的 left 、top 就是 canvas 的坐标原点
const [cssX, cssY] = [clientX - left, clientY - top]
// canvas 坐标转换为 webgl 坐标系
// 解决坐标原点位置的差异
// canvas的中心坐标
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]
gl.vertexAttrib2f(a_Position, x, y)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, 1)
})
</script>
</body>
</html>在上面的例子中,大家每点击一次 canvas 画布,都会画出一个点,而上一次画的点就会消失,我们无法连续画出多个点
webgl 的同步绘图原理
具备 canvas 2d 可能会认为无法画出多点是 gl.clear(gl.COLOR_BUFFER_BIT) 清理画布导致,因为我们在用 canvas 2d 做动画时,其中就有一个ctx.clearRect() 清理画布的方法。
那咱们将 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() 方法会把画布上的图像都刷掉
案例 1
我先画两个点,然后在一秒后再绘画一个点
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>03-webgl同步绘图原理</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</style>
</head>
<body>
<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,1,0,1);
}
</script>
<script type="module">
import { initShaders } from "../jsm/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")
initShaders(gl, vsSource, fsSource)
// 获取 attribute 变量
const a_Position = gl.getAttribLocation(gl.program, "a_Position")
// 声明颜色 rgba
gl.clearColor(0, 0, 0, 1)
// 刷底色
gl.clear(gl.COLOR_BUFFER_BIT)
// 修改 attribute 变量
gl.vertexAttrib2f(a_Position, -0.3, 0)
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
// 修改 attribute 变量 绘画两个点
gl.vertexAttrib2f(a_Position, 0.3, 0)
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
// 在一秒后再绘画一个点
setTimeout(() => {
gl.vertexAttrib2f(a_Position, 0, 0)
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
}, 1000)
</script>
</body>
</html>以前画好的两个点没了,黑色背景也没了。这就是咱们之前说过的 webgl 同步绘图原理。那这个问题如何解决呢?这就是一个简单的逻辑问题了
案例 2
我们可以用数组把一开始的那两个顶点存起来,在异步绘制第3个顶点的时候,把那两个顶点也一起画上
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>绘图原理</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</style>
</head>
<body>
<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,1,0,1);
}
</script>
<script type="module">
import { initShaders } from "../jsm/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")
initShaders(gl, vsSource, fsSource)
// 获取 attribute 变量
const a_Position = gl.getAttribLocation(gl.program, "a_Position")
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
// 存储顶点数据的数组
const a_points = [
{ x: -0.3, y: 0 },
{ x: 0.3, y: 0 },
]
render()
setTimeout(() => {
a_points.push({ x: 0, y: 0 })
render()
}, 1000)
// 渲染方法
function render() {
gl.clear(gl.COLOR_BUFFER_BIT)
a_points.forEach(({ x, y }) => {
gl.vertexAttrib2f(a_Position, x, y)
gl.drawArrays(gl.POINTS, 0, 1)
})
}
</script>
</body>
</html>这样就可以以叠加覆盖的方式画出第三个点了
案例 3:连续画点
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>连续画点</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</style>
</head>
<body>
<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,1,0,1);
}
</script>
<script type="module">
import { initShaders } from "../jsm/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")
initShaders(gl, vsSource, fsSource)
const a_Position = gl.getAttribLocation(gl.program, "a_Position")
const a_points = [{ x: 0, y: 0 }]
gl.clearColor(0, 0, 0, 1)
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]
a_points.push({ x, y })
render()
})
// 渲染方法
function render() {
gl.clear(gl.COLOR_BUFFER_BIT)
a_points.forEach(({ x, y }) => {
gl.vertexAttrib2f(a_Position, x, y)
gl.drawArrays(gl.POINTS, 0, 1)
})
}
</script>
</body>
</html>JS 控制顶点尺寸
用 js 控制顶点尺寸的方法和控制顶点位置的方法是一样的
1.首先还是要在着色器里暴露出一个可以控制顶点尺寸的 attribute 变量,a_PointSize 是一个浮点类型的变量。
<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>2.在 js 里获取 attribute 变量 const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
3.修改 attribute 变量 gl.vertexAttrib1f(a_PointSize,100.0);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>js改变顶点尺寸</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</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">
void main(){
gl_FragColor=vec4(1,1,0,1);
}
</script>
<script type="module">
import { initShaders } from "../jsm/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")
initShaders(gl, vsSource, fsSource)
const a_Position = gl.getAttribLocation(gl.program, "a_Position")
const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize")
// 修改 attribute 变量
gl.vertexAttrib1f(a_Position, 0.1)
gl.vertexAttrib1f(a_PointSize, 10)
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.drawArrays(gl.POINTS, 0, 1)
</script>
</body>
</html>鼠标随机改变顶点大小
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>随机改变顶点尺寸</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</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">
void main(){
gl_FragColor=vec4(1,1,0,1);
}
</script>
<script type="module">
import {initShaders} from "./jsm/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");
initShaders(gl, vsSource, fsSource);
// 设置attribute 变量
const a_Position = gl.getAttribLocation(gl.program, "a_Position");
const a_PointSize = gl.getAttribLocation(gl.program, "a_PointSize");
const a_points = [{x: 0, y: 0, size: 10}];
// 声明颜色 rgba
gl.clearColor(0, 0, 0, 1);
// 刷底色
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];
const size = Math.random() * 50 + 10;
a_points.push({x, y, size});
render();
});
// 渲染方法
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
a_points.forEach(({x, y, size}) => {
gl.vertexAttrib2f(a_Position, x, y);
gl.vertexAttrib1f(a_PointSize, size);
gl.drawArrays(gl.POINTS, 0, 1);
});
}
</script>
</body>
</html>无论是控制点位的尺寸,还是控制点位的位置,实际上都是对 attribute 变量的操控。但是想要再改变顶点的颜色呢?那就不能再用 attribute 限定符了,因为 attribute 限定符限定的就是顶点相关的数据
JS 控制顶点的颜色
首先我们要知道,限定颜色变量的限定符叫 uniform,翻译过来是一致、统一的意思
1.在片元着色器里把控制顶点颜色的变量暴露出来
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
</script>第一行的 precision mediump float 是对浮点数精度的定义,mediump 是中等精度的意思,这个必须要有,不然画不出东西来
2.在 js 中获取片元着色器暴露出的 uniform 变量 const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');
3.修改uniform 变量 gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>修改顶点颜色</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</style>
</head>
<body>
<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">
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.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")
initShaders(gl, vsSource, fsSource)
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
// 获取 attribute 变量
const a_Position = gl.getAttribLocation(gl.program, "a_Position")
// 获取 uniform 变量
const u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor")
// 修改 attribute 变量
gl.vertexAttrib1f(a_Position, 0.1)
// 修改 uniform 变量
gl.uniform4f(u_FragColor, 1, 0, 1, 1)
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1)
</script>
</body>
</html>鼠标随机改变顶点的颜色
使用 uniform4fv() 修改的顶点颜色
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>随机点</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</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;
uniform vec4 u_FragColor;
void main(){
gl_FragColor=u_FragColor;
}
</script>
<script type="module">
import { initShaders } from "../jsm/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")
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")
const a_points = [{ x: 0, y: 0, size: 10, color: { r: 1, g: 0, b: 0, a: 1 } }]
gl.clearColor(0, 0, 0, 1)
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]
const size = Math.random() * 50 + 10
const n = Math.random()
const color = { r: n, g: n, b: 1, a: 1 }
a_points.push({ x, y, size, color })
render()
})
// 渲染方法
function render() {
gl.clear(gl.COLOR_BUFFER_BIT)
a_points.forEach(({ x, y, size, color: { r, g, b, a } }) => {
gl.vertexAttrib2f(a_Position, x, y)
gl.vertexAttrib1f(a_PointSize, size)
// gl.uniform4f(u_FragColor, r, g, b, a);
// 颜色是随机产生的
const arr = new Float32Array([r, g, b, a])
gl.uniform4fv(u_FragColor, arr)
gl.drawArrays(gl.POINTS, 0, 1)
})
}
</script>
</body>
</html>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 高很多
案例-用鼠标绘制星空
用鼠标绘制圆形的顶点
星星的形状是圆形的,所以,我们需要绘制一个圆形的顶点。
<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 片元在一个点中的位置,此位置是被归一化的(宽高为 1)
- discard 丢弃,即不会一个片元进行渲染
着色器语法参考地址:https://www.khronos.org/registry/OpenGL-Refpages/gl4/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>绘制圆点</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background-color: antiquewhite;
}
</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;
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>
<script type="module">
import { initShaders } from "../jsm/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")
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")
const a_points = [{ x: 0, y: 0, size: 10, color: { r: 1, g: 0, b: 0, a: 1 } }]
gl.clearColor(0, 0, 0, 1)
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]
const size = Math.random() * 50 + 10
const n = Math.random()
const color = { r: n, g: n, b: 1, a: 1 }
a_points.push({ x, y, size, color })
render()
})
// 渲染方法
function render() {
gl.clear(gl.COLOR_BUFFER_BIT)
a_points.forEach(({ x, y, size, color: { r, g, b, a } }) => {
gl.vertexAttrib2f(a_Position, x, y)
gl.vertexAttrib1f(a_PointSize, size)
// gl.uniform4f(u_FragColor, r, g, b, a);
const arr = new Float32Array([r, g, b, a])
gl.uniform4fv(u_FragColor, arr)
gl.drawArrays(gl.POINTS, 0, 1)
})
}
</script>
</body>
</html>绘制随机透明度的星星
首先我们可以先给 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)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>绘制星空</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background: url("./images/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;
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>
<script type="module">
import { initShaders } from "../jsm/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)
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 = []
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]
const s = Math.random() * 5 + 2
const a = Math.random()
stars.push({ x, y, s, a })
render()
})
// 渲染方法
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>制作闪烁的繁星
当星星会眨眼睛,会变得灵动而可爱,接下来我要让星星对你眨眼睛
建立补间动画的意识
在这里推荐大家玩一下 AE,因为它可以让你对动画的运行原理和架构方式有一个具象的认知
比如,我在 AE 里画一颗星星,加几个关键帧,让它眨一下眼睛
在这里会涉及以下概念:
- 合成:多个时间轨的集合
- 时间轨:通过关键帧,对其中目标对象的状态进行插值计算
- 补间动画:通过两个关键帧,对一个对象在这两个关键帧之间的状态进行插值计算,从而实现这个对象在两个关键帧间的平滑过渡
架构代码
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)
})
}
}属性
- parent 父对象,合成对象可以相互嵌套
- children 子对象集合,其集合元素可以是时间轨,也可以是合成对象
方法:
- add(obj) 添加子对象方法
- 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 {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)
}
}
}
}属性
- 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.x
const b=fm1[1]-fm1[0]*k
return k*time+b
}
}
}-
getValBetweenFms(time,fms,last)其实现思路如下:
-
time 本地时间
-
fms 某个属性的关键帧集合
-
last 最后一个关键帧的索引位置
-
遍历所有关键帧
-
判断当前时间在哪两个关键帧之间
-
基于这两个关键帧的时间和状态,求点斜式
-
基于点斜式求本地时间对应的状态
使用合成对象和轨道对象制作补间动画
- 建立动画相关的对象
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);
})
}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>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>星星向你眨眼睛</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
background: url("./images/sky.jpg");
background-size: cover;
background-position: right bottom;
}
#audio {
position: absolute;
right: 20px;
bottom: 20px;
opacity: 10%;
transition: opacity 200ms;
z-index: 20;
}
#audio:hover {
opacity: 90%;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<audio id="audio" controls loop autoplay>
<source src="./audio/cef.mp3" type="audio/mpeg" />
</audio>
<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(){
float dist=distance(gl_PointCoord,vec2(0.5,0.5));
if(dist<0.5){
gl_FragColor=u_FragColor;
}else{
discard;
}
}
</script>
<script type="module">
import { initShaders } from "../jsm/Utils.js"
import Compose from "../jsm/Compose.js"
import Track from "../jsm/Track.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)
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()
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]
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)
})
// 自调用函数
!(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>