1. 概述

在上一篇教程《WebGL简易教程(九):综合实例:地形的绘制》中,实现了对一个地形场景的渲染。在这篇教程中,就给这个地形场景加上光照,让其更加真实,立体感更强。

2. 原理

2.1. 光源类型

在现实中,即使是一个纯白色的物体,你也能很容易识别物体的轮廓。事实上,这是因为光照的产生的阴暗差异给了其立体感。类似于现实,WebGL有三种基本类型的光:

  1. 点光源光:一个点向周围发出的光,如灯泡、火焰等。定义一个点光源光需要光源的位置、光线方向以及颜色。根据照射点的位置不同,光线的方向也不同。
  2. 平行光:平行光可以看成是无限远处的光源发出的光,如太阳光。因为离光源的位置特别远,所以到达被照物体时可以认为光线是平行的。只需要用一个方向和颜色来定义即可。
  3. 环境光:环境光也就是间接光,指的是那些光源发出后,经过其他物体各种发射,然后照到物体表面上的光线。比如说夜间打开冰箱的门,这个厨房产生的亮光。因为经过多次反射后,强度差距已经非常小,没有必要精确计算光线强度。所以一般认为环境光是均匀照射到物体表面的,只需要一个颜色来定义。

如图所示:

2.2. 反射类型

由于物体最终显示的颜色也就是光线反射造成的颜色,由两部分因素决定:入射光和物体表面的类型。入射光信息包括入射光的方向和颜色,而物体表面的信息包含基底色和反射特性。根据物体反射光线的方式有环境反射(enviroment/ambient reflection)和漫反射(diffuse reflection)两种类型的光:

2.2.1. 环境反射(enviroment/ambient reflection)

环境反射是针对环境光而言的,在环境反射中,环境光照射物体是各方面均匀、强度相等的,反射的方向可以认为就是入射光的反方向。也就是最终物体的颜色只跟入射光颜色和基底色有关。那么可以这样定义环境反射光颜色:

\[<环境反射光颜色>=<入射光颜色>×<表面基底色>\tag{1}
\]

注意在式子中,这个乘法操作指的是颜色矢量上逐分量相乘。

2.2.2. 漫反射(diffuse reflection)

漫反射是针对平行光和点光源光而言的。相信在初中物理的时候就已经接触过镜面反射和漫反射。如果物体表面像镜子一样平滑,那么光线就会以特定的角度反射过去,从视觉效果来说就是刺眼的反光效果;如果物体表面是凹凸不平的,反射光就会以不固定的角度发射出去。在现实中大多数的物体表面都是粗糙的,所以才能看清各种各样的物体。如图所示:

漫反射中,反射光的颜色除了取决于入射光的颜色、表面的基底色,还有入射光与物体表面的法向量形成的入射角。令入射角为θ,漫反射光的颜色可以根据下式计算:

\[<漫反射光颜色>=<入射光颜色>×<表面基底色>×cosθ\tag{2}
\]

入射角θ可以通过矢量的点积来计算:

\[<光线方向>·<法线方向> = |光线方向|*|法线方向|*cosθ
\]

如果光线方向和法线方向都是归一化的,那么向量的模(长度)就为1,则有:

\[<漫反射光颜色>=<入射光颜色>×<表面基底色>×(<光线方向>·<法线方向>)
\]

注意,这里的“光线方向”,实际上指的是入射方向的反方向,即从入射点指向光源方向,如图所示:

2.2.3. 综合

当漫反射和环境反射同时存在时,将两者加起来,就会得到物体最终被观察到的颜色:

\[<表面的反射光颜色> = <漫反射光颜色>+<环境反射光颜色>\tag{3}
\]

3. 实例

3.1. 具体代码

改进上一篇教程的JS代码如下:

