下边准备了线立方体和面立方体的实现方式。他们的主要不同点也是在于数据构造和绘制方式上。绘制立方体个人感觉你要学会自己自己去构造立方体所需要的数据。明白为什么么要构造出这种数据格式。关于物体的旋转矩阵的构造的原理后面会细说,先学会用就行。
在这片文章中用到了新的GLSL
知识。
- mat4:4维矩阵
- gl.drawArrays(gl.LINE_LOOP,0,4): 以闭合线的形式绘制一个面
- gl.drawArrays(gl.LINES,8,8):绘制线条。
# 1、面立方体
- 顶点着色器代码
<script id="vertexShader" type="x-shader/x-vertex">
//浮点数设置为中等精度
precision mediump float;
attribute vec4 apos;
attribute vec4 acolor;
varying vec4 vcolor;
//矩阵变量
uniform mat4 mx;
//矩阵变量
uniform mat4 my;
void main() {
//两个旋转矩阵、顶点齐次坐标连乘
gl_Position = mx*my*apos;
vcolor=acolor;
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 片元着色器代码
<script id="fragmentShader" type="x-shader/x-fragment">
// 所有float类型数据的精度是lowp
precision mediump float;
varying vec4 vcolor;
void main() {
gl_FragColor =vcolor;
}
</script>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- javascript代码部分
WebGL
上下文获取
function init() { //通过getElementById()方法获取canvas画布 const canvas = document.getElementById('WebGL'); //通过方法getContext()获取WebGL上下文 const gl = canvas.getContext('WebGL'); //顶点着色器源码 const vertexShaderSource = document.getElementById('vertexShader').innerText; //片元着色器源码 const fragShaderSource = document.getElementById('fragmentShader').innerText; //初始化着色器 const program = initShader(gl, vertexShaderSource, fragShaderSource); initBuffer(gl, program); }
1
2
3
4
5
6
7
8
9
10
11
12
13- 声明初始化着色器函数
function initShader(gl, vertexShaderSource, fragmentShaderSource) { const vertexShader = gl.createShader(gl.VERTEX_SHADER); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(vertexShader, vertexShaderSource); gl.shaderSource(fragmentShader, fragmentShaderSource); gl.compileShader(vertexShader); gl.compileShader(fragmentShader); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); return program; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15- 着色器变量获取及赋值
//着色器变量获取及赋值 function initBuffer(gl, program) { //获取顶点着色器的位置变量apos const aposLocation = gl.getAttribLocation(program, 'apos'); const acolor = gl.getAttribLocation(program, 'acolor'); //创建立方体的顶点坐标数据 const data = new Float32Array([ -0.5, -0.5, 0.5, 1, 0, 0, 1, 0.5, -0.5, 0.5, 1, 0, 0, 1, 0.5, 0.5, 0.5, 1, 0, 0, 1, -0.5, 0.5, 0.5, 1, 0, 0, 1, -0.5, 0.5, 0.5, 0, 1, 0, 1, -0.5, 0.5, -0.5, 0, 1, 0, 1, -0.5, -0.5, -0.5, 0, 1, 0, 1, -0.5, -0.5, 0.5, 0, 1, 0, 1, 0.5, 0.5, 0.5, 0, 0, 1, 1, 0.5, -0.5, 0.5, 0, 0, 1, 1, 0.5, -0.5, -0.5, 0, 0, 1, 1, 0.5, 0.5, -0.5, 0, 0, 1, 1, 0.5, 0.5, -0.5, 1, 0, 1, 1, 0.5, -0.5, -0.5, 1, 0, 1, 1, -0.5, -0.5, -0.5, 1, 0, 1, 1, -0.5, 0.5, -0.5, 1, 0, 1, 1, -0.5, 0.5, 0.5, 1, 1, 0, 1, 0.5, 0.5, 0.5, 1, 1, 0, 1, 0.5, 0.5, -0.5, 1, 1, 0, 1, -0.5, 0.5, -0.5, 1, 1, 0, 1, -0.5, -0.5, 0.5, 0, 1, 1, 1, -0.5, -0.5, -0.5, 0, 1, 1, 1, 0.5, -0.5, -0.5, 0, 1, 1, 1, 0.5, -0.5, 0.5, 0, 1, 1, 1, ]); //顶点索引数据构造 const indexdata = new Uint16Array([ 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23 ]) vertexBuffer(gl,data,aposLocation,3,'',4*7,0); vertexBuffer(gl,indexdata,acolor,4,'index',4*7,12); render(gl, program, indexdata.length) } //判断是否是顶点索引还是常规方式,并创建缓冲区 function vertexBuffer(gl,data,position,n,type,rowCount=0,offset=0){ //创建缓冲区对象 const buffer = gl.createBuffer(); if(type==='index'){ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW); }else{ //绑定缓冲区对象 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); //顶点数组data数据传入缓冲区 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); } ///缓冲区中的数据按照一定的规律传递给位置变量apos gl.vertexAttribPointer(position, n, gl.FLOAT, false, rowCount, offset); //允许数据传递 gl.enableVertexAttribArray(position); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68WebGL
渲染
在渲染时,有以下两点需要注意。 1、在绘制立方体时,需要对立方体旋转一定角度,才能看出立方体的三维效果,不然只能看到一个面。 2、在渲染时,要开启深度测试,不然看到的立方体不是我们想看到的效果。
function render(gl, program, count=36) { /**执行绘制之前,一定要开启深度测试,以免颜色混乱 * gl.CULL_FACE:表示隐藏正面 * gl.DEPTH_TEST:启用深度测试。根据坐标的远近自动隐藏被遮住的图形 * **/ gl.enable(gl.DEPTH_TEST); // gl.enable(gl.CULL_FACE); //旋转矩阵 tranlate(gl, program); //设置清屏颜色为黑色。 gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, 0); } //构造矩阵,使矩阵向x,y分别旋转30度 let angle=30.0; function tranlate(gl, program) { const rad = angle * Math.PI / 180; const cos = Math.cos(rad); const sin = Math.sin(rad); const mx = gl.getUniformLocation(program, 'mx'); const mxArr = new Float32Array([ 1, 0, 0, 0, 0, cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1 ]) gl.uniformMatrix4fv(mx, false, mxArr); const my = gl.getUniformLocation(program, 'my'); const myArr = new Float32Array([ cos, 0, -sin, 0, 0, 1, 0, 0, sin, 0, cos, 0, 0, 0, 0, 1 ]) gl.uniformMatrix4fv(my, false, myArr); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 2、线立方体
- 顶点着色器代码
<script id="vertexShader" type="x-shader/x-vertex">
//attribute声明vec4类型变量apos
attribute vec4 apos;
uniform mat4 mx;
uniform mat4 my;
void main() {
//两个旋转矩阵、顶点齐次坐标连乘
gl_Position = mx*my*apos;
gl_PointSize=10.0;
}
</script>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 片元着色器代码
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
// 逐片元处理数据,所有片元(像素)设置为红色
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
</script>
1
2
3
4
5
6
2
3
4
5
6
- javascript代码部分
它跟面立方体的不同主要在于数据的构造和渲染方面。线立方体的数据构造相对来说简单一些,没有用到顶点索引,是正常的顶点数据构造。
- 着色器的变量获取及赋值
function initbuffer(gl, program){ //获取顶点着色器的位置变量apos var aposLocation = gl.getAttribLocation(program, 'apos'); //创建立方体的顶点坐标数据 var data = new Float32Array([ //z为0.5时,xOy平面上的四个点坐标 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, //z为-0.5时,xOy平面上的四个点坐标 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, //上面两组坐标分别对应起来组成一对 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, ]); //创建缓冲区对象 var buffer = gl.createBuffer(); //绑定缓冲区对象 gl.bindBuffer(gl.ARRAY_BUFFER, buffer); //顶点数组data数据传入缓冲区 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); //缓冲区中的数据按照一定的规律传递给位置变量apos gl.vertexAttribPointer(aposLocation, 3, gl.FLOAT, false, 0, 0); //允许数据传递 gl.enableVertexAttribArray(aposLocation); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40WebGL
渲染
function render(gl, program) { tranlate(gl,program); //设置清屏颜色为黑色。 gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); //LINE_LOOP模式绘制前四个点 gl.drawArrays(gl.LINE_LOOP,0,4); //LINE_LOOP模式从第五个点开始绘制四个点 gl.drawArrays(gl.LINE_LOOP,4,4); //LINES模式绘制后8个点 gl.drawArrays(gl.LINES,8,8); }
1
2
3
4
5
6
7
8
9
10
11
12
# 3、立方体的旋转
通过不断改变x、y、z
轴的坐标来实现立方体的旋转。只需要在上面translate
和render
修改一下就行。
- 修改后的
translate
let angle=30.0;
function tranlate(gl, program) {
const rad = angle * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
const mx = gl.getUniformLocation(program, 'mx');
const mxArr = new Float32Array([
1, 0, 0, 0, 0, cos, -sin, 0, 0, sin, cos, 0, 0, 0, 0, 1
])
gl.uniformMatrix4fv(mx, false, mxArr);
const my = gl.getUniformLocation(program, 'my');
const myArr = new Float32Array([
cos, 0, -sin, 0, 0, 1, 0, 0, sin, 0, cos, 0, 0, 0, 0, 1
])
gl.uniformMatrix4fv(my, false, myArr);
//每次渲染时,沿x轴和y轴旋转1度
angle+=1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 修改后的
render
function render(gl, program, count=36) {
tranlate(gl, program);
//设置清屏颜色为黑色。
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, 0);
//调用requestAnimationFrame动画函数,使立方体动起来
requestAnimationFrame(()=>{
render(gl, program,count);
})
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
参考
WebGL零基础入门教程(郭隆邦) (opens new window)
WebGL 入门与实践 (opens new window)
WebGL官方文档 (opens new window)