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. poj 1062 昂贵的聘礼(floyd path的应用)

    题目链接:http://poj.org/problem?id=1062 题意就不解释了中问题. 这题用dfs也行.但是感觉还是floyd比较好一点主要是他们交易是有条件的交易总的等级差不能超过m 所以 ...

  2. CSU 1803 2016 湖南省2016省赛

    1803: 2016 Submit Page   Summary   Time Limit: 5 Sec     Memory Limit: 128 Mb     Submitted: 1416    ...

  3. 简单粗暴详细讲解javascript实现函数柯里化

    函数柯里化(黑人问号脸)???Currying(黑人问号脸)???妥妥的中式翻译既视感:下面来一起看看究竟什么是函数柯里化: 维基百科的解释是:把接收多个参数的函数变换成接收一个单一参数(最初函数的第 ...

  4. python每日经典算法题5(基础题)+1(中难题)

    现在,越来越多的公司面试以及考验面试对算法要求都提高了一个层次,从现在,我讲每日抽出时间进行5+1算法题讲解,5是指基础题,1是指1道中等偏难.希望能够让大家熟练掌握python的语法结构已经一些高级 ...

  5. mybatis_plus插件——生成器

    最近在学习mybatis框架,虽然已经简化了一些Dao代码,但是还想更上一层楼吗?不再被基本的pojo层,controller层,service层,dao层基本重复代码所困恼吗?这里,让我们来学习一下 ...

  6. 新手学习FFmpeg - 调用API计算关键帧渲染时间点

    通过简单的计算来,线上I帧在视频中出现的时间点. 完整代码请参考 https://andy-zhangtao.github.io/ffmpeg-examples/ 名词解释 首先需要明确以下名词概念: ...

  7. Spring+Mybatis整合的练手小项目(一)项目部署

    声明:教程是网上找的,代码是自己敲的 项目目录大致如下: 1. 首先创建Maven工程,在pom.xml中加入项目所需依赖: <?xml version="1.0" enco ...

  8. C++解决最基本的迷宫问题

    问题描述:给定一个最基本的迷宫图,用一个数组表示,值0表示有路,1表示有障碍物,找一条,从矩阵的左上角,到右下角的最短路.求最短路,大家最先想到的可能是用BFS求,本文也是BFS求最短路的. 源代码如 ...

  9. 使用Hexo搭建个人博客并部署到GitHub或码云上全过程

    一.前言 如上图所示:GitHub有Github Pages,而码云也有码云 Pages 1.Github Pages或Gitee Pages是什么呢? Github Pages或者Gitee Pag ...

  10. Java优化策略小积累

    1.尽量避免大量使用静态变量 package com.cfang.jvm; public class Test2 { private static Test1 test1 = new Test1(); ...