参考
【《WebGL编程指南》读书笔记】
【《WebGL编程指南》读书笔记-WebGL概述】
【《WebGL编程指南》读书笔记-WebGL入门】
关键词:
- 顶点着色器
- 片元着色器
- 使用JS向着色器传递参数
一、使用Canvas画个实心蓝色矩形
//DrawRectangle.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Draw a blue rectangle (canvas version)</title>
</head>
<body onload="main()">
<canvas id="example" width="400" height="400">
Please use a browser that supports "canvas"
</canvas>
<script src="DrawRectangle.js"></script>
</body>
</html>
// DrawTriangle.js (c) 2012 matsuda
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('example');
if (!canvas) {
console.log('Failed to retrieve the <canvas> element');
return false;
}
// Get the rendering context for 2DCG
var ctx = canvas.getContext('2d');
// Draw a blue rectangle
ctx.fillStyle = 'rgba(0, 0, 255, 1.0)'; // Set color to blue
ctx.fillRect(120, 10, 150, 150); // Fill a rectangle with the color
}
二、清空绘图区
上一个示例还不能严格认为是WebGL的程序,而是<canvas>标签原本的2d方法和CanvasRenderingContext2D对象,下面进入WebGL的简单示例。
//HelloCanvas.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Clear "canvas"</title>
</head>
<body onload="main()">
<canvas id="webgl" width="400" height="400">
Please use a browser that supports "canvas"
</canvas>
<script src="../lib/webgl-utils.js"></script>
<script src="../lib/webgl-debug.js"></script>
<script src="../lib/cuon-utils.js"></script>
<script src="HelloCanvas.js"></script>
</body>
</html>
// HelloCanvas.js (c) 2012 matsuda
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');
// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
// Set clear color
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
}
1.getWebGLContext(canvas)
抹平不同浏览器之间的差异,返回一个3D的绘图上下文。定义在cuon-utils.js中。也可以简单地使用这行代码:
let gl = canvas.getContext('webgl');
2.gl.clearColor
在前面的二维图形程序DrawRectangle中,颜色分量值在0到255之间。但是,由于WebGL是继承自OpenGL的,所以它遵循传统的OpenGL颜色分量的取值范围,即从0到1。RGB的值越高,颜色就越亮。类似地,第4分量值越高,颜色就越不透明。
一旦指定了背景色之后,背景色就会驻存在WebGL System中,在下一次调用gl.clearColor()方法前不会改变。换句话说,如果将来什么时候你还想用同一个颜色再清空一次绘图区,没必要再指定一次背景色。
3.gl.clear(gl.COLOR_BUFF_BIT)
此方法继承自OpenGL,参数表示清空颜色缓冲区。除了颜色缓冲区,还有深度缓冲区(gl.clearDepth)和模板缓冲区(gl.clearStencil)。
清空颜色缓冲区后,使用gl.clearColor指定的值,如果未指定,则使用默认值(0.0,0.0,0.0,0.0)
三、绘制一个矩形点
在前面的DrawRectangle中,绘制一个矩形如下:
ctx.fillStyle = 'rgba(0, 0, 255, 1.0)';
ctx.fillRect(120, 10, 150, 150);
你可能会认为WebGL也差不多,比如:
gl.drawColor(1.0,0.0,0.0,1.0);
gl.drawPoint(0,0,0,10);//点的位置和大小
不幸的是,事情没这么简单。WebGL依赖于一种新的称为着色器(shader)的绘图机制。
//HelloPoint1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Draw a point (1)</title>
</head>
<body onload="main()">
<canvas id="webgl" width="400" height="400">
Please use a browser that supports "canvas"
</canvas>
<script src="../lib/webgl-utils.js"></script>
<script src="../lib/webgl-debug.js"></script>
<script src="../lib/cuon-utils.js"></script>
<script src="HelloPoint1.js"></script>
</body>
</html>
// HelloPoint1.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
'void main() {\n' +
// Set the vertex coordinates of the point
' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' +
// Set the point size
' gl_PointSize = 10.0;\n' +
'}\n';
// Fragment shader program
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // Set the point color
'}\n';
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');
// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// Specify the color for clearing <canvas>
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// Draw a point
gl.drawArrays(gl.POINTS, 0, 1);
}
在代码中,出现了顶点着色器和片元着色器。它们是以字符串的形式嵌入在js文件中。
1.顶点着色器 Vertex shader
用来描述顶点特性(如位置、颜色等)
2.片元着色器 Fragment shader
进行逐片元处理过程如光照的程序。片元是一个WebGL术语,可以理解为像素(图像的单元)。
本书后续部分将仔细研究着色器。简单地说,在三维场景中,仅仅用线条和颜色把图形画出来是远远不够的。你必须考虑,比如,光线照上去之后,或者观察者的视角发生变化,对场景会有什么影响。着色器可以高度灵活地完成这些工作,提供各种渲染效果。这也是当今计算机制作出的三维场景如此逼真和令人震撼的原因。
3.GLSL ES 着色器语言
因为着色器代码必须预先处理成单个字符串的形式,所以我们用+号将多行字符串连成一个长字符串。第一行以\n结束,这是由于当着色器内部出错时,就能获取出错的行号,这对于检查源代码中的错误很有帮助。但是,\n并不是必须的,也可不用它。
和C语言类似,必须包含一个main函数。gl_Position 和gl_PointSize 这两个变量是内置在顶点着色器中的,而且有着特殊的含义:一个表示位置,一个表示尺寸。
gl_Position 必须被赋值,gl_PointSize 并不是必须的,如果不赋值,会取默认值1.0.
GLSL ES语言是强类型的,如果你将gl_PointSize = 10.0
改为gl_PointSize = 10
就会报错。因为要求传入一个浮点数,而10是一个整形数。
gl_Position = vec4(0.0, 0.0, 0.0, 1.0)
需要传入四个浮点数的分量,这被称为齐次坐标。
齐次坐标能够提高处理三维数据的效率,所以在三维图形系统中被大量使用。齐次坐标使用如下符号描述:(x,y,z,w),它等价于三维坐标(x/w,y/w,z/w)。所以如果齐次坐标的第4个分量是1,你就可以将它当作三维坐标来使用。w的值必须是大于等于0的。如果w趋近于0,那么它所表示的点将趋近无穷远,所以在齐次坐标系中可以有无穷的概念。齐次坐标的存在,使得用矩阵乘法来描述顶点变换成为可能,三维图形系统在计算过程中,通常使用齐次坐标来表示顶点的三维坐标。
4.initShaders()
对字符串形式的着色器进行初始化,它被定义在cuon.util.js中,将在本书第9章研究内部细节。
5.绘制操作gl.drawArrays(gl.POINTS,0,1);
第一个参数为mode,指定了绘制方式,可接收以下常量符号:
- gl.POINTS
- gl.LINES
- gl.LINE_STRIP
- gl.LINE_LOOP
- gl.TRIANGLES
- gl.TRIANGLE_STRIP
- gl.TRIANGLE_FAN
第二个参数为first,指定从哪个顶点开始绘制(整形数)
第三个参数为count,指定绘制需要用到多少个顶点(整形数)
因为我们绘制的是单独的点,所以第一个参数设置成gl.POINTS;设置第二个参数为0,表示从第1个顶点开始画起(虽然只有1个顶点);设置第三个参数为1,表示仅绘制1个点
6.GLSL vs HLSL vs Cg
参考
GLSL vs HLSL vs Cg
GLSL 到 HLSL 参考
三大 Shader 编程语言(CG/HLSL/GLSL)
Shader language目前有3种主流语言:基于OpenGL的GLSL(OpenGL Shading Language,也称为GLslang),基于Direct3D的HLSL(High Level Shading Language),还有NVIDIA公司的Cg (C for Graphic)语言。
GLSL与HLSL分别提基于OpenGL和Direct3D的接口,两者不能混用,事实上OpenGL和Direct3D一直都是冤家对头,曹操和刘备还有一段和平共处的甜美时光,但OpenGL和Direct3D各自的东家则从来都是争斗不休。争斗良久,既然没有分出胜负,那么必然是两败俱伤的局面。
首先ATI系列显卡对OpenGL扩展支持不够,例如我在使用OSG(Open Scene Graphic)开源图形引擎时,由于该引擎完全基于OpenGL,导致其上编写的3D仿真程序在较老的显卡上常常出现纹理无法显示的问题。其次GLSL 的语法体系自成一家,而HLSL和Cg语言的语法基本相同,这就意味着,只要学习HLSL和Cg中的任何一种,就等同于学习了两种语言。不过OpenGL 毕竟图形API的曾经领袖,通常介绍OpenGL都会附加上一句“事实上的工业标准”,所以在其长期发展中积累下的用户群庞大,这些用户当然会选择 GLSL学习。此外,GLSL继承了OpenGL的良好移植性,一度在unix等操作系统上独领风骚(已是曾经的往事)。
微软的HLSL移植性较差,在windows平台上可谓一家独大,可一出自己的院子(还好院子够大),就是落地凤凰不如鸡。这一点在很大程度上限制了 HLSL的推广和发展。目前HLSL多半都是用于游戏领域。我可以负责任的断言,在Shader language领域,HLSL可以凭借微软的老本成为割据一方的诸侯,但,决不可能成为君临天下的霸主。这和微软现在的局面很像,就是一个被带刺鲜花簇拥着的大财主,富贵已极,寸步难行。
Unity官方手册上讲Shader程序嵌入的小片段是用Cg/HLSL编写的,从“CGPROGRAM”开始,到“CGEND”结束。所以,Unity官方主要是用Cg/HLSL编写Shader程序片段。Unity官方手册也说明对于Cg/HLSL程序进行扩展也可以使用GLSL,不过Unity官方建议使用原生的GLSL进行编写和测试。如果不使用原生GLSL,你就需要知道你的平台必须是Mac OS X、OpenGL ES 2.0以上的移动设备或者是Linux。在一般情况下Unity会把Cg/HLSL交叉编译成优化过的GLSL。因此我们有多种选择,我们既可以考虑使用Cg/HLSL,也可以使用GLSL。不过由于Cg/HLSL更好的跨平台性,更倾向于使用Cg/HLSL编写Shader程序。
7.与unity中的shader对比
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter5-SimpleShader"
{
SubShader
{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v:POSITION):SV_POSITION{
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
return UnityObjectToClipPos(v);
}
fixed4 frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
POSITION 和SV_POSITION都是CG/HLSL中的语义,是不可忽略的,POSITION告诉Unity把模型的顶点坐标填充到输入参数v中,SV_POSITION告诉unity顶点着色器的输出是裁剪空间中的顶点坐标。
UnityObjectToClipPos,是将顶点坐标从模型空间转换到剪裁空间中。
在本例中frag 函数没有任何输入,它的输出是一个fixed4 类型的变量,并使用了SV_Target语义进行限定,它等于告诉渲染器,把用户的输出颜色存储到一个渲染 目标(render target)中,这里将输出到默认的帧缓存中。片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,(1,1,1)表示白色。
现在来对比一下WEBGL的版本:
// Vertex shader program
var VSHADER_SOURCE =
'void main() {\n' +
' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' + // Set the vertex coordinates of the point
' gl_PointSize = 10.0;\n' + // Set the point size
'}\n';
// Fragment shader program
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // Set the point color
'}\n';
和POSITION 和SV_POSITION类似,GLSL中也有内置变量gl_Position 和gl_PointSize。片元着色器写法类似。
四、将顶点坐标从JS传到着色器程序中
在上面的HelloPoint1例子中,点的位置写死在着色器程序中,缺乏扩展性。有两种方式可以把位置信息通过JS传给着色器:
- attribute变量 传输的是那些与顶点相关的数据
- uniform变量 传输那些对所有顶点都相同(或与顶点无关)的数据
本例使用attribute变量来传输顶点坐标,显然不同的顶点通常具有不同的坐标。
// HelloPint2.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + // attribute variable
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
// Fragment shader program
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');
// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// Get the storage location of a_Position
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return;
}
// Pass vertex position to attribute variable
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
// Specify the color for clearing <canvas>
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// Draw
gl.drawArrays(gl.POINTS, 0, 1);
}
1.着色器代码
- 声明一个attribute变量 叫a_Position(注:本书所有attribute变量都以a_前缀开始,同理uniform变量以u_前缀开始)
- 把a_Position赋值给gl_Position
- 这样JS就可以向a_Position传输数据了
2.JS代码
拿到暴露的变量:
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
返回的是个存储地址,为了便于理解,本书中,存储着色器变量地址的js变量名称与着色器中的变量名称保持一致。传输数据:
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
第2、3、4个参数是三个浮点型数值,即点的xyz坐标值。
3.gl.vertexAttrib3f的同族函数
你可能已经注意到,第4行的a_Position变量是vec4类型的,但是gl.vertex-Attrib3f()仅传了三个分量值。是不是漏掉了1个呢?实际上,如果你省略了第4个参数,这个方法就会默认地将第4个分量设置为1.0。
gl.vertexAttrib3f是一系列同族函数中的一个,gl.vertexAttrib1f传输1个单精度值,相应的23分量默认为0.0,4分量默认1.0。
gl.vertexAttrib4f则传输4个值。
你也可以使用这些方法的矢量版本,它们的名字以v(vector)结尾,并接受类型化数组作为参数,函数名中的数字表示数组中的元素个数,比如:
var position = new Float32Array([1.0, 2.0, 3.0, 1.0]);
gl.vertexAttrib4fv(a_Position, position);
4.改变点的大小
注意是float类型
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute float a_PointSize; \n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = a_PointSize;\n' +
'}\n';
var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
gl.vertexAttrib1f(a_PointSize, 5.0);
五、通过鼠标点击绘点
//ClickedPoints.js
...
// Register function (event handler) to be called on a mouse press
canvas.onmousedown = function(ev){ click(ev, gl, canvas, a_Position); };
// Specify the color for clearing <canvas>
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
}
var g_points = []; // The array for the position of a mouse press
function click(ev, gl, canvas, a_Position) {
var x = ev.clientX; // x coordinate of a mouse pointer
var y = ev.clientY; // y coordinate of a mouse pointer
var rect = ev.target.getBoundingClientRect() ;
x = ((x - rect.left) - canvas.width/2)/(canvas.width/2);
y = (canvas.height/2 - (y - rect.top))/(canvas.height/2);
// Store the coordinates to g_points array
g_points.push(x); g_points.push(y);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
var len = g_points.length;
for(var i = 0; i < len; i += 2) {
// Pass the position of a point to a_Position variable
gl.vertexAttrib3f(a_Position, g_points[i], g_points[i+1], 0.0);
// Draw
gl.drawArrays(gl.POINTS, 0, 1);
}
}
省略了部分代码,主要难点是坐标转换和使用数组记录点击位置。WebGL使用的是颜色缓冲区,绘制结束后系统将缓冲区内容显示在屏幕上,然后颜色缓冲区就会被重置,其中的内容会丢失( 这是默认操作,下一章将详细讨论)。因此,我们有必要将每次鼠标点击的位置都记录下来,每次点击后,程序都重新绘制了从第一次点击到最近一次点击中所有的点。
现在,让我们看看,如果不执行gl.clear(gl.COLOR_BUFFER_BIT);
会怎么样:
首先会看到黑色的背景,第一次点击鼠标后,背景就变成了白色,然后绘制了一个红点。这是因为绘制点之后,颜色缓冲区被WebGL重置为默认的颜色(0.0,0.0,0.0,0.0)。这个默认颜色的alpha分量是0.0,因此canvas就成了透明的了,可以看到网页的背景颜色(这里还是白色)。如果你不希望这样,应当在每次绘制之前都调用gl.clear来用指定的背景色清空。
1.浏览器、canvas、webgl坐标系统相互转换
通过event可以获得鼠标点击的位置(浏览器页面坐标),该坐标需要转换到canvas坐标系统下,再转换到webgl坐标系统下
var x = ev.clientX; // 鼠标点击处的x坐标
var y = ev.clientY; // 鼠标点击处的y坐标
var rect = ev.target.getBoundingClientRect();
ev.target表示canvas元素,getBoundingClientRect用于获取某个元素相对于视窗的位置集合,返回包含top、left、bottom、right、width和height属性的对象。
所以,转换到canvas坐标系下就是:
x - rect.left
y - rect.top
然后,看一下WEBGL坐标系,位于屏幕中心点,并且Y轴方向与canvas坐标系相反
所以,转到WEBGL坐标系后就是:
x - rect.left - canvas.width/2
-(y - rect.top - canvas.height/2)
然后,WEBGL坐标系要归一化,区间为[-1,1]。以x值举例,最小为0,最大为canvas.width,转到webgl坐标系后,中心点变成canvas.width/2。所以,归一化只要除以canvas.width/2即可。
[x - rect.left - canvas.width/2] / (canvas.width/2)
[-(y - rect.top - canvas.height/2)] / (canvas.height/2)
这个结果再化简一下,就是代码常常看到的如下形式:
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
六、改变点的颜色
可以用uniform变量将颜色值传给片元着色器,而不是顶点着色器。
// ColoredPoint.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
// Fragment shader program
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec4 u_FragColor;\n' + // uniform变量
'void main() {\n' +
' gl_FragColor = u_FragColor;\n' +
'}\n';
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');
// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// // Get the storage location of a_Position
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return;
}
// Get the storage location of u_FragColor
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) {
console.log('Failed to get the storage location of u_FragColor');
return;
}
// Register function (event handler) to be called on a mouse press
canvas.onmousedown = function(ev){ click(ev, gl, canvas, a_Position, u_FragColor) };
// Specify the color for clearing <canvas>
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
}
var g_points = []; // The array for the position of a mouse press
var g_colors = []; // The array to store the color of a point
function click(ev, gl, canvas, a_Position, u_FragColor) {
var x = ev.clientX; // x coordinate of a mouse pointer
var y = ev.clientY; // y coordinate of a mouse pointer
var rect = ev.target.getBoundingClientRect();
x = ((x - rect.left) - canvas.width/2)/(canvas.width/2);
y = (canvas.height/2 - (y - rect.top))/(canvas.height/2);
// Store the coordinates to g_points array
g_points.push([x, y]);
// Store the coordinates to g_points array
if (x >= 0.0 && y >= 0.0) { // 第一象限
g_colors.push([1.0, 0.0, 0.0, 1.0]); // Red
} else if (x < 0.0 && y < 0.0) { // 第三象限
g_colors.push([0.0, 1.0, 0.0, 1.0]); // Green
} else { // 其它
g_colors.push([1.0, 1.0, 1.0, 1.0]); // White
}
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
var len = g_points.length;
for(var i = 0; i < len; i++) {
var xy = g_points[i];
var rgba = g_colors[i];
// Pass the position of a point to a_Position variable
gl.vertexAttrib3f(a_Position, xy[0], xy[1], 0.0);
// Pass the color of a point to u_FragColor variable
gl.uniform4f(u_FragColor, rgba[0], rgba[1], rgba[2], rgba[3]);
// Draw
gl.drawArrays(gl.POINTS, 0, 1);
}
}
1.精度限定词
'precision mediump float;\n'
用来指定变量的范围(最大值与最小值)和精度,本例中为中等精度。第5章将会详细讨论精度的问题。
2. gl.uniform4f
与gl.vertexAttrib3f类似,也有同族函数