// 顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + //位置
'attribute vec4 a_Color;\n' + //颜色
'attribute vec4 a_Normal;\n' + //法向量
'uniform mat4 u_MvpMatrix;\n' +
'varying vec4 v_Color;\n' +
'varying vec4 v_Normal;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' + //设置顶点的坐标
' v_Color = a_Color;\n' +
' v_Normal = a_Normal;\n' +
'}\n'; // 片元着色器程序
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec3 u_DiffuseLight;\n' + // 漫反射光颜色
'uniform vec3 u_LightDirection;\n' + // 漫反射光的方向
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'varying vec4 v_Normal;\n' +
'void main() {\n' +
//对法向量归一化
' vec3 normal = normalize(v_Normal.xyz);\n' +
//计算光线向量与法向量的点积
' float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
//计算漫发射光的颜色
' vec3 diffuse = u_DiffuseLight * v_Color.rgb * nDotL;\n' +
//计算环境光的颜色
' vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +
' gl_FragColor = vec4(diffuse+ambient, v_Color.a);\n' +
'}\n'; //定义一个矩形体:混合构造函数原型模式
function Cuboid(minX, maxX, minY, maxY, minZ, maxZ) {
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
this.minZ = minZ;
this.maxZ = maxZ;
} Cuboid.prototype = {
constructor: Cuboid,
CenterX: function () {
return (this.minX + this.maxX) / 2.0;
},
CenterY: function () {
return (this.minY + this.maxY) / 2.0;
},
CenterZ: function () {
return (this.minZ + this.maxZ) / 2.0;
},
LengthX: function () {
return (this.maxX - this.minX);
},
LengthY: function () {
return (this.maxY - this.minY);
}
} //定义DEM
function Terrain() {}
Terrain.prototype = {
constructor: Terrain,
setWH: function (col, row) {
this.col = col;
this.row = row;
}
} var currentAngle = [0.0, 0.0]; // 绕X轴Y轴的旋转角度 ([x-axis, y-axis])
var curScale = 1.0; //当前的缩放比例 function main() {
var demFile = document.getElementById('demFile');
if (!demFile) {
console.log("Failed to get demFile element!");
return;
} demFile.addEventListener("change", function (event) {
//判断浏览器是否支持FileReader接口
if (typeof FileReader == 'undefined') {
console.log("你的浏览器不支持FileReader接口!");
return;
} var input = event.target;
var reader = new FileReader();
reader.onload = function () {
if (reader.result) { //读取
var terrain = new Terrain();
if (!readDEMFile(reader.result, terrain)) {
console.log("文件格式有误,不能读取该文件!");
} //绘制
onDraw(gl, canvas, terrain);
}
} reader.readAsText(input.files[0]);
}); // 获取 <canvas> 元素
var canvas = document.getElementById('webgl'); // 获取WebGL渲染上下文
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
} // 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
} // 指定清空<canvas>的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 开启深度测试
gl.enable(gl.DEPTH_TEST); //清空颜色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
} //绘制函数
function onDraw(gl, canvas, terrain) {
// 设置顶点位置
var n = initVertexBuffers(gl, terrain);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
} //注册鼠标事件
initEventHandlers(canvas); //设置灯光
setLight(gl); //绘制函数
var tick = function () {
//设置MVP矩阵
setMVPMatrix(gl, canvas, terrain.cuboid); //清空颜色和深度缓冲区
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //绘制矩形体
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_SHORT, 0); //请求浏览器调用tick
requestAnimationFrame(tick);
}; //开始绘制
tick();
} //设置灯光
function setLight(gl) {
var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
var u_DiffuseLight = gl.getUniformLocation(gl.program, 'u_DiffuseLight');
var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
if (!u_DiffuseLight || !u_LightDirection || !u_AmbientLight) {
console.log('Failed to get the storage location');
return;
} //设置漫反射光
gl.uniform3f(u_DiffuseLight, 1.0, 1.0, 1.0); // 设置光线方向(世界坐标系下的)
var solarAltitude = 45.0;
var solarAzimuth = 315.0;
var fAltitude = solarAltitude * Math.PI / 180; //光源高度角
var fAzimuth = solarAzimuth * Math.PI / 180; //光源方位角 var arrayvectorX = Math.cos(fAltitude) * Math.cos(fAzimuth);
var arrayvectorY = Math.cos(fAltitude) * Math.sin(fAzimuth);
var arrayvectorZ = Math.sin(fAltitude); var lightDirection = new Vector3([arrayvectorX, arrayvectorY, arrayvectorZ]);
lightDirection.normalize(); // Normalize
gl.uniform3fv(u_LightDirection, lightDirection.elements); //设置环境光
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
} //读取DEM函数
function readDEMFile(result, terrain) {
var stringlines = result.split("\n");
if (!stringlines || stringlines.length <= 0) {
return false;
} //读取头信息
var subline = stringlines[0].split("\t");
if (subline.length != 6) {
return false;
}
var col = parseInt(subline[4]); //DEM宽
var row = parseInt(subline[5]); //DEM高
var verticeNum = col * row;
if (verticeNum + 1 > stringlines.length) {
return false;
}
terrain.setWH(col, row); //读取点信息
var ci = 0;
var pSize = 9;
terrain.verticesColors = new Float32Array(verticeNum * pSize);
for (var i = 1; i < stringlines.length; i++) {
if (!stringlines[i]) {
continue;
} var subline = stringlines[i].split(',');
if (subline.length != pSize) {
continue;
} for (var j = 0; j < pSize; j++) {
terrain.verticesColors[ci] = parseFloat(subline[j]);
ci++;
}
} if (ci !== verticeNum * pSize) {
return false;
} //包围盒
var minX = terrain.verticesColors[0];
var maxX = terrain.verticesColors[0];
var minY = terrain.verticesColors[1];
var maxY = terrain.verticesColors[1];
var minZ = terrain.verticesColors[2];
var maxZ = terrain.verticesColors[2];
for (var i = 0; i < verticeNum; i++) {
minX = Math.min(minX, terrain.verticesColors[i * pSize]);
maxX = Math.max(maxX, terrain.verticesColors[i * pSize]);
minY = Math.min(minY, terrain.verticesColors[i * pSize + 1]);
maxY = Math.max(maxY, terrain.verticesColors[i * pSize + 1]);
minZ = Math.min(minZ, terrain.verticesColors[i * pSize + 2]);
maxZ = Math.max(maxZ, terrain.verticesColors[i * pSize + 2]);
} terrain.cuboid = new Cuboid(minX, maxX, minY, maxY, minZ, maxZ); return true;
} //注册鼠标事件
function initEventHandlers(canvas) {
var dragging = false; // Dragging or not
var lastX = -1,
lastY = -1; // Last position of the mouse //鼠标按下
canvas.onmousedown = function (ev) {
var x = ev.clientX;
var y = ev.clientY;
// Start dragging if a moue is in <canvas>
var rect = ev.target.getBoundingClientRect();
if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
lastX = x;
lastY = y;
dragging = true;
}
}; //鼠标离开时
canvas.onmouseleave = function (ev) {
dragging = false;
}; //鼠标释放
canvas.onmouseup = function (ev) {
dragging = false;
}; //鼠标移动
canvas.onmousemove = function (ev) {
var x = ev.clientX;
var y = ev.clientY;
if (dragging) {
var factor = 100 / canvas.height; // The rotation ratio
var dx = factor * (x - lastX);
var dy = factor * (y - lastY);
currentAngle[0] = currentAngle[0] + dy;
currentAngle[1] = currentAngle[1] + dx;
}
lastX = x, lastY = y;
}; //鼠标缩放
canvas.onmousewheel = function (event) {
if (event.wheelDelta > 0) {
curScale = curScale * 1.1;
} else {
curScale = curScale * 0.9;
}
};
} //设置MVP矩阵
function setMVPMatrix(gl, canvas, cuboid) {
// Get the storage location of u_MvpMatrix
var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
if (!u_MvpMatrix) {
console.log('Failed to get the storage location of u_MvpMatrix');
return;
} //模型矩阵
var modelMatrix = new Matrix4();
modelMatrix.scale(curScale, curScale, curScale);
modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis
modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis
modelMatrix.translate(-cuboid.CenterX(), -cuboid.CenterY(), -cuboid.CenterZ()); //投影矩阵
var fovy = 60;
var near = 1;
var projMatrix = new Matrix4();
projMatrix.setPerspective(fovy, canvas.width / canvas.height, 1, 10000); //计算lookAt()函数初始视点的高度
var angle = fovy / 2 * Math.PI / 180.0;
var eyeHight = (cuboid.LengthY() * 1.2) / 2.0 / angle; //视图矩阵
var viewMatrix = new Matrix4(); // View matrix
viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0); //MVP矩阵
var mvpMatrix = new Matrix4();
mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix); //将MVP矩阵传输到着色器的uniform变量u_MvpMatrix
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
} //
function initVertexBuffers(gl, terrain) {
//DEM的一个网格是由两个三角形组成的
// 0------1 1
// | |
// | |
// col col------col+1
var col = terrain.col;
var row = terrain.row; var indices = new Uint16Array((row - 1) * (col - 1) * 6);
var ci = 0;
for (var yi = 0; yi < row - 1; yi++) {
//for (var yi = 0; yi < 10; yi++) {
for (var xi = 0; xi < col - 1; xi++) {
indices[ci * 6] = yi * col + xi;
indices[ci * 6 + 1] = (yi + 1) * col + xi;
indices[ci * 6 + 2] = yi * col + xi + 1;
indices[ci * 6 + 3] = (yi + 1) * col + xi;
indices[ci * 6 + 4] = (yi + 1) * col + xi + 1;
indices[ci * 6 + 5] = yi * col + xi + 1;
ci++;
}
} //
var verticesColors = terrain.verticesColors;
var FSIZE = verticesColors.BYTES_PER_ELEMENT; //数组中每个元素的字节数 // 创建缓冲区对象
var vertexColorBuffer = gl.createBuffer();
var indexBuffer = gl.createBuffer();
if (!vertexColorBuffer || !indexBuffer) {
console.log('Failed to create the buffer object');
return -1;
} // 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
// 向缓冲区对象写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW); //获取着色器中attribute变量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 -1;
}
// 将缓冲区对象分配给a_Position变量
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 9, 0); // 连接a_Position变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position); //获取着色器中attribute变量a_Color的地址
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
if (a_Color < 0) {
console.log('Failed to get the storage location of a_Color');
return -1;
}
// 将缓冲区对象分配给a_Color变量
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 9, FSIZE * 3);
// 连接a_Color变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Color); // 向缓冲区对象分配a_Normal变量,传入的这个变量要在着色器使用才行
var a_Normal = gl.getAttribLocation(gl.program, 'a_Normal');
if (a_Normal < 0) {
console.log('Failed to get the storage location of a_Normal');
return -1;
}
gl.vertexAttribPointer(a_Normal, 3, gl.FLOAT, false, FSIZE * 9, FSIZE * 6);
//开启a_Normal变量
gl.enableVertexAttribArray(a_Normal); // 将顶点索引写入到缓冲区对象
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); return indices.length;
}

