别人能掌握的点,都不算难点。
写在前面
在很早之前偶然刷到一个在页面上运行的 3D 效果,顿时感觉平时切的图弱爆了有木有,后来百度了解一番后自然而然地找到了 THREEJS 框架,当时看文档那叫一个晕头转向啊,什么灯光、照相机一些平常能够接触到的东西怎么组合起来就看起来这么陌生呢?
在兴趣的驱使之下,硬着头皮学了一段时间。不入门不知道,一入门发现前面有着汪洋大海般庞大的知识体系在等着我。 而那座大海称之为 “图形学”,这可是计算机三大魅力之一啊!
它固然很迷人,但在理性(懒惰)的思考之后,给出的结论是目前学习的是前端开发,不能走偏了路线,应该把前端知识学透才好。就这样给到自己一个放弃的理由之后,学习 WebGL 这个计划就打消了,转而开始研究一些 CSS3 的滤镜、混合、渐变等等的知识,但最近B站 3D 的推送层出不穷,我下定决心,要把 WebGL 学明白,就这样,这个系列就这样产生了。
WebGl的运行步骤
众所周知,WebGL是在GPU上运行的, 因此你需要使用能够在GPU上运行的代码。下面是一些入门的关键词
- 着色程序
由顶点着色器和片段着色器和GlSL组成,千万不要被几个高大上的名词所吓到,其实从字面意义上来看用白话来说,顶点着色器决定像素点的位置,而片段着色器来决定像素点的颜色。
先上一段代码吧。
-
- function createShader(gl, type, source) {
- var shader = gl.createShader(type);
- gl.shaderSource(shader, source);
- gl.compileShader(shader);
- var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
- if (success) {
- return shader;
- }
- }
读代码就跟读英语是差不多的,只要你掌握了一些关键名词的单词,那你就很容易入手。
- 着色器:Shader
- 片段:Fragment
- 顶点:Vertex
知道上面意思之后,很容易看出创建Shader的流程是:
创建着色器对象 (createShader) =》提供数据源(shaderSource) =》 编译(compileShader)
创建好Shader以后,需要将两个着色器组成一个找着色程序,代码如下:
- function createProgram(gl, vertexShader, fragmentShader) {
- var program = gl.createProgram();
- gl.attachShader(program, vertexShader);
- gl.attachShader(program, fragmentShader);
- gl.linkProgram(program);
- var success = gl.getProgramParameter(program, gl.LINK_STATUS);
- if (success) {
- return program;
- }
- }
依然是单词时间:
- Program:程序
- attach:附加
- link:链接
很容易看出将着色器组成着色程序的流程是:
创建程序(createProgram) =》 附加着色器(attachShader) =》 链接(linkProgram)
而在上面的 createShader 函数中,第三个参数source 就是GLSL字符串,千万也不要被这个陌生的名词所吓到,他仅仅只是一门不同语法的强类型语言而已。
先来一段代码
- // 一个属性值,将会从缓冲中获取数据
- attribute vec4 a_position;
- void main() {
- gl_Position = a_position;
gl_PointSize = 60.0;
- }
今天是不准备讲GLSL语法,只是简单的过一下,你只需要了解GLSL相当于一个函数,vec4是值的类型,a_position是变量名,attribute是变量的类型。从代码可以得出,定义了一个 数据类型为vec4的名为a_position的attribute
而这个attribute还有三个同胞,分别为uniforms(全局变量)、Varying(可变量)、Textures(纹理)。
- uniforms,全局变量,必须在void main函数之前声明,可以通过JavaScript程序给其进行赋值。如果两个着色器同时声明同一个全局变量,则双方都可用。
- Varying,可变量,用于顶点着色器向片段着色器传值,需声明同一变量名。
- attribute: 属性变量,通常用于声明顶点数据,只能在顶点着色器中使用
- texture:贴图变量,这个还没学到。。
既然我们在GLSL声明了这么多变量,那该如何给其赋值呢?
首先是attribute,Javascript可以使用 getAttribLocation方法来获取GLSL中的attribute
var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
准备顶点数据并赋值
var positions = new Float32Array([0.5, 0.0, 0.0, 1.0]);
gl.vertexAttrib4fv(a_Position,positions);
Float32Array是用于存储浮点型的类型化数组,只能存储Nubmer类型,如设置其他类型如String,Array等会被转成NaN。
为什么会出现这种类型化数组,主要还是以下原因
- 浏览器通过
WebGL
与显卡进行通信,因此对性能要求较高,传统的Array动态数组无法满足 - 传统的Array可以存储任何类型的值,因此无法直接操作内存确定数据所占字节大小
- 有一些程序数据的交互通过二进制数据会更加快速,比如上面的显卡通信以及
webSockets
等
现在已经将顶点数据、着色程序等都准备就绪,可以绘制了。
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
WebGLRenderingContextBase.drawArrays(mode: number, first: number, count: number): void
从上面结构可以看出有三个参数,mode参数是决定绘制模式的,如点(gl.Points)、线(gl.Lines)、三角形(gl.TRIANGLES) 与其中顶点之间的连接方式,以后会讲到。 first参数从第几个点开始绘制, 可以理解为偏移(offset), count参数为绘制点的数量。
接下来我们运行起来看看,效果如下图
没错,你没看错,写了一连串代码,最后就只得到一个用Canvas2D ctx.rect一行代码就能画出的点。
但如果仔细思考一下,以上代码动了脑筋的只有顶点数据和定义点的大小。
源码如下
<template>
<canvas id="canvas" width="500" height="500"></canvas>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
onMounted(() => {
let gl = new Gl();
gl.init();
});
class Gl {
public webgl: WebGLRenderingContext | null = null;
public program: WebGLProgram | null = null;
static vsString = `
attribute vec3 a_position;
void main(){
gl_Position = vec4(a_position,1.0);
gl_PointSize = 60.0;
}
`;
static fsString = `
precision mediump float;
void main(){
gl_FragColor = vec4(1, 0, 0.5, 1);
}
`;
constructor() { }
init() {
this.initGl();
this.initShader();
this.initBuffer();
this.draw();
}
initGl() {
let canvas = document.getElementById("canvas") as HTMLCanvasElement;
this.webgl = canvas.getContext("webgl");
this.webgl?.viewport(0, 0, canvas.clientWidth, canvas.clientHeight);
}
initShader() {
let gl = this.webgl;
if (gl) {
let vsShader = gl.createShader(gl.VERTEX_SHADER)!;
let fsShader = gl.createShader(gl.FRAGMENT_SHADER)!;
gl.shaderSource(vsShader, Gl.vsString);
gl.shaderSource(fsShader, Gl.fsString);
gl.compileShader(vsShader);
gl.compileShader(fsShader);
let program = gl.createProgram()!;
gl.attachShader(program, vsShader);
gl.attachShader(program, fsShader);
gl.linkProgram(program);
gl.useProgram(program);
this.program = program;
}
}
initBuffer() {
if (this.webgl) {
let pointPosition = new Float32Array([0.2, 0.0, 0.0, 1.0]);
let aPosition = this.webgl.getAttribLocation(this.program!, "a_position");
this.webgl.vertexAttrib4fv(aPosition, pointPosition);
}
}
draw() {
let gl = this.webgl;
if (gl) {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
}
}
}
</script>
<style scoped></style>
总结
这一篇想要表达的更多的是学习WebGL的方式与部分最为基础的API,在学习代码前先了解大致的绘制过程,然后下笔才会如有神助的效果。。