【读书笔记】WebGL Programming Guide
Your First Step with WebGL
What Is a Canvas
DrawRectangle.html
1 |
|
DrawRectangle.js
1 | function main() |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/DrawRectangle.html
canvas的坐标系:以左上角为原点,x正半轴向右,y正半轴向下。
The World’s Shortest WebGL Program: Clean Drawing Area
需要下载一些库:https://github.com/hushhw/WebGL-Programming-Guide/tree/master/lib
HelloCanvas.html
1 |
|
HelloCanvas.js
1 | function main() { |
getWebGLContext()函数是三个库文件定义的方法,防止一些浏览器间的差异
WebGL有三个Buffer:COLOR_BUFFER_BIT DEPTH_BUFFER_BIT STENCIL_BUFFER_BIT
Buffer Name | Default Value | Setting Method |
---|---|---|
Color Buffer | (0, 0, 0, 0) | clearColor(r, g, b, a) |
Depth Buffer | 1.0 | clearDepth(depth) |
Stencil Buffer | 0 | clearStencil(s) |
Draw a Point (V1)
需要用shader写:
HelloPoint.js
1 | // Vertex Shader |
vertex shader确定点的位置和半径,fragment shader设置点的颜色,drawArrays绘制
drawArrays(mode, first, count)
- mode: POINTS, LINES, LINE_STRIP, LINE_LOOP, TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN
- first: 开始绘制的顶点
- count: 绘制的总顶点数量
执行这个函数的时候,顶点着色器被执行count次,,每次计算一个顶点。
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/HelloPoint1.html
从文件读入shader
不过这么用字符串写shader也太复杂了,所以我自己实现新建了一个库函数文件,在里面实现了从文件读入shader的函数(书的附录F):
MyLoadShader.js
1 | // Load shader from file |
将原来的js文件进行更改(调用函数,实现start函数):
1 | // Vertex Shader |
再使用glsl实现顶点着色器和片元着色器:
HelloPoint1.vert
1 | void main() |
HelloPoint1.frag
1 | void main() |
这时,使用chorme直接打开,按F12在控制台会报错,出现加载本地文件跨域的情况。按照 https://zhuanlan.zhihu.com/p/359881121 解决就好了。
或者也可以开一个本地 server。
WebGL坐标系
对于画布而言,Y轴向上的右手系。画布最左边X为-1,最右边为1,上下则是Y。
Draw a Point (V2)
通过外部传入将参数传入shader
在shader中声明全局变量:
1
2
3
4
5
6attribute vec4 a_Position;
void main()
{
gl_Position = a_Position;
gl_PointSize = 10.0;
}其中 attribute 是储存占位符
- attribute:与顶点有关的数据,只能在顶点着色其中使用;
- uniform:与顶点无关,或者所有顶点都相同的数据;
JS中获得变量储存地址
1
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
返回值为-1时表示变量不存在,或者命名有 gl_ 或 webgl_ 前缀
传入值
1
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
- gl.vertexAttrib1f (location, v0);
- gl.vertexAttrib2f (location, v0, v1);
- gl.vertexAttrib3f (location, v0, v1, v2);
- gl.vertexAttrib4f (location, v0, v1, v2, v3);
使用v时,传入一个数组,前面的数字代表数组长度;
Draw a Point with Mouse Click
创建结构体:
1 | function Point(x, y) |
绑定点击事件:
1 | canvas.onmousedown = function(event){ click(event, a_Position); }; |
结果:
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/ClickPoints.html
Change the Point Color
通过声明在 Fragment Shader 中的 Uniform 变量,控制点的颜色。
1 | precision mediump float; |
1 | var u_PointColor = gl.getUniformLocation(gl.program, 'u_PointColor'); |
其中 getUniformLocation 返回空时(不是-1)表示变量获取失败。
这里简单的根据坐标上色:
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/2_YourFirstStepwithWebGL/ColoredPoints.html
Drawing and Transforming Triangles
Drawing Multiple Points
使用 Buffer 对象储存顶点:
1 | function initVertexBuffers(gl) |
结果:
通过 Buffer 对象传递多个数据的步骤:
创建 Buffer 对象
1
2gl.createBuffer(); // 创建一个 Buffer Object
gl.deleteBuffer(buffer); // 删除一个 Buffer Object绑定对象到目标
1
gl.bindBuffer(target, buffer);
target 用于表示 Buffer 对象中包含的数据类型:
- gl.ARRAY_BUFFER: 包含顶点数据
- gl.ELEMENT_ARRAY_BUFFER: 包含顶点索引值
向对象中写入数据
1
gl.bufferData(target, buffer, usage);
其中usage表示程序该如何优化性能,不会导致程序错误:
- gl.STATIC_DRAW: 数据被传入一次, 绘制很多次。
- gl.STREAM_DRAW: 数据传入一次, 绘制若干次。
- gl.DYNAMIC_DRAW: 数据不断传入,不断绘制。
JS原生的 Array 没有对处理大量同类型数据做优化,所以这里引入了类型化数组。
将对象分配给 attribute 变量
gl.vertexAttribPointer(location, size, type, normalized, stride, offset)
将整个 Buffer 对象的指针分配给 attribute 变量。
location: 待分配的 attribute 变量的位置
size: 每个顶点的分量个数(1 - 4)
type:
gl.UNSIGNED_BYTE: Uint8Array
gll.SHORT: Int16Arrat
gl.UNSIGNED_SHORT: Uint16Array
gl.INT: Int32Array
gl.UUNSIGNED_INT: Uint32Array
gl.FLOAT: Float32Array
normalized: 是否被归一化
stride: 不同顶点间的字节数量,默认是0
offset: attribute 变量的值从何处开始储存,默认是0
启用 attribute 变量
gl.enableVertexAttribArray(position):开启数组(作用于 Buffer 对象)。
gl.disableVertexAttribArray(location):关闭数组
Hello Triangle
只需要将 drawArray 的第一个参数更改为gl.TRIANGLES,然后将顶点着色器中的 PointSize 行删除:
基础形状
模式 | 介绍 |
---|---|
gl.POINTS | 不相关的点,绘制顺序:v0, v1, v2··· |
gl.LINES | 不相关的线,绘制顺序:(v0, v1), (v2, v3)··· 如果有奇数个点最后一个点会被忽略 |
gl.LINE_STRIP | 相连的线,绘制顺序:(v0, v1), (v1, v2)··· |
gl.LINE_LOOP | 与strip类似,只是最后一个点会与第一个点相连 |
gl.TRIANGLES | 不相关的三角形, 绘制顺序:(v0, v1, v2), (v3, v4, v5)··· 剩下的点会被忽略 |
gl.TRIANGLE_STRIP | 三角形条,绘制顺序:(v0, v1, v2), (v2, v1, v3), (v2, v3, v4)··· |
gl.TRIANGLE_FAN | 三角形扇,绘制顺序:(v0, v1, v2), (v0, v2, v3), (v0, v3, v4)··· |
类型化数组
Typed Array | Number of Bytes per Element | Description(C) |
---|---|---|
Int8Array | 1 | 8-bit signed integer |
Uint8Array | 1 | 8-bit unsigned integer |
Int16Array | 2 | 16-bit signed integer |
Uint16Array | 2 | 16-bit unsigned integer |
Int32Array | 4 | 32-bit signed integer |
Uint32Array | 4 | 32-bit unsigned integer |
Float32Array | 4 | 32-bit floating point number |
Float64Array | 8 | 64-bit floating point number |
类型化数组不支持push和pop方法。
方法、属性和常量 | 描述 |
---|---|
get(i) | 获取第 i 个元素 |
set(i, value) | 设置第i各元素的值为value |
set(array, offset) | 从第offset个元素开始将array数组填充进去 |
length | 长度 |
BYTE_PER_ELEMENT | 每个元素的字节数 |
More Transformations and Basic Animation
Translate and Then Rotate
首先手写旋转矩阵实现一下:
先在顶点着色其中引入变换矩阵:
1 | attribute vec4 a_Position; |
然后在JS中引入变量:
1 | var angle = 45; |
然后引入矩阵变换。由于WebGL没有自带的矩阵库,所以这里使用为本书制作的矩阵库:cuon-matrix.js。
1 | var transformMatrix = new Matrix4(); |
注意这个库里的矩阵变换是在右边乘,所以实际顺序是反过来的。
Animation
制作每帧运行的动画效果
1 | var tick = function() |
1 | function animate(angle) |
现在三角形就可以动起来了。
Using Colors and Texture Images
Passing Other Types of Information to Vertex Shader
1 | function initVertexBuffers(gl) |
或者也可以这么写:
1 | var n = 3; |
Color Triangle
通过varying变量从顶点着色器传值到片元。
1 | attribute vec4 a_Position; |
1 | precision mediump float; |
Pasting an Image onto a Rectangle
在顶点和片元着色其中传递 UV 坐标
1 | attribute vec4 a_Position; |
1 | precision mediump float; |
将顶点坐标和顶点 UV 坐标传入shader
1 | var n = 4; |
创建贴图并且加载贴图:
1 | function initAndLoadTexture(gl, n) { |
使用 gl.CreatTexture() 创建 Texture 对象,然后让浏览器加载图片
因为 WebGl的纵轴和图片纵轴相反,所以需要翻转Y轴:
gl.pixelStorei(pname, param)
pname
gl.UNPACK_FLIP_Y_WEBGL:加载图像后,翻转图像Y轴
gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL:把RGB分别乘以A
param:0位假,非0为真。
开启贴图单元:gl.activeTexture(texUnit)
- texUnit:gl.TEXTURE0 - gl.TEXTURE7(可能会更多)
指定贴图类型:gl.bindTexture(target, texture)
- target:gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP
设置贴图参数:gl.texParameteri(target, pname, param)
target: gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP
pname:
pname description default value gl.TEXTURE_MAG_FLITER 贴图放大采样方法 gl.LINEAR gl.TEXTURE_MIN_FLITER 贴图缩小采样方法 gl.NEAREST_MIPMAP_LINEAR gl.TEXTURE_WRAP_S 纵向 warp 模式 gl.REPEAT gl.TEXTURE_WRAP_TA 横向 warp 模式 gl.REPEAT param:
对于贴图采样方法:
gl.LINEAR, gl.NEAREST
gl.NEAREST_MIPMAP_NEAREST, gl.LINEAR_MIPMAP_NEAREST, gl.NEAREST_MIPMAP_LINEAR, gl,LINEAR_MIPMAP_LINEAR
对于 Wrap 模式:gl.REPEAT, gl.MIRRORED_REPEAT, gl.CLAMP_TO_EDGE
为贴图对象指定图片: gl.texImage(target, level, internalformat, format, type, image)
target: gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP
level: MipMap层级,一般都是0
internalformat: 设置图片格式
Format 纹素 gl.RGB (R, G, B, 1) gl.RGBA (R, G, B, A) gl.ALPHA (0, 0, 0, A) gl.LUMINANCE (L, L, L, 1) gl.LUMINANCE_ALPHA (L, L, L, A) format: 纹素格式,要与 internal format 一样。
type: 纹素的数据类型
Type 介绍 gl.UNSIGNED_BYTE 无符号字节,每个元素有1字节 gl.UNSIGNED_SHORT_5_6_5 RGB 分别有5, 6, 5字节 gl.UNSIGNED_SHORT_4_4_4_4 RGBA 每个4字节 gl.UNSIGNED_SHORT_5_5_5_1 RGB 每个5字节,A 1字节 image: 图像
将纹理单元分配给uniform变量:gl.uniform1i(paramLoc, num)
Pasting Multiple Textures to a Shape
传入多张贴图:
1 | precision mediump float; |
1 | // 创建 Texture 对象 |
在 loadTexture 函数中分配 Texture Unit 即可。
The OpenGL ES Shading Language (GLSL ES)
官方文档:https://www.khronos.org/registry/OpenGL-Refpages/
Toward the 3D World
Specifying the Viewing Direction
可以使用cuon-matrix中的函数setLookAt来创建一个视图矩阵,然后输入三维坐标就好了:
1 | var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix'); |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/7_Towardthe3DWorld/1_LookAtTriangles.html
多个矩阵时在CPU算出结果后一并传入GPU:
1 | var viewMatrix = new Matrix4(); |
写按键控制摄像机位置:
1 | // 注册按键按下事件 |
可以用键盘上下左右键移动摄像机了
Specifying the Visible Range (Box Type)
WebGL只会显示在摄像机前2*2*1的标准立方体中的东西,所以可以用投影矩阵把点变换到这个空间里。
使用cuon_matrix中的函数 setOrtho(left, right, bottom, top, near, far) 即可:
1 | projMatrix.setOrtho(-1, 1, -1, 1, near, far); |
Specifying the Visible Range Using a Quadrangular Pyramid
使用cuon_matrix中的函数 setPerspective(fov, aspect, near, far) 即可:
1 | projMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100); |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/7_Towardthe3DWorld/5_PerspectiveView.html
Correct Handling Foreground and Background Objects
WebGL默认是按照顶点输入顺序渲染的,所以有会造成前后遮挡错误。
因此我们需要开启深度测试:
1 | gl.enable(gl.DEPTH_TEST); |
Hello Cube
一个一个指定顶点很复杂,因为模型有很多共用顶点。
可以使用顶点索引的方式绘制模型
需要用到gl.drawElements(mode, num, type, offset)
修改InitVertexBuffer函数,
1 | function initVertexBuffers(gl) |
使用drawElement函数
1 | function draw(gl, n) |
注意,如果定点数量大于256个,则需要使用Uint16Array,同时数据类型为gl.USIGNED_SHORT。
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/7_Towardthe3DWorld/6_HelloCube.html
Lighting Objects
Lighting 3D Objects
通过外部传入的光线颜色方向和顶点法线计算最终颜色:
1 | // Vertex Attribute |
在JS中传入更多矩阵和顶点信息:
1 | var mMatrix = new Matrix4(); |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/8_LightingObjects/1_LightedCube.html
Lighting the Translated-Rotated Object
在含有缩放的变换中,法线变换会出问题。
使用M矩阵的逆转置矩阵变换法线即可。
Using a Point Light Object
需要计算光照方向了(现在还没计算衰减)
1 | gl_Position = u_Matrix_MVP * a_Position; |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/8_LightingObjects/2_PointLightedCube.html
把光照计算挪到片元着色器更好
1 | void main() |
1 | void main() |
Hierarchical Objects
前面没啥用
Shader and Program Objects: The Role of initShaders()
在cuon-utils.js中定义,大概流程:
- gl.createShader(type);
- gl.shaderSource(shader, source);
- gl.compileShader(shader);
- gl.createProgram();
- gl.attachShader(program, shader);
- gl.linkProgram(program);
- gl.useProgram(program);
Advanced Techniques
Rotate an Object with the Mouse
用鼠标事件就好了
1 | function initEventHandler(canvas) |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/1_RotateObject.html
Select an Object
点击时绘制一个单色图片,并使用gl.readPixel读取鼠标位置像素颜色值即可。不如添加bounding box用cpu做射线检测。
HUD
只要两个canvas即可,一个渲染3D一个2D,用z-index排序;
1 | <canvas id="webgl" height="400" width="400" style="position: absolute; z-index: 0;"> |
1 | hud = document.getElementById('hud'); |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/2_HUD.html
把gl.clearColor的A设置为0即可做出3D浮在网页表面的效果了
Alpha Blending
使用以下命令:
1 | gl.enable(gl.BLEND); // 开启混合 |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/3_BlendTriangles.html
使用混合要关闭深度测试:
1 | gl.depthMask(false); // 关闭深度写入 |
Switching Shaders
先把program都创建出来,然后运行时使用gl.useProgram()切换
这里我大幅度重构了代码,并使用object储存两个材质以及模型(这里之后肯定要做成类):
1 | // 材质1 |
记得传入变量的时候也要useProgram
经历一系列初始化后,draw中useProgram:
1 | function draw(gl, model, material, vpMatrix, transform) |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/4_SwitchShader.html
Use What You’ve Drawn as a Texture Image
Color Buffer直接显示在屏幕上 Frame Buffer不是
- 创建FrameBuffer对象
- 创建贴图,设置大小和参数
- 创建renderbuffer
- 设置renderbuffer的参数
- 把贴图绑到FrameBuffer的颜色上
- 把RenderBuffer绑到FrameBuffer的深度上
- 检查FrameBuffer是否完善
- 绘制
代码如下:
1 | function initFrameBufferObject(gl) |
渲染的时候绘制两次:
1 | var tick = function () |
Display Shadows
- 到光源的位置绘制一张深度贴图,当做阴影贴图
- 正常绘制的时候对比当前像素距离光源的距离与阴影贴图的大小,以得知当前像素在不在阴影中
ShadowCater shader
1 | void main() |
1 | void main() |
正常shader中采样阴影贴图
1 | attribute vec4 a_Position; |
1 |
|
然后先绘制阴影贴图 再渲染
1 | // 绘制ShadowMap |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/6_Shadow.html
不过这个阴影精准度不高,因为只用了一个R通道存,只有8bit的精准度。
可以使用rgba四个通道就有4字节32bit的储存空间了,先在ShadowCaster中给深度信息编码存在rgba中
1 | void main() |
然后使用的时候解码:
1 | float unpackDepth(const in vec4 rgbaDepth) |
这样就有精度极高的阴影了,与深度比对的时候基本不需要给深度做任何偏移。
1 | float shadow = (shadowCoord.z > depth + 0.0001) ? 0.0 : 1.0; |
Load and Display 3D Models
- 以文本文件形式读取OBJ文件
- 根据每行开头分别储存模型信息
- 根据模型信息生成buffer数据数组
- 然后使用数组渲染
读取OBJ文件
1 | function initOBJ(fileName, gl, model, scale, reverse) |
OBJDoc类 解码OBJ文件
1 | let OBJDoc = function (fileName) |
生成Buffer数组:
1 | OBJDoc.prototype.DecodeBufferArrays = function () |
链接:https://1keven1.github.io/BlogSrc/WebGLProgrammngGuide/10_AdvancedTechniques/7_ReadOBJ.html
Handling Lost Context
为了防止打开网页后休眠设备导致的上下文丢失
1 | function main() |
其他
WebGL不需要Swap Buffer,浏览器自动完成了这步操作
WebGL在剪裁空间是左手系,所以开启深度测试后会变为左手系,也就是Y轴正半轴指向屏幕内部。需要使用投影矩阵转换坐标系、