3.2. 改动详解

3.2.1. 设置日照

主要改动是在绘制函数onDraw()中添加了一个设置光照的函数setLight():

//绘制函数
function onDraw(gl, canvas, terrain) {
//... //注册鼠标事件
initEventHandlers(canvas); //设置灯光
setLight(gl); //绘制函数
var tick = function () {
//...
}; //开始绘制
tick();
}

具体展开这个函数,可以看到这段代码主要是给着色器传入了环境光颜色u_AmbientLight、漫反射光颜色u_DiffuseLight、漫反射光方向u_LightDirection这三个参数。环境光颜色是由其他物体反射照成的,所以环境光强度较弱,设置为(0.2,0.2,0.2)。这里用漫反射光颜色来模拟太阳光,可以设为最强(1.0,1.0,1.0):

//设置灯光
function setLight(gl) {
var u_AmbientLight = gl.getUniformLocation(gl.program, 'u_AmbientLight');
var u_DiffuseLight = gl.getUniformLocation(gl.program, 'u_DiffuseLight');
var u_LightDirection = gl.getUniformLocation(gl.program, 'u_LightDirection');
if (!u_DiffuseLight || !u_LightDirection || !u_AmbientLight) {
console.log('Failed to get the storage location');
return;
} //设置漫反射光
gl.uniform3f(u_DiffuseLight, 1.0, 1.0, 1.0); //... gl.uniform3fv(u_LightDirection, lightDirection.elements); //设置环境光
gl.uniform3f(u_AmbientLight, 0.2, 0.2, 0.2);
}

