每个人的花期都不同,不用焦虑别人比你提前拥有。
写在最前
今天是六一儿童节,公司下班前在楼下举办了活动,我赶紧下楼活动了一下筋骨,别的不敢说,凑热闹这种事情我最在行了😄不过过节归过节,文还是要写的,开始吧。
捋捋逻辑
- 拖拽
- 贴图
- 自转
- 矩形
这几个功能在DOM操作上肯定是非常简单的,那遇到webGL该如何实现呢?
准备顶点数据
首先我们先完成绘制矩形这个功能:
遗憾的是drawArray并没有直接绘制矩形的API,它的做法是用两个三角形进行拼接形成矩形。
我们需要准备12个顶点。
-100, -100, 100, -100, -100, 100, 100, 100, 100, -100, -100, 100
由这6个点形成两个三角形也形成了矩形。细心的朋友可以看到P3、P6点 和 P2、P5点重合了,后续我们会以索引的方式进行改良,此文不详说。
转换坐标系
我们像往常一样先把基本的着色器写好。
static VERTEX_SHADER: string = `
attribute vec2 a_position;
uniform vec2 u_translation;
uniform vec2 u_resolution;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
vec2 position = (a_position + u_translation) / u_resolution * 2.0 - 1.0;
gl_Position = vec4(position * vec2(1,-1), 0,1);
v_texCoord = a_texCoord;
}
`;
仔细观察,我们会发现和昨天的着色器多了一些东西。
u_translation 位移常量
u_resolution 这里指的是屏幕宽高的变量
在上一篇文章中讲到,我们浏览器的坐标系和webGL坐标系并不一样。
如果想要使用习惯的浏览器坐标系,则需要将WebGL坐标系进行变换。
接下来举个例子,我们通常使用像素来表示位置(50,50)。
-
将500乘500的画布转为1乘1的坐标系,即该点需除以500,转换后它的位置为(0.1,0.1)。
-
浏览器坐标系是长度为1,而webGL为 -1到1长度为2 ,则扩大一倍 即 *2。
-
坐标系为0到2,若要变成-1到1,则坐标系往左移1,即 -1。
再然后因为WebGL坐标系Y轴与浏览器坐标轴相反,则需Y轴翻转,即 *-1。
由此得到以下程序完成坐标转换。
vec2 position = (a_position + u_translation) / u_resolution * 2.0 - 1.0;
gl_Position = vec4(position * vec2(1,-1), 0,1);
绘制矩形
和昨天学习的缓冲区对象一样,创建、绑定、赋值、开启一梭哈。
let uPosition = this.webgl.getAttribLocation(this.program!, "a_position");
// 设置顶点
let positionBuffer = this.webgl?.createBuffer()!;
this.webgl?.bindBuffer(this.webgl.ARRAY_BUFFER, positionBuffer);
this.webgl?.bufferData(
this.webgl.ARRAY_BUFFER,
new Float32Array(this.positionData),
this.webgl.STATIC_DRAW
);
this.webgl?.enableVertexAttribArray(uPosition);
this.webgl?.vertexAttribPointer(uPosition,2,this.webgl.FLOAT,false,0,0);
再把着色器定义的两个变量赋值。
// 设置长宽
this.webgl?.uniform2fv(u_resolution[this.canvas!.clientWidth,this.canvas!.clientHeight]);
// 设置位移
this.webgl?.uniform2fv(u_translation, [200 , 200]);
gl.uniform2fv 是给着色器uniform常量赋值的方法,其中2表示分量长度,f表示浮点数
我们拖拽会产生不同的位移数值,所以将位移设置为变量。
紧接着我们把与固定代码如初始化Shader、绘制drawArray跳过,最后会放上源码,此时我们就可以看到矩形的效果了
矩形呈现
目前来看,完成了4个需求里面的第一个,接着我们来看自转。
要想实现旋转,我们需要回到当年的数学知识:三角函数
我们来画一张图来解析一下旋转
我们要求得P旋转N°后的P1点。
通过三角函数得知: X2 = sin(b+a), Y2 = cos(b+a);
再通过和角公式可得:
X2 = sina⋅cosb+cosa⋅sinb
Y2 = cosa⋅cosb−sina⋅sinb
又得知:sin(a) = X1,cos(a) = Y1;所以最终可以得出
X2 = X1 * cos(radian) + Y1 * sin(radian)
Y2 = Y1 * cos(radian) - X1 * sin(radian)
所以我们可以写一个旋转函数:
let animate = () => {
let radian = (Math.PI / 180) * 1;
this.positionData = this.positionData.map((v, key) => {
let number = 0;
if (key % 2 === 0) {
number =
v * Math.cos(radian) -
this.positionData[key + 1] * Math.sin(radian);
} else {
number =
v * Math.cos(radian) +
this.positionData[key - 1] * Math.sin(radian);
}
return number;
});
this.draw();
requestAnimationFrame(animate);
};
通过旋转改变顶点位置,从而达到旋转效果。
至此我们就完成了第二个业务:自转。
添加纹理贴图
tips: 今天并不会详细的讲纹理方面的东西,主要还是实现功能,后面我们会有专门的文章进行学习
我们会发现在上面的顶点着色器有这两行代码
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
我们需要告诉矩形中每个顶点对应的纹理坐标,绘制点的顺序需要与顶点坐标一致。v_texCoord就是纹理坐标。
首先我们需要定义片元着色器
static FRAGMENT_SHADER: string = `
uniform sampler2D u_image;
precision mediump float;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
}
`;
新增陌生全局常量:u_image = 纹理单元,texture2D 寻找纹理的颜色值方法。
紧接着我们就需要给到纹理坐标赋值。
let image = new Image();
image.src = "/src/assets/image/2106311D2_0.jpg";
image.onload = () => {
var texCoordLocation = this.webgl!.getAttribLocation(
this.program!,
"a_texCoord"
);
let texCoordBuffer = this.webgl!.createBuffer();
this.webgl!.bindBuffer(this.webgl!.ARRAY_BUFFER, texCoordBuffer);
this.webgl!.bufferData(
this.webgl!.ARRAY_BUFFER,
new Float32Array([
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0
]),
this.webgl!.STATIC_DRAW
);
this.webgl!.enableVertexAttribArray(texCoordLocation);
this.webgl!.vertexAttribPointer(
texCoordLocation,
2,
this.webgl!.FLOAT,
false,
0,
0
);
var texture = this.webgl!.createTexture();
this.webgl!.bindTexture(this.webgl?.TEXTURE_2D!, texture);
this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_WRAP_S,this.webgl!.CLAMP_TO_EDGE);
this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_WRAP_T,this.webgl!.CLAMP_TO_EDGE);
this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_MIN_FILTER,this.webgl!.NEAREST);
this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_MAG_FILTER,this.webgl!.NEAREST);
// 将图像上传到纹理
this.webgl!.texImage2D(this.webgl!.TEXTURE_2D,0,this.webgl!.RGBA,this.webgl!.RGBA,this.webgl!.UNSIGNED_BYTE,image);
};
突然来了这么一长串代码,请不要慌张。
- 首先我们需要等待图片加载完成之后才能操作,所以需要在image.onLoad函数中执行绑定纹理贴图。
- 从代码中我们可以看到老朋友缓冲区对象,将矩形提供纹理坐标。纹理坐标需与顶点绘制顺序一致。
- 纹理贴图步骤:createTexture(创建纹理)=》bindTexture(绑定纹理) =》texParameteri(设置纹理参数)=》(texImage2D)将图像上传到纹理。
完成加载图像纹理后效果如下:
至此:我们完成了第三个业务:纹理贴图。
还剩下最后一个拖拽,相信在座的童鞋们肯定不在话下,我也就不在此献丑了,献上拖拽代码:
judgeEventInPoint(eventX, eventY) {
let sizeX = 100;
let sizeY = 100;
if (
eventX > this.translateX - sizeX &&
eventX < this.translateX + sizeX &&
eventY > this.translateY - sizeY &&
eventY < this.translateY + sizeY
) {
return true;
} else {
return false;
}
}
addDragHandle() {
if (this.webgl) {
if (this.canvas) {
this.canvas.onmousedown = downEvent => {
let x = downEvent.clientX;
let y = downEvent.clientY;
let offsetLeft = this.translateX - x;
let offsetTop = this.translateY - y;
if (this.judgeEventInPoint(x, y)) {
window.onmousemove = e => {
console.log(this.translateX - x);
this.translateX = e.clientX + offsetLeft;
this.translateY = e.clientY + offsetTop;
};
window.onmouseup = () => {
window.onmousemove = null;
window.onmouseup = null;
};
}
};
}
}
}
主要逻辑是点击判断是否落在矩形上,随后拖动改变位移 u_resolution的值,从而完成拖拽功能。
效果如下:
好了,以上4个功能点全部完成,可以看到效果还是不错的!
最后
这一章主要还是过了一遍位移和旋转还有纹理的一些知识,可以看到旋转需要每次更改顶点位置,但其实有更好的方式,相信有人已经猜到了,那就是矩阵!敬请期待。
夜深了,写完后感觉收获颇多,很多在学习的时候含糊不清的东西,写完以后也是清晰了很多。
好了,买瓶可乐续下命,下篇文章见!
源码如下:
<template>
<div>
<canvas class="canvas" width="500" height="500"></canvas>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
class GL {
static VERTEX_SHADER: string = `
attribute vec2 a_position;
uniform vec2 u_translation;
uniform vec2 u_resolution;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
vec2 position = (a_position + u_translation) / u_resolution * 2.0 - 1.0;
gl_Position = vec4(position * vec2(1,-1), 0,1);
v_texCoord = a_texCoord;
}
`;
static FRAGMENT_SHADER: string = `
uniform sampler2D u_image;
precision mediump float;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
}
`;
public translateX: number = 200;
public translateY: number = 200;
public webgl: WebGLRenderingContext | null = null;
public canvas: HTMLCanvasElement | null = null;
public program: WebGLProgram | null = null;
public positionData: number[] = [
-100, -100, 100, -100, -100, 100, 100, 100, 100, -100, -100, 100
];
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.webgl = canvas.getContext("webgl");
this.webgl!.viewport(0, 0, canvas.clientWidth, canvas.clientHeight);
}
init() {
this.addDragHandle();
this.initShader();
this.draw();
this.loadTexture();
let animate = () => {
let radian = (Math.PI / 180) * 1;
this.positionData = this.positionData.map((v, key) => {
let number = 0;
if (key % 2 === 0) {
number =
v * Math.cos(radian) -
this.positionData[key + 1] * Math.sin(radian);
} else {
number =
v * Math.cos(radian) +
this.positionData[key - 1] * Math.sin(radian);
}
return number;
});
this.draw();
requestAnimationFrame(animate);
};
animate();
}
judgeEventInPoint(eventX, eventY) {
let sizeX = 100;
let sizeY = 100;
if (
eventX > this.translateX - sizeX &&
eventX < this.translateX + sizeX &&
eventY > this.translateY - sizeY &&
eventY < this.translateY + sizeY
) {
return true;
} else {
return false;
}
}
loadTexture() {
let image = new Image();
image.src = "/src/assets/image/2106311D2_0.jpg";
image.onload = () => {
var texCoordLocation = this.webgl!.getAttribLocation(
this.program!,
"a_texCoord"
);
let texCoordBuffer = this.webgl!.createBuffer();
this.webgl!.bindBuffer(this.webgl!.ARRAY_BUFFER, texCoordBuffer);
this.webgl!.bufferData(
this.webgl!.ARRAY_BUFFER,
new Float32Array([
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0
]),
this.webgl!.STATIC_DRAW
);
this.webgl!.enableVertexAttribArray(texCoordLocation);
this.webgl!.vertexAttribPointer(
texCoordLocation,
2,
this.webgl!.FLOAT,
false,
0,
0
);
var texture = this.webgl!.createTexture();
this.webgl!.bindTexture(this.webgl?.TEXTURE_2D!, texture);
this.webgl!.texParameteri(
this.webgl!.TEXTURE_2D,
this.webgl!.TEXTURE_WRAP_S,
this.webgl!.CLAMP_TO_EDGE
);
this.webgl!.texParameteri(
this.webgl!.TEXTURE_2D,
this.webgl!.TEXTURE_WRAP_T,
this.webgl!.CLAMP_TO_EDGE
);
this.webgl!.texParameteri(
this.webgl!.TEXTURE_2D,
this.webgl!.TEXTURE_MIN_FILTER,
this.webgl!.NEAREST
);
this.webgl!.texParameteri(
this.webgl!.TEXTURE_2D,
this.webgl!.TEXTURE_MAG_FILTER,
this.webgl!.NEAREST
);
// 将图像上传到纹理
this.webgl!.texImage2D(
this.webgl!.TEXTURE_2D,
0,
this.webgl!.RGBA,
this.webgl!.RGBA,
this.webgl!.UNSIGNED_BYTE,
image
);
};
}
addDragHandle() {
if (this.webgl) {
if (this.canvas) {
this.canvas.onmousedown = downEvent => {
let x = downEvent.clientX;
let y = downEvent.clientY;
let offsetLeft = this.translateX - x;
let offsetTop = this.translateY - y;
if (this.judgeEventInPoint(x, y)) {
window.onmousemove = e => {
console.log(this.translateX - x);
this.translateX = e.clientX + offsetLeft;
this.translateY = e.clientY + offsetTop;
};
window.onmouseup = () => {
window.onmousemove = null;
window.onmouseup = null;
};
}
};
}
}
}
initShader() {
if (this.webgl) {
let vertexShader = this.webgl.createShader(this.webgl.VERTEX_SHADER)!;
let fragmentShader = this.webgl.createShader(this.webgl.FRAGMENT_SHADER)!;
this.webgl.shaderSource(vertexShader, GL.VERTEX_SHADER);
this.webgl.shaderSource(fragmentShader, GL.FRAGMENT_SHADER);
this.webgl.compileShader(vertexShader);
this.webgl.compileShader(fragmentShader);
let program = this.webgl.createProgram()!;
this.webgl.attachShader(program, vertexShader);
this.webgl.attachShader(program, fragmentShader);
if (
!this.webgl.getShaderParameter(vertexShader, this.webgl.COMPILE_STATUS)
) {
console.log(this.webgl.getShaderInfoLog(vertexShader));
}
this.webgl.linkProgram(program);
this.webgl.useProgram(program);
if (!this.webgl.getProgramParameter(program, this.webgl.LINK_STATUS)) {
var info = this.webgl.getProgramInfoLog(program);
console.log(info);
}
this.program = program;
}
}
draw() {
if (this.webgl) {
let uPosition = this.webgl.getAttribLocation(this.program!, "a_position");
let u_resolution = this.webgl?.getUniformLocation(
this.program!,
"u_resolution"
);
let u_translation = this.webgl?.getUniformLocation(
this.program!,
"u_translation"
);
// 设置顶点
let positionBuffer = this.webgl?.createBuffer()!;
this.webgl?.bindBuffer(this.webgl.ARRAY_BUFFER, positionBuffer);
this.webgl?.bufferData(
this.webgl.ARRAY_BUFFER,
new Float32Array(this.positionData),
this.webgl.STATIC_DRAW
);
this.webgl?.enableVertexAttribArray(uPosition);
this.webgl?.vertexAttribPointer(
uPosition,
2,
this.webgl.FLOAT,
false,
0,
0
);
// 设置长宽
this.webgl?.uniform2fv(u_resolution, [
this.canvas!.clientWidth,
this.canvas!.clientHeight
]);
// 设置位移
this.webgl?.uniform2fv(u_translation, [this.translateX, this.translateY]);
// 绘制
this.webgl?.clearColor(0.0, 0.0, 0.0, 1.0);
this.webgl?.clear(
this.webgl.COLOR_BUFFER_BIT | this.webgl.DEPTH_BUFFER_BIT
);
this.webgl?.drawArrays(
this.webgl.TRIANGLES,
0,
new Float32Array(this.positionData).length / 2
);
}
}
}
onMounted(() => {
let gl = new GL(document.querySelector(".canvas") as HTMLCanvasElement);
gl.init();
});
</script>
<style scoped></style>