前面提到过,太阳光是一种平行光,所以只需要设置方向就行了。这个方向的计算与两个地理学参数太阳高度角solarAltitude和太阳方位角solarAzimuth有关。可以暂时不用去关注其具体的推算细节(可参看我的另外一篇博文通过OSG实现对模型的日照模拟第二节和第四节),只需要知道这里的漫反射方向不是随意指定,是根据实际情况参数计算出来的。

function setLight(gl) {
{
//... // 设置光线方向(世界坐标系下的)
var solarAltitude = 45.0;
var solarAzimuth = 315.0;
var fAltitude = solarAltitude * Math.PI / 180; //光源高度角
var fAzimuth = solarAzimuth * Math.PI / 180; //光源方位角 var arrayvectorX = Math.cos(fAltitude) * Math.cos(fAzimuth);
var arrayvectorY = Math.cos(fAltitude) * Math.sin(fAzimuth);
var arrayvectorZ = Math.sin(fAltitude); var lightDirection = new Vector3([arrayvectorX, arrayvectorY, arrayvectorZ]);
lightDirection.normalize(); // Normalize //...
}

3.2.2. 着色器光照设置

这里顶点着色器中并没有用到传入的光照参数,而是把顶点缓冲区对象的颜色值和法向量值保存为varying变量,用来传入片元缓冲区:

// 顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + //位置
'attribute vec4 a_Color;\n' + //颜色
'attribute vec4 a_Normal;\n' + //法向量
'uniform mat4 u_MvpMatrix;\n' +
'varying vec4 v_Color;\n' +
'varying vec4 v_Normal;\n' +
'void main() {\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' + //设置顶点的坐标
' v_Color = a_Color;\n' +
' v_Normal = a_Normal;\n' +
'}\n';

在片元缓冲区中,传入到片元缓冲区的颜色值和法向量值都经过了内插,变成了每个片元的基底色和法向量值。将该法向量归一化,与传入的漫反射方向做点积,得到漫反射入射角。漫反射入射角与传入的漫反射光强度以及片元基底色,根据公式(2)计算漫反射光颜色。片元基底色与传入的环境光颜色,根据公式(1)计算环境反射光颜色。根据公式(3)将两者相加,得到最终显示的片元颜色。

// 片元着色器程序
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform vec3 u_DiffuseLight;\n' + // 漫反射光颜色
'uniform vec3 u_LightDirection;\n' + // 漫反射光的方向
'uniform vec3 u_AmbientLight;\n' + // 环境光颜色
'varying vec4 v_Color;\n' +
'varying vec4 v_Normal;\n' +
'void main() {\n' +
//对法向量归一化
' vec3 normal = normalize(v_Normal.xyz);\n' +
//计算光线向量与法向量的点积
' float nDotL = max(dot(u_LightDirection, normal), 0.0);\n' +
//计算漫发射光的颜色
' vec3 diffuse = u_DiffuseLight * v_Color.rgb * nDotL;\n' +
//计算环境光的颜色
' vec3 ambient = u_AmbientLight * v_Color.rgb;\n' +
' gl_FragColor = vec4(diffuse+ambient, v_Color.a);\n' +
'}\n';

4. 结果

浏览器最终显示的结果如下:



相比上一篇教程的渲染效果,可以明显发现立体感增强,能够清楚看到地形的起伏情况。

5. 参考

本来部分代码和插图来自《WebGL编程指南》,源代码链接:地址 。会在此共享目录中持续更新后续的内容。

WebGL简易教程(十):光照的更多相关文章

  1. WebGL简易教程(十二):包围球与投影

    目录 1. 概述 2. 实现详解 3. 具体代码 4. 参考 1. 概述 在之前的教程中,都是通过物体的包围盒来设置模型视图投影矩阵(MVP矩阵),来确定物体合适的位置的.但是在很多情况下,使用包围盒 ...

  2. WebGL简易教程(十四):阴影

    目录 1. 概述 2. 示例 2.1. 着色器部分 2.1.1. 帧缓存着色器 2.1.2. 颜色缓存着色器 2.2. 绘制部分 2.2.1. 整体结构 2.2.2. 具体改动 3. 结果 4. 参考 ...

  3. WebGL简易教程(十五):加载gltf模型

    目录 1. 概述 2. 实例 2.1. 数据 2.2. 程序 2.2.1. 文件读取 2.2.2. glTF格式解析 2.2.3. 初始化顶点缓冲区 2.2.4. 其他 3. 结果 4. 参考 5. ...

  4. WebGL简易教程——目录

    目录 1. 绪论 2. 目录 3. 资源 1. 绪论 最近研究WebGL,看了<WebGL编程指南>这本书,结合自己的专业知识写的一系列教程.之前在看OpenGL/WebGL的时候总是感觉 ...

  5. WebGL简易教程(九):综合实例:地形的绘制

    目录 1. 概述 2. 实例 2.1. TerrainViewer.html 2.2. TerrainViewer.js 3. 结果 4. 参考 1. 概述 在上一篇教程<WebGL简易教程(八 ...

  6. WebGL简易教程(二):向着色器传输数据

    目录 1. 概述 2. 示例:绘制一个点(改进版) 1) attribute变量 2) uniform变量 3) varying变量 3. 结果 4. 参考 1. 概述 在上一篇教程<WebGL ...

  7. WebGL简易教程(三):绘制一个三角形(缓冲区对象)

    目录 1. 概述 2. 示例:绘制三角形 1) HelloTriangle.html 2) HelloTriangle.js 3) 缓冲区对象 (1) 创建缓冲区对象(gl.createBuffer( ...

  8. WebGL简易教程(四):颜色

    目录 1. 概述 2. 示例:绘制三角形 1) 数据的组织 2) varying变量 3. 结果 4. 理解 1) 图形装配和光栅化 2) 内插过程 5. 参考 1. 概述 在上一篇教程<Web ...

  9. WebGL简易教程(六):第一个三维示例(使用模型视图投影变换)

    目录 1. 概述 2. 示例:绘制多个三角形 2.1. Triangle_MVPMatrix.html 2.2. Triangle_MVPMatrix.js 2.2.1. 数据加入Z值 2.2.2. ...

随机推荐

  1. [Swoole入门到进阶] [精选公开课] Swoole服务器-Server的四层生命周期

    PHP 完整生命周期 执行PHP文件 PHP扩展模块初始化(MINIT) PHP扩展请求初始化(RINIT) 执行 PHP 逻辑 PHP扩展请求结束(RSHUTDOWN) PHP脚本清理 PHP扩展模 ...

  2. Quartz.Net使用教程

    在项目的开发过程中,难免会遇见后需要后台处理的任务,例如定时发送邮件通知.后台处理耗时的数据处理等,这个时候你就需要Quartz.Net了. Quartz.Net是纯净的,它是一个.Net程序集,是非 ...

  3. R:ggplot2数据可视化——进阶(1)

    ,分为三个部分,此篇为Part1,推荐学习一些基础知识后阅读~ Part 1: Introduction to ggplot2, 覆盖构建简单图表并进行修饰的基础知识 Part 2: Customiz ...

  4. bzoj2141_排队

    题意 给定\(n\)个数,每次交换两个数,输出交换后的逆序数. 分析 交换两个数只会影响到对应区间内的逆序数,具体为减少区间\([l+1,r-1]\)中比\(a[r]\)大的数的个数,增加比\(a[r ...

  5. 云开发数据库VS传统数据库丨云开发101

    云开发数据库与传统数据库的不同 在小程序·云开发中,最核心的便是三大组件:数据库.云存储和云函数,从今天开始,我们将开始隔日更的专栏文章,云开发101,在第一周,我们将从最最核心的数据库开始说起. 云 ...

  6. Optional和Stream的map与flatMap

    Optional的map和flatMap Optional存在map和flatMap方法.map源码如下 public<U> Optional<U> map(Function& ...

  7. MD5字符串加密

    MD5字符串加密 Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护.该算法的文件号为RFC 132 ...

  8. JavaScript如何给td赋值

    td里加个标签,如: <td><div id="aa"></div></td> document.getElementById('a ...

  9. MySQL索引原理及SQL优化

    目录 索引(Index) 索引的原理 b+树 MySQL如何使用索引 如何优化 索引虽好,不可滥用 如何验证索引使用情况? SQL优化 explain查询执行计划 id select_type tab ...

  10. 猿类如何捕获少女心--难以琢磨的try-catch

    背景故事 影片<金刚>是2005年上映的一部冒险电影,它讲述1933年的美国,一名勇于冒险的企业家及电影制作者,率领摄制队伍到荒岛拍摄,其中包括女主角安及编剧杰克,他们遇到恐龙及当地土著的